diff --git a/src/app.rs b/src/app.rs index 48f70bb..048d600 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,12 +17,10 @@ use cosmic::iced::widget::button::focus; use cosmic::iced::widget::scrollable; use cosmic::iced::widget::scrollable::AbsoluteOffset; use cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId}; -use cosmic::iced::{ - self, Alignment, Event, Length, Rectangle, Size, Subscription, event, mouse, stream, -}; +use cosmic::iced::{self, Alignment, Event, Length, Size, Subscription, event, mouse, stream}; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use cosmic::iced::{ - Limits, Point, + Limits, Point, Rectangle, event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, platform_specific::runtime::wayland::layer_surface::{ IcedMargin, IcedOutput, SctkLayerSurfaceSettings, @@ -672,7 +670,6 @@ pub struct MounterData(MounterKey, MounterItem); #[derive(Clone, Debug)] pub enum WindowKind { - ContextMenu(Entity, widget::Id), Desktop(Entity), DesktopViewOptions, Dialogs(widget::Id), @@ -1457,12 +1454,6 @@ impl App { fn remove_window(&mut self, id: &window::Id) { if let Some(window) = self.windows.remove(id) { match window.kind { - WindowKind::ContextMenu(entity, _) => { - // Close context menu - if let Some(tab) = self.tab_model.data_mut::(entity) { - tab.context_menu = None; - } - } WindowKind::Desktop(entity) => { // Remove the tab from the tab model self.tab_model.remove(entity); @@ -1750,12 +1741,6 @@ impl App { let active = self.tab_model.active(); if let Some(tab) = self.tab_model.data_mut::(active) { tab.location_context_menu_index = None; - if tab.context_menu.is_some() { - return self.update(Message::TabMessage( - Some(active), - tab::Message::ContextMenu(None, None), - )); - } } Task::none() @@ -1774,9 +1759,7 @@ impl App { for (favorite_i, favorite) in self.config.favorites.iter().enumerate() { if let Some(path) = favorite.path_opt() { - let name = favorite - .display_name() - .unwrap_or_else(|| fl!("filesystem")); + let name = favorite.display_name().unwrap_or_else(|| fl!("filesystem")); nav_model = nav_model.insert(move |b| { b.text(name.clone()) .icon( @@ -2824,13 +2807,6 @@ impl Application for App { return Task::none(); } - if tab.context_menu.is_some() { - return self.update(Message::TabMessage( - Some(entity), - tab::Message::ContextMenu(None, None), - )); - } - if tab.edit_location.is_some() { tab.edit_location = None; return Task::none(); @@ -4505,6 +4481,8 @@ impl Application for App { } Message::TabMessage(entity_opt, tab_message) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + // The context menu opens on right-button release, so refresh paste availability now + let right_click = matches!(tab_message, tab::Message::RightClick(..)); let tab_commands = match self.tab_model.data_mut::(entity) { Some(tab) => tab.update(tab_message, self.modifiers), @@ -4512,6 +4490,9 @@ impl Application for App { }; let mut commands = Vec::new(); + if right_click { + commands.push(self.update(Message::CheckClipboard)); + } for tab_command in tab_commands { match tab_command { tab::Command::Action(action) => { @@ -4552,93 +4533,8 @@ impl Application for App { self.update_tab(entity, tab_path, selection_paths), ])); } - tab::Command::ContextMenu(point_opt, parent_id) => { - #[cfg(feature = "wayland")] - if let Some(point) = point_opt { - if crate::is_wayland() { - // Open context menu - use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ - Anchor, Gravity, - }; - use cosmic::{iced::runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }, widget::menu::StyleSheet as _}; - - let window_id = WindowId::unique(); - self.windows.insert( - window_id, - Window::new(WindowKind::ContextMenu( - entity, - widget::Id::unique(), - )), - ); - commands.push(self.update(Message::CheckClipboard)); - let t = self.core.system_theme(); - let styling = t.appearance( - &cosmic::theme::menu_bar::MenuBarStyle::Default, - false, - ); - let rad = styling.menu_border_radius; - - commands.push(self.update(Message::Surface( - cosmic::surface::action::app_popup( - move |_| cosmic::surface::action::LiveSettings { - corners: Some(iced::runtime::platform_specific::wayland::CornerRadius { - top_left: rad[0] as u32, - top_right: rad[1] as u32, - bottom_left: rad[2] as u32, - bottom_right: rad[3] as u32, - }), - ..Default::default() - }, move |app: &mut Self| -> SctkPopupSettings { - let anchor_rect = Rectangle { - x: point.x as i32, - y: point.y as i32, - width: 1, - height: 1, - }; - let positioner = SctkPositioner { - size: None, - anchor_rect, - anchor: Anchor::None, - gravity: Gravity::BottomRight, - reactive: true, - ..Default::default() - }; - SctkPopupSettings { - parent: parent_id.unwrap_or( - app.core - .main_window_id() - .unwrap_or(WindowId::NONE), - ), - id: window_id, - positioner, - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - } - }, - None, - ), - ))); - } - } else { - // Destroy previous popup - let mut window_ids = Vec::new(); - for (window_id, window) in &self.windows { - if let WindowKind::ContextMenu(e, _) = &window.kind - && *e == entity - { - window_ids.push(*window_id); - } - } - for window_id in window_ids { - commands.push(self.update(Message::Surface( - cosmic::surface::action::destroy_popup(window_id), - ))); - } - } + tab::Command::Surface(action) => { + commands.push(self.update(Message::Surface(action))); } tab::Command::Delete(paths) => commands.push(self.delete(paths)), tab::Command::DropFiles(to, from) => { @@ -5127,10 +5023,6 @@ impl Application for App { tab.edit_location = None; // Close other context menus. tab.location_context_menu_index = None; - return Task::done(cosmic::Action::App(Message::TabMessage( - Some(tab_entity), - tab::Message::ContextMenu(None, None), - ))); } } Message::NavMenuAction(action) => match action { @@ -5311,16 +5203,10 @@ impl Application for App { } NavMenuAction::ChangeSidebarLabel(entity) => { - if let Some(favorite) = self - .nav_model - .data::(entity) - .and_then(|FavoriteIndex(favorite_i)| { - self.config.favorites.get(*favorite_i) - }) - { - let label = favorite - .display_name() - .unwrap_or_else(|| fl!("filesystem")); + if let Some(favorite) = self.nav_model.data::(entity).and_then( + |FavoriteIndex(favorite_i)| self.config.favorites.get(*favorite_i), + ) { + let label = favorite.display_name().unwrap_or_else(|| fl!("filesystem")); return Task::batch([ self.dialog_pages .push_back(DialogPage::ChangeSidebarLabel { entity, label }), @@ -6672,23 +6558,6 @@ impl Application for App { fn view_window(&self, id: WindowId) -> Element<'_, Self::Message> { let content = match self.windows.get(&id) { Some(window) => match &window.kind { - WindowKind::ContextMenu(entity, id) => match self.tab_model.data::(*entity) { - Some(tab) => { - return widget::autosize::autosize( - menu::context_menu( - tab, - &self.key_binds, - &window.modifiers, - self.clipboard_has_content(), - &self.config.context_actions, - ) - .map(|x| Message::TabMessage(Some(*entity), x)), - id.clone(), - ) - .into(); - } - None => widget::text("Unknown tab ID").into(), - }, WindowKind::Desktop(entity) => { let mut tab_column = widget::column::with_capacity(3); diff --git a/src/dialog.rs b/src/dialog.rs index 315691a..8f5cd5d 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -541,7 +541,6 @@ struct App { title: String, accept_label: DialogLabel, choices: Vec, - context_menu_window: Option, context_page: ContextPage, dialog_pages: VecDeque, dialog_text_input: widget::Id, @@ -867,9 +866,6 @@ impl App { fn close_context_menus(&mut self) -> Task { self.tab.location_context_menu_index = None; - if self.tab.context_menu.is_some() { - return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None))); - } Task::none() } @@ -1048,7 +1044,6 @@ impl Application for App { title, accept_label: DialogLabel::from(accept_label), choices: Vec::new(), - context_menu_window: None, context_page: ContextPage::Preview(None, PreviewKind::Selected), dialog_pages: VecDeque::new(), dialog_text_input: widget::Id::new("Dialog Text Input"), @@ -1324,10 +1319,6 @@ impl Application for App { return Task::none(); } - if self.tab.context_menu.is_some() { - return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None))); - } - if self.tab.edit_location.is_some() { // Close location editing if enabled self.tab.edit_location = None; @@ -1769,94 +1760,8 @@ impl Application for App { self.rescan_tab(selection_paths), ])); } - tab::Command::ContextMenu(point_opt, parent_id) => { - #[cfg(feature = "wayland")] - match point_opt { - Some(point) => { - if crate::is_wayland() { - // Open context menu - use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ - Anchor, Gravity, - }; - use cosmic::iced::runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - use cosmic::iced::Rectangle; - use cosmic::widget::menu::StyleSheet as _; - - let window_id = window::Id::unique(); - self.context_menu_window = Some(window_id); - let autosize_id = widget::Id::unique(); - let t = self.core.system_theme(); - let styling = t.appearance( - &cosmic::theme::menu_bar::MenuBarStyle::Default, - false, - ); - let rad = styling.menu_border_radius; - commands.push(self.update(Message::Surface( - cosmic::surface::action::app_popup( - move |_| cosmic::surface::action::LiveSettings { - corners: Some(iced::runtime::platform_specific::wayland::CornerRadius { - top_left: rad[0] as u32, - top_right: rad[1] as u32, - bottom_left: rad[2] as u32, - bottom_right: rad[3] as u32, - }), - ..Default::default() - }, - move |app: &mut Self| -> SctkPopupSettings { - let anchor_rect = Rectangle { - x: point.x as i32, - y: point.y as i32, - width: 1, - height: 1, - }; - let positioner = SctkPositioner { - size: None, - anchor_rect, - anchor: Anchor::None, - gravity: Gravity::BottomRight, - reactive: true, - ..Default::default() - }; - SctkPopupSettings { - parent: parent_id - .unwrap_or(app.flags.window_id), - id: window_id, - positioner, - parent_size: None, - grab: true, - close_with_children: false, - input_zone: None, - } - }, - Some(Box::new(move |app: &Self| { - widget::autosize::autosize( - menu::context_menu( - &app.tab, - &app.key_binds, - &app.modifiers, - false, // Paste not used in dialogs - &app.flags.config.context_actions, - ) - .map(Message::TabMessage) - .map(cosmic::Action::App), - autosize_id.clone(), - ) - .into() - })), - ), - ))); - } - } - None => { - if let Some(window_id) = self.context_menu_window.take() { - commands.push(self.update(Message::Surface( - cosmic::surface::action::destroy_popup(window_id), - ))); - } - } - } + tab::Command::Surface(action) => { + commands.push(self.update(Message::Surface(action))); } tab::Command::Iced(iced_command) => { commands.push(iced_command.0.map(|tab_message| { diff --git a/src/menu.rs b/src/menu.rs index 86db158..fc7584c 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,14 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::app::Core; -use cosmic::iced::advanced::widget::text::Style as TextStyle; use cosmic::iced::keyboard::Modifiers; use cosmic::iced::{Alignment, Background, Border, Length}; +use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar}; -use cosmic::widget::{ - self, Row, button, column, container, divider, responsive_menu_bar, space, text, -}; +use cosmic::widget::{self, Row, button, column, container, divider, responsive_menu_bar, text}; use cosmic::{Element, theme}; use i18n_embed::LanguageLoader; use mime_guess::Mime; @@ -53,82 +51,46 @@ const fn menu_button_optional( } } +/// A menu action dispatched to the tab the menu was opened on. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TabAction(pub Action); + +impl MenuAction for TabAction { + type Message = tab::Message; + + fn message(&self) -> tab::Message { + tab::Message::ContextAction(self.0) + } +} + pub fn context_menu<'a>( tab: &Tab, key_binds: &HashMap, modifiers: &Modifiers, clipboard_paste_available: bool, context_actions: &[ContextActionPreset], -) -> Element<'a, tab::Message> { - let find_key = |action: &Action| -> String { - for (key_bind, key_action) in key_binds { - if action == key_action { - return key_bind.to_string(); - } - } - String::new() - }; - fn key_style(theme: &cosmic::Theme) -> TextStyle { - let mut color = theme.cosmic().background(theme.transparent).component.on; - color.alpha *= 0.75; - TextStyle { - color: Some(color.into()), - ..Default::default() - } - } - fn disabled_style(theme: &cosmic::Theme) -> TextStyle { - let mut color = theme.cosmic().background(theme.transparent).component.on; - color.alpha *= 0.5; - TextStyle { - color: Some(color.into()), - ..Default::default() - } - } - - let menu_item = |label, action| { - let key = find_key(&action); - menu_button!( - text::body(label), - space::horizontal(), - text::body(key).class(theme::Text::Custom(key_style)) - ) - .on_press(tab::Message::ContextAction(action)) - }; - - let menu_item_disabled = |label, action: Action| { - let key = find_key(&action); - menu_button!( - text::body(label).class(theme::Text::Custom(disabled_style)), - space::horizontal(), - text::body(key).class(theme::Text::Custom(disabled_style)) - ) - }; +) -> Vec> { + let menu_item = + |label: String, action: Action| menu::Item::Button(label, None, TabAction(action)); + let menu_item_disabled = + |label: String, action: Action| menu::Item::ButtonDisabled(label, None, TabAction(action)); // Allow paste when clipboard has data and we're in a location that supports it let can_paste = clipboard_paste_available && tab.location.supports_paste(); let (sort_name, sort_direction, _) = tab.sort_options(); - let sort_item = |label, variant| { - let key = find_key(&Action::ToggleSort(variant)); - let leading: Element<'a, tab::Message> = if sort_name == variant { + let sort_item = |label: String, variant| { + let entry = menu::Entry::new(label, TabAction(Action::ToggleSort(variant))); + menu::Item::Entry(if sort_name == variant { let icon_name = if sort_direction { "view-sort-ascending-symbolic" } else { "view-sort-descending-symbolic" }; - widget::icon::from_name(icon_name).size(14).into() + entry.icon(widget::icon::from_name(icon_name).size(14).handle()) } else { - space::horizontal().width(Length::Fixed(14.0)).into() - }; - menu_button!( - leading, - space::horizontal().width(Length::Fixed(theme::spacing().space_xxs.into())), - text::body(label), - space::horizontal(), - text::body(key).class(theme::Text::Custom(key_style)) - ) - .on_press(tab::Message::ContextAction(Action::ToggleSort(variant))) - .into() + entry.reserve_icon() + }) }; let mut selected_dir = 0; @@ -173,8 +135,8 @@ pub fn context_menu<'a>( .iter() .enumerate() .filter(|(_, action)| action.matches_selection(selected, selected_dir)) - .map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into()) - .collect::>>() + .map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i))) + .collect::>() }; // Parse the desktop entry if it is the only selection #[cfg(feature = "desktop")] @@ -189,7 +151,7 @@ pub fn context_menu<'a>( } }); - let mut children: Vec> = Vec::new(); + let mut children: Vec> = Vec::new(); match (&tab.mode, &tab.location) { ( tab::Mode::App | tab::Mode::Desktop, @@ -201,106 +163,113 @@ pub fn context_menu<'a>( | Location::Network(_, _, Some(_)), ) => { if selected_trash_only { - children.push(menu_item(fl!("open"), Action::Open).into()); + children.push(menu_item(fl!("open"), Action::Open)); if !Trash::is_empty() { - children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); + children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash)); } } 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)); #[cfg(feature = "desktop")] { - children.extend(entry.desktop_actions.into_iter().enumerate().map( - |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(), - )); + children.extend( + entry + .desktop_actions + .into_iter() + .enumerate() + .map(|(i, action)| menu_item(action.name, Action::ExecEntryAction(i))), + ); } - children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("rename"), Action::Rename).into()); - children.push(menu_item(fl!("cut"), Action::Cut).into()); + children.push(menu::Item::Divider); + children.push(menu_item(fl!("rename"), Action::Rename)); + children.push(menu_item(fl!("cut"), Action::Cut)); if modifiers.shift() && !modifiers.control() { - children.push(menu_item(fl!("copy-path"), Action::CopyPath).into()); + children.push(menu_item(fl!("copy-path"), Action::CopyPath)); } else { - children.push(menu_item(fl!("copy"), Action::Copy).into()); + children.push(menu_item(fl!("copy"), Action::Copy)); } // Should this simply bypass trash and remove the shortcut? - children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + children.push(menu_item(fl!("move-to-trash"), Action::Delete)); let action_items = context_action_items(selected, selected_dir); if !action_items.is_empty() { - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); children.extend(action_items); } } else if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { - children.push(menu_item(fl!("open"), Action::Open).into()); + children.push(menu_item(fl!("open"), Action::Open)); } if selected == 1 { - children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into()); + children.push(menu_item(fl!("menu-open-with"), Action::OpenWith)); if selected_dir == 1 { - children - .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); + children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal)); } } if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) { - children.push( - menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), - ); + children.push(menu_item( + fl!("open-item-location"), + Action::OpenItemLocation, + )); } // All selected items are directories if selected == selected_dir && matches!(tab.mode, tab::Mode::App) { - children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into()); - children - .push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into()); + children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab)); + children.push(menu_item( + fl!("open-in-new-window"), + Action::OpenInNewWindow, + )); } let action_items = context_action_items(selected, selected_dir); if !action_items.is_empty() { - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); children.extend(action_items); } - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); if selected_mount_point == 0 { - children.push(menu_item(fl!("rename"), Action::Rename).into()); - children.push(menu_item(fl!("cut"), Action::Cut).into()); + children.push(menu_item(fl!("rename"), Action::Rename)); + children.push(menu_item(fl!("cut"), Action::Cut)); } if modifiers.shift() && !modifiers.control() { - children.push(menu_item(fl!("copy-path"), Action::CopyPath).into()); + children.push(menu_item(fl!("copy-path"), Action::CopyPath)); } else { - children.push(menu_item(fl!("copy"), Action::Copy).into()); + children.push(menu_item(fl!("copy"), Action::Copy)); } if selected_mount_point == 0 { - children.push(menu_item(fl!("move-to"), Action::MoveTo).into()); + children.push(menu_item(fl!("move-to"), Action::MoveTo)); } - children.push(menu_item(fl!("copy-to"), Action::CopyTo).into()); + children.push(menu_item(fl!("copy-to"), Action::CopyTo)); - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES; selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m)); if selected_types.is_empty() { - children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into()); - children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into()); + children.push(menu_item(fl!("extract-here"), Action::ExtractHere)); + children.push(menu_item(fl!("extract-to"), Action::ExtractTo)); } - children.push(menu_item(fl!("compress"), Action::Compress).into()); - children.push(divider::horizontal::light().into()); + children.push(menu_item(fl!("compress"), Action::Compress)); + children.push(menu::Item::Divider); //TODO: Print? - children.push(menu_item(fl!("show-details"), Action::Preview).into()); + children.push(menu_item(fl!("show-details"), Action::Preview)); if any_trash_item { - children.push(divider::horizontal::light().into()); - children.push( - menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into(), - ); - children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("delete-permanently"), Action::Delete).into()); + children.push(menu::Item::Divider); + children.push(menu_item( + fl!("restore-from-trash"), + Action::RestoreFromTrash, + )); + children.push(menu::Item::Divider); + children.push(menu_item(fl!("delete-permanently"), Action::Delete)); } else { if matches!(tab.mode, tab::Mode::App) { - children.push(divider::horizontal::light().into()); - children - .push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); + children.push(menu::Item::Divider); + children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar)); } - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); if tab.location.is_recents() { - children.push( - menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(), - ); - children.push(divider::horizontal::light().into()); + children.push(menu_item( + fl!("remove-from-recents"), + Action::RemoveFromRecents, + )); + children.push(menu::Item::Divider); } if selected_mount_point == 0 { if modifiers.shift() && !modifiers.control() { @@ -309,55 +278,59 @@ pub fn context_menu<'a>( .into(), ); } else { - children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + children.push(menu_item(fl!("move-to-trash"), Action::Delete)); } } else if selected == 1 { - children.push(menu_item(fl!("eject"), Action::Eject).into()); + children.push(menu_item(fl!("eject"), Action::Eject)); } } } else { //TODO: need better designs for menu with no selection //TODO: have things like properties but they apply to the folder? if tab.location != Location::Recents { - children.push(menu_item(fl!("new-folder"), Action::NewFolder).into()); - 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!("new-folder"), Action::NewFolder)); + children.push(menu_item(fl!("new-file"), Action::NewFile)); + children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal)); + children.push(menu::Item::Divider); } if tab.mode.multiple() { - children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); + children.push(menu_item(fl!("select-all"), Action::SelectAll)); } if can_paste { - children.push(menu_item(fl!("paste"), Action::Paste).into()); + children.push(menu_item(fl!("paste"), Action::Paste)); } else { - children.push(menu_item_disabled(fl!("paste"), Action::Paste).into()); + children.push(menu_item_disabled(fl!("paste"), Action::Paste)); } //TODO: only show if cosmic-settings is found? if matches!(tab.mode, tab::Mode::Desktop) { - children.push(divider::horizontal::light().into()); - children.push( - menu_item(fl!("change-wallpaper"), Action::CosmicSettingsWallpaper).into(), - ); - children.push( - menu_item(fl!("desktop-appearance"), Action::CosmicSettingsDesktop).into(), - ); - children.push( - menu_item(fl!("display-settings"), Action::CosmicSettingsDisplays).into(), - ); + children.push(menu::Item::Divider); + children.push(menu_item( + fl!("change-wallpaper"), + Action::CosmicSettingsWallpaper, + )); + children.push(menu_item( + fl!("desktop-appearance"), + Action::CosmicSettingsDesktop, + )); + children.push(menu_item( + fl!("display-settings"), + Action::CosmicSettingsDisplays, + )); } - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); // 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)); if matches!(tab.location, Location::Desktop(..)) { - children.push(divider::horizontal::light().into()); - children.push( - menu_item(fl!("desktop-view-options"), Action::DesktopViewOptions).into(), - ); + children.push(menu::Item::Divider); + children.push(menu_item( + fl!("desktop-view-options"), + Action::DesktopViewOptions, + )); } } } @@ -372,24 +345,25 @@ pub fn context_menu<'a>( ) => { if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { - children.push(menu_item(fl!("open"), Action::Open).into()); + children.push(menu_item(fl!("open"), Action::Open)); } if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() { - children.push( - menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), - ); + children.push(menu_item( + fl!("open-item-location"), + Action::OpenItemLocation, + )); } - children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("show-details"), Action::Preview).into()); + children.push(menu::Item::Divider); + children.push(menu_item(fl!("show-details"), Action::Preview)); } else { if dialog_kind.save() { - children.push(menu_item(fl!("new-folder"), Action::NewFolder).into()); + children.push(menu_item(fl!("new-folder"), Action::NewFolder)); } if tab.mode.multiple() { - children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); + children.push(menu_item(fl!("select-all"), Action::SelectAll)); } if !children.is_empty() { - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); } children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); @@ -399,14 +373,14 @@ pub fn context_menu<'a>( (_, Location::Network(..)) => { if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { - children.push(menu_item(fl!("open"), Action::Open).into()); + children.push(menu_item(fl!("open"), Action::Open)); } } else { if tab.mode.multiple() { - children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); + children.push(menu_item(fl!("select-all"), Action::SelectAll)); } if !children.is_empty() { - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); } children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); @@ -415,18 +389,20 @@ pub fn context_menu<'a>( } (_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => { if tab.mode.multiple() { - children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); + children.push(menu_item(fl!("select-all"), Action::SelectAll)); } if !children.is_empty() { - children.push(divider::horizontal::light().into()); + children.push(menu::Item::Divider); } if selected > 0 { - 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()); - children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("delete-permanently"), Action::Delete).into()); + children.push(menu_item(fl!("show-details"), Action::Preview)); + children.push(menu::Item::Divider); + children.push(menu_item( + fl!("restore-from-trash"), + Action::RestoreFromTrash, + )); + children.push(menu::Item::Divider); + children.push(menu_item(fl!("delete-permanently"), Action::Delete)); } else { // TODO: Nested menu children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); @@ -436,26 +412,11 @@ pub fn context_menu<'a>( } } - container(cosmic::widget::menu::menu_column::MenuColumn::with_children(children)) - .padding(1) - //TODO: move style to libcosmic - .style(|theme| { - let cosmic = theme.cosmic(); - let component = &cosmic.background(theme.transparent); - container::Style { - icon_color: Some(component.on.into()), - text_color: Some(component.on.into()), - background: Some(Background::Color(component.base.into())), - border: Border { - radius: cosmic.radius_s().map(|x| x + 1.0).into(), - width: 1.0, - color: component.divider.into(), - }, - ..Default::default() - } - }) - .width(Length::Fixed(360.0)) - .into() + let key_binds: HashMap = key_binds + .iter() + .map(|(key_bind, action)| (key_bind.clone(), TabAction(*action))) + .collect(); + menu::items(&key_binds, children) } pub fn dialog_menu( diff --git a/src/tab.rs b/src/tab.rs index 79a5091..3b1a361 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1742,11 +1742,11 @@ impl fmt::Debug for TaskWrapper { #[derive(Debug)] pub enum Command { Action(Action), + Surface(cosmic::surface::Action), AddNetworkDrive, AddToSidebar(PathBuf), AutoScroll(Option), ChangeLocation(String, Location, Option>), - ContextMenu(Option, Option), Delete(Vec), DropFiles(PathBuf, ClipboardPaste), ClearRecents, @@ -1777,7 +1777,8 @@ pub enum Message { ClickRelease(Option), Config(TabConfig), ContextAction(Action), - ContextMenu(Option, Option), + RightClickBackground, + Surface(cosmic::surface::Action), LocationContextMenuPoint(Option), LocationContextMenuIndex(Option, Option), LocationMenuAction(LocationMenuAction), @@ -2807,7 +2808,6 @@ pub struct Tab { pub location_title: String, pub location_context_menu_point: Option, pub location_context_menu_index: Option, - pub context_menu: Option, pub mode: Mode, pub scroll_opt: Option, pub size_opt: Cell>, @@ -2952,7 +2952,6 @@ impl Tab { location, location_ancestors, location_title, - context_menu: None, location_context_menu_point: None, location_context_menu_index: None, mode: Mode::App, @@ -3495,7 +3494,6 @@ impl Tab { self.location = location.normalize(); self.location_ancestors = self.location.ancestors(); self.location_title = self.location.title(); - self.context_menu = None; self.edit_location = None; self.items_opt = None; //TODO: remember scroll by location? @@ -3540,7 +3538,6 @@ 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_context_menu = self.context_menu; match message { Message::AddNetworkDrive => { commands.push(Command::AddNetworkDrive); @@ -3575,7 +3572,6 @@ impl Tab { } if click_i_opt != self.clicked.take() { - self.context_menu = None; self.location_context_menu_index = None; if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { @@ -3618,7 +3614,6 @@ impl Tab { } Message::Click(click_i_opt) => { self.selected_clicked = false; - self.context_menu = None; self.edit_location = None; self.location_context_menu_index = None; if click_i_opt.is_none() { @@ -3762,24 +3757,17 @@ impl Tab { } } Message::ContextAction(action) => { - // Close context menu - self.context_menu = None; - commands.push(Command::Action(action)); } Message::RunContextAction(action) => { - self.context_menu = None; - commands.push(Command::RunContextAction(action)); } - Message::ContextMenu(point_opt, _) => { + Message::RightClickBackground => { self.edit_location = None; - self.context_menu = point_opt; self.location_context_menu_index = None; //TODO: hack for clearing selecting when right clicking empty space - if self.context_menu.is_some() - && self.last_right_click.take().is_none() + if self.last_right_click.take().is_none() && let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { @@ -3787,12 +3775,13 @@ impl Tab { } } } + Message::Surface(action) => { + commands.push(Command::Surface(action)); + } Message::LocationContextMenuPoint(point_opt) => { - self.context_menu = None; self.location_context_menu_point = point_opt; } Message::LocationContextMenuIndex(p, index_opt) => { - self.context_menu = None; self.location_context_menu_point = p; self.location_context_menu_index = index_opt; } @@ -3849,7 +3838,6 @@ impl Tab { Message::Drag(rect_opt) => { self.watch_drag = false; if let Some(rect) = rect_opt { - self.context_menu = None; self.location_context_menu_index = None; if self.mode.multiple() { self.select_rect(rect, mod_ctrl, mod_shift); @@ -4972,16 +4960,6 @@ impl Tab { } } - // Update context menu popup - if self.context_menu != last_context_menu { - if last_context_menu.is_some() { - commands.push(Command::ContextMenu(None, self.window_id)); - } - if let Some(point) = self.context_menu { - commands.push(Command::ContextMenu(Some(point), self.window_id)); - } - } - commands } @@ -5878,18 +5856,13 @@ impl Tab { .height(Length::Fixed(item_height as f32)) .width(Length::Fixed(item_width as f32)); for button in buttons { - if self.context_menu.is_some() { - column = column.push(button); - } else { - column = column.push( - mouse_area::MouseArea::new(button) - .on_right_press_no_capture() - .wayland_on_right_press_window_position() - .on_right_press(move |point_opt| { - Message::RightClick(point_opt, Some(i)) - }), - ); - } + column = column.push( + mouse_area::MouseArea::new(button) + .on_right_press_no_capture() + .on_right_press(move |point_opt| { + Message::RightClick(point_opt, Some(i)) + }), + ); } let column: Element = @@ -6294,39 +6267,33 @@ impl Tab { .spacing(space_xxs) }; - let button = |row| { - let mouse_area = crate::mouse_area::MouseArea::new( - widget::button::custom(row) - .width(Length::Fill) - .id(item.button_id.clone()) - .padding([0, space_xxs]) - .class(button_style( - item.selected, - item.highlighted, - item.cut, - true, - true, - false, - )), - ) - .on_press(move |_| Message::Click(Some(i))) - .on_double_click(move |_| Message::DoubleClick(Some(i))) - .on_release(move |_| Message::ClickRelease(Some(i))) - .on_middle_press(move |_| Message::MiddleClick(i)) - .on_enter(move || Message::HighlightActivate(i)) - .on_exit(move || Message::HighlightDeactivate(i)); + let button = + |row| { + let mouse_area = crate::mouse_area::MouseArea::new( + widget::button::custom(row) + .width(Length::Fill) + .id(item.button_id.clone()) + .padding([0, space_xxs]) + .class(button_style( + item.selected, + item.highlighted, + item.cut, + true, + true, + false, + )), + ) + .on_press(move |_| Message::Click(Some(i))) + .on_double_click(move |_| Message::DoubleClick(Some(i))) + .on_release(move |_| Message::ClickRelease(Some(i))) + .on_middle_press(move |_| Message::MiddleClick(i)) + .on_enter(move || Message::HighlightActivate(i)) + .on_exit(move || Message::HighlightDeactivate(i)); - if self.context_menu.is_some() { - mouse_area - } else { - mouse_area - .on_right_press_no_capture() - .wayland_on_right_press_window_position() - .on_right_press(move |point_opt| { - Message::RightClick(point_opt, Some(i)) - }) - } - }; + mouse_area.on_right_press_no_capture().on_right_press( + move |point_opt| Message::RightClick(point_opt, Some(i)), + ) + }; let button_row = button(row.into()); let button_row: Element<_> = if item.metadata.is_dir() @@ -6550,51 +6517,47 @@ impl Tab { .on_back_press(move |_point_opt| Message::GoPrevious) .on_forward_press(move |_point_opt| Message::GoNext) .on_scroll(|delta| respond_to_scroll_direction(delta, modifiers)) - .on_right_press(move |p| { - Message::ContextMenu( - if self.context_menu.is_some() { None } else { p }, - self.window_id, - ) - }) - .wayland_on_right_press_window_position(); + .on_right_press(|_| Message::RightClickBackground); - let mut popover = widget::popover(mouse_area); - if let Some(point) = self.context_menu - && (!cfg!(feature = "wayland") || !crate::is_wayland()) - { - let context_menu = menu::context_menu( + let items_area: Element<'_, Message> = if can_scroll { + // FIXME: new responsive widget will remove the state from the scrollable + // id_container with custom id forces the state to be extracted in a diff + // pre-processing step + widget::id_container( + widget::scrollable(mouse_area) + .id(self.scrollable_id.clone()) + .on_scroll(Message::Scroll) + .width(Length::Fill) + .height(Length::Fill), + widget::Id::new(format!("{}-scrollable", self.scrollable_id)), + ) + .into() + } else { + mouse_area.into() + }; + + // Wrap the scrollable, not its content, so the popup anchors in window coordinates + let mut context_menu = widget::context_menu( + items_area, + Some(menu::context_menu( self, key_binds, modifiers, clipboard_paste_available, context_actions, - ); - popover = popover - .popup(context_menu) - .position(widget::popover::Position::Point(point)); + )), + ) + .item_width(cosmic::widget::menu::ItemWidth::Uniform(360)) + .on_surface_action(Message::Surface); + if let Some(window_id) = self.window_id { + context_menu = context_menu.window_id(window_id); } let mut tab_column = widget::column::with_capacity(3); if let Some(location_view) = location_view_opt { tab_column = tab_column.push(location_view); } - if can_scroll { - tab_column = tab_column.push( - // FIXME: new responsive widget will remove the state from the scrollable - // id_container with custom id forces the state to be extracted in a diff - // pre-processing step - widget::id_container( - widget::scrollable(popover) - .id(self.scrollable_id.clone()) - .on_scroll(Message::Scroll) - .width(Length::Fill) - .height(Length::Fill), - widget::Id::new(format!("{}-scrollable", self.scrollable_id)), - ), - ); - } else { - tab_column = tab_column.push(popover); - } + tab_column = tab_column.push(context_menu); match &self.location { Location::Trash | Location::Search(SearchLocation::Trash, ..) => { if let Some(items) = self.items_opt() @@ -7156,8 +7119,7 @@ impl Tab { let path = path.clone(); // Acquire semaphore permit - let _permit = - THUMB_SEMAPHORE.acquire().await.unwrap(); + let _permit = THUMB_SEMAPHORE.acquire().await.unwrap(); tokio::task::spawn_blocking(move || { let start = Instant::now();