Merge pull request #1728 from darkfated/add-context-actions
Add user-defined context actions
This commit is contained in:
commit
175f8ba724
8 changed files with 252 additions and 9 deletions
|
|
@ -99,6 +99,13 @@ open-with-title = How do you want to open "{$name}"?
|
||||||
browse-store = Browse {$store}
|
browse-store = Browse {$store}
|
||||||
other-apps = Other applications
|
other-apps = Other applications
|
||||||
related-apps = Related applications
|
related-apps = Related applications
|
||||||
|
context-action = Context action
|
||||||
|
context-action-confirm-title = Run "{$name}"?
|
||||||
|
context-action-confirm-warning = This will run on {$items} {$items ->
|
||||||
|
[one] item
|
||||||
|
*[other] items
|
||||||
|
}.
|
||||||
|
run = Run
|
||||||
|
|
||||||
## Permanently delete Dialog
|
## Permanently delete Dialog
|
||||||
selected-items = The {$items} selected items
|
selected-items = The {$items} selected items
|
||||||
|
|
|
||||||
104
src/app.rs
104
src/app.rs
|
|
@ -81,6 +81,7 @@ use crate::{
|
||||||
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
||||||
TimeConfig, TypeToSearch,
|
TimeConfig, TypeToSearch,
|
||||||
},
|
},
|
||||||
|
context_action,
|
||||||
dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings},
|
dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings},
|
||||||
fl, home_dir,
|
fl, home_dir,
|
||||||
key_bind::key_binds,
|
key_bind::key_binds,
|
||||||
|
|
@ -110,6 +111,9 @@ static DELETE_TRASH_BUTTON_ID: LazyLock<widget::Id> =
|
||||||
static CONFIRM_OPEN_WITH_BUTTON_ID: LazyLock<widget::Id> =
|
static CONFIRM_OPEN_WITH_BUTTON_ID: LazyLock<widget::Id> =
|
||||||
LazyLock::new(|| widget::Id::new("confirm-open-with-button"));
|
LazyLock::new(|| widget::Id::new("confirm-open-with-button"));
|
||||||
|
|
||||||
|
static CONFIRM_CONTEXT_ACTION_BUTTON_ID: LazyLock<widget::Id> =
|
||||||
|
LazyLock::new(|| widget::Id::new("confirm-context-action-button"));
|
||||||
|
|
||||||
static EMPTY_TRASH_BUTTON_ID: LazyLock<widget::Id> =
|
static EMPTY_TRASH_BUTTON_ID: LazyLock<widget::Id> =
|
||||||
LazyLock::new(|| widget::Id::new("empty-trash-button"));
|
LazyLock::new(|| widget::Id::new("empty-trash-button"));
|
||||||
|
|
||||||
|
|
@ -181,6 +185,7 @@ pub enum Action {
|
||||||
OpenItemLocation,
|
OpenItemLocation,
|
||||||
OpenTerminal,
|
OpenTerminal,
|
||||||
OpenWith,
|
OpenWith,
|
||||||
|
RunContextAction(usize),
|
||||||
Paste,
|
Paste,
|
||||||
PermanentlyDelete,
|
PermanentlyDelete,
|
||||||
Preview,
|
Preview,
|
||||||
|
|
@ -253,6 +258,9 @@ impl Action {
|
||||||
Self::OpenItemLocation => Message::OpenItemLocation(entity_opt),
|
Self::OpenItemLocation => Message::OpenItemLocation(entity_opt),
|
||||||
Self::OpenTerminal => Message::OpenTerminal(entity_opt),
|
Self::OpenTerminal => Message::OpenTerminal(entity_opt),
|
||||||
Self::OpenWith => Message::OpenWithDialog(entity_opt),
|
Self::OpenWith => Message::OpenWithDialog(entity_opt),
|
||||||
|
Self::RunContextAction(action) => {
|
||||||
|
Message::TabMessage(entity_opt, tab::Message::RunContextAction(*action))
|
||||||
|
}
|
||||||
Self::Paste => Message::Paste(entity_opt),
|
Self::Paste => Message::Paste(entity_opt),
|
||||||
Self::PermanentlyDelete => Message::PermanentlyDelete(entity_opt),
|
Self::PermanentlyDelete => Message::PermanentlyDelete(entity_opt),
|
||||||
Self::Preview => Message::Preview(entity_opt),
|
Self::Preview => Message::Preview(entity_opt),
|
||||||
|
|
@ -324,6 +332,7 @@ pub enum NavMenuAction {
|
||||||
OpenInNewTab(segmented_button::Entity),
|
OpenInNewTab(segmented_button::Entity),
|
||||||
OpenInNewWindow(segmented_button::Entity),
|
OpenInNewWindow(segmented_button::Entity),
|
||||||
Preview(segmented_button::Entity),
|
Preview(segmented_button::Entity),
|
||||||
|
RunContextAction(segmented_button::Entity, usize),
|
||||||
RemoveFromSidebar(segmented_button::Entity),
|
RemoveFromSidebar(segmented_button::Entity),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -559,6 +568,10 @@ pub enum DialogPage {
|
||||||
name: String,
|
name: String,
|
||||||
dir: bool,
|
dir: bool,
|
||||||
},
|
},
|
||||||
|
RunContextAction {
|
||||||
|
action: usize,
|
||||||
|
paths: Box<[PathBuf]>,
|
||||||
|
},
|
||||||
OpenWith {
|
OpenWith {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
mime: mime_guess::Mime,
|
mime: mime_guess::Mime,
|
||||||
|
|
@ -2594,6 +2607,28 @@ impl Application for App {
|
||||||
NavMenuAction::OpenInNewWindow(entity),
|
NavMenuAction::OpenInNewWindow(entity),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(path) = location_opt.and_then(Location::path_opt) {
|
||||||
|
let selected_dir = usize::from(path.is_dir());
|
||||||
|
let action_items: Vec<_> = self
|
||||||
|
.config
|
||||||
|
.context_actions
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, action)| action.matches_selection(1, selected_dir))
|
||||||
|
.map(|(i, action)| {
|
||||||
|
cosmic::widget::menu::Item::Button(
|
||||||
|
action.name.clone(),
|
||||||
|
None,
|
||||||
|
NavMenuAction::RunContextAction(entity, i),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !action_items.is_empty() {
|
||||||
|
items.push(cosmic::widget::menu::Item::Divider);
|
||||||
|
items.extend(action_items);
|
||||||
|
}
|
||||||
|
}
|
||||||
items.push(cosmic::widget::menu::Item::Divider);
|
items.push(cosmic::widget::menu::Item::Divider);
|
||||||
if matches!(location_opt, Some(Location::Path(..))) {
|
if matches!(location_opt, Some(Location::Path(..))) {
|
||||||
items.push(cosmic::widget::menu::Item::Button(
|
items.push(cosmic::widget::menu::Item::Button(
|
||||||
|
|
@ -3186,6 +3221,9 @@ impl Application for App {
|
||||||
Operation::NewFile { path }
|
Operation::NewFile { path }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
DialogPage::RunContextAction { action, paths } => {
|
||||||
|
context_action::run(&self.config.context_actions, action, &paths);
|
||||||
|
}
|
||||||
DialogPage::OpenWith {
|
DialogPage::OpenWith {
|
||||||
path,
|
path,
|
||||||
mime,
|
mime,
|
||||||
|
|
@ -4506,6 +4544,25 @@ impl Application for App {
|
||||||
tab::Command::ExecEntryAction(entry, action) => {
|
tab::Command::ExecEntryAction(entry, action) => {
|
||||||
Self::exec_entry_action(&entry, action);
|
Self::exec_entry_action(&entry, action);
|
||||||
}
|
}
|
||||||
|
tab::Command::RunContextAction(action) => {
|
||||||
|
let paths: Box<[_]> = self.selected_paths(Some(entity)).collect();
|
||||||
|
if let Some(preset) = self.config.context_actions.get(action) {
|
||||||
|
if preset.confirm {
|
||||||
|
commands.push(self.push_dialog(
|
||||||
|
DialogPage::RunContextAction { action, paths },
|
||||||
|
Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
context_action::run(
|
||||||
|
&self.config.context_actions,
|
||||||
|
action,
|
||||||
|
&paths,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("invalid context action index `{action}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
tab::Command::Iced(iced_command) => {
|
tab::Command::Iced(iced_command) => {
|
||||||
commands.push(iced_command.0.map(move |x| {
|
commands.push(iced_command.0.map(move |x| {
|
||||||
cosmic::action::app(Message::TabMessage(Some(entity), x))
|
cosmic::action::app(Message::TabMessage(Some(entity), x))
|
||||||
|
|
@ -5013,6 +5070,30 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NavMenuAction::RunContextAction(entity, action) => {
|
||||||
|
if let Some(path) = self
|
||||||
|
.nav_model
|
||||||
|
.data::<Location>(entity)
|
||||||
|
.and_then(Location::path_opt)
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
let paths = vec![path];
|
||||||
|
if let Some(preset) = self.config.context_actions.get(action) {
|
||||||
|
if preset.confirm {
|
||||||
|
return self.push_dialog(
|
||||||
|
DialogPage::RunContextAction {
|
||||||
|
action,
|
||||||
|
paths: paths.into_boxed_slice(),
|
||||||
|
},
|
||||||
|
Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
context_action::run(&self.config.context_actions, action, &paths);
|
||||||
|
} else {
|
||||||
|
log::warn!("invalid context action index `{action}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
NavMenuAction::OpenInNewTab(entity) => {
|
NavMenuAction::OpenInNewTab(entity) => {
|
||||||
let open_task = match self.nav_model.data::<Location>(entity) {
|
let open_task = match self.nav_model.data::<Location>(entity) {
|
||||||
Some(Location::Network(uri, display_name, path)) => self.open_tab(
|
Some(Location::Network(uri, display_name, path)) => self.open_tab(
|
||||||
|
|
@ -5789,6 +5870,26 @@ impl Application for App {
|
||||||
.spacing(space_xxs),
|
.spacing(space_xxs),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
DialogPage::RunContextAction { action, paths } => {
|
||||||
|
let name = self
|
||||||
|
.config
|
||||||
|
.context_actions
|
||||||
|
.get(*action)
|
||||||
|
.map_or_else(|| fl!("context-action"), |preset| preset.name.clone());
|
||||||
|
|
||||||
|
widget::dialog()
|
||||||
|
.title(fl!("context-action-confirm-title", name = name))
|
||||||
|
.body(fl!("context-action-confirm-warning", items = paths.len()))
|
||||||
|
.icon(icon::from_name("dialog-error").size(64))
|
||||||
|
.primary_action(
|
||||||
|
widget::button::suggested(fl!("run"))
|
||||||
|
.on_press(Message::DialogComplete)
|
||||||
|
.id(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()),
|
||||||
|
)
|
||||||
|
.secondary_action(
|
||||||
|
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||||
|
)
|
||||||
|
}
|
||||||
DialogPage::OpenWith {
|
DialogPage::OpenWith {
|
||||||
path,
|
path,
|
||||||
mime,
|
mime,
|
||||||
|
|
@ -6335,6 +6436,7 @@ impl Application for App {
|
||||||
&self.key_binds,
|
&self.key_binds,
|
||||||
&self.modifiers,
|
&self.modifiers,
|
||||||
self.clipboard_has_content(),
|
self.clipboard_has_content(),
|
||||||
|
&self.config.context_actions,
|
||||||
)
|
)
|
||||||
.map(move |message| Message::TabMessage(Some(entity), message));
|
.map(move |message| Message::TabMessage(Some(entity), message));
|
||||||
tab_column = tab_column.push(tab_view);
|
tab_column = tab_column.push(tab_view);
|
||||||
|
|
@ -6363,6 +6465,7 @@ impl Application for App {
|
||||||
&self.key_binds,
|
&self.key_binds,
|
||||||
&window.modifiers,
|
&window.modifiers,
|
||||||
self.clipboard_has_content(),
|
self.clipboard_has_content(),
|
||||||
|
&self.config.context_actions,
|
||||||
)
|
)
|
||||||
.map(|x| Message::TabMessage(Some(*entity), x)),
|
.map(|x| Message::TabMessage(Some(*entity), x)),
|
||||||
id.clone(),
|
id.clone(),
|
||||||
|
|
@ -6380,6 +6483,7 @@ impl Application for App {
|
||||||
&self.key_binds,
|
&self.key_binds,
|
||||||
&window.modifiers,
|
&window.modifiers,
|
||||||
self.clipboard_has_content(),
|
self.clipboard_has_content(),
|
||||||
|
&self.config.context_actions,
|
||||||
)
|
)
|
||||||
.map(move |message| Message::TabMessage(Some(*entity), message)),
|
.map(move |message| Message::TabMessage(Some(*entity), message)),
|
||||||
None => widget::space::vertical().into(),
|
None => widget::space::vertical().into(),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ use crate::{
|
||||||
tab::{HeadingOptions, Location, View},
|
tab::{HeadingOptions, Location, View},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use crate::context_action::{ContextActionPreset, ContextActionSelection};
|
||||||
|
|
||||||
pub const CONFIG_VERSION: u64 = 1;
|
pub const CONFIG_VERSION: u64 = 1;
|
||||||
|
|
||||||
// Default icon sizes
|
// Default icon sizes
|
||||||
|
|
@ -164,6 +166,7 @@ pub struct Config {
|
||||||
pub app_theme: AppTheme,
|
pub app_theme: AppTheme,
|
||||||
pub dialog: DialogConfig,
|
pub dialog: DialogConfig,
|
||||||
pub desktop: DesktopConfig,
|
pub desktop: DesktopConfig,
|
||||||
|
pub context_actions: Vec<ContextActionPreset>,
|
||||||
pub thumb_cfg: ThumbCfg,
|
pub thumb_cfg: ThumbCfg,
|
||||||
pub favorites: Vec<Favorite>,
|
pub favorites: Vec<Favorite>,
|
||||||
pub show_details: bool,
|
pub show_details: bool,
|
||||||
|
|
@ -220,6 +223,7 @@ impl Default for Config {
|
||||||
app_theme: AppTheme::System,
|
app_theme: AppTheme::System,
|
||||||
desktop: DesktopConfig::default(),
|
desktop: DesktopConfig::default(),
|
||||||
dialog: DialogConfig::default(),
|
dialog: DialogConfig::default(),
|
||||||
|
context_actions: Vec::new(),
|
||||||
thumb_cfg: ThumbCfg::default(),
|
thumb_cfg: ThumbCfg::default(),
|
||||||
favorites: vec![
|
favorites: vec![
|
||||||
Favorite::Home,
|
Favorite::Home,
|
||||||
|
|
|
||||||
79
src/context_action.rs
Normal file
79
src/context_action.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{mime_app, spawn_detached::spawn_detached};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum ContextActionSelection {
|
||||||
|
#[default]
|
||||||
|
#[serde(alias = "any")]
|
||||||
|
Any,
|
||||||
|
#[serde(alias = "files")]
|
||||||
|
Files,
|
||||||
|
#[serde(alias = "folders")]
|
||||||
|
Folders,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ContextActionPreset {
|
||||||
|
pub name: String,
|
||||||
|
pub confirm: bool,
|
||||||
|
pub selection: ContextActionSelection,
|
||||||
|
pub steps: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextActionPreset {
|
||||||
|
pub fn matches_selection(&self, selected: usize, selected_dir: usize) -> bool {
|
||||||
|
if selected == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.selection {
|
||||||
|
ContextActionSelection::Any => true,
|
||||||
|
ContextActionSelection::Files => selected_dir == 0,
|
||||||
|
ContextActionSelection::Folders => selected_dir == selected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self, paths: &[PathBuf]) {
|
||||||
|
if self.steps.is_empty() {
|
||||||
|
log::warn!("context action {:?} has no steps", self.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for step in &self.steps {
|
||||||
|
let Some(commands) = mime_app::exec_to_command(step, paths) else {
|
||||||
|
log::warn!(
|
||||||
|
"failed to parse context action {:?}: invalid Exec {:?}",
|
||||||
|
self.name,
|
||||||
|
step
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for mut command in commands {
|
||||||
|
if let Err(err) = spawn_detached(&mut command) {
|
||||||
|
log::warn!(
|
||||||
|
"failed to run context action {:?} step {:?}: {}",
|
||||||
|
self.name,
|
||||||
|
step,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(actions: &[ContextActionPreset], action: usize, paths: &[PathBuf]) {
|
||||||
|
if let Some(preset) = actions.get(action) {
|
||||||
|
preset.run(paths);
|
||||||
|
} else {
|
||||||
|
log::warn!("invalid context action index `{action}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -441,8 +441,13 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum DialogPage {
|
enum DialogPage {
|
||||||
NewFolder { parent: PathBuf, name: String },
|
NewFolder {
|
||||||
Replace { filename: String },
|
parent: PathBuf,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
Replace {
|
||||||
|
filename: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -1833,6 +1838,7 @@ impl Application for App {
|
||||||
&app.key_binds,
|
&app.key_binds,
|
||||||
&app.modifiers,
|
&app.modifiers,
|
||||||
false, // Paste not used in dialogs
|
false, // Paste not used in dialogs
|
||||||
|
&app.flags.config.context_actions,
|
||||||
)
|
)
|
||||||
.map(Message::TabMessage)
|
.map(Message::TabMessage)
|
||||||
.map(cosmic::Action::App),
|
.map(cosmic::Action::App),
|
||||||
|
|
@ -2025,8 +2031,8 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
col = col.push(
|
col = col.push(
|
||||||
self.tab
|
self.tab
|
||||||
.view(&self.key_binds, &self.modifiers, false)
|
.view(&self.key_binds, &self.modifiers, false, &[])
|
||||||
.map(Message::TabMessage),
|
.map(Message::TabMessage),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use app::{App, Flags};
|
||||||
pub mod app;
|
pub mod app;
|
||||||
mod archive;
|
mod archive;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
mod context_action;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
|
|
|
||||||
21
src/menu.rs
21
src/menu.rs
|
|
@ -20,7 +20,7 @@ use std::{collections::HashMap, sync::LazyLock};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{Action, Message},
|
app::{Action, Message},
|
||||||
config::Config,
|
config::{Config, ContextActionPreset},
|
||||||
fl,
|
fl,
|
||||||
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
|
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
|
||||||
};
|
};
|
||||||
|
|
@ -60,6 +60,7 @@ pub fn context_menu<'a>(
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
modifiers: &Modifiers,
|
modifiers: &Modifiers,
|
||||||
clipboard_paste_available: bool,
|
clipboard_paste_available: bool,
|
||||||
|
context_actions: &[ContextActionPreset],
|
||||||
) -> Element<'a, tab::Message> {
|
) -> Element<'a, tab::Message> {
|
||||||
let find_key = |action: &Action| -> String {
|
let find_key = |action: &Action| -> String {
|
||||||
for (key_bind, key_action) in key_binds {
|
for (key_bind, key_action) in key_binds {
|
||||||
|
|
@ -157,6 +158,14 @@ pub fn context_menu<'a>(
|
||||||
selected_types.sort_unstable();
|
selected_types.sort_unstable();
|
||||||
selected_types.dedup();
|
selected_types.dedup();
|
||||||
selected_trash_only = selected_trash_only && selected == 1;
|
selected_trash_only = selected_trash_only && selected == 1;
|
||||||
|
let context_action_items = |selected: usize, selected_dir: usize| {
|
||||||
|
context_actions
|
||||||
|
.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>>>()
|
||||||
|
};
|
||||||
// Parse the desktop entry if it is the only selection
|
// Parse the desktop entry if it is the only selection
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
let selected_desktop_entry = selected_desktop_entry.and_then(|path| {
|
let selected_desktop_entry = selected_desktop_entry.and_then(|path| {
|
||||||
|
|
@ -204,6 +213,11 @@ pub fn context_menu<'a>(
|
||||||
}
|
}
|
||||||
// Should this simply bypass trash and remove the shortcut?
|
// 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).into());
|
||||||
|
let action_items = context_action_items(selected, selected_dir);
|
||||||
|
if !action_items.is_empty() {
|
||||||
|
children.push(divider::horizontal::light().into());
|
||||||
|
children.extend(action_items);
|
||||||
|
}
|
||||||
} else if selected > 0 {
|
} else if selected > 0 {
|
||||||
if selected_dir == 1 && selected == 1 || selected_dir == 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).into());
|
||||||
|
|
@ -226,6 +240,11 @@ pub fn context_menu<'a>(
|
||||||
children
|
children
|
||||||
.push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into());
|
.push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into());
|
||||||
}
|
}
|
||||||
|
let action_items = context_action_items(selected, selected_dir);
|
||||||
|
if !action_items.is_empty() {
|
||||||
|
children.push(divider::horizontal::light().into());
|
||||||
|
children.extend(action_items);
|
||||||
|
}
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
if selected_mount_point == 0 {
|
if selected_mount_point == 0 {
|
||||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||||
|
|
|
||||||
31
src/tab.rs
31
src/tab.rs
|
|
@ -66,7 +66,10 @@ use crate::{
|
||||||
FxOrderMap,
|
FxOrderMap,
|
||||||
app::{Action, PreviewItem, PreviewKind},
|
app::{Action, PreviewItem, PreviewKind},
|
||||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
||||||
config::{DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, ThumbCfg},
|
config::{
|
||||||
|
ContextActionPreset, DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig,
|
||||||
|
ThumbCfg,
|
||||||
|
},
|
||||||
dialog::DialogKind,
|
dialog::DialogKind,
|
||||||
fl,
|
fl,
|
||||||
large_image::{
|
large_image::{
|
||||||
|
|
@ -1794,6 +1797,7 @@ pub enum Command {
|
||||||
OpenInNewWindow(PathBuf),
|
OpenInNewWindow(PathBuf),
|
||||||
OpenTrash,
|
OpenTrash,
|
||||||
Preview(PreviewKind),
|
Preview(PreviewKind),
|
||||||
|
RunContextAction(usize),
|
||||||
SetOpenWith(Mime, String),
|
SetOpenWith(Mime, String),
|
||||||
SetPermissions(PathBuf, u32),
|
SetPermissions(PathBuf, u32),
|
||||||
SetMultiplePermissions(Vec<(PathBuf, u32)>),
|
SetMultiplePermissions(Vec<(PathBuf, u32)>),
|
||||||
|
|
@ -1852,6 +1856,7 @@ pub enum Message {
|
||||||
SelectFirst,
|
SelectFirst,
|
||||||
SelectLast,
|
SelectLast,
|
||||||
SetOpenWith(Mime, String),
|
SetOpenWith(Mime, String),
|
||||||
|
RunContextAction(usize),
|
||||||
SetPermissions(PathBuf, u32),
|
SetPermissions(PathBuf, u32),
|
||||||
ShiftPermissions(Option<(PathBuf, u32)>, u32, u32),
|
ShiftPermissions(Option<(PathBuf, u32)>, u32, u32),
|
||||||
SetSort(HeadingOptions, bool),
|
SetSort(HeadingOptions, bool),
|
||||||
|
|
@ -3566,6 +3571,11 @@ impl Tab {
|
||||||
|
|
||||||
commands.push(Command::Action(action));
|
commands.push(Command::Action(action));
|
||||||
}
|
}
|
||||||
|
Message::RunContextAction(action) => {
|
||||||
|
self.context_menu = None;
|
||||||
|
|
||||||
|
commands.push(Command::RunContextAction(action));
|
||||||
|
}
|
||||||
Message::ContextMenu(point_opt, _) => {
|
Message::ContextMenu(point_opt, _) => {
|
||||||
self.edit_location = None;
|
self.edit_location = None;
|
||||||
self.context_menu = point_opt;
|
self.context_menu = point_opt;
|
||||||
|
|
@ -6083,6 +6093,7 @@ impl Tab {
|
||||||
modifiers: &'a Modifiers,
|
modifiers: &'a Modifiers,
|
||||||
size: Size,
|
size: Size,
|
||||||
clipboard_paste_available: bool,
|
clipboard_paste_available: bool,
|
||||||
|
context_actions: &'a [ContextActionPreset],
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
// Update cached size
|
// Update cached size
|
||||||
self.size_opt.set(Some(size));
|
self.size_opt.set(Some(size));
|
||||||
|
|
@ -6170,8 +6181,13 @@ impl Tab {
|
||||||
if let Some(point) = self.context_menu
|
if let Some(point) = self.context_menu
|
||||||
&& (!cfg!(feature = "wayland") || !crate::is_wayland())
|
&& (!cfg!(feature = "wayland") || !crate::is_wayland())
|
||||||
{
|
{
|
||||||
let context_menu =
|
let context_menu = menu::context_menu(
|
||||||
menu::context_menu(self, key_binds, modifiers, clipboard_paste_available);
|
self,
|
||||||
|
key_binds,
|
||||||
|
modifiers,
|
||||||
|
clipboard_paste_available,
|
||||||
|
context_actions,
|
||||||
|
);
|
||||||
popover = popover
|
popover = popover
|
||||||
.popup(context_menu)
|
.popup(context_menu)
|
||||||
.position(widget::popover::Position::Point(point));
|
.position(widget::popover::Position::Point(point));
|
||||||
|
|
@ -6536,10 +6552,17 @@ impl Tab {
|
||||||
key_binds: &'a HashMap<KeyBind, Action>,
|
key_binds: &'a HashMap<KeyBind, Action>,
|
||||||
modifiers: &'a Modifiers,
|
modifiers: &'a Modifiers,
|
||||||
clipboard_paste_available: bool,
|
clipboard_paste_available: bool,
|
||||||
|
context_actions: &'a [ContextActionPreset],
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
widget::responsive(move |size| {
|
widget::responsive(move |size| {
|
||||||
widget::id_container(
|
widget::id_container(
|
||||||
self.view_responsive(key_binds, modifiers, size, clipboard_paste_available),
|
self.view_responsive(
|
||||||
|
key_binds,
|
||||||
|
modifiers,
|
||||||
|
size,
|
||||||
|
clipboard_paste_available,
|
||||||
|
context_actions,
|
||||||
|
),
|
||||||
Id::new(format!(
|
Id::new(format!(
|
||||||
"tab-{}-{}",
|
"tab-{}-{}",
|
||||||
self.scrollable_id, self.location_title
|
self.scrollable_id, self.location_title
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue