yoda: cosmic-files customizations (squashed 21 commits)
This commit squashes the 21 local commits that customize cosmic-files for the yoda stack, to allow a clean rebase on upstream/master. Original commits (chronological): -9bcfe7aCargo.toml: patch libcosmic via local path for dev builds -04abd13yoda: depend on libcosmic-yoda (path) instead of upstream libcosmic -02adcc3lockfile: libcosmic-yoda 0.1.0-yoda -> 0.1.0-yoda.2 -a025fd6yoda: prefer cosmic-yoterm over upstream cosmic-term in terminal fallback -e8d62aeyoda: add "Always use this app" toggle to OpenWith dialog -8fb2b15yoda wayland-v5: redirect window_clipboard + cosmic-text to local forks -0595296yoda: Dolphin-style quick actions toolbar under the headerbar -4b6d345yoda: fix missing rename icon in toolbar -8b51af1yoda: use pencil-symbolic for the Rename toolbar button -33a5c8fyoda: phase 2 - customizable toolbar (settings toggles per button) -1cf17dcyoda: phase 3 - drag-drop toolbar editor in Settings -11d4357yoda: add up/down buttons next to drag handle in toolbar editor -af843d2yoda: direct drag-drop reorder on the toolbar itself -94c3e6cyoda: toolbar as segmented_button for working drag reorder -f053819yoda: toolbar icon-only + clean visual (Control style, 32px squares) -338354cImprove initial directory listing latency -d080bc8Resolve cosmic-files warnings without masking -69c35abyoda: switch window_clipboard patch to public Forgejo fork -35e115fyoda: switch cosmic-text patch to public Forgejo fork -6f3adcdchore: clean feature-gated warnings -57ab1ecfix: clean files warnings for terminal build Original tip preserved as tag backup/pre-rebase-upstream-20260524.
This commit is contained in:
parent
784200a253
commit
f1b1f8d799
17 changed files with 1173 additions and 646 deletions
492
src/app.rs
492
src/app.rs
|
|
@ -41,15 +41,21 @@ use notify_debouncer_full::notify::{self, RecommendedWatcher};
|
|||
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use slotmap::Key as SlotMapKey;
|
||||
use std::any::TypeId;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
||||
use std::future::Future;
|
||||
use std::num::NonZeroU16;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use std::time::{self, Duration, Instant};
|
||||
use std::{env, fmt, fs, io, process};
|
||||
#[cfg(feature = "notify")]
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::{BTreeMap, BTreeSet, HashMap, VecDeque},
|
||||
env, fmt, fs,
|
||||
future::Future,
|
||||
io,
|
||||
num::NonZeroU16,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
process,
|
||||
sync::{Arc, LazyLock},
|
||||
time::{self, Duration, Instant},
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use trash::TrashItem;
|
||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||
|
|
@ -61,7 +67,7 @@ use crate::clipboard::{
|
|||
};
|
||||
use crate::config::{
|
||||
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
||||
TimeConfig, TypeToSearch,
|
||||
TimeConfig, ToolbarAction, TypeToSearch, default_toolbar,
|
||||
};
|
||||
use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
||||
use crate::key_bind::key_binds;
|
||||
|
|
@ -127,6 +133,105 @@ pub struct Flags {
|
|||
pub uris: Vec<url::Url>,
|
||||
}
|
||||
|
||||
/// Yoda phase 3: MIME for the DnD payload carried when a user drags a
|
||||
/// toolbar row in the Settings editor. A single byte = ToolbarAction
|
||||
/// discriminant (see `ToolbarAction::to_u8`).
|
||||
const TOOLBAR_MIME: &str = "application/x-cosmic-files-toolbar-action";
|
||||
|
||||
/// Yoda phase 3: DnD payload wrapping a ToolbarAction discriminant.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ToolbarActionPayload(pub u8);
|
||||
|
||||
impl cosmic::iced::clipboard::mime::AsMimeTypes for ToolbarActionPayload {
|
||||
fn available(&self) -> std::borrow::Cow<'static, [String]> {
|
||||
std::borrow::Cow::Owned(vec![TOOLBAR_MIME.to_owned()])
|
||||
}
|
||||
fn as_bytes(&self, mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
if mime_type == TOOLBAR_MIME {
|
||||
Some(std::borrow::Cow::Owned(vec![self.0]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl cosmic::iced::clipboard::mime::AllowedMimeTypes for ToolbarActionPayload {
|
||||
fn allowed() -> std::borrow::Cow<'static, [String]> {
|
||||
std::borrow::Cow::Owned(vec![TOOLBAR_MIME.to_owned()])
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(Vec<u8>, String)> for ToolbarActionPayload {
|
||||
type Error = ();
|
||||
fn try_from((data, _mime): (Vec<u8>, String)) -> Result<Self, Self::Error> {
|
||||
if data.len() == 1 {
|
||||
Ok(Self(data[0]))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Yoda phase 3 helper: map a ToolbarAction to its button UI (icon name,
|
||||
/// localized label, app Message). Shared by the toolbar renderer in
|
||||
/// `view()` and by the Settings page row renderer so the two stay in
|
||||
/// sync.
|
||||
fn toolbar_action_ui(a: ToolbarAction) -> (&'static str, String, Message) {
|
||||
match a {
|
||||
ToolbarAction::LocationUp => (
|
||||
"go-up-symbolic",
|
||||
fl!("parent-directory"),
|
||||
Action::LocationUp.message(None),
|
||||
),
|
||||
ToolbarAction::Reload => (
|
||||
"view-refresh-symbolic",
|
||||
fl!("reload-folder"),
|
||||
Action::Reload.message(None),
|
||||
),
|
||||
ToolbarAction::NewFolder => (
|
||||
"folder-new-symbolic",
|
||||
fl!("new-folder"),
|
||||
Action::NewFolder.message(None),
|
||||
),
|
||||
ToolbarAction::NewFile => (
|
||||
"document-new-symbolic",
|
||||
fl!("new-file"),
|
||||
Action::NewFile.message(None),
|
||||
),
|
||||
ToolbarAction::Rename => (
|
||||
"pencil-symbolic",
|
||||
fl!("rename"),
|
||||
Action::Rename.message(None),
|
||||
),
|
||||
ToolbarAction::Delete => (
|
||||
"edit-delete-symbolic",
|
||||
fl!("delete"),
|
||||
Action::Delete.message(None),
|
||||
),
|
||||
ToolbarAction::Cut => ("edit-cut-symbolic", fl!("cut"), Action::Cut.message(None)),
|
||||
ToolbarAction::Copy => (
|
||||
"edit-copy-symbolic",
|
||||
fl!("copy"),
|
||||
Action::Copy.message(None),
|
||||
),
|
||||
ToolbarAction::Paste => (
|
||||
"edit-paste-symbolic",
|
||||
fl!("paste"),
|
||||
Action::Paste.message(None),
|
||||
),
|
||||
ToolbarAction::ToggleShowHidden => (
|
||||
"view-reveal-symbolic",
|
||||
fl!("show-hidden-files"),
|
||||
Action::ToggleShowHidden.message(None),
|
||||
),
|
||||
ToolbarAction::OpenTerminal => (
|
||||
"utilities-terminal-symbolic",
|
||||
fl!("open-in-terminal"),
|
||||
Action::OpenTerminal.message(None),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
About,
|
||||
|
|
@ -389,6 +494,7 @@ pub enum Message {
|
|||
OpenWithBrowse,
|
||||
OpenWithDialog(Option<Entity>),
|
||||
OpenWithSelection(usize),
|
||||
OpenWithToggleDefault(bool),
|
||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||
Overlap(window::Id, OverlapNotifyEvent),
|
||||
Paste(Option<Entity>),
|
||||
|
|
@ -429,6 +535,22 @@ pub enum Message {
|
|||
SearchInput(String),
|
||||
SetShowDetails(bool),
|
||||
SetShowRecents(bool),
|
||||
/// Yoda phase 3 — toolbar editing messages.
|
||||
ToolbarAdd(ToolbarAction),
|
||||
ToolbarRemove(ToolbarAction),
|
||||
ToolbarReorder {
|
||||
src: ToolbarAction,
|
||||
target: ToolbarAction,
|
||||
},
|
||||
/// Move one step up (toward index 0) inside the enabled toolbar list.
|
||||
ToolbarMoveUp(ToolbarAction),
|
||||
/// Move one step down (toward the end) inside the enabled toolbar list.
|
||||
ToolbarMoveDown(ToolbarAction),
|
||||
ToolbarReset,
|
||||
/// Click on a toolbar button (via segmented_button activation).
|
||||
ToolbarTabActivate(segmented_button::Entity),
|
||||
/// Drag-reorder inside the toolbar (via segmented_button drag).
|
||||
ToolbarTabReorder(segmented_button::ReorderEvent),
|
||||
SetTypeToSearch(TypeToSearch),
|
||||
SystemThemeModeChange,
|
||||
Size(window::Id, Size),
|
||||
|
|
@ -559,6 +681,9 @@ pub enum DialogPage {
|
|||
mime: mime_guess::Mime,
|
||||
selected: usize,
|
||||
store_opt: Option<MimeApp>,
|
||||
/// When true, the chosen app is written to mimeapps.list as the
|
||||
/// default handler for `mime` after it spawns.
|
||||
set_default: bool,
|
||||
},
|
||||
PermanentlyDelete {
|
||||
paths: Box<[PathBuf]>,
|
||||
|
|
@ -716,6 +841,11 @@ pub struct App {
|
|||
nav_bar_context_id: segmented_button::Entity,
|
||||
nav_model: segmented_button::SingleSelectModel,
|
||||
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||
/// Yoda phase 3: segmented_button model mirroring config.toolbar so the
|
||||
/// toolbar row gets free drag-reorder + click activation (same widget
|
||||
/// that powers the tab bar, its reorder is proven to work in this
|
||||
/// setup unlike the generic dnd_source/dnd_destination wrappers).
|
||||
toolbar_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
state_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
|
|
@ -1071,7 +1201,7 @@ impl App {
|
|||
.sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height)));
|
||||
|
||||
for (w_id, overlap) in sorted_overlaps {
|
||||
let Some((bl, br, tl, tr, mut size)) = self.layer_sizes.get(w_id).map(|s| {
|
||||
let Some((bl, br, tl, tr, size)) = self.layer_sizes.get(w_id).map(|s| {
|
||||
(
|
||||
Rectangle::new(
|
||||
Point::new(0., s.height / 2.),
|
||||
|
|
@ -1121,30 +1251,18 @@ impl App {
|
|||
if tl && !(tr || bl) {
|
||||
*top += min_dim.1;
|
||||
*left += min_dim.0;
|
||||
|
||||
size.height -= min_dim.1;
|
||||
size.width -= min_dim.0;
|
||||
}
|
||||
if tr && !(tl || br) {
|
||||
*top += min_dim.1;
|
||||
*right += min_dim.0;
|
||||
|
||||
size.height -= min_dim.1;
|
||||
size.width -= min_dim.0;
|
||||
}
|
||||
if bl && !(br || tl) {
|
||||
*bottom += min_dim.1;
|
||||
*left += min_dim.0;
|
||||
|
||||
size.height -= min_dim.1;
|
||||
size.width -= min_dim.0;
|
||||
}
|
||||
if br && !(bl || tr) {
|
||||
*bottom += min_dim.1;
|
||||
*right += min_dim.0;
|
||||
|
||||
size.height -= min_dim.1;
|
||||
size.width -= min_dim.0;
|
||||
}
|
||||
}
|
||||
self.margin = overlaps;
|
||||
|
|
@ -1503,12 +1621,18 @@ impl App {
|
|||
) -> Task<Message> {
|
||||
log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}");
|
||||
let icon_sizes = self.config.tab.icon_sizes;
|
||||
#[cfg(feature = "gvfs")]
|
||||
let mounter_items = self.mounter_items.clone();
|
||||
|
||||
Task::future(async move {
|
||||
let location2 = location.clone();
|
||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||
Ok((parent_item_opt, mut items)) => {
|
||||
Ok((parent_item_opt, items)) => {
|
||||
#[cfg(feature = "gvfs")]
|
||||
let mut items = items;
|
||||
#[cfg(not(feature = "gvfs"))]
|
||||
let items = items;
|
||||
|
||||
#[cfg(feature = "gvfs")]
|
||||
{
|
||||
let mounter_paths: Box<[_]> = mounter_items
|
||||
|
|
@ -1678,6 +1802,7 @@ impl App {
|
|||
|
||||
fn update_config(&mut self) -> Task<Message> {
|
||||
self.update_nav_model();
|
||||
self.rebuild_toolbar_model();
|
||||
// Tabs are collected first to placate the borrowck
|
||||
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
||||
// Update main conf and each tab with the new config
|
||||
|
|
@ -1691,6 +1816,52 @@ impl App {
|
|||
Task::batch(commands)
|
||||
}
|
||||
|
||||
/// Yoda phase 3: rebuild `toolbar_model` so it matches `config.toolbar`.
|
||||
/// Called on init and on every config update. Each entity carries the
|
||||
/// associated `ToolbarAction` as data so click/reorder handlers can
|
||||
/// round-trip Entity → ToolbarAction without maintaining a side map.
|
||||
///
|
||||
/// We insert ONLY the icon (no `.text()`) so the toolbar renders as a
|
||||
/// clean icon row — user-visible labels stay in Settings/tooltips on
|
||||
/// other surfaces.
|
||||
fn rebuild_toolbar_model(&mut self) {
|
||||
self.toolbar_model.clear();
|
||||
for action in self.config.toolbar.iter().copied() {
|
||||
let (icon_name, _label, _msg) = toolbar_action_ui(action);
|
||||
self.toolbar_model
|
||||
.insert()
|
||||
.icon(widget::icon::from_name(icon_name).size(16).icon())
|
||||
.data::<ToolbarAction>(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// Yoda phase 3: after a drag-reorder, sync `config.toolbar` with the
|
||||
/// new entity order in `toolbar_model`. Inlines what `config_set!`
|
||||
/// would do (the macro lives inside update()).
|
||||
fn sync_toolbar_config_from_model(&mut self) -> Task<Message> {
|
||||
let new_toolbar: Vec<ToolbarAction> = self
|
||||
.toolbar_model
|
||||
.iter()
|
||||
.filter_map(|e| self.toolbar_model.data::<ToolbarAction>(e).copied())
|
||||
.collect();
|
||||
if new_toolbar == self.config.toolbar {
|
||||
return Task::none();
|
||||
}
|
||||
match self.config_handler.as_ref() {
|
||||
Some(h) => {
|
||||
if let Err(err) = self.config.set_toolbar(h, new_toolbar) {
|
||||
log::warn!("failed to save toolbar order: {err}");
|
||||
}
|
||||
}
|
||||
None => self.config.toolbar = new_toolbar,
|
||||
}
|
||||
// Don't call update_config() — that would rebuild the
|
||||
// toolbar_model from config and undo the reorder the user just
|
||||
// made. The model already has the new order; config is just
|
||||
// catching up for persistence.
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn update_desktop(&mut self) -> Task<Message> {
|
||||
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
||||
.filter_map(|entity| {
|
||||
|
|
@ -2268,10 +2439,134 @@ impl App {
|
|||
.toggler(self.config.show_recents, Message::SetShowRecents)
|
||||
})
|
||||
.into(),
|
||||
// Yoda phase 3: toolbar editor. Two stacked lists:
|
||||
// - top: enabled buttons in their current order (drag to reorder)
|
||||
// - bottom: available (not-yet-enabled) buttons
|
||||
// Each row's toggle adds/removes; enabled rows are also
|
||||
// drag sources + drop targets.
|
||||
self.toolbar_settings_section(),
|
||||
])
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Yoda phase 3: build the Toolbar settings section.
|
||||
fn toolbar_settings_section(&self) -> Element<'_, Message> {
|
||||
use iced::clipboard::dnd::DndAction;
|
||||
let enabled = &self.config.toolbar;
|
||||
let disabled: Vec<ToolbarAction> = ToolbarAction::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|a| !enabled.contains(a))
|
||||
.collect();
|
||||
|
||||
let space_xxs = theme::active().cosmic().spacing.space_xxs;
|
||||
|
||||
let drag_icon = |size: u16| -> Element<'static, Message> {
|
||||
widget::icon::from_name("list-drag-handle-symbolic")
|
||||
.size(size)
|
||||
.into()
|
||||
};
|
||||
|
||||
let row_enabled =
|
||||
|action: ToolbarAction, pos: usize, last: usize| -> Element<'_, Message> {
|
||||
let (icon, label, _msg) = toolbar_action_ui(action);
|
||||
let up_btn =
|
||||
widget::button::icon(widget::icon::from_name("go-up-symbolic").size(14));
|
||||
let up_btn = if pos > 0 {
|
||||
up_btn.on_press(Message::ToolbarMoveUp(action))
|
||||
} else {
|
||||
up_btn
|
||||
};
|
||||
let down_btn =
|
||||
widget::button::icon(widget::icon::from_name("go-down-symbolic").size(14));
|
||||
let down_btn = if pos < last {
|
||||
down_btn.on_press(Message::ToolbarMoveDown(action))
|
||||
} else {
|
||||
down_btn
|
||||
};
|
||||
|
||||
let row_content: Element<_> = widget::row::with_children(vec![
|
||||
drag_icon(14),
|
||||
widget::icon::from_name(icon).size(16).into(),
|
||||
widget::text::body(label).width(Length::Fill).into(),
|
||||
up_btn.into(),
|
||||
down_btn.into(),
|
||||
widget::button::icon(widget::icon::from_name("list-remove-symbolic").size(14))
|
||||
.on_press(Message::ToolbarRemove(action))
|
||||
.into(),
|
||||
])
|
||||
.spacing(space_xxs)
|
||||
.align_y(Alignment::Center)
|
||||
.into();
|
||||
|
||||
let row_container = widget::container(row_content)
|
||||
.width(Length::Fill)
|
||||
.padding(space_xxs);
|
||||
|
||||
// Wrap as DnD source (drags itself) + DnD destination (accepts
|
||||
// drops from other enabled rows; on drop, move the src before
|
||||
// this row).
|
||||
let source = widget::dnd_source::<Message, ToolbarActionPayload>(row_container)
|
||||
.drag_content(move || ToolbarActionPayload(action.to_u8()));
|
||||
widget::dnd_destination(source, vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)])
|
||||
.data_received_for::<ToolbarActionPayload>(
|
||||
move |payload: Option<ToolbarActionPayload>| {
|
||||
match payload.and_then(|p| ToolbarAction::from_u8(p.0)) {
|
||||
Some(src) if src != action => Message::ToolbarReorder {
|
||||
src,
|
||||
target: action,
|
||||
},
|
||||
// No-op if payload missing / malformed / same row.
|
||||
_ => Message::ToolbarReorder {
|
||||
src: action,
|
||||
target: action,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.action(DndAction::Move)
|
||||
.into()
|
||||
};
|
||||
|
||||
let row_disabled = |action: ToolbarAction| -> Element<'_, Message> {
|
||||
let (icon, label, _msg) = toolbar_action_ui(action);
|
||||
widget::row::with_children(vec![
|
||||
widget::icon::from_name(icon).size(16).into(),
|
||||
widget::text::body(label).width(Length::Fill).into(),
|
||||
widget::button::icon(widget::icon::from_name("list-add-symbolic").size(14))
|
||||
.on_press(Message::ToolbarAdd(action))
|
||||
.into(),
|
||||
])
|
||||
.spacing(space_xxs)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(space_xxs)
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut section = widget::settings::section().title(fl!("toolbar"));
|
||||
if enabled.is_empty() {
|
||||
section = section.add(widget::text::body(fl!("toolbar-empty-hint")));
|
||||
} else {
|
||||
let last = enabled.len() - 1;
|
||||
for (pos, a) in enabled.iter().copied().enumerate() {
|
||||
section = section.add(row_enabled(a, pos, last));
|
||||
}
|
||||
}
|
||||
|
||||
let mut col = widget::column::with_capacity(3).spacing(space_xxs);
|
||||
col = col.push(section);
|
||||
if !disabled.is_empty() {
|
||||
let mut avail = widget::settings::section().title(fl!("toolbar-available"));
|
||||
for a in disabled {
|
||||
avail = avail.add(row_disabled(a));
|
||||
}
|
||||
col = col.push(avail);
|
||||
}
|
||||
col = col
|
||||
.push(widget::button::standard(fl!("toolbar-reset")).on_press(Message::ToolbarReset));
|
||||
col.into()
|
||||
}
|
||||
|
||||
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
|
|
@ -2443,6 +2738,7 @@ impl Application for App {
|
|||
nav_bar_context_id: segmented_button::Entity::null(),
|
||||
nav_model: segmented_button::ModelBuilder::default().build(),
|
||||
tab_model: segmented_button::ModelBuilder::default().build(),
|
||||
toolbar_model: segmented_button::ModelBuilder::default().build(),
|
||||
config_handler: flags.config_handler,
|
||||
state_handler: flags.state_handler,
|
||||
config: flags.config,
|
||||
|
|
@ -3222,6 +3518,7 @@ impl Application for App {
|
|||
path,
|
||||
mime,
|
||||
selected,
|
||||
set_default,
|
||||
..
|
||||
} => {
|
||||
let available_apps = self.get_apps_for_mime(&mime);
|
||||
|
|
@ -3240,6 +3537,11 @@ impl Application for App {
|
|||
None,
|
||||
);
|
||||
}
|
||||
// Yoda: persist as default if the user asked for it.
|
||||
if set_default {
|
||||
self.mime_app_cache
|
||||
.set_default(mime.clone(), app.id.clone());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
|
|
@ -3862,6 +4164,7 @@ impl Application for App {
|
|||
.and_then(|mime| {
|
||||
self.mime_app_cache.get(&mime).first().cloned()
|
||||
}),
|
||||
set_default: false,
|
||||
},
|
||||
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
||||
);
|
||||
|
|
@ -3873,6 +4176,13 @@ impl Application for App {
|
|||
*selected = index;
|
||||
}
|
||||
}
|
||||
Message::OpenWithToggleDefault(enabled) => {
|
||||
if let Some(DialogPage::OpenWith { set_default, .. }) =
|
||||
self.dialog_pages.front_mut()
|
||||
{
|
||||
*set_default = enabled;
|
||||
}
|
||||
}
|
||||
Message::Paste(entity_opt) => {
|
||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
|
||||
|
|
@ -4349,6 +4659,86 @@ impl Application for App {
|
|||
config_set!(show_recents, show_recents);
|
||||
return self.update_config();
|
||||
}
|
||||
Message::ToolbarAdd(action) => {
|
||||
let mut tb = self.config.toolbar.clone();
|
||||
if !tb.contains(&action) {
|
||||
tb.push(action);
|
||||
}
|
||||
config_set!(toolbar, tb);
|
||||
return self.update_config();
|
||||
}
|
||||
Message::ToolbarRemove(action) => {
|
||||
let mut tb = self.config.toolbar.clone();
|
||||
tb.retain(|a| a != &action);
|
||||
config_set!(toolbar, tb);
|
||||
return self.update_config();
|
||||
}
|
||||
Message::ToolbarReorder { src, target } => {
|
||||
let mut tb = self.config.toolbar.clone();
|
||||
if let (Some(src_idx), Some(tgt_idx)) = (
|
||||
tb.iter().position(|a| a == &src),
|
||||
tb.iter().position(|a| a == &target),
|
||||
) && src_idx != tgt_idx
|
||||
{
|
||||
// Pull src out, then insert before the target's new position.
|
||||
let item = tb.remove(src_idx);
|
||||
let new_tgt = if src_idx < tgt_idx {
|
||||
tgt_idx - 1
|
||||
} else {
|
||||
tgt_idx
|
||||
};
|
||||
tb.insert(new_tgt, item);
|
||||
config_set!(toolbar, tb);
|
||||
return self.update_config();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
Message::ToolbarMoveUp(action) => {
|
||||
let mut tb = self.config.toolbar.clone();
|
||||
if let Some(i) = tb.iter().position(|a| a == &action)
|
||||
&& i > 0
|
||||
{
|
||||
tb.swap(i, i - 1);
|
||||
config_set!(toolbar, tb);
|
||||
return self.update_config();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
Message::ToolbarMoveDown(action) => {
|
||||
let mut tb = self.config.toolbar.clone();
|
||||
if let Some(i) = tb.iter().position(|a| a == &action)
|
||||
&& i + 1 < tb.len()
|
||||
{
|
||||
tb.swap(i, i + 1);
|
||||
config_set!(toolbar, tb);
|
||||
return self.update_config();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
Message::ToolbarReset => {
|
||||
config_set!(toolbar, default_toolbar());
|
||||
return self.update_config();
|
||||
}
|
||||
Message::ToolbarTabActivate(entity) => {
|
||||
// Dispatch the stored ToolbarAction's message, then clear
|
||||
// the "active" selection so the button doesn't stay
|
||||
// highlighted after a click (we use segmented_button for
|
||||
// layout/drag but toolbar buttons are action-firing, not
|
||||
// a mutual-exclusive choice).
|
||||
let action = self.toolbar_model.data::<ToolbarAction>(entity).copied();
|
||||
self.toolbar_model.deactivate();
|
||||
if let Some(action) = action {
|
||||
let (_, _, msg) = toolbar_action_ui(action);
|
||||
return self.update(msg);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
Message::ToolbarTabReorder(event) => {
|
||||
let _ = self
|
||||
.toolbar_model
|
||||
.reorder(event.dragged, event.target, event.position);
|
||||
return self.sync_toolbar_config_from_model();
|
||||
}
|
||||
Message::SetTypeToSearch(type_to_search) => {
|
||||
config_set!(type_to_search, type_to_search);
|
||||
return self.update_config();
|
||||
|
|
@ -5108,6 +5498,7 @@ impl Application for App {
|
|||
.and_then(|mime| {
|
||||
self.mime_app_cache.get(&mime).first().cloned()
|
||||
}),
|
||||
set_default: false,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
|
@ -5951,7 +6342,7 @@ impl Application for App {
|
|||
mime,
|
||||
selected,
|
||||
store_opt,
|
||||
..
|
||||
set_default,
|
||||
} => {
|
||||
let name = match path.file_name() {
|
||||
Some(file_name) => file_name.to_str(),
|
||||
|
|
@ -6036,7 +6427,21 @@ impl Application for App {
|
|||
} else {
|
||||
Length::Shrink
|
||||
}
|
||||
}));
|
||||
}))
|
||||
// Yoda: let the user make this choice stick. A plain row
|
||||
// instead of settings::item::builder because the latter
|
||||
// returns a section Item, not an Element usable in .control().
|
||||
.control(
|
||||
widget::row::with_children([
|
||||
widget::text::body(fl!("open-with-set-default")).into(),
|
||||
widget::space::horizontal().into(),
|
||||
widget::toggler(*set_default)
|
||||
.on_toggle(Message::OpenWithToggleDefault)
|
||||
.into(),
|
||||
])
|
||||
.spacing(space_s)
|
||||
.align_y(Alignment::Center),
|
||||
);
|
||||
|
||||
if let Some(app) = store_opt {
|
||||
dialog = dialog.tertiary_action(
|
||||
|
|
@ -6442,7 +6847,10 @@ impl Application for App {
|
|||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs, space_s, ..
|
||||
space_xxs,
|
||||
space_xs,
|
||||
space_s,
|
||||
..
|
||||
} = theme::spacing();
|
||||
|
||||
let mut tab_column = widget::column::with_capacity(4);
|
||||
|
|
@ -6486,6 +6894,36 @@ impl Application for App {
|
|||
);
|
||||
}
|
||||
|
||||
// Yoda phase 3: Dolphin-style quick actions toolbar via
|
||||
// segmented_button::horizontal — the same widget that powers the
|
||||
// tab bar, so its built-in drag reorder works reliably (unlike the
|
||||
// generic dnd_source+dnd_destination pairing we tried earlier).
|
||||
// Short click = action (ToolbarTabActivate → dispatch the stored
|
||||
// ToolbarAction's message). Drag past threshold = reorder
|
||||
// (ToolbarTabReorder → model.reorder + sync to config).
|
||||
if !self.config.toolbar.is_empty() {
|
||||
// Use Control style (no TabBar underline, no bottom border)
|
||||
// and let each button shrink to its icon. Spacing = space_xs
|
||||
// keeps the buttons visually separated so it looks like an
|
||||
// icon toolbar rather than a conjoined segmented control.
|
||||
let toolbar = widget::segmented_button::horizontal(&self.toolbar_model)
|
||||
.style(theme::SegmentedButton::Control)
|
||||
.button_height(32)
|
||||
.button_spacing(space_xs)
|
||||
.button_alignment(Alignment::Center)
|
||||
.minimum_button_width(32)
|
||||
.maximum_button_width(32)
|
||||
.enable_tab_drag(String::from("x-cosmic-files/toolbar-dnd"))
|
||||
.on_reorder(Message::ToolbarTabReorder)
|
||||
.tab_drag_threshold(8.)
|
||||
.on_activate(Message::ToolbarTabActivate);
|
||||
tab_column = tab_column.push(
|
||||
widget::container(toolbar)
|
||||
.width(Length::Shrink)
|
||||
.padding([space_xxs, space_s]),
|
||||
);
|
||||
}
|
||||
|
||||
let entity = self.tab_model.active();
|
||||
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||
let tab_view = tab
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue