1# Copyright (C) 2011 Chris Dekter 2# Copyright (C) 2018 Thomas Hess 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17import logging 18from typing import Optional, Callable, TYPE_CHECKING 19 20from PyQt5.QtGui import QIcon 21from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu 22 23from autokey.qtui import popupmenu 24import autokey.qtui.common as ui_common 25from autokey import configmanager as cm 26 27if TYPE_CHECKING: 28 from autokey.qtapp import Application 29 30TOOLTIP_RUNNING = "AutoKey - running" 31TOOLTIP_PAUSED = "AutoKey - paused" 32 33logger = ui_common.logger.getChild("System-tray-notifier") # type: logging.Logger 34 35 36class Notifier(QSystemTrayIcon): 37 38 def __init__(self, app): 39 logger.debug("Creating system tray icon notifier.") 40 icon = self._load_default_icon() 41 super(Notifier, self).__init__(icon, app) 42 # Actions 43 self.action_view_script_error = None # type: QAction 44 self.action_hide_icon = None # type: QAction 45 self.action_show_config_window = None # type: QAction 46 self.action_quit = None # type: QAction 47 self.action_enable_monitoring = None # type: QAction 48 49 self.app = app # type: Application 50 self.config_manager = self.app.configManager 51 self.activated.connect(self.on_activate) 52 53 self._create_static_actions() 54 self.create_assign_context_menu() 55 self.update_tool_tip(cm.ConfigManager.SETTINGS[cm.SERVICE_RUNNING]) 56 self.app.monitoring_disabled.connect(self.update_tool_tip) 57 if cm.ConfigManager.SETTINGS[cm.SHOW_TRAY_ICON]: 58 logger.debug("About to show the tray icon.") 59 self.show() 60 logger.info("System tray icon notifier created.") 61 62 def create_assign_context_menu(self): 63 """ 64 Create a context menu, then set the created QMenu as the context menu. 65 This builds the menu with all required actions and signal-slot connections. 66 """ 67 menu = QMenu("AutoKey") 68 self._build_menu(menu) 69 self.setContextMenu(menu) 70 71 def update_tool_tip(self, service_running: bool): 72 """Slot function that updates the tooltip when the user activates or deactivates the expansion service.""" 73 if service_running: 74 self.setToolTip(TOOLTIP_RUNNING) 75 else: 76 self.setToolTip(TOOLTIP_PAUSED) 77 78 @staticmethod 79 def _load_default_icon() -> QIcon: 80 return QIcon.fromTheme( 81 cm.ConfigManager.SETTINGS[cm.NOTIFICATION_ICON], 82 ui_common.load_icon(ui_common.AutoKeyIcon.SYSTEM_TRAY) 83 ) 84 85 @staticmethod 86 def _load_error_state_icon() -> QIcon: 87 return QIcon.fromTheme( 88 "autokey-status-error", 89 ui_common.load_icon(ui_common.AutoKeyIcon.SYSTEM_TRAY_ERROR) 90 ) 91 92 def _create_action( 93 self, 94 icon_name: Optional[str], 95 title: str, 96 slot_function: Callable[[None], None], 97 tool_tip: Optional[str]=None)-> QAction: 98 """ 99 QAction factory. All items created belong to the calling instance, i.e. created QAction parent is self. 100 """ 101 action = QAction(title, self) 102 if icon_name: 103 action.setIcon(QIcon.fromTheme(icon_name)) 104 action.triggered.connect(slot_function) 105 if tool_tip: 106 action.setToolTip(tool_tip) 107 return action 108 109 def _create_static_actions(self): 110 """ 111 Create all static menu actions. The created actions will be placed in the tray icon context menu. 112 """ 113 logger.info("Creating static context menu actions.") 114 self.action_view_script_error = self._create_action( 115 None, "&View script error", self.reset_tray_icon, 116 "View the last script error." 117 ) 118 self.action_view_script_error.triggered.connect(self.app.show_script_error) 119 # The action should disable itself 120 self.action_view_script_error.setDisabled(True) 121 self.action_view_script_error.triggered.connect(self.action_view_script_error.setEnabled) 122 self.action_hide_icon = self._create_action( 123 "edit-clear", "Temporarily &Hide Icon", self.hide, 124 "Temporarily hide the system tray icon.\nUse the settings to hide it permanently." 125 ) 126 self.action_show_config_window = self._create_action( 127 "configure", "&Show Main Window", self.app.show_configure, 128 "Show the main AutoKey window. This does the same as left clicking the tray icon." 129 ) 130 self.action_quit = self._create_action("application-exit", "Exit AutoKey", self.app.shutdown) 131 # TODO: maybe import this from configwindow.py ? The exact same Action is defined in the main window. 132 self.action_enable_monitoring = self._create_action( 133 None, "&Enable Monitoring", self.app.toggle_service, 134 "Pause the phrase expansion and script execution, both by abbreviations and hotkeys.\n" 135 "The global hotkeys to show the main window and to toggle this setting, as defined in the AutoKey " 136 "settings, are not affected and will work regardless." 137 ) 138 self.action_enable_monitoring.setCheckable(True) 139 self.action_enable_monitoring.setChecked(self.app.service.is_running()) 140 self.action_enable_monitoring.setDisabled(self.app.serviceDisabled) 141 # Sync action state with internal service state 142 self.app.monitoring_disabled.connect(self.action_enable_monitoring.setChecked) 143 144 def _fill_context_menu_with_model_item_actions(self, context_menu: QMenu): 145 """ 146 Find all model items that should be available in the context menu and create QActions for each, by 147 using the available logic in popupmenu.PopupMenu. 148 """ 149 # Get phrase folders to add to main menu 150 logger.info("Rebuilding model item actions, adding all items marked for access through the tray icon.") 151 folders = [folder for folder in self.config_manager.allFolders if folder.show_in_tray_menu] 152 items = [item for item in self.config_manager.allItems if item.show_in_tray_menu] 153 # Only extract the QActions, but discard the PopupMenu instance. 154 # This is done, because the PopupMenu class is not directly usable as a context menu here. 155 menu = popupmenu.PopupMenu(self.app.service, folders, items, False, "AutoKey") 156 new_item_actions = menu.actions() 157 context_menu.addActions(new_item_actions) 158 for action in new_item_actions: # type: QAction 159 # QMenu does not take the ownership when adding QActions, so manually re-parent all actions. 160 # This causes the QActions to be destroyed when the context menu is cleared or re-created. 161 action.setParent(context_menu) 162 163 if not context_menu.isEmpty(): 164 # Avoid a stray separator line, if no items are marked for display in the context menu. 165 context_menu.addSeparator() 166 167 def _build_menu(self, context_menu: QMenu): 168 """Build the context menu.""" 169 logger.debug("Show tray icon enabled in settings: {}".format(cm.ConfigManager.SETTINGS[cm.SHOW_TRAY_ICON])) 170 # Items selected for display are shown on top 171 self._fill_context_menu_with_model_item_actions(context_menu) 172 # The static actions are added at the bottom 173 context_menu.addAction(self.action_view_script_error) 174 context_menu.addAction(self.action_enable_monitoring) 175 context_menu.addAction(self.action_hide_icon) 176 context_menu.addAction(self.action_show_config_window) 177 context_menu.addAction(self.action_quit) 178 179 def update_visible_status(self): 180 visible = cm.ConfigManager.SETTINGS[cm.SHOW_TRAY_ICON] 181 if visible: 182 self.create_assign_context_menu() 183 self.setVisible(visible) 184 logger.info("Updated tray icon visibility. Is icon shown: {}".format(visible)) 185 186 def notify_error(self, message: str): 187 self.setIcon(self._load_error_state_icon()) 188 self.action_view_script_error.setEnabled(True) 189 self.showMessage("AutoKey Error", message) 190 191 def reset_tray_icon(self): 192 """ 193 Slot function that resets the icon to the default, as configured in the settings. 194 Used when the user switches the icon theme in the settings and when a script error condition is cleared. 195 """ 196 self.setIcon(self._load_default_icon()) 197 198 def on_activate(self, reason: QSystemTrayIcon.ActivationReason): 199 logger.debug("Triggered system tray icon with reason: {}".format(reason)) 200 if reason == QSystemTrayIcon.ActivationReason(QSystemTrayIcon.Trigger): 201 self.app.show_configure() 202