diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 4dd5d53..bc05043 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -13,3 +13,9 @@ syntax-dark = Syntax dark syntax-light = Syntax light default-font = Default font default-font-size = Default font size + +# Context menu +copy = Copy +paste = Paste +select-all = Select all +new-tab = New tab diff --git a/src/main.rs b/src/main.rs index 5226980..bb68f66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use cosmic::{ keyboard::{Event as KeyEvent, KeyCode, Modifiers}, subscription::{self, Subscription}, widget::row, - window, Alignment, Event, Length, + window, Alignment, Event, Length, Point, }, style, widget::{self, segmented_button}, @@ -30,6 +30,8 @@ mod config; mod localize; +mod menu; + use self::terminal::{Terminal, TerminalScroll}; mod terminal; @@ -107,20 +109,40 @@ pub struct Flags { term_config: TermConfig, } +#[derive(Clone, Debug)] +pub enum Action { + Copy, + Paste, + SelectAll, +} + +impl Action { + pub fn message(self, entity: segmented_button::Entity) -> Message { + match self { + Action::Copy => Message::Copy(Some(entity)), + Action::Paste => Message::Paste(Some(entity)), + Action::SelectAll => Message::SelectAll(Some(entity)), + } + } +} + /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { AppTheme(AppTheme), Config(Config), - Copy, + Copy(Option), DefaultFont(usize), DefaultFontSize(usize), - Paste, - PasteValue(String), + Paste(Option), + PasteValue(Option, String), + SelectAll(Option), SystemThemeModeChange(cosmic_theme::ThemeMode), SyntaxTheme(usize, bool), TabActivate(segmented_button::Entity), TabClose(segmented_button::Entity), + TabContextAction(segmented_button::Entity, Action), + TabContextMenu(segmented_button::Entity, Option), TabNew, TermEvent(segmented_button::Entity, TermEvent), TermEventTx(mpsc::Sender<(segmented_button::Entity, TermEvent)>), @@ -370,11 +392,9 @@ impl Application for App { return self.update_config(); } } - Message::Copy => { - if let Some(terminal) = self - .tab_model - .data::>(self.tab_model.active()) - { + Message::Copy(entity_opt) => { + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let Some(terminal) = self.tab_model.data::>(entity) { let terminal = terminal.lock().unwrap(); let term = terminal.term.lock(); if let Some(text) = term.selection_to_string() { @@ -420,21 +440,25 @@ impl Application for App { log::warn!("failed to find font with index {}", index); } }, - Message::Paste => { - return clipboard::read(|value_opt| match value_opt { - Some(value) => message::app(Message::PasteValue(value)), + Message::Paste(entity_opt) => { + return clipboard::read(move |value_opt| match value_opt { + Some(value) => message::app(Message::PasteValue(entity_opt, value)), None => message::none(), }); } - Message::PasteValue(value) => { - if let Some(terminal) = self - .tab_model - .data::>(self.tab_model.active()) - { + Message::PasteValue(entity_opt, value) => { + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let Some(terminal) = self.tab_model.data::>(entity) { let terminal = terminal.lock().unwrap(); terminal.paste(value); } } + Message::SelectAll(entity_opt) => { + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let Some(terminal) = self.tab_model.data::>(entity) { + log::warn!("TODO: SELECT ALL"); + } + } Message::SystemThemeModeChange(_theme_mode) => { return self.update_config(); } @@ -475,6 +499,30 @@ impl Application for App { return self.update_title(); } + Message::TabContextAction(entity, action) => { + match self.tab_model.data::>(entity) { + Some(terminal) => { + // Close context menu + { + let mut terminal = terminal.lock().unwrap(); + terminal.context_menu = None; + } + // Run action's message + return self.update(action.message(entity)); + } + _ => {} + } + } + Message::TabContextMenu(entity, position_opt) => { + match self.tab_model.data::>(entity) { + Some(terminal) => { + // Update context menu position + let mut terminal = terminal.lock().unwrap(); + terminal.context_menu = position_opt; + } + _ => {} + } + } Message::TabNew => match &self.term_event_tx_opt { Some(term_event_tx) => match self.themes.get(self.config.syntax_theme()) { Some(colors) => { @@ -627,13 +675,28 @@ impl Application for App { ); } - match self - .tab_model - .data::>(self.tab_model.active()) - { + let entity = self.tab_model.active(); + match self.tab_model.data::>(entity) { Some(terminal) => { - //TODO - tab_column = tab_column.push(terminal_box(terminal)); + let terminal_box = terminal_box(terminal).on_context_menu(move |position_opt| { + Message::TabContextMenu(entity, position_opt) + }); + + let context_menu = { + let terminal = terminal.lock().unwrap(); + terminal.context_menu + }; + + let tab_element: Element<'_, Message> = match context_menu { + Some(position) => widget::popover( + terminal_box.context_menu(position), + menu::context_menu(entity), + ) + .position(position) + .into(), + None => terminal_box.into(), + }; + tab_column = tab_column.push(tab_element); } None => { //TODO @@ -654,12 +717,22 @@ impl Application for App { Subscription::batch([ event::listen_with(|event, _status| match event { + Event::Keyboard(KeyEvent::KeyPressed { + key_code: KeyCode::A, + modifiers, + }) => { + if modifiers == Modifiers::CTRL | Modifiers::SHIFT { + Some(Message::SelectAll(None)) + } else { + None + } + } Event::Keyboard(KeyEvent::KeyPressed { key_code: KeyCode::C, modifiers, }) => { if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::Copy) + Some(Message::Copy(None)) } else { None } @@ -679,7 +752,7 @@ impl Application for App { modifiers, }) => { if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::Paste) + Some(Message::Paste(None)) } else { None } diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 0000000..7f80544 --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::{ + //TODO: export in cosmic::widget + iced::{ + widget::{column, horizontal_rule}, + Alignment, Background, Length, + }, + theme, + widget::{self, segmented_button}, + Element, +}; + +use crate::{fl, Action, ContextPage, Message}; + +macro_rules! menu_button { + ($($x:expr),+ $(,)?) => ( + widget::button( + widget::Row::with_children( + vec![$(Element::from($x)),+] + ) + .align_items(Alignment::Center) + ) + .height(Length::Fixed(32.0)) + .padding([4, 16]) + .width(Length::Fill) + .style(theme::Button::MenuItem) + ); +} + +pub fn context_menu<'a>(entity: segmented_button::Entity) -> Element<'a, Message> { + let menu_message = |label, message| menu_button!(widget::text(label)).on_press(message); + + let menu_action = + |label, action| menu_message(label, Message::TabContextAction(entity, action)); + + widget::container(column!( + menu_action(fl!("copy"), Action::Copy), + menu_action(fl!("paste"), Action::Paste), + menu_action(fl!("select-all"), Action::SelectAll), + horizontal_rule(1), + menu_message(fl!("new-tab"), Message::TabNew), + menu_message( + fl!("settings"), + Message::ToggleContextPage(ContextPage::Settings) + ), + )) + .padding(1) + //TODO: move style to libcosmic + .style(theme::Container::custom(|theme| { + let cosmic = theme.cosmic(); + let component = &cosmic.background.component; + widget::container::Appearance { + icon_color: Some(component.on.into()), + text_color: Some(component.on.into()), + background: Some(Background::Color(component.base.into())), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: component.divider.into(), + } + })) + .width(Length::Fixed(240.0)) + .into() +} diff --git a/src/terminal.rs b/src/terminal.rs index 3647a04..34cd98e 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -102,6 +102,7 @@ pub struct Terminal { pub term: Arc>>, colors: Colors, notifier: Notifier, + pub context_menu: Option, } impl Terminal { @@ -164,6 +165,7 @@ impl Terminal { size, term, notifier, + context_menu: None, } }