talkatu/talkatu

Merge the GTK4 branch as it is now usable

20 months ago, Gary Kramlich
033dcb62164a
Merge the GTK4 branch as it is now usable
  • +159 -406
    demo/data/demo.ui
  • +1 -1
    demo/meson.build
  • +62 -99
    demo/talkatudemowindow.c
  • +1 -2
    meson.build
  • +6 -5
    po/POTFILES
  • +18 -73
    talkatu/data/attachmentdialog.ui
  • +34 -84
    talkatu/data/attachmentpreview.ui
  • +15 -49
    talkatu/data/editor.ui
  • +31 -0
    talkatu/data/history.ui
  • +20 -65
    talkatu/data/historyrow.ui
  • +43 -0
    talkatu/data/input.ui
  • +26 -116
    talkatu/data/linkdialog.ui
  • +0 -67
    talkatu/data/messageactions.ui
  • +4 -1
    talkatu/data/talkatu.gresource.xml
  • +40 -105
    talkatu/data/toolbar.ui
  • +33 -0
    talkatu/data/typinglabel.ui
  • +62 -0
    talkatu/data/view.ui
  • +7 -13
    talkatu/meson.build
  • +40 -26
    talkatu/talkatuactiongroup.c
  • +3 -4
    talkatu/talkatuattachmentdialog.c
  • +8 -7
    talkatu/talkatuattachmentpreview.c
  • +1 -1
    talkatu/talkatuattachmentpreview.h
  • +154 -0
    talkatu/talkatuautoscroller.c
  • +45 -0
    talkatu/talkatuautoscroller.h
  • +2 -2
    talkatu/talkatubuffer.c
  • +32 -10
    talkatu/talkatuhistory.c
  • +2 -2
    talkatu/talkatuhistory.h
  • +0 -3
    talkatu/talkatuhistoryrow.c
  • +14 -14
    talkatu/talkatuhtmlbuffer.c
  • +64 -158
    talkatu/talkatuinput.c
  • +0 -1
    talkatu/talkatuinput.h
  • +2 -2
    talkatu/talkatulinkdialog.c
  • +7 -7
    talkatu/talkatumarkdownbuffer.c
  • +0 -179
    talkatu/talkatumenutoolbutton.c
  • +0 -44
    talkatu/talkatumenutoolbutton.h
  • +0 -171
    talkatu/talkatumessageactions.c
  • +0 -45
    talkatu/talkatumessageactions.h
  • +0 -181
    talkatu/talkatuscrolledwindow.c
  • +0 -44
    talkatu/talkatuscrolledwindow.h
  • +10 -25
    talkatu/talkatutagtable.c
  • +0 -5
    talkatu/talkatutagtable.h
  • +2 -2
    talkatu/talkatutoolbar.c
  • +1 -1
    talkatu/talkatutoolbar.h
  • +0 -357
    talkatu/talkatutooldrawer.c
  • +0 -53
    talkatu/talkatutooldrawer.h
  • +40 -37
    talkatu/talkatutypinglabel.c
  • +2 -13
    talkatu/talkatutypinglabel.h
  • +115 -223
    talkatu/talkatuview.c
  • +1 -2
    talkatu/talkatuview.h
  • +2 -2
    talkatu/tests/meson.build
  • +1 -1
    talkatu/tests/talkatutestactiongroup.c
  • +1 -1
    talkatu/tests/talkatutesthtmlpangorenderer.c
  • +1 -1
    talkatu/tests/talkatutesthtmlrenderer.c
  • +1 -1
    talkatu/tests/talkatutesthtmlserialization.c
  • +1 -1
    talkatu/tests/test-wrapper.py
  • +1 -1
    vapi/meson.build
  • --- a/demo/data/demo.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/demo/data/demo.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -20,549 +20,302 @@
    -->
    <interface>
    <requires lib="Talkatu" version="0.0"/>
    - <requires lib="gtk+" version="3.18"/>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    <!-- interface-name Talkatu -->
    <!-- interface-description GTK widgets for chat applications -->
    <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    + <object class="TalkatuAutoScroller" id="auto_scroller"/>
    <template class="TalkatuDemoWindow" parent="GtkApplicationWindow">
    - <property name="can-focus">False</property>
    - <property name="border-width">12</property>
    - <child>
    + <property name="child">
    <object class="GtkPaned">
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    + <property name="shrink-end-child">0</property>
    + <property name="shrink-start-child">0</property>
    <property name="orientation">vertical</property>
    - <property name="wide-handle">True</property>
    + <property name="wide-handle">1</property>
    <child>
    - <object class="TalkatuScrolledWindow">
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="shadow-type">in</property>
    + <object class="GtkScrolledWindow">
    + <property name="vadjustment">auto_scroller</property>
    <child>
    <object class="TalkatuHistory" id="history">
    <property name="name">history</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    - <property name="hexpand-set">True</property>
    - <property name="vexpand-set">True</property>
    </object>
    </child>
    </object>
    - <packing>
    - <property name="resize">True</property>
    - <property name="shrink">False</property>
    - </packing>
    </child>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    <property name="orientation">vertical</property>
    <child>
    - <object class="GtkToolbar">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkBox">
    + <style>
    + <class name="toolbar"/>
    + </style>
    + <property name="can-focus">0</property>
    <child>
    - <object class="GtkRadioToolButton" id="toggle_plain">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_plain">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Use a TalkatuBuffer</property>
    <property name="label" translatable="yes">Plain</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    <property name="icon-name">text-x-generic</property>
    - <property name="active">True</property>
    + <property name="active">1</property>
    <signal name="toggled" handler="talkatu_demo_window_buffer_changed_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkRadioToolButton" id="toggle_whole">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_whole">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Use a TalkatuWholeBuffer</property>
    <property name="label" translatable="yes">Whole</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    <property name="icon-name">ascii</property>
    <property name="group">toggle_plain</property>
    <signal name="toggled" handler="talkatu_demo_window_buffer_changed_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkRadioToolButton" id="toggle_html">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_html">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Use a TalkatuHTMLBuffer</property>
    <property name="label" translatable="yes">HTML</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    <property name="icon-name">text-html</property>
    <property name="group">toggle_plain</property>
    <signal name="toggled" handler="talkatu_demo_window_buffer_changed_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkRadioToolButton" id="toggle_markdown">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_markdown">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Use a TalkatuMarkdownBuffer</property>
    <property name="label" translatable="yes">Markdown</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    <property name="icon-name">text-x-generic-template</property>
    <property name="group">toggle_plain</property>
    <signal name="toggled" handler="talkatu_demo_window_buffer_changed_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkSeparatorToolItem">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkSeparator">
    + <property name="orientation">vertical</property>
    + <property name="can-focus">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkButton">
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Insert HTML from a file</property>
    <property name="label" translatable="yes">Insert HTML</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    <property name="icon-name">text-html</property>
    <signal name="clicked" handler="talkatu_demo_window_insert_html_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkButton">
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Insert Markdown from a file</property>
    <property name="label" translatable="yes">Insert Markdown</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    <property name="icon-name">text-x-generic</property>
    <signal name="clicked" handler="talkatu_demo_window_insert_markdown_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkSeparatorToolItem">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkSeparator">
    + <property name="orientation">vertical</property>
    + <property name="can-focus">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="author_button">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <property name="label" translatable="yes">Author</property>
    - <property name="use-underline">True</property>
    + <object class="GtkMenuButton" id="author_button">
    + <property name="can-focus">0</property>
    <property name="icon-name">system-users</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_toggled_cb" object="author_popover" swapped="no"/>
    + <property name="label" translatable="yes">Author</property>
    + <property name="menu-model">author_menu</property>
    + <property name="use-underline">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="author_name_color_button">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <property name="label" translatable="yes">Color</property>
    - <property name="use-underline">True</property>
    + <object class="GtkMenuButton" id="author_name_color_button">
    + <property name="can-focus">0</property>
    <property name="icon-name">color-select-symbolic</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_toggled_cb" object="author_name_color_popover" swapped="no"/>
    + <property name="label" translatable="yes">Color</property>
    + <property name="menu-model">author_name_color_menu</property>
    + <property name="use-underline">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    + </child>
    + <child>
    + <object class="GtkSeparator">
    + <property name="orientation">vertical</property>
    + <property name="can-focus">0</property>
    + </object>
    </child>
    <child>
    - <object class="GtkSeparatorToolItem">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkToggleToolButton" id="toggle_toolbar">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_toolbar">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Toggle toolbar visibility</property>
    <property name="label" translatable="yes">Toolbar</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="toggle_send_button">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_send_button">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Toggle send button visibility</property>
    <property name="label" translatable="yes">Send Button</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="toggle_edited">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <object class="GtkToggleButton" id="toggle_edited">
    + <style>
    + <class name="flat"/>
    + </style>
    + <property name="can-focus">0</property>
    <property name="tooltip-text" translatable="yes">Toggle whether the message is edited</property>
    <property name="label" translatable="yes">Edited</property>
    - <property name="use-underline">True</property>
    + <property name="use-underline">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="TalkatuEditor" id="editor">
    + <property name="vexpand">1</property>
    <property name="orientation">vertical</property>
    <property name="visible">True</property>
    - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    - <property name="hexpand-set">True</property>
    <property name="vexpand-set">True</property>
    <child internal-child="toolbar">
    <object class="TalkatuToolbar">
    - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    <property name="hexpand-set">True</property>
    <property name="vexpand-set">True</property>
    </object>
    - <packing>
    - <property name="fill">False</property>
    - </packing>
    </child>
    <child internal-child="input">
    <object class="TalkatuInput">
    - <property name="hexpand-set">True</property>
    - <property name="vexpand-set">True</property>
    <property name="buffer">buffer_plain</property>
    + <property name="author">Alice</property>
    + <property name="send-binding">1</property>
    <signal name="open-url" handler="talkatu_demo_window_view_open_url_cb" object="TalkatuDemoWindow" swapped="no"/>
    <signal name="send-message" handler="talkatu_demo_window_view_send_message_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <packing>
    - <property name="fill">False</property>
    - </packing>
    </child>
    <child internal-child="send_button">
    <object class="GtkButton">
    - <property name="can-focus">False</property>
    - <property name="receives-default">False</property>
    + <property name="visible">0</property>
    + <property name="can-focus">0</property>
    </object>
    - <packing>
    - <property name="fill">False</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    <child>
    <object class="TalkatuTypingLabel" id="typing">
    <property name="visible">True</property>
    - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">2</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="resize">True</property>
    - <property name="shrink">False</property>
    - </packing>
    - </child>
    - </object>
    - </child>
    - </template>
    - <object class="GtkPopover" id="author_name_color_popover">
    - <property name="can-focus">False</property>
    - <property name="relative-to">author_name_color_button</property>
    - <signal name="closed" handler="talkatu_demo_window_author_name_color_popover_closed_cb" object="author_name_color_button" swapped="no"/>
    - <child>
    - <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <property name="border-width">6</property>
    - <property name="orientation">vertical</property>
    - <child>
    - <object class="GtkRadioButton" id="author_name_color_item">
    - <property name="label" translatable="yes">Not set</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="active">True</property>
    - <property name="draw-indicator">True</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Red</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_name_color_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Green</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_name_color_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">2</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Blue</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_name_color_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">3</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Yellow</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_name_color_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">4</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Purple</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_name_color_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_name_color_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">5</property>
    - </packing>
    </child>
    </object>
    - </child>
    - </object>
    - <object class="GtkPopover" id="author_popover">
    - <property name="can-focus">False</property>
    - <property name="relative-to">author_button</property>
    - <signal name="closed" handler="talkatu_demo_window_author_popover_closed_cb" object="author_button" swapped="no"/>
    - <child>
    - <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <property name="border-width">6</property>
    - <property name="orientation">vertical</property>
    - <child>
    - <object class="GtkRadioButton" id="author_item">
    - <property name="label" translatable="yes">Alice</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="active">True</property>
    - <property name="draw-indicator">True</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Bob</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Carol</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">2</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">David</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">3</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Eve</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">4</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkRadioButton">
    - <property name="label" translatable="yes">Mallory</property>
    - <property name="visible">True</property>
    - <property name="can-focus">True</property>
    - <property name="receives-default">False</property>
    - <property name="draw-indicator">True</property>
    - <property name="group">author_item</property>
    - <signal name="toggled" handler="talkatu_demo_window_author_changed" object="TalkatuDemoWindow" swapped="no"/>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">5</property>
    - </packing>
    - </child>
    - </object>
    - </child>
    - </object>
    - <object class="TalkatuTagTable" id="table_history"/>
    - <object class="TalkatuTagTable" id="table_html"/>
    + </property>
    + </template>
    + <menu id="author_menu">
    + <section>
    + <item>
    + <attribute name="label" translatable="yes">Alice</attribute>
    + <attribute name="action">win.author-name</attribute>
    + <attribute name="target" translatable="yes">Alice</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Bob</attribute>
    + <attribute name="action">win.author-name</attribute>
    + <attribute name="target" translatable="yes">Bob</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Carol</attribute>
    + <attribute name="action">win.author-name</attribute>
    + <attribute name="target" translatable="yes">Carol</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">David</attribute>
    + <attribute name="action">win.author-name</attribute>
    + <attribute name="target" translatable="yes">David</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Eve</attribute>
    + <attribute name="action">win.author-name</attribute>
    + <attribute name="target" translatable="yes">Eve</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Mallory</attribute>
    + <attribute name="action">win.author-name</attribute>
    + <attribute name="target" translatable="yes">Mallory</attribute>
    + </item>
    + </section>
    + </menu>
    + <menu id="author_name_color_menu">
    + <section>
    + <item>
    + <attribute name="label" translatable="yes">Not set</attribute>
    + <attribute name="action">win.author-name-color</attribute>
    + <attribute name="target"></attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Red</attribute>
    + <attribute name="action">win.author-name-color</attribute>
    + <attribute name="target">red</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Green</attribute>
    + <attribute name="action">win.author-name-color</attribute>
    + <attribute name="target">green</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Blue</attribute>
    + <attribute name="action">win.author-name-color</attribute>
    + <attribute name="target">blue</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Yellow</attribute>
    + <attribute name="action">win.author-name-color</attribute>
    + <attribute name="target">yellow</attribute>
    + </item>
    + <item>
    + <attribute name="label" translatable="yes">Purple</attribute>
    + <attribute name="action">win.author-name-color</attribute>
    + <attribute name="target">purple</attribute>
    + </item>
    + </section>
    + </menu>
    <object class="TalkatuHtmlBuffer" id="buffer_html">
    - <property name="tag-table">table_html</property>
    <signal name="changed" handler="talkatu_demo_window_buffer_modified_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <object class="TalkatuTagTable" id="table_markdown"/>
    <object class="TalkatuMarkdownBuffer" id="buffer_markdown">
    - <property name="tag-table">table_markdown</property>
    <signal name="changed" handler="talkatu_demo_window_buffer_modified_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    - <object class="TalkatuTagTable" id="table_plain"/>
    <object class="TalkatuBuffer" id="buffer_plain">
    - <property name="tag-table">table_plain</property>
    <signal name="changed" handler="talkatu_demo_window_buffer_modified_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    <object class="TalkatuWholeBuffer" id="buffer_whole">
    - <property name="tag-table">table_plain</property>
    <property name="style">whole</property>
    <signal name="changed" handler="talkatu_demo_window_buffer_modified_cb" object="TalkatuDemoWindow" swapped="no"/>
    </object>
    --- a/demo/meson.build Sat Aug 13 22:03:08 2022 -0500
    +++ b/demo/meson.build Sun Aug 28 22:44:59 2022 -0500
    @@ -24,7 +24,7 @@
    sources,
    talkatudemo_resources,
    c_args : ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Talkatu-Demo"'],
    - dependencies: [GLIB, GTK3, talkatu_dep],
    + dependencies: [GLIB, GTK4, talkatu_dep],
    include_directories : top_srcdir,
    install: get_option('install-demo'),
    )
    --- a/demo/talkatudemowindow.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/demo/talkatudemowindow.c Sun Aug 28 22:44:59 2022 -0500
    @@ -28,14 +28,14 @@
    GtkWidget *editor;
    GtkWidget *typing;
    - GtkRadioToolButton *toggle_plain;
    - GtkRadioToolButton *toggle_whole;
    - GtkRadioToolButton *toggle_html;
    - GtkRadioToolButton *toggle_markdown;
    + GtkToggleButton *toggle_plain;
    + GtkToggleButton *toggle_whole;
    + GtkToggleButton *toggle_html;
    + GtkToggleButton *toggle_markdown;
    - GtkToggleToolButton *toggle_toolbar;
    - GtkToggleToolButton *toggle_send_button;
    - GtkToggleToolButton *toggle_edited;
    + GtkToggleButton *toggle_toolbar;
    + GtkToggleButton *toggle_send_button;
    + GtkToggleButton *toggle_edited;
    GtkTextBuffer *buffer_plain;
    GtkTextBuffer *buffer_whole;
    @@ -43,8 +43,6 @@
    GtkTextBuffer *buffer_markdown;
    GtkWidget *author_button;
    - GtkWidget *author_popover;
    - GtkWidget *author_item;
    };
    G_DEFINE_TYPE(TalkatuDemoWindow, talkatu_demo_window, GTK_TYPE_APPLICATION_WINDOW);
    @@ -56,11 +54,11 @@
    TalkatuDemoWindow *window = TALKATU_DEMO_WINDOW(data);
    if(response == GTK_RESPONSE_ACCEPT) {
    - gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
    + GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog));
    gchar *contents = NULL;
    gsize len;
    - if(g_file_get_contents(filename, &contents, &len, NULL)) {
    + if(g_file_load_contents(file, NULL, &contents, &len, NULL, NULL)) {
    GtkTextMark *mark = NULL;
    GtkTextIter iter;
    @@ -71,7 +69,7 @@
    g_free(contents);
    }
    - g_free(filename);
    + g_object_unref(file);
    }
    gtk_native_dialog_destroy(dialog);
    @@ -111,11 +109,11 @@
    TalkatuDemoWindow *window = TALKATU_DEMO_WINDOW(data);
    if(response == GTK_RESPONSE_ACCEPT) {
    - gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
    + GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog));
    gchar *contents = NULL;
    gsize len;
    - if(g_file_get_contents(filename, &contents, &len, NULL)) {
    + if(g_file_load_contents(file, NULL, &contents, &len, NULL, NULL)) {
    GtkTextMark *mark = NULL;
    GtkTextIter iter;
    @@ -126,7 +124,7 @@
    g_free(contents);
    }
    - g_free(filename);
    + g_object_unref(file);
    }
    gtk_native_dialog_destroy(dialog);
    @@ -164,16 +162,16 @@
    TalkatuDemoWindow *window = TALKATU_DEMO_WINDOW(data);
    GtkWidget *view = talkatu_editor_get_input(TALKATU_EDITOR(window->editor));
    - if(gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->toggle_plain))) {
    + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(window->toggle_plain))) {
    g_message("switching to plain buffer");
    gtk_text_view_set_buffer(GTK_TEXT_VIEW(view), window->buffer_plain);
    - } else if(gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->toggle_whole))) {
    + } else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(window->toggle_whole))) {
    g_message("switching to whole buffer");
    gtk_text_view_set_buffer(GTK_TEXT_VIEW(view), window->buffer_whole);
    - } else if(gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->toggle_html))) {
    + } else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(window->toggle_html))) {
    g_message("switching to html buffer");
    gtk_text_view_set_buffer(GTK_TEXT_VIEW(view), window->buffer_html);
    - } else if(gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->toggle_markdown))) {
    + } else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(window->toggle_markdown))) {
    g_message("switching to markdown buffer");
    gtk_text_view_set_buffer(GTK_TEXT_VIEW(view), window->buffer_markdown);
    }
    @@ -199,21 +197,10 @@
    }
    static void
    -talkatu_demo_window_view_open_url_cb(TalkatuView *view, const gchar *url, gpointer data) {
    - GError *error = NULL;
    - gboolean success = FALSE;
    -
    - success = gtk_show_uri_on_window(
    - GTK_WINDOW(data), url, GDK_CURRENT_TIME, &error);
    - if(!success) {
    - g_message(
    - "failed to open uri '%s': %s",
    - url,
    - (error) ? error->message : NULL
    - );
    -
    - g_error_free(error);
    - }
    +talkatu_demo_window_view_open_url_cb(G_GNUC_UNUSED TalkatuView *view,
    + const gchar *url, gpointer data)
    +{
    + gtk_show_uri(GTK_WINDOW(data), url, GDK_CURRENT_TIME);
    }
    static void
    @@ -236,69 +223,47 @@
    }
    static void
    -talkatu_demo_window_author_toggled_cb(GtkToolButton *button, gpointer data) {
    - GtkPopover *popover = GTK_POPOVER(data);
    -
    - gtk_popover_popup(popover);
    -}
    -
    -static void
    -talkatu_demo_window_author_popover_closed_cb(GtkPopover *popover, gpointer data) {
    - GtkToggleToolButton *button = GTK_TOGGLE_TOOL_BUTTON(data);
    +talkatu_demo_window_author_changed(GSimpleAction *action, GVariant *parameter,
    + gpointer data)
    +{
    + TalkatuDemoWindow *window = TALKATU_DEMO_WINDOW(data);
    + const gchar *author = NULL;
    + TalkatuEditor *editor = NULL;
    + GtkWidget *input = NULL;
    - gtk_toggle_tool_button_set_active(button, FALSE);
    -}
    -
    -static void
    -talkatu_demo_window_author_changed(GtkRadioButton *item, gpointer data) {
    - TalkatuDemoWindow *window = TALKATU_DEMO_WINDOW(data);
    + author = g_variant_get_string(parameter, NULL);
    + g_message("Changing author to %s", author);
    - if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(item))) {
    - TalkatuEditor *editor = TALKATU_EDITOR(window->editor);
    - GtkWidget *input = talkatu_editor_get_input(editor);
    + editor = TALKATU_EDITOR(window->editor);
    + input = talkatu_editor_get_input(editor);
    + talkatu_message_set_author(TALKATU_MESSAGE(input), author);
    - talkatu_message_set_author(TALKATU_MESSAGE(input),
    - gtk_button_get_label(GTK_BUTTON(item)));
    - }
    + g_action_change_state(G_ACTION(action), parameter);
    }
    static void
    -talkatu_demo_window_author_name_color_toggled_cb(GtkToolButton *button,
    - gpointer data)
    -{
    - GtkPopover *popover = GTK_POPOVER(data);
    -
    - gtk_popover_popup(popover);
    -}
    -
    -static void
    -talkatu_demo_window_author_name_color_popover_closed_cb(GtkPopover *popover,
    - gpointer data)
    -{
    - GtkToggleToolButton *button = GTK_TOGGLE_TOOL_BUTTON(data);
    -
    - gtk_toggle_tool_button_set_active(button, FALSE);
    -}
    -
    -static void
    -talkatu_demo_window_author_name_color_changed(GtkRadioButton *item,
    +talkatu_demo_window_author_name_color_changed(GSimpleAction *action,
    + GVariant *parameter,
    gpointer data)
    {
    TalkatuDemoWindow *window = TALKATU_DEMO_WINDOW(data);
    + const gchar *color_str = NULL;
    + TalkatuEditor *editor = NULL;
    + GtkWidget *input = NULL;
    + GdkRGBA color;
    - if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(item))) {
    - TalkatuEditor *editor = TALKATU_EDITOR(window->editor);
    - GtkWidget *input = talkatu_editor_get_input(editor);
    - GdkRGBA color;
    - const gchar *label = gtk_button_get_label(GTK_BUTTON(item));
    + color_str = g_variant_get_string(parameter, NULL);
    + g_message("Changing author name colour to %s", color_str);
    - if(gdk_rgba_parse(&color, label)) {
    - talkatu_message_set_author_name_color(TALKATU_MESSAGE(input),
    - &color);
    - } else {
    - talkatu_message_set_author_name_color(TALKATU_MESSAGE(input), NULL);
    - }
    + editor = TALKATU_EDITOR(window->editor);
    + input = talkatu_editor_get_input(editor);
    + if(gdk_rgba_parse(&color, color_str)) {
    + talkatu_message_set_author_name_color(TALKATU_MESSAGE(input), &color);
    + } else {
    + talkatu_message_set_author_name_color(TALKATU_MESSAGE(input), NULL);
    }
    +
    + g_action_change_state(G_ACTION(action), parameter);
    }
    /******************************************************************************
    @@ -306,12 +271,16 @@
    *****************************************************************************/
    static void
    talkatu_demo_window_init(TalkatuDemoWindow *window) {
    - gtk_widget_init_template(GTK_WIDGET(window));
    + const GActionEntry entries[] = {
    + { "author-name", talkatu_demo_window_author_changed, "s", "'Alice'" },
    + { "author-name-color", talkatu_demo_window_author_name_color_changed,
    + "s", "''" }
    + };
    - /* activate the first menu item to make sure its label gets stored
    - * correctly.
    - */
    - talkatu_demo_window_author_changed(GTK_RADIO_BUTTON(window->author_item), window);
    + g_action_map_add_action_entries(G_ACTION_MAP(window), entries,
    + G_N_ELEMENTS(entries), window);
    +
    + gtk_widget_init_template(GTK_WIDGET(window));
    g_object_bind_property(
    window->editor, "show-toolbar",
    @@ -356,8 +325,6 @@
    gtk_widget_class_bind_template_child(widget_class, TalkatuDemoWindow, toggle_markdown);
    gtk_widget_class_bind_template_child(widget_class, TalkatuDemoWindow, author_button);
    - gtk_widget_class_bind_template_child(widget_class, TalkatuDemoWindow, author_popover);
    - gtk_widget_class_bind_template_child(widget_class, TalkatuDemoWindow, author_item);
    gtk_widget_class_bind_template_child(widget_class, TalkatuDemoWindow, toggle_toolbar);
    gtk_widget_class_bind_template_child(widget_class, TalkatuDemoWindow, toggle_send_button);
    @@ -371,13 +338,9 @@
    gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_insert_html_cb);
    gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_insert_markdown_cb);
    - gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_author_toggled_cb);
    - gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_author_popover_closed_cb);
    - gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_author_changed);
    -
    - gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_author_name_color_toggled_cb);
    - gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_author_name_color_popover_closed_cb);
    - gtk_widget_class_bind_template_callback(widget_class, talkatu_demo_window_author_name_color_changed);
    + gtk_widget_class_add_binding_action(widget_class,
    + GDK_KEY_w, GDK_CONTROL_MASK,
    + "window.close", NULL);
    }
    /******************************************************************************
    --- a/meson.build Sat Aug 13 22:03:08 2022 -0500
    +++ b/meson.build Sun Aug 28 22:44:59 2022 -0500
    @@ -37,8 +37,7 @@
    GLIB = dependency('glib-2.0', version : '>=2.52.0')
    GOBJECT = dependency('gobject-2.0')
    -GTK3 = dependency('gtk+-3.0', version : '>=3.24.0')
    -GSPELL = dependency('gspell-1', version : '>=1.2')
    +GTK4 = dependency('gtk4', version : '>=4.0.0')
    GUMBO = dependency('gumbo', version : '>=0.10')
    --- a/po/POTFILES Sat Aug 13 22:03:08 2022 -0500
    +++ b/po/POTFILES Sun Aug 28 22:44:59 2022 -0500
    @@ -5,13 +5,18 @@
    talkatu/data/attachmentpreview.ui
    talkatu/data/editor.ui
    talkatu/data/historyrow.ui
    +talkatu/data/history.ui
    +talkatu/data/input.ui
    talkatu/data/linkdialog.ui
    -talkatu/data/messageactions.ui
    +talkatu/data/scrolledwindow.ui
    talkatu/data/toolbar.ui
    +talkatu/data/typinglabel.ui
    +talkatu/data/view.ui
    talkatu/talkatuactiongroup.c
    talkatu/talkatuattachment.c
    talkatu/talkatuattachmentdialog.c
    talkatu/talkatuattachmentpreview.c
    +talkatu/talkatuautoscroller.c
    talkatu/talkatubuffer.c
    talkatu/talkatucodeset.c
    talkatu/talkatucore.c
    @@ -24,15 +29,11 @@
    talkatu/talkatulinkdialog.c
    talkatu/talkatumarkdownbuffer.c
    talkatu/talkatumarkup.c
    -talkatu/talkatumenutoolbutton.c
    -talkatu/talkatumessageactions.c
    talkatu/talkatumessage.c
    -talkatu/talkatuscrolledwindow.c
    talkatu/talkatusimpleattachment.c
    talkatu/talkatutag.c
    talkatu/talkatutagtable.c
    talkatu/talkatutoolbar.c
    -talkatu/talkatutooldrawer.c
    talkatu/talkatutypinglabel.c
    talkatu/talkatuview.c
    talkatu/talkatuwholebuffer.c
    --- a/talkatu/data/attachmentdialog.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/attachmentdialog.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -19,102 +19,34 @@
    -->
    <interface>
    - <requires lib="gtk+" version="3.18"/>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    <!-- interface-name Talkatu -->
    <!-- interface-description GTK widgets for chat applications -->
    <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    <template class="TalkatuAttachmentDialog" parent="GtkDialog">
    - <property name="can_focus">False</property>
    - <property name="border_width">12</property>
    - <property name="modal">True</property>
    - <property name="type_hint">dialog</property>
    - <child>
    - <placeholder/>
    - </child>
    - <child internal-child="vbox">
    + <property name="modal">1</property>
    + <child internal-child="content_area">
    <object class="GtkBox">
    - <property name="can_focus">False</property>
    <property name="orientation">vertical</property>
    <property name="spacing">2</property>
    - <child internal-child="action_area">
    - <object class="GtkButtonBox">
    - <property name="can_focus">False</property>
    - <property name="layout_style">end</property>
    - <child>
    - <object class="GtkButton" id="cancel">
    - <property name="label" translatable="yes">Cancel</property>
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="receives_default">True</property>
    - </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkButton" id="upload">
    - <property name="label" translatable="yes">Upload</property>
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="can_default">True</property>
    - <property name="has_default">True</property>
    - <property name="receives_default">True</property>
    - <accelerator key="Return" signal="clicked"/>
    - </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    - </child>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">False</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    <child>
    <object class="GtkImage" id="preview">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="pixel_size">256</property>
    <property name="icon_name">text-x-generic-template</property>
    - <property name="icon_size">6</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    <child>
    <object class="GtkLabel" id="filename">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="label" translatable="yes">&lt;filename&gt;</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">2</property>
    - </packing>
    </child>
    <child>
    <object class="GtkEntry" id="comment">
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="has_focus">True</property>
    + <property name="activates_default">1</property>
    + <property name="focusable">1</property>
    <property name="placeholder_text" translatable="yes">Comment (optional)</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">3</property>
    - </packing>
    </child>
    </object>
    </child>
    @@ -122,5 +54,18 @@
    <action-widget response="-6">cancel</action-widget>
    <action-widget response="-3">upload</action-widget>
    </action-widgets>
    + <child type="action">
    + <object class="GtkButton" id="cancel">
    + <property name="label" translatable="yes">Cancel</property>
    + <property name="focusable">1</property>
    + </object>
    + </child>
    + <child type="action">
    + <object class="GtkButton" id="upload">
    + <property name="label" translatable="yes">Upload</property>
    + <property name="focusable">1</property>
    + <property name="receives_default">1</property>
    + </object>
    + </child>
    </template>
    </interface>
    --- a/talkatu/data/attachmentpreview.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/attachmentpreview.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -19,112 +19,62 @@
    -->
    <interface>
    - <requires lib="gtk+" version="3.18"/>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    <!-- interface-name Talkatu -->
    <!-- interface-description GTK widgets for chat applications -->
    <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    - <template class="TalkatuAttachmentPreview" parent="GtkInfoBar">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <child internal-child="action_area">
    - <object class="GtkButtonBox">
    - <property name="can_focus">False</property>
    - <property name="spacing">6</property>
    - <property name="layout_style">end</property>
    - <child>
    - <object class="GtkButton">
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="receives_default">True</property>
    - <property name="relief">none</property>
    - <signal name="clicked" handler="talkatu_attachment_preview_download_cb" object="TalkatuAttachmentPreview" swapped="no"/>
    + <template class="TalkatuAttachmentPreview" parent="GtkWidget">
    + <child>
    + <object class="GtkInfoBar">
    + <property name="hexpand">1</property>
    + <signal name="response" handler="talkatu_attachment_preview_download_cb" object="TalkatuAttachmentPreview" swapped="no"/>
    + <child type="action">
    + <object class="GtkButton" id="download_button">
    <child>
    <object class="GtkImage">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="icon_name">document-save-symbolic</property>
    - <property name="icon_size">5</property>
    </object>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">False</property>
    - <property name="position">0</property>
    - <property name="non_homogeneous">True</property>
    - </packing>
    </child>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">False</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child internal-child="content_area">
    - <object class="GtkBox">
    - <property name="can_focus">False</property>
    - <property name="spacing">16</property>
    - <child>
    - <object class="GtkImage" id="preview">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="icon_name">text-x-generic</property>
    - <property name="icon_size">6</property>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    + <action-widgets>
    + <action-widget response="0">download_button</action-widget>
    + </action-widgets>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="orientation">vertical</property>
    + <property name="can_focus">0</property>
    + <property name="spacing">16</property>
    <child>
    - <object class="GtkLabel" id="filename">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="label" translatable="yes">unknown</property>
    - <property name="xalign">0</property>
    + <object class="GtkImage" id="preview">
    + <property name="can_focus">0</property>
    + <property name="icon_name">text-x-generic</property>
    + <property name="icon_size">large</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkLabel" id="filesize">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="xalign">0</property>
    + <object class="GtkBox">
    + <property name="hexpand">1</property>
    + <property name="can_focus">0</property>
    + <property name="orientation">vertical</property>
    + <child>
    + <object class="GtkLabel" id="filename">
    + <property name="can_focus">0</property>
    + <property name="label" translatable="yes">unknown</property>
    + <property name="xalign">0</property>
    + </object>
    + </child>
    + <child>
    + <object class="GtkLabel" id="filesize">
    + <property name="can_focus">0</property>
    + <property name="xalign">0</property>
    + </object>
    + </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">False</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child>
    - <placeholder/>
    </child>
    </template>
    </interface>
    --- a/talkatu/data/editor.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/editor.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -1,8 +1,7 @@
    <?xml version="1.0" encoding="UTF-8"?>
    -<!-- Generated with glade 3.22.1
    -
    +<!--
    Talkatu - GTK widgets for chat applications
    -Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
    +Copyright (C) 2017-2022 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
    @@ -16,82 +15,49 @@
    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="Talkatu" version="0.0"/>
    - <requires lib="gtk+" version="3.20"/>
    - <object class="TalkatuTagTable" id="table"/>
    - <object class="TalkatuBuffer" id="buffer">
    - <property name="tag_table">table</property>
    - </object>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    - <!-- interface-name Talkatu -->
    - <!-- interface-description GTK widgets for chat applications -->
    - <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    + <!-- interface-name Pidgin -->
    + <!-- interface-description Internet Messenger -->
    + <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
    + <object class="TalkatuBuffer" id="buffer"/>
    <template class="TalkatuEditor" parent="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="orientation">vertical</property>
    <child>
    - <object class="TalkatuToolbar" id="toolbar">
    - <property name="visible">True</property>
    - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    + <object class="TalkatuToolbar" id="toolbar"/>
    </child>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <property name="vexpand">1</property>
    <property name="spacing">4</property>
    <child>
    <object class="GtkScrolledWindow">
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    + <property name="hexpand">1</property>
    + <property name="focusable">1</property>
    <property name="hscrollbar_policy">never</property>
    - <property name="shadow_type">in</property>
    <child>
    <object class="TalkatuInput" id="input">
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    + <property name="can_focus">1</property>
    <property name="wrap_mode">word</property>
    <property name="buffer">buffer</property>
    <signal name="notify::buffer" handler="talkatu_editor_view_buffer_changed_handler" object="TalkatuEditor" swapped="no"/>
    </object>
    </child>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkButton" id="send_button">
    + <property name="visible">0</property>
    <property name="label" translatable="yes">Send</property>
    - <property name="can_focus">True</property>
    - <property name="receives_default">True</property>
    - <property name="no_show_all">True</property>
    + <property name="focusable">1</property>
    + <property name="receives_default">1</property>
    <signal name="clicked" handler="talkatu_editor_send_button_clicked_cb" object="TalkatuEditor" swapped="no"/>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </template>
    </interface>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/talkatu/data/history.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -0,0 +1,31 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--
    +Talkatu - GTK widgets for chat applications
    +Copyright (C) 2017-2022 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 Talkatu -->
    + <!-- interface-description GTK widgets for chat applications -->
    + <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    + <template class="TalkatuHistory" parent="GtkWidget">
    + <property name="height-request">120</property>
    + <child>
    + <object class="GtkListBox" id="list_box"/>
    + </child>
    + </template>
    +</interface>
    --- a/talkatu/data/historyrow.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/historyrow.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -19,112 +19,67 @@
    -->
    <interface>
    - <requires lib="gtk+" version="3.18"/>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    <!-- interface-name Talkatu -->
    <!-- interface-description GTK widgets for chat applications -->
    <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    <template class="TalkatuHistoryRow" parent="GtkListBoxRow">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <child>
    + <property name="can-focus">0</property>
    + <property name="child">
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <property name="can-focus">0</property>
    <child>
    - <object class="GtkEventBox" id="avatar_event">
    - <property name="can-focus">False</property>
    - <child>
    - <object class="GtkImage" id="avatar_image">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <property name="icon-name">image-missing</property>
    - </object>
    - </child>
    + <object class="GtkImage" id="avatar_image">
    + <property name="can-focus">0</property>
    + <property name="icon-name">image-missing</property>
    + <property name="visible">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <property name="hexpand">1</property>
    + <property name="can-focus">0</property>
    <property name="orientation">vertical</property>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <property name="can-focus">0</property>
    <property name="spacing">8</property>
    <child>
    <object class="GtkLabel" id="author">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <property name="can-focus">0</property>
    <property name="label" translatable="yes">Author</property>
    - <property name="use-markup">True</property>
    + <property name="use-markup">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkLabel" id="timestamp">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    + <property name="can-focus">0</property>
    <property name="label" translatable="yes">12:34PM</property>
    - <property name="use-markup">True</property>
    + <property name="use-markup">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    <child>
    <object class="GtkLabel" id="edited">
    - <property name="can-focus">False</property>
    + <property name="visible">0</property>
    + <property name="can-focus">0</property>
    <property name="label" translatable="yes">(edited)</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">2</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkLabel" id="content">
    - <property name="visible">True</property>
    - <property name="can-focus">False</property>
    - <property name="wrap">True</property>
    + <property name="can-focus">0</property>
    + <property name="wrap">1</property>
    <property name="wrap-mode">word-char</property>
    <property name="xalign">0</property>
    <property name="yalign">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - </child>
    + </property>
    </template>
    </interface>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/talkatu/data/input.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -0,0 +1,43 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--
    +Talkatu - GTK widgets for chat applications
    +Copyright (C) 2017-2022 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">_Send Message</attribute>
    + <attribute name="action">message.send</attribute>
    + </item>
    + </section>
    + </menu>
    + <template class="TalkatuInput" parent="TalkatuView">
    + <property name="extra-menu">model</property>
    + <signal name="notify::buffer" handler="talkatu_input_buffer_set_cb"/>
    + <child>
    + <object class="GtkEventControllerKey">
    + <signal name="key-pressed" handler="talkatu_input_key_pressed_cb" object="TalkatuInput" swapped="no"/>
    + </object>
    + </child>
    + </template>
    +</interface>
    --- a/talkatu/data/linkdialog.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/linkdialog.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -19,180 +19,74 @@
    -->
    <interface>
    - <requires lib="gtk+" version="3.10"/>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    <!-- interface-name Talkatu -->
    <!-- interface-description GTK widgets for chat applications -->
    <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    <template class="TalkatuLinkDialog" parent="GtkDialog">
    - <property name="can_focus">False</property>
    <property name="title" translatable="yes">Insert Link</property>
    - <property name="resizable">False</property>
    - <property name="type_hint">dialog</property>
    - <child>
    - <placeholder/>
    - </child>
    - <child internal-child="vbox">
    + <property name="resizable">0</property>
    + <child internal-child="content_area">
    <object class="GtkBox">
    - <property name="can_focus">False</property>
    <property name="orientation">vertical</property>
    <property name="spacing">2</property>
    - <child internal-child="action_area">
    - <object class="GtkButtonBox">
    - <property name="can_focus">False</property>
    - <property name="layout_style">end</property>
    - <child>
    - <object class="GtkButton" id="cancel">
    - <property name="label" translatable="yes">Cancel</property>
    - <property name="name">cancel</property>
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="receives_default">True</property>
    - </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkButton" id="insert">
    - <property name="label" translatable="yes">Insert</property>
    - <property name="name">insert</property>
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    - <property name="can_default">True</property>
    - <property name="has_default">True</property>
    - <property name="receives_default">True</property>
    - </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    - </child>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">False</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="orientation">vertical</property>
    <property name="spacing">4</property>
    <child>
    <object class="GtkLabel">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="label" translatable="yes">&lt;span font-size="large" font-weight="bold"&gt;Insert Link&lt;/span&gt;</property>
    - <property name="use_markup">True</property>
    + <property name="label" translatable="yes">&lt;span font-size=&quot;large&quot; font-weight=&quot;bold&quot;&gt;Insert Link&lt;/span&gt;</property>
    + <property name="use_markup">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkLabel">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="label" translatable="yes">Please enter the URL and optional display text you would like to insert.</property>
    - <property name="wrap">True</property>
    + <property name="wrap">1</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="spacing">2</property>
    <child>
    <object class="GtkLabel" id="url_label">
    <property name="name">url_label</property>
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="label" translatable="yes">URL:</property>
    <property name="xalign">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkEntry" id="url">
    + <property name="hexpand">1</property>
    <property name="name">url</property>
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    + <property name="focusable">1</property>
    <property name="input_purpose">url</property>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">2</property>
    - </packing>
    </child>
    <child>
    <object class="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="spacing">2</property>
    <child>
    <object class="GtkLabel" id="display_label">
    <property name="name">display_label</property>
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    <property name="label" translatable="yes">Display Text:</property>
    <property name="xalign">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    </child>
    <child>
    <object class="GtkEntry" id="display">
    + <property name="hexpand">1</property>
    <property name="name">display</property>
    - <property name="visible">True</property>
    - <property name="can_focus">True</property>
    + <property name="focusable">1</property>
    </object>
    - <packing>
    - <property name="expand">True</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">3</property>
    - </packing>
    </child>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    </child>
    </object>
    </child>
    @@ -200,6 +94,22 @@
    <action-widget response="-6">cancel</action-widget>
    <action-widget response="-3">insert</action-widget>
    </action-widgets>
    + <child type="action">
    + <object class="GtkButton" id="cancel">
    + <property name="label" translatable="yes">Cancel</property>
    + <property name="name">cancel</property>
    + <property name="focusable">1</property>
    + <property name="receives_default">1</property>
    + </object>
    + </child>
    + <child type="action">
    + <object class="GtkButton" id="insert">
    + <property name="label" translatable="yes">Insert</property>
    + <property name="name">insert</property>
    + <property name="focusable">1</property>
    + <property name="receives_default">1</property>
    + </object>
    + </child>
    </template>
    <object class="GtkSizeGroup">
    <widgets>
    --- a/talkatu/data/messageactions.ui Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,67 +0,0 @@
    -<?xml version="1.0" encoding="UTF-8"?>
    -<!-- Generated with glade 3.22.1
    -
    -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="3.20"/>
    - <!-- interface-license-type gplv2 -->
    - <!-- interface-name Talkatu -->
    - <!-- interface-description GTK widgets for chat applications -->
    - <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    - <template class="TalkatuMessageActions" parent="GtkBox">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <child>
    - <object class="GtkEventBox" id="reactions">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <child>
    - <object class="GtkImage">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="icon_name">face-smile</property>
    - </object>
    - </child>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">0</property>
    - </packing>
    - </child>
    - <child>
    - <object class="GtkEventBox" id="actions">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <child>
    - <object class="GtkImage">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    - <property name="icon_name">open-menu-symbolic</property>
    - </object>
    - </child>
    - </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="fill">True</property>
    - <property name="position">1</property>
    - </packing>
    - </child>
    - </template>
    -</interface>
    --- a/talkatu/data/talkatu.gresource.xml Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/talkatu.gresource.xml Sun Aug 28 22:44:59 2022 -0500
    @@ -4,9 +4,12 @@
    <file compressed="true">attachmentdialog.ui</file>
    <file compressed="true">attachmentpreview.ui</file>
    <file compressed="true">editor.ui</file>
    + <file compressed="true">history.ui</file>
    <file compressed="true">historyrow.ui</file>
    + <file compressed="true">input.ui</file>
    <file compressed="true">linkdialog.ui</file>
    - <file compressed="true">messageactions.ui</file>
    <file compressed="true">toolbar.ui</file>
    + <file compressed="true">typinglabel.ui</file>
    + <file compressed="true">view.ui</file>
    </gresource>
    </gresources>
    --- a/talkatu/data/toolbar.ui Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/data/toolbar.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -19,189 +19,124 @@
    -->
    <interface>
    - <requires lib="gtk+" version="3.20"/>
    + <requires lib="gtk" version="4.0"/>
    <!-- interface-license-type gplv2 -->
    <!-- interface-name Talkatu -->
    <!-- interface-description GTK widgets for chat applications -->
    <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    - <template class="TalkatuToolbar" parent="GtkToolbar">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <template class="TalkatuToolbar" parent="GtkBox">
    + <style>
    + <class name="toolbar"/>
    + </style>
    + <property name="can_focus">0</property>
    <child>
    - <object class="GtkToggleToolButton" id="format_bold">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkToggleButton" id="format_bold">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-bold</property>
    <property name="label" translatable="yes">Bold</property>
    <property name="icon_name">format-text-bold</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="format_italic">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkToggleButton" id="format_italic">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-italic</property>
    <property name="label" translatable="yes">Italic</property>
    <property name="icon_name">format-text-italic</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="format_underline">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkToggleButton" id="format_underline">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-underline</property>
    <property name="label" translatable="yes">Underline</property>
    <property name="icon_name">format-text-underline</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="format_strikethrough">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkToggleButton" id="format_strikethrough">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-strikethrough</property>
    <property name="label" translatable="yes">Strikethrough</property>
    <property name="icon_name">format-text-strikethrough</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkSeparatorToolItem">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkSeparator">
    + <property name="orientation">vertical</property>
    + <property name="can_focus">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton" id="format_larger">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkButton" id="format_larger">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-grow</property>
    <property name="label" translatable="yes">Increase Font Size</property>
    <property name="icon_name">zoom-in</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton" id="format_smaller">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkButton" id="format_smaller">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-shrink</property>
    <property name="label" translatable="yes">Decrease Font Size</property>
    <property name="icon_name">zoom-out</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkSeparatorToolItem">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkSeparator">
    + <property name="orientation">vertical</property>
    + <property name="can_focus">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton" id="format_reset">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkButton" id="format_reset">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.format-reset</property>
    <property name="label" translatable="yes">Clear Formatting</property>
    <property name="icon_name">edit-clear</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkSeparatorToolItem">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkSeparator">
    + <property name="orientation">vertical</property>
    + <property name="can_focus">0</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToggleToolButton" id="insert_link">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkToggleButton" id="insert_link">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.insert-link</property>
    <property name="label" translatable="yes">Insert Link</property>
    - <property name="use_underline">True</property>
    + <property name="use_underline">1</property>
    <property name="icon_name">insert-link</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton" id="insert_file">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkButton" id="insert_file">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.attach-file</property>
    <property name="label" translatable="yes">Attach File</property>
    <property name="icon_name">insert-object</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton" id="insert_code">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkButton" id="insert_code">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.insert-code</property>
    <property name="label" translatable="yes">Insert Code</property>
    - <property name="use_underline">True</property>
    + <property name="use_underline">1</property>
    <property name="icon_name">insert-text</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    <child>
    - <object class="GtkToolButton" id="insert_emoji">
    - <property name="visible">True</property>
    - <property name="can_focus">False</property>
    + <object class="GtkButton" id="insert_emoji">
    + <property name="can_focus">0</property>
    <property name="action_name">talkatu.insert-emoji</property>
    <property name="label" translatable="yes">Insert Emoji</property>
    - <property name="use_underline">True</property>
    + <property name="use_underline">1</property>
    <property name="icon_name">face-smile-big</property>
    </object>
    - <packing>
    - <property name="expand">False</property>
    - <property name="homogeneous">True</property>
    - </packing>
    </child>
    </template>
    </interface>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/talkatu/data/typinglabel.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -0,0 +1,33 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--
    +Talkatu - GTK widgets for chat applications
    +Copyright (C) 2017-2022 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 Talkatu -->
    + <!-- interface-description GTK widgets for chat applications -->
    + <!-- interface-copyright Gary Kramlich <grim@reaperworld.com> -->
    + <template class="TalkatuTypingLabel" parent="GtkWidget">
    + <child>
    + <object class="GtkLabel" id="label">
    + <property name="xalign">0</property>
    + </object>
    + </child>
    + </template>
    +</interface>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/talkatu/data/view.ui Sun Aug 28 22:44:59 2022 -0500
    @@ -0,0 +1,62 @@
    +<?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">
    + <property name="has-tooltip">1</property>
    + <property name="wrap-mode">3</property>
    +
    + <signal name="notify::buffer" handler="talkatu_view_buffer_set_cb"/>
    + <child>
    + <object class="GtkPopoverMenu" id="menu">
    + <property name="has-arrow">0</property>
    + <property name="menu-model">model</property>
    + </object>
    + </child>
    + <child>
    + <object class="GtkGestureClick">
    + <property name="button">0</property>
    + <signal name="pressed" handler="talkatu_view_pressed_cb"/>
    + </object>
    + </child>
    + <child>
    + <object class="GtkEventControllerMotion">
    + <signal name="motion" handler="talkatu_view_motion_cb"/>
    + </object>
    + </child>
    + </template>
    +</interface>
    --- a/talkatu/meson.build Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/meson.build Sun Aug 28 22:44:59 2022 -0500
    @@ -8,6 +8,7 @@
    'talkatuattachment.h',
    'talkatuattachmentdialog.h',
    'talkatuattachmentpreview.h',
    + 'talkatuautoscroller.h',
    'talkatubuffer.h',
    'talkatucodeset.h',
    'talkatucore.h',
    @@ -21,15 +22,11 @@
    'talkatulinkdialog.h',
    'talkatumarkdownbuffer.h',
    'talkatumarkup.h',
    - 'talkatumenutoolbutton.h',
    'talkatumessage.h',
    - 'talkatumessageactions.h',
    - 'talkatuscrolledwindow.h',
    'talkatusimpleattachment.h',
    'talkatutag.h',
    'talkatutagtable.h',
    'talkatutoolbar.h',
    - 'talkatutooldrawer.h',
    'talkatutypinglabel.h',
    'talkatuview.h',
    'talkatuwholebuffer.h',
    @@ -40,6 +37,7 @@
    'talkatuattachment.c',
    'talkatuattachmentdialog.c',
    'talkatuattachmentpreview.c',
    + 'talkatuautoscroller.c',
    'talkatubuffer.c',
    'talkatucodeset.c',
    'talkatucore.c',
    @@ -53,15 +51,11 @@
    'talkatulinkdialog.c',
    'talkatumarkdownbuffer.c',
    'talkatumarkup.c',
    - 'talkatumenutoolbutton.c',
    'talkatumessage.c',
    - 'talkatumessageactions.c',
    - 'talkatuscrolledwindow.c',
    'talkatusimpleattachment.c',
    'talkatutag.c',
    'talkatutagtable.c',
    'talkatutoolbar.c',
    - 'talkatutooldrawer.c',
    'talkatutypinglabel.c',
    'talkatuview.c',
    'talkatuwholebuffer.c',
    @@ -177,7 +171,7 @@
    ###############################################################################
    # Library target
    ###############################################################################
    -talkatu = shared_library('talkatu',
    +talkatu = library('talkatu',
    TALKATU_SOURCES,
    TALKATU_PUBLIC_BUILT_SOURCES,
    TALKATU_PRIVATE_SOURCES,
    @@ -189,7 +183,7 @@
    talkatu_resources,
    c_args : ['-DTALKATU_COMPILATION', '-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Talkatu"'],
    include_directories : toplevel_inc,
    - dependencies : [CMARK, GLIB, GOBJECT, GUMBO, GTK3, GSPELL],
    + dependencies : [CMARK, GLIB, GOBJECT, GUMBO, GTK4],
    version : TALKATU_LIBRARY_VERSION,
    install : true
    )
    @@ -203,7 +197,7 @@
    filebase : 'talkatu',
    subdirs : 'talkatu-1.0',
    libraries : talkatu,
    - requires : ['glib-2.0', 'gobject-2.0', 'gtk+-3.0'],
    + requires : ['glib-2.0', 'gobject-2.0', 'gtk4'],
    )
    ###############################################################################
    @@ -217,7 +211,7 @@
    export_packages : ['talkatu'],
    extra_args : ['--quiet', '--warn-all', '-DTALKATU_COMPILATION'],
    header : 'talkatu/talkatu.h',
    - includes : ['GModule-2.0', 'GObject-2.0', 'Gtk-3.0'],
    + includes : ['GModule-2.0', 'GObject-2.0', 'Gtk-4.0'],
    install : true,
    namespace : 'Talkatu',
    nsversion : '@0@.0'.format(TALKATU_MAJOR_VERSION),
    @@ -233,7 +227,7 @@
    include_directories : [toplevel_inc, talkatu_inc],
    link_with : talkatu,
    sources : TALKATU_PUBLIC_BUILT_HEADERS + TALKATU_GENERATED_TARGETS,
    - dependencies : [GLIB, GOBJECT, GTK3]
    + dependencies : [GLIB, GOBJECT, GTK4]
    )
    if meson.version().version_compare('>=0.54.0')
    --- a/talkatu/talkatuactiongroup.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuactiongroup.c Sun Aug 28 22:44:59 2022 -0500
    @@ -347,7 +347,7 @@
    /* we call this separately for GTK_RESPONSE_CANCEL because
    * GTK_RESPONSE_DELETE_EVENT already destroys the dialog.
    */
    - gtk_widget_destroy(GTK_WIDGET(dialog));
    + gtk_window_destroy(GTK_WINDOW(dialog));
    } else if(response == GTK_RESPONSE_ACCEPT) {
    TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
    TalkatuActionGroupPrivate *priv = NULL;
    @@ -369,29 +369,28 @@
    /* Send the message! */
    talkatu_input_send_message(priv->input);
    - gtk_widget_destroy(GTK_WIDGET(dialog));
    + gtk_window_destroy(GTK_WINDOW(dialog));
    }
    +
    + g_object_unref(dialog);
    }
    static void
    talkatu_action_attach_file_response_cb(GtkDialog *dialog, gint response,
    gpointer data)
    {
    - if(response == GTK_RESPONSE_CANCEL) {
    - /* we call this separately for GTK_RESPONSE_CANCEL because
    - * GTK_RESPONSE_DELETE_EVENT already destroys the dialog.
    - */
    - gtk_widget_destroy(GTK_WIDGET(dialog));
    - } else if(response == GTK_RESPONSE_ACCEPT) {
    + if(response == GTK_RESPONSE_ACCEPT) {
    TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
    TalkatuActionGroupPrivate *priv = NULL;
    TalkatuAttachment *attachment = NULL;
    GtkWidget *attach_dialog = NULL;
    + GFile *file = NULL;
    gchar *filename = NULL, *content_type = NULL, *comment = NULL;
    priv = talkatu_action_group_get_instance_private(ag);
    - filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
    + file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog));
    + filename = g_file_get_path(file);
    content_type = g_content_type_guess(filename, NULL, 0, NULL);
    attachment = talkatu_simple_attachment_new(G_GUINT64_CONSTANT(0),
    @@ -400,8 +399,7 @@
    talkatu_attachment_set_local_uri(attachment, filename);
    g_free(filename);
    -
    - gtk_widget_destroy(GTK_WIDGET(dialog));
    + g_object_unref(file);
    comment = talkatu_markup_get_html(priv->buffer, NULL);
    attach_dialog = talkatu_attachment_dialog_new(attachment, comment);
    @@ -410,8 +408,11 @@
    g_signal_connect(G_OBJECT(attach_dialog), "response",
    G_CALLBACK(talkatu_action_attach_file_attach_response_cb),
    data);
    - gtk_widget_show_all(attach_dialog);
    + gtk_widget_show(attach_dialog);
    }
    +
    + gtk_native_dialog_destroy(GTK_NATIVE_DIALOG(dialog));
    + g_object_unref(dialog);
    }
    static void
    @@ -420,34 +421,32 @@
    {
    TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
    TalkatuActionGroupPrivate *priv = NULL;
    - GtkWidget *dialog = NULL;
    + GtkFileChooserNative *dialog = NULL;
    priv = talkatu_action_group_get_instance_private(ag);
    g_return_if_fail(TALKATU_IS_INPUT(priv->input));
    - dialog = gtk_file_chooser_dialog_new(_("Attach file..."),
    + dialog = gtk_file_chooser_native_new(_("Attach file..."),
    NULL,
    GTK_FILE_CHOOSER_ACTION_OPEN,
    - _("Cancel"), GTK_RESPONSE_CANCEL,
    - _("Open"), GTK_RESPONSE_ACCEPT,
    - NULL);
    + _("Open"),
    + _("Cancel"));
    g_signal_connect(G_OBJECT(dialog), "response",
    G_CALLBACK(talkatu_action_attach_file_response_cb),
    ag);
    - gtk_widget_show_all(dialog);
    + gtk_native_dialog_show(GTK_NATIVE_DIALOG(dialog));
    }
    static void
    -talkatu_action_insert_link(GSimpleAction *action, GVariant *parameter,
    - gpointer data)
    +talkatu_action_insert_link_response_cb(GtkDialog *dialog, gint response,
    + gpointer data)
    {
    TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
    TalkatuActionGroupPrivate *priv = NULL;
    - GtkWidget *link_dialog = talkatu_link_dialog_new();
    priv = talkatu_action_group_get_instance_private(ag);
    - if(gtk_dialog_run(GTK_DIALOG(link_dialog)) == GTK_RESPONSE_ACCEPT) {
    + if(response == GTK_RESPONSE_ACCEPT) {
    GtkTextTagTable *table = gtk_text_buffer_get_tag_table(priv->buffer);
    GtkTextTag *anchor, *anchor_data;
    GtkTextMark *insert_mark = NULL;
    @@ -460,8 +459,8 @@
    }
    /* grab our inputs from the dialog */
    - url = talkatu_link_dialog_get_url(TALKATU_LINK_DIALOG(link_dialog));
    - label = talkatu_link_dialog_get_display_text(TALKATU_LINK_DIALOG(link_dialog));
    + url = talkatu_link_dialog_get_url(TALKATU_LINK_DIALOG(dialog));
    + label = talkatu_link_dialog_get_display_text(TALKATU_LINK_DIALOG(dialog));
    /* find the anchor tag from the table */
    anchor = gtk_text_tag_table_lookup(table, TALKATU_TAG_ANCHOR);
    @@ -472,7 +471,8 @@
    * bother with that.
    */
    anchor_data = gtk_text_tag_new(NULL);
    - g_object_set_data_full(G_OBJECT(anchor_data), "talkatu-anchor-url", url, g_free);
    + g_object_set_data_full(G_OBJECT(anchor_data), "talkatu-anchor-url",
    + url, g_free);
    gtk_text_tag_table_add(table, anchor_data);
    insert_mark = gtk_text_buffer_get_insert(priv->buffer);
    @@ -491,7 +491,21 @@
    g_free(label);
    }
    - gtk_widget_destroy(link_dialog);
    + gtk_window_destroy(GTK_WINDOW(dialog));
    + g_object_unref(dialog);
    +}
    +
    +static void
    +talkatu_action_insert_link(GSimpleAction *action, GVariant *parameter,
    + gpointer data)
    +{
    + GtkWidget *link_dialog = talkatu_link_dialog_new();
    +
    + g_signal_connect(G_OBJECT(link_dialog), "response",
    + G_CALLBACK(talkatu_action_insert_link_response_cb),
    + data);
    + gtk_window_set_modal(GTK_WINDOW(link_dialog), TRUE);
    + gtk_widget_show(link_dialog);
    }
    /******************************************************************************
    --- a/talkatu/talkatuattachmentdialog.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuattachmentdialog.c Sun Aug 28 22:44:59 2022 -0500
    @@ -57,8 +57,7 @@
    gchar *filename = NULL;
    if(G_IS_ICON(preview)) {
    - gtk_image_set_from_gicon(GTK_IMAGE(dialog->preview), preview,
    - GTK_ICON_SIZE_DIALOG);
    + gtk_image_set_from_gicon(GTK_IMAGE(dialog->preview), preview);
    g_object_unref(G_OBJECT(preview));
    }
    @@ -77,7 +76,7 @@
    const gchar *comment)
    {
    if(GTK_IS_ENTRY(dialog->comment)) {
    - gtk_entry_set_text(GTK_ENTRY(dialog->comment), comment);
    + gtk_editable_set_text(GTK_EDITABLE(dialog->comment), comment);
    g_object_notify_by_pspec(G_OBJECT(dialog), properties[PROP_COMMENT]);
    }
    @@ -228,7 +227,7 @@
    g_return_val_if_fail(TALKATU_IS_ATTACHMENT_DIALOG(dialog), "");
    if(GTK_IS_ENTRY(dialog->comment)) {
    - return gtk_entry_get_text(GTK_ENTRY(dialog->comment));
    + return gtk_editable_get_text(GTK_EDITABLE(dialog->comment));
    }
    return "";
    --- a/talkatu/talkatuattachmentpreview.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuattachmentpreview.c Sun Aug 28 22:44:59 2022 -0500
    @@ -27,7 +27,7 @@
    * user to save it.
    */
    struct _TalkatuAttachmentPreview {
    - GtkInfoBar parent;
    + GtkWidget parent;
    TalkatuAttachment *attachment;
    @@ -47,9 +47,11 @@
    /******************************************************************************
    * Callbacks
    *****************************************************************************/
    -static gboolean
    -talkatu_attachment_preview_download_cb(GtkButton *button, gpointer data) {
    - return FALSE;
    +static void
    +talkatu_attachment_preview_download_cb(G_GNUC_UNUSED GtkInfoBar *self,
    + G_GNUC_UNUSED gint response_id,
    + G_GNUC_UNUSED gpointer user_data)
    +{
    }
    /******************************************************************************
    @@ -66,8 +68,7 @@
    gchar *filesize = NULL;
    if(G_IS_ICON(icon)) {
    - gtk_image_set_from_gicon(GTK_IMAGE(preview->preview), icon,
    - GTK_ICON_SIZE_DIALOG);
    + gtk_image_set_from_gicon(GTK_IMAGE(preview->preview), icon);
    g_object_unref(G_OBJECT(icon));
    }
    @@ -88,7 +89,7 @@
    /******************************************************************************
    * GObject Implementation
    *****************************************************************************/
    -G_DEFINE_TYPE(TalkatuAttachmentPreview, talkatu_attachment_preview, GTK_TYPE_INFO_BAR)
    +G_DEFINE_TYPE(TalkatuAttachmentPreview, talkatu_attachment_preview, GTK_TYPE_WIDGET)
    static void
    talkatu_attachment_preview_get_property(GObject *obj, guint prop_id,
    --- a/talkatu/talkatuattachmentpreview.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuattachmentpreview.h Sun Aug 28 22:44:59 2022 -0500
    @@ -33,7 +33,7 @@
    G_BEGIN_DECLS
    #define TALKATU_TYPE_ATTACHMENT_PREVIEW (talkatu_attachment_preview_get_type())
    -G_DECLARE_FINAL_TYPE(TalkatuAttachmentPreview, talkatu_attachment_preview, TALKATU, ATTACHMENT_PREVIEW, GtkInfoBar)
    +G_DECLARE_FINAL_TYPE(TalkatuAttachmentPreview, talkatu_attachment_preview, TALKATU, ATTACHMENT_PREVIEW, GtkWidget)
    GtkWidget *talkatu_attachment_preview_new(TalkatuAttachment *attachment);
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/talkatu/talkatuautoscroller.c Sun Aug 28 22:44:59 2022 -0500
    @@ -0,0 +1,154 @@
    +/*
    + * 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/>.
    + */
    +
    +#include <gtk/gtk.h>
    +
    +#include <talkatu/talkatuautoscroller.h>
    +
    +/**
    + * TalkatuAutoScroller:
    + *
    + * This is a simple subclass of [class@Gtk.Adjustment] that has helpers for
    + * keyboard navigation as well as the ability to automatically scroll to the
    + * max when new items are added if the widget was already scrolled all the
    + * way to the bottom.
    + */
    +struct _TalkatuAutoScroller {
    + GtkAdjustment parent;
    +
    + gboolean auto_scroll;
    +};
    +
    +G_DEFINE_TYPE(TalkatuAutoScroller, talkatu_auto_scroller, GTK_TYPE_ADJUSTMENT)
    +
    +/******************************************************************************
    + * Callbacks
    + *****************************************************************************/
    +static gboolean
    +talkatu_auto_scroller_scroll(gpointer data) {
    + GtkAdjustment *adjustment = data;
    +
    + gdouble upper, pagesize;
    +
    + upper = gtk_adjustment_get_upper(adjustment);
    + pagesize = gtk_adjustment_get_page_size(adjustment);
    +
    + gtk_adjustment_set_value(adjustment, upper - pagesize);
    +
    + return G_SOURCE_REMOVE;
    +}
    +
    +static void
    +talkatu_auto_scroller_changed(GtkAdjustment *adjustment) {
    + TalkatuAutoScroller *auto_scroller = TALKATU_AUTO_SCROLLER(adjustment);
    +
    + if(auto_scroller->auto_scroll) {
    + /* If we set the value here, we interrupt the updates to the other
    + * values of the adjustment which breaks the adjustment.
    + *
    + * The specific behavior we've seen is that the upper property doesn't
    + * get updated when we call set value here. Which means you can't even
    + * scroll down anymore in a scrolled window because you're already at
    + * the upper bound. However, if you scroll up, even if there's no where
    + * to scroll, it will update the adjustment's properties and you can
    + * then scroll down.
    + *
    + * By using a timeout to set the value during the next main loop
    + * iteration we avoid this problem.
    + */
    + g_timeout_add(0, talkatu_auto_scroller_scroll, adjustment);
    + }
    +}
    +
    +static void
    +talkatu_auto_scroller_value_changed(GtkAdjustment *adjustment) {
    + TalkatuAutoScroller *auto_scroller = TALKATU_AUTO_SCROLLER(adjustment);
    + gdouble current, upper, pagesize;
    +
    + current = gtk_adjustment_get_value(adjustment);
    + upper = gtk_adjustment_get_upper(adjustment);
    + pagesize = gtk_adjustment_get_page_size(adjustment);
    +
    + auto_scroller->auto_scroll = (current + pagesize >= upper);
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +static void
    +talkatu_auto_scroller_init(TalkatuAutoScroller *auto_scroller) {
    + auto_scroller->auto_scroll = TRUE;
    +}
    +
    +static void
    +talkatu_auto_scroller_class_init(TalkatuAutoScrollerClass *klass) {
    + GtkAdjustmentClass *adjustment_class = GTK_ADJUSTMENT_CLASS(klass);
    +
    + adjustment_class->changed = talkatu_auto_scroller_changed;
    + adjustment_class->value_changed = talkatu_auto_scroller_value_changed;
    +}
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +
    +/**
    + * talkatu_auto_scroller_new:
    + *
    + * Creates a new #TalkatuAutoScroller.
    + *
    + * Returns: (transfer full): The new #TalkatuAutoScroller instance.
    + */
    +GtkAdjustment *
    +talkatu_auto_scroller_new(void) {
    + return g_object_new(TALKATU_TYPE_AUTO_SCROLLER, NULL);
    +}
    +
    +/**
    + * talkatu_auto_scroller_decrement:
    + * @auto_scroller: The #TalkatuAutoScroller instance.
    + *
    + * Decrements the value of @auto_scroller by a page increment.
    + */
    +void
    +talkatu_auto_scroller_decrement(TalkatuAutoScroller *auto_scroller) {
    + gdouble value = 0.0;
    +
    + g_return_if_fail(TALKATU_IS_AUTO_SCROLLER(auto_scroller));
    +
    + value = gtk_adjustment_get_value(GTK_ADJUSTMENT(auto_scroller));
    + value -= gtk_adjustment_get_page_increment(GTK_ADJUSTMENT(auto_scroller));
    + gtk_adjustment_set_value(GTK_ADJUSTMENT(auto_scroller), value);
    +}
    +
    +/**
    + * talkatu_auto_scroller_increment:
    + * @auto_scroller: The #TalkatuAutoScroller instance.
    + *
    + * Increments the value of @auto_scroller by a page increment.
    + */
    +void
    +talkatu_auto_scroller_increment(TalkatuAutoScroller *auto_scroller) {
    + gdouble value = 0.0;
    +
    + g_return_if_fail(TALKATU_IS_AUTO_SCROLLER(auto_scroller));
    +
    + value = gtk_adjustment_get_value(GTK_ADJUSTMENT(auto_scroller));
    + value += gtk_adjustment_get_page_increment(GTK_ADJUSTMENT(auto_scroller));
    + gtk_adjustment_set_value(GTK_ADJUSTMENT(auto_scroller), value);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/talkatu/talkatuautoscroller.h Sun Aug 28 22:44:59 2022 -0500
    @@ -0,0 +1,45 @@
    +/*
    + * 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/>.
    + */
    +
    +#if !defined(TALKATU_GLOBAL_HEADER_INSIDE) && !defined(TALKATU_COMPILATION)
    +#error "only <talkatu.h> may be included directly"
    +#endif
    +
    +#ifndef TALKATU_AUTO_SCROLLER_H
    +#define TALKATU_AUTO_SCROLLER_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <gtk/gtk.h>
    +
    +G_BEGIN_DECLS
    +
    +#define TALKATU_TYPE_AUTO_SCROLLER (talkatu_auto_scroller_get_type())
    +
    +G_DECLARE_FINAL_TYPE(TalkatuAutoScroller, talkatu_auto_scroller, TALKATU,
    + AUTO_SCROLLER, GtkAdjustment)
    +
    +GtkAdjustment *talkatu_auto_scroller_new(void);
    +
    +void talkatu_auto_scroller_decrement(TalkatuAutoScroller *auto_scroller);
    +void talkatu_auto_scroller_increment(TalkatuAutoScroller *auto_scroller);
    +
    +G_END_DECLS
    +
    +#endif /* TALKATU_AUTO_SCROLLER_H */
    --- a/talkatu/talkatubuffer.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatubuffer.c Sun Aug 28 22:44:59 2022 -0500
    @@ -376,8 +376,8 @@
    static void
    talkatu_buffer_init(TalkatuBuffer *buffer) {
    - gtk_text_buffer_register_serialize_tagset(GTK_TEXT_BUFFER(buffer), NULL);
    - gtk_text_buffer_register_deserialize_tagset(GTK_TEXT_BUFFER(buffer), NULL);
    + // gtk_text_buffer_register_serialize_tagset(GTK_TEXT_BUFFER(buffer), NULL);
    + // gtk_text_buffer_register_deserialize_tagset(GTK_TEXT_BUFFER(buffer), NULL);
    }
    static void
    --- a/talkatu/talkatuhistory.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuhistory.c Sun Aug 28 22:44:59 2022 -0500
    @@ -1,6 +1,6 @@
    /*
    * Talkatu - GTK widgets for chat applications
    - * Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
    + * Copyright (C) 2017-2022 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
    @@ -20,7 +20,6 @@
    #include <talkatu/talkatuhistory.h>
    -#include <talkatu/talkatuattachmentpreview.h>
    #include <talkatu/talkatuhistoryrow.h>
    #include <talkatu/talkatumessage.h>
    @@ -30,20 +29,46 @@
    * A #TalkatuView subclass that is used to display a conversation.
    */
    struct _TalkatuHistory {
    - GtkListBox parent;
    + GtkWidget parent;
    +
    + GtkWidget *list_box;
    };
    -G_DEFINE_TYPE(TalkatuHistory, talkatu_history, GTK_TYPE_LIST_BOX)
    +G_DEFINE_TYPE(TalkatuHistory, talkatu_history, GTK_TYPE_WIDGET)
    /******************************************************************************
    - * GObject Stuff
    + * GObject Implementation
    *****************************************************************************/
    static void
    +talkatu_history_dispose(GObject *obj) {
    + TalkatuHistory *history = TALKATU_HISTORY(obj);
    +
    + g_clear_pointer(&history->list_box, gtk_widget_unparent);
    +
    + G_OBJECT_CLASS(talkatu_history_parent_class)->dispose(obj);
    +}
    +
    +static void
    talkatu_history_init(TalkatuHistory *history) {
    + gtk_widget_init_template(GTK_WIDGET(history));
    }
    static void
    talkatu_history_class_init(TalkatuHistoryClass *klass) {
    + GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    +
    + obj_class->dispose = talkatu_history_dispose;
    +
    + gtk_widget_class_set_template_from_resource(
    + widget_class,
    + "/org/imfreedom/keep/talkatu/talkatu/ui/history.ui"
    + );
    +
    + gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT);
    +
    + gtk_widget_class_bind_template_child(widget_class, TalkatuHistory,
    + list_box);
    }
    /******************************************************************************
    @@ -58,10 +83,7 @@
    * Returns: (transfer full): The new #TalkatuHistory instance.
    */
    GtkWidget *talkatu_history_new(void) {
    - return GTK_WIDGET(g_object_new(
    - TALKATU_TYPE_HISTORY,
    - NULL
    - ));
    + return g_object_new(TALKATU_TYPE_HISTORY, NULL);
    }
    /**
    @@ -82,5 +104,5 @@
    g_return_if_fail(TALKATU_IS_MESSAGE(message));
    row = talkatu_history_row_new(message);
    - gtk_container_add(GTK_CONTAINER(history), row);
    + gtk_list_box_append(GTK_LIST_BOX(history->list_box), row);
    }
    --- a/talkatu/talkatuhistory.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuhistory.h Sun Aug 28 22:44:59 2022 -0500
    @@ -1,6 +1,6 @@
    /*
    * Talkatu - GTK widgets for chat applications
    - * Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
    + * Copyright (C) 2017-2022 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
    @@ -34,7 +34,7 @@
    #define TALKATU_TYPE_HISTORY (talkatu_history_get_type())
    -G_DECLARE_FINAL_TYPE(TalkatuHistory, talkatu_history, TALKATU, HISTORY, GtkListBox)
    +G_DECLARE_FINAL_TYPE(TalkatuHistory, talkatu_history, TALKATU, HISTORY, GtkWidget)
    GtkWidget *talkatu_history_new(void);
    --- a/talkatu/talkatuhistoryrow.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuhistoryrow.c Sun Aug 28 22:44:59 2022 -0500
    @@ -34,7 +34,6 @@
    TalkatuMessage *message;
    - GtkWidget *avatar_event;
    GtkWidget *avatar_image;
    GtkWidget *author;
    GtkWidget *timestamp;
    @@ -230,8 +229,6 @@
    g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    gtk_widget_class_bind_template_child(widget_class, TalkatuHistoryRow,
    - avatar_event);
    - gtk_widget_class_bind_template_child(widget_class, TalkatuHistoryRow,
    avatar_image);
    gtk_widget_class_bind_template_child(widget_class, TalkatuHistoryRow,
    author);
    --- a/talkatu/talkatuhtmlbuffer.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuhtmlbuffer.c Sun Aug 28 22:44:59 2022 -0500
    @@ -65,21 +65,21 @@
    *****************************************************************************/
    static void
    talkatu_html_buffer_init(TalkatuHtmlBuffer *buffer) {
    - gtk_text_buffer_register_deserialize_format(
    - GTK_TEXT_BUFFER(buffer),
    - "text/html",
    - talkatu_markup_deserialize_html,
    - NULL,
    - NULL
    - );
    + // gtk_text_buffer_register_deserialize_format(
    + // GTK_TEXT_BUFFER(buffer),
    + // "text/html",
    + // talkatu_markup_deserialize_html,
    + // NULL,
    + // NULL
    + // );
    - gtk_text_buffer_register_serialize_format(
    - GTK_TEXT_BUFFER(buffer),
    - "text/html",
    - talkatu_markup_serialize_html,
    - NULL,
    - NULL
    - );
    + // gtk_text_buffer_register_serialize_format(
    + // GTK_TEXT_BUFFER(buffer),
    + // "text/html",
    + // talkatu_markup_serialize_html,
    + // NULL,
    + // NULL
    + // );
    }
    static void
    --- a/talkatu/talkatuinput.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuinput.c Sun Aug 28 22:44:59 2022 -0500
    @@ -1,6 +1,6 @@
    /*
    * Talkatu - GTK widgets for chat applications
    - * Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
    + * Copyright (C) 2017-2022 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
    @@ -20,8 +20,6 @@
    #include <glib/gi18n-lib.h>
    #include <glib/gstdio.h>
    -#include <gspell/gspell.h>
    -
    #include "talkatu/talkatuinput.h"
    #include "talkatu/talkatuactiongroup.h"
    @@ -47,8 +45,6 @@
    /**
    * TalkatuInputClass:
    - * @should_send_message: The class handler for the
    - * #TalkatuInput::should_send_message signal.
    * @send_message: The class handler for the #TalkatuInput::send_message signal.
    *
    * The backing class to #TalkatuInput instances.
    @@ -66,20 +62,11 @@
    */
    typedef struct {
    - TalkatuView parent;
    -
    GHashTable *attachments;
    guint64 attachment_id;
    - GspellTextView *gspell_view;
    -
    TalkatuInputSendBinding send_binding;
    - /* this mark is used to keep track of our context for the context menu. It
    - * is updated via cursor-moved and button-press callbacks.
    - */
    - GtkTextMark *context_mark;
    -
    /* TalkatuMessage properties: content type and contents are derived from
    * the widget itself.
    */
    @@ -365,64 +352,51 @@
    * Callbacks
    *****************************************************************************/
    static void
    -talkatu_input_send_message_cb(GtkMenuItem *item, gpointer data) {
    - talkatu_input_send_message(TALKATU_INPUT(data));
    +talkatu_input_send_message_cb(GtkWidget *widget,
    + G_GNUC_UNUSED const gchar *action_name,
    + G_GNUC_UNUSED GVariant *parameter)
    +{
    + talkatu_input_send_message(TALKATU_INPUT(widget));
    - gtk_widget_grab_focus(GTK_WIDGET(data));
    + gtk_widget_grab_focus(widget);
    }
    +static gboolean
    +talkatu_input_key_pressed_cb(G_GNUC_UNUSED GtkEventControllerKey *self,
    + guint keyval, G_GNUC_UNUSED guint keycode,
    + GdkModifierType state, gpointer data)
    +{
    + TalkatuInput *input = data;
    + TalkatuInputPrivate *priv = talkatu_input_get_instance_private(input);
    + TalkatuInputSendBinding binding = 0;
    + gboolean handled = FALSE;
    -static void
    -talkatu_input_populate_popup_cb(GtkTextView *view, GtkWidget *popup) {
    - TalkatuInputPrivate *priv = NULL;
    - GtkTextBuffer *buffer = NULL;
    - GtkTextIter iter;
    - GtkWidget *item = NULL;
    - gint pos = 0;
    -
    - /* if the popup isn't a menu, bail */
    - if(!GTK_IS_MENU(popup)) {
    - return;
    + if(keyval == GDK_KEY_Return) {
    + if(state == GDK_SHIFT_MASK) {
    + binding = TALKATU_INPUT_SEND_BINDING_SHIFT_RETURN;
    + } else if(state == GDK_CONTROL_MASK) {
    + binding = TALKATU_INPUT_SEND_BINDING_CONTROL_RETURN;
    + } else {
    + binding = TALKATU_INPUT_SEND_BINDING_RETURN;
    + }
    + } else if(keyval == GDK_KEY_KP_Enter) {
    + binding = TALKATU_INPUT_SEND_BINDING_KP_ENTER;
    }
    - priv = talkatu_input_get_instance_private(TALKATU_INPUT(view));
    -
    - buffer = gtk_text_view_get_buffer(view);
    -
    - gtk_text_buffer_get_iter_at_mark(buffer, &iter, priv->context_mark);
    + if(binding != 0) {
    + if((priv->send_binding & binding) != 0) {
    + talkatu_input_send_message(input);
    + handled = TRUE;
    + }
    + }
    - /* add the send message item */
    - if(gtk_text_view_get_editable(view)) {
    - item = gtk_menu_item_new_with_label(_("Send message"));
    - g_signal_connect_after(
    - G_OBJECT(item),
    - "activate",
    - G_CALLBACK(talkatu_input_send_message_cb),
    - view
    - );
    - gtk_menu_shell_insert(GTK_MENU_SHELL(popup), item, pos++);
    - gtk_widget_show(item);
    -
    - item = gtk_separator_menu_item_new();
    - gtk_menu_shell_insert(GTK_MENU_SHELL(popup), item , pos++);
    - gtk_widget_show(item);
    - }
    + return handled;
    }
    static void
    talkatu_input_buffer_set_cb(GObject *view, GParamSpec *pspec, gpointer data) {
    - TalkatuInputPrivate *priv = NULL;
    TalkatuInput *input = TALKATU_INPUT(view);
    GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
    - GtkTextIter start;
    -
    - priv = talkatu_input_get_instance_private(input);
    -
    - gspell_text_view_basic_setup(priv->gspell_view);
    -
    - /* grab our context_mark */
    - gtk_text_buffer_get_start_iter(buffer, &start);
    - priv->context_mark = gtk_text_buffer_create_mark(buffer, NULL, &start, TRUE);
    if(TALKATU_IS_BUFFER(buffer)) {
    GSimpleActionGroup *ag = NULL;
    @@ -448,7 +422,7 @@
    /* we call this separately for GTK_RESPONSE_CANCEL because
    * GTK_RESPONSE_DELETE_EVENT already destroys the dialog.
    */
    - gtk_widget_destroy(GTK_WIDGET(dialog));
    + gtk_window_destroy(GTK_WINDOW(dialog));
    } else if(response == GTK_RESPONSE_ACCEPT) {
    GtkTextBuffer *buffer = NULL;
    TalkatuAttachment *attachment = NULL;
    @@ -474,10 +448,13 @@
    talkatu_input_send_message(input);
    /* kill the dialog */
    - gtk_widget_destroy(GTK_WIDGET(dialog));
    + gtk_window_destroy(GTK_WINDOW(dialog));
    }
    +
    + g_object_unref(dialog);
    }
    +#if 0
    static void
    talkatu_input_image_received_cb(GtkClipboard *clipboard, GdkPixbuf *pixbuf,
    gpointer data)
    @@ -565,56 +542,25 @@
    g_free(comment);
    g_object_unref(G_OBJECT(stream));
    }
    -
    -/******************************************************************************
    - * Default Signal Handlers
    - *****************************************************************************/
    -static gboolean
    -talkatu_input_popup_menu(GtkWidget *widget) {
    - TalkatuInputPrivate *priv = talkatu_input_get_instance_private(TALKATU_INPUT(widget));
    - GtkTextBuffer *buffer = NULL;
    - GtkTextIter iter;
    -
    - buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
    -
    - gtk_text_buffer_get_iter_at_mark(
    - buffer,
    - &iter,
    - gtk_text_buffer_get_insert(buffer)
    - );
    -
    - gtk_text_buffer_move_mark(buffer, priv->context_mark, &iter);
    -
    - return GTK_WIDGET_CLASS(talkatu_input_parent_class)->popup_menu(widget);
    -}
    -
    -static void
    -talkatu_input_should_send_message(TalkatuInput *input, TalkatuInputSendBinding binding) {
    - TalkatuInputPrivate *priv = talkatu_input_get_instance_private(input);
    -
    - if((priv->send_binding & binding) != 0) {
    - talkatu_input_send_message(input);
    - } else if(gtk_text_view_get_editable(GTK_TEXT_VIEW(input))) {
    - GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(input));
    -
    - gtk_text_buffer_insert_at_cursor(buffer, "\n", 1);
    - }
    -}
    +#endif
    /******************************************************************************
    * GtkTextViewClass overrides
    *****************************************************************************/
    static void
    talkatu_input_paste_clipboard(GtkTextView *view) {
    - GtkClipboard *clipboard =
    - gtk_widget_get_clipboard(GTK_WIDGET(view), GDK_SELECTION_CLIPBOARD);
    +#if 0
    + GdkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(view));
    if(gtk_clipboard_wait_is_image_available(clipboard)) {
    gtk_clipboard_request_image(clipboard, talkatu_input_image_received_cb,
    view);
    } else {
    +#endif
    GTK_TEXT_VIEW_CLASS(talkatu_input_parent_class)->paste_clipboard(view);
    +#if 0
    }
    +#endif
    }
    /******************************************************************************
    @@ -694,31 +640,10 @@
    talkatu_input_init(TalkatuInput *input) {
    TalkatuInputPrivate *priv = talkatu_input_get_instance_private(input);
    + gtk_widget_init_template(GTK_WIDGET(input));
    +
    priv->attachments = g_hash_table_new_full(g_int64_hash, g_int64_equal,
    NULL, g_object_unref);
    -
    - /* we need to know when the buffer is changed in our parent so we can
    - * update our actions and other stuff.
    - */
    - g_signal_connect(
    - G_OBJECT(input),
    - "notify::buffer",
    - G_CALLBACK(talkatu_input_buffer_set_cb),
    - NULL
    - );
    -
    - /* setup GSpell */
    - priv->gspell_view = gspell_text_view_get_from_gtk_text_view(GTK_TEXT_VIEW(input));
    -
    - /* we need to connect this signal *AFTER* everything to make sure our items
    - * end up in the correct place.
    - */
    - g_signal_connect_after(
    - G_OBJECT(input),
    - "populate-popup",
    - G_CALLBACK(talkatu_input_populate_popup_cb),
    - NULL
    - );
    }
    static void
    @@ -741,21 +666,17 @@
    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->get_property = talkatu_input_get_property;
    obj_class->set_property = talkatu_input_set_property;
    obj_class->finalize = talkatu_input_finalize;
    - widget_class->popup_menu = talkatu_input_popup_menu;
    -
    text_view_class->paste_clipboard = talkatu_input_paste_clipboard;
    - klass->should_send_message = talkatu_input_should_send_message;
    -
    /* add our properties */
    properties[PROP_SEND_BINDING] = g_param_spec_flags(
    - "send-binding", "send-binding", "The keybindings that will trigger the send signal",
    + "send-binding", "send-binding",
    + "The keybindings that will trigger the send signal",
    TALKATU_TYPE_INPUT_SEND_BINDING,
    TALKATU_INPUT_SEND_BINDING_RETURN | TALKATU_INPUT_SEND_BINDING_KP_ENTER,
    G_PARAM_READWRITE | G_PARAM_CONSTRUCT
    @@ -772,28 +693,6 @@
    g_object_class_override_property(obj_class, PROP_EDITED, "edited");
    /**
    - * TalkatuInput::should-send-message:
    - * @talkatuinput: The #TalkatuInput instance.
    - * @arg1: The #TalkatuInputSendBinding that was entered.
    - * @user_data: User supplied data.
    - *
    - * Emitted when a potential keybinding to send the message is entered to
    - * determine if the message should be sent.
    - */
    - signals[SIG_SHOULD_SEND_MESSAGE] = g_signal_new(
    - "should-send-message",
    - G_TYPE_FROM_CLASS(klass),
    - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
    - G_STRUCT_OFFSET(TalkatuInputClass, should_send_message),
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 1,
    - TALKATU_TYPE_INPUT_SEND_BINDING
    - );
    -
    - /**
    * TalkatuInput::send-message:
    * @talkatuinput: The #TalkatuInput instance.
    * @user_data: User supplied data.
    @@ -812,13 +711,20 @@
    0
    );
    - /* setup key bindings */
    - binding_set = gtk_binding_set_by_class(talkatu_input_parent_class);
    + /* Setup actions */
    + gtk_widget_class_install_action(widget_class, "message.send", NULL,
    + talkatu_input_send_message_cb);
    - gtk_binding_entry_add_signal(binding_set, GDK_KEY_Return, 0, "should-send-message", 1, TALKATU_TYPE_INPUT_SEND_BINDING, TALKATU_INPUT_SEND_BINDING_RETURN);
    - gtk_binding_entry_add_signal(binding_set, GDK_KEY_Return, GDK_SHIFT_MASK, "should-send-message", 1, TALKATU_TYPE_INPUT_SEND_BINDING, TALKATU_INPUT_SEND_BINDING_SHIFT_RETURN);
    - gtk_binding_entry_add_signal(binding_set, GDK_KEY_Return, GDK_CONTROL_MASK, "should-send-message", 1, TALKATU_TYPE_INPUT_SEND_BINDING, TALKATU_INPUT_SEND_BINDING_CONTROL_RETURN);
    - gtk_binding_entry_add_signal(binding_set, GDK_KEY_KP_Enter, 0, "should-send-message", 1, TALKATU_TYPE_INPUT_SEND_BINDING, TALKATU_INPUT_SEND_BINDING_KP_ENTER);
    + /* Template setup */
    + gtk_widget_class_set_template_from_resource(
    + widget_class,
    + "/org/imfreedom/keep/talkatu/talkatu/ui/input.ui"
    + );
    +
    + gtk_widget_class_bind_template_callback(widget_class,
    + talkatu_input_buffer_set_cb);
    + gtk_widget_class_bind_template_callback(widget_class,
    + talkatu_input_key_pressed_cb);
    }
    /******************************************************************************
    @@ -834,7 +740,7 @@
    */
    GtkWidget *
    talkatu_input_new(void) {
    - return GTK_WIDGET(g_object_new(TALKATU_TYPE_INPUT, NULL));
    + return g_object_new(TALKATU_TYPE_INPUT, NULL);
    }
    /**
    @@ -852,7 +758,7 @@
    g_return_if_fail(TALKATU_IS_INPUT(input));
    - priv = talkatu_input_get_instance_private(TALKATU_INPUT(input));
    + priv = talkatu_input_get_instance_private(input);
    priv->send_binding = bindings;
    @@ -874,7 +780,7 @@
    g_return_val_if_fail(TALKATU_IS_INPUT(input), 0);
    - priv = talkatu_input_get_instance_private(TALKATU_INPUT(priv));
    + priv = talkatu_input_get_instance_private(input);
    return priv->send_binding;
    }
    --- a/talkatu/talkatuinput.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuinput.h Sun Aug 28 22:44:59 2022 -0500
    @@ -48,7 +48,6 @@
    TalkatuViewClass parent;
    /*< public >*/
    - void (*should_send_message)(TalkatuInput *input, TalkatuInputSendBinding binding);
    void (*send_message)(TalkatuInput *input);
    /*< private >*/
    --- a/talkatu/talkatulinkdialog.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatulinkdialog.c Sun Aug 28 22:44:59 2022 -0500
    @@ -90,7 +90,7 @@
    if(GTK_IS_ENTRY(dialog->url)) {
    gchar *raw = NULL, *ret = NULL;
    - raw = g_strdup(gtk_entry_get_text(GTK_ENTRY(dialog->url)));
    + raw = g_strdup(gtk_editable_get_text(GTK_EDITABLE(dialog->url)));
    ret = g_strdup(g_strstrip(raw));
    g_free(raw);
    @@ -116,7 +116,7 @@
    if(GTK_IS_ENTRY(dialog->display)) {
    gchar *ret = NULL;
    - ret = g_strdup(gtk_entry_get_text(GTK_ENTRY(dialog->display)));
    + ret = g_strdup(gtk_editable_get_text(GTK_EDITABLE(dialog->display)));
    return g_strstrip(ret);
    }
    --- a/talkatu/talkatumarkdownbuffer.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatumarkdownbuffer.c Sun Aug 28 22:44:59 2022 -0500
    @@ -270,13 +270,13 @@
    *****************************************************************************/
    static void
    talkatu_markdown_buffer_init(TalkatuMarkdownBuffer *buffer) {
    - gtk_text_buffer_register_deserialize_format(
    - GTK_TEXT_BUFFER(buffer),
    - "text/markdown",
    - talkatu_markdown_buffer_deserialize_markdown,
    - NULL,
    - NULL
    - );
    + // gtk_text_buffer_register_deserialize_format(
    + // GTK_TEXT_BUFFER(buffer),
    + // "text/markdown",
    + // talkatu_markdown_buffer_deserialize_markdown,
    + // NULL,
    + // NULL
    + // );
    }
    static void
    --- a/talkatu/talkatumenutoolbutton.c Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,179 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#include <stdio.h>
    -
    -#include "talkatu/talkatumenutoolbutton.h"
    -
    -/**
    - * TalkatuMenuToolButton:
    - *
    - * A #GtkToolButton subclass that behaves like a #GtkComboBox.
    - */
    -
    -struct _TalkatuMenuToolButton {
    - GtkToolButton parent;
    -
    - GtkWidget *menu;
    -};
    -
    -G_DEFINE_TYPE(TalkatuMenuToolButton, talkatu_menu_tool_button, GTK_TYPE_TOOL_BUTTON)
    -
    -enum {
    - PROP_0 = 0,
    - PROP_MENU,
    - N_PROPERTIES,
    -};
    -
    -static GParamSpec *properties[N_PROPERTIES] = {NULL,};
    -
    -/******************************************************************************
    - * GObject Stuff
    - *****************************************************************************/
    -
    -static void
    -talkatu_menu_tool_button_clicked(GtkToolButton *button) {
    - TalkatuMenuToolButton *menu_button = TALKATU_MENU_TOOL_BUTTON(button);
    -
    - gtk_menu_popup_at_widget(GTK_MENU(menu_button->menu),
    - GTK_WIDGET(button),
    - GDK_GRAVITY_SOUTH_WEST,
    - GDK_GRAVITY_NORTH_WEST,
    - NULL);
    -}
    -
    -static void
    -talkatu_menu_tool_button_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) {
    - TalkatuMenuToolButton *menu_button = TALKATU_MENU_TOOL_BUTTON(obj);
    -
    - switch(prop_id) {
    - case PROP_MENU:
    - g_value_set_object(value, talkatu_menu_tool_button_get_menu(menu_button));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -talkatu_menu_tool_button_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) {
    - TalkatuMenuToolButton *menu_button = TALKATU_MENU_TOOL_BUTTON(obj);
    -
    - switch(prop_id) {
    - case PROP_MENU:
    - talkatu_menu_tool_button_set_menu(menu_button, g_value_get_object(value));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -talkatu_menu_tool_button_init(TalkatuMenuToolButton *menu_button) {
    -}
    -
    -static void
    -talkatu_menu_tool_button_class_init(TalkatuMenuToolButtonClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    - GtkToolButtonClass *button_class = GTK_TOOL_BUTTON_CLASS(klass);
    -
    - obj_class->get_property = talkatu_menu_tool_button_get_property;
    - obj_class->set_property = talkatu_menu_tool_button_set_property;
    -
    - button_class->clicked = talkatu_menu_tool_button_clicked;
    -
    - properties[PROP_MENU] = g_param_spec_object(
    - "menu", "menu", "The menu to show",
    - GTK_TYPE_MENU,
    - G_PARAM_READWRITE | G_PARAM_CONSTRUCT
    - );
    -
    - g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -
    -/**
    - * talkatu_menu_tool_button_new:
    - * @label: The label to display.
    - * @icon_name: The optional name of the icon to display.
    - * @menu: The menu to display.
    - *
    - * Creates a new #TalkatuMenuToolButton with the given @label, @icon_name, and
    - * @menu.
    - *
    - * Returns: (transfer full): The new #TalkatuMenuToolButton instance.
    - */
    -GtkToolItem *
    -talkatu_menu_tool_button_new(const gchar *label, const gchar *icon_name, GtkWidget *menu) {
    - return g_object_new(
    - TALKATU_TYPE_MENU_TOOL_BUTTON,
    - "label", label,
    - "icon-name", icon_name,
    - "menu", menu,
    - NULL
    - );
    -}
    -
    -/**
    - * talkatu_menu_tool_button_get_menu:
    - * @menu_button: The #TalkatuMenuToolButton instance.
    - *
    - * Gets the menu that this tool button will display on click or #NULL if no
    - * menu is set.
    - *
    - * Returns: (transfer full): The menu.
    - */
    -GtkWidget *
    -talkatu_menu_tool_button_get_menu(TalkatuMenuToolButton *menu_button) {
    - g_return_val_if_fail(TALKATU_IS_MENU_TOOL_BUTTON(menu_button), FALSE);
    -
    - if(menu_button->menu) {
    - return g_object_ref(menu_button->menu);
    - }
    -
    - return NULL;
    -}
    -
    -/**
    - * talkatu_menu_tool_button_set_menu:
    - * @menu_button: The #TalkatuMenuToolButton instance.
    - * @menu: The menu to set.
    - *
    - * Sets the menu to be displayed when the user clicks the button.
    - */
    -void
    -talkatu_menu_tool_button_set_menu(TalkatuMenuToolButton *menu_button, GtkWidget *menu) {
    - g_return_if_fail(TALKATU_IS_MENU_TOOL_BUTTON(menu_button));
    -
    - if(menu_button->menu) {
    - gtk_menu_detach(GTK_MENU(menu_button->menu));
    - g_object_unref(G_OBJECT(menu_button->menu));
    - }
    -
    - if(menu) {
    - menu_button->menu = GTK_WIDGET(g_object_ref(G_OBJECT(menu)));
    - gtk_menu_attach_to_widget(GTK_MENU(menu_button->menu), GTK_WIDGET(menu_button), NULL);
    - } else {
    - menu_button->menu = NULL;
    - }
    -}
    --- a/talkatu/talkatumenutoolbutton.h Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,44 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#if !defined(TALKATU_GLOBAL_HEADER_INSIDE) && !defined(TALKATU_COMPILATION)
    -#error "only <talkatu.h> may be included directly"
    -#endif
    -
    -#ifndef TALKATU_MENU_TOOL_BUTTON_H
    -#define TALKATU_MENU_TOOL_BUTTON_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gtk/gtk.h>
    -
    -G_BEGIN_DECLS
    -
    -#define TALKATU_TYPE_MENU_TOOL_BUTTON (talkatu_menu_tool_button_get_type())
    -
    -G_DECLARE_FINAL_TYPE(TalkatuMenuToolButton, talkatu_menu_tool_button, TALKATU, MENU_TOOL_BUTTON, GtkToolButton)
    -
    -GtkToolItem *talkatu_menu_tool_button_new(const gchar *label, const gchar *icon_name, GtkWidget *menu);
    -
    -GtkWidget *talkatu_menu_tool_button_get_menu(TalkatuMenuToolButton *menu_button);
    -void talkatu_menu_tool_button_set_menu(TalkatuMenuToolButton *menu_button, GtkWidget *menu);
    -
    -G_END_DECLS
    -
    -#endif /* TALKATU_MENU_TOOL_BUTTON_H */
    --- a/talkatu/talkatumessageactions.c Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,171 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#include <gtk/gtk.h>
    -
    -#include "talkatu/talkatumessageactions.h"
    -
    -/**
    - * TalkatuMessageActions:
    - *
    - * A composite #GtkWidget to allow the user to interact with a message in a
    - * #TalkatuHistory.
    - */
    -struct _TalkatuMessageActions {
    - GtkBox parent;
    -
    - TalkatuMessage *message;
    -};
    -
    -enum {
    - PROP_0,
    - PROP_MESSAGE,
    - N_PROPERTIES,
    -};
    -
    -static GParamSpec *properties[N_PROPERTIES] = {NULL,};
    -
    -G_DEFINE_TYPE(TalkatuMessageActions, talkatu_message_actions, GTK_TYPE_BOX)
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static void
    -talkatu_message_actions_set_message(TalkatuMessageActions *message_actions,
    - TalkatuMessage *message)
    -{
    - if(message_actions->message) {
    - g_object_unref(G_OBJECT(message_actions->message));
    - }
    -
    - if(g_set_object(&message_actions->message, message)) {
    - g_object_notify_by_pspec(G_OBJECT(message_actions), properties[PROP_MESSAGE]);
    - }
    -}
    -
    -/******************************************************************************
    - * GObject Stuff
    - *****************************************************************************/
    -static void
    -talkatu_message_actions_get_property(GObject *obj,
    - guint prop_id,
    - GValue *value,
    - GParamSpec *pspec)
    -{
    - TalkatuMessageActions *message_actions = TALKATU_MESSAGE_ACTIONS(obj);
    -
    - switch(prop_id) {
    - case PROP_MESSAGE:
    - g_value_set_object(value, talkatu_message_actions_get_message(message_actions));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -talkatu_message_actions_set_property(GObject *obj,
    - guint prop_id,
    - const GValue *value,
    - GParamSpec *pspec)
    -{
    - TalkatuMessageActions *message_actions = TALKATU_MESSAGE_ACTIONS(obj);
    -
    - switch(prop_id) {
    - case PROP_MESSAGE:
    - talkatu_message_actions_set_message(message_actions, g_value_get_object(value));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -talkatu_message_actions_finalize(GObject *obj) {
    - TalkatuMessageActions *message_actions = TALKATU_MESSAGE_ACTIONS(obj);
    -
    - g_object_unref(G_OBJECT(message_actions->message));
    -
    - G_OBJECT_CLASS(talkatu_message_actions_parent_class)->finalize(obj);
    -}
    -
    -static void
    -talkatu_message_actions_init(TalkatuMessageActions *message_actions) {
    - gtk_widget_init_template(GTK_WIDGET(message_actions));
    -}
    -
    -static void
    -talkatu_message_actions_class_init(TalkatuMessageActionsClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    -
    - obj_class->get_property = talkatu_message_actions_get_property;
    - obj_class->set_property = talkatu_message_actions_set_property;
    - obj_class->finalize = talkatu_message_actions_finalize;
    -
    - properties[PROP_MESSAGE] = g_param_spec_object(
    - "message", "message", "The message that this widget is for",
    - G_TYPE_OBJECT,
    - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
    - );
    -
    - g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    -
    - gtk_widget_class_set_template_from_resource(
    - GTK_WIDGET_CLASS(klass),
    - "/org/imfreedom/keep/talkatu/talkatu/ui/messageactions.ui"
    - );
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -
    -/**
    - * talkatu_message_actions_new:
    - * @message: The #TalkatuMessage instance.
    - *
    - * Creates a new #TalkatuMessageActions that provides the user with an interface to
    - * control the formatting of a #TalkatuBuffer.
    - *
    - * Returns: (transfer full): The new #TalkatuMessageActions instance.
    - */
    -GtkWidget *
    -talkatu_message_actions_new(TalkatuMessage *message) {
    - return GTK_WIDGET(g_object_new(
    - TALKATU_TYPE_MESSAGE_ACTIONS,
    - "message", message,
    - NULL
    - ));
    -}
    -
    -/**
    - * talkatu_message_actions_get_message:
    - * @message_actions: The #TalkatuMessageActions instance.
    - *
    - * Gets the #TalkatuMessage associated with @message_action.
    - *
    - * Returns: (transfer none): The #TalkatuMessage associated with @message_action.
    - */
    -TalkatuMessage *
    -talkatu_message_actions_get_message(TalkatuMessageActions *message_actions) {
    - g_return_val_if_fail(TALKATU_IS_MESSAGE_ACTIONS(message_actions), NULL);
    -
    - return message_actions->message;
    -}
    --- a/talkatu/talkatumessageactions.h Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,45 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#if !defined(TALKATU_GLOBAL_HEADER_INSIDE) && !defined(TALKATU_COMPILATION)
    -#error "only <talkatu.h> may be included directly"
    -#endif
    -
    -#ifndef TALKATU_MESSAGE_ACTIONS_H
    -#define TALKATU_MESSAGE_ACTIONS_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gtk/gtk.h>
    -
    -#include <talkatu/talkatumessage.h>
    -
    -G_BEGIN_DECLS
    -
    -#define TALKATU_TYPE_MESSAGE_ACTIONS (talkatu_message_actions_get_type())
    -
    -G_DECLARE_FINAL_TYPE(TalkatuMessageActions, talkatu_message_actions, TALKATU, MESSAGE_ACTIONS, GtkBox)
    -
    -GtkWidget *talkatu_message_actions_new(TalkatuMessage *message);
    -
    -TalkatuMessage *talkatu_message_actions_get_message(TalkatuMessageActions *message_actions);
    -
    -G_END_DECLS
    -
    -#endif /* TALKATU_MESSAGE_ACTIONS_H */
    --- a/talkatu/talkatuscrolledwindow.c Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,181 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#include <gtk/gtk.h>
    -
    -#include <talkatu/talkatuscrolledwindow.h>
    -
    -/**
    - * TalkatuScrolledWindow:
    - * This widget is a simple subclass of #GtkScrolledWindow that has helpers for
    - * keyboard navigation as well as the ability to automatically scroll to the
    - * bottom when new items are added if the widget was already scrolled all the
    - * way to the bottom.
    - */
    -struct _TalkatuScrolledWindow {
    - GtkScrolledWindow parent;
    -
    - GtkAdjustment *vadjustment;
    - gboolean auto_scroll;
    -};
    -
    -G_DEFINE_TYPE(TalkatuScrolledWindow, talkatu_scrolled_window,
    - GTK_TYPE_SCROLLED_WINDOW)
    -
    -/******************************************************************************
    - * Callbacks
    - *****************************************************************************/
    -static void
    -talkatu_scrolled_window_vadjustment_changed_cb(GtkAdjustment *adjustment,
    - gpointer data)
    -{
    - TalkatuScrolledWindow *sw = TALKATU_SCROLLED_WINDOW(data);
    -
    - if(sw->auto_scroll) {
    - gdouble upper, pagesize;
    -
    - upper = gtk_adjustment_get_upper(adjustment);
    - pagesize = gtk_adjustment_get_page_size(adjustment);
    -
    - gtk_adjustment_set_value(adjustment, upper - pagesize);
    - }
    -}
    -
    -static void
    -talkatu_scrolled_window_vadjustment_value_changed_cb(GtkAdjustment *adjustment,
    - gpointer data)
    -{
    - TalkatuScrolledWindow *sw = TALKATU_SCROLLED_WINDOW(data);
    - gdouble current, upper, pagesize;
    -
    - current = gtk_adjustment_get_value(adjustment);
    - upper = gtk_adjustment_get_upper(adjustment);
    - pagesize = gtk_adjustment_get_page_size(adjustment);
    -
    - sw->auto_scroll = (current + pagesize >= upper);
    -}
    -
    -static void
    -talkatu_scrolled_window_vadjustment_notify_cb(GObject *obj, GParamSpec *pspec,
    - gpointer data)
    -{
    - TalkatuScrolledWindow *sw = TALKATU_SCROLLED_WINDOW(obj);
    - GtkAdjustment *adjustment = NULL;
    -
    - adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(obj));
    - if(g_set_object(&sw->vadjustment, adjustment)) {
    - sw->auto_scroll = TRUE;
    -
    - g_signal_connect(G_OBJECT(adjustment), "value-changed",
    - G_CALLBACK(talkatu_scrolled_window_vadjustment_value_changed_cb),
    - sw);
    - g_signal_connect(G_OBJECT(adjustment), "changed",
    - G_CALLBACK(talkatu_scrolled_window_vadjustment_changed_cb),
    - sw);
    - }
    -}
    -
    -/******************************************************************************
    - * GObject Stuff
    - *****************************************************************************/
    -static void
    -talkatu_scrolled_window_init(TalkatuScrolledWindow *sw) {
    - sw->auto_scroll = TRUE;
    -
    - g_signal_connect(G_OBJECT(sw), "notify::vadjustment",
    - G_CALLBACK(talkatu_scrolled_window_vadjustment_notify_cb),
    - NULL);
    -}
    -
    -static void
    -talkatu_scrolled_window_finalize(GObject *obj) {
    - TalkatuScrolledWindow *sw = TALKATU_SCROLLED_WINDOW(obj);
    -
    - g_clear_object(&sw->vadjustment);
    -
    - G_OBJECT_CLASS(talkatu_scrolled_window_parent_class)->finalize(obj);
    -}
    -
    -static void
    -talkatu_scrolled_window_class_init(TalkatuScrolledWindowClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    -
    - obj_class->finalize = talkatu_scrolled_window_finalize;
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -
    -/**
    - * talkatu_scrolled_window_new:
    - * @hadjustment: The GtkAdjustment for the horizontal position.
    - * @vadjustment: The GtkAdjustment for the vertical position.
    - *
    - * Creates a new #TalkatuScrolledWindow.
    - *
    - * Returns: (transfer full): The new #TalkatuScrolledWindow instance.
    - */
    -GtkWidget *talkatu_scrolled_window_new(GtkAdjustment *hadjustment,
    - GtkAdjustment *vadjustment)
    -{
    - return GTK_WIDGET(g_object_new(
    - TALKATU_TYPE_SCROLLED_WINDOW,
    - "hadjustment", hadjustment,
    - "vadjustment", vadjustment,
    - NULL
    - ));
    -}
    -
    -/**
    - * talkatu_scrolled_window_page_up:
    - * @sw: The #TalkatuScrolledWindow instance.
    - *
    - * Scrolls @sw up one page.
    - */
    -void
    -talkatu_scrolled_window_page_up(TalkatuScrolledWindow *sw) {
    - g_return_if_fail(TALKATU_IS_SCROLLED_WINDOW(sw));
    -
    - if(sw->vadjustment != NULL) {
    - double value = 0.0;
    -
    - value = gtk_adjustment_get_value(sw->vadjustment);
    - value -= gtk_adjustment_get_page_increment(sw->vadjustment);
    - gtk_adjustment_set_value(sw->vadjustment, value);
    - }
    -}
    -
    -/**
    - * talkatu_scrolled_window_page_down:
    - * @sw: The #TalkatuScrolledWindow instance.
    - *
    - * Scrolls @sw down one page.
    - */
    -void
    -talkatu_scrolled_window_page_down(TalkatuScrolledWindow *sw) {
    - g_return_if_fail(TALKATU_IS_SCROLLED_WINDOW(sw));
    -
    - if(sw->vadjustment != NULL) {
    - double value = 0.0;
    -
    - value = gtk_adjustment_get_value(sw->vadjustment);
    - value += gtk_adjustment_get_page_increment(sw->vadjustment);
    - gtk_adjustment_set_value(sw->vadjustment, value);
    - }
    -}
    --- a/talkatu/talkatuscrolledwindow.h Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,44 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#if !defined(TALKATU_GLOBAL_HEADER_INSIDE) && !defined(TALKATU_COMPILATION)
    -#error "only <talkatu.h> may be included directly"
    -#endif
    -
    -#ifndef TALKATU_SCROLLED_WINDOW_H
    -#define TALKATU_SCROLLED_WINDOW_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gtk/gtk.h>
    -
    -G_BEGIN_DECLS
    -
    -#define TALKATU_TYPE_SCROLLED_WINDOW (talkatu_scrolled_window_get_type())
    -
    -G_DECLARE_FINAL_TYPE(TalkatuScrolledWindow, talkatu_scrolled_window, TALKATU, SCROLLED_WINDOW, GtkScrolledWindow)
    -
    -GtkWidget *talkatu_scrolled_window_new(GtkAdjustment *hadjustment, GtkAdjustment *vadjustment);
    -
    -void talkatu_scrolled_window_page_up(TalkatuScrolledWindow *sw);
    -void talkatu_scrolled_window_page_down(TalkatuScrolledWindow *sw);
    -
    -G_END_DECLS
    -
    -#endif /* TALKATU_SCROLLED_WINDOW_H */
    --- a/talkatu/talkatutagtable.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatutagtable.c Sun Aug 28 22:44:59 2022 -0500
    @@ -24,24 +24,12 @@
    #include <talkatu/talkatutag.h>
    #include <talkatu/talkatutagtable.h>
    -/**
    - * TalkatuTagTable:
    - *
    - * A #GtkTextTagTable subclass that is preloaded with all of the #TalkatuTag's.
    - */
    -struct _TalkatuTagTable {
    - GtkTextTagTable parent;
    -};
    -
    -G_DEFINE_TYPE(TalkatuTagTable, talkatu_tag_table, GTK_TYPE_TEXT_TAG_TABLE)
    -
    /******************************************************************************
    - * GObject Stuff
    + * Helpers
    *****************************************************************************/
    static void
    -talkatu_tag_table_init(TalkatuTagTable *table) {
    +talkatu_tag_table_populate(GtkTextTagTable *tag_table) {
    GdkRGBA color = {0.0, 0.0, 0.0, 0.0};
    - GtkTextTagTable *tag_table = GTK_TEXT_TAG_TABLE(table);
    gtk_text_tag_table_add(
    tag_table,
    @@ -276,10 +264,6 @@
    );
    }
    -static void
    -talkatu_tag_table_class_init(TalkatuTagTableClass *klass) {
    -}
    -
    /******************************************************************************
    * Public API
    *****************************************************************************/
    @@ -287,14 +271,15 @@
    /**
    * talkatu_tag_table_new:
    *
    - * Creates a new #TalkatuTagTable that provides the user with an interface to
    - * control the formatting of a #TalkatuBuffer.
    + * Creates a new [class@Gtk.TextTagTable] that is populated with all of the
    + * tags that Talkatu uses.
    *
    - * Returns: (transfer full): The new #TalkatuTagTable instance.
    + * Returns: (transfer full): The new instance.
    */
    GtkTextTagTable *talkatu_tag_table_new(void) {
    - return GTK_TEXT_TAG_TABLE(g_object_new(
    - TALKATU_TYPE_TAG_TABLE,
    - NULL
    - ));
    + GtkTextTagTable *table = gtk_text_tag_table_new();
    +
    + talkatu_tag_table_populate(table);
    +
    + return table;
    }
    --- a/talkatu/talkatutagtable.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatutagtable.h Sun Aug 28 22:44:59 2022 -0500
    @@ -24,16 +24,11 @@
    #define TALKATU_TAG_TABLE_H
    #include <glib.h>
    -#include <glib-object.h>
    #include <gtk/gtk.h>
    G_BEGIN_DECLS
    -#define TALKATU_TYPE_TAG_TABLE (talkatu_tag_table_get_type())
    -
    -G_DECLARE_FINAL_TYPE(TalkatuTagTable, talkatu_tag_table, TALKATU, TAG_TABLE, GtkTextTagTable)
    -
    GtkTextTagTable *talkatu_tag_table_new(void);
    G_END_DECLS
    --- a/talkatu/talkatutoolbar.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatutoolbar.c Sun Aug 28 22:44:59 2022 -0500
    @@ -28,10 +28,10 @@
    * autonomous.
    */
    struct _TalkatuToolbar {
    - GtkToolbar parent;
    + GtkBox parent;
    };
    -G_DEFINE_TYPE(TalkatuToolbar, talkatu_toolbar, GTK_TYPE_TOOLBAR)
    +G_DEFINE_TYPE(TalkatuToolbar, talkatu_toolbar, GTK_TYPE_BOX)
    /******************************************************************************
    * GObject Stuff
    --- a/talkatu/talkatutoolbar.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatutoolbar.h Sun Aug 28 22:44:59 2022 -0500
    @@ -32,7 +32,7 @@
    #define TALKATU_TYPE_TOOLBAR (talkatu_toolbar_get_type())
    -G_DECLARE_FINAL_TYPE(TalkatuToolbar, talkatu_toolbar, TALKATU, TOOLBAR, GtkToolbar)
    +G_DECLARE_FINAL_TYPE(TalkatuToolbar, talkatu_toolbar, TALKATU, TOOLBAR, GtkBox)
    GtkWidget *talkatu_toolbar_new(void);
    --- a/talkatu/talkatutooldrawer.c Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,357 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#include <stdio.h>
    -
    -#include "talkatu/talkatutooldrawer.h"
    -#include "talkatu/talkatumenutoolbutton.h"
    -
    -typedef struct {
    - GAction *action;
    - gchar *markup;
    - gchar *icon_name;
    - gchar *tooltip;
    - GCallback callback;
    -} TalkatuToolDrawerItem;
    -
    -/**
    - * TalkatuToolDrawer:
    - *
    - * A #GtkToolItem subclass that displays a menu of items or can be expanded
    - * to show all of the items as a toolbar.
    - *
    - * Stability: Unstable
    - */
    -struct _TalkatuToolDrawer {
    - GtkToolItem parent;
    -
    - gchar *label;
    - gchar *icon_name;
    - gboolean expanded;
    -
    - GtkWidget *menu;
    -
    - GtkToolItem *menu_button;
    - GtkWidget *box;
    - GtkWidget *icons;
    -};
    -
    -enum {
    - PROP_0 = 0,
    - PROP_LABEL,
    - PROP_ICON_NAME,
    - PROP_EXPANDED,
    - N_PROPERTIES,
    -};
    -
    -static GParamSpec *properties[N_PROPERTIES] = {NULL,};
    -
    -G_DEFINE_TYPE(TalkatuToolDrawer, talkatu_tool_drawer, GTK_TYPE_TOOL_ITEM)
    -
    -/******************************************************************************
    - * GObject Stuff
    - *****************************************************************************/
    -static void
    -talkatu_tool_drawer_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) {
    - TalkatuToolDrawer *drawer = TALKATU_TOOL_DRAWER(obj);
    -
    - switch(prop_id) {
    - case PROP_LABEL:
    - g_value_set_string(value, talkatu_tool_drawer_get_label(drawer));
    - break;
    - case PROP_ICON_NAME:
    - g_value_set_string(value, talkatu_tool_drawer_get_icon_name(drawer));
    - break;
    - case PROP_EXPANDED:
    - g_value_set_boolean(value, talkatu_tool_drawer_get_expanded(drawer));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -talkatu_tool_drawer_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) {
    - TalkatuToolDrawer *drawer = TALKATU_TOOL_DRAWER(obj);
    -
    - switch(prop_id) {
    - case PROP_LABEL:
    - talkatu_tool_drawer_set_label(drawer, g_value_get_string(value));
    - break;
    - case PROP_ICON_NAME:
    - talkatu_tool_drawer_set_icon_name(drawer, g_value_get_string(value));
    - break;
    - case PROP_EXPANDED:
    - talkatu_tool_drawer_set_expanded(drawer, g_value_get_boolean(value));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -talkatu_tool_drawer_init(TalkatuToolDrawer *drawer) {
    -}
    -
    -static void
    -talkatu_tool_drawer_constructed(GObject *obj) {
    - TalkatuToolDrawer *drawer = TALKATU_TOOL_DRAWER(obj);
    -
    - drawer->menu = gtk_menu_new();
    -
    - drawer->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    - gtk_container_add(GTK_CONTAINER(drawer), drawer->box);
    -
    - drawer->menu_button = talkatu_menu_tool_button_new(drawer->label, drawer->icon_name, drawer->menu);
    - gtk_box_pack_start(GTK_BOX(drawer->box), GTK_WIDGET(drawer->menu_button), FALSE, FALSE, 0);
    -
    - drawer->icons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 3);
    - gtk_box_pack_start(GTK_BOX(drawer->box), drawer->icons, FALSE, FALSE, 0);
    -
    - /* Start collapsed. */
    - g_object_set(G_OBJECT(drawer->menu_button), "no-show-all", FALSE, NULL);
    - gtk_widget_show_all(GTK_WIDGET(drawer->menu_button));
    - g_object_set(G_OBJECT(drawer->icons), "no-show-all", TRUE, NULL);
    -}
    -
    -static void
    -talkatu_tool_drawer_class_init(TalkatuToolDrawerClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    -
    - obj_class->get_property = talkatu_tool_drawer_get_property;
    - obj_class->set_property = talkatu_tool_drawer_set_property;
    - obj_class->constructed = talkatu_tool_drawer_constructed;
    -
    - properties[PROP_LABEL] = g_param_spec_string(
    - "label", "label", "The label to display when not expanded",
    - NULL,
    - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
    - );
    - properties[PROP_ICON_NAME] = g_param_spec_string(
    - "icon-name", "icon-name", "The name of the icon to display when not expanded",
    - NULL,
    - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
    - );
    - properties[PROP_EXPANDED] = g_param_spec_boolean(
    - "expanded", "expanded", "Whether or not the drawer is expanded",
    - FALSE,
    - G_PARAM_READWRITE | G_PARAM_CONSTRUCT
    - );
    -
    - g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -
    -/**
    - * talkatu_tool_drawer_new:
    - * @label: The label to give item.
    - * @icon_name: The name of the icon for this item.
    - *
    - * Creates a new #TalkatuToolDrawer instance.
    - *
    - * Returns: (transfer full): The new #TalkatuToolDrawer instance.
    - */
    -GtkToolItem *
    -talkatu_tool_drawer_new(const gchar *label, const gchar *icon_name) {
    - return g_object_new(
    - TALKATU_TYPE_TOOL_DRAWER,
    - "label", label,
    - "icon-name", icon_name,
    - NULL
    - );
    -}
    -
    -/**
    - * talkatu_tool_drawer_add_item:
    - * @drawer: The #TalkatuToolDrawer instance.
    - * @action: The #GAction to add.
    - * @markup: Pango markup to use as a label.
    - * @icon_name: The name of the icon to display.
    - * @tooltip: UTF-8 text to display as a tooltip.
    - * @callback: (scope notified): The callback to call when the item is
    - * activated.
    - *
    - * Adds a new item to @drawer.
    - */
    -void
    -talkatu_tool_drawer_add_item(TalkatuToolDrawer *drawer, GAction *action, const gchar *markup, const gchar *icon_name, gchar *tooltip, GCallback callback) {
    - GtkWidget *button = NULL, *item = NULL, *label = NULL, *image = NULL;
    - const GVariantType *state_type = NULL;
    -
    - g_return_if_fail(TALKATU_IS_TOOL_DRAWER(drawer));
    -
    - /* create the menu item */
    - state_type = g_action_get_state_type(action);
    - if(state_type) {
    - item = gtk_check_menu_item_new();
    - } else {
    - item = gtk_menu_item_new();
    - }
    -
    - label = gtk_label_new(markup);
    - gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
    - gtk_label_set_xalign(GTK_LABEL(label), 0);
    - gtk_container_add(GTK_CONTAINER(item), label);
    -
    - gtk_menu_shell_append(GTK_MENU_SHELL(drawer->menu), item);
    - gtk_widget_show_all(item);
    -
    - /* create the toolbar button and add it to the toolbar */
    - if(state_type) {
    - button = gtk_toggle_button_new();
    - } else {
    - button = gtk_button_new();
    - }
    -
    - gtk_widget_set_can_focus(button, FALSE);
    - gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    -
    - image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR);
    - gtk_container_add(GTK_CONTAINER(button), image);
    -
    - gtk_box_pack_start(GTK_BOX(drawer->icons), button, FALSE, FALSE, 0);
    -}
    -
    -/**
    - * talkatu_tool_drawer_add_separator:
    - * @drawer: The #TalkatuToolDrawer instance.
    - *
    - * Adds a separator to the end of @drawer.
    - */
    -void
    -talkatu_tool_drawer_add_separator(TalkatuToolDrawer *drawer) {
    - g_return_if_fail(TALKATU_IS_TOOL_DRAWER(drawer));
    -
    - gtk_menu_shell_append(GTK_MENU_SHELL(drawer->menu), gtk_separator_menu_item_new());
    -
    - gtk_box_pack_start(GTK_BOX(drawer->icons), gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
    -}
    -
    -/**
    - * talkatu_tool_drawer_get_label:
    - * @drawer: The #TalkatuToolDrawer instance.
    - *
    - * Gets the label that's displayed when @drawer is collapsed.
    - *
    - * Returns: The label to use when @drawer is collapsed.
    - */
    -const gchar *
    -talkatu_tool_drawer_get_label(TalkatuToolDrawer *drawer) {
    - g_return_val_if_fail(TALKATU_IS_TOOL_DRAWER(drawer), FALSE);
    -
    - return drawer->label;
    -}
    -
    -/**
    - * talkatu_tool_drawer_set_label:
    - * @drawer: The #TalkatuToolDrawer instance.
    - * @label: The label to use when @drawer is collapsed.
    - *
    - * Sets the label to be displayed when @drawer is collapsed.
    - */
    -void
    -talkatu_tool_drawer_set_label(TalkatuToolDrawer *drawer, const gchar *label) {
    - g_return_if_fail(TALKATU_IS_TOOL_DRAWER(drawer));
    -
    - g_free(drawer->label);
    -
    - drawer->label = g_strdup(label);
    -
    - g_object_notify_by_pspec(G_OBJECT(drawer), properties[PROP_LABEL]);
    -}
    -
    -/**
    - * talkatu_tool_drawer_get_icon_name:
    - * @drawer: The #TalkatuToolDrawer instance.
    - *
    - * Gets the icon name for @drawer.
    - *
    - * Returns: The icon name for @drawer.
    - */
    -const gchar *
    -talkatu_tool_drawer_get_icon_name(TalkatuToolDrawer *drawer) {
    - g_return_val_if_fail(TALKATU_IS_TOOL_DRAWER(drawer), FALSE);
    -
    - return drawer->icon_name;
    -}
    -
    -/**
    - * talkatu_tool_drawer_set_icon_name:
    - * @drawer: The #TalkatuToolDrawer instance.
    - * @icon_name: The name of the icon to display when collapse.
    - *
    - * Sets the name of the icon to be displayed when @drawer is collapsed.
    - */
    -void
    -talkatu_tool_drawer_set_icon_name(TalkatuToolDrawer *drawer, const gchar *icon_name) {
    - g_return_if_fail(TALKATU_IS_TOOL_DRAWER(drawer));
    -
    - g_free(drawer->icon_name);
    -
    - drawer->icon_name = g_strdup(icon_name);
    -
    - g_object_notify_by_pspec(G_OBJECT(drawer), properties[PROP_ICON_NAME]);
    -}
    -
    -/**
    - * talkatu_tool_drawer_set_expanded:
    - * @drawer: The #TalkatuToolDrawer instance.
    - * @expanded: %TRUE to expand @drawer, %FALSE to collapse.
    - *
    - * Sets whether or not @drawer is expanded.
    - */
    -void
    -talkatu_tool_drawer_set_expanded(TalkatuToolDrawer *drawer, gboolean expanded) {
    - g_return_if_fail(TALKATU_IS_TOOL_DRAWER(drawer));
    -
    - if(expanded != drawer->expanded) {
    - drawer->expanded = expanded;
    -
    - g_object_set(G_OBJECT(drawer->menu_button), "no-show-all", drawer->expanded, NULL);
    - g_object_set(G_OBJECT(drawer->icons), "no-show-all", !drawer->expanded, NULL);
    - if(expanded) {
    - gtk_widget_hide(GTK_WIDGET(drawer->menu_button));
    - gtk_widget_show_all(drawer->icons);
    - } else {
    - gtk_widget_show_all(GTK_WIDGET(drawer->menu_button));
    - gtk_widget_hide(drawer->icons);
    - }
    -
    - g_object_notify_by_pspec(G_OBJECT(drawer), properties[PROP_EXPANDED]);
    - }
    -}
    -
    -/**
    - * talkatu_tool_drawer_get_expanded:
    - * @drawer: The #TalkatuToolDrawer instance.
    - *
    - * Gets whether or not @drawer is expanded.
    - *
    - * Returns: %TRUE if @drawer is expanded, %FALSE otherwise.
    - */
    -gboolean
    -talkatu_tool_drawer_get_expanded(TalkatuToolDrawer *drawer) {
    - g_return_val_if_fail(TALKATU_IS_TOOL_DRAWER(drawer), FALSE);
    -
    - return drawer->expanded;
    -}
    --- a/talkatu/talkatutooldrawer.h Sat Aug 13 22:03:08 2022 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,53 +0,0 @@
    -/*
    - * 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/>.
    - */
    -
    -#if !defined(TALKATU_GLOBAL_HEADER_INSIDE) && !defined(TALKATU_COMPILATION)
    -#error "only <talkatu.h> may be included directly"
    -#endif
    -
    -#ifndef TALKATU_TOOL_DRAWER_H
    -#define TALKATU_TOOL_DRAWER_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gtk/gtk.h>
    -
    -G_BEGIN_DECLS
    -
    -#define TALKATU_TYPE_TOOL_DRAWER (talkatu_tool_drawer_get_type())
    -
    -G_DECLARE_FINAL_TYPE(TalkatuToolDrawer, talkatu_tool_drawer, TALKATU, TOOL_DRAWER, GtkToolItem)
    -
    -GtkToolItem *talkatu_tool_drawer_new(const gchar *label, const gchar *icon_name);
    -
    -void talkatu_tool_drawer_add_item(TalkatuToolDrawer *drawer, GAction *action, const gchar *markup, const gchar *icon_name, gchar *tooltip, GCallback callback);
    -void talkatu_tool_drawer_add_separator(TalkatuToolDrawer *drawer);
    -
    -const gchar *talkatu_tool_drawer_get_label(TalkatuToolDrawer *drawer);
    -void talkatu_tool_drawer_set_label(TalkatuToolDrawer *drawer, const gchar *label);
    -
    -const gchar *talkatu_tool_drawer_get_icon_name(TalkatuToolDrawer *drawer);
    -void talkatu_tool_drawer_set_icon_name(TalkatuToolDrawer *drawer, const gchar *icon_name);
    -
    -void talkatu_tool_drawer_set_expanded(TalkatuToolDrawer *drawer, gboolean expanded);
    -gboolean talkatu_tool_drawer_get_expanded(TalkatuToolDrawer *drawer);
    -
    -G_END_DECLS
    -
    -#endif /* TALKATU_TOOL_DRAWER_H */
    --- a/talkatu/talkatutypinglabel.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatutypinglabel.c Sun Aug 28 22:44:59 2022 -0500
    @@ -37,11 +37,13 @@
    * Usernames are passed in as strings and their default display can be
    * overridden by connecting to the #TalkatuTypingLabel::changed signal.
    */
    -typedef struct {
    - GtkLabel parent;
    +struct _TalkatuTypingLabel {
    + GtkWidget parent;
    +
    + GtkWidget *label;
    GHashTable *typers;
    -} TalkatuTypingLabelPrivate;
    +};
    /**
    * TalkatuTypingLabelClass:
    @@ -62,7 +64,7 @@
    };
    static guint signals[LAST_SIGNAL] = {0, };
    -G_DEFINE_TYPE_WITH_PRIVATE(TalkatuTypingLabel, talkatu_typing_label, GTK_TYPE_LABEL)
    +G_DEFINE_TYPE(TalkatuTypingLabel, talkatu_typing_label, GTK_TYPE_WIDGET)
    #define TALKATU_TYPING_LABEL_TIMEOUT (30)
    @@ -101,14 +103,14 @@
    gint n_typers = 0;
    if(typers == NULL) {
    - gtk_label_set_text(GTK_LABEL(label), "");
    + gtk_label_set_text(GTK_LABEL(label->label), "");
    return;
    }
    n_typers = g_list_length(typers);
    if(n_typers > 3) {
    - gtk_label_set_text(GTK_LABEL(label),
    + gtk_label_set_text(GTK_LABEL(label->label),
    _("Several people are typing..."));
    } else {
    gchar *text = NULL;
    @@ -126,7 +128,7 @@
    (gchar *)typers->next->next->data);
    }
    - gtk_label_set_text(GTK_LABEL(label), text);
    + gtk_label_set_text(GTK_LABEL(label->label), text);
    g_free(text);
    }
    }
    @@ -136,24 +138,26 @@
    *****************************************************************************/
    static void
    talkatu_typing_label_init(TalkatuTypingLabel *label) {
    - TalkatuTypingLabelPrivate *priv = NULL;
    + gtk_widget_init_template(GTK_WIDGET(label));
    - /* set our xalign to 0 to make it left justified... */
    - gtk_label_set_xalign(GTK_LABEL(label), 0);
    + label->typers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
    + talkatu_typing_label_typer_value_free);
    +}
    - priv = talkatu_typing_label_get_instance_private(TALKATU_TYPING_LABEL(label));
    +static void
    +talkatu_typing_label_dispose(GObject *obj) {
    + TalkatuTypingLabel *label = TALKATU_TYPING_LABEL(obj);
    - priv->typers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
    - talkatu_typing_label_typer_value_free);
    + g_clear_pointer(&label->label, gtk_widget_unparent);
    +
    + G_OBJECT_CLASS(talkatu_typing_label_parent_class)->dispose(obj);
    }
    static void
    talkatu_typing_label_finalize(GObject *obj) {
    - TalkatuTypingLabelPrivate *priv = NULL;
    + TalkatuTypingLabel *label = TALKATU_TYPING_LABEL(obj);
    - priv = talkatu_typing_label_get_instance_private(TALKATU_TYPING_LABEL(obj));
    -
    - g_hash_table_destroy(priv->typers);
    + g_hash_table_destroy(label->typers);
    G_OBJECT_CLASS(talkatu_typing_label_parent_class)->finalize(obj);
    }
    @@ -161,11 +165,11 @@
    static void
    talkatu_typing_label_class_init(TalkatuTypingLabelClass *klass) {
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    + obj_class->dispose = talkatu_typing_label_dispose;
    obj_class->finalize = talkatu_typing_label_finalize;
    - klass->changed = talkatu_typing_label_default_changed;
    -
    /**
    * TalkatuTypingLabel::changed:
    * @talkatutypinglabel: The #TalkatuTypingLabel instance.
    @@ -174,11 +178,11 @@
    *
    * Emitted when the typing state of an individual has changed.
    */
    - signals[SIG_CHANGED] = g_signal_new(
    + signals[SIG_CHANGED] = g_signal_new_class_handler(
    "changed",
    G_TYPE_FROM_CLASS(klass),
    G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
    - G_STRUCT_OFFSET(TalkatuTypingLabelClass, changed),
    + G_CALLBACK(talkatu_typing_label_default_changed),
    NULL,
    NULL,
    NULL,
    @@ -186,6 +190,16 @@
    1,
    G_TYPE_POINTER
    );
    +
    + gtk_widget_class_set_template_from_resource(
    + GTK_WIDGET_CLASS(klass),
    + "/org/imfreedom/keep/talkatu/talkatu/ui/typinglabel.ui"
    + );
    +
    + gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT);
    +
    + gtk_widget_class_bind_template_child(widget_class, TalkatuTypingLabel,
    + label);
    }
    /******************************************************************************
    @@ -200,10 +214,7 @@
    * Returns: (transfer full): The new #TalkatuTypingLabel instance.
    */
    GtkWidget *talkatu_typing_label_new(void) {
    - return GTK_WIDGET(g_object_new(
    - TALKATU_TYPE_TYPING_LABEL,
    - NULL
    - ));
    + return g_object_new(TALKATU_TYPE_TYPING_LABEL, NULL);
    }
    /**
    @@ -215,7 +226,6 @@
    */
    void
    talkatu_typing_label_start_typing(TalkatuTypingLabel *label, const gchar *who) {
    - TalkatuTypingLabelPrivate *priv = NULL;
    TalkatuTypingLabelTimeoutData *typing_data = NULL;
    guint timeout_id = 0;
    gpointer value = NULL;
    @@ -223,8 +233,6 @@
    g_return_if_fail(TALKATU_IS_TYPING_LABEL(label));
    g_return_if_fail(who != NULL);
    - priv = talkatu_typing_label_get_instance_private(label);
    -
    /* create a timeout to remove this person from the list */
    typing_data = g_new(TalkatuTypingLabelTimeoutData, 1);
    typing_data->label = label;
    @@ -240,9 +248,9 @@
    value = GUINT_TO_POINTER(timeout_id);
    - if(g_hash_table_replace(priv->typers, typing_data->who, value)) {
    + if(g_hash_table_replace(label->typers, typing_data->who, value)) {
    /* the user wasn't in the list so we need to emit our changed signal */
    - GList *typers = g_hash_table_get_keys(priv->typers);
    + GList *typers = g_hash_table_get_keys(label->typers);
    g_signal_emit(label, signals[SIG_CHANGED], 0, typers);
    @@ -261,15 +269,11 @@
    talkatu_typing_label_finish_typing(TalkatuTypingLabel *label,
    const gchar* who)
    {
    - TalkatuTypingLabelPrivate *priv = NULL;
    -
    g_return_if_fail(TALKATU_IS_TYPING_LABEL(label));
    g_return_if_fail(who != NULL);
    - priv = talkatu_typing_label_get_instance_private(label);
    -
    - if(g_hash_table_remove(priv->typers, who)) {
    - GList *typers = g_hash_table_get_keys(priv->typers);
    + if(g_hash_table_remove(label->typers, who)) {
    + GList *typers = g_hash_table_get_keys(label->typers);
    /* emit our changed signal */
    g_signal_emit(label, signals[SIG_CHANGED], 0, typers);
    @@ -277,4 +281,3 @@
    g_list_free(typers);
    }
    }
    -
    --- a/talkatu/talkatutypinglabel.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatutypinglabel.h Sun Aug 28 22:44:59 2022 -0500
    @@ -1,6 +1,6 @@
    /*
    * Talkatu - GTK widgets for chat applications
    - * Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
    + * Copyright (C) 2017-2022 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
    @@ -31,18 +31,7 @@
    G_BEGIN_DECLS
    #define TALKATU_TYPE_TYPING_LABEL (talkatu_typing_label_get_type())
    -G_DECLARE_DERIVABLE_TYPE(TalkatuTypingLabel, talkatu_typing_label, TALKATU, TYPING_LABEL, GtkLabel)
    -
    -struct _TalkatuTypingLabelClass {
    - /*< private >*/
    - GtkLabelClass parent;
    -
    - /*< public >*/
    - void (*changed)(TalkatuTypingLabel *label, GList *typers);
    -
    - /*< private >*/
    - gpointer reserved[4];
    -};
    +G_DECLARE_FINAL_TYPE(TalkatuTypingLabel, talkatu_typing_label, TALKATU, TYPING_LABEL, GtkWidget)
    GtkWidget *talkatu_typing_label_new(void);
    --- a/talkatu/talkatuview.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuview.c Sun Aug 28 22:44:59 2022 -0500
    @@ -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.
    @@ -48,15 +46,11 @@
    typedef struct {
    GSimpleActionGroup *action_group;
    - /* we cache the cursor that's displayed while hovering over a link as well
    - * the tag for links/anchors to avoid extra lookups.
    - */
    - GdkCursor *cursor_hand;
    - GtkTextTag *tag_anchor;
    + GtkWidget *menu;
    + gchar *url;
    } TalkatuViewPrivate;
    enum {
    - SIG_FORMAT_ACTIVATE,
    SIG_OPEN_URL,
    LAST_SIGNAL,
    };
    @@ -67,7 +61,7 @@
    /******************************************************************************
    * Helpers
    *****************************************************************************/
    -static gchar *
    +static const gchar *
    talkatu_view_url_from_iter(TalkatuView *view, GtkTextIter *iter) {
    GSList *tag = NULL;
    gchar *url = NULL;
    @@ -89,158 +83,113 @@
    }
    /******************************************************************************
    - * 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_pressed_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);
    + GtkTextIter iter;
    + 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) {
    + gtk_widget_set_cursor_from_name(GTK_WIDGET(view), "pointer");
    + } else {
    + gtk_widget_set_cursor_from_name(GTK_WIDGET(view), "text");
    + }
    }
    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 +198,7 @@
    GtkTooltip *tooltip)
    {
    GtkTextIter iter;
    - gchar *url = NULL;
    + const gchar *url = NULL;
    gint adj_x, adj_y;
    if(keyboard) {
    @@ -283,35 +232,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
    *****************************************************************************/
    @@ -327,37 +247,24 @@
    talkatu_view_finalize(GObject *obj) {
    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);
    }
    static void
    -talkatu_view_init(TalkatuView *view) {
    +talkatu_view_dispose(GObject *obj) {
    + TalkatuView *view = TALKATU_VIEW(obj);
    TalkatuViewPrivate *priv = talkatu_view_get_instance_private(view);
    - priv->cursor_hand = gdk_cursor_new_from_name(gdk_display_get_default(), "pointer");
    -
    - /* 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);
    + g_clear_pointer(&priv->menu, gtk_widget_unparent);
    - /* 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
    - );
    + G_OBJECT_CLASS(talkatu_view_parent_class)->dispose(obj);
    +}
    - /* we need to know when the buffer is changed in our parent so we can
    - * update our actions and other stuff.
    - */
    - g_signal_connect(
    - G_OBJECT(view),
    - "notify::buffer",
    - G_CALLBACK(talkatu_view_buffer_set_cb),
    - NULL
    - );
    +static void
    +talkatu_view_init(TalkatuView *view) {
    + gtk_widget_init_template(GTK_WIDGET(view));
    }
    static void
    @@ -365,42 +272,22 @@
    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->dispose = talkatu_view_dispose;
    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 +308,30 @@
    G_TYPE_STRING
    );
    - /* setup key bindings */
    - binding_set = gtk_binding_set_by_class(talkatu_view_parent_class);
    + gtk_widget_class_bind_template_callback(widget_class,
    + talkatu_view_pressed_cb);
    + gtk_widget_class_bind_template_callback(widget_class,
    + talkatu_view_motion_cb);
    + gtk_widget_class_bind_template_callback(widget_class,
    + talkatu_view_buffer_set_cb);
    - /* remove existing keybindings that we're overriding */
    - gtk_binding_entry_remove(binding_set, GDK_KEY_slash, GDK_CONTROL_MASK);
    + gtk_widget_class_bind_template_child_private(widget_class, TalkatuView, menu);
    +
    + /* 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", "🐣");
    }
    /******************************************************************************
    @@ -463,8 +358,5 @@
    * Returns: (transfer full): The new #TalkatuView.
    */
    GtkWidget *talkatu_view_new_with_buffer(GtkTextBuffer *buffer) {
    - return GTK_WIDGET(g_object_new(
    - TALKATU_TYPE_VIEW,
    - "buffer", buffer,
    - NULL));
    + return g_object_new(TALKATU_TYPE_VIEW, "buffer", buffer, NULL);
    }
    --- a/talkatu/talkatuview.h Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/talkatuview.h Sun Aug 28 22:44:59 2022 -0500
    @@ -30,8 +30,7 @@
    G_BEGIN_DECLS
    -#define TALKATU_TYPE_VIEW (talkatu_view_get_type())
    -
    +#define TALKATU_TYPE_VIEW (talkatu_view_get_type())
    G_DECLARE_DERIVABLE_TYPE(TalkatuView, talkatu_view, TALKATU, VIEW, GtkTextView)
    struct _TalkatuViewClass {
    --- a/talkatu/tests/meson.build Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/tests/meson.build Sun Aug 28 22:44:59 2022 -0500
    @@ -8,14 +8,14 @@
    e = executable(
    'test-html-serialization',
    'talkatutesthtmlserialization.c',
    - dependencies : [talkatu_dep, GLIB, GTK3]
    + dependencies : [talkatu_dep, GLIB, GTK4]
    )
    test('html', TEST_WRAPPER, args : e, is_parallel : false, env : testenv)
    e = executable(
    'test-action-group',
    'talkatutestactiongroup.c',
    - dependencies : [talkatu_dep, GLIB, GTK3]
    + dependencies : [talkatu_dep, GLIB, GTK4]
    )
    test('action-group', TEST_WRAPPER, args : e, is_parallel : false, env : testenv)
    --- a/talkatu/tests/talkatutestactiongroup.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/tests/talkatutestactiongroup.c Sun Aug 28 22:44:59 2022 -0500
    @@ -84,7 +84,7 @@
    g_test_init(&argc, &argv, NULL);
    - gtk_init(&argc, &argv);
    + gtk_init();
    talkatu_init();
    --- a/talkatu/tests/talkatutesthtmlpangorenderer.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/tests/talkatutesthtmlpangorenderer.c Sun Aug 28 22:44:59 2022 -0500
    @@ -140,7 +140,7 @@
    g_test_init(&argc, &argv, NULL);
    - gtk_init(&argc, &argv);
    + gtk_init();
    talkatu_init();
    --- a/talkatu/tests/talkatutesthtmlrenderer.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/tests/talkatutesthtmlrenderer.c Sun Aug 28 22:44:59 2022 -0500
    @@ -205,7 +205,7 @@
    g_test_init(&argc, &argv, NULL);
    - gtk_init(&argc, &argv);
    + gtk_init();
    talkatu_init();
    --- a/talkatu/tests/talkatutesthtmlserialization.c Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/tests/talkatutesthtmlserialization.c Sun Aug 28 22:44:59 2022 -0500
    @@ -79,7 +79,7 @@
    g_test_init(&argc, &argv, NULL);
    - gtk_init(&argc, &argv);
    + gtk_init();
    talkatu_init();
    --- a/talkatu/tests/test-wrapper.py Sat Aug 13 22:03:08 2022 -0500
    +++ b/talkatu/tests/test-wrapper.py Sun Aug 28 22:44:59 2022 -0500
    @@ -27,7 +27,7 @@
    def main():
    # start broadway
    - broadwayd = subprocess.Popen(['broadwayd'])
    + broadwayd = subprocess.Popen(['gtk4-broadwayd'])
    # run the unit test but set the GDK_BACKEND envvar to broadway
    env = {**os.environ, 'GDK_BACKEND': 'broadway'}
    --- a/vapi/meson.build Sat Aug 13 22:03:08 2022 -0500
    +++ b/vapi/meson.build Sun Aug 28 22:44:59 2022 -0500
    @@ -7,7 +7,7 @@
    talkatu_vapi = gnome.generate_vapi('talkatu',
    sources : talkatu_gir[0],
    - packages : [ 'gtk+-3.0' ],
    + packages : [ 'gtk4' ],
    install : true,
    gir_dirs : meson.current_build_dir() / '..' / 'talkatu',
    )