diff --git a/src/app.rs b/src/app.rs index 5c068fb..5d1049e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -112,7 +112,7 @@ pub enum Action { OpenTerminal, OpenWith, Paste, - Properties, + Preview, Rename, RestoreFromTrash, SearchActivate, @@ -164,7 +164,9 @@ impl Action { Action::OpenTerminal => Message::OpenTerminal(entity_opt), Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith), Action::Paste => Message::Paste(entity_opt), - Action::Properties => Message::ToggleContextPage(ContextPage::Properties(None)), + Action::Preview => { + Message::ToggleContextPage(ContextPage::Preview(entity_opt, PreviewKind::Selected)) + } Action::Rename => Message::Rename(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), Action::SearchActivate => Message::SearchActivate, @@ -210,18 +212,29 @@ impl MenuAction for Action { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ContextItem { - NavBar(segmented_button::Entity), - TabBar(segmented_button::Entity), - BreadCrumbs(usize), +#[derive(Clone, Debug)] +pub struct PreviewItem(pub tab::Item); + +impl PartialEq for PreviewItem { + fn eq(&self, other: &Self) -> bool { + self.0.location_opt == other.0.location_opt + } +} + +impl Eq for PreviewItem {} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PreviewKind { + Custom(PreviewItem), + Location(Location), + Selected, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum NavMenuAction { OpenInNewTab(segmented_button::Entity), OpenInNewWindow(segmented_button::Entity), - Properties(segmented_button::Entity), + Preview(segmented_button::Entity), RemoveFromSidebar(segmented_button::Entity), EmptyTrash, } @@ -279,6 +292,7 @@ pub enum Message { PendingComplete(u64), PendingError(u64, String), PendingProgress(u64, f32), + Preview(Entity, PreviewKind, time::Duration), RescanTrash, Rename(Option), ReplaceResult(ReplaceResult), @@ -317,13 +331,13 @@ pub enum Message { None, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum ContextPage { About, EditHistory, NetworkDrive, OpenWith, - Properties(Option), + Preview(Option, PreviewKind), Settings, } @@ -334,7 +348,7 @@ impl ContextPage { Self::EditHistory => fl!("edit-history"), Self::NetworkDrive => fl!("add-network-drive"), Self::OpenWith => fl!("open-with"), - Self::Properties(..) => String::default(), + Self::Preview(..) => String::default(), Self::Settings => fl!("settings"), } } @@ -461,6 +475,7 @@ pub struct App { pending_operations: BTreeMap, complete_operations: BTreeMap, failed_operations: BTreeMap, + preview_opt: Option<(Entity, PreviewKind, time::Instant)>, search_active: bool, search_id: widget::Id, search_input: String, @@ -949,59 +964,42 @@ impl App { widget::settings::view_column(children).into() } - fn properties(&self, entity: Option) -> Element { - match entity { - None => self.tab_properties(self.tab_model.active()), - Some(ContextItem::TabBar(entity)) => self.tab_properties(entity), - Some(ContextItem::NavBar(item)) => { - let mut children = Vec::with_capacity(1); - if let Some(location) = self.nav_model.data::(item) { - if let Location::Path(path) = location { - //TODO: this should be done once, not when generating the view! - if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) { - children.push(item.property_view(IconSizes::default())); + fn preview(&self, entity_opt: &Option, kind: &PreviewKind) -> Element { + let mut children = Vec::with_capacity(1); + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + match kind { + PreviewKind::Custom(PreviewItem(item)) => { + children.push(item.property_view(IconSizes::default())); + } + PreviewKind::Location(location) => { + if let Some(tab) = self.tab_model.data::(entity) { + if let Some(items) = tab.items_opt() { + for item in items.iter() { + if item.location_opt.as_ref() == Some(location) { + children.push(item.property_view(tab.config.icon_sizes)); + // Only show one property view to avoid issues like hangs when generating + // preview images on thousands of files + break; + } } } } - widget::settings::view_column(children).into() } - - Some(ContextItem::BreadCrumbs(index)) => { - let mut children = Vec::with_capacity(1); - if let Some(tab) = self.tab_model.active_data::() { - let path_opt = tab - .location - .path_opt() - .and_then(|path| path.ancestors().nth(index)) - .map(|path| path.to_path_buf()); - if let Some(ref path) = path_opt { - //TODO: this should be done once, not when generating the view! - if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) { - children.push(item.property_view(IconSizes::default())); + PreviewKind::Selected => { + if let Some(tab) = self.tab_model.data::(entity) { + if let Some(items) = tab.items_opt() { + for item in items.iter() { + if item.selected { + children.push(item.property_view(tab.config.icon_sizes)); + // Only show one property view to avoid issues like hangs when generating + // preview images on thousands of files + break; + } } - }; - } - widget::settings::view_column(children).into() - } - } - } - - fn tab_properties(&self, entity: segmented_button::Entity) -> Element { - let mut children = Vec::new(); - - if let Some(tab) = self.tab_model.data::(entity) { - if let Some(items) = tab.items_opt() { - for item in items.iter() { - if item.selected { - children.push(item.property_view(tab.config.icon_sizes)); - // Only show one property view to avoid issues like hangs when generating - // preview images on thousands of files - break; } } } } - widget::settings::view_column(children).into() } @@ -1205,6 +1203,7 @@ impl Application for App { pending_operations: BTreeMap::new(), complete_operations: BTreeMap::new(), failed_operations: BTreeMap::new(), + preview_opt: None, search_active: false, search_id: widget::Id::unique(), search_input: String::new(), @@ -1307,10 +1306,7 @@ impl Application for App { NavMenuAction::OpenInNewWindow(id), ), cosmic::widget::menu::Item::Divider, - cosmic::widget::menu::Item::Button( - fl!("show-details"), - NavMenuAction::Properties(id), - ), + cosmic::widget::menu::Item::Button(fl!("show-details"), NavMenuAction::Preview(id)), cosmic::widget::menu::Item::Divider, if is_context_trash { cosmic::widget::menu::Item::Button( @@ -2031,6 +2027,17 @@ impl Application for App { } return self.update_notification(); } + Message::Preview(entity, kind, timeout) => { + if self + .preview_opt + .as_ref() + .is_some_and(|(e, k, i)| *e == entity && *k == kind && i.elapsed() > timeout) + { + self.context_page = ContextPage::Preview(Some(entity), kind); + self.set_show_context(true); + self.set_context_title(self.context_page.title()); + } + } Message::RescanTrash => { // Update trash icon if empty/full let maybe_entity = self.nav_model.iter().find(|&entity| { @@ -2273,10 +2280,9 @@ impl Application for App { commands.push(self.update(action.message(Some(entity)))); } tab::Command::AddNetworkDrive => { - let context_page = ContextPage::NetworkDrive; - self.context_page = context_page; + self.context_page = ContextPage::NetworkDrive; self.set_show_context(true); - self.set_context_title(context_page.title()); + self.set_context_title(self.context_page.title()); } tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => { self.activate_nav_model_location(&tab_path); @@ -2299,12 +2305,6 @@ impl Application for App { message::app(Message::TabMessage(Some(entity), tab_message)) })); } - tab::Command::LocationProperties(index) => { - self.context_page = - ContextPage::Properties(Some(ContextItem::BreadCrumbs(index))); - self.set_show_context(true); - self.set_context_title(self.context_page.title()); - } tab::Command::MoveToTrash(paths) => { self.operation(Operation::Delete { paths }); } @@ -2375,6 +2375,23 @@ impl Application for App { log::error!("failed to get current executable path: {}", err); } }, + tab::Command::Preview(kind, mut timeout) => { + self.preview_opt = Some((entity, kind.clone(), Instant::now())); + if self.core.window.show_context { + // If the context window is already open, immediately show the preview + timeout = time::Duration::new(0, 0) + }; + commands.push(Command::perform( + async move { + tokio::time::sleep(timeout).await; + message::app(Message::Preview(entity, kind, timeout)) + }, + |x| x, + )); + } + tab::Command::PreviewCancel => { + self.preview_opt = None; + } } } return Command::batch(commands); @@ -2405,10 +2422,10 @@ impl Application for App { if self.context_page == context_page { self.set_show_context(!self.core.window.show_context); } else { - self.context_page = context_page; self.set_show_context(true); } - self.set_context_title(context_page.title()); + self.context_page = context_page; + self.set_context_title(self.context_page.title()); } Message::Undo(id) => { // TODO; @@ -2624,10 +2641,22 @@ impl Application for App { } } - NavMenuAction::Properties(entity) => { - self.context_page = ContextPage::Properties(Some(ContextItem::NavBar(entity))); - self.set_show_context(true); - self.set_context_title(self.context_page.title()); + NavMenuAction::Preview(entity) => { + if let Some(Location::Path(path)) = self.nav_model.data::(entity) { + match tab::item_from_path(path, IconSizes::default()) { + Ok(item) => { + self.context_page = ContextPage::Preview( + None, + PreviewKind::Custom(PreviewItem(item)), + ); + self.set_show_context(true); + self.set_context_title(self.context_page.title()); + } + Err(err) => { + log::warn!("failed to get item from path {:?}: {}", path, err); + } + } + } } NavMenuAction::RemoveFromSidebar(entity) => { @@ -2732,12 +2761,12 @@ impl Application for App { return None; } - Some(match self.context_page { + Some(match &self.context_page { ContextPage::About => self.about(), ContextPage::EditHistory => self.edit_history(), ContextPage::NetworkDrive => self.network_drive(), ContextPage::OpenWith => self.open_with(), - ContextPage::Properties(entity) => self.properties(entity), + ContextPage::Preview(entity_opt, kind) => self.preview(entity_opt, kind), ContextPage::Settings => self.settings(), }) } diff --git a/src/key_bind.rs b/src/key_bind.rs index 255fb89..ca56f0a 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -46,7 +46,7 @@ pub fn key_binds() -> HashMap { bind!([Ctrl], Key::Named(Named::Enter), OpenInNewTab); bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow); bind!([Ctrl], Key::Character("v".into()), Paste); - bind!([], Key::Named(Named::Space), Properties); + bind!([], Key::Named(Named::Space), Preview); bind!([], Key::Named(Named::F2), Rename); bind!([Ctrl], Key::Character("f".into()), SearchActivate); bind!([Ctrl], Key::Character("a".into()), SelectAll); diff --git a/src/menu.rs b/src/menu.rs index d02976b..1cc8739 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -155,7 +155,7 @@ pub fn context_menu<'a>( children.push(divider::horizontal::light().into()); //TODO: Print? - children.push(menu_item(fl!("show-details"), Action::Properties).into()); + children.push(menu_item(fl!("show-details"), Action::Preview).into()); children.push(divider::horizontal::light().into()); children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); children.push(divider::horizontal::light().into()); @@ -231,7 +231,7 @@ pub fn context_menu<'a>( children.push(divider::horizontal::light().into()); } if selected > 0 { - children.push(menu_item(fl!("show-details"), Action::Properties).into()); + children.push(menu_item(fl!("show-details"), Action::Preview).into()); children.push(divider::horizontal::light().into()); children .push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into()); @@ -371,7 +371,7 @@ pub fn menu_bar<'a>( menu::Item::Divider, menu::Item::Button(fl!("rename"), Action::Rename), menu::Item::Divider, - menu::Item::Button(fl!("menu-show-details"), Action::Properties), + menu::Item::Button(fl!("menu-show-details"), Action::Preview), menu::Item::Divider, menu::Item::Button(fl!("add-to-sidebar"), Action::AddToSidebar), menu::Item::Divider, @@ -486,7 +486,7 @@ pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Mess divider::horizontal::light().into(), menu_button!(text::body(fl!("show-details"))) .on_press(tab::Message::LocationMenuAction( - LocationMenuAction::Properties(ancestor_index), + LocationMenuAction::Preview(ancestor_index), )) .into(), ]; diff --git a/src/tab.rs b/src/tab.rs index 938069d..89b0896 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -55,7 +55,7 @@ use std::{ }; use crate::{ - app::{self, Action}, + app::{self, Action, PreviewItem, PreviewKind}, clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, config::{IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID}, dialog::DialogKind, @@ -687,11 +687,9 @@ fn uri_to_path(uri: String) -> Option { } pub fn scan_recents(sizes: IconSizes) -> Vec { - let mut recent_files = recently_used_xbel::parse_file(); - let mut recents = Vec::new(); - match recent_files { + match recently_used_xbel::parse_file() { Ok(recent_files) => { for bookmark in recent_files.bookmarks { let uri = bookmark.href; @@ -814,11 +812,12 @@ pub enum Command { DropFiles(PathBuf, ClipboardPaste), EmptyTrash, Iced(cosmic::Command), - LocationProperties(usize), MoveToTrash(Vec), OpenFile(PathBuf), OpenInNewTab(PathBuf), OpenInNewWindow(PathBuf), + Preview(PreviewKind, Duration), + PreviewCancel, } #[derive(Clone, Debug)] @@ -870,7 +869,7 @@ pub enum Message { pub enum LocationMenuAction { OpenInNewTab(usize), OpenInNewWindow(usize), - Properties(usize), + Preview(usize), } impl MenuAction for LocationMenuAction { @@ -1574,6 +1573,7 @@ impl Tab { let mut history_i_opt = None; let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple(); let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple(); + let last_select_focus = self.select_focus; match message { Message::AddNetworkDrive => { commands.push(Command::AddNetworkDrive); @@ -1627,6 +1627,9 @@ impl Tab { } else { log::warn!("no item for click index {:?}", click_i_opt); } + + // Cancel any preview timers + commands.push(Command::PreviewCancel); } Message::Click(click_i_opt) => { self.selected_clicked = false; @@ -1742,7 +1745,7 @@ impl Tab { item.selected = true; self.select_range = Some((i, i)); } - + self.select_focus = click_i_opt; self.selected_clicked = true; } else if !dont_unset && item.selected { self.clicked = click_i_opt; @@ -1750,10 +1753,6 @@ impl Tab { } } } - if self.select_focus.take().is_some() { - // Unfocus currently focused button - commands.push(Command::Iced(widget::button::focus(widget::Id::unique()))); - } } } Message::Config(config) => { @@ -1806,8 +1805,22 @@ impl Tab { commands.push(Command::OpenInNewWindow(path)); } } - LocationMenuAction::Properties(ancestor_index) => { - commands.push(Command::LocationProperties(ancestor_index)); + LocationMenuAction::Preview(ancestor_index) => { + if let Some(path) = path_for_index(ancestor_index) { + //TODO: blocking code, run in command + match item_from_path(&path, IconSizes::default()) { + Ok(item) => { + // Show preview instantly + commands.push(Command::Preview( + PreviewKind::Custom(PreviewItem(item)), + Duration::new(0, 0), + )); + } + Err(err) => { + log::warn!("failed to get item from path {:?}: {}", path, err); + } + } + } } } } @@ -2160,9 +2173,11 @@ impl Tab { self.dnd_hovered = None; } Message::DndHover(loc) => { - if self.dnd_hovered.as_ref().is_some_and(|(l, i)| { - *l == loc && Instant::now().duration_since(*i) > HOVER_DURATION - }) { + if self + .dnd_hovered + .as_ref() + .is_some_and(|(l, i)| *l == loc && i.elapsed() > HOVER_DURATION) + { cd = Some(loc); } } @@ -2177,6 +2192,9 @@ impl Tab { |x| x, ))); } + + // Clear preview timer + commands.push(Command::PreviewCancel); } Message::DndLeave(loc) => { if Some(&loc) == self.dnd_hovered.as_ref().map(|(l, _)| l) { @@ -2226,6 +2244,26 @@ impl Tab { } } } + + // Update preview timer + //TODO: make this configurable + if last_select_focus != self.select_focus { + if let Some(index) = self.select_focus { + if let Some(ref items) = self.items_opt { + if let Some(item) = items.get(index) { + if let Some(location) = item.location_opt.clone() { + // Show preview after double click timeout + commands.push(Command::Preview( + PreviewKind::Location(location), + DOUBLE_CLICK_DURATION, + )); + } + } + } + } + } + + // Change directory if requested if let Some(location) = cd { if matches!(self.mode, Mode::Desktop) { match location { @@ -2252,6 +2290,7 @@ impl Tab { } } } + commands } @@ -3238,54 +3277,7 @@ impl Tab { let button_row = button(row.into()); let button_row: Element<_> = if item.metadata.is_dir() && item.location_opt.is_some() { - let tab_location = item.location_opt.clone().unwrap(); - let tab_location_enter = tab_location.clone(); - let tab_location_leave = tab_location.clone(); - let is_dnd_hovered = - self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location); - cosmic::widget::container( - DndDestination::for_data(button_row, move |data, action| { - if let Some(mut data) = data { - if action == DndAction::Copy { - Message::Drop(Some((tab_location.clone(), data))) - } else if action == DndAction::Move { - data.kind = ClipboardKind::Cut; - Message::Drop(Some((tab_location.clone(), data))) - } else { - log::warn!("unsupported action: {:?}", action); - Message::Drop(None) - } - } else { - log::warn!("No data for drop."); - Message::Drop(None) - } - }) - .on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone())) - .on_leave(move || Message::DndLeave(tab_location_leave.clone())), - ) - // todo refactor into the dnd destination wrapper - .style(if is_dnd_hovered { - theme::Container::custom(|t| { - let mut a = cosmic::iced_style::container::StyleSheet::appearance( - t, - &theme::Container::default(), - ); - let t = t.cosmic(); - // todo use theme drop target color - let mut bg = t.accent_color(); - bg.alpha = 0.2; - a.background = Some(Color::from(bg).into()); - a.border = Border { - color: t.accent_color().into(), - width: 1.0, - radius: t.radius_s().into(), - }; - a - }) - } else { - theme::Container::default() - }) - .into() + self.dnd_dest(item.location_opt.as_ref().unwrap(), button_row) } else { button_row.into() }; @@ -3669,10 +3661,7 @@ impl Tab { } } - //TODO: how to properly kill this task? - loop { - tokio::time::sleep(std::time::Duration::new(1, 0)).await; - } + std::future::pending().await }, )); }