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)]
pub enum Action {
Todo,
NoOp,
About,
CloseFile,
CloseProject(usize),
@ -244,6 +245,7 @@ impl Action {
fn message(&self, entity_opt: Option<Entity>) -> Message {
match self {
Self::Todo => Message::Todo,
Self::NoOp => Message::NoOp,
Self::About => Message::ToggleContextPage(ContextPage::About),
Self::CloseFile => Message::CloseFile,
Self::CloseProject(project_i) => Message::CloseProject(*project_i),
@ -371,6 +373,7 @@ pub enum Message {
Modifiers(Modifiers),
NewFile,
NewWindow,
NoOp,
NotifyEvent(notify::Event),
NotifyWatcher(WatcherWrapper),
OpenFile(PathBuf),
@ -2161,6 +2164,9 @@ impl Application for App {
}
}
}
Message::NoOp => {
// Do nothing
}
Message::NotifyEvent(event) => {
// Reload tabs that changed
let mut tab_reload = Vec::new();
@ -3027,12 +3033,19 @@ impl Application for App {
}
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(
&self.core,
&self.config,
&self.config_state,
&self.key_binds,
&self.projects,
has_selection,
)]
}
@ -3088,9 +3101,10 @@ impl Application for App {
text_box = text_box.line_numbers();
}
let mut popover = widget::popover(text_box);
let has_selection = tab.editor.lock().unwrap().selection() != Selection::None;
if let Some(point) = tab.context_menu {
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));
}
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>(
key_binds: &HashMap<KeyBind, Action>,
entity: segmented_button::Entity,
has_selection: bool,
) -> Element<'a, Message> {
fn key_style(theme: &cosmic::Theme) -> TextStyle {
// 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();
for (key_bind, key_action) in key_binds.iter() {
if key_action == &menu_action {
@ -145,6 +146,18 @@ pub fn context_menu<'a>(
.class(theme::Text::Custom(key_style))
.into(),
])
};
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))
};
@ -153,8 +166,8 @@ pub fn context_menu<'a>(
menu_item(fl!("undo"), Action::Undo).into(),
menu_item(fl!("redo"), Action::Redo).into(),
divider::horizontal::light().into(),
menu_item(fl!("cut"), Action::Cut).into(),
menu_item(fl!("copy"), Action::Copy).into(),
optional_menu_item(fl!("cut"), Action::Cut, !has_selection).into(),
optional_menu_item(fl!("copy"), Action::Copy, !has_selection).into(),
menu_item(fl!("paste"), Action::Paste).into(),
menu_item(fl!("select-all"), Action::SelectAll).into(),
]),
@ -186,6 +199,7 @@ pub fn menu_bar<'a>(
config_state: &ConfigState,
key_binds: &HashMap<KeyBind, Action>,
projects: &Vec<(String, PathBuf)>,
has_selection: bool,
) -> Element<'a, Message> {
//TODO: port to libcosmic
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!("redo"), None, Action::Redo),
MenuItem::Divider,
MenuItem::Button(fl!("cut"), None, Action::Cut),
MenuItem::Button(fl!("copy"), None, Action::Copy),
if has_selection {
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!("select-all"), None, Action::SelectAll),
MenuItem::Divider,