Philipp Hörist pushed to branch master at gajim / gajim
Commits: e33d0ba5 by lovetox at 2022-01-10T23:37:17+01:00 StatusIcon: Refactor
- Split into multiple backends - Add type annotations
- - - - - e104f5a4 by lovetox at 2022-01-10T23:38:36+01:00 Add type annoations
- - - - -
7 changed files:
- gajim/gtk/main.py - gajim/gtk/menus.py - gajim/gtk/notification_manager.py - gajim/gtk/status_icon.py - gajim/gtk/structs.py - gajim/gtk/util.py - pyrightconfig.json
Changes:
===================================== gajim/gtk/main.py ===================================== @@ -53,12 +53,14 @@ from .dialogs import ConfirmationCheckDialog from .types import ControlType from .builder import get_builder +from .util import get_key_theme from .util import resize_window from .util import restore_main_window_position from .util import save_main_window_position from .util import open_window from .util import set_urgency_hint from .const import UNLOAD_CHAT_TIME +from .structs import AddChatActionParams
log = logging.getLogger('gajim.gui.main')
@@ -236,8 +238,7 @@ def _add_actions2(self) -> None: 'close-tab' )
- key_theme = Gtk.Settings.get_default().get_property( - 'gtk-key-theme-name') + key_theme = get_key_theme()
for action in actions: if key_theme == 'Emacs' and action in disabled_for_emacs: @@ -370,7 +371,7 @@ def is_chat_active(self, account: str, jid: JID) -> bool:
def _add_workspace(self, _action: Gio.SimpleAction, - param: Optional[GLib.Variant]) -> None: + param: GLib.Variant) -> None:
workspace_id = param.get_string() if workspace_id is not None: @@ -416,7 +417,7 @@ def remove_workspace(self, workspace_id: str) -> None:
def _activate_workspace(self, _action: Gio.SimpleAction, - param: Optional[GLib.Variant]) -> None: + param: GLib.Variant) -> None:
workspace_id = param.get_string() if workspace_id is not None: @@ -456,10 +457,10 @@ def add_group_chat(self, account: str, jid: JID,
def _add_chat(self, _action: Gio.SimpleAction, - param: Optional[GLib.Variant]) -> None: + param: GLib.Variant) -> None:
- account, jid, type_, select = param.unpack() - self.add_chat(account, JID.from_string(jid), type_, select) + params = AddChatActionParams.from_variant(param) + self.add_chat(params.account, params.jid, params.type, params.select)
def add_chat(self, account: str, @@ -518,7 +519,7 @@ def select_chat_number(self, number: int) -> None:
@staticmethod def _add_to_roster(_action: Gio.SimpleAction, - param: Optional[GLib.Variant]) -> None: + param: GLib.Variant) -> None:
_workspace, account, jid = param.unpack() open_window('AddContact', account=account, jid=JID.from_string(jid))
===================================== gajim/gtk/menus.py ===================================== @@ -40,6 +40,7 @@ from gajim.common.const import URIType from gajim.common.const import URIAction from gajim.common.structs import URI +from gajim.gtk.structs import AddChatActionParams
from gajim.gui.structs import ForgetGroupchatActionParams from gajim.gui.structs import RemoveHistoryActionParams @@ -475,19 +476,16 @@ def get_roster_menu(account: str, jid: str, gateway: bool = False) -> Gio.Menu: return menu
-def get_subscription_menu(account: str, jid: str) -> Gio.Menu: +def get_subscription_menu(account: str, jid: JID) -> Gio.Menu: menu = Gio.Menu()
- action_name = 'win.add-chat' - add_chat = Gio.MenuItem.new(_('Start Chat'), action_name) - dict_ = { - 'account': GLib.Variant('s', account), - 'jid': GLib.Variant('s', str(jid)), - 'type_': GLib.Variant('s', 'contact'), - 'select': GLib.Variant.new_boolean(True), - } - variant_dict = GLib.Variant('a{sv}', dict_) - add_chat.set_action_and_target_value(action_name, variant_dict) + add_chat = Gio.MenuItem.new(_('Start Chat')) + params = AddChatActionParams(account=account, + jid=jid, + type='contact', + select=True) + + add_chat.set_action_and_target_value('win.add-chat', params.to_variant()) menu.append_item(add_chat)
menu_items = [
===================================== gajim/gtk/notification_manager.py ===================================== @@ -299,7 +299,7 @@ def __init__(self, more_button = Gtk.MenuButton() more_button.set_valign(Gtk.Align.CENTER) more_button.add(more_image) - subscription_menu = get_subscription_menu(self._account, str(self.jid)) + subscription_menu = get_subscription_menu(self._account, self.jid) more_button.set_menu_model(subscription_menu) self.grid.attach(more_button, 4, 1, 1, 2)
===================================== gajim/gtk/status_icon.py ===================================== @@ -19,7 +19,11 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see http://www.gnu.org/licenses/.
-import os +import typing +from typing import Any +from typing import Union + +import sys
from gi.repository import Gtk from gi.repository import GLib @@ -28,6 +32,9 @@ from gajim.common import configpaths from gajim.common import ged from gajim.common.i18n import _ +from gajim.common.events import AccountConnected +from gajim.common.events import AccountDisonnected +from gajim.common.events import ShowChanged from gajim.common.helpers import get_global_show from gajim.common.helpers import get_uf_show from gajim.common.ged import EventHelper @@ -38,125 +45,200 @@ from .util import restore_main_window_position from .util import open_window
-HAS_INDICATOR = False if app.is_installed('APPINDICATOR'): from gi.repository import AppIndicator3 as appindicator # pylint: disable=ungrouped-imports - HAS_INDICATOR = True + elif app.is_installed('AYATANA_APPINDICATOR'): from gi.repository import AyatanaAppIndicator3 as appindicator # pylint: disable=ungrouped-imports - HAS_INDICATOR = True + +if typing.TYPE_CHECKING: + from gi.repository import AyatanaAppIndicator3 as appindicator
-class StatusIcon(EventHelper): - """ - Class for the notification area icon - """ +class StatusIcon: def __init__(self): - EventHelper.__init__(self) + self._backend = None
- self._ui = get_builder('systray_context_menu.ui') - self._ui.connect_signals(self) + def connect_unread_changed(self, chat_list_stack: Gtk.Stack) -> None: + # We trigger the signal connection from the main window because it + # is not yet available when initializing StatusIcon + chat_list_stack.connect( + 'unread-count-changed', self._on_unread_count_changed) + + def _on_unread_count_changed(self, + _chat_list_stack: Gtk.Stack, + _workspace_id: str, + _count: int) -> None: + if app.settings.get('trayicon_notification_on_events'): + count = app.window.get_total_unread_count() + self._backend._update_icon(count=count) # type: ignore + + def show_icon(self) -> None: + if self._backend is not None: + self._backend.show_icon() + return + + indicator_installed = (app.is_installed('APPINDICATOR') or + app.is_installed('AYATANA_APPINDICATOR')) + use_indicator = app.settings.get('use_libappindicator') + if indicator_installed and use_indicator: + self._backend = AppIndicator() + else: + self._backend = GtkStatusIcon() + + def hide_icon(self) -> None: + if self._backend is None: + return + self._backend.hide_icon()
- self._popup_menus = [] - self._hide_menuitem_added = False - self._status_icon = None - self._use_libappindicator = app.settings.get('use_libappindicator')
+class GtkMenuBackend(EventHelper): + def __init__(self): + EventHelper.__init__(self) + self._popup_menus: list[Gtk.Menu] = [] + self._enabled = True + + self._ui = get_builder('systray_context_menu.ui') self._ui.sounds_mute_menuitem.set_active( not app.settings.get('sounds_on')) self._add_status_menu()
+ self._ui.connect_signals(self) + self.register_events([ ('our-show', ged.GUI1, self._on_our_show), ('account-connected', ged.CORE, self._on_account_state), ('account-disconnected', ged.CORE, self._on_account_state), ])
- def connect_unread_changed(self, chat_list_stack): - # We trigger the signal connection from the main window because it - # is not yet available when initializing StatusIcon - chat_list_stack.connect( - 'unread-count-changed', self._on_unread_count_changed) + def show_icon(self) -> None: + raise NotImplementedError
- def _on_our_show(self, _event): - self.update_icon() + def hide_icon(self) -> None: + raise NotImplementedError + + def _update_icon(self, count: int = 0) -> None: + raise NotImplementedError + + def _add_status_menu(self) -> None: + sub_menu = Gtk.Menu() + + for show in ('online', 'away', 'xa', 'dnd'): + uf_show = get_uf_show(show, use_mnemonic=True) + item = Gtk.MenuItem.new_with_mnemonic(uf_show) + sub_menu.append(item) + item.connect('activate', self._on_show, show)
- def _on_account_state(self, _event): + sub_menu.append(Gtk.SeparatorMenuItem()) + + uf_show = get_uf_show('offline', use_mnemonic=True) + item = Gtk.MenuItem.new_with_mnemonic(uf_show) + item.connect('activate', self._on_show, 'offline') + sub_menu.append(item) + sub_menu.show_all() + + self._popup_menus.append(sub_menu) + self._ui.status_menu.set_submenu(sub_menu) + + def _on_our_show(self, _event: ShowChanged) -> None: + self._update_icon() + + def _on_account_state(self, + _event: Union[AccountConnected, AccountDisonnected] + ) -> None: account_connected = bool(app.get_number_of_connected_accounts() > 0) self._ui.start_chat_menuitem.set_sensitive(account_connected)
+ @staticmethod + def _on_new_chat(_widget: Gtk.MenuItem) -> None: + app.app.activate_action('start-chat', GLib.Variant('s', '')) + + @staticmethod + def _on_sounds_mute(widget: Gtk.CheckMenuItem) -> None: + app.settings.set('sounds_on', not widget.get_active()) + + def _on_toggle_window(self, _widget: Gtk.MenuItem) -> None: + # When using Gtk.StatusIcon, app.window will never return True for + # 'has-toplevel-focus' while clicking the menu item + GLib.idle_add(self._on_activate) + + def _on_activate(self, *args: Any) -> None: + if app.window.get_property('has-toplevel-focus'): + save_main_window_position() + app.window.hide() + return + + if not app.window.get_property('visible'): + # Window was minimized + restore_main_window_position() + + if not app.settings.get('main_window_skip_taskbar'): + app.window.set_property('skip-taskbar-hint', False) + app.window.present_with_time(Gtk.get_current_event_time()) + + @staticmethod + def _on_preferences(_widget: Gtk.MenuItem) -> None: + open_window('Preferences') + + @staticmethod + def _on_quit(_widget: Gtk.MenuItem) -> None: + app.window.quit() + + @staticmethod + def _on_show(_widget: Gtk.MenuItem, show: str) -> None: + app.interface.change_status(status=show) + + +class GtkStatusIcon(GtkMenuBackend): + + def __init__(self): + GtkMenuBackend.__init__(self) + self._hide_menuitem_added = False + + self._status_icon = Gtk.StatusIcon() + self._status_icon.connect('activate', self._on_activate) + self._status_icon.connect('popup-menu', self._on_popup_menu) + self._status_icon.connect('size-changed', self._on_size_changed) + + self._update_icon() + def show_icon(self): - if not self._status_icon: - if HAS_INDICATOR and self._use_libappindicator: - self._status_icon = appindicator.Indicator.new( - 'Gajim', - 'dcraven-online', - appindicator.IndicatorCategory.COMMUNICATIONS) - self._status_icon.set_icon_theme_path( - str(configpaths.get('ICONS'))) - self._status_icon.set_attention_icon_full( - 'dcraven-message-new', 'New Message') - self._status_icon.set_status( - appindicator.IndicatorStatus.ACTIVE) - self._status_icon.set_menu(self._ui.systray_context_menu) - self._status_icon.set_secondary_activate_target( - self._ui.toggle_window_menuitem) - else: - self._status_icon = Gtk.StatusIcon() - self._status_icon.connect( - 'activate', self._on_activate) - self._status_icon.connect( - 'popup-menu', self._on_popup_menu) - self._status_icon.connect( - 'size-changed', self.update_icon) - - self.update_icon() - - def hide_icon(self): - if HAS_INDICATOR and self._use_libappindicator: - self._status_icon.set_status(appindicator.IndicatorStatus.PASSIVE) - else: - self._status_icon.set_visible(False) + self._enabled = True + self._status_icon.set_visible(True) + self._update_icon()
- def _on_unread_count_changed(self, _chat_list_stack, _workspace_id, _count): - if app.settings.get('trayicon_notification_on_events'): - count = app.window.get_total_unread_count() - self.update_icon(count=count) + def hide_icon(self) -> None: + self._enabled = False + self._status_icon.set_visible(False)
- def update_icon(self, *args, count=0): - if not app.interface.systray_enabled: + def _update_icon(self, count: int = 0): + if not self._enabled: return + if app.settings.get('trayicon') == 'always': - if HAS_INDICATOR and self._use_libappindicator: - self._status_icon.set_status( - appindicator.IndicatorStatus.ACTIVE) - else: - self._status_icon.set_visible(True) + self._status_icon.set_visible(True) + if count > 0: - icon_name = 'dcraven-message-new' - if HAS_INDICATOR and self._use_libappindicator: - self._status_icon.set_icon_full(icon_name, _('Pending Event')) - else: - self._status_icon.set_visible(True) - self._status_icon.set_from_icon_name(icon_name) + self._status_icon.set_visible(True) + self._status_icon.set_from_icon_name('dcraven-message-new') return
if app.settings.get('trayicon') == 'on_event': - if HAS_INDICATOR and self._use_libappindicator: - self._status_icon.set_status( - appindicator.IndicatorStatus.PASSIVE) - else: - self._status_icon.set_visible(False) + self._status_icon.set_visible(False)
show = get_global_show() icon_name = get_icon_name(show) - if HAS_INDICATOR and self._use_libappindicator: - self._status_icon.set_icon_full(icon_name, show) - self._status_icon.set_status(appindicator.IndicatorStatus.ACTIVE) - else: - self._status_icon.set_from_icon_name(icon_name) + self._status_icon.set_from_icon_name(icon_name) + + def _on_size_changed(self, + _status_icon: Gtk.StatusIcon, + size: int) -> None: + self._update_icon()
- def _on_popup_menu(self, _status_icon, button, activate_time): + def _on_popup_menu(self, + _status_icon: Gtk.StatusIcon, + button: int, + activate_time: int) -> None: if button == 1: self._on_activate() elif button == 2: @@ -164,7 +246,7 @@ def _on_popup_menu(self, _status_icon, button, activate_time): elif button == 3: self._build_menu(activate_time)
- def _build_menu(self, event_time): + def _build_menu(self, event_time: int) -> None: for menu in self._popup_menus: menu.destroy()
@@ -173,11 +255,11 @@ def _build_menu(self, event_time): self._ui.sounds_mute_menuitem.set_active( not app.settings.get('sounds_on'))
- if os.name == 'nt': + if sys.platform == 'win32': # Workaround for popup menu on Windows if not self._hide_menuitem_added: self._ui.systray_context_menu.prepend( - Gtk.SeparatorMenuItem.new()) + Gtk.SeparatorMenuItem()) item = Gtk.MenuItem.new_with_label( _('Hide this menu')) self._ui.systray_context_menu.prepend(item) @@ -187,61 +269,50 @@ def _build_menu(self, event_time): self._ui.systray_context_menu.popup( None, None, None, None, 0, event_time)
- def _add_status_menu(self): - sub_menu = Gtk.Menu()
- for show in ('online', 'away', 'xa', 'dnd'): - uf_show = get_uf_show(show, use_mnemonic=True) - item = Gtk.MenuItem.new_with_mnemonic(uf_show) - sub_menu.append(item) - item.connect('activate', self._on_show, show) - - sub_menu.append(Gtk.SeparatorMenuItem()) - - uf_show = get_uf_show('offline', use_mnemonic=True) - item = Gtk.MenuItem.new_with_mnemonic(uf_show) - item.connect('activate', self._on_show, 'offline') - sub_menu.append(item) - sub_menu.show_all() +class AppIndicator(GtkMenuBackend):
- self._popup_menus.append(sub_menu) - self._ui.status_menu.set_submenu(sub_menu) - - @staticmethod - def _on_new_chat(_widget): - app.app.activate_action('start-chat', GLib.Variant('s', '')) - - @staticmethod - def _on_sounds_mute(widget): - app.settings.set('sounds_on', not widget.get_active()) - - def _on_toggle_window(self, *args): - # When using Gtk.StatusIcon, app.window will never return True for - # 'has-toplevel-focus' while clicking the menu item - GLib.idle_add(self._on_activate) + def __init__(self): + GtkMenuBackend.__init__(self) + + self._status_icon = appindicator.Indicator.new( + 'Gajim', + 'dcraven-online', + appindicator.IndicatorCategory.COMMUNICATIONS) + self._status_icon.set_icon_theme_path(str(configpaths.get('ICONS'))) + self._status_icon.set_attention_icon_full('dcraven-message-new', + 'New Message') + self._status_icon.set_status(appindicator.IndicatorStatus.ACTIVE) + self._status_icon.set_menu(self._ui.systray_context_menu) + self._status_icon.set_secondary_activate_target( + self._ui.toggle_window_menuitem) + + self._update_icon()
- @staticmethod - def _on_preferences(_widget): - open_window('Preferences') + def show_icon(self): + self._enabled = True + self._status_icon.set_status(appindicator.IndicatorStatus.ACTIVE) + self._update_icon()
- @staticmethod - def _on_quit(_widget): - app.window.quit() + def hide_icon(self) -> None: + self._enabled = False + self._status_icon.set_status(appindicator.IndicatorStatus.PASSIVE)
- def _on_activate(self, *args): - if app.window.get_property('has-toplevel-focus'): - save_main_window_position() - app.window.hide() + def _update_icon(self, count: int = 0) -> None: + if not self._enabled: return
- if not app.window.get_property('visible'): - # Window was minimized - restore_main_window_position() + if app.settings.get('trayicon') == 'always': + self._status_icon.set_status(appindicator.IndicatorStatus.ACTIVE)
- if not app.settings.get('main_window_skip_taskbar'): - app.window.set_property('skip-taskbar-hint', False) - app.window.present_with_time(Gtk.get_current_event_time()) + if count > 0: + icon_name = 'dcraven-message-new' + self._status_icon.set_icon_full(icon_name, _('Pending Event'))
- @staticmethod - def _on_show(_widget, show): - app.interface.change_status(status=show) + if app.settings.get('trayicon') == 'on_event': + self._status_icon.set_status(appindicator.IndicatorStatus.PASSIVE) + + show = get_global_show() + icon_name = get_icon_name(show) + self._status_icon.set_icon_full(icon_name, show) + self._status_icon.set_status(appindicator.IndicatorStatus.ACTIVE)
===================================== gajim/gtk/structs.py ===================================== @@ -41,3 +41,11 @@ class RemoveHistoryActionParams(VariantMixin): class ForgetGroupchatActionParams(VariantMixin): account: str jid: JID + + +@dataclass +class AddChatActionParams(VariantMixin): + account: str + jid: JID + type: str + select: bool
===================================== gajim/gtk/util.py ===================================== @@ -894,3 +894,10 @@ def get_style_attribute_with_name(name: str) -> Pango.Attribute: return Pango.attr_family_new('monospace')
raise ValueError('unknown attribute %s' % name) + + +def get_key_theme() -> Optional[str]: + settings = Gtk.Settings.get_default() + if settings is None: + return None + return settings.get_property('gtk-key-theme-name')
===================================== pyrightconfig.json ===================================== @@ -38,6 +38,7 @@ "gajim/gtk/conversation/scrolled.py", "gajim/gtk/notification.py", "gajim/gtk/side_bar_switcher.py", + "gajim/gtk/status_icon.py", "gajim/gtk/status_message_selector.py", "gajim/gtk/status_selector.py", ],
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/2329842328bec2b04a91961e8506a4c7...