Add context menu for opening links
This commit is contained in:
parent
8c409f5be3
commit
e7e47d5bac
6 changed files with 154 additions and 41 deletions
|
|
@ -86,6 +86,9 @@ select-all = Select all
|
|||
find = Find
|
||||
clear-scrollback = Clear scrollback
|
||||
|
||||
## Open
|
||||
open-link = Open Link
|
||||
|
||||
## View
|
||||
view = View
|
||||
zoom-in = Larger text
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ paste = Klistra in
|
|||
select-all = Välj alla
|
||||
find = Sök
|
||||
|
||||
## Öppna
|
||||
open-link = Öppna länk
|
||||
|
||||
## Visa
|
||||
|
||||
view = Visa
|
||||
|
|
|
|||
56
src/main.rs
56
src/main.rs
|
|
@ -12,7 +12,7 @@ use cosmic::{
|
|||
cosmic_config::{self, ConfigSet, CosmicConfigEntry},
|
||||
cosmic_theme, executor,
|
||||
iced::{
|
||||
self, Alignment, Color, Event, Length, Limits, Padding, Point, Subscription,
|
||||
self, Alignment, Color, Event, Length, Limits, Padding, Subscription,
|
||||
advanced::graphics::text::font_system,
|
||||
clipboard, event,
|
||||
futures::SinkExt,
|
||||
|
|
@ -63,6 +63,7 @@ mod terminal;
|
|||
use terminal_box::terminal_box;
|
||||
|
||||
use crate::dnd::DndDrop;
|
||||
use crate::menu::MenuState;
|
||||
mod terminal_box;
|
||||
|
||||
#[cfg(feature = "password_manager")]
|
||||
|
|
@ -225,6 +226,7 @@ pub enum Action {
|
|||
CopyOrSigint,
|
||||
CopyPrimary,
|
||||
Find,
|
||||
LaunchUrlByMenu,
|
||||
PaneFocusDown,
|
||||
PaneFocusLeft,
|
||||
PaneFocusRight,
|
||||
|
|
@ -275,6 +277,7 @@ impl Action {
|
|||
Self::CopyOrSigint => Message::CopyOrSigint(entity_opt),
|
||||
Self::CopyPrimary => Message::CopyPrimary(entity_opt),
|
||||
Self::Find => Message::Find(true),
|
||||
Self::LaunchUrlByMenu => Message::LaunchUrlByMenu,
|
||||
Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down),
|
||||
Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left),
|
||||
Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right),
|
||||
|
|
@ -359,6 +362,7 @@ pub enum Message {
|
|||
FocusFollowMouse(bool),
|
||||
Key(Modifiers, Key),
|
||||
LaunchUrl(String),
|
||||
LaunchUrlByMenu,
|
||||
Modifiers(Modifiers),
|
||||
MouseEnter(pane_grid::Pane),
|
||||
Opacity(u8),
|
||||
|
|
@ -396,7 +400,7 @@ pub enum Message {
|
|||
TabActivateJump(usize),
|
||||
TabClose(Option<segmented_button::Entity>),
|
||||
TabContextAction(segmented_button::Entity, Action),
|
||||
TabContextMenu(pane_grid::Pane, Option<Point>),
|
||||
TabContextMenu(pane_grid::Pane, MenuState),
|
||||
TabNew,
|
||||
TabNewNoProfile,
|
||||
TabNext,
|
||||
|
|
@ -2118,6 +2122,23 @@ impl Application for App {
|
|||
log::warn!("failed to open {:?}: {}", url, err);
|
||||
}
|
||||
}
|
||||
Message::LaunchUrlByMenu => {
|
||||
if let Some(tab_model) = self.pane_model.active() {
|
||||
let entity = tab_model.active();
|
||||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
// Update context menu position
|
||||
let mut terminal = terminal.lock().unwrap();
|
||||
if let Some(url) =
|
||||
terminal.context_menu.as_ref().and_then(|m| m.link.as_ref())
|
||||
{
|
||||
if let Err(err) = open::that_detached(url) {
|
||||
log::warn!("failed to open {:?}: {}", url, err);
|
||||
}
|
||||
}
|
||||
terminal.context_menu = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Modifiers(modifiers) => {
|
||||
self.modifiers = modifiers;
|
||||
}
|
||||
|
|
@ -2413,14 +2434,25 @@ impl Application for App {
|
|||
// Close context menu
|
||||
{
|
||||
let mut terminal = terminal.lock().unwrap();
|
||||
//Some actions need the menu_state,
|
||||
//so only clear the position for them.
|
||||
match action {
|
||||
Action::LaunchUrlByMenu => {
|
||||
if let Some(context_menu) = terminal.context_menu.as_mut() {
|
||||
context_menu.position = None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
terminal.context_menu = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Run action's message
|
||||
return self.update(action.message(Some(entity)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::TabContextMenu(pane, position_opt) => {
|
||||
Message::TabContextMenu(pane, menu_state) => {
|
||||
// Close any existing context menues
|
||||
let panes: Vec<_> = self.pane_model.panes.iter().collect();
|
||||
for (_pane, tab_model) in panes {
|
||||
|
|
@ -2437,7 +2469,7 @@ impl Application for App {
|
|||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
// Update context menu position
|
||||
let mut terminal = terminal.lock().unwrap();
|
||||
terminal.context_menu = position_opt;
|
||||
terminal.context_menu = Some(menu_state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2797,9 +2829,7 @@ impl Application for App {
|
|||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
let mut terminal_box = terminal_box(terminal)
|
||||
.id(terminal_id)
|
||||
.on_context_menu(move |position_opt| {
|
||||
Message::TabContextMenu(pane, position_opt)
|
||||
})
|
||||
.on_context_menu(move |menu_state| Message::TabContextMenu(pane, menu_state))
|
||||
.on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click)))
|
||||
.on_open_hyperlink(Some(Box::new(Message::LaunchUrl)))
|
||||
.on_window_focused(|| Message::WindowFocused)
|
||||
|
|
@ -2815,15 +2845,23 @@ impl Application for App {
|
|||
|
||||
let context_menu = {
|
||||
let terminal = terminal.lock().unwrap();
|
||||
terminal.context_menu
|
||||
terminal.context_menu.clone()
|
||||
};
|
||||
|
||||
let tab_element: Element<'_, Message> = match context_menu {
|
||||
Some(menu_state) => match menu_state.position {
|
||||
Some(point) => widget::popover(terminal_box.context_menu(point))
|
||||
.popup(menu::context_menu(&self.config, &self.key_binds, entity))
|
||||
.popup(menu::context_menu(
|
||||
&self.config,
|
||||
&self.key_binds,
|
||||
entity,
|
||||
menu_state.link,
|
||||
))
|
||||
.position(widget::popover::Position::Point(point))
|
||||
.into(),
|
||||
None => terminal_box.into(),
|
||||
},
|
||||
None => terminal_box.into(),
|
||||
};
|
||||
tab_column = tab_column.push(tab_element);
|
||||
}
|
||||
|
|
|
|||
25
src/menu.rs
25
src/menu.rs
|
|
@ -1,5 +1,6 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::iced::Point;
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::{Item as MenuItem, menu_button};
|
||||
use cosmic::{
|
||||
|
|
@ -25,10 +26,17 @@ use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl};
|
|||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
||||
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MenuState {
|
||||
pub position: Option<Point>,
|
||||
pub link: Option<String>,
|
||||
}
|
||||
|
||||
pub fn context_menu<'a>(
|
||||
config: &Config,
|
||||
key_binds: &HashMap<KeyBind, Action>,
|
||||
entity: segmented_button::Entity,
|
||||
link: Option<String>,
|
||||
) -> Element<'a, Message> {
|
||||
let find_key = |action: &Action| -> String {
|
||||
for (key_bind, key_action) in key_binds {
|
||||
|
|
@ -58,6 +66,21 @@ pub fn context_menu<'a>(
|
|||
.on_press(Message::TabContextAction(entity, action))
|
||||
};
|
||||
|
||||
let menu_item_conditional = |label, action, condition| {
|
||||
let key = find_key(&action);
|
||||
let mut button = menu_button(vec![
|
||||
widget::text(label).into(),
|
||||
horizontal_space().into(),
|
||||
widget::text(key)
|
||||
.class(theme::Text::Custom(key_style))
|
||||
.into(),
|
||||
]);
|
||||
if condition {
|
||||
button = button.on_press(Message::TabContextAction(entity, action));
|
||||
}
|
||||
button
|
||||
};
|
||||
|
||||
let menu_checkbox = |label, value, action| {
|
||||
menu_button(vec![
|
||||
widget::text(label).into(),
|
||||
|
|
@ -75,6 +98,8 @@ pub fn context_menu<'a>(
|
|||
menu_item(fl!("paste"), Action::Paste),
|
||||
menu_item(fl!("select-all"), Action::SelectAll),
|
||||
divider::horizontal::light(),
|
||||
menu_item_conditional(fl!("open-link"), Action::LaunchUrlByMenu, link.is_some()),
|
||||
divider::horizontal::light(),
|
||||
menu_item(fl!("clear-scrollback"), Action::ClearScrollback),
|
||||
divider::horizontal::light(),
|
||||
menu_item(fl!("split-horizontal"), Action::PaneSplitHorizontal),
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ pub use alacritty_terminal::grid::Scroll as TerminalScroll;
|
|||
|
||||
use crate::{
|
||||
config::{ColorSchemeKind, Config as AppConfig, ProfileId},
|
||||
menu::MenuState,
|
||||
mouse_reporter::MouseReporter,
|
||||
};
|
||||
|
||||
|
|
@ -236,7 +237,7 @@ impl Metadata {
|
|||
}
|
||||
|
||||
pub struct Terminal {
|
||||
pub context_menu: Option<cosmic::iced::Point>,
|
||||
pub context_menu: Option<MenuState>,
|
||||
pub metadata_set: IndexSet<Metadata>,
|
||||
pub needs_update: bool,
|
||||
pub profile_id_opt: Option<ProfileId>,
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
Action, Terminal, TerminalScroll, key_bind::key_binds, mouse_reporter::MouseReporter,
|
||||
terminal::Metadata,
|
||||
Action, Terminal, TerminalScroll, key_bind::key_binds, menu::MenuState,
|
||||
mouse_reporter::MouseReporter, terminal::Metadata,
|
||||
};
|
||||
|
||||
pub struct TerminalBox<'a, Message> {
|
||||
|
|
@ -56,7 +56,7 @@ pub struct TerminalBox<'a, Message> {
|
|||
show_headerbar: bool,
|
||||
click_timing: Duration,
|
||||
context_menu: Option<Point>,
|
||||
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||
on_context_menu: Option<Box<dyn Fn(MenuState) -> Message + 'a>>,
|
||||
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
|
||||
opacity: Option<f32>,
|
||||
mouse_inside_boundary: Option<bool>,
|
||||
|
|
@ -124,10 +124,7 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_context_menu(
|
||||
mut self,
|
||||
on_context_menu: impl Fn(Option<Point>) -> Message + 'a,
|
||||
) -> Self {
|
||||
pub fn on_context_menu(mut self, on_context_menu: impl Fn(MenuState) -> Message + 'a) -> Self {
|
||||
self.on_context_menu = Some(Box::new(on_context_menu));
|
||||
self
|
||||
}
|
||||
|
|
@ -927,7 +924,7 @@ where
|
|||
} else {
|
||||
None
|
||||
};
|
||||
update_active_regex_match(&mut terminal, location, &state.modifiers);
|
||||
update_active_regex_match(&mut terminal, location, Some(&state.modifiers));
|
||||
}
|
||||
}
|
||||
Event::Keyboard(KeyEvent::KeyPressed {
|
||||
|
|
@ -1101,10 +1098,37 @@ where
|
|||
// Update context menu state
|
||||
if let Some(on_context_menu) = &self.on_context_menu {
|
||||
shell.publish((on_context_menu)(match self.context_menu {
|
||||
Some(_) => None,
|
||||
Some(_) => MenuState {
|
||||
position: None,
|
||||
link: None,
|
||||
},
|
||||
None => match button {
|
||||
Button::Right => Some(p),
|
||||
_ => None,
|
||||
Button::Right => {
|
||||
let x = p.x - self.padding.left;
|
||||
let y = p.y - self.padding.top;
|
||||
//TODO: better calculation of position
|
||||
let col = x / terminal.size().cell_width;
|
||||
let row = y / terminal.size().cell_height;
|
||||
|
||||
let location = terminal.viewport_to_point(TermPoint::new(
|
||||
row as usize,
|
||||
TermColumn(col as usize),
|
||||
));
|
||||
update_active_regex_match(
|
||||
&mut terminal,
|
||||
Some(location),
|
||||
None,
|
||||
);
|
||||
let link = get_hyperlink(&terminal, location);
|
||||
MenuState {
|
||||
position: Some(p),
|
||||
link,
|
||||
}
|
||||
}
|
||||
_ => MenuState {
|
||||
position: None,
|
||||
link: None,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
|
@ -1125,14 +1149,7 @@ where
|
|||
.viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize)));
|
||||
if state.modifiers.control() {
|
||||
if let Some(on_open_hyperlink) = &self.on_open_hyperlink {
|
||||
if let Some(match_) = terminal
|
||||
.regex_matches
|
||||
.iter()
|
||||
.find(|bounds| bounds.contains(&location))
|
||||
{
|
||||
let term = terminal.term.lock();
|
||||
let hyperlink =
|
||||
term.bounds_to_string(*match_.start(), *match_.end());
|
||||
if let Some(hyperlink) = get_hyperlink(&terminal, location) {
|
||||
shell.publish(on_open_hyperlink(hyperlink));
|
||||
status = Status::Captured;
|
||||
}
|
||||
|
|
@ -1182,7 +1199,11 @@ where
|
|||
let row = y / terminal.size().cell_height;
|
||||
let location = terminal
|
||||
.viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize)));
|
||||
update_active_regex_match(&mut terminal, Some(location), &state.modifiers);
|
||||
update_active_regex_match(
|
||||
&mut terminal,
|
||||
Some(location),
|
||||
Some(&state.modifiers),
|
||||
);
|
||||
|
||||
if is_mouse_mode {
|
||||
terminal.report_mouse(event, &state.modifiers, col as u32, row as u32);
|
||||
|
|
@ -1283,7 +1304,11 @@ where
|
|||
row as usize,
|
||||
TermColumn(col as usize),
|
||||
));
|
||||
update_active_regex_match(&mut terminal, Some(location), &state.modifiers);
|
||||
update_active_regex_match(
|
||||
&mut terminal,
|
||||
Some(location),
|
||||
Some(&state.modifiers),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1294,11 +1319,28 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn get_hyperlink(
|
||||
terminal: &std::sync::MutexGuard<'_, Terminal>,
|
||||
location: TermPoint,
|
||||
) -> Option<String> {
|
||||
if let Some(match_) = terminal
|
||||
.regex_matches
|
||||
.iter()
|
||||
.find(|bounds| bounds.contains(&location))
|
||||
{
|
||||
let term = terminal.term.lock();
|
||||
Some(term.bounds_to_string(*match_.start(), *match_.end()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_regex_match(
|
||||
terminal: &mut std::sync::MutexGuard<'_, Terminal>,
|
||||
location: Option<TermPoint>,
|
||||
modifiers: &Modifiers,
|
||||
modifiers: Option<&Modifiers>,
|
||||
) {
|
||||
if let Some(modifiers) = modifiers {
|
||||
if !modifiers.contains(Modifiers::CTRL) {
|
||||
if terminal.active_regex_match.is_some() {
|
||||
terminal.active_regex_match = None;
|
||||
|
|
@ -1306,6 +1348,7 @@ fn update_active_regex_match(
|
|||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some(location) = location else {
|
||||
if terminal.active_regex_match.is_some() {
|
||||
terminal.active_regex_match = None;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue