class Window(QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) self.iconGroupBox = QGroupBox() self.iconLabel = QLabel() self.iconComboBox = QComboBox() self.showIconCheckBox = QCheckBox() self.messageGroupBox = QGroupBox() self.typeLabel = QLabel() self.durationLabel = QLabel() self.durationWarningLabel = QLabel() self.titleLabel = QLabel() self.bodyLabel = QLabel() self.typeComboBox = QComboBox() self.durationSpinBox = QSpinBox() self.titleEdit = QLineEdit() self.bodyEdit = QTextEdit() self.showMessageButton = QPushButton() self.minimizeAction = QAction() self.maximizeAction = QAction() self.restoreAction = QAction() self.quitAction = QAction() self.trayIcon = QSystemTrayIcon() self.trayIconMenu = QMenu() self.createIconGroupBox() self.createMessageGroupBox() self.iconLabel.setMinimumWidth(self.durationLabel.sizeHint().width()) self.createActions() self.createTrayIcon() self.showMessageButton.clicked.connect(self.showMessage) self.showIconCheckBox.toggled.connect(self.trayIcon.setVisible) self.iconComboBox.currentIndexChanged.connect(self.setIcon) self.trayIcon.messageClicked.connect(self.messageClicked) self.trayIcon.activated.connect(self.iconActivated) self.mainLayout = QVBoxLayout() self.mainLayout.addWidget(self.iconGroupBox) self.mainLayout.addWidget(self.messageGroupBox) self.setLayout(self.mainLayout) self.iconComboBox.setCurrentIndex(1) self.trayIcon.show() self.setWindowTitle("Systray") self.resize(400, 300) def setVisible(self, visible): self.minimizeAction.setEnabled(visible) self.maximizeAction.setEnabled(not self.isMaximized()) self.restoreAction.setEnabled(self.isMaximized() or not visible) super().setVisible(visible) def closeEvent(self, event): if not event.spontaneous() or not self.isVisible(): return if self.trayIcon.isVisible(): QMessageBox.information(self, "Systray", "The program will keep running in the system tray. " "To terminate the program, choose <b>Quit</b> in the context " "menu of the system tray entry.") self.hide() event.ignore() @Slot(int) def setIcon(self, index): icon = self.iconComboBox.itemIcon(index) self.trayIcon.setIcon(icon) self.setWindowIcon(icon) self.trayIcon.setToolTip(self.iconComboBox.itemText(index)) @Slot(str) def iconActivated(self, reason): if reason == QSystemTrayIcon.Trigger: pass if reason == QSystemTrayIcon.DoubleClick: self.iconComboBox.setCurrentIndex( (self.iconComboBox.currentIndex() + 1) % self.iconComboBox.count() ) if reason == QSystemTrayIcon.MiddleClick: self.showMessage() @Slot() def showMessage(self): self.showIconCheckBox.setChecked(True) selectedIcon = self.typeComboBox.itemData(self.typeComboBox.currentIndex()) msgIcon = QSystemTrayIcon.MessageIcon(selectedIcon) if selectedIcon == -1: # custom icon icon = QIcon(self.iconComboBox.itemIcon(self.iconComboBox.currentIndex())) self.trayIcon.showMessage( self.titleEdit.text(), self.bodyEdit.toPlainText(), icon, self.durationSpinBox.value() * 1000, ) else: self.trayIcon.showMessage( self.titleEdit.text(), self.bodyEdit.toPlainText(), msgIcon, self.durationSpinBox.value() * 1000, ) @Slot() def messageClicked(self): QMessageBox.information(None, "Systray", "Sorry, I already gave what help I could.\n" "Maybe you should try asking a human?") def createIconGroupBox(self): self.iconGroupBox = QGroupBox("Tray Icon") self.iconLabel = QLabel("Icon:") self.iconComboBox = QComboBox() self.iconComboBox.addItem(QIcon(":/images/bad.png"), "Bad") self.iconComboBox.addItem(QIcon(":/images/heart.png"), "Heart") self.iconComboBox.addItem(QIcon(":/images/trash.png"), "Trash") self.showIconCheckBox = QCheckBox("Show icon") self.showIconCheckBox.setChecked(True) iconLayout = QHBoxLayout() iconLayout.addWidget(self.iconLabel) iconLayout.addWidget(self.iconComboBox) iconLayout.addStretch() iconLayout.addWidget(self.showIconCheckBox) self.iconGroupBox.setLayout(iconLayout) def createMessageGroupBox(self): self.messageGroupBox = QGroupBox("Balloon Message") self.typeLabel = QLabel("Type:") self.typeComboBox = QComboBox() self.typeComboBox.addItem("None", QSystemTrayIcon.NoIcon) self.typeComboBox.addItem( self.style().standardIcon(QStyle.SP_MessageBoxInformation), "Information", QSystemTrayIcon.Information, ) self.typeComboBox.addItem( self.style().standardIcon(QStyle.SP_MessageBoxWarning), "Warning", QSystemTrayIcon.Warning, ) self.typeComboBox.addItem( self.style().standardIcon(QStyle.SP_MessageBoxCritical), "Critical", QSystemTrayIcon.Critical, ) self.typeComboBox.addItem(QIcon(), "Custom icon", -1) self.typeComboBox.setCurrentIndex(1) self.durationLabel = QLabel("Duration:") self.durationSpinBox = QSpinBox() self.durationSpinBox.setRange(5, 60) self.durationSpinBox.setSuffix(" s") self.durationSpinBox.setValue(15) self.durationWarningLabel = QLabel("(some systems might ignore this hint)") self.durationWarningLabel.setIndent(10) self.titleLabel = QLabel("Title:") self.titleEdit = QLineEdit("Cannot connect to network") self.bodyLabel = QLabel("Body:") self.bodyEdit = QTextEdit() self.bodyEdit.setPlainText("Don't believe me. Honestly, I don't have a clue." "\nClick this balloon for details.") self.showMessageButton = QPushButton("Show Message") self.showMessageButton.setDefault(True) messageLayout = QGridLayout() messageLayout.addWidget(self.typeLabel, 0, 0) messageLayout.addWidget(self.typeComboBox, 0, 1, 1, 2) messageLayout.addWidget(self.durationLabel, 1, 0) messageLayout.addWidget(self.durationSpinBox, 1, 1) messageLayout.addWidget(self.durationWarningLabel, 1, 2, 1, 3) messageLayout.addWidget(self.titleLabel, 2, 0) messageLayout.addWidget(self.titleEdit, 2, 1, 1, 4) messageLayout.addWidget(self.bodyLabel, 3, 0) messageLayout.addWidget(self.bodyEdit, 3, 1, 2, 4) messageLayout.addWidget(self.showMessageButton, 5, 4) messageLayout.setColumnStretch(3, 1) messageLayout.setRowStretch(4, 1) self.messageGroupBox.setLayout(messageLayout) def createActions(self): self.minimizeAction = QAction("Minimize", self) self.minimizeAction.triggered.connect(self.hide) self.maximizeAction = QAction("Maximize", self) self.maximizeAction.triggered.connect(self.showMaximized) self.restoreAction = QAction("Restore", self) self.restoreAction.triggered.connect(self.showNormal) self.quitAction = QAction("Quit", self) self.quitAction.triggered.connect(qApp.quit) def createTrayIcon(self): self.trayIconMenu = QMenu(self) self.trayIconMenu.addAction(self.minimizeAction) self.trayIconMenu.addAction(self.maximizeAction) self.trayIconMenu.addAction(self.restoreAction) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(self.quitAction) self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setContextMenu(self.trayIconMenu)
class KnechtWindow(QMainWindow): system_tray_click_connected = False tree_focus_changed = Signal(QTreeView) is_about_to_quit = Signal() def __init__(self, app): """ The GUI MainWindow Class :param modules.gui.main_app.KnechtApp app: Main QApplication class """ super(KnechtWindow, self).__init__() self.app = app SetupWidget.from_ui_file(self, Resource.ui_paths['knecht_model_gui'], custom_widgets={'QColorButton': QColorButton}) self.rk_icon = QIcon(QPixmap(Resource.icon_paths['RK_Icon'])) # Set version window title self.setWindowTitle(f'{self.windowTitle()} - v{self.app.version}') # ---- Setup Main Menu ---- self.main_menu = MainWindowMenu(self) # ---- Tree Setup ---- tree_view_list = [self.variantTree, self.renderTree] tree_file_list = [_('Variantenbaum'), _('Renderliste')] tree_filter_widgets = [ self.lineEdit_Var_filter, self.lineEdit_Ren_filter ] # View Mgr will replace placeholder presetTree self.view_mgr = UiViewManager(self) self.view_mgr.view_updated.connect(self._connect_message_browser) self.view_mgr.setup_initial_tab_view(self.presetTree) # Set presetTree to current View Mgr view to avoid accessing deleted object self.presetTree = self.view_mgr.current_view() replaced_views = self.view_mgr.setup_default_views( tree_view_list, tree_file_list, tree_filter_widgets) self.variantTree, self.renderTree = replaced_views[0], replaced_views[ 1] for default_view in [self.variantTree, self.renderTree]: default_view.setFocusPolicy(Qt.ClickFocus) default_view.undo_stack.cleanChanged.disconnect() # ---- Setup renderTree ---- self.renderTree.is_render_view = True self.renderTree.accepted_item_types = [Kg.render_preset, Kg.preset] # ---- Internal Clipboard ---- self.clipboard = TreeClipboard() # ---- Store last tree with focus ---- self.last_focus_tree = self.presetTree # ---- System tray and taskbar ---- self.system_tray = QSystemTrayIcon(self.rk_icon, self) self.system_tray.hide() # ---- Windows taskbar progress indicator ---- self.taskbar_btn = QWinTaskbarButton(self) self.taskbar_progress = self.taskbar_btn.progress() # Delayed Taskbar Setup (Main Window needs to be created for correct window handle) QTimer.singleShot(1, self.init_taskbar) # ---- Generic Info Overlay ---- self.overlay = MainWindowOverlay(self.centralWidget()) # ---- Close Action ---- self.actionBeenden.triggered.connect(self.close) # ---- Setup Main UI Widgets ---- MainWindowWidgets(self) # Updater self.updater = KnechtUpdate(self) self.updater.update_available.connect( self.main_menu.info_menu.update_ready) QTimer.singleShot(20000, self.auto_update) # Initial Update check self.app.focusChanged.connect(self.app_focus_changed) # ---- Translate Ui elements loaded from ui file ---- translate_main_ui(self) self.setAcceptDrops(True) def _get_drop_event_files(self, mime_data): files = [] for url in mime_data.urls(): if not url.isLocalFile(): continue file = Path(url.toLocalFile()) if file.suffix.casefold( ) in self.main_menu.file_menu.supported_file_types: files.append(file) return files def dragEnterEvent(self, event: QDragEnterEvent): if event.mimeData().hasUrls(): if self._get_drop_event_files(event.mimeData()): event.acceptProposedAction() else: event.ignore() def dropEvent(self, event: QDropEvent): if event.mimeData().hasUrls(): files = self._get_drop_event_files(event.mimeData()) if files: for file in files: self.main_menu.file_menu.guess_open_file(file) event.accept() return True event.ignore() return False def app_focus_changed(self, old_widget: QWidget, new_widget: QWidget): if isinstance(new_widget, KnechtTreeView): self.set_last_focus_tree(new_widget) def init_taskbar(self): """ Initializes the MS Windows taskbar button""" # Needs to be called after window is created/shown self.taskbar_btn.setWindow(self.windowHandle()) self.taskbar_progress.setRange(0, 100) self.taskbar_progress.valueChanged.connect(self.taskbar_progress.show) def _connect_message_browser(self, view: KnechtTreeView): LOGGER.debug('Setting up message browser for: %s', view.objectName()) view.info_overlay.setup_message_browser(self.messageBrowser, self.tabWidget) def show_tray_notification(self, title: str, message: str, clicked_callback=None): if not self.system_tray.isVisible(): self.system_tray.show() # Disconnect existing callback if self.system_tray_click_connected: try: self.system_tray.messageClicked.disconnect() except RuntimeError: LOGGER.info( 'Could not disconnect system tray messageClicked handler.') finally: self.system_tray_click_connected = False if clicked_callback is not None: self.system_tray.messageClicked.connect(clicked_callback) self.system_tray_click_connected = True self.system_tray.showMessage(title, message, self.rk_icon) def set_last_focus_tree(self, set_tree_focus): if isinstance(set_tree_focus, KnechtTreeView): self.last_focus_tree = set_tree_focus self.tree_focus_changed.emit(self.last_focus_tree) def tree_with_focus(self) -> KnechtTreeView: """ Return the current or last known QTreeView in focus """ widget_in_focus = self.focusWidget() if isinstance(widget_in_focus, KnechtTreeView): self.last_focus_tree = widget_in_focus return self.last_focus_tree def check_for_updates(self): self.updater.run_update() def auto_update(self): if self.updater.first_run: if FROZEN: self.check_for_updates() def report_missing_reset(self): msg = _( 'Die Varianten enthalten keine Reset Schaltung! Die zu sendenden Varianten ' 'werden mit vorangegangen Schaltungen kollidieren.') self.overlay.display(msg, duration=5000, immediate=True) def msg(self, txt: str, duration: int = 4000) -> None: self.statusBar().showMessage(txt, duration) self.overlay.display(txt, duration, immediate=True) def play_finished_sound(self): self._play_sound(SoundRsc.finished) def play_confirmation_sound(self): self._play_sound(SoundRsc.positive) def play_hint_sound(self): self._play_sound(SoundRsc.hint) def play_warning_sound(self): self._play_sound(SoundRsc.warning) def _play_sound(self, resource_key): try: sfx = SoundRsc.get_sound(resource_key, self) sfx.play() except Exception as e: LOGGER.error(e)