mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-03-31 08:35:28 +00:00
implement context menu for the header tags (including fragment URL copy)
This commit is contained in:
parent
12edd5a4f4
commit
a1d9c080d1
3 changed files with 171 additions and 32 deletions
|
|
@ -4,8 +4,8 @@ mod tags;
|
|||
use super::{ItemAction, WindowAction};
|
||||
use crate::{app::browser::window::action::Position, profile::Profile};
|
||||
use gtk::{
|
||||
EventControllerMotion, GestureClick, TextBuffer, TextSearchFlags, TextTag, TextTagTable,
|
||||
TextView, TextWindowType, UriLauncher, Window, WrapMode,
|
||||
EventControllerMotion, GestureClick, PopoverMenu, TextBuffer, TextSearchFlags, TextTag,
|
||||
TextTagTable, TextView, TextWindowType, UriLauncher, Window, WrapMode,
|
||||
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA},
|
||||
gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup},
|
||||
glib::{ControlFlow, GString, Uri, idle_add_local, uri_unescape_string, uuid_string_random},
|
||||
|
|
@ -33,6 +33,7 @@ impl Markdown {
|
|||
) -> Self {
|
||||
// Init HashMap storage (for event controllers)
|
||||
let mut links: HashMap<TextTag, Uri> = HashMap::new();
|
||||
let mut headers: HashMap<TextTag, (String, Uri)> = HashMap::new();
|
||||
|
||||
// Init hovered tag storage for `links`
|
||||
// * maybe less expensive than update entire HashMap by iter
|
||||
|
|
@ -75,9 +76,72 @@ impl Markdown {
|
|||
let gutter = Gutter::build(&text_view);
|
||||
|
||||
// Render markdown tags
|
||||
let title = tags.render(&buffer, base, &link_color.0, &mut links);
|
||||
let title = tags.render(&buffer, base, &link_color.0, &mut links, &mut headers);
|
||||
|
||||
// Context menu
|
||||
// Headers context menu (fragment capture)
|
||||
let action_header_copy_url =
|
||||
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||
action_header_copy_url.connect_activate(|this, _| {
|
||||
Display::default()
|
||||
.unwrap()
|
||||
.clipboard()
|
||||
.set_text(&this.state().unwrap().get::<String>().unwrap())
|
||||
});
|
||||
let action_header_copy_text =
|
||||
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||
action_header_copy_text.connect_activate(|this, _| {
|
||||
Display::default()
|
||||
.unwrap()
|
||||
.clipboard()
|
||||
.set_text(&this.state().unwrap().get::<String>().unwrap())
|
||||
});
|
||||
let action_header_copy_text_selected =
|
||||
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||
action_header_copy_text_selected.connect_activate(|this, _| {
|
||||
Display::default()
|
||||
.unwrap()
|
||||
.clipboard()
|
||||
.set_text(&this.state().unwrap().get::<String>().unwrap())
|
||||
});
|
||||
let header_context_group_id = uuid_string_random();
|
||||
text_view.insert_action_group(
|
||||
&header_context_group_id,
|
||||
Some(&{
|
||||
let g = SimpleActionGroup::new();
|
||||
g.add_action(&action_header_copy_url);
|
||||
g.add_action(&action_header_copy_text);
|
||||
g.add_action(&action_header_copy_text_selected);
|
||||
g
|
||||
}),
|
||||
);
|
||||
let header_context = PopoverMenu::from_model(Some(&{
|
||||
let m = Menu::new();
|
||||
m.append(
|
||||
Some("Copy Header Link"),
|
||||
Some(&format!(
|
||||
"{header_context_group_id}.{}",
|
||||
action_header_copy_url.name()
|
||||
)),
|
||||
);
|
||||
m.append(
|
||||
Some("Copy Header Text"),
|
||||
Some(&format!(
|
||||
"{header_context_group_id}.{}",
|
||||
action_header_copy_text.name()
|
||||
)),
|
||||
);
|
||||
m.append(
|
||||
Some("Copy Text Selected"),
|
||||
Some(&format!(
|
||||
"{header_context_group_id}.{}",
|
||||
action_header_copy_text_selected.name()
|
||||
)),
|
||||
);
|
||||
m
|
||||
}));
|
||||
header_context.set_parent(&text_view);
|
||||
|
||||
// Link context menu
|
||||
let action_link_tab =
|
||||
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||
action_link_tab.connect_activate({
|
||||
|
|
@ -165,7 +229,7 @@ impl Markdown {
|
|||
g
|
||||
}),
|
||||
);
|
||||
let link_context = gtk::PopoverMenu::from_model(Some(&{
|
||||
let link_context = PopoverMenu::from_model(Some(&{
|
||||
let m = Menu::new();
|
||||
m.append(
|
||||
Some("Open Link in New Tab"),
|
||||
|
|
@ -191,7 +255,7 @@ impl Markdown {
|
|||
)),
|
||||
);
|
||||
m_copy.append(
|
||||
Some("Copy Link Text Selected"),
|
||||
Some("Copy Text Selected"),
|
||||
Some(&format!(
|
||||
"{link_context_group_id}.{}",
|
||||
action_link_copy_text_selected.name()
|
||||
|
|
@ -244,6 +308,7 @@ impl Markdown {
|
|||
|
||||
// Init shared reference container for HashTable collected
|
||||
let links = Rc::new(links);
|
||||
let headers = Rc::new(headers);
|
||||
|
||||
// Init events
|
||||
primary_button_controller.connect_released({
|
||||
|
|
@ -274,6 +339,7 @@ impl Markdown {
|
|||
|
||||
secondary_button_controller.connect_pressed({
|
||||
let links = links.clone();
|
||||
let headers = headers.clone();
|
||||
let text_view = text_view.clone();
|
||||
let link_context = link_context.clone();
|
||||
move |_, _, window_x, window_y| {
|
||||
|
|
@ -288,16 +354,18 @@ impl Markdown {
|
|||
if let Some(uri) = links.get(&tag) {
|
||||
let request_str = uri.to_str();
|
||||
let request_var = request_str.to_variant();
|
||||
let is_prefix_link = is_prefix_link(&request_str);
|
||||
|
||||
// Open in the new tab
|
||||
action_link_tab.set_state(&request_var);
|
||||
action_link_copy_text.set_enabled(!request_str.is_empty());
|
||||
action_link_tab.set_enabled(!request_str.is_empty());
|
||||
|
||||
// Copy link to the clipboard
|
||||
action_link_copy_url.set_state(&request_var);
|
||||
action_link_copy_text.set_enabled(!request_str.is_empty());
|
||||
action_link_copy_url.set_enabled(!request_str.is_empty());
|
||||
|
||||
// Copy link text
|
||||
{
|
||||
// Copy link text
|
||||
let mut start_iter = iter;
|
||||
let mut end_iter = iter;
|
||||
if !start_iter.starts_tag(Some(&tag)) {
|
||||
|
|
@ -318,33 +386,64 @@ impl Markdown {
|
|||
}
|
||||
|
||||
// Copy link text (if) selected
|
||||
if let Some((sel_start, sel_end)) = buffer.selection_bounds() {
|
||||
let selected_tag_text = buffer.text(&sel_start, &sel_end, false);
|
||||
action_link_copy_text_selected
|
||||
.set_state(&selected_tag_text.to_variant());
|
||||
action_link_copy_text_selected
|
||||
.set_enabled(!selected_tag_text.is_empty());
|
||||
} else {
|
||||
action_link_copy_text_selected.set_enabled(false);
|
||||
}
|
||||
action_link_copy_text_selected.set_enabled(
|
||||
if let Some((start, end)) = buffer.selection_bounds() {
|
||||
let selected = buffer.text(&start, &end, false);
|
||||
action_link_copy_text_selected
|
||||
.set_state(&selected.to_variant());
|
||||
!selected.is_empty()
|
||||
} else {
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
// Bookmark
|
||||
action_link_bookmark.set_state(&request_var);
|
||||
action_link_bookmark.set_enabled(is_prefixable_link(&request_str));
|
||||
action_link_bookmark.set_enabled(is_prefix_link);
|
||||
|
||||
// Download (new tab)
|
||||
action_link_download.set_state(&request_var);
|
||||
action_link_download.set_enabled(is_prefixable_link(&request_str));
|
||||
action_link_download.set_enabled(is_prefix_link);
|
||||
|
||||
// View as Source (new tab)
|
||||
action_link_source.set_state(&request_var);
|
||||
action_link_source.set_enabled(is_prefixable_link(&request_str));
|
||||
action_link_source.set_enabled(is_prefix_link);
|
||||
|
||||
// Toggle
|
||||
link_context
|
||||
.set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1)));
|
||||
link_context.popup()
|
||||
}
|
||||
// Tag is header
|
||||
if let Some((title, uri)) = headers.get(&tag) {
|
||||
let request_str = uri.to_str();
|
||||
let request_var = request_str.to_variant();
|
||||
|
||||
// Copy link to the clipboard
|
||||
action_header_copy_url.set_state(&request_var);
|
||||
action_header_copy_url.set_enabled(!request_str.is_empty());
|
||||
|
||||
// Copy header text
|
||||
action_header_copy_text.set_state(&title.to_variant());
|
||||
action_header_copy_text.set_enabled(!title.is_empty());
|
||||
|
||||
// Copy header text (if) selected
|
||||
action_header_copy_text_selected.set_enabled(
|
||||
if let Some((start, end)) = buffer.selection_bounds() {
|
||||
let selected = buffer.text(&start, &end, false);
|
||||
action_header_copy_text_selected
|
||||
.set_state(&selected.to_variant());
|
||||
!selected.is_empty()
|
||||
} else {
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
// Toggle
|
||||
header_context
|
||||
.set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1)));
|
||||
header_context.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -465,7 +564,7 @@ fn is_internal_link(request: &str) -> bool {
|
|||
|| request.starts_with("source:")
|
||||
}
|
||||
|
||||
fn is_prefixable_link(request: &str) -> bool {
|
||||
fn is_prefix_link(request: &str) -> bool {
|
||||
request.starts_with("gemini://")
|
||||
|| request.starts_with("nex://")
|
||||
|| request.starts_with("file://")
|
||||
|
|
|
|||
|
|
@ -53,13 +53,14 @@ impl Tags {
|
|||
base: &Uri,
|
||||
link_color: &RGBA,
|
||||
links: &mut HashMap<TextTag, Uri>,
|
||||
headers: &mut HashMap<TextTag, (String, Uri)>,
|
||||
) -> Option<String> {
|
||||
// Collect all code blocks first,
|
||||
// and temporarily replace them with placeholder ID
|
||||
self.code.collect(buffer);
|
||||
|
||||
// Keep in order!
|
||||
let title = self.header.render(buffer);
|
||||
let title = self.header.render(buffer, base, headers);
|
||||
|
||||
list::render(buffer);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use gtk::{
|
||||
TextBuffer, TextTag, WrapMode,
|
||||
glib::Uri,
|
||||
prelude::{TextBufferExt, TextBufferExtManual},
|
||||
};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const REGEX_HEADER: &str = r"(?m)^(?P<level>#{1,6})\s+(?P<title>.*)$";
|
||||
|
||||
|
|
@ -71,7 +73,12 @@ impl Header {
|
|||
}
|
||||
|
||||
/// Apply title `Tag` to given `TextBuffer`
|
||||
pub fn render(&self, buffer: &TextBuffer) -> Option<String> {
|
||||
pub fn render(
|
||||
&self,
|
||||
buffer: &TextBuffer,
|
||||
base: &Uri,
|
||||
headers: &mut HashMap<TextTag, (String, Uri)>,
|
||||
) -> Option<String> {
|
||||
let mut raw_title = None;
|
||||
|
||||
let table = buffer.tag_table();
|
||||
|
|
@ -105,6 +112,7 @@ impl Header {
|
|||
let mut start_iter = buffer.iter_at_offset(start_char_offset);
|
||||
let mut end_iter = buffer.iter_at_offset(end_char_offset);
|
||||
|
||||
// Skip escaped entries
|
||||
if start_char_offset > 0
|
||||
&& buffer
|
||||
.text(
|
||||
|
|
@ -117,19 +125,50 @@ impl Header {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Create unique phantom tag for each header
|
||||
// * it is required for context menu relationships
|
||||
let h = TextTag::builder().build();
|
||||
assert!(table.add(&h));
|
||||
|
||||
// Render header in text buffer
|
||||
buffer.delete(&mut start_iter, &mut end_iter);
|
||||
|
||||
match cap["level"].chars().count() {
|
||||
1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h1]),
|
||||
2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h2]),
|
||||
3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h3]),
|
||||
4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h4]),
|
||||
5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h5]),
|
||||
6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h6]),
|
||||
_ => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[]),
|
||||
1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h1]),
|
||||
2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h2]),
|
||||
3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h3]),
|
||||
4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h4]),
|
||||
5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h5]),
|
||||
6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h6]),
|
||||
_ => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h]), // unexpected
|
||||
}
|
||||
}
|
||||
|
||||
// Register fragment reference
|
||||
assert!(
|
||||
headers
|
||||
.insert(
|
||||
h,
|
||||
(
|
||||
cap["title"].into(),
|
||||
Uri::build(
|
||||
base.flags(),
|
||||
&base.scheme(),
|
||||
base.userinfo().as_deref(),
|
||||
base.host().as_deref(),
|
||||
base.port(),
|
||||
&base.path(),
|
||||
base.query().as_deref(),
|
||||
Some(&Uri::escape_string(
|
||||
&cap["title"].to_lowercase().replace(" ", "-"),
|
||||
None,
|
||||
true
|
||||
)),
|
||||
)
|
||||
),
|
||||
)
|
||||
.is_none()
|
||||
)
|
||||
}
|
||||
raw_title
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue