From 8d8a68e83c4443c4e0559dcf4687bab493ec128c Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 24 Jun 2021 12:54:34 +0000 Subject: [PATCH 01/47] Granite.HyperTextView --- demo/GraniteDemo.vala | 2 + demo/Views/HyperTextView.vala | 36 +++++++++ demo/meson.build | 1 + lib/Widgets/HyperTextView.vala | 142 +++++++++++++++++++++++++++++++++ lib/meson.build | 1 + 5 files changed, 182 insertions(+) create mode 100644 demo/Views/HyperTextView.vala create mode 100644 lib/Widgets/HyperTextView.vala diff --git a/demo/GraniteDemo.vala b/demo/GraniteDemo.vala index 6b1b84a12..50b5d8ccc 100644 --- a/demo/GraniteDemo.vala +++ b/demo/GraniteDemo.vala @@ -39,6 +39,7 @@ public class Granite.Demo : Gtk.Application { var date_time_picker_view = new DateTimePickerView (); var dynamic_notebook_view = new DynamicNotebookView (); var form_view = new FormView (); + var hypertext_view = new HyperTextView (); var mode_button_view = new ModeButtonView (); var overlaybar_view = new OverlayBarView (); var seekbar_view = new SeekBarView (); @@ -60,6 +61,7 @@ public class Granite.Demo : Gtk.Application { main_stack.add_titled (date_time_picker_view, "pickers", "Date & Time"); main_stack.add_titled (dynamic_notebook_view, "dynamictab", "DynamicNotebook"); main_stack.add_titled (form_view, "formview", "Forms"); + main_stack.add_titled (hypertext_view, "hypertextview", "Editable Hypertext"); main_stack.add_titled (mode_button_view, "selection_controls", "Selection Controls"); main_stack.add_titled (overlaybar_view, "overlaybar", "OverlayBar"); main_stack.add_titled (seekbar_view, "seekbar", "SeekBar"); diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextView.vala new file mode 100644 index 000000000..82453175c --- /dev/null +++ b/demo/Views/HyperTextView.vala @@ -0,0 +1,36 @@ +/* +* Copyright 2021 elementary, Inc. (https://elementary.io) +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA. +*/ + +public class HyperTextView : Gtk.Grid { + construct { + var hypertext_label = new Granite.HeaderLabel ("Navigatable URLs in Gtk.TextView"); + var hypertext_textview = new Granite.Widgets.HyperTextView (); + hypertext_textview.buffer.text = "elementary OS - https://elementary.io/\nThe fast, open and privacy-respecting replacement for Windows and macOS."; + + margin = 12; + orientation = Gtk.Orientation.VERTICAL; + row_spacing = 3; + halign = Gtk.Align.CENTER; + valign = Gtk.Align.CENTER; + vexpand = true; + add (hypertext_label); + add (hypertext_textview); + show_all (); + } +} \ No newline at end of file diff --git a/demo/meson.build b/demo/meson.build index 043c3c3ff..a90a56f76 100644 --- a/demo/meson.build +++ b/demo/meson.build @@ -12,6 +12,7 @@ executable( 'Views/DialogsView.vala', 'Views/DynamicNotebookView.vala', 'Views/FormView.vala', + 'Views/HyperTextView.vala', 'Views/ModeButtonView.vala', 'Views/OverlayBarView.vala', 'Views/SeekBarView.vala', diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala new file mode 100644 index 000000000..1015e2337 --- /dev/null +++ b/lib/Widgets/HyperTextView.vala @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012–2021 elementary, Inc. + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + + namespace Granite.Widgets { + + /** + * This class enables navigatable URLs in Gtk.TextView + */ + public class HyperTextView : Gtk.TextView { + + private GLib.SList uri_text_tags; + private Regex uri_regex; + + construct { + uri_text_tags = new GLib.SList (); + try { + uri_regex = new Regex ("([^\\s]+:\\/\\/)?[^\\s]{2,}\\.[^\\s]{2,}"); + } catch (GLib.RegexError e) { + critical ("RegexError while constructing URI regex: %s", e.message); + } + + buffer.changed.connect (on_buffer_changed); + button_press_event.connect_after (on_after_button_press_event); + motion_notify_event.connect (on_motion_notify_event); + } + + private void on_buffer_changed () { + uri_text_tags.foreach ((tag) => { + buffer.tag_table.remove (tag); + }); + uri_text_tags = new GLib.SList (); + + GLib.MatchInfo matchInfo; + + var buffer_text = buffer.text; + uri_regex.match (buffer_text, 0, out matchInfo); + + while (matchInfo.matches ()) { + Gtk.TextIter start, end; + int start_pos, end_pos; + string text = matchInfo.fetch(0); + matchInfo.fetch_pos (0, out start_pos, out end_pos); + buffer.get_iter_at_offset(out start, start_pos); + buffer.get_iter_at_offset(out end, end_pos); + + var tag = buffer.create_tag("%i-%i".printf (start_pos, end_pos), "underline", Pango.Underline.SINGLE); + if (!text.contains ("://")) { + text = "http://" + text; + } + tag.set_data("uri", text); + buffer.apply_tag(tag, start, end); + uri_text_tags.append (tag); + + try { + matchInfo.next(); + } catch (GLib.RegexError e) { + warning ("RegexError while scanning for the next URI match: %s", e.message); + } + } + } + + private bool on_after_button_press_event () { + Gtk.TextIter text_iter; + buffer.get_iter_at_mark (out text_iter, buffer.get_insert ()); + + var tags = text_iter.get_tags (); + foreach (var tag in tags) { + if (tag.get_data ("uri") != null) { + var uri = tag.get_data ("uri"); + + try { + GLib.AppInfo.launch_default_for_uri (uri, null); + } catch (GLib.Error e) { + warning ("Unable to open URI '%s': %s", uri, e.message); + + var error_dialog = new Granite.MessageDialog ( + _("Unable to open URI"), + e.message, + new ThemedIcon ("dialog-error"), + Gtk.ButtonsType.CLOSE + ); + error_dialog.run (); + error_dialog.destroy (); + } + break; + } + } + return Gdk.EVENT_PROPAGATE; + } + + private bool was_hovering = false; + + private bool on_motion_notify_event (Gtk.Widget widget, Gdk.EventMotion event) { + var window = get_window (Gtk.TextWindowType.TEXT); + + if (window != null) { + bool is_hovering = false; + + int x, y; + window_to_buffer_coords (Gtk.TextWindowType.WIDGET, (int) event.x, (int) event.y, out x, out y); + + Gtk.TextIter text_iter; + if (get_iter_at_location (out text_iter, x, y)) { + var tags = text_iter.get_tags (); + + foreach (var tag in tags) { + if (tag.get_data ("uri") != null) { + is_hovering = true; + break; + } + } + } + + if (is_hovering && !was_hovering) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); + was_hovering = is_hovering; + + } else if (!is_hovering && was_hovering) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "text"); + was_hovering = is_hovering; + } + } + return Gdk.EVENT_PROPAGATE; + } + } +} diff --git a/lib/meson.build b/lib/meson.build index 0fa300400..5bf96b4dd 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -31,6 +31,7 @@ libgranite_sources = files( 'Widgets/Dialog.vala', 'Widgets/DynamicNotebook.vala', 'Widgets/HeaderLabel.vala', + 'Widgets/HyperTextView.vala', 'Widgets/MessageDialog.vala', 'Widgets/ModeButton.vala', 'Widgets/ModeSwitch.vala', From d8a7f4bf873577b75ec5de9a341881c13e039c82 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 24 Jun 2021 13:21:38 +0000 Subject: [PATCH 02/47] Make Linter Happy --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 1015e2337..9a41e219c 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -34,7 +34,7 @@ } catch (GLib.RegexError e) { critical ("RegexError while constructing URI regex: %s", e.message); } - + buffer.changed.connect (on_buffer_changed); button_press_event.connect_after (on_after_button_press_event); motion_notify_event.connect (on_motion_notify_event); From 52354e9542f9d3e4d576e4910194ecbc0d983a88 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 24 Jun 2021 13:26:49 +0000 Subject: [PATCH 03/47] Lint Me Happy --- lib/Widgets/HyperTextView.vala | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 9a41e219c..51bc6165e 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -46,34 +46,34 @@ }); uri_text_tags = new GLib.SList (); - GLib.MatchInfo matchInfo; + GLib.MatchInfo match_info; - var buffer_text = buffer.text; - uri_regex.match (buffer_text, 0, out matchInfo); + var buffer_text = buffer.text; + uri_regex.match (buffer_text, 0, out match_info); - while (matchInfo.matches ()) { - Gtk.TextIter start, end; - int start_pos, end_pos; - string text = matchInfo.fetch(0); - matchInfo.fetch_pos (0, out start_pos, out end_pos); - buffer.get_iter_at_offset(out start, start_pos); - buffer.get_iter_at_offset(out end, end_pos); + while (match_info.matches ()) { + Gtk.TextIter start, end; + int start_pos, end_pos; + string text = match_info.fetch (0); + match_info.fetch_pos (0, out start_pos, out end_pos); + buffer.get_iter_at_offset (out start, start_pos); + buffer.get_iter_at_offset (out end, end_pos); - var tag = buffer.create_tag("%i-%i".printf (start_pos, end_pos), "underline", Pango.Underline.SINGLE); + var tag = buffer.create_tag ("%i-%i".printf (start_pos, end_pos), "underline", Pango.Underline.SINGLE); if (!text.contains ("://")) { text = "http://" + text; } - tag.set_data("uri", text); - buffer.apply_tag(tag, start, end); + tag.set_data ("uri", text); + buffer.apply_tag (tag, start, end); uri_text_tags.append (tag); - try { - matchInfo.next(); + try { + match_info.next (); } catch (GLib.RegexError e) { warning ("RegexError while scanning for the next URI match: %s", e.message); } - } - } + } + } private bool on_after_button_press_event () { Gtk.TextIter text_iter; From cfb39846970e8593e06880d1a34c241fa6b67efc Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 24 Jun 2021 13:28:46 +0000 Subject: [PATCH 04/47] Eventually the Linter shuts up --- demo/Views/HyperTextView.vala | 2 +- lib/Widgets/HyperTextView.vala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextView.vala index 82453175c..bcfee79e8 100644 --- a/demo/Views/HyperTextView.vala +++ b/demo/Views/HyperTextView.vala @@ -33,4 +33,4 @@ public class HyperTextView : Gtk.Grid { add (hypertext_textview); show_all (); } -} \ No newline at end of file +} diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 51bc6165e..71031a78a 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -78,7 +78,7 @@ private bool on_after_button_press_event () { Gtk.TextIter text_iter; buffer.get_iter_at_mark (out text_iter, buffer.get_insert ()); - + var tags = text_iter.get_tags (); foreach (var tag in tags) { if (tag.get_data ("uri") != null) { @@ -111,7 +111,7 @@ if (window != null) { bool is_hovering = false; - + int x, y; window_to_buffer_coords (Gtk.TextWindowType.WIDGET, (int) event.x, (int) event.y, out x, out y); From d6159c3e8701d2940ba10f728b1c4bd2d2a1e5e8 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Fri, 25 Jun 2021 08:46:02 +0000 Subject: [PATCH 05/47] Ignore parentheses, support local file paths --- lib/Widgets/HyperTextView.vala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 71031a78a..56dea1877 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -30,7 +30,7 @@ construct { uri_text_tags = new GLib.SList (); try { - uri_regex = new Regex ("([^\\s]+:\\/\\/)?[^\\s]{2,}\\.[^\\s]{2,}"); + uri_regex = new Regex ("([^\\s\\.\"'`]+:\\/\\/)?[^\\s\"'`]{2,}\\.[^\\s\"'`]{2,}"); } catch (GLib.RegexError e) { critical ("RegexError while constructing URI regex: %s", e.message); } @@ -61,7 +61,15 @@ var tag = buffer.create_tag ("%i-%i".printf (start_pos, end_pos), "underline", Pango.Underline.SINGLE); if (!text.contains ("://")) { - text = "http://" + text; + if (text[0] == '~') { + text = Environment.get_home_dir () + text.substring (1); + } + + if (text[0] == '/') { + text = "file://" + text; + } else { + text = "http://" + text; + } } tag.set_data ("uri", text); buffer.apply_tag (tag, start, end); From e28dcc5e0439ba19842f0a15defd09f02ee15150 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Fri, 25 Jun 2021 09:21:51 +0000 Subject: [PATCH 06/47] Ignore < and >, improves HTML/XML handling. --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 56dea1877..e6375aeec 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -30,7 +30,7 @@ construct { uri_text_tags = new GLib.SList (); try { - uri_regex = new Regex ("([^\\s\\.\"'`]+:\\/\\/)?[^\\s\"'`]{2,}\\.[^\\s\"'`]{2,}"); + uri_regex = new Regex ("([^\\s\\.\"'`<>]+:\\/\\/)?[^\\s\"'`<>]{2,}\\.[^\\s\"'`<>]{2,}"); } catch (GLib.RegexError e) { critical ("RegexError while constructing URI regex: %s", e.message); } From 06024abf434b0636bd3ce11df14ada1795134b8a Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Fri, 25 Jun 2021 09:39:41 +0000 Subject: [PATCH 07/47] Improved Markdown Support --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index e6375aeec..c759eef97 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -30,7 +30,7 @@ construct { uri_text_tags = new GLib.SList (); try { - uri_regex = new Regex ("([^\\s\\.\"'`<>]+:\\/\\/)?[^\\s\"'`<>]{2,}\\.[^\\s\"'`<>]{2,}"); + uri_regex = new Regex ("([^\\(\\[\\s\\.\"'`<>]+:\\/\\/)?[^\\(\\[\\s\"'`<>]{2,}\\.[^\\)\\]\\s\"'`<>]{2,}"); } catch (GLib.RegexError e) { critical ("RegexError while constructing URI regex: %s", e.message); } From de7f580880092da39dfc8ccd3e45b8a914552a51 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Fri, 25 Jun 2021 13:49:22 +0200 Subject: [PATCH 08/47] Added mailto: prefix check --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index c759eef97..3ae68b083 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -60,7 +60,7 @@ buffer.get_iter_at_offset (out end, end_pos); var tag = buffer.create_tag ("%i-%i".printf (start_pos, end_pos), "underline", Pango.Underline.SINGLE); - if (!text.contains ("://")) { + if (!text.contains ("://") && !text.has_prefix ("mailto:")) { if (text[0] == '~') { text = Environment.get_home_dir () + text.substring (1); } From 4025a136ec26db3dfb7b492249c586b92b85f0cb Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 27 Jun 2021 19:05:21 +0200 Subject: [PATCH 09/47] Improved Performance for Large Texts --- lib/Widgets/HyperTextView.vala | 132 ++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 27 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 3ae68b083..1a072a2cc 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -24,56 +24,134 @@ */ public class HyperTextView : Gtk.TextView { - private GLib.SList uri_text_tags; + private uint buffer_changed_debounce_timeout_id = 0; + private int buffer_cursor_position_when_change_started = 0; + + private GLib.HashTable uri_text_tags; private Regex uri_regex; construct { - uri_text_tags = new GLib.SList (); + uri_text_tags = new GLib.HashTable (str_hash, direct_equal); try { uri_regex = new Regex ("([^\\(\\[\\s\\.\"'`<>]+:\\/\\/)?[^\\(\\[\\s\"'`<>]{2,}\\.[^\\)\\]\\s\"'`<>]{2,}"); } catch (GLib.RegexError e) { critical ("RegexError while constructing URI regex: %s", e.message); } - buffer.changed.connect (on_buffer_changed); + buffer.notify["cursor-position"].connect (on_buffer_cursor_position_changed); + buffer.paste_done.connect (on_paste_done); + buffer.changed.connect_after (on_after_buffer_changed); + button_press_event.connect_after (on_after_button_press_event); motion_notify_event.connect (on_motion_notify_event); } - private void on_buffer_changed () { - uri_text_tags.foreach ((tag) => { - buffer.tag_table.remove (tag); + private void on_buffer_cursor_position_changed () { + if (buffer_cursor_position_when_change_started == 0) { + buffer_cursor_position_when_change_started = buffer.cursor_position; + } + } + + private void on_paste_done (Gtk.Clipboard clipboard) { + // force rescan of whole buffer: + buffer_cursor_position_when_change_started = -1; + } + + private void on_after_buffer_changed () { + if (buffer_changed_debounce_timeout_id != 0) { + Source.remove (buffer_changed_debounce_timeout_id); + buffer_changed_debounce_timeout_id = 0; + } + + buffer_changed_debounce_timeout_id = GLib.Timeout.add (300, () => { + buffer_changed_debounce_timeout_id = 0; + + var change_start_offset = buffer_cursor_position_when_change_started; + var change_end_offset = buffer.cursor_position; + + buffer_cursor_position_when_change_started = 0; + + if (change_start_offset < 0) { + change_start_offset = 0; + change_end_offset = buffer.text.length; + } + + update_tags_in_buffer_for_range.begin ( + int.min (change_start_offset, change_end_offset), + int.max (change_start_offset, change_end_offset) + ); + + return GLib.Source.REMOVE; }); - uri_text_tags = new GLib.SList (); + } - GLib.MatchInfo match_info; + private async void update_tags_in_buffer_for_range (int buffer_start_offset, int buffer_end_offset) { + Gtk.TextIter buffer_start_iter, buffer_end_iter; + buffer.get_iter_at_offset (out buffer_start_iter, buffer_start_offset); + buffer_start_iter.backward_line (); + buffer_start_offset = buffer_start_iter.get_offset (); + + buffer.get_iter_at_offset (out buffer_end_iter, buffer_end_offset); + buffer_end_iter.forward_line (); + buffer_end_offset = buffer_end_iter.get_offset (); + + // Delete all tags in buffer for range [start_iter.offset,end_iter.offset] + lock (uri_text_tags) { + foreach (var tag_key in uri_text_tags.get_keys ()) { + int tag_start_offset, tag_end_offset; + tag_key.scanf ("[%i,%i]", out tag_start_offset, out tag_end_offset); + + if ( + tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset + || + tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset + ) { + buffer.tag_table.remove (uri_text_tags.take (tag_key)); + } + } + } + + var buffer_substring = buffer.text.substring (buffer_start_offset, buffer_end_offset - buffer_start_offset); + if (buffer_substring.strip () == "") { + // if the substring is empty, we do not have anything to do... + return; + } - var buffer_text = buffer.text; - uri_regex.match (buffer_text, 0, out match_info); + // Add new tags in buffer for range [start_iter.offset,end_iter.offset] + GLib.MatchInfo match_info; + uri_regex.match (buffer_substring, 0, out match_info); while (match_info.matches ()) { - Gtk.TextIter start, end; - int start_pos, end_pos; - string text = match_info.fetch (0); - match_info.fetch_pos (0, out start_pos, out end_pos); - buffer.get_iter_at_offset (out start, start_pos); - buffer.get_iter_at_offset (out end, end_pos); - - var tag = buffer.create_tag ("%i-%i".printf (start_pos, end_pos), "underline", Pango.Underline.SINGLE); - if (!text.contains ("://") && !text.has_prefix ("mailto:")) { - if (text[0] == '~') { - text = Environment.get_home_dir () + text.substring (1); + string match_text = match_info.fetch (0); + + int match_start_offset, match_end_offset; + match_info.fetch_pos (0, out match_start_offset, out match_end_offset); + + var buffer_match_start_offset = buffer_start_offset + match_start_offset; + var buffer_match_end_offset = buffer_start_offset + match_end_offset; + + Gtk.TextIter buffer_match_start_iter, buffer_match_end_iter; + buffer.get_iter_at_offset (out buffer_match_start_iter, buffer_match_start_offset); + buffer.get_iter_at_offset (out buffer_match_end_iter, buffer_match_end_offset); + + var tag = buffer.create_tag (null, "underline", Pango.Underline.SINGLE); + if (!match_text.contains ("://") && !match_text.has_prefix ("mailto:")) { + if (match_text[0] == '~') { + match_text = Environment.get_home_dir () + match_text.substring (1); } - if (text[0] == '/') { - text = "file://" + text; + if (match_text[0] == '/') { + match_text = "file://" + match_text; } else { - text = "http://" + text; + match_text = "http://" + match_text; } } - tag.set_data ("uri", text); - buffer.apply_tag (tag, start, end); - uri_text_tags.append (tag); + tag.set_data ("uri", match_text); + buffer.apply_tag (tag, buffer_match_start_iter, buffer_match_end_iter); + + lock (uri_text_tags) { + uri_text_tags.set ("[%i,%i]".printf (buffer_match_start_offset, buffer_match_end_offset), tag); + } try { match_info.next (); From db7a52beaff934e604c50eaf44e89cdfa17da30c Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 27 Jun 2021 19:07:47 +0200 Subject: [PATCH 10/47] Some text adjustments --- demo/GraniteDemo.vala | 2 +- demo/Views/HyperTextView.vala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/GraniteDemo.vala b/demo/GraniteDemo.vala index 50b5d8ccc..5a0b3b790 100644 --- a/demo/GraniteDemo.vala +++ b/demo/GraniteDemo.vala @@ -61,7 +61,7 @@ public class Granite.Demo : Gtk.Application { main_stack.add_titled (date_time_picker_view, "pickers", "Date & Time"); main_stack.add_titled (dynamic_notebook_view, "dynamictab", "DynamicNotebook"); main_stack.add_titled (form_view, "formview", "Forms"); - main_stack.add_titled (hypertext_view, "hypertextview", "Editable Hypertext"); + main_stack.add_titled (hypertext_view, "hypertextview", "HyperTextView"); main_stack.add_titled (mode_button_view, "selection_controls", "Selection Controls"); main_stack.add_titled (overlaybar_view, "overlaybar", "OverlayBar"); main_stack.add_titled (seekbar_view, "seekbar", "SeekBar"); diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextView.vala index bcfee79e8..81deb479e 100644 --- a/demo/Views/HyperTextView.vala +++ b/demo/Views/HyperTextView.vala @@ -19,7 +19,7 @@ public class HyperTextView : Gtk.Grid { construct { - var hypertext_label = new Granite.HeaderLabel ("Navigatable URLs in Gtk.TextView"); + var hypertext_label = new Granite.HeaderLabel ("Clickable URLs in a TextView"); var hypertext_textview = new Granite.Widgets.HyperTextView (); hypertext_textview.buffer.text = "elementary OS - https://elementary.io/\nThe fast, open and privacy-respecting replacement for Windows and macOS."; From 0d189a4a61ba993a224308a9e65610881493512e Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 27 Jun 2021 19:10:29 +0200 Subject: [PATCH 11/47] Fixed Linter error --- lib/Widgets/HyperTextView.vala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 1a072a2cc..cdd2b5c8f 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -102,8 +102,7 @@ tag_key.scanf ("[%i,%i]", out tag_start_offset, out tag_end_offset); if ( - tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset - || + tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset || tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset ) { buffer.tag_table.remove (uri_text_tags.take (tag_key)); From 7aadf7e385bd5e53c500d06ee832687e5f0d919d Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 27 Jun 2021 19:11:38 +0200 Subject: [PATCH 12/47] Fixed Linting Error --- lib/Widgets/HyperTextView.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index cdd2b5c8f..ab5f92542 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -102,8 +102,9 @@ tag_key.scanf ("[%i,%i]", out tag_start_offset, out tag_end_offset); if ( - tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset || - tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset + tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset + || + tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset ) { buffer.tag_table.remove (uri_text_tags.take (tag_key)); } From 2d7e7dc9f58c48ffcc00faed858c7fc2bb77a406 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 27 Jun 2021 19:25:46 +0200 Subject: [PATCH 13/47] HyperTextView packed into ScrolledWindow --- demo/Views/HyperTextView.vala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextView.vala index 81deb479e..ddea736d2 100644 --- a/demo/Views/HyperTextView.vala +++ b/demo/Views/HyperTextView.vala @@ -23,6 +23,12 @@ public class HyperTextView : Gtk.Grid { var hypertext_textview = new Granite.Widgets.HyperTextView (); hypertext_textview.buffer.text = "elementary OS - https://elementary.io/\nThe fast, open and privacy-respecting replacement for Windows and macOS."; + var hypertext_scrolled_window = new Gtk.ScrolledWindow (null, null) { + height_request = 300, + width_request = 600 + }; + hypertext_scrolled_window.add (hypertext_textview); + margin = 12; orientation = Gtk.Orientation.VERTICAL; row_spacing = 3; @@ -30,7 +36,7 @@ public class HyperTextView : Gtk.Grid { valign = Gtk.Align.CENTER; vexpand = true; add (hypertext_label); - add (hypertext_textview); + add (hypertext_scrolled_window); show_all (); } } From ae48a190544835285a669b00d2b5723edb598ec8 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Tue, 29 Jun 2021 16:00:03 +0200 Subject: [PATCH 14/47] Dealing with multibyte characters --- lib/Widgets/HyperTextView.vala | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index ab5f92542..620750a05 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -111,7 +111,16 @@ } } - var buffer_substring = buffer.text.substring (buffer_start_offset, buffer_end_offset - buffer_start_offset); + /** + * Character counts are usually referred to as offsets, while byte counts are called indexes. + * If you confuse these two, things will work fine with ASCII, but as soon as your + * buffer contains multibyte characters, bad things will happen. + * https://developer.gnome.org/gtk3/stable/TextWidget.html + */ + var buffer_start_index = buffer.text.index_of_nth_char (buffer_start_offset); + var buffer_end_index = buffer.text.index_of_nth_char (buffer_end_offset); + var buffer_substring = buffer.text.substring (buffer_start_index, buffer_end_index - buffer_start_index); + if (buffer_substring.strip () == "") { // if the substring is empty, we do not have anything to do... return; @@ -123,9 +132,19 @@ while (match_info.matches ()) { string match_text = match_info.fetch (0); - + + /** + * Character counts are usually referred to as offsets, while byte counts are called indexes. + * If you confuse these two, things will work fine with ASCII, but as soon as your + * buffer contains multibyte characters, bad things will happen. + * https://developer.gnome.org/gtk3/stable/TextWidget.html + */ + int match_start_index, match_end_index; + match_info.fetch_pos (0, out match_start_index, out match_end_index); + int match_start_offset, match_end_offset; - match_info.fetch_pos (0, out match_start_offset, out match_end_offset); + match_start_offset = buffer_substring.substring (0, match_start_index).char_count (match_start_index + 1); + match_end_offset = buffer_substring.substring (0, match_end_index).char_count (match_end_index + 1); var buffer_match_start_offset = buffer_start_offset + match_start_offset; var buffer_match_end_offset = buffer_start_offset + match_end_offset; From c5126bc77b51c26d313278e2d76321f801682f5b Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Tue, 29 Jun 2021 16:01:37 +0200 Subject: [PATCH 15/47] Fixed whitespace --- lib/Widgets/HyperTextView.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 620750a05..27540cd4d 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -132,7 +132,7 @@ while (match_info.matches ()) { string match_text = match_info.fetch (0); - + /** * Character counts are usually referred to as offsets, while byte counts are called indexes. * If you confuse these two, things will work fine with ASCII, but as soon as your @@ -141,7 +141,7 @@ */ int match_start_index, match_end_index; match_info.fetch_pos (0, out match_start_index, out match_end_index); - + int match_start_offset, match_end_offset; match_start_offset = buffer_substring.substring (0, match_start_index).char_count (match_start_index + 1); match_end_offset = buffer_substring.substring (0, match_end_index).char_count (match_end_index + 1); From 203c370c124d446acdaf1c3140b4481f25680c44 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Tue, 29 Jun 2021 16:31:53 +0200 Subject: [PATCH 16/47] Dropped potential harmful max param --- lib/Widgets/HyperTextView.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 27540cd4d..57890fd57 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -143,8 +143,8 @@ match_info.fetch_pos (0, out match_start_index, out match_end_index); int match_start_offset, match_end_offset; - match_start_offset = buffer_substring.substring (0, match_start_index).char_count (match_start_index + 1); - match_end_offset = buffer_substring.substring (0, match_end_index).char_count (match_end_index + 1); + match_start_offset = buffer_substring.substring (0, match_start_index).char_count (); + match_end_offset = buffer_substring.substring (0, match_end_index).char_count (); var buffer_match_start_offset = buffer_start_offset + match_start_offset; var buffer_match_end_offset = buffer_start_offset + match_end_offset; From a80f5c342e2753c1fa8c0bfc3d7e2f90f5c51df6 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 30 Jun 2021 07:35:43 +0200 Subject: [PATCH 17/47] Add mailto protocol if needed --- lib/Widgets/HyperTextView.vala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 57890fd57..f9caab1f4 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -161,6 +161,8 @@ if (match_text[0] == '/') { match_text = "file://" + match_text; + } else if (!match_text.contains (":") && match_text.contains ("@")) { + match_text = "mailto:" + match_text; } else { match_text = "http://" + match_text; } From d19ee186d2888f9a800fc6d889032e27392e481f Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 30 Jun 2021 08:54:05 +0200 Subject: [PATCH 18/47] Control + Click --- lib/Widgets/HyperTextView.vala | 80 ++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index f9caab1f4..f93afcb06 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -30,6 +30,8 @@ private GLib.HashTable uri_text_tags; private Regex uri_regex; + private bool is_control_key_pressed = false; + construct { uri_text_tags = new GLib.HashTable (str_hash, direct_equal); try { @@ -42,8 +44,11 @@ buffer.paste_done.connect (on_paste_done); buffer.changed.connect_after (on_after_buffer_changed); - button_press_event.connect_after (on_after_button_press_event); + key_press_event.connect (on_key_press_event); + key_release_event.connect (on_key_release_event); + button_release_event.connect (on_button_release_event); motion_notify_event.connect (on_motion_notify_event); + focus_out_event.connect (on_focus_out_event); } private void on_buffer_cursor_position_changed () { @@ -182,7 +187,38 @@ } } - private bool on_after_button_press_event () { + private bool on_key_press_event (Gdk.EventKey event) { + if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { + var window = get_window (Gtk.TextWindowType.TEXT); + if (window != null) { + int pointer_x, pointer_y; + window.get_pointer (out pointer_x, out pointer_y, null); + + var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); + if (uri_hovering_over != null) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); + } + } + is_control_key_pressed = true; + } + return Gdk.EVENT_PROPAGATE; + } + + private bool on_key_release_event (Gdk.EventKey event) { + if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { + var window = get_window (Gtk.TextWindowType.TEXT); + if (is_control_key_pressed && window != null) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "text"); + } + is_control_key_pressed = false; + } + return Gdk.EVENT_PROPAGATE; + } + + private bool on_button_release_event () { + if (!is_control_key_pressed) { + return Gdk.EVENT_PROPAGATE; + } Gtk.TextIter text_iter; buffer.get_iter_at_mark (out text_iter, buffer.get_insert ()); @@ -211,16 +247,30 @@ return Gdk.EVENT_PROPAGATE; } - private bool was_hovering = false; - private bool on_motion_notify_event (Gtk.Widget widget, Gdk.EventMotion event) { + var uri_hovering_over = get_uri_at_location ((int) event.x, (int) event.y); + + if (uri_hovering_over != null && !has_tooltip) { + has_tooltip = true; + tooltip_markup = string.joinv ("\n", { + _("Follow Link"), + TOOLTIP_SECONDARY_TEXT_MARKUP.printf (_("Control + Click")) + }); + + } else if (uri_hovering_over == null && has_tooltip) { + has_tooltip = false; + } + + return Gdk.EVENT_PROPAGATE; + } + + private string? get_uri_at_location (int location_x, int location_y) { + string? uri = null; var window = get_window (Gtk.TextWindowType.TEXT); if (window != null) { - bool is_hovering = false; - int x, y; - window_to_buffer_coords (Gtk.TextWindowType.WIDGET, (int) event.x, (int) event.y, out x, out y); + window_to_buffer_coords (Gtk.TextWindowType.WIDGET, location_x, location_y, out x, out y); Gtk.TextIter text_iter; if (get_iter_at_location (out text_iter, x, y)) { @@ -228,21 +278,17 @@ foreach (var tag in tags) { if (tag.get_data ("uri") != null) { - is_hovering = true; + uri = tag.get_data ("uri"); break; } } } - - if (is_hovering && !was_hovering) { - window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); - was_hovering = is_hovering; - - } else if (!is_hovering && was_hovering) { - window.cursor = new Gdk.Cursor.from_name (get_display (), "text"); - was_hovering = is_hovering; - } } + return uri; + } + + private bool on_focus_out_event (Gdk.EventFocus event) { + is_control_key_pressed = false; return Gdk.EVENT_PROPAGATE; } } From 002fa006cb09dbdf7b1cc9b42aca445cb1897a9b Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 06:38:53 +0000 Subject: [PATCH 19/47] Fixed deprecated window.get_pointer --- lib/Widgets/HyperTextView.vala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index f93afcb06..c117d9c34 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -191,12 +191,15 @@ if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { var window = get_window (Gtk.TextWindowType.TEXT); if (window != null) { - int pointer_x, pointer_y; - window.get_pointer (out pointer_x, out pointer_y, null); - - var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); - if (uri_hovering_over != null) { - window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); + var pointer_device = window.get_display ().get_default_seat ().get_pointer (); + if (pointer_device != null) { + int pointer_x, pointer_y; + window.get_device_position (pointer_device, out pointer_x, out pointer_y, null); + + var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); + if (uri_hovering_over != null) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); + } } } is_control_key_pressed = true; From 6132d1d97e0dbc3c3f6d7844b7a54fdcc002c690 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 07:59:18 +0000 Subject: [PATCH 20/47] Bind key_press/key_release to toplevel window This enables us to recognize Control key presses even when HyperTextView is not focused --- lib/Widgets/HyperTextView.vala | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index c117d9c34..8dbe1fd92 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -44,11 +44,27 @@ buffer.paste_done.connect (on_paste_done); buffer.changed.connect_after (on_after_buffer_changed); - key_press_event.connect (on_key_press_event); - key_release_event.connect (on_key_release_event); button_release_event.connect (on_button_release_event); motion_notify_event.connect (on_motion_notify_event); focus_out_event.connect (on_focus_out_event); + + foreach (unowned var toplevel_window in Gtk.Window.list_toplevels ()) { + if (toplevel_window.get_parent_window () == null) { + toplevel_window.key_press_event.connect (on_key_press_event); + toplevel_window.key_release_event.connect (on_key_release_event); + break; + } + } + + destroy.connect (() => { + foreach (unowned var toplevel_window in Gtk.Window.list_toplevels ()) { + if (toplevel_window.get_parent_window () == null) { + toplevel_window.key_press_event.disconnect (on_key_press_event); + toplevel_window.key_release_event.disconnect (on_key_release_event); + break; + } + } + }); } private void on_buffer_cursor_position_changed () { From eff3a8b6061218262698dae1c704361626d3e139 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 07:59:46 +0000 Subject: [PATCH 21/47] Bind key_press/key_release signals to toplevel window This enables us to detect when the Control key is pressed even when HyperTextView is not focused. --- lib/Widgets/HyperTextView.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 8dbe1fd92..36aac74fa 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -48,6 +48,12 @@ motion_notify_event.connect (on_motion_notify_event); focus_out_event.connect (on_focus_out_event); + /** + * Binding key_press/key_release signals to toplevel window + * enables us to detect when the Control key is pressed + * even when HyperTextView is not focused. + */ + foreach (unowned var toplevel_window in Gtk.Window.list_toplevels ()) { if (toplevel_window.get_parent_window () == null) { toplevel_window.key_press_event.connect (on_key_press_event); From b5180f435fb784ddd4ab55f8f40b57c29af0ea7c Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 08:08:52 +0000 Subject: [PATCH 22/47] Made toplevel_window signal handling more robust --- lib/Widgets/HyperTextView.vala | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 36aac74fa..d4b2d2f81 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -31,6 +31,7 @@ private Regex uri_regex; private bool is_control_key_pressed = false; + private Gtk.Window? toplevel_window = null; construct { uri_text_tags = new GLib.HashTable (str_hash, direct_equal); @@ -50,25 +51,30 @@ /** * Binding key_press/key_release signals to toplevel window - * enables us to detect when the Control key is pressed - * even when HyperTextView is not focused. + * if possible enables us to detect when the Control key + * is pressed even when HyperTextView is not focused. */ - foreach (unowned var toplevel_window in Gtk.Window.list_toplevels ()) { - if (toplevel_window.get_parent_window () == null) { - toplevel_window.key_press_event.connect (on_key_press_event); - toplevel_window.key_release_event.connect (on_key_release_event); + foreach (unowned var window in Gtk.Window.list_toplevels ()) { + if (window.get_parent_window () == null) { + toplevel_window = window; break; } } + if (toplevel_window != null) { + toplevel_window.key_press_event.connect (on_key_press_event); + toplevel_window.key_release_event.connect (on_key_release_event); + } else { + // bind to this as a fallback + key_press_event.connect (on_key_press_event); + key_release_event.connect (on_key_release_event); + } + destroy.connect (() => { - foreach (unowned var toplevel_window in Gtk.Window.list_toplevels ()) { - if (toplevel_window.get_parent_window () == null) { - toplevel_window.key_press_event.disconnect (on_key_press_event); - toplevel_window.key_release_event.disconnect (on_key_release_event); - break; - } + if (toplevel_window != null) { + toplevel_window.key_press_event.disconnect (on_key_press_event); + toplevel_window.key_release_event.disconnect (on_key_release_event); } }); } From 5e9dfd1c024b100643b9225281864819fa90f8de Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 08:09:57 +0000 Subject: [PATCH 23/47] Disconnect from toplevel window seems not necessary --- lib/Widgets/HyperTextView.vala | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index d4b2d2f81..93fc6e1f4 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -70,13 +70,6 @@ key_press_event.connect (on_key_press_event); key_release_event.connect (on_key_release_event); } - - destroy.connect (() => { - if (toplevel_window != null) { - toplevel_window.key_press_event.disconnect (on_key_press_event); - toplevel_window.key_release_event.disconnect (on_key_release_event); - } - }); } private void on_buffer_cursor_position_changed () { From e3f0864b89a3f3ab37d4ea72979532f15dd9e536 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 08:11:18 +0000 Subject: [PATCH 24/47] Added warning in case toplevel window is missing --- lib/Widgets/HyperTextView.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 93fc6e1f4..3ddc22c96 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -66,6 +66,7 @@ toplevel_window.key_press_event.connect (on_key_press_event); toplevel_window.key_release_event.connect (on_key_release_event); } else { + warning ("Unable to bind key press events to toplevel window, Control + Click may not behave correct under all circumstances."); // bind to this as a fallback key_press_event.connect (on_key_press_event); key_release_event.connect (on_key_release_event); From 59324d7dc39a598d34ff043ab9e154e11207d9d4 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 08:12:47 +0000 Subject: [PATCH 25/47] Make toplevel_window local variable --- lib/Widgets/HyperTextView.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 3ddc22c96..43b99c736 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -31,7 +31,6 @@ private Regex uri_regex; private bool is_control_key_pressed = false; - private Gtk.Window? toplevel_window = null; construct { uri_text_tags = new GLib.HashTable (str_hash, direct_equal); @@ -54,7 +53,8 @@ * if possible enables us to detect when the Control key * is pressed even when HyperTextView is not focused. */ - + + Gtk.Window? toplevel_window = null; foreach (unowned var window in Gtk.Window.list_toplevels ()) { if (window.get_parent_window () == null) { toplevel_window = window; From ddf7600af74fa514fc6dcf01312c3ed61a00795e Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 08:28:59 +0000 Subject: [PATCH 26/47] Fixed Linting Errors --- lib/Widgets/HyperTextView.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 43b99c736..c05f0f28f 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -53,7 +53,7 @@ * if possible enables us to detect when the Control key * is pressed even when HyperTextView is not focused. */ - + Gtk.Window? toplevel_window = null; foreach (unowned var window in Gtk.Window.list_toplevels ()) { if (window.get_parent_window () == null) { @@ -71,6 +71,8 @@ key_press_event.connect (on_key_press_event); key_release_event.connect (on_key_release_event); } + + } private void on_buffer_cursor_position_changed () { From a17292f9a4b1516c8c54c7db0a42e11d1a16e32d Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 1 Jul 2021 08:33:07 +0000 Subject: [PATCH 27/47] Removed empty lines --- lib/Widgets/HyperTextView.vala | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index c05f0f28f..ac0a94517 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -71,8 +71,6 @@ key_press_event.connect (on_key_press_event); key_release_event.connect (on_key_release_event); } - - } private void on_buffer_cursor_position_changed () { From c9d8f480703cca385410036feee215cf1d74e12e Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Tue, 6 Jul 2021 09:19:03 +0200 Subject: [PATCH 28/47] Dropped on comment and use single star --- lib/Widgets/HyperTextView.vala | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index ac0a94517..418016743 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -138,12 +138,12 @@ } } - /** - * Character counts are usually referred to as offsets, while byte counts are called indexes. - * If you confuse these two, things will work fine with ASCII, but as soon as your - * buffer contains multibyte characters, bad things will happen. - * https://developer.gnome.org/gtk3/stable/TextWidget.html - */ + /* + Character counts are usually referred to as offsets, while byte counts are called indexes. + If you confuse these two, things will work fine with ASCII, but as soon as your + buffer contains multibyte characters, bad things will happen. + https://developer.gnome.org/gtk3/stable/TextWidget.html + */ var buffer_start_index = buffer.text.index_of_nth_char (buffer_start_offset); var buffer_end_index = buffer.text.index_of_nth_char (buffer_end_offset); var buffer_substring = buffer.text.substring (buffer_start_index, buffer_end_index - buffer_start_index); @@ -160,12 +160,6 @@ while (match_info.matches ()) { string match_text = match_info.fetch (0); - /** - * Character counts are usually referred to as offsets, while byte counts are called indexes. - * If you confuse these two, things will work fine with ASCII, but as soon as your - * buffer contains multibyte characters, bad things will happen. - * https://developer.gnome.org/gtk3/stable/TextWidget.html - */ int match_start_index, match_end_index; match_info.fetch_pos (0, out match_start_index, out match_end_index); From cb8ed5557aa8783cf0597e8529fd4e9cc2c087df Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 7 Jul 2021 12:39:07 +0000 Subject: [PATCH 29/47] Require http(s):// but made mailto: optional --- lib/Widgets/HyperTextView.vala | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 418016743..02e97e26f 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -33,9 +33,22 @@ private bool is_control_key_pressed = false; construct { + var http_charset = "[a-zA-Z0-9_\\/\\-\\.:@]"; + var email_charset = "[a-zA-Z0-9_\\-\\.]"; + var email_tld_charset = "[a-zA-Z0-9_\\-]"; + + var http_match_str = "https?:\\/\\/" + http_charset + "{1,}"; + var email_match_str = "(mailto:)?" + email_charset + "{1,}@" + email_charset + "{1,}\\." + email_tld_charset + "{1,}"; + + var uri_regex_str = "(?:(" + + http_match_str + + ")|(" + + email_match_str + + "))"; + uri_text_tags = new GLib.HashTable (str_hash, direct_equal); try { - uri_regex = new Regex ("([^\\(\\[\\s\\.\"'`<>]+:\\/\\/)?[^\\(\\[\\s\"'`<>]{2,}\\.[^\\)\\]\\s\"'`<>]{2,}"); + uri_regex = new Regex (uri_regex_str); } catch (GLib.RegexError e) { critical ("RegexError while constructing URI regex: %s", e.message); } @@ -175,18 +188,8 @@ buffer.get_iter_at_offset (out buffer_match_end_iter, buffer_match_end_offset); var tag = buffer.create_tag (null, "underline", Pango.Underline.SINGLE); - if (!match_text.contains ("://") && !match_text.has_prefix ("mailto:")) { - if (match_text[0] == '~') { - match_text = Environment.get_home_dir () + match_text.substring (1); - } - - if (match_text[0] == '/') { - match_text = "file://" + match_text; - } else if (!match_text.contains (":") && match_text.contains ("@")) { - match_text = "mailto:" + match_text; - } else { - match_text = "http://" + match_text; - } + if (!match_text.contains ("://") && match_text.contains ("@") && !match_text.has_prefix ("mailto:")) { + match_text = "mailto:" + match_text; } tag.set_data ("uri", match_text); buffer.apply_tag (tag, buffer_match_start_iter, buffer_match_end_iter); From c4a91df64e7c4894ab632dfaf81e172c108f70c9 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 7 Jul 2021 12:40:28 +0000 Subject: [PATCH 30/47] Linter kills me --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 02e97e26f..05b882bda 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -45,7 +45,7 @@ ")|(" + email_match_str + "))"; - + uri_text_tags = new GLib.HashTable (str_hash, direct_equal); try { uri_regex = new Regex (uri_regex_str); From 69d6cf84c814bc5ab7b915baf8115cba5656a0bf Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 7 Jul 2021 12:59:29 +0000 Subject: [PATCH 31/47] Added missing chars to http charset --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 05b882bda..497dc5b4a 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -33,7 +33,7 @@ private bool is_control_key_pressed = false; construct { - var http_charset = "[a-zA-Z0-9_\\/\\-\\.:@]"; + var http_charset = "[a-zA-Z0-9_\\/\\-\\.:@\\?&%=]"; var email_charset = "[a-zA-Z0-9_\\-\\.]"; var email_tld_charset = "[a-zA-Z0-9_\\-]"; From c7cca7a3876fbc38326b7dc24f5379f9dbcf8019 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 7 Jul 2021 13:06:04 +0000 Subject: [PATCH 32/47] Allow plus and hash signs --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 497dc5b4a..c8d3963fa 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -33,7 +33,7 @@ private bool is_control_key_pressed = false; construct { - var http_charset = "[a-zA-Z0-9_\\/\\-\\.:@\\?&%=]"; + var http_charset = "[a-zA-Z0-9_\\/\\-\\+\\.:@\\?&%=#]"; var email_charset = "[a-zA-Z0-9_\\-\\.]"; var email_tld_charset = "[a-zA-Z0-9_\\-]"; From 0055516b3bc10bba3166e72e674d3f9e926a9ad7 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 21 Jul 2021 09:51:44 +0200 Subject: [PATCH 33/47] Changed "Unable" to "Could not" --- lib/Widgets/HyperTextView.vala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index c8d3963fa..282d23feb 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -79,7 +79,7 @@ toplevel_window.key_press_event.connect (on_key_press_event); toplevel_window.key_release_event.connect (on_key_release_event); } else { - warning ("Unable to bind key press events to toplevel window, Control + Click may not behave correct under all circumstances."); + warning ("Could not bind key-press events to top-level window, Control + Click may not always behave correctly."); // bind to this as a fallback key_press_event.connect (on_key_press_event); key_release_event.connect (on_key_release_event); @@ -252,10 +252,10 @@ try { GLib.AppInfo.launch_default_for_uri (uri, null); } catch (GLib.Error e) { - warning ("Unable to open URI '%s': %s", uri, e.message); + warning ("Could not open URI '%s': %s", uri, e.message); var error_dialog = new Granite.MessageDialog ( - _("Unable to open URI"), + _("Could not open URI"), e.message, new ThemedIcon ("dialog-error"), Gtk.ButtonsType.CLOSE From ae41c78b97f0280b59555468daad74fd6a8e1d82 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 21 Jul 2021 09:59:15 +0200 Subject: [PATCH 34/47] Use magic const to make full buffer rescan logic more obvious --- lib/Widgets/HyperTextView.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 282d23feb..729a7e057 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -24,6 +24,8 @@ */ public class HyperTextView : Gtk.TextView { + private const int FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET = -1; + private uint buffer_changed_debounce_timeout_id = 0; private int buffer_cursor_position_when_change_started = 0; @@ -94,7 +96,7 @@ private void on_paste_done (Gtk.Clipboard clipboard) { // force rescan of whole buffer: - buffer_cursor_position_when_change_started = -1; + buffer_cursor_position_when_change_started = FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET; } private void on_after_buffer_changed () { @@ -111,7 +113,7 @@ buffer_cursor_position_when_change_started = 0; - if (change_start_offset < 0) { + if (change_start_offset == FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET) { change_start_offset = 0; change_end_offset = buffer.text.length; } From f7843ebb1ae0dac36b266ba759fc13eb8c63bd9f Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Wed, 21 Jul 2021 10:03:09 +0200 Subject: [PATCH 35/47] Updated header label to make functionality more obvious --- demo/Views/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextView.vala index ddea736d2..7c91ecdf6 100644 --- a/demo/Views/HyperTextView.vala +++ b/demo/Views/HyperTextView.vala @@ -19,7 +19,7 @@ public class HyperTextView : Gtk.Grid { construct { - var hypertext_label = new Granite.HeaderLabel ("Clickable URLs in a TextView"); + var hypertext_label = new Granite.HeaderLabel ("Hold Ctrl and click to follow the link"); var hypertext_textview = new Granite.Widgets.HyperTextView (); hypertext_textview.buffer.text = "elementary OS - https://elementary.io/\nThe fast, open and privacy-respecting replacement for Windows and macOS."; From 797519fa79ea3f4ff369c4a7f0ed32ac918298aa Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 26 Aug 2021 07:58:06 +0200 Subject: [PATCH 36/47] Use inline namespacing --- lib/Widgets/HyperTextView.vala | 465 ++++++++++++++++----------------- 1 file changed, 231 insertions(+), 234 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 729a7e057..075173f73 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -17,303 +17,300 @@ * Boston, MA 02110-1301 USA. */ - namespace Granite.Widgets { +/** +* This class enables navigatable URLs in Gtk.TextView +*/ +public class Granite.HyperTextView : Gtk.TextView { - /** - * This class enables navigatable URLs in Gtk.TextView - */ - public class HyperTextView : Gtk.TextView { + private const int FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET = -1; - private const int FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET = -1; + private uint buffer_changed_debounce_timeout_id = 0; + private int buffer_cursor_position_when_change_started = 0; - private uint buffer_changed_debounce_timeout_id = 0; - private int buffer_cursor_position_when_change_started = 0; + private GLib.HashTable uri_text_tags; + private Regex uri_regex; - private GLib.HashTable uri_text_tags; - private Regex uri_regex; + private bool is_control_key_pressed = false; - private bool is_control_key_pressed = false; + construct { + var http_charset = "[a-zA-Z0-9_\\/\\-\\+\\.:@\\?&%=#]"; + var email_charset = "[a-zA-Z0-9_\\-\\.]"; + var email_tld_charset = "[a-zA-Z0-9_\\-]"; - construct { - var http_charset = "[a-zA-Z0-9_\\/\\-\\+\\.:@\\?&%=#]"; - var email_charset = "[a-zA-Z0-9_\\-\\.]"; - var email_tld_charset = "[a-zA-Z0-9_\\-]"; + var http_match_str = "https?:\\/\\/" + http_charset + "{1,}"; + var email_match_str = "(mailto:)?" + email_charset + "{1,}@" + email_charset + "{1,}\\." + email_tld_charset + "{1,}"; - var http_match_str = "https?:\\/\\/" + http_charset + "{1,}"; - var email_match_str = "(mailto:)?" + email_charset + "{1,}@" + email_charset + "{1,}\\." + email_tld_charset + "{1,}"; + var uri_regex_str = "(?:(" + + http_match_str + + ")|(" + + email_match_str + + "))"; - var uri_regex_str = "(?:(" + - http_match_str + - ")|(" + - email_match_str + - "))"; + uri_text_tags = new GLib.HashTable (str_hash, direct_equal); + try { + uri_regex = new Regex (uri_regex_str); + } catch (GLib.RegexError e) { + critical ("RegexError while constructing URI regex: %s", e.message); + } - uri_text_tags = new GLib.HashTable (str_hash, direct_equal); - try { - uri_regex = new Regex (uri_regex_str); - } catch (GLib.RegexError e) { - critical ("RegexError while constructing URI regex: %s", e.message); - } + buffer.notify["cursor-position"].connect (on_buffer_cursor_position_changed); + buffer.paste_done.connect (on_paste_done); + buffer.changed.connect_after (on_after_buffer_changed); - buffer.notify["cursor-position"].connect (on_buffer_cursor_position_changed); - buffer.paste_done.connect (on_paste_done); - buffer.changed.connect_after (on_after_buffer_changed); - - button_release_event.connect (on_button_release_event); - motion_notify_event.connect (on_motion_notify_event); - focus_out_event.connect (on_focus_out_event); - - /** - * Binding key_press/key_release signals to toplevel window - * if possible enables us to detect when the Control key - * is pressed even when HyperTextView is not focused. - */ - - Gtk.Window? toplevel_window = null; - foreach (unowned var window in Gtk.Window.list_toplevels ()) { - if (window.get_parent_window () == null) { - toplevel_window = window; - break; - } - } + button_release_event.connect (on_button_release_event); + motion_notify_event.connect (on_motion_notify_event); + focus_out_event.connect (on_focus_out_event); - if (toplevel_window != null) { - toplevel_window.key_press_event.connect (on_key_press_event); - toplevel_window.key_release_event.connect (on_key_release_event); - } else { - warning ("Could not bind key-press events to top-level window, Control + Click may not always behave correctly."); - // bind to this as a fallback - key_press_event.connect (on_key_press_event); - key_release_event.connect (on_key_release_event); - } - } + /** + * Binding key_press/key_release signals to toplevel window + * if possible enables us to detect when the Control key + * is pressed even when HyperTextView is not focused. + */ - private void on_buffer_cursor_position_changed () { - if (buffer_cursor_position_when_change_started == 0) { - buffer_cursor_position_when_change_started = buffer.cursor_position; + Gtk.Window? toplevel_window = null; + foreach (unowned var window in Gtk.Window.list_toplevels ()) { + if (window.get_parent_window () == null) { + toplevel_window = window; + break; } } - private void on_paste_done (Gtk.Clipboard clipboard) { - // force rescan of whole buffer: - buffer_cursor_position_when_change_started = FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET; + if (toplevel_window != null) { + toplevel_window.key_press_event.connect (on_key_press_event); + toplevel_window.key_release_event.connect (on_key_release_event); + } else { + warning ("Could not bind key-press events to top-level window, Control + Click may not always behave correctly."); + // bind to this as a fallback + key_press_event.connect (on_key_press_event); + key_release_event.connect (on_key_release_event); } + } - private void on_after_buffer_changed () { - if (buffer_changed_debounce_timeout_id != 0) { - Source.remove (buffer_changed_debounce_timeout_id); - buffer_changed_debounce_timeout_id = 0; - } + private void on_buffer_cursor_position_changed () { + if (buffer_cursor_position_when_change_started == 0) { + buffer_cursor_position_when_change_started = buffer.cursor_position; + } + } - buffer_changed_debounce_timeout_id = GLib.Timeout.add (300, () => { - buffer_changed_debounce_timeout_id = 0; + private void on_paste_done (Gtk.Clipboard clipboard) { + // force rescan of whole buffer: + buffer_cursor_position_when_change_started = FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET; + } - var change_start_offset = buffer_cursor_position_when_change_started; - var change_end_offset = buffer.cursor_position; + private void on_after_buffer_changed () { + if (buffer_changed_debounce_timeout_id != 0) { + Source.remove (buffer_changed_debounce_timeout_id); + buffer_changed_debounce_timeout_id = 0; + } - buffer_cursor_position_when_change_started = 0; + buffer_changed_debounce_timeout_id = GLib.Timeout.add (300, () => { + buffer_changed_debounce_timeout_id = 0; - if (change_start_offset == FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET) { - change_start_offset = 0; - change_end_offset = buffer.text.length; - } + var change_start_offset = buffer_cursor_position_when_change_started; + var change_end_offset = buffer.cursor_position; - update_tags_in_buffer_for_range.begin ( - int.min (change_start_offset, change_end_offset), - int.max (change_start_offset, change_end_offset) - ); + buffer_cursor_position_when_change_started = 0; - return GLib.Source.REMOVE; - }); - } + if (change_start_offset == FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET) { + change_start_offset = 0; + change_end_offset = buffer.text.length; + } - private async void update_tags_in_buffer_for_range (int buffer_start_offset, int buffer_end_offset) { - Gtk.TextIter buffer_start_iter, buffer_end_iter; - buffer.get_iter_at_offset (out buffer_start_iter, buffer_start_offset); - buffer_start_iter.backward_line (); - buffer_start_offset = buffer_start_iter.get_offset (); + update_tags_in_buffer_for_range.begin ( + int.min (change_start_offset, change_end_offset), + int.max (change_start_offset, change_end_offset) + ); - buffer.get_iter_at_offset (out buffer_end_iter, buffer_end_offset); - buffer_end_iter.forward_line (); - buffer_end_offset = buffer_end_iter.get_offset (); + return GLib.Source.REMOVE; + }); + } - // Delete all tags in buffer for range [start_iter.offset,end_iter.offset] - lock (uri_text_tags) { - foreach (var tag_key in uri_text_tags.get_keys ()) { - int tag_start_offset, tag_end_offset; - tag_key.scanf ("[%i,%i]", out tag_start_offset, out tag_end_offset); - - if ( - tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset - || - tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset - ) { - buffer.tag_table.remove (uri_text_tags.take (tag_key)); - } + private async void update_tags_in_buffer_for_range (int buffer_start_offset, int buffer_end_offset) { + Gtk.TextIter buffer_start_iter, buffer_end_iter; + buffer.get_iter_at_offset (out buffer_start_iter, buffer_start_offset); + buffer_start_iter.backward_line (); + buffer_start_offset = buffer_start_iter.get_offset (); + + buffer.get_iter_at_offset (out buffer_end_iter, buffer_end_offset); + buffer_end_iter.forward_line (); + buffer_end_offset = buffer_end_iter.get_offset (); + + // Delete all tags in buffer for range [start_iter.offset,end_iter.offset] + lock (uri_text_tags) { + foreach (var tag_key in uri_text_tags.get_keys ()) { + int tag_start_offset, tag_end_offset; + tag_key.scanf ("[%i,%i]", out tag_start_offset, out tag_end_offset); + + if ( + tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset + || + tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset + ) { + buffer.tag_table.remove (uri_text_tags.take (tag_key)); } } + } - /* - Character counts are usually referred to as offsets, while byte counts are called indexes. - If you confuse these two, things will work fine with ASCII, but as soon as your - buffer contains multibyte characters, bad things will happen. - https://developer.gnome.org/gtk3/stable/TextWidget.html - */ - var buffer_start_index = buffer.text.index_of_nth_char (buffer_start_offset); - var buffer_end_index = buffer.text.index_of_nth_char (buffer_end_offset); - var buffer_substring = buffer.text.substring (buffer_start_index, buffer_end_index - buffer_start_index); - - if (buffer_substring.strip () == "") { - // if the substring is empty, we do not have anything to do... - return; - } + /* + Character counts are usually referred to as offsets, while byte counts are called indexes. + If you confuse these two, things will work fine with ASCII, but as soon as your + buffer contains multibyte characters, bad things will happen. + https://developer.gnome.org/gtk3/stable/TextWidget.html + */ + var buffer_start_index = buffer.text.index_of_nth_char (buffer_start_offset); + var buffer_end_index = buffer.text.index_of_nth_char (buffer_end_offset); + var buffer_substring = buffer.text.substring (buffer_start_index, buffer_end_index - buffer_start_index); + + if (buffer_substring.strip () == "") { + // if the substring is empty, we do not have anything to do... + return; + } - // Add new tags in buffer for range [start_iter.offset,end_iter.offset] - GLib.MatchInfo match_info; - uri_regex.match (buffer_substring, 0, out match_info); + // Add new tags in buffer for range [start_iter.offset,end_iter.offset] + GLib.MatchInfo match_info; + uri_regex.match (buffer_substring, 0, out match_info); - while (match_info.matches ()) { - string match_text = match_info.fetch (0); + while (match_info.matches ()) { + string match_text = match_info.fetch (0); - int match_start_index, match_end_index; - match_info.fetch_pos (0, out match_start_index, out match_end_index); + int match_start_index, match_end_index; + match_info.fetch_pos (0, out match_start_index, out match_end_index); - int match_start_offset, match_end_offset; - match_start_offset = buffer_substring.substring (0, match_start_index).char_count (); - match_end_offset = buffer_substring.substring (0, match_end_index).char_count (); + int match_start_offset, match_end_offset; + match_start_offset = buffer_substring.substring (0, match_start_index).char_count (); + match_end_offset = buffer_substring.substring (0, match_end_index).char_count (); - var buffer_match_start_offset = buffer_start_offset + match_start_offset; - var buffer_match_end_offset = buffer_start_offset + match_end_offset; + var buffer_match_start_offset = buffer_start_offset + match_start_offset; + var buffer_match_end_offset = buffer_start_offset + match_end_offset; - Gtk.TextIter buffer_match_start_iter, buffer_match_end_iter; - buffer.get_iter_at_offset (out buffer_match_start_iter, buffer_match_start_offset); - buffer.get_iter_at_offset (out buffer_match_end_iter, buffer_match_end_offset); + Gtk.TextIter buffer_match_start_iter, buffer_match_end_iter; + buffer.get_iter_at_offset (out buffer_match_start_iter, buffer_match_start_offset); + buffer.get_iter_at_offset (out buffer_match_end_iter, buffer_match_end_offset); - var tag = buffer.create_tag (null, "underline", Pango.Underline.SINGLE); - if (!match_text.contains ("://") && match_text.contains ("@") && !match_text.has_prefix ("mailto:")) { - match_text = "mailto:" + match_text; - } - tag.set_data ("uri", match_text); - buffer.apply_tag (tag, buffer_match_start_iter, buffer_match_end_iter); + var tag = buffer.create_tag (null, "underline", Pango.Underline.SINGLE); + if (!match_text.contains ("://") && match_text.contains ("@") && !match_text.has_prefix ("mailto:")) { + match_text = "mailto:" + match_text; + } + tag.set_data ("uri", match_text); + buffer.apply_tag (tag, buffer_match_start_iter, buffer_match_end_iter); - lock (uri_text_tags) { - uri_text_tags.set ("[%i,%i]".printf (buffer_match_start_offset, buffer_match_end_offset), tag); - } + lock (uri_text_tags) { + uri_text_tags.set ("[%i,%i]".printf (buffer_match_start_offset, buffer_match_end_offset), tag); + } - try { - match_info.next (); - } catch (GLib.RegexError e) { - warning ("RegexError while scanning for the next URI match: %s", e.message); - } + try { + match_info.next (); + } catch (GLib.RegexError e) { + warning ("RegexError while scanning for the next URI match: %s", e.message); } } + } - private bool on_key_press_event (Gdk.EventKey event) { - if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { - var window = get_window (Gtk.TextWindowType.TEXT); - if (window != null) { - var pointer_device = window.get_display ().get_default_seat ().get_pointer (); - if (pointer_device != null) { - int pointer_x, pointer_y; - window.get_device_position (pointer_device, out pointer_x, out pointer_y, null); - - var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); - if (uri_hovering_over != null) { - window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); - } + private bool on_key_press_event (Gdk.EventKey event) { + if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { + var window = get_window (Gtk.TextWindowType.TEXT); + if (window != null) { + var pointer_device = window.get_display ().get_default_seat ().get_pointer (); + if (pointer_device != null) { + int pointer_x, pointer_y; + window.get_device_position (pointer_device, out pointer_x, out pointer_y, null); + + var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); + if (uri_hovering_over != null) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "pointer"); } } - is_control_key_pressed = true; } - return Gdk.EVENT_PROPAGATE; + is_control_key_pressed = true; } + return Gdk.EVENT_PROPAGATE; + } - private bool on_key_release_event (Gdk.EventKey event) { - if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { - var window = get_window (Gtk.TextWindowType.TEXT); - if (is_control_key_pressed && window != null) { - window.cursor = new Gdk.Cursor.from_name (get_display (), "text"); - } - is_control_key_pressed = false; + private bool on_key_release_event (Gdk.EventKey event) { + if (event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) { + var window = get_window (Gtk.TextWindowType.TEXT); + if (is_control_key_pressed && window != null) { + window.cursor = new Gdk.Cursor.from_name (get_display (), "text"); } + is_control_key_pressed = false; + } + return Gdk.EVENT_PROPAGATE; + } + + private bool on_button_release_event () { + if (!is_control_key_pressed) { return Gdk.EVENT_PROPAGATE; } + Gtk.TextIter text_iter; + buffer.get_iter_at_mark (out text_iter, buffer.get_insert ()); - private bool on_button_release_event () { - if (!is_control_key_pressed) { - return Gdk.EVENT_PROPAGATE; - } - Gtk.TextIter text_iter; - buffer.get_iter_at_mark (out text_iter, buffer.get_insert ()); - - var tags = text_iter.get_tags (); - foreach (var tag in tags) { - if (tag.get_data ("uri") != null) { - var uri = tag.get_data ("uri"); - - try { - GLib.AppInfo.launch_default_for_uri (uri, null); - } catch (GLib.Error e) { - warning ("Could not open URI '%s': %s", uri, e.message); - - var error_dialog = new Granite.MessageDialog ( - _("Could not open URI"), - e.message, - new ThemedIcon ("dialog-error"), - Gtk.ButtonsType.CLOSE - ); - error_dialog.run (); - error_dialog.destroy (); - } - break; + var tags = text_iter.get_tags (); + foreach (var tag in tags) { + if (tag.get_data ("uri") != null) { + var uri = tag.get_data ("uri"); + + try { + GLib.AppInfo.launch_default_for_uri (uri, null); + } catch (GLib.Error e) { + warning ("Could not open URI '%s': %s", uri, e.message); + + var error_dialog = new Granite.MessageDialog ( + _("Could not open URI"), + e.message, + new ThemedIcon ("dialog-error"), + Gtk.ButtonsType.CLOSE + ); + error_dialog.run (); + error_dialog.destroy (); } + break; } - return Gdk.EVENT_PROPAGATE; } + return Gdk.EVENT_PROPAGATE; + } - private bool on_motion_notify_event (Gtk.Widget widget, Gdk.EventMotion event) { - var uri_hovering_over = get_uri_at_location ((int) event.x, (int) event.y); - - if (uri_hovering_over != null && !has_tooltip) { - has_tooltip = true; - tooltip_markup = string.joinv ("\n", { - _("Follow Link"), - TOOLTIP_SECONDARY_TEXT_MARKUP.printf (_("Control + Click")) - }); + private bool on_motion_notify_event (Gtk.Widget widget, Gdk.EventMotion event) { + var uri_hovering_over = get_uri_at_location ((int) event.x, (int) event.y); - } else if (uri_hovering_over == null && has_tooltip) { - has_tooltip = false; - } + if (uri_hovering_over != null && !has_tooltip) { + has_tooltip = true; + tooltip_markup = string.joinv ("\n", { + _("Follow Link"), + TOOLTIP_SECONDARY_TEXT_MARKUP.printf (_("Control + Click")) + }); - return Gdk.EVENT_PROPAGATE; + } else if (uri_hovering_over == null && has_tooltip) { + has_tooltip = false; } - private string? get_uri_at_location (int location_x, int location_y) { - string? uri = null; - var window = get_window (Gtk.TextWindowType.TEXT); + return Gdk.EVENT_PROPAGATE; + } - if (window != null) { - int x, y; - window_to_buffer_coords (Gtk.TextWindowType.WIDGET, location_x, location_y, out x, out y); - - Gtk.TextIter text_iter; - if (get_iter_at_location (out text_iter, x, y)) { - var tags = text_iter.get_tags (); - - foreach (var tag in tags) { - if (tag.get_data ("uri") != null) { - uri = tag.get_data ("uri"); - break; - } + private string? get_uri_at_location (int location_x, int location_y) { + string? uri = null; + var window = get_window (Gtk.TextWindowType.TEXT); + + if (window != null) { + int x, y; + window_to_buffer_coords (Gtk.TextWindowType.WIDGET, location_x, location_y, out x, out y); + + Gtk.TextIter text_iter; + if (get_iter_at_location (out text_iter, x, y)) { + var tags = text_iter.get_tags (); + + foreach (var tag in tags) { + if (tag.get_data ("uri") != null) { + uri = tag.get_data ("uri"); + break; } } } - return uri; } + return uri; + } - private bool on_focus_out_event (Gdk.EventFocus event) { - is_control_key_pressed = false; - return Gdk.EVENT_PROPAGATE; - } + private bool on_focus_out_event (Gdk.EventFocus event) { + is_control_key_pressed = false; + return Gdk.EVENT_PROPAGATE; } } From 017c210a2050117cc76b5d5a1dfd1ac2fa1044b2 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 26 Aug 2021 08:07:29 +0200 Subject: [PATCH 37/47] Using inline namespace --- demo/Views/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextView.vala index 7c91ecdf6..947ee964f 100644 --- a/demo/Views/HyperTextView.vala +++ b/demo/Views/HyperTextView.vala @@ -20,7 +20,7 @@ public class HyperTextView : Gtk.Grid { construct { var hypertext_label = new Granite.HeaderLabel ("Hold Ctrl and click to follow the link"); - var hypertext_textview = new Granite.Widgets.HyperTextView (); + var hypertext_textview = new Granite.HyperTextView (); hypertext_textview.buffer.text = "elementary OS - https://elementary.io/\nThe fast, open and privacy-respecting replacement for Windows and macOS."; var hypertext_scrolled_window = new Gtk.ScrolledWindow (null, null) { From 3b3a10d539d9102e866e69013eb1704ec2b42e60 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 26 Aug 2021 08:09:17 +0200 Subject: [PATCH 38/47] Added changes from Calendar tests Specifically: - https://github.com/elementary/calendar/pull/692/commits/951ac4e2698b9c831d2d63b97efd85b906af34d2 - https://github.com/elementary/calendar/pull/692/commits/d3f6c3d91410888ded13480478fcd8c2176f8225 - https://github.com/elementary/calendar/pull/692/commits/9c61e89235bd7d8a7068b6ba47a32c99b2292b6f --- lib/Widgets/HyperTextView.vala | 38 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 075173f73..86da5457d 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -53,31 +53,29 @@ public class Granite.HyperTextView : Gtk.TextView { critical ("RegexError while constructing URI regex: %s", e.message); } - buffer.notify["cursor-position"].connect (on_buffer_cursor_position_changed); - buffer.paste_done.connect (on_paste_done); - buffer.changed.connect_after (on_after_buffer_changed); + buffer_connect (buffer); + notify["buffer"].connect (() => { + buffer_connect (buffer); + buffer.changed (); + }); button_release_event.connect (on_button_release_event); motion_notify_event.connect (on_motion_notify_event); focus_out_event.connect (on_focus_out_event); /** - * Binding key_press/key_release signals to toplevel window - * if possible enables us to detect when the Control key - * is pressed even when HyperTextView is not focused. - */ - - Gtk.Window? toplevel_window = null; - foreach (unowned var window in Gtk.Window.list_toplevels ()) { - if (window.get_parent_window () == null) { - toplevel_window = window; - break; + * Binding key_press/key_release signals to all toplevel + * windows possible enables us to detect when the Control + * key is pressed even when HyperTextView is not focused. + */ + var toplevel_windows = Gtk.Window.list_toplevels (); + + if (toplevel_windows.length () != 0) { + foreach (unowned var toplevel_window in toplevel_windows) { + toplevel_window.key_press_event.connect (on_key_press_event); + toplevel_window.key_release_event.connect (on_key_release_event); } - } - if (toplevel_window != null) { - toplevel_window.key_press_event.connect (on_key_press_event); - toplevel_window.key_release_event.connect (on_key_release_event); } else { warning ("Could not bind key-press events to top-level window, Control + Click may not always behave correctly."); // bind to this as a fallback @@ -86,6 +84,12 @@ public class Granite.HyperTextView : Gtk.TextView { } } + private void buffer_connect (Gtk.TextBuffer buffer) { + buffer.notify["cursor-position"].connect (on_buffer_cursor_position_changed); + buffer.paste_done.connect (on_paste_done); + buffer.changed.connect_after (on_after_buffer_changed); + } + private void on_buffer_cursor_position_changed () { if (buffer_cursor_position_when_change_started == 0) { buffer_cursor_position_when_change_started = buffer.cursor_position; From 050585b9ab2623917d619fa4b45f505940e28f7a Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 26 Aug 2021 08:26:15 +0200 Subject: [PATCH 39/47] Use Granite prefix --- lib/Widgets/HyperTextView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 86da5457d..2f0963f06 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -280,7 +280,7 @@ public class Granite.HyperTextView : Gtk.TextView { has_tooltip = true; tooltip_markup = string.joinv ("\n", { _("Follow Link"), - TOOLTIP_SECONDARY_TEXT_MARKUP.printf (_("Control + Click")) + Granite.TOOLTIP_SECONDARY_TEXT_MARKUP.printf (_("Control + Click")) }); } else if (uri_hovering_over == null && has_tooltip) { From edccbed51666d98f7b23b5536878f95f3959496c Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Mon, 6 Sep 2021 08:17:46 +0200 Subject: [PATCH 40/47] Addressed naming concerns --- demo/Views/{HyperTextView.vala => HyperTextViewGrid.vala} | 2 +- demo/meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename demo/Views/{HyperTextView.vala => HyperTextViewGrid.vala} (97%) diff --git a/demo/Views/HyperTextView.vala b/demo/Views/HyperTextViewGrid.vala similarity index 97% rename from demo/Views/HyperTextView.vala rename to demo/Views/HyperTextViewGrid.vala index 947ee964f..af3e246f3 100644 --- a/demo/Views/HyperTextView.vala +++ b/demo/Views/HyperTextViewGrid.vala @@ -17,7 +17,7 @@ * Boston, MA 02110-1301 USA. */ -public class HyperTextView : Gtk.Grid { +public class HyperTextViewGrid : Gtk.Grid { construct { var hypertext_label = new Granite.HeaderLabel ("Hold Ctrl and click to follow the link"); var hypertext_textview = new Granite.HyperTextView (); diff --git a/demo/meson.build b/demo/meson.build index a90a56f76..b807bb897 100644 --- a/demo/meson.build +++ b/demo/meson.build @@ -12,7 +12,7 @@ executable( 'Views/DialogsView.vala', 'Views/DynamicNotebookView.vala', 'Views/FormView.vala', - 'Views/HyperTextView.vala', + 'Views/HyperTextViewGrid.vala', 'Views/ModeButtonView.vala', 'Views/OverlayBarView.vala', 'Views/SeekBarView.vala', From 828ae63638eb9dd99f946a1ced68a3c2c78852f8 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Mon, 6 Sep 2021 09:07:05 +0200 Subject: [PATCH 41/47] Fixed issue where initialization sometimes failed --- lib/Widgets/HyperTextView.vala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 2f0963f06..86a17b76b 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -115,7 +115,7 @@ public class Granite.HyperTextView : Gtk.TextView { buffer_cursor_position_when_change_started = 0; - if (change_start_offset == FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET) { + if (change_start_offset == FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET || change_start_offset == change_end_offset) { change_start_offset = 0; change_end_offset = buffer.text.length; } @@ -130,6 +130,10 @@ public class Granite.HyperTextView : Gtk.TextView { } private async void update_tags_in_buffer_for_range (int buffer_start_offset, int buffer_end_offset) { + if (buffer_start_offset == buffer_end_offset) { + return; + } + Gtk.TextIter buffer_start_iter, buffer_end_iter; buffer.get_iter_at_offset (out buffer_start_iter, buffer_start_offset); buffer_start_iter.backward_line (); From dd27f430e4c69326ac0eebdd2a4b8b5d5cad9325 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Mon, 6 Sep 2021 09:14:51 +0200 Subject: [PATCH 42/47] Fixed preview of HyperTextView caused by name-clash --- demo/GraniteDemo.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/GraniteDemo.vala b/demo/GraniteDemo.vala index c2e95e48f..7a4a3f22e 100644 --- a/demo/GraniteDemo.vala +++ b/demo/GraniteDemo.vala @@ -18,7 +18,7 @@ public class Granite.Demo : Gtk.Application { var date_time_picker_view = new DateTimePickerView (); var dynamic_notebook_view = new DynamicNotebookView (); var form_view = new FormView (); - var hypertext_view = new HyperTextView (); + var hypertext_view = new HyperTextViewGrid (); var mode_button_view = new ModeButtonView (); var overlaybar_view = new OverlayBarView (); var seekbar_view = new SeekBarView (); From 304db29bbb402f92b43a582fe814e1ed11ad2ed7 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 19 Sep 2021 11:47:55 +0200 Subject: [PATCH 43/47] Added suggestions of @mcclurgm --- lib/Widgets/HyperTextView.vala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 86a17b76b..52869264c 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -33,12 +33,12 @@ public class Granite.HyperTextView : Gtk.TextView { private bool is_control_key_pressed = false; construct { - var http_charset = "[a-zA-Z0-9_\\/\\-\\+\\.:@\\?&%=#]"; - var email_charset = "[a-zA-Z0-9_\\-\\.]"; - var email_tld_charset = "[a-zA-Z0-9_\\-]"; + var http_charset = "[\\w\\/\\-\\+\\.:@\\?&%=#]"; + var email_charset = "[\\w\\-\\.]"; + var email_tld_charset = "[\\w\\-]"; - var http_match_str = "https?:\\/\\/" + http_charset + "{1,}"; - var email_match_str = "(mailto:)?" + email_charset + "{1,}@" + email_charset + "{1,}\\." + email_tld_charset + "{1,}"; + var http_match_str = @"https?:\\/\\/$(http_charset)+\\.$(http_charset)+"; + var email_match_str = @"(mailto:)?$(email_charset)+@$(email_charset)+\\.$(email_tld_charset)+"; var uri_regex_str = "(?:(" + http_match_str + From be393e1513a21177d6cd0ea8ef354273448d5ff4 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Mon, 20 Sep 2021 17:03:10 +0200 Subject: [PATCH 44/47] Added popover fix from @jeremypw --- lib/Widgets/HyperTextView.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 52869264c..32bb83821 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -296,11 +296,11 @@ public class Granite.HyperTextView : Gtk.TextView { private string? get_uri_at_location (int location_x, int location_y) { string? uri = null; - var window = get_window (Gtk.TextWindowType.TEXT); + var window = get_window (Gtk.TextWindowType.WIDGET); if (window != null) { int x, y; - window_to_buffer_coords (Gtk.TextWindowType.WIDGET, location_x, location_y, out x, out y); + window_to_buffer_coords (Gtk.TextWindowType.TEXT, location_x, location_y, out x, out y); Gtk.TextIter text_iter; if (get_iter_at_location (out text_iter, x, y)) { From 684f2d8413c68d4deb25dd2cc81b85be634df1a5 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Mon, 4 Oct 2021 21:42:33 +0200 Subject: [PATCH 45/47] Update lib/Widgets/HyperTextView.vala Co-authored-by: Michael McClurg --- lib/Widgets/HyperTextView.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 32bb83821..5eda7369b 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -19,7 +19,8 @@ /** * This class enables navigatable URLs in Gtk.TextView -*/ + * @since 6.1.2 + */ public class Granite.HyperTextView : Gtk.TextView { private const int FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET = -1; From d63967ab650990d62e4c917429d93e5866c40b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 4 Nov 2021 13:08:07 -0700 Subject: [PATCH 46/47] Update HyperTextView.vala --- lib/Widgets/HyperTextView.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/HyperTextView.vala b/lib/Widgets/HyperTextView.vala index 5eda7369b..e0995e985 100644 --- a/lib/Widgets/HyperTextView.vala +++ b/lib/Widgets/HyperTextView.vala @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012–2021 elementary, Inc. + * Copyright 2021 elementary, Inc. * * This program or library is free software; you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public @@ -19,7 +19,7 @@ /** * This class enables navigatable URLs in Gtk.TextView - * @since 6.1.2 + * @since 6.1.3 */ public class Granite.HyperTextView : Gtk.TextView { From 87dbe2eec59c8ec51efd8368b76efde222f4639a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 4 Nov 2021 13:08:54 -0700 Subject: [PATCH 47/47] Update granite.appdata.xml.in --- data/granite.appdata.xml.in | 1 + 1 file changed, 1 insertion(+) diff --git a/data/granite.appdata.xml.in b/data/granite.appdata.xml.in index 851ba1d1b..89f322dc4 100644 --- a/data/granite.appdata.xml.in +++ b/data/granite.appdata.xml.in @@ -15,6 +15,7 @@

New Features:

  • min_length property for Granite.ValidatedEntry
  • +
  • Granite.HyperTextView for navigatable URLs in text views

Improvements: