[quick_replies] Port to Gtk4

This commit is contained in:
Philipp Hörist
2025-01-26 10:14:13 +01:00
parent d5e0bf07ee
commit 69db16720d
3 changed files with 124 additions and 168 deletions

View File

@@ -16,15 +16,13 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from gi.repository import Gdk
from gi.repository import Gtk from gi.repository import Gtk
from gajim.common import app from gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
@@ -32,34 +30,48 @@ if TYPE_CHECKING:
from ..plugin import QuickRepliesPlugin from ..plugin import QuickRepliesPlugin
class ConfigDialog(Gtk.ApplicationWindow): class ConfigDialog(GajimAppWindow):
def __init__(self, plugin: QuickRepliesPlugin, transient: Gtk.Window) -> None: def __init__(self, plugin: QuickRepliesPlugin, transient: Gtk.Window) -> None:
Gtk.ApplicationWindow.__init__(self) GajimAppWindow.__init__(
self.set_application(app.app) self,
self.set_show_menubar(False) name="QuickRepliesConfigDialog",
self.set_title(_("Quick Replies Configuration")) title=_("Quick Replies Configuration"),
self.set_transient_for(transient) default_width=400,
self.set_default_size(400, 400) default_height=400,
self.set_type_hint(Gdk.WindowTypeHint.DIALOG) transient_for=transient,
self.set_modal(True) modal=True,
self.set_destroy_with_parent(True) )
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(str(ui_path.resolve() / "config.ui")) self._ui = get_builder(str(ui_path.resolve() / "config.ui"))
self._plugin = plugin self._plugin = plugin
self.add(self._ui.box) self.set_child(self._ui.box)
self._fill_list() self._load_replies()
self.show_all()
self._ui.connect_signals(self) self._connect(self._ui.add_button, "clicked", self._on_add_clicked)
self.connect("destroy", self._on_destroy) self._connect(self._ui.remove_button, "clicked", self._on_remove_clicked)
self._connect(self._ui.cellrenderer, "edited", self._on_reply_edited)
self._connect(self.window, "close-request", self._on_close_request)
def _fill_list(self) -> None: self.show()
for reply in self._plugin.quick_replies:
def _cleanup(self) -> None:
del self._plugin
def _on_close_request(self, win: Gtk.ApplicationWindow) -> None:
replies: list[str] = []
for row in self._ui.replies_store:
if row[0] == "":
continue
replies.append(row[0])
self._plugin.set_quick_replies(replies)
def _load_replies(self) -> None:
for reply in self._plugin.get_quick_replies():
self._ui.replies_store.append([reply]) self._ui.replies_store.append([reply])
def _on_reply_edited( def _on_reply_edited(
@@ -85,11 +97,3 @@ class ConfigDialog(Gtk.ApplicationWindow):
for ref in references: for ref in references:
iter_ = model.get_iter(ref.get_path()) iter_ = model.get_iter(ref.get_path())
self._ui.replies_store.remove(iter_) self._ui.replies_store.remove(iter_)
def _on_destroy(self, *args: Any) -> None:
replies: list[str] = []
for row in self._ui.replies_store:
if row[0] == "":
continue
replies.append(row[0])
self._plugin.set_quick_replies(replies)

View File

@@ -1,28 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk" version="4.0"/>
<object class="GtkListStore" id="replies_store"> <object class="GtkListStore" id="replies_store">
<columns> <columns>
<!-- column-name reply -->
<column type="gchararray"/> <column type="gchararray"/>
</columns> </columns>
</object> </object>
<object class="GtkBox" id="box"> <object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">18</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="visible">True</property> <property name="focusable">1</property>
<property name="can_focus">True</property> <property name="vexpand">1</property>
<property name="vexpand">True</property> <property name="hexpand">1</property>
<property name="shadow_type">in</property> <property name="child">
<child>
<object class="GtkTreeView" id="replies_treeview"> <object class="GtkTreeView" id="replies_treeview">
<property name="visible">True</property> <property name="focusable">1</property>
<property name="can_focus">True</property>
<property name="model">replies_store</property> <property name="model">replies_store</property>
<property name="search_column">1</property> <property name="search_column">1</property>
<child internal-child="selection"> <child internal-child="selection">
@@ -32,15 +25,14 @@
</child> </child>
<child> <child>
<object class="GtkTreeViewColumn"> <object class="GtkTreeViewColumn">
<property name="resizable">True</property> <property name="resizable">1</property>
<property name="title" translatable="yes">Quick Reply</property> <property name="title" translatable="1">Quick Reply</property>
<property name="clickable">True</property> <property name="clickable">1</property>
<property name="sort_indicator">True</property> <property name="sort_indicator">1</property>
<property name="sort_column_id">0</property> <property name="sort_column_id">0</property>
<child> <child>
<object class="GtkCellRendererText"> <object class="GtkCellRendererText" id="cellrenderer">
<property name="editable">True</property> <property name="editable">1</property>
<signal name="edited" handler="_on_reply_edited" swapped="no"/>
</object> </object>
<attributes> <attributes>
<attribute name="text">0</attribute> <attribute name="text">0</attribute>
@@ -49,55 +41,28 @@
</object> </object>
</child> </child>
</object> </object>
</child> </property>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkToolbar"> <object class="GtkBox">
<property name="visible">True</property> <property name="css-classes">toolbar</property>
<property name="can_focus">False</property>
<property name="toolbar_style">icons</property>
<property name="icon_size">4</property>
<child>
<object class="GtkToolButton">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Add</property>
<property name="icon_name">list-add-symbolic</property>
<signal name="clicked" handler="_on_add_clicked" 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>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="icon_name">list-remove-symbolic</property>
<signal name="clicked" handler="_on_remove_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<style> <style>
<class name="inline-toolbar"/> <class name="inline-toolbar"/>
</style> </style>
<child>
<object class="GtkButton" id="add_button">
<property name="tooltip_text" translatable="1">Add</property>
<property name="icon_name">list-add-symbolic</property>
</object>
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="tooltip_text" translatable="1">Remove</property>
<property name="icon_name">list-remove-symbolic</property>
</object>
</child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child> </child>
</object> </object>
</interface> </interface>

View File

@@ -15,8 +15,6 @@
from __future__ import annotations from __future__ import annotations
from typing import cast
import json import json
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
@@ -28,7 +26,6 @@ from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
from gajim.gtk.message_actions_box import MessageActionsBox from gajim.gtk.message_actions_box import MessageActionsBox
from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
@@ -43,21 +40,55 @@ class QuickRepliesPlugin(GajimPlugin):
self.gui_extension_points = { self.gui_extension_points = {
"message_actions_box": (self._message_actions_box_created, None), "message_actions_box": (self._message_actions_box_created, None),
} }
self._button = None
self.quick_replies = self._load_quick_replies() self._quick_replies = self._load_quick_replies()
self._actions: list[Gio.SimpleAction] = []
self._button = self._create_menu_button()
self._create_actions()
def _create_menu_button(self) -> Gtk.MenuButton:
plugin_path = Path(__file__).parent
img_path = plugin_path.resolve() / "quick_replies.png"
img = Gtk.Image.new_from_file(str(img_path))
button = Gtk.MenuButton(
tooltip_text=_("Quick Replies"),
menu_model=self._create_menu(),
child=img,
)
return button
def _create_actions(self) -> None:
actions = [
("quick-reply", "s"),
("quick-reply-config", None),
]
for action, variant_type in actions:
if variant_type is not None:
variant_type = GLib.VariantType(variant_type)
act = Gio.SimpleAction.new(action, variant_type)
act.connect("activate", self._on_action)
self._actions.append(act)
def deactivate(self) -> None: def deactivate(self) -> None:
assert self._button is not None self._action_box.remove(self._button)
self._button.destroy() for action in self._actions:
del self._button app.window.remove_action(action.get_name())
def _message_actions_box_created( def _message_actions_box_created(
self, message_actions_box: MessageActionsBox, gtk_box: Gtk.Box self, message_actions_box: MessageActionsBox, gtk_box: Gtk.Box
) -> None: ) -> None:
self._button = QuickRepliesButton(self, message_actions_box.msg_textview) for action in self._actions:
gtk_box.pack_start(self._button, False, False, 0) app.window.add_action(action)
self._button.show()
self._message_input = message_actions_box.msg_textview
self._action_box = gtk_box
self._action_box.append(self._button)
@staticmethod @staticmethod
def _load_quick_replies() -> list[str]: def _load_quick_replies() -> list[str]:
@@ -92,77 +123,33 @@ class QuickRepliesPlugin(GajimPlugin):
json.dump(quick_replies, file) json.dump(quick_replies, file)
def set_quick_replies(self, quick_replies: list[str]) -> None: def set_quick_replies(self, quick_replies: list[str]) -> None:
self.quick_replies = quick_replies self._quick_replies = quick_replies
self._save_quick_replies(quick_replies) self._save_quick_replies(quick_replies)
assert self._button is not None self._button.set_menu_model(self._create_menu())
self._button.update_menu()
def get_quick_replies(self) -> list[str]:
return self._quick_replies
class QuickRepliesButton(Gtk.MenuButton): def _create_menu(self) -> Gio.Menu:
def __init__( menu = Gio.Menu()
self, plugin: QuickRepliesPlugin, message_input: MessageInputTextView menu.append_item(
) -> None: Gio.MenuItem.new(_("Manage Replies…"), "win.quick-reply-config")
)
Gtk.MenuButton.__init__(self) for reply in self._quick_replies:
self.get_style_context().add_class("chatcontrol-actionbar-button") menu.append_item(Gio.MenuItem.new(reply[:15], f"win.quick-reply::{reply}"))
self.set_property("relief", Gtk.ReliefStyle.NONE)
self.set_can_focus(False)
plugin_path = Path(__file__).parent
img_path = plugin_path.resolve() / "quick_replies.png"
img = Gtk.Image.new_from_file(str(img_path))
self.set_image(img)
self.set_tooltip_text(_("Quick Replies"))
self._plugin = plugin return menu
self._message_input = message_input
self._menu = Gio.Menu() def _on_action(
self._popover = Gtk.Popover() self, action: Gio.SimpleAction, param: GLib.Variant | None
self._popover.bind_model(self._menu) ) -> int | None:
self.set_popover(self._popover) name = action.get_name()
if name == "quick-reply-config":
self.config_dialog(app.window)
self.update_menu() elif name == "quick-reply":
assert param is not None
def update_menu(self) -> None: message_buffer = self._message_input.get_buffer()
self._menu.remove_all() message_buffer.insert_at_cursor(param.get_string().rstrip() + " ")
self._message_input.grab_focus()
# Add config item
action_data = GLib.Variant("s", "plugin-configuration")
menu_item = Gio.MenuItem()
menu_item.set_label(_("Manage Replies…"))
menu_item.set_attribute_value("action-data", action_data)
self._menu.append_item(menu_item)
# Add quick replies
for reply in self._plugin.quick_replies:
assert isinstance(reply, str)
action_data = GLib.Variant("s", reply)
menu_item = Gio.MenuItem()
menu_item.set_label(reply)
menu_item.set_attribute_value("action-data", action_data)
self._menu.append_item(menu_item)
menu_buttons = self._get_menu_buttons()
for button in menu_buttons:
button.connect(
"clicked", self._on_button_clicked, menu_buttons.index(button)
)
def _on_button_clicked(self, _button: Gtk.MenuButton, index: int) -> None:
variant = self._menu.get_item_attribute_value(index, "action-data")
if variant.get_string() == "plugin-configuration":
self._popover.popdown()
self._plugin.config_dialog(app.window)
return
message_buffer = self._message_input.get_buffer()
message_buffer.insert_at_cursor(variant.get_string().rstrip() + " ")
self._popover.popdown()
self._message_input.grab_focus()
def _get_menu_buttons(self) -> list[Gtk.ModelButton]:
stack = cast(Gtk.Stack, self._popover.get_children()[0])
menu_section_box = cast(Gtk.Box, stack.get_children()[0])
box = cast(Gtk.Box, menu_section_box.get_children()[0])
items = cast(list[Gtk.ModelButton], box.get_children())
return items