# HG changeset patch
# Node ID 626428200c9e
# Parent  edbbedfee064
diff --git a/po/POTFILES b/po/POTFILES
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -7,6 +7,7 @@
 talkatu/data/historyrow.ui
 talkatu/data/linkdialog.ui
 talkatu/data/toolbar.ui
+talkatu/data/view.ui
 talkatu/talkatuactiongroup.c
 talkatu/talkatuattachment.c
 talkatu/talkatuattachmentdialog.c
diff --git a/talkatu/data/talkatu.gresource.xml b/talkatu/data/talkatu.gresource.xml
--- a/talkatu/data/talkatu.gresource.xml
+++ b/talkatu/data/talkatu.gresource.xml
@@ -8,5 +8,6 @@
     <file compressed="true">linkdialog.ui</file>
     <file compressed="true">toolbar.ui</file>
     <file compressed="true">typinglabel.ui</file>
+    <file compressed="true">view.ui</file>
   </gresource>
 </gresources>
diff --git a/talkatu/data/view.ui b/talkatu/data/view.ui
new file mode 100644
--- /dev/null
+++ b/talkatu/data/view.ui
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Talkatu - GTK widgets for chat applications
+Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
+
+This library is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this library; if not, see <https://www.gnu.org/licenses/>.
+
+-->
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <!-- interface-license-type gplv2 -->
+  <!-- interface-name Pidgin -->
+  <!-- interface-description Internet Messenger -->
+  <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
+  <menu id="model">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Open Link</attribute>
+        <attribute name="action">link.open</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Copy Link</attribute>
+        <attribute name="action">link.copy</attribute>
+      </item>
+    </section>
+  </menu>
+  <template class="TalkatuView" parent="GtkTextView">
+    <child>
+      <object class="GtkPopoverMenu" id="menu">
+        <property name="has-arrow">0</property>
+        <property name="menu-model">model</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/talkatu/talkatuview.c b/talkatu/talkatuview.c
--- a/talkatu/talkatuview.c
+++ b/talkatu/talkatuview.c
@@ -32,8 +32,6 @@
 
 /**
  * TalkatuViewClass:
- * @format_activate: The class handler for the #TalkatuView::format_activate
- *                   signal.
  * @open_url: The class handler for the #TalkatuView::open_url signal.
  *
  * The backing class to #TalkatuView instances.
@@ -52,7 +50,9 @@
 	 * the tag for links/anchors to avoid extra lookups.
 	 */
 	GdkCursor *cursor_hand;
