mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-03-31 16:45:27 +00:00
implement context menu for the gemtext viewer link tags
This commit is contained in:
parent
0357edccfe
commit
1d6cfb88ef
1 changed files with 203 additions and 53 deletions
|
|
@ -5,23 +5,23 @@ mod icon;
|
||||||
mod syntax;
|
mod syntax;
|
||||||
mod tag;
|
mod tag;
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
use gutter::Gutter;
|
|
||||||
use icon::Icon;
|
|
||||||
use syntax::Syntax;
|
|
||||||
use tag::Tag;
|
|
||||||
|
|
||||||
use super::{ItemAction, WindowAction};
|
use super::{ItemAction, WindowAction};
|
||||||
use crate::app::browser::window::action::Position;
|
use crate::app::browser::window::action::Position;
|
||||||
|
pub use error::Error;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
EventControllerMotion, GestureClick, TextBuffer, TextTag, TextView, TextWindowType,
|
EventControllerMotion, GestureClick, TextBuffer, TextTag, TextView, TextWindowType,
|
||||||
UriLauncher, Window, WrapMode,
|
UriLauncher, Window, WrapMode,
|
||||||
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, RGBA},
|
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, RGBA},
|
||||||
gio::Cancellable,
|
gio::{Cancellable, SimpleAction, SimpleActionGroup},
|
||||||
glib::Uri,
|
glib::{Uri, uuid_string_random},
|
||||||
prelude::{TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt},
|
prelude::{PopoverExt, TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt},
|
||||||
};
|
};
|
||||||
|
use gutter::Gutter;
|
||||||
|
use icon::Icon;
|
||||||
|
use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant};
|
||||||
use std::{cell::Cell, collections::HashMap, rc::Rc};
|
use std::{cell::Cell, collections::HashMap, rc::Rc};
|
||||||
|
use syntax::Syntax;
|
||||||
|
use tag::Tag;
|
||||||
|
|
||||||
pub const NEW_LINE: &str = "\n";
|
pub const NEW_LINE: &str = "\n";
|
||||||
|
|
||||||
|
|
@ -284,14 +284,113 @@ impl Gemini {
|
||||||
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
let action_link_tab =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_tab.connect_activate({
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |this, _| {
|
||||||
|
open_link_in_new_tab(
|
||||||
|
&this.state().unwrap().get::<String>().unwrap(),
|
||||||
|
&window_action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let action_link_copy =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_copy.connect_activate(|this, _| {
|
||||||
|
gtk::gdk::Display::default()
|
||||||
|
.unwrap()
|
||||||
|
.clipboard()
|
||||||
|
.set_text(&this.state().unwrap().get::<String>().unwrap())
|
||||||
|
});
|
||||||
|
let action_link_download =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_download.connect_activate({
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |this, _| {
|
||||||
|
open_link_in_new_tab(
|
||||||
|
&link_prefix(
|
||||||
|
this.state().unwrap().get::<String>().unwrap(),
|
||||||
|
LINK_PREFIX_DOWNLOAD,
|
||||||
|
),
|
||||||
|
&window_action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let action_link_source =
|
||||||
|
SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant());
|
||||||
|
action_link_source.connect_activate({
|
||||||
|
let window_action = window_action.clone();
|
||||||
|
move |this, _| {
|
||||||
|
open_link_in_new_tab(
|
||||||
|
&link_prefix(
|
||||||
|
this.state().unwrap().get::<String>().unwrap(),
|
||||||
|
LINK_PREFIX_SOURCE,
|
||||||
|
),
|
||||||
|
&window_action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let link_context_group_id = uuid_string_random();
|
||||||
|
text_view.insert_action_group(
|
||||||
|
&link_context_group_id,
|
||||||
|
Some(&{
|
||||||
|
let g = SimpleActionGroup::new();
|
||||||
|
g.add_action(&action_link_tab);
|
||||||
|
g.add_action(&action_link_copy);
|
||||||
|
g.add_action(&action_link_download);
|
||||||
|
g.add_action(&action_link_source);
|
||||||
|
g
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let link_context = gtk::PopoverMenu::from_model(Some(&{
|
||||||
|
let m = gtk::gio::Menu::new();
|
||||||
|
m.append(
|
||||||
|
Some("Open Link in New Tab"),
|
||||||
|
Some(&format!(
|
||||||
|
"{link_context_group_id}.{}",
|
||||||
|
action_link_tab.name()
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
m.append(
|
||||||
|
Some("Copy Link"),
|
||||||
|
Some(&format!(
|
||||||
|
"{link_context_group_id}.{}",
|
||||||
|
action_link_copy.name()
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
m.append(
|
||||||
|
Some("Download Link"),
|
||||||
|
Some(&format!(
|
||||||
|
"{link_context_group_id}.{}",
|
||||||
|
action_link_download.name()
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
m.append(
|
||||||
|
Some("View Link as Source"),
|
||||||
|
Some(&format!(
|
||||||
|
"{link_context_group_id}.{}",
|
||||||
|
action_link_source.name()
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
m
|
||||||
|
}));
|
||||||
|
link_context.set_parent(&text_view);
|
||||||
|
|
||||||
// Init additional controllers
|
// Init additional controllers
|
||||||
let primary_button_controller = GestureClick::builder().button(BUTTON_PRIMARY).build();
|
|
||||||
let middle_button_controller = GestureClick::builder().button(BUTTON_MIDDLE).build();
|
let middle_button_controller = GestureClick::builder().button(BUTTON_MIDDLE).build();
|
||||||
|
let primary_button_controller = GestureClick::builder().button(BUTTON_PRIMARY).build();
|
||||||
|
let secondary_button_controller = GestureClick::builder()
|
||||||
|
.button(BUTTON_SECONDARY)
|
||||||
|
.propagation_phase(gtk::PropagationPhase::Capture)
|
||||||
|
.build();
|
||||||
let motion_controller = EventControllerMotion::new();
|
let motion_controller = EventControllerMotion::new();
|
||||||
|
|
||||||
text_view.add_controller(primary_button_controller.clone());
|
|
||||||
text_view.add_controller(middle_button_controller.clone());
|
text_view.add_controller(middle_button_controller.clone());
|
||||||
text_view.add_controller(motion_controller.clone());
|
text_view.add_controller(motion_controller.clone());
|
||||||
|
text_view.add_controller(primary_button_controller.clone());
|
||||||
|
text_view.add_controller(secondary_button_controller.clone());
|
||||||
|
|
||||||
// Init shared reference container for HashTable collected
|
// Init shared reference container for HashTable collected
|
||||||
let links = Rc::new(links);
|
let links = Rc::new(links);
|
||||||
|
|
@ -308,27 +407,46 @@ impl Gemini {
|
||||||
window_x as i32,
|
window_x as i32,
|
||||||
window_y as i32,
|
window_y as i32,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
||||||
for tag in iter.tags() {
|
for tag in iter.tags() {
|
||||||
// Tag is link
|
// Tag is link
|
||||||
if let Some(uri) = links.get(&tag) {
|
if let Some(uri) = links.get(&tag) {
|
||||||
// Select link handler by scheme
|
return open_link_in_current_tab(&uri.to_string(), &item_action);
|
||||||
return match uri.scheme().as_str() {
|
}
|
||||||
"gemini" | "titan" | "nex" | "file" => {
|
}
|
||||||
item_action.load.activate(Some(&uri.to_str()), true, false)
|
}
|
||||||
}
|
}
|
||||||
// Scheme not supported, delegate
|
});
|
||||||
_ => UriLauncher::new(&uri.to_str()).launch(
|
|
||||||
Window::NONE,
|
secondary_button_controller.connect_pressed({
|
||||||
Cancellable::NONE,
|
let links = links.clone();
|
||||||
|result| {
|
let text_view = text_view.clone();
|
||||||
if let Err(e) = result {
|
let link_context = link_context.clone();
|
||||||
println!("{e}")
|
move |_, _, window_x, window_y| {
|
||||||
}
|
let x = window_x as i32;
|
||||||
},
|
let y = window_y as i32;
|
||||||
),
|
// Detect tag match current coords hovered
|
||||||
}; // @TODO common handler?
|
let (buffer_x, buffer_y) =
|
||||||
|
text_view.window_to_buffer_coords(TextWindowType::Widget, x, y);
|
||||||
|
if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) {
|
||||||
|
for tag in iter.tags() {
|
||||||
|
// Tag is link
|
||||||
|
if let Some(uri) = links.get(&tag) {
|
||||||
|
let request_str = uri.to_str();
|
||||||
|
let request_var = request_str.to_variant();
|
||||||
|
|
||||||
|
action_link_tab.set_state(&request_var);
|
||||||
|
action_link_copy.set_state(&request_var);
|
||||||
|
|
||||||
|
action_link_download.set_state(&request_var);
|
||||||
|
action_link_download.set_enabled(is_prefixable_link(&request_str));
|
||||||
|
|
||||||
|
action_link_source.set_state(&request_var);
|
||||||
|
action_link_source.set_enabled(is_prefixable_link(&request_str));
|
||||||
|
|
||||||
|
link_context
|
||||||
|
.set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1)));
|
||||||
|
link_context.popup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -350,30 +468,7 @@ impl Gemini {
|
||||||
for tag in iter.tags() {
|
for tag in iter.tags() {
|
||||||
// Tag is link
|
// Tag is link
|
||||||
if let Some(uri) = links.get(&tag) {
|
if let Some(uri) = links.get(&tag) {
|
||||||
// Select link handler by scheme
|
return open_link_in_new_tab(&uri.to_string(), &window_action);
|
||||||
return match uri.scheme().as_str() {
|
|
||||||
"gemini" | "titan" | "nex" | "file" => {
|
|
||||||
// Open new page in browser
|
|
||||||
window_action.append.activate_stateful_once(
|
|
||||||
Position::After,
|
|
||||||
Some(uri.to_string()),
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Scheme not supported, delegate
|
|
||||||
_ => UriLauncher::new(&uri.to_str()).launch(
|
|
||||||
Window::NONE,
|
|
||||||
Cancellable::NONE,
|
|
||||||
|result| {
|
|
||||||
if let Err(e) = result {
|
|
||||||
println!("{e}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}; // @TODO common handler?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -432,3 +527,58 @@ impl Gemini {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_internal_link(request: &str) -> bool {
|
||||||
|
// schemes
|
||||||
|
request.starts_with("gemini://")
|
||||||
|
|| request.starts_with("titan://")
|
||||||
|
|| request.starts_with("nex://")
|
||||||
|
|| request.starts_with("file://")
|
||||||
|
// prefix
|
||||||
|
|| request.starts_with("download:")
|
||||||
|
|| request.starts_with("source:")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_prefixable_link(request: &str) -> bool {
|
||||||
|
request.starts_with("gemini://")
|
||||||
|
|| request.starts_with("nex://")
|
||||||
|
|| request.starts_with("file://")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_link_in_external_app(request: &str) {
|
||||||
|
UriLauncher::new(request).launch(Window::NONE, Cancellable::NONE, |r| {
|
||||||
|
if let Err(e) = r {
|
||||||
|
println!("{e}") // @TODO use warn macro
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_link_in_current_tab(request: &str, item_action: &ItemAction) {
|
||||||
|
if is_internal_link(request) {
|
||||||
|
item_action.load.activate(Some(request), true, false)
|
||||||
|
} else {
|
||||||
|
open_link_in_external_app(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_link_in_new_tab(request: &str, window_action: &WindowAction) {
|
||||||
|
if is_internal_link(request) {
|
||||||
|
window_action.append.activate_stateful_once(
|
||||||
|
Position::After,
|
||||||
|
Some(request.into()),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
open_link_in_external_app(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_prefix(request: String, prefix: &str) -> String {
|
||||||
|
format!("{prefix}{}", request.trim_start_matches(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINK_PREFIX_DOWNLOAD: &str = "download:";
|
||||||
|
const LINK_PREFIX_SOURCE: &str = "source:";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue