feat: disable Cut and Copy buttons when no text is selected (#588)

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
This commit is contained in:
Jeremy Soller 2026-08-20 19:01:35 -06:00 • committed by GitHub
commit b23384043e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 7 deletions

View file

@ -194,6 +194,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Action { pub enum Action {
Todo, Todo,
NoOp,
About, About,
CloseFile, CloseFile,
CloseProject(usize), CloseProject(usize),
@ -244,6 +245,7 @@ impl Action {
fn message(&self, entity_opt: Option<Entity>) -> Message { fn message(&self, entity_opt: Option<Entity>) -> Message {
match self { match self {
Self::Todo => Message::Todo, Self::Todo => Message::Todo,
Self::NoOp => Message::NoOp,
Self::About => Message::ToggleContextPage(ContextPage::About), Self::About => Message::ToggleContextPage(ContextPage::About),
Self::CloseFile => Message::CloseFile, Self::CloseFile => Message::CloseFile,
Self::CloseProject(project_i) => Message::CloseProject(*project_i), Self::CloseProject(project_i) => Message::CloseProject(*project_i),
@ -371,6 +373,7 @@ pub enum Message {
Modifiers(Modifiers), Modifiers(Modifiers),
NewFile, NewFile,
NewWindow, NewWindow,
NoOp,
NotifyEvent(notify::Event), NotifyEvent(notify::Event),
NotifyWatcher(WatcherWrapper), NotifyWatcher(WatcherWrapper),
OpenFile(PathBuf), OpenFile(PathBuf),
@ -2161,6 +2164,9 @@ impl Application for App {
} }
} }
} }
Message::NoOp => {
// Do nothing
}
Message::NotifyEvent(event) => { Message::NotifyEvent(event) => {
// Reload tabs that changed // Reload tabs that changed
let mut tab_reload = Vec::new(); let mut tab_reload = Vec::new();
@ -3027,12 +3033,19 @@ impl Application for App {
} }
fn header_start(&self) -> Vec<Element<'_, Message>> { fn header_start(&self) -> Vec<Element<'_, Message>> {
let has_selection = match self.active_tab() {
Some(Tab::Editor(tab)) => tab.editor.lock().unwrap().selection() != Selection::None,
Some(Tab::GitDiff(_)) => false,
None => false,
};
vec![menu_bar( vec![menu_bar(
&self.core, &self.core,
&self.config, &self.config,
&self.config_state, &self.config_state,
&self.key_binds, &self.key_binds,
&self.projects, &self.projects,
has_selection,
)] )]
} }
@ -3088,9 +3101,10 @@ impl Application for App {
text_box = text_box.line_numbers(); text_box = text_box.line_numbers();
} }
let mut popover = widget::popover(text_box); let mut popover = widget::popover(text_box);
let has_selection = tab.editor.lock().unwrap().selection() != Selection::None;
if let Some(point) = tab.context_menu { if let Some(point) = tab.context_menu {
popover = popover popover = popover
.popup(menu::context_menu(&self.key_binds, tab_id)) .popup(menu::context_menu(&self.key_binds, tab_id, has_selection))
.position(widget::popover::Position::Point(point)); .position(widget::popover::Position::Point(point));
} }
tab_column = tab_column.push(popover); tab_column = tab_column.push(popover);

View file

@ -119,6 +119,7 @@ fn format_recent_menu_path(path: &PathBuf, home_dir_opt: Option<&PathBuf>) -> St
pub fn context_menu<'a>( pub fn context_menu<'a>(
key_binds: &HashMap<KeyBind, Action>, key_binds: &HashMap<KeyBind, Action>,
entity: segmented_button::Entity, entity: segmented_button::Entity,
has_selection: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
fn key_style(theme: &cosmic::Theme) -> TextStyle { fn key_style(theme: &cosmic::Theme) -> TextStyle {
// TODO use wayland popups // TODO use wayland popups
@ -130,7 +131,7 @@ pub fn context_menu<'a>(
} }
} }
let menu_item = |menu_label, menu_action| { let base_menu_item = |menu_label, menu_action| {
let mut key = String::new(); let mut key = String::new();
for (key_bind, key_action) in key_binds.iter() { for (key_bind, key_action) in key_binds.iter() {
if key_action == &menu_action { if key_action == &menu_action {
@ -145,7 +146,19 @@ pub fn context_menu<'a>(
.class(theme::Text::Custom(key_style)) .class(theme::Text::Custom(key_style))
.into(), .into(),
]) ])
.on_press(Message::TabContextAction(entity, menu_action)) };
let optional_menu_item = |menu_label, menu_action, disabled| {
base_menu_item(menu_label, menu_action).on_press_maybe(if disabled {
None
} else {
Some(Message::TabContextAction(entity, menu_action))
})
};
let menu_item = |menu_label, menu_action| {
base_menu_item(menu_label, menu_action)
.on_press(Message::TabContextAction(entity, menu_action))
}; };
widget::container( widget::container(
@ -153,8 +166,8 @@ pub fn context_menu<'a>(
menu_item(fl!("undo"), Action::Undo).into(), menu_item(fl!("undo"), Action::Undo).into(),
menu_item(fl!("redo"), Action::Redo).into(), menu_item(fl!("redo"), Action::Redo).into(),
divider::horizontal::light().into(), divider::horizontal::light().into(),
menu_item(fl!("cut"), Action::Cut).into(), optional_menu_item(fl!("cut"), Action::Cut, !has_selection).into(),
menu_item(fl!("copy"), Action::Copy).into(), optional_menu_item(fl!("copy"), Action::Copy, !has_selection).into(),
menu_item(fl!("paste"), Action::Paste).into(), menu_item(fl!("paste"), Action::Paste).into(),
menu_item(fl!("select-all"), Action::SelectAll).into(), menu_item(fl!("select-all"), Action::SelectAll).into(),
]), ]),
@ -186,6 +199,7 @@ pub fn menu_bar<'a>(
config_state: &ConfigState, config_state: &ConfigState,
key_binds: &HashMap<KeyBind, Action>, key_binds: &HashMap<KeyBind, Action>,
projects: &Vec<(String, PathBuf)>, projects: &Vec<(String, PathBuf)>,
has_selection: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
//TODO: port to libcosmic //TODO: port to libcosmic
let menu_tab_width = |tab_width: u16| { let menu_tab_width = |tab_width: u16| {
@ -278,8 +292,16 @@ pub fn menu_bar<'a>(
MenuItem::Button(fl!("undo"), None, Action::Undo), MenuItem::Button(fl!("undo"), None, Action::Undo),
MenuItem::Button(fl!("redo"), None, Action::Redo), MenuItem::Button(fl!("redo"), None, Action::Redo),
MenuItem::Divider, MenuItem::Divider,
MenuItem::Button(fl!("cut"), None, Action::Cut), if has_selection {
MenuItem::Button(fl!("copy"), None, Action::Copy), MenuItem::Button(fl!("cut"), None, Action::Cut)
} else {
MenuItem::ButtonDisabled(fl!("cut"), None, Action::NoOp)
},
if has_selection {
MenuItem::Button(fl!("copy"), None, Action::Copy)
} else {
MenuItem::ButtonDisabled(fl!("copy"), None, Action::NoOp)
},
MenuItem::Button(fl!("paste"), None, Action::Paste), MenuItem::Button(fl!("paste"), None, Action::Paste),
MenuItem::Button(fl!("select-all"), None, Action::SelectAll), MenuItem::Button(fl!("select-all"), None, Action::SelectAll),
MenuItem::Divider, MenuItem::Divider,