mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-03-31 16:45:27 +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 super::{ItemAction, WindowAction};
|
||||||
use crate::{app::browser::window::action::Position, profile::Profile};
|
use crate::{app::browser::window::action::Position, profile::Profile};
|
||||||
use gtk::{
|
use gtk::{
|
||||||
EventControllerMotion, GestureClick, TextBuffer, TextSearchFlags, TextTag, TextTagTable,
|
EventControllerMotion, GestureClick, PopoverMenu, TextBuffer, TextSearchFlags, TextTag,
|
||||||
TextView, TextWindowType, UriLauncher, Window, WrapMode,
|
TextTagTable, TextView, TextWindowType, UriLauncher, Window, WrapMode,
|
||||||
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA},
|
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA},
|
||||||
gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup},
|
gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup},
|
||||||
glib::{ControlFlow, GString, Uri, idle_add_local, uri_unescape_string, uuid_string_random},
|
glib::{ControlFlow, GString, Uri, idle_add_local, uri_unescape_string, uuid_string_random},
|
||||||
|
|
@ -33,6 +33,7 @@ impl Markdown {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Init HashMap storage (for event controllers)
|
// Init HashMap storage (for event controllers)
|
||||||
let mut links: HashMap<TextTag, Uri> = HashMap::new();
|
let mut links: HashMap<TextTag, Uri> = HashMap::new();
|
||||||
|
let mut headers: HashMap<TextTag, (String, Uri)> = HashMap::new();
|
||||||
|
|
||||||
// Init hovered tag storage for `links`
|
// Init hovered tag storage for `links`
|
||||||
// * maybe less expensive than update entire HashMap by iter
|
// * maybe less expensive than update entire HashMap by iter
|
||||||
|
|
@ -75,9 +76,72 @@ impl Markdown {
|
||||||
let gutter = Gutter::build(&text_view);
|
let gutter = Gutter::build(&text_view);
|
||||||
|
|
||||||
// Render markdown tags
|
// 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 =
|
let action_link_tab =
|
||||||
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
action_link_tab.connect_activate({
|
action_link_tab.connect_activate({
|
||||||
|
|
@ -165,7 +229,7 @@ impl Markdown {
|
||||||
g
|
g
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let link_context = gtk::PopoverMenu::from_model(Some(&{
|
let link_context = PopoverMenu::from_model(Some(&{
|
||||||
let m = Menu::new();
|
let m = Menu::new();
|
||||||
m.append(
|
m.append(
|
||||||
Some("Open Link in New Tab"),
|
Some("Open Link in New Tab"),
|
||||||
|
|
@ -191,7 +255,7 @@ impl Markdown {
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
m_copy.append(
|
m_copy.append(
|
||||||
Some("Copy Link Text Selected"),
|
Some("Copy Text Selected"),
|
||||||
Some(&format!(
|
Some(&format!(
|
||||||
"{link_context_group_id}.{}",
|
"{link_context_group_id}.{}",
|
||||||
action_link_copy_text_selected.name()
|
action_link_copy_text_selected.name()
|
||||||
|
|
@ -244,6 +308,7 @@ impl Markdown {
|
||||||
|
|
||||||
// Init shared reference container for HashTable collected
|
// Init shared reference container for HashTable collected
|
||||||
let links = Rc::new(links);
|
let links = Rc::new(links);
|
||||||
|
let headers = Rc::new(headers);
|
||||||
|
|
||||||
// Init events
|
// Init events
|
||||||
primary_button_controller.connect_released({
|
primary_button_controller.connect_released({
|
||||||
|
|
@ -274,6 +339,7 @@ impl Markdown {
|
||||||
|
|
||||||
secondary_button_controller.connect_pressed({
|
secondary_button_controller.connect_pressed({
|
||||||
let links = links.clone();
|
let links = links.clone();
|
||||||
|
let headers = headers.clone();
|
||||||
let text_view = text_view.clone();
|
let text_view = text_view.clone();
|
||||||
let link_context = link_context.clone();
|
let link_context = link_context.clone();
|
||||||
move |_, _, window_x, window_y| {
|
move |_, _, window_x, window_y| {
|
||||||
|
|
@ -288,16 +354,18 @@ impl Markdown {
|
||||||
if let Some(uri) = links.get(&tag) {
|
if let Some(uri) = links.get(&tag) {
|
||||||
let request_str = uri.to_str();
|
let request_str = uri.to_str();
|
||||||
let request_var = request_str.to_variant();
|
let request_var = request_str.to_variant();
|
||||||
|
let is_prefix_link = is_prefix_link(&request_str);
|
||||||
|
|
||||||
// Open in the new tab
|
// Open in the new tab
|
||||||
action_link_tab.set_state(&request_var);
|
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_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 start_iter = iter;
|
||||||
let mut end_iter = iter;
|
let mut end_iter = iter;
|
||||||
if !start_iter.starts_tag(Some(&tag)) {
|
if !start_iter.starts_tag(Some(&tag)) {
|
||||||
|
|
@ -318,33 +386,64 @@ impl Markdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy link text (if) selected
|
// Copy link text (if) selected
|
||||||
if let Some((sel_start, sel_end)) = buffer.selection_bounds() {
|
action_link_copy_text_selected.set_enabled(
|
||||||
let selected_tag_text = buffer.text(&sel_start, &sel_end, false);
|
if let Some((start, end)) = buffer.selection_bounds() {
|
||||||
|
let selected = buffer.text(&start, &end, false);
|
||||||
action_link_copy_text_selected
|
action_link_copy_text_selected
|
||||||
.set_state(&selected_tag_text.to_variant());
|
.set_state(&selected.to_variant());
|
||||||
action_link_copy_text_selected
|
!selected.is_empty()
|
||||||
.set_enabled(!selected_tag_text.is_empty());
|
|
||||||
} else {
|
} else {
|
||||||
action_link_copy_text_selected.set_enabled(false);
|
false
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Bookmark
|
// Bookmark
|
||||||
action_link_bookmark.set_state(&request_var);
|
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)
|
// Download (new tab)
|
||||||
action_link_download.set_state(&request_var);
|
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)
|
// View as Source (new tab)
|
||||||
action_link_source.set_state(&request_var);
|
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
|
// Toggle
|
||||||
link_context
|
link_context
|
||||||
.set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1)));
|
.set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1)));
|
||||||
link_context.popup()
|
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:")
|
|| 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("gemini://")
|
||||||
|| request.starts_with("nex://")
|
|| request.starts_with("nex://")
|
||||||
|| request.starts_with("file://")
|
|| request.starts_with("file://")
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,14 @@ impl Tags {
|
||||||
base: &Uri,
|
base: &Uri,
|
||||||
link_color: &RGBA,
|
link_color: &RGBA,
|
||||||
links: &mut HashMap<TextTag, Uri>,
|
links: &mut HashMap<TextTag, Uri>,
|
||||||
|
headers: &mut HashMap<TextTag, (String, Uri)>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
// Collect all code blocks first,
|
// Collect all code blocks first,
|
||||||
// and temporarily replace them with placeholder ID
|
// and temporarily replace them with placeholder ID
|
||||||
self.code.collect(buffer);
|
self.code.collect(buffer);
|
||||||
|
|
||||||
// Keep in order!
|
// Keep in order!
|
||||||
let title = self.header.render(buffer);
|
let title = self.header.render(buffer, base, headers);
|
||||||
|
|
||||||
list::render(buffer);
|
list::render(buffer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use gtk::{
|
use gtk::{
|
||||||
TextBuffer, TextTag, WrapMode,
|
TextBuffer, TextTag, WrapMode,
|
||||||
|
glib::Uri,
|
||||||
prelude::{TextBufferExt, TextBufferExtManual},
|
prelude::{TextBufferExt, TextBufferExtManual},
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
const REGEX_HEADER: &str = r"(?m)^(?P<level>#{1,6})\s+(?P<title>.*)$";
|
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`
|
/// 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 mut raw_title = None;
|
||||||
|
|
||||||
let table = buffer.tag_table();
|
let table = buffer.tag_table();
|
||||||
|
|
@ -105,6 +112,7 @@ impl Header {
|
||||||
let mut start_iter = buffer.iter_at_offset(start_char_offset);
|
let mut start_iter = buffer.iter_at_offset(start_char_offset);
|
||||||
let mut end_iter = buffer.iter_at_offset(end_char_offset);
|
let mut end_iter = buffer.iter_at_offset(end_char_offset);
|
||||||
|
|
||||||
|
// Skip escaped entries
|
||||||
if start_char_offset > 0
|
if start_char_offset > 0
|
||||||
&& buffer
|
&& buffer
|
||||||
.text(
|
.text(
|
||||||
|
|
@ -117,19 +125,50 @@ impl Header {
|
||||||
continue;
|
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);
|
buffer.delete(&mut start_iter, &mut end_iter);
|
||||||
|
|
||||||
match cap["level"].chars().count() {
|
match cap["level"].chars().count() {
|
||||||
1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h1]),
|
1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h1]),
|
||||||
2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h2]),
|
2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h2]),
|
||||||
3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h3]),
|
3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h3]),
|
||||||
4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h4]),
|
4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h4]),
|
||||||
5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h5]),
|
5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h5]),
|
||||||
6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h6]),
|
6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h6]),
|
||||||
_ => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[]),
|
_ => 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
|
raw_title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue