Merge pull request #599 from snaggen/open_link_menu
Add context menu for opening links
This commit is contained in:
commit
96d38604f5
6 changed files with 181 additions and 63 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
|
||||
|
|
|
|||
64
src/main.rs
64
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, Option<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();
|
||||
terminal.context_menu = None;
|
||||
//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 = 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,14 +2845,22 @@ 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(point) => widget::popover(terminal_box.context_menu(point))
|
||||
.popup(menu::context_menu(&self.config, &self.key_binds, entity))
|
||||
.position(widget::popover::Position::Point(point))
|
||||
.into(),
|
||||
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,
|
||||
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);
|
||||
|
|
|
|||
66
src/menu.rs
66
src/menu.rs
|
|
@ -1,14 +1,14 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::iced::Point;
|
||||
use cosmic::widget::Column;
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::{Item as MenuItem, menu_button};
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::Core,
|
||||
iced::{
|
||||
Background, Length,
|
||||
advanced::widget::text::Style as TextStyle,
|
||||
widget::{column, horizontal_space},
|
||||
Background, Length, advanced::widget::text::Style as TextStyle, widget::horizontal_space,
|
||||
},
|
||||
iced_core::Border,
|
||||
theme,
|
||||
|
|
@ -25,10 +25,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 {
|
||||
|
|
@ -70,32 +77,49 @@ pub fn context_menu<'a>(
|
|||
.on_press(Message::TabContextAction(entity, action))
|
||||
};
|
||||
|
||||
let mut content = column!(
|
||||
menu_item(fl!("copy"), Action::Copy),
|
||||
menu_item(fl!("paste"), Action::Paste),
|
||||
menu_item(fl!("select-all"), Action::SelectAll),
|
||||
divider::horizontal::light(),
|
||||
menu_item(fl!("clear-scrollback"), Action::ClearScrollback),
|
||||
divider::horizontal::light(),
|
||||
menu_item(fl!("split-horizontal"), Action::PaneSplitHorizontal),
|
||||
menu_item(fl!("split-vertical"), Action::PaneSplitVertical),
|
||||
menu_item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
|
||||
divider::horizontal::light(),
|
||||
menu_item(fl!("new-tab"), Action::TabNew),
|
||||
menu_item(fl!("menu-settings"), Action::Settings),
|
||||
);
|
||||
let mut rows = vec![
|
||||
Element::from(menu_item(fl!("copy"), Action::Copy)),
|
||||
Element::from(menu_item(fl!("paste"), Action::Paste)),
|
||||
Element::from(menu_item(fl!("select-all"), Action::SelectAll)),
|
||||
Element::from(divider::horizontal::light()),
|
||||
Element::from(menu_item(fl!("clear-scrollback"), Action::ClearScrollback)),
|
||||
Element::from(divider::horizontal::light()),
|
||||
Element::from(menu_item(
|
||||
fl!("split-horizontal"),
|
||||
Action::PaneSplitHorizontal,
|
||||
)),
|
||||
Element::from(menu_item(fl!("split-vertical"), Action::PaneSplitVertical)),
|
||||
Element::from(menu_item(
|
||||
fl!("pane-toggle-maximize"),
|
||||
Action::PaneToggleMaximized,
|
||||
)),
|
||||
Element::from(divider::horizontal::light()),
|
||||
Element::from(menu_item(fl!("new-tab"), Action::TabNew)),
|
||||
Element::from(menu_item(fl!("menu-settings"), Action::Settings)),
|
||||
];
|
||||
#[cfg(feature = "password_manager")]
|
||||
{
|
||||
content = content.push(menu_item(
|
||||
rows.push(Element::from(menu_item(
|
||||
fl!("menu-password-manager"),
|
||||
Action::PasswordManager,
|
||||
));
|
||||
)));
|
||||
}
|
||||
content = content.push(menu_checkbox(
|
||||
rows.push(Element::from(menu_checkbox(
|
||||
fl!("show-headerbar"),
|
||||
config.show_headerbar,
|
||||
Action::ShowHeaderBar(!config.show_headerbar),
|
||||
));
|
||||
)));
|
||||
|
||||
//If we have a link
|
||||
//prepend the Open Link item
|
||||
if link.is_some() {
|
||||
rows.insert(
|
||||
0,
|
||||
Element::from(menu_item(fl!("open-link"), Action::LaunchUrlByMenu)),
|
||||
);
|
||||
rows.insert(1, Element::from(divider::horizontal::light()));
|
||||
}
|
||||
let content = Column::with_children(rows);
|
||||
widget::container(content)
|
||||
.padding(1)
|
||||
//TODO: move style to libcosmic
|
||||
|
|
|
|||
|
|
@ -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(Option<MenuState>) -> Message + 'a>>,
|
||||
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
|
||||
opacity: Option<f32>,
|
||||
mouse_inside_boundary: Option<bool>,
|
||||
|
|
@ -126,7 +126,7 @@ where
|
|||
|
||||
pub fn on_context_menu(
|
||||
mut self,
|
||||
on_context_menu: impl Fn(Option<Point>) -> Message + 'a,
|
||||
on_context_menu: impl Fn(Option<MenuState>) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_context_menu = Some(Box::new(on_context_menu));
|
||||
self
|
||||
|
|
@ -927,7 +927,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 {
|
||||
|
|
@ -1100,13 +1100,35 @@ 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,
|
||||
None => match button {
|
||||
Button::Right => Some(p),
|
||||
_ => None,
|
||||
},
|
||||
}));
|
||||
match self.context_menu {
|
||||
Some(_) => {
|
||||
shell.publish(on_context_menu(None));
|
||||
}
|
||||
None => {
|
||||
if button == 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);
|
||||
shell.publish(on_context_menu(Some(MenuState {
|
||||
position: Some(p),
|
||||
link,
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
status = Status::Captured;
|
||||
}
|
||||
|
|
@ -1125,14 +1147,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 +1197,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 +1302,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,18 +1317,44 @@ 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 !modifiers.contains(Modifiers::CTRL) {
|
||||
if terminal.active_regex_match.is_some() {
|
||||
terminal.active_regex_match = None;
|
||||
terminal.needs_update = true;
|
||||
}
|
||||
//Do not update any highlights if
|
||||
//there is a context_menu shown
|
||||
//to the user
|
||||
if terminal.context_menu.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
//Require CTRL for keyboard and mouse interaction
|
||||
if let Some(modifiers) = modifiers {
|
||||
if !modifiers.contains(Modifiers::CTRL) {
|
||||
if terminal.active_regex_match.is_some() {
|
||||
terminal.active_regex_match = None;
|
||||
terminal.needs_update = true;
|
||||
}
|
||||
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