def _initSystemTray(self, withMenu: bool = False): LOG.info("Initializing the system tray: available: %s, messages: %s", QSystemTrayIcon.isSystemTrayAvailable(), QSystemTrayIcon.supportsMessages()) if not QSystemTrayIcon.isSystemTrayAvailable(): return systemTrayIcon = QSystemTrayIcon(QIcon('assets/logo.png'), self._app) systemTrayIcon.setToolTip("The Deep Visualization Toolbox") systemTrayIcon.activated.connect(self.onSystemTrayActivated) if withMenu: contextMenu = QMenu() contextMenu.aboutToShow.connect(self.onSystemTrayMenuAboutToShow) actionTest = QAction('Test', contextMenu) actionTest.triggered.connect(self.onSystemTrayMenuActionTriggered) systemTrayIcon.setContextMenu(contextMenu) if QSystemTrayIcon.supportsMessages(): systemTrayIcon.\ messageClicked.connect(self.onSystemTrayMessageClicked) systemTrayIcon.show()
def setSystemTrayGUI(self): """Manages how DailyWiki appears in the system tray. Note: the installer automatically edits Windows' registry to make sure that balloons mssages are supported.""" self.app.setQuitOnLastWindowClosed(False) self.icon = QIcon("resources/wikipedia_icon.png") self.tray = QSystemTrayIcon() self.tray.setIcon(self.icon) self.tray.setToolTip("DailyWiki") self.tray.setVisible(True) self.menu = QMenu() self.settings_action = QAction("Settings") self.exit_action = QAction("Exit") self.menu.addAction(self.settings_action) self.menu.addAction(self.exit_action) self.settings_action.triggered.connect(self.openSettings) self.exit_action.triggered.connect(self.exitFromBackground) self.tray.setContextMenu(self.menu) self.logger.debug("Balloon messages supported by the platform: " + str(QSystemTrayIcon.supportsMessages())) self.tray.showMessage("Your daily article is ready!", self.article_title) self.tray.messageClicked.connect(self.accessArticleFromTray) self.tray.activated.connect(self.accessArticleFromTray) self.tray.show()
def __init__(self, parent: QObject = None) -> None: super().__init__(parent) if not QSystemTrayIcon.isSystemTrayAvailable(): raise Error("No system tray available") if not QSystemTrayIcon.supportsMessages(): raise Error("System tray does not support messages") self._systray = QSystemTrayIcon(self) self._systray.setIcon(objects.qapp.windowIcon()) self._systray.messageClicked.connect(self._on_systray_clicked)
def __init__(self, widget: QtWidgets.QWidget, icon: QtGui.QIcon, **kwargs): super().__init__() self.menu_actions: Dict[str, MenuAction] = {} self.on_trigger: Optional[Callable] = kwargs.get('on_trigger') self.on_double_click: Optional[Callable] = kwargs.get( 'on_double_click') self.on_middle_click: Optional[Callable] = kwargs.get( 'on_middle_click') self.on_message_click: Optional[Callable] = kwargs.get( 'on_message_click') self.context_menu = QtWidgets.QMenu() self.tray_available = QSystemTrayIcon.isSystemTrayAvailable() self.messages_available = QSystemTrayIcon.supportsMessages() self.tray_icon = QSystemTrayIcon(widget) self.tray_icon.setIcon(icon) self.tray_icon.setContextMenu(self.context_menu) self._connect_signals()
class Magneto(MagnetoCore): """ Magneto Updates Notification Applet class. """ def __init__(self): self._app = QApplication([sys.argv[0]]) from dbus.mainloop.pyqt5 import DBusQtMainLoop super(Magneto, self).__init__(main_loop_class = DBusQtMainLoop) self._window = QSystemTrayIcon(self._app) icon_name = self.icons.get("okay") self._window.setIcon(QIcon.fromTheme(icon_name)) self._window.activated.connect(self._applet_activated) self._menu = QMenu(_("Magneto Entropy Updates Applet")) self._window.setContextMenu(self._menu) self._menu_items = {} for item in self._menu_item_list: if item is None: self._menu.addSeparator() continue myid, _unused, mytxt, myslot_func = item name = self.get_menu_image(myid) action_icon = QIcon.fromTheme(name) w = QAction(action_icon, mytxt, self._window, triggered=myslot_func) self._menu_items[myid] = w self._menu.addAction(w) self._menu.hide() def _first_check(self): def _do_check(): self.send_check_updates_signal(startup_check = True) return False if self._dbus_service_available: const_debug_write("_first_check", "spawning check.") QTimer.singleShot(10000, _do_check) def startup(self): self._dbus_service_available = self.setup_dbus() if config.settings["APPLET_ENABLED"] and \ self._dbus_service_available: self.enable_applet(do_check = False) const_debug_write("startup", "applet enabled, dbus service available.") else: const_debug_write("startup", "applet disabled.") self.disable_applet() if not self._dbus_service_available: const_debug_write("startup", "dbus service not available.") QTimer.singleShot(30000, self.show_service_not_available) else: const_debug_write("startup", "spawning first check.") self._first_check() # Notice Window instance self._notice_window = AppletNoticeWindow(self) self._window.show() # Enter main loop self._app.exec_() def close_service(self): super(Magneto, self).close_service() self._app.quit() def change_icon(self, icon_name): name = self.icons.get(icon_name) self._window.setIcon(QIcon.fromTheme(name)) def disable_applet(self, *args): super(Magneto, self).disable_applet() self._menu_items["disable_applet"].setEnabled(False) self._menu_items["enable_applet"].setEnabled(True) def enable_applet(self, w = None, do_check = True): done = super(Magneto, self).enable_applet(do_check = do_check) if done: self._menu_items["disable_applet"].setEnabled(True) self._menu_items["enable_applet"].setEnabled(False) def show_alert(self, title, text, urgency = None, force = False, buttons = None): # NOTE: there is no support for buttons via QSystemTrayIcon. if ((title, text) == self.last_alert) and not force: return def _action_activate_cb(action_num): if not buttons: return try: action_info = buttons[action_num - 1] except IndexError: return _action_id, _button_name, button_callback = action_info button_callback() def do_show(): if not self._window.supportsMessages(): const_debug_write("show_alert", "messages not supported.") return icon_id = QSystemTrayIcon.Information if urgency == "critical": icon_id = QSystemTrayIcon.Critical self._window.showMessage(title, text, icon_id) self.last_alert = (title, text) QTimer.singleShot(0, do_show) def update_tooltip(self, tip): def do_update(): self._window.setToolTip(tip) QTimer.singleShot(0, do_update) def applet_context_menu(self): """No action for now.""" def _applet_activated(self, reason): const_debug_write("applet_activated", "Applet activated: %s" % reason) if reason == QSystemTrayIcon.DoubleClick: const_debug_write("applet_activated", "Double click event.") self.applet_doubleclick() def hide_notice_window(self): self.notice_window_shown = False self._notice_window.hide() def show_notice_window(self): if self.notice_window_shown: const_debug_write("show_notice_window", "Notice window already shown.") return if not self.package_updates: const_debug_write("show_notice_window", "No computed updates.") return entropy_ver = None packages = [] for atom in self.package_updates: key = entropy.dep.dep_getkey(atom) avail_rev = entropy.dep.dep_get_entropy_revision(atom) avail_tag = entropy.dep.dep_gettag(atom) my_pkg = entropy.dep.remove_entropy_revision(atom) my_pkg = entropy.dep.remove_tag(my_pkg) pkgcat, pkgname, pkgver, pkgrev = entropy.dep.catpkgsplit(my_pkg) ver = pkgver if pkgrev != "r0": ver += "-%s" % (pkgrev,) if avail_tag: ver += "#%s" % (avail_tag,) if avail_rev: ver += "~%s" % (avail_tag,) if key == "sys-apps/entropy": entropy_ver = ver packages.append("%s (%s)" % (key, ver,)) critical_msg = "" if entropy_ver is not None: critical_msg = "%s <b>sys-apps/entropy</b> %s, %s <b>%s</b>. %s." % ( _("Your system currently has an outdated version of"), _("installed"), _("the latest available version is"), entropy_ver, _("It is recommended that you upgrade to " "the latest before updating any other packages") ) self._notice_window.populate(packages, critical_msg) self._notice_window.show() self.notice_window_shown = True
class Sensei(QMainWindow): def __init__(self): super().__init__() self.initUI() print("meta:", USER_ID, SESSION_ID) self.history = {} self.history[USER_ID] = {} self.history[USER_ID][SESSION_ID] = {} self.history[USER_ID][SESSION_ID][datetime.datetime.now().strftime( '%Y-%m-%d_%H-%M-%S')] = "sensitivity: " + str(SENSITIVITY) # Create the worker Thread # TODO: See if any advantage to using thread or if timer alone works. # TODO: Compare to workerThread example at # https://stackoverflow.com/questions/31945125/pyqt5-qthread-passing-variables-to-main self.capture = Capture(self) self.timer = QTimer(self, timeout=self.calibrate) self.mode = 0 # 0: Initial, 1: Calibrate, 2: Monitor self.checkDependencies() def checkDependencies(self): global TERMINAL_NOTIFIER_INSTALLED if 'darwin' in sys.platform and not TERMINAL_NOTIFIER_INSTALLED: # FIXME: Add check for Brew installation and installation of # terminal-notifier. self.instructions.setText( 'Installing terminal-notifier dependency') print('Installing terminal-notifier is required') # FIXME: This line hangs. # subprocess.call( # ['brew', 'install', 'terminal-notifier'], stdout=subprocess.PIPE) # TERMINAL_NOTIFIER_INSTALLED = True self.instructions.setText('Sit upright and click \'Calibrate\'') def aboutEvent(self, event): dialog = QDialog() aboutDialog = Ui_AboutWindow() aboutDialog.setupUi(dialog) aboutDialog.githubButton.clicked.connect(self.openGitHub) dialog.exec_() self.trayIcon.showMessage("Notice 🙇👊", "Keep strait posture", QSystemTrayIcon.Information, 4000) def closeEvent(self, event): """ Override QWidget close event to save history on exit. """ # TODO: Replace with CSV method. # if os.path.exists('posture.dat'): # with open('posture.dat','rb') as saved_history: # history =pickle.load(saved_history) if self.history: if hasattr(sys, "_MEIPASS"): # PyInstaller deployed here = os.path.join(sys._MEIPASS) else: here = os.path.dirname(os.path.realpath(__file__)) directory = os.path.join(here, 'data', str(USER_ID)) # directory = os.path.join(here, 'data', 'test') if not os.path.exists(directory): os.makedirs(directory) with open(os.path.join(directory, str(SESSION_ID) + '.dat'), 'wb') as f: pickle.dump(self.history, f) qApp.quit() def start(self): self.timer.start() def stop(self): self.timer.stop() def initUI(self): menu = QMenu() # Use Buddha in place of smiley face # iconPath = pyInstallerResourcePath('exit-gray.png') iconPath = pyInstallerResourcePath('meditate.png') self.trayIcon = QSystemTrayIcon(self) supported = self.trayIcon.supportsMessages() self.trayIcon.setIcon(QIcon(iconPath)) self.trayIcon.setContextMenu(menu) self.trayIcon.showMessage('a', 'b') self.trayIcon.show() self.postureIcon = QSystemTrayIcon(self) self.postureIcon.setIcon(QIcon(pyInstallerResourcePath('posture.png'))) self.postureIcon.setContextMenu(menu) self.postureIcon.show() exitAction = QAction("&Quit Sensei", self, shortcut="Ctrl+Q", triggered=self.closeEvent) preferencesAction = QAction("&Preferences...", self, triggered=self.showApp) # preferencesAction.setStatusTip('Sensei Preferences') aboutAction = QAction("&About Sensei", self, triggered=self.aboutEvent) menu.addAction(aboutAction) menu.addSeparator() menu.addAction(preferencesAction) menu.addSeparator() menu.addAction(exitAction) optionsMenu = menu.addMenu('&Options') soundToggleAction = QAction("Toggle Sound", self, triggered=self.toggleSound) optionsMenu.addAction(soundToggleAction) # TODO: Add settings panel. # changeSettings = QAction(QIcon('exit.png'), "&Settings", self, shortcut="Cmd+,", triggered=self.changeSettings) # changeSettings.setStatusTip('Change Settings') # menu.addAction(changeSettings) self.pbar = QProgressBar(self) self.pbar.setGeometry(30, 40, 200, 25) self.pbarValue = 0 self.setGeometry(300, 300, 290, 150) self.setWindowTitle('Posture Monitor') self.startButton = QPushButton('Calibrate', self) self.startButton.move(30, 60) self.startButton.clicked.connect(self.calibrate) self.stopButton = QPushButton('Stop', self) self.stopButton.move(30, 60) self.stopButton.clicked.connect(self.endCalibration) self.stopButton.hide() self.settingsButton = QPushButton('Settings', self) self.settingsButton.move(140, 60) self.settingsButton.clicked.connect(self.settings) self.doneButton = QPushButton('Done', self) self.doneButton.move(30, 60) self.doneButton.hide() self.doneButton.clicked.connect(self.minimize) # TODO: Create QWidget panel for Settings with MONITOR_DELAY # and SENSITIVITY options. # layout = QFormLayout() # self.le = QLineEdit() self.instructions = QLabel(self) self.instructions.move(40, 20) self.instructions.setText('Sit upright and click \'Calibrate\'') self.instructions.setGeometry(40, 20, 230, 25) if not supported: self.instructions.setText( 'Error: Notification is not available on your system.') self.show() def toggleSound(self): global soundOn # FIXME: Replace with preferences dictionary or similar soundOn = not soundOn def minimize(self): self.reset() self.hide() def reset(self): pass def showApp(self): self.show() self.raise_() self.doneButton.hide() self.startButton.show() self.pbar.show() self.settingsButton.show() self.activateWindow() def settings(self): global MONITOR_DELAY seconds, ok = QInputDialog.getInt( self, "Delay Settings", "Enter number of seconds to check posture\n(Default = 2)") if ok: seconds = seconds if seconds >= 1 else 0.5 MONITOR_DELAY = seconds * 1000 def endCalibration(self): self.mode = 2 # Monitor mode self.timer.stop() self.stopButton.hide() self.startButton.setText('Recalibrate') # Keep hidden. self.instructions.setText('Sit upright and click \'Recalibrate\'') self.instructions.hide() self.pbar.hide() self.settingsButton.hide() self.history[USER_ID][SESSION_ID][datetime.datetime.now().strftime( '%Y-%m-%d_%H-%M-%S')] = "baseline: " + str(self.upright) self.animateClosing() # Begin monitoring posture. self.timer = QTimer(self, timeout=self.monitor) self.timer.start(MONITOR_DELAY) def animateClosing(self): self.doneButton.show() animation = QPropertyAnimation(self.doneButton, b"geometry") animation.setDuration(1000) animation.setStartValue(QRect(10, 60, 39, 20)) animation.setEndValue(QRect(120, 60, 39, 20)) animation.start() self.animation = animation def monitor(self): """ Grab the picture, find the face, and sent notification if needed. """ photo = self.capture.takePhoto() faces = getFaces(photo) while not len(faces): print("No faces detected.") time.sleep(2) photo = self.capture.takePhoto() faces = getFaces(photo) # Record history for later analyis. # TODO: Make this into cvs-friendly format. self.history[USER_ID][SESSION_ID][datetime.datetime.now().strftime( '%Y-%m-%d_%H-%M-%S')] = faces x, y, w, h = faces[0] if w > self.upright * SENSITIVITY: self.notify( title='Sensei 🙇👊', # TODO: Add doctor emoji `👨⚕️` subtitle='Whack!', message='Sit up strait 🙏⛩', appIcon=APP_ICON_PATH) def notify(self, title, subtitle, message, sound=None, appIcon=None): """ Mac-only and requires `terminal-notifier` to be installed. # TODO: Add check that terminal-notifier is installed. # TODO: Add Linux and windows compatibility. # TODO: Linux example: # TODO: sudo apt-get install libnotify-bin # TODO: from gi.repository import Notify # TODO: Notify.init("App Name") # TODO: Notify.Notification.new("Hi").show() """ # FIXME: Test following line on windows / linux. # Doesn't work on Mac and might replace `terminal-notifier` dependency # self.trayIcon.showMessage('Title', 'Content') if 'darwin' in sys.platform and TERMINAL_NOTIFIER_INSTALLED: # Check if on a Mac. t = '-title {!r}'.format(title) s = '-subtitle {!r}'.format(subtitle) m = '-message {!r}'.format(message) snd = '-sound {!r}'.format(sound) i = '-appIcon {!r}'.format(appIcon) os.system('terminal-notifier {}'.format(' '.join([m, t, s, snd, i]))) else: self.trayIcon.showMessage("Notice 🙇👊", "Keep strait posture", QSystemTrayIcon.Information, 4000) def calibrate(self): if self.mode == 2: # Came from 'Recalibrate' # Set up for calibrate mode. self.mode = 1 self.stopButton.show() self.startButton.hide() self.instructions.setText('Press \'stop\' when ready') self.timer.stop() self.timer = QTimer(self, timeout=self.calibrate) self.timer.start(CALIBRATION_SAMPLE_RATE) # Interpolate posture information from face. photo = self.capture.takePhoto() faces = getFaces(photo) while not len(faces): print("No faces detected.") time.sleep(2) photo = self.capture.takePhoto() faces = getFaces(photo) # TODO: Focus on user's face rather than artifacts of face detector of others # on camera # if len(faces) > 1: # print(faces) # Take argmax of faces x, y, w, h = faces[0] self.upright = w self.history[USER_ID][SESSION_ID][ datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + ': calibration'] = self.upright self.history["upright_face_width"] = self.upright if self.mode == 0: # Initial mode self.timer.start(CALIBRATION_SAMPLE_RATE) self.startButton.hide() self.stopButton.show() self.instructions.setText('Press \'stop\' when ready') self.mode = 1 # Calibrate mode elif self.mode == 1: # Update posture monitor bar. self.pbar.setValue(self.upright / 4) time.sleep(0.05) def openGitHub(self): import webbrowser webbrowser.open_new_tab('https://github.com/JustinShenk/sensei')
class Posture(QMainWindow): def __init__(self): super().__init__() self.initUI() self.capture = Capture(self) self.timer = QTimer(self, timeout=self.calibrate) self.mode = 0 # 0: Initial, 1: Calibrate, 2: Monitor self.instructions.setText('Sit upright and click \'Calibrate\'') def closeEvent(self, event): qApp.quit() def start(self): self.timer.start() def stop(self): self.timer.stop() def initUI(self): menu = QMenu() self.trayIcon = QSystemTrayIcon(self) supported = self.trayIcon.supportsMessages() self.trayIcon.setContextMenu(menu) self.trayIcon.showMessage('a', 'b') self.trayIcon.show() self.postureIcon = QSystemTrayIcon(self) self.postureIcon.setContextMenu(menu) self.postureIcon.show() self.pbar = QProgressBar(self) self.pbar.setGeometry(30, 40, 200, 25) self.pbarValue = 0 self.setGeometry(300, 300, 290, 150) self.setWindowTitle('Posture Monitor') self.startButton = QPushButton('Calibrate', self) self.startButton.move(30, 60) self.startButton.clicked.connect(self.calibrate) self.stopButton = QPushButton('Stop', self) self.stopButton.move(30, 60) self.stopButton.clicked.connect(self.endCalibration) self.stopButton.hide() self.settingsButton = QPushButton('Settings', self) self.settingsButton.move(140, 60) self.settingsButton.clicked.connect(self.settings) self.doneButton = QPushButton('Done', self) self.doneButton.move(30, 60) self.doneButton.hide() self.doneButton.clicked.connect(self.minimize) self.instructions = QLabel(self) self.instructions.move(40, 20) self.instructions.setText('Sit upright and click \'Calibrate\'') self.instructions.setGeometry(40, 20, 230, 25) if not supported: self.instructions.setText( 'Error: Notification is not available on your system.') self.show() def minimize(self): self.reset() self.hide() def reset(self): pass def settings(self): global MONITOR_DELAY seconds, ok = QInputDialog.getInt( self, "Delay Settings", "Enter number of seconds to check posture\n(Default = 2)") if ok: seconds = seconds if seconds >= 1 else 0.5 MONITOR_DELAY = seconds * 1000 def endCalibration(self): self.mode = 2 # Monitor mode self.timer.stop() self.stopButton.hide() self.startButton.setText('Recalibrate') # Keep hidden. self.instructions.setText('Sit upright and click \'Recalibrate\'') self.instructions.hide() self.pbar.hide() self.settingsButton.hide() self.animateClosing() # Begin monitoring posture. self.timer = QTimer(self, timeout=self.monitor) self.timer.start(MONITOR_DELAY) def animateClosing(self): self.doneButton.show() animation = QPropertyAnimation(self.doneButton, b"geometry") animation.setDuration(1000) animation.setStartValue(QRect(10, 60, 39, 20)) animation.setEndValue(QRect(120, 60, 39, 20)) animation.start() self.animation = animation def monitor(self): """ Take the picture, find the face, and send notification if needed. """ photo = self.capture.takePhoto() faces = getFaces(photo) while not len(faces): print("No faces detected.") time.sleep(2) photo = self.capture.takePhoto() faces = getFaces(photo) x, y, w, h = faces[0] if w > self.upright * SENSITIVITY: self.notify( title='PostureAlert 🙇👊', subtitle='notice', message='Sit up straight 🙏⛩', ) def notify(self, title, subtitle, message, sound=None, appIcon=None): self.trayIcon.showMessage("Notice 🙇👊", "Keep straight posture", QSystemTrayIcon.Information, 4000) def calibrate(self): if self.mode == 2: self.mode = 1 self.stopButton.show() self.startButton.hide() self.instructions.setText('Press \'stop\' when ready') self.timer.stop() self.timer = QTimer(self, timeout=self.calibrate) self.timer.start(CALIBRATION_SAMPLE_RATE) photo = self.capture.takePhoto() faces = getFaces(photo) while not len(faces): print("No faces detected.") time.sleep(2) photo = self.capture.takePhoto() faces = getFaces(photo) x, y, w, h = faces[0] self.upright = w if self.mode == 0: # Initial mode self.timer.start(CALIBRATION_SAMPLE_RATE) self.startButton.hide() self.stopButton.show() self.instructions.setText('Press \'stop\' when ready') self.mode = 1 # Calibrate mode elif self.mode == 1: # Update posture monitor bar. self.pbar.setValue(self.upright / 4) time.sleep(0.05)
class XNova_MainWindow(QWidget): STATE_NOT_AUTHED = 0 STATE_AUTHED = 1 def __init__(self, parent=None): super(XNova_MainWindow, self).__init__(parent, Qt.Window) # state vars self.config_store_dir = './cache' self.cfg = configparser.ConfigParser() self.cfg.read('config/net.ini', encoding='utf-8') self.state = self.STATE_NOT_AUTHED self.login_email = '' self.cookies_dict = {} self._hidden_to_tray = False # # init UI self.setWindowIcon(QIcon(':/i/xnova_logo_64.png')) self.setWindowTitle('XNova Commander') # main layouts self._layout = QVBoxLayout() self._layout.setContentsMargins(0, 2, 0, 0) self._layout.setSpacing(3) self.setLayout(self._layout) self._horizontal_layout = QHBoxLayout() self._horizontal_layout.setContentsMargins(0, 0, 0, 0) self._horizontal_layout.setSpacing(6) # flights frame self._fr_flights = QFrame(self) self._fr_flights.setMinimumHeight(22) self._fr_flights.setFrameShape(QFrame.NoFrame) self._fr_flights.setFrameShadow(QFrame.Plain) # planets bar scrollarea self._sa_planets = QScrollArea(self) self._sa_planets.setMinimumWidth(125) self._sa_planets.setMaximumWidth(125) self._sa_planets.setFrameShape(QFrame.NoFrame) self._sa_planets.setFrameShadow(QFrame.Plain) self._sa_planets.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self._sa_planets.setWidgetResizable(True) self._panel_planets = QWidget(self._sa_planets) self._layout_pp = QVBoxLayout() self._panel_planets.setLayout(self._layout_pp) self._lbl_planets = QLabel(self.tr('Planets:'), self._panel_planets) self._lbl_planets.setMaximumHeight(32) self._layout_pp.addWidget(self._lbl_planets) self._layout_pp.addStretch() self._sa_planets.setWidget(self._panel_planets) # # tab widget self._tabwidget = XTabWidget(self) self._tabwidget.enableButtonAdd(False) self._tabwidget.tabCloseRequested.connect(self.on_tab_close_requested) self._tabwidget.addClicked.connect(self.on_tab_add_clicked) # # create status bar self._statusbar = XNCStatusBar(self) self.set_status_message(self.tr('Not connected: Log in!')) # # tab widget pages self.login_widget = None self.flights_widget = None self.overview_widget = None self.imperium_widget = None # # settings widget self.settings_widget = SettingsWidget(self) self.settings_widget.settings_changed.connect(self.on_settings_changed) self.settings_widget.hide() # # finalize layouts self._horizontal_layout.addWidget(self._sa_planets) self._horizontal_layout.addWidget(self._tabwidget) self._layout.addWidget(self._fr_flights) self._layout.addLayout(self._horizontal_layout) self._layout.addWidget(self._statusbar) # # system tray icon self.tray_icon = None show_tray_icon = False if 'tray' in self.cfg: if (self.cfg['tray']['icon_usage'] == 'show') or \ (self.cfg['tray']['icon_usage'] == 'show_min'): self.create_tray_icon() # # try to restore last window size ssz = self.load_cfg_val('main_size') if ssz is not None: self.resize(ssz[0], ssz[1]) # # world initialization self.world = XNovaWorld_instance() self.world_timer = QTimer(self) self.world_timer.timeout.connect(self.on_world_timer) # overrides QWidget.closeEvent # cleanup just before the window close def closeEvent(self, close_event: QCloseEvent): logger.debug('closing') if self.tray_icon is not None: self.tray_icon.hide() self.tray_icon = None if self.world_timer.isActive(): self.world_timer.stop() self.world.script_command = 'stop' # also stop possible running scripts if self.world.isRunning(): self.world.quit() logger.debug('waiting for world thread to stop (5 sec)...') wait_res = self.world.wait(5000) if not wait_res: logger.warn('wait failed, last chance, terminating!') self.world.terminate() # store window size ssz = (self.width(), self.height()) self.store_cfg_val('main_size', ssz) # accept the event close_event.accept() def showEvent(self, evt: QShowEvent): super(XNova_MainWindow, self).showEvent(evt) self._hidden_to_tray = False def changeEvent(self, evt: QEvent): super(XNova_MainWindow, self).changeEvent(evt) if evt.type() == QEvent.WindowStateChange: if not isinstance(evt, QWindowStateChangeEvent): return # make sure we only do this for minimize events if (evt.oldState() != Qt.WindowMinimized) and self.isMinimized(): # we were minimized! explicitly hide settings widget # if it is open, otherwise it will be lost forever :( if self.settings_widget is not None: if self.settings_widget.isVisible(): self.settings_widget.hide() # should we minimize to tray? if self.cfg['tray']['icon_usage'] == 'show_min': if not self._hidden_to_tray: self._hidden_to_tray = True self.hide() def create_tray_icon(self): if QSystemTrayIcon.isSystemTrayAvailable(): logger.debug('System tray icon is available, showing') self.tray_icon = QSystemTrayIcon(QIcon(':/i/xnova_logo_32.png'), self) self.tray_icon.setToolTip(self.tr('XNova Commander')) self.tray_icon.activated.connect(self.on_tray_icon_activated) self.tray_icon.show() else: self.tray_icon = None def hide_tray_icon(self): if self.tray_icon is not None: self.tray_icon.hide() self.tray_icon.deleteLater() self.tray_icon = None def set_tray_tooltip(self, tip: str): if self.tray_icon is not None: self.tray_icon.setToolTip(tip) def set_status_message(self, msg: str): self._statusbar.set_status(msg) def store_cfg_val(self, category: str, value): pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category) try: cache_dir = pathlib.Path(self.config_store_dir) if not cache_dir.exists(): cache_dir.mkdir() with open(pickle_filename, 'wb') as f: pickle.dump(value, f) except pickle.PickleError as pe: pass except IOError as ioe: pass def load_cfg_val(self, category: str, default_value=None): value = None pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category) try: with open(pickle_filename, 'rb') as f: value = pickle.load(f) if value is None: value = default_value except pickle.PickleError as pe: pass except IOError as ioe: pass return value @pyqtSlot() def on_settings_changed(self): self.cfg.read('config/net.ini', encoding='utf-8') # maybe show/hide tray icon now? show_tray_icon = False if 'tray' in self.cfg: icon_usage = self.cfg['tray']['icon_usage'] if (icon_usage == 'show') or (icon_usage == 'show_min'): show_tray_icon = True # show if needs show and hidden, or hide if shown and needs to hide if show_tray_icon and (self.tray_icon is None): logger.debug('settings changed, showing tray icon') self.create_tray_icon() elif (not show_tray_icon) and (self.tray_icon is not None): logger.debug('settings changed, hiding tray icon') self.hide_tray_icon() # also notify world about changed config! self.world.reload_config() def add_tab(self, widget: QWidget, title: str, closeable: bool = True) -> int: tab_index = self._tabwidget.addTab(widget, title, closeable) return tab_index def remove_tab(self, index: int): self._tabwidget.removeTab(index) # called by main application object just after main window creation # to show login widget and begin login process def begin_login(self): # create flights widget self.flights_widget = FlightsWidget(self._fr_flights) self.flights_widget.load_ui() install_layout_for_widget(self._fr_flights, Qt.Vertical, margins=(1, 1, 1, 1), spacing=1) self._fr_flights.layout().addWidget(self.flights_widget) self.flights_widget.set_online_state(False) self.flights_widget.requestShowSettings.connect(self.on_show_settings) # create and show login widget as first tab self.login_widget = LoginWidget(self._tabwidget) self.login_widget.load_ui() self.login_widget.loginError.connect(self.on_login_error) self.login_widget.loginOk.connect(self.on_login_ok) self.login_widget.show() self.add_tab(self.login_widget, self.tr('Login'), closeable=False) # self.test_setup_planets_panel() # self.test_planet_tab() def setup_planets_panel(self, planets: list): layout = self._panel_planets.layout() layout.setSpacing(0) remove_trailing_spacer_from_layout(layout) # remove all previous planet widgets from planets panel if layout.count() > 0: for i in range(layout.count() - 1, -1, -1): li = layout.itemAt(i) if li is not None: wi = li.widget() if wi is not None: if isinstance(wi, PlanetSidebarWidget): layout.removeWidget(wi) wi.close() wi.deleteLater() # fix possible mem leak del wi for pl in planets: pw = PlanetSidebarWidget(self._panel_planets) pw.setPlanet(pl) layout.addWidget(pw) pw.show() # connections from each planet bar widget pw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab) pw.requestOpenPlanet.connect(self.on_request_open_planet_tab) append_trailing_spacer_to_layout(layout) def update_planets_panel(self): """ Calls QWidget.update() on every PlanetBarWidget embedded in ui.panel_planets, causing repaint """ layout = self._panel_planets.layout() if layout.count() > 0: for i in range(layout.count()): li = layout.itemAt(i) if li is not None: wi = li.widget() if wi is not None: if isinstance(wi, PlanetSidebarWidget): wi.update() def add_tab_for_planet(self, planet: XNPlanet): # construct planet widget and setup signals/slots plw = PlanetWidget(self._tabwidget) plw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab) plw.setPlanet(planet) # construct tab title tab_title = '{0} {1}'.format(planet.name, planet.coords.coords_str()) # add tab and make it current tab_index = self.add_tab(plw, tab_title, closeable=True) self._tabwidget.setCurrentIndex(tab_index) self._tabwidget.tabBar().setTabIcon(tab_index, QIcon(':/i/planet_32.png')) return tab_index def add_tab_for_galaxy(self, coords: XNCoords = None): gw = GalaxyWidget(self._tabwidget) tab_title = '{0}'.format(self.tr('Galaxy')) if coords is not None: tab_title = '{0} {1}'.format(self.tr('Galaxy'), coords.coords_str()) gw.setCoords(coords.galaxy, coords.system) idx = self.add_tab(gw, tab_title, closeable=True) self._tabwidget.setCurrentIndex(idx) self._tabwidget.tabBar().setTabIcon(idx, QIcon(':/i/galaxy_32.png')) @pyqtSlot(int) def on_tab_close_requested(self, idx: int): # logger.debug('tab close requested: {0}'.format(idx)) if idx <= 1: # cannot close overview or imperium tabs return self.remove_tab(idx) @pyqtSlot() def on_tab_add_clicked(self): pos = QCursor.pos() planets = self.world.get_planets() # logger.debug('tab bar add clicked, cursor pos = ({0}, {1})'.format(pos.x(), pos.y())) menu = QMenu(self) # galaxy view galaxy_action = QAction(menu) galaxy_action.setText(self.tr('Add galaxy view')) galaxy_action.setData(QVariant('galaxy')) menu.addAction(galaxy_action) # planets menu.addSection(self.tr('-- Planet tabs: --')) for planet in planets: action = QAction(menu) action.setText('{0} {1}'.format(planet.name, planet.coords.coords_str())) action.setData(QVariant(planet.planet_id)) menu.addAction(action) action_ret = menu.exec(pos) if action_ret is not None: # logger.debug('selected action data = {0}'.format(str(action_ret.data()))) if action_ret == galaxy_action: logger.debug('action_ret == galaxy_action') self.add_tab_for_galaxy() return # else consider this is planet widget planet_id = int(action_ret.data()) self.on_request_open_planet_tab(planet_id) @pyqtSlot(str) def on_login_error(self, errstr): logger.error('Login error: {0}'.format(errstr)) self.state = self.STATE_NOT_AUTHED self.set_status_message(self.tr('Login error: {0}').format(errstr)) QMessageBox.critical(self, self.tr('Login error:'), errstr) @pyqtSlot(str, dict) def on_login_ok(self, login_email, cookies_dict): # logger.debug('Login OK, login: {0}, cookies: {1}'.format(login_email, str(cookies_dict))) # save login data: email, cookies self.state = self.STATE_AUTHED self.set_status_message(self.tr('Login OK, loading world')) self.login_email = login_email self.cookies_dict = cookies_dict # # destroy login widget and remove its tab self.remove_tab(0) self.login_widget.close() self.login_widget.deleteLater() self.login_widget = None # # create overview widget and add it as first tab self.overview_widget = OverviewWidget(self._tabwidget) self.overview_widget.load_ui() self.add_tab(self.overview_widget, self.tr('Overview'), closeable=False) self.overview_widget.show() self.overview_widget.setEnabled(False) # # create 2nd tab - Imperium self.imperium_widget = ImperiumWidget(self._tabwidget) self.add_tab(self.imperium_widget, self.tr('Imperium'), closeable=False) self.imperium_widget.setEnabled(False) # # initialize XNova world updater self.world.initialize(cookies_dict) self.world.set_login_email(self.login_email) # connect signals from world self.world.world_load_progress.connect(self.on_world_load_progress) self.world.world_load_complete.connect(self.on_world_load_complete) self.world.net_request_started.connect(self.on_net_request_started) self.world.net_request_finished.connect(self.on_net_request_finished) self.world.flight_arrived.connect(self.on_flight_arrived) self.world.build_complete.connect(self.on_building_complete) self.world.loaded_overview.connect(self.on_loaded_overview) self.world.loaded_imperium.connect(self.on_loaded_imperium) self.world.loaded_planet.connect(self.on_loaded_planet) self.world.start() @pyqtSlot(str, int) def on_world_load_progress(self, comment: str, progress: int): self._statusbar.set_world_load_progress(comment, progress) @pyqtSlot() def on_world_load_complete(self): logger.debug('main: on_world_load_complete()') # enable adding new tabs self._tabwidget.enableButtonAdd(True) # update statusbar self._statusbar.set_world_load_progress( '', -1) # turn off progress display self.set_status_message(self.tr('World loaded.')) # update account info if self.overview_widget is not None: self.overview_widget.setEnabled(True) self.overview_widget.update_account_info() self.overview_widget.update_builds() # update flying fleets self.flights_widget.set_online_state(True) self.flights_widget.update_flights() # update planets planets = self.world.get_planets() self.setup_planets_panel(planets) if self.imperium_widget is not None: self.imperium_widget.setEnabled(True) self.imperium_widget.update_planets() # update statusbar self._statusbar.update_online_players_count() # update tray tooltip, add account name self.set_tray_tooltip( self.tr('XNova Commander') + ' - ' + self.world.get_account_info().login) # set timer to do every-second world recalculation self.world_timer.setInterval(1000) self.world_timer.setSingleShot(False) self.world_timer.start() @pyqtSlot() def on_loaded_overview(self): logger.debug('on_loaded_overview') # A lot of things are updated when overview is loaded # * Account information and stats if self.overview_widget is not None: self.overview_widget.update_account_info() # * flights will be updated every second anyway in on_world_timer(), so no need to call # self.flights_widget.update_flights() # * messages count also, is updated with flights # * current planet may have changed self.update_planets_panel() # * server time is updated also self._statusbar.update_online_players_count() @pyqtSlot() def on_loaded_imperium(self): logger.debug('on_loaded_imperium') # need to update imperium widget if self.imperium_widget is not None: self.imperium_widget.update_planets() # The important note here is that imperium update is the only place where # the planets list is read, so number of planets, their names, etc may change here # Also, imperium update OVERWRITES full planets array, so, all prev # references to planets in all GUI elements must be invalidated, because # they will point to unused, outdated planets planets = self.world.get_planets() # re-create planets sidebar self.setup_planets_panel(planets) # update all builds in overview widget if self.overview_widget: self.overview_widget.update_builds() # update all planet tabs with new planet references cnt = self._tabwidget.count() if cnt > 2: for index in range(2, cnt): tab_page = self._tabwidget.tabWidget(index) if tab_page is not None: try: tab_type = tab_page.get_tab_type() if tab_type == 'planet': tab_planet = tab_page.planet() new_planet = self.world.get_planet( tab_planet.planet_id) tab_page.setPlanet(new_planet) except AttributeError: # not all pages may have method get_tab_type() pass @pyqtSlot(int) def on_loaded_planet(self, planet_id: int): logger.debug('Got signal on_loaded_planet({0}), updating overview ' 'widget and planets panel'.format(planet_id)) if self.overview_widget: self.overview_widget.update_builds() self.update_planets_panel() # update also planet tab, if any planet = self.world.get_planet(planet_id) if planet is not None: tab_idx = self.find_tab_for_planet(planet_id) if tab_idx != -1: tab_widget = self._tabwidget.tabWidget(tab_idx) if isinstance(tab_widget, PlanetWidget): logger.debug('Updating planet tab #{}'.format(tab_idx)) tab_widget.setPlanet(planet) @pyqtSlot() def on_world_timer(self): if self.world: self.world.world_tick() self.update_planets_panel() if self.flights_widget: self.flights_widget.update_flights() if self.overview_widget: self.overview_widget.update_builds() if self.imperium_widget: self.imperium_widget.update_planet_resources() @pyqtSlot() def on_net_request_started(self): self._statusbar.set_loading_status(True) @pyqtSlot() def on_net_request_finished(self): self._statusbar.set_loading_status(False) @pyqtSlot(int) def on_tray_icon_activated(self, reason): # QSystemTrayIcon::Unknown 0 Unknown reason # QSystemTrayIcon::Context 1 The context menu for the system tray entry was requested # QSystemTrayIcon::DoubleClick 2 The system tray entry was double clicked # QSystemTrayIcon::Trigger 3 The system tray entry was clicked # QSystemTrayIcon::MiddleClick 4 The system tray entry was clicked with the middle mouse button if reason == QSystemTrayIcon.Trigger: # left-click self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.show() return def show_tray_message(self, title, message, icon_type=None, timeout_ms=None): """ Shows message from system tray icon, if system supports it. If no support, this is just a no-op :param title: message title :param message: message text :param icon_type: one of: QSystemTrayIcon.NoIcon 0 No icon is shown. QSystemTrayIcon.Information 1 An information icon is shown. QSystemTrayIcon.Warning 2 A standard warning icon is shown. QSystemTrayIcon.Critical 3 A critical warning icon is shown """ if self.tray_icon is None: return if self.tray_icon.supportsMessages(): if icon_type is None: icon_type = QSystemTrayIcon.Information if timeout_ms is None: timeout_ms = 10000 self.tray_icon.showMessage(title, message, icon_type, timeout_ms) else: logger.info('This system does not support tray icon messages.') @pyqtSlot() def on_show_settings(self): if self.settings_widget is not None: self.settings_widget.show() self.settings_widget.showNormal() @pyqtSlot(XNFlight) def on_flight_arrived(self, fl: XNFlight): logger.debug('main: flight arrival: {0}'.format(fl)) mis_str = flight_mission_for_humans(fl.mission) if fl.direction == 'return': mis_str += ' ' + self.tr('return') short_fleet_info = self.tr('{0} {1} => {2}, {3} ship(s)').format( mis_str, fl.src, fl.dst, len(fl.ships)) self.show_tray_message(self.tr('XNova: Fleet arrived'), short_fleet_info) @pyqtSlot(XNPlanet, XNPlanetBuildingItem) def on_building_complete(self, planet: XNPlanet, bitem: XNPlanetBuildingItem): logger.debug('main: build complete: on planet {0}: {1}'.format( planet.name, str(bitem))) # update also planet tab, if any if isinstance(planet, XNPlanet): tab_idx = self.find_tab_for_planet(planet.planet_id) if tab_idx != -1: tab_widget = self._tabwidget.tabWidget(tab_idx) if isinstance(tab_widget, PlanetWidget): logger.debug('Updating planet tab #{}'.format(tab_idx)) tab_widget.setPlanet(planet) # construct message to show in tray if bitem.is_shipyard_item: binfo_str = '{0} x {1}'.format(bitem.quantity, bitem.name) else: binfo_str = self.tr('{0} lv.{1}').format(bitem.name, bitem.level) msg = self.tr('{0} has built {1}').format(planet.name, binfo_str) self.show_tray_message(self.tr('XNova: Building complete'), msg) @pyqtSlot(XNCoords) def on_request_open_galaxy_tab(self, coords: XNCoords): tab_index = self.find_tab_for_galaxy(coords.galaxy, coords.system) if tab_index == -1: # create new tab for these coords self.add_tab_for_galaxy(coords) return # else switch to that tab self._tabwidget.setCurrentIndex(tab_index) @pyqtSlot(int) def on_request_open_planet_tab(self, planet_id: int): tab_index = self.find_tab_for_planet(planet_id) if tab_index == -1: # create new tab for planet planet = self.world.get_planet(planet_id) if planet is not None: self.add_tab_for_planet(planet) return # else switch to that tab self._tabwidget.setCurrentIndex(tab_index) def find_tab_for_planet(self, planet_id: int) -> int: """ Finds tab index where specified planet is already opened :param planet_id: planet id to search for :return: tab index, or -1 if not found """ cnt = self._tabwidget.count() if cnt < 3: return -1 # only overview and imperium tabs are present for index in range(2, cnt): tab_page = self._tabwidget.tabWidget(index) if tab_page is not None: try: tab_type = tab_page.get_tab_type() if tab_type == 'planet': tab_planet = tab_page.planet() if tab_planet.planet_id == planet_id: # we have found tab index where this planet is already opened return index except AttributeError: # not all pages may have method get_tab_type() pass return -1 def find_tab_for_galaxy(self, galaxy: int, system: int) -> int: """ Finds tab index where specified galaxy view is already opened :param galaxy: galaxy target coordinate :param system: system target coordinate :return: tab index, or -1 if not found """ cnt = self._tabwidget.count() if cnt < 3: return -1 # only overview and imperium tabs are present for index in range(2, cnt): tab_page = self._tabwidget.tabWidget(index) if tab_page is not None: try: tab_type = tab_page.get_tab_type() if tab_type == 'galaxy': coords = tab_page.coords() if (coords[0] == galaxy) and (coords[1] == system): # we have found galaxy tab index where this place is already opened return index except AttributeError: # not all pages may have method get_tab_type() pass return -1 def test_setup_planets_panel(self): """ Testing only - add 'fictive' planets to test planets panel without loading data :return: None """ pl1 = XNPlanet('Arnon', XNCoords(1, 7, 6)) pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg' pl1.fields_busy = 90 pl1.fields_total = 167 pl1.is_current = True pl2 = XNPlanet('Safizon', XNCoords(1, 232, 7)) pl2.pic_url = 'skins/default/planeten/small/s_dschjungelplanet05.jpg' pl2.fields_busy = 84 pl2.fields_total = 207 pl2.is_current = False test_planets = [pl1, pl2] self.setup_planets_panel(test_planets) def test_planet_tab(self): """ Testing only - add 'fictive' planet tab to test UI without loading world :return: """ # construct planet pl1 = XNPlanet('Arnon', coords=XNCoords(1, 7, 6), planet_id=12345) pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg' pl1.fields_busy = 90 pl1.fields_total = 167 pl1.is_current = True pl1.res_current.met = 10000000 pl1.res_current.cry = 50000 pl1.res_current.deit = 250000000 # 250 mil pl1.res_per_hour.met = 60000 pl1.res_per_hour.cry = 30000 pl1.res_per_hour.deit = 15000 pl1.res_max_silos.met = 6000000 pl1.res_max_silos.cry = 3000000 pl1.res_max_silos.deit = 1000000 pl1.energy.energy_left = 10 pl1.energy.energy_total = 1962 pl1.energy.charge_percent = 92 # planet building item bitem = XNPlanetBuildingItem() bitem.gid = 1 bitem.name = 'Рудник металла' bitem.level = 29 bitem.remove_link = '' bitem.build_link = '?set=buildings&cmd=insert&building={0}'.format( bitem.gid) bitem.seconds_total = 23746 bitem.cost_met = 7670042 bitem.cost_cry = 1917510 bitem.is_building_item = True # second bitem bitem2 = XNPlanetBuildingItem() bitem2.gid = 2 bitem2.name = 'Рудник кристалла' bitem2.level = 26 bitem2.remove_link = '' bitem2.build_link = '?set=buildings&cmd=insert&building={0}'.format( bitem2.gid) bitem2.seconds_total = 13746 bitem2.cost_met = 9735556 bitem2.cost_cry = 4667778 bitem2.is_building_item = True bitem2.is_downgrade = True bitem2.seconds_left = bitem2.seconds_total // 2 bitem2.calc_end_time() # add bitems pl1.buildings_items = [bitem, bitem2] # add self.add_tab_for_planet(pl1)
class XNova_MainWindow(QWidget): STATE_NOT_AUTHED = 0 STATE_AUTHED = 1 def __init__(self, parent=None): super(XNova_MainWindow, self).__init__(parent, Qt.Window) # state vars self.config_store_dir = './cache' self.cfg = configparser.ConfigParser() self.cfg.read('config/net.ini', encoding='utf-8') self.state = self.STATE_NOT_AUTHED self.login_email = '' self.cookies_dict = {} self._hidden_to_tray = False # # init UI self.setWindowIcon(QIcon(':/i/xnova_logo_64.png')) self.setWindowTitle('XNova Commander') # main layouts self._layout = QVBoxLayout() self._layout.setContentsMargins(0, 2, 0, 0) self._layout.setSpacing(3) self.setLayout(self._layout) self._horizontal_layout = QHBoxLayout() self._horizontal_layout.setContentsMargins(0, 0, 0, 0) self._horizontal_layout.setSpacing(6) # flights frame self._fr_flights = QFrame(self) self._fr_flights.setMinimumHeight(22) self._fr_flights.setFrameShape(QFrame.NoFrame) self._fr_flights.setFrameShadow(QFrame.Plain) # planets bar scrollarea self._sa_planets = QScrollArea(self) self._sa_planets.setMinimumWidth(125) self._sa_planets.setMaximumWidth(125) self._sa_planets.setFrameShape(QFrame.NoFrame) self._sa_planets.setFrameShadow(QFrame.Plain) self._sa_planets.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self._sa_planets.setWidgetResizable(True) self._panel_planets = QWidget(self._sa_planets) self._layout_pp = QVBoxLayout() self._panel_planets.setLayout(self._layout_pp) self._lbl_planets = QLabel(self.tr('Planets:'), self._panel_planets) self._lbl_planets.setMaximumHeight(32) self._layout_pp.addWidget(self._lbl_planets) self._layout_pp.addStretch() self._sa_planets.setWidget(self._panel_planets) # # tab widget self._tabwidget = XTabWidget(self) self._tabwidget.enableButtonAdd(False) self._tabwidget.tabCloseRequested.connect(self.on_tab_close_requested) self._tabwidget.addClicked.connect(self.on_tab_add_clicked) # # create status bar self._statusbar = XNCStatusBar(self) self.set_status_message(self.tr('Not connected: Log in!')) # # tab widget pages self.login_widget = None self.flights_widget = None self.overview_widget = None self.imperium_widget = None # # settings widget self.settings_widget = SettingsWidget(self) self.settings_widget.settings_changed.connect(self.on_settings_changed) self.settings_widget.hide() # # finalize layouts self._horizontal_layout.addWidget(self._sa_planets) self._horizontal_layout.addWidget(self._tabwidget) self._layout.addWidget(self._fr_flights) self._layout.addLayout(self._horizontal_layout) self._layout.addWidget(self._statusbar) # # system tray icon self.tray_icon = None show_tray_icon = False if 'tray' in self.cfg: if (self.cfg['tray']['icon_usage'] == 'show') or \ (self.cfg['tray']['icon_usage'] == 'show_min'): self.create_tray_icon() # # try to restore last window size ssz = self.load_cfg_val('main_size') if ssz is not None: self.resize(ssz[0], ssz[1]) # # world initialization self.world = XNovaWorld_instance() self.world_timer = QTimer(self) self.world_timer.timeout.connect(self.on_world_timer) # overrides QWidget.closeEvent # cleanup just before the window close def closeEvent(self, close_event: QCloseEvent): logger.debug('closing') if self.tray_icon is not None: self.tray_icon.hide() self.tray_icon = None if self.world_timer.isActive(): self.world_timer.stop() self.world.script_command = 'stop' # also stop possible running scripts if self.world.isRunning(): self.world.quit() logger.debug('waiting for world thread to stop (5 sec)...') wait_res = self.world.wait(5000) if not wait_res: logger.warn('wait failed, last chance, terminating!') self.world.terminate() # store window size ssz = (self.width(), self.height()) self.store_cfg_val('main_size', ssz) # accept the event close_event.accept() def showEvent(self, evt: QShowEvent): super(XNova_MainWindow, self).showEvent(evt) self._hidden_to_tray = False def changeEvent(self, evt: QEvent): super(XNova_MainWindow, self).changeEvent(evt) if evt.type() == QEvent.WindowStateChange: if not isinstance(evt, QWindowStateChangeEvent): return # make sure we only do this for minimize events if (evt.oldState() != Qt.WindowMinimized) and self.isMinimized(): # we were minimized! explicitly hide settings widget # if it is open, otherwise it will be lost forever :( if self.settings_widget is not None: if self.settings_widget.isVisible(): self.settings_widget.hide() # should we minimize to tray? if self.cfg['tray']['icon_usage'] == 'show_min': if not self._hidden_to_tray: self._hidden_to_tray = True self.hide() def create_tray_icon(self): if QSystemTrayIcon.isSystemTrayAvailable(): logger.debug('System tray icon is available, showing') self.tray_icon = QSystemTrayIcon(QIcon(':/i/xnova_logo_32.png'), self) self.tray_icon.setToolTip(self.tr('XNova Commander')) self.tray_icon.activated.connect(self.on_tray_icon_activated) self.tray_icon.show() else: self.tray_icon = None def hide_tray_icon(self): if self.tray_icon is not None: self.tray_icon.hide() self.tray_icon.deleteLater() self.tray_icon = None def set_tray_tooltip(self, tip: str): if self.tray_icon is not None: self.tray_icon.setToolTip(tip) def set_status_message(self, msg: str): self._statusbar.set_status(msg) def store_cfg_val(self, category: str, value): pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category) try: cache_dir = pathlib.Path(self.config_store_dir) if not cache_dir.exists(): cache_dir.mkdir() with open(pickle_filename, 'wb') as f: pickle.dump(value, f) except pickle.PickleError as pe: pass except IOError as ioe: pass def load_cfg_val(self, category: str, default_value=None): value = None pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category) try: with open(pickle_filename, 'rb') as f: value = pickle.load(f) if value is None: value = default_value except pickle.PickleError as pe: pass except IOError as ioe: pass return value @pyqtSlot() def on_settings_changed(self): self.cfg.read('config/net.ini', encoding='utf-8') # maybe show/hide tray icon now? show_tray_icon = False if 'tray' in self.cfg: icon_usage = self.cfg['tray']['icon_usage'] if (icon_usage == 'show') or (icon_usage == 'show_min'): show_tray_icon = True # show if needs show and hidden, or hide if shown and needs to hide if show_tray_icon and (self.tray_icon is None): logger.debug('settings changed, showing tray icon') self.create_tray_icon() elif (not show_tray_icon) and (self.tray_icon is not None): logger.debug('settings changed, hiding tray icon') self.hide_tray_icon() # also notify world about changed config! self.world.reload_config() def add_tab(self, widget: QWidget, title: str, closeable: bool = True) -> int: tab_index = self._tabwidget.addTab(widget, title, closeable) return tab_index def remove_tab(self, index: int): self._tabwidget.removeTab(index) # called by main application object just after main window creation # to show login widget and begin login process def begin_login(self): # create flights widget self.flights_widget = FlightsWidget(self._fr_flights) self.flights_widget.load_ui() install_layout_for_widget(self._fr_flights, Qt.Vertical, margins=(1, 1, 1, 1), spacing=1) self._fr_flights.layout().addWidget(self.flights_widget) self.flights_widget.set_online_state(False) self.flights_widget.requestShowSettings.connect(self.on_show_settings) # create and show login widget as first tab self.login_widget = LoginWidget(self._tabwidget) self.login_widget.load_ui() self.login_widget.loginError.connect(self.on_login_error) self.login_widget.loginOk.connect(self.on_login_ok) self.login_widget.show() self.add_tab(self.login_widget, self.tr('Login'), closeable=False) # self.test_setup_planets_panel() # self.test_planet_tab() def setup_planets_panel(self, planets: list): layout = self._panel_planets.layout() layout.setSpacing(0) remove_trailing_spacer_from_layout(layout) # remove all previous planet widgets from planets panel if layout.count() > 0: for i in range(layout.count()-1, -1, -1): li = layout.itemAt(i) if li is not None: wi = li.widget() if wi is not None: if isinstance(wi, PlanetSidebarWidget): layout.removeWidget(wi) wi.close() wi.deleteLater() # fix possible mem leak del wi for pl in planets: pw = PlanetSidebarWidget(self._panel_planets) pw.setPlanet(pl) layout.addWidget(pw) pw.show() # connections from each planet bar widget pw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab) pw.requestOpenPlanet.connect(self.on_request_open_planet_tab) append_trailing_spacer_to_layout(layout) def update_planets_panel(self): """ Calls QWidget.update() on every PlanetBarWidget embedded in ui.panel_planets, causing repaint """ layout = self._panel_planets.layout() if layout.count() > 0: for i in range(layout.count()): li = layout.itemAt(i) if li is not None: wi = li.widget() if wi is not None: if isinstance(wi, PlanetSidebarWidget): wi.update() def add_tab_for_planet(self, planet: XNPlanet): # construct planet widget and setup signals/slots plw = PlanetWidget(self._tabwidget) plw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab) plw.setPlanet(planet) # construct tab title tab_title = '{0} {1}'.format(planet.name, planet.coords.coords_str()) # add tab and make it current tab_index = self.add_tab(plw, tab_title, closeable=True) self._tabwidget.setCurrentIndex(tab_index) self._tabwidget.tabBar().setTabIcon(tab_index, QIcon(':/i/planet_32.png')) return tab_index def add_tab_for_galaxy(self, coords: XNCoords = None): gw = GalaxyWidget(self._tabwidget) tab_title = '{0}'.format(self.tr('Galaxy')) if coords is not None: tab_title = '{0} {1}'.format(self.tr('Galaxy'), coords.coords_str()) gw.setCoords(coords.galaxy, coords.system) idx = self.add_tab(gw, tab_title, closeable=True) self._tabwidget.setCurrentIndex(idx) self._tabwidget.tabBar().setTabIcon(idx, QIcon(':/i/galaxy_32.png')) @pyqtSlot(int) def on_tab_close_requested(self, idx: int): # logger.debug('tab close requested: {0}'.format(idx)) if idx <= 1: # cannot close overview or imperium tabs return self.remove_tab(idx) @pyqtSlot() def on_tab_add_clicked(self): pos = QCursor.pos() planets = self.world.get_planets() # logger.debug('tab bar add clicked, cursor pos = ({0}, {1})'.format(pos.x(), pos.y())) menu = QMenu(self) # galaxy view galaxy_action = QAction(menu) galaxy_action.setText(self.tr('Add galaxy view')) galaxy_action.setData(QVariant('galaxy')) menu.addAction(galaxy_action) # planets menu.addSection(self.tr('-- Planet tabs: --')) for planet in planets: action = QAction(menu) action.setText('{0} {1}'.format(planet.name, planet.coords.coords_str())) action.setData(QVariant(planet.planet_id)) menu.addAction(action) action_ret = menu.exec(pos) if action_ret is not None: # logger.debug('selected action data = {0}'.format(str(action_ret.data()))) if action_ret == galaxy_action: logger.debug('action_ret == galaxy_action') self.add_tab_for_galaxy() return # else consider this is planet widget planet_id = int(action_ret.data()) self.on_request_open_planet_tab(planet_id) @pyqtSlot(str) def on_login_error(self, errstr): logger.error('Login error: {0}'.format(errstr)) self.state = self.STATE_NOT_AUTHED self.set_status_message(self.tr('Login error: {0}').format(errstr)) QMessageBox.critical(self, self.tr('Login error:'), errstr) @pyqtSlot(str, dict) def on_login_ok(self, login_email, cookies_dict): # logger.debug('Login OK, login: {0}, cookies: {1}'.format(login_email, str(cookies_dict))) # save login data: email, cookies self.state = self.STATE_AUTHED self.set_status_message(self.tr('Login OK, loading world')) self.login_email = login_email self.cookies_dict = cookies_dict # # destroy login widget and remove its tab self.remove_tab(0) self.login_widget.close() self.login_widget.deleteLater() self.login_widget = None # # create overview widget and add it as first tab self.overview_widget = OverviewWidget(self._tabwidget) self.overview_widget.load_ui() self.add_tab(self.overview_widget, self.tr('Overview'), closeable=False) self.overview_widget.show() self.overview_widget.setEnabled(False) # # create 2nd tab - Imperium self.imperium_widget = ImperiumWidget(self._tabwidget) self.add_tab(self.imperium_widget, self.tr('Imperium'), closeable=False) self.imperium_widget.setEnabled(False) # # initialize XNova world updater self.world.initialize(cookies_dict) self.world.set_login_email(self.login_email) # connect signals from world self.world.world_load_progress.connect(self.on_world_load_progress) self.world.world_load_complete.connect(self.on_world_load_complete) self.world.net_request_started.connect(self.on_net_request_started) self.world.net_request_finished.connect(self.on_net_request_finished) self.world.flight_arrived.connect(self.on_flight_arrived) self.world.build_complete.connect(self.on_building_complete) self.world.loaded_overview.connect(self.on_loaded_overview) self.world.loaded_imperium.connect(self.on_loaded_imperium) self.world.loaded_planet.connect(self.on_loaded_planet) self.world.start() @pyqtSlot(str, int) def on_world_load_progress(self, comment: str, progress: int): self._statusbar.set_world_load_progress(comment, progress) @pyqtSlot() def on_world_load_complete(self): logger.debug('main: on_world_load_complete()') # enable adding new tabs self._tabwidget.enableButtonAdd(True) # update statusbar self._statusbar.set_world_load_progress('', -1) # turn off progress display self.set_status_message(self.tr('World loaded.')) # update account info if self.overview_widget is not None: self.overview_widget.setEnabled(True) self.overview_widget.update_account_info() self.overview_widget.update_builds() # update flying fleets self.flights_widget.set_online_state(True) self.flights_widget.update_flights() # update planets planets = self.world.get_planets() self.setup_planets_panel(planets) if self.imperium_widget is not None: self.imperium_widget.setEnabled(True) self.imperium_widget.update_planets() # update statusbar self._statusbar.update_online_players_count() # update tray tooltip, add account name self.set_tray_tooltip(self.tr('XNova Commander') + ' - ' + self.world.get_account_info().login) # set timer to do every-second world recalculation self.world_timer.setInterval(1000) self.world_timer.setSingleShot(False) self.world_timer.start() @pyqtSlot() def on_loaded_overview(self): logger.debug('on_loaded_overview') # A lot of things are updated when overview is loaded # * Account information and stats if self.overview_widget is not None: self.overview_widget.update_account_info() # * flights will be updated every second anyway in on_world_timer(), so no need to call # self.flights_widget.update_flights() # * messages count also, is updated with flights # * current planet may have changed self.update_planets_panel() # * server time is updated also self._statusbar.update_online_players_count() @pyqtSlot() def on_loaded_imperium(self): logger.debug('on_loaded_imperium') # need to update imperium widget if self.imperium_widget is not None: self.imperium_widget.update_planets() # The important note here is that imperium update is the only place where # the planets list is read, so number of planets, their names, etc may change here # Also, imperium update OVERWRITES full planets array, so, all prev # references to planets in all GUI elements must be invalidated, because # they will point to unused, outdated planets planets = self.world.get_planets() # re-create planets sidebar self.setup_planets_panel(planets) # update all builds in overview widget if self.overview_widget: self.overview_widget.update_builds() # update all planet tabs with new planet references cnt = self._tabwidget.count() if cnt > 2: for index in range(2, cnt): tab_page = self._tabwidget.tabWidget(index) if tab_page is not None: try: tab_type = tab_page.get_tab_type() if tab_type == 'planet': tab_planet = tab_page.planet() new_planet = self.world.get_planet(tab_planet.planet_id) tab_page.setPlanet(new_planet) except AttributeError: # not all pages may have method get_tab_type() pass @pyqtSlot(int) def on_loaded_planet(self, planet_id: int): logger.debug('Got signal on_loaded_planet({0}), updating overview ' 'widget and planets panel'.format(planet_id)) if self.overview_widget: self.overview_widget.update_builds() self.update_planets_panel() # update also planet tab, if any planet = self.world.get_planet(planet_id) if planet is not None: tab_idx = self.find_tab_for_planet(planet_id) if tab_idx != -1: tab_widget = self._tabwidget.tabWidget(tab_idx) if isinstance(tab_widget, PlanetWidget): logger.debug('Updating planet tab #{}'.format(tab_idx)) tab_widget.setPlanet(planet) @pyqtSlot() def on_world_timer(self): if self.world: self.world.world_tick() self.update_planets_panel() if self.flights_widget: self.flights_widget.update_flights() if self.overview_widget: self.overview_widget.update_builds() if self.imperium_widget: self.imperium_widget.update_planet_resources() @pyqtSlot() def on_net_request_started(self): self._statusbar.set_loading_status(True) @pyqtSlot() def on_net_request_finished(self): self._statusbar.set_loading_status(False) @pyqtSlot(int) def on_tray_icon_activated(self, reason): # QSystemTrayIcon::Unknown 0 Unknown reason # QSystemTrayIcon::Context 1 The context menu for the system tray entry was requested # QSystemTrayIcon::DoubleClick 2 The system tray entry was double clicked # QSystemTrayIcon::Trigger 3 The system tray entry was clicked # QSystemTrayIcon::MiddleClick 4 The system tray entry was clicked with the middle mouse button if reason == QSystemTrayIcon.Trigger: # left-click self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.show() return def show_tray_message(self, title, message, icon_type=None, timeout_ms=None): """ Shows message from system tray icon, if system supports it. If no support, this is just a no-op :param title: message title :param message: message text :param icon_type: one of: QSystemTrayIcon.NoIcon 0 No icon is shown. QSystemTrayIcon.Information 1 An information icon is shown. QSystemTrayIcon.Warning 2 A standard warning icon is shown. QSystemTrayIcon.Critical 3 A critical warning icon is shown """ if self.tray_icon is None: return if self.tray_icon.supportsMessages(): if icon_type is None: icon_type = QSystemTrayIcon.Information if timeout_ms is None: timeout_ms = 10000 self.tray_icon.showMessage(title, message, icon_type, timeout_ms) else: logger.info('This system does not support tray icon messages.') @pyqtSlot() def on_show_settings(self): if self.settings_widget is not None: self.settings_widget.show() self.settings_widget.showNormal() @pyqtSlot(XNFlight) def on_flight_arrived(self, fl: XNFlight): logger.debug('main: flight arrival: {0}'.format(fl)) mis_str = flight_mission_for_humans(fl.mission) if fl.direction == 'return': mis_str += ' ' + self.tr('return') short_fleet_info = self.tr('{0} {1} => {2}, {3} ship(s)').format( mis_str, fl.src, fl.dst, len(fl.ships)) self.show_tray_message(self.tr('XNova: Fleet arrived'), short_fleet_info) @pyqtSlot(XNPlanet, XNPlanetBuildingItem) def on_building_complete(self, planet: XNPlanet, bitem: XNPlanetBuildingItem): logger.debug('main: build complete: on planet {0}: {1}'.format( planet.name, str(bitem))) # update also planet tab, if any if isinstance(planet, XNPlanet): tab_idx = self.find_tab_for_planet(planet.planet_id) if tab_idx != -1: tab_widget = self._tabwidget.tabWidget(tab_idx) if isinstance(tab_widget, PlanetWidget): logger.debug('Updating planet tab #{}'.format(tab_idx)) tab_widget.setPlanet(planet) # construct message to show in tray if bitem.is_shipyard_item: binfo_str = '{0} x {1}'.format(bitem.quantity, bitem.name) else: binfo_str = self.tr('{0} lv.{1}').format(bitem.name, bitem.level) msg = self.tr('{0} has built {1}').format(planet.name, binfo_str) self.show_tray_message(self.tr('XNova: Building complete'), msg) @pyqtSlot(XNCoords) def on_request_open_galaxy_tab(self, coords: XNCoords): tab_index = self.find_tab_for_galaxy(coords.galaxy, coords.system) if tab_index == -1: # create new tab for these coords self.add_tab_for_galaxy(coords) return # else switch to that tab self._tabwidget.setCurrentIndex(tab_index) @pyqtSlot(int) def on_request_open_planet_tab(self, planet_id: int): tab_index = self.find_tab_for_planet(planet_id) if tab_index == -1: # create new tab for planet planet = self.world.get_planet(planet_id) if planet is not None: self.add_tab_for_planet(planet) return # else switch to that tab self._tabwidget.setCurrentIndex(tab_index) def find_tab_for_planet(self, planet_id: int) -> int: """ Finds tab index where specified planet is already opened :param planet_id: planet id to search for :return: tab index, or -1 if not found """ cnt = self._tabwidget.count() if cnt < 3: return -1 # only overview and imperium tabs are present for index in range(2, cnt): tab_page = self._tabwidget.tabWidget(index) if tab_page is not None: try: tab_type = tab_page.get_tab_type() if tab_type == 'planet': tab_planet = tab_page.planet() if tab_planet.planet_id == planet_id: # we have found tab index where this planet is already opened return index except AttributeError: # not all pages may have method get_tab_type() pass return -1 def find_tab_for_galaxy(self, galaxy: int, system: int) -> int: """ Finds tab index where specified galaxy view is already opened :param galaxy: galaxy target coordinate :param system: system target coordinate :return: tab index, or -1 if not found """ cnt = self._tabwidget.count() if cnt < 3: return -1 # only overview and imperium tabs are present for index in range(2, cnt): tab_page = self._tabwidget.tabWidget(index) if tab_page is not None: try: tab_type = tab_page.get_tab_type() if tab_type == 'galaxy': coords = tab_page.coords() if (coords[0] == galaxy) and (coords[1] == system): # we have found galaxy tab index where this place is already opened return index except AttributeError: # not all pages may have method get_tab_type() pass return -1 def test_setup_planets_panel(self): """ Testing only - add 'fictive' planets to test planets panel without loading data :return: None """ pl1 = XNPlanet('Arnon', XNCoords(1, 7, 6)) pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg' pl1.fields_busy = 90 pl1.fields_total = 167 pl1.is_current = True pl2 = XNPlanet('Safizon', XNCoords(1, 232, 7)) pl2.pic_url = 'skins/default/planeten/small/s_dschjungelplanet05.jpg' pl2.fields_busy = 84 pl2.fields_total = 207 pl2.is_current = False test_planets = [pl1, pl2] self.setup_planets_panel(test_planets) def test_planet_tab(self): """ Testing only - add 'fictive' planet tab to test UI without loading world :return: """ # construct planet pl1 = XNPlanet('Arnon', coords=XNCoords(1, 7, 6), planet_id=12345) pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg' pl1.fields_busy = 90 pl1.fields_total = 167 pl1.is_current = True pl1.res_current.met = 10000000 pl1.res_current.cry = 50000 pl1.res_current.deit = 250000000 # 250 mil pl1.res_per_hour.met = 60000 pl1.res_per_hour.cry = 30000 pl1.res_per_hour.deit = 15000 pl1.res_max_silos.met = 6000000 pl1.res_max_silos.cry = 3000000 pl1.res_max_silos.deit = 1000000 pl1.energy.energy_left = 10 pl1.energy.energy_total = 1962 pl1.energy.charge_percent = 92 # planet building item bitem = XNPlanetBuildingItem() bitem.gid = 1 bitem.name = 'Рудник металла' bitem.level = 29 bitem.remove_link = '' bitem.build_link = '?set=buildings&cmd=insert&building={0}'.format(bitem.gid) bitem.seconds_total = 23746 bitem.cost_met = 7670042 bitem.cost_cry = 1917510 bitem.is_building_item = True # second bitem bitem2 = XNPlanetBuildingItem() bitem2.gid = 2 bitem2.name = 'Рудник кристалла' bitem2.level = 26 bitem2.remove_link = '' bitem2.build_link = '?set=buildings&cmd=insert&building={0}'.format(bitem2.gid) bitem2.seconds_total = 13746 bitem2.cost_met = 9735556 bitem2.cost_cry = 4667778 bitem2.is_building_item = True bitem2.is_downgrade = True bitem2.seconds_left = bitem2.seconds_total // 2 bitem2.calc_end_time() # add bitems pl1.buildings_items = [bitem, bitem2] # add self.add_tab_for_planet(pl1)