triggers/0000755000175500017550000000000014505220544012437 5ustar debacledebacletriggers/triggers.py0000644000175500017550000002675414505220544014655 0ustar debacledebacle# Copyright (C) 2011-2017 Yann Leboulanger # Copyright (C) 2022 Philipp Hörist # # This file is part of Gajim. # # Gajim 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; version 3 only. # # Gajim 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 Gajim. If not, see . from __future__ import annotations from typing import Any from typing import Callable from typing import cast from typing import Union import logging from functools import partial import subprocess from nbxmpp.protocol import JID from gajim.common import app from gajim.common import ged from gajim.common.const import PROPAGATE_EVENT from gajim.common.const import STOP_EVENT from gajim.common.events import Notification from gajim.common.events import GcMessageReceived from gajim.common.events import MessageReceived from gajim.common.events import PresenceReceived from gajim.common.helpers import play_sound_file from gajim.plugins import GajimPlugin from gajim.plugins.plugins_i18n import _ from triggers.gtk.config import ConfigDialog from triggers.util import log_result from triggers.util import RuleResult log = logging.getLogger('gajim.p.triggers') MessageEventsT = Union[GcMessageReceived, MessageReceived] ProcessableEventsT = Union[MessageEventsT, Notification, PresenceReceived] RuleT = dict[str, Any] class Triggers(GajimPlugin): def init(self) -> None: self.description = _( 'Configure Gajim’s behaviour with triggers for each contact') self.config_dialog = partial(ConfigDialog, self) self.config_default_values = {} self.events_handlers = { 'notification': (ged.PREGUI, self._on_notification), 'message-received': (ged.PREGUI2, self._on_message_received), 'gc-message-received': (ged.PREGUI2, self._on_message_received), # 'presence-received': (ged.PREGUI, self._on_presence_received), } def _on_notification(self, event: Notification) -> bool: log.info('Process %s, %s', event.name, event.type) result = self._check_all(event, self._check_rule_apply_notification, self._apply_rule) log.info('Result: %s', result) return self._excecute_notification_rules(result, event) def _on_message_received(self, event: MessageEventsT) -> bool: log.info('Process %s', event.name) if not event.msgtxt: log.info('Discard event because it has no message text') return PROPAGATE_EVENT result = self._check_all(event, self._check_rule_apply_msg_received, self._apply_rule) log.info('Result: %s', result) return self._excecute_message_rules(result) def _on_presence_received(self, event: PresenceReceived) -> None: # TODO return if event.old_show < 2 and event.new_show > 1: check_func = self._check_rule_apply_connected elif event.old_show > 1 and event.new_show < 2: check_func = self._check_rule_apply_disconnected else: check_func = self._check_rule_apply_status_changed self._check_all(event, check_func, self._apply_rule) def _check_all(self, event: ProcessableEventsT, check_func: Callable[..., bool], apply_func: Callable[..., Any] ) -> RuleResult: result = RuleResult() rules_num = [int(item) for item in self.config.keys()] 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']: to_remove.append(num) decal = 0 num = 0 while str(num) in self.config: if num + decal in to_remove: num2 = num while str(num2 + 1) in self.config: copy = self.config[str(num2 + 1)].copy() # type: ignore self.config[str(num2)] = copy num2 += 1 del self.config[str(num2)] decal += 1 else: num += 1 return result @log_result def _check_rule_apply_msg_received(self, event: MessageEventsT, 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': notif_type = 'message_received' # if event.type == 'pres': # # TODO: # if (event.base_event.old_show < 2 and # event.base_event.new_show > 1): # notif_type = 'contact_connected' # elif (event.base_event.old_show > 1 and # event.base_event.new_show < 2): # notif_type = 'contact_disconnected' # else: # notif_type = 'contact_status_change' return self._check_rule_all(notif_type, event, rule) def _check_rule_all(self, notif_type: str, event: ProcessableEventsT, rule: RuleT ) -> bool: # Check notification type if rule['event'] != notif_type: return False # notification type is ok. Now check recipient if not self._check_rule_recipients(event, rule): return False # recipient is ok. Now check our status if not self._check_rule_status(event, rule): return False # our_status is ok. Now check opened chat window if not self._check_rule_tab_opened(event, rule): return False # tab_opened is ok. Now check opened chat window if not self._check_rule_has_focus(event, rule): return False # All is ok return True @log_result def _check_rule_recipients(self, event: ProcessableEventsT, rule: RuleT ) -> bool: rule_recipients = [t.strip() for t in rule['recipients'].split(',')] if rule['recipient_type'] == 'groupchat': if event.jid in rule_recipients: return True return False if (rule['recipient_type'] == 'contact' and event.jid not in rule_recipients): return False 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: return False group_found = False for group in contact.groups: if group in rule_recipients: group_found = True break if rule['recipient_type'] == 'group' and not group_found: return False return True @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: return False return True @log_result def _check_rule_tab_opened(self, event: ProcessableEventsT, rule: RuleT ) -> bool: if rule['tab_opened'] == 'both': return True tab_opened = False assert isinstance(event.jid, JID) if app.window.chat_exists(event.account, event.jid): tab_opened = True if tab_opened and rule['tab_opened'] == 'no': return False elif not tab_opened and rule['tab_opened'] == 'yes': return False return True @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': # Does not apply in this case return True assert isinstance(event.jid, JID) chat_active = app.window.is_chat_active(event.account, event.jid) if chat_active and rule['has_focus'] == 'no': return False elif not chat_active and rule['has_focus'] == 'yes': return False return True def _apply_rule(self, result: RuleResult, rule: RuleT) -> None: if rule['sound'] == 'no': result.sound = False result.sound_file = None elif rule['sound'] == 'yes': result.sound = False result.sound_file = rule['sound_file'] if rule['run_command']: result.command = rule['command'] if rule['popup'] == 'no': result.show_notification = False elif rule['popup'] == 'yes': result.show_notification = True def _excecute_notification_rules(self, result: RuleResult, event: Notification ) -> bool: if result.sound is False: event.sound = None if result.sound_file is not None: play_sound_file(result.sound_file) if result.show_notification is False: return STOP_EVENT return PROPAGATE_EVENT def _excecute_message_rules(self, result: RuleResult) -> bool: if result.command is not None: try: subprocess.Popen(f'{result.command} &', shell=True).wait() except Exception: pass return PROPAGATE_EVENT triggers/plugin-manifest.json0000644000175500017550000000073514505220544016441 0ustar debacledebacle{ "authors": [ "Yann Leboulanger " ], "description": "Configure Gajim's behaviour for each contact.", "homepage": "https://dev.gajim.org/gajim/gajim-plugins/wikis/TriggersPlugin", "config_dialog": true, "name": "Triggers", "platforms": [ "others", "linux", "darwin", "win32" ], "requirements": [ "gajim>=1.5.0" ], "short_name": "triggers", "version": "1.4.9" }triggers/util.py0000644000175500017550000000247214505220544013773 0ustar debacledebacle# This file is part of Gajim. # # Gajim 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; version 3 only. # # Gajim 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 Gajim. If not, see . from __future__ import annotations from typing import Any from typing import Callable from typing import TYPE_CHECKING import logging from dataclasses import dataclass if TYPE_CHECKING: from .triggers import ProcessableEventsT from .triggers import RuleT 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}') return res return wrapper @dataclass class RuleResult: show_notification: bool | None = None command: str | None = None sound: bool | None = None sound_file: str | None = None triggers/gtk/0000755000175500017550000000000014505220544013224 5ustar debacledebacletriggers/gtk/config.py0000644000175500017550000005341314505220544015051 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # This file is part of Gajim. # # Gajim 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; version 3 only. # # Gajim 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 Gajim. If not, see . from __future__ import annotations from typing import Any from typing import TYPE_CHECKING from pathlib import Path from gi.repository import Gtk from gi.repository import Gdk from gajim.common import app from gajim.common.helpers import get_uf_show from gajim.common.helpers import play_sound_file from gajim.plugins.plugins_i18n import _ from gajim.plugins.helpers import get_builder if TYPE_CHECKING: from ..triggers import Triggers EVENTS: dict[str, Any] = { 'message_received': [], } RECIPIENT_TYPES = [ 'contact', 'group', 'groupchat', 'all' ] class ConfigDialog(Gtk.ApplicationWindow): 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) ui_path = Path(__file__).parent self._ui = get_builder(str(ui_path.resolve() / 'config.ui')) self._plugin = plugin self.add(self._ui.box) self.show_all() self._active_num = -1 self._config: dict[int, Any] = {} self._initialize() self._ui.connect_signals(self) self.connect('destroy', self._on_destroy) def _on_destroy(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(): 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() # Fill conditions_treeview num = 0 while num in self._config: iter_ = model.append((num, '')) path = model.get_path(iter_) self._ui.conditions_treeview.set_cursor(path) self._active_num = num self._initiate_rule_state() self._set_treeview_string() num += 1 # No rule selected at init time self._ui.conditions_treeview.get_selection().unselect_all() self._active_num = -1 self._ui.config_box.set_sensitive(False) self._ui.delete_button.set_sensitive(False) 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(_('Wav Sounds')) filter_.add_pattern('*.wav') self._ui.filechooser.add_filter(filter_) self._ui.filechooser.set_filter(filter_) def _initiate_rule_state(self) -> None: """ Set values for all widgets """ if self._active_num < 0: return # event value = self._config[self._active_num]['event'] legacy_values = [ 'contact_connected', 'contact_disconnected', 'contact_status_change'] if value and value not in legacy_values: self._ui.event_combobox.set_active( list(EVENTS.keys()).index(value)) else: self._ui.event_combobox.set_active(-1) # recipient_type value = self._config[self._active_num]['recipient_type'] if value: self._ui.recipient_type_combobox.set_active( RECIPIENT_TYPES.index(value)) else: self._ui.recipient_type_combobox.set_active(-1) # recipient value = self._config[self._active_num]['recipients'] if not value: value = '' self._ui.recipient_list_entry.set_text(value) # status value = self._config[self._active_num]['status'] if value == 'all': self._ui.all_status_rb.set_active(True) 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) else: self._ui.__dict__[val + '_cb'].set_active(False) self._on_status_radiobutton_toggled(self._ui.all_status_rb) # tab_opened value = self._config[self._active_num]['tab_opened'] self._ui.tab_opened_cb.set_active(True) self._ui.not_tab_opened_cb.set_active(True) if value == 'no': self._ui.tab_opened_cb.set_active(False) elif value == 'yes': self._ui.not_tab_opened_cb.set_active(False) # has_focus if 'has_focus' not in self._config[self._active_num]: self._config[self._active_num]['has_focus'] = 'both' value = self._config[self._active_num]['has_focus'] self._ui.has_focus_cb.set_active(True) self._ui.not_has_focus_cb.set_active(True) if value == 'no': self._ui.has_focus_cb.set_active(False) elif value == 'yes': self._ui.not_has_focus_cb.set_active(False) # 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) # 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) else: self._ui.__dict__['use_' + option + '_cb'].set_active(False) if value == 'no': self._ui.__dict__['disable_' + option + '_cb'].set_active(True) else: self._ui.__dict__['disable_' + option + '_cb'].set_active(False) # run_command value = self._config[self._active_num]['run_command'] self._ui.run_command_cb.set_active(value) # command value = self._config[self._active_num]['command'] 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 self._ui.one_shot_cb.set_active(value) def _set_treeview_string(self) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() if not iter_: return ind = self._ui.event_combobox.get_active() event = '' if ind > -1: event = self._ui.event_combobox.get_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() recipient_type = recipient_type_model[ind][0] recipient = '' if recipient_type != 'everybody': recipient = self._ui.recipient_list_entry.get_text() if self._ui.all_status_rb.get_active(): status = '' else: status = _('and I am ') for st in ('online', 'away', 'xa', 'dnd'): if self._ui.__dict__[st + '_cb'].get_active(): status += get_uf_show(st) + ' ' model[iter_][1] = _('%(event)s (%(recipient_type)s) %(recipient)s ' '%(status)s') % { 'event': event, 'recipient_type': recipient_type, 'recipient': recipient, 'status': status} 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 return self._active_num = model[iter_][0] if self._active_num == 0: self._ui.up_button.set_sensitive(False) else: self._ui.up_button.set_sensitive(True) model = widget.get_model() assert model is not None _max = model.iter_n_children(None) if self._active_num == _max - 1: self._ui.down_button.set_sensitive(False) else: self._ui.down_button.set_sensitive(True) self._initiate_rule_state() self._ui.config_box.set_sensitive(True) 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) self._config[num] = { 'event': 'message_received', 'recipient_type': 'all', 'recipients': '', 'status': 'all', 'tab_opened': 'both', 'has_focus': 'both', 'sound': '', 'sound_file': '', 'popup': '', 'run_command': False, 'command': '', 'one_shot': False, } iter_ = model.append((num, '')) path = model.get_path(iter_) self._ui.conditions_treeview.set_cursor(path) self._active_num = num self._set_treeview_string() self._ui.config_box.set_sensitive(True) def _on_delete_button_clicked(self, button: Gtk.Button) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() if not iter_: return # up all others iter2 = model.iter_next(iter_) num = self._active_num while iter2: num = model[iter2][0] 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 button.set_sensitive(False) self._ui.up_button.set_sensitive(False) self._ui.down_button.set_sensitive(False) self._ui.config_box.set_sensitive(False) def _on_up_button_clicked(self, _button: Gtk.Button) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() 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 # get previous iter path = model.get_path(iter_) iter_ = model.get_iter((path[0] - 1,)) model[iter_][0] = self._active_num self._on_conditions_treeview_cursor_changed( self._ui.conditions_treeview) def _on_down_button_clicked(self, _button: Gtk.Button) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() 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_) model[iter_][0] = self._active_num self._on_conditions_treeview_cursor_changed( self._ui.conditions_treeview) def _on_event_combobox_changed(self, combo: Gtk.ComboBox) -> None: if self._active_num < 0: return active = combo.get_active() if active == -1: return event = list(EVENTS.keys())[active] self._config[self._active_num]['event'] = event for widget in EVENTS[event]: self._ui.__dict__[widget].set_sensitive(False) self._ui.__dict__[widget].set_state(False) self._set_treeview_string() def _on_recipient_type_combobox_changed(self, widget: Gtk.ComboBox ) -> None: if self._active_num < 0: return recipient_type = RECIPIENT_TYPES[widget.get_active()] self._config[self._active_num]['recipient_type'] = recipient_type if recipient_type == 'all': self._ui.recipient_list_entry.set_sensitive(False) else: self._ui.recipient_list_entry.set_sensitive(True) self._set_treeview_string() def _on_recipient_list_entry_changed(self, widget: Gtk.Entry) -> None: if self._active_num < 0: return recipients = widget.get_text() # TODO: do some check self._config[self._active_num]['recipients'] = recipients self._set_treeview_string() def _set_status_config(self) -> None: if self._active_num < 0: return status = '' for st in ('online', 'away', 'xa', 'dnd'): if self._ui.__dict__[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: if self._active_num < 0: return if self._ui.all_status_rb.get_active(): self._ui.status_expander.set_expanded(False) 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) 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) self._set_treeview_string() def _on_status_cb_toggled(self, _widget: Gtk.CheckButton) -> None: if self._active_num < 0: return self._set_status_config() # tab_opened OR (not xor) not_tab_opened must be active def _on_tab_opened_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): self._ui.has_focus_cb.set_sensitive(True) self._ui.not_has_focus_cb.set_sensitive(True) if self._ui.not_tab_opened_cb.get_active(): self._config[self._active_num]['tab_opened'] = 'both' else: self._config[self._active_num]['tab_opened'] = 'yes' else: self._ui.has_focus_cb.set_sensitive(False) self._ui.not_has_focus_cb.set_sensitive(False) self._ui.not_tab_opened_cb.set_active(True) self._config[self._active_num]['tab_opened'] = 'no' def _on_not_tab_opened_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): if self._ui.tab_opened_cb.get_active(): self._config[self._active_num]['tab_opened'] = 'both' else: self._config[self._active_num]['tab_opened'] = 'no' else: self._ui.tab_opened_cb.set_active(True) self._config[self._active_num]['tab_opened'] = 'yes' # has_focus OR (not xor) not_has_focus must be active def _on_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): if self._ui.not_has_focus_cb.get_active(): self._config[self._active_num]['has_focus'] = 'both' else: self._config[self._active_num]['has_focus'] = 'yes' else: self._ui.not_has_focus_cb.set_active(True) self._config[self._active_num]['has_focus'] = 'no' def _on_not_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): if self._ui.has_focus_cb.get_active(): self._config[self._active_num]['has_focus'] = 'both' else: self._config[self._active_num]['has_focus'] = 'no' else: self._ui.has_focus_cb.set_active(True) self._config[self._active_num]['has_focus'] = 'yes' def _on_use_it_toggled(self, widget: Gtk.CheckButton, opposite_widget: Gtk.CheckButton, option: str ) -> None: if widget.get_active(): if opposite_widget.get_active(): opposite_widget.set_active(False) self._config[self._active_num][option] = 'yes' elif opposite_widget.get_active(): self._config[self._active_num][option] = 'no' else: self._config[self._active_num][option] = '' def _on_disable_it_toggled(self, widget: Gtk.CheckButton, opposite_widget: Gtk.CheckButton, option: str ) -> None: if widget.get_active(): if opposite_widget.get_active(): opposite_widget.set_active(False) self._config[self._active_num][option] = 'no' elif opposite_widget.get_active(): self._config[self._active_num][option] = 'yes' else: self._config[self._active_num][option] = '' def _on_use_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_use_it_toggled(widget, self._ui.disable_sound_cb, 'sound') if widget.get_active(): self._ui.sound_file_box.set_sensitive(True) else: self._ui.sound_file_box.set_sensitive(False) def _on_sound_file_set(self, widget: Gtk.FileChooserButton) -> None: self._config[self._active_num]['sound_file'] = widget.get_filename() def _on_play_button_clicked(self, _button: Gtk.Button) -> None: play_sound_file(self._ui.filechooser.get_filename()) def _on_disable_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_disable_it_toggled(widget, self._ui.use_sound_cb, 'sound') def _on_use_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_use_it_toggled(widget, self._ui.disable_popup_cb, 'popup') def _on_disable_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_disable_it_toggled(widget, self._ui.use_popup_cb, 'popup') def _on_run_command_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._config[self._active_num]['run_command'] = widget.get_active() if widget.get_active(): self._ui.command_entry.set_sensitive(True) else: self._ui.command_entry.set_sensitive(False) def _on_command_entry_changed(self, widget: Gtk.Entry) -> None: self._config[self._active_num]['command'] = widget.get_text() def _on_one_shot_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._config[self._active_num]['one_shot'] = widget.get_active() self._ui.command_entry.set_sensitive(widget.get_active()) triggers/gtk/config.ui0000644000175500017550000015273514505220544015045 0ustar debacledebacle Contact Group Groupchat participant Everybody Receive a Message True False 18 vertical 6 True True True True True False True False vertical 6 True False vertical 100 True True in True True horizontal False True 0 True False False 1 True False False Up Up True go-up-symbolic False True True False False Down Down True go-down-symbolic False True True False New rule New rule True list-add-symbolic False True True False Delete rule Delete rule True list-remove-symbolic False True False True 1 False True 0 True False vertical 5 True False center vertical 6 True False 6 Conditions True False False 0 True False center 6 12 True False end Event True 0 0 200 True False liststore2 0 1 0 True False end Category True 0 1 True False liststore1 0 1 1 300 True False True comma separated list 1 2 True False end List 0 2 False True 1 True False center 6 6 True False start My status True False True 0 All statuses True True False start True True special_status_rb False True 1 Certain status True True False start True True all_status_rb False True 2 True True start 3 True True False vertical 6 Online True False True False start True True False True 0 Away True False True False start True True False True 1 Not Available True False True False start True True False True 2 Busy True False True False start True True False True 3 True False Status False True 3 False True 2 True False 6 12 True False end Chat Window 0 0 True False end Focus 0 1 Opened True True False start True True 1 0 Has focus True True False start True 1 1 Not opened True True False start True True 2 0 Does not have focus True True False start True 2 1 False True 5 False True 0 True False center vertical 6 True False 6 Actions True False False 0 True False vertical 6 True False start Notifications False True 0 Not_ify me with a popup True True False start True True False True 1 _Disable existing notification True True False start True True False True 2 False True 1 True False vertical 6 True False start Sounds True False True 0 True False start 12 Play sound True True False start True True False True 0 True False 12 True False False False Select Sound 15 False True 0 True True False True False media-playback-start-symbolic False True 1 False True 1 False False 1 _Disable existing sound for this event True True False start True True False True 2 False True 2 True False start Advanced False True 3 True False 6 Launch command True True False True True False True 0 200 True True Command... False True 1 False False 4 Delete this rule once applied True True False start True False True 5 False True 1 True True 1 True True 2 triggers/__init__.py0000644000175500017550000000005714505220544014552 0ustar debacledebaclefrom .triggers import Triggers # type: ignore triggers/triggers.png0000644000175500017550000000343714505220544015002 0ustar debacledebaclePNG  IHDR DgAMA7tEXtSoftwareAdobe ImageReadyqe<PLTECnr喼rrh5yyR1{{Ԣӑlddx1/d»@S-*zlLҫ|YV9DEu ׆8rۄXXqo:cg 2Ay{abTUƂҌ훛햖Յ- \]mcf9&gjy̡nmӗ΋kIiiuuv'::b>hLr;࡭3b ֊]ON㥭…`ܮR!*u\\llr`ADXǀ͡>:ЎY؍qS+FNc-@Z@⑔C{^`t`t'๿ټsmU_[^\MJHHBBqPPdgK)Ӯԧ0tRNSS%IDATxbO0 _ Ňn)á@lwZ^MX<~xboеM|c(v3nVP~+kN)CV|J{%|ba@L﮵$u m4*8$**os<" FN I_7w6}z]@!*SIs;ϮIdPA_#ˋb3.}ƝIލ ,k+*9Q|v;|? g0{#(p͐Qm F,=[QɨI% ?DX>N]=Z`h mv(`5W?M9/Ǐ_7BBrϛv@yO&+^Iф5K||Xm07eFDVb'H;)=mdJm/%,!ߝgjz:bMM^ڦJ?IE^.TWUU͸:I @;n `OߥIl)o_ֽ-XfGLu*hn=Z\, WAtqq!"Mں2^!K]]HHh !.=_,f֙-!QXX(!bL8oQ.7IENDB`