yoda: phase 3 — drag-drop toolbar editor in Settings
Migrates the config model from the phase-2 bag-of-bools (ToolbarItems)
to an ordered Vec<ToolbarAction> so the user can pick BOTH the set of
buttons AND their order in the toolbar.
Config (config.rs):
- new ToolbarAction enum with 11 variants (LocationUp, Reload,
NewFolder, NewFile, Rename, Delete, Cut, Copy, Paste,
ToggleShowHidden, OpenTerminal) + to_u8/from_u8 for DnD payload
- Config.toolbar: Vec<ToolbarAction>, default = default_toolbar()
(NewFolder, Rename, Delete, Cut, Copy, Paste — same 6 as phase 2)
Rendering (view()):
- iterate self.config.toolbar in order and emit a tooltip'd icon button
per entry via the new toolbar_action_ui(action) helper shared with
the Settings page. Paste stays disabled when clipboard empty.
- No hardcoded groups or auto-dividers anymore — order is 100% user.
Settings page (toolbar_settings_section):
- two stacked lists:
* 'Toolbar': currently-enabled actions in their Vec order. Each row
is wrapped in dnd_source (drags a ToolbarActionPayload carrying
the enum discriminant) + dnd_destination (accepts drops from other
rows, fires Message::ToolbarReorder { src, target } to move src
before target in the Vec). A list-drag-handle icon + a minus button
(ToolbarRemove) per row.
* 'Available': actions not yet enabled, each with a plus button
(ToolbarAdd) that pushes to the end of the Vec.
- 'Reset to defaults' button at the bottom (ToolbarReset).
DnD infra (app.rs top):
- TOOLBAR_MIME constant: 'application/x-cosmic-files-toolbar-action'
- ToolbarActionPayload(u8) with AsMimeTypes + AllowedMimeTypes +
TryFrom<(Vec<u8>, String)> impls — single-byte wire format matching
the enum discriminant.
Messages:
- ToolbarAdd(ToolbarAction) — append to toolbar vec if absent
- ToolbarRemove(ToolbarAction)
- ToolbarReorder { src, target } — remove src, reinsert before target
- ToolbarReset — restore default_toolbar()
i18n (en + fr):
- new keys: toolbar-available, toolbar-empty-hint, toolbar-reset
Migration: existing installs with a phase-2 ToolbarItems struct in
their config will error at load time (different shape); cosmic_config
falls back to Self::default() which gives the phase-2 minimal-6 set —
a safe reset rather than a broken partial read.
This commit is contained in:
parent
33a5c8ff99
commit
1cf17dcde8
4 changed files with 356 additions and 182 deletions
|
|
@ -140,6 +140,9 @@ owner = Owner
|
||||||
group = Group
|
group = Group
|
||||||
other = Other
|
other = Other
|
||||||
toolbar = Toolbar
|
toolbar = Toolbar
|
||||||
|
toolbar-available = Available
|
||||||
|
toolbar-empty-hint = No buttons. Drag or add from below.
|
||||||
|
toolbar-reset = Reset to defaults
|
||||||
parent-directory = Parent directory
|
parent-directory = Parent directory
|
||||||
mixed = Mixed
|
mixed = Mixed
|
||||||
### Mode 0
|
### Mode 0
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,9 @@ owner = Propriétaire
|
||||||
group = Groupe
|
group = Groupe
|
||||||
other = Autre
|
other = Autre
|
||||||
toolbar = Barre d'outils
|
toolbar = Barre d'outils
|
||||||
|
toolbar-available = Disponibles
|
||||||
|
toolbar-empty-hint = Aucun bouton. Glisser-déposer ou ajouter depuis la liste ci-dessous.
|
||||||
|
toolbar-reset = Rétablir par défaut
|
||||||
parent-directory = Dossier parent
|
parent-directory = Dossier parent
|
||||||
|
|
||||||
### Mode 0
|
### Mode 0
|
||||||
|
|
|
||||||
412
src/app.rs
412
src/app.rs
|
|
@ -77,7 +77,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
config::{
|
config::{
|
||||||
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
||||||
TimeConfig, ToolbarItems, TypeToSearch,
|
TimeConfig, ToolbarAction, TypeToSearch, default_toolbar,
|
||||||
},
|
},
|
||||||
context_action,
|
context_action,
|
||||||
dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings},
|
dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings},
|
||||||
|
|
@ -145,6 +145,105 @@ pub struct Flags {
|
||||||
pub uris: Vec<url::Url>,
|
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)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
About,
|
About,
|
||||||
|
|
@ -448,8 +547,11 @@ pub enum Message {
|
||||||
SearchInput(String),
|
SearchInput(String),
|
||||||
SetShowDetails(bool),
|
SetShowDetails(bool),
|
||||||
SetShowRecents(bool),
|
SetShowRecents(bool),
|
||||||
/// Yoda: toggle a single toolbar button visibility.
|
/// Yoda phase 3 — toolbar editing messages.
|
||||||
SetToolbar(ToolbarItems),
|
ToolbarAdd(ToolbarAction),
|
||||||
|
ToolbarRemove(ToolbarAction),
|
||||||
|
ToolbarReorder { src: ToolbarAction, target: ToolbarAction },
|
||||||
|
ToolbarReset,
|
||||||
SetTypeToSearch(TypeToSearch),
|
SetTypeToSearch(TypeToSearch),
|
||||||
SystemThemeModeChange,
|
SystemThemeModeChange,
|
||||||
Size(window::Id, Size),
|
Size(window::Id, Size),
|
||||||
|
|
@ -2286,53 +2388,115 @@ impl App {
|
||||||
.toggler(self.config.show_recents, Message::SetShowRecents)
|
.toggler(self.config.show_recents, Message::SetShowRecents)
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
// Yoda: configure which quick-action buttons show in the
|
// Yoda phase 3: toolbar editor. Two stacked lists:
|
||||||
// toolbar under the tab bar. Each toggle maps to one button;
|
// - top: enabled buttons in their current order (drag to reorder)
|
||||||
// layout order inside the toolbar is fixed (file ops →
|
// - bottom: available (not-yet-enabled) buttons
|
||||||
// clipboard → view).
|
// Each row's toggle adds/removes; enabled rows are also
|
||||||
{
|
// drag sources + drop targets.
|
||||||
let tb = self.config.toolbar;
|
self.toolbar_settings_section(),
|
||||||
widget::settings::section()
|
|
||||||
.title(fl!("toolbar"))
|
|
||||||
.add(widget::settings::item::builder(fl!("new-folder"))
|
|
||||||
.toggler(tb.new_folder, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { new_folder: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("new-file"))
|
|
||||||
.toggler(tb.new_file, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { new_file: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("rename"))
|
|
||||||
.toggler(tb.rename, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { rename: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("delete"))
|
|
||||||
.toggler(tb.delete, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { delete: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("cut"))
|
|
||||||
.toggler(tb.cut, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { cut: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("copy"))
|
|
||||||
.toggler(tb.copy, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { copy: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("paste"))
|
|
||||||
.toggler(tb.paste, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { paste: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("reload-folder"))
|
|
||||||
.toggler(tb.reload, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { reload: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("show-hidden-files"))
|
|
||||||
.toggler(tb.toggle_show_hidden, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { toggle_show_hidden: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("open-in-terminal"))
|
|
||||||
.toggler(tb.open_terminal, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { open_terminal: v, ..tb })))
|
|
||||||
.add(widget::settings::item::builder(fl!("parent-directory"))
|
|
||||||
.toggler(tb.location_up, move |v|
|
|
||||||
Message::SetToolbar(ToolbarItems { location_up: v, ..tb })))
|
|
||||||
.into()
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
.into()
|
.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| -> Element<'_, Message> {
|
||||||
|
let (icon, label, _msg) = toolbar_action_ui(action);
|
||||||
|
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(),
|
||||||
|
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 {
|
||||||
|
for a in enabled.iter().copied() {
|
||||||
|
section = section.add(row_enabled(a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)> {
|
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
|
@ -4422,8 +4586,37 @@ impl Application for App {
|
||||||
config_set!(show_recents, show_recents);
|
config_set!(show_recents, show_recents);
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
}
|
}
|
||||||
Message::SetToolbar(toolbar) => {
|
Message::ToolbarAdd(action) => {
|
||||||
config_set!(toolbar, toolbar);
|
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::ToolbarReset => {
|
||||||
|
config_set!(toolbar, default_toolbar());
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
}
|
}
|
||||||
Message::SetTypeToSearch(type_to_search) => {
|
Message::SetTypeToSearch(type_to_search) => {
|
||||||
|
|
@ -6570,22 +6763,21 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yoda: Dolphin-style quick actions toolbar under the headerbar.
|
// Yoda phase 3: Dolphin-style quick actions toolbar. Items are
|
||||||
// Items are rendered from self.config.toolbar (ToolbarItems). Order
|
// rendered from self.config.toolbar (Vec<ToolbarAction>) — the user
|
||||||
// is fixed (file ops / clipboard / view toggles); visibility per
|
// picks the set AND the order via drag-drop in Settings. Dispatch
|
||||||
// item is configurable from the Settings page.
|
// goes through Action::message so keybinding and toolbar share the
|
||||||
// Dispatch goes through Action::message so keybindings and toolbar
|
// same code path.
|
||||||
// share exactly the same code path.
|
if !self.config.toolbar.is_empty() {
|
||||||
{
|
|
||||||
let clipboard_has = self.clipboard_has_content();
|
let clipboard_has = self.clipboard_has_content();
|
||||||
let tb = self.config.toolbar;
|
let buttons: Vec<Element<_>> = self
|
||||||
let mut buttons: Vec<Element<_>> = Vec::new();
|
.config
|
||||||
|
.toolbar
|
||||||
let tb_btn =
|
.iter()
|
||||||
|icon_name: &'static str, label: String, msg: Message, enabled: bool| -> Element<_> {
|
.map(|a| {
|
||||||
let btn = widget::button::icon(
|
let (icon, label, msg) = toolbar_action_ui(*a);
|
||||||
widget::icon::from_name(icon_name).size(16),
|
let enabled = !matches!(a, ToolbarAction::Paste) || clipboard_has;
|
||||||
);
|
let btn = widget::button::icon(widget::icon::from_name(icon).size(16));
|
||||||
let btn = if enabled { btn.on_press(msg) } else { btn };
|
let btn = if enabled { btn.on_press(msg) } else { btn };
|
||||||
widget::tooltip(
|
widget::tooltip(
|
||||||
btn,
|
btn,
|
||||||
|
|
@ -6593,90 +6785,16 @@ impl Application for App {
|
||||||
widget::tooltip::Position::Bottom,
|
widget::tooltip::Position::Bottom,
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
};
|
})
|
||||||
let divider = || -> Element<_> {
|
.collect();
|
||||||
widget::divider::vertical::light().height(16).into()
|
let toolbar = widget::row::with_children(buttons)
|
||||||
};
|
.spacing(space_xxs)
|
||||||
|
.align_y(Alignment::Center);
|
||||||
// Group 1: location
|
tab_column = tab_column.push(
|
||||||
let mut added_any = false;
|
widget::container(toolbar)
|
||||||
if tb.location_up {
|
.width(Length::Fill)
|
||||||
buttons.push(tb_btn("go-up-symbolic", fl!("parent-directory"),
|
.padding([space_xxs, space_s]),
|
||||||
Action::LocationUp.message(None), true));
|
);
|
||||||
added_any = true;
|
|
||||||
}
|
|
||||||
if tb.reload {
|
|
||||||
buttons.push(tb_btn("view-refresh-symbolic", fl!("reload-folder"),
|
|
||||||
Action::Reload.message(None), true));
|
|
||||||
added_any = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group 2: create / edit
|
|
||||||
let mut group_started = false;
|
|
||||||
for (enabled, icon, label, msg) in [
|
|
||||||
(tb.new_folder, "folder-new-symbolic", fl!("new-folder"),
|
|
||||||
Action::NewFolder.message(None)),
|
|
||||||
(tb.new_file, "document-new-symbolic", fl!("new-file"),
|
|
||||||
Action::NewFile.message(None)),
|
|
||||||
(tb.rename, "pencil-symbolic", fl!("rename"),
|
|
||||||
Action::Rename.message(None)),
|
|
||||||
(tb.delete, "edit-delete-symbolic", fl!("delete"),
|
|
||||||
Action::Delete.message(None)),
|
|
||||||
] {
|
|
||||||
if enabled {
|
|
||||||
if !group_started && added_any { buttons.push(divider()); }
|
|
||||||
buttons.push(tb_btn(icon, label, msg, true));
|
|
||||||
group_started = true;
|
|
||||||
added_any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group 3: clipboard
|
|
||||||
let mut group_started = false;
|
|
||||||
for (enabled, icon, label, msg, avail) in [
|
|
||||||
(tb.cut, "edit-cut-symbolic", fl!("cut"),
|
|
||||||
Action::Cut.message(None), true),
|
|
||||||
(tb.copy, "edit-copy-symbolic", fl!("copy"),
|
|
||||||
Action::Copy.message(None), true),
|
|
||||||
(tb.paste, "edit-paste-symbolic", fl!("paste"),
|
|
||||||
Action::Paste.message(None), clipboard_has),
|
|
||||||
] {
|
|
||||||
if enabled {
|
|
||||||
if !group_started && added_any { buttons.push(divider()); }
|
|
||||||
buttons.push(tb_btn(icon, label, msg, avail));
|
|
||||||
group_started = true;
|
|
||||||
added_any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group 4: view toggles + misc
|
|
||||||
let mut group_started = false;
|
|
||||||
for (enabled, icon, label, msg) in [
|
|
||||||
(tb.toggle_show_hidden, "view-reveal-symbolic",
|
|
||||||
fl!("show-hidden-files"),
|
|
||||||
Action::ToggleShowHidden.message(None)),
|
|
||||||
(tb.open_terminal, "utilities-terminal-symbolic",
|
|
||||||
fl!("open-in-terminal"),
|
|
||||||
Action::OpenTerminal.message(None)),
|
|
||||||
] {
|
|
||||||
if enabled {
|
|
||||||
if !group_started && added_any { buttons.push(divider()); }
|
|
||||||
buttons.push(tb_btn(icon, label, msg, true));
|
|
||||||
group_started = true;
|
|
||||||
added_any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if added_any {
|
|
||||||
let toolbar = widget::row::with_children(buttons)
|
|
||||||
.spacing(space_xxs)
|
|
||||||
.align_y(Alignment::Center);
|
|
||||||
tab_column = tab_column.push(
|
|
||||||
widget::container(toolbar)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.padding([space_xxs, space_s]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
|
|
|
||||||
120
src/config.rs
120
src/config.rs
|
|
@ -172,10 +172,11 @@ pub struct Config {
|
||||||
pub show_details: bool,
|
pub show_details: bool,
|
||||||
pub show_recents: bool,
|
pub show_recents: bool,
|
||||||
pub tab: TabConfig,
|
pub tab: TabConfig,
|
||||||
/// Yoda: Dolphin-style quick actions toolbar under the tab bar.
|
/// Yoda phase 3: Dolphin-style quick actions toolbar. An ordered list
|
||||||
/// Each bool toggles one button; order in the UI is fixed (logical
|
/// of enabled buttons — position in the vec drives the toolbar order.
|
||||||
/// grouping file-ops then clipboard then view toggles).
|
/// Reorder in Settings via drag-drop; items not in the vec are
|
||||||
pub toolbar: ToolbarItems,
|
/// hidden. Default = the minimal-6 set from phase 1.
|
||||||
|
pub toolbar: Vec<ToolbarAction>,
|
||||||
pub type_to_search: TypeToSearch,
|
pub type_to_search: TypeToSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,46 +241,95 @@ impl Default for Config {
|
||||||
show_details: false,
|
show_details: false,
|
||||||
show_recents: true,
|
show_recents: true,
|
||||||
tab: TabConfig::default(),
|
tab: TabConfig::default(),
|
||||||
toolbar: ToolbarItems::default(),
|
toolbar: default_toolbar(),
|
||||||
type_to_search: TypeToSearch::Recursive,
|
type_to_search: TypeToSearch::Recursive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Yoda: visibility toggles for each quick-action toolbar button.
|
/// Yoda phase 3: ordered enum of quick-action toolbar buttons.
|
||||||
/// Default = the original "minimal 6" set (new_folder, rename, delete,
|
/// The Config stores `Vec<ToolbarAction>` so the user can pick BOTH
|
||||||
/// cut, copy, paste). Other items default to false so users opt in.
|
/// visibility (just include/exclude the variant) AND order (position in
|
||||||
#[derive(Clone, Copy, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
/// the vec). Drag-drop reorder in the Settings page moves items around.
|
||||||
pub struct ToolbarItems {
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
pub new_folder: bool,
|
pub enum ToolbarAction {
|
||||||
pub new_file: bool,
|
LocationUp,
|
||||||
pub rename: bool,
|
Reload,
|
||||||
pub delete: bool,
|
NewFolder,
|
||||||
pub cut: bool,
|
NewFile,
|
||||||
pub copy: bool,
|
Rename,
|
||||||
pub paste: bool,
|
Delete,
|
||||||
pub reload: bool,
|
Cut,
|
||||||
pub toggle_show_hidden: bool,
|
Copy,
|
||||||
pub open_terminal: bool,
|
Paste,
|
||||||
pub location_up: bool,
|
ToggleShowHidden,
|
||||||
|
OpenTerminal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ToolbarItems {
|
impl ToolbarAction {
|
||||||
fn default() -> Self {
|
/// Stable list of every supported action. Ordered roughly by logical
|
||||||
Self {
|
/// grouping (location → create/edit → clipboard → view/misc) so that
|
||||||
new_folder: true,
|
/// the default enabled set follows a sensible shape and the Settings
|
||||||
new_file: false,
|
/// row for a not-yet-enabled action lands in a predictable spot.
|
||||||
rename: true,
|
pub const ALL: &'static [Self] = &[
|
||||||
delete: true,
|
Self::LocationUp,
|
||||||
cut: true,
|
Self::Reload,
|
||||||
copy: true,
|
Self::NewFolder,
|
||||||
paste: true,
|
Self::NewFile,
|
||||||
reload: false,
|
Self::Rename,
|
||||||
toggle_show_hidden: false,
|
Self::Delete,
|
||||||
open_terminal: false,
|
Self::Cut,
|
||||||
location_up: false,
|
Self::Copy,
|
||||||
|
Self::Paste,
|
||||||
|
Self::ToggleShowHidden,
|
||||||
|
Self::OpenTerminal,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// u8 discriminant used to carry the action over a DnD mime payload.
|
||||||
|
pub const fn to_u8(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::LocationUp => 0,
|
||||||
|
Self::Reload => 1,
|
||||||
|
Self::NewFolder => 2,
|
||||||
|
Self::NewFile => 3,
|
||||||
|
Self::Rename => 4,
|
||||||
|
Self::Delete => 5,
|
||||||
|
Self::Cut => 6,
|
||||||
|
Self::Copy => 7,
|
||||||
|
Self::Paste => 8,
|
||||||
|
Self::ToggleShowHidden => 9,
|
||||||
|
Self::OpenTerminal => 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn from_u8(v: u8) -> Option<Self> {
|
||||||
|
match v {
|
||||||
|
0 => Some(Self::LocationUp),
|
||||||
|
1 => Some(Self::Reload),
|
||||||
|
2 => Some(Self::NewFolder),
|
||||||
|
3 => Some(Self::NewFile),
|
||||||
|
4 => Some(Self::Rename),
|
||||||
|
5 => Some(Self::Delete),
|
||||||
|
6 => Some(Self::Cut),
|
||||||
|
7 => Some(Self::Copy),
|
||||||
|
8 => Some(Self::Paste),
|
||||||
|
9 => Some(Self::ToggleShowHidden),
|
||||||
|
10 => Some(Self::OpenTerminal),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default set shown on a fresh install — same "minimal 6" as phase 1/2.
|
||||||
|
pub fn default_toolbar() -> Vec<ToolbarAction> {
|
||||||
|
vec![
|
||||||
|
ToolbarAction::NewFolder,
|
||||||
|
ToolbarAction::Rename,
|
||||||
|
ToolbarAction::Delete,
|
||||||
|
ToolbarAction::Cut,
|
||||||
|
ToolbarAction::Copy,
|
||||||
|
ToolbarAction::Paste,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue