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
947
Cargo.lock
generated
947
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
41
Cargo.toml
41
Cargo.toml
|
|
@ -73,8 +73,9 @@ version = "0.18"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["fs", "io", "macros", "polling", "runtime"]
|
features = ["fs", "io", "macros", "polling", "runtime"]
|
||||||
|
|
||||||
[dependencies.libcosmic]
|
# Yoda fork — depend on libcosmic-yoda directly by path (no git/no patch).
|
||||||
git = "https://github.com/pop-os/libcosmic.git"
|
[dependencies.libcosmic-yoda]
|
||||||
|
path = "../libcosmic"
|
||||||
default-features = false
|
default-features = false
|
||||||
#TODO: a11y feature crashes
|
#TODO: a11y feature crashes
|
||||||
features = [
|
features = [
|
||||||
|
|
@ -83,7 +84,7 @@ features = [
|
||||||
"autosize",
|
"autosize",
|
||||||
"multi-window",
|
"multi-window",
|
||||||
"tokio",
|
"tokio",
|
||||||
"winit",
|
"wayland",
|
||||||
"surface-message",
|
"surface-message",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -111,15 +112,15 @@ default = [
|
||||||
"wayland",
|
"wayland",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
]
|
]
|
||||||
dbus-config = ["libcosmic/dbus-config"]
|
dbus-config = ["libcosmic-yoda/dbus-config"]
|
||||||
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
|
desktop = ["libcosmic-yoda/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
|
||||||
desktop-applet = []
|
desktop-applet = []
|
||||||
gvfs = ["dep:gio", "dep:glib"]
|
gvfs = ["dep:gio", "dep:glib"]
|
||||||
io-uring = ["compio/io-uring"]
|
io-uring = ["compio/io-uring"]
|
||||||
jemalloc = ["dep:tikv-jemallocator"]
|
jemalloc = ["dep:tikv-jemallocator"]
|
||||||
notify = ["dep:notify-rust"]
|
notify = ["dep:notify-rust"]
|
||||||
wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"]
|
wayland = ["libcosmic-yoda/wayland", "dep:cctk", "dep:wayland-client"]
|
||||||
wgpu = ["libcosmic/wgpu"]
|
wgpu = ["libcosmic-yoda/wgpu"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
@ -145,20 +146,22 @@ fastrand = "2"
|
||||||
test-log = "0.2"
|
test-log = "0.2"
|
||||||
tokio = { version = "1", features = ["rt", "macros"] }
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
|
||||||
# [patch.'https://github.com/pop-os/cosmic-text']
|
# Yoda fork — libcosmic dep is now a direct path dep (libcosmic-yoda above),
|
||||||
# cosmic-text = { path = "../cosmic-text" }
|
# no [patch] block needed anymore. Keeping the block below would be a no-op
|
||||||
|
# since nothing in the dep graph still asks for pop-os/libcosmic.git.
|
||||||
|
|
||||||
# [patch.'https://github.com/pop-os/libcosmic']
|
# Yoda wayland cut: redirect window_clipboard (x11 gated behind opt-in
|
||||||
# libcosmic = { path = "../libcosmic" }
|
# feature) and cosmic-text (PR#503: EAW monospace width fix) to our public
|
||||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
# Forgejo forks. The window_clipboard patch is needed to consolidate the
|
||||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
# upstream pop-os/libcosmic chain (pulled by cosmic-settings-daemon) onto
|
||||||
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
# the same fork iced uses, otherwise cargo compiles two versions.
|
||||||
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
[patch.'https://github.com/pop-os/window_clipboard.git']
|
||||||
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
window_clipboard = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" }
|
||||||
|
dnd = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" }
|
||||||
|
mime = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" }
|
||||||
|
|
||||||
|
[patch.'https://github.com/pop-os/cosmic-text.git']
|
||||||
# [patch.'https://github.com/pop-os/smithay-clipboard']
|
cosmic-text = { git = "https://forge.aditua.com/leyoda/cosmic-text.git", branch = "local/pr-503" }
|
||||||
# smithay-clipboard = { path = "../smithay-clipboard" }
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["cosmic-files-applet"]
|
members = ["cosmic-files-applet"]
|
||||||
|
|
|
||||||
78
docs/local-performance-and-portal-notes.md
Normal file
78
docs/local-performance-and-portal-notes.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Local performance and portal notes
|
||||||
|
|
||||||
|
Date: 2026-05-05
|
||||||
|
|
||||||
|
This repository carries a local patch aimed at improving initial directory
|
||||||
|
display latency in large folders, plus a system-level workaround for a COSMIC
|
||||||
|
portal FileChooser crash observed with Firefox and Chromium.
|
||||||
|
|
||||||
|
## Directory listing latency
|
||||||
|
|
||||||
|
The slow path was the initial construction of `Item` values in `src/tab.rs`.
|
||||||
|
On a test folder with about 2000 entries, raw filesystem enumeration and stat
|
||||||
|
calls completed in a few milliseconds, while COSMIC Files took multiple seconds
|
||||||
|
before showing the directory.
|
||||||
|
|
||||||
|
The local patch keeps initial item construction cheap:
|
||||||
|
|
||||||
|
- directory child counts are no longer computed synchronously in
|
||||||
|
`item_from_entry` and `item_from_gvfs_info`;
|
||||||
|
- initial MIME detection uses extension-based `mime_guess`;
|
||||||
|
- regular files use a generic file icon during the initial scan instead of
|
||||||
|
resolving the full MIME icon immediately.
|
||||||
|
|
||||||
|
This keeps folders and `.desktop` files special-cased, while avoiding expensive
|
||||||
|
per-file work for ordinary files during first paint.
|
||||||
|
|
||||||
|
Measured locally during investigation:
|
||||||
|
|
||||||
|
- before the MIME/icon changes: about 3.1 seconds for `~/Téléchargements`;
|
||||||
|
- after avoiding full MIME icon resolution during scan: below the temporary
|
||||||
|
100 ms perf-log threshold for the same folder.
|
||||||
|
|
||||||
|
## File chooser portal workaround
|
||||||
|
|
||||||
|
Firefox and Chromium were failing to open `Save As` on the first attempt because
|
||||||
|
`xdg-desktop-portal-cosmic` crashed while handling `FileChooser`.
|
||||||
|
|
||||||
|
Logs showed:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Backend call failed: Remote peer disconnected
|
||||||
|
xdg-desktop-portal-cosmic ... status=11/SEGV
|
||||||
|
```
|
||||||
|
|
||||||
|
The working local system workaround is to remove
|
||||||
|
`org.freedesktop.impl.portal.FileChooser` from:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/share/xdg-desktop-portal/portals/cosmic.portal
|
||||||
|
```
|
||||||
|
|
||||||
|
so the file chooser falls back to GTK. The resulting local `cosmic.portal`
|
||||||
|
keeps COSMIC for the other interfaces:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[portal]
|
||||||
|
DBusName=org.freedesktop.impl.portal.desktop.cosmic
|
||||||
|
Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast
|
||||||
|
UseIn=COSMIC
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups created locally:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/share/xdg-desktop-portal/portals/cosmic.portal.bak-codex-20260505-filechooser
|
||||||
|
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505
|
||||||
|
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505-2
|
||||||
|
```
|
||||||
|
|
||||||
|
After editing portal files, restart:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-gtk.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Package updates may overwrite `/usr/share/xdg-desktop-portal/portals/cosmic.portal`.
|
||||||
|
If `Save As` starts needing two attempts again, re-check that `FileChooser` has
|
||||||
|
not been reintroduced in `cosmic.portal`.
|
||||||
|
|
@ -96,6 +96,7 @@ save-file = Save file
|
||||||
|
|
||||||
## Open With Dialog
|
## Open With Dialog
|
||||||
open-with-title = How do you want to open "{$name}"?
|
open-with-title = How do you want to open "{$name}"?
|
||||||
|
open-with-set-default = Always use this app for this file type
|
||||||
browse-store = Browse {$store}
|
browse-store = Browse {$store}
|
||||||
other-apps = Other applications
|
other-apps = Other applications
|
||||||
related-apps = Related applications
|
related-apps = Related applications
|
||||||
|
|
@ -139,6 +140,11 @@ open-with = Open with
|
||||||
owner = Owner
|
owner = Owner
|
||||||
group = Group
|
group = Group
|
||||||
other = Other
|
other = Other
|
||||||
|
toolbar = Toolbar
|
||||||
|
toolbar-available = Available
|
||||||
|
toolbar-empty-hint = No buttons. Drag or add from below.
|
||||||
|
toolbar-reset = Reset to defaults
|
||||||
|
parent-directory = Parent directory
|
||||||
mixed = Mixed
|
mixed = Mixed
|
||||||
### Mode 0
|
### Mode 0
|
||||||
none = None
|
none = None
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ save-file = Enregistrer fichier
|
||||||
## Open With Dialog
|
## Open With Dialog
|
||||||
|
|
||||||
open-with-title = Comment souhaitez-vous ouvrir "{ $name }" ?
|
open-with-title = Comment souhaitez-vous ouvrir "{ $name }" ?
|
||||||
|
open-with-set-default = Toujours utiliser cette application pour ce type de fichier
|
||||||
browse-store = Parcourir { $store }
|
browse-store = Parcourir { $store }
|
||||||
|
|
||||||
## Permanently delete Dialog
|
## Permanently delete Dialog
|
||||||
|
|
@ -130,6 +131,11 @@ open-with = Ouvrir avec
|
||||||
owner = Propriétaire
|
owner = Propriétaire
|
||||||
group = Groupe
|
group = Groupe
|
||||||
other = Autre
|
other = Autre
|
||||||
|
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
|
||||||
|
|
||||||
### Mode 0
|
### Mode 0
|
||||||
|
|
||||||
|
|
|
||||||
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 notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use slotmap::Key as SlotMapKey;
|
use slotmap::Key as SlotMapKey;
|
||||||
use std::any::TypeId;
|
#[cfg(feature = "notify")]
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
use std::sync::Mutex;
|
||||||
use std::future::Future;
|
use std::{
|
||||||
use std::num::NonZeroU16;
|
any::TypeId,
|
||||||
use std::path::{Path, PathBuf};
|
collections::{BTreeMap, BTreeSet, HashMap, VecDeque},
|
||||||
use std::pin::Pin;
|
env, fmt, fs,
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
future::Future,
|
||||||
use std::time::{self, Duration, Instant};
|
io,
|
||||||
use std::{env, fmt, fs, io, process};
|
num::NonZeroU16,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
|
process,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
|
time::{self, Duration, Instant},
|
||||||
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use trash::TrashItem;
|
use trash::TrashItem;
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
|
|
@ -61,7 +67,7 @@ use crate::clipboard::{
|
||||||
};
|
};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
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::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
||||||
use crate::key_bind::key_binds;
|
use crate::key_bind::key_binds;
|
||||||
|
|
@ -127,6 +133,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,
|
||||||
|
|
@ -389,6 +494,7 @@ pub enum Message {
|
||||||
OpenWithBrowse,
|
OpenWithBrowse,
|
||||||
OpenWithDialog(Option<Entity>),
|
OpenWithDialog(Option<Entity>),
|
||||||
OpenWithSelection(usize),
|
OpenWithSelection(usize),
|
||||||
|
OpenWithToggleDefault(bool),
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
Overlap(window::Id, OverlapNotifyEvent),
|
Overlap(window::Id, OverlapNotifyEvent),
|
||||||
Paste(Option<Entity>),
|
Paste(Option<Entity>),
|
||||||
|
|
@ -429,6 +535,22 @@ pub enum Message {
|
||||||
SearchInput(String),
|
SearchInput(String),
|
||||||
SetShowDetails(bool),
|
SetShowDetails(bool),
|
||||||
SetShowRecents(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),
|
SetTypeToSearch(TypeToSearch),
|
||||||
SystemThemeModeChange,
|
SystemThemeModeChange,
|
||||||
Size(window::Id, Size),
|
Size(window::Id, Size),
|
||||||
|
|
@ -559,6 +681,9 @@ pub enum DialogPage {
|
||||||
mime: mime_guess::Mime,
|
mime: mime_guess::Mime,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
store_opt: Option<MimeApp>,
|
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 {
|
PermanentlyDelete {
|
||||||
paths: Box<[PathBuf]>,
|
paths: Box<[PathBuf]>,
|
||||||
|
|
@ -716,6 +841,11 @@ pub struct App {
|
||||||
nav_bar_context_id: segmented_button::Entity,
|
nav_bar_context_id: segmented_button::Entity,
|
||||||
nav_model: segmented_button::SingleSelectModel,
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
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>,
|
config_handler: Option<cosmic_config::Config>,
|
||||||
state_handler: Option<cosmic_config::Config>,
|
state_handler: Option<cosmic_config::Config>,
|
||||||
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)));
|
.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 {
|
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(
|
Rectangle::new(
|
||||||
Point::new(0., s.height / 2.),
|
Point::new(0., s.height / 2.),
|
||||||
|
|
@ -1121,30 +1251,18 @@ impl App {
|
||||||
if tl && !(tr || bl) {
|
if tl && !(tr || bl) {
|
||||||
*top += min_dim.1;
|
*top += min_dim.1;
|
||||||
*left += min_dim.0;
|
*left += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
if tr && !(tl || br) {
|
if tr && !(tl || br) {
|
||||||
*top += min_dim.1;
|
*top += min_dim.1;
|
||||||
*right += min_dim.0;
|
*right += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
if bl && !(br || tl) {
|
if bl && !(br || tl) {
|
||||||
*bottom += min_dim.1;
|
*bottom += min_dim.1;
|
||||||
*left += min_dim.0;
|
*left += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
if br && !(bl || tr) {
|
if br && !(bl || tr) {
|
||||||
*bottom += min_dim.1;
|
*bottom += min_dim.1;
|
||||||
*right += min_dim.0;
|
*right += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.margin = overlaps;
|
self.margin = overlaps;
|
||||||
|
|
@ -1503,12 +1621,18 @@ impl App {
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}");
|
log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}");
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mounter_items = self.mounter_items.clone();
|
let mounter_items = self.mounter_items.clone();
|
||||||
|
|
||||||
Task::future(async move {
|
Task::future(async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
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")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
let mounter_paths: Box<[_]> = mounter_items
|
let mounter_paths: Box<[_]> = mounter_items
|
||||||
|
|
@ -1678,6 +1802,7 @@ impl App {
|
||||||
|
|
||||||
fn update_config(&mut self) -> Task<Message> {
|
fn update_config(&mut self) -> Task<Message> {
|
||||||
self.update_nav_model();
|
self.update_nav_model();
|
||||||
|
self.rebuild_toolbar_model();
|
||||||
// Tabs are collected first to placate the borrowck
|
// Tabs are collected first to placate the borrowck
|
||||||
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
||||||
// Update main conf and each tab with the new config
|
// Update main conf and each tab with the new config
|
||||||
|
|
@ -1691,6 +1816,52 @@ impl App {
|
||||||
Task::batch(commands)
|
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> {
|
fn update_desktop(&mut self) -> Task<Message> {
|
||||||
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
|
|
@ -2268,10 +2439,134 @@ impl App {
|
||||||
.toggler(self.config.show_recents, Message::SetShowRecents)
|
.toggler(self.config.show_recents, Message::SetShowRecents)
|
||||||
})
|
})
|
||||||
.into(),
|
.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()
|
.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)> {
|
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
|
@ -2443,6 +2738,7 @@ impl Application for App {
|
||||||
nav_bar_context_id: segmented_button::Entity::null(),
|
nav_bar_context_id: segmented_button::Entity::null(),
|
||||||
nav_model: segmented_button::ModelBuilder::default().build(),
|
nav_model: segmented_button::ModelBuilder::default().build(),
|
||||||
tab_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,
|
config_handler: flags.config_handler,
|
||||||
state_handler: flags.state_handler,
|
state_handler: flags.state_handler,
|
||||||
config: flags.config,
|
config: flags.config,
|
||||||
|
|
@ -3222,6 +3518,7 @@ impl Application for App {
|
||||||
path,
|
path,
|
||||||
mime,
|
mime,
|
||||||
selected,
|
selected,
|
||||||
|
set_default,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let available_apps = self.get_apps_for_mime(&mime);
|
let available_apps = self.get_apps_for_mime(&mime);
|
||||||
|
|
@ -3240,6 +3537,11 @@ impl Application for App {
|
||||||
None,
|
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) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|
@ -3862,6 +4164,7 @@ impl Application for App {
|
||||||
.and_then(|mime| {
|
.and_then(|mime| {
|
||||||
self.mime_app_cache.get(&mime).first().cloned()
|
self.mime_app_cache.get(&mime).first().cloned()
|
||||||
}),
|
}),
|
||||||
|
set_default: false,
|
||||||
},
|
},
|
||||||
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
||||||
);
|
);
|
||||||
|
|
@ -3873,6 +4176,13 @@ impl Application for App {
|
||||||
*selected = index;
|
*selected = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::OpenWithToggleDefault(enabled) => {
|
||||||
|
if let Some(DialogPage::OpenWith { set_default, .. }) =
|
||||||
|
self.dialog_pages.front_mut()
|
||||||
|
{
|
||||||
|
*set_default = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Paste(entity_opt) => {
|
Message::Paste(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
|
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);
|
config_set!(show_recents, show_recents);
|
||||||
return self.update_config();
|
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) => {
|
Message::SetTypeToSearch(type_to_search) => {
|
||||||
config_set!(type_to_search, type_to_search);
|
config_set!(type_to_search, type_to_search);
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
|
|
@ -5108,6 +5498,7 @@ impl Application for App {
|
||||||
.and_then(|mime| {
|
.and_then(|mime| {
|
||||||
self.mime_app_cache.get(&mime).first().cloned()
|
self.mime_app_cache.get(&mime).first().cloned()
|
||||||
}),
|
}),
|
||||||
|
set_default: false,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
@ -5951,7 +6342,7 @@ impl Application for App {
|
||||||
mime,
|
mime,
|
||||||
selected,
|
selected,
|
||||||
store_opt,
|
store_opt,
|
||||||
..
|
set_default,
|
||||||
} => {
|
} => {
|
||||||
let name = match path.file_name() {
|
let name = match path.file_name() {
|
||||||
Some(file_name) => file_name.to_str(),
|
Some(file_name) => file_name.to_str(),
|
||||||
|
|
@ -6036,7 +6427,21 @@ impl Application for App {
|
||||||
} else {
|
} else {
|
||||||
Length::Shrink
|
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 {
|
if let Some(app) = store_opt {
|
||||||
dialog = dialog.tertiary_action(
|
dialog = dialog.tertiary_action(
|
||||||
|
|
@ -6442,7 +6847,10 @@ impl Application for App {
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
fn view(&self) -> Element<'_, Self::Message> {
|
fn view(&self) -> Element<'_, Self::Message> {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xxs, space_s, ..
|
space_xxs,
|
||||||
|
space_xs,
|
||||||
|
space_s,
|
||||||
|
..
|
||||||
} = theme::spacing();
|
} = theme::spacing();
|
||||||
|
|
||||||
let mut tab_column = widget::column::with_capacity(4);
|
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();
|
let entity = self.tab_model.active();
|
||||||
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||||
let tab_view = tab
|
let tab_view = tab
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,11 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
|
||||||
match mime.as_str() {
|
match mime.as_str() {
|
||||||
"text/uri-list" => {
|
"text/uri-list" => {
|
||||||
let text = str::from_utf8(&data)?;
|
let text = str::from_utf8(&data)?;
|
||||||
let _lines = text.lines();
|
|
||||||
|
|
||||||
for line in text.lines() {
|
for line in text.lines().filter(|line| {
|
||||||
|
let line = line.trim();
|
||||||
|
!line.is_empty() && !line.starts_with('#')
|
||||||
|
}) {
|
||||||
let url = Url::parse(line)?;
|
let url = Url::parse(line)?;
|
||||||
match url.to_file_path() {
|
match url.to_file_path() {
|
||||||
Ok(path) => paths.push(path),
|
Ok(path) => paths.push(path),
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,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 phase 3: Dolphin-style quick actions toolbar. An ordered list
|
||||||
|
/// of enabled buttons — position in the vec drives the toolbar order.
|
||||||
|
/// Reorder in Settings via drag-drop; items not in the vec are
|
||||||
|
/// hidden. Default = the minimal-6 set from phase 1.
|
||||||
|
pub toolbar: Vec<ToolbarAction>,
|
||||||
pub type_to_search: TypeToSearch,
|
pub type_to_search: TypeToSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,11 +239,97 @@ impl Default for Config {
|
||||||
show_details: false,
|
show_details: false,
|
||||||
show_recents: true,
|
show_recents: true,
|
||||||
tab: TabConfig::default(),
|
tab: TabConfig::default(),
|
||||||
|
toolbar: default_toolbar(),
|
||||||
type_to_search: TypeToSearch::Recursive,
|
type_to_search: TypeToSearch::Recursive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yoda phase 3: ordered enum of quick-action toolbar buttons.
|
||||||
|
/// The Config stores `Vec<ToolbarAction>` so the user can pick BOTH
|
||||||
|
/// visibility (just include/exclude the variant) AND order (position in
|
||||||
|
/// the vec). Drag-drop reorder in the Settings page moves items around.
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub enum ToolbarAction {
|
||||||
|
LocationUp,
|
||||||
|
Reload,
|
||||||
|
NewFolder,
|
||||||
|
NewFile,
|
||||||
|
Rename,
|
||||||
|
Delete,
|
||||||
|
Cut,
|
||||||
|
Copy,
|
||||||
|
Paste,
|
||||||
|
ToggleShowHidden,
|
||||||
|
OpenTerminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolbarAction {
|
||||||
|
/// Stable list of every supported action. Ordered roughly by logical
|
||||||
|
/// grouping (location → create/edit → clipboard → view/misc) so that
|
||||||
|
/// the default enabled set follows a sensible shape and the Settings
|
||||||
|
/// row for a not-yet-enabled action lands in a predictable spot.
|
||||||
|
pub const ALL: &'static [Self] = &[
|
||||||
|
Self::LocationUp,
|
||||||
|
Self::Reload,
|
||||||
|
Self::NewFolder,
|
||||||
|
Self::NewFile,
|
||||||
|
Self::Rename,
|
||||||
|
Self::Delete,
|
||||||
|
Self::Cut,
|
||||||
|
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)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct DesktopConfig {
|
pub struct DesktopConfig {
|
||||||
|
|
|
||||||
|
|
@ -732,11 +732,17 @@ impl App {
|
||||||
fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {
|
fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {
|
||||||
let location = self.tab.location.clone();
|
let location = self.tab.location.clone();
|
||||||
let icon_sizes = self.tab.config.icon_sizes;
|
let icon_sizes = self.tab.config.icon_sizes;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mounter_items = self.mounter_items.clone();
|
let mounter_items = self.mounter_items.clone();
|
||||||
Task::future(async move {
|
Task::future(async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
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")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
let mounter_paths: Box<[_]> = mounter_items
|
let mounter_paths: Box<[_]> = mounter_items
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ mod zoom;
|
||||||
|
|
||||||
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
|
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
|
||||||
|
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
||||||
err.to_string()
|
err.to_string()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use cosmic::widget::{
|
||||||
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
|
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
|
||||||
};
|
};
|
||||||
use cosmic::{Element, theme};
|
use cosmic::{Element, theme};
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
use i18n_embed::LanguageLoader;
|
use i18n_embed::LanguageLoader;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -190,11 +191,11 @@ pub fn context_menu<'a>(
|
||||||
if !Trash::is_empty() {
|
if !Trash::is_empty() {
|
||||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
||||||
}
|
}
|
||||||
} else if let Some(entry) = selected_desktop_entry {
|
} else if let Some(_entry) = selected_desktop_entry {
|
||||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
{
|
{
|
||||||
children.extend(entry.desktop_actions.into_iter().enumerate().map(
|
children.extend(_entry.desktop_actions.into_iter().enumerate().map(
|
||||||
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,19 @@ use cosmic::desktop;
|
||||||
use cosmic::widget;
|
use cosmic::widget;
|
||||||
pub use mime_guess::Mime;
|
pub use mime_guess::Mime;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::cmp::Ordering;
|
#[cfg(feature = "desktop")]
|
||||||
use std::ffi::OsStr;
|
use std::{cmp::Ordering, fs, io, time::Instant};
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::{
|
||||||
use std::path::{Path, PathBuf};
|
ffi::OsStr,
|
||||||
use std::time::Instant;
|
os::unix::ffi::OsStrExt,
|
||||||
use std::{fs, io, process};
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supported exec key field codes
|
||||||
|
const EXEC_HANDLERS: [&str; 4] = ["%f", "%F", "%u", "%U"];
|
||||||
|
// Deprecated field codes. The spec advises to ignore these handlers.
|
||||||
|
const DEPRECATED_HANDLERS: [&str; 6] = ["%d", "%D", "%n", "%N", "%v", "%m"];
|
||||||
|
|
||||||
pub fn exec_to_command(
|
pub fn exec_to_command(
|
||||||
exec: &str,
|
exec: &str,
|
||||||
|
|
@ -364,9 +371,12 @@ impl MimeAppCache {
|
||||||
// The current approach works but might not adhere to the spec (yet)
|
// The current approach works but might not adhere to the spec (yet)
|
||||||
|
|
||||||
// Look for and return preferred terminals
|
// Look for and return preferred terminals
|
||||||
//TODO: fallback order beyond cosmic-term?
|
// Yoda: cosmic-yoterm (our fork) wins over upstream cosmic-term if both
|
||||||
|
// are installed — useful when xdg-mime default is not set.
|
||||||
let mut preference_order = vec!["com.system76.CosmicTerm".to_string()];
|
let mut preference_order = vec![
|
||||||
|
"com.aditua.CosmicYoterm".to_string(),
|
||||||
|
"com.system76.CosmicTerm".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
if let Some(id) = self.get_default_terminal() {
|
if let Some(id) = self.get_default_terminal() {
|
||||||
preference_order.insert(0, id);
|
preference_order.insert(0, id);
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,10 @@ impl MounterItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {
|
pub fn icon(&self, _symbolic: bool) -> Option<widget::icon::Handle> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
Self::Gvfs(item) => item.icon(symbolic),
|
Self::Gvfs(item) => item.icon(_symbolic),
|
||||||
Self::None => unreachable!(),
|
Self::None => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +103,7 @@ impl MounterItem {
|
||||||
pub type MounterItems = Vec<MounterItem>;
|
pub type MounterItems = Vec<MounterItem>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum MounterMessage {
|
pub enum MounterMessage {
|
||||||
Items(MounterItems),
|
Items(MounterItems),
|
||||||
MountResult(MounterItem, Result<bool, String>),
|
MountResult(MounterItem, Result<bool, String>),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use compio::driver::ToSharedFd;
|
||||||
use compio::driver::op::AsyncifyFd;
|
use compio::driver::op::AsyncifyFd;
|
||||||
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
||||||
use cosmic::iced::futures;
|
use cosmic::iced::futures;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
|
||||||
83
src/tab.rs
83
src/tab.rs
|
|
@ -18,6 +18,7 @@ use cosmic::widget::menu::action::MenuAction;
|
||||||
use cosmic::widget::menu::key_bind::KeyBind;
|
use cosmic::widget::menu::key_bind::KeyBind;
|
||||||
use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space};
|
use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space};
|
||||||
use cosmic::{Apply, Element, cosmic_theme, font, theme};
|
use cosmic::{Apply, Element, cosmic_theme, font, theme};
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
use i18n_embed::LanguageLoader;
|
use i18n_embed::LanguageLoader;
|
||||||
use icu::datetime::input::DateTime;
|
use icu::datetime::input::DateTime;
|
||||||
use icu::datetime::options::TimePrecision;
|
use icu::datetime::options::TimePrecision;
|
||||||
|
|
@ -288,6 +289,26 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han
|
||||||
.handle()
|
.handle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generic_file_icons(
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> (
|
||||||
|
widget::icon::Handle,
|
||||||
|
widget::icon::Handle,
|
||||||
|
widget::icon::Handle,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.grid())
|
||||||
|
.handle(),
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.list())
|
||||||
|
.handle(),
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.list_condensed())
|
||||||
|
.handle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: replace with Path::has_trailing_sep when stable
|
//TODO: replace with Path::has_trailing_sep when stable
|
||||||
fn has_trailing_sep(path: &Path) -> bool {
|
fn has_trailing_sep(path: &Path) -> bool {
|
||||||
path.as_os_str()
|
path.as_os_str()
|
||||||
|
|
@ -544,7 +565,7 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "desktop"))]
|
#[cfg(not(feature = "desktop"))]
|
||||||
fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
fn get_desktop_file_display_name(_path: &Path) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,7 +584,7 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "desktop"))]
|
#[cfg(not(feature = "desktop"))]
|
||||||
fn get_desktop_file_icon(path: &Path) -> Option<String> {
|
fn get_desktop_file_icon(_path: &Path) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -654,9 +675,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive
|
// Keep the initial directory scan cheap. Opening files still
|
||||||
// @todo - expose this as a config option?
|
// recalculates MIME from the real path before launching apps.
|
||||||
let mime = mime_for_path(&path, None, true);
|
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||||
|
|
||||||
//TODO: clean this up, implement for trash
|
//TODO: clean this up, implement for trash
|
||||||
let icon_name_opt = if mime == "application/x-desktop" {
|
let icon_name_opt = if mime == "application/x-desktop" {
|
||||||
|
|
@ -673,28 +694,21 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
generic_file_icons(sizes);
|
||||||
(
|
(
|
||||||
mime.clone(),
|
mime,
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
icon_handle_grid,
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
icon_handle_list,
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
icon_handle_list_condensed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut children_opt = None;
|
let children_opt = None;
|
||||||
let mut dir_size = DirSize::NotDirectory;
|
let mut dir_size = DirSize::NotDirectory;
|
||||||
if is_dir && !remote {
|
if is_dir && !remote {
|
||||||
dir_size = DirSize::Calculating(Controller::default());
|
dir_size = DirSize::Calculating(Controller::default());
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
children_opt = Some(entries.count());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {}: {}", path.display(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
|
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
|
||||||
|
|
@ -748,7 +762,10 @@ pub fn item_from_entry(
|
||||||
sizes: IconSizes,
|
sizes: IconSizes,
|
||||||
) -> Item {
|
) -> Item {
|
||||||
let mut is_desktop = false;
|
let mut is_desktop = false;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mut is_gvfs = false;
|
let mut is_gvfs = false;
|
||||||
|
#[cfg(not(feature = "gvfs"))]
|
||||||
|
let is_gvfs = false;
|
||||||
|
|
||||||
let hidden = name.starts_with('.') || hidden_attribute(&metadata);
|
let hidden = name.starts_with('.') || hidden_attribute(&metadata);
|
||||||
|
|
||||||
|
|
@ -796,7 +813,9 @@ pub fn item_from_entry(
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let mime = mime_for_path(&path, Some(&metadata), remote);
|
// Keep the initial directory scan cheap. Opening files still
|
||||||
|
// recalculates MIME from the real path before launching apps.
|
||||||
|
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||||
//TODO: clean this up, implement for trash
|
//TODO: clean this up, implement for trash
|
||||||
let icon_name_opt = if mime == "application/x-desktop" {
|
let icon_name_opt = if mime == "application/x-desktop" {
|
||||||
is_desktop = true;
|
is_desktop = true;
|
||||||
|
|
@ -812,28 +831,21 @@ pub fn item_from_entry(
|
||||||
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
generic_file_icons(sizes);
|
||||||
(
|
(
|
||||||
mime.clone(),
|
mime,
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
icon_handle_grid,
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
icon_handle_list,
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
icon_handle_list_condensed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut children_opt = None;
|
let children_opt = None;
|
||||||
let mut dir_size = DirSize::NotDirectory;
|
let mut dir_size = DirSize::NotDirectory;
|
||||||
if metadata.is_dir() && !remote {
|
if metadata.is_dir() && !remote {
|
||||||
dir_size = DirSize::Calculating(Controller::default());
|
dir_size = DirSize::Calculating(Controller::default());
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
children_opt = Some(entries.count());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {}: {}", path.display(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);
|
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);
|
||||||
|
|
@ -947,7 +959,10 @@ pub fn item_from_path<P: Into<PathBuf>>(path: P, sizes: IconSizes) -> Result<Ite
|
||||||
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let mut hidden_files = Box::from([]);
|
let mut hidden_files = Box::from([]);
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mut remote_scannable = false;
|
let mut remote_scannable = false;
|
||||||
|
#[cfg(not(feature = "gvfs"))]
|
||||||
|
let remote_scannable = false;
|
||||||
|
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@
|
||||||
use cosmic::desktop::fde::GenericEntry;
|
use cosmic::desktop::fde::GenericEntry;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::path::Path;
|
#[cfg(feature = "desktop")]
|
||||||
use std::sync::{LazyLock, Mutex};
|
use std::{fs, time::Instant};
|
||||||
use std::time::Instant;
|
use std::{
|
||||||
use std::{fs, process};
|
path::Path,
|
||||||
|
process,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Thumbnailer {
|
pub struct Thumbnailer {
|
||||||
|
|
|
||||||
10
src/trash.rs
10
src/trash.rs
|
|
@ -142,4 +142,12 @@ impl TrashExt for Trash {
|
||||||
not(target_os = "android")
|
not(target_os = "android")
|
||||||
)
|
)
|
||||||
)))]
|
)))]
|
||||||
impl TrashExt for Trash {}
|
impl TrashExt for Trash {
|
||||||
|
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
|
||||||
|
log::warn!(
|
||||||
|
"searching trash not supported on this platform for pattern {:?}",
|
||||||
|
regex.as_str()
|
||||||
|
);
|
||||||
|
drop(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue