class MainWindow(QMainWindow): def __init__(self, app): QMainWindow.__init__(self) # Config app icon self.app_icon = QtGui.QIcon("assets/icon.png") self.setWindowIcon(self.app_icon) self.tray_icon = QSystemTrayIcon(self.app_icon, self) self.tray_icon.activated.connect(self.tray_icon_event) menu = QMenu(self) quit_action = menu.addAction("Fechar aplicação") quit_action.triggered.connect(app.quit) tray_menu = QMenu() tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() def tray_icon_event(self, reason): if reason == QSystemTrayIcon.DoubleClick: if not self.isVisible(): self.show() def hide_window_and_tray(self): if self.tray_icon.isVisible(): self.tray_icon.hide() if self.isVisible(): self.hide() # When user clicks on window quit button def closeEvent(self, event): event.ignore() self.hide() self.tray_icon.showMessage("File upload", "Aplicação foi minimizada", QSystemTrayIcon.Information, 1000)
class main(QDialog): #托盘 def __init__(self): super().__init__() self.loadMenu() self.initUI() def loadMenu(self): menuItems = [] menuItems.append({"text": "启动", "icon": "./icon/launch.png", "event": self.show, "hot": "D"}) menuItems.append({"text": "退出", "icon": "./icon/car.png", "event": self.close, "hot": "Q"}) self.trayIconMenu = QMenu(self) for i in menuItems: tmp = QAction(QIcon(i["icon"]), i["text"],self, triggered=i["event"]) tmp.setShortcut(self.tr(i["hot"])) self.trayIconMenu.addAction(tmp) def initUI(self): self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(QIcon("./icon/car.png")) self.trayIcon.setContextMenu(self.trayIconMenu) self.trayIcon.show() self.setWindowIcon(QIcon("./icon/car.png")) self.setGeometry(300, 300, 180, 300) self.setWindowTitle('窗体标题') def closeEvent(self, event): if self.trayIcon.isVisible(): self.trayIcon.hide()
class Tray: def __init__(self, target_platform: QMainWindow): self.target_platform = target_platform self.tray_icon = QSystemTrayIcon() show_action = QAction("Show", self.target_platform) quit_action = QAction("Exit", self.target_platform) show_action.triggered.connect(self.target_platform_show) quit_action.triggered.connect(self.target_platform_close) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.setIcon(QIcon(os.path.join(Common.current_directory(), 'tray.png'))) def target_platform_show(self): self.target_platform.show() self.tray_icon.hide() def target_platform_close(self): self.target_platform.close() def show(self): self.tray_icon.show() def hide(self): self.tray_icon.hide() self.target_platform.show()
class QTGui(QWidget): def __init__(self): super().__init__() self.showWindow() def changeEvent(self, QEvent): if QEvent.type() == QEvent.WindowStateChange: if self.isMinimized(): print("minimized") self.minimizetotray() super().changeEvent(QEvent) def showWindow(self): self.setGeometry(300, 300, 300, 63) self.setFixedSize(self.size()) self.setWindowIcon(QIcon("icon.png")) self.setWindowTitle("pyWall UI") global btn btn = QPushButton("Change", self) btn.resize(75, 23) btn.move(0, self.height() - btn.height()) btn.setToolTip("Change the wallpaper right now.") btn.clicked.connect(newWallpaperInNewThread) global txtinterval txtinterval = QTextEdit("100", self) txtinterval.setToolTip("Time interval in seconds between wallpaper changes.") txtinterval.resize(70, 23) txtinterval.move(0, btn.y() - txtinterval.height()) global chkbox chkbox = QCheckBox("Timer", self) chkbox.setToolTip("Use timer for auto wallpaper change.") chkbox.resize(49, 17) chkbox.move(0, txtinterval.y() - chkbox.height()) chkbox.stateChanged.connect(checkBoxStateChanged) global label label = QLabel("", self) label.setFont(QFont("Times", 8, QFont.Bold)) label.move(btn.width() + 5, 0) label.resize(self.width()-btn.width(),self.height()) label.setWordWrap(True) self.show() def minimizetotray(self): self.hide() self.tray = QSystemTrayIcon() self.tray.setIcon(QIcon("icon.png")) self.tray.setToolTip("pyWall Tray") self.tray.show() self.tray.showMessage("pyWall", "pyWall will run in background.", msecs=500) self.tray.activated.connect(self.trayiconactivated) def trayiconactivated(self, reason): if reason == QSystemTrayIcon.Trigger: self.tray.hide() self.show()
class MainWindow(QMainWindow): tray_icon = None def __init__(self): QMainWindow.__init__(self) self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(QIcon('icon.png')) self.hide() central_widget = QWidget(self) self.setCentralWidget(central_widget) quit_action = QAction("quit", self) quit_action.triggered.connect(qApp.quit) tray_menu = QMenu() tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() self.pouple = Pouple() self.history = History() self.bind_keys() def __exit__(self, exc_type, exc_val, exc_tb): self.tray_icon.hide() def command(self, cmd): def execute(): print(cmd.__name__) self.history.add(cmd) cmd() return execute def bind_keys(self): cfg = configparser.RawConfigParser() cfg.read('settings.cfg') hot_keys = { 'align_left': self.pouple.align_left, 'align_right': self.pouple.align_right, 'align_top': self.pouple.align_top, 'align_bottom': self.pouple.align_bottom, 'center': self.pouple.center, 'screen': self.pouple.screen, 'fullscreen': self.pouple.fullscreen, 'undo': self.history.undo, 'redo': self.history.redo, } for name in hot_keys: keyboard.add_hotkey(cfg.get('keys', name), self.command(hot_keys[name]))
class Main(QDialog): def __init__(self): super().__init__() self.loadMenu() self.initUI() def loadMenu(self): menuItems = [] # 菜单列表 menuItems.append({ "text": "启动", "icon": "./icons/set.png", "event": self.show, "hot": "D" }) menuItems.append({ "text": "退出", "icon": "./icons/close.png", "event": self.close, "hot": "Q" }) self.trayIconMenu = QMenu(self) # 创建菜单 #遍历绑定 显示的文字、图标、热键和点击事件 #热键可能是无效的 我这里只是为了显示效果而已 for i in menuItems: tmp = QAction(QIcon(i["icon"]), i["text"], self, triggered=i["event"]) tmp.setShortcut(self.tr(i["hot"])) self.trayIconMenu.addAction(tmp) def initUI(self): self.trayIcon = QSystemTrayIcon(self) # <===创建通知栏托盘图标 self.trayIcon.setIcon(QIcon("./joyrun/request/pic.ico")) #<===设置托盘图标 self.trayIcon.setContextMenu(self.trayIconMenu) #<===创建右键连接菜单 self.trayIcon.show() #<====显示托盘 self.setWindowIcon(QIcon("./joyrun/request/pic.ico")) #<===设置窗体图标 self.setGeometry(300, 300, 180, 300) # <===设置窗体打开位置与宽高 self.setWindowTitle('窗体标题') self.show() #<====显示窗体 # self.hide()#<====隐藏窗体 # 默认不显示窗体 # 重写窗体关闭事件,让其点击关闭时隐藏 def closeEvent(self, event): if self.trayIcon.isVisible(): self.trayIcon.hide()
class AppWindow(QMainWindow): def __init__(self): super().__init__() self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_ComputerIcon)) show_action = QAction("Show", self) quit_action = QAction("Exit", self) hide_action = QAction("Hide", self) show_action.triggered.connect(self.show) hide_action.triggered.connect(self.hide) quit_action.triggered.connect(self.close) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() self.tray_icon.activated.connect(self.hand_tray_activated) self.ui = Control_Panel() self.ui.setupUi(self) self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint) self.show() self.ui.th.hide_signal.connect(self.minimize_to_tray) def hand_tray_activated(self, reason): # On double clicking the tray icon the windows is shown again if reason == QSystemTrayIcon.DoubleClick: self.show() def minimize_to_tray(self): # when go button is clicked the app is minimized to tray self.hide() self.tray_icon.showMessage("Hand of The King", "Application was minimized to tray", QSystemTrayIcon.Information, 2000) def closeEvent(self, event): # overriding the closeEvent function of the window to cleanup all open windows and release the camera self.tray_icon.hide() self.ui.application_on = False self.ui.gesture_detector.camera.release() self.ui.hand_window.close() event.accept()
class MainWindow(QMainWindow): """ Сheckbox and system tray icons. Will initialize in the constructor. """ check_box = None tray_icon = None # Override the class constructor def __init__(self): # Be sure to call the super class method QMainWindow.__init__(self) self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_ComputerIcon)) show_action = QAction("Show", self) quit_action = QAction("Exit", self) hide_action = QAction("Hide", self) show_action.triggered.connect(self.show) hide_action.triggered.connect(self.hide) quit_action.triggered.connect(self.quit) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() def quit(self): self.tray_icon.hide() qApp.quit() # Override closeEvent, to intercept the window closing event # The window will be closed only if there is no check mark in the check box def closeEvent(self, event): event.ignore() self.hide() self.tray_icon.showMessage("Tray Program", "Application was minimized to Tray", QSystemTrayIcon.Information, 2000)
class BaseTray(object): def __init__(self, icon_path): self.tray = QSystemTrayIcon() self.tray.setIcon(QIcon(icon_path)) self.tray_menu = QMenu() self._set_menu() def _set_menu(self): pass def show_message(self, text): icon = self.tray.MessageIcon() self.tray.showMessage('message', text, icon, 1000) def show(self): self.tray.show() def quit(self): self.tray.hide() sys.exit()
class Window(QWidget): def __init__(self, *args, **kwargs): super(Window, self).__init__(*args, **kwargs) layout = QHBoxLayout(self) layout.addWidget(QPushButton('开始闪烁', self, clicked=self.start_flash)) layout.addWidget(QPushButton('停止闪烁', self, clicked=self.stop_flash)) # 创建托盘图标 self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_ComputerIcon)) self.tray_icon.show() # 图标闪烁定时器 self.tray_visible = True self.flash_timer = QTimer(self, timeout=self.flash_icon) def closeEvent(self, event): self.stop_flash() self.tray_icon.hide() super(Window, self).closeEvent(event) def start_flash(self): """开始闪烁""" if not self.flash_timer.isActive(): self.flash_timer.start(500) def stop_flash(self): """停止闪烁后需要显示图标""" if self.flash_timer.isActive(): self.flash_timer.stop() self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_ComputerIcon)) def flash_icon(self): """根据当前图标是否可见切换图标""" if self.tray_visible: self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_TrashIcon)) else: self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_ComputerIcon)) self.tray_visible = not self.tray_visible
class MainWindow(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.tray_icon = QSystemTrayIcon(self) # Set icon to a standard or custom icon self.tray_icon.setIcon(self.style().standardIcon( QStyle.SP_ComputerIcon)) # self.tray_icon.setIcon(QtGui.QIcon("icons/devdungeon32x32.png")) exit_action = QAction("Exit", self) exit_action.triggered.connect(self.exit_app) tray_menu = QMenu() tray_menu.addAction(exit_action) self.tray_icon.setContextMenu(tray_menu) # Set right-click menu self.tray_icon.show() def notify(self, message): """Generate a desktop notification""" self.tray_icon.showMessage("Pssst!", message, QSystemTrayIcon.Information, 3000) def exit_app(self): self.tray_icon.hide( ) # Do this or icon will linger until you hover after exit qApp.quit() def closeEvent(self, event): """ By overriding closeEvent, we can ignore the event and instead hide the window, effectively performing a "close-to-system-tray" action. To exit, the right-click->Exit option from the system tray must be used. """ event.ignore() self.hide() self.notify("App minimize to system tray.")
class DlgMain(QDialog): def addSystemTray(self): minimizeAction = QAction("Mi&nimize", self, triggered=self.hide) maximizeAction = QAction("Ma&ximize", self, triggered=self.showMaximized) restoreAction = QAction("&Restore", self, triggered=self.showNormal) quitAction = QAction("&Quit", self, triggered=self.close) self.trayIconMenu = QMenu(self) self.trayIconMenu.addAction(minimizeAction) self.trayIconMenu.addAction(maximizeAction) self.trayIconMenu.addAction(restoreAction) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(quitAction) self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(QIcon("skin/icons/logo.png")) self.setWindowIcon(QIcon("skin/icons/logo.png")) self.trayIcon.setContextMenu(self.trayIconMenu) self.trayIcon.show() sys.exit(self.exec_()) def closeEvent(self, event): if self.trayIcon.isVisible(): self.trayIcon.hide()
class MainWindow(MainWindow_Ui): def __init__(self): super().__init__() self.statusbar.showMessage('Please Wait ...') #threads self.threadPool=[] #starting aria start_aria = StartAria2Thread() self.threadPool.append(start_aria) self.threadPool[0].start() self.threadPool[0].ARIA2RESPONDSIGNAL.connect(self.startAriaMessage) #initializing #add downloads to the download_table f_download_list_file = Open(download_list_file) download_list_file_lines = f_download_list_file.readlines() f_download_list_file.close() for line in download_list_file_lines: gid = line.strip() self.download_table.insertRow(0) download_info_file = download_info_folder + "/" + gid f = Open(download_info_file) download_info_file_lines = f.readlines() f.close() for i in range(10): item = QTableWidgetItem(download_info_file_lines[i].strip()) self.download_table.setItem(0 , i , item) row_numbers = self.download_table.rowCount() for row in range(row_numbers): status = self.download_table.item(row , 1).text() if (status != "complete" and status != "error"): gid = self.download_table.item(row,8).text() add_link_dictionary_str = self.download_table.item(row,9).text() add_link_dictionary = ast.literal_eval(add_link_dictionary_str.strip()) add_link_dictionary['start_hour'] = None add_link_dictionary['start_minute'] = None add_link_dictionary['end_hour'] = None add_link_dictionary['end_minute'] = None add_link_dictionary['after_download'] = 'None' download_info_file = download_info_folder + "/" + gid f = Open(download_info_file) download_info_file_lines = f.readlines() f.close() f = Open(download_info_file , "w") for i in range(10): if i == 1 : f.writelines("stopped" + "\n") item = QTableWidgetItem('stopped') self.download_table.setItem(row , i , item ) elif i == 9 : f.writelines(str(add_link_dictionary) + "\n") item = QTableWidgetItem(str(add_link_dictionary)) self.download_table.setItem(row,i , item) else: f.writelines(download_info_file_lines[i].strip() + "\n") f.close() self.addlinkwindows_list = [] self.propertieswindows_list = [] self.progress_window_list = [] self.progress_window_list_dict = {} check_download_info = CheckDownloadInfoThread() self.threadPool.append(check_download_info) self.threadPool[1].start() self.threadPool[1].DOWNLOAD_INFO_SIGNAL.connect(self.checkDownloadInfo) check_selected_row = CheckSelectedRowThread() self.threadPool.append(check_selected_row) self.threadPool[2].start() self.threadPool[2].CHECKSELECTEDROWSIGNAL.connect(self.checkSelectedRow) check_flashgot = CheckFlashgot() self.threadPool.append(check_flashgot) self.threadPool[3].start() self.threadPool[3].CHECKFLASHGOTSIGNAL.connect(self.checkFlashgot) self.system_tray_icon = QSystemTrayIcon() self.system_tray_icon.setIcon(QIcon('icon')) system_tray_menu = QMenu() system_tray_menu.addAction(self.addlinkAction) system_tray_menu.addAction(self.pauseAllAction) system_tray_menu.addAction(self.stopAllAction) system_tray_menu.addAction(self.minimizeAction) system_tray_menu.addAction(self.exitAction) self.system_tray_icon.setContextMenu(system_tray_menu) self.system_tray_icon.activated.connect(self.systemTrayPressed) self.system_tray_icon.show() def startAriaMessage(self,message): global aria_startup_answer if message == 'yes': sleep (2) self.statusbar.showMessage('Ready...') aria_startup_answer = 'ready' else: self.statusbar.showMessage('Error...') notifySend('Persepolis can not connect to Aria2' , 'Restart Persepolis' ,10000,'critical' ) def checkDownloadInfo(self,gid): try: #get download information from download_info_file according to gid and write them in download_table cells download_info_file = config_folder + "/download_info/" + gid f = Open(download_info_file) download_info_file_lines = f.readlines() f.close() #finding row of this gid! for i in range(self.download_table.rowCount()): row_gid = self.download_table.item(i , 8).text() if gid == row_gid : row = i break for i in range(10): #remove gid of completed download from active downloads list file if i == 1 : status = download_info_file_lines[i].strip() status = str(status) status_download_table = str(self.download_table.item(row , 1 ) . text()) if status == "complete": f = Open(download_list_file_active) download_list_file_active_lines = f.readlines() f.close() f = Open(download_list_file_active , "w") for line in download_list_file_active_lines : if line.strip() != gid : f.writelines(line.strip() + "\n") f.close() #update download_table cells item = QTableWidgetItem(download_info_file_lines[i].strip()) self.download_table.setItem(row , i , item) self.download_table.viewport().update() #update progresswindow try : member_number = self.progress_window_list_dict[gid] progress_window = self.progress_window_list[member_number] #link add_link_dictionary_str = str(download_info_file_lines[9].strip()) add_link_dictionary = ast.literal_eval(add_link_dictionary_str) link = "<b>Link</b> : " + str(add_link_dictionary ['link']) progress_window.link_label.setText(link) progress_window.setToolTip(link) #Save as final_download_path = add_link_dictionary['final_download_path'] if final_download_path == None : final_download_path = str(add_link_dictionary['download_path']) save_as = "<b>Save as</b> : " + final_download_path + "/" + str(download_info_file_lines[0].strip()) progress_window.save_label.setText(save_as) file_name = str(download_info_file_lines[0].strip()) if file_name != "***": progress_window.setWindowTitle(file_name ) #status progress_window.status = download_info_file_lines[1].strip() status = "<b>status</b> : " + progress_window.status progress_window.status_label.setText(status) if progress_window.status == "downloading": progress_window.resume_pushButton.setEnabled(False) progress_window.stop_pushButton.setEnabled(True) progress_window.pause_pushButton.setEnabled(True) elif progress_window.status == "paused": progress_window.resume_pushButton.setEnabled(True) progress_window.stop_pushButton.setEnabled(True) progress_window.pause_pushButton.setEnabled(False) elif progress_window.status == "waiting": progress_window.resume_pushButton.setEnabled(False) progress_window.stop_pushButton.setEnabled(False) progress_window.pause_pushButton.setEnabled(False) elif progress_window.status == "scheduled": progress_window.resume_pushButton.setEnabled(False) progress_window.stop_pushButton.setEnabled(True) progress_window.pause_pushButton.setEnabled(False) elif progress_window.status == "stopped" or progress_window.status == "error" or progress_window.status == "complete" : progress_window.close() self.progress_window_list[member_number] = [] del self.progress_window_list_dict[gid] if progress_window.status == "complete": notifySend("Download Complete" ,str(download_info_file_lines[0]) , 10000 , 'ok' ) elif progress_window.status == "stopped": notifySend("Download Stopped" , str(download_info_file_lines[0]) , 10000 , 'no') elif progress_window.status == "error": notifySend("Download Error" , str(download_info_file_lines[0]) , 10000 , 'fail') add_link_dictionary['start_hour'] = None add_link_dictionary['start_minute'] = None add_link_dictionary['end_hour'] = None add_link_dictionary['end_minute'] = None add_link_dictionary['after_download'] = 'None' f = Open(download_info_file , "w") for i in range(10): if i == 9 : f.writelines(str(add_link_dictionary) + "\n") else: f.writelines(download_info_file_lines[i].strip() + "\n") f.close() if os.path.isfile('/tmp/persepolis/shutdown/' + gid ) == True and progress_window.status != 'stopped': answer = download.shutDown() if answer == 'error': os.system('killall aria2c') f = Open('/tmp/persepolis/shutdown/' + gid , 'w') f.writelines('shutdown') f.close() elif os.path.isfile('/tmp/persepolis/shutdown/' + gid ) == True and progress_window.status == 'stopped': f = Open('/tmp/persepolis/shutdown/' + gid , 'w') f.writelines('canceled') f.close() #downloaded downloaded = "<b>Downloaded</b> : " + str(download_info_file_lines[3].strip()) + "/" + str(download_info_file_lines[2].strip()) progress_window.downloaded_label.setText(downloaded) #Transfer rate rate = "<b>Transfer rate</b> : " + str(download_info_file_lines[6].strip()) progress_window.rate_label.setText(rate) #Estimate time left estimate_time_left = "<b>Estimate time left</b> : " + str(download_info_file_lines[7].strip()) progress_window.time_label.setText(estimate_time_left) #Connections connections = "<b>Connections</b> : " + str(download_info_file_lines[5].strip()) progress_window.connections_label.setText(connections) #progressbar value = download_info_file_lines[4].strip() value = value[:-1] progress_window.download_progressBar.setValue(int(value)) except : pass except: pass #contex menu def contextMenuEvent(self, event): self.tablewidget_menu = QMenu(self) self.tablewidget_menu.addAction(self.resumeAction) self.tablewidget_menu.addAction(self.pauseAction) self.tablewidget_menu.addAction(self.stopAction) self.tablewidget_menu.addAction(self.removeAction) self.tablewidget_menu.addAction(self.propertiesAction) self.tablewidget_menu.addAction(self.progressAction) self.tablewidget_menu.popup(QtGui.QCursor.pos()) #drag and drop for links def dragEnterEvent(self, droplink): text = str(droplink.mimeData().text()) if ("tp:/" in text[2:6]) or ("tps:/" in text[2:7]) : droplink.accept() else: droplink.ignore() def dropEvent(self, droplink): link_clipborad = QApplication.clipboard() link_clipborad.clear(mode=link_clipborad.Clipboard ) link_string = droplink.mimeData().text() link_clipborad.setText(str(link_string), mode=link_clipborad.Clipboard) self.addLinkButtonPressed(button =link_clipborad ) def gidGenerator(self): my_gid = hex(random.randint(1152921504606846976,18446744073709551615)) my_gid = my_gid[2:18] my_gid = str(my_gid) f = Open(download_list_file_active) active_gid_list = f.readlines() f.close() while my_gid in active_gid_list : my_gid = self.gidGenerator() active_gids = download.activeDownloads() while my_gid in active_gids: my_gid = self.gidGenerator() return my_gid def selectedRow(self): try: item = self.download_table.selectedItems() selected_row_return = self.download_table.row(item[1]) download_info = self.download_table.item(selected_row_return , 9).text() download_info = ast.literal_eval(download_info) link = download_info['link'] self.statusbar.showMessage(str(link)) except : selected_row_return = None return selected_row_return def checkSelectedRow(self): try: item = self.download_table.selectedItems() selected_row_return = self.download_table.row(item[1]) except : selected_row_return = None if selected_row_return != None : status = self.download_table.item(selected_row_return , 1).text() if status == "scheduled": self.resumeAction.setEnabled(False) self.pauseAction.setEnabled(False) self.stopAction.setEnabled(True) self.removeAction.setEnabled(False) self.propertiesAction.setEnabled(False) self.progressAction.setEnabled(True) elif status == "stopped" or status == "error" : self.stopAction.setEnabled(False) self.pauseAction.setEnabled(False) self.resumeAction.setEnabled(True) self.removeAction.setEnabled(True) self.propertiesAction.setEnabled(True) self.progressAction.setEnabled(False) elif status == "downloading": self.resumeAction.setEnabled(False) self.pauseAction.setEnabled(True) self.stopAction.setEnabled(True) self.removeAction.setEnabled(False) self.propertiesAction.setEnabled(False) self.progressAction.setEnabled(True) elif status == "waiting": self.stopAction.setEnabled(False) self.resumeAction.setEnabled(False) self.pauseAction.setEnabled(False) self.removeAction.setEnabled(False) self.propertiesAction.setEnabled(False) self.progressAction.setEnabled(True) elif status == "complete": self.stopAction.setEnabled(False) self.resumeAction.setEnabled(False) self.pauseAction.setEnabled(False) self.removeAction.setEnabled(True) self.propertiesAction.setEnabled(True) self.progressAction.setEnabled(False) elif status == "paused": self.stopAction.setEnabled(True) self.resumeAction.setEnabled(True) self.pauseAction.setEnabled(False) self.removeAction.setEnabled(False) self.propertiesAction.setEnabled(False) self.progressAction.setEnabled(True) else: self.resumeAction.setEnabled(True) self.stopAction.setEnabled(True) self.pauseAction.setEnabled(True) self.propertiesAction.setEnabled(True) else: self.resumeAction.setEnabled(True) self.stopAction.setEnabled(True) self.pauseAction.setEnabled(True) self.removeAction.setEnabled(True) self.propertiesAction.setEnabled(True) def checkFlashgot(self): sleep(0.5) flashgot_file = Open("/tmp/persepolis-flashgot") flashgot_line = flashgot_file.readlines() flashgot_file.close() flashgot_file.remove() flashgot_add_link_dictionary_str = flashgot_line[0] flashgot_add_link_dictionary = ast.literal_eval(flashgot_add_link_dictionary_str) self.flashgotAddLink(flashgot_add_link_dictionary) def flashgotAddLink(self,flashgot_add_link_dictionary): addlinkwindow = AddLinkWindow(self.callBack , flashgot_add_link_dictionary) self.addlinkwindows_list.append(addlinkwindow) self.addlinkwindows_list[len(self.addlinkwindows_list) - 1].show() def addLinkButtonPressed(self ,button): addlinkwindow = AddLinkWindow(self.callBack) self.addlinkwindows_list.append(addlinkwindow) self.addlinkwindows_list[len(self.addlinkwindows_list) - 1].show() def callBack(self , add_link_dictionary): gid = self.gidGenerator() download_info_file_list = ['***','waiting','***','***','***','***','***','***',gid , str(add_link_dictionary)] download_info_file = config_folder + "/download_info/" + gid os.system("touch " + download_info_file ) f = Open(download_info_file , "w") for i in range(10): f.writelines(download_info_file_list[i] + "\n") f.close() self.download_table.insertRow(0) j = 0 for i in download_info_file_list : item = QTableWidgetItem(i) self.download_table.setItem(0,j,item) j = j + 1 f = Open (download_list_file , "a") f.writelines(gid + "\n") f.close() f = Open (download_list_file_active , "a") f.writelines(gid + "\n") f.close() new_download = DownloadLink(gid) self.threadPool.append(new_download) self.threadPool[len(self.threadPool) - 1].start() self.progressBarOpen(gid) if add_link_dictionary['start_hour'] == None : message = "Download Starts" else: message = "Download Scheduled" notifySend(message ,'' , 10000 , 'no') def resumeButtonPressed(self,button): selected_row_return = self.selectedRow() if selected_row_return != None: gid = self.download_table.item(selected_row_return , 8 ).text() download_status = self.download_table.item(selected_row_return , 1).text() if download_status == "paused" : answer = download.downloadUnpause(gid) if answer == 'None': notifySend("Aria2 did not respond!","Try agian!",10000,'warning' ) else: new_download = DownloadLink(gid) self.threadPool.append(new_download) self.threadPool[len(self.threadPool) - 1].start() sleep(1) self.progressBarOpen(gid) else: self.statusbar.showMessage("Please select an item first!") def stopButtonPressed(self,button): selected_row_return = self.selectedRow() if selected_row_return != None: gid = self.download_table.item(selected_row_return , 8 ).text() answer = download.downloadStop(gid) if answer == 'None': notifySend("Aria2 did not respond!","Try agian!" , 10000 , 'critical' ) else: self.statusbar.showMessage("Please select an item first!") def pauseButtonPressed(self,button): selected_row_return = self.selectedRow() if selected_row_return != None: gid = self.download_table.item(selected_row_return , 8 ).text() answer = download.downloadPause(gid) if answer == 'None': notifySend("Aria2 did not respond!" , "Try agian!" , 10000 , 'critical' ) else: self.statusbar.showMessage("Please select an item first!") sleep(1) def removeButtonPressed(self,button): self.removeAction.setEnabled(False) selected_row_return = self.selectedRow() if selected_row_return != None: gid = self.download_table.item(selected_row_return , 8 ).text() try: file_name = self.download_table.item(selected_row_return , 0).text() except: file_name = None sleep(0.5) self.download_table.removeRow(selected_row_return) #remove gid of download from download list file f = Open(download_list_file) download_list_file_lines = f.readlines() f.close() f = Open(download_list_file , "w") for i in download_list_file_lines: if i.strip() != gid: f.writelines(i.strip() + "\n") f.close() #remove gid of download from active download list file f = Open(download_list_file_active) download_list_file_active_lines = f.readlines() f.close() f = Open(download_list_file_active , "w") for i in download_list_file_active_lines: if i.strip() != gid: f.writelines(i.strip() + "\n") f.close() #remove download_info_file download_info_file = download_info_folder + "/" + gid f = Open(download_info_file) f.close() f.remove() #remove file of download form download temp folder if file_name != None : file_name_path = temp_download_folder + "/" + str(file_name) os.system('rm "' + str(file_name_path) +'"') file_name_aria = file_name_path + str('.aria2') os.system('rm "' + str(file_name_aria) +'"') else: self.statusbar.showMessage("Please select an item first!") self.selectedRow() def propertiesButtonPressed(self,button): selected_row_return = self.selectedRow() if selected_row_return != None : add_link_dictionary_str = self.download_table.item(selected_row_return , 9).text() add_link_dictionary = ast.literal_eval(add_link_dictionary_str) gid = self.download_table.item(selected_row_return , 8 ).text() propertieswindow = PropertiesWindow(self.propertiesCallback ,gid) self.propertieswindows_list.append(propertieswindow) self.propertieswindows_list[len(self.propertieswindows_list) - 1].show() def propertiesCallback(self,add_link_dictionary , gid ): download_info_file = download_info_folder + "/" + gid f = Open(download_info_file) download_info_file_lines = f.readlines() f.close() f = Open(download_info_file , "w") for i in range(10): if i == 9 : f.writelines(str(add_link_dictionary) + "\n") else: f.writelines(download_info_file_lines[i].strip() + "\n") f.close() def progressButtonPressed(self,button): selected_row_return = self.selectedRow() if selected_row_return != None: gid = self.download_table.item(selected_row_return , 8 ).text() member_number = self.progress_window_list_dict[gid] if self.progress_window_list[member_number].isVisible() == False: self.progress_window_list[member_number].show() else : self.progress_window_list[member_number].hide() def progressBarOpen(self,gid): progress_window = ProgressWindow(gid) self.progress_window_list.append(progress_window) member_number = len(self.progress_window_list) - 1 self.progress_window_list_dict[gid] = member_number self.progress_window_list[member_number].show() #close event def closeEvent(self, event): self.hide() self.system_tray_icon.hide() download.shutDown() sleep(0.5) global shutdown_notification shutdown_notification = 1 while shutdown_notification != 2: sleep (0.1) QCoreApplication.instance().closeAllWindows() for qthread in self.threadPool : try: qthread.exit(0) sleep(0.1) answer = qthread.isRunning() print(answer) except: print("not quit") QCoreApplication.instance().quit print("Persepolis Closed") def systemTrayPressed(self,click): if click == 3 : self.minMaxTray(click) def minMaxTray(self,menu): if self.isVisible() == False: self.minimizeAction.setText('Minimize to system tray') self.minimizeAction.setIcon(QIcon(icons + 'minimize')) self.show() else : self.hide() self.minimizeAction.setText('Show main Window') self.minimizeAction.setIcon(QIcon(icons + 'window')) def stopAllDownloads(self,menu): active_gids = [] for i in range(self.download_table.rowCount()): try: row_status = self.download_table.item(i , 1).text() if row_status == 'downloading' or row_status == 'paused' or row_status == 'waiting': row_gid = self.download_table.item(i , 8).text() active_gids.append(row_gid) except : pass for gid in active_gids: answer = download.downloadStop(gid) if answer == 'None': notifySend("Aria2 did not respond!" , "Try agian!" , 10000 , 'critical' ) sleep(0.3) def pauseAllDownloads(self,menu): #get active gid of downloads from aria active_gids = download.activeDownloads() #check that if gid is in download_list_file_active f = Open(download_list_file_active) download_list_file_active_lines = f.readlines() f.close() for i in range(len(download_list_file_active_lines)): download_list_file_active_lines[i] = download_list_file_active_lines[i].strip() for gid in active_gids : if gid in download_list_file_active_lines : answer = download.downloadPause(gid) if answer == 'None': notifySend("Aria2 did not respond!" , "Try agian!" , 10000 , 'critical' ) sleep(0.3) def openPreferences(self,menu): self.preferenceswindow = PreferencesWindow() self.preferenceswindow.show() def openAbout(self,menu): self.about_window = AboutWindow() self.about_window.show()
class TreeMainControl(QObject): """Class to handle all global controls. Provides methods for all controls and stores local control objects. """ def __init__(self, pathObjects, parent=None): """Initialize the main tree controls Arguments: pathObjects -- a list of file objects to open parent -- the parent QObject if given """ super().__init__(parent) self.localControls = [] self.activeControl = None self.trayIcon = None self.isTrayMinimized = False self.configDialog = None self.sortDialog = None self.numberingDialog = None self.findTextDialog = None self.findConditionDialog = None self.findReplaceDialog = None self.filterTextDialog = None self.filterConditionDialog = None self.basicHelpView = None self.passwords = {} globalref.mainControl = self self.allActions = {} try: # check for existing TreeLine session socket = QLocalSocket() socket.connectToServer('treeline3-session', QIODevice.WriteOnly) # if found, send files to open and exit TreeLine if socket.waitForConnected(1000): socket.write( bytes(repr([str(path) for path in pathObjects]), 'utf-8')) if socket.waitForBytesWritten(1000): socket.close() sys.exit(0) # start local server to listen for attempt to start new session self.serverSocket = QLocalServer() self.serverSocket.listen('treeline3-session') self.serverSocket.newConnection.connect(self.getSocket) except AttributeError: print(_('Warning: Could not create local socket')) mainVersion = '.'.join(__version__.split('.')[:2]) globalref.genOptions = options.Options('general', 'TreeLine', mainVersion, 'bellz') optiondefaults.setGenOptionDefaults(globalref.genOptions) globalref.miscOptions = options.Options('misc') optiondefaults.setMiscOptionDefaults(globalref.miscOptions) globalref.histOptions = options.Options('history') optiondefaults.setHistOptionDefaults(globalref.histOptions) globalref.toolbarOptions = options.Options('toolbar') optiondefaults.setToolbarOptionDefaults(globalref.toolbarOptions) globalref.keyboardOptions = options.Options('keyboard') optiondefaults.setKeyboardOptionDefaults(globalref.keyboardOptions) try: globalref.genOptions.readFile() globalref.miscOptions.readFile() globalref.histOptions.readFile() globalref.toolbarOptions.readFile() globalref.keyboardOptions.readFile() except IOError: errorDir = options.Options.basePath if not errorDir: errorDir = _('missing directory') QMessageBox.warning( None, 'TreeLine', _('Error - could not write config file to {}').format( errorDir)) options.Options.basePath = None iconPathList = self.findResourcePaths('icons', iconPath) globalref.toolIcons = icondict.IconDict( [path / 'toolbar' for path in iconPathList], ['', '32x32', '16x16']) globalref.toolIcons.loadAllIcons() windowIcon = globalref.toolIcons.getIcon('treelogo') if windowIcon: QApplication.setWindowIcon(windowIcon) globalref.treeIcons = icondict.IconDict(iconPathList, ['', 'tree']) icon = globalref.treeIcons.getIcon('default') qApp.setStyle(QStyleFactory.create('Fusion')) setThemeColors() self.recentFiles = recentfiles.RecentFileList() if globalref.genOptions['AutoFileOpen'] and not pathObjects: recentPath = self.recentFiles.firstPath() if recentPath: pathObjects = [recentPath] self.setupActions() self.systemFont = QApplication.font() self.updateAppFont() if globalref.genOptions['MinToSysTray']: self.createTrayIcon() qApp.focusChanged.connect(self.updateActionsAvail) if pathObjects: for pathObj in pathObjects: self.openFile(pathObj, True) else: self.createLocalControl() def getSocket(self): """Open a socket from an attempt to open a second Treeline instance. Opens the file (or raise and focus if open) in this instance. """ socket = self.serverSocket.nextPendingConnection() if socket and socket.waitForReadyRead(1000): data = str(socket.readAll(), 'utf-8') try: paths = ast.literal_eval(data) if paths: for path in paths: self.openFile(pathlib.Path(path), True) else: self.activeControl.activeWindow.activateAndRaise() except (SyntaxError, ValueError, TypeError): pass def findResourcePaths(self, resourceName, preferredPath=''): """Return list of potential non-empty pathlib objects for the resource. List includes preferred, module and user option paths. Arguments: resourceName -- the typical name of the resource directory preferredPath -- add this as the second path if given """ modPath = pathlib.Path(sys.path[0]).resolve() if modPath.is_file(): modPath = modPath.parent # for frozen binary pathList = [modPath / '..' / resourceName, modPath / resourceName] if options.Options.basePath: basePath = pathlib.Path(options.Options.basePath) pathList.insert(0, basePath / resourceName) if preferredPath: pathList.insert(1, pathlib.Path(preferredPath)) return [ path.resolve() for path in pathList if path.is_dir() and list(path.iterdir()) ] def findResourceFile(self, fileName, resourceName, preferredPath=''): """Return a path object for a resource file. Add a language code before the extension if it exists. Arguments: fileName -- the name of the file to find resourceName -- the typical name of the resource directory preferredPath -- search this path first if given """ fileList = [fileName] if globalref.lang and globalref.lang != 'C': fileList[0:0] = [ fileName.replace('.', '_{0}.'.format(globalref.lang)), fileName.replace('.', '_{0}.'.format(globalref.lang[:2])) ] for fileName in fileList: for path in self.findResourcePaths(resourceName, preferredPath): if (path / fileName).is_file(): return path / fileName return None def defaultPathObj(self, dirOnly=False): """Return a reasonable default file path object. Used for open, save-as, import and export. Arguments: dirOnly -- if True, do not include basename of file """ pathObj = None if self.activeControl: pathObj = self.activeControl.filePathObj if not pathObj: pathObj = self.recentFiles.firstDir() if not pathObj: pathObj = pathlib.Path.home() if dirOnly: pathObj = pathObj.parent return pathObj def openFile(self, pathObj, forceNewWindow=False, checkModified=False, importOnFail=True): """Open the file given by path if not already open. If already open in a different window, focus and raise the window. Arguments: pathObj -- the path object to read forceNewWindow -- if True, use a new window regardless of option checkModified -- if True & not new win, prompt if file modified importOnFail -- if True, prompts for import on non-TreeLine files """ match = [ control for control in self.localControls if pathObj == control.filePathObj ] if match and self.activeControl not in match: control = match[0] control.activeWindow.activateAndRaise() self.updateLocalControlRef(control) return if checkModified and not (forceNewWindow or globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): return if not self.checkAutoSave(pathObj): if not self.localControls: self.createLocalControl() return QApplication.setOverrideCursor(Qt.WaitCursor) try: self.createLocalControl(pathObj, None, forceNewWindow) self.recentFiles.addItem(pathObj) if not (globalref.genOptions['SaveTreeStates'] and self.recentFiles.retrieveTreeState(self.activeControl)): self.activeControl.expandRootNodes() self.activeControl.selectRootSpot() QApplication.restoreOverrideCursor() except IOError: QApplication.restoreOverrideCursor() QMessageBox.warning( QApplication.activeWindow(), 'TreeLine', _('Error - could not read file {0}').format(pathObj)) self.recentFiles.removeItem(pathObj) except (ValueError, KeyError, TypeError): fileObj = pathObj.open('rb') fileObj, encrypted = self.decryptFile(fileObj) if not fileObj: if not self.localControls: self.createLocalControl() QApplication.restoreOverrideCursor() return fileObj, compressed = self.decompressFile(fileObj) if compressed or encrypted: try: textFileObj = io.TextIOWrapper(fileObj, encoding='utf-8') self.createLocalControl(textFileObj, None, forceNewWindow) fileObj.close() textFileObj.close() self.recentFiles.addItem(pathObj) if not (globalref.genOptions['SaveTreeStates'] and self.recentFiles.retrieveTreeState( self.activeControl)): self.activeControl.expandRootNodes() self.activeControl.selectRootSpot() self.activeControl.compressed = compressed self.activeControl.encrypted = encrypted QApplication.restoreOverrideCursor() return except (ValueError, KeyError, TypeError): pass fileObj.close() importControl = imports.ImportControl(pathObj) structure = importControl.importOldTreeLine() if structure: self.createLocalControl(pathObj, structure, forceNewWindow) self.activeControl.printData.readData( importControl.treeLineRootAttrib) self.recentFiles.addItem(pathObj) self.activeControl.expandRootNodes() self.activeControl.imported = True QApplication.restoreOverrideCursor() return QApplication.restoreOverrideCursor() if importOnFail: importControl = imports.ImportControl(pathObj) structure = importControl.interactiveImport(True) if structure: self.createLocalControl(pathObj, structure, forceNewWindow) self.activeControl.imported = True return else: QMessageBox.warning( QApplication.activeWindow(), 'TreeLine', _('Error - invalid TreeLine file {0}').format(pathObj)) self.recentFiles.removeItem(pathObj) if not self.localControls: self.createLocalControl() def decryptFile(self, fileObj): """Check for encryption and decrypt the fileObj if needed. Return a tuple of the file object and True if it was encrypted. Return None for the file object if the user cancels. Arguments: fileObj -- the file object to check and decrypt """ if fileObj.read(len(encryptPrefix)) != encryptPrefix: fileObj.seek(0) return (fileObj, False) while True: pathObj = pathlib.Path(fileObj.name) password = self.passwords.get(pathObj, '') if not password: QApplication.restoreOverrideCursor() dialog = miscdialogs.PasswordDialog( False, pathObj.name, QApplication.activeWindow()) if dialog.exec_() != QDialog.Accepted: fileObj.close() return (None, True) QApplication.setOverrideCursor(Qt.WaitCursor) password = dialog.password if miscdialogs.PasswordDialog.remember: self.passwords[pathObj] = password try: text = p3.p3_decrypt(fileObj.read(), password.encode()) fileIO = io.BytesIO(text) fileIO.name = fileObj.name fileObj.close() return (fileIO, True) except p3.CryptError: try: del self.passwords[pathObj] except KeyError: pass def decompressFile(self, fileObj): """Check for compression and decompress the fileObj if needed. Return a tuple of the file object and True if it was compressed. Arguments: fileObj -- the file object to check and decompress """ prefix = fileObj.read(2) fileObj.seek(0) if prefix != b'\037\213': return (fileObj, False) try: newFileObj = gzip.GzipFile(fileobj=fileObj) except zlib.error: return (fileObj, False) newFileObj.name = fileObj.name return (newFileObj, True) def checkAutoSave(self, pathObj): """Check for presence of auto save file & prompt user. Return True if OK to contimue, False if aborting or already loaded. Arguments: pathObj -- the base path object to search for a backup """ if not globalref.genOptions['AutoSaveMinutes']: return True basePath = pathObj pathObj = pathlib.Path(str(pathObj) + '~') if not pathObj.is_file(): return True msgBox = QMessageBox( QMessageBox.Information, 'TreeLine', _('Backup file "{}" exists.\nA previous ' 'session may have crashed').format(pathObj), QMessageBox.NoButton, QApplication.activeWindow()) restoreButton = msgBox.addButton(_('&Restore Backup'), QMessageBox.ApplyRole) deleteButton = msgBox.addButton(_('&Delete Backup'), QMessageBox.DestructiveRole) cancelButton = msgBox.addButton(_('&Cancel File Open'), QMessageBox.RejectRole) msgBox.exec_() if msgBox.clickedButton() == restoreButton: self.openFile(pathObj) if self.activeControl.filePathObj != pathObj: return False try: basePath.unlink() pathObj.rename(basePath) except OSError: QMessageBox.warning( QApplication.activeWindow(), 'TreeLine', _('Error - could not rename "{0}" to "{1}"').format( pathObj, basePath)) return False self.activeControl.filePathObj = basePath self.activeControl.updateWindowCaptions() self.recentFiles.removeItem(pathObj) self.recentFiles.addItem(basePath) return False elif msgBox.clickedButton() == deleteButton: try: pathObj.unlink() except OSError: QMessageBox.warning( QApplication.activeWindow(), 'TreeLine', _('Error - could not remove backup file {}').format( pathObj)) else: # cancel button return False return True def createLocalControl(self, pathObj=None, treeStruct=None, forceNewWindow=False): """Create a new local control object and add it to the list. Use an imported structure if given or open the file if path is given. Arguments: pathObj -- the path object or file object for the control to open treeStruct -- the imported structure to use forceNewWindow -- if True, use a new window regardless of option """ localControl = treelocalcontrol.TreeLocalControl( self.allActions, pathObj, treeStruct, forceNewWindow) localControl.controlActivated.connect(self.updateLocalControlRef) localControl.controlClosed.connect(self.removeLocalControlRef) self.localControls.append(localControl) self.updateLocalControlRef(localControl) localControl.updateRightViews() localControl.updateCommandsAvail() def updateLocalControlRef(self, localControl): """Set the given local control as active. Called by signal from a window becoming active. Also updates non-modal dialogs. Arguments: localControl -- the new active local control """ if localControl != self.activeControl: self.activeControl = localControl if self.configDialog and self.configDialog.isVisible(): self.configDialog.setRefs(self.activeControl) def removeLocalControlRef(self, localControl): """Remove ref to local control based on a closing signal. Also do application exit clean ups if last control closing. Arguments: localControl -- the local control that is closing """ self.localControls.remove(localControl) if globalref.genOptions['SaveTreeStates']: self.recentFiles.saveTreeState(localControl) if not self.localControls: if globalref.genOptions['SaveWindowGeom']: localControl.windowList[0].saveWindowGeom() else: localControl.windowList[0].resetWindowGeom() self.recentFiles.writeItems() localControl.windowList[0].saveToolbarPosition() globalref.histOptions.writeFile() if self.trayIcon: self.trayIcon.hide() localControl.deleteLater() def createTrayIcon(self): """Create a new system tray icon if not already created. """ if QSystemTrayIcon.isSystemTrayAvailable: if not self.trayIcon: self.trayIcon = QSystemTrayIcon(qApp.windowIcon(), qApp) self.trayIcon.activated.connect(self.toggleTrayShow) self.trayIcon.show() def trayMinimize(self): """Minimize to tray based on window minimize signal. """ if self.trayIcon and QSystemTrayIcon.isSystemTrayAvailable: # skip minimize to tray if not all windows minimized for control in self.localControls: for window in control.windowList: if not window.isMinimized(): return for control in self.localControls: for window in control.windowList: window.hide() self.isTrayMinimized = True def toggleTrayShow(self): """Toggle show and hide application based on system tray icon click. """ if self.isTrayMinimized: for control in self.localControls: for window in control.windowList: window.show() window.showNormal() self.activeControl.activeWindow.treeView.setFocus() else: for control in self.localControls: for window in control.windowList: window.hide() self.isTrayMinimized = not self.isTrayMinimized def updateConfigDialog(self): """Update the config dialog for changes if it exists. """ if self.configDialog: self.configDialog.reset() def currentStatusBar(self): """Return the status bar from the current main window. """ return self.activeControl.activeWindow.statusBar() def windowActions(self): """Return a list of window menu actions from each local control. """ actions = [] for control in self.localControls: actions.extend( control.windowActions( len(actions) + 1, control == self.activeControl)) return actions def updateActionsAvail(self, oldWidget, newWidget): """Update command availability based on focus changes. Arguments: oldWidget -- the previously focused widget newWidget -- the newly focused widget """ self.allActions['FormatSelectAll'].setEnabled( hasattr(newWidget, 'selectAll') and not hasattr(newWidget, 'editTriggers')) def setupActions(self): """Add the actions for contols at the global level. """ fileNewAct = QAction(_('&New...'), self, toolTip=_('New File'), statusTip=_('Start a new file')) fileNewAct.triggered.connect(self.fileNew) self.allActions['FileNew'] = fileNewAct fileOpenAct = QAction(_('&Open...'), self, toolTip=_('Open File'), statusTip=_('Open a file from disk')) fileOpenAct.triggered.connect(self.fileOpen) self.allActions['FileOpen'] = fileOpenAct fileSampleAct = QAction(_('Open Sa&mple...'), self, toolTip=_('Open Sample'), statusTip=_('Open a sample file')) fileSampleAct.triggered.connect(self.fileOpenSample) self.allActions['FileOpenSample'] = fileSampleAct fileImportAct = QAction(_('&Import...'), self, statusTip=_('Open a non-TreeLine file')) fileImportAct.triggered.connect(self.fileImport) self.allActions['FileImport'] = fileImportAct fileQuitAct = QAction(_('&Quit'), self, statusTip=_('Exit the application')) fileQuitAct.triggered.connect(self.fileQuit) self.allActions['FileQuit'] = fileQuitAct dataConfigAct = QAction( _('&Configure Data Types...'), self, statusTip=_('Modify data types, fields & output lines'), checkable=True) dataConfigAct.triggered.connect(self.dataConfigDialog) self.allActions['DataConfigType'] = dataConfigAct dataVisualConfigAct = QAction( _('Show C&onfiguration Structure...'), self, statusTip=_('Show read-only visualization of type structure')) dataVisualConfigAct.triggered.connect(self.dataVisualConfig) self.allActions['DataVisualConfig'] = dataVisualConfigAct dataSortAct = QAction(_('Sor&t Nodes...'), self, statusTip=_('Define node sort operations'), checkable=True) dataSortAct.triggered.connect(self.dataSortDialog) self.allActions['DataSortNodes'] = dataSortAct dataNumberingAct = QAction(_('Update &Numbering...'), self, statusTip=_('Update node numbering fields'), checkable=True) dataNumberingAct.triggered.connect(self.dataNumberingDialog) self.allActions['DataNumbering'] = dataNumberingAct toolsFindTextAct = QAction( _('&Find Text...'), self, statusTip=_('Find text in node titles & data'), checkable=True) toolsFindTextAct.triggered.connect(self.toolsFindTextDialog) self.allActions['ToolsFindText'] = toolsFindTextAct toolsFindConditionAct = QAction( _('&Conditional Find...'), self, statusTip=_('Use field conditions to find nodes'), checkable=True) toolsFindConditionAct.triggered.connect(self.toolsFindConditionDialog) self.allActions['ToolsFindCondition'] = toolsFindConditionAct toolsFindReplaceAct = QAction( _('Find and &Replace...'), self, statusTip=_('Replace text strings in node data'), checkable=True) toolsFindReplaceAct.triggered.connect(self.toolsFindReplaceDialog) self.allActions['ToolsFindReplace'] = toolsFindReplaceAct toolsFilterTextAct = QAction( _('&Text Filter...'), self, statusTip=_('Filter nodes to only show text matches'), checkable=True) toolsFilterTextAct.triggered.connect(self.toolsFilterTextDialog) self.allActions['ToolsFilterText'] = toolsFilterTextAct toolsFilterConditionAct = QAction( _('C&onditional Filter...'), self, statusTip=_('Use field conditions to filter nodes'), checkable=True) toolsFilterConditionAct.triggered.connect( self.toolsFilterConditionDialog) self.allActions['ToolsFilterCondition'] = toolsFilterConditionAct toolsGenOptionsAct = QAction( _('&General Options...'), self, statusTip=_('Set user preferences for all files')) toolsGenOptionsAct.triggered.connect(self.toolsGenOptions) self.allActions['ToolsGenOptions'] = toolsGenOptionsAct toolsShortcutAct = QAction(_('Set &Keyboard Shortcuts...'), self, statusTip=_('Customize keyboard commands')) toolsShortcutAct.triggered.connect(self.toolsCustomShortcuts) self.allActions['ToolsShortcuts'] = toolsShortcutAct toolsToolbarAct = QAction(_('C&ustomize Toolbars...'), self, statusTip=_('Customize toolbar buttons')) toolsToolbarAct.triggered.connect(self.toolsCustomToolbars) self.allActions['ToolsToolbars'] = toolsToolbarAct toolsFontsAct = QAction( _('Customize Fo&nts...'), self, statusTip=_('Customize fonts in various views')) toolsFontsAct.triggered.connect(self.toolsCustomFonts) self.allActions['ToolsFonts'] = toolsFontsAct helpBasicAct = QAction(_('&Basic Usage...'), self, statusTip=_('Display basic usage instructions')) helpBasicAct.triggered.connect(self.helpViewBasic) self.allActions['HelpBasic'] = helpBasicAct helpFullAct = QAction( _('&Full Documentation...'), self, statusTip=_('Open a TreeLine file with full documentation')) helpFullAct.triggered.connect(self.helpViewFull) self.allActions['HelpFull'] = helpFullAct helpAboutAct = QAction( _('&About TreeLine...'), self, statusTip=_('Display version info about this program')) helpAboutAct.triggered.connect(self.helpAbout) self.allActions['HelpAbout'] = helpAboutAct formatSelectAllAct = QAction( _('&Select All'), self, statusTip=_('Select all text in an editor')) formatSelectAllAct.setEnabled(False) formatSelectAllAct.triggered.connect(self.formatSelectAll) self.allActions['FormatSelectAll'] = formatSelectAllAct helpAboutAct = QAction( _('&About TreeLine...'), self, statusTip=_('Display version info about this program')) helpAboutAct.triggered.connect(self.helpAbout) self.allActions['HelpAbout'] = helpAboutAct for name, action in self.allActions.items(): icon = globalref.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) key = globalref.keyboardOptions[name] if not key.isEmpty(): action.setShortcut(key) def fileNew(self): """Start a new blank file. """ if (globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): searchPaths = self.findResourcePaths('templates', templatePath) if searchPaths: dialog = miscdialogs.TemplateFileDialog( _('New File'), _('&Select Template'), searchPaths) if dialog.exec_() == QDialog.Accepted: self.createLocalControl(dialog.selectedPath()) self.activeControl.filePathObj = None self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() else: self.createLocalControl() self.activeControl.selectRootSpot() def fileOpen(self): """Prompt for a filename and open it. """ if (globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): filters = ';;'.join((globalref.fileFilters['trlnopen'], globalref.fileFilters['all'])) fileName, selFilter = QFileDialog.getOpenFileName( QApplication.activeWindow(), _('TreeLine - Open File'), str(self.defaultPathObj(True)), filters) if fileName: self.openFile(pathlib.Path(fileName)) def fileOpenSample(self): """Open a sample file from the doc directories. """ if (globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): searchPaths = self.findResourcePaths('samples', samplePath) dialog = miscdialogs.TemplateFileDialog(_('Open Sample File'), _('&Select Sample'), searchPaths, False) if dialog.exec_() == QDialog.Accepted: self.createLocalControl(dialog.selectedPath()) name = dialog.selectedName() + '.trln' self.activeControl.filePathObj = pathlib.Path(name) self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() self.activeControl.imported = True def fileImport(self): """Prompt for an import type, then a file to import. """ importControl = imports.ImportControl() structure = importControl.interactiveImport() if structure: self.createLocalControl(importControl.pathObj, structure) if importControl.treeLineRootAttrib: self.activeControl.printData.readData( importControl.treeLineRootAttrib) self.activeControl.imported = True def fileQuit(self): """Close all windows to exit the applications. """ for control in self.localControls[:]: control.closeWindows() def dataConfigDialog(self, show): """Show or hide the non-modal data config dialog. Arguments: show -- true if dialog should be shown, false to hide it """ if show: if not self.configDialog: self.configDialog = configdialog.ConfigDialog() dataConfigAct = self.allActions['DataConfigType'] self.configDialog.dialogShown.connect(dataConfigAct.setChecked) self.configDialog.setRefs(self.activeControl, True) self.configDialog.show() else: self.configDialog.close() def dataVisualConfig(self): """Show a TreeLine file to visualize the config structure. """ structure = ( self.activeControl.structure.treeFormats.visualConfigStructure( str(self.activeControl.filePathObj))) self.createLocalControl(treeStruct=structure, forceNewWindow=True) self.activeControl.filePathObj = pathlib.Path('structure.trln') self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() self.activeControl.imported = True win = self.activeControl.activeWindow win.rightTabs.setCurrentWidget(win.outputSplitter) def dataSortDialog(self, show): """Show or hide the non-modal data sort nodes dialog. Arguments: show -- true if dialog should be shown, false to hide it """ if show: if not self.sortDialog: self.sortDialog = miscdialogs.SortDialog() dataSortAct = self.allActions['DataSortNodes'] self.sortDialog.dialogShown.connect(dataSortAct.setChecked) self.sortDialog.show() else: self.sortDialog.close() def dataNumberingDialog(self, show): """Show or hide the non-modal update node numbering dialog. Arguments: show -- true if dialog should be shown, false to hide it """ if show: if not self.numberingDialog: self.numberingDialog = miscdialogs.NumberingDialog() dataNumberingAct = self.allActions['DataNumbering'] self.numberingDialog.dialogShown.connect( dataNumberingAct.setChecked) self.numberingDialog.show() if not self.numberingDialog.checkForNumberingFields(): self.numberingDialog.close() else: self.numberingDialog.close() def toolsFindTextDialog(self, show): """Show or hide the non-modal find text dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.findTextDialog: self.findTextDialog = miscdialogs.FindFilterDialog() toolsFindTextAct = self.allActions['ToolsFindText'] self.findTextDialog.dialogShown.connect( toolsFindTextAct.setChecked) self.findTextDialog.selectAllText() self.findTextDialog.show() else: self.findTextDialog.close() def toolsFindConditionDialog(self, show): """Show or hide the non-modal conditional find dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.findConditionDialog: dialogType = conditional.FindDialogType.findDialog self.findConditionDialog = (conditional.ConditionDialog( dialogType, _('Conditional Find'))) toolsFindConditionAct = self.allActions['ToolsFindCondition'] (self.findConditionDialog.dialogShown.connect( toolsFindConditionAct.setChecked)) else: self.findConditionDialog.loadTypeNames() self.findConditionDialog.show() else: self.findConditionDialog.close() def toolsFindReplaceDialog(self, show): """Show or hide the non-modal find and replace text dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.findReplaceDialog: self.findReplaceDialog = miscdialogs.FindReplaceDialog() toolsFindReplaceAct = self.allActions['ToolsFindReplace'] self.findReplaceDialog.dialogShown.connect( toolsFindReplaceAct.setChecked) else: self.findReplaceDialog.loadTypeNames() self.findReplaceDialog.show() else: self.findReplaceDialog.close() def toolsFilterTextDialog(self, show): """Show or hide the non-modal filter text dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.filterTextDialog: self.filterTextDialog = miscdialogs.FindFilterDialog(True) toolsFilterTextAct = self.allActions['ToolsFilterText'] self.filterTextDialog.dialogShown.connect( toolsFilterTextAct.setChecked) self.filterTextDialog.selectAllText() self.filterTextDialog.show() else: self.filterTextDialog.close() def toolsFilterConditionDialog(self, show): """Show or hide the non-modal conditional filter dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.filterConditionDialog: dialogType = conditional.FindDialogType.filterDialog self.filterConditionDialog = (conditional.ConditionDialog( dialogType, _('Conditional Filter'))) toolsFilterConditionAct = ( self.allActions['ToolsFilterCondition']) (self.filterConditionDialog.dialogShown.connect( toolsFilterConditionAct.setChecked)) else: self.filterConditionDialog.loadTypeNames() self.filterConditionDialog.show() else: self.filterConditionDialog.close() def toolsGenOptions(self): """Set general user preferences for all files. """ oldAutoSaveMinutes = globalref.genOptions['AutoSaveMinutes'] oldColorTheme = globalref.genOptions['ColorTheme'] dialog = options.OptionDialog(globalref.genOptions, QApplication.activeWindow()) dialog.setWindowTitle(_('General Options')) if (dialog.exec_() == QDialog.Accepted and globalref.genOptions.modified): globalref.genOptions.writeFile() self.recentFiles.updateOptions() if globalref.genOptions['MinToSysTray']: self.createTrayIcon() elif self.trayIcon: self.trayIcon.hide() autoSaveMinutes = globalref.genOptions['AutoSaveMinutes'] for control in self.localControls: for window in control.windowList: window.updateWinGenOptions() control.structure.undoList.setNumLevels() control.updateAll(False) if autoSaveMinutes != oldAutoSaveMinutes: control.resetAutoSave() if globalref.genOptions['ColorTheme'] != oldColorTheme: QMessageBox.warning( QApplication.activeWindow(), 'TreeLine', _('Application must be restarted for ' 'color theme changes to take effect')) def toolsCustomShortcuts(self): """Show dialog to customize keyboard commands. """ actions = self.activeControl.activeWindow.allActions dialog = miscdialogs.CustomShortcutsDialog(actions, QApplication.activeWindow()) dialog.exec_() def toolsCustomToolbars(self): """Show dialog to customize toolbar buttons. """ actions = self.activeControl.activeWindow.allActions dialog = miscdialogs.CustomToolbarDialog(actions, self.updateToolbars, QApplication.activeWindow()) dialog.exec_() def updateToolbars(self): """Update toolbars after changes in custom toolbar dialog. """ for control in self.localControls: for window in control.windowList: window.setupToolbars() def toolsCustomFonts(self): """Show dialog to customize fonts in various views. """ dialog = miscdialogs.CustomFontDialog(QApplication.activeWindow()) dialog.updateRequired.connect(self.updateCustomFonts) dialog.exec_() def updateCustomFonts(self): """Update fonts in all windows based on a dialog signal. """ self.updateAppFont() for control in self.localControls: for window in control.windowList: window.updateFonts() control.printData.setDefaultFont() for control in self.localControls: control.updateAll(False) def updateAppFont(self): """Update application default font from settings. """ appFont = QFont(self.systemFont) appFontName = globalref.miscOptions['AppFont'] if appFontName: appFont.fromString(appFontName) QApplication.setFont(appFont) def formatSelectAll(self): """Select all text in any currently focused editor. """ try: QApplication.focusWidget().selectAll() except AttributeError: pass def helpViewBasic(self): """Display basic usage instructions. """ if not self.basicHelpView: path = self.findResourceFile('basichelp.html', 'doc', docPath) if not path: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - basic help file not found')) return self.basicHelpView = helpview.HelpView(path, _('TreeLine Basic Usage'), globalref.toolIcons) self.basicHelpView.show() def helpViewFull(self): """Open a TreeLine file with full documentation. """ path = self.findResourceFile('documentation.trln', 'doc', docPath) if not path: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - documentation file not found')) return self.createLocalControl(path, forceNewWindow=True) self.activeControl.filePathObj = pathlib.Path('documentation.trln') self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() self.activeControl.imported = True win = self.activeControl.activeWindow win.rightTabs.setCurrentWidget(win.outputSplitter) def helpAbout(self): """ Display version info about this program. """ pyVersion = '.'.join([repr(num) for num in sys.version_info[:3]]) textLines = [ _('TreeLine version {0}').format(__version__), _('written by {0}').format(__author__), '', _('Library versions:'), ' Python: {0}'.format(pyVersion), ' Qt: {0}'.format(qVersion()), ' PyQt: {0}'.format(PYQT_VERSION_STR), ' OS: {0}'.format(platform.platform()) ] dialog = miscdialogs.AboutDialog('TreeLine', textLines, QApplication.windowIcon(), QApplication.activeWindow()) dialog.exec_()
class Ui_MainWindow(QMainWindow, object): def setupUi(self, MainWindow): #载入资源文件 #载入字体 for i in font_path: if exists(settings["workingDir"]+font_path[i]): QtGui.QFontDatabase.addApplicationFont(settings["workingDir"]+font_path[i]) else: log("字体不存在:" + font_path[i] + "(" + settings["lang"] + ")", level="error") font_path[i] = "Arial" self.setObjectName("MainWindow") self.resize(400, 300) self.setFixedSize(MainWindow.width(), MainWindow.height()) self.setWindowFlags(Qt.FramelessWindowHint) self.setWindowOpacity(settings["WindowOpacity"]) self.setAttribute(Qt.WA_TranslucentBackground) self.setWindowFlags(QtCore.Qt.FramelessWindowHint|QtCore.Qt.WindowStaysOnTopHint) #widget if exists(settings["workingDir"]+settings["backgroundPath"]): backgroundPath=settings["workingDir"]+settings["backgroundPath"] log("加载背景图片:" + backgroundPath) else: backgroundPath = ":/black.png" log("未找到背景图片") self.RoundWidget = QWidget(parent=MainWindow) self.RoundWidget.setGeometry(MainWindow.geometry()) self.RoundWidget.setStyleSheet("QWidget{border-image: url("+backgroundPath.replace(sep,"/")+r") 100% 100% round;border-radius:15px;}") self.RoundWidget.show() #拖动功能 self._startPos,self._endPos=QPoint(0,0),QPoint(0,0) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") QApplication.setQuitOnLastWindowClosed(False) self.BtnSearch = QtWidgets.QPushButton(self.centralwidget) self.BtnSearch.setGeometry(QtCore.QRect(350, 10, 40, 40)) self.BtnSearch.setText("") self.BtnSearch.setIcon(QIcon(":/AuraProII.ico")) self.BtnSearch.setIconSize(QtCore.QSize(40, 40)) self.BtnSearch.setStyleSheet(r"QPushButton{border:1px solid;padding:5px;border-radius:10px;}") self.BtnSearch.setObjectName("BtnSearch") QShortcut(QKeySequence(settings["SearchShortCut"]), self.BtnSearch).activated.connect(self.BtnSearchClickEvent) self.BtnSearch.setToolTip("快捷键:"+settings["SearchShortCut"]) self.BtnSearch.clicked.connect(self.EndSearchEvent) self.BtnSearch.clicked.connect(self.BtnSearchClickEvent) self.StatusBar = QtWidgets.QStatusBar(self.centralwidget) self.StatusBar.setStyleSheet("QStatusBar{color:rgba"+settings["clStatusBar"]+";}") self.setStatusBar(self.StatusBar) self.StatusBar.showMessage("增强型奥拉 II:"+version) self.EdtName = TEdtName(self.centralwidget) self.EdtName.setGeometry(QtCore.QRect(10, 10, 340, 40)) self.EdtName.setPlaceholderText("在这里输入名字...") self.EdtName.setStyleSheet(""" TEdtName{ border:1px grey; border-style: none; border-radius:10px; padding:1px 2px; color:rgb(255,0,0); background-color:rgba(0,0,0,127) } """) self.EdtName.show() self.TrayMenu = QMenu(self) self.TrayMenu.addAction(QAction("显示/隐藏主界面", parent=self.TrayMenu,checkable=True,checked=True, triggered=self.ToggleShowHide)) self.TrayMenu.addAction(QAction("主界面置顶", parent=self.TrayMenu, checkable=True, checked=True, triggered=self.ToggleStayOnTop)) self.TrayMenu.addSeparator() self.TrayMenu.addAction(QAction("关于...", parent=self.TrayMenu, triggered=self.ShowAbout)) self.TrayMenu.addSeparator() self.TrayMenu.addAction(QAction("停用 增强型奥拉II",parent=self.TrayMenu, triggered=self.quit)) self.TrayIcon = QSystemTrayIcon(self) self.TrayIcon.setIcon(QIcon(":/AuraProII.ico")) self.TrayIcon.setToolTip(u'增强型奥拉 II:激活中') self.TrayIcon.show() self.TrayIcon.showMessage(u"增强型奥拉 II:"+version, "Fly safe o/", 0) self.TrayIcon.activated.connect(self.TrayIconClickEvent) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda:self.TrayMenu.exec_(QCursor.pos())) self.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) #窗体创建完毕 #信息列表 self.MsgEntryList = {} # MsgEntryList结构 # 每项为每个过程加入的信息 # 如,StartSearchKB后,MsgEntryList中的内容应为: # {"SearchName":{ # "Label":"正在搜索角色", # "ID":******* # } # "SearchKB":{...} # RefreshLabelList只会显示被SerializeMsgEntryList抽出的TMsgLabel对象 #用于显示信息列表的Label列表 self.LabelList = [] self.LabelList_buffer = [] self.LabelList_click_no = -1 self.LabelList_pos=0 #窗体创建完毕 log("窗体创建完毕") def retranslateUi(self, MainWindow): _t = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_t("MainWindow", "MainWindow")) self.BtnSearch.setShortcut(_t("MainWindow", "Enter")) def quit(self): """ 终止程序。 """ self.TrayIcon.hide() self.closeEvent(None) self.EndSearchEvent() log("退出") QApplication.quit() def ToggleShowHide(self): """ 显示/隐藏主界面。 """ if self.sender().isChecked(): self.show() else: self.hide() def ToggleStayOnTop(self): """ 切换窗口置顶。 """ if self.sender().isChecked(): self.setWindowFlags(Qt.FramelessWindowHint|Qt.WindowStaysOnTopHint) else: self.setWindowFlags(Qt.FramelessWindowHint) def ShowAbout(self): msgBox = QMessageBox() msgBox.setWindowTitle(u'关于...') msgBox.setIconPixmap(QPixmap(":/AuraProII.png")) msgBox.setText(u""" 增强型奥拉 II """+version+""" by 百万光年 [email protected]""") msgBox.setWindowIcon(QIcon(":/AuraProII.ico")) msgBox.exec_() #event def mouseMoveEvent(self, e): # 重写移动事件 try: self.on_events=True self._endPos = e.pos() - self._startPos self.move(self.pos() + self._endPos) except Exception as error: self._endPos = e.pos() self._startPos = e.pos() finally: self.on_events = False def mousePressEvent(self, e): self.EdtName.clearFocus() if e.button() == Qt.LeftButton: self._isTracking = True self._startPos = QPoint(e.x(), e.y()) def mouseReleaseEvent(self, e): if e.button() == Qt.LeftButton: self._isTracking = False self._startPos = None self._endPos = None def closeEvent(self, event): self.hide() def TrayIconClickEvent(self, reason): if reason == 1: self.TrayMenu.exec_(QCursor.pos()) elif reason == 2: self.TrayMenu.actions()[0].setChecked(True) self.show() def wheelEvent(self, event): if len(self.LabelList)>0: self.on_events = True ori=event.angleDelta().y()/120 if not isinstance(event,int) else event if ori > 0: #向上移 if self.LabelList[0].geometry().top() < self.EdtName.geometry().height()+self.EdtName.geometry().top(): self.LabelList_pos+=1 for i in self.LabelList: i.move(i.geometry().left(),i.geometry().top() + 10) else: #向下移 if self.LabelList[-1].geometry().top() + self.LabelList[-1].geometry().height() > self.StatusBar.geometry().top(): self.LabelList_pos-=1 for i in self.LabelList: i.move(i.geometry().left(),i.geometry().top() - 10) if not isinstance(event, int): event.accept() self.on_events = False def BtnSearchClickEvent(self): return self.StartSearchName() def EndSearchEvent(self,e=None): """ 搜索结束事件。 将传递信息的线程传来的 """ if isinstance(self.sender(), TThread): log("EndSearch:"+str(self.sender().__name__)) MutEndSearch.lock() Msg = self.sender().Msg log("Msg="+str(Msg)) if id(self.sender()) not in current_thread_set: #说明不是此次搜索的返回线程,直接丢弃 MutEndSearch.unlock() return - 1 #由SearchName返回 if "getKMList" in Msg:#完成了一轮完整的搜索流程 self.MsgEntryList = {} self.MsgEntryList.update(Msg) self.statusBar().showMessage("搜索完成") self.EdtName.setStyleSheet(""" TEdtName{ border: 2px groove white; border-radius: 3px; padding:2px 3px; color:rgb(255,0,0); background-color:rgba(0,0,0,0) } TEdtName:focus{ color:rgb(0,255,0); background-color: rgb(0,0,0) } """) elif "Error" in Msg: #SearchName返回错误 self.MsgEntryList = {} self.MsgEntryList.update(Msg) if Msg["Error"] == "getKMListError": self.MsgEntryList.update({"ErrorLabel": TMsgEntry("获取KM列表失败",style_str=MDStyleStr(color=settings["clFailed"],font_size=settings["labelFontSize"]))}) elif Msg["Error"] == "zkbError": self.MsgEntryList.update({"ErrorLabel": TMsgEntry("zkb查询失败",style_str=MDStyleStr(color=settings["clFailed"],font_size=settings["labelFontSize"]))}) elif Msg["Error"] == "esiError": self.MsgEntryList.update({"ErrorLabel": TMsgEntry("查询角色ID失败", style_str=MDStyleStr(color=settings["clFailed"], font_size=settings["labelFontSize"]))}) elif Msg["Error"] == "SearchKMError": self.MsgEntryList.update({"ErrorLabel": TMsgEntry("查询KM失败", style_str=MDStyleStr(color=settings["clFailed"], font_size=settings["labelFontSize"]))}) elif Msg["Error"] == "NoSuchCharacterError": self.MsgEntryList.update({"ErrorLabel": TMsgEntry("无此角色", style_str=MDStyleStr(color=settings["clFailed"], font_size=settings["labelFontSize"]))}) elif Msg["Error"] == "PlayerNoPVPData": self.MsgEntryList.update({"ErrorLabel": TMsgEntry("没有该角色的统计数据",style_str=MDStyleStr(color=settings["clHint"],font_size=settings["labelFontSize"]))}) elif "NameList" in Msg: #多个搜索结果命中 self.MsgEntryList.update({"MultipleHits": TMsgEntry("命中" + str(len(Msg["NameList"])) + "条搜索结果...",style_str=MDStyleStr(color=settings["clHint"],font_size=settings["labelFontSize"]))}) NameList = Msg["NameList"][:] no=0 for c in NameList: no+=1 self.MultiThreadRun(func=addName, args=(c, no)) elif "TooManyResults" in Msg: #搜索结果命中数超过ResultCountLimit self.MsgEntryList.update(Msg) self.MultiThreadRun(func=SearchName,args=(Msg["name"],-1,True)) #由addName返回 elif "addName" in Msg: #处理addName返回的情况 #addName会多并发调用EndSearchEvent,因此需要QMutex #addName会返回成对的name和characterID,每个返回都应被添加至self.MsgEntryList["addName"] if "addName" not in self.MsgEntryList: self.MsgEntryList["addName"]=[] self.MsgEntryList["addName"] += Msg["addName"] #由SearchKM返回 elif "SearchKM" in Msg: if self.LabelList_click_no != -1: #需要找到被单击的Label在self.MsgEntryList中的位置 for i in range(len(self.MsgEntryList["getKMList"])): #如果getKMList中记录的killmail_id==LabelList中记录的killmail_id if (self.MsgEntryList["getKMList"][i][0][0]==self.LabelList[self.LabelList_click_no].MsgEntry.ClickArgs[1]): self.MsgEntryList["getKMList"][i][2]=Msg break self.statusBar().showMessage("KM已获取") MutEndSearch.unlock() self.RefreshLabelList() #custom def MultiThreadRun(self, *args, **kwargs): """ 启动多线程,用于网络IO """ global thread_pool,current_thread_set t = TThread(*args, **kwargs) if not isinstance(t, TThread): return -1 t.finished.connect(self.EndSearchEvent) if id(t) not in thread_pool: thread_pool.add(t) current_thread_set.add(id(t)) else: QMessageBox.warning(self, "错误", "无法创建线程。", QMessageBox.standardButton) self.quit() return t.start() def RefreshLabelList(self, Msg=None): """ 刷新LabelList显示。 Msg(None or dict):作为回调时需要在self.MsgEntryList中添加的信息 """ #作为多线程的回调,此进程需要加锁。 global MutLabelList while not MutLabelList.tryLock(1): sleep(0.1) self.statusBar().showMessage("错误:无法刷新信息列表") ret=0 #由于MsgLabel可能正在响应事件所以不能直接deleteLater #使用double buffer for i in self.LabelList_buffer: while (i.on_events): i.on_events=False sleep(0.1) i.deleteLater() for i in self.LabelList: i.hide() self.LabelList_buffer=self.LabelList[:] self.LabelList = [] if isinstance(Msg,dict): self.MsgEntryList.update(Msg) #把MsgEntryList展平 new_label_list = Serialize(self.MsgEntryList) count=-1 for i in new_label_list: #每个i都是一个TMsgEntry count += 1 if i.enable: self.LabelList.append(TMsgLabel(m=i,no=count)) for i in self.LabelList: i.setParent(self.centralwidget) i.setOpenExternalLinks(True) i.show() i.lower() #向上滚动至之前位置 wheel_pos = self.LabelList_pos self.LabelList_pos=0 for i in range(abs(wheel_pos)): if i != -self.LabelList_pos: break self.wheelEvent(int(wheel_pos / abs(wheel_pos))) log("RefreshLabelList:"+','.join([i.text() for i in self.LabelList])) MutLabelList.unlock() return ret def StartSearchName(self,name:str=""): """ 开始一轮搜索。 """ global current_thread_set if name=="": name = self.EdtName.text() #检查这是否是一个合适的name name = name.strip(" \n\"\'“”‘’") if name == "": self.statusBar().showMessage("错误:没有输入名字") return - 1 #如果是一个zkb链接 fetch = re.search(r"https://zkillboard\.com/character/([0-9]*)/", name) if fetch != None: log("导入zkb链接"+fetch.group(0)) self.RefreshLabelList({"LoadFromzkbLink": [TMsgEntry( "导入zkb链接 " + fetch.group(0), style_str=MDStyleStr(color=settings["clHint"], font_size=settings["labelFontSize"]) ),TMsgEntry( "正在搜索...", style_str=MDStyleStr(color=settings["clHint"], font_size=settings["labelFontSize"]) )]}) self.MultiThreadRun(func=SearchName, args=(None, int(fetch.group(1)))) else: name=re.search(r"^[a-zA-Z0-9 '\-_]*$", name) if name==None: #名字中含有非法字符 self.statusBar().showMessage("错误:名字中含有非法字符") return - 1 else: name=name.group(0) self.MsgEntryList = {} current_thread_set = set() log("搜索"+name) self.RefreshLabelList({"SearchName": { "Searching": [name, TMsgEntry("正在搜索名字包含" + name + '的角色...', style_str=MDStyleStr( color=settings["clHint"], font_size=settings["labelFontSize"] ) ), ] }}) self.MultiThreadRun(func=SearchName, args=(name,)) return 0
class GalacteekApplication(QApplication): """ Galacteek application class :param bool debug: enable debugging :param str profile: application profile """ manualAvailable = pyqtSignal(str, dict) messageDisplayRequest = pyqtSignal(str, str) appImageImported = pyqtSignal(str) dbConfigured = AsyncSignal(bool) def __init__(self, debug=False, profile='main', sslverify=True, enableOrbital=False, progName=None, cmdArgs={}): QApplication.__init__(self, sys.argv) QCoreApplication.setApplicationName(GALACTEEK_NAME) self.dbConfigured.connectTo(self.onDbConfigured) self.setQuitOnLastWindowClosed(False) self._cmdArgs = cmdArgs self._debugEnabled = debug self._appProfile = profile self._loop = None self._executor = None self._ipfsClient = None self._ipfsOpMain = None self._ipfsd = None self._sslverify = sslverify self._progName = progName self._progCid = None self._system = platform.system() self._urlSchemes = {} self._shuttingDown = False self._icons = {} self._ipfsIconsCache = {} self._ipfsIconsCacheMax = 32 self.enableOrbital = enableOrbital self.orbitConnector = None self.translator = None self.mainWindow = None self.feedFollowerTask = None self.webProfiles = {} self.ipfsCtx = IPFSContext(self) self.peersTracker = peers.PeersTracker(self.ipfsCtx) self.desktopWidget = QDesktopWidget() self.desktopGeometry = self.desktopWidget.screenGeometry() self.setWindowIcon(getIcon('galacteek.png')) self.setupAsyncLoop() self.setupPaths() @property def cmdArgs(self): return self._cmdArgs @property def shuttingDown(self): return self._shuttingDown @property def offline(self): return self.cmdArgs.offline @property def system(self): return self._system @property def debugEnabled(self): return self._debugEnabled @property def ipfsIconsCacheMax(self): return self._ipfsIconsCacheMax @property def ipfsIconsCache(self): return self._ipfsIconsCache @property def progName(self): return self._progName @property def progCid(self): return self._progCid @property def sslverify(self): return self._sslverify @property def appProfile(self): return self._appProfile @property def ipfsd(self): return self._ipfsd @property def loop(self): return self._loop @property def executor(self): return self._executor @loop.setter def loop(self, newLoop): self._loop = newLoop @property def allTasks(self): return asyncio.Task.all_tasks(loop=self.loop) @property def pendingTasks(self): return [task for task in self.allTasks if not task.done()] @property def ipfsClient(self): return self._ipfsClient @ipfsClient.setter def ipfsClient(self, client): self.debug('IPFS client changed: {}'.format(client)) self._ipfsClient = client @property def ipfsOpMain(self): return self._ipfsOpMain @ipfsOpMain.setter def ipfsOpMain(self, op): """ The main IPFS operator, used by @ipfsOp """ self.debug('Main IPFS operator upgrade: ID {}'.format(op.uid)) self._ipfsOpMain = op @property def gatewayAuthority(self): params = self.getIpfsConnectionParams() return '{0}:{1}'.format(params.host, params.gatewayPort) @property def gatewayUrl(self): params = self.getIpfsConnectionParams() return params.gatewayUrl @property def dataLocation(self): return self._dataLocation @property def ipfsBinLocation(self): return self._ipfsBinLocation @property def ipfsDataLocation(self): return self._ipfsDataLocation @property def nsCacheLocation(self): return self._nsCacheLocation @property def orbitDataLocation(self): return self._orbitDataLocation def applyStyle(self, theme='default'): qssPath = ":/share/static/qss/{theme}/galacteek.qss".format( theme=theme) qssFile = QFile(qssPath) try: qssFile.open(QFile.ReadOnly) styleSheetBa = qssFile.readAll() styleSheetStr = styleSheetBa.data().decode('utf-8') self.setStyleSheet(styleSheetStr) except BaseException: # that would probably occur if the QSS is not # in the resources file.. set some default stylesheet here? pass self.gStyle = GalacteekStyle() self.setStyle(self.gStyle) def debug(self, msg): if self.debugEnabled: log.debug(msg) def initSystemTray(self): self.systemTray = QSystemTrayIcon(self) self.systemTray.setIcon(getIcon('galacteek-incandescent.png')) self.systemTray.show() self.systemTray.activated.connect(self.onSystemTrayIconClicked) systemTrayMenu = QMenu(self.mainWindow) actionShow = systemTrayMenu.addAction('Show') actionShow.setIcon(getIcon('galacteek-incandescent.png')) actionShow.triggered.connect(self.onShowWindow) systemTrayMenu.addSeparator() actionQuit = systemTrayMenu.addAction('Quit') actionQuit.setIcon(getIcon('quit.png')) actionQuit.triggered.connect(self.onExit) self.systemTray.setContextMenu(systemTrayMenu) def initMisc(self): self.multihashDb = IPFSObjectMetadataDatabase(self._mHashDbLocation, loop=self.loop) self.resourceOpener = IPFSResourceOpener(parent=self) self.downloadsManager = downloads.DownloadsManager(self) self.marksLocal = IPFSMarks(self.localMarksFileLocation, backup=True) self.marksLocal.addCategory('general') self.marksLocal.addCategory('uncategorized') self.marksNetwork = IPFSMarks(self.networkMarksFileLocation, autosave=False) self.tempDir = QTemporaryDir() self.tempDirWeb = self.tempDirCreate(self.tempDir.path(), 'webdownloads') def tempDirCreate(self, basedir, name=None): tmpdir = QDir(basedir) if not tmpdir.exists(): return uid = name if name else str(uuid.uuid4()) path = tmpdir.absoluteFilePath(uid) if tmpdir.mkpath(path): return path async def setupHashmarks(self): pkg = 'galacteek.hashmarks.default' res = await database.hashmarkSourceSearch( name='core', url=pkg, type=models.HashmarkSource.TYPE_PYMODULE) if not res: await database.hashmarkSourceAdd( type=models.HashmarkSource.TYPE_PYMODULE, url=pkg, name='core') await self.hmSynchronizer.sync() await database.hashmarkSourceAdd( type=models.HashmarkSource.TYPE_GITREPOS, url='https://github.com/galacteek/hashmarks-dwebland') await self.scheduler.spawn(self.hmSynchronizer.syncTask()) def setupTranslator(self): if self.translator: QApplication.removeTranslator(self.translator) self.translator = QTranslator() QApplication.installTranslator(self.translator) lang = self.settingsMgr.getSetting(CFG_SECTION_UI, CFG_KEY_LANG) self.translator.load( ':/share/translations/galacteek_{0}.qm'.format(lang)) def createMainWindow(self, show=True): self.mainWindow = mainui.MainWindow(self) if show is True: self.mainWindow.show() def onSystemTrayIconClicked(self, reason): if reason == QSystemTrayIcon.Unknown: pass elif reason == QSystemTrayIcon.Context: pass elif reason == QSystemTrayIcon.DoubleClick: self.mainWindow.showMaximized() else: pass def systemTrayMessage(self, title, message, timeout=2000, messageIcon=QSystemTrayIcon.Information): self.systemTray.showMessage(title, message, messageIcon, timeout) @ipfsOp async def setupRepository(self, op): pubsubEnabled = True # mandatory now .. hExchEnabled = self.settingsMgr.isTrue(CFG_SECTION_IPFS, CFG_KEY_HASHMARKSEXCH) self.ipfsCtx.resources['ipfs-logo-ice'] = await self.importQtResource( '/share/icons/ipfs-logo-128-ice.png') self.ipfsCtx.resources['ipfs-cube-64'] = await self.importQtResource( '/share/icons/ipfs-cube-64.png') self.ipfsCtx.resources['atom-feed'] = await self.importQtResource( '/share/icons/atom-feed.png') self.ipfsCtx.resources['markdown-reference'] = \ await self.importQtResource( '/share/static/docs/markdown-reference.html') await self.ipfsCtx.setup(pubsubEnable=pubsubEnabled, pubsubHashmarksExch=hExchEnabled, offline=self.offline) await self.ipfsCtx.profilesInit() await self.qSchemeHandler.start() await self.importLdContexts() self.feedFollower = FeedFollower(self) self.feedFollowerTask = await self.scheduler.spawn( self.feedFollower.process()) await self.ipfsCtx.ipfsRepositoryReady.emit() self.ipfsCtx._ipfsRepositoryReady.emit() # # If the application's binary name is a valid CID, pin it! # This happens when running the AppImage and ensures # self-seeding of the image! # if isinstance(self.progName, str): progNameClean = re.sub(r'[\.\/]*', '', self.progName) if cidhelpers.cidValid(progNameClean): self._progCid = progNameClean log.debug("Auto pinning program's CID: {0}".format( self.progCid)) await self.ipfsCtx.pin(joinIpfs(self.progCid), False, self.onAppReplication, qname='self-seeding') if self.cmdArgs.seed and self.cmdArgs.appimage: await self.seedAppImage() @ipfsOp async def seedAppImage(self, ipfsop): # Automatic AppImage seeding if os.path.isfile(self.cmdArgs.binarypath): log.info( 'AppImage seeding: {img}'.format(img=self.cmdArgs.binarypath)) ensure(ipfsop.addPath(self.cmdArgs.binarypath, wrap=True), futcallback=self.onAppSeed) def onAppSeed(self, future): try: replResult = future.result() except Exception as err: log.debug('AppImage seed: failed', exc_info=err) else: if isinstance(replResult, dict): cid = replResult.get('Hash') self.appImageImported.emit(cid) log.info('AppImage seed OK: CID {cid}'.format(cid=cid)) def onAppReplication(self, future): try: replResult = future.result() except Exception as err: log.debug('App replication: failed', exc_info=err) else: log.debug('App replication: success ({result})'.format( result=replResult)) @ipfsOp async def importLdContexts(self, ipfsop): """ Import the JSON-LD contexts and associate the directory entry with the 'galacteek.ld.contexts' key """ contextsPath = ipfsop.ldContextsRootPath() if not os.path.isdir(contextsPath): log.debug('LD contexts not found') return entry = await ipfsop.addPath(contextsPath, recursive=True, hidden=False) if entry: ldKeyName = 'galacteek.ld.contexts' log.debug('LD contexts sitting at: {}'.format(entry.get('Hash'))) await ipfsop.keyGen(ldKeyName, checkExisting=True) ensure( ipfsop.publish(entry['Hash'], key=ldKeyName, allow_offline=True)) @ipfsOp async def importQtResource(self, op, path): rscFile = QFile(':{0}'.format(path)) try: rscFile.open(QFile.ReadOnly) data = rscFile.readAll().data() entry = await op.addBytes(data) except Exception as e: log.debug('importQtResource: {}'.format(str(e))) else: return entry def ipfsTask(self, fn, *args, **kw): """ Schedule an async IPFS task """ return self.loop.create_task(fn(self.ipfsClient, *args, **kw)) def ipfsTaskOp(self, fn, *args, **kw): """ Schedule an async IPFS task using an IPFSOperator instance """ client = self.ipfsClient if client: return self.loop.create_task( fn(self.getIpfsOperator(), *args, **kw)) def getIpfsOperator(self): """ Returns a new IPFSOperator with the currently active IPFS client""" return IPFSOperator(self.ipfsClient, ctx=self.ipfsCtx, debug=self.debugEnabled, nsCachePath=self.nsCacheLocation) def getIpfsConnectionParams(self): mgr = self.settingsMgr section = CFG_SECTION_IPFSD if mgr.isTrue(section, CFG_KEY_ENABLED): return IPFSConnectionParams( '127.0.0.1', mgr.getSetting(section, CFG_KEY_APIPORT), mgr.getSetting(section, CFG_KEY_HTTPGWPORT)) else: section = CFG_SECTION_IPFSCONN1 return IPFSConnectionParams( mgr.getSetting(section, CFG_KEY_HOST), mgr.getSetting(section, CFG_KEY_APIPORT), mgr.getSetting(section, CFG_KEY_HTTPGWPORT)) def getEthParams(self): mgr = self.settingsMgr provType = mgr.getSetting(CFG_SECTION_ETHEREUM, CFG_KEY_PROVIDERTYPE) rpcUrl = mgr.getSetting(CFG_SECTION_ETHEREUM, CFG_KEY_RPCURL) return EthereumConnectionParams(rpcUrl, provType=provType) async def updateIpfsClient(self): from galacteek.ipfs import ConnectionError connParams = self.getIpfsConnectionParams() client = aioipfs.AsyncIPFS(host=connParams.host, port=connParams.apiPort, loop=self.loop) self.ipfsClient = client self.ipfsCtx.ipfsClient = client self.ipfsOpMain = self.getIpfsOperator() self.ipfsOpMain.ipidManager = self.ipidManager IPFSOpRegistry.regDefault(self.ipfsOpMain) try: await self.setupRepository() except ConnectionError: await messageBoxAsync( 'IPFS connection error (is your daemon running ?)') await self.ipfsCtx.ipfsConnectionReady.emit() async def stopIpfsServices(self): try: await self.ipfsCtx.shutdown() except BaseException as err: log.debug( 'Error shutting down context: {err}'.format(err=str(err))) if self.feedFollowerTask is not None: await self.feedFollowerTask.close() def setupDb(self): ensure(self.setupOrmDb(self._mainDbLocation)) def jobsExceptionHandler(self, scheduler, context): pass async def setupOrmDb(self, dbpath): self.scheduler = await aiojobs.create_scheduler(close_timeout=1.0, limit=150, pending_limit=1000) # Old database, just for Atom feeds right now self.sqliteDb = SqliteDatabase(self._sqliteDbLocation) ensure(self.sqliteDb.setup()) self.modelAtomFeeds = AtomFeedsModel(self.sqliteDb.feeds, parent=self) self.urlHistory = history.URLHistory( self.sqliteDb, enabled=self.settingsMgr.urlHistoryEnabled, parent=self) if not await database.initOrm(self._mainDbLocation): await self.dbConfigured.emit(False) return await self.setupHashmarks() await self.dbConfigured.emit(True) async def onDbConfigured(self, configured): if not configured: return self.debug('Database ready') self.setupClipboard() self.setupTranslator() self.initSystemTray() self.initMisc() self.initEthereum() self.initWebProfiles() self.initDapps() self.createMainWindow() self.clipboardInit() await self.setupIpfsConnection() async def fetchGoIpfs(self): ipfsPath = self.which('ipfs') fsMigratePath = self.which('fs-repo-migrations') if fsMigratePath is None or self.cmdArgs.forcegoipfsdl: await fetchFsMigrateWrapper(self) if ipfsPath is None or self.cmdArgs.forcegoipfsdl: path = await fetchGoIpfsWrapper(self) if path is None: self.systemTrayMessage('Galacteek', iGoIpfsFetchError()) async def setupIpfsConnection(self): sManager = self.settingsMgr await self.fetchGoIpfs() if sManager.isTrue(CFG_SECTION_IPFSD, CFG_KEY_ENABLED): fsMigratePath = shutil.which('fs-repo-migrations') hasFsMigrate = fsMigratePath is not None if hasFsMigrate is False and self.cmdArgs.migrate is True: self.systemTrayMessage('Galacteek', iFsRepoMigrateNotFound()) enableMigrate = hasFsMigrate is True and \ self.cmdArgs.migrate is True ipfsPath = self.which('ipfs') await self.startIpfsDaemon(goIpfsPath=ipfsPath, migrateRepo=enableMigrate) else: await self.updateIpfsClient() def setupMainObjects(self): self.manuals = ManualsManager(self) self.mimeDb = QMimeDatabase() self.jinjaEnv = defaultJinjaEnv() self.solarSystem = SolarSystem() self.mimeTypeIcons = preloadMimeIcons() self.hmSynchronizer = HashmarksSynchronizer() self.ipidManager = IPIDManager( resolveTimeout=self.settingsMgr.ipidIpnsTimeout) self.towers = { 'dags': DAGSignalsTower(self), 'schemes': URLSchemesTower(self), 'did': DIDTower() } self.rscAnalyzer = ResourceAnalyzer(parent=self) self.messageDisplayRequest.connect( lambda msg, title: ensure(messageBoxAsync(msg, title=title))) self.appImageImported.connect(lambda cid: ensure( messageBoxAsync('AppImage was imported in IPFS!'))) def setupAsyncLoop(self): """ Install the quamash event loop and enable debugging """ loop = QEventLoop(self) asyncio.set_event_loop(loop) logging.getLogger('quamash').setLevel(logging.INFO) if self.debugEnabled: logging.getLogger('asyncio').setLevel(logging.DEBUG) loop.set_debug(True) warnings.simplefilter('always', ResourceWarning) warnings.simplefilter('always', BytesWarning) warnings.simplefilter('always', ImportWarning) self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) self.loop = loop return loop def task(self, fn, *args, **kw): return self.loop.create_task(fn(*args, **kw)) def configure(self): self.initSettings() self.setupMainObjects() self.setupSchemeHandlers() self.applyStyle() self.setupDb() def acquireLock(self): lpath = Path(self._pLockLocation) if not lpath.exists(): lpath.touch() self.lock = FileLock(lpath, timeout=2) try: self.lock.acquire() except Exception: return questionBox( 'Lock', 'The profile lock could not be acquired ' '(another instance could be running). Ignore ?') return True def setupPaths(self): qtDataLocation = QStandardPaths.writableLocation( QStandardPaths.DataLocation) if not qtDataLocation: raise Exception('No writable data location found') self._dataLocation = os.path.join(qtDataLocation, self._appProfile) self._ipfsBinLocation = os.path.join(qtDataLocation, 'ipfs-bin') self._ipfsDataLocation = os.path.join(self.dataLocation, 'ipfs') self._orbitDataLocation = os.path.join(self.dataLocation, 'orbitdb') self._mHashDbLocation = os.path.join(self.dataLocation, 'mhashmetadb') self._sqliteDbLocation = os.path.join(self.dataLocation, 'db.sqlite') self._pLockLocation = os.path.join(self.dataLocation, 'profile.lock') self._mainDbLocation = os.path.join(self.dataLocation, 'db_main.sqlite3') self.marksDataLocation = os.path.join(self.dataLocation, 'marks') self.uiDataLocation = os.path.join(self.dataLocation, 'ui') self.cryptoDataLocation = os.path.join(self.dataLocation, 'crypto') self.gpgDataLocation = os.path.join(self.cryptoDataLocation, 'gpg') self.localMarksFileLocation = os.path.join(self.marksDataLocation, 'ipfsmarks.local.json') self.networkMarksFileLocation = os.path.join(self.marksDataLocation, 'ipfsmarks.network.json') self.pinStatusLocation = os.path.join(self.dataLocation, 'pinstatus.json') self._nsCacheLocation = os.path.join(self.dataLocation, 'nscache.json') qtConfigLocation = QStandardPaths.writableLocation( QStandardPaths.ConfigLocation) self.configDirLocation = os.path.join(qtConfigLocation, GALACTEEK_NAME, self._appProfile) self.settingsFileLocation = os.path.join( self.configDirLocation, '{}.conf'.format(GALACTEEK_NAME)) for dir in [ self._ipfsDataLocation, self._mHashDbLocation, self.ipfsBinLocation, self.marksDataLocation, self.cryptoDataLocation, self.gpgDataLocation, self.uiDataLocation, self.configDirLocation ]: if not os.path.exists(dir): os.makedirs(dir) self.defaultDownloadsLocation = QStandardPaths.writableLocation( QStandardPaths.DownloadLocation) self.debug('Datapath: {0}, config: {1}, configfile: {2}'.format( self._dataLocation, self.configDirLocation, self.settingsFileLocation)) os.environ['PATH'] += os.pathsep + self.ipfsBinLocation def which(self, prog='ipfs'): path = self.ipfsBinLocation + os.pathsep + os.environ['PATH'] result = shutil.which(prog, path=path) self.debug('Program {0} found at {1}'.format(prog, result)) return result def initSettings(self): self.settingsMgr = SettingsManager(path=self.settingsFileLocation) setDefaultSettings(self) self.settingsMgr.sync() async def startIpfsDaemon(self, goIpfsPath='ipfs', migrateRepo=False): if self.ipfsd is not None: # we only support one daemon for now return pubsubEnabled = True # mandatory now .. corsEnabled = self.settingsMgr.isTrue(CFG_SECTION_IPFSD, CFG_KEY_CORS) sManager = self.settingsMgr section = CFG_SECTION_IPFSD # Instantiate an IPFS daemon using asyncipfsd and # start it in a task, monitoring the initialization process self._ipfsd = asyncipfsd.AsyncIPFSDaemon( self.ipfsDataLocation, goIpfsPath=goIpfsPath, apiport=sManager.getInt(section, CFG_KEY_APIPORT), swarmport=sManager.getInt(section, CFG_KEY_SWARMPORT), swarmportQuic=sManager.getInt(section, CFG_KEY_SWARMPORT_QUIC), swarmProtos=sManager.swarmProtosList, gatewayport=sManager.getInt(section, CFG_KEY_HTTPGWPORT), swarmLowWater=sManager.getInt(section, CFG_KEY_SWARMLOWWATER), swarmHighWater=sManager.getInt(section, CFG_KEY_SWARMHIGHWATER), storageMax=sManager.getInt(section, CFG_KEY_STORAGEMAX), gwWritable=sManager.isTrue(section, CFG_KEY_HTTPGWWRITABLE), routingMode=sManager.getSetting(section, CFG_KEY_ROUTINGMODE), pubsubRouter=sManager.getSetting(section, CFG_KEY_PUBSUB_ROUTER), namesysPubsub=sManager.isTrue(section, CFG_KEY_NAMESYS_PUBSUB), pubsubSigning=sManager.isTrue(section, CFG_KEY_PUBSUB_USESIGNING), fileStore=sManager.isTrue(section, CFG_KEY_FILESTORE), nice=sManager.getInt(section, CFG_KEY_NICE), pubsubEnable=pubsubEnabled, corsEnable=corsEnabled, migrateRepo=migrateRepo, debug=self.cmdArgs.goipfsdebug, offline=self.cmdArgs.offline, loop=self.loop) await self.scheduler.spawn(self.startIpfsdTask(self.ipfsd)) async def startIpfsdTask(self, ipfsd): started = await ipfsd.start() if started is False: return self.systemTrayMessage('IPFS', iIpfsDaemonProblem()) running = False logUser.info(iIpfsDaemonStarted()) # Use asyncio.wait_for to wait for the proto.eventStarted # event to be fired. for attempt in range(1, 32): logUser.info(iIpfsDaemonWaiting(attempt)) with async_timeout.timeout(1): try: await ipfsd.proto.eventStarted.wait() except asyncio.CancelledError: continue except asyncio.TimeoutError: # Event not set yet, wait again log.debug('IPFSD: timeout occured while waiting for ' 'daemon to start (attempt: {0})'.format(attempt)) continue else: # Event was set, good to go logUser.info(iIpfsDaemonReady()) running = True break if running is True: ensure(self.updateIpfsClient()) else: logUser.info(iIpfsDaemonInitProblem()) def initEthereum(self): try: from galacteek.dweb.ethereum.ctrl import EthereumController self.ethereum = EthereumController(self.getEthParams(), loop=self.loop, parent=self, executor=self.executor) if self.settingsMgr.ethereumEnabled: ensure(self.ethereum.start()) except ImportError: self.ethereum = MockEthereumController() def normalCursor(self): cursor = QCursor(Qt.ArrowCursor) QApplication.setOverrideCursor(cursor) QApplication.changeOverrideCursor(cursor) def setupClipboard(self): self.appClipboard = self.clipboard() self.clipTracker = ClipboardTracker(self, self.appClipboard) def clipboardInit(self): self.clipTracker.clipboardInit() def setClipboardText(self, text): self.clipTracker.setText(text) def getClipboardText(self): return self.clipTracker.getText() def initWebProfiles(self): self.scriptsIpfs = ipfsClientScripts(self.getIpfsConnectionParams()) self.webProfiles = { 'minimal': MinimalProfile(parent=self), 'ipfs': IPFSProfile(parent=self), 'web3': Web3Profile(parent=self) } def availableWebProfilesNames(self): return [p.profileName for n, p in self.webProfiles.items()] def initDapps(self): self.dappsRegistry = DappsRegistry(self.ethereum, parent=self) def setupSchemeHandlers(self): self.dwebSchemeHandler = DWebSchemeHandlerGateway(self) self.ensSchemeHandler = EthDNSSchemeHandler(self) self.ensProxySchemeHandler = EthDNSProxySchemeHandler(self) self.nativeIpfsSchemeHandler = NativeIPFSSchemeHandler( self, noMutexes=self.cmdArgs.noipfsmutexlock) self.qSchemeHandler = MultiObjectHostSchemeHandler(self) def subUrl(self, path): """ Joins the gatewayUrl and path to form a new URL """ sub = QUrl(str(self.gatewayUrl)) sub.setPath(path) return sub def getJinjaTemplate(self, name): try: tmpl = self.jinjaEnv.get_template(name) except jinja2.exceptions.TemplateNotFound: return None else: return tmpl @asyncify async def checkReleases(self): self.debug('Checking for new releases') newR = await pypicheck.newReleaseAvailable() if newR: self.systemTrayMessage('Galacteek', iNewReleaseAvailable(), timeout=8000) def showTasks(self): for task in self.pendingTasks: self.debug('Pending task: {}'.format(task)) def onShowWindow(self): self.mainWindow.showMaximized() def restart(self): ensure(self.restartApp()) async def restartApp(self): from galacteek.guientrypoint import appStarter pArgs = self.arguments() await self.exitApp() time.sleep(1) appStarter.startProcess(pArgs) def onExit(self): ensure(self.exitApp()) async def shutdownScheduler(self): # It ain't that bad. STFS with dignity for stry in range(0, 12): try: async with async_timeout.timeout(2): await self.scheduler.close() except asyncio.TimeoutError: log.warning('Timeout shutting down the scheduler (not fooled)') continue else: log.debug(f'Scheduler went down (try: {stry})') return async def exitApp(self): self._shuttingDown = True self.lock.release() self.mainWindow.stopTimers() try: self.systemTray.hide() except: pass try: await self.sqliteDb.close() await database.closeOrm() except: pass await self.stopIpfsServices() await self.loop.shutdown_asyncgens() await self.shutdownScheduler() await cancelAllTasks() await self.ethereum.stop() if self.ipfsClient: await self.ipfsClient.close() if self.ipfsd: self.ipfsd.stop() self.mainWindow.close() if self.debug: self.showTasks() self.tempDir.remove() self.quit()
class MainWindow(QMainWindow): restartOdooMenuItem = QtCore.pyqtSignal() stopOdooMenuItem = QtCore.pyqtSignal() restartPostgreMenuItem = QtCore.pyqtSignal() stopPostgreMenuItem = QtCore.pyqtSignal() def __init__(self): super(MainWindow, self).__init__() if OdooInstallationFound == False: if not os.path.isdir(appfolder + '\\Temp'): os.makedirs(appfolder + '\\Temp') unzipToPath = appfolder + '\\Temp\\Unzip' destinationPath = appfolder + "\\Runtime\\Odoo" self.downloadFileWorker(mainSettings['github']['downloadPath']) if os.path.isfile(appfolder + '\\Temp\\GitHub-Odoo.zip'): if not os.path.isdir(appfolder + '\\Temp\\unzip'): os.makedirs(appfolder + '\\Temp\\unzip') self.zipFileWorker(appfolder + '\\Temp\\GitHub-Odoo.zip', unzipToPath) self.zipWindow.close() #Check if the file is etxracted to subfolder - Files on github includes branch name -> Correct this countFolders = 0 extractFolder = None for name in os.listdir(unzipToPath): extractFolder = name countFolders += 1 if countFolders == 1: shutil.move(unzipToPath + "\\" + extractFolder + "\\", destinationPath) self.startCybeSystemsApplication() else: self.startCybeSystemsApplication() if os.path.isdir(appfolder + '\\Temp'): shutil.rmtree(appfolder + '\\Temp',ignore_errors=True) def zipFileWorker(self,file, destination_folder): self.zipWindow = ZipWindow(file, destination_folder) self.zipWindow.show() def downloadFileWorker(self,url): self.httpWin = HttpWindow(url) self.httpWin.exec() def startCybeSystemsApplication(self): #Set Loading TrayIcon self.setWindowIcon(QtGui.QIcon(appfolder + '/ressource/icons/icon.png')) img = QtGui.QImage() img.load(appfolder + '/ressource/icons/icon_loading.png') self.pixmap = QtGui.QPixmap.fromImage(img) self.icon = QtGui.QIcon() self.icon.addPixmap(self.pixmap) self.tray = QSystemTrayIcon(self.icon, self) self.tray.show() traymenu = QMenu() #Set Real Icon self.tray.hide() img = QtGui.QImage() img.load(appfolder + '/ressource/icons/icon.png') self.pixmap = QtGui.QPixmap.fromImage(img) self.icon = QtGui.QIcon() self.icon.addPixmap(self.pixmap) self.tray = QSystemTrayIcon(self.icon, self) self.tray.activated.connect(self.onTrayIconActivated) self.tray.setContextMenu(traymenu) self.tray.show() #Load Stylesheet if mainSettings['other']['theme'].lower() != 'default': if mainSettings['other']['theme'].lower() == 'steamlike': stylesheetFile = open(appfolder + '/ressource/ui/steamlike.stylesheet', "r") elif mainSettings['other']['theme'].lower() == 'darkorange': stylesheetFile = open(appfolder + '/ressource/ui/darkorange.stylesheet', "r") elif mainSettings['other']['theme'].lower() == 'maya': stylesheetFile = open(appfolder + '/ressource/ui/maya.stylesheet', "r") stylesheet = stylesheetFile.read() traymenu.setStyleSheet(stylesheet) stylesheetFile.close() trayoption_openBrowser_entry = QAction(QtGui.QIcon(self.icon), "Open Odoo", self) trayoption_openBrowser_entry.triggered.connect(lambda: webbrowser.open(mainSettings['odoo']['startpage'])) traymenu.addAction(trayoption_openBrowser_entry) trayoption_openBrowser_entry.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/world.png')) traymenu.addSeparator() tools = traymenu.addMenu('&Odoo') tools.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/icon.png')) tools_odoo_restart = QAction(QtGui.QIcon(self.icon), "Restart Server", self) tools_odoo_restart.triggered.connect(lambda: (self.restartOdooMenuItem.emit())) tools.addAction(tools_odoo_restart) tools_odoo_restart.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/start_server.png')) tools_odoo_stop = QAction(QtGui.QIcon(self.icon), "Stop Server", self) tools_odoo_stop.triggered.connect(lambda: (self.stopOdooMenuItem.emit())) tools.addAction(tools_odoo_stop) tools_odoo_stop.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/stop_server.png')) #traymenu.addSeparator() tools = traymenu.addMenu('&PostgreSQL') tools.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/postgresql.png')) tools_postgre_restart = QAction(QtGui.QIcon(self.icon), "Restart Server", self) tools_postgre_restart.triggered.connect(lambda: (self.restartPostgreMenuItem.emit())) tools.addAction(tools_postgre_restart) tools_postgre_restart.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/start_server.png')) tools_postgre_stop = QAction(QtGui.QIcon(self.icon), "Stop Server", self) tools_postgre_stop.triggered.connect(lambda: (self.stopPostgreMenuItem.emit())) tools.addAction(tools_postgre_stop) tools_postgre_stop.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/stop_server.png')) tools.addSeparator() tools_pgadmin = QAction(QtGui.QIcon(self.icon), "pgAdmin III", self) tools_pgadmin.triggered.connect(lambda: self.startpgadmin()) tools.addAction(tools_pgadmin) tools_pgadmin.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/cog.png')) traymenu.addSeparator() trayoption_quickconfig = QAction(QtGui.QIcon(self.icon), "Show Output/Config", self) trayoption_quickconfig.triggered.connect(lambda: self.showCommandLineWindowTryOption()) traymenu.addAction(trayoption_quickconfig) trayoption_quickconfig.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/application_osx_terminal.png')) traymenu.addSeparator() trayoption_exit_entry = QAction(QtGui.QIcon(self.icon), "Exit", self) trayoption_exit_entry.triggered.connect(lambda: self.trayOptionExit()) traymenu.addAction(trayoption_exit_entry) trayoption_exit_entry.setIcon(QtGui.QIcon(appfolder + '/ressource/icons/cancel.png')) self.tray.showMessage('Odoo is Loading - Please wait','\nLeft click to open CommandWindow\nRight click to open Traymenu') self.showCommandLineWindow() def startpgadmin(self): os.startfile(appfolder + '/Runtime/PostgreSQL/bin/pgAdmin3.exe') def showCommandLineWindow(self): self.ShowCommandLineWindow=CommandLineWindow(self) self.ShowCommandLineWindow.setWindowIcon(QtGui.QIcon(appfolder + '/ressource/icons/icon.png')) self.ShowCommandLineWindow.show() if mainSettings['other']['minimizeToTray']: self.ShowCommandLineWindow.setVisible(False) else: self.ShowCommandLineWindow.setVisible(True) def toggleCommandLineWindow(self): if self.ShowCommandLineWindow.isMinimized(): self.ShowCommandLineWindow.setVisible(True) self.ShowCommandLineWindow.showNormal() self.ShowCommandLineWindow.activateWindow() elif self.ShowCommandLineWindow.isVisible (): self.ShowCommandLineWindow.showNormal() self.ShowCommandLineWindow.setVisible(False) else: self.ShowCommandLineWindow.setVisible(True) self.ShowCommandLineWindow.showNormal() self.ShowCommandLineWindow.activateWindow() def showCommandLineWindowTryOption(self): self.ShowCommandLineWindow.setVisible(True) self.ShowCommandLineWindow.showNormal() self.ShowCommandLineWindow.activateWindow() def onTrayIconActivated(self,reason): if reason == QSystemTrayIcon.DoubleClick: pass if reason == QSystemTrayIcon.Trigger: self.toggleCommandLineWindow() if reason == QSystemTrayIcon.Context: pass def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def trayOptionExit(self,msgbox=True): if msgbox: quit_msg = "Stop all running Server ?" reply = QMessageBox.question(self.center(), 'Exit', quit_msg, QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: confirmed = True else: confirmed = False else: confirmed = True if confirmed: runtimeSettings["closeMainWindow"] = True self.ShowCommandLineWindow.setVisible(True) app = QApplication.instance() app.closeAllWindows() self.tray.hide() os._exit(1)
class MainApp(QMainWindow, Ui_MainWindow): _translate = QCoreApplication.translate tab_list = [] # TODO - add dutch translation files def __init__(self, isolated, *args): super(MainApp, self).__init__(*args) Lumberjack.info('spawning the <<< MainApp >>> hey says: I am the Main man here see!') self.load_settings() self.setup_tray(isolated) self.dbhelper = DbHelper() self.setupUi(self) self.iconize_controls() self.load_styling() self.tabWidget = QTabWidget(self.centralwidget) self.tabWidget.setTabsClosable(True) self.tabWidget.setMovable(True) self.tabWidget.setTabBarAutoHide(True) self.tabWidget.setObjectName("tabWidget") self.verticalLayout.addWidget(self.tabWidget) builderLabel = QLabel('made by: MazeFX Solutions') self.statusbar.addPermanentWidget(builderLabel) self.menuPAT.triggered.connect(self.handle_menu_event) self.menuLists.triggered.connect(self.handle_menu_event) self.menuHelp.triggered.connect(self.handle_menu_event) self.tabWidget.tabCloseRequested.connect(self.close_tab) self.actionHome.trigger() self._retranslateUi(self) def iconize_controls(self): Lumberjack.info('< MainApp > - -> (iconize_controls)') homeIcon = qta.icon('fa.home', color='white') self.actionHome.setIcon(homeIcon) wrenchIcon = qta.icon('fa.wrench', color='white') self.actionSettings.setIcon(wrenchIcon) bankIcon = qta.icon('fa.bank', color='white') self.actionListBankAccounts.setIcon(bankIcon) contractIcon = QIcon(':/app_icons/rc/handshake_icon.svg') self.actionListContracts.setIcon(contractIcon) atIcon = qta.icon('fa.at', color='white') self.actionListEmailAddresses.setIcon(atIcon) envelopeIcon = qta.icon('fa.envelope', color='white') self.actionListLetters.setIcon(envelopeIcon) relationIcon = qta.icon('fa.group', color='white') self.actionListRelations.setIcon(relationIcon) transactionIcon = qta.icon('fa.money', color='white') self.actionListTransactions.setIcon(transactionIcon) userIcon = qta.icon('fa.user', color='white') self.actionListUsers.setIcon(userIcon) helpIcon = qta.icon('fa.question', color='white') self.actionHelp.setIcon(helpIcon) aboutIcon = qta.icon('fa.info', color='white') self.actionAbout.setIcon(aboutIcon) def setup_tray(self, isolated): Lumberjack.info('< MainApp > - -> (setup_tray)') self.trayIcon = QSystemTrayIcon(QIcon(':/app_icons/rc/PAT_icon.png'), self) self.trayMenu = QMenu(self) showAction = self.trayMenu.addAction("Open PAT") self.trayMenu.addSeparator() exitAction = self.trayMenu.addAction("Exit") self.trayIcon.setContextMenu(self.trayMenu) self.trayMenu.triggered.connect(self.handle_tray_event) self.trayIcon.activated.connect(self.handle_tray_event) self.trayIcon.show() if isolated: self.trayIcon.showMessage('PAT Service', 'PAT service is now running..') def handle_tray_event(self, *args): Lumberjack.info('< MainApp > - -> (handle_tray_event)') print(Fore.MAGENTA + '$! Received a tray action with args: ', args) if args[0] == 3: self.show() return elif hasattr(args[0], 'text'): print(Fore.MAGENTA + '$! Tray event has text!!') if args[0].text() == 'Open PAT': self.show() elif args[0].text() == 'Exit': self.close() def _retranslateUi(self, MainWindow): pass def handle_menu_event(self, *args): Lumberjack.info('< MainApp > - -> (handle_menu_event)') Lumberjack.debug('(handle_menu_event) - args = {}'.format(args)) action_text = args[0].text() icon = args[0].icon() Lumberjack.debug('(handle_menu_event) - Action text selector = {}'.format(action_text)) print(Fore.MAGENTA + '$! Action text received: ', action_text) if action_text == 'Home': Lumberjack.info('(handle_menu_event) >User action> : Adding Home tab to self') self.add_tab(HomeTab, 'Home', icon) if action_text == 'Settings': Lumberjack.info('(handle_menu_event) >User action> : Showing settings dialog') self.show_settings() elif action_text == 'Bank accounts': Lumberjack.info('(handle_menu_event) >User action> : Adding Bank account List tab to self') self.add_tab(BankAccountListTab, 'Bank accounts', icon) elif action_text == 'Contracts': Lumberjack.info('(handle_menu_event) >User action> : Adding Contracts List tab to self') self.add_tab(ContractListTab, 'Contracts', icon) elif action_text == 'Email addresses': Lumberjack.info('(handle_menu_event) >User action> : Adding EmailAddress List tab to self') self.add_tab(EmailAddressListTab, 'Email addresses', icon) elif action_text == 'Letters': Lumberjack.info('(handle_menu_event) >User action> : Adding Letter List tab to self') self.add_tab(LetterListTab, 'Letters', icon) elif action_text == 'Users': Lumberjack.info('(handle_menu_event) >User action> : Adding User List tab to self') self.add_tab(UserListTab, 'Users', icon) elif action_text == 'Relations': Lumberjack.info('(handle_menu_event) >User action> : Adding Relation List tab to self') self.add_tab(RelationListTab, 'Relations', icon) elif action_text == 'Transactions': Lumberjack.info('(handle_menu_event) >User action> : Adding Transaction List tab to self') self.add_tab(TransactionListTab, 'Transactions', icon) elif action_text == 'Help': Lumberjack.info('(handle_menu_event) >User action> : Showing help dialog') # TODO - build help dialog and help files elif action_text == 'About': Lumberjack.info('(handle_menu_event) >User action> : Showing about dialog') # TODO build About dialog. def show_settings(self): Lumberjack.info('< MainApp > - -> (show_settings)') settings_dialog = SettingsDialog() settings_dialog.exec_() def add_tab(self, tab_cls, tab_name, icon): Lumberjack.info('< MainApp > - -> (add_tab)') new_tab = tab_cls(self.dbhelper) print(Fore.MAGENTA + 'Adding a tab with class: ', str(tab_cls)) new_tab.setObjectName(str(tab_cls)) self.tabWidget.addTab(new_tab, icon, self._translate("MainWindow", tab_name)) print(Fore.MAGENTA + 'New tab added to tab list.') self.tabWidget.setCurrentIndex(self.tabWidget.indexOf(new_tab)) self.tab_list.append(new_tab) def close_tab(self, index): # TODO - Check if index stays correct when moving tabs around requesting_tab = self.tab_list[index] print(Fore.MAGENTA + 'requesting tab is: ', requesting_tab) if hasattr(requesting_tab, 'form'): if requesting_tab.form.edit_mode: print(Fore.MAGENTA + 'Tab is in edit mode.') requesting_tab.form.toggle_edit_mode(False, None, None) if requesting_tab.form.edit_mode is None: print(Fore.MAGENTA + 'Tab is now in equil.') self.tabWidget.removeTab(index) del self.tab_list[index] else: self.tabWidget.removeTab(index) del self.tab_list[index] def load_settings(self): self.settings = QSettings() db_path = self.settings.value('db_base_path') db_name = self.settings.value('db_name') if db_path is not None and db_name is not None: db_file = os.path.join(db_path, db_name) Lumberjack.debug('__init__ - db_file = {}'.format(db_file)) if os.path.exists(db_file): return Lumberjack.warning('(load_settings) - database not found') settings_dialog = SettingsDialog() settings_dialog.exec_() int_value = self.settings.value('db_type', type=int) print(Fore.MAGENTA + "load choosen database setting: %s" % repr(int_value)) def load_styling(self): style.set_window_style(self) def closeEvent(self, event): print(Fore.MAGENTA + "User has clicked the red x on the main window") for tab in self.tab_list: if hasattr(tab, 'form'): if tab.form.edit_mode: print(Fore.MAGENTA + 'Tab is in edit mode.') tab.form.toggle_edit_mode(False, None, None) close_dialog = CloseDialog() result = close_dialog.exec_() if result == close_dialog.Minimize: self.hide() event.ignore() elif result == close_dialog.Rejected: event.ignore() elif result == close_dialog.Exit: print(Fore.MAGENTA + "Exiting via save dialog, result = ", result) self.trayIcon.hide() event.accept()
class ElectrumGui(PrintError): @profiler def __init__(self, config, daemon, plugins): set_language(config.get('language', get_default_language())) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.nd = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() # init tray self.dark_icon = self.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('Electrum') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' if use_dark_theme: try: import qdarkstyle self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False self.print_error('Error setting dark theme: {}'.format(repr(e))) # Even if we ourselves don't set the dark theme, # the OS/window manager/etc might set *a dark theme*. # Hence, try to choose colors accordingly: ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.close) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline')) return if self.nd: self.nd.on_update() self.nd.show() self.nd.raise_() return self.nd = NetworkDialog(self.daemon.network, self.config, self.network_updated_signal_obj) self.nd.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) w.warn_if_watching_only() return w def count_wizards_in_progress(func): def wrapper(self: 'ElectrumGui', *args, **kwargs): with self._num_wizards_lock: self._num_wizards_in_progress += 1 try: return func(self, *args, **kwargs) finally: with self._num_wizards_lock: self._num_wizards_in_progress -= 1 return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None try: wallet = self.daemon.load_wallet(path, None) except BaseException as e: traceback.print_exc(file=sys.stdout) QMessageBox.warning(None, _('Error'), _('Cannot load wallet') + ' (1):\n' + str(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: wallet = self._start_wizard_to_select_or_create_wallet(path) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except BaseException as e: traceback.print_exc(file=sys.stdout) QMessageBox.warning(None, _('Error'), _('Cannot create window for wallet') + ':\n' + str(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins) try: path, storage = wizard.select_storage(path, self.daemon.get_wallet) # storage is None if file does not exist if storage is None: wizard.path = path # needed by trustedcoin plugin wizard.run('new') storage = wizard.create_storage(path) else: wizard.run_upgrades(storage) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet except (WalletFileException, BitcoinException) as e: traceback.print_exc(file=sys.stderr) QMessageBox.warning(None, _('Error'), _('Cannot load wallet') + ' (2):\n' + str(e)) return finally: wizard.terminate() # return if wallet creation is not complete if storage is None or storage.get_action(): return wallet = Wallet(storage) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): try: self.init_network() except UserCancelled: return except GoBack: return except BaseException as e: traceback.print_exc(file=sys.stdout) return self.timer.start() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url'), app_is_starting=True): return signal.signal(signal.SIGINT, lambda *args: self.app.quit()) def quit_after_last_window(): # keep daemon running after close if self.config.get('daemon'): return # check if a wizard is in progress with self._num_wizards_lock: if self._num_wizards_in_progress > 0 or len(self.windows) > 0: return self.app.quit() self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(quit_after_last_window) def clean_up(): # Shut down the timer cleanly self.timer.stop() # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide() self.app.aboutToQuit.connect(clean_up) # main loop self.app.exec_() # on some platforms the exec_ call may not return, so use clean_up() def stop(self): self.print_error('closing GUI') self.app.quit()
class BlenderLauncher(QMainWindow, BaseWindow, Ui_MainWindow): show_signal = pyqtSignal() close_signal = pyqtSignal() def __init__(self, app): super().__init__() self.setupUi(self) self.setAcceptDrops(True) # Server self.server = QLocalServer() self.server.listen("blender-launcher-server") self.server.newConnection.connect(self.new_connection) # Global scope self.app = app self.favorite = None self.status = "None" self.app_state = AppState.IDLE self.cashed_builds = [] self.notification_pool = [] self.windows = [self] self.manager = PoolManager(num_pools=50, maxsize=10) self.timer = None self.started = True self.latest_tag = "" self.new_downloads = False # Setup window self.setWindowTitle("Blender Launcher") self.app.setWindowIcon( QIcon(taskbar_icon_paths[get_taskbar_icon_color()])) # Setup font QFontDatabase.addApplicationFont( ":/resources/fonts/OpenSans-SemiBold.ttf") self.font = QFont("Open Sans SemiBold", 10) self.font.setHintingPreference(QFont.PreferNoHinting) self.app.setFont(self.font) # Setup style file = QFile(":/resources/styles/global.qss") file.open(QFile.ReadOnly | QFile.Text) self.style_sheet = QTextStream(file).readAll() self.app.setStyleSheet(self.style_sheet) # Check library folder if is_library_folder_valid() is False: self.dlg = DialogWindow( self, title="Information", text="First, choose where Blender<br>builds will be stored", accept_text="Continue", cancel_text=None, icon=DialogIcon.INFO) self.dlg.accepted.connect(self.set_library_folder) else: create_library_folders(get_library_folder()) self.draw() def set_library_folder(self): library_folder = Path.cwd().as_posix() new_library_folder = QFileDialog.getExistingDirectory( self, "Select Library Folder", library_folder, options=QFileDialog.DontUseNativeDialog | QFileDialog.ShowDirsOnly) if new_library_folder: set_library_folder(new_library_folder) self.draw(True) else: self.app.quit() def draw(self, polish=False): self.HeaderLayout = QHBoxLayout() self.HeaderLayout.setContentsMargins(1, 1, 1, 0) self.HeaderLayout.setSpacing(0) self.CentralLayout.addLayout(self.HeaderLayout) self.SettingsButton = \ QPushButton(QIcon(":resources/icons/settings.svg"), "") self.SettingsButton.setIconSize(QSize(20, 20)) self.SettingsButton.setFixedSize(36, 32) self.SettingsButton.setToolTip("Show settings window") self.WikiButton = \ QPushButton(QIcon(":resources/icons/wiki.svg"), "") self.WikiButton.setIconSize(QSize(20, 20)) self.WikiButton.setFixedSize(36, 32) self.WikiButton.setToolTip("Open documentation") self.MinimizeButton = \ QPushButton(QIcon(":resources/icons/minimize.svg"), "") self.MinimizeButton.setIconSize(QSize(20, 20)) self.MinimizeButton.setFixedSize(36, 32) self.CloseButton = \ QPushButton(QIcon(":resources/icons/close.svg"), "") self.CloseButton.setIconSize(QSize(20, 20)) self.CloseButton.setFixedSize(36, 32) self.HeaderLabel = QLabel("Blender Launcher") self.HeaderLabel.setAlignment(Qt.AlignCenter) self.HeaderLayout.addWidget(self.SettingsButton, 0, Qt.AlignLeft) self.HeaderLayout.addWidget(self.WikiButton, 0, Qt.AlignLeft) self.HeaderLayout.addWidget(self.HeaderLabel, 1) self.HeaderLayout.addWidget(self.MinimizeButton, 0, Qt.AlignRight) self.HeaderLayout.addWidget(self.CloseButton, 0, Qt.AlignRight) self.SettingsButton.setProperty("HeaderButton", True) self.WikiButton.setProperty("HeaderButton", True) self.MinimizeButton.setProperty("HeaderButton", True) self.CloseButton.setProperty("HeaderButton", True) self.CloseButton.setProperty("CloseButton", True) # Tab layout self.TabWidget = QTabWidget() self.CentralLayout.addWidget(self.TabWidget) self.LibraryTab = QWidget() self.LibraryTabLayout = QVBoxLayout() self.LibraryTabLayout.setContentsMargins(0, 0, 0, 0) self.LibraryTab.setLayout(self.LibraryTabLayout) self.TabWidget.addTab(self.LibraryTab, "Library") self.DownloadsTab = QWidget() self.DownloadsTabLayout = QVBoxLayout() self.DownloadsTabLayout.setContentsMargins(0, 0, 0, 0) self.DownloadsTab.setLayout(self.DownloadsTabLayout) self.TabWidget.addTab(self.DownloadsTab, "Downloads") self.LibraryToolBox = BaseToolBoxWidget(self) self.LibraryStableListWidget = \ self.LibraryToolBox.add_list_widget( "Stable Releases", "LibraryStableListWidget", "Nothing to show yet", "Commit Time", extended_selection=True) self.LibraryDailyListWidget = \ self.LibraryToolBox.add_list_widget( "Daily Builds", "LibraryDailyListWidget", "Nothing to show yet", "Commit Time", extended_selection=True) self.LibraryExperimentalListWidget = \ self.LibraryToolBox.add_list_widget( "Experimental Branches", "LibraryExperimentalListWidget", "Nothing to show yet", "Commit Time", extended_selection=True) self.LibraryCustomListWidget = \ self.LibraryToolBox.add_list_widget( "Custom Builds", "LibraryCustomListWidget", "Nothing to show yet", "Commit Time", show_reload=True, extended_selection=True) self.LibraryTab.layout().addWidget(self.LibraryToolBox) self.DownloadsToolBox = BaseToolBoxWidget(self) self.DownloadsStableListWidget = \ self.DownloadsToolBox.add_list_widget( "Stable Releases", "DownloadsStableListWidget", "No new builds available", "Upload Time", False) self.DownloadsDailyListWidget = \ self.DownloadsToolBox.add_list_widget( "Daily Builds", "DownloadsDailyListWidget", "No new builds available", "Upload Time",) self.DownloadsExperimentalListWidget = \ self.DownloadsToolBox.add_list_widget( "Experimental Branches", "DownloadsExperimentalListWidget", "No new builds available", "Upload Time",) self.DownloadsTab.layout().addWidget(self.DownloadsToolBox) self.LibraryToolBox.setCurrentIndex(get_default_library_page()) self.DownloadsToolBox.setCurrentIndex(get_default_downloads_page()) # Connect buttons self.SettingsButton.clicked.connect(self.show_settings_window) self.WikiButton.clicked.connect(lambda: webbrowser.open( "https://github.com/DotBow/Blender-Launcher/wiki")) self.MinimizeButton.clicked.connect(self.showMinimized) self.CloseButton.clicked.connect(self.close) self.StatusBar.setContentsMargins(0, 0, 0, 2) self.StatusBar.setFont(self.font) self.statusbarLabel = QLabel() self.statusbarLabel.setIndent(8) self.NewVersionButton = QPushButton() self.NewVersionButton.hide() self.NewVersionButton.clicked.connect(self.show_update_window) self.statusbarVersion = QLabel(self.app.applicationVersion()) self.statusbarVersion.setToolTip( "The version of Blender Laucnher that is currently run") self.StatusBar.addPermanentWidget(self.statusbarLabel, 1) self.StatusBar.addPermanentWidget(self.NewVersionButton) self.StatusBar.addPermanentWidget(self.statusbarVersion) # Draw library self.draw_library() # Setup tray icon context Menu quit_action = QAction("Quit", self) quit_action.triggered.connect(self.quit) hide_action = QAction("Hide", self) hide_action.triggered.connect(self.close) show_action = QAction("Show", self) show_action.triggered.connect(self._show) launch_favorite_action = QAction( QIcon(":resources/icons/favorite.svg"), "Blender", self) launch_favorite_action.triggered.connect(self.launch_favorite) tray_menu = QMenu() tray_menu.setFont(self.font) tray_menu.addAction(launch_favorite_action) tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addAction(quit_action) # Setup tray icon self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon( QIcon(taskbar_icon_paths[get_taskbar_icon_color()])) self.tray_icon.setToolTip("Blender Launcher") self.tray_icon.activated.connect(self.tray_icon_activated) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.messageClicked.connect(self._show) self.tray_icon.show() # Forse style update if polish is True: self.style().unpolish(self.app) self.style().polish(self.app) # Show window if get_launch_minimized_to_tray() is False: self._show() def show_update_window(self): download_widgets = [] download_widgets.extend(self.DownloadsStableListWidget.items()) download_widgets.extend(self.DownloadsDailyListWidget.items()) download_widgets.extend(self.DownloadsExperimentalListWidget.items()) for widget in download_widgets: if widget.state == DownloadState.DOWNLOADING: self.dlg = DialogWindow( self, title="Warning", text="In order to update Blender Launcher<br> \ complete all active downloads!", accept_text="OK", cancel_text=None, icon=DialogIcon.WARNING) return self.tray_icon.hide() self.close() self.update_window = UpdateWindow(self, self.latest_tag) def _show(self): platform = get_platform() if platform == "Windows": self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) self.show() self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) self.show() elif platform == "Linux": self.show() self.activateWindow() self.set_status() self.show_signal.emit() def show_message(self, message, value=None, type=None): if (type == MessageType.DOWNLOADFINISHED and get_enable_download_notifications() is False): return elif (type == MessageType.NEWBUILDS and get_enable_new_builds_notifications() is False): return if value not in self.notification_pool: if value is not None: self.notification_pool.append(value) self.tray_icon.showMessage( "Blender Launcher", message, QIcon(taskbar_icon_paths[get_taskbar_icon_color()]), 10000) def launch_favorite(self): try: self.favorite.launch() except Exception: self.dlg = DialogWindow(self, text="Favorite build not found!", accept_text="OK", cancel_text=None) def tray_icon_activated(self, reason): if reason == QSystemTrayIcon.Trigger: self._show() elif reason == QSystemTrayIcon.MiddleClick: self.launch_favorite() def quit(self): download_widgets = [] download_widgets.extend(self.DownloadsStableListWidget.items()) download_widgets.extend(self.DownloadsDailyListWidget.items()) download_widgets.extend(self.DownloadsExperimentalListWidget.items()) for widget in download_widgets: if widget.state == DownloadState.DOWNLOADING: self.dlg = DialogWindow( self, title="Warning", text="Active downloads in progress!<br>\ Are you sure you want to quit?", accept_text="Yes", cancel_text="No", icon=DialogIcon.WARNING) self.dlg.accepted.connect(self.destroy) return self.destroy() def destroy(self): if self.timer is not None: self.timer.cancel() self.tray_icon.hide() self.app.quit() def draw_library(self, clear=False): self.set_status("Reading local builds") if clear: self.timer.cancel() self.scraper.quit() self.DownloadsStableListWidget._clear() self.DownloadsDailyListWidget._clear() self.DownloadsExperimentalListWidget._clear() self.started = True self.favorite = None self.LibraryStableListWidget._clear() self.LibraryDailyListWidget._clear() self.LibraryExperimentalListWidget._clear() self.LibraryCustomListWidget._clear() self.library_drawer = LibraryDrawer() self.library_drawer.build_found.connect(self.draw_to_library) self.library_drawer.finished.connect(self.draw_downloads) self.library_drawer.start() def reload_custom_builds(self): self.LibraryCustomListWidget._clear() self.library_drawer = LibraryDrawer() self.library_drawer.build_found.connect(self.draw_to_library) self.library_drawer.start() def draw_downloads(self): for page in self.DownloadsToolBox.pages: page.set_info_label_text("Checking for new builds") self.cashed_builds.clear() self.new_downloads = False self.app_state = AppState.CHECKINGBUILDS self.set_status("Checking for new builds") self.scraper = Scraper(self, self.manager) self.scraper.links.connect(self.draw_to_downloads) self.scraper.new_bl_version.connect(self.set_version) self.scraper.error.connect(self.connection_error) self.scraper.finished.connect(self.scraper_finished) self.scraper.start() def connection_error(self): set_locale() utcnow = strftime(('%H:%M'), localtime()) self.set_status("Connection Error at " + utcnow) self.app_state = AppState.IDLE self.timer = threading.Timer(600.0, self.draw_downloads) self.timer.start() def scraper_finished(self): if self.new_downloads and not self.started: self.show_message("New builds of Blender is available!", type=MessageType.NEWBUILDS) for list_widget in self.DownloadsToolBox.list_widgets: for widget in list_widget.widgets: if widget.build_info not in self.cashed_builds: widget.destroy() set_locale() utcnow = strftime(('%H:%M'), localtime()) self.set_status("Last check at " + utcnow) self.app_state = AppState.IDLE for page in self.DownloadsToolBox.pages: page.set_info_label_text("No new builds available") self.timer = threading.Timer(600.0, self.draw_downloads) self.timer.start() self.started = False def draw_from_cashed(self, build_info): if self.app_state == AppState.IDLE: for cashed_build in self.cashed_builds: if build_info == cashed_build: self.draw_to_downloads(cashed_build, False) return def draw_to_downloads(self, build_info, show_new=True): if self.started: show_new = False if build_info not in self.cashed_builds: self.cashed_builds.append(build_info) branch = build_info.branch if (branch == 'stable') or (branch == 'lts'): downloads_list_widget = self.DownloadsStableListWidget library_list_widget = self.LibraryStableListWidget elif branch == 'daily': downloads_list_widget = self.DownloadsDailyListWidget library_list_widget = self.LibraryDailyListWidget else: downloads_list_widget = self.DownloadsExperimentalListWidget library_list_widget = self.LibraryExperimentalListWidget if not library_list_widget.contains_build_info(build_info) and \ not downloads_list_widget.contains_build_info(build_info): item = BaseListWidgetItem(build_info.commit_time) widget = DownloadWidget(self, downloads_list_widget, item, build_info, show_new) downloads_list_widget.add_item(item, widget) self.new_downloads = True def draw_to_library(self, path, show_new=False): branch = Path(path).parent.name if (branch == 'stable') or (branch == 'lts'): list_widget = self.LibraryStableListWidget elif branch == 'daily': list_widget = self.LibraryDailyListWidget elif branch == 'experimental': list_widget = self.LibraryExperimentalListWidget elif branch == 'custom': list_widget = self.LibraryCustomListWidget else: return item = BaseListWidgetItem() widget = LibraryWidget(self, item, path, list_widget, show_new) list_widget.insert_item(item, widget) def set_status(self, status=None): if status is not None: self.status = status self.statusbarLabel.setText("Status: {0}".format(self.status)) def set_version(self, latest_tag): current_tag = self.app.applicationVersion() latest_ver = re.sub(r'\D', '', latest_tag) current_ver = re.sub(r'\D', '', current_tag) if int(latest_ver) > int(current_ver): if latest_tag not in self.notification_pool: self.NewVersionButton.setText("Update to version {0}".format( latest_tag.replace('v', ''))) self.NewVersionButton.show() self.show_message( "New version of Blender Launcher is available!", latest_tag) self.latest_tag = latest_tag def show_settings_window(self): self.settings_window = SettingsWindow(self) def clear_temp(self): temp_folder = Path(get_library_folder()) / ".temp" self.remover = Remover(temp_folder) self.remover.start() def closeEvent(self, event): event.ignore() self.hide() self.close_signal.emit() def new_connection(self): self._show() def dragEnterEvent(self, e): if e.mimeData().hasFormat('text/plain'): e.accept() else: e.ignore() def dropEvent(self, e): print(e.mimeData().text())
class Controller(Ui_UI): def __init__(self): super(Controller, self).__init__() # self.init() def init(self): QApplication.setQuitOnLastWindowClosed(False) self.mainWin = QMainWindow() self.mainWin.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.setupUi(self.mainWin) # print("开始gui创建111") # self.mainWin.show() self.set_icon() self.set_menus() self.set_icon_menus() def set_icon(self): self.icon = QSystemTrayIcon(self.mainWin) try: # icon_dir = Path("gui") # url = "{0}{2}{1}{3}{1}{4}{1}{5}".format(icon_dir.path["base"], icon_dir.path["sep"], "UI", "Data", "Assets", "icon.png") icon_dir = Path("Data") url = "{0}{2}{1}{3}{1}{4}".format(icon_dir.path["base"], icon_dir.path["sep"], "statics", "images", "icon.png") print(">>>>url", url) self.icon.setIcon(QIcon(url)) except Exception as err: print("【err】【Controller ->set_icon】 icon图片文件错误") def set_menus(self): self.menu = {} self.menu["show"] = QAction('&显示(Show)', triggered=self.mainWin.show) self.menu["hide"] = QAction('&隐藏(Hide)', triggered=self.mainWin.hide) self.menu["exit"] = QAction('&退出(Exit)', triggered=self.exit) self.menus = QMenu() for act in self.menu: self.menus.addAction(self.menu[act]) def set_icon_menus(self): self.icon.setContextMenu(self.menus) self.icon.activated.connect(self.click_btn) self.icon.show() print(">>>>show icon") def click_btn(self, reason): if reason == 2 or reason == 3: self.mainWin.show() def exit(self): re = QMessageBox.question(self.mainWin, "退出", "是否退出", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if re == QMessageBox.Yes: self.mainWin.hide() self.icon.hide() print("退出成功") sys.exit(0) def restart(self): # if self.main_exit: # self.main_exit() python = sys.executable os.execl(python, python, *sys.argv) def txt_run_status(self, info): self.txtIOCenterLink.setText("状态:" + info) def txt_run(self, info): self.txtLogRun.setText(info) def txt_err(self, info): self.txtLogErr.setText(info) def txt_recv(self, info): self.txtIoReq.setText(info) def txt_send(self, info): self.txtIoSend.setText(info) def txt_web(self, info): self.txtWeb.setText(info)
class MainWindow(QMainWindow, Ui_main.Ui_MainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setupUi(self) # 时钟计时器 self.clockTimer = QTimer() self.clockTimer.timeout.connect(self.singer) self.clockTimer.start(1 * 1000) self.workerTimer = QTimer() self.workerTimer.timeout.connect(self.worker) self.workerTimer.start(60 * 1000) self.setup() self.bindSignal() self.updateGui() self.worker() self.painter() def setup(self): self.setWindowIcon(QIcon(':/logo.png')) # 初始化剪贴板 self.clip = QApplication.clipboard() # 闹钟提示框 self.remindBox = RemindMessageBox(self) # 初始化窗口 self.settingDialog = SettingDialog(self) self.tafSender = TafSender(self) self.trendSender = TrendSender(self) self.sigmetSender = SigmetSender(self) self.reSender = ReSender(self) self.tafEditor = TafEditor(self, self.tafSender) self.trendEditor = TrendEditor(self, self.trendSender) self.sigmetEditor = SigmetEditor(self, self.sigmetSender) if boolean(conf.value('General/Serious')): self.taskBrowser = TaskBrowser(self) self.taskTafSender = TaskTafSender(self) self.taskTafEditor = TaskTafEditor(self, self.taskTafSender) self.setRecent() self.setTable() self.setContractMenu() # 设置切换联系人菜单 self.setSysTray() self.setStatus() self.setThread() self.setSound() def bindSignal(self): context.taf.warningSignal.connect(self.dialer) context.taf.clockSignal.connect(self.remindTaf) # 连接菜单信号 self.tafAction.triggered.connect(self.tafEditor.show) self.trendAction.triggered.connect(self.trendEditor.show) self.sigmetAction.triggered.connect(self.sigmetEditor.show) # 连接设置对话框的槽 self.settingAction.triggered.connect(self.settingDialog.exec_) self.settingAction.setIcon(QIcon(':/setting.png')) self.openDocsAction.triggered.connect(self.openDocs) self.reportIssueAction.triggered.connect(self.reportIssue) self.checkUpgradeAction.triggered.connect( self.checkUpgradeThread.start) # 连接关于信息的槽 self.aboutAction.triggered.connect(self.about) # 连接切换联系人的槽 self.contractsActionGroup.triggered.connect(self.changeContract) self.contractsActionGroup.triggered.connect(self.settingDialog.load) def setRecent(self): self.clock = Clock(self, self.tipsLayout) self.tipsLayout.addSpacerItem( QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) self.currentTaf = CurrentTaf(self, self.tipsLayout) self.recentFt = RecentMessage(self, self.recentLayout, 'FT') self.recentFc = RecentMessage(self, self.recentLayout, 'FC') self.recentSigmet = RecentMessage(self, self.recentLayout, 'WS') self.recentTrend = RecentMessage(self, self.recentLayout, 'TREND') def setTable(self): self.tafTable = TafTable(self, self.tafLayout) self.metarTable = MetarTable(self, self.metarLayout) self.sigmetTable = SigmetTable(self, self.sigmetLayout) def setContractMenu(self): self.contractsActionGroup = QActionGroup(self) self.contractsActionGroup.addAction(self.contractNo) contacts = db.query(User).all() for person in contacts: setattr(self, 'contract' + str(person.id), QAction(self)) target = getattr(self, 'contract' + str(person.id)) target.setText(person.name) target.setCheckable(True) self.contractsActionGroup.addAction(target) self.contractsMenu.addAction(target) self.updateContractMenu() def setSysTray(self): # 设置系统托盘 self.tray = QSystemTrayIcon(self) self.tray.setIcon(QIcon(':/logo.png')) self.tray.show() # 连接系统托盘的槽 self.tray.activated.connect(self.restoreWindow) self.trayMenu = QMenu(self) self.trayMenu.addAction(self.contractsMenu.menuAction()) self.trayMenu.addAction(self.settingAction) self.trayMenu.addAction(self.aboutAction) self.trayMenu.addSeparator() self.trayMenu.addAction(self.quitAction) self.tray.setContextMenu(self.trayMenu) title = QCoreApplication.translate( 'MainWindow', 'Terminal Aerodrome Forecast Encoding Software') message = '{} v {}'.format(title, __version__) self.tray.setToolTip(message) def setStatus(self): self.webApiStatus = WebAPIStatus(self, self.statusBar) self.callServiceStatus = CallServiceStatus(self, self.statusBar, last=True) # self.statusBar.setStyleSheet('QStatusBar::item{border: 0px}') def setThread(self): self.workThread = WorkThread(self) self.workThread.finished.connect(self.updateMessage) self.callThread = CallThread(self) self.firInfoThread = FirInfoThread() self.checkUpgradeThread = CheckUpgradeThread(self) self.checkUpgradeThread.doneSignal.connect(self.checkUpgrade) def setSound(self): self.ringSound = Sound('ring.wav', conf.value('Monitor/RemindTAFVolume')) self.notificationSound = Sound('notification.wav', 100) self.alarmSound = Sound('alarm.wav', conf.value('Monitor/WarnTAFVolume')) self.trendSound = Sound('trend.wav', conf.value('Monitor/RemindTrendVolume')) self.sigmetSound = Sound('sigmet.wav', conf.value('Monitor/RemindSIGMETVolume')) self.settingDialog.warnTafVolume.valueChanged.connect( lambda vol: self.alarmSound.play(volume=vol, loop=False)) self.settingDialog.remindTafVolume.valueChanged.connect( lambda vol: self.ringSound.play(volume=vol, loop=False)) self.settingDialog.remindTrendVolume.valueChanged.connect( lambda vol: self.trendSound.play(volume=vol, loop=False)) self.settingDialog.remindSigmetVolume.valueChanged.connect( lambda vol: self.sigmetSound.play(volume=vol, loop=False)) def changeContract(self): target = self.contractsActionGroup.checkedAction() if self.contractNo == target: conf.setValue('Monitor/SelectedMobile', '') logger.info('关闭电话提醒') else: name = target.text() person = db.query(User).filter_by(name=name).first() mobile = person.mobile if person else '' conf.setValue('Monitor/SelectedMobile', mobile) logger.info('切换联系人 %s %s' % (name, mobile)) def event(self, event): if event.type() == QEvent.WindowStateChange and self.isMinimized(): # 此时窗口已经最小化, # 从任务栏中移除窗口 self.setWindowFlags(self.windowFlags() & Qt.Tool) self.tray.show() return True else: return super(self.__class__, self).event(event) def keyPressEvent(self, event): if boolean(conf.value('General/Serious')): if event.modifiers() == (Qt.ShiftModifier | Qt.ControlModifier): if event.key() == Qt.Key_P: self.taskTafEditor.show() if event.key() == Qt.Key_T: self.taskBrowser.show() def closeEvent(self, event): if event.spontaneous(): event.ignore() self.hide() else: self.tafSender.setAttribute(Qt.WA_DeleteOnClose) self.trendSender.setAttribute(Qt.WA_DeleteOnClose) self.sigmetSender.setAttribute(Qt.WA_DeleteOnClose) self.reSender.setAttribute(Qt.WA_DeleteOnClose) self.tafEditor.setAttribute(Qt.WA_DeleteOnClose) self.trendEditor.setAttribute(Qt.WA_DeleteOnClose) self.sigmetEditor.setAttribute(Qt.WA_DeleteOnClose) self.settingDialog.setAttribute(Qt.WA_DeleteOnClose) self.tafSender.close() self.trendSender.close() self.sigmetSender.close() self.reSender.close() self.tafEditor.close() self.trendEditor.close() self.sigmetEditor.close() self.settingDialog.close() if boolean(conf.value('General/Serious')): self.taskTafSender.setAttribute(Qt.WA_DeleteOnClose) self.taskTafEditor.setAttribute(Qt.WA_DeleteOnClose) self.taskBrowser.setAttribute(Qt.WA_DeleteOnClose) self.taskTafSender.close() self.taskTafEditor.close() self.taskBrowser.close() self.tray.hide() event.accept() def restoreWindow(self, reason): if reason == QSystemTrayIcon.Trigger: self.showNormal() def singer(self): warnSwitch = self.warnTafAction.isChecked() trendSwitch = boolean(conf.value('Monitor/RemindTrend')) # 管理趋势声音 utc = datetime.datetime.utcnow() if trendSwitch and utc.minute in (58, 59): self.trendSound.play() else: self.trendSound.stop() # 管理报文告警声音 if warnSwitch and context.taf.isWarning(): self.alarmSound.play() else: self.alarmSound.stop() def worker(self): self.workThread.start() def painter(self): utc = datetime.datetime.utcnow() nextTime = utc.replace(microsecond=0, second=0, minute=0) + datetime.timedelta(hours=1, minutes=10) delta = nextTime - utc QTimer.singleShot(delta.total_seconds() * 1000, self.painter) if conf.value('Monitor/FirApiURL'): self.firInfoThread.start() def dialer(self, test=False): callSwitch = conf.value('Monitor/SelectedMobile') if callSwitch and context.taf.isWarning() or test: self.callThread.start() def remindTaf(self, tt): remindSwitch = boolean(conf.value('Monitor/RemindTAF')) if not remindSwitch: return None state = context.taf.state() clock = state[tt]['clock'] period = state[tt]['period'] sent = state[tt]['sent'] warnning = state[tt]['warnning'] if clock and not warnning and not sent: current = tt + period[2:] text = QCoreApplication.translate( 'MainWindow', 'Time to post {}').format(current) self.ringSound.play() self.remindBox.setText(text) ret = self.remindBox.exec_() if not ret: QTimer.singleShot(1000 * 60 * 5, lambda: self.remindTaf(tt)) self.ringSound.stop() def remindSigmet(self): remindSwitch = boolean(conf.value('Monitor/RemindSIGMET')) if not remindSwitch: return None text = QCoreApplication.translate('MainWindow', 'Time to post {}').format('SIGMET') self.sigmetSound.play() self.remindBox.setText(text) ret = self.remindBox.exec_() if not ret: QTimer.singleShot(1000 * 60 * 5, self.remindSigmet) self.sigmetSound.stop() def updateMessage(self): listen = Listen(parent=self) [listen(i) for i in ('FC', 'FT', 'SA', 'SP')] self.updateGui() def updateGui(self): self.updateTable() self.updateRecent() self.updateContractMenu() logger.debug('Update GUI') def updateContractMenu(self): mobile = conf.value('Monitor/SelectedMobile') person = db.query(User).filter_by(mobile=mobile).first() if person: action = getattr(self, 'contract' + str(person.id), None) if action: action.setChecked(True) else: self.contractNo.setChecked(True) def updateRecent(self): self.currentTaf.updateGui() self.recentFt.updateGui() self.recentFc.updateGui() self.recentSigmet.updateGui() self.recentTrend.updateGui() def updateTable(self): self.tafTable.updateGui() self.metarTable.updateGui() self.sigmetTable.updateGui() def about(self): title = QCoreApplication.translate( 'MainWindow', 'Terminal Aerodrome Forecast Encoding Software') head = '<b>{}</b> v <a href="https://github.com/up1and/tafor">{}</a>'.format( title, __version__) description = QCoreApplication.translate( 'MainWindow', '''The software is used to encode and post terminal aerodrome forecast, trend forecast, significant meteorological information, monitor the message, return the alarm by sound or telephone''' ) tail = QCoreApplication.translate( 'MainWindow', '''The project is under GPL-2.0 License, Pull Request and Issue are welcome''' ) copyright = '<br/>© UP1AND 2018' text = '<p>'.join([head, description, tail, copyright]) self.showNormal() QMessageBox.about(self, title, text) def openDocs(self): devDocs = os.path.join(BASEDIR, '../docs/_build/html/index.html') releaseDocs = os.path.join(BASEDIR, 'docs/_build/html/index.html') if os.path.exists(devDocs): url = QUrl.fromLocalFile(devDocs) elif os.path.exists(releaseDocs): url = QUrl.fromLocalFile(releaseDocs) else: url = QUrl('https://tafor.readthedocs.io') QDesktopServices.openUrl(url) def reportIssue(self): QDesktopServices.openUrl( QUrl('https://github.com/up1and/tafor/issues')) def checkUpgrade(self, data): hasNewVersion = checkVersion(data.get('tag_name', __version__), __version__) if not hasNewVersion: return False download = 'https://github.com/up1and/tafor/releases' title = QCoreApplication.translate('MainWindow', 'Check for Updates') text = QCoreApplication.translate( 'MainWindow', 'New version found {}, do you want to download now?').format( data.get('tag_name')) ret = QMessageBox.question(self, title, text) if ret == QMessageBox.Yes: QDesktopServices.openUrl(QUrl(download))
class TrayIcon(QObject): def __init__(self): super().__init__() self._supported = QSystemTrayIcon.isSystemTrayAvailable() self._context_menu = None self._enabled = False self._trayicon = None self._state_icons = {} for state in ( 'disconnected', 'disabled', 'enabled', ): icon = QIcon(':/state-%s.svg' % state) if hasattr(icon, 'setIsMask'): icon.setIsMask(True) self._state_icons[state] = icon self._machine = None self._machine_state = 'disconnected' self._is_running = False self._update_state() def set_menu(self, menu): self._context_menu = menu if self._enabled: self._trayicon.setContextMenu(menu) def log(self, level, message): if self._enabled: if level <= log.INFO: icon = QSystemTrayIcon.Information timeout = 10 elif level <= log.WARNING: icon = QSystemTrayIcon.Warning timeout = 15 else: icon = QSystemTrayIcon.Critical timeout = 25 self._trayicon.showMessage(__software_name__.capitalize(), message, icon, timeout * 1000) else: if level <= log.INFO: icon = QMessageBox.Information elif level <= log.WARNING: icon = QMessageBox.Warning else: icon = QMessageBox.Critical msgbox = QMessageBox() msgbox.setText(message) msgbox.setIcon(icon) msgbox.exec_() def is_supported(self): return self._supported def enable(self): if not self._supported: return self._trayicon = QSystemTrayIcon() # On OS X, the context menu is activated with either mouse buttons, # and activation messages are still sent, so ignore those... if not sys.platform.startswith('darwin'): self._trayicon.activated.connect(self._on_activated) if self._context_menu is not None: self._trayicon.setContextMenu(self._context_menu) self._enabled = True self._update_state() self._trayicon.show() def disable(self): if not self._enabled: return self._trayicon.hide() self._trayicon = None self._enabled = False def is_enabled(self): return self._enabled def update_machine_state(self, machine, state): self._machine = machine self._machine_state = state self._update_state() def update_output(self, enabled): self._is_running = enabled self._update_state() clicked = pyqtSignal() def _update_state(self): if self._machine_state not in ('initializing', 'connected'): state = 'disconnected' else: state = 'enabled' if self._is_running else 'disabled' icon = self._state_icons[state] if not self._enabled: return machine_state = _('{machine} is {state}').format( machine=_(self._machine), state=_(self._machine_state), ) if self._is_running: output_state = _('output is enabled') else: output_state = _('output is disabled') self._trayicon.setIcon(icon) self._trayicon.setToolTip( 'Plover:\n- %s\n- %s' % (output_state, machine_state) ) def _on_activated(self, reason): if reason == QSystemTrayIcon.Trigger: self.clicked.emit()
class Qt4SysTrayIcon: def __init__(self): self.snapshots = snapshots.Snapshots() self.config = self.snapshots.config self.decode = None if len(sys.argv) > 1: if not self.config.setCurrentProfile(sys.argv[1]): logger.warning("Failed to change Profile_ID %s" %sys.argv[1], self) self.qapp = qt4tools.createQApplication(self.config.APP_NAME) translator = qt4tools.translator() self.qapp.installTranslator(translator) self.qapp.setQuitOnLastWindowClosed(False) import icon self.icon = icon self.qapp.setWindowIcon(icon.BIT_LOGO) self.status_icon = QSystemTrayIcon(icon.BIT_LOGO) #self.status_icon.actionCollection().clear() self.contextMenu = QMenu() self.menuProfileName = self.contextMenu.addAction(_('Profile: "%s"') % self.config.profileName()) qt4tools.setFontBold(self.menuProfileName) self.contextMenu.addSeparator() self.menuStatusMessage = self.contextMenu.addAction(_('Done')) self.menuProgress = self.contextMenu.addAction('') self.menuProgress.setVisible(False) self.contextMenu.addSeparator() self.btnPause = self.contextMenu.addAction(icon.PAUSE, _('Pause snapshot process')) action = lambda: os.kill(self.snapshots.pid(), signal.SIGSTOP) self.btnPause.triggered.connect(action) self.btnResume = self.contextMenu.addAction(icon.RESUME, _('Resume snapshot process')) action = lambda: os.kill(self.snapshots.pid(), signal.SIGCONT) self.btnResume.triggered.connect(action) self.btnResume.setVisible(False) self.btnStop = self.contextMenu.addAction(icon.STOP, _('Stop snapshot process')) self.btnStop.triggered.connect(self.onBtnStop) self.contextMenu.addSeparator() self.btnDecode = self.contextMenu.addAction(icon.VIEW_SNAPSHOT_LOG, _('decode paths')) self.btnDecode.setCheckable(True) self.btnDecode.setVisible(self.config.snapshotsMode() == 'ssh_encfs') self.btnDecode.toggled.connect(self.onBtnDecode) self.openLog = self.contextMenu.addAction(icon.VIEW_LAST_LOG, _('View Last Log')) self.openLog.triggered.connect(self.onOpenLog) self.startBIT = self.contextMenu.addAction(icon.BIT_LOGO, _('Start BackInTime')) self.startBIT.triggered.connect(self.onStartBIT) self.status_icon.setContextMenu(self.contextMenu) self.pixmap = icon.BIT_LOGO.pixmap(24) self.progressBar = QProgressBar() self.progressBar.setMinimum(0) self.progressBar.setMaximum(100) self.progressBar.setValue(0) self.progressBar.setTextVisible(False) self.progressBar.resize(24, 6) self.progressBar.render(self.pixmap, sourceRegion = QRegion(0, -14, 24, 6), flags = QWidget.RenderFlags(QWidget.DrawChildren)) self.first_error = self.config.notify() self.popup = None self.last_message = None self.timer = QTimer() self.timer.timeout.connect(self.updateInfo) self.ppid = os.getppid() def prepairExit(self): self.timer.stop() if not self.status_icon is None: self.status_icon.hide() self.status_icon = None if not self.popup is None: self.popup.deleteLater() self.popup = None self.qapp.processEvents() def run(self): self.status_icon.show() self.timer.start(500) logger.info("[qt4systrayicon] begin loop", self) self.qapp.exec_() logger.info("[qt4systrayicon] end loop", self) self.prepairExit() def updateInfo(self): if not tools.processAlive(self.ppid): self.prepairExit() self.qapp.exit(0) return paused = tools.processPaused(self.snapshots.pid()) self.btnPause.setVisible(not paused) self.btnResume.setVisible(paused) message = self.snapshots.takeSnapshotMessage() if message is None and self.last_message is None: message = (0, _('Working...')) if not message is None: if message != self.last_message: self.last_message = message if self.decode: message = (message[0], self.decode.log(message[1])) self.menuStatusMessage.setText('\n'.join(tools.wrapLine(message[1],\ size = 80,\ delimiters = '',\ new_line_indicator = '') \ )) self.status_icon.setToolTip(message[1]) pg = progress.ProgressFile(self.config) if pg.fileReadable(): pg.load() percent = pg.intValue('percent') if percent != self.progressBar.value(): self.progressBar.setValue(percent) self.progressBar.render(self.pixmap, sourceRegion = QRegion(0, -14, 24, 6), flags = QWidget.RenderFlags(QWidget.DrawChildren)) self.status_icon.setIcon(QIcon(self.pixmap)) self.menuProgress.setText(' | '.join(self.getMenuProgress(pg))) self.menuProgress.setVisible(True) else: self.status_icon.setIcon(self.icon.BIT_LOGO) self.menuProgress.setVisible(False) def getMenuProgress(self, pg): d = (('sent', _('Sent:')), \ ('speed', _('Speed:')),\ ('eta', _('ETA:'))) for key, txt in d: value = pg.strValue(key, '') if not value: continue yield txt + ' ' + value def onStartBIT(self): profileID = self.config.currentProfile() cmd = ['backintime-qt4',] if not profileID == '1': cmd += ['--profile-id', profileID] proc = subprocess.Popen(cmd) def onOpenLog(self): dlg = logviewdialog.LogViewDialog(self, systray = True) dlg.decode = self.decode dlg.cbDecode.setChecked(self.btnDecode.isChecked()) dlg.exec_() def onBtnDecode(self, checked): if checked: self.decode = encfstools.Decode(self.config) self.last_message = None self.updateInfo() else: self.decode = None def onBtnStop(self): os.kill(self.snapshots.pid(), signal.SIGKILL) self.btnStop.setEnabled(False) self.btnPause.setEnabled(False) self.btnResume.setEnabled(False) self.snapshots.setTakeSnapshotMessage(0, 'Snapshot terminated')
class DemoImpl(QDialog): def __init__(self, *args): super(DemoImpl, self).__init__(*args) loadUi('dict2.ui',self) self.setLayout(self.verticalLayout) self.plainTextEdit.setReadOnly(True) self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint | Qt.WindowMinMaxButtonsHint) self.trayicon = QSystemTrayIcon() self.traymenu = QMenu() self.quitAction = QAction('GQuit', self) self.quitAction.triggered.connect(self.close) self.quitAction.setShortcut(QKeySequence('Ctrl+q')) self.addAction(self.quitAction) self.traymenu.addAction('&Normal', self.showNormal, QKeySequence('Ctrl+n')) self.traymenu.addAction('Mi&nimize', self.showMinimized, QKeySequence('Ctrl+i')) self.traymenu.addAction('&Maximum', self.showMaximized, QKeySequence('Ctrl+m')) self.traymenu.addAction('&Quit',self.close, QKeySequence('Ctrl+q')) self.trayicon.setContextMenu(self.traymenu) self.ticon = QIcon('icon_dict2.ico') self.trayicon.setIcon(self.ticon) self.trayicon.setToolTip('YYDict') self.trayicon.activated.connect(self.on_systemTrayIcon_activated) self.traymsg_firstshow = True self.button1.clicked.connect(self.searchword) self.comboBox.activated.connect(self.searchword) def changeEvent(self, event): if event.type() == QEvent.WindowStateChange: if self.isMinimized(): self.hide() self.trayicon.show() if self.traymsg_firstshow: self.trayicon.showMessage('', 'YYDict is running', QSystemTrayIcon.Information, 2000) self.traymsg_firstshow = False else: self.trayicon.hide() def closeEvent(self, event): self.trayicon.hide() @pyqtSlot(QSystemTrayIcon.ActivationReason) def on_systemTrayIcon_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: self.activateWindow() self.showNormal() @pyqtSlot() def searchword(self): word = self.lineEdit.text().strip() if not len(word): self.plainTextEdit.setPlainText('') self.lineEdit.setFocus(Qt.MouseFocusReason) else: self.workThread = WorkThread(word, self.comboBox.currentIndex()) self.workThread.received.connect(self.updateResult) self.workThread.start() self.workThread.wait() @pyqtSlot('QString') def updateResult(self, rt): self.plainTextEdit.setPlainText(rt) self.lineEdit.selectAll() self.lineEdit.setFocus(Qt.MouseFocusReason)
class MyApp(QtWidgets.QMainWindow): mouseLeaveTimer=0 def __init__(self): # Ui_MainWindow.__init__(self) #自己有__init__函数时,不会默认调用基类的__init__函数 # 因为这里重写了__init__将基类的覆盖掉了,故需要主动调用之 # QtWidgets.QMainWindow.__init__(self) # super(MyApp,self).__init__() #上面两句的作用是相同的,下面这句是python3的新写法 super().__init__() # Get the Screen size self.screenWidth=QDesktopWidget().availableGeometry().width() self.screenHeight=QDesktopWidget().availableGeometry().height() #初始化字体 font=QFont('黑体') font.setPointSize(12) app.setFont(font) # ColorSetting self.bgColor=QColor(66,66,77,88) # # self.setupUi(self) self.initUI() #用来控制半透明的bg面板自动消失 self.timer=QTimer() self.timer.start(30) self.setGeometry(0,30,self.screenWidth,self.screenHeight//3) #Flagsq self.IsMouseHover=False self.MouseOver=False self.Locked=False self.Hidden=False self.isDrag=False self.isResize=False #变量初始化 GLOBAL.WINDOWWIDTH=self.width() GLOBAL.WINDOWHEIGHT=self.height() self.bullets=[] self.dragPos=QPoint(22,22) self.savedName='' # self.screenBuffer=QBitmap(GLOBAL.WINDOWWIDTH,GLOBAL.WINDOWHEIGHT) # self.bufferPainter=QPainter(self.screenBuffer) # self.picture=QPicture() # 建立connection和slot的回调连接 self.createConnections() # 连接到nodejs建立的服务器 self.connect2Server() def initUI(self): #构建托盘 self.trayIcon=QSystemTrayIcon(self) self.trayIcon.setIcon(QtGui.QIcon("tmpIcon.ico")) self.trayIcon.show() self.trayIcon.setToolTip('BulletGo') # 构建托盘菜单 action_quit=QAction('退出',self) action_quit.triggered.connect(self.exitApp) action_switchLock=QAction('锁定/解锁(F6)',self) action_switchLock.triggered.connect(self.switchLock) action_showHide=QAction('显示/隐藏(F7)',self) action_showHide.triggered.connect(lambda:self.switchVisible(self)) action_Settings=QAction('设置',self) action_Settings.triggered.connect(lambda:self.switchVisible(self.settingWindow)) trayIconMenu=QtWidgets.QMenu(self) trayIconMenu.addAction(action_switchLock) trayIconMenu.addAction(action_showHide) trayIconMenu.addSeparator() trayIconMenu.addAction(action_Settings) trayIconMenu.addAction(action_quit) #设定快捷键 QtWidgets.QShortcut(QtGui.QKeySequence(\ QtCore.Qt.Key_F7),self,\ (lambda:self.switchVisible(self.settingWindow))) QtWidgets.QShortcut(QtGui.QKeySequence(\ QtCore.Qt.Key_F6),self,\ (self.switchLock)) self.trayIcon.setContextMenu(trayIconMenu) # 保障不按下鼠标也追踪mouseMove事件 self.setMouseTracking(True) self.setMinimumSize(600,260) self.setWindowTitle("BulletGo") sizeGrip=QtWidgets.QSizeGrip(self) self.setWindowFlags(Qt.FramelessWindowHint\ |Qt.WindowStaysOnTopHint|Qt.Window|\ Qt.X11BypassWindowManagerHint) #Plan A self.setAttribute(Qt.WA_TranslucentBackground,True) #这一句是给Mac系统用的,防止它绘制(很黯淡的)背景 self.setAutoFillBackground(False) QSizeGrip(self).setVisible(True) sizeGrip.setVisible(True) #Plan B 失败 # palette=QPalette() # color=QColor(190, 230, 250) # color.setAlphaF(0.6) # palette.setBrush(self.backgroundRole(), color) # self.setPalette(palette) # self.setAutoFillBackground(True) # self.setBackgroundRole(QPalette.Window) #创建房间的Button和 输入框 self.roomName=QPlainTextEdit() self.roomName.setPlaceholderText('请输入房间名') # self.roomName.resize(50,20) # self.roomName.move(0,0) # self.roomName.setBackgroundVisible(False) self.createBtn=QPushButton("创建/进入") self.hideBtn=QPushButton('隐藏本设置窗口') self.hideBtn.clicked.connect(self.joinRoom) # lambda:self.switchVisible(self.settingWindow)) # self.createBtn.resize(50,20) # self.move(0,100) # self.d settingLayout=QVBoxLayout() hLayout=QHBoxLayout() settingLayout.addWidget(self.roomName) hLayout.addWidget(self.hideBtn) hLayout.addWidget(self.createBtn) self.settingWindow=QWidget() # self.hideBtn=setShortcut(QtGui.QKeySequence('Ctrl+B')) settingLayout.addLayout(hLayout) self.settingWindow.setLayout(settingLayout) # Qt.Tool的作用是 不在任务栏显示 self.settingWindow.setWindowFlags(Qt.FramelessWindowHint|Qt.Tool\ |Qt.X11BypassWindowManagerHint|Qt.Popup) self.roomName.show() self.createBtn.show() self.settingWindow.resize(160,26) self.settingWindow.show() # self.btnFire=QPushButton("Fire",self) # self.btnFire.resize(60,60) # self.btnFire.move(100,30) # self.btnFire.show() # self.btnLock=QPushButton("Lock",self) # self.btnLock.resize(40,40) # self.btnLock.move(self.screenWidth/2,30) # self.btnLock.setFlat(True) # self.btnLock.setIcon(QtGui.QIcon("tmpIcon.png")) # self.btnLock.show() # self.danmakuEditText=QPlainTextEdit(self) # self.danmakuEditText.resize(200,100) # self.danmakuEditText.move(100,100) # self.danmakuEditText.setBackgroundVisible(False) # self.danmakuEditText.show() def joinRoom(self): name=self.roomName.toPlainText() self.socketio.emit('join',name) def connect2Server(self): self.socketio=SocketIO('115.159.102.76/bulletgo_client',80,LoggingNamespace) self.registerEvents() # 开新线程监听服务器发来的消息,否则主线程被阻塞 _thread.start_new_thread(self.socketio.wait,()) def registerEvents(self): self.socketio.on('create_rsp',lambda rsp:self.handleIncomeBullet(rsp)) self.socketio.on('bullet',lambda msg:self.handleIncomeBullet(msg)) self.socketio.on('control_msg',lambda msg:print\ ('---control message--- : '+msg)) self.socketio.on('sys_notification',lambda msg:print\ ('---system notification--- : '+msg)) def handleIncomeBullet(self,bulletMsg): textsAndInfo=self.preProcessText(bulletMsg) if(len(textsAndInfo)>1): self.fireABullet(textsAndInfo[0],self.genQColorFromStr(textsAndInfo[1])) def createRoom_Nodejs(self,name): self.socketio.emit('create_room',name) def fireBtn(self): txt=self.danmakuEditText.toPlainText() tmpbullet=Bullet(txt,GLOBAL.ORANGE,random.randrange(9,16,2)) self.bullets.append(tmpbullet) tmpbullet.prepare() # print(len(self.bullets)) # testStr="line1\nline2\nline3" # textsAndInfo=self.preProcessText(testStr) # print(len(textsAndInfo)) # print def fireABullet(self,txt,color=GLOBAL.ORANGE): tmpbullet=Bullet(txt,color,random.randrange(12,22,2)) self.bullets.append(tmpbullet) tmpbullet.prepare() def createConnections(self): self.timer.timeout.connect(self.update) # self.btnFire.clicked.connect(self.fireBtn) self.createBtn.clicked.connect(\ lambda:self.createRoom_Nodejs\ (self.roomName.toPlainText())) # self.btnLock.clicked.connect(self.switchLock) # self.btnLock.clicked.connect(self.pullMsg) self.trayIcon.activated.connect(self.trayClick) def switchVisible(self,handle): if(handle.isHidden()): handle.activateWindow() handle.setHidden(not handle.isHidden()) def trayClick(self,reason): #单击事件还没设计好 # if(reason==QSystemTrayIcon.Trigger): # self.switchVisible(self) if(reason==QSystemTrayIcon.DoubleClick): self.switchVisible(self.settingWindow) def switchLock(self): self.Locked=not self.Locked '''这个神奇的用法, 在js中也可用''' '''博客搞好后, 这个要单独写个文章''' def genQColorFromStr(self,color): # print(color) return{ 'white':GLOBAL.WHITE, 'green':GLOBAL.GREEN, 'red':GLOBAL.RED, 'pink':GLOBAL.PINK, 'purple':GLOBAL.PURPLE, 'darkblue':GLOBAL.DARKBLUE, 'blue':GLOBAL.BLUE, 'yellow':GLOBAL.YELLOW, 'cyan':GLOBAL.CYAN, 'orange':GLOBAL.ORANGE, '':GLOBAL.ORANGE }[color] def preProcessText(self,string): return string.split('`<') '''---[deprecated]---''' def realPullMsg(self): url='http://danmaku.applinzi.com/message.php' r = requests.post(url,data=self.savedName) r.encoding='utf-8' #预处理收到的字符串 # print(r.text) # r.te textsAndInfo=self.preProcessText(r.text) i=0 # print(textsAndInfo) # print(r.text) if(len(textsAndInfo)>1): while(i<len(textsAndInfo)-1): # print(len(textsAndInfo)) # print('ddddd') # print(i) self.fireABullet(textsAndInfo[i],self.genQColorFromStr(textsAndInfo[i+1])) i+=2 '''---[deprecated]---''' def pullMsg(self): _thread.start_new_thread(self.realPullMsg,()) '''---[deprecated]---''' def createRoom(self): #编码问题实在天坑!!! self.savedName=self.roomName.toPlainText().encode('utf-8')#保存自己的房间号 postData=self.roomName.toPlainText().encode('utf-8') r = requests.post('http://danmaku.applinzi.com/createroom.php',data=postData) r.encoding='utf-8' self.fireABullet(r.text) # print(r.encoding) if(len(r.text)==7): # 开始自动获取服务器上的消息内容 self.pullTimer=QTimer() self.pullTimer.start(2000) self.pullTimer.timeout.connect(self.pullMsg) # print(r.content) # print(r.text) def closeEvent(self,e): e.accept() def mouseReleaseEvent(self,e): if(e.button()==Qt.LeftButton): self.isDrag=False self.isResize=False def mousePressEvent(self,e): if e.button()==Qt.LeftButton: self.LDown=True # self.dragPos=e.globalPos()-self.frameGeometry().topLeft() self.dragPos=e.pos()#效果同上,鼠标相对窗口左上角的位置 if(GLOBAL.WINDOWWIDTH-e.pos().x()<16\ and GLOBAL.WINDOWHEIGHT-e.pos().y()<16): self.topLeft=self.frameGeometry().topLeft() self.isResize=True else: if(not self.Locked): self.isDrag=True # else: # if e.button()==Qt.RightButton: # self.exitApp() e.accept() def mouseMoveEvent(self,e): if(GLOBAL.WINDOWWIDTH-e.pos().x()<16\ and GLOBAL.WINDOWHEIGHT-e.pos().y()<16): #更改鼠标样式 self.setCursor(Qt.SizeFDiagCursor) else: self.setCursor(Qt.ArrowCursor) #如果是Resize,改变窗口大小 if(self.isResize): tmp=e.globalPos()-self.topLeft self.move(self.topLeft) self.resize(tmp.x(),tmp.y()) if (self.isDrag): self.move(e.globalPos()-self.dragPos) e.accept(); def enterEvent(self,e): self.MouseOver=True self.IsMouseHover=True return super(MyApp,self).enterEvent(e) def setMouseHoverFalse(self): # if(not self.MouseOver): self.IsMouseHover=self.MouseOver def leaveEvent(self,e): QTimer.singleShot(800,self.setMouseHoverFalse) self.MouseOver=False return super(MyApp,self).leaveEvent(e) def resizeEvent(self,e): GLOBAL.WINDOWWIDTH=self.width() GLOBAL.WINDOWHEIGHT=self.height() # self.screenBuffer=QBitmap(GLOBAL.WINDOWWIDTH,GLOBAL.WINDOWHEIGHT) # self.bufferPainter=QPainter(self.screenBuffer) # print('resized') # self.repaint() e.accept() def paintEvent(self,e): # Get the Painter painter=QPainter(self) font=QFont('黑体',GLOBAL.BULLETFONTSIZE,QFont.Bold) painter.setFont(font) #Draw a semi-Transparent rect whose size is the same with this window if(self.IsMouseHover and (not self.Locked)): painter.fillRect(0,0,GLOBAL.WINDOWWIDTH,GLOBAL.WINDOWHEIGHT\ ,self.bgColor) # painter.setBackground(QBrush(QColor(123,222,123,122))) #画所有bullet for b in self.bullets: b.draw(painter) for b in self.bullets: if(b.IsExpired): self.bullets.remove(b) # painter.drawPicture(0,0,self.picture) # painter.drawText(30,100,"Hello this is a PyQt5 App我也会说中文") return super(MyApp,self).paintEvent(e) def exitApp(self): self.trayIcon.hide() sys.exit()
class Example(QMainWindow): def __init__(self): super().__init__() self.initUI() self.trayIcon = None self.kill = False self.setupTrayIcon() try: cherrypy_server.start_server() except Exception as e: logging.getLogger(__name__).warn("Problem starting print server! {}".format(e)) traceback.print_exc() def reallyClose(self): self.kill = True self.close() def openConsole(self): from web.server import cfg url = r'https://localhost:{}'.format(cfg.port) webbrowser.open(url) def initUI(self): openAction = QAction("&Open Console", self) openAction.setShortcut('Ctrl+O') openAction.setStatusTip('Open Console') openAction.triggered.connect(self.openConsole) exitAction = QAction(QIcon(os.path.join('web', 'static', 'img', 'exit-icon-3.png')), '&Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(self.reallyClose) self.statusBar() menubar = self.menuBar() fileMenu = menubar.addMenu('&File') fileMenu.addAction(openAction) fileMenu.addSeparator() fileMenu.addAction(exitAction) wrapper = QWidget() txt = QTextEdit() txt.setReadOnly(True) txt.setLineWrapMode(QTextEdit.NoWrap) txt.setUndoRedoEnabled(False) txt.setAcceptDrops(False) txt.setAcceptRichText(False) font = txt.font() font.setFamily("Courier") font.setPointSize(9) txt.setFont(font) policy = txt.sizePolicy() policy.setVerticalStretch(1) txt.setSizePolicy(policy) layout = QGridLayout() layout.addWidget(txt) wrapper.setLayout(layout) self.setCentralWidget(wrapper) def _write(s): txt.moveCursor(QTextCursor.End) txt.insertPlainText(str(s)) txt.moveCursor(QTextCursor.End) setup_logging(_write) self.setGeometry(300, 300, 800, 600) self.setWindowTitle('Antix Print Server') def setupTrayIcon(self): _icon = QIcon(os.path.join('web', 'static', 'img', 'Logo.144x144.png')) self.trayIcon = QSystemTrayIcon(_icon, self) menu = QMenu(self) menu.addAction("Show", self.show) menu.addAction("Hide", self.hide) menu.addAction("Exit", self.reallyClose) self.trayIcon.setContextMenu(menu) self.trayIcon.show() self.trayIcon.showMessage("Antix Printer Server", "Is running") def closeEvent(self, evnt): if self.kill: self.trayIcon.hide() try: cherrypy_server.stop_server() except Exception as e: logging.getLogger(__name__).warn("Problem stopping server! {}".format(e)) qApp.exit(0) else: evnt.ignore() self.hide()
class MainWindow(QWidget): """ Main window of application. It is represented by icon in taskbar. Useful attributes ----------------- addWindow : AddWindow object addActive : boolean If user wants to add new task, 'addWindow' becomes the QWidget for it, and while it is active 'addActive' remains True. editWindow : EditWindow object editActive : boolean If user wants to edit tasks, 'editWindow' becomes the QWidget for it, and while it is active 'editActive' remains True. tray : QSystemTrayIcon object Tray icon and all its attributes like context menu and activated action. timer : QTimer object Timer that fires every second. If one reminder's time is up, shows message. backupTimer : QTimer object Timer that fires every 5 minutes. Saves current state of tasks file to backup file. """ def __init__(self): """ Init GUI and all required things. """ super().__init__() iconAdd = Icon(byte=icons.add).convertToIcon().getIcon() self.tray = QSystemTrayIcon(iconAdd, self) menu = QMenu() menu.addAction(conf.lang.ADD_ACTION, lambda: self.showWindow(QSystemTrayIcon.Trigger)) menu.addAction(conf.lang.EDIT_ACTION, lambda: self.showWindow('editAction')) menu.addSeparator() menu.addAction(conf.lang.OPT_ACTION, lambda: self.showWindow('optAction')) menu.addAction(conf.lang.RESTORE, self.restore) menu.addAction(conf.lang.QUIT, self.quit) self.tray.setContextMenu(menu) self.tray.activated.connect(self.showWindow) self.tray.show() self.addWindow = None self.addActive = False self.editWindow = None self.editActive = False self.optWindow = None self.optActive = False self.timer = QTimer() self.timer.timeout.connect(self.timerTick) self.timer.start(1000) self.backup() self.backupTimer = QTimer() self.backupTimer.timeout.connect(self.backup) self.backupTimer.start(conf.backup * 1000) def timerTick(self): """ Checks tasks entry's time if it is up. Shows message with entry's text if its time's up. """ global tasks global rewrite for entry in tasks: date = entry.getDateTime() today = dt.datetime.today() if (date - today).days < 0: tasks.remove(entry) class EvMessageBox(QMessageBox): """ QMessageBox with timer. Parameters ---------- text : string Text of message. title : string Title of message window. wicon : QIcon object Icon of message window. icon : QMessageBox.Icon int Icon of message body. timeout : int Time for message has being shown. Useful attributes ----------------- timer : QTimer object Timer attached to message. """ def __init__(self, text, title, wicon, icon, timeout): super().__init__() self.timeout = timeout self.setText(text) self.setWindowTitle(title) self.setWindowIcon(wicon) self.setIcon(icon) self.addButton(QPushButton(conf.lang.REPEAT.format( parseSeconds(conf.tdelta))), QMessageBox.YesRole) self.addButton(QPushButton(conf.lang.CLOSE), QMessageBox.NoRole) self.setWindowFlags(Qt.WindowStaysOnTopHint) # self.setTextFormat(Qt.RichText) self.timer = QTimer() self.timer.timeout.connect(self.timerTick) def showEvent(self, event): """ Start timer on message showEvent. """ self.currentTime = 0 self.timer.start(1000) def timerTick(self): """ Done message on timeout. """ self.currentTime += 1 if self.currentTime >= self.timeout: self.timer.stop() self.done(-1) msgBox = EvMessageBox( entry.text, '{} {}'.format(conf.lang.TASK, date), Icon(byte=icons.alert).convertToIcon().getIcon(), QMessageBox.Information, conf.mesout) reply = msgBox.exec_() msgBox.raise_() if reply != 1: td = conf.tdelta if reply == 0 else 300 - conf.mesout date = dateToStr(dt.datetime.now() + dt.timedelta(0, td)) date['date'] = date['date'].replace('/', '.') date['time'] = date['time'].replace('.', ':') tasks.append(Entry(date['date'], date['time'], entry.text)) rewrite() if self.editActive: self.editWindow.filterApply() def showWindow(self, event): """ Show child windows. If event is QSystemTrayIcon.Trigger then it checks if all windows are not open and show addWindow. If event is 'addAction' it means that user from editWindow want to edit reminder, so it opens addWindow. Then it checks if addAction or editAction is True, and alert appropriate window. Then if event is 'editAction' then it opens editWindow. """ if event == 'editAction': if self.editActive: QApplication.alert(self.editWindow) else: self.editWindow = EditWindow(self) self.editWindow.show() self.editWindow.setFocus(True) self.editWindow.activateWindow() return self.editWindow elif event == 'addAction': self.addWindow = AddWindow(self) self.addWindow.show() self.addWindow.setFocus(True) self.addWindow.activateWindow() return self.addWindow elif event == QSystemTrayIcon.Trigger: if self.addActive: QApplication.alert(self.addWindow) else: self.addWindow = AddWindow(self) self.addWindow.show() self.addWindow.setFocus(True) self.addWindow.activateWindow() return self.addWindow elif event == 'optAction': if self.addActive: self.addWindow.hide() if self.editActive: self.editWindow.hide() self.optWindow = OptionsWindow(self) self.optWindow.show() self.optWindow.setFocus(True) self.optWindow.activateWindow() def backup(self): """ Copies content of tasks file to backup file. """ with open(backup, 'w') as to_, open(filename, 'r') as from_: to_.write(from_.read()) def restore(self): """ Restores content of tasks file from backup file after user confirmation. """ global tasks shure = QMessageBox.question(self, conf.lang.RESTORE, conf.lang.RESTORE_TEXT, QMessageBox.No | QMessageBox.Yes, QMessageBox.No) if shure == QMessageBox.No: pass else: temp = open(backup).read() # don't forget to read backup self.backup() with open(filename, 'w') as to_: to_.write(temp) tasks = [] readTasks() if self.editActive: self.editWindow.filterApply() def quit(self, really=True): """ Quits application. Hides tray icon firstly. If really is not True, do not quits the application. It is used to re-launch the application. """ self.tray.hide() self.timer.stop() self.backupTimer.stop() if really: QCoreApplication.instance().quit()
class ElectrumGui(Logger): @profiler def __init__(self, config, daemon, plugins): set_language(config.get('language', get_default_language())) Logger.__init__(self) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.network_dialog = None self.lightning_dialog = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() # init tray self.dark_icon = self.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('Electrum') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' if use_dark_theme: try: import qdarkstyle self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False self.logger.warning(f'Error setting dark theme: {repr(e)}') # Apply any necessary stylesheet patches patch_qt_stylesheet(use_dark_theme=use_dark_theme) # Even if we ourselves don't set the dark theme, # the OS/window manager/etc might set *a dark theme*. # Hence, try to choose colors accordingly: ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() if self.config.get('lightning'): m.addAction(_("Lightning"), self.show_lightning_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.close) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() if self.network_dialog: self.network_dialog.close() if self.lightning_dialog: self.lightning_dialog.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_lightning_dialog(self): if not self.lightning_dialog: self.lightning_dialog = LightningDialog(self) self.lightning_dialog.bring_to_top() def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline')) return if self.network_dialog: self.network_dialog.on_update() self.network_dialog.show() self.network_dialog.raise_() return self.network_dialog = NetworkDialog(self.daemon.network, self.config, self.network_updated_signal_obj) self.network_dialog.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) w.warn_if_testnet() w.warn_if_watching_only() return w def count_wizards_in_progress(func): def wrapper(self: 'ElectrumGui', *args, **kwargs): with self._num_wizards_lock: self._num_wizards_in_progress += 1 try: return func(self, *args, **kwargs) finally: with self._num_wizards_lock: self._num_wizards_in_progress -= 1 return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None try: wallet = self.daemon.load_wallet(path, None) except BaseException as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: try: wallet = self._start_wizard_to_select_or_create_wallet(path) except (WalletFileException, BitcoinException) as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (2):\n' + repr(e)) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except BaseException as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot create window for wallet') + ':\n' + repr(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins) try: path, storage = wizard.select_storage(path, self.daemon.get_wallet) # storage is None if file does not exist if storage is None: wizard.path = path # needed by trustedcoin plugin wizard.run('new') storage = wizard.create_storage(path) else: wizard.run_upgrades(storage) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet finally: wizard.terminate() # return if wallet creation is not complete if storage is None or storage.get_action(): return wallet = Wallet(storage) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window: ElectrumWindow): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): try: self.init_network() except UserCancelled: return except GoBack: return except BaseException as e: self.logger.exception('') return self.timer.start() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url'), app_is_starting=True): return signal.signal(signal.SIGINT, lambda *args: self.app.quit()) def quit_after_last_window(): # keep daemon running after close if self.config.get('daemon'): return # check if a wizard is in progress with self._num_wizards_lock: if self._num_wizards_in_progress > 0 or len(self.windows) > 0: return if self.config.get('persist_daemon'): return self.app.quit() self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(quit_after_last_window) def clean_up(): # Shut down the timer cleanly self.timer.stop() # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide() self.app.aboutToQuit.connect(clean_up) # main loop self.app.exec_() # on some platforms the exec_ call may not return, so use clean_up() def stop(self): self.logger.info('closing GUI') self.app.quit()
class WindowGUI(QWidget): def __init__(self): super().__init__() self.appTitle = 'Keasy' self.version = '2.0' self.icon_path = 'icon.png' self.icon = QIcon(self.resource_path(self.icon_path)) self.icon2_path = 'icon2.png' self.icon2 = QIcon(self.resource_path(self.icon2_path)) self.command_maxLengthTxt = '60字まで' self.tray_toolchipTxt = 'Ctrlキーを2回押すことで展開します' self.history = [''] # コマンド履歴用 self.setWindowTitle(self.appTitle) self.setWindowFlags(Qt.CustomizeWindowHint) # タイトルバーを消す self.setMinimumSize(600, 500) self.setMaximumSize(1200, 800) self.savedMyGeometry = None # タスクトレイ格納時のウィンドウサイズ self.setStyleSheet('color:rgb(200,200,200);' 'background:rgb(50,50,50);') # コマンド処理のインスタンス self.cmdEvt = CommandEvents() self.cmdEvt.setObj(self) self.cmdEvt.exitSIGNAL.connect(self.exitKeasy) # OSによって適用フォントを変える import platform MyOS = platform.system() if MyOS == 'Windows': MainFont = 'font-family:MS Gothic;font-size:14px;' ConsoleFont = 'font-family:MS Gothic;font-size:18px;' elif MyOS == 'Darwin': MainFont = 'font-family:Monospace;font-size:13px;' ConsoleFont = 'font-family:Monospace;font-size:16px;' else: MainFont = 'font-family:Monospace;font-size:14px;' ConsoleFont = 'font-family:Monospace;font-size:18px;' self.tray = QSystemTrayIcon() # トレイアイコン(通知領域) self.initSystemTray(self.tray) # トレイ設定 self.initUI(MainFont, ConsoleFont) # アプリの外観 self.show() # ウィンドウ表示 # OSによってウィンドウ操作プログラムを変える import WindowController WindowController.controlWindow(self) self.cmdEvt.receiver('') # 最初にパスワード認証させる # pyinstallerでexe/app化した後でも画像を使えるように絶対パスへ変更 # Win/Macどちらも,この処理をしないと画像・アイコンが表示されない def resource_path(self, relative_path): if hasattr(sys, '_MEIPASS'): return os_path.join(sys._MEIPASS, relative_path) return os_path.join(os_path.abspath("."), relative_path) # ウィンドウを左クリックすると発動 def mousePressEvent(self, QMouseEvent): """ :type QMouseEvent:PyQt5.QtGui.QMouseEvent.QMouseEvent """ try: # クリック時のマウス座標を保持 self.previousMousePos = QMouseEvent.pos() self.prevX = self.previousMousePos.x() self.prevY = self.previousMousePos.y() except: traceback.print_exc() # ウィンドウをマウスドラッグ(左クリックしたままマウス移動)すると発動 def mouseMoveEvent(self, QMouseEvent): """ :type QMouseEvent:PyQt5.QtGui.QMouseEvent.QMouseEvent """ try: # ドラッグ中はマウス座標とウィンドウの座標を取得し続ける currentMousePos = QMouseEvent.pos() currentX = currentMousePos.x() currentY = currentMousePos.y() appPos = self.pos() appPos_x = appPos.x() appPos_y = appPos.y() except: traceback.print_exc() # クリック時と現在のマウス座標差分だけ,今のウィンドウ位置から移動 # ※クリック時マウス位置は不変 diffPos_x = self.prevX - currentX diffPos_y = self.prevY - currentY self.move(appPos_x - diffPos_x, appPos_y - diffPos_y) # ウィンドウにフォーカスしている時のキー入力判定 def keyPressEvent(self, evt): try: # Tabキーが押された&Ctrl,Shift,Altのどれも押されていない場合, # Tabキーイベントを発生させる if evt.key() == Qt.Key_Tab and \ (not evt.modifiers() & Qt.ControlModifier and not evt.modifiers() & Qt.ShiftModifier and not evt.modifiers() & Qt.AltModifier): self.tabEvent() pass except: traceback.print_exc() # 現在のコマンド入力内容でTabキーイベントの処理 def tabEvent(self): text = self.console.text() self.cmdEvt.tabReceiver(text) # autoComplete_singleで自動入力する対象を切り替える def switchMemorize(self): try: # completeTarget= # 0: ユーザID/Mail # 1:パスワード if self.cmdEvt.completeTarget == 0: self.cmdEvt.completeTarget = 1 self.tray.setIcon(self.icon2) else: self.cmdEvt.completeTarget = 0 self.tray.setIcon(self.icon) print(self.cmdEvt.completeTarget) except: traceback.print_exc() # 現在のmodeにおけるflag処理を1つ戻す def rollBack_at_flag(self): self.cmdEvt.shiftTabReceiver() # システムトレイ(通知領域)の設置 def initSystemTray(self, tray): try: tray.setIcon(QIcon(self.resource_path(self.icon_path))) tray.setToolTip(self.tray_toolchipTxt) tray.show() except: traceback.print_exc() # コンソールに1文字入力される度に,その文字が使用可能か確認・修正する def checkConsoleText(self, text: str): if text == '': return txtList = list(text) newChar = txtList[-1] # 最後の文字 # パスワード入力かどうかで確認方法を変える if self.cmdEvt.currentFlag == self.cmdEvt.flag_PassWord: checked = self.passwordCheck(newChar) else: checked = self.newtralCheck(newChar) # 使用可能な文字だったら合格(何もしない) if checked: return # 使用不可能な文字なら消す del txtList[-1] fixedTxt = ''.join(txtList) self.changeConsoleText(fixedTxt) # 入力された文字がパスワードに使用可能か確認 # True:使用可能 # False:使用不可能 def passwordCheck(self, newChar: str): checkTxt = 'abcdefghijklmnopqrstuvwxyz' \ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '1234567890#$%&_' checkList = list(checkTxt) # 使用可能な文字のどれかに当てはまれば合格 for char in checkList: if char == newChar: return True return False # 入力された文字がサービス名などに使用可能か確認 # True:使用可能 # False:使用不可能 # 今の所は何を入力してもOK def newtralCheck(self, text: str): return True # コンソールの入力内容を変更 def changeConsoleText(self, text: str): self.console.setText(text) # コマンドによる処理結果を表示 def dispResponseText(self, txt: str): try: self.responseTxt.setText(txt) except: traceback.print_exc() # コマンドを送る度に,現在のモードを表示 def dispMyMode(self, mode: str): self.myMode.setText(mode) # アプリ終了処理 def exitKeasy(self): self.tray.hide() # 通知領域にアイコンだけ残らないようにする # 復号化された状態のDBを削除 if os_path.exists(self.cmdEvt.keasy_path): os_remove(self.cmdEvt.keasy_path) sys.exit() # コンソールの入力文字を伏せる def setConsoleMode_password(self): self.console.setEchoMode(QLineEdit.Password) # コンソールの入力文字を表示する def setConsoleMode_raw(self): self.console.setEchoMode(QLineEdit.Normal) # アプリUI設定 def initUI(self, MainFont: str, ConsoleFont: str): try: hline = QFrame() hline.setFrameShape(QFrame.HLine) vline = QFrame() vline.setFrameShape(QFrame.VLine) vline.setStyleSheet('color:rgb(0,200,0);') vline.setLineWidth(3) # 最上部======================================== topBar = QHBoxLayout() icon = QPixmap(self.resource_path(self.icon_path)).scaled(30, 30) self.icon_area = QLabel() self.icon_area.setPixmap(icon) title = QLabel(self.appTitle + self.version) title.setStyleSheet('font-size:28px;') topBar.addWidget(self.icon_area) topBar.addWidget(title) topBar.addStretch() # 説明部======================================== explainBar = QHBoxLayout() explainTxt = QLabel('exit : Keasyを終了します\n' 'find [検索ワード] : アカウントを検索します\n' 'memorize [サービス名] [ユーザID/Mail] : ' 'IDとパスワードを一時記憶します.\n' ' Shiftキーを2回押すことで自動入力できます\n' 'show : パスワード文字列を表示します\n' 'hide : パスワードを伏せ字(*)で隠します\n' 'add : アカウントを登録します\n' 'edit [サービス名] [ユーザID/Mail] [変更したい情報の種類] : ' 'アカウントを編集します\n' 'delete [サービス名] [ユーザID/Mail] : アカウントを削除します\n' 'master : マスターパスワードを変更します') explainTxt.setStyleSheet(MainFont) explainBar.addWidget(explainTxt) # 区切り======================================== hSeparater = QHBoxLayout() hSeparater.addWidget(hline) vSeparater = QHBoxLayout() vSeparater.addWidget(vline) # コマンド履歴部====================================== historyBar = QHBoxLayout() historyMark = QLabel('$ ') historyMark.setStyleSheet(MainFont) self.historyTxt = QLabel('aaa') self.historyTxt.setStyleSheet(MainFont) historyBar.addWidget(historyMark) historyBar.addWidget(self.historyTxt) historyBar.addStretch() # コンソール部====================================== commandBar = QHBoxLayout() self.console = QLineEdit() self.console.setMaxLength(60) self.console.setStyleSheet(ConsoleFont + 'color:rgb(0,255,0);' 'background:rgb(30,30,30);' 'border-style:solid;' 'border-width:1px;' 'border-color:rgb(0,150,0);') # コマンドラインでEnterを押されたら, # 入力文字をreceiver関数に投げる # ※connectに直接関数を書いてもエラーを吐く # 無名関数lambdaとして書くと通る self.console.returnPressed.connect( lambda: self.cmdEvt.receiver(self.console.text())) self.console.textChanged.connect( lambda: self.checkConsoleText(self.console.text())) tellMaxInput = QLabel(self.command_maxLengthTxt) tellMaxInput.setStyleSheet(MainFont) tellMaxInput.setStyleSheet('font-size:16px;') self.myMode = QLabel() self.myMode.setStyleSheet(MainFont) dollar = QLabel('$') dollar.setStyleSheet(MainFont) commandBar.addWidget(self.myMode) commandBar.addWidget(dollar) commandBar.addWidget(self.console) commandBar.addWidget(tellMaxInput) # 応答部====================================== responseBar = QHBoxLayout() responseMark = QLabel('>> ') responseMark.setAlignment(Qt.AlignTop) # 上寄せ responseMark.setStyleSheet(MainFont) self.responseTxt = QLabel() self.responseTxt.setStyleSheet(MainFont) responseBar.addWidget(responseMark) responseBar.addWidget(self.responseTxt) responseBar.addStretch() # 表示部============================================ resultArea = QHBoxLayout() self.resultTxt = QLabel() self.resultTxt.setStyleSheet(MainFont) # DB検索結果の表 self.resultTable = QTableWidget() # Tabキーでフォーカスさせない self.resultTable.setFocusPolicy(Qt.NoFocus) # 行と列は後で追加する self.resultTable.setColumnCount(0) self.resultTable.setRowCount(0) # 1行目headerを無効化 hHeader = self.resultTable.horizontalHeader() hHeader.hide() # 1列目headerを無効化 vHeader = self.resultTable.verticalHeader() vHeader.hide() # 表のフォントサイズと枠線色 self.resultTable.setStyleSheet( 'font-size:14px; gridline-color:"#555";') # 表のダブルクリックを無効化 self.resultTable.setEditTriggers(QAbstractItemView.NoEditTriggers) # 表の範囲選択&選択時の色変更を無効化(選択セルのコピーは可能) self.resultTable.setSelectionMode(QAbstractItemView.NoSelection) # 表をクリックした場合の処理 # self.resultTable.cellClicked.connect(self.myFunction) # 表の選択セルについて,文字と背景色を変更 # tPalette = QPalette(self.resultTable.palette()) # tPalette.setColor(QPalette.HighlightedText, QColor('#ccc')) # tPalette.setColor(QPalette.Highlight, QColor('#242')) # self.resultTable.setPalette(tPalette) # 表に適用 # resultArea.addWidget(self.resultTxt) resultArea.addWidget(self.resultTable) # レイアウト格納==================================== mainLayout = QVBoxLayout() mainLayout.addLayout(topBar) mainLayout.addLayout(explainBar) mainLayout.addLayout(hSeparater) mainLayout.addLayout(historyBar) mainLayout.addLayout(commandBar) mainLayout.addLayout(responseBar) mainLayout.addLayout(hSeparater) mainLayout.addLayout(resultArea) # mainLayout.addStretch() # 余白を埋める self.setLayout(mainLayout) except: traceback.print_exc()
class Main(QMainWindow, form_class): def __init__(self): super().__init__() self.setupUi(self) self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(QIcon('ui/icon.png')) self.trayIcon.activated.connect(self.restore_window) self.WM = WindowManager() self.pre_window = self.WM.get_fore_window() self.rnn = RNN() self.runState = False self.startButton.clicked.connect(self.btn_clk_start) self.startState = True self.trainButton.clicked.connect(self.btn_clk_train) self.helpButton.clicked.connect(self.btn_clk_help) self.helpState = True self.timer = QTimer(self) self.timer.start(200) self.timer.timeout.connect(self.run) def restore_window(self, reason): if reason == QSystemTrayIcon.DoubleClick: self.trayIcon.hide() self.showNormal() def btn_clk_start(self): if self.startState: self.startState = False self.startButton.setText('Stop') self.runState = True self.back = Background() self.back.show() self.trayIcon.setVisible(True) self.hide() else: self.startState = True self.startButton.setText('Start') self.runState = False self.back.close() self.timer.stop() def btn_clk_train(self): if os.path.isfile('params.pkl'): os.remove('params.pkl') time.sleep(2) self.rnn = RNN() self.rnn.train() QMessageBox.information(self, "RNN", "train finished") def btn_clk_help(self): if self.helpState: self.helpState = False self.helpButton.setText('go to tray icon') QMessageBox.information(self, "Help", "*****@*****.**") else: self.helpState = True self.helpButton.setText('Help') self.trayIcon.setVisible(True) self.hide() def get_hash(self, text): hash = hashlib.md5() hash.update(text.encode()) return hash.hexdigest() def run(self): try: with open("data.txt", 'a') as f: cur_window = self.WM.get_fore_window().split()[0] if cur_window != self.pre_window: self.pre_window = cur_window cur_window_hash = self.get_hash(cur_window) f.write(" " + cur_window_hash) if self.runState: target_windows = list( self.rnn.test([cur_window_hash], 3)) target_windows.append(cur_window_hash) print(target_windows) self.WM.set_window(self.WM.find_window('dimmer')) all_windows = list(self.WM.get_windows()) for t_window in target_windows: for i, window in enumerate(all_windows): if t_window == self.get_hash( window.split()[0]): self.WM.set_window( self.WM.find_window(all_windows[i])) except: pass
class Gui(Ui_UI): def __init__(self): super(Gui, self).__init__() # self.init() def init(self): QApplication.setQuitOnLastWindowClosed(False) self.mainWin = QMainWindow() self.mainWin.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.setupUi(self.mainWin) self.timer = self.QTimer # print("开始gui创建111") # self.mainWin.show() self.set_icon() self.set_menus() self.set_icon_menus() @property def QTimer(self): return QtCore.QTimer() def set_icon(self): self.icon = QSystemTrayIcon(self.mainWin) try: # icon_dir = Path("gui") # url = "{0}{2}{1}{3}{1}{4}{1}{5}".format(icon_dir.path["base"], icon_dir.path["sep"], "UI", "Data", "Assets", "icon.png") icon_dir = Path("Data") url = "{0}{2}{1}{3}{1}{4}".format(icon_dir.path["base"], icon_dir.path["sep"], "statics", "images", "icon.png") print(">>>>url", url) self.icon.setIcon(QIcon(url)) except Exception as err: print("【err】【Gui ->set_icon】 icon图片文件错误") def set_menus(self): self.menu = {} self.menu["show"] = QAction('&显示(Show)', triggered=self.mainWin.show) self.menu["hide"] = QAction('&隐藏(Hide)', triggered=self.mainWin.hide) self.menu["exit"] = QAction('&退出(Exit)', triggered=self.exit) self.menus = QMenu() for act in self.menu: self.menus.addAction(self.menu[act]) def set_icon_menus(self): self.icon.setContextMenu(self.menus) self.icon.activated.connect(self.click_btn) self.icon.show() print(">>>>show icon") def click_btn(self, reason): if reason == 2 or reason == 3: self.mainWin.show() def exit(self): re = QMessageBox.question(self.mainWin, "退出", "是否退出", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if re == QMessageBox.Yes: self.mainWin.hide() self.icon.hide() print("执行GUI退出功能!") hasattr(self, "exited") and self.exited() sys.exit(0) def restart(self): # if self.main_exit: # self.main_exit() python = sys.executable os.execl(python, python, *sys.argv) def setExit(self, func): self.exited = func def __getitem__(self, item): return getattr(self, item, None) def clearText(self, objKey): # self[objKey].clear() self.links.clear() def setText(self, objKey, txt): Qobject = self[objKey] Qobject.setText(txt) def insertText(self, objKey, txt): Qobject = self[objKey] Qobject.insertPlainText(txt)
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 SVApplication(QApplication): # Signals need to be on a QObject create_new_window_signal = pyqtSignal(str, object) cosigner_received_signal = pyqtSignal(object, object) labels_changed_signal = pyqtSignal(object) window_opened_signal = pyqtSignal(object) window_closed_signal = pyqtSignal(object) # Async tasks async_tasks_done = pyqtSignal() # Logging new_category = pyqtSignal(str) new_log = pyqtSignal(object) # Preferences updates fiat_ccy_changed = pyqtSignal() custom_fee_changed = pyqtSignal() op_return_enabled_changed = pyqtSignal() num_zeros_changed = pyqtSignal() base_unit_changed = pyqtSignal() fiat_history_changed = pyqtSignal() fiat_balance_changed = pyqtSignal() update_check_signal = pyqtSignal(bool, object) def __init__(self, argv): super().__init__(argv) self.windows = [] self.log_handler = SVLogHandler() self.log_window = None self.net_dialog = None self.timer = QTimer() self.exception_hook = None # A floating point number, e.g. 129.1 self.dpi = self.primaryScreen().physicalDotsPerInch() # init tray self.dark_icon = app_state.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self._tray_icon(), None) self.tray.setToolTip('ElectrumSV') self.tray.activated.connect(self._tray_activated) self._build_tray_menu() self.tray.show() # FIXME set_language(app_state.config.get('language')) logs.add_handler(self.log_handler) self._start() def _start(self): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-sv.desktop') self.setWindowIcon(read_QIcon("electrum-sv.png")) self.installEventFilter(OpenFileEventFilter(self.windows)) self.create_new_window_signal.connect(self.start_new_window) self.async_tasks_done.connect(app_state.async_.run_pending_callbacks) self.num_zeros_changed.connect(partial(self._signal_all, 'on_num_zeros_changed')) self.fiat_ccy_changed.connect(partial(self._signal_all, 'on_fiat_ccy_changed')) self.base_unit_changed.connect(partial(self._signal_all, 'on_base_unit_changed')) self.fiat_history_changed.connect(partial(self._signal_all, 'on_fiat_history_changed')) self.fiat_balance_changed.connect(partial(self._signal_all, 'on_fiat_balance_changed')) self.update_check_signal.connect(partial(self._signal_all, 'on_update_check')) ColorScheme.update_from_widget(QWidget()) def _signal_all(self, method, *args): for window in self.windows: getattr(window, method)(*args) def _close(self): for window in self.windows: window.close() def close_window(self, window): app_state.daemon.stop_wallet_at_path(window.wallet.storage.path) self.windows.remove(window) self.window_closed_signal.emit(window) self._build_tray_menu() # save wallet path of last open window if not self.windows: app_state.config.save_last_wallet(window.wallet) self._last_window_closed() def _build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self._toggle_tray_icon) m.addSeparator() m.addAction(_("Exit ElectrumSV"), self._close) self.tray.setContextMenu(m) def _tray_icon(self): if self.dark_icon: return read_QIcon('electrumsv_dark_icon.png') else: return read_QIcon('electrumsv_light_icon.png') def _toggle_tray_icon(self): self.dark_icon = not self.dark_icon app_state.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self._tray_icon()) def _tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.create_new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not app_state.daemon.network: parent.show_warning(_('You are using ElectrumSV in offline mode; restart ' 'ElectrumSV if you want to get connected'), title=_('Offline')) return if self.net_dialog: self.net_dialog.on_update() self.net_dialog.show() self.net_dialog.raise_() return self.net_dialog = NetworkDialog(app_state.daemon.network, app_state.config) self.net_dialog.show() def show_log_viewer(self): if self.log_window is None: self.log_window = SVLogWindow(None, self.log_handler) self.log_window.show() def _last_window_closed(self): for dialog in (self.net_dialog, self.log_window): if dialog: dialog.accept() def _maybe_choose_server(self): # Show network dialog if config does not exist if app_state.daemon.network and app_state.config.get('auto_connect') is None: try: wizard = InstallWizard(None) wizard.init_network(app_state.daemon.network) wizard.terminate() except Exception as e: if not isinstance(e, (UserCancelled, GoBack)): logger.exception("") self.quit() def on_label_change(self, wallet: Abstract_Wallet, name: str, text: str) -> None: self.label_sync.set_label(wallet, name, text) def _create_window_for_wallet(self, wallet: Abstract_Wallet): if wallet.is_hardware_wallet(): dialogs.show_named('hardware-wallet-quality') w = ElectrumWindow(wallet) self.windows.append(w) self._build_tray_menu() self.window_opened_signal.emit(w) return w def get_wallet_window(self, path: str) -> Optional[ElectrumWindow]: for w in self.windows: if w.wallet.storage.path == path: return w def start_new_window(self, path, uri, is_startup=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it.''' for w in self.windows: if w.wallet.storage.path == path: w.bring_to_top() break else: try: wallet = app_state.daemon.load_wallet(path, None) if not wallet: wizard = InstallWizard(None) try: if wizard.select_storage(path, is_startup=is_startup): wallet = wizard.run_and_get_wallet() except UserQuit: pass except UserCancelled: pass except GoBack as e: logger.error('[start_new_window] Exception caught (GoBack) %s', e) finally: wizard.terminate() if not wallet: return app_state.daemon.start_wallet(wallet) except Exception as e: logger.exception("") error_str = str(e) if '2fa' in error_str: error_str = _('2FA wallets are not supported') msg = '\n'.join((_('Cannot load wallet "{}"').format(path), error_str)) MessageBox.show_error(msg) return w = self._create_window_for_wallet(wallet) if uri: w.pay_to_URI(uri) w.bring_to_top() w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) # this will activate the window w.activateWindow() return w def update_check(self) -> None: if (not app_state.config.get('check_updates', True) or app_state.config.get("offline", False)): return def f(): import requests try: response = requests.request( 'GET', "https://electrumsv.io/release.json", headers={'User-Agent' : 'ElectrumSV'}, timeout=10) result = response.json() self._on_update_check(True, result) except Exception: self._on_update_check(False, sys.exc_info()) t = threading.Thread(target=f) t.setDaemon(True) t.start() def _on_update_check(self, success: bool, result: dict) -> None: if success: when_checked = datetime.datetime.now().astimezone().isoformat() app_state.config.set_key('last_update_check', result) app_state.config.set_key('last_update_check_time', when_checked, True) self.update_check_signal.emit(success, result) def initial_dialogs(self) -> None: '''Suppressible dialogs that are shown when first opening the app.''' dialogs.show_named('welcome-ESV-1.3.0a1') # This needs to be reworked or removed, as non-advanced users aren't sure whether # it is safe, and likely many people aren't quite sure if it should be done. # old_items = [] # headers_path = os.path.join(app_state.config.path, 'blockchain_headers') # if os.path.exists(headers_path): # old_items.append((_('the file "blockchain_headers"'), os.remove, headers_path)) # forks_dir = os.path.join(app_state.config.path, 'forks') # if os.path.exists(forks_dir): # old_items.append((_('the directory "forks/"'), shutil.rmtree, forks_dir)) # if old_items: # main_text = _('Delete the following obsolete items in <br>{}?' # .format(app_state.config.path)) # info_text = '<ul>{}</ul>'.format(''.join('<li>{}</li>'.format(text) # for text, *rest in old_items)) # if dialogs.show_named('delete-obsolete-headers', main_text=main_text, # info_text=info_text): # try: # for _text, rm_func, *args in old_items: # rm_func(*args) # except OSError as e: # logger.exception('deleting obsolete files') # dialogs.error_dialog(_('Error deleting files:'), info_text=str(e)) def event_loop_started(self) -> None: self.cosigner_pool = CosignerPool() self.label_sync = LabelSync() if app_state.config.get("show_crash_reporter", default=True): self.exception_hook = Exception_Hook(self) self.timer.start() signal.signal(signal.SIGINT, lambda *args: self.quit()) self.initial_dialogs() self._maybe_choose_server() app_state.config.open_last_wallet() path = app_state.config.get_wallet_path() if not self.start_new_window(path, app_state.config.get('url'), is_startup=True): self.quit() def run_app(self) -> None: when_started = datetime.datetime.now().astimezone().isoformat() app_state.config.set_key('previous_start_time', app_state.config.get("start_time")) app_state.config.set_key('start_time', when_started, True) self.update_check() threading.current_thread().setName('GUI') self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.timer.timeout.connect(app_state.device_manager.timeout_clients) QTimer.singleShot(0, self.event_loop_started) self.exec_() logs.remove_handler(self.log_handler) # Shut down the timer cleanly self.timer.stop() # clipboard persistence # see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.sendEvent(self.clipboard(), event) self.tray.hide() def run_coro(self, coro, *args, on_done=None): '''Run a coroutine. on_done, if given, is passed the future containing the reuslt or exception, and is guaranteed to be called in the context of the GUI thread. ''' def task_done(future): self.async_tasks_done.emit() future = app_state.async_.spawn(coro, *args, on_done=on_done) future.add_done_callback(task_done) return future def run_in_thread(self, func, *args, on_done=None): '''Run func(*args) in a thread. on_done, if given, is passed the future containing the reuslt or exception, and is guaranteed to be called in the context of the GUI thread. ''' return self.run_coro(run_in_thread, func, *args, on_done=on_done)
class MainWindow(QMainWindow): """The main GUI application.""" def __init__(self, config): """Initializer for the GUI widgets. Pass in an instance of Config class, so that it may interact with the config.""" super().__init__() self.config = config self.setWindowTitle("Livestreamer GUI v{}".format(APPVERSION)) self.setup_systray() self.setup_menu() self.setup_geometry() self.livestreamer_thread = None self.thread_exit_grace_time = 10000 # How long a thread can take to exit in milliseconds self.timestamp_format = self.config.get_config_value("timestamp-format") self.setup_control_widgets() self.update_colors() # Load all streaming-related data self.selections = {"streamer": None, "channel": None} self.load_streamers() self.load_channels(self.streamer_input.currentText()) # Do the first configuration, if the application was run for the first time self.do_init_config() # Finally show the window and the system tray icon, if it should be shown self.show() self.close_override = False self.show_hide_systray() self.check_and_do_database_migration() def do_init_config(self): do_config = self.config.get_config_value("is-configured") if do_config == 0: self.menu_cmd_configure() self.config.set_config_value("is-configured", 1) self.insertText("Using config database version '{}'".format(self.config.get_config_value("db-version"))) def setup_systray(self): if not self.config.get_config_value("enable-systray-icon"): self.systray = None return self.systray = QSystemTrayIcon(self) self.systray.activated.connect(self.systray_activated) main_menu = QMenu(self) quit_action = QAction("&Quit", self) quit_action.triggered.connect(self.on_close_override) main_menu.addAction(quit_action) self.systray.setContextMenu(main_menu) def systray_activated(self, reason): if reason == QSystemTrayIcon.Trigger: if self.isVisible(): self.hide() else: self.showNormal() def check_and_do_database_migration(self): current_version = self.config.get_config_value("db-version") if self.config.is_migration_needed(): self.insertText("Detected pending config database upgrade to version '{}'. Awaiting user input...".format(DBVERSION)) message = "You are using an older version of the application config database.\n\nWould you like to upgrade the database now? Your existing config database will be backed up." upgrade_is_mandatory = current_version < MANDATORY_DBVERSION if upgrade_is_mandatory: message = message + "\n\nWARNING: Your config database is not compatible with this version of Livestreamer GUI. UPDATE IS MANDATORY! If you cancel the update, the application will exit." reply = QMessageBox.question(self, "Pending config database upgrade", message, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.Yes: self.insertText("Backing up config database...") backup = self.config.make_database_backup() self.insertText("Current config database backed up to '{}'".format(backup)) self.insertText("Config database update initialized...") self.update() self.config.execute_migration() new_version = self.config.get_config_value("db-version") self.insertText("Config database update from version '{}' to '{}' finished.".format(current_version, new_version)) elif reply == QMessageBox.No and upgrade_is_mandatory: QtCore.QTimer.singleShot(500, self.on_close_override) # self.on_close_override() # Calling this in an __init__()-called method doesn't seem to work... else: self.insertText("Config database update cancelled. No changes were made.") def setup_menu(self): config_action = QAction("&Configure...", self) config_action.triggered.connect(self.menu_cmd_configure) quit_action = QAction("&Quit", self) quit_action.setShortcut("Ctrl+Q") quit_action.triggered.connect(self.on_close_override) menu = self.menuBar() file_menu = menu.addMenu("&File") file_menu.addAction(config_action) file_menu.addSeparator() file_menu.addAction(quit_action) def setup_geometry(self): width = self.config.get_config_value("root-width") height = self.config.get_config_value("root-height") topleft = QApplication.desktop().availableGeometry().topLeft() if self.config.get_config_value("remember-window-position"): xoffset = self.config.get_config_value("root-xoffset") yoffset = self.config.get_config_value("root-yoffset") topleft.setX(self.config.get_config_value("root-xoffset")) topleft.setY(self.config.get_config_value("root-yoffset")) self.resize(width, height) self.setMinimumSize(500, 300) self.move(topleft) # Center the window # center_point = QApplication.desktop().availableGeometry().center() # frame_geometry = self.frameGeometry() # frame_geometry.moveCenter(center_point) # self.move(frame_geometry.topLeft()) def setup_control_widgets(self): self.cwidget = QWidget(self) self.setCentralWidget(self.cwidget) layout = QGridLayout(self.cwidget) self.cwidget.setLayout(layout) fg_fav = self.config.get_config_value("button-foreground-favorite") fg_edit = self.config.get_config_value("button-foreground-edit") fg_add = self.config.get_config_value("button-foreground-add") fg_delete = self.config.get_config_value("button-foreground-delete") control_button_width = 30 control_button_font_style = "QPushButton { font-family: Arial, sans-serif; font-size: 16px }" column = 0 label_streamer_input = QLabel("Streamer", self.cwidget) layout.addWidget(label_streamer_input, 0, column) label_channel_input = QLabel("Channel", self.cwidget) layout.addWidget(label_channel_input, 1, column) label_quality_input = QLabel("Stream quality", self.cwidget) layout.addWidget(label_quality_input, 2, column) column += 1 self.streamer_input = QComboBox(self.cwidget) self.streamer_input.setEnabled(False) self.streamer_input.currentIndexChanged.connect(self.on_streamer_select) layout.addWidget(self.streamer_input, 0, column) self.channel_input = QComboBox(self.cwidget) self.channel_input.setEnabled(False) self.channel_input.currentIndexChanged.connect(self.on_channel_select) layout.addWidget(self.channel_input, 1, column) self.quality_input = QComboBox(self.cwidget) self.quality_input.addItem("(auto-refresh is disabled; please refresh manually)") self.quality_input.setEnabled(False) layout.addWidget(self.quality_input, 2, column) layout.setColumnStretch(column, 5) column += 1 self.fav_streamer_button = QPushButton("\u2764", self.cwidget) self.fav_streamer_button.setMaximumWidth(control_button_width) self.fav_streamer_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_fav, control_button_font_style)) self.fav_streamer_button.setEnabled(False) self.fav_streamer_button.setToolTip("Set the selected streamer as your most favorite streamer") self.fav_streamer_button.clicked.connect(self.cmd_set_favorite_streamer) layout.addWidget(self.fav_streamer_button, 0, column) self.fav_channel_button = QPushButton("\u2764", self.cwidget) self.fav_channel_button.setMaximumWidth(control_button_width) self.fav_channel_button.setStyleSheet(':enabled {{ color: {0} }} {1}'.format(fg_fav, control_button_font_style)) self.fav_channel_button.setEnabled(False) self.fav_channel_button.setToolTip("Set the selected channel as your most favorite channel") self.fav_channel_button.clicked.connect(self.cmd_set_favorite_channel) layout.addWidget(self.fav_channel_button, 1, column) self.clear_quality_cache_button = QPushButton("Refresh streams", self.cwidget) self.clear_quality_cache_button.setEnabled(False) self.clear_quality_cache_button.clicked.connect(self.cmd_refresh_quality_cache) layout.addWidget(self.clear_quality_cache_button, 2, column, 1, 4) column += 1 self.edit_streamer_button = QPushButton("\u270E", self.cwidget) self.edit_streamer_button.setMaximumWidth(control_button_width) self.edit_streamer_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_edit, control_button_font_style)) self.edit_streamer_button.setEnabled(False) self.edit_streamer_button.setToolTip("Edit data about the selected streamer") self.edit_streamer_button.clicked.connect(self.cmd_edit_streamer) layout.addWidget(self.edit_streamer_button, 0, column) self.edit_channel_button = QPushButton("\u270E", self.cwidget) self.edit_channel_button.setMaximumWidth(control_button_width) self.edit_channel_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_edit, control_button_font_style)) self.edit_channel_button.setToolTip("Edit data about the selected channel") self.edit_channel_button.clicked.connect(self.cmd_edit_channel) layout.addWidget(self.edit_channel_button, 1, column) column += 1 self.add_streamer_button = QPushButton("\u271A", self.cwidget) self.add_streamer_button.setMaximumWidth(control_button_width) self.add_streamer_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_add, control_button_font_style)) self.add_streamer_button.setEnabled(False) self.add_streamer_button.setToolTip("Add a new streamer") self.add_streamer_button.clicked.connect(self.cmd_add_streamer) layout.addWidget(self.add_streamer_button, 0, column) self.add_channel_button = QPushButton("\u271A", self.cwidget) self.add_channel_button.setMaximumWidth(control_button_width) self.add_channel_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_add, control_button_font_style)) self.add_channel_button.setToolTip("Add a new channel") self.add_channel_button.clicked.connect(self.cmd_add_channel) layout.addWidget(self.add_channel_button, 1, column) column += 1 self.delete_streamer_button = QPushButton("\u2716", self.cwidget) self.delete_streamer_button.setMaximumWidth(control_button_width) self.delete_streamer_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_delete, control_button_font_style)) self.delete_streamer_button.setEnabled(False) self.delete_streamer_button.setToolTip("Remove the selected streamer permanently") self.delete_streamer_button.clicked.connect(self.cmd_delete_streamer) layout.addWidget(self.delete_streamer_button, 0, column) self.delete_channel_button = QPushButton("\u2716", self.cwidget) self.delete_channel_button.setMaximumWidth(control_button_width) self.delete_channel_button.setStyleSheet(":enabled {{ color: {0} }} {1}".format(fg_delete, control_button_font_style)) self.delete_channel_button.setToolTip("Remove the selected channel permanently") self.delete_channel_button.clicked.connect(self.cmd_delete_channel) layout.addWidget(self.delete_channel_button, 1, column) # Add button for running livestreamer at the fourth row self.run_livestreamer_button = QPushButton("Run Livestreamer", self.cwidget) self.run_livestreamer_button.setEnabled(False) self.run_livestreamer_button.clicked.connect(self.run_livestreamer) layout.addWidget(self.run_livestreamer_button, 3, 0) self.log_widget = QTextEdit(self.cwidget) layout.addWidget(self.log_widget, 4, 0, 1, column+1) self.log_widget.setAcceptRichText(False) self.log_widget.setReadOnly(True) self.log_widget.setTabChangesFocus(True) def set_window_icon(self): """Sets the root window's icon, which is also shown in the taskbar.""" streamer = self.config.get_streamer(self.streamer_input.currentText()) icon = QIcon(os.path.join(IMAGESROOT, streamer["icon"])) self.setWindowIcon(icon) if self.systray is not None: self.systray.setIcon(icon) def closeEvent(self, event): """When the QWidget is closed, QCloseEvent is triggered, and this method catches and handles it.""" if not self.close_override and self.put_to_systray("close"): event.ignore() return if self.livestreamer_thread is not None and self.livestreamer_thread.keep_running: reply = QMessageBox.question(self, "Really quit Livestreamer GUI?", "Livestreamer is still running. Quitting will close it and the opened player.\n\nQuit?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: # Terminate the child process, else it'll keep running even after this application is closed if self.livestreamer_thread is not None: self.livestreamer_thread.term_process() self.livestreamer_thread.wait(self.thread_exit_grace_time) self.update() event.accept() else: event.ignore() # Explicitly hide the icon, if it remains visible after the application closes if self.systray is not None: self.systray.hide() # Remember the position of the window self.remember_window_position() event.accept() def changeEvent(self, event): if type(event) is not QWindowStateChangeEvent: return # It's one of the window state change events (normal, minimize, maximize, fullscreen, active) if self.isMinimized(): self.put_to_systray("minimize") def remember_window_position(self): if self.config.get_config_value("remember-window-position"): point = self.frameGeometry().topLeft() self.config.set_config_value("root-xoffset", point.x()) self.config.set_config_value("root-yoffset", point.y()) self.insertText("Window position saved.") def show_hide_systray(self): if self.systray is None: self.setup_systray() if self.systray is None: return if self.config.get_config_value("enable-systray-icon"): self.systray.show() else: self.systray.hide() def put_to_systray(self, event): if event == "minimize": config_value = "minimize-to-systray" elif event == "close": config_value = "close-to-systray" else: return False if self.systray is not None and self.config.get_config_value(config_value) and self.isVisible(): self.hide() return True return False def menu_cmd_configure(self): streamer = self.config.get_streamer(self.streamer_input.currentText()) dialog = AppConfigDialog(self, self.config, streamer_icon=os.path.join(IMAGESROOT, streamer["icon"])) dialog.exec() if dialog.result() == QDialog.Accepted: self.show_hide_systray() self.update_colors() dialog.close() dialog = None def cmd_set_favorite_streamer(self): raise NotImplementedException() # self.fav_streamer_button.setEnabled(False) # self.config.set_favorite_streamer(self.streamer_input.setCurrentText()) # self.insertText("Favorited streamer '{}'.".format(self.streamer_input.setCurrentText())) def cmd_edit_streamer(self): raise NotImplementedException() def cmd_add_streamer(self): raise NotImplementedException() def cmd_delete_streamer(self): raise NotImplementedException() def cmd_set_favorite_channel(self): self.fav_channel_button.setEnabled(False) self.config.set_favorite_channel(self.streamer_input.currentText(), self.channel_input.currentText()) self.insertText("Favorited channel '{}'.".format(self.channel_input.currentText())) def cmd_edit_channel(self): streamer = self.config.get_streamer(self.streamer_input.currentText()) streamer_icon = os.path.join(IMAGESROOT, streamer["icon"]) channel_data = self.config.get_streamer_channel(streamer["name"], self.channel_input.currentText()) dialog = AddEditChannelsDialog(self, self.config, title="Edit the channel", streamer_icon=streamer_icon, streamer=streamer, channel_data=channel_data) dialog.exec() result = dialog.result_data dialog.close() dialog = None if result is not None: self.insertText("Updated channel name '{old_name}' => '{new_name}, URL '{old_url}' => '{new_url}'".format(old_name=channel_data["name"], new_name=result["name"], old_url=channel_data["url"], new_url=result["url"])) self.load_channels(streamer["name"]) # Set the active channel to the previously selected (due to possible name change and sorting) self.channel_input.setCurrentIndex(self.channel_input.findText(result["name"])) def cmd_add_channel(self): streamer = self.config.get_streamer(self.streamer_input.currentText()) streamer_icon = os.path.join(IMAGESROOT, streamer["icon"]) dialog = AddEditChannelsDialog(self, self.config, title="Add a channel", streamer_icon=streamer_icon, streamer=streamer) dialog.exec() result = dialog.result_data dialog.close() dialog = None if result is not None: self.insertText("Added channel '{}' with URL '{}'".format(result["name"], result["url"])) self.load_channels(streamer["name"]) def cmd_delete_channel(self): channel = self.config.get_streamer_channel(self.streamer_input.currentText(), self.channel_input.currentText()) reply = QMessageBox.question(self, "Delete channel", "Are you sure you want to remove the channel?\nName: {}\nURL: {}".format(channel["name"], channel["url"]), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.config.delete_channel(self.streamer_input.currentText(), channel["name"]) self.insertText("Removed channel '{}' with URL '{}'".format(channel["name"], channel["url"])) self.load_channels(self.streamer_input.currentText()) def cmd_refresh_quality_cache(self): self.insertText("Refreshing cache for channel '{}'.".format(self.channel_input.currentText())) self.clear_quality_cache_button.setEnabled(False) self.clear_quality_cache_button.repaint() # Loading streams seems to block repainting of the GUI, so force a repaint here self.config.clean_quality_cache(self.streamer_input.currentText(), self.channel_input.currentText(), True) self.load_streams(True) self.clear_quality_cache_button.setEnabled(True) def on_close_override(self): self.close_override = True self.close() def on_streamer_select(self, event): # If the previously selected item is selected again, don't do anything if self.selections["streamer"] == self.streamer_input.currentText(): return self.selections["streamer"] = self.streamer_input.currentText() streamer = self.config.get_streamer(self.streamer_input.currentText()) self.set_window_icon() if streamer["favorite"]: self.fav_streamer_button.setEnabled(False) else: self.fav_streamer_button.setEnabled(True) def on_channel_select(self, event): # If the previously selected item is selected again, don't do anything if self.selections["channel"] == self.channel_input.currentText() or not self.channel_input.currentText(): return self.selections["channel"] = self.channel_input.currentText() channel = self.config.get_streamer_channel(self.streamer_input.currentText(), self.channel_input.currentText()) if channel and channel["favorite"]: self.fav_channel_button.setEnabled(False) else: self.fav_channel_button.setEnabled(True) self.load_streams() self.channel_input.setFocus(True) def load_streamers(self): streamers = self.config.get_streamers() favorite_streamer_index = 0 streamer_list = [] for index, streamer in enumerate(streamers): streamer_list.append(streamer["name"]) if streamer["favorite"]: favorite_streamer_index = index self.streamer_input.clear() self.streamer_input.addItems(streamer_list) if len(streamer_list) != 0: self.streamer_input.setCurrentIndex(favorite_streamer_index) self.selections["streamer"] = self.streamer_input.currentText() self.fav_streamer_button.setEnabled(False) def load_channels(self, streamer_name): channels = self.config.get_streamer_channels(streamer_name) self.channel_input.clear() favorite_channel = None channel_list = [] self.fav_channel_button.setEnabled(False) for index, channel in enumerate(channels): channel_list.append(channel["name"]) if channel["favorite"]: favorite_channel = channel["name"] self.channel_input.addItems(sorted(channel_list)) if len(channel_list) == 0: self.channel_input.addItem("(no channels exist for this streamer)") self.fav_channel_button.setEnabled(False) self.edit_channel_button.setEnabled(False) self.delete_channel_button.setEnabled(False) self.clear_quality_cache_button.setEnabled(False) self.channel_input.setEnabled(False) else: self.edit_channel_button.setEnabled(True) self.delete_channel_button.setEnabled(True) self.clear_quality_cache_button.setEnabled(True) self.channel_input.setEnabled(True) if favorite_channel is None: self.channel_input.setCurrentIndex(0) self.fav_channel_button.setEnabled(True) else: self.channel_input.setCurrentIndex(self.channel_input.findText(favorite_channel)) self.selections["channel"] = self.channel_input.currentText() def display_loaded_streams(self, streams, skip_caching=False): self.quality_input.clear() if len(streams) == 0: self.quality_input.addItem("(channel is currently not streaming)") else: self.run_livestreamer_button.setEnabled(True) self.clear_quality_cache_button.setEnabled(True) self.quality_input.addItems(sorted(streams)) self.quality_input.setCurrentIndex(0) self.quality_input.setEnabled(True) if not skip_caching: self.insertText("Cleaning any cached streams for channel '{}'...".format(self.channel_input.currentText())) self.config.clean_quality_cache(self.streamer_input.currentText(), self.channel_input.currentText()) self.insertText("Adding probed streams for channel '{}' to cache...".format(self.channel_input.currentText())) self.config.add_quality_to_cache(self.streamer_input.currentText(), self.channel_input.currentText(), streams) self.insertText("Done.") def load_streams(self, force_refresh=False): self.quality_input.clear() self.run_livestreamer_button.setEnabled(False) self.channel_input.setEnabled(False) self.quality_input.setEnabled(False) if self.channel_input.count() == 0: return streams = self.config.get_quality_from_cache(self.streamer_input.currentText(), self.channel_input.currentText()) if len(streams) > 0: self.display_loaded_streams(streams, True) self.insertText("Loaded streams for channel '{}' from cache.".format(self.channel_input.currentText())) else: self.insertText("No cached channel streams found for channel '{}'".format(self.channel_input.currentText())) if not force_refresh and self.config.get_config_value('auto-refresh-quality') == 0: self.quality_input.addItem("(auto-refresh is disabled; please refresh manually)") self.quality_input.setEnabled(False) else: stream_url = self.get_streamer_url() if stream_url is None: self.insertText("Failed to form a complete streamer URL (missing streamer/channel/stream)!") return self.probe_for_streams(stream_url) self.channel_input.setEnabled(True) def probe_for_streams(self, stream_url): self.insertText("Probing streamer's channel for live streams: {}".format(stream_url)) livestreamer = self.config.get_config_value("livestreamer-path") if livestreamer is None or livestreamer.strip() == "" or not os.path.isfile(livestreamer): self.insertText("Livestreamer path is not configured or file doesn't exist!") return command_format = self.config.get_config_value("probe-command-format") command = command_format.format(livestreamer=livestreamer, url=stream_url) self.livestreamer_thread = LivestreamerWorker(shlex.split(command)) self.livestreamer_thread.statusMessage.connect(self.parse_probed_streams, False) self.livestreamer_thread.start() self.livestreamer_thread.wait(self.thread_exit_grace_time) def parse_probed_streams(self, event): streams = [] message = event.message.lower() if "no streams found on this url" in message: self.insertText("No streams found. The channel is probably not streaming.") else: pos = message.find("available streams:") if pos == -1: return if "(best, worst)" in message: message = message.replace("(best, worst)", "(best and worst)") elif "(worst, best)" in message: message = message.replace("(worst, best)", "(worst and best)") qualities = message[pos+18:].split(",") for item in qualities: streams.append(item.strip()) left_parenthesis = item.find("(") if left_parenthesis == -1: continue if item.find("worst", left_parenthesis) >= left_parenthesis: streams.append("worst") if item.find("best", left_parenthesis) >= left_parenthesis: streams.append("best") streams.sort() self.insertText("Found {} stream(s): {}".format(len(streams), ", ".join(streams))) self.display_loaded_streams(streams) def get_streamer_url(self): streamer = self.config.get_streamer(self.streamer_input.currentText()) if streamer is None: self.insertText("No streamer selected!") return if streamer["url"] is None or streamer["url"].strip() == "": self.insertText("Invalid streamer URL!") return if self.channel_input.count() == 0: self.insertText("No channels exist!") return channel = self.config.get_streamer_channel(streamer["name"], self.channel_input.currentText()) return urljoin(streamer["url"], channel["url"]) def run_livestreamer(self): if self.livestreamer_thread is not None: if self.livestreamer_thread.isRunning(): self.insertText("Livestreamer should still be running!") return else: self.livestreamer_thread.wait(self.thread_exit_grace_time) self.livestreamer_thread = None self.update() if self.livestreamer_thread is None: livestreamer = self.config.get_config_value("livestreamer-path") if livestreamer is None or livestreamer.strip() == "" or not os.path.isfile(livestreamer): self.insertText("Livestreamer path is not configured or file doesn't exist!") return player = self.config.get_config_value("player-path") if player is None or player.strip() == "" or not os.path.isfile(player): self.insertText("Player path is not configured or file doesn't exist!") return stream_url = self.get_streamer_url() if stream_url is None: self.insertText("Failed to form a complete streamer URL (missing streamer/channel/stream)!") return command_format = self.config.get_config_value("command-format") quality = self.quality_input.currentText() if "(" in quality: quality = quality[:quality.find("(")].strip() command = command_format.format(livestreamer=livestreamer, player=player, url=stream_url, quality=quality) self.livestreamer_thread = LivestreamerWorker(shlex.split(command)) self.insertText("Starting Livestreamer thread.") self.livestreamer_thread.finished.connect(self.handle_livestreamer_thread_finished_signal) self.livestreamer_thread.statusMessage.connect(self.handle_livestreamer_thread_message_signal) self.livestreamer_thread.start() @QtCore.pyqtSlot(object) def handle_livestreamer_thread_message_signal(self, event): self.insertText(event.message, event.add_newline, event.add_timestamp) def handle_livestreamer_thread_finished_signal(self): self.livestreamer_thread = None def update_colors(self): foreground_color = self.config.get_config_value("foreground-color") background_color = self.config.get_config_value("background-color") self.cwidget.setStyleSheet("QWidget QLabel {{ color: {0} }} .QWidget {{ background-color: {1} }}".format(foreground_color, background_color)) self.cwidget.update() def insertText(self, msg, add_newline=True, timestamp=True): """Helper method for outputting text to the text box.""" text = "" if timestamp and self.timestamp_format is not None: timestamp = format(datetime.now().strftime(self.timestamp_format)) text = "{} ".format(timestamp) text += msg self.log_widget.moveCursor(QTextCursor.End) self.log_widget.insertPlainText(text) if add_newline: self.log_widget.insertPlainText("\n") self.log_widget.update()
class TrayIcon(QObject): def __init__(self): super().__init__() self._supported = QSystemTrayIcon.isSystemTrayAvailable() self._context_menu = None self._enabled = False self._trayicon = None self._state_icons = {} for state in ( 'disconnected', 'disabled', 'enabled', ): icon = QIcon(':/state-%s.svg' % state) if hasattr(icon, 'setIsMask'): icon.setIsMask(True) self._state_icons[state] = icon self._machine = None self._machine_state = 'disconnected' self._is_running = False self._update_state() def set_menu(self, menu): self._context_menu = menu if self._enabled: self._trayicon.setContextMenu(menu) def log(self, level, message): if self._enabled: if level <= log.INFO: icon = QSystemTrayIcon.Information timeout = 10 elif level <= log.WARNING: icon = QSystemTrayIcon.Warning timeout = 15 else: icon = QSystemTrayIcon.Critical timeout = 25 self._trayicon.showMessage(__software_name__.capitalize(), message, icon, timeout * 1000) else: if level <= log.INFO: icon = QMessageBox.Information elif level <= log.WARNING: icon = QMessageBox.Warning else: icon = QMessageBox.Critical msgbox = QMessageBox() msgbox.setText(message) msgbox.setIcon(icon) msgbox.exec_() def is_supported(self): return self._supported def enable(self): if not self._supported: return self._trayicon = QSystemTrayIcon() # On OS X, the context menu is activated with either mouse buttons, # and activation messages are still sent, so ignore those... if PLATFORM != 'mac': self._trayicon.activated.connect(self._on_activated) if self._context_menu is not None: self._trayicon.setContextMenu(self._context_menu) self._enabled = True self._update_state() self._trayicon.show() def disable(self): if not self._enabled: return self._trayicon.hide() self._trayicon = None self._enabled = False def is_enabled(self): return self._enabled def update_machine_state(self, machine, state): self._machine = machine self._machine_state = state self._update_state() def update_output(self, enabled): self._is_running = enabled self._update_state() clicked = pyqtSignal() def _update_state(self): if self._machine_state not in (STATE_INITIALIZING, STATE_RUNNING): state = 'disconnected' else: state = 'enabled' if self._is_running else 'disabled' icon = self._state_icons[state] if not self._enabled: return machine_state = _('{machine} is {state}').format( machine=_(self._machine), state=_(self._machine_state), ) if self._is_running: # i18n: Tray icon tooltip. output_state = _('output is enabled') else: # i18n: Tray icon tooltip. output_state = _('output is disabled') self._trayicon.setIcon(icon) self._trayicon.setToolTip( # i18n: Tray icon tooltip. 'Plover:\n- %s\n- %s' % (output_state, machine_state) ) def _on_activated(self, reason): if reason == QSystemTrayIcon.Trigger: self.clicked.emit()
class MainWindow(Window): tray = None isMain = True updater = None def __init__(self, isMain=True, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.isMain = isMain self.showWindowTitle = Util.getConfigValue('showTitle') self.showWindowIcon = Util.getConfigValue('showWindowIcon') self.defaultStyle = Util.getConfigValue('style') self.closeConfirm = Util.getConfigValue('closeConfirm') self.closeToTray = Util.getConfigValue('closeToTray') if not self.isMain: self.titleBar.buttonMore.hide() if not self.showWindowIcon: self.titleBar.iconLabel.hide() if not self.showWindowTitle: self.titleBar.titleLabel.hide() self.titleBar.settionAction.triggered.disconnect() self.titleBar.settionAction.triggered.connect(self.on_moreMenu_setting) self.titleBar.exitAction.triggered.disconnect() self.titleBar.exitAction.triggered.connect(self.on_moreMenu_exit) self.titleBar.settingLabel.setText(Util.lang('setting', 'Setting')) self.titleBar.exitLabel.setText(Util.lang('exit', 'Exit')) #updater self.updater = Updater() self.updater.signal.connect(self.log) self.updater.init() def on_moreMenu_setting(self): from setting import DlgSetting self.settingDialog = DlgSetting(self) self.settingWindow = Window() self.settingWindow.setWindowTitle(Util.lang('setting', 'Setting')) self.settingWindow.setWindowIcon(Icon.getLogoIcon()) self.settingWindow.setWidget(self.settingDialog) self.settingWindow.titleBar.buttonMinimum.hide() self.settingWindow.titleBar.buttonMaximum.hide() self.settingWindow.titleBar.buttonMore.hide() self.settingWindow.resizeCenter(500, 420) self.settingWindow.showPopupWindow() def reloadConfig(self): Util.reloadConfig() self.showWindowTitle = Util.getConfigValue('showTitle') self.showWindowIcon = Util.getConfigValue('showWindowIcon') self.defaultStyle = Util.getConfigValue('style') self.closeConfirm = Util.getConfigValue('closeConfirm') self.closeToTray = Util.getConfigValue('closeToTray') Util.loadTheme() def on_settingOK(self): old_style = self.defaultStyle self.reloadConfig() if self.showWindowIcon: self.titleBar.iconLabel.show() else: self.titleBar.iconLabel.hide() if self.showWindowTitle: self.titleBar.titleLabel.show() else: self.titleBar.titleLabel.hide() #change theme new_style = Util.getConfigValue('style') if old_style != new_style: self._widget.webview.page().runJavaScript( "changeBrowserTheme('%s')" % new_style) def on_moreMenu_exit(self): if self.closeConfirm and self.isMain: reply = MessageBox.question( self.parentWidget(), Util.lang('msg_title', 'Information'), Util.lang('exit_confirm', 'Are you sure to exit?'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.exitAll() else: self.exitAll() def closeEvent(self, event): if self.closeToTray and self.isMain: self.hide() event.ignore() elif self.closeConfirm and self.isMain: reply = MessageBox.question( self.parentWidget(), Util.lang('msg_title', 'Information'), Util.lang('exit_confirm', 'Are you sure to exit?'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.exitAll() else: event.ignore() elif self.isMain: self.exitAll() else: event.accept() def addSystemTray(self): self.tray = QSystemTrayIcon() self.icon = Icon.getLogoIcon() self.tray.setIcon(self.icon) self.tray.activated.connect(self.clickTray) #self.tray.messageClicked.connect(self.clickTray) self.tray_menu = QMenu(QApplication.desktop()) self.ShowAction = QAction(Util.lang('show_window', 'Window'), self, triggered=self.showWindow) self.SettingAction = QAction(Util.lang('setting', 'Setting'), self, triggered=self.on_moreMenu_setting) self.QuitAction = QAction(Util.lang('exit', 'Exit'), self, triggered=self.on_moreMenu_exit) self.tray_menu.addAction(self.ShowAction) self.tray_menu.addAction(self.SettingAction) self.tray_menu.addAction(self.QuitAction) self.tray.setContextMenu(self.tray_menu) self.tray.show() def clickTray(self, reason): if reason != QSystemTrayIcon.DoubleClick: return False self.showWindow() def showWindow(self): if self.isMaximized(): self.showMaximized() elif self.isFullScreen(): self.showFullScreen() else: self.showNormal() self.activateWindow() def loadData(self, splash=None): self._widget.loadUrl(Util.getConfigValue('url')) if Util.isWindows(): self.addSystemTray() #check upgrade mainScript = Util.getScriptName() t1 = threading.Thread(target=self.updater.checkUpgrade, args=(False, mainScript)) t1.setDaemon(True) t1.start() #splash if splash: splash.showMessage("Loading... 100%", Qt.AlignHCenter | Qt.AlignBottom, Qt.white) qApp.processEvents() def log(self, o): if o['extra']: if o['extra'][0] == 'check-end' and o['extra'][1]['allowUpgrade']: reply = MessageBox.question( self.parentWidget(), Util.lang('msg_title', 'Information'), Util.lang( 'allow_upgrade', 'There is a newer version, do you want to download it?' ), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.Yes: mainScript = Util.getScriptName() self.updater.runScriptFile(self.updater.updaterFilePath, ' -s' + mainScript) sys.exit() def exitAll(self): self.tray and self.tray.hide() sys.exit() def __del__(self): #print('close') self.tray and self.tray.hide()
class UI_Handler: def __init__(self, ui, MainWindow, config): # Initialize input value self.ui = ui self.timer_enabled = False self.random_movement_enabled = False self.random_delay_enabled = False self.tray_minimize_enabled = False self.mouse_handler = Mouse_Handler() self.ui_input = UI_Input(config) # Iinitialize minimize to tray handler self.MainWindow = MainWindow self.MainWindow.closeEvent = self.closeEvent self.tray_icon = QSystemTrayIcon(self.MainWindow) self.tray_icon.setIcon(QIcon('resource/256x256.png')) show_action = QAction("Restore", self.MainWindow) quit_action = QAction("Exit", self.MainWindow) show_action.triggered.connect(self.MainWindow.show) quit_action.triggered.connect(qApp.quit) quit_action.triggered.connect(self.tray_icon.hide) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() self.tray_icon.activated.connect(self.MainWindow.show) def enable_timer(self, element): # Enable timer if element.isChecked(): self.timer_enabled = True self.ui.timerHour.setEnabled(True) self.ui.timerMinute.setEnabled(True) self.ui.timerSecond.setEnabled(True) else: self.timer_enabled = False self.ui.timerHour.setEnabled(False) self.ui.timerMinute.setEnabled(False) self.ui.timerSecond.setEnabled(False) self.ui_input.timer_enabled = self.timer_enabled def enable_random_movement(self, element): # Enable random movement if element.isChecked(): self.random_movement_enabled = True else: self.random_movement_enabled = False self.ui_input.random_movement_enabled = self.random_movement_enabled def enable_random_delay(self, element): # Enable random delay interval between each movement if element.isChecked(): self.random_delay_enabled = True else: self.random_delay_enabled = False self.ui_input.random_delay_enabled = self.random_delay_enabled def enable_tray_minimize(self, element): # Enable the program to be minimize to system tray if element.isChecked(): self.tray_minimize_enabled = True else: self.tray_minimize_enabled = False def start_mouse_movement(self, element): # Start mouse movement element.setEnabled(False) self.ui.stopButton.setEnabled(True) self.enable_settings_input(False) self.mouse_handler.start_mouse_movement(self.ui_input) if self.timer_enabled: self.get_timer_values() self.start_timer() def stop_mouse_movement(self): # Stop mouse movement self.ui.stopButton.setEnabled(False) self.ui.startButton.setEnabled(True) self.enable_settings_input(True) self.mouse_handler.stop_mouse_movement() self.ui.currentTimerValue.setText("00:00:00") def start_timer(self): # Convert the timer inputs to seconds total_time = (self.ui_input.timer_Hour * 60 * 60) + \ (self.ui_input.timer_Minute * 60) + (self.ui_input.timer_Second) self.mouse_handler.start_timer(total_time, self) def enable_settings_input(self, state): """ Enable or disable all the settings input This will be call when the user press "Start" or "Stop" button """ self.ui.timerEnabled.setEnabled(state) if state is True: if self.ui.timerEnabled.isChecked(): self.ui.timerHour.setEnabled(True) self.ui.timerMinute.setEnabled(True) self.ui.timerSecond.setEnabled(True) else: self.ui.timerHour.setEnabled(False) self.ui.timerMinute.setEnabled(False) self.ui.timerSecond.setEnabled(False) else: self.ui.timerHour.setEnabled(state) self.ui.timerMinute.setEnabled(state) self.ui.timerSecond.setEnabled(state) self.ui.randomMovementEnabled.setEnabled(state) self.ui.randomDelayEnabled.setEnabled(state) def get_timer_values(self): # Get the input value from the timer field if (self.ui.timerHour.text() != ''): self.ui_input.timer_Hour = int(self.ui.timerHour.text()) else: self.ui_input.timer_Hour = 0 if (self.ui.timerMinute.text() != ''): self.ui_input.timer_Minute = int(self.ui.timerMinute.text()) else: self.ui_input.timer_Minute = 0 if (self.ui.timerSecond.text() != ''): self.ui_input.timer_Second = int(self.ui.timerSecond.text()) else: self.ui_input.timer_Second = 0 def closeEvent(self, event): # Will trigger when user pressed close button on the UI window if self.tray_minimize_enabled: event.ignore() self.MainWindow.hide() # self.tray_icon.showMessage( # "Mouse Mover", # "Minimized to tray", # QSystemTrayIcon.Information, # 2000 # ) else: self.stop_mouse_movement() self.tray_icon.hide()
class UI_Handler: def __init__(self, ui, MainWindow, config): # Initialize input value self.ui = ui self.timer_enabled = False self.random_movement_enabled = False self.random_delay_enabled = False self.tray_minimize_enabled = False self.mouse_handler = Mouse_Handler() self.ui_input = UI_Input(config) # Iinitialize minimize to tray handler self.MainWindow = MainWindow self.MainWindow.closeEvent = self.closeEvent self.tray_icon = QSystemTrayIcon(self.MainWindow) self.tray_icon.setIcon(QIcon('resource/256x256.png')) show_action = QAction("Restore", self.MainWindow) quit_action = QAction("Exit", self.MainWindow) show_action.triggered.connect(self.MainWindow.show) quit_action.triggered.connect(qApp.quit) quit_action.triggered.connect(self.tray_icon.hide) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() self.tray_icon.activated.connect(self.MainWindow.show) def enable_timer(self, element): if element.isChecked(): self.timer_enabled = True self.ui.timerHour.setEnabled(True) self.ui.timerMinute.setEnabled(True) self.ui.timerSecond.setEnabled(True) else: self.timer_enabled = False self.ui.timerHour.setEnabled(False) self.ui.timerMinute.setEnabled(False) self.ui.timerSecond.setEnabled(False) self.ui_input.timer_enabled = self.timer_enabled def enable_random_movement(self, element): if element.isChecked(): self.random_movement_enabled = True else: self.random_movement_enabled = False self.ui_input.random_movement_enabled = self.random_movement_enabled def enable_random_delay(self, element): if element.isChecked(): self.random_delay_enabled = True else: self.random_delay_enabled = False self.ui_input.random_delay_enabled = self.random_delay_enabled def enable_tray_minimize(self, element): if element.isChecked(): self.tray_minimize_enabled = True else: self.tray_minimize_enabled = False def start_mouse_movement(self, element): element.setEnabled(False) self.ui.stopButton.setEnabled(True) self.enable_settings_input(False) self.mouse_handler.start_mouse_movement(self.ui_input) if self.timer_enabled: self.get_timer_values() self.start_timer() def stop_mouse_movement(self): self.ui.stopButton.setEnabled(False) self.ui.startButton.setEnabled(True) self.enable_settings_input(True) self.mouse_handler.stop_mouse_movement() def start_timer(self): total_time = (self.ui_input.timer_Hour * 60 * 60) + (self.ui_input.timer_Minute * 60) + (self.ui_input.timer_Second) self.mouse_handler.start_timer(total_time, self) def enable_settings_input(self, state): self.ui.timerEnabled.setEnabled(state) if state == True: if self.ui.timerEnabled.isChecked(): self.ui.timerHour.setEnabled(True) self.ui.timerMinute.setEnabled(True) self.ui.timerSecond.setEnabled(True) else: self.ui.timerHour.setEnabled(False) self.ui.timerMinute.setEnabled(False) self.ui.timerSecond.setEnabled(False) else: self.ui.timerHour.setEnabled(state) self.ui.timerMinute.setEnabled(state) self.ui.timerSecond.setEnabled(state) self.ui.randomMovementEnabled.setEnabled(state) self.ui.randomDelayEnabled.setEnabled(state) def get_timer_values(self): if(self.ui.timerHour.text() != ''): self.ui_input.timer_Hour = int(self.ui.timerHour.text()) else: self.ui_input.timer_Hour = 0 if(self.ui.timerMinute.text() != ''): self.ui_input.timer_Minute = int(self.ui.timerMinute.text()) else: self.ui_input.timer_Minute = 0 if(self.ui.timerSecond.text() != ''): self.ui_input.timer_Second = int(self.ui.timerSecond.text()) else: self.ui_input.timer_Second = 0 def closeEvent(self, event): if self.tray_minimize_enabled: event.ignore() self.MainWindow.hide() # self.tray_icon.showMessage( # "Mouse Mover", # "Minimized to tray", # QSystemTrayIcon.Information, # 2000 # ) else: self.stop_mouse_movement() self.tray_icon.hide()
class ElectrumGui(Logger): network_dialog: Optional['NetworkDialog'] @profiler def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): set_language(config.get('language', get_default_language())) Logger.__init__(self) self.logger.info( f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}" ) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-dash.desktop') self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] # type: List[ElectrumWindow] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum-dash.png")) self._cleaned_up = False # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.network_dialog = None self.dash_net_dialog = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self.dash_net_sobj = QDashNetSignalsObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() self.dark_icon = self.config.get("dark_icon", False) self.tray = None self._init_tray() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def _init_tray(self): self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('Dash Electrum') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' self.app.setStyle('Fusion') if use_dark_theme: from .dark_dash_style import dash_stylesheet self.app.setStyleSheet(dash_stylesheet) else: from .dash_style import dash_stylesheet self.app.setStyleSheet(dash_stylesheet) # Apply any necessary stylesheet patches patch_qt_stylesheet(use_dark_theme=use_dark_theme) # Even if we ourselves don't set the dark theme, # the OS/window manager/etc might set *a dark theme*. # Hence, try to choose colors accordingly: ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme) def build_tray_menu(self): if not self.tray: return # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() m.addAction(_("Network"), self.show_network_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Dash Electrum"), self.app.quit) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): if not self.tray: return self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def _cleanup_before_exit(self): if self._cleaned_up: return self._cleaned_up = True self.app.new_window_signal.disconnect() self.efilter = None # If there are still some open windows, try to clean them up. for window in list(self.windows): window.close() window.clean_up() if self.network_dialog: self.network_dialog.close() self.network_dialog.clean_up() self.network_dialog = None self.network_updated_signal_obj = None if self.dash_net_dialog: self.dash_net_dialog.close() self.dash_net_dialog.clean_up() self.dash_net_dialog = None self.dash_net_sobj = None # Shut down the timer cleanly self.timer.stop() self.timer = None # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) if self.tray: self.tray.hide() self.tray.deleteLater() self.tray = None def _maybe_quit_if_no_windows_open(self) -> None: """Check if there are any open windows and decide whether we should quit.""" # keep daemon running after close if self.config.get('daemon'): return # check if a wizard is in progress with self._num_wizards_lock: if self._num_wizards_in_progress > 0 or len(self.windows) > 0: return self.app.quit() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self): if self.network_dialog: self.network_dialog.on_update() self.network_dialog.show() self.network_dialog.raise_() return self.network_dialog = NetworkDialog( network=self.daemon.network, config=self.config, network_updated_signal_obj=self.network_updated_signal_obj) self.network_dialog.show() def show_dash_net_dialog(self): if self.dash_net_dialog: self.dash_net_dialog.on_updated() self.dash_net_dialog.show() self.dash_net_dialog.raise_() return self.dash_net_dialog = DashNetDialog(network=self.daemon.network, config=self.config, dash_net_sobj=self.dash_net_sobj) self.dash_net_dialog.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() w.warn_if_testnet() w.warn_if_watching_only() return w def count_wizards_in_progress(func): def wrapper(self: 'ElectrumGui', *args, **kwargs): with self._num_wizards_lock: self._num_wizards_in_progress += 1 try: return func(self, *args, **kwargs) finally: with self._num_wizards_lock: self._num_wizards_in_progress -= 1 self._maybe_quit_if_no_windows_open() return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False) -> Optional[ElectrumWindow]: '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None if self.config.get('tor_auto_on', True): network = self.daemon.network if network: proxy_modifiable = self.config.is_modifiable('proxy') if not proxy_modifiable or not network.detect_tor_proxy(): warn_d = TorWarnDialog(self, path) warn_d.exec_() if warn_d.result() < 0: return try: wallet = self.daemon.load_wallet(path, None) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: try: wallet = self._start_wizard_to_select_or_create_wallet(path) except (WalletFileException, BitcoinException) as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (2):\n' + repr(e)) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot create window for wallet') + ':\n' + repr(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet( self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) try: path, storage = wizard.select_storage(path, self.daemon.get_wallet) # storage is None if file does not exist if storage is None: wizard.path = path # needed by trustedcoin plugin wizard.run('new') storage, db = wizard.create_storage(path) if db.check_unfinished_multisig(): wizard.show_message(_('Saved unfinished multisig wallet')) return else: db = WalletDB(storage.read(), manual_upgrades=False) if db.upgrade_done: storage.backup_old_version() wizard.run_upgrades(storage, db) if getattr(storage, 'backup_message', None): custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Information'), text=storage.backup_message) storage.backup_message = '' if db.check_unfinished_multisig(): wizard.continue_multisig_setup(storage) storage, db = wizard.create_storage(storage.path) if db.check_unfinished_multisig(): wizard.show_message(_('Saved unfinished multisig wallet')) return except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet finally: wizard.terminate() # return if wallet creation is not complete if storage is None or db.get_action(): return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window: ElectrumWindow): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever self.app.setQuitOnLastWindowClosed( False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open) self.app.aboutToQuit.connect(self._cleanup_before_exit) signal.signal(signal.SIGINT, lambda *args: self.app.quit()) # hook for crash reporter Exception_Hook.maybe_setup(config=self.config) # first-start network-setup try: self.init_network() except UserCancelled: return except GoBack: return except Exception as e: self.logger.exception('') return # start wizard to select/create wallet self.timer.start() path = self.config.get_wallet_path(use_gui_last_wallet=True) try: if not self.start_new_window( path, self.config.get('url'), app_is_starting=True): return except Exception as e: self.logger.error( "error loading wallet (or creating window for it)") send_exception_to_crash_reporter(e) # Let Qt event loop start properly so that crash reporter window can appear. # We will shutdown when the user closes that window, via lastWindowClosed signal. # main loop self.logger.info("starting Qt main loop") self.app.exec_() # on some platforms the exec_ call may not return, so use _cleanup_before_exit def stop(self): self.logger.info('closing GUI') self.app.quit()
class bridgePanel(QMainWindow, QObject): start = pyqtSignal() stop = pyqtSignal() def __init__(self, app): super().__init__() self.__v2rayshellConfigFile = { "preferences": { "v2ray-core": "", "v2ray-coreFilePath": "", "connection": { "connect": "switch", "interval": 45, "timeout": 3, "enable": True, "trytimes": 3 } }, "configFiles": [{ "enable": True, "hostName": "", "configFileName": "" }] } self.bridgetreasureChest = bridgetreasureChest.bridgetreasureChest() self.app = app self.translate = QCoreApplication.translate self.__v2rayshellVersion = "20180204" self.__windowTitile = "V2Ray-shell" + " " + self.__v2rayshellVersion self.runv2raycore = False self.iconStart = QIcon() self.iconStop = QIcon() self.__iconSize = QSize(32, 32) self.iconStart.addPixmap(QPixmap(filePath + "/icons/start.png"), QIcon.Normal, QIcon.On) self.iconStop.addPixmap(QPixmap(filePath + "/icons/stop.png"), QIcon.Disabled, QIcon.On) self.currentRowRightClicked = False self.v2rayshellTrayIcon = QSystemTrayIcon() self.v2rayshellTrayIcon.setIcon(self.iconStart) self.v2rayshellTrayIcon.show() self.radioButtonGroup = QButtonGroup() self.setV2RayshellLanguage() self.trytimes = self.bridgetreasureChest.getConnectiontrytimes() self.interval = self.bridgetreasureChest.getConnectioninterval() self.proxyTryConnect = proxyTryconnect() if v2rayshellDebug: self.proxyTryConnect.setresetTime(6, 3) else: self.proxyTryConnect.setresetTime(self.interval, self.trytimes) self.labelBridge = (self.translate("bridgePanel", "Start/Stop"), self.translate("bridgePanel", "Host Name"), self.translate("bridgePanel", "Config Name"), self.translate("bridgePanel", "Proxy"), self.translate("bridgePanel", "Time Lag")) self.createBridgePanel() def createBridgePanel(self): self.setWindowTitle(self.__windowTitile) self.setWindowIcon(self.iconStart) menubar = self.menuBar() self.statusBar() self.actionNewV2rayConfigFile = QAction( self.translate("bridgePanel", "Add V2Ray-core Config File"), self) self.actionNewV2rayConfigFile.setShortcut(QKeySequence.New) self.actionNewV2rayConfigFile.setStatusTip( self.translate("bridgePanel", "Add V2Ray-core Config File")) self.actionSaveV2rayshellConfigFile = QAction( self.translate("bridgePanel", "Save V2Ray-shell Config File"), self) self.actionSaveV2rayshellConfigFile.setShortcut(QKeySequence.Save) self.actionSaveV2rayshellConfigFile.setStatusTip( self.translate("bridgePanel", "Save V2Ray-shell Config File")) self.actionReloadV2rayshellConfigFile = QAction( self.translate("bridgePanel", "Open V2Ray-shell Config File"), self) self.actionReloadV2rayshellConfigFile.setShortcut(QKeySequence.Open) self.actionReloadV2rayshellConfigFile.setStatusTip( self.translate("bridgePanel", "Open V2Ray-shell Config File")) self.actionQuitV2rayshellPanel = QAction( self.translate("bridgePanel", "Quit"), self) if sys.platform.startswith('win'): self.actionQuitV2rayshellPanel.setShortcut("Ctrl+Q") else: self.actionQuitV2rayshellPanel.setShortcut(QKeySequence.Quit) self.actionQuitV2rayshellPanel.setStatusTip( self.translate("bridgePanel", "Quit V2Ray-shell")) fileMenu = menubar.addMenu(self.translate("bridgePanel", "&File")) fileMenu.addAction(self.actionNewV2rayConfigFile) fileMenu.addSeparator() fileMenu.addAction(self.actionReloadV2rayshellConfigFile) fileMenu.addAction(self.actionSaveV2rayshellConfigFile) fileMenu.addSeparator() fileMenu.addAction(self.actionQuitV2rayshellPanel) self.texteditBridge = QTextEdit(self) self.texteditBridge.setReadOnly(True) self.tableWidgetBridge = QTableWidget() self.tableWidgetBridge.setRowCount(0) self.tableWidgetBridge.setColumnCount(5) self.tableWidgetBridge.setHorizontalHeaderLabels(self.labelBridge) self.tableWidgetBridge.setSelectionMode( QAbstractItemView.SingleSelection) self.tableWidgetBridge.setSelectionBehavior( QAbstractItemView.SelectRows) self.tableWidgetBridge.setEditTriggers( QAbstractItemView.NoEditTriggers) #self.tableWidgetBridge.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tableWidgetBridge.setContextMenuPolicy(Qt.CustomContextMenu) self.popMenu = popMenu = QMenu(self.tableWidgetBridge) self.actionpopMenuAddV2rayConfigFile = QAction( self.translate("bridgePanel", "Add V2Ray Config File"), self) self.actionpopMenuAddV2rayConfigFile.setShortcut("Ctrl+n") self.actionpopMenuEditV2rayConfigFile = QAction( self.translate("bridgePanel", "Edit V2Ray Config File"), self) self.actionpopMenuProxyCheckTimeLag = QAction( self.translate("bridgePanel", "Proxy Time Lag Check..."), self) self.actionpopMenuDeleteRow = QAction( self.translate("bridgePanel", "Delete"), self) popMenu.addAction(self.actionpopMenuAddV2rayConfigFile) popMenu.addAction(self.actionpopMenuEditV2rayConfigFile) popMenu.addAction(self.actionpopMenuProxyCheckTimeLag) popMenu.addAction(self.actionpopMenuDeleteRow) self.actionopenV2rayshellPreferencesPanel = QAction( self.translate("bridgePanel", "preferences"), self) self.actionopenV2rayshellPreferencesPanel.setStatusTip( self.translate("bridgePanel", "Setting V2Ray-shell")) optionMenu = menubar.addMenu(self.translate("bridgePanel", "&options")) optionMenu.addAction(self.actionpopMenuProxyCheckTimeLag) optionMenu.addAction(self.actionopenV2rayshellPreferencesPanel) helpMenu = menubar.addMenu(self.translate("bridgePanel", "&help")) self.actioncheckv2raycoreupdate = QAction( self.translate("bridgePanel", "check V2Ray-core update"), self) self.actionv2rayshellBugreport = QAction( self.translate("bridgePanel", "Bug Report"), self) self.actionaboutv2rayshell = QAction( self.translate("bridgePanel", "About"), self) helpMenu.addAction(self.actioncheckv2raycoreupdate) helpMenu.addAction(self.actionv2rayshellBugreport) helpMenu.addAction(self.actionaboutv2rayshell) toolBar = QToolBar() self.actionV2rayStart = QAction(self.translate("bridgePanel", "Start")) self.actionV2rayStart.setIcon(self.style().standardIcon( getattr(QStyle, "SP_MediaPlay"))) self.actionV2rayStop = QAction(self.translate("bridgePanel", "Stop")) self.actionV2rayStop.setIcon(self.style().standardIcon( getattr(QStyle, "SP_MediaStop"))) toolBar.addAction(self.actionV2rayStart) toolBar.addAction(self.actionV2rayStop) self.addToolBar(toolBar) self.trayIconMenu = QMenu() self.v2rayshellTrayIcon.setContextMenu(self.trayIconMenu) self.trayIconMenushowhidePanel = QAction( self.translate("bridgePanel", "Show/Hide")) self.trayIconMenuclosePanel = QAction( self.translate("bridgePanel", "Quit")) self.trayIconMenu.addAction(self.trayIconMenushowhidePanel) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(self.trayIconMenuclosePanel) self.splitterBridge = QSplitter(Qt.Vertical) self.splitterBridge.addWidget(self.tableWidgetBridge) self.splitterBridge.addWidget(self.texteditBridge) self.setCentralWidget(self.splitterBridge) self.createBridgePanelSignals() self.onloadV2rayshellConfigFile(init=True) self.onv2raycoreStart() self.autocheckv2raycoreUpdate() def createBridgePanelSignals(self): self.actionNewV2rayConfigFile.triggered.connect( self.tableWidgetBridgeAddNewV2rayConfigFile) self.actionReloadV2rayshellConfigFile.triggered.connect( self.onloadV2rayshellConfigFile) self.actionSaveV2rayshellConfigFile.triggered.connect( self.onsaveV2rayshellConfigFile) self.actionopenV2rayshellPreferencesPanel.triggered.connect( self.createBridgepreferencesPanel) self.actionpopMenuAddV2rayConfigFile.triggered.connect( self.tableWidgetBridgeAddNewV2rayConfigFile) self.actionpopMenuEditV2rayConfigFile.triggered.connect( self.oncreatenauticalChartPanel) self.actionpopMenuDeleteRow.triggered.connect( self.tableWidgetBridgeDelete) self.actionpopMenuProxyCheckTimeLag.triggered.connect( self.onproxyserverTimeLagTest) self.actioncheckv2raycoreupdate.triggered.connect( self.onopenv2rayupdatePanel) self.actionv2rayshellBugreport.triggered.connect(self.bugReportPanel) self.actionQuitV2rayshellPanel.triggered.connect(self.close) self.actionV2rayStart.triggered.connect(self.onv2raycoreStart) self.actionV2rayStop.triggered.connect(self.onv2raycoreStop) self.actionaboutv2rayshell.triggered.connect(self.about) self.radioButtonGroup.buttonClicked.connect(self.onradioButtonClicked) self.tableWidgetBridge.cellDoubleClicked.connect( self.ontableWidgetBridgecellDoubleClicked) self.tableWidgetBridge.customContextMenuRequested.connect( self.ontableWidgetBridgeRightClicked) self.v2rayshellTrayIcon.activated.connect(self.restorebridgePanel) self.trayIconMenushowhidePanel.triggered.connect( self.onsystemTrayIconMenushowhidebridgePanel) self.trayIconMenuclosePanel.triggered.connect(self.close) self.proxyTryConnect.reconnectproxy.connect(self.swapNextConfigFile) self.start.connect(self.onupdateinstallFinishedstartNewV2raycore) self.stop.connect(self.onv2raycoreStop) def setV2RayshellLanguage(self): self.trans = QTranslator() language = self.bridgetreasureChest.getLanguage() allLanguages = self.bridgetreasureChest.getAllLanguage() if language and allLanguages: if language in allLanguages: self.trans.load(allLanguages[language]) self.app.installTranslator(self.trans) def autocheckv2raycoreUpdate(self): self.v2rayshellautoUpdate = updatePanel.updateV2ray() self.bridgeSingal = (self.start, self.stop) self.trycheckUpdate = QTimer() self.trycheckUpdate.timeout.connect( lambda: self.v2rayshellautoUpdate.enableUpdateSchedule( self.bridgetreasureChest, self.bridgeSingal)) self.trycheckUpdate.start(1000 * 60 * 60 * 4) ### Check every four hours self.trycheckUpdate.singleShot( ### Check when the script is started 1000 * 15, ### fifty seconds lambda: self.v2rayshellautoUpdate.enableUpdateSchedule( self.bridgetreasureChest, self.bridgeSingal)) def event(self, event): if (event.type() == QEvent.WindowStateChange and self.isMinimized()): self.setWindowFlags(self.windowFlags() & ~Qt.Tool) self.v2rayshellTrayIcon.show() return True else: return super(bridgePanel, self).event(event) def onsystemTrayIconMenushowhidebridgePanel(self): if self.isHidden(): self.showNormal() elif self.isVisible(): self.hide() def restorebridgePanel(self, reason): if reason == QSystemTrayIcon.Trigger: if self.isVisible(): self.hide() elif self.isHidden(): self.showNormal() def close(self): super(bridgePanel, self).close() self.v2rayshellTrayIcon.hide() self.onv2raycoreStop() def closeEvent(self, event): self.close() event.accept() sys.exit(self.app.exec_()) def onv2raycoreStop(self): if (self.runv2raycore): self.runv2raycore.stop.emit() try: ### force stop checking proxy time lag del self.autoCheckTimer except Exception: pass def onupdateinstallFinishedstartNewV2raycore(self): self.onloadV2rayshellConfigFile(init=True) self.onv2raycoreStart() def onv2raycoreStart(self): currentActiveRow = False rowCount = self.tableWidgetBridge.rowCount() for i in range(rowCount): currentActiveRow = self.tableWidgetBridge.cellWidget(i, 0) if currentActiveRow.isChecked(): self.texteditBridge.clear() option = self.tableWidgetBridge.item(i, 2) if option: option = '-config="{}" -format=json'.format(option.text()) else: option = "" filePath = self.bridgetreasureChest.getV2raycoreFilePath() if (filePath == False or filePath == ""): filePath = "v2ray" self.runv2raycore = runV2raycore.runV2raycore( outputTextEdit=self.texteditBridge, v2rayPath=filePath, v2rayOption=option, bridgetreasureChest=self.bridgetreasureChest) self.runv2raycore.start.emit() self.autocheckProxy(i) break else: del currentActiveRow def autocheckProxy(self, row): ### TODO """ Frequent access to the server may cause suspicion of DDOS attacks, which may put the VPS server at risk. """ enableAutoCheck = self.bridgetreasureChest.getConnectionEnable() if (enableAutoCheck): self.proxyStatus = proxyTest.proxyStatus() self.autoCheckTimer = QTimer() invervalTime = self.bridgetreasureChest.getConnectioninterval() timeout = self.bridgetreasureChest.getConnectiontimeout() proxyAddress = self.getProxyAddressFromTableWidget(row) if proxyAddress: self.autoCheckTimer.timeout.connect( lambda: self.startCheckProxy(timeout=timeout, proxyAddress=proxyAddress, row=row, proxyStatus=self.proxyStatus)) self.bridgetreasureChest.setProxy(proxyAddress) if v2rayshellDebug: self.autoCheckTimer.start(6000) else: self.autoCheckTimer.start(1000 * invervalTime) self.autoCheckTimer.singleShot( 100, lambda: self.startCheckProxy(timeout=timeout, proxyAddress=proxyAddress, row=row, proxyStatus=self.proxyStatus)) def setTableWidgetTimelag(self, row, proxyStatus): newlabelTimelag = self.setlabelTimeLagColor(proxyStatus) oldlabelTimelag = self.tableWidgetBridge.cellWidget(row, 4) del oldlabelTimelag self.tableWidgetBridge.setCellWidget(row, 4, newlabelTimelag) self.tableWidgetBridge.resizeColumnsToContents() def startCheckProxy(self, timeout, proxyAddress, row, proxyStatus): if (proxyAddress): proxyStatus.clear() proxyStatus.signal.connect( lambda: self.setTableWidgetTimelag(row, proxyStatus)) self.proxy = proxyTest.proxyTest(proxyprotocol=proxyAddress[0], proxyhostname=proxyAddress[1], proxyhostport=int( proxyAddress[2]), getproxyStatus=proxyStatus, timeout=int(timeout)) def setlabelTimeLagColor(self, proxyStatus=False): labelTimeLag = QLabel() if (proxyStatus and proxyStatus.getProxyError() == False): labelFont = QFont() labelFont.setPointSize(12) labelFont.setBold(True) labelTimeLag.setFont(labelFont) forestGreen = "QLabel {color: rgb(34, 139, 34)}" darkOrange = "QLabel {color: rgb(255, 140, 0)}" red = "QLabel {color: rgb(194,24,7)}" if (proxyStatus.getElapsedTime() < 260): labelTimeLag.setStyleSheet(forestGreen) elif (proxyStatus.getElapsedTime() > 420): labelTimeLag.setStyleSheet(red) else: labelTimeLag.setStyleSheet(darkOrange) labelTimeLag.setText("{} ms".format( str(proxyStatus.getElapsedTime()))) return labelTimeLag elif (proxyStatus and proxyStatus.getProxyError()): labelTimeLag.setText("{}:{}".format( proxyStatus.getProxyErrorString(), proxyStatus.getProxyErrorCode())) self.proxyTryConnect.trytimesDecrease() return labelTimeLag def swapNextConfigFile(self): self.onv2raycoreStop() try: self.trytimes = self.bridgetreasureChest.getConnectiontrytimes() self.interval = self.bridgetreasureChest.getConnectioninterval() self.proxyTryConnect.stopperiodicCheckProxyStatus() if v2rayshellDebug: self.proxyTryConnect.setresetTime(6, 3) else: self.proxyTryConnect.setresetTime(self.interval, self.trytimes) except Exception: self.proxyTryConnect.setresetTime(60, 3) if (self.bridgetreasureChest.connectionisSwitch()): ### swap next row's configFile buttons = self.radioButtonGroup.buttons() buttonsNumber = len(buttons) activeRow = False for i in range(buttonsNumber): if buttons[i].isChecked(): buttons[i].setChecked(False) if i == buttonsNumber - 1: buttons[0].setChecked(True) activeRow = 0 break else: buttons[i + 1].setChecked(True) activeRow = i + 1 break ### change the row icons for i in range(buttonsNumber): widget = self.tableWidgetBridge.cellWidget(i, 0) if (widget): widget.setIcon(self.iconStop) widget.setIconSize(self.__iconSize) if (widget.isChecked()): pass widget = self.tableWidgetBridge.cellWidget(activeRow, 0) if widget: widget.setIcon(self.iconStart) widget.setIconSize(self.__iconSize) self.onv2raycoreStart() def onopenv2rayupdatePanel(self): currentActiveRow = False rowCount = self.tableWidgetBridge.rowCount() currentRow = False for i in range(rowCount): currentActiveRow = self.tableWidgetBridge.cellWidget(i, 0) if currentActiveRow.isChecked(): currentRow = i break if (currentActiveRow and currentActiveRow.isChecked()): proxy = self.tableWidgetBridge.item(currentRow, 3) proxy = proxy.text().split(":") protocol = QNetworkProxy.Socks5Proxy if (proxy[0] == "socks"): protocol = QNetworkProxy.Socks5Proxy elif (proxy[0] == "http"): protocol = QNetworkProxy.HttpProxy hostName = proxy[1] hostPort = int(proxy[2]) v2rayAPI = updatePanel.v2rayAPI() self.createupdatePanel = updatePanel.v2rayUpdatePanel( v2rayapi=v2rayAPI, protocol=protocol, proxyhostName=hostName, port=hostPort, bridgetreasureChest=self.bridgetreasureChest) self.createupdatePanel.createPanel() self.createupdatePanel.setAttribute(Qt.WA_DeleteOnClose) self.createupdatePanel.setWindowIcon(self.iconStart) self.createupdatePanel.setWindowTitle( self.translate("bridgePanel", "Check V2Ray-core update")) self.createupdatePanel.resize(QSize(1024, 320)) self.createupdatePanel.move( QApplication.desktop().screen().rect().center() - self.createupdatePanel.rect().center()) self.createupdatePanel.show() self.createupdatePanel.exec_() else: self.noPoxyServerRunning() def ontableWidgetBridgeRightClicked(self, pos): index = self.tableWidgetBridge.indexAt(pos) clickedRow.rightClickedRow = index.row() clickedRow.mousePos = QCursor().pos() self.popMenu.move(QCursor().pos()) self.popMenu.show() def ontableWidgetBridgecellDoubleClicked(self, row, column): if (column == 1): hostName, ok = QInputDialog.getText( self, self.translate("bridgePanel", 'Host Name'), self.translate("bridgePanel", 'Enter Host Name:')) if (ok): self.tableWidgetBridge.setItem(row, column, QTableWidgetItem(str(hostName))) self.tableWidgetBridge.resizeColumnsToContents() elif (column == 2): fileNames = self.onopenV2rayConfigJSONFile() if (fileNames): for fileName in fileNames: self.tableWidgetBridge.setItem( row, column, QTableWidgetItem(str(fileName))) self.tableWidgetBridge.resizeColumnsToContents() elif (column == 3): self.onproxyserverTimeLagTest() elif (column == 4): self.onproxyserverTimeLagTest() def getProxyAddressFromTableWidget(self, row): proxy = self.tableWidgetBridge.item(row, 3) try: proxy = proxy.text().split(":") except Exception: return False if (proxy[0] == "socks"): proxy[0] = QNetworkProxy.Socks5Proxy elif (proxy[0] == "http"): proxy[0] = QNetworkProxy.HttpProxy if len(proxy) < 3: return False else: return proxy def onproxyserverTimeLagTest(self): proxyStatus = proxyTest.proxyStatus() """ right clicked mouse button pop a menu check proxy """ currentActiveRow = False rowCount = self.tableWidgetBridge.rowCount() currentRow = False for i in range(rowCount): currentActiveRow = self.tableWidgetBridge.cellWidget(i, 0) if currentActiveRow.isChecked(): currentRow = i break if (currentActiveRow and currentActiveRow.isChecked()): proxy = self.getProxyAddressFromTableWidget(currentRow) protocol = proxy[0] hostName = proxy[1] hostPort = int(proxy[2]) proxy = proxyTest.proxyTestPanel(proxyhostname=hostName, proxyhostport=hostPort, proxyprotocol=protocol, getproxyStatus=proxyStatus) proxy.createproxyTestPanel() proxy.setAttribute(Qt.WA_DeleteOnClose) proxy.setWindowTitle( self.translate("bridgePanel", "Proxy Time Lag Check")) proxy.setWindowIcon(self.iconStart) proxy.resize(QSize(600, 480)) proxy.move(QApplication.desktop().screen().rect().center() - proxy.rect().center()) proxy.show() proxy.exec_() else: self.noPoxyServerRunning() def noPoxyServerRunning(self): warningPanel = QDialog() warningPanel.setAttribute(Qt.WA_DeleteOnClose) warningPanel.setWindowTitle(self.translate("bridgePanel", "Warnnig...")) warningPanel.setWindowIcon(self.iconStop) labelMsg = QLabel( self.translate( "bridgePanel", "There no any server is running, \n[File]->[Add V2Ray-core Config File] (Ctrl+n) add a config.json." )) vbox = QVBoxLayout() vbox.addWidget(labelMsg) warningPanel.setLayout(vbox) warningPanel.move(QApplication.desktop().screen().rect().center() - warningPanel.rect().center()) warningPanel.open() warningPanel.exec_() def getProxyAddressFromJSONFile(self, filePath): from bridgehouse.editMap.port import treasureChest, openV2rayJSONFile tempTreasureChest = treasureChest.treasureChest() openV2rayJSONFile.openV2rayJSONFile( filePath, tempTreasureChest, disableLog=True).initboundJSONData() inbound = tempTreasureChest.getInbound() if (inbound): protocol = inbound["protocol"] ipAddress = inbound["listen"] port = inbound["port"] if (protocol == "socks" or protocol == "http"): return "{}:{}:{}".format(protocol, ipAddress, port) else: return False else: return False def onradioButtonClicked(self, e): rowCount = self.tableWidgetBridge.rowCount() #radioButtonClickedRow = 0 for i in range(rowCount): widget = self.tableWidgetBridge.cellWidget(i, 0) if (widget): widget.setIcon(self.iconStop) widget.setIconSize(self.__iconSize) if (widget.isChecked()): #radioButtonClickedRow = i pass e.setIcon(self.iconStart) e.setIconSize(self.__iconSize) def onloadV2rayshellConfigFile(self, init=False): """ when the script first start, and auto load v2ray-shell config file. """ if init: self.settingv2rayshelltableWidget() else: def openV2rayshellConfigFile(): options = QFileDialog.Options() filePath, _ = QFileDialog.getOpenFileName( self, self.translate("bridgePanel", "Open V2Ray-sehll Config File"), "", "V2Ray-shell config file (*.v2rayshell)", options=options) if (filePath): self.bridgetreasureChest.clear() self.tableWidgetBridge.setRowCount(0) self.bridgetreasureChest.inibridgeJSONData( v2rayshellConfigFileName=filePath) self.settingv2rayshelltableWidget() openV2rayshellConfigFile() def onopenV2rayConfigJSONFile(self): """ open a new v2ray config file to tabelWidget """ options = QFileDialog.Options() filePaths, _ = QFileDialog.getOpenFileNames( self, self.translate("bridgePanel", "Open V2Ray-core Config File"), "", """ V2Ray config file (*.json);; All File (*);; """, options=options) if (filePaths): return filePaths else: return False def createBridgepreferencesPanel(self): self.createpreferencesPanel = bridgePreference.bridgepreferencesPanel( self.bridgetreasureChest) self.createpreferencesPanel.setAttribute(Qt.WA_DeleteOnClose) self.createpreferencesPanel.createpreferencesPanel() self.createpreferencesPanel.setWindowIcon(self.iconStart) self.createpreferencesPanel.move( QApplication.desktop().screen().rect().center() - self.createpreferencesPanel.rect().center()) self.createpreferencesPanel.open() self.createpreferencesPanel.exec_() def settingv2rayshelltableWidget(self): v2rayConfigFiles = self.bridgetreasureChest.getV2raycoreconfigFiles() if v2rayConfigFiles == False: return v2rayConfigFilesNumber = len(v2rayConfigFiles) if (v2rayConfigFilesNumber > 0): self.tableWidgetBridge.setRowCount(0) for i in range(v2rayConfigFilesNumber): try: enable = bool(v2rayConfigFiles[i]["enable"]) hostName = str(v2rayConfigFiles[i]["hostName"]) configFileName = str(v2rayConfigFiles[i]["configFileName"]) except Exception: pass radioButtonStopStart = QRadioButton(self) radioButtonStopStart.setIcon( self.iconStop if not enable else self.iconStart) radioButtonStopStart.setChecked(True if enable else False) radioButtonStopStart.setIconSize(self.__iconSize) self.radioButtonGroup.addButton(radioButtonStopStart) self.tableWidgetBridge.setRowCount(i + 1) self.tableWidgetBridge.setCellWidget(i, 0, radioButtonStopStart) self.tableWidgetBridge.setItem(i, 1, QTableWidgetItem(hostName)) self.tableWidgetBridge.setItem( i, 2, QTableWidgetItem(configFileName)) self.tableWidgetBridge.setItem( i, 3, QTableWidgetItem( self.getProxyAddressFromJSONFile(configFileName))) self.tableWidgetBridge.resizeColumnsToContents() #self.tableWidgetBridge.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) def onsaveV2rayshellConfigFile(self): self.bridgetreasureChest.clearconfigFiles() rowCount = self.tableWidgetBridge.rowCount() for i in range(rowCount): enable = self.tableWidgetBridge.cellWidget(i, 0) if enable and enable.isChecked(): enable = True else: enable = False hostName = self.tableWidgetBridge.item(i, 1) if hostName: hostName = hostName.text() else: hostName = "" config = self.tableWidgetBridge.item(i, 2) if config: config = config.text() else: config = "" self.bridgetreasureChest.setV2raycoreconfigFiles( enable, hostName, configFileName=config) self.bridgetreasureChest.save.emit() def oncreatenauticalChartPanel(self): v2rayConfigFileName = self.tableWidgetBridge.item( clickedRow.rightClickedRow, 2) if (v2rayConfigFileName): nc = nauticalChartPanel.nauticalChartPanel( v2rayConfigFileName.text()) nc.setAttribute(Qt.WA_DeleteOnClose) nc.createPanel() nc.setWindowTitle( self.translate("bridgePanel", "V2Ray config file edit")) nc.setWindowIcon(self.iconStart) nc.setGeometry(0, 0, 1024, 768) ### move widget to center nc.move(QApplication.desktop().screen().rect().center() - nc.rect().center()) nc.show() nc.exec_() def tableWidgetBridgeAddNewV2rayConfigFile(self): configFileNames = self.onopenV2rayConfigJSONFile() if (configFileNames): for configFileName in configFileNames: rowCount = self.tableWidgetBridge.rowCount() radioButtonStopStart = QRadioButton(self) radioButtonStopStart.setIcon(self.iconStop) radioButtonStopStart.setIconSize(self.__iconSize) self.radioButtonGroup.addButton(radioButtonStopStart) self.tableWidgetBridge.setRowCount(rowCount + 1) self.tableWidgetBridge.setCellWidget(rowCount, 0, radioButtonStopStart) self.tableWidgetBridge.setItem(rowCount, 1, QTableWidgetItem("")) self.tableWidgetBridge.setItem( rowCount, 2, QTableWidgetItem(configFileName)) self.tableWidgetBridge.setItem( rowCount, 3, QTableWidgetItem( self.getProxyAddressFromJSONFile(configFileName))) self.tableWidgetBridge.resizeColumnsToContents() else: pass def tableWidgetBridgeDelete(self): self.tableWidgetBridge.removeRow(clickedRow.rightClickedRow) def validateV2rayJSONFile(self, JSONData): """ simply validate a V2Ray json file. """ try: JSONData["inbound"] JSONData["outbound"] except KeyError: return False else: return True def about(self): NineteenEightySeven = QLabel( self.translate( "bridgePanel", """Across the Great Wall, we can reach every corner in the world.""" )) ### Crossing the Great Wall to Join the World Timeless = QLabel( self.translate( "bridgePanel", """You weren't thinking about that when you were creating it.\nBecause if you did? You never would have gone through with it.""" )) DwayneRichardHipp = QLabel( self.translate( "bridgePanel", """May you do good and not evil.\nMay you find forgiveness for yourself and forgive others.\nMay you share freely, never taking more than you give.""" )) vbox = QVBoxLayout() vbox.addWidget(NineteenEightySeven) vbox.addWidget(Timeless) vbox.addWidget(DwayneRichardHipp) dialogAbout = QDialog() dialogAbout.setAttribute(Qt.WA_DeleteOnClose) dialogAbout.setWindowTitle( self.translate("bridgePanel", "About V2Ray-shell")) dialogAbout.setWindowIcon(self.iconStart) dialogAbout.move(QApplication.desktop().screen().rect().center() - dialogAbout.rect().center()) dialogAbout.setLayout(vbox) dialogAbout.open() dialogAbout.exec_() def bugReportPanel(self): self.bugReport = bugReport.bugReport() self.bugReport.setAttribute(Qt.WA_DeleteOnClose) self.bugReport.setWindowTitle( self.translate("bridgePanel", "Bug Report")) self.bugReport.setWindowIcon(self.iconStart) self.bugReport.createPanel() self.bugReport.show() self.bugReport.setGeometry(250, 150, 1024, 768)
class BlenderLauncher(QMainWindow, BaseWindow, Ui_MainWindow): def __init__(self, app): super().__init__() self.setupUi(self) # Server self.server = QLocalServer() self.server.listen("blender-launcher-server") self.server.newConnection.connect(self.new_connection) # Global scope self.app = app self.favorite = None self.status = "None" self.app_state = AppState.IDLE self.cashed_builds = [] self.manager = PoolManager(200) self.timer = None # Setup window self.setWindowTitle("Blender Launcher") self.app.setWindowIcon(QIcon(":resources/icons/tray.ico")) # Setup font QFontDatabase.addApplicationFont( ":/resources/fonts/OpenSans-SemiBold.ttf") self.font = QFont("Open Sans SemiBold", 10) self.font.setHintingPreference(QFont.PreferNoHinting) self.app.setFont(self.font) # Setup style file = QFile(":/resources/styles/global.qss") file.open(QFile.ReadOnly | QFile.Text) self.style_sheet = QTextStream(file).readAll() self.app.setStyleSheet(self.style_sheet) # Check library folder if is_library_folder_valid() is False: self.dlg = DialogWindow( self, title="Information", text="First, choose where Blender\nbuilds will be stored", accept_text="Continue", cancel_text=None, icon=DialogIcon.INFO) self.dlg.accepted.connect(self.set_library_folder) else: self.draw() def set_library_folder(self): library_folder = Path.cwd().as_posix() new_library_folder = QFileDialog.getExistingDirectory( self, "Select Library Folder", library_folder) if new_library_folder: set_library_folder(new_library_folder) self.draw() def draw(self): self.HeaderLayout = QHBoxLayout() self.HeaderLayout.setContentsMargins(1, 1, 1, 0) self.HeaderLayout.setSpacing(0) self.CentralLayout.addLayout(self.HeaderLayout) self.SettingsButton = \ QPushButton(QIcon(":resources/icons/settings.svg"), "") self.SettingsButton.setIconSize(QSize(20, 20)) self.SettingsButton.setFixedSize(36, 32) self.WikiButton = \ QPushButton(QIcon(":resources/icons/wiki.svg"), "") self.WikiButton.setIconSize(QSize(20, 20)) self.WikiButton.setFixedSize(36, 32) self.MinimizeButton = \ QPushButton(QIcon(":resources/icons/minimize.svg"), "") self.MinimizeButton.setIconSize(QSize(20, 20)) self.MinimizeButton.setFixedSize(36, 32) self.CloseButton = \ QPushButton(QIcon(":resources/icons/close.svg"), "") self.CloseButton.setIconSize(QSize(20, 20)) self.CloseButton.setFixedSize(36, 32) self.HeaderLabel = QLabel("Blender Launcher") self.HeaderLabel.setAlignment(Qt.AlignCenter) self.HeaderLayout.addWidget(self.SettingsButton, 0, Qt.AlignLeft) self.HeaderLayout.addWidget(self.WikiButton, 0, Qt.AlignLeft) self.HeaderLayout.addWidget(self.HeaderLabel, 1) self.HeaderLayout.addWidget(self.MinimizeButton, 0, Qt.AlignRight) self.HeaderLayout.addWidget(self.CloseButton, 0, Qt.AlignRight) self.SettingsButton.setProperty("HeaderButton", True) self.WikiButton.setProperty("HeaderButton", True) self.MinimizeButton.setProperty("HeaderButton", True) self.CloseButton.setProperty("HeaderButton", True) self.CloseButton.setProperty("CloseButton", True) # Tab layout self.TabWidget = QTabWidget() self.CentralLayout.addWidget(self.TabWidget) self.LibraryTab = QWidget() self.LibraryTabLayout = QVBoxLayout() self.LibraryTabLayout.setContentsMargins(0, 0, 0, 0) self.LibraryTab.setLayout(self.LibraryTabLayout) self.TabWidget.addTab(self.LibraryTab, "Library") self.DownloadsTab = QWidget() self.DownloadsTabLayout = QVBoxLayout() self.DownloadsTabLayout.setContentsMargins(0, 0, 0, 0) self.DownloadsTab.setLayout(self.DownloadsTabLayout) self.TabWidget.addTab(self.DownloadsTab, "Downloads") self.LibraryToolBox = BaseToolBoxWidget(self) self.LibraryStableListWidget = \ self.LibraryToolBox.add_list_widget("Stable Releases") self.LibraryDailyListWidget = \ self.LibraryToolBox.add_list_widget("Daily Builds") self.LibraryExperimentalListWidget = \ self.LibraryToolBox.add_list_widget("Experimental Branches") self.LibraryCustomListWidget = \ self.LibraryToolBox.add_list_widget("Custom Builds") self.LibraryTab.layout().addWidget(self.LibraryToolBox) self.DownloadsToolBox = BaseToolBoxWidget(self) self.DownloadsStableListWidget = \ self.DownloadsToolBox.add_list_widget("Stable Releases") self.DownloadsDailyListWidget = \ self.DownloadsToolBox.add_list_widget("Daily Builds") self.DownloadsExperimentalListWidget = \ self.DownloadsToolBox.add_list_widget("Experimental Branches") self.DownloadsTab.layout().addWidget(self.DownloadsToolBox) self.LibraryToolBox.setCurrentIndex(get_default_library_page()) # Connect buttons self.SettingsButton.clicked.connect(self.show_settings_window) self.WikiButton.clicked.connect(lambda: webbrowser.open( "https://github.com/DotBow/Blender-Launcher/wiki")) self.MinimizeButton.clicked.connect(self.showMinimized) self.CloseButton.clicked.connect(self.close) self.StatusBar.setFont(self.font) self.statusbarLabel = QLabel() self.statusbarVersion = QLabel(self.app.applicationVersion()) self.StatusBar.addPermanentWidget(self.statusbarLabel, 1) self.StatusBar.addPermanentWidget(self.statusbarVersion) # Draw library self.draw_library() # Setup tray icon context Menu quit_action = QAction("Quit", self) quit_action.triggered.connect(self.quit) hide_action = QAction("Hide", self) hide_action.triggered.connect(self.hide) show_action = QAction("Show", self) show_action.triggered.connect(self._show) launch_favorite_action = QAction( QIcon(":resources/icons/favorite.svg"), "Blender", self) launch_favorite_action.triggered.connect(self.launch_favorite) tray_menu = QMenu() tray_menu.setFont(self.font) tray_menu.addAction(launch_favorite_action) tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addAction(quit_action) # Setup tray icon self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(QIcon(":resources/icons/tray.ico")) self.tray_icon.setToolTip("Blender Launcher") self.tray_icon.activated.connect(self.tray_icon_activated) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() # Forse style update self.style().unpolish(self.app) self.style().polish(self.app) # Show window if get_launch_minimized_to_tray() is False: self._show() def _show(self): self.activateWindow() self.show() self.set_status() def launch_favorite(self): try: self.favorite.launch() except Exception: self.dlg = DialogWindow( self, text="Favorite build not found!", accept_text="OK", cancel_text=None) def tray_icon_activated(self, reason): if reason == QSystemTrayIcon.Trigger: self._show() elif reason == QSystemTrayIcon.MiddleClick: self.launch_favorite() def quit(self): download_widgets = [] download_widgets.extend(self.DownloadsStableListWidget.items()) download_widgets.extend(self.DownloadsDailyListWidget.items()) download_widgets.extend(self.DownloadsExperimentalListWidget.items()) for widget in download_widgets: if widget.state == DownloadState.DOWNLOADING: self.dlg = DialogWindow( self, title="Warning", text="Download task in progress!<br>\ Are you sure you want to quit?", accept_text="Yes", cancel_text="No", icon=DialogIcon.WARNING) self.dlg.accepted.connect(self.quit2) return self.quit2() def quit2(self): if self.timer is not None: self.timer.cancel() self.tray_icon.hide() self.app.quit() def draw_library(self, clear=False): self.set_status("Reading local builds") if clear: self.timer.cancel() self.scraper.quit() self.DownloadsStableListWidget.clear() self.DownloadsDailyListWidget.clear() self.DownloadsExperimentalListWidget.clear() self.favorite = None self.LibraryStableListWidget.clear() self.LibraryDailyListWidget.clear() self.LibraryExperimentalListWidget.clear() self.library_drawer = LibraryDrawer(self) self.library_drawer.build_found.connect(self.draw_to_library) self.library_drawer.finished.connect(self.draw_downloads) self.library_drawer.start() def draw_downloads(self): self.app_state = AppState.CHECKINGBUILDS self.set_status("Checking for new builds") self.scraper = Scraper(self, self.manager) self.scraper.links.connect(self.draw_new_builds) self.scraper.error.connect(self.connection_error) self.scraper.start() def connection_error(self): set_locale() utcnow = strftime(('%H:%M'), localtime()) self.set_status("Connection Error at " + utcnow) self.app_state = AppState.IDLE self.timer = threading.Timer(600.0, self.draw_downloads) self.timer.start() def draw_new_builds(self, builds): self.cashed_builds.clear() self.cashed_builds.extend(builds) library_widgets = [] download_widgets = [] library_widgets.extend(self.LibraryStableListWidget.items()) library_widgets.extend(self.LibraryDailyListWidget.items()) library_widgets.extend(self.LibraryExperimentalListWidget.items()) download_widgets.extend(self.DownloadsStableListWidget.items()) download_widgets.extend(self.DownloadsDailyListWidget.items()) download_widgets.extend(self.DownloadsExperimentalListWidget.items()) for widget in download_widgets: if widget.build_info in builds: builds.remove(widget.build_info) elif widget.state != DownloadState.DOWNLOADING: widget.destroy() for widget in library_widgets: if widget.build_info in builds: builds.remove(widget.build_info) for build_info in builds: self.draw_to_downloads(build_info) set_locale() utcnow = strftime(('%H:%M'), localtime()) self.set_status("Last check at " + utcnow) self.app_state = AppState.IDLE self.timer = threading.Timer(600.0, self.draw_downloads) self.timer.start() def draw_from_cashed(self, build_info): if self.app_state == AppState.IDLE: if build_info in self.cashed_builds: i = self.cashed_builds.index(build_info) self.draw_to_downloads(self.cashed_builds[i]) def draw_to_downloads(self, build_info): branch = build_info.branch if branch == 'stable': list_widget = self.DownloadsStableListWidget elif branch == 'daily': list_widget = self.DownloadsDailyListWidget else: list_widget = self.DownloadsExperimentalListWidget item = BaseListWidgetItem(build_info.commit_time) widget = DownloadWidget(self, list_widget, item, build_info) item.setSizeHint(widget.sizeHint()) list_widget.addItem(item) list_widget.setItemWidget(item, widget) def draw_to_library(self, path): category = Path(path).parent.name if category == 'stable': list_widget = self.LibraryStableListWidget elif category == 'daily': list_widget = self.LibraryDailyListWidget elif category == 'experimental': list_widget = self.LibraryExperimentalListWidget elif category == 'custom': list_widget = self.LibraryCustomListWidget else: return item = BaseListWidgetItem() widget = LibraryWidget(self, item, path, list_widget) list_widget.insertItem(0, item) list_widget.setItemWidget(item, widget) def set_status(self, status=None): if status is not None: self.status = status self.statusbarLabel.setText("Status: {0}".format(self.status)) def show_settings_window(self): self.settings_window = SettingsWindow(self) def clear_temp(self): temp_folder = Path(get_library_folder()) / ".temp" self.remover = Remover(temp_folder) self.remover.start() def closeEvent(self, event): event.ignore() self.hide() def new_connection(self): self._show()
class PlayerUi(QDialog): def __init__(self, parent = None): super(PlayerUi, self).__init__(parent) self.create_actions() self.setup_ui() self.managePage.ui_initial() self.load_style_sheet(Configures.QssFileDefault, Configures.IconsDir) def load_style_sheet(self, sheetName, iconsDir): """load qss file""" print('Using qss file: %s'%sheetName) qss = QFile(sheetName) qss.open(QFile.ReadOnly) styleSheet = str(qss.readAll(), encoding='utf8').replace(':PathPrefix', iconsDir) QApplication.instance().setStyleSheet(styleSheet) qss.close() def create_connections(self): self.searchBox.search_musics_signal.connect(self.search_musics_online) self.searchBox.searchtype_changed_signal.connect(self.managePage.searchFrame.searchtype_changed) self.aboutButton.clicked.connect(self.show_about_page) self.preferenceButton.clicked.connect(self.show_settings_frame) self.minButton.clicked.connect(self.show_minimized) self.closeButton.clicked.connect(self.close_button_acted) self.playbackPanel.show_artist_info_signal.connect(self.show_artist_info) self.playbackPanel.dont_hide_main_window_signal.connect(self.dont_hide_mainwindow) self.managePage.playlistWidget.show_artist_info_signal.connect(self.show_artist_info) self.managePage.playlistWidget.show_song_info_signal.connect(self.show_song_info) self.managePage.playlistWidget.musics_added_signal.connect(self.hide_song_info_page) self.managePage.playlistWidget.musics_removed_signal.connect(self.hide_song_info_page) self.managePage.playlistWidget.musics_cleared_signal.connect(self.hide_song_info_page) self.managePage.playlist_removed_signal.connect(self.hide_song_info_page) self.managePage.playlist_renamed_signal.connect(self.hide_song_info_page) self.managePage.playlistWidget.playlist_changed_signal.connect(self.hide_song_info_page) def setup_ui(self): self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowIcon(QIcon(IconsHub.Xyplayer)) self.setFocusPolicy(Qt.NoFocus) self.setObjectName(app_name) self.setWindowFlags(Qt.MSWindowsFixedSizeDialogHint | Qt.FramelessWindowHint) self.setMaximumSize(QSize(Configures.WindowWidth, Configures.WindowHeight)) self.setGeometry((Configures.DesktopSize.width() - Configures.WindowWidth)//2, (Configures.DesktopSize.height() - Configures.WindowHeight)//2, Configures.WindowWidth, Configures.WindowHeight) self.setAttribute(Qt.WA_QuitOnClose,True) self.dragPosition = QPoint(0, 0) self.mousePressedFlag = False #title_widgets self.titleIconLabel = QLabel() pixmap = QPixmap(IconsHub.Xyplayer) self.titleIconLabel.setFixedSize(18, 18) self.titleIconLabel.setScaledContents(True) self.titleIconLabel.setPixmap(pixmap) self.titleLabel = QLabel("XYPLAYER") self.titleLabel.setObjectName('titleLabel') self.titleLabel.setFixedHeight(30) self.titleLabel.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.titleLabel.setObjectName('titleLabel') self.minButton = PushButton() self.minButton.setToolTip(self.tr('最小化')) self.minButton.setFocusPolicy(Qt.NoFocus) self.closeButton = PushButton() self.closeButton.setToolTip(self.tr('关闭主面板')) self.closeButton.setFocusPolicy(Qt.NoFocus) self.aboutButton = PushButton() self.aboutButton.setToolTip(self.tr('关于')) self.aboutButton.setFocusPolicy(Qt.NoFocus) self.preferenceButton = PushButton() self.preferenceButton.setToolTip(self.tr('选项')) self.preferenceButton.setFocusPolicy(Qt.NoFocus) self.minButton.loadPixmap(IconsHub.MinButton) self.closeButton.loadPixmap(IconsHub.CloseButton) self.aboutButton.loadPixmap(IconsHub.AboutButton) self.preferenceButton.loadPixmap(IconsHub.PreferenceButton) #管理页面 self.managePage = manage_page.ManagePage() #播放器 self.playbackPanel = playback_panel.PlaybackPanel() self.mainPageButton = QToolButton(self, clicked = self.show_lyric_text) self.mainPageButton.setToolTip('返回歌词页面') self.mainPageButton.hide() self.mainPageButton.setGeometry(625, 5, 30, 30) self.mainPageButton.setObjectName('homeButton') self.mainPageButton.setFocusPolicy(Qt.NoFocus) self.mainPageButton.setIcon(QIcon(IconsHub.Home)) self.mainPageButton.setIconSize(QSize(20, 20)) self.pageLabel = QLabel(self) self.pageLabel.setObjectName('pageLabel') self.pageLabel.hide() self.pageLabel.setGeometry(660, 5, 100, 30) self.searchBox = search_page.SearchBox(self) self.searchBox.setGeometry(180, 6, 280, 28) #综合布局 titleLayout = QHBoxLayout() titleLayout.setContentsMargins(3, 0, 0, 0) titleLayout.setSpacing(0) titleLayout.addWidget(self.titleIconLabel) titleLayout.addSpacing(2) titleLayout.addWidget(self.titleLabel) titleLayout.addStretch() titleLayout.addWidget(self.aboutButton) titleLayout.addWidget(self.preferenceButton) titleLayout.addWidget(self.minButton) titleLayout.addWidget(self.closeButton) #主堆栈框 self.mainStack = QStackedWidget() mainLayout = QVBoxLayout(self) mainLayout.setSpacing(2) mainLayout.setContentsMargins(3, 0, 3, 0) mainLayout.addLayout(titleLayout) mainLayout.addWidget(self.managePage) mainLayout.addWidget(self.playbackPanel) #创建托盘图标 self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(QIcon(IconsHub.Xyplayer)) self.showDesktopLyricAction = QAction( QIcon(IconsHub.DesktopLyric), "开启桌面歌词", self, triggered = self.playbackPanel.show_desktop_lyric ) trayMenu = QMenu() trayMenu.addAction(self.showMainWindowAction) trayMenu.addAction(self.showDesktopLyricAction) trayMenu.addSeparator() trayMenu.addAction(self.playbackPanel.get_previous_button_action()) trayMenu.addAction(self.playbackPanel.get_play_button_action()) trayMenu.addAction(self.playbackPanel.get_next_button_action()) trayMenu.addAction(self.playbackPanel.get_stop_button_action()) trayMenu.addSeparator() trayMenu.addAction(self.playmodeRandomAction) trayMenu.addAction(self.playmodeOrderAction) trayMenu.addAction(self.playmodeSingleAction) trayMenu.addSeparator() trayMenu.addAction(self.exitNowAction) self.trayIcon.setContextMenu(trayMenu) self.trayIcon.show() def create_actions(self): self.showMainWindowAction = QAction( QIcon(IconsHub.Home), "隐藏主界面", self, triggered = self.show_mainwindow ) self.playmodeOrderAction = QAction( QIcon(IconsHub.PlaymodeSelectedNo), Configures.PlaymodeOrderText, self, triggered = self.playmode_order_seted) self.playmodeRandomAction = QAction( QIcon(IconsHub.PlaymodeSelected), Configures.PlaymodeRandomText, self, triggered = self.playmode_random_seted) self.playmodeSingleAction = QAction( QIcon(IconsHub.PlaymodeSelectedNo), Configures.PlaymodeSingleText, self, triggered = self.playmode_single_seted) self.exitNowAction = QAction( QIcon(IconsHub.Close), "立即退出", self, triggered = self.close) self.playmodeActions = [self.playmodeRandomAction, self.playmodeOrderAction, self.playmodeSingleAction] def mousePressEvent(self, event): self.mousePressedFlag = True self.dragPosition = event.globalPos() - self.frameGeometry().topLeft() event.accept() def mouseReleaseEvent(self, event): self.mousePressedFlag = False def mouseMoveEvent(self, event): if self.mousePressedFlag: self.move(event.globalPos() - self.dragPosition) event.accept() def force_close(self): """用于程序成功更新之后跳过确认窗口强制关闭。""" self.forceCloseFlag = True self.close() def check_if_can_close(self): closeChecked = False if self.managePage.exitmodePanel.mountoutDialog.countoutMode and not self.managePage.exitmodePanel.mountoutDialog.remainMount or self.managePage.exitmodePanel.timeoutDialog.timeoutFlag or self.forceCloseFlag: closeChecked = True else: if threading.active_count() == 1: closeChecked = True else: self.show() ok = QMessageBox.question(self, '退出', '当前有下载任务正在进行,您是否要挂起全部下载任务并退出?',QMessageBox.Cancel|QMessageBox.Ok, QMessageBox.Cancel ) if ok == QMessageBox.Ok: closeChecked = True else: closeChecked = False return closeChecked def closeEvent(self, event): if self.check_if_can_close(): self.handle_before_close() event.accept() else: event.ignore() def handle_before_close(self): self.playbackPanel.stop_music() self.hide() self.trayIcon.hide() self.managePage.handle_before_app_exit() for t in threading.enumerate(): if t.name == 'downloadLrc': t.stop() elif t!= threading.main_thread(): t.pause() t.join() def playmode_changed(self, oldPlaymode, newPlaymode): self.playmodeActions[oldPlaymode].setIcon(QIcon(IconsHub.PlaymodeSelectedNo)) self.playmodeActions[newPlaymode].setIcon(QIcon(IconsHub.PlaymodeSelected)) def playmode_order_seted(self): if self.playbackPanel.playmode != Configures.PlaymodeOrder: self.playmode_changed(self.playbackPanel.playmode, Configures.PlaymodeOrder) self.playbackPanel.set_new_playmode(Configures.PlaymodeOrder) def playmode_random_seted(self): if self.playbackPanel.playmode != Configures.PlaymodeRandom: self.playmode_changed(self.playbackPanel.playmode, Configures.PlaymodeRandom) self.playbackPanel.set_new_playmode(Configures.PlaymodeRandom) def playmode_single_seted(self): if self.playbackPanel.playmode != Configures.PlaymodeSingle: self.playmode_changed(self.playbackPanel.playmode, Configures.PlaymodeSingle) self.playbackPanel.set_new_playmode(Configures.PlaymodeSingle) def show_minimized(self): self.showMinimized() def show_mainwindow(self): if self.isHidden(): self.showMainWindowAction.setText('隐藏主界面') self.show() else: self.showMainWindowAction.setText('显示主界面') self.hide() def dont_hide_mainwindow(self): if self.isHidden(): self.showMainWindowAction.setText('隐藏主界面') self.show() def ui_initial(self): self.playbackPanel.ui_initial() self.managePage.ui_initial() def show_lyric_text(self): self.pageLabel.hide() self.mainPageButton.hide() self.managePage.show_main_stack_window() def show_about_page(self): self.show_page_label_and_button(self.tr('关于')) self.managePage.show_about_page() def show_settings_frame(self): self.show_page_label_and_button(self.tr('选项')) self.managePage.show_settings_frame() def search_musics_online(self, keyword): if self.managePage.searchFrame.search_musics(keyword): self.show_page_label_and_button(self.tr('搜索')) self.managePage.show_search_frame() def show_artist_info(self, name): self.show_page_label_and_button(self.tr('歌手')) self.managePage.show_artist_info(name) def show_song_info(self, row): self.show_page_label_and_button(self.tr('歌曲')) self.managePage.show_song_info(row) def hide_song_info_page(self, arg1=None, arg2=None): if self.managePage.get_page_description() == '歌曲': self.show_lyric_text() def show_page_label_and_button(self, pageName): self.pageLabel.setText(self.tr(pageName)) self.mainPageButton.show() self.pageLabel.show() def close_button_acted(self): if self.closeButtonAct == Configures.SettingsHide: self.show_mainwindow() else: self.close()