feat: use libcosmic's context_menu widget for the file context menu

This commit is contained in:
Hojjat 2026-09-02 19:07:32 -06:00 • committed by Michael Murphy
parent 089ad2b417
commit 77f062647c
4 changed files with 229 additions and 532 deletions

View file

@ -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::<Tab>(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::<Tab>(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::<Tab>(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::<FavoriteIndex>(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::<FavoriteIndex>(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::<Tab>(*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);

View file

@ -541,7 +541,6 @@ struct App {
title: String,
accept_label: DialogLabel,
choices: Vec<DialogChoice>,
context_menu_window: Option<window::Id>,
context_page: ContextPage,
dialog_pages: VecDeque<DialogPage>,
dialog_text_input: widget::Id,
@ -867,9 +866,6 @@ impl App {
fn close_context_menus(&mut self) -> Task<Message> {
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| {

View file

@ -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<KeyBind, Action>,
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<menu::Tree<tab::Message>> {
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::<Vec<Element<'a, tab::Message>>>()
.map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)))
.collect::<Vec<_>>()
};
// 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<Element<_>> = Vec::new();
let mut children: Vec<menu::Item<TabAction, String>> = 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<KeyBind, TabAction> = key_binds
.iter()
.map(|(key_bind, action)| (key_bind.clone(), TabAction(*action)))
.collect();
menu::items(&key_binds, children)
}
pub fn dialog_menu(

View file

@ -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<f32>),
ChangeLocation(String, Location, Option<Vec<PathBuf>>),
ContextMenu(Option<Point>, Option<window::Id>),
Delete(Vec<PathBuf>),
DropFiles(PathBuf, ClipboardPaste),
ClearRecents,
@ -1777,7 +1777,8 @@ pub enum Message {
ClickRelease(Option<usize>),
Config(TabConfig),
ContextAction(Action),
ContextMenu(Option<Point>, Option<window::Id>),
RightClickBackground,
Surface(cosmic::surface::Action),
LocationContextMenuPoint(Option<Point>),
LocationContextMenuIndex(Option<Point>, Option<usize>),
LocationMenuAction(LocationMenuAction),
@ -2807,7 +2808,6 @@ pub struct Tab {
pub location_title: String,
pub location_context_menu_point: Option<Point>,
pub location_context_menu_index: Option<usize>,
pub context_menu: Option<Point>,
pub mode: Mode,
pub scroll_opt: Option<AbsoluteOffset>,
pub size_opt: Cell<Option<Size>>,
@ -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<Message> =
@ -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();