-	GtkTextTag *tag_anchor;
+
+	GtkWidget *menu;
+	gchar *url;
 } TalkatuViewPrivate;
 
 enum {
@@ -67,7 +67,7 @@
 /******************************************************************************
  * Helpers
  *****************************************************************************/
-static gchar *
+static const gchar *
 talkatu_view_url_from_iter(TalkatuView *view, GtkTextIter *iter) {
 	GSList *tag = NULL;
 	gchar *url = NULL;
@@ -89,158 +89,117 @@
 }
 
 /******************************************************************************
- * Callbacks
+ * Actions
  *****************************************************************************/
 static void
-talkatu_view_open_url_cb(GtkMenuItem *item, gpointer data) {
-	TalkatuView *view = g_object_get_data(G_OBJECT(item), "view");
-	gchar *url = g_object_get_data(G_OBJECT(item), "url");
+talkatu_view_link_open_cb(GtkWidget *widget, const gchar *action_name,
+                          GVariant *parameter)
+{
+	TalkatuView *view = TALKATU_VIEW(widget);
+	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
 
-	g_signal_emit(view, signals[SIG_OPEN_URL], 0, url);
+	g_signal_emit(view, signals[SIG_OPEN_URL], 0, priv->url);
 }
 
 static void
-talkatu_view_copy_url_cb(GtkMenuItem *item, gpointer data) {
-	GtkClipboard *clipboard = gtk_widget_get_clipboard(
-		GTK_WIDGET(item),
-		GDK_SELECTION_CLIPBOARD
-	);
+talkatu_view_link_copy_cb(GtkWidget *widget, const gchar *action_name,
+                          GVariant *parameter)
+{
+	TalkatuView *view = TALKATU_VIEW(widget);
+	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
 
-	gtk_clipboard_set_text(clipboard, (gchar *)data, -1);
+	if(priv->url != NULL) {
+		GdkClipboard *clipboard = NULL;
+
+		clipboard = gtk_widget_get_clipboard(widget);
+
+		gdk_clipboard_set_text(clipboard, priv->url);
+	}
 }
 
-gboolean
-talkatu_view_anchor_tag_event_cb(GtkTextTag *tag,
-                                 GObject *object,
-                                 GdkEvent *event,
-                                 GtkTextIter *iter,
-                                 gpointer data)
+/******************************************************************************
+ * Signal Handlers
+ *****************************************************************************/
+static void
+talkatu_view_released_cb(GtkGestureClick *gesture, guint n_press, double gx,
+                         double gy, gpointer data)
 {
-	GdkEventType event_type = gdk_event_get_event_type(event);
-
-	if(event_type == GDK_BUTTON_PRESS) {
-		GdkEventButton *event_button = (GdkEventButton *)event;
-
-		/* the user is right clicking on a link so stop the default handler */
-		if(event_button->button == GDK_BUTTON_SECONDARY) {
-			return TRUE;
-		}
-	} else if(gdk_event_triggers_context_menu(event)) {
-		GdkEventButton *event_button = (GdkEventButton *)event;
-		TalkatuView *view = TALKATU_VIEW(object);
-		gchar *url;
-
-		url = talkatu_view_url_from_iter(view, iter);
-
-		/* if we didn't find a url, bail */
-		if(url == NULL) {
-			return FALSE;
-		}
-
-		if(event_button->button == GDK_BUTTON_PRIMARY) {
-			GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
-
-			/* old behavior from pidgin2, if the users has something selected
-			 * we don't open links.  Other clients don't do this.. but it seems
-			 * to be a work around for someone pressing a button over text,
-			 * then moving the cursor to the link and then releasing.  Without
-			 * the selection check that'll cause that to open the link if the
-			 * button is released in the middle of the link.
-			 */
-			if(gtk_text_buffer_get_has_selection(buffer)) {
-				return FALSE;
-			}
-
-			g_signal_emit(view, signals[SIG_OPEN_URL], 0, url);
+	TalkatuView *view = TALKATU_VIEW(data);
+	GtkTextIter iter;
+	const gchar *url = NULL;
+	gint x, y;
 
-			return TRUE;
-		} else if(gdk_event_triggers_context_menu(event)) {
-			GtkWidget *menu = gtk_menu_new();
-			GtkWidget *item = NULL;
+	gtk_text_view_window_to_buffer_coords(
+		GTK_TEXT_VIEW(view),
+		GTK_TEXT_WINDOW_TEXT,
+		(gint)gx, (gint)gy,
+		&x, &y
+	);
+
+	gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(view), &iter, x, y);
 
-			item = gtk_menu_item_new_with_label(_("Open Link"));
-			/* to make it easier to deal with the life cycle, we just add data
-			 * to the menu item itself with destroy notifies.
-			 */
-			g_object_set_data_full(
-				G_OBJECT(item),
-				"view",
-				g_object_ref(view),
-				g_object_unref
-			);
-			g_object_set_data_full(
-				G_OBJECT(item),
-				"url",
-				g_strdup(url),
-				g_free
-			);
-			g_signal_connect(
-				G_OBJECT(item),
-				"activate",
-				G_CALLBACK(talkatu_view_open_url_cb),
-				NULL
-			);
-			gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
+	url = talkatu_view_url_from_iter(view, &iter);
+	if(url != NULL) {
+		gint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
 
-			item = gtk_menu_item_new_with_label(_("Copy Link"));
-			g_signal_connect(
-				G_OBJECT(item),
-				"activate",
-				G_CALLBACK(talkatu_view_copy_url_cb),
-				url
-			);
-			gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
+		if(button == 1) {
+			g_signal_emit(view, signals[SIG_OPEN_URL], 0, url);
+		} else if(button == 2) {
+			TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
 
-			gtk_widget_show_all(menu);
-			G_GNUC_BEGIN_IGNORE_DEPRECATIONS
-			gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, event_button->time);
-			G_GNUC_END_IGNORE_DEPRECATIONS
+			g_clear_pointer(&priv->url, g_free);
+			priv->url = g_strdup(url);
 
-			return TRUE;
+			gtk_popover_set_pointing_to(GTK_POPOVER(priv->menu),
+			                            &(const GdkRectangle){ x, y, 1, 1});
+			gtk_popover_popup(GTK_POPOVER(priv->menu));
 		}
 	}
+}
 
-	return FALSE;
+static void
+talkatu_view_motion_cb(GtkEventControllerMotion *controller, gdouble wx,
+                       gdouble wy, gpointer data)
+{
+	TalkatuView *view = TALKATU_VIEW(data);
+	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
+	GtkTextIter iter;
+	GdkCursor *cursor = NULL;
+	const gchar *url = NULL;
+	gint x, y;
+
+	gtk_text_view_window_to_buffer_coords(
+		GTK_TEXT_VIEW(view),
+		GTK_TEXT_WINDOW_TEXT,
+		(gint)wx, (gint)wy,
+		&x, &y
+	);
+
+	gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(view), &iter, x, y);
+
+	url = talkatu_view_url_from_iter(view, &iter);
+	if(url == NULL) {
+		cursor = priv->cursor_hand;
+	}
+
+	if(cursor != gtk_widget_get_cursor(GTK_WIDGET(view))) {
+		gtk_widget_set_cursor(GTK_WIDGET(view), cursor);
+	}
 }
 
 static void
 talkatu_view_buffer_set_cb(GObject *view, GParamSpec *pspec, gpointer data) {
 	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(TALKATU_VIEW(view));
 	GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
-	GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);
 
 	if(TALKATU_IS_BUFFER(buffer)) {
 		priv->action_group = talkatu_buffer_get_action_group(TALKATU_BUFFER(buffer));
 	}
-
-	/* check for the anchor tag, if we have it, add a signal handler to it */
-	priv->tag_anchor = gtk_text_tag_table_lookup(table, TALKATU_TAG_ANCHOR);
-	if(priv->tag_anchor != NULL) {
-		g_signal_connect(
-			priv->tag_anchor,
-			"event",
-			G_CALLBACK(talkatu_view_anchor_tag_event_cb),
-			NULL
-		);
-	}
 }
 
 /******************************************************************************
  * Default Signal Handlers
  *****************************************************************************/
-static void
-talkatu_view_format_activate(TalkatuView *view, const gchar *action_name) {
-	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
-
-	if(priv->action_group) {
-		GAction *action = g_action_map_lookup_action(G_ACTION_MAP(priv->action_group), action_name);
-
-		if(action) {
-			g_action_activate(action, NULL);
-		}
-	}
-}
-
 static gboolean
 talkatu_view_query_tooltip(GtkWidget *widget,
                            gint x,
@@ -249,7 +208,7 @@
                            GtkTooltip *tooltip)
 {
 	GtkTextIter iter;
-	gchar *url = NULL;
+	const gchar *url = NULL;
 	gint adj_x, adj_y;
 
 	if(keyboard) {
@@ -283,35 +242,6 @@
 	return GTK_WIDGET_CLASS(talkatu_view_parent_class)->query_tooltip(widget, x, y, keyboard, tooltip);
 }
 
-static gboolean
-talkatu_view_motion_notify_event(GtkWidget *widget, GdkEventMotion *event) {
-	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(TALKATU_VIEW(widget));
-	GtkTextIter iter;
-	GdkCursor *cursor = NULL;
-	gint x, y;
-
-	gtk_text_view_window_to_buffer_coords(
-		GTK_TEXT_VIEW(widget),
-		GTK_TEXT_WINDOW_TEXT,
-		event->x, event->y,
-		&x, &y
-	);
-
-	gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, x, y);
-
-	if(gtk_text_iter_has_tag(&iter, priv->tag_anchor)) {
-		cursor = priv->cursor_hand;
-	}
-
-	if(cursor != gdk_window_get_cursor(event->window)) {
-		gdk_window_set_cursor(event->window, cursor);
-
-		return TRUE;
-	}
-
-	return GTK_WIDGET_CLASS(talkatu_view_parent_class)->motion_notify_event(widget, event);
-}
-
 /******************************************************************************
  * GtkTextViewClass overrides
  *****************************************************************************/
@@ -328,6 +258,7 @@
 	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(TALKATU_VIEW(obj));
 
 	g_clear_object(&priv->cursor_hand);
+	g_clear_pointer(&priv->url, g_free);
 
 	G_OBJECT_CLASS(talkatu_view_parent_class)->finalize(obj);
 }
@@ -335,19 +266,28 @@
 static void
 talkatu_view_init(TalkatuView *view) {
 	TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
+	GtkEventController *controller = NULL;
 
-	priv->cursor_hand = gdk_cursor_new_from_name(gdk_display_get_default(), "pointer");
+	gtk_widget_init_template(GTK_WIDGET(view));
+
+	priv->cursor_hand = gdk_cursor_new_from_name("pointer", NULL);
 
 	/* tell the widget class that we support tooltips.  This is used to show
 	 * link targets, and probably other stuff at some point.
 	 */
 	gtk_widget_set_has_tooltip(GTK_WIDGET(view), TRUE);
 
-	/* set our event mask for the signals we care about */
-	gtk_widget_set_events(
-		GTK_WIDGET(view),
-		GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK
-	);
+
+	/* Connect some signals we care about */
+	controller = gtk_event_controller_motion_new();
+	g_signal_connect(controller, "motion", G_CALLBACK(talkatu_view_motion_cb),
+	                 view);
+	gtk_widget_add_controller(GTK_WIDGET(view), controller);
+
+	controller = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
+	g_signal_connect(controller, "released",
+	                 G_CALLBACK(talkatu_view_released_cb), view);
+	gtk_widget_add_controller(GTK_WIDGET(view), controller);
 
 	/* we need to know when the buffer is changed in our parent so we can
 	 * update our actions and other stuff.
@@ -365,42 +305,21 @@
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
 	GtkTextViewClass *text_view_class = GTK_TEXT_VIEW_CLASS(klass);
-	GtkBindingSet *binding_set = NULL;
 
 	obj_class->finalize = talkatu_view_finalize;
 
-	widget_class->motion_notify_event = talkatu_view_motion_notify_event;
 	widget_class->query_tooltip = talkatu_view_query_tooltip;
 
+	gtk_widget_class_set_template_from_resource(
+		widget_class,
+		"/org/imfreedom/keep/talkatu/talkatu/ui/view.ui"
+	);
+
 	text_view_class->create_buffer = talkatu_view_create_buffer;
 
-	/* add our default signal handlers */
-	klass->format_activate = talkatu_view_format_activate;
-
 	/* add our signals */
 
 	/**
-	 * TalkatuView::format-activate
-	 * @talkatutextview: The #TalkatuView instance.
-	 * @arg1: The name of the action to activated.
-	 * @user_data: User supplied data.
-	 *
-	 * Emitted by the keybindings to apply a format to the underlying buffer.
-	 */
-	signals[SIG_FORMAT_ACTIVATE] = g_signal_new(
-		"format-activate",
-		G_TYPE_FROM_CLASS(klass),
-		G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
-		G_STRUCT_OFFSET(TalkatuViewClass, format_activate),
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		1,
-		G_TYPE_STRING
-	);
-
-	/**
 	 * TalkatuView::open-url:
 	 * @talkatutextview: The #TalkatuView instances.
 	 * @url: The URL to open.
@@ -421,22 +340,23 @@
 		G_TYPE_STRING
 	);
 
-	/* setup key bindings */
-	binding_set = gtk_binding_set_by_class(talkatu_view_parent_class);
+	gtk_widget_class_bind_template_child_private(widget_class, TalkatuView, menu);
 
-	/* remove existing keybindings that we're overriding */
-	gtk_binding_entry_remove(binding_set, GDK_KEY_slash, GDK_CONTROL_MASK);
+	/* add our actions */
+	gtk_widget_class_install_action(widget_class, "link.open", NULL,
+	                                talkatu_view_link_open_cb);
+	gtk_widget_class_install_action(widget_class, "link.copy", NULL,
+	                                talkatu_view_link_copy_cb);
 
 	/* add our custom keybindings */
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_b, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_BOLD);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_i, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_ITALIC);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_u, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_UNDERLINE);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_slash, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_STRIKETHROUGH);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_plus, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_GROW);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_equal, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_GROW);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_minus, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_SHRINK);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_r, GDK_CONTROL_MASK, "format-activate", 1, G_TYPE_STRING, TALKATU_ACTION_FORMAT_RESET);
-	gtk_binding_entry_add_signal(binding_set, GDK_KEY_Insert, GDK_MOD1_MASK | GDK_SHIFT_MASK, "insert-at-cursor", 1, G_TYPE_STRING, "🐣");
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_b, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_BOLD, NULL);
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_i, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_ITALIC, NULL);
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_slash, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_STRIKETHROUGH, NULL);
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_plus, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_GROW, NULL);
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_equal, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_GROW, NULL);
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_minus, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_SHRINK, NULL);
+	gtk_widget_class_add_binding_action(widget_class, GDK_KEY_r, GDK_CONTROL_MASK, TALKATU_ACTION_FORMAT_RESET, NULL);
+	gtk_widget_class_add_binding_signal(widget_class, GDK_KEY_Insert, GDK_META_MASK | GDK_SHIFT_MASK, "insert-at-cursor", "s", "🐣");
 }
 
 /******************************************************************************
