talkatu/talkatu

Convert TalkatuView to Gtk4
gtk4
2022-01-31, Gary Kramlich
159cf1e35402
Parents 9dc271918fe6
Children 950d6686b59c
Convert TalkatuView to Gtk4

Testing Done:
Tested with `gtk4-builder-tool preview view.ui` and compiled with `ninja talkatu/libtalkatu.so.0.1.0.p/talkatuview.c.o` to verify there were no warnings/errors.

Reviewed at https://reviews.imfreedom.org/r/1255/
--- a/po/POTFILES Mon Jan 31 22:49:01 2022 -0600
+++ b/po/POTFILES Mon Jan 31 22:49:29 2022 -0600
@@ -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
--- a/talkatu/data/talkatu.gresource.xml Mon Jan 31 22:49:01 2022 -0600
+++ b/talkatu/data/talkatu.gresource.xml Mon Jan 31 22:49:29 2022 -0600
@@ -9,5 +9,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>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/talkatu/data/view.ui Mon Jan 31 22:49:29 2022 -0600
@@ -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>
--- a/talkatu/talkatuview.c Mon Jan 31 22:49:01 2022 -0600
+++ b/talkatu/talkatuview.c Mon Jan 31 22:49:29 2022 -0600
@@ -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 wx,
+ double wy, 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)wx, (gint)wy,
+ &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,30 @@
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());
+ /* check all buttons */
+ gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(controller), 0);
+ 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 +307,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 +342,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", "🐣");
}
/******************************************************************************