@@ -5,23 +5,23 @@ mod icon;
55mod syntax;
66mod tag;
77
8- pub use error:: Error ;
9- use gutter:: Gutter ;
10- use icon:: Icon ;
11- use syntax:: Syntax ;
12- use tag:: Tag ;
13-
148use super :: { ItemAction , WindowAction } ;
159use crate :: app:: browser:: window:: action:: Position ;
10+ pub use error:: Error ;
1611use gtk:: {
1712 EventControllerMotion , GestureClick , TextBuffer , TextTag , TextView , TextWindowType ,
1813 UriLauncher , Window , WrapMode ,
19- gdk:: { BUTTON_MIDDLE , BUTTON_PRIMARY , RGBA } ,
20- gio:: Cancellable ,
21- glib:: Uri ,
22- prelude:: { TextBufferExt , TextBufferExtManual , TextTagExt , TextViewExt , WidgetExt } ,
14+ gdk:: { BUTTON_MIDDLE , BUTTON_PRIMARY , BUTTON_SECONDARY , RGBA } ,
15+ gio:: { Cancellable , SimpleAction , SimpleActionGroup } ,
16+ glib:: { Uri , uuid_string_random } ,
17+ prelude:: { PopoverExt , TextBufferExt , TextBufferExtManual , TextTagExt , TextViewExt , WidgetExt } ,
2318} ;
19+ use gutter:: Gutter ;
20+ use icon:: Icon ;
21+ use sourceview:: prelude:: { ActionExt , ActionMapExt , DisplayExt , ToVariant } ;
2422use std:: { cell:: Cell , collections:: HashMap , rc:: Rc } ;
23+ use syntax:: Syntax ;
24+ use tag:: Tag ;
2525
2626pub const NEW_LINE : & str = "\n " ;
2727
@@ -284,14 +284,113 @@ impl Gemini {
284284 buffer. insert ( & mut buffer. end_iter ( ) , NEW_LINE ) ;
285285 }
286286
287+ // Context menu
288+ let action_link_tab =
289+ SimpleAction :: new_stateful ( & uuid_string_random ( ) , None , & String :: new ( ) . to_variant ( ) ) ;
290+ action_link_tab. connect_activate ( {
291+ let window_action = window_action. clone ( ) ;
292+ move |this, _| {
293+ open_link_in_new_tab (
294+ & this. state ( ) . unwrap ( ) . get :: < String > ( ) . unwrap ( ) ,
295+ & window_action,
296+ )
297+ }
298+ } ) ;
299+ let action_link_copy =
300+ SimpleAction :: new_stateful ( & uuid_string_random ( ) , None , & String :: new ( ) . to_variant ( ) ) ;
301+ action_link_copy. connect_activate ( |this, _| {
302+ gtk:: gdk:: Display :: default ( )
303+ . unwrap ( )
304+ . clipboard ( )
305+ . set_text ( & this. state ( ) . unwrap ( ) . get :: < String > ( ) . unwrap ( ) )
306+ } ) ;
307+ let action_link_download =
308+ SimpleAction :: new_stateful ( & uuid_string_random ( ) , None , & String :: new ( ) . to_variant ( ) ) ;
309+ action_link_download. connect_activate ( {
310+ let window_action = window_action. clone ( ) ;
311+ move |this, _| {
312+ open_link_in_new_tab (
313+ & link_prefix (
314+ this. state ( ) . unwrap ( ) . get :: < String > ( ) . unwrap ( ) ,
315+ LINK_PREFIX_DOWNLOAD ,
316+ ) ,
317+ & window_action,
318+ )
319+ }
320+ } ) ;
321+ let action_link_source =
322+ SimpleAction :: new_stateful ( & uuid_string_random ( ) , None , & String :: new ( ) . to_variant ( ) ) ;
323+ action_link_source. connect_activate ( {
324+ let window_action = window_action. clone ( ) ;
325+ move |this, _| {
326+ open_link_in_new_tab (
327+ & link_prefix (
328+ this. state ( ) . unwrap ( ) . get :: < String > ( ) . unwrap ( ) ,
329+ LINK_PREFIX_SOURCE ,
330+ ) ,
331+ & window_action,
332+ )
333+ }
334+ } ) ;
335+ let link_context_group_id = uuid_string_random ( ) ;
336+ text_view. insert_action_group (
337+ & link_context_group_id,
338+ Some ( & {
339+ let g = SimpleActionGroup :: new ( ) ;
340+ g. add_action ( & action_link_tab) ;
341+ g. add_action ( & action_link_copy) ;
342+ g. add_action ( & action_link_download) ;
343+ g. add_action ( & action_link_source) ;
344+ g
345+ } ) ,
346+ ) ;
347+ let link_context = gtk:: PopoverMenu :: from_model ( Some ( & {
348+ let m = gtk:: gio:: Menu :: new ( ) ;
349+ m. append (
350+ Some ( "Open Link in New Tab" ) ,
351+ Some ( & format ! (
352+ "{link_context_group_id}.{}" ,
353+ action_link_tab. name( )
354+ ) ) ,
355+ ) ;
356+ m. append (
357+ Some ( "Copy Link" ) ,
358+ Some ( & format ! (
359+ "{link_context_group_id}.{}" ,
360+ action_link_copy. name( )
361+ ) ) ,
362+ ) ;
363+ m. append (
364+ Some ( "Download Link" ) ,
365+ Some ( & format ! (
366+ "{link_context_group_id}.{}" ,
367+ action_link_download. name( )
368+ ) ) ,
369+ ) ;
370+ m. append (
371+ Some ( "View Link as Source" ) ,
372+ Some ( & format ! (
373+ "{link_context_group_id}.{}" ,
374+ action_link_source. name( )
375+ ) ) ,
376+ ) ;
377+ m
378+ } ) ) ;
379+ link_context. set_parent ( & text_view) ;
380+
287381 // Init additional controllers
288- let primary_button_controller = GestureClick :: builder ( ) . button ( BUTTON_PRIMARY ) . build ( ) ;
289382 let middle_button_controller = GestureClick :: builder ( ) . button ( BUTTON_MIDDLE ) . build ( ) ;
383+ let primary_button_controller = GestureClick :: builder ( ) . button ( BUTTON_PRIMARY ) . build ( ) ;
384+ let secondary_button_controller = GestureClick :: builder ( )
385+ . button ( BUTTON_SECONDARY )
386+ . propagation_phase ( gtk:: PropagationPhase :: Capture )
387+ . build ( ) ;
290388 let motion_controller = EventControllerMotion :: new ( ) ;
291389
292- text_view. add_controller ( primary_button_controller. clone ( ) ) ;
293390 text_view. add_controller ( middle_button_controller. clone ( ) ) ;
294391 text_view. add_controller ( motion_controller. clone ( ) ) ;
392+ text_view. add_controller ( primary_button_controller. clone ( ) ) ;
393+ text_view. add_controller ( secondary_button_controller. clone ( ) ) ;
295394
296395 // Init shared reference container for HashTable collected
297396 let links = Rc :: new ( links) ;
@@ -308,27 +407,46 @@ impl Gemini {
308407 window_x as i32 ,
309408 window_y as i32 ,
310409 ) ;
410+ if let Some ( iter) = text_view. iter_at_location ( buffer_x, buffer_y) {
411+ for tag in iter. tags ( ) {
412+ // Tag is link
413+ if let Some ( uri) = links. get ( & tag) {
414+ return open_link_in_current_tab ( & uri. to_string ( ) , & item_action) ;
415+ }
416+ }
417+ }
418+ }
419+ } ) ;
311420
421+ secondary_button_controller. connect_pressed ( {
422+ let links = links. clone ( ) ;
423+ let text_view = text_view. clone ( ) ;
424+ let link_context = link_context. clone ( ) ;
425+ move |_, _, window_x, window_y| {
426+ let x = window_x as i32 ;
427+ let y = window_y as i32 ;
428+ // Detect tag match current coords hovered
429+ let ( buffer_x, buffer_y) =
430+ text_view. window_to_buffer_coords ( TextWindowType :: Widget , x, y) ;
312431 if let Some ( iter) = text_view. iter_at_location ( buffer_x, buffer_y) {
313432 for tag in iter. tags ( ) {
314433 // Tag is link
315434 if let Some ( uri) = links. get ( & tag) {
316- // Select link handler by scheme
317- return match uri. scheme ( ) . as_str ( ) {
318- "gemini" | "titan" | "nex" | "file" => {
319- item_action. load . activate ( Some ( & uri. to_str ( ) ) , true , false )
320- }
321- // Scheme not supported, delegate
322- _ => UriLauncher :: new ( & uri. to_str ( ) ) . launch (
323- Window :: NONE ,
324- Cancellable :: NONE ,
325- |result| {
326- if let Err ( e) = result {
327- println ! ( "{e}" )
328- }
329- } ,
330- ) ,
331- } ; // @TODO common handler?
435+ let request_str = uri. to_str ( ) ;
436+ let request_var = request_str. to_variant ( ) ;
437+
438+ action_link_tab. set_state ( & request_var) ;
439+ action_link_copy. set_state ( & request_var) ;
440+
441+ action_link_download. set_state ( & request_var) ;
442+ action_link_download. set_enabled ( is_prefixable_link ( & request_str) ) ;
443+
444+ action_link_source. set_state ( & request_var) ;
445+ action_link_source. set_enabled ( is_prefixable_link ( & request_str) ) ;
446+
447+ link_context
448+ . set_pointing_to ( Some ( & gtk:: gdk:: Rectangle :: new ( x, y, 1 , 1 ) ) ) ;
449+ link_context. popup ( ) ;
332450 }
333451 }
334452 }
@@ -350,30 +468,7 @@ impl Gemini {
350468 for tag in iter. tags ( ) {
351469 // Tag is link
352470 if let Some ( uri) = links. get ( & tag) {
353- // Select link handler by scheme
354- return match uri. scheme ( ) . as_str ( ) {
355- "gemini" | "titan" | "nex" | "file" => {
356- // Open new page in browser
357- window_action. append . activate_stateful_once (
358- Position :: After ,
359- Some ( uri. to_string ( ) ) ,
360- false ,
361- false ,
362- true ,
363- true ,
364- ) ;
365- }
366- // Scheme not supported, delegate
367- _ => UriLauncher :: new ( & uri. to_str ( ) ) . launch (
368- Window :: NONE ,
369- Cancellable :: NONE ,
370- |result| {
371- if let Err ( e) = result {
372- println ! ( "{e}" )
373- }
374- } ,
375- ) ,
376- } ; // @TODO common handler?
471+ return open_link_in_new_tab ( & uri. to_string ( ) , & window_action) ;
377472 }
378473 }
379474 }
@@ -432,3 +527,58 @@ impl Gemini {
432527 }
433528 }
434529}
530+
531+ fn is_internal_link ( request : & str ) -> bool {
532+ // schemes
533+ request. starts_with ( "gemini://" )
534+ || request. starts_with ( "titan://" )
535+ || request. starts_with ( "nex://" )
536+ || request. starts_with ( "file://" )
537+ // prefix
538+ || request. starts_with ( "download:" )
539+ || request. starts_with ( "source:" )
540+ }
541+
542+ fn is_prefixable_link ( request : & str ) -> bool {
543+ request. starts_with ( "gemini://" )
544+ || request. starts_with ( "nex://" )
545+ || request. starts_with ( "file://" )
546+ }
547+
548+ fn open_link_in_external_app ( request : & str ) {
549+ UriLauncher :: new ( request) . launch ( Window :: NONE , Cancellable :: NONE , |r| {
550+ if let Err ( e) = r {
551+ println ! ( "{e}" ) // @TODO use warn macro
552+ }
553+ } )
554+ }
555+
556+ fn open_link_in_current_tab ( request : & str , item_action : & ItemAction ) {
557+ if is_internal_link ( request) {
558+ item_action. load . activate ( Some ( request) , true , false )
559+ } else {
560+ open_link_in_external_app ( request)
561+ }
562+ }
563+
564+ fn open_link_in_new_tab ( request : & str , window_action : & WindowAction ) {
565+ if is_internal_link ( request) {
566+ window_action. append . activate_stateful_once (
567+ Position :: After ,
568+ Some ( request. into ( ) ) ,
569+ false ,
570+ false ,
571+ true ,
572+ true ,
573+ ) ;
574+ } else {
575+ open_link_in_external_app ( request)
576+ }
577+ }
578+
579+ fn link_prefix ( request : String , prefix : & str ) -> String {
580+ format ! ( "{prefix}{}" , request. trim_start_matches( prefix) )
581+ }
582+
583+ const LINK_PREFIX_DOWNLOAD : & str = "download:" ;
584+ const LINK_PREFIX_SOURCE : & str = "source:" ;
0 commit comments