[triggers] Port to Gtk4

This commit is contained in:
Philipp Hörist
2025-01-26 18:48:25 +01:00
parent fd5d9fd33c
commit 1692ecda41
5 changed files with 401 additions and 672 deletions

View File

@@ -22,7 +22,6 @@ exclude = [
".venv",
"openpgp/*",
"pgp/*",
"triggers/*",
]
[tool.ruff]

View File

@@ -17,16 +17,16 @@
from __future__ import annotations
from typing import Any
from typing import cast
from typing import TYPE_CHECKING
from pathlib import Path
from gi.repository import Gdk
from gi.repository import Gtk
from gajim.common import app
from gajim.common.helpers import play_sound_file
from gajim.common.util.status import get_uf_show
from gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
@@ -40,98 +40,139 @@ EVENTS: dict[str, Any] = {
RECIPIENT_TYPES = ["contact", "group", "groupchat", "all"]
class ConfigDialog(Gtk.ApplicationWindow):
class ConfigBuilder(Gtk.Builder):
liststore1: Gtk.ListStore
liststore2: Gtk.ListStore
box: Gtk.Box
rules_box: Gtk.Box
conditions_treeview: Gtk.TreeView
up_button: Gtk.Button
down_button: Gtk.Button
new_button: Gtk.Button
delete_button: Gtk.Button
config_box: Gtk.Box
event_combobox: Gtk.ComboBox
recipient_type_combobox: Gtk.ComboBox
recipient_list_entry: Gtk.Entry
all_status_rb: Gtk.CheckButton
special_status_rb: Gtk.CheckButton
status_expander: Gtk.Expander
online_cb: Gtk.CheckButton
away_cb: Gtk.CheckButton
xa_cb: Gtk.CheckButton
dnd_cb: Gtk.CheckButton
tab_opened_cb: Gtk.CheckButton
has_focus_cb: Gtk.CheckButton
not_tab_opened_cb: Gtk.CheckButton
not_has_focus_cb: Gtk.CheckButton
use_popup_cb: Gtk.CheckButton
disable_popup_cb: Gtk.CheckButton
use_sound_cb: Gtk.CheckButton
sound_file_box: Gtk.Box
play_button: Gtk.Button
disable_sound_cb: Gtk.CheckButton
run_command_cb: Gtk.CheckButton
command_entry: Gtk.Entry
one_shot_cb: Gtk.CheckButton
class ConfigDialog(GajimAppWindow):
def __init__(self, plugin: Triggers, transient: Gtk.Window) -> None:
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title(_("Triggers Configuration"))
self.set_transient_for(transient)
self.set_default_size(600, 800)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_modal(True)
self.set_destroy_with_parent(True)
GajimAppWindow.__init__(
self,
name="TriggersConfigDialog",
title=_("Triggers Configuration"),
default_width=600,
default_height=800,
transient_for=transient,
modal=True,
)
ui_path = Path(__file__).parent
self._ui = get_builder(str(ui_path.resolve() / "config.ui"))
self._ui = cast(
ConfigBuilder, get_builder(str(ui_path.resolve() / "config.ui"))
)
self._plugin = plugin
self.add(self._ui.box)
self.show_all()
self.set_child(self._ui.box)
self._active_num = -1
self._config: dict[int, Any] = {}
self._initialize()
self._ui.connect_signals(self)
self.connect("destroy", self._on_destroy)
self._connect(
self._ui.conditions_treeview,
"cursor-changed",
self._on_conditions_treeview_cursor_changed,
)
self._connect(self._ui.up_button, "clicked", self._on_up_button_clicked)
self._connect(self._ui.down_button, "clicked", self._on_down_button_clicked)
self._connect(self._ui.new_button, "clicked", self._on_new_button_clicked)
self._connect(self._ui.delete_button, "clicked", self._on_delete_button_clicked)
self._connect(
self._ui.event_combobox, "changed", self._on_event_combobox_changed
)
self._connect(
self._ui.recipient_type_combobox,
"changed",
self._on_recipient_type_combobox_changed,
)
self._connect(
self._ui.recipient_list_entry,
"changed",
self._on_recipient_list_entry_changed,
)
self._connect(
self._ui.all_status_rb, "toggled", self._on_status_radiobutton_toggled
)
self._connect(self._ui.online_cb, "toggled", self._on_status_cb_toggled)
self._connect(self._ui.away_cb, "toggled", self._on_status_cb_toggled)
self._connect(self._ui.xa_cb, "toggled", self._on_status_cb_toggled)
self._connect(self._ui.dnd_cb, "toggled", self._on_status_cb_toggled)
self._connect(self._ui.tab_opened_cb, "toggled", self._on_tab_opened_cb_toggled)
self._connect(self._ui.has_focus_cb, "toggled", self._on_has_focus_cb_toggled)
self._connect(
self._ui.not_tab_opened_cb, "toggled", self._on_not_tab_opened_cb_toggled
)
self._connect(
self._ui.not_has_focus_cb, "toggled", self._on_not_has_focus_cb_toggled
)
self._connect(self._ui.use_popup_cb, "toggled", self._on_use_popup_cb_toggled)
self._connect(
self._ui.disable_popup_cb, "toggled", self._on_disable_popup_cb_toggled
)
self._connect(self._ui.use_sound_cb, "toggled", self._on_use_sound_cb_toggled)
self._connect(self._ui.play_button, "clicked", self._on_play_button_clicked)
self._connect(
self._ui.disable_sound_cb, "toggled", self._on_disable_sound_cb_toggled
)
self._connect(
self._ui.run_command_cb, "toggled", self._on_run_command_cb_toggled
)
self._connect(self._ui.command_entry, "changed", self._on_command_entry_changed)
self._connect(self._ui.one_shot_cb, "toggled", self._on_one_shot_cb_toggled)
self._connect(self.window, "close-request", self._on_close_request)
def _on_destroy(self, *args: Any) -> None:
self.show()
def _cleanup(self) -> None:
pass
def _on_close_request(self, *args: Any) -> None:
for num in list(self._plugin.config.keys()):
del self._plugin.config[num]
for num in self._config:
self._plugin.config[str(num)] = self._config[num]
def _initialize(self) -> None:
# Fill window
widgets = [
"conditions_treeview",
"config_box",
"event_combobox",
"recipient_type_combobox",
"recipient_list_entry",
"delete_button",
"online_cb",
"away_cb",
"xa_cb",
"dnd_cb",
"use_sound_cb",
"disable_sound_cb",
"use_popup_cb",
"disable_popup_cb",
"tab_opened_cb",
"not_tab_opened_cb",
"has_focus_cb",
"not_has_focus_cb",
"filechooser",
"sound_file_box",
"up_button",
"down_button",
"run_command_cb",
"command_entry",
"one_shot_cb",
]
for widget in widgets:
self._ui.__dict__[widget] = self._ui.get_object(widget)
self._config = {}
for num in self._plugin.config.keys():
for num in self._plugin.config:
self._config[int(num)] = self._plugin.config[num]
if not self._ui.conditions_treeview.get_column(0):
# Window never opened
model = Gtk.ListStore(int, str)
model.set_sort_column_id(0, Gtk.SortType.ASCENDING)
self._ui.conditions_treeview.set_model(model)
# '#' Means number
col = Gtk.TreeViewColumn(_("#"))
self._ui.conditions_treeview.append_column(col)
renderer = Gtk.CellRendererText()
col.pack_start(renderer, expand=False)
col.add_attribute(renderer, "text", 0)
col = Gtk.TreeViewColumn(_("Condition"))
self._ui.conditions_treeview.append_column(col)
renderer = Gtk.CellRendererText()
col.pack_start(renderer, expand=True)
col.add_attribute(renderer, "text", 1)
else:
model = self._ui.conditions_treeview.get_model()
model.clear()
model = cast(Gtk.ListStore, self._ui.conditions_treeview.get_model())
# Fill conditions_treeview
num = 0
@@ -152,16 +193,16 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._ui.down_button.set_sensitive(False)
self._ui.up_button.set_sensitive(False)
filter_ = Gtk.FileFilter()
filter_.set_name(_("All Files"))
filter_.add_pattern("*")
self._ui.filechooser.add_filter(filter_)
# filter_ = Gtk.FileFilter()
# filter_.set_name(_("All Files"))
# filter_.add_pattern("*")
# self._ui.filechooser.add_filter(filter_)
filter_ = Gtk.FileFilter()
filter_.set_name(_("Wav Sounds"))
filter_.add_pattern("*.wav")
self._ui.filechooser.add_filter(filter_)
self._ui.filechooser.set_filter(filter_)
# filter_ = Gtk.FileFilter()
# filter_.set_name(_("Wav Sounds"))
# filter_.add_pattern("*.wav")
# self._ui.filechooser.add_filter(filter_)
# self._ui.filechooser.set_filter(filter_)
def _initiate_rule_state(self) -> None:
"""
@@ -202,11 +243,11 @@ class ConfigDialog(Gtk.ApplicationWindow):
else:
self._ui.special_status_rb.set_active(True)
values = value.split()
for val in ("online", "away", "xa", "dnd"):
if val in values:
self._ui.__dict__[val + "_cb"].set_active(True)
for st in ("online", "away", "xa", "dnd"):
if st in values:
getattr(self._ui, f"{st}_cb").set_active(True)
else:
self._ui.__dict__[val + "_cb"].set_active(False)
getattr(self._ui, f"{st}_cb").set_active(False)
self._on_status_radiobutton_toggled(self._ui.all_status_rb)
@@ -230,24 +271,25 @@ class ConfigDialog(Gtk.ApplicationWindow):
elif value == "yes":
self._ui.not_has_focus_cb.set_active(False)
# TODO
# sound_file
value = self._config[self._active_num]["sound_file"]
if value is None:
self._ui.filechooser.unselect_all()
else:
self._ui.filechooser.set_filename(value)
# value = self._config[self._active_num]["sound_file"]
# if value is None:
# self._ui.filechooser.unselect_all()
# else:
# self._ui.filechooser.set_filename(value)
# sound, popup, auto_open, systray, roster
for option in ("sound", "popup"):
value = self._config[self._active_num][option]
if value == "yes":
self._ui.__dict__["use_" + option + "_cb"].set_active(True)
getattr(self._ui, f"use_{option}_cb").set_active(True)
else:
self._ui.__dict__["use_" + option + "_cb"].set_active(False)
getattr(self._ui, f"use_{option}_cb").set_active(False)
if value == "no":
self._ui.__dict__["disable_" + option + "_cb"].set_active(True)
getattr(self._ui, f"disable_{option}_cb").set_active(True)
else:
self._ui.__dict__["disable_" + option + "_cb"].set_active(False)
getattr(self._ui, f"disable_{option}_cb").set_active(False)
# run_command
value = self._config[self._active_num]["run_command"]
@@ -258,10 +300,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._ui.command_entry.set_text(value)
# one shot
if "one_shot" in self._config[self._active_num]:
value = self._config[self._active_num]["one_shot"]
else:
value = False
value = self._config[self._active_num].get("one_shot", False)
self._ui.one_shot_cb.set_active(value)
def _set_treeview_string(self) -> None:
@@ -269,15 +308,21 @@ class ConfigDialog(Gtk.ApplicationWindow):
(model, iter_) = selection.get_selected()
if not iter_:
return
ind = self._ui.event_combobox.get_active()
event = ""
ind = self._ui.event_combobox.get_active()
if ind > -1:
event = self._ui.event_combobox.get_model()[ind][0]
event_model = self._ui.event_combobox.get_model()
assert event_model is not None
event = event_model[ind][0]
ind = self._ui.recipient_type_combobox.get_active()
recipient_type = ""
if ind > -1:
recipient_type_model = self._ui.recipient_type_combobox.get_model()
assert recipient_type_model is not None
recipient_type = recipient_type_model[ind][0]
recipient = ""
if recipient_type != "everybody":
recipient = self._ui.recipient_list_entry.get_text()
@@ -286,8 +331,9 @@ class ConfigDialog(Gtk.ApplicationWindow):
else:
status = _("and I am ")
for st in ("online", "away", "xa", "dnd"):
if self._ui.__dict__[st + "_cb"].get_active():
if getattr(self._ui, f"{st}_cb").get_active():
status += get_uf_show(st) + " "
model[iter_][1] = _(
"%(event)s (%(recipient_type)s) %(recipient)s %(status)s"
) % {
@@ -298,7 +344,6 @@ class ConfigDialog(Gtk.ApplicationWindow):
}
def _on_conditions_treeview_cursor_changed(self, widget: Gtk.TreeView) -> None:
(model, iter_) = widget.get_selection().get_selected()
if not iter_:
self._active_num = -1
@@ -320,8 +365,9 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._ui.delete_button.set_sensitive(True)
def _on_new_button_clicked(self, _button: Gtk.Button) -> None:
model = self._ui.conditions_treeview.get_model()
num = self._ui.conditions_treeview.get_model().iter_n_children(None)
model = cast(Gtk.ListStore, self._ui.conditions_treeview.get_model())
assert model is not None
num = model.iter_n_children(None)
self._config[num] = {
"event": "message_received",
"recipient_type": "all",
@@ -336,6 +382,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
"command": "",
"one_shot": False,
}
iter_ = model.append((num, ""))
path = model.get_path(iter_)
self._ui.conditions_treeview.set_cursor(path)
@@ -346,8 +393,10 @@ class ConfigDialog(Gtk.ApplicationWindow):
def _on_delete_button_clicked(self, button: Gtk.Button) -> None:
selection = self._ui.conditions_treeview.get_selection()
(model, iter_) = selection.get_selected()
assert isinstance(model, Gtk.ListStore)
if not iter_:
return
# up all others
iter2 = model.iter_next(iter_)
num = self._active_num
@@ -356,6 +405,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
model[iter2][0] = num - 1
self._config[num - 1] = self._config[num].copy()
iter2 = model.iter_next(iter2)
model.remove(iter_)
del self._config[num]
self._active_num = -1
@@ -367,8 +417,10 @@ class ConfigDialog(Gtk.ApplicationWindow):
def _on_up_button_clicked(self, _button: Gtk.Button) -> None:
selection = self._ui.conditions_treeview.get_selection()
(model, iter_) = selection.get_selected()
assert isinstance(model, Gtk.ListStore)
if not iter_:
return
conf = self._config[self._active_num].copy()
self._config[self._active_num] = self._config[self._active_num - 1]
self._config[self._active_num - 1] = conf
@@ -383,14 +435,17 @@ class ConfigDialog(Gtk.ApplicationWindow):
def _on_down_button_clicked(self, _button: Gtk.Button) -> None:
selection = self._ui.conditions_treeview.get_selection()
(model, iter_) = selection.get_selected()
assert isinstance(model, Gtk.ListStore)
if not iter_:
return
conf = self._config[self._active_num].copy()
self._config[self._active_num] = self._config[self._active_num + 1]
self._config[self._active_num + 1] = conf
model[iter_][0] = self._active_num + 1
iter_ = model.iter_next(iter_)
assert iter_ is not None
model[iter_][0] = self._active_num
self._on_conditions_treeview_cursor_changed(self._ui.conditions_treeview)
@@ -432,14 +487,14 @@ class ConfigDialog(Gtk.ApplicationWindow):
return
status = ""
for st in ("online", "away", "xa", "dnd"):
if self._ui.__dict__[st + "_cb"].get_active():
if getattr(self._ui, f"{st}_cb").get_active():
status += st + " "
if status:
status = status[:-1]
self._config[self._active_num]["status"] = status
self._set_treeview_string()
def _on_status_radiobutton_toggled(self, _widget: Gtk.RadioButton) -> None:
def _on_status_radiobutton_toggled(self, _widget: Gtk.CheckButton) -> None:
if self._active_num < 0:
return
if self._ui.all_status_rb.get_active():
@@ -447,13 +502,13 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._config[self._active_num]["status"] = "all"
# 'All status' clicked
for st in ("online", "away", "xa", "dnd"):
self._ui.__dict__[st + "_cb"].set_sensitive(False)
getattr(self._ui, f"{st}_cb").set_sensitive(False)
else:
self._ui.status_expander.set_expanded(True)
self._set_status_config()
# 'special status' clicked
for st in ("online", "away", "xa", "dnd"):
self._ui.__dict__[st + "_cb"].set_sensitive(True)
getattr(self._ui, f"{st}_cb").set_sensitive(True)
self._set_treeview_string()

File diff suppressed because it is too large Load Diff

View File

@@ -18,12 +18,11 @@
from __future__ import annotations
from typing import Any
from typing import Callable
from typing import cast
from typing import Union
import logging
import subprocess
from collections.abc import Callable
from functools import partial
from nbxmpp.protocol import JID
@@ -36,6 +35,7 @@ from gajim.common.events import MessageReceived
from gajim.common.events import Notification
from gajim.common.events import PresenceReceived
from gajim.common.helpers import play_sound_file
from gajim.common.modules.contacts import BareContact
from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
@@ -45,7 +45,7 @@ from triggers.util import RuleResult
log = logging.getLogger("gajim.p.triggers")
ProcessableEventsT = Union[MessageReceived, Notification, PresenceReceived]
ProcessableEventsT = MessageReceived | Notification | PresenceReceived
RuleT = dict[str, Any]
@@ -106,14 +106,14 @@ class Triggers(GajimPlugin):
result = RuleResult()
rules_num = [int(item) for item in self.config.keys()]
rules_num = [int(item) for item in self.config]
rules_num.sort()
to_remove: list[int] = []
for num in rules_num:
rule = cast(RuleT, self.config[str(num)])
if check_func(event, rule):
apply_func(result, rule)
if "one_shot" in rule and rule["one_shot"]:
if rule.get("one_shot"):
to_remove.append(num)
decal = 0
@@ -136,31 +136,26 @@ class Triggers(GajimPlugin):
def _check_rule_apply_msg_received(
self, event: MessageReceived, rule: RuleT
) -> bool:
return self._check_rule_all("message_received", event, rule)
@log_result
def _check_rule_apply_connected(self, event: PresenceReceived, rule: RuleT) -> bool:
return self._check_rule_all("contact_connected", event, rule)
@log_result
def _check_rule_apply_disconnected(
self, event: PresenceReceived, rule: RuleT
) -> bool:
return self._check_rule_all("contact_disconnected", event, rule)
@log_result
def _check_rule_apply_status_changed(
self, event: PresenceReceived, rule: RuleT
) -> bool:
return self._check_rule_all("contact_status_change", event, rule)
@log_result
def _check_rule_apply_notification(self, event: Notification, rule: RuleT) -> bool:
# Check notification type
notif_type = ""
if event.type == "incoming-message":
@@ -181,7 +176,6 @@ class Triggers(GajimPlugin):
def _check_rule_all(
self, notif_type: str, event: ProcessableEventsT, rule: RuleT
) -> bool:
# Check notification type
if rule["event"] != notif_type:
return False
@@ -207,7 +201,7 @@ class Triggers(GajimPlugin):
@log_result
def _check_rule_recipients(self, event: ProcessableEventsT, rule: RuleT) -> bool:
assert event.jid is not None
rule_recipients = [t.strip() for t in rule["recipients"].split(",")]
if rule["recipient_type"] == "groupchat":
if event.jid in rule_recipients:
@@ -218,7 +212,11 @@ class Triggers(GajimPlugin):
client = app.get_client(event.account)
contact = client.get_module("Contacts").get_contact(event.jid)
if contact.is_groupchat or not contact.is_in_roster:
if contact.is_groupchat:
return False
assert isinstance(contact, BareContact)
if not contact.is_in_roster:
return False
group_found = False
@@ -233,7 +231,6 @@ class Triggers(GajimPlugin):
@log_result
def _check_rule_status(self, event: ProcessableEventsT, rule: RuleT) -> bool:
rule_statuses = rule["status"].split()
client = app.get_client(event.account)
if rule["status"] != "all" and client.status not in rule_statuses:
@@ -243,7 +240,6 @@ class Triggers(GajimPlugin):
@log_result
def _check_rule_tab_opened(self, event: ProcessableEventsT, rule: RuleT) -> bool:
if rule["tab_opened"] == "both":
return True
tab_opened = False
@@ -259,7 +255,6 @@ class Triggers(GajimPlugin):
@log_result
def _check_rule_has_focus(self, event: ProcessableEventsT, rule: RuleT) -> bool:
if rule["has_focus"] == "both":
return True
if rule["tab_opened"] == "no":
@@ -311,7 +306,7 @@ class Triggers(GajimPlugin):
if result.command is not None:
try:
subprocess.Popen(f"{result.command} &", shell=True).wait()
subprocess.Popen(f"{result.command} &", shell=True).wait() # noqa: S602
except Exception:
pass

View File

@@ -15,10 +15,10 @@
from __future__ import annotations
from typing import Any
from typing import Callable
from typing import TYPE_CHECKING
import logging
from collections.abc import Callable
from dataclasses import dataclass
if TYPE_CHECKING:
@@ -31,7 +31,7 @@ log = logging.getLogger("gajim.p.triggers")
def log_result(func: Callable[..., Any]) -> Callable[..., bool]:
def wrapper(self: Any, event: ProcessableEventsT, rule: RuleT):
res = func(self, event, rule)
log.info(f"{event.name} -> {func.__name__} -> {res}")
log.info("%s -> %s -> %s", event.name, func.__name__, res)
return res
return wrapper