feat: add context menus to nav bar

This commit is contained in:
Michael Aaron Murphy 2024-04-22 23:14:44 +02:00 committed by Jeremy Soller
parent d80c358ca5
commit 8c3af501ca
6 changed files with 314 additions and 193 deletions

View file

@ -29,6 +29,7 @@ use notify_debouncer_full::{
notify::{self, RecommendedWatcher, Watcher},
DebouncedEvent, Debouncer, FileIdMap,
};
use slotmap::Key as SlotMapKey;
use std::{
any::TypeId,
collections::{BTreeMap, HashMap, HashSet, VecDeque},
@ -121,7 +122,7 @@ impl MenuAction for Action {
Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith),
Action::Operations => Message::ToggleContextPage(ContextPage::Operations),
Action::Paste => Message::Paste(entity_opt),
Action::Properties => Message::ToggleContextPage(ContextPage::Properties),
Action::Properties => Message::ToggleContextPage(ContextPage::Properties(None)),
Action::Rename => Message::Rename(entity_opt),
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
Action::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll),
@ -144,6 +145,27 @@ impl MenuAction for Action {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ContextItem {
NavBar(segmented_button::Entity),
TabBar(segmented_button::Entity),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum NavMenuAction {
OpenInNewTab(segmented_button::Entity),
OpenInNewWindow(segmented_button::Entity),
Properties(segmented_button::Entity),
}
impl MenuAction for NavMenuAction {
type Message = cosmic::app::Message<Message>;
fn message(&self, _entity: Option<Entity>) -> Self::Message {
cosmic::app::Message::App(Message::NavMenuAction(*self))
}
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
@ -160,6 +182,8 @@ pub enum Message {
Modifiers(Modifiers),
MoveToTrash(Option<Entity>),
MounterItems(MounterKey, MounterItems),
NavBarContext(Entity),
NavMenuAction(NavMenuAction),
NewItem(Option<Entity>, bool),
NotifyEvents(Vec<DebouncedEvent>),
NotifyWatcher(WatcherWrapper),
@ -200,7 +224,7 @@ pub enum ContextPage {
About,
OpenWith,
Operations,
Properties,
Properties(Option<ContextItem>),
Settings,
}
@ -210,7 +234,7 @@ impl ContextPage {
Self::About => String::new(),
Self::OpenWith => fl!("open-with"),
Self::Operations => fl!("operations"),
Self::Properties => fl!("properties"),
Self::Properties(..) => fl!("properties"),
Self::Settings => fl!("settings"),
}
}
@ -259,6 +283,7 @@ impl PartialEq for WatcherWrapper {
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
nav_bar_context_id: segmented_button::Entity,
nav_model: segmented_button::SingleSelectModel,
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
config_handler: Option<cosmic_config::Config>,
@ -511,9 +536,35 @@ impl App {
widget::settings::view_column(children).into()
}
fn properties(&self) -> Element<Message> {
fn properties(&self, entity: Option<ContextItem>) -> Element<Message> {
match entity {
None => self.tab_properties(self.tab_model.active()),
Some(ContextItem::TabBar(entity)) => self.tab_properties(entity),
Some(ContextItem::NavBar(item)) => {
let mut children = Vec::new();
if let Some(location) = self.nav_model.data::<Location>(item) {
if let Location::Path(path) = location {
let parent = path.parent().unwrap_or(path);
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
if item.path_opt.as_deref() == Some(path) {
children.push(item.property_view(IconSizes::default()));
}
}
};
}
widget::settings::view_column(children).into()
}
}
}
fn tab_properties(&self, entity: segmented_button::Entity) -> Element<Message> {
let mut children = Vec::new();
let entity = self.tab_model.active();
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
if let Some(items) = tab.items_opt() {
for item in items.iter() {
@ -526,6 +577,7 @@ impl App {
}
}
}
widget::settings::view_column(children).into()
}
@ -672,16 +724,18 @@ impl Application for App {
let nav_model = self.nav_model()?;
let mut nav = cosmic::widget::nav_bar_dnd(
nav_model,
|entity| cosmic::app::Message::Cosmic(cosmic::app::cosmic::Message::NavBar(entity)),
|entity, _| cosmic::app::Message::App(Message::DndEnterNav(entity)),
|_| cosmic::app::Message::App(Message::DndExitNav),
|entity, data, action| {
cosmic::app::Message::App(Message::DndDropNav(entity, data, action))
},
self.nav_drag_id,
);
let mut nav = cosmic::widget::nav_bar(nav_model, |entity| {
cosmic::app::Message::Cosmic(cosmic::app::cosmic::Message::NavBar(entity))
})
.drag_id(self.nav_drag_id)
.on_dnd_enter(|entity, _| cosmic::app::Message::App(Message::DndEnterNav(entity)))
.on_dnd_leave(|_| cosmic::app::Message::App(Message::DndExitNav))
.on_dnd_drop(|entity, data, action| {
cosmic::app::Message::App(Message::DndDropNav(entity, data, action))
})
.on_context(|entity| cosmic::app::Message::App(Message::NavBarContext(entity)))
.context_menu(self.nav_context_menu(self.nav_bar_context_id))
.into_container();
if !self.core().is_condensed() {
nav = nav.max_width(280);
@ -734,6 +788,7 @@ impl Application for App {
let mut app = App {
core,
nav_bar_context_id: segmented_button::Entity::null(),
nav_model: nav_model.build(),
tab_model: segmented_button::ModelBuilder::default().build(),
config_handler: flags.config_handler,
@ -787,6 +842,30 @@ impl Application for App {
(app, Command::batch(commands))
}
fn nav_context_menu(
&self,
id: widget::nav_bar::Id,
) -> Option<Vec<widget::menu::Tree<cosmic::app::Message<Self::Message>>>> {
Some(cosmic::widget::menu::items(
&HashMap::new(),
vec![
cosmic::widget::menu::Item::Button(
fl!("open-in-new-tab"),
NavMenuAction::OpenInNewTab(id),
),
cosmic::widget::menu::Item::Button(
fl!("open-in-new-window"),
NavMenuAction::OpenInNewWindow(id),
),
cosmic::widget::menu::Item::Divider,
cosmic::widget::menu::Item::Button(
fl!("properties"),
NavMenuAction::Properties(id),
),
],
))
}
fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> {
Some(&self.nav_model)
}
@ -1570,6 +1649,47 @@ impl Application for App {
return self.update(Message::TabActivate(entity));
}
}
// Tracks which nav bar item to show a context menu for.
Message::NavBarContext(entity) => {
self.nav_bar_context_id = entity;
}
// Applies selected nav bar context menu operation.
Message::NavMenuAction(action) => match action {
NavMenuAction::OpenInNewTab(entity) => {
match self.nav_model.data::<Location>(entity) {
Some(Location::Path(ref path)) => {
return self.open_tab(Location::Path(path.clone()));
}
_ => {}
}
}
// Open the selected path in a new cosmic-files window.
NavMenuAction::OpenInNewWindow(entity) => {
if let Some(&Location::Path(ref path)) = self.nav_model.data::<Location>(entity)
{
match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).arg(path).spawn() {
Ok(_child) => {}
Err(err) => {
log::error!("failed to execute {:?}: {}", exe, err);
}
},
Err(err) => {
log::error!("failed to get current executable path: {}", err);
}
}
}
}
NavMenuAction::Properties(entity) => {
self.context_page = ContextPage::Properties(Some(ContextItem::NavBar(entity)));
self.core.window.show_context = true;
self.set_context_title(self.context_page.title());
}
},
}
Command::none()
@ -1584,7 +1704,7 @@ impl Application for App {
ContextPage::About => self.about(),
ContextPage::OpenWith => self.open_with(),
ContextPage::Operations => self.operations(),
ContextPage::Properties => self.properties(),
ContextPage::Properties(entity) => self.properties(entity),
ContextPage::Settings => self.settings(),
})
}

View file

@ -1,15 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::menu_tree::{menu_items, menu_root, MenuItem};
use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
use cosmic::{
//TODO: export iced::widget::horizontal_rule in cosmic::widget
iced::{widget::horizontal_rule, Alignment, Background, Border, Length},
theme,
widget::{
self,
menu::{ItemHeight, ItemWidth, MenuBar, MenuTree},
},
widget,
Element,
};
use std::collections::HashMap;
@ -173,53 +170,53 @@ pub fn context_menu<'a>(
pub fn menu_bar<'a>(key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
MenuBar::new(vec![
MenuTree::with_children(
menu_root(fl!("file")),
menu_items(
menu::Tree::with_children(
menu::root(fl!("file")),
menu::items(
key_binds,
vec![
MenuItem::Button(fl!("new-tab"), Action::TabNew),
MenuItem::Button(fl!("new-window"), Action::WindowNew),
MenuItem::Button(fl!("new-file"), Action::NewFile),
MenuItem::Button(fl!("new-folder"), Action::NewFolder),
MenuItem::Button(fl!("open"), Action::Open),
MenuItem::Divider,
MenuItem::Button(fl!("rename"), Action::Rename),
menu::Item::Button(fl!("new-tab"), Action::TabNew),
menu::Item::Button(fl!("new-window"), Action::WindowNew),
menu::Item::Button(fl!("new-file"), Action::NewFile),
menu::Item::Button(fl!("new-folder"), Action::NewFolder),
menu::Item::Button(fl!("open"), Action::Open),
menu::Item::Divider,
menu::Item::Button(fl!("rename"), Action::Rename),
//TOOD: add to sidebar, then divider
MenuItem::Divider,
MenuItem::Button(fl!("move-to-trash"), Action::MoveToTrash),
MenuItem::Divider,
MenuItem::Button(fl!("close-tab"), Action::TabClose),
MenuItem::Button(fl!("quit"), Action::WindowClose),
menu::Item::Divider,
menu::Item::Button(fl!("move-to-trash"), Action::MoveToTrash),
menu::Item::Divider,
menu::Item::Button(fl!("close-tab"), Action::TabClose),
menu::Item::Button(fl!("quit"), Action::WindowClose),
],
),
),
MenuTree::with_children(
menu_root(fl!("edit")),
menu_items(
menu::Tree::with_children(
menu::root(fl!("edit")),
menu::items(
key_binds,
vec![
MenuItem::Button(fl!("cut"), Action::Cut),
MenuItem::Button(fl!("copy"), Action::Copy),
MenuItem::Button(fl!("paste"), Action::Paste),
MenuItem::Button(fl!("select-all"), Action::SelectAll),
MenuItem::Divider,
menu::Item::Button(fl!("cut"), Action::Cut),
menu::Item::Button(fl!("copy"), Action::Copy),
menu::Item::Button(fl!("paste"), Action::Paste),
menu::Item::Button(fl!("select-all"), Action::SelectAll),
menu::Item::Divider,
//TODO: edit history
MenuItem::Button(fl!("operations"), Action::Operations),
menu::Item::Button(fl!("operations"), Action::Operations),
],
),
),
MenuTree::with_children(
menu_root(fl!("view")),
menu_items(
menu::Tree::with_children(
menu::root(fl!("view")),
menu::items(
key_binds,
vec![
MenuItem::Button(fl!("grid-view"), Action::TabViewGrid),
MenuItem::Button(fl!("list-view"), Action::TabViewList),
MenuItem::Divider,
MenuItem::Button(fl!("menu-settings"), Action::Settings),
MenuItem::Divider,
MenuItem::Button(fl!("menu-about"), Action::About),
menu::Item::Button(fl!("grid-view"), Action::TabViewGrid),
menu::Item::Button(fl!("list-view"), Action::TabViewList),
menu::Item::Divider,
menu::Item::Button(fl!("menu-settings"), Action::Settings),
menu::Item::Divider,
menu::Item::Button(fl!("menu-about"), Action::About),
],
),
),

View file

@ -523,7 +523,7 @@ pub struct Item {
}
impl Item {
fn preview(&self, sizes: IconSizes) -> Element<app::Message> {
fn preview(&self, sizes: IconSizes) -> Element<'static, app::Message> {
// This loads the image only if thumbnailing worked
let icon = widget::icon::icon(self.icon_handle_grid.clone())
.content_fit(ContentFit::Contain)
@ -598,7 +598,7 @@ impl Item {
column.into()
}
pub fn property_view(&self, sizes: IconSizes) -> Element<app::Message> {
pub fn property_view(&self, sizes: IconSizes) -> Element<'static, app::Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut column = widget::column().spacing(space_xxxs);