Desktop mode
This commit is contained in:
parent
bbcfe19375
commit
5d596239be
12 changed files with 640 additions and 218 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -1286,11 +1286,19 @@ dependencies = [
|
|||
"uzers",
|
||||
"vergen",
|
||||
"walkdir",
|
||||
"wayland-client",
|
||||
"xdg",
|
||||
"xdg-mime",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-files-applet"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmic-files",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-protocols"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ tokio = { version = "1", features = ["sync"] }
|
|||
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "delete-info" }
|
||||
url = "2.5"
|
||||
walkdir = "2.5.0"
|
||||
wayland-client = { version = "0.31.5", optional = true }
|
||||
xdg = { version = "2.5.2", optional = true }
|
||||
xdg-mime = "0.3"
|
||||
# Internationalization
|
||||
|
|
@ -58,7 +59,7 @@ uzers = "0.12.0"
|
|||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic.git"
|
||||
default-features = false
|
||||
features = ["a11y", "multi-window", "tokio"]
|
||||
features = ["a11y", "clipboard", "multi-window", "tokio"]
|
||||
|
||||
[dependencies.smol_str]
|
||||
version = "0.2.1"
|
||||
|
|
@ -69,7 +70,7 @@ default = ["desktop", "gvfs", "notify", "winit", "wgpu"]
|
|||
desktop = ["libcosmic/desktop", "dep:freedesktop_entry_parser", "dep:xdg"]
|
||||
gvfs = ["dep:gio", "dep:glib"]
|
||||
notify = ["dep:notify-rust"]
|
||||
wayland = ["libcosmic/wayland"]
|
||||
wayland = ["libcosmic/wayland", "dep:wayland-client"]
|
||||
winit = ["libcosmic/winit"]
|
||||
wgpu = ["libcosmic/wgpu"]
|
||||
|
||||
|
|
@ -108,3 +109,6 @@ filetime = { git = "https://github.com/jackpot51/filetime" }
|
|||
|
||||
# [patch.'https://github.com/pop-os/smithay-clipboard']
|
||||
# smithay-clipboard = { path = "../smithay-clipboard" }
|
||||
|
||||
[workspace]
|
||||
members = ["cosmic-files-applet"]
|
||||
|
|
|
|||
9
cosmic-files-applet/Cargo.toml
Normal file
9
cosmic-files-applet/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "cosmic-files-applet"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.cosmic-files]
|
||||
path = ".."
|
||||
default-features = false
|
||||
features = ["desktop", "gvfs", "wayland", "wgpu"]
|
||||
3
cosmic-files-applet/src/main.rs
Normal file
3
cosmic-files-applet/src/main.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
cosmic_files::desktop()
|
||||
}
|
||||
|
|
@ -131,7 +131,6 @@ restored = Restored {$items} {$items ->
|
|||
[one] item
|
||||
*[other] items
|
||||
} from {trash}
|
||||
undo = Undo
|
||||
unknown-folder = unknown folder
|
||||
|
||||
## Open with
|
||||
|
|
|
|||
8
justfile
8
justfile
|
|
@ -12,6 +12,10 @@ cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
|
|||
bin-src := cargo-target-dir / 'release' / name
|
||||
bin-dst := base-dir / 'bin' / name
|
||||
|
||||
applet-name := name + '-applet'
|
||||
applet-src := cargo-target-dir / 'release' / applet-name
|
||||
applet-dst := base-dir / 'bin' / applet-name
|
||||
|
||||
desktop := APPID + '.desktop'
|
||||
desktop-src := 'res' / desktop
|
||||
desktop-dst := clean(rootdir / prefix) / 'share' / 'applications' / desktop
|
||||
|
|
@ -40,6 +44,7 @@ clean-dist: clean clean-vendor
|
|||
# Compiles with debug profile
|
||||
build-debug *args:
|
||||
cargo build {{args}}
|
||||
cargo build --package {{applet-name}} {{args}}
|
||||
|
||||
# Compiles with release profile
|
||||
build-release *args: (build-debug '--release' args)
|
||||
|
|
@ -71,6 +76,7 @@ test *args:
|
|||
# Installs files
|
||||
install:
|
||||
install -Dm0755 {{bin-src}} {{bin-dst}}
|
||||
install -Dm0755 {{applet-src}} {{applet-dst}}
|
||||
install -Dm0644 {{desktop-src}} {{desktop-dst}}
|
||||
install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
|
||||
for size in `ls {{icons-src}}`; do \
|
||||
|
|
@ -79,7 +85,7 @@ install:
|
|||
|
||||
# Uninstalls installed files
|
||||
uninstall:
|
||||
rm {{bin-dst}}
|
||||
rm -f {{bin-dst}} {{applet-dst}}
|
||||
|
||||
# Vendor dependencies locally
|
||||
vendor:
|
||||
|
|
|
|||
281
src/app.rs
281
src/app.rs
|
|
@ -1,8 +1,19 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
use cosmic::iced::{
|
||||
event::wayland::{Event as WaylandEvent, OutputEvent},
|
||||
wayland::{
|
||||
actions::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
||||
layer_surface::{
|
||||
destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity, Layer,
|
||||
},
|
||||
},
|
||||
Limits,
|
||||
};
|
||||
use cosmic::{
|
||||
app::{message, Command, Core},
|
||||
app::{self, message, Command, Core},
|
||||
cosmic_config, cosmic_theme, executor,
|
||||
iced::{
|
||||
clipboard::dnd::DndAction,
|
||||
|
|
@ -10,8 +21,7 @@ use cosmic::{
|
|||
futures::{self, SinkExt},
|
||||
keyboard::{Event as KeyEvent, Key, Modifiers},
|
||||
subscription::{self, Subscription},
|
||||
widget::scrollable,
|
||||
window::{self, Event as WindowEvent},
|
||||
window::{self, Event as WindowEvent, Id as WindowId},
|
||||
Alignment, Event, Length,
|
||||
},
|
||||
iced_runtime::clipboard,
|
||||
|
|
@ -43,6 +53,8 @@ use std::{
|
|||
};
|
||||
use tokio::sync::mpsc;
|
||||
use trash::TrashItem;
|
||||
#[cfg(feature = "wayland")]
|
||||
use wayland_client::{protocol::wl_output::WlOutput, Proxy};
|
||||
|
||||
use crate::{
|
||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
||||
|
|
@ -50,17 +62,25 @@ use crate::{
|
|||
fl, home_dir,
|
||||
key_bind::key_binds,
|
||||
localize::LANGUAGE_SORTER,
|
||||
menu, mime_app,
|
||||
menu, mime_app, mime_icon,
|
||||
mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters},
|
||||
operation::{Operation, ReplaceResult},
|
||||
spawn_detached::spawn_detached,
|
||||
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Mode {
|
||||
App,
|
||||
Desktop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Flags {
|
||||
pub config_handler: Option<cosmic_config::Config>,
|
||||
pub config: Config,
|
||||
pub mode: Mode,
|
||||
pub locations: Vec<Location>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -280,6 +300,10 @@ pub enum Message {
|
|||
DndDropTab(Entity, Option<ClipboardPaste>, DndAction),
|
||||
DndDropNav(Entity, Option<ClipboardPaste>, DndAction),
|
||||
Recents,
|
||||
#[cfg(feature = "wayland")]
|
||||
OutputEvent(OutputEvent, WlOutput),
|
||||
Cosmic(app::cosmic::Message),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -393,6 +417,7 @@ pub struct App {
|
|||
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
mode: Mode,
|
||||
app_themes: Vec<String>,
|
||||
default_view: Vec<String>,
|
||||
sort_by_names: Vec<String>,
|
||||
|
|
@ -413,6 +438,10 @@ pub struct App {
|
|||
search_active: bool,
|
||||
search_id: widget::Id,
|
||||
search_input: String,
|
||||
#[cfg(feature = "wayland")]
|
||||
surface_ids: HashMap<WlOutput, WindowId>,
|
||||
#[cfg(feature = "wayland")]
|
||||
surface_names: HashMap<WindowId, String>,
|
||||
toasts: widget::toaster::Toasts<Message>,
|
||||
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
|
||||
window_id_opt: Option<window::Id>,
|
||||
|
|
@ -429,7 +458,11 @@ impl App {
|
|||
activate: bool,
|
||||
selection_path: Option<PathBuf>,
|
||||
) -> Command<Message> {
|
||||
let tab = Tab::new(location.clone(), self.config.tab);
|
||||
let mut tab = Tab::new(location.clone(), self.config.tab);
|
||||
tab.mode = match self.mode {
|
||||
Mode::App => tab::Mode::App,
|
||||
Mode::Desktop => tab::Mode::Desktop,
|
||||
};
|
||||
let entity = self
|
||||
.tab_model
|
||||
.insert()
|
||||
|
|
@ -1043,6 +1076,19 @@ impl Application for App {
|
|||
|
||||
/// Creates the application, and optionally emits command on initialize.
|
||||
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
match flags.mode {
|
||||
Mode::App => {}
|
||||
Mode::Desktop => {
|
||||
core.window.content_container = false;
|
||||
core.window.show_window_menu = false;
|
||||
core.window.show_headerbar = false;
|
||||
core.window.sharp_corners = false;
|
||||
core.window.show_maximize = false;
|
||||
core.window.show_minimize = false;
|
||||
core.window.use_template = true;
|
||||
}
|
||||
}
|
||||
|
||||
let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")];
|
||||
|
||||
let mut app = App {
|
||||
|
|
@ -1052,6 +1098,7 @@ impl Application for App {
|
|||
tab_model: segmented_button::ModelBuilder::default().build(),
|
||||
config_handler: flags.config_handler,
|
||||
config: flags.config,
|
||||
mode: flags.mode,
|
||||
app_themes,
|
||||
default_view: vec![fl!("grid-view"), fl!("list-view")],
|
||||
sort_by_names: HeadingOptions::names(),
|
||||
|
|
@ -1072,6 +1119,10 @@ impl Application for App {
|
|||
search_active: false,
|
||||
search_id: widget::Id::unique(),
|
||||
search_input: String::new(),
|
||||
#[cfg(feature = "wayland")]
|
||||
surface_ids: HashMap::new(),
|
||||
#[cfg(feature = "wayland")]
|
||||
surface_names: HashMap::new(),
|
||||
toasts: widget::toaster::Toasts::new(Message::CloseToast),
|
||||
watcher_opt: None,
|
||||
window_id_opt: Some(window::Id::MAIN),
|
||||
|
|
@ -1083,18 +1134,7 @@ impl Application for App {
|
|||
|
||||
let mut commands = vec![app.update_config()];
|
||||
|
||||
for arg in env::args().skip(1) {
|
||||
let location = if &arg == "--trash" {
|
||||
Location::Trash
|
||||
} else {
|
||||
match fs::canonicalize(&arg) {
|
||||
Ok(absolute) => Location::Path(absolute),
|
||||
Err(err) => {
|
||||
log::warn!("failed to canonicalize {:?}: {}", arg, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
for location in flags.locations {
|
||||
commands.push(app.open_tab(location, true, None));
|
||||
}
|
||||
|
||||
|
|
@ -1199,7 +1239,10 @@ impl Application for App {
|
|||
}
|
||||
|
||||
fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> {
|
||||
Some(&self.nav_model)
|
||||
match self.mode {
|
||||
Mode::App => Some(&self.nav_model),
|
||||
Mode::Desktop => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_nav_select(&mut self, entity: Entity) -> Command<Self::Message> {
|
||||
|
|
@ -2049,27 +2092,76 @@ impl Application for App {
|
|||
self.rescan_tab(entity, tab_path, selection_path),
|
||||
]));
|
||||
}
|
||||
tab::Command::DropFiles(to, from) => {
|
||||
commands.push(self.update(Message::PasteContents(to, from)));
|
||||
}
|
||||
tab::Command::EmptyTrash => {
|
||||
self.dialog_pages.push_back(DialogPage::EmptyTrash);
|
||||
}
|
||||
tab::Command::FocusButton(id) => {
|
||||
commands.push(widget::button::focus(id));
|
||||
tab::Command::Iced(iced_command) => {
|
||||
commands.push(iced_command.map(move |tab_message| {
|
||||
message::app(Message::TabMessage(Some(entity), tab_message))
|
||||
}));
|
||||
}
|
||||
tab::Command::FocusTextInput(id) => {
|
||||
commands.push(widget::text_input::focus(id));
|
||||
tab::Command::LocationProperties(index) => {
|
||||
self.context_page =
|
||||
ContextPage::Properties(Some(ContextItem::BreadCrumbs(index)));
|
||||
self.core.window.show_context = true;
|
||||
self.set_context_title(self.context_page.title());
|
||||
}
|
||||
tab::Command::OpenFile(item_path) => {
|
||||
match open::that_detached(&item_path) {
|
||||
Ok(()) => {
|
||||
let _ = recently_used_xbel::update_recently_used(
|
||||
&item_path,
|
||||
App::APP_ID.to_string(),
|
||||
"cosmic-files".to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to open {:?}: {}", item_path, err);
|
||||
tab::Command::MoveToTrash(paths) => {
|
||||
self.operation(Operation::Delete { paths });
|
||||
}
|
||||
tab::Command::OpenFile(path) => {
|
||||
let mut found_desktop_exec = false;
|
||||
if mime_icon::mime_for_path(&path) == "application/x-desktop" {
|
||||
match freedesktop_entry_parser::parse_entry(&path) {
|
||||
Ok(entry) => {
|
||||
match entry.section("Desktop Entry").attr("Exec") {
|
||||
Some(exec) => {
|
||||
match mime_app::exec_to_command(exec, None) {
|
||||
Some(mut command) => {
|
||||
match spawn_detached(&mut command) {
|
||||
Ok(()) => {
|
||||
found_desktop_exec = true;
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to execute {:?}: {}",
|
||||
path,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::warn!("failed to parse {:?}: invalid Desktop Entry/Exec", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::warn!("failed to parse {:?}: missing Desktop Entry/Exec", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse {:?}: {}", path, err);
|
||||
}
|
||||
};
|
||||
}
|
||||
if !found_desktop_exec {
|
||||
match open::that_detached(&path) {
|
||||
Ok(()) => {
|
||||
let _ = recently_used_xbel::update_recently_used(
|
||||
&path,
|
||||
App::APP_ID.to_string(),
|
||||
"cosmic-files".to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to open {:?}: {}", path, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2087,35 +2179,6 @@ impl Application for App {
|
|||
log::error!("failed to get current executable path: {}", err);
|
||||
}
|
||||
},
|
||||
tab::Command::LocationProperties(index) => {
|
||||
self.context_page =
|
||||
ContextPage::Properties(Some(ContextItem::BreadCrumbs(index)));
|
||||
self.core.window.show_context = true;
|
||||
self.set_context_title(self.context_page.title());
|
||||
}
|
||||
tab::Command::Scroll(id, offset) => {
|
||||
commands.push(scrollable::scroll_to(id, offset));
|
||||
}
|
||||
tab::Command::DropFiles(to, from) => {
|
||||
commands.push(self.update(Message::PasteContents(to, from)));
|
||||
}
|
||||
tab::Command::Timeout(d, tab_msg) => {
|
||||
commands.push(Command::perform(
|
||||
async move {
|
||||
tokio::time::sleep(d).await;
|
||||
tab_msg
|
||||
},
|
||||
move |msg| {
|
||||
cosmic::app::Message::App(Message::TabMessage(
|
||||
Some(entity),
|
||||
msg,
|
||||
))
|
||||
},
|
||||
));
|
||||
}
|
||||
tab::Command::MoveToTrash(paths) => {
|
||||
self.operation(Operation::Delete { paths });
|
||||
}
|
||||
}
|
||||
}
|
||||
return Command::batch(commands);
|
||||
|
|
@ -2386,6 +2449,80 @@ impl Application for App {
|
|||
Message::Recents => {
|
||||
return self.open_tab(Location::Recents, false, None);
|
||||
}
|
||||
#[cfg(feature = "wayland")]
|
||||
Message::OutputEvent(output_event, output) => {
|
||||
match output_event {
|
||||
OutputEvent::Created(output_info_opt) => {
|
||||
log::info!("output {}: created", output.id());
|
||||
|
||||
let surface_id = WindowId::unique();
|
||||
match self.surface_ids.insert(output.clone(), surface_id) {
|
||||
Some(old_surface_id) => {
|
||||
//TODO: remove old surface?
|
||||
log::warn!(
|
||||
"output {}: already had surface ID {:?}",
|
||||
output.id(),
|
||||
old_surface_id
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
match output_info_opt {
|
||||
Some(output_info) => match output_info.name {
|
||||
Some(output_name) => {
|
||||
self.surface_names.insert(surface_id, output_name.clone());
|
||||
}
|
||||
None => {
|
||||
log::warn!("output {}: no output name", output.id());
|
||||
}
|
||||
},
|
||||
None => {
|
||||
log::warn!("output {}: no output info", output.id());
|
||||
}
|
||||
}
|
||||
|
||||
return Command::batch([get_layer_surface(SctkLayerSurfaceSettings {
|
||||
id: surface_id,
|
||||
layer: Layer::Bottom,
|
||||
keyboard_interactivity: KeyboardInteractivity::OnDemand,
|
||||
pointer_interactivity: true,
|
||||
anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT,
|
||||
output: IcedOutput::Output(output),
|
||||
namespace: "cosmic-files-applet".into(),
|
||||
size: Some((None, None)),
|
||||
margin: IcedMargin {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
exclusive_zone: -1,
|
||||
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
|
||||
})]);
|
||||
}
|
||||
OutputEvent::Removed => {
|
||||
log::info!("output {}: removed", output.id());
|
||||
match self.surface_ids.remove(&output) {
|
||||
Some(surface_id) => {
|
||||
self.surface_names.remove(&surface_id);
|
||||
return destroy_layer_surface(surface_id);
|
||||
}
|
||||
None => {
|
||||
log::warn!("output {}: no surface found", output.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
OutputEvent::InfoUpdate(_output_info) => {
|
||||
log::info!("output {}: info update", output.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Cosmic(cosmic) => {
|
||||
// Forward cosmic messages
|
||||
return Command::perform(async move { cosmic }, |cosmic| message::cosmic(cosmic));
|
||||
}
|
||||
Message::None => {}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
|
|
@ -2796,6 +2933,15 @@ impl Application for App {
|
|||
content
|
||||
}
|
||||
|
||||
fn view_window(&self, id: WindowId) -> Element<Self::Message> {
|
||||
//TODO: distinct views per window?
|
||||
self.view_main().map(|message| match message {
|
||||
app::Message::App(app) => app,
|
||||
app::Message::Cosmic(cosmic) => Message::Cosmic(cosmic),
|
||||
app::Message::None => Message::None,
|
||||
})
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
struct ThemeSubscription;
|
||||
struct WatcherSubscription;
|
||||
|
|
@ -2811,6 +2957,15 @@ impl Application for App {
|
|||
Some(Message::Modifiers(modifiers))
|
||||
}
|
||||
Event::Window(_id, WindowEvent::CloseRequested) => Some(Message::WindowClose),
|
||||
#[cfg(feature = "wayland")]
|
||||
Event::PlatformSpecific(event::PlatformSpecific::Wayland(wayland_event)) => {
|
||||
match wayland_event {
|
||||
WaylandEvent::Output(output_event, output) => {
|
||||
Some(Message::OutputEvent(output_event, output))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
Config::subscription().map(|update| {
|
||||
|
|
|
|||
|
|
@ -13,11 +13,14 @@ use cosmic::{
|
|||
futures::{self, SinkExt},
|
||||
keyboard::{Event as KeyEvent, Modifiers},
|
||||
subscription::{self, Subscription},
|
||||
widget::scrollable,
|
||||
window, Alignment, Event, Length, Size,
|
||||
},
|
||||
theme,
|
||||
widget::{self, menu::KeyBind, segmented_button},
|
||||
widget::{
|
||||
self,
|
||||
menu::{Action as MenuAction, KeyBind},
|
||||
segmented_button,
|
||||
},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use notify_debouncer_full::{
|
||||
|
|
@ -36,7 +39,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
app::Action,
|
||||
app::{Action, Message as AppMessage},
|
||||
config::{Config, Favorite, TabConfig},
|
||||
fl, home_dir,
|
||||
localize::LANGUAGE_SORTER,
|
||||
|
|
@ -556,7 +559,7 @@ impl Application for App {
|
|||
..Default::default()
|
||||
};
|
||||
let mut tab = Tab::new(location, tab_config);
|
||||
tab.dialog = Some(flags.kind.clone());
|
||||
tab.mode = tab::Mode::Dialog(flags.kind.clone());
|
||||
|
||||
let view_model = segmented_button::SingleSelectModel::builder()
|
||||
.insert(|b| {
|
||||
|
|
@ -957,24 +960,24 @@ impl Application for App {
|
|||
let mut commands = Vec::new();
|
||||
for tab_command in tab_commands {
|
||||
match tab_command {
|
||||
tab::Command::Action(action) => {
|
||||
log::warn!("Action {:?} not supported in dialog", action);
|
||||
}
|
||||
tab::Command::Action(action) => match action.message() {
|
||||
AppMessage::TabMessage(_entity_opt, tab_message) => {
|
||||
commands.push(self.update(Message::TabMessage(tab_message)));
|
||||
}
|
||||
unsupported => {
|
||||
log::warn!("{unsupported:?} not supported in dialog mode");
|
||||
}
|
||||
},
|
||||
tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_path) => {
|
||||
commands
|
||||
.push(Command::batch([self.update_watcher(), self.rescan_tab()]));
|
||||
}
|
||||
tab::Command::DropFiles(_, _) => {
|
||||
log::warn!("DropFiles not supported in dialog");
|
||||
}
|
||||
tab::Command::EmptyTrash => {
|
||||
log::warn!("EmptyTrash not supported in dialog");
|
||||
}
|
||||
tab::Command::FocusButton(id) => {
|
||||
commands.push(widget::button::focus(id));
|
||||
}
|
||||
tab::Command::FocusTextInput(id) => {
|
||||
commands.push(widget::text_input::focus(id));
|
||||
tab::Command::Iced(iced_command) => {
|
||||
commands.push(
|
||||
iced_command.map(|tab_message| {
|
||||
message::app(Message::TabMessage(tab_message))
|
||||
}),
|
||||
);
|
||||
}
|
||||
tab::Command::OpenFile(_item_path) => {
|
||||
if self.flags.kind.save() {
|
||||
|
|
@ -983,23 +986,8 @@ impl Application for App {
|
|||
commands.push(self.update(Message::Open));
|
||||
}
|
||||
}
|
||||
tab::Command::OpenInNewTab(_path) => {
|
||||
log::warn!("OpenInNewTab not supported in dialog");
|
||||
}
|
||||
tab::Command::OpenInNewWindow(_path) => {
|
||||
log::warn!("OpenInNewWindow not supported in dialog");
|
||||
}
|
||||
tab::Command::LocationProperties(_path) => {
|
||||
log::warn!("LocationProperties not supported in dialog");
|
||||
}
|
||||
tab::Command::Scroll(id, offset) => {
|
||||
commands.push(scrollable::scroll_to(id, offset));
|
||||
}
|
||||
tab::Command::Timeout(_, _) => {
|
||||
log::warn!("Timeout not supported in dialog");
|
||||
}
|
||||
tab::Command::MoveToTrash(_) => {
|
||||
log::warn!("MoveToTrash not supported in dialog");
|
||||
unsupported => {
|
||||
log::warn!("{unsupported:?} not supported in dialog mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
src/lib.rs
64
src/lib.rs
|
|
@ -5,13 +5,13 @@ use cosmic::{
|
|||
app::{Application, Settings},
|
||||
iced::Limits,
|
||||
};
|
||||
use std::{path::PathBuf, process};
|
||||
use std::{env, fs, path::PathBuf, process};
|
||||
|
||||
use app::{App, Flags};
|
||||
mod app;
|
||||
pub mod app;
|
||||
pub mod clipboard;
|
||||
use config::Config;
|
||||
mod config;
|
||||
pub mod config;
|
||||
pub mod dialog;
|
||||
mod key_bind;
|
||||
mod localize;
|
||||
|
|
@ -22,7 +22,8 @@ mod mounter;
|
|||
mod mouse_area;
|
||||
mod operation;
|
||||
mod spawn_detached;
|
||||
mod tab;
|
||||
use tab::Location;
|
||||
pub mod tab;
|
||||
|
||||
pub fn home_dir() -> PathBuf {
|
||||
match dirs::home_dir() {
|
||||
|
|
@ -34,6 +35,43 @@ pub fn home_dir() -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Runs application in desktop mode
|
||||
#[rustfmt::skip]
|
||||
pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
|
||||
localize::localize();
|
||||
|
||||
let (config_handler, config) = Config::load();
|
||||
|
||||
let locations = vec![
|
||||
match dirs::desktop_dir() {
|
||||
Some(path) => Location::Path(path),
|
||||
None => Location::Path(home_dir()),
|
||||
}
|
||||
];
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings = settings.theme(config.app_theme.theme());
|
||||
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
||||
settings = settings.exit_on_close(false);
|
||||
settings = settings.transparent(true);
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
settings = settings.no_main_window(true);
|
||||
}
|
||||
|
||||
let flags = Flags {
|
||||
config_handler,
|
||||
config,
|
||||
mode: app::Mode::Desktop,
|
||||
locations,
|
||||
};
|
||||
cosmic::app::run::<App>(settings, flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
@ -53,6 +91,22 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
let (config_handler, config) = Config::load();
|
||||
|
||||
let mut locations = Vec::new();
|
||||
for arg in env::args().skip(1) {
|
||||
let location = if &arg == "--trash" {
|
||||
Location::Trash
|
||||
} else {
|
||||
match fs::canonicalize(&arg) {
|
||||
Ok(absolute) => Location::Path(absolute),
|
||||
Err(err) => {
|
||||
log::warn!("failed to canonicalize {:?}: {}", arg, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
locations.push(location);
|
||||
}
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings = settings.theme(config.app_theme.theme());
|
||||
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
||||
|
|
@ -61,6 +115,8 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let flags = Flags {
|
||||
config_handler,
|
||||
config,
|
||||
mode: app::Mode::App,
|
||||
locations,
|
||||
};
|
||||
cosmic::app::run::<App>(settings, flags)?;
|
||||
|
||||
|
|
|
|||
57
src/menu.rs
57
src/menu.rs
|
|
@ -97,8 +97,11 @@ pub fn context_menu<'a>(
|
|||
selected_types.dedup();
|
||||
|
||||
let mut children: Vec<Element<_>> = Vec::new();
|
||||
match tab.location {
|
||||
Location::Path(_) | Location::Search(_, _) | Location::Recents => {
|
||||
match (&tab.mode, &tab.location) {
|
||||
(
|
||||
tab::Mode::App | tab::Mode::Desktop,
|
||||
Location::Path(_) | Location::Search(_, _) | Location::Recents,
|
||||
) => {
|
||||
if selected > 0 {
|
||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
|
|
@ -155,7 +158,9 @@ pub fn context_menu<'a>(
|
|||
children.push(menu_item(fl!("new-file"), Action::NewFile).into());
|
||||
children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
}
|
||||
children.push(menu_item(fl!("paste"), Action::Paste).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
// TODO: Nested menu
|
||||
|
|
@ -164,20 +169,52 @@ pub fn context_menu<'a>(
|
|||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
}
|
||||
Location::Trash => {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
(
|
||||
tab::Mode::Dialog(dialog_kind),
|
||||
Location::Path(_) | Location::Search(_, _) | Location::Recents,
|
||||
) => {
|
||||
if selected > 0 {
|
||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
}
|
||||
if matches!(tab.location, Location::Search(_, _)) {
|
||||
children.push(
|
||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if dialog_kind.save() {
|
||||
children.push(menu_item(fl!("new-folder"), Action::NewFolder).into());
|
||||
}
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
}
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
}
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
}
|
||||
(_, Location::Trash) => {
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
}
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
}
|
||||
if selected > 0 {
|
||||
children.push(menu_item(fl!("show-details"), Action::Properties).into());
|
||||
children.push(divider::horizontal::light().into());
|
||||
children
|
||||
.push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into());
|
||||
} else {
|
||||
// TODO: Nested menu
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
children.push(divider::horizontal::light().into());
|
||||
// TODO: Nested menu
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,30 @@ use std::{
|
|||
cmp::Ordering, collections::HashMap, env, path::PathBuf, process, sync::Mutex, time::Instant,
|
||||
};
|
||||
|
||||
pub fn exec_to_command(exec: &str, path_opt: Option<PathBuf>) -> Option<process::Command> {
|
||||
let args_vec: Vec<String> = shlex::split(exec)?;
|
||||
let mut args = args_vec.iter();
|
||||
let mut command = process::Command::new(args.next()?);
|
||||
for arg in args {
|
||||
if arg.starts_with('%') {
|
||||
match arg.as_str() {
|
||||
"%f" | "%F" | "%u" | "%U" => {
|
||||
if let Some(path) = &path_opt {
|
||||
command.arg(path);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("unsupported Exec code {:?} in {:?}", arg, exec);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
command.arg(arg);
|
||||
}
|
||||
}
|
||||
Some(command)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MimeApp {
|
||||
pub id: String,
|
||||
|
|
@ -23,27 +47,7 @@ pub struct MimeApp {
|
|||
impl MimeApp {
|
||||
//TODO: move to libcosmic, support multiple files
|
||||
pub fn command(&self, path_opt: Option<PathBuf>) -> Option<process::Command> {
|
||||
let args_vec: Vec<String> = self.exec.as_deref().and_then(shlex::split)?;
|
||||
let mut args = args_vec.iter();
|
||||
let mut command = process::Command::new(args.next()?);
|
||||
for arg in args {
|
||||
if arg.starts_with('%') {
|
||||
match arg.as_str() {
|
||||
"%f" | "%F" | "%u" | "%U" => {
|
||||
if let Some(path) = &path_opt {
|
||||
command.arg(path);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("unsupported Exec code {:?} in {:?}", arg, self.id);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
command.arg(arg);
|
||||
}
|
||||
}
|
||||
Some(command)
|
||||
exec_to_command(self.exec.as_deref()?, path_opt)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
315
src/tab.rs
315
src/tab.rs
|
|
@ -13,7 +13,7 @@ use cosmic::{
|
|||
//TODO: export in cosmic::widget
|
||||
widget::{
|
||||
container, horizontal_rule,
|
||||
scrollable::{AbsoluteOffset, Viewport},
|
||||
scrollable::{self, AbsoluteOffset, Viewport},
|
||||
},
|
||||
Alignment,
|
||||
Border,
|
||||
|
|
@ -111,6 +111,7 @@ fn button_appearance(
|
|||
focused: bool,
|
||||
accent: bool,
|
||||
condensed_radius: bool,
|
||||
desktop: bool,
|
||||
) -> widget::button::Appearance {
|
||||
let cosmic = theme.cosmic();
|
||||
let mut appearance = widget::button::Appearance::new();
|
||||
|
|
@ -122,6 +123,10 @@ fn button_appearance(
|
|||
} else {
|
||||
appearance.background = Some(Color::from(cosmic.bg_component_color()).into());
|
||||
}
|
||||
} else if desktop {
|
||||
appearance.background = Some(Color::from(cosmic.bg_color()).into());
|
||||
appearance.icon_color = Some(Color::from(cosmic.on_bg_color()));
|
||||
appearance.text_color = Some(Color::from(cosmic.on_bg_color()));
|
||||
}
|
||||
if focused && accent {
|
||||
appearance.outline_width = 1.0;
|
||||
|
|
@ -137,20 +142,25 @@ fn button_appearance(
|
|||
appearance
|
||||
}
|
||||
|
||||
fn button_style(selected: bool, accent: bool, condensed_radius: bool) -> theme::Button {
|
||||
fn button_style(
|
||||
selected: bool,
|
||||
accent: bool,
|
||||
condensed_radius: bool,
|
||||
desktop: bool,
|
||||
) -> theme::Button {
|
||||
//TODO: move to libcosmic?
|
||||
theme::Button::Custom {
|
||||
active: Box::new(move |focused, theme| {
|
||||
button_appearance(theme, selected, focused, accent, condensed_radius)
|
||||
button_appearance(theme, selected, focused, accent, condensed_radius, desktop)
|
||||
}),
|
||||
disabled: Box::new(move |theme| {
|
||||
button_appearance(theme, selected, false, accent, condensed_radius)
|
||||
button_appearance(theme, selected, false, accent, condensed_radius, desktop)
|
||||
}),
|
||||
hovered: Box::new(move |focused, theme| {
|
||||
button_appearance(theme, selected, focused, accent, condensed_radius)
|
||||
button_appearance(theme, selected, focused, accent, condensed_radius, desktop)
|
||||
}),
|
||||
pressed: Box::new(move |focused, theme| {
|
||||
button_appearance(theme, selected, focused, accent, condensed_radius)
|
||||
button_appearance(theme, selected, focused, accent, condensed_radius, desktop)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -263,13 +273,33 @@ fn hidden_attribute(metadata: &Metadata) -> bool {
|
|||
metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN
|
||||
}
|
||||
|
||||
pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
|
||||
let entry = match freedesktop_entry_parser::parse_entry(path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse {:?}: {}", path, err);
|
||||
return (None, None);
|
||||
}
|
||||
};
|
||||
(
|
||||
entry
|
||||
.section("Desktop Entry")
|
||||
.attr("Name")
|
||||
.map(|x| x.to_string()),
|
||||
entry
|
||||
.section("Desktop Entry")
|
||||
.attr("Icon")
|
||||
.map(|x| x.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn item_from_entry(
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
metadata: fs::Metadata,
|
||||
sizes: IconSizes,
|
||||
) -> Item {
|
||||
let grid_name = Item::grid_name(&name);
|
||||
let mut display_name = Item::display_name(&name);
|
||||
|
||||
let hidden = name.starts_with(".") || hidden_attribute(&metadata);
|
||||
|
||||
|
|
@ -284,12 +314,37 @@ pub fn item_from_entry(
|
|||
)
|
||||
} else {
|
||||
let mime = mime_for_path(&path);
|
||||
(
|
||||
mime.clone(),
|
||||
mime_icon(mime.clone(), sizes.grid()),
|
||||
mime_icon(mime.clone(), sizes.list()),
|
||||
mime_icon(mime, sizes.list_condensed()),
|
||||
)
|
||||
//TODO: clean this up, implement for trash
|
||||
let icon_name_opt = if mime == "application/x-desktop" {
|
||||
let (desktop_name_opt, icon_name_opt) = parse_desktop_file(&path);
|
||||
if let Some(desktop_name) = desktop_name_opt {
|
||||
display_name = Item::display_name(&desktop_name);
|
||||
}
|
||||
icon_name_opt
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(icon_name) = icon_name_opt {
|
||||
(
|
||||
mime.clone(),
|
||||
widget::icon::from_name(&*icon_name)
|
||||
.size(sizes.grid())
|
||||
.handle(),
|
||||
widget::icon::from_name(&*icon_name)
|
||||
.size(sizes.list())
|
||||
.handle(),
|
||||
widget::icon::from_name(&*icon_name)
|
||||
.size(sizes.list_condensed())
|
||||
.handle(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
mime.clone(),
|
||||
mime_icon(mime.clone(), sizes.grid()),
|
||||
mime_icon(mime.clone(), sizes.list()),
|
||||
mime_icon(mime, sizes.list_condensed()),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let open_with = mime_apps(&mime);
|
||||
|
|
@ -319,7 +374,7 @@ pub fn item_from_entry(
|
|||
|
||||
Item {
|
||||
name,
|
||||
grid_name,
|
||||
display_name,
|
||||
metadata: ItemMetadata::Path { metadata, children },
|
||||
hidden,
|
||||
path_opt: Some(path),
|
||||
|
|
@ -401,7 +456,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
|||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
|
||||
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
||||
});
|
||||
items
|
||||
}
|
||||
|
|
@ -538,7 +593,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
|||
|
||||
let original_path = entry.original_path();
|
||||
let name = entry.name.to_string_lossy().to_string();
|
||||
let grid_name = Item::grid_name(&name);
|
||||
let display_name = Item::display_name(&name);
|
||||
|
||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||
match metadata.size {
|
||||
|
|
@ -563,7 +618,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
|||
|
||||
items.push(Item {
|
||||
name,
|
||||
grid_name,
|
||||
display_name,
|
||||
metadata: ItemMetadata::Trash { metadata, entry },
|
||||
hidden: false,
|
||||
path_opt: None,
|
||||
|
|
@ -588,7 +643,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
|||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
|
||||
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
||||
});
|
||||
items
|
||||
}
|
||||
|
|
@ -699,21 +754,18 @@ impl Location {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
Action(Action),
|
||||
ChangeLocation(String, Location, Option<PathBuf>),
|
||||
DropFiles(PathBuf, ClipboardPaste),
|
||||
EmptyTrash,
|
||||
FocusButton(widget::Id),
|
||||
FocusTextInput(widget::Id),
|
||||
Iced(cosmic::Command<Message>),
|
||||
LocationProperties(usize),
|
||||
MoveToTrash(Vec<PathBuf>),
|
||||
OpenFile(PathBuf),
|
||||
OpenInNewTab(PathBuf),
|
||||
OpenInNewWindow(PathBuf),
|
||||
LocationProperties(usize),
|
||||
Scroll(widget::Id, AbsoluteOffset),
|
||||
DropFiles(PathBuf, ClipboardPaste),
|
||||
Timeout(Duration, Message),
|
||||
MoveToTrash(Vec<PathBuf>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -808,7 +860,7 @@ pub enum ItemThumbnail {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct Item {
|
||||
pub name: String,
|
||||
pub grid_name: String,
|
||||
pub display_name: String,
|
||||
pub metadata: ItemMetadata,
|
||||
pub hidden: bool,
|
||||
pub path_opt: Option<PathBuf>,
|
||||
|
|
@ -826,7 +878,7 @@ pub struct Item {
|
|||
}
|
||||
|
||||
impl Item {
|
||||
fn grid_name(name: &str) -> String {
|
||||
fn display_name(name: &str) -> String {
|
||||
// In order to wrap at periods and underscores, add a zero width space after each one
|
||||
name.replace(".", ".\u{200B}").replace("_", "_\u{200B}")
|
||||
}
|
||||
|
|
@ -1081,6 +1133,23 @@ impl HeadingOptions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Mode {
|
||||
App,
|
||||
Desktop,
|
||||
Dialog(DialogKind),
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Whether multiple files can be selected in this mode
|
||||
pub fn multiple(&self) -> bool {
|
||||
match self {
|
||||
Mode::App | Mode::Desktop => true,
|
||||
Mode::Dialog(dialog) => dialog.multiple(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO when creating items, pass <Arc<SelectedItems>> to each item
|
||||
// as a drag data, so that when dnd is initiated, they are all included
|
||||
#[derive(Clone)]
|
||||
|
|
@ -1090,7 +1159,7 @@ pub struct Tab {
|
|||
pub location_context_menu_point: Option<Point>,
|
||||
pub location_context_menu_index: Option<usize>,
|
||||
pub context_menu: Option<Point>,
|
||||
pub dialog: Option<DialogKind>,
|
||||
pub mode: Mode,
|
||||
pub scroll_opt: Option<AbsoluteOffset>,
|
||||
pub size_opt: Cell<Option<Size>>,
|
||||
pub item_view_size_opt: Cell<Option<Size>>,
|
||||
|
|
@ -1136,7 +1205,7 @@ impl Tab {
|
|||
context_menu: None,
|
||||
location_context_menu_point: None,
|
||||
location_context_menu_index: None,
|
||||
dialog: None,
|
||||
mode: Mode::App,
|
||||
scroll_opt: None,
|
||||
size_opt: Cell::new(None),
|
||||
item_view_size_opt: Cell::new(None),
|
||||
|
|
@ -1181,6 +1250,10 @@ impl Tab {
|
|||
self.items_opt.as_ref()
|
||||
}
|
||||
|
||||
pub fn items_opt_mut(&mut self) -> Option<&mut Vec<Item>> {
|
||||
self.items_opt.as_mut()
|
||||
}
|
||||
|
||||
pub fn set_items(&mut self, items: Vec<Item>) {
|
||||
self.items_opt = Some(items);
|
||||
}
|
||||
|
|
@ -1441,10 +1514,8 @@ impl Tab {
|
|||
let mut commands = Vec::new();
|
||||
let mut cd = None;
|
||||
let mut history_i_opt = None;
|
||||
let mod_ctrl = modifiers.contains(Modifiers::CTRL)
|
||||
&& self.dialog.as_ref().map_or(true, |x| x.multiple());
|
||||
let mod_shift = modifiers.contains(Modifiers::SHIFT)
|
||||
&& self.dialog.as_ref().map_or(true, |x| x.multiple());
|
||||
let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();
|
||||
let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();
|
||||
match message {
|
||||
Message::ClickRelease(click_i_opt) => {
|
||||
if click_i_opt == self.clicked.take() {
|
||||
|
|
@ -1590,7 +1661,7 @@ impl Tab {
|
|||
for (i, item) in items.iter_mut().enumerate() {
|
||||
if Some(i) == click_i_opt {
|
||||
// Filter out selection if it does not match dialog kind
|
||||
if let Some(dialog) = &self.dialog {
|
||||
if let Mode::Dialog(dialog) = &self.mode {
|
||||
let item_is_dir = item.metadata.is_dir();
|
||||
if item_is_dir != dialog.is_dir() {
|
||||
// Allow selecting folder if dialog is for files to make it
|
||||
|
|
@ -1616,7 +1687,7 @@ impl Tab {
|
|||
}
|
||||
if self.select_focus.take().is_some() {
|
||||
// Unfocus currently focused button
|
||||
commands.push(Command::FocusButton(widget::Id::unique()));
|
||||
commands.push(Command::Iced(widget::button::focus(widget::Id::unique())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1674,14 +1745,16 @@ impl Tab {
|
|||
self.select_rect(rect, mod_ctrl, mod_shift);
|
||||
if self.select_focus.take().is_some() {
|
||||
// Unfocus currently focused button
|
||||
commands.push(Command::FocusButton(widget::Id::unique()));
|
||||
commands.push(Command::Iced(widget::button::focus(widget::Id::unique())));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
Message::EditLocation(edit_location) => {
|
||||
if self.edit_location.is_none() && edit_location.is_some() {
|
||||
commands.push(Command::FocusTextInput(self.edit_location_id.clone()));
|
||||
commands.push(Command::Iced(widget::text_input::focus(
|
||||
self.edit_location_id.clone(),
|
||||
)));
|
||||
}
|
||||
self.edit_location = edit_location;
|
||||
}
|
||||
|
|
@ -1727,10 +1800,13 @@ impl Tab {
|
|||
self.select_position(0, 0, mod_shift);
|
||||
}
|
||||
if let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Scroll(self.scrollable_id.clone(), offset));
|
||||
commands.push(Command::Iced(scrollable::scroll_to(
|
||||
self.scrollable_id.clone(),
|
||||
offset,
|
||||
)));
|
||||
}
|
||||
if let Some(id) = self.select_focus_id() {
|
||||
commands.push(Command::FocusButton(id));
|
||||
commands.push(Command::Iced(widget::button::focus(id)));
|
||||
}
|
||||
}
|
||||
Message::ItemLeft => {
|
||||
|
|
@ -1772,10 +1848,13 @@ impl Tab {
|
|||
self.select_position(0, 0, mod_shift);
|
||||
}
|
||||
if let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Scroll(self.scrollable_id.clone(), offset));
|
||||
commands.push(Command::Iced(scrollable::scroll_to(
|
||||
self.scrollable_id.clone(),
|
||||
offset,
|
||||
)));
|
||||
}
|
||||
if let Some(id) = self.select_focus_id() {
|
||||
commands.push(Command::FocusButton(id));
|
||||
commands.push(Command::Iced(widget::button::focus(id)));
|
||||
}
|
||||
}
|
||||
Message::ItemRight => {
|
||||
|
|
@ -1799,10 +1878,13 @@ impl Tab {
|
|||
self.select_position(0, 0, mod_shift);
|
||||
}
|
||||
if let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Scroll(self.scrollable_id.clone(), offset));
|
||||
commands.push(Command::Iced(scrollable::scroll_to(
|
||||
self.scrollable_id.clone(),
|
||||
offset,
|
||||
)));
|
||||
}
|
||||
if let Some(id) = self.select_focus_id() {
|
||||
commands.push(Command::FocusButton(id));
|
||||
commands.push(Command::Iced(widget::button::focus(id)));
|
||||
}
|
||||
}
|
||||
Message::ItemUp => {
|
||||
|
|
@ -1829,10 +1911,13 @@ impl Tab {
|
|||
self.select_position(0, 0, mod_shift);
|
||||
}
|
||||
if let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Scroll(self.scrollable_id.clone(), offset));
|
||||
commands.push(Command::Iced(scrollable::scroll_to(
|
||||
self.scrollable_id.clone(),
|
||||
offset,
|
||||
)));
|
||||
}
|
||||
if let Some(id) = self.select_focus_id() {
|
||||
commands.push(Command::FocusButton(id));
|
||||
commands.push(Command::Iced(widget::button::focus(id)));
|
||||
}
|
||||
}
|
||||
Message::Location(location) => {
|
||||
|
|
@ -1929,7 +2014,7 @@ impl Tab {
|
|||
self.select_all();
|
||||
if self.select_focus.take().is_some() {
|
||||
// Unfocus currently focused button
|
||||
commands.push(Command::FocusButton(widget::Id::unique()));
|
||||
commands.push(Command::Iced(widget::button::focus(widget::Id::unique())));
|
||||
}
|
||||
}
|
||||
Message::Thumbnail(path, thumbnail) => {
|
||||
|
|
@ -2011,7 +2096,13 @@ impl Tab {
|
|||
Message::DndEnter(loc) => {
|
||||
self.dnd_hovered = Some((loc.clone(), Instant::now()));
|
||||
if loc != self.location {
|
||||
commands.push(Command::Timeout(HOVER_DURATION, Message::DndHover(loc)));
|
||||
commands.push(Command::Iced(cosmic::Command::perform(
|
||||
async move {
|
||||
tokio::time::sleep(HOVER_DURATION).await;
|
||||
Message::DndHover(loc)
|
||||
},
|
||||
|x| x,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Message::DndLeave(loc) => {
|
||||
|
|
@ -2063,7 +2154,14 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
if let Some(location) = cd {
|
||||
if location != self.location {
|
||||
if matches!(self.mode, Mode::Desktop) {
|
||||
match location {
|
||||
Location::Path(path) => {
|
||||
commands.push(Command::OpenFile(path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if location != self.location {
|
||||
if match &location {
|
||||
Location::Path(path) => path.is_dir(),
|
||||
Location::Search(path, _term) => path.is_dir(),
|
||||
|
|
@ -2129,12 +2227,15 @@ impl Tab {
|
|||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => check_reverse(
|
||||
LANGUAGE_SORTER.compare(&a.1.name, &b.1.name),
|
||||
LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),
|
||||
heading_sort,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
check_reverse(LANGUAGE_SORTER.compare(&a.1.name, &b.1.name), heading_sort)
|
||||
check_reverse(
|
||||
LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),
|
||||
heading_sort,
|
||||
)
|
||||
}
|
||||
}),
|
||||
HeadingOptions::Modified => {
|
||||
|
|
@ -2494,21 +2595,25 @@ impl Tab {
|
|||
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||
|
||||
//TODO: left clicking on an empty folder does not clear context menu
|
||||
widget::column::with_children(vec![widget::container(
|
||||
widget::column::with_children(vec![
|
||||
widget::icon::from_name("folder-symbolic")
|
||||
.size(64)
|
||||
.icon()
|
||||
widget::column::with_children(match self.mode {
|
||||
Mode::App | Mode::Dialog(_) => vec![
|
||||
widget::icon::from_name("folder-symbolic")
|
||||
.size(64)
|
||||
.icon()
|
||||
.into(),
|
||||
widget::text(if has_hidden {
|
||||
fl!("empty-folder-hidden")
|
||||
} else if matches!(self.location, Location::Search(_, _)) {
|
||||
fl!("no-results")
|
||||
} else {
|
||||
fl!("empty-folder")
|
||||
})
|
||||
.into(),
|
||||
widget::text(if has_hidden {
|
||||
fl!("empty-folder-hidden")
|
||||
} else if matches!(self.location, Location::Search(_, _)) {
|
||||
fl!("no-results")
|
||||
} else {
|
||||
fl!("empty-folder")
|
||||
})
|
||||
.into(),
|
||||
])
|
||||
],
|
||||
Mode::Desktop => Vec::new(),
|
||||
})
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(space_xxs),
|
||||
)
|
||||
|
|
@ -2568,6 +2673,12 @@ impl Tab {
|
|||
(cols, spacing as u16)
|
||||
};
|
||||
|
||||
let rows = {
|
||||
let height_m1 = height.checked_sub(item_height).unwrap_or(0);
|
||||
let rows_m1 = height_m1 / (item_height + space_xxs as usize);
|
||||
rows_m1 + 1
|
||||
};
|
||||
|
||||
let mut grid = widget::grid()
|
||||
.column_spacing(column_spacing)
|
||||
.row_spacing(space_xxs)
|
||||
|
|
@ -2584,7 +2695,9 @@ impl Tab {
|
|||
let mut count = 0;
|
||||
let mut col = 0;
|
||||
let mut row = 0;
|
||||
let mut page_row = 0;
|
||||
let mut hidden = 0;
|
||||
let mut grid_elements = Vec::new();
|
||||
for &(i, item) in items.iter() {
|
||||
if !show_hidden && item.hidden {
|
||||
item.pos_opt.set(None);
|
||||
|
|
@ -2609,11 +2722,16 @@ impl Tab {
|
|||
.size(icon_sizes.grid()),
|
||||
)
|
||||
.padding(space_xxxs)
|
||||
.style(button_style(item.selected, false, false)),
|
||||
widget::button(widget::text::body(&item.grid_name))
|
||||
.style(button_style(item.selected, false, false, false)),
|
||||
widget::button(widget::text::body(&item.display_name))
|
||||
.id(item.button_id.clone())
|
||||
.padding([0, space_xxxs])
|
||||
.style(button_style(item.selected, true, true)),
|
||||
.style(button_style(
|
||||
item.selected,
|
||||
true,
|
||||
true,
|
||||
matches!(self.mode, Mode::Desktop),
|
||||
)),
|
||||
];
|
||||
|
||||
let mut column = widget::column::with_capacity(buttons.len())
|
||||
|
|
@ -2696,17 +2814,41 @@ impl Tab {
|
|||
.on_double_click(move |_| Message::DoubleClick(Some(i)))
|
||||
.on_release(move |_| Message::ClickRelease(Some(i)))
|
||||
.on_middle_press(move |_| Message::MiddleClick(i));
|
||||
grid = grid.push(mouse_area);
|
||||
|
||||
//TODO: error if the row or col is already set?
|
||||
while grid_elements.len() <= row {
|
||||
grid_elements.push(Vec::new());
|
||||
}
|
||||
grid_elements[row].push(mouse_area);
|
||||
|
||||
count += 1;
|
||||
col += 1;
|
||||
if col >= cols {
|
||||
col = 0;
|
||||
if matches!(self.mode, Mode::Desktop) {
|
||||
row += 1;
|
||||
grid = grid.insert_row();
|
||||
if row >= page_row + rows {
|
||||
row = 0;
|
||||
col += 1;
|
||||
}
|
||||
if col >= cols {
|
||||
col = 0;
|
||||
page_row += rows;
|
||||
row = page_row;
|
||||
}
|
||||
} else {
|
||||
col += 1;
|
||||
if col >= cols {
|
||||
col = 0;
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for row_elements in grid_elements {
|
||||
for element in row_elements {
|
||||
grid = grid.push(element);
|
||||
}
|
||||
grid = grid.insert_row();
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return (None, self.empty_view(hidden > 0), false);
|
||||
}
|
||||
|
|
@ -2770,12 +2912,13 @@ impl Tab {
|
|||
item.selected,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)),
|
||||
widget::button(widget::text(item.grid_name.clone()))
|
||||
widget::button(widget::text(item.display_name.clone()))
|
||||
.id(item.button_id.clone())
|
||||
.on_press(Message::Click(Some(*i)))
|
||||
.padding([0, space_xxxs])
|
||||
.style(button_style(item.selected, true, true)),
|
||||
.style(button_style(item.selected, true, true, false)),
|
||||
];
|
||||
|
||||
let mut column = widget::column::with_capacity(buttons.len())
|
||||
|
|
@ -2914,7 +3057,7 @@ impl Tab {
|
|||
.size(icon_size)
|
||||
.into(),
|
||||
widget::column::with_children(vec![
|
||||
widget::text(item.name.clone()).into(),
|
||||
widget::text(item.display_name.clone()).into(),
|
||||
//TODO: translate?
|
||||
widget::text::caption(format!("{} - {}", modified_text, size_text))
|
||||
.into(),
|
||||
|
|
@ -2930,7 +3073,9 @@ impl Tab {
|
|||
.content_fit(ContentFit::Contain)
|
||||
.size(icon_size)
|
||||
.into(),
|
||||
widget::text(item.name.clone()).width(Length::Fill).into(),
|
||||
widget::text(item.display_name.clone())
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
widget::text(modified_text.clone())
|
||||
.width(Length::Fixed(modified_width))
|
||||
.into(),
|
||||
|
|
@ -2949,7 +3094,7 @@ impl Tab {
|
|||
.width(Length::Fill)
|
||||
.id(item.button_id.clone())
|
||||
.padding([0, space_xxs])
|
||||
.style(button_style(item.selected, true, false)),
|
||||
.style(button_style(item.selected, true, false, false)),
|
||||
)
|
||||
.on_press(move |_| Message::Click(Some(i)))
|
||||
.on_double_click(move |_| Message::DoubleClick(Some(i)))
|
||||
|
|
@ -3029,7 +3174,7 @@ impl Tab {
|
|||
.size(icon_size)
|
||||
.into(),
|
||||
widget::column::with_children(vec![
|
||||
widget::text(item.name.clone()).into(),
|
||||
widget::text(item.display_name.clone()).into(),
|
||||
//TODO: translate?
|
||||
widget::text(format!("{} - {}", modified_text, size_text)).into(),
|
||||
])
|
||||
|
|
@ -3044,7 +3189,9 @@ impl Tab {
|
|||
.content_fit(ContentFit::Contain)
|
||||
.size(icon_size)
|
||||
.into(),
|
||||
widget::text(item.name.clone()).width(Length::Fill).into(),
|
||||
widget::text(item.display_name.clone())
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
widget::text(modified_text)
|
||||
.width(Length::Fixed(modified_width))
|
||||
.into(),
|
||||
|
|
@ -3121,7 +3268,11 @@ impl Tab {
|
|||
// Update cached size
|
||||
self.size_opt.set(Some(size));
|
||||
|
||||
let location_view = self.location_view();
|
||||
let location_view_opt = if matches!(self.mode, Mode::Desktop) {
|
||||
None
|
||||
} else {
|
||||
Some(self.location_view())
|
||||
};
|
||||
let (drag_list, mut item_view, can_scroll) = match self.config.view {
|
||||
View::Grid => self.grid_view(),
|
||||
View::List => self.list_view(),
|
||||
|
|
@ -3180,7 +3331,9 @@ impl Tab {
|
|||
.position(widget::popover::Position::Point(point));
|
||||
}
|
||||
let mut tab_column = widget::column::with_capacity(3);
|
||||
tab_column = tab_column.push(location_view);
|
||||
if let Some(location_view) = location_view_opt {
|
||||
tab_column = tab_column.push(location_view);
|
||||
}
|
||||
if can_scroll {
|
||||
tab_column = tab_column.push(
|
||||
widget::scrollable(popover)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue