From ca664f009b2d91663a6c530ca959bc1418531e3d Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 11 Sep 2024 09:08:20 -0600 Subject: [PATCH] dialog: show configured navbar items and drives, part of #335 --- src/app.rs | 88 ++++++++--------- src/config.rs | 42 +++++++- src/dialog.rs | 265 ++++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 20 +--- 4 files changed, 303 insertions(+), 112 deletions(-) diff --git a/src/app.rs b/src/app.rs index b562f00..ac9874b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,7 +46,7 @@ use trash::TrashItem; use crate::{ clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, - config::{AppTheme, Config, Favorite, IconSizes, TabConfig, CONFIG_VERSION}, + config::{AppTheme, Config, Favorite, IconSizes, TabConfig}, fl, home_dir, key_bind::key_binds, localize::LANGUAGE_SORTER, @@ -1041,45 +1041,6 @@ impl Application for App { &mut self.core } - fn nav_bar(&self) -> Option>> { - if !self.core().nav_bar_active() { - return None; - } - - let nav_model = self.nav_model()?; - - let mut nav = cosmic::widget::nav_bar(nav_model, |entity| { - cosmic::app::Message::Cosmic(cosmic::app::cosmic::Message::NavBar(entity)) - }) - .drag_id(self.nav_drag_id) - .on_dnd_enter(|entity, _| cosmic::app::Message::App(Message::DndEnterNav(entity))) - .on_dnd_leave(|_| cosmic::app::Message::App(Message::DndExitNav)) - .on_dnd_drop(|entity, data, action| { - cosmic::app::Message::App(Message::DndDropNav(entity, data, action)) - }) - .on_context(|entity| cosmic::app::Message::App(Message::NavBarContext(entity))) - .on_close(|entity| cosmic::app::Message::App(Message::NavBarClose(entity))) - .on_middle_press(|entity| { - cosmic::app::Message::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity))) - }) - .context_menu(self.nav_context_menu(self.nav_bar_context_id)) - .close_icon( - widget::icon::from_name("media-eject-symbolic") - .size(16) - .icon(), - ) - .into_container(); - - if !self.core().is_condensed() { - nav = nav.max_width(280); - } - - Some(Element::from( - // XXX both must be shrink to avoid flex layout from ignoring it - nav.width(Length::Shrink).height(Length::Shrink), - )) - } - /// Creates the application, and optionally emits command on initialize. fn init(mut core: Core, flags: Self::Flags) -> (Self, Command) { let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")]; @@ -1152,6 +1113,45 @@ impl Application for App { self.window_id_opt.unwrap_or(window::Id::MAIN) } + fn nav_bar(&self) -> Option>> { + if !self.core().nav_bar_active() { + return None; + } + + let nav_model = self.nav_model()?; + + let mut nav = cosmic::widget::nav_bar(nav_model, |entity| { + cosmic::app::Message::Cosmic(cosmic::app::cosmic::Message::NavBar(entity)) + }) + .drag_id(self.nav_drag_id) + .on_dnd_enter(|entity, _| cosmic::app::Message::App(Message::DndEnterNav(entity))) + .on_dnd_leave(|_| cosmic::app::Message::App(Message::DndExitNav)) + .on_dnd_drop(|entity, data, action| { + cosmic::app::Message::App(Message::DndDropNav(entity, data, action)) + }) + .on_context(|entity| cosmic::app::Message::App(Message::NavBarContext(entity))) + .on_close(|entity| cosmic::app::Message::App(Message::NavBarClose(entity))) + .on_middle_press(|entity| { + cosmic::app::Message::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity))) + }) + .context_menu(self.nav_context_menu(self.nav_bar_context_id)) + .close_icon( + widget::icon::from_name("media-eject-symbolic") + .size(16) + .icon(), + ) + .into_container(); + + if !self.core().is_condensed() { + nav = nav.max_width(280); + } + + Some(Element::from( + // XXX both must be shrink to avoid flex layout from ignoring it + nav.width(Length::Shrink).height(Length::Shrink), + )) + } + fn nav_context_menu( &self, id: widget::nav_bar::Id, @@ -2797,7 +2797,6 @@ impl Application for App { } fn subscription(&self) -> Subscription { - struct ConfigSubscription; struct ThemeSubscription; struct WatcherSubscription; struct TrashWatcherSubscription; @@ -2814,12 +2813,7 @@ impl Application for App { Event::Window(_id, WindowEvent::CloseRequested) => Some(Message::WindowClose), _ => None, }), - cosmic_config::config_subscription( - TypeId::of::(), - Self::APP_ID.into(), - CONFIG_VERSION, - ) - .map(|update| { + Config::subscription().map(|update| { if !update.errors.is_empty() { log::info!( "errors loading config {:?}: {:?}", diff --git a/src/config.rs b/src/config.rs index 96c5ed3..f23a4fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-only -use std::{num::NonZeroU16, path::PathBuf}; +use std::{any::TypeId, num::NonZeroU16, path::PathBuf}; use cosmic::{ cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry}, - theme, + iced::subscription::Subscription, + theme, Application, }; use serde::{Deserialize, Serialize}; -use crate::tab::View; - -use super::tab::HeadingOptions; +use crate::{ + app::App, + tab::{HeadingOptions, View}, +}; pub const CONFIG_VERSION: u64 = 1; @@ -97,6 +99,36 @@ pub struct Config { pub tab: TabConfig, } +impl Config { + pub fn load() -> (Option, Self) { + match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) { + Ok(config_handler) => { + let config = match Config::get_entry(&config_handler) { + Ok(ok) => ok, + Err((errs, config)) => { + log::info!("errors loading config: {:?}", errs); + config + } + }; + (Some(config_handler), config) + } + Err(err) => { + log::error!("failed to create config handler: {}", err); + (None, Config::default()) + } + } + } + + pub fn subscription() -> Subscription> { + struct ConfigSubscription; + cosmic_config::config_subscription( + TypeId::of::(), + App::APP_ID.into(), + CONFIG_VERSION, + ) + } +} + impl Default for Config { fn default() -> Self { Self { diff --git a/src/dialog.rs b/src/dialog.rs index 47ec081..c85b8e3 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -7,7 +7,7 @@ use cosmic::iced::multi_window::Application as IcedApplication; use cosmic::iced::Application as IcedApplication; use cosmic::{ app::{self, cosmic::Cosmic, message, Command, Core}, - cosmic_theme, executor, + cosmic_config, cosmic_theme, executor, iced::{ event, futures::{self, SinkExt}, @@ -37,8 +37,10 @@ use std::{ use crate::{ app::Action, - config::TabConfig, + config::{Config, Favorite, TabConfig}, fl, home_dir, + localize::LANGUAGE_SORTER, + mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters}, tab::{self, ItemMetadata, Location, Tab}, }; @@ -152,6 +154,8 @@ impl Dialog { //TODO: only do this once somehow? crate::localize::localize(); + let (config_handler, config) = Config::load(); + let mut settings = window::Settings::default(); settings.decorations = false; settings.exit_on_close_request = false; @@ -181,6 +185,8 @@ impl Dialog { } }), window_id, + config_handler, + config, }; let (cosmic, cosmic_command) = as IcedApplication>::new((core, flags)); @@ -279,6 +285,8 @@ struct Flags { kind: DialogKind, path_opt: Option, window_id: window::Id, + config_handler: Option, + config: Config, } /// Messages that are used specifically by our [`App`]. @@ -286,9 +294,11 @@ struct Flags { enum Message { Cancel, Choice(usize, usize), + Config(Config), Filename(String), Filter(usize), Modifiers(Modifiers), + MounterItems(MounterKey, MounterItems), NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), Open, @@ -297,6 +307,8 @@ enum Message { TabRescan(Vec), } +pub struct MounterData(MounterKey, MounterItem); + struct WatcherWrapper { watcher_opt: Option>, } @@ -330,6 +342,8 @@ struct App { filter_selected: Option, filename_id: widget::Id, modifiers: Modifiers, + mounters: Mounters, + mounter_items: HashMap, nav_model: segmented_button::SingleSelectModel, result_opt: Option, replace_dialog: bool, @@ -356,6 +370,93 @@ impl App { ) } + fn update_config(&mut self) -> Command { + self.update_nav_model(); + Command::none() + } + + fn activate_nav_model_location(&mut self, location: &Location) { + let nav_bar_id = self.nav_model.iter().find(|&id| { + self.nav_model + .data::(id) + .map(|l| l == location) + .unwrap_or_default() + }); + + if let Some(id) = nav_bar_id { + self.nav_model.activate(id); + } else { + let active = self.nav_model.active(); + segmented_button::Selectable::deactivate(&mut self.nav_model, active); + } + } + + fn update_nav_model(&mut self) { + let mut nav_model = segmented_button::ModelBuilder::default(); + + nav_model = nav_model.insert(|b| { + b.text(fl!("recents")) + .icon(widget::icon::from_name("accessories-clock-symbolic")) + .data(Location::Recents) + }); + + for (_favorite_i, favorite) in self.flags.config.favorites.iter().enumerate() { + if let Some(path) = favorite.path_opt() { + let name = if matches!(favorite, Favorite::Home) { + fl!("home") + } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) { + file_name.to_string() + } else { + continue; + }; + nav_model = nav_model.insert(move |b| { + b.text(name.clone()) + .icon( + widget::icon::icon(if path.is_dir() { + tab::folder_icon_symbolic(&path, 16) + } else { + widget::icon::from_name("text-x-generic-symbolic") + .size(16) + .handle() + }) + .size(16), + ) + .data(Location::Path(path.clone())) + }); + } + } + + // Collect all mounter items + let mut nav_items = Vec::new(); + for (key, items) in self.mounter_items.iter() { + for item in items.iter() { + nav_items.push((*key, item)); + } + } + // Sort by name lexically + nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name())); + // Add items to nav model + for (key, item) in nav_items { + nav_model = nav_model.insert(|mut b| { + b = b.text(item.name()).data(MounterData(key, item.clone())); + if let Some(path) = item.path() { + b = b.data(Location::Path(path.clone())); + } + if let Some(icon) = item.icon() { + b = b.icon(widget::icon::icon(icon).size(16)); + } + if item.is_mounted() { + b = b.closable(); + } + b + }); + } + + self.nav_model = nav_model.build(); + + self.activate_nav_model_location(&self.tab.location.clone()); + } + fn update_title(&mut self) -> Command { self.set_header_title(self.title.clone()); self.set_window_title(self.title.clone(), self.main_window_id()) @@ -440,40 +541,6 @@ impl Application for App { let title = flags.kind.title(); let accept_label = flags.kind.accept_label(); - let mut nav_model = segmented_button::ModelBuilder::default(); - - nav_model = nav_model.insert(move |b| { - b.text(fl!("recents")) - .icon(widget::icon::from_name("accessories-clock-symbolic").size(16)) - .data(Location::Recents) - }); - - if let Some(dir) = dirs::home_dir() { - nav_model = nav_model.insert(move |b| { - b.text(fl!("home")) - .icon(widget::icon::icon(tab::folder_icon_symbolic(&dir, 16)).size(16)) - .data(Location::Path(dir.clone())) - }); - } - //TODO: Sort by name? - for dir_opt in &[ - dirs::document_dir(), - dirs::download_dir(), - dirs::audio_dir(), - dirs::picture_dir(), - dirs::video_dir(), - ] { - if let Some(dir) = dir_opt { - if let Some(file_name) = dir.file_name().and_then(|x| x.to_str()) { - nav_model = nav_model.insert(move |b| { - b.text(file_name.to_string()) - .icon(widget::icon::icon(tab::folder_icon_symbolic(&dir, 16)).size(16)) - .data(Location::Path(dir.clone())) - }); - } - } - } - let location = Location::Path(match &flags.path_opt { Some(path) => path.to_path_buf(), None => match env::current_dir() { @@ -496,7 +563,9 @@ impl Application for App { filter_selected: None, filename_id: widget::Id::unique(), modifiers: Modifiers::empty(), - nav_model: nav_model.build(), + mounters: mounters(), + mounter_items: HashMap::new(), + nav_model: segmented_button::ModelBuilder::default().build(), result_opt: None, replace_dialog: false, tab, @@ -504,7 +573,12 @@ impl Application for App { watcher_opt: None, }; - let commands = Command::batch([app.update_title(), app.update_watcher(), app.rescan_tab()]); + let commands = Command::batch([ + app.update_config(), + app.update_title(), + app.update_watcher(), + app.rescan_tab(), + ]); (app, commands) } @@ -533,6 +607,34 @@ impl Application for App { None } + fn nav_bar(&self) -> Option>> { + if !self.core().nav_bar_active() { + return None; + } + + let nav_model = self.nav_model()?; + + let mut nav = cosmic::widget::nav_bar(nav_model, |entity| { + cosmic::app::Message::Cosmic(cosmic::app::cosmic::Message::NavBar(entity)) + }) + //TODO .on_close(|entity| cosmic::app::Message::App(Message::NavBarClose(entity))) + .close_icon( + widget::icon::from_name("media-eject-symbolic") + .size(16) + .icon(), + ) + .into_container(); + + if !self.core().is_condensed() { + nav = nav.max_width(280); + } + + Some(Element::from( + // XXX both must be shrink to avoid flex layout from ignoring it + nav.width(Length::Shrink).height(Length::Shrink), + )) + } + fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> { Some(&self.nav_model) } @@ -545,11 +647,17 @@ impl Application for App { fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Command { let location_opt = self.nav_model.data::(entity).clone(); - if let Some(location) = location_opt { + self.nav_model.activate(entity); + if let Some(location) = self.nav_model.data::(entity) { let message = Message::TabMessage(tab::Message::Location(location.clone())); return self.update(message); } + if let Some(data) = self.nav_model.data::(entity).clone() { + if let Some(mounter) = self.mounters.get(&data.0) { + return mounter.mount(data.1.clone()).map(|_| message::none()); + } + } Command::none() } @@ -584,6 +692,13 @@ impl Application for App { } } } + Message::Config(config) => { + if config != self.flags.config { + log::info!("update config"); + self.flags.config = config; + return self.update_config(); + } + } Message::Filename(new_filename) => { // Select based on filename self.tab.select_name(&new_filename); @@ -603,6 +718,52 @@ impl Application for App { Message::Modifiers(modifiers) => { self.modifiers = modifiers; } + Message::MounterItems(mounter_key, mounter_items) => { + // Check for unmounted folders + let mut unmounted = Vec::new(); + if let Some(old_items) = self.mounter_items.get(&mounter_key) { + for old_item in old_items.iter() { + if let Some(old_path) = old_item.path() { + if old_item.is_mounted() { + let mut still_mounted = false; + for item in mounter_items.iter() { + if let Some(path) = item.path() { + if path == old_path { + if item.is_mounted() { + still_mounted = true; + break; + } + } + } + } + if !still_mounted { + unmounted.push(Location::Path(old_path)); + } + } + } + } + } + + // Go back to home in any tabs that were unmounted + let mut commands = Vec::new(); + { + let home_location = Location::Path(home_dir()); + if unmounted.contains(&self.tab.location) { + self.tab.change_location(&home_location, None); + commands.push(self.update_watcher()); + commands.push(self.rescan_tab()); + } + } + + // Insert new items + self.mounter_items.insert(mounter_key, mounter_items); + + // Update nav bar + //TODO: this could change favorites IDs while they are in use + self.update_nav_model(); + + return Command::batch(commands); + } Message::NotifyEvents(events) => { log::debug!("{:?}", events); @@ -957,14 +1118,23 @@ impl Application for App { fn subscription(&self) -> Subscription { struct WatcherSubscription; - - Subscription::batch([ + let mut subscriptions = vec![ event::listen_with(|event, _status| match event { Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Some(Message::Modifiers(modifiers)) } _ => None, }), + Config::subscription().map(|update| { + if !update.errors.is_empty() { + log::info!( + "errors loading config {:?}: {:?}", + update.keys, + update.errors + ); + } + Message::Config(update.config) + }), subscription::channel( TypeId::of::(), 100, @@ -1041,6 +1211,17 @@ impl Application for App { }, ), self.tab.subscription().map(Message::TabMessage), - ]) + ]; + + for (key, mounter) in self.mounters.iter() { + let key = *key; + subscriptions.push( + mounter + .subscription() + .map(move |items| Message::MounterItems(key, items)), + ); + } + + Subscription::batch(subscriptions) } } diff --git a/src/lib.rs b/src/lib.rs index 6f29c89..0263f89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ use cosmic::{ app::{Application, Settings}, - cosmic_config::{self, CosmicConfigEntry}, iced::Limits, }; use std::{path::PathBuf, process}; @@ -11,7 +10,7 @@ use std::{path::PathBuf, process}; use app::{App, Flags}; mod app; pub mod clipboard; -use config::{Config, CONFIG_VERSION}; +use config::Config; mod config; pub mod dialog; mod key_bind; @@ -52,22 +51,7 @@ pub fn main() -> Result<(), Box> { localize::localize(); - let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) { - Ok(config_handler) => { - let config = match Config::get_entry(&config_handler) { - Ok(ok) => ok, - Err((errs, config)) => { - log::info!("errors loading config: {:?}", errs); - config - } - }; - (Some(config_handler), config) - } - Err(err) => { - log::error!("failed to create config handler: {}", err); - (None, Config::default()) - } - }; + let (config_handler, config) = Config::load(); let mut settings = Settings::default(); settings = settings.theme(config.app_theme.theme());