def f_main(): """主程序,保证程序单实例运行""" app = QtWidgets.QApplication(sys.argv) app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) servername = "TransTools" socket = QLocalSocket() socket.connectToServer(servername) if socket.waitForConnected(500): # 程序只允许单例运行 showmsg("程序已运行!") return(app.quit()) # 没有实例运行,创建服务器 localServer = QLocalServer() localServer.listen(servername) try: main = Main() main.show() sys.exit(app.exec_()) except Exception as e: showmsg(str(e), type=QMessageBox.Critical) finally: localServer.close()
class SingleApplication(QObject): newInstance = pyqtSignal() urlPost = pyqtSignal(str) def __init__(self): super().__init__() self.mServer = QLocalServer() self.mServer.newConnection.connect(self.newConnection) def listen(self, client): self.mServer.removeServer(client) self.mServer.listen(client) print(self.mServer.errorString()) def hasPrevious(self, name, args): socket = QLocalSocket() socket.connectToServer(name, QLocalSocket.ReadWrite) if socket.waitForConnected(): if len(args) > 1: socket.write(args[1]) socket.flush() return True return False def newConnection(self): self.newInstance.emit() self.mSocket = self.mServer.nextPendingConnection() self.mSocket.readyRead.connect(self.readyRead) def readyRead(self): self.urlPost.emit(str(self.mSocket.readAll())) self.mSocket.close()
def main(): import sys app = QApplication(sys.argv) translator = QTranslator() locale = QLocale.system().name() translateFile = os.path.join(BASEDIR, 'i18n\\translations', '{}.qm'.format(locale)) if translator.load(translateFile): app.installTranslator(translator) # QApplication.setStyle(QStyleFactory.create('Fusion')) if boolean(conf.value('General/LargeFont')): font = QFont('Courier New', 14) app.setFont(font) serverName = 'Tafor' socket = QLocalSocket() socket.connectToServer(serverName) # 如果连接成功,表明server已经存在,当前已有实例在运行 if socket.waitForConnected(500): return(app.quit()) # 没有实例运行,创建服务器 localServer = QLocalServer() localServer.listen(serverName) try: window = MainWindow() window.show() sys.exit(app.exec_()) except Exception as e: logger.error(e, exc_info=True) finally: localServer.close()
def init_nxdrive_listener(self) -> None: """ Set up a QLocalServer to listen to nxdrive protocol calls. On Windows, when an nxdrive:// URL is opened, it creates a new instance of Nuxeo Drive. As we want the already running instance to receive this call (particularly during the login process), we set up a QLocalServer in that instance to listen to the new ones who will send their data. The Qt implementation of QLocalSocket on Windows makes use of named pipes. We just need to connect a handler to the newConnection signal to process the URLs. """ named_pipe = f"{BUNDLE_IDENTIFIER}.protocol.{os.getpid()}" server = QLocalServer() server.setSocketOptions(QLocalServer.WorldAccessOption) server.newConnection.connect(self._handle_connection) try: server.listen(named_pipe) log.info(f"Listening for nxdrive:// calls on {server.fullServerName()}") except: log.info( f"Unable to start local server on {named_pipe}: {server.errorString()}" ) self._nxdrive_listener = server self.aboutToQuit.connect(self._nxdrive_listener.close)
def main(): # ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("AirMemo_appid") app = QApplication(sys.argv) serverName = 'AirMemo_client' socket = QLocalSocket() socket.connectToServer(serverName) # 如果连接成功,表明server已经存在,当前已有实例在运行 if socket.waitForConnected(500): sys.exit(app.quit()) # 没有实例运行,创建服务器 localServer = QLocalServer() localServer.listen(serverName) try: setApp(app) link_db(config.LDB_FILENAME) tray = AirTray() mainWindow = Ui_MainWindow(tray) setting_win = Ui_Settings(tray) dict = {'main_win': mainWindow, 'setting_win': setting_win} tray.set_menu(dict) tray.show() # mainWindow.show() setting_win.show() sys.exit(app.exec_()) finally: localServer.close()
def init(): """Start listening to incoming connections.""" global _server if _server is not None: return server = QLocalServer(None) # find a free socket name to use for name in ids(): if server.listen(name): break else: # all names failed, try to contact and remove stale file if that fails socket = QLocalSocket() for name in ids(): socket.connectToServer(name) if not socket.waitForConnected(10000): QLocalServer.removeServer(name) if server.listen(name): break else: socket.disconnectFromServer() else: # no ids left, don't listen return app.aboutToQuit.connect(server.close) server.newConnection.connect(slot_new_connection) os.environ["FRESCOBALDI_SOCKET"] = name _server = server
def main(): # ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("AirMemo_appid") app = QApplication(sys.argv) serverName = 'rest_clock' socket = QLocalSocket() socket.connectToServer(serverName) # 如果连接成功,表明server已经存在,当前已有实例在运行 if socket.waitForConnected(500): sys.exit(app.quit()) # 没有实例运行,创建服务器 localServer = QLocalServer() localServer.listen(serverName) try: set_app(app) tray = clock_tray() timeout_win = Ui_Timeout(tray) settings_win = Ui_Settings(tray) tran_win = TranSignalWidget() tran_win.showSignal.connect(timeout_win.show) win_dict = {"timeout_win": timeout_win, 'settings_win': settings_win} tray.set_menu(win_dict) set_aps(tran_win) timeout_win.show() TIME_MISSION.set_aps_mission() MYAPS.start() logging.debug('APS jobs {}'.format(MYAPS.get_jobs())) sys.exit(app.exec_()) finally: localServer.close()
class SocketServer(SocketInterface): def __init__(self, serverName, parent=None): SocketInterface.__init__(self, serverName, parent=parent) self.server = QLocalServer(parent) self.server.listen(serverName) self.server.newConnection.connect(self.newConnection) def nextMemoryKey(self): return SocketInterface.nextMemoryKey(self) + "S" def newConnection(self): conn = self.conn if not conn: conn = self.server.nextPendingConnection() conn.disconnected.connect(conn.deleteLater) conn.waitForReadyRead() data = conn.readAll().data() if not self.conn and data == "Hello {0}!".format( self.serverName).encode("utf-8"): self.conn = conn self.conn.readyRead.connect(self.receiveMessage) self.conn.disconnected.connect(self.connDisconnected) self.log("Connection established.") else: conn.disconnectFromServer() self.log("Connection refused.") def connDisconnected(self): self.conn = None
class SingleApplication(QApplication): def __init__(self, args): super().__init__(args) self.TIME_OUT = 1000 self.isRuning = False self.serverName = 'skyutils' self.args = args self.mousePos = QPointF(0, 0) self.initLocalConnection() def initLocalConnection(self): socket = QLocalSocket() socket.connectToServer(self.serverName) if socket.waitForConnected(self.TIME_OUT): self.isRuning = True if len(self.args) >= 3: data = self.args[1] + ',' + self.args[2] socket.writeData(data.encode()) socket.flush() socket.waitForBytesWritten() return self.newLocalServer() def newLocalServer(self): self.localServer = QLocalServer(self) self.localServer.newConnection.connect(self.newLocalConnection) if not self.localServer.listen(self.serverName): if self.localServer.serverError( ) == QAbstractSocket.AddressInUseError: QLocalServer.removeServer(self.serverName) self.localServer.listen(self.serverName) def newLocalConnection(self): socket = self.localServer.nextPendingConnection() socket.readyRead.connect(self.readyRead) def readyRead(self): socket = self.sender() if socket: data = socket.readData(1000) socket.close() if data: data = data.decode().split(',') x, y = 0, 0 if self.mousePos: x = self.mousePos.x() y = self.mousePos.y() self.mainWindow.move(int(data[0]) - x, int(data[1]) - y) self.mainWindow.setWindowFlags(self.mainWindow.windowFlags() | Qt.WindowStaysOnTopHint) self.mainWindow.show() def notify(self, receiver, event): if event.type() == QEvent.MouseButtonPress: self.mousePos = event.localPos() return super().notify(receiver, event)
class SingleApplication(QApplication): messageAvailable = pyqtSignal(type(u'')) def __init__(self, argv, key): QApplication.__init__(self, argv) self._key = key self._timeout = 1000 socket = QLocalSocket(self) socket.connectToServer(self._key) if socket.waitForConnected(self._timeout): self._isRunning = True socket.abort() return socket.abort() self._isRunning = False self._server = QLocalServer(self) self._server.newConnection.connect(self.__onNewConnection) self._server.listen(self._key) self.aboutToQuit.connect(self.__onAboutToQuit) def __onAboutToQuit(self): if self._server: self._server.close() self._server = None def __onNewConnection(self): socket = self._server.nextPendingConnection() if socket.waitForReadyRead(self._timeout): self.messageAvailable.emit(socket.readAll().data().decode('utf-8')) socket.disconnectFromServer() else: pass def isRunning(self): return self._isRunning def sendMessage(self, message): assert (self._isRunning) if self.isRunning(): socket = QLocalSocket(self) socket.connectToServer(self._key, QIODevice.WriteOnly) if not socket.waitForConnected(self._timeout): return False socket.write(message.encode('utf-8')) if not socket.waitForBytesWritten(self._timeout): return False socket.disconnectFromServer() return True return False
class IpcServer(QObject): @classmethod def get_sock_name(cls): return "webmacs.ipc" @classmethod def check_server_connection(cls): sock = QLocalSocket() sock.connectToServer(cls.get_sock_name()) if sock.waitForConnected(1000): return IPcReader(sock) return None def __init__(self): QObject.__init__(self) self._server = QLocalServer() self._server.newConnection.connect(self._on_new_connection) self._server.listen(self.get_sock_name()) self._readers = {} def cleanup(self): try: os.unlink(self._server.fullServerName()) except OSError: pass @Slot() def _on_new_connection(self): conn = self._server.nextPendingConnection() reader = IPcReader(conn) reader.message_received.connect(self.handle_data) conn.readyRead.connect(reader.on_ready_read) conn.disconnected.connect(self.reader_disconnected) self._readers[conn] = reader @Slot(object) def handle_data(self, data): reader = self.sender() try: res = ipc_dispatch(data) except Exception as exc: res = str(exc) if res in (True, None): reader.send_data({"result": True}) else: reader.send_data({"result": False, "message": res}) def reader_disconnected(self): conn = self.sender() reader = self._readers.pop(conn) reader.clear() reader.deleteLater()
def main(): global win signal.signal(signal.SIGINT, exit) args = parse_arguments() appKey = "scudcloud.pid" socket = QLocalSocket() socket.connectToServer(appKey) if socket.isOpen(): socket.close() socket.deleteLater() return 0 socket.deleteLater() app = QtWidgets.QApplication(sys.argv) app.setApplicationName(Resources.APP_NAME + ' Slack') app.setWindowIcon(QtGui.QIcon(Resources.get_path('scudcloud.png'))) try: settings_path, cache_path = load_settings(args.confdir, args.cachedir) except: print("Data directories " + args.confdir + " and " + args.cachedir + " could not be created! Exiting...") raise SystemExit() minimized = True if args.minimized is True else None urgent_hint = True if args.urgent_hint is True else None # Let's move the CSS to cachedir to enable additional actions copyfile(Resources.get_path('resources.css'), os.path.join(cache_path, 'resources.css')) # If there is an qt4 config and not a qt5, let's copy the old one qt4_config = os.path.join(settings_path, 'scudcloud.cfg') qt5_config = os.path.join(settings_path, 'scudcloud_qt5.cfg') if os.path.exists(qt4_config) and not os.path.exists(qt5_config): copyfile(qt4_config, qt5_config) win = sca.ScudCloud(debug=args.debug, minimized=minimized, urgent_hint=urgent_hint, settings_path=settings_path, cache_path=cache_path) app.commitDataRequest.connect(win.setForceClose, type=QtCore.Qt.DirectConnection) server = QLocalServer() server.newConnection.connect(restore) server.listen(appKey) win.restore() if win.minimized is None: win.show() sys.exit(app.exec_())
def main(now_version, new_version): try: serverName = 'dig_word_update_Server' socket = QLocalSocket() socket.connectToServer(serverName) # 如果连接成功,表明server已经存在,当前已有实例在运行 if socket.waitForConnected(500): pass else: localServer = QLocalServer() # 没有实例运行,创建服务器 localServer.listen(serverName) # 处理其他 downwin_class(now_version, new_version) except: pass
class Server(QDialog): def __init__(self, parent=None): super(Server, self).__init__(parent) statusLabel = QLabel() statusLabel.setWordWrap(True) quitButton = QPushButton("Quit") quitButton.setAutoDefault(False) self.fortunes = ( "You've been leading a dog's life. Stay off the furniture.", "You've got to think about tomorrow.", "You will be surprised by a loud noise.", "You will feel hungry again in another hour.", "You might have mail.", "You cannot kill time without injuring eternity.", "Computers are not intelligent. They only think they are.", ) self.server = QLocalServer() if not self.server.listen('fortune'): QMessageBox.critical( self, "Fortune Server", "Unable to start the server: %s." % self.server.errorString()) self.close() return statusLabel.setText("The server is running.\nRun the Fortune Client " "example now.") quitButton.clicked.connect(self.close) self.server.newConnection.connect(self.sendFortune) buttonLayout = QHBoxLayout() buttonLayout.addStretch(1) buttonLayout.addWidget(quitButton) buttonLayout.addStretch(1) mainLayout = QVBoxLayout() mainLayout.addWidget(statusLabel) mainLayout.addLayout(buttonLayout) self.setLayout(mainLayout) self.setWindowTitle("Fortune Server") def sendFortune(self): block = QByteArray() out = QDataStream(block, QIODevice.WriteOnly) out.setVersion(QDataStream.Qt_4_0) out.writeUInt16(0) out.writeQString(random.choice(self.fortunes)) out.device().seek(0) out.writeUInt16(block.size() - 2) clientConnection = self.server.nextPendingConnection() clientConnection.disconnected.connect(clientConnection.deleteLater) clientConnection.write(block) clientConnection.flush() clientConnection.disconnectFromServer()
class Server(QDialog): def __init__(self, parent=None): super(Server, self).__init__(parent) statusLabel = QLabel() statusLabel.setWordWrap(True) quitButton = QPushButton("Quit") quitButton.setAutoDefault(False) self.fortunes = ( "You've been leading a dog's life. Stay off the furniture.", "You've got to think about tomorrow.", "You will be surprised by a loud noise.", "You will feel hungry again in another hour.", "You might have mail.", "You cannot kill time without injuring eternity.", "Computers are not intelligent. They only think they are.", ) self.server = QLocalServer() if not self.server.listen('fortune'): QMessageBox.critical(self, "Fortune Server", "Unable to start the server: %s." % self.server.errorString()) self.close() return statusLabel.setText("The server is running.\nRun the Fortune Client " "example now.") quitButton.clicked.connect(self.close) self.server.newConnection.connect(self.sendFortune) buttonLayout = QHBoxLayout() buttonLayout.addStretch(1) buttonLayout.addWidget(quitButton) buttonLayout.addStretch(1) mainLayout = QVBoxLayout() mainLayout.addWidget(statusLabel) mainLayout.addLayout(buttonLayout) self.setLayout(mainLayout) self.setWindowTitle("Fortune Server") def sendFortune(self): block = QByteArray() out = QDataStream(block, QIODevice.WriteOnly) out.setVersion(QDataStream.Qt_4_0) out.writeUInt16(0) out.writeQString(random.choice(self.fortunes)) out.device().seek(0) out.writeUInt16(block.size() - 2) clientConnection = self.server.nextPendingConnection() clientConnection.disconnected.connect(clientConnection.deleteLater) clientConnection.write(block) clientConnection.flush() clientConnection.disconnectFromServer()
class InstanceHandler(QObject): """ Makes sure that only one instance of this application can be run. """ received = pyqtSignal(str) def __init__(self, parent, name): super(__class__, self).__init__(parent) self._parent = parent self._name = name self._timeout = 1000 self._socket = QLocalSocket(self) self._socket.connectToServer(self._name) self._is_already_running = self._socket.waitForConnected(self._timeout) if not self.isAlreadyRunning(): self._server = QLocalServer(self) self._server.newConnection.connect(self._receive_data) self._server.removeServer(self._name) self._server.listen(self._name) def _send_data(self, data=None): """ Sends model to an server-application. """ if not data: data = "" self._socket.write(data.encode()) self._socket.waitForBytesWritten(self._timeout) self._socket.disconnectFromServer() def _receive_data(self): """ Receives model from an client-application. """ socket = self._server.nextPendingConnection() if socket.waitForReadyRead(self._timeout): # Emit transmitted model. self.received.emit(socket.readAll().data().decode( "utf-8", "surrogateescape")) else: # When no model was transmitted just emit empty string. self.received.emit("") def newTab(self, input): """ Opens a new tab in an already running instance. """ self._send_data(input) def isAlreadyRunning(self) -> bool: """ Returns True when an instance of the application is already running, otherwise False. """ return self._is_already_running
class NotificaitonServer(object): """docstring for NotificaitonServer""" notifications = [] def __init__(self, name, app): super(NotificaitonServer, self).__init__() self.name = name self.app = app def startServer(self): self.server = QLocalServer() self.server.listen(self.name) self.server.newConnection.connect(self.connected) pass def connected(self): # print("conncted!") if self.server.hasPendingConnections(): self.conn = self.server.nextPendingConnection() # print(type(self.conn)) self.conn.readyRead.connect(self.received) pass pass def received(self): # print("received!") content = self.conn.readAll() # print() self.notifications = [w for w in self.notifications if w.isVisible()] # print(len(self.notifications)) for w in self.notifications: w.moveDown() pass payload = json.loads(bytes(content).decode("utf-8")) no = notification.NotifyOnce(payload) self.notifications.append(no) # ret = app.exec_() # print("app able to exit") # sys.exit(ret) pass
def test_socket_options_address_in_use_problem(qlocalserver, short_tmpdir): """Qt seems to ignore AddressInUseError when using socketOptions. With this test we verify this bug still exists. If it fails, we can probably start using setSocketOptions again. """ servername = str(short_tmpdir / 'x') s1 = QLocalServer() ok = s1.listen(servername) assert ok s2 = QLocalServer() s2.setSocketOptions(QLocalServer.UserAccessOption) ok = s2.listen(servername) print(s2.errorString()) # We actually would expect ok == False here - but we want the test to fail # when the Qt bug is fixed. assert ok
class QSingleApplication(QApplication): sock_file = 'sumokoin_wallet_sock' if sys.platform == 'win32': sock_file = "\\\\.\\pipe\\%s" % sock_file elif sys.platform == 'darwin': sock_file = os.path.join(DATA_DIR, '.%s' % sock_file) else: sock_file = os.path.join(getSockDir(), sock_file) def singleStart(self, appMain): self.appMain = appMain # Socket self.m_socket = QLocalSocket() self.m_socket.connected.connect(self.connectToExistingApp) self.m_socket.error.connect( lambda: self.startApplication(first_start=True)) self.m_socket.connectToServer(self.sock_file, QIODevice.WriteOnly) def connectToExistingApp(self): # Quit application in 250 ms QTimer.singleShot(250, self.quit) print("App is already running.", file=sys.stderr) def startApplication(self, first_start=True): self.m_server = QLocalServer() if self.m_server.listen(self.sock_file): print("Starting app...") self.appMain.run() else: if not first_start: print("Error listening the socket. App can't start!", file=sys.stderr) QTimer.singleShot(250, self.quit) return # remove the listener path file and try to restart app one more time print("Error listening the socket. Try to restart application...", file=sys.stderr) if sys.platform != 'win32': try: os.unlink(self.sock_file) except Exception, err: print(err, file=sys.stderr) QTimer.singleShot(250, lambda: self.startApplication(first_start=False))
class Server(QObject): dataReceived = pyqtSignal(list) quit = pyqtSignal() def __init__(self): super().__init__() self.conn = None self.server = None def create(self, name=piony.G_SOCKET_NAME): QLocalServer.removeServer(name) self.server = QLocalServer() if not self.server.listen(name): print("Error: server -- unable to start: {}." .format(self.server.errorString())) self.quit.emit() self.server.newConnection.connect(self.notify) def close(self): self.server.close() def notify(self): logger.info("1 new conn") # WARNING: when multiple connections, each will overwrite previous! self.conn = self.server.nextPendingConnection() self.conn.readyRead.connect(self.receiveData) self.conn.disconnected.connect(self.conn.deleteLater) def receiveData(self): logger.info("waits for data") ins = QDataStream(self.conn) ins.setVersion(QDataStream.Qt_5_0) if ins.atEnd(): return argv = ins.readQVariant() logger.info("reads '%s'", str(argv)) # Must be setted up on 'show' action. Move from beginning to appropriate. action.search_dst_window() self.dataReceived.emit(argv)
class Server(QObject): dataReceived = pyqtSignal(list) quit = pyqtSignal() def __init__(self): super().__init__() self.conn = None self.server = None def create(self, name=piony.G_SOCKET_NAME): QLocalServer.removeServer(name) self.server = QLocalServer() if not self.server.listen(name): print("Error: server -- unable to start: {}.".format( self.server.errorString())) self.quit.emit() self.server.newConnection.connect(self.notify) def close(self): self.server.close() def notify(self): logger.info("1 new conn") # WARNING: when multiple connections, each will overwrite previous! self.conn = self.server.nextPendingConnection() self.conn.readyRead.connect(self.receiveData) self.conn.disconnected.connect(self.conn.deleteLater) def receiveData(self): logger.info("waits for data") ins = QDataStream(self.conn) ins.setVersion(QDataStream.Qt_5_0) if ins.atEnd(): return argv = ins.readQVariant() logger.info("reads '%s'", str(argv)) # Must be setted up on 'show' action. Move from beginning to appropriate. action.search_dst_window() self.dataReceived.emit(argv)
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 QSingleApplication(QApplication): messageReceived = pyqtSignal(str) def __init__(self, id, *argv): super(QSingleApplication, self).__init__(*argv) self._id = id self._activationWindow = None self._activateOnMessage = False self._server = None # Is there another instance running? self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._id) self._outSocket.error.connect(self.handleError) self._isRunning = self._outSocket.waitForConnected() if self._isRunning: # Yes, there is. self._outStream = QTextStream(self._outSocket) self._outStream.setCodec('UTF-8') else: # No, there isn't. self._outSocket = None self._outStream = None self._inSocket = None self._inStream = None self._server = QLocalServer() self._server.listen(self._id) self._server.newConnection.connect(self._onNewConnection) self.aboutToQuit.connect(self.removeServer) def handleError(self, msg): print(msg) def server(self): return self._server def isRunning(self): return self._isRunning def id(self): return self._id def activationWindow(self): return self._activationWindow def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage def activateWindow(self): if not self._activationWindow: print("No registered ActivationWindow") return # Unfortunately this *doesn't* do much of any use, as it won't # bring the window to the foreground under KDE... sigh. self._activationWindow.setWindowState( self._activationWindow.windowState() & ~Qt.WindowMinimized) self._activationWindow.raise_() self._activationWindow.requestActivate() def sendMessage(self, msg, msecs=5000): if not self._outStream: return False self._outStream << msg << '' if not self._outSocket.waitForBytesWritten(msecs): raise RuntimeError("Bytes not written within %ss" % (msecs / 1000.)) def _onNewConnection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._onReadyRead) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QTextStream(self._inSocket) self._inStream.setCodec('UTF-8') self._inSocket.readyRead.connect(self._onReadyRead) if self._activateOnMessage: self.activateWindow() def _onReadyRead(self): while True: msg = self._inStream.readLine() if not msg: break print("Message received") self.messageReceived.emit(msg) def removeServer(self): self._server.close() self._server.removeServer(self._id)
class SingleApplication(QApplication): messageReceived = pyqtSignal(str) def __init__(self, appid, *argv): super(SingleApplication, self).__init__(*argv) self._appid = appid self._activationWindow = None self._activateOnMessage = False self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._appid) self._isRunning = self._outSocket.waitForConnected() self._outStream = None self._inSocket = None self._inStream = None self._server = None self.settings = QSettings(SingleApplication.getSettingsPath(), QSettings.IniFormat) self.singleInstance = self.settings.value('singleInstance', 'on', type=str) in {'on', 'true'} if self._isRunning and self.singleInstance: self._outStream = QTextStream(self._outSocket) for a in argv[0][1:]: a = os.path.join(os.getcwd(), a) if os.path.isfile(a): self.sendMessage(a) break sys.exit(0) else: error = self._outSocket.error() if error == QLocalSocket.ConnectionRefusedError: self.close() QLocalServer.removeServer(self._appid) self._outSocket = None self._server = QLocalServer() self._server.listen(self._appid) self._server.newConnection.connect(self._onNewConnection) def close(self): if self._inSocket: self._inSocket.disconnectFromServer() if self._outSocket: self._outSocket.disconnectFromServer() if self._server: self._server.close() @staticmethod def getSettingsPath() -> str: if sys.platform == 'win32': settings_path = os.path.join(QDir.homePath(), 'AppData', 'Local', 'vidcutter') elif sys.platform == 'darwin': settings_path = os.path.join(QDir.homePath(), 'Library', 'Preferences', 'vidcutter') else: if QFileInfo(__file__).absolutePath().startswith('/app/'): settings_path = QProcessEnvironment.systemEnvironment().value( 'XDG_CONFIG_HOME', '') if not len(settings_path): settings_path = os.path.join(QDir.homePath(), '.var', 'app', vidcutter.__desktopid__, 'config') else: settings_path = os.path.join(QDir.homePath(), '.config', 'vidcutter') return os.path.join(settings_path, 'vidcutter.ini') def isRunning(self): return self._isRunning def appid(self): return self._appid def activationWindow(self): return self._activationWindow def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage def activateWindow(self): if not self._activationWindow: return self._activationWindow.setWindowState( self._activationWindow.windowState() & ~Qt.WindowMinimized) self._activationWindow.raise_() self._activationWindow.activateWindow() def sendMessage(self, msg): if not self._outStream: return False # noinspection PyUnresolvedReferences self._outStream << msg << '\n' self._outStream.flush() return self._outSocket.waitForBytesWritten() def _onNewConnection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._onReadyRead) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QTextStream(self._inSocket) self._inSocket.readyRead.connect(self._onReadyRead) if self._activateOnMessage: self.activateWindow() def _onReadyRead(self): while True: msg = self._inStream.readLine() if not msg: break self.messageReceived.emit(msg)
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 QtSingleApplication(QApplication): messageReceived = pyqtSignal(str) def __init__(self, _id, _viewer_id, *argv): super(QtSingleApplication, self).__init__(*argv) self._id = _id self._viewer_id = _viewer_id self._activationWindow = None self._activateOnMessage = False # Is there another instance running? self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._id) self._isRunning = self._outSocket.waitForConnected(-1) if self._isRunning: # Yes, there is. self._outStream = QTextStream(self._outSocket) self._outStream.setCodec('UTF-8') # Is there another viewer runnging? self._outSocketViewer = QLocalSocket() self._outSocketViewer.connectToServer(self._viewer_id) self._isRunningViewer = self._outSocketViewer.waitForConnected(-1) if self._isRunningViewer: self._outStreamViewer = QTextStream(self._outSocketViewer) self._outStreamViewer.setCodec('UTF-8') else: # app is running, we announce us as viewer app # First we remove existing servers of that name that might not have been properly closed as the server died QLocalServer.removeServer(self._viewer_id) self._outSocketViewer = None self._outStreamViewer = None self._inSocket = None self._inStream = None self._server = QLocalServer() self._server.listen(self._viewer_id) self._server.newConnection.connect(self._onNewConnection) else: self._isRunningViewer = False # No, there isn't. # First we remove existing servers of that name that might not have been properly closed as the server died QLocalServer.removeServer(self._id) self._outSocket = None self._outStream = None self._inSocket = None self._inStream = None self._server = QLocalServer() self._server.listen(self._id) self._server.newConnection.connect(self._onNewConnection) def isRunning(self): return self._isRunning def isRunningViewer(self): return self._isRunningViewer def id(self): return self._id def activationWindow(self): return self._activationWindow def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage def activateWindow(self): if not self._activationWindow: return self._activationWindow.show() self._activationWindow.setWindowState( self._activationWindow.windowState() & ~Qt.WindowMinimized) self._activationWindow.raise_() self._activationWindow.activateWindow() def sendMessage(self, msg): if not self._outStream: return False self._outStream << msg << '\n' self._outStream.flush() return self._outSocket.waitForBytesWritten() @pyqtSlot() def _onNewConnection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._onReadyRead) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QTextStream(self._inSocket) self._inStream.setCodec('UTF-8') self._inSocket.readyRead.connect(self._onReadyRead) if self._activateOnMessage and self._isRunning: self.activateWindow() @pyqtSlot() def _onReadyRead(self): while True: msg = self._inStream.readLine() if not msg: break self.messageReceived.emit(msg)
class IPCServer(QObject): """IPC server to which clients connect to. Attributes: ignored: Whether requests are ignored (in exception hook). _timer: A timer to handle timeouts. _server: A QLocalServer to accept new connections. _socket: The QLocalSocket we're currently connected to. """ def __init__(self, parent=None): """Start the IPC server and listen to commands.""" super().__init__(parent) self.ignored = False self._remove_server() self._timer = usertypes.Timer(self, 'ipc-timeout') self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) self._server = QLocalServer(self) ok = self._server.listen(SOCKETNAME) if not ok: raise IPCError("Error while listening to IPC server: {} " "(error {})".format(self._server.errorString(), self._server.serverError())) self._server.newConnection.connect(self.handle_connection) self._socket = None def _remove_server(self): """Remove an existing server.""" ok = QLocalServer.removeServer(SOCKETNAME) if not ok: raise IPCError( "Error while removing server {}!".format(SOCKETNAME)) @pyqtSlot(int) def on_error(self, error): """Convenience method which calls _socket_error on an error.""" self._timer.stop() log.ipc.debug("Socket error {}: {}".format(self._socket.error(), self._socket.errorString())) if error != QLocalSocket.PeerClosedError: _socket_error("handling IPC connection", self._socket) @pyqtSlot() def handle_connection(self): """Handle a new connection to the server.""" if self.ignored: return if self._socket is not None: log.ipc.debug("Got new connection but ignoring it because we're " "still handling another one.") return socket = self._server.nextPendingConnection() if socket is None: log.ipc.debug("No new connection to handle.") return log.ipc.debug("Client connected.") self._timer.start() self._socket = socket socket.readyRead.connect(self.on_ready_read) if socket.canReadLine(): log.ipc.debug("We can read a line immediately.") self.on_ready_read() socket.error.connect(self.on_error) if socket.error() not in (QLocalSocket.UnknownSocketError, QLocalSocket.PeerClosedError): log.ipc.debug("We got an error immediately.") self.on_error(socket.error()) socket.disconnected.connect(self.on_disconnected) if socket.state() == QLocalSocket.UnconnectedState: log.ipc.debug("Socket was disconnected immediately.") self.on_disconnected() @pyqtSlot() def on_disconnected(self): """Clean up socket when the client disconnected.""" log.ipc.debug("Client disconnected.") self._timer.stop() self._socket.deleteLater() self._socket = None # Maybe another connection is waiting. self.handle_connection() @pyqtSlot() def on_ready_read(self): """Read json data from the client.""" if self._socket is None: # this happened once and I don't know why log.ipc.warn("In on_ready_read with None socket!") return self._timer.start() while self._socket is not None and self._socket.canReadLine(): data = bytes(self._socket.readLine()) log.ipc.debug("Read from socket: {}".format(data)) try: decoded = data.decode('utf-8') except UnicodeDecodeError: log.ipc.error("Ignoring invalid IPC data.") log.ipc.debug("invalid data: {}".format( binascii.hexlify(data))) return log.ipc.debug("Processing: {}".format(decoded)) try: json_data = json.loads(decoded) except ValueError: log.ipc.error("Ignoring invalid IPC data.") log.ipc.debug("invalid json: {}".format(decoded.strip())) return try: args = json_data['args'] except KeyError: log.ipc.error("Ignoring invalid IPC data.") log.ipc.debug("no args: {}".format(decoded.strip())) return cwd = json_data.get('cwd', None) app = objreg.get('app') app.process_args(args, via_ipc=True, cwd=cwd) @pyqtSlot() def on_timeout(self): """Cancel the current connection if it was idle for too long.""" log.ipc.error("IPC connection timed out.") self._socket.close() def shutdown(self): """Shut down the IPC server cleanly.""" if self._socket is not None: self._socket.deleteLater() self._socket = None self._timer.stop() self._server.close() self._server.deleteLater() self._remove_server()
class Application(QApplication): """Main Nuxeo Drive application controlled by a system tray icon + menu""" tray_icon = None icon_state = None def __init__(self, manager: "Manager", *args: Any) -> None: super().__init__(list(*args)) self.manager = manager self.dialogs = dict() self.osi = self.manager.osi self.setWindowIcon(QIcon(self.get_window_icon())) self.setApplicationName(manager.app_name) self._init_translator() self.setQuitOnLastWindowClosed(False) self._delegator = None self.filters_dlg = None self._conflicts_modals = dict() self.current_notification = None self.default_tooltip = self.manager.app_name font = QFont("Helvetica, Arial, sans-serif", 12) self.setFont(font) self.ratio = sqrt(QFontMetricsF(font).height() / 12) self.init_gui() self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Direct Edit self.manager.direct_edit.directEditConflict.connect( self._direct_edit_conflict) self.manager.direct_edit.directEditError.connect( self._direct_edit_error) # Check if actions is required, separate method so it can be overridden self.init_checks() # Setup notification center for macOS if MAC: self._setup_notification_center() # Application update self.manager.updater.appUpdated.connect(self.quit) # Display release notes on new version if self.manager.old_version != self.manager.version: self.show_release_notes(self.manager.version) # Listen for nxdrive:// sent by a new instance self.init_nxdrive_listener() def init_gui(self) -> None: self.api = QMLDriveApi(self) self.conflicts_model = FileModel() self.errors_model = FileModel() self.engine_model = EngineModel() self.file_model = FileModel() self.ignoreds_model = FileModel() self.language_model = LanguageModel() self.add_engines(list(self.manager._engines.values())) self.engine_model.statusChanged.connect(self.update_status) self.language_model.addLanguages(Translator.languages()) flags = Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint if WINDOWS: self.conflicts_window = QQuickView() self.settings_window = QQuickView() self.systray_window = SystrayWindow() self._fill_qml_context(self.conflicts_window.rootContext()) self._fill_qml_context(self.settings_window.rootContext()) self._fill_qml_context(self.systray_window.rootContext()) self.systray_window.rootContext().setContextProperty( "systrayWindow", self.systray_window) self.conflicts_window.setSource( QUrl.fromLocalFile(find_resource("qml", "Conflicts.qml"))) self.settings_window.setSource( QUrl.fromLocalFile(find_resource("qml", "Settings.qml"))) self.systray_window.setSource( QUrl.fromLocalFile(find_resource("qml", "Systray.qml"))) flags |= Qt.Popup else: self.app_engine = QQmlApplicationEngine() self._fill_qml_context(self.app_engine.rootContext()) self.app_engine.load( QUrl.fromLocalFile(find_resource("qml", "Main.qml"))) root = self.app_engine.rootObjects()[0] self.conflicts_window = root.findChild(QQuickWindow, "conflictsWindow") self.settings_window = root.findChild(QQuickWindow, "settingsWindow") self.systray_window = root.findChild(SystrayWindow, "systrayWindow") if LINUX: flags |= Qt.Drawer self.systray_window.setFlags(flags) self.manager.newEngine.connect(self.add_engines) self.manager.initEngine.connect(self.add_engines) self.manager.dropEngine.connect(self.remove_engine) self._window_root(self.conflicts_window).changed.connect( self.refresh_conflicts) self._window_root(self.systray_window).appUpdate.connect( self.api.app_update) self._window_root(self.systray_window).getLastFiles.connect( self.get_last_files) self.api.setMessage.connect( self._window_root(self.settings_window).setMessage) if self.manager.get_engines(): current_uid = self.engine_model.engines_uid[0] self.get_last_files(current_uid) self.update_status(self.engine_model.engines[current_uid]) self.manager.updater.updateAvailable.connect( self._window_root(self.systray_window).updateAvailable) self.manager.updater.updateProgress.connect( self._window_root(self.systray_window).updateProgress) def add_engines(self, engines: Union["Engine", List["Engine"]]) -> None: if not engines: return engines = engines if isinstance(engines, list) else [engines] for engine in engines: self.engine_model.addEngine(engine) def remove_engine(self, uid: str) -> None: self.engine_model.removeEngine(uid) def _fill_qml_context(self, context: "QQmlContext") -> None: """ Fill the context of a QML element with the necessary resources. """ context.setContextProperty("ConflictsModel", self.conflicts_model) context.setContextProperty("ErrorsModel", self.errors_model) context.setContextProperty("EngineModel", self.engine_model) context.setContextProperty("FileModel", self.file_model) context.setContextProperty("IgnoredsModel", self.ignoreds_model) context.setContextProperty("languageModel", self.language_model) context.setContextProperty("api", self.api) context.setContextProperty("application", self) context.setContextProperty("currentLanguage", self.current_language()) context.setContextProperty("manager", self.manager) context.setContextProperty("updater", self.manager.updater) context.setContextProperty("ratio", self.ratio) context.setContextProperty("isFrozen", Options.is_frozen) context.setContextProperty("tl", Translator._singleton) context.setContextProperty( "nuxeoVersionText", f"{self.manager.app_name} {self.manager.version}") metrics = self.manager.get_metrics() context.setContextProperty( "modulesVersionText", (f'Python {metrics["python_version"]}, ' f'Qt {metrics["qt_version"]}, ' f'SIP {metrics["sip_version"]}'), ) colors = { "darkBlue": "#1F28BF", "nuxeoBlue": "#0066FF", "lightBlue": "#00ADED", "teal": "#73D2CF", "purple": "#8400FF", "red": "#C02828", "orange": "#FF9E00", "darkGray": "#495055", "mediumGray": "#7F8284", "lightGray": "#BCBFBF", "lighterGray": "#F5F5F5", } for name, value in colors.items(): context.setContextProperty(name, value) def _window_root(self, window): if WINDOWS: return window.rootObject() return window def translate(self, message: str, values: dict = None) -> str: return Translator.get(message, values) def _show_window(self, window: "QWindow") -> None: window.show() window.raise_() def _destroy_dialog(self) -> None: sender = self.sender() name = sender.objectName() self.dialogs.pop(name, None) def _create_unique_dialog(self, name: str, dialog: QDialog) -> None: dialog.setObjectName(name) dialog.destroyed.connect(self._destroy_dialog) self.dialogs[name] = dialog def _init_translator(self) -> None: locale = Options.force_locale or Options.locale Translator( self.manager, find_resource("i18n"), self.manager.get_config("locale", locale), ) self.installTranslator(Translator._singleton) @staticmethod def get_window_icon() -> str: return find_icon("app_icon.svg") @pyqtSlot(str, str, str) def _direct_edit_conflict(self, filename: str, ref: str, digest: str) -> None: log.trace("Entering _direct_edit_conflict for %r / %r", filename, ref) try: if filename in self._conflicts_modals: log.trace("Filename already in _conflicts_modals: %r", filename) return log.trace("Putting filename in _conflicts_modals: %r", filename) self._conflicts_modals[filename] = True msg = QMessageBox() msg.setInformativeText( Translator.get("DIRECT_EDIT_CONFLICT_MESSAGE", [short_name(filename)])) overwrite = msg.addButton( Translator.get("DIRECT_EDIT_CONFLICT_OVERWRITE"), QMessageBox.AcceptRole) msg.addButton(Translator.get("CANCEL"), QMessageBox.RejectRole) msg.setIcon(QMessageBox.Warning) if msg.clickedButton() == overwrite: self.manager.direct_edit.force_update(ref, digest) del self._conflicts_modals[filename] except: log.exception( "Error while displaying Direct Edit conflict modal dialog for %r", filename, ) @pyqtSlot(str, list) def _direct_edit_error(self, message: str, values: List[str]) -> None: """ Display a simple Direct Edit error message. """ msg = QMessageBox() msg.setWindowTitle("Direct Edit - {self.manager.app_name}") msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setIcon(QMessageBox.Warning) msg.setTextFormat(Qt.RichText) msg.setText(self.translate(message, values)) msg.exec_() @pyqtSlot() def _root_deleted(self) -> None: engine = self.sender() log.debug("Root has been deleted for engine: %s", engine.uid) msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setText(Translator.get("DRIVE_ROOT_DELETED", [engine.local_folder])) recreate = msg.addButton(Translator.get("DRIVE_ROOT_RECREATE"), QMessageBox.AcceptRole) disconnect = msg.addButton(Translator.get("DRIVE_ROOT_DISCONNECT"), QMessageBox.RejectRole) msg.exec_() res = msg.clickedButton() if res == disconnect: self.manager.unbind_engine(engine.uid) elif res == recreate: engine.reinit() engine.start() @pyqtSlot() def _no_space_left(self) -> None: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setText(Translator.get("NO_SPACE_LEFT_ON_DEVICE")) msg.addButton(Translator.get("OK"), QMessageBox.AcceptRole) msg.exec_() @pyqtSlot(str) def _root_moved(self, new_path: str) -> None: engine = self.sender() log.debug("Root has been moved for engine: %s to %r", engine.uid, new_path) info = [engine.local_folder, new_path] msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setText(Translator.get("DRIVE_ROOT_MOVED", info)) move = msg.addButton(Translator.get("DRIVE_ROOT_UPDATE"), QMessageBox.AcceptRole) recreate = msg.addButton(Translator.get("DRIVE_ROOT_RECREATE"), QMessageBox.AcceptRole) disconnect = msg.addButton(Translator.get("DRIVE_ROOT_DISCONNECT"), QMessageBox.RejectRole) msg.exec_() res = msg.clickedButton() if res == disconnect: self.manager.unbind_engine(engine.uid) elif res == recreate: engine.reinit() engine.start() elif res == move: engine.set_local_folder(new_path) engine.start() @pyqtSlot(object) def dropped_engine(self, engine: "Engine") -> None: # Update icon in case the engine dropped was syncing self.change_systray_icon() @pyqtSlot() def change_systray_icon(self) -> None: # Update status has the precedence over other ones if self.manager.updater.status not in ( UPDATE_STATUS_UNAVAILABLE_SITE, UPDATE_STATUS_UP_TO_DATE, ): self.set_icon_state("update") return syncing = conflict = False engines = self.manager.get_engines() invalid_credentials = paused = offline = True for engine in engines.values(): syncing |= engine.is_syncing() invalid_credentials &= engine.has_invalid_credentials() paused &= engine.is_paused() offline &= engine.is_offline() conflict |= bool(engine.get_conflicts()) if offline: new_state = "error" Action(Translator.get("OFFLINE")) elif invalid_credentials: new_state = "error" Action(Translator.get("AUTH_EXPIRED")) elif not engines: new_state = "disabled" Action.finish_action() elif paused: new_state = "paused" Action.finish_action() elif syncing: new_state = "syncing" elif conflict: new_state = "conflict" else: new_state = "idle" Action.finish_action() self.set_icon_state(new_state) def refresh_conflicts(self, uid: str) -> None: """ Update the content of the conflicts/errors window. """ self.conflicts_model.empty() self.errors_model.empty() self.ignoreds_model.empty() self.conflicts_model.addFiles(self.api.get_conflicts(uid)) self.errors_model.addFiles(self.api.get_errors(uid)) self.ignoreds_model.addFiles(self.api.get_unsynchronizeds(uid)) @pyqtSlot() def show_conflicts_resolution(self, engine: "Engine") -> None: """ Display the conflicts/errors window. """ self.refresh_conflicts(engine.uid) self._window_root(self.conflicts_window).setEngine.emit(engine.uid) self.conflicts_window.show() self.conflicts_window.requestActivate() @pyqtSlot() def show_settings(self, section: str = "General") -> None: sections = {"General": 0, "Accounts": 1, "About": 2} self._window_root(self.settings_window).setSection.emit( sections[section]) self.settings_window.show() self.settings_window.requestActivate() @pyqtSlot() def show_systray(self) -> None: icon = self.tray_icon.geometry() if not icon or icon.isEmpty(): # On Ubuntu it's likely we can't retrieve the geometry. # We're simply displaying the systray in the top right corner. screen = self.desktop().screenGeometry() pos_x = screen.right() - self.systray_window.width() - 20 pos_y = 30 else: pos_x = max(0, icon.x() + icon.width() - self.systray_window.width()) pos_y = icon.y() - self.systray_window.height() if pos_y < 0: pos_y = icon.y() + icon.height() self.systray_window.setX(pos_x) self.systray_window.setY(pos_y) self.systray_window.show() self.systray_window.raise_() @pyqtSlot() def hide_systray(self): self.systray_window.hide() @pyqtSlot() def open_help(self) -> None: self.manager.open_help() @pyqtSlot() def destroyed_filters_dialog(self) -> None: self.filters_dlg = None @pyqtSlot() def show_filters(self, engine: "Engine") -> None: if self.filters_dlg: self.filters_dlg.close() self.filters_dlg = None from ..gui.folders_dialog import FiltersDialog self.filters_dlg = FiltersDialog(self, engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self) -> None: from ..gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().items(): self.status = StatusDialog(engine.get_dao()) self.status.show() break def show_activities(self) -> None: return # TODO: Create activities window self.activities.show() @pyqtSlot(str, object) def _open_authentication_dialog(self, url: str, callback_params: Dict[str, str]) -> None: self.api._callback_params = callback_params if Options.is_frozen: """ Authenticate through the browser. This authentication requires the server's Nuxeo Drive addon to include NXP-25519. Instead of opening the server's login page in a WebKit view through the app, it opens in the browser and retrieves the login token by opening an nxdrive:// URL. """ self.manager.open_local_file(url) else: self._web_auth_not_frozen(url) def _web_auth_not_frozen(self, url: str) -> None: """ Open a dialog box to fill the credentials. Then a request will be done using the Python client to get a token. This is used when the application is not frozen as there is no custom protocol handler in this case. """ from PyQt5.QtWidgets import QLineEdit from nuxeo.client import Nuxeo dialog = QDialog() dialog.setWindowTitle( self.translate("WEB_AUTHENTICATION_WINDOW_TITLE")) dialog.setWindowIcon(QIcon(self.get_window_icon())) dialog.resize(250, 100) layout = QVBoxLayout() username = QLineEdit("Administrator", parent=dialog) password = QLineEdit("Administrator", parent=dialog) password.setEchoMode(QLineEdit.Password) layout.addWidget(username) layout.addWidget(password) def auth() -> None: """Retrieve a token and create the account.""" user = str(username.text()) pwd = str(password.text()) nuxeo = Nuxeo(host=url, auth=(user, pwd)) try: token = nuxeo.client.request_auth_token( device_id=self.manager.device_id, app_name=APP_NAME, permission=TOKEN_PERMISSION, device=get_device(), ) except Exception as exc: log.error(f"Connection error: {exc}") token = "" finally: del nuxeo self.api.handle_token(token, user) dialog.close() buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) buttons.accepted.connect(auth) buttons.rejected.connect(dialog.close) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() @pyqtSlot(object) def _connect_engine(self, engine: "Engine") -> None: engine.syncStarted.connect(self.change_systray_icon) engine.syncCompleted.connect(self.change_systray_icon) engine.invalidAuthentication.connect(self.change_systray_icon) engine.syncSuspended.connect(self.change_systray_icon) engine.syncResumed.connect(self.change_systray_icon) engine.offline.connect(self.change_systray_icon) engine.online.connect(self.change_systray_icon) engine.rootDeleted.connect(self._root_deleted) engine.rootMoved.connect(self._root_moved) engine.noSpaceLeftOnDevice.connect(self._no_space_left) self.change_systray_icon() @pyqtSlot() def _debug_toggle_invalid_credentials(self) -> None: sender = self.sender() engine = sender.data() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason="debug") @pyqtSlot() def _debug_show_file_status(self) -> None: from ..gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data() self.status_dialog = StatusDialog(engine.get_dao()) self.status_dialog.show() def _create_debug_engine_menu(self, engine: "Engine", parent: QMenu) -> QMenu: menu = QMenu(parent) action = QAction(Translator.get("DEBUG_INVALID_CREDENTIALS"), menu) action.setCheckable(True) action.setChecked(engine.has_invalid_credentials()) action.setData(engine) action.triggered.connect(self._debug_toggle_invalid_credentials) menu.addAction(action) action = QAction(Translator.get("DEBUG_FILE_STATUS"), menu) action.setData(engine) action.triggered.connect(self._debug_show_file_status) menu.addAction(action) return menu def create_debug_menu(self, menu: QMenu) -> None: menu.addAction(Translator.get("DEBUG_WINDOW"), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QAction(engine.name, menu) action.setMenu(self._create_debug_engine_menu(engine, menu)) action.setData(engine) menu.addAction(action) @pyqtSlot() def show_debug_window(self) -> None: return debug = self.dialogs.get("debug") # TODO: if not debug: Create debug window self._show_window(debug) def init_checks(self) -> None: if Options.debug: self.show_debug_window() for _, engine in self.manager.get_engines().items(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.notification_service.newNotification.connect( self._new_notification) self.manager.updater.updateAvailable.connect(self._update_notification) if not self.manager.get_engines(): self.show_settings() else: for engine in self.manager.get_engines().values(): # Prompt for settings if needed if engine.has_invalid_credentials(): self.show_settings("Accounts_" + engine.uid) break self.manager.start() @pyqtSlot() @if_frozen def _update_notification(self) -> None: self.change_systray_icon() # Display a notification status, version = self.manager.updater.status, self.manager.updater.version msg = ( "AUTOUPDATE_UPGRADE", "AUTOUPDATE_DOWNGRADE")[status == UPDATE_STATUS_DOWNGRADE_NEEDED] description = Translator.get(msg, [version]) flags = Notification.FLAG_BUBBLE | Notification.FLAG_UNIQUE if LINUX: description += " " + Translator.get("AUTOUPDATE_MANUAL") flags |= Notification.FLAG_SYSTRAY log.warning(description) notification = Notification( uuid="AutoUpdate", flags=flags, title=Translator.get("NOTIF_UPDATE_TITLE", [version]), description=description, ) self.manager.notification_service.send_notification(notification) @pyqtSlot() def message_clicked(self) -> None: if self.current_notification: self.manager.notification_service.trigger_notification( self.current_notification.uid) def _setup_notification_center(self) -> None: from ..osi.darwin.pyNotificationCenter import ( setup_delegator, NotificationDelegator, ) if not self._delegator: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager setup_delegator(self._delegator) @pyqtSlot(object) def _new_notification(self, notif: Notification) -> None: if not notif.is_bubble(): return if self._delegator is not None: # Use notification center from ..osi.darwin.pyNotificationCenter import notify return notify(notif.title, None, notif.description, user_info={"uuid": notif.uid}) icon = QSystemTrayIcon.Information if notif.level == Notification.LEVEL_WARNING: icon = QSystemTrayIcon.Warning elif notif.level == Notification.LEVEL_ERROR: icon = QSystemTrayIcon.Critical self.current_notification = notif self.tray_icon.showMessage(notif.title, notif.description, icon, 10000) def set_icon_state(self, state: str) -> bool: """ Execute systray icon change operations triggered by state change. The synchronization thread can update the state info but cannot directly call QtGui widget methods. This should be executed by the main thread event loop, hence the delegation to this method that is triggered by a signal to allow for message passing between the 2 threads. Return True of the icon has changed state. """ if self.icon_state == state: # Nothing to update return False self.tray_icon.setToolTip(self.get_tooltip()) self.tray_icon.setIcon(self.icons[state]) self.icon_state = state return True def get_tooltip(self) -> str: actions = Action.get_actions() if not actions: return self.default_tooltip # Display only the first action for now for action in actions.values(): if action and action.type and not action.type.startswith("_"): break else: return self.default_tooltip if isinstance(action, FileAction): if action.get_percent() is not None: return "%s - %s - %s - %d%%" % ( self.default_tooltip, action.type, action.filename, action.get_percent(), ) return "%s - %s - %s" % (self.default_tooltip, action.type, action.filename) elif action.get_percent() is not None: return "%s - %s - %d%%" % ( self.default_tooltip, action.type, action.get_percent(), ) return "%s - %s" % (self.default_tooltip, action.type) @if_frozen def show_release_notes(self, version: str) -> None: """ Display release notes of a given version. """ beta = self.manager.get_beta_channel() log.debug("Showing release notes, version=%r beta=%r", version, beta) # For now, we do care about beta only if not beta: return url = ("https://api.github.com/repos/nuxeo/nuxeo-drive" "/releases/tags/release-" + version) if beta: version += " beta" try: content = requests.get(url) except requests.HTTPError as exc: if exc.response.status_code == 404: log.error("[%s] Release does not exist", version) else: log.exception( "[%s] Network error while fetching release notes", version) return except: log.exception("[%s] Unknown error while fetching release notes", version) return try: data = content.json() except ValueError: log.exception("[%s] Invalid release notes", version) return finally: del content try: html = markdown(data["body"]) except KeyError: log.error("[%s] Release notes is missing its body", version) return except (UnicodeDecodeError, ValueError): log.exception("[%s] Release notes conversion error", version) return dialog = QDialog() dialog.setWindowTitle( f"{self.manager.app_name} {version} - Release notes") dialog.setWindowIcon(QIcon(self.get_window_icon())) dialog.resize(600, 400) notes = QTextEdit() notes.setStyleSheet("background-color: #eee; border: none;") notes.setReadOnly(True) notes.setHtml(html) buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Ok) buttons.clicked.connect(dialog.accept) layout = QVBoxLayout() layout.addWidget(notes) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() def show_metadata(self, file_path: str) -> None: self.manager.ctx_edit_metadata(file_path) def setup_systray(self) -> None: icons = {} for state in { "conflict", "disabled", "error", "idle", "notification", "paused", "syncing", "update", }: name = "{}{}.svg".format(state, "_light" if WINDOWS else "") icon = QIcon() icon.addFile(find_icon(name)) if MAC: icon.addFile(find_icon("active.svg"), mode=QIcon.Selected) icons[state] = icon setattr(self, "icons", icons) self.tray_icon = DriveSystrayIcon(self) if not self.tray_icon.isSystemTrayAvailable(): log.critical("There is no system tray available!") else: self.tray_icon.setToolTip(self.manager.app_name) self.set_icon_state("disabled") self.tray_icon.show() def event(self, event: QEvent) -> bool: """ Handle URL scheme events under macOS. """ url = getattr(event, "url", None) if not url: # This is not an event for us! return super().event(event) try: final_url = unquote(event.url().toString()) return self._handle_nxdrive_url(final_url) except: log.exception("Error handling URL event %r", url) return False def _handle_nxdrive_url(self, url: str) -> bool: """ Handle an nxdrive protocol URL. """ info = parse_protocol_url(url) if not info: return False cmd = info["command"] path = info.get("filepath", None) manager = self.manager log.debug("Event URL=%s, info=%r", url, info) # Event fired by a context menu item func = { "access-online": manager.ctx_access_online, "copy-share-link": manager.ctx_copy_share_link, "edit-metadata": manager.ctx_edit_metadata, }.get(cmd, None) if func: func(path) elif "edit" in cmd: manager.direct_edit.edit( info["server_url"], info["doc_id"], user=info["user"], download_url=info["download_url"], ) elif cmd == "trigger-watch": for engine in manager._engine_definitions: manager.osi.watch_folder(engine.local_folder) elif cmd == "token": self.api.handle_token(info["token"], info["username"]) else: log.warning("Unknown event URL=%r, info=%r", url, info) return False return True @if_frozen def init_nxdrive_listener(self) -> None: """ Set up a QLocalServer to listen to nxdrive protocol calls. On Windows, when an nxdrive:// URL is opened, it creates a new instance of Nuxeo Drive. As we want the already running instance to receive this call (particularly during the login process), we set up a QLocalServer in that instance to listen to the new ones who will send their data. The Qt implementation of QLocalSocket on Windows makes use of named pipes. We just need to connect a handler to the newConnection signal to process the URLs. """ self._nxdrive_listener = QLocalServer() self._nxdrive_listener.newConnection.connect(self._handle_connection) self._nxdrive_listener.listen("com.nuxeo.drive.protocol") self.aboutToQuit.connect(self._nxdrive_listener.close) @if_frozen def _handle_connection(self) -> None: """ Retrieve the connection with other instances and handle the incoming data. """ con: QLocalSocket = self._nxdrive_listener.nextPendingConnection() log.debug("Receiving socket connection for nxdrive protocol handling") if not con or not con.waitForConnected(): log.error(f"Unable to open server socket: {con.errorString()}") return if con.waitForReadyRead(): payload = con.readAll() url = force_decode(payload.data()) self._handle_nxdrive_url(url) con.disconnectFromServer() con.waitForDisconnected() del con log.debug("Successfully closed server socket") # Bring settings window to front self.settings_window.show() def update_status(self, engine: "Engine") -> None: """ Update the systray status for synchronization, conflicts/errors and software updates. """ sync_state = error_state = update_state = "" update_state = self.manager.updater.status self.refresh_conflicts(engine.uid) # Check synchronization state if engine.is_paused(): sync_state = "suspended" elif engine.is_syncing(): sync_state = "syncing" # Check error state if engine.has_invalid_credentials(): error_state = "auth_expired" elif self.conflicts_model.count: error_state = "conflicted" elif self.errors_model.count: error_state = "error" self._window_root(self.systray_window).setStatus.emit( sync_state, error_state, update_state) @pyqtSlot(str) def get_last_files(self, uid: str) -> None: files = self.api.get_last_files(uid, 10, "", None) self.file_model.empty() self.file_model.addFiles(files) def current_language(self) -> Optional[str]: lang = Translator.locale() for tag, name in self.language_model.languages: if tag == lang: return name return None
class QtSingleApplication(QApplication): """ This class makes sure that we can only start one Tribler application. When a user tries to open a second Tribler instance, the current active one will be brought to front. """ messageReceived = pyqtSignal(unicode) def __init__(self, win_id, *argv): logfunc = logging.info logfunc(sys._getframe().f_code.co_name + '()') QApplication.__init__(self, *argv) self._id = win_id self._activation_window = None self._activate_on_message = False # Is there another instance running? self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._id) self._isRunning = self._outSocket.waitForConnected() self._outStream = None self._inSocket = None self._inStream = None self._server = None if self._isRunning: # Yes, there is. self._outStream = QTextStream(self._outSocket) self._outStream.setCodec('UTF-8') else: # No, there isn't, at least not properly. # Cleanup any past, crashed server. error = self._outSocket.error() logfunc(LOGVARSTR % ('self._outSocket.error()', error)) if error == QLocalSocket.ConnectionRefusedError: logfunc('received QLocalSocket.ConnectionRefusedError; ' + \ 'removing server.') self.close() QLocalServer.removeServer(self._id) self._outSocket = None self._server = QLocalServer() self._server.listen(self._id) self._server.newConnection.connect(self._on_new_connection) logfunc(sys._getframe().f_code.co_name + '(): returning') def close(self): logfunc = logging.info logfunc(sys._getframe().f_code.co_name + '()') if self._inSocket: self._inSocket.disconnectFromServer() if self._outSocket: self._outSocket.disconnectFromServer() if self._server: self._server.close() logfunc(sys._getframe().f_code.co_name + '(): returning') def is_running(self): return self._isRunning def get_id(self): return self._id def activation_window(self): return self._activation_window def set_activation_window(self, activation_window, activate_on_message=True): self._activation_window = activation_window self._activate_on_message = activate_on_message def activate_window(self): if not self._activation_window: return self._activation_window.setWindowState( self._activation_window.windowState() & ~Qt.WindowMinimized) self._activation_window.raise_() def send_message(self, msg): if not self._outStream: return False self._outStream << msg << '\n' self._outStream.flush() return self._outSocket.waitForBytesWritten() def _on_new_connection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._on_ready_read) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QTextStream(self._inSocket) self._inStream.setCodec('UTF-8') self._inSocket.readyRead.connect(self._on_ready_read) if self._activate_on_message: self.activate_window() def _on_ready_read(self): while True: msg = self._inStream.readLine() if not msg: break self.messageReceived.emit(msg)
class SingleInstance: def __init__(self, application, files_to_open: Optional[List[str]]): self._application = application self._files_to_open = files_to_open self._single_instance_server = None # Starts a client that checks for a single instance server and sends the files that need to opened if the server # exists. Returns True if the single instance server is found, otherwise False. def startClient(self) -> bool: Logger.log( "i", "Checking for the presence of an ready running Cura instance.") single_instance_socket = QLocalSocket(self._application) Logger.log("d", "Full single instance server name: %s", single_instance_socket.fullServerName()) single_instance_socket.connectToServer("ultimaker-cura") single_instance_socket.waitForConnected( msecs=3000) # wait for 3 seconds if single_instance_socket.state() != QLocalSocket.ConnectedState: return False # We only send the files that need to be opened. if not self._files_to_open: Logger.log("i", "No file need to be opened, do nothing.") return True if single_instance_socket.state() == QLocalSocket.ConnectedState: Logger.log( "i", "Connection has been made to the single-instance Cura socket.") # Protocol is one line of JSON terminated with a carriage return. # "command" field is required and holds the name of the command to execute. # Other fields depend on the command. payload = {"command": "clear-all"} single_instance_socket.write( bytes(json.dumps(payload) + "\n", encoding="ascii")) payload = {"command": "focus"} single_instance_socket.write( bytes(json.dumps(payload) + "\n", encoding="ascii")) for filename in self._files_to_open: payload = { "command": "open", "filePath": os.path.abspath(filename) } single_instance_socket.write( bytes(json.dumps(payload) + "\n", encoding="ascii")) payload = {"command": "close-connection"} single_instance_socket.write( bytes(json.dumps(payload) + "\n", encoding="ascii")) single_instance_socket.flush() single_instance_socket.waitForDisconnected() return True def startServer(self) -> None: self._single_instance_server = QLocalServer() self._single_instance_server.newConnection.connect( self._onClientConnected) self._single_instance_server.listen("ultimaker-cura") def _onClientConnected(self): Logger.log("i", "New connection recevied on our single-instance server") connection = self._single_instance_server.nextPendingConnection() if connection is not None: connection.readyRead.connect( lambda c=connection: self.__readCommands(c)) def __readCommands(self, connection): line = connection.readLine() while len(line) != 0: # There is also a .canReadLine() try: payload = json.loads(str(line, encoding="ascii").strip()) command = payload["command"] # Command: Remove all models from the build plate. if command == "clear-all": self._application.callLater( lambda: self._application.deleteAll()) # Command: Load a model file elif command == "open": self._application.callLater(lambda f=payload["filePath"]: self._application._openFile(f)) # Command: Activate the window and bring it to the top. elif command == "focus": # Operating systems these days prevent windows from moving around by themselves. # 'alert' or flashing the icon in the taskbar is the best thing we do now. self._application.callLater( lambda: self._application.getMainWindow().alert(0)) # Command: Close the socket connection. We're done. elif command == "close-connection": connection.close() else: Logger.log( "w", "Received an unrecognized command " + str(command)) except json.decoder.JSONDecodeError as ex: Logger.log("w", "Unable to parse JSON command '%s': %s", line, repr(ex)) line = connection.readLine()
class IPCServer(QObject): """IPC server to which clients connect to. Attributes: ignored: Whether requests are ignored (in exception hook). _timer: A timer to handle timeouts. _server: A QLocalServer to accept new connections. _socket: The QLocalSocket we're currently connected to. _socketname: The socketname to use. _atime_timer: Timer to update the atime of the socket regularly. Signals: got_args: Emitted when there was an IPC connection and arguments were passed. got_args: Emitted with the raw data an IPC connection got. got_invalid_data: Emitted when there was invalid incoming data. """ got_args = pyqtSignal(list, str, str) got_raw = pyqtSignal(bytes) got_invalid_data = pyqtSignal() def __init__(self, socketname, parent=None): """Start the IPC server and listen to commands. Args: socketname: The socketname to use. parent: The parent to be used. """ super().__init__(parent) self.ignored = False self._socketname = socketname self._timer = usertypes.Timer(self, 'ipc-timeout') self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) if utils.is_windows: # pragma: no cover self._atime_timer = None else: self._atime_timer = usertypes.Timer(self, 'ipc-atime') self._atime_timer.setInterval(ATIME_INTERVAL) self._atime_timer.timeout.connect(self.update_atime) self._atime_timer.setTimerType(Qt.VeryCoarseTimer) self._server = QLocalServer(self) self._server.newConnection.connect( # type: ignore[attr-defined] self.handle_connection) self._socket = None self._old_socket = None if utils.is_windows: # pragma: no cover # As a WORKAROUND for a Qt bug, we can't use UserAccessOption on Unix. If we # do, we don't get an AddressInUseError anymore: # https://bugreports.qt.io/browse/QTBUG-48635 # # Thus, we only do so on Windows, and handle permissions manually in # listen() on Linux. log.ipc.debug("Calling setSocketOptions") self._server.setSocketOptions(QLocalServer.UserAccessOption) else: # pragma: no cover log.ipc.debug("Not calling setSocketOptions") def _remove_server(self): """Remove an existing server.""" ok = QLocalServer.removeServer(self._socketname) if not ok: raise Error("Error while removing server {}!".format( self._socketname)) def listen(self): """Start listening on self._socketname.""" log.ipc.debug("Listening as {}".format(self._socketname)) if self._atime_timer is not None: # pragma: no branch self._atime_timer.start() self._remove_server() ok = self._server.listen(self._socketname) if not ok: if self._server.serverError() == QAbstractSocket.AddressInUseError: raise AddressInUseError(self._server) raise ListenError(self._server) if not utils.is_windows: # pragma: no cover # WORKAROUND for QTBUG-48635, see the comment in __init__ for details. try: os.chmod(self._server.fullServerName(), 0o700) except FileNotFoundError: # https://github.com/qutebrowser/qutebrowser/issues/1530 # The server doesn't actually exist even if ok was reported as # True, so report this as an error. raise ListenError(self._server) @pyqtSlot('QLocalSocket::LocalSocketError') def on_error(self, err): """Raise SocketError on fatal errors.""" if self._socket is None: # Sometimes this gets called from stale sockets. log.ipc.debug("In on_error with None socket!") return self._timer.stop() log.ipc.debug("Socket 0x{:x}: error {}: {}".format( id(self._socket), self._socket.error(), self._socket.errorString())) if err != QLocalSocket.PeerClosedError: raise SocketError("handling IPC connection", self._socket) @pyqtSlot() def handle_connection(self): """Handle a new connection to the server.""" if self.ignored: return if self._socket is not None: log.ipc.debug("Got new connection but ignoring it because we're " "still handling another one (0x{:x}).".format( id(self._socket))) return socket = self._server.nextPendingConnection() if socket is None: log.ipc.debug( # type: ignore[unreachable] "No new connection to handle.") return log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket))) self._socket = socket self._timer.start() socket.readyRead.connect( # type: ignore[attr-defined] self.on_ready_read) if socket.canReadLine(): log.ipc.debug("We can read a line immediately.") self.on_ready_read() socket.error.connect(self.on_error) # type: ignore[attr-defined] if socket.error() not in [ QLocalSocket.UnknownSocketError, QLocalSocket.PeerClosedError ]: log.ipc.debug("We got an error immediately.") self.on_error(socket.error()) socket.disconnected.connect( # type: ignore[attr-defined] self.on_disconnected) if socket.state() == QLocalSocket.UnconnectedState: log.ipc.debug("Socket was disconnected immediately.") self.on_disconnected() @pyqtSlot() def on_disconnected(self): """Clean up socket when the client disconnected.""" log.ipc.debug("Client disconnected from socket 0x{:x}.".format( id(self._socket))) self._timer.stop() if self._old_socket is not None: self._old_socket.deleteLater() self._old_socket = self._socket self._socket = None # Maybe another connection is waiting. self.handle_connection() def _handle_invalid_data(self): """Handle invalid data we got from a QLocalSocket.""" assert self._socket is not None log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format( id(self._socket))) self.got_invalid_data.emit() self._socket.error.connect(self.on_error) self._socket.disconnectFromServer() def _handle_data(self, data): """Handle data (as bytes) we got from on_ready_read.""" try: decoded = data.decode('utf-8') except UnicodeDecodeError: log.ipc.error("invalid utf-8: {!r}".format(binascii.hexlify(data))) self._handle_invalid_data() return log.ipc.debug("Processing: {}".format(decoded)) try: json_data = json.loads(decoded) except ValueError: log.ipc.error("invalid json: {}".format(decoded.strip())) self._handle_invalid_data() return for name in ['args', 'target_arg']: if name not in json_data: log.ipc.error("Missing {}: {}".format(name, decoded.strip())) self._handle_invalid_data() return try: protocol_version = int(json_data['protocol_version']) except (KeyError, ValueError): log.ipc.error("invalid version: {}".format(decoded.strip())) self._handle_invalid_data() return if protocol_version != PROTOCOL_VERSION: log.ipc.error("incompatible version: expected {}, got {}".format( PROTOCOL_VERSION, protocol_version)) self._handle_invalid_data() return args = json_data['args'] target_arg = json_data['target_arg'] if target_arg is None: # https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html target_arg = '' cwd = json_data.get('cwd', '') assert cwd is not None self.got_args.emit(args, target_arg, cwd) @pyqtSlot() def on_ready_read(self): """Read json data from the client.""" if self._socket is None: # pragma: no cover # This happens when doing a connection while another one is already # active for some reason. if self._old_socket is None: log.ipc.warning("In on_ready_read with None socket and " "old_socket!") return log.ipc.debug("In on_ready_read with None socket!") socket = self._old_socket else: socket = self._socket if sip.isdeleted(socket): # pragma: no cover log.ipc.warning("Ignoring deleted IPC socket") return self._timer.stop() while socket is not None and socket.canReadLine(): data = bytes(socket.readLine()) self.got_raw.emit(data) log.ipc.debug("Read from socket 0x{:x}: {!r}".format( id(socket), data)) self._handle_data(data) if self._socket is not None: self._timer.start() @pyqtSlot() def on_timeout(self): """Cancel the current connection if it was idle for too long.""" assert self._socket is not None log.ipc.error("IPC connection timed out " "(socket 0x{:x}).".format(id(self._socket))) self._socket.disconnectFromServer() if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None self._socket.waitForDisconnected(CONNECT_TIMEOUT) if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None self._socket.abort() @pyqtSlot() def update_atime(self): """Update the atime of the socket file all few hours. From the XDG basedir spec: To ensure that your files are not removed, they should have their access time timestamp modified at least once every 6 hours of monotonic time or the 'sticky' bit should be set on the file. """ path = self._server.fullServerName() if not path: log.ipc.error("In update_atime with no server path!") return log.ipc.debug("Touching {}".format(path)) try: os.utime(path) except OSError: log.ipc.exception("Failed to update IPC socket, trying to " "re-listen...") self._server.close() self.listen() @pyqtSlot() def shutdown(self): """Shut down the IPC server cleanly.""" log.ipc.debug("Shutting down IPC (socket 0x{:x})".format( id(self._socket))) if self._socket is not None: self._socket.deleteLater() self._socket = None self._timer.stop() if self._atime_timer is not None: # pragma: no branch self._atime_timer.stop() try: self._atime_timer.timeout.disconnect(self.update_atime) except TypeError: pass self._server.close() self._server.deleteLater() self._remove_server()
class QtSingleApplication(QApplication): messageReceived = pyqtSignal(str) def __init__(self, id, *argv): super(QtSingleApplication, self).__init__(*argv) self._id = id self._activationWindow = None self._activateOnMessage = False # Is there another instance running? self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._id) self._isRunning = self._outSocket.waitForConnected() if self._isRunning: # Yes, there is. self._outStream = QTextStream(self._outSocket) self._outStream.setCodec("UTF-8") else: # No, there isn't. self._outSocket = None self._outStream = None self._inSocket = None self._inStream = None self._server = QLocalServer() self._server.listen(self._id) self._server.newConnection.connect(self._onNewConnection) def isRunning(self): return self._isRunning def id(self): return self._id def activationWindow(self): return self._activationWindow def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage def activateWindow(self): if not self._activationWindow: return self._activationWindow.show() self._activationWindow.setWindowState(self._activationWindow.windowState() & ~Qt.WindowMinimized) self._activationWindow.raise_() self._activationWindow.activateWindow() def sendMessage(self, msg): if not self._outStream: return False self._outStream << msg << "\n" self._outStream.flush() return self._outSocket.waitForBytesWritten() def _onNewConnection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._onReadyRead) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QTextStream(self._inSocket) self._inStream.setCodec("UTF-8") self._inSocket.readyRead.connect(self._onReadyRead) if self._activateOnMessage: self.activateWindow() def _onReadyRead(self): while True: msg = self._inStream.readLine() if not msg: break self.messageReceived.emit(msg)
class MainForm(QDialog): def __init__(self, parent = None): QDialog.__init__(self, parent) # If a Nemu instance is already running, this is as far as we go self.connectToRunning() self.holdOpen = False self.menuItems = [] self.allItems = [] self.favorites = [] self.currentItem = None self.menuFile = os.path.expanduser('~/.nemu/menu') self.favoritesFile = os.path.expanduser('~/.nemu/favorites') # NOTE: If you change this, also update migrate-settings self.settingsFile = os.path.expanduser('~/.nemu/settings') self.initSettings() self.server = QLocalServer() self.server.newConnection.connect(self.handleConnection) QLocalServer.removeServer('nemuSocket') self.server.listen('nemuSocket') self.configDir = os.path.expanduser('~/.nemu') if not os.path.isdir(self.configDir): os.mkdir(self.configDir) self.menuItems = self.loadConfig(self.menuFile, self.menuItems) self.favorites = self.loadConfig(self.favoritesFile, self.favorites) # Don't load directly into self.settings so we can add new default values as needed try: tempSettings = self.loadConfig(self.settingsFile, self.settings) for key, value in tempSettings.items(): self.settings[key] = value except SystemError: print('ERROR: Failed to load settings. You may need to run migrate-settings.') raise # This should never happen, but unfortunately bugs do, so clean up orphaned items. # We need to do this because these items won't show up in the UI, but may interfere with # merges if they duplicate something that is being merged in. self.menuItems[:] = [i for i in self.menuItems if i.parent == None or i.parent in self.menuItems] # Look for broken icon paths needSave = False for i in self.menuItems + self.favorites: if not os.path.exists(i.icon): i.findIcon() needSave = True if needSave: self.saveMenu() for i in self.menuItems: if not hasattr(i, 'imported'): i.imported = False self.setupUI() self.setContextMenuPolicy(Qt.ActionsContextMenu) self.createMenu(self) self.refresh(False) if len(self.menuItems) == 0: self.firstRun() self.show() self.keepaliveTimer = QTimer(self) self.keepaliveTimer.timeout.connect(self.keepalive) self.keepaliveTimer.start(60000) def initSettings(self): self.settings = dict() self.settings['width'] = 400 self.settings['height'] = 400 self.settings['quit'] = False self.settings['imported'] = [] self.settings['iconTheme'] = None def loadConfig(self, filename, default): if os.path.exists(filename): with open(filename, 'rb') as f: data = f.read().replace('PyQt4', 'PyQt5') return cPickle.loads(data) else: return default def setupUI(self): self.resize(self.settings['width'], self.settings['height']) self.setWindowFlags(Qt.FramelessWindowHint | Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint) #self.setWindowFlags(Qt.X11BypassWindowManagerHint) self.setWindowTitle('Nemu') self.setMouseTracking(True) iconPath = os.path.join(os.path.dirname(__file__), 'images') iconPath = os.path.join(iconPath, 'nemu.png') self.setWindowIcon(IconCache()[iconPath]) self.place() self.buttonListLayout = QVBoxLayout(self) self.setMargins(self.buttonListLayout) self.buttonLayout = QHBoxLayout() self.setMargins(self.buttonLayout) # Settings and Filter box self.filterLayout = QHBoxLayout() self.settingsButton = QPushButton() self.settingsButton.setIcon(QIcon(iconPath)) self.settingsButton.setMinimumHeight(35) self.settingsButton.clicked.connect(self.settingsClicked) self.filterLayout.addWidget(self.settingsButton, 0) self.filterLabel = QLabel("Filter") self.filterLayout.addWidget(self.filterLabel) self.filterBox = QLineEdit() self.filterBox.textChanged.connect(self.refresh) self.filterLayout.addWidget(self.filterBox) self.sizeGrip = QSizeGrip(self) self.sizeGrip.setMinimumSize(QSize(25, 25)) self.filterLayout.addWidget(self.sizeGrip, 0, Qt.AlignRight | Qt.AlignTop) self.buttonListLayout.addLayout(self.filterLayout) # Top buttons and labels self.backButton = QPushButton('Favorites') self.backButton.setMinimumHeight(35) self.backButton.clicked.connect(self.backClicked) self.buttonLayout.addWidget(self.backButton, 1) self.currentLabel = QLabel() self.currentLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.buttonLayout.addWidget(self.currentLabel, 1) self.buttonListLayout.addLayout(self.buttonLayout, 0) # Menu item display self.listSplitter = QSplitter() self.buttonListLayout.addWidget(self.listSplitter, 1) self.leftList = ListWidget(self.clearListMouseOver) self.listSplitter.addWidget(self.leftList) self.rightList = ListWidget(self.clearListMouseOver) self.listSplitter.addWidget(self.rightList) # Has to be done after adding widgets to the splitter or the size will get reset again if 'splitterState' in self.settings: self.listSplitter.restoreState(self.settings['splitterState']) def setMargins(self, layout, margin = 0): layout.setSpacing(margin) layout.setContentsMargins(margin, margin, margin, margin) def createMenu(self, widget): addFavoriteAction = QAction('Add to Favorites', self) addFavoriteAction.triggered.connect(self.addFavoriteClicked) widget.insertAction(None, addFavoriteAction) addAction = QAction("New...", self) addAction.triggered.connect(self.newClicked) widget.insertAction(None, addAction) editAction = QAction("Edit...", self) editAction.triggered.connect(self.editClicked) widget.insertAction(None, editAction) deleteAction = QAction("Delete", self) deleteAction.triggered.connect(self.deleteClicked) widget.insertAction(None, deleteAction) def hideOrClose(self): if self.settings['quit']: self.close() else: self.hide() def closeEvent(self, event): self.saveSettings() def hideEvent(self, event): self.releaseMouse() self.saveSettings() def mouseMoveEvent(self, event): if self.hasMouse(): self.releaseMouse() def leaveEvent(self, event): # If we set holdOpen, it means that we've opened a dialog, so we shouldn't grab if not self.hasMouse(): self.grabMouse() def mousePressEvent(self, event): if not self.hasMouse(): self.hideOrClose() def hasMouse(self): return self.geometry().contains(QCursor.pos()) def saveSettings(self): self.settings['splitterState'] = self.listSplitter.saveState() self.settings['width'] = self.width() self.settings['height'] = self.height() with open(self.settingsFile, 'wb') as f: cPickle.dump(self.settings, f) def place(self): desktop = qApp.desktop() screenSize = desktop.availableGeometry(QCursor.pos()) self.move(screenSize.x(), screenSize.y() + screenSize.height() - self.height()) def newClicked(self): form = AddForm() self.holdOpen = True form.exec_() self.checkMouse() self.holdOpen = False if form.accepted: item = MenuItem() item.name = form.name item.command = form.command item.working = form.working item.folder = form.folder item.icon = form.icon item.findIcon() clicked = self.getClicked() if clicked: parent = clicked.item.parent elif self.leftList.mouseOver: if self.currentItem != None: parent = self.currentItem.parent else: parent = None else: parent = self.currentItem item.parent = parent self.menuItems.append(item) self.refresh() def editClicked(self): form = AddForm() clicked = self.getClicked() if clicked == None: return item = clicked.item form.name = item.name form.command = item.command form.working = item.working form.folder = item.folder form.icon = item.icon form.populateFields() self.holdOpen = True form.exec_() self.checkMouse() self.holdOpen = False if form.accepted: item.name = form.name item.command = form.command item.working = form.working item.folder = form.folder item.icon = form.icon item.imported = False item.findIcon() self.refresh() def checkMouse(self): if not self.hasMouse(): self.grabMouse() def deleteClicked(self): clicked = self.getClicked() if clicked == None: return self.delete(clicked.item) self.refresh() # Delete item and all of its children so we don't leave around orphaned items def delete(self, item): for i in self.menuItems: if i.parent == item: i.deleted = True if item in self.menuItems: item.deleted = True item.imported = False if item in self.favorites: self.favorites.remove(item) def addFavoriteClicked(self): newFavorite = copy.copy(self.getClicked().item) newFavorite.parent = None self.favorites.append(newFavorite) self.refresh() def getClicked(self): for i in self.allItems: if i.mouseOver: return i def clearMouseOver(self): for i in self.allItems: i.mouseOver = False def clearListMouseOver(self): self.leftList.mouseOver = False self.rightList.mouseOver = False def refresh(self, save = True): self.leftList.clear() self.rightList.clear() self.allItems = [] sortedLeft = [] sortedRight = [] self.updateFilter() if self.currentItem != None: currParent = self.currentItem.parent for i in self.menuItems: if i.parent == currParent and not i.deleted and i.matchedFilter: sortedLeft.append(i) else: for i in self.favorites: sortedLeft.append(i) for i in self.menuItems: if i.parent == self.currentItem and not i.deleted and i.matchedFilter: sortedRight.append(i) sortedLeft.sort(key = lambda x: x.name) sortedLeft.sort(key = lambda x: not x.folder) sortedRight.sort(key = lambda x: x.name) sortedRight.sort(key = lambda x: not x.folder) for i in sortedLeft: self.leftList.add(self.createItem(i)) for i in sortedRight: self.rightList.add(self.createItem(i)) if save: self.saveMenu() def saveMenu(self): # Save the current menu status with open(self.menuFile, 'wb') as f: cPickle.dump(self.menuItems, f) with open(self.favoritesFile, 'wb') as f: cPickle.dump(self.favorites, f) def createItem(self, item): newItem = ListItem(item, self.clearMouseOver) newItem.clicked.connect(self.itemClicked) self.allItems.append(newItem) return newItem def updateFilter(self): filterValue = str(self.filterBox.text()) for i in self.menuItems: i.checkFilter(filterValue) def itemClicked(self): sender = self.sender() if sender.item.folder: self.setCurrentItem(sender.item) self.refresh(False) else: flags = ['f', 'F', 'u', 'U', 'd', 'D', 'n', 'N', 'i', 'k', 'v', 'm'] command = sender.item.command for i in flags: command = command.replace('%' + i, '') # %c needs a proper value in some cases command = command.replace('%c', '"%s"' % sender.item.name) working = sender.item.working if not os.path.isdir(working): working = None # Need to redirect stdout and stderr so if the process writes something it won't fail with open(os.path.devnull, 'w') as devnull: Popen(command + '&', stdout=devnull, stderr=devnull, shell=True, cwd=working) self.hideOrClose() def backClicked(self): if self.currentItem: self.setCurrentItem(self.currentItem.parent) self.refresh(False) def setCurrentItem(self, item): self.currentItem = item if item != None: self.currentLabel.setText(item.name) if item.parent != None: self.backButton.setText(item.parent.name) else: self.backButton.setText('Favorites') else: self.currentLabel.setText('') self.backButton.setText('Favorites') def settingsClicked(self): form = SettingsForm(self) form.quitCheck.setChecked(self.settings['quit']) theme = self.settings.get('iconTheme') if theme: form.themeCombo.setCurrentIndex(form.themeCombo.findText(theme)) self.holdOpen = True form.exec_() self.checkMouse() self.holdOpen = False if form.accepted: self.settings['quit'] = form.quitCheck.isChecked() def firstRun(self): QMessageBox.information(self, 'First Time?', 'Your menu is currently empty. It is recommended that you import an existing menu file.') self.settingsClicked() def connectToRunning(self): self.socket = QLocalSocket() self.socket.connectToServer('nemuSocket') self.socket.waitForConnected(1000) if self.socket.state() == QLocalSocket.ConnectedState: print 'Server found' if self.socket.waitForReadyRead(3000): line = self.socket.readLine() print line else: print self.socket.errorString() sys.exit() else: print 'No server running' def handleConnection(self): import datetime print "Got connection", datetime.datetime.now() connection = self.server.nextPendingConnection() connection.write('connected') del connection self.setCurrentItem(None) self.filterBox.setText('') self.refresh(False) self.show() print "Showed", datetime.datetime.now() return # Call periodically to keep data resident in memory (hopefully) def keepalive(self): if self.isHidden(): self.refresh(False)
class IDE(QMainWindow): """This class is like the Sauron's Ring: One ring to rule them all, One ring to find them, One ring to bring them all and in the darkness bind them. This Class knows all the containers, and its know by all the containers, but the containers don't need to know between each other, in this way we can keep a better api without the need to tie the behaviour between the widgets, and let them just consume the 'actions' they need.""" ############################################################################### # SIGNALS ############################################################################### goingDown = pyqtSignal() filesAndProjectsLoaded = pyqtSignal() __IDESERVICES = {} __IDECONNECTIONS = {} __IDESHORTCUTS = {} __IDEBARCATEGORIES = {} __IDEMENUS = {} __IDETOOLBAR = {} # CONNECTIONS structure: # ({'target': service_name, 'signal_name': string, 'slot': function_obj},) # On modify add: {connected: True} __instance = None __created = False def __init__(self, start_server=False): QMainWindow.__init__(self) self.setWindowTitle('NINJA-IDE {Ninja-IDE Is Not Just Another IDE}') self.setMinimumSize(750, 500) QToolTip.setFont(QFont(settings.FONT.family(), 10)) # Load the size and the position of the main window self.load_window_geometry() # self.__project_to_open = 0 # Editables self.__neditables = {} # Filesystem self.filesystem = nfilesystem.NVirtualFileSystem() # Interpreter service self.interpreter = interpreter_service.InterpreterService() # Sessions handler self._session_manager = session_manager.SessionsManager(self) IDE.register_service("session_manager", self._session_manager) self._session = None # Opacity self.opacity = settings.MAX_OPACITY # ToolBar # self.toolbar = QToolBar(self) # if settings.IS_MAC_OS: # self.toolbar.setIconSize(QSize(36, 36)) # else: # self.toolbar.setIconSize(QSize(24, 24)) # self.toolbar.setToolTip(translations.TR_IDE_TOOLBAR_TOOLTIP) # self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly) # # Set toggleViewAction text and tooltip # self.toggleView = self.toolbar.toggleViewAction() # self.toggleView.setText(translations.TR_TOOLBAR_VISIBILITY) # self.toggleView.setToolTip(translations.TR_TOOLBAR_VISIBILITY) # self.addToolBar(settings.TOOLBAR_AREA, self.toolbar) # if settings.HIDE_TOOLBAR: # self.toolbar.hide() # Notificator self.notification = notification.Notification(self) # Plugin Manager # CHECK ACTIVATE PLUGINS SETTING # services = { # 'editor': plugin_services.MainService(), # 'toolbar': plugin_services.ToolbarService(self.toolbar), # 'menuApp': plugin_services.MenuAppService(self.pluginsMenu), # 'menuApp': plugin_services.MenuAppService(None), # 'explorer': plugin_services.ExplorerService(), # 'misc': plugin_services.MiscContainerService(self.misc)} # serviceLocator = plugin_manager.ServiceLocator(services) # serviceLocator = plugin_manager.ServiceLocator(None) # self.plugin_manager = plugin_manager.PluginManager(resources.PLUGINS, # serviceLocator) # self.plugin_manager.discover() # load all plugins! # self.plugin_manager.load_all() # Tray Icon # self.trayIcon = updates.TrayIconUpdates(self) # self.trayIcon.closeTrayIcon.connect(self._close_tray_icon) # self.trayIcon.show() # TODO: # key = Qt.Key_1 # for i in range(10): # if settings.IS_MAC_OS: # short = ui_tools.TabShortcuts( # QKeySequence(Qt.CTRL + Qt.ALT + key), self, i) # else: # short = ui_tools.TabShortcuts( # QKeySequence(Qt.ALT + key), self, i) # key += 1 # short.activated.connect(self._change_tab_index) # short = ui_tools.TabShortcuts( # QKeySequence(Qt.ALT + Qt.Key_0), self, 10) # short.activated.connect(self._change_tab_index) # Register menu categories IDE.register_bar_category(translations.TR_MENU_FILE, 100) IDE.register_bar_category(translations.TR_MENU_EDIT, 110) IDE.register_bar_category(translations.TR_MENU_VIEW, 120) IDE.register_bar_category(translations.TR_MENU_SOURCE, 130) IDE.register_bar_category(translations.TR_MENU_PROJECT, 140) IDE.register_bar_category(translations.TR_MENU_EXTENSIONS, 150) IDE.register_bar_category(translations.TR_MENU_ABOUT, 160) # Register General Menu Items ui_tools.install_shortcuts(self, actions.ACTIONS_GENERAL, self) self.register_service("ide", self) self.register_service("interpreter", self.interpreter) # self.register_service('toolbar', self.toolbar) self.register_service("filesystem", self.filesystem) self.toolbar = IDE.get_service("toolbar") # Register signals connections connections = ( { "target": "main_container", "signal_name": "fileSaved", "slot": self.show_message }, { "target": "main_container", "signal_name": "currentEditorChanged", "slot": self.change_window_title }, { "target": "main_container", "signal_name": "openPreferences", "slot": self.show_preferences }, { "target": "main_container", "signal_name": "currentEditorChanged", "slot": self._change_item_in_project }, { "target": "main_container", "signal_name": "allFilesClosed", "slot": self.change_window_title }, { "target": "projects_explorer", "signal_name": "activeProjectChanged", "slot": self.change_window_title } ) self.register_signals('ide', connections) # connections = ( # {'target': 'main_container', # 'signal_name': 'openPreferences()', # 'slot': self.show_preferences}, # {'target': 'main_container', # 'signal_name': 'allTabsClosed()', # 'slot': self._last_tab_closed}, # {'target': 'explorer_container', # 'signal_name': 'changeWindowTitle(QString)', # 'slot': self.change_window_title}, # {'target': 'explorer_container', # 'signal_name': 'projectClosed(QString)', # 'slot': self.close_project}, # ) # Central Widget MUST always exists self.central = IDE.get_service('central_container') self.setCentralWidget(self.central) # Install Services for service_name in self.__IDESERVICES: self.install_service(service_name) IDE.__created = True # Place Status Bar main_container = IDE.get_service('main_container') status_bar = IDE.get_service('status_bar') main_container.add_status_bar(status_bar) # Load Menu Bar menu_bar = IDE.get_service('menu_bar') if menu_bar: # These two are the same service, I think that's ok menu_bar.load_menu(self) menu_bar.load_toolbar(self) # Start server if needed self.s_listener = None if start_server: self.s_listener = QLocalServer() self.s_listener.listen("ninja_ide") self.s_listener.newConnection.connect(self._process_connection) # Load interpreters self.load_interpreters() IDE.__instance = self def _change_item_in_project(self, filename): project_explorer = IDE.get_service("projects_explorer") if project_explorer is not None: project_explorer.set_current_item(filename) @classmethod def get_service(cls, service_name): """Return the instance of a registered service.""" service = cls.__IDESERVICES.get(service_name, None) if service is None: logger.debug("Service '{}' unregistered".format(service_name)) return service def get_menuitems(self): """Return a dictionary with the registered menu items.""" return IDE.__IDEMENUS # def get_bar_categories(self): """Get the registered Categories for the Application menus.""" return IDE.__IDEBARCATEGORIES def get_toolbaritems(self): """Return a dictionary with the registered menu items.""" return IDE.__IDETOOLBAR @classmethod def register_service(cls, service_name, obj): """Register a service providing the service name and the instance.""" cls.__IDESERVICES[service_name] = obj if cls.__created: cls.__instance.install_service(service_name) def install_service(self, service_name): """ Activate the registered service """ obj = IDE.__IDESERVICES.get(service_name, None) func = getattr(obj, 'install', None) if isinstance(func, collections.Callable): func() self._connect_signals() def place_me_on(self, name, obj, region="central", top=False): """Place a widget in some of the areas in the IDE. @name: id to access to that widget later if needed. @obj: the instance of the widget to be placed. @region: the area where to put the widget [central, lateral] @top: place the widget as the first item in the split.""" self.central.add_to_region(name, obj, region, top) @classmethod def register_signals(cls, service_name, connections): """Register all the signals that a particular service wants to be attached of. @service_name: id of the service @connections: list of dictionaries for the connection with: - 'target': 'the_other_service_name', - 'signal_name': 'name of the signal in the other service', - 'slot': function object in this service""" cls.__IDECONNECTIONS[service_name] = connections if cls.__created: cls.__instance._connect_signals() def _connect_signals(self): """Connect the signals between the different services.""" for service_name in IDE.__IDECONNECTIONS: connections = IDE.__IDECONNECTIONS[service_name] for connection in connections: if connection.get('connected', False): continue target = IDE.__IDESERVICES.get( connection['target'], None) slot = connection['slot'] signal_name = connection['signal_name'] if target and isinstance(slot, collections.Callable): # FIXME: sl = getattr(target, signal_name, None) if sl is not None: sl.connect(slot) connection['connected'] = True # print("Falta conectar {} a {}".format(signal_name, # slot.__name__)) # self.connect(target, SIGNAL(signal_name), slot) # connection['connected'] = True @classmethod def register_shortcut(cls, shortcut_name, shortcut, action=None): """ Register a shortcut and action """ cls.__IDESHORTCUTS[shortcut_name] = (shortcut, action) @classmethod def register_menuitem(cls, menu_action, section, weight): """Register a QAction or QMenu in the IDE to be loaded later in the menubar using the section(string) to define where is going to be contained, and the weight define the order where is going to be placed. @menu_action: QAction or QMenu @section: String (name) @weight: int""" cls.__IDEMENUS[menu_action] = (section, weight) @classmethod def register_toolbar(cls, action, section, weight): """Register a QAction in the IDE to be loaded later in the toolbar using the section(string) to define where is going to be contained, and the weight define the order where is going to be placed. @action: QAction @section: String (name) @weight: int""" cls.__IDETOOLBAR[action] = (section, weight) @classmethod def register_bar_category(cls, category_name, weight): """Register a Menu Category to be created with the proper weight. @category_name: string @weight: int""" cls.__IDEBARCATEGORIES[category_name] = weight @classmethod def update_shortcut(cls, shortcut_name): """Update all the shortcuts of the application.""" short = resources.get_shortcut shortcut, action = cls.__IDESHORTCUTS.get(shortcut_name) if shortcut: shortcut.setKey(short(shortcut_name)) if action: action.setShortcut(short(shortcut_name)) def get_or_create_nfile(self, filename): """For convenience access to files from ide""" return self.filesystem.get_file(nfile_path=filename) def get_editable(self, nfile=None): return self.__neditables.get(nfile) def get_or_create_editable(self, filename="", nfile=None): if nfile is None: nfile = self.filesystem.get_file(nfile_path=filename) editable = self.__neditables.get(nfile) if editable is None: editable = neditable.NEditable(nfile) editable.fileClosing['PyQt_PyObject'].connect( self._unload_neditable) self.__neditables[nfile] = editable return editable def _unload_neditable(self, editable): self.__neditables.pop(editable.nfile) editable.nfile.deleteLater() editable.editor.deleteLater() editable.deleteLater() @property def opened_files(self): return tuple(self.__neditables.keys()) def get_project_for_file(self, filename): project = None if filename: project = self.filesystem.get_project_for_file(filename) return project def create_project(self, path): nproj = nproject.NProject(path) self.filesystem.open_project(nproj) return nproj def close_project(self, project_path): self.filesystem.close_project(project_path) def get_projects(self): return self.filesystem.get_projects() def get_current_project(self): current_project = None projects = self.filesystem.get_projects() for project in projects: if projects[project].is_current: current_project = projects[project] break return current_project def get_interpreters(self): return self.interpreter.get_interpreters() def get_interpreter(self, path): return self.interpreter.get_interpreter(path) def set_interpreter(self, path): self.interpreter.set_interpreter(path) def load_interpreters(self): # ds = self.data_settings() # settings.PYTHON_EXEC = ds.value("ide/interpreter") self.interpreter.load() @classmethod def select_current(cls, widget): """Show the widget with a 4px lightblue border line.""" widget.setProperty("highlight", True) widget.style().unpolish(widget) widget.style().polish(widget) @classmethod def unselect_current(cls, widget): """Remove the 4px lightblue border line from the widget.""" widget.setProperty("highlight", False) widget.style().unpolish(widget) widget.style().polish(widget) def _close_tray_icon(self): """Close the System Tray Icon.""" self.trayIcon.hide() self.trayIcon.deleteLater() # def _change_tab_index(self): # """Change the tabs of the current TabWidget using alt+numbers.""" # widget = QApplication.focusWidget() # shortcut_index = getattr(widget, 'shortcut_index', None) # if shortcut_index: # obj = self.sender() # shortcut_index(obj.index) def _process_connection(self): """Read the ipc input from another instance of ninja.""" connection = self.s_listener.nextPendingConnection() connection.waitForReadyRead() data = connection.readAll() connection.close() if data: files, projects = str(data).split(ipc.project_delimiter, 1) files = [(x.split(':')[0], int(x.split(':')[1])) for x in files.split(ipc.file_delimiter)] projects = projects.split(ipc.project_delimiter) self.load_session_files_projects(files, [], projects, None) def fullscreen_mode(self): """Change to fullscreen mode.""" if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def change_toolbar_visibility(self): """Switch the toolbar visibility""" if self.toolbar.isVisible(): self.toolbar.hide() else: self.toolbar.show() def change_toolsdock_visibility(self): """Switch the tools dock visibility""" tools_dock = IDE.get_service("tools_dock").buttons_widget if tools_dock.isVisible(): tools_dock.hide() else: tools_dock.show() def load_external_plugins(self, paths): """Load external plugins, the ones added to ninja throw the cmd.""" for path in paths: self.plugin_manager.add_plugin_dir(path) # load all plugins! self.plugin_manager.discover() self.plugin_manager.load_all() def _last_tab_closed(self): """ Called when the last tasb is closed """ self.explorer.cleanup_tabs() def show_preferences(self): """Open the Preferences Dialog.""" pref = preferences.Preferences(self) # main_container = IDE.get_service("main_container") # if main_container: # main_container.show_dialog(pref) # else: pref.setModal(True) pref.show() def load_session_files_projects(self, files, projects, current_file): """Load the files and projects from previous session.""" # Load projects projects_explorer = IDE.get_service('projects_explorer') if projects_explorer is not None: projects_explorer.load_session_projects(projects) # Load files main_container = IDE.get_service('main_container') for path, cursor_pos in files: line, col = cursor_pos main_container.open_file(path, line, col) if current_file: main_container.open_file(current_file) self.filesAndProjectsLoaded.emit() # projects_explorer = IDE.get_service('projects_explorer') # if main_container and files: # for fileData in files: # if file_manager.file_exists(fileData[0]): # mtime = os.stat(fileData[0]).st_mtime # ignore_checkers = (mtime == fileData[2]) # line, col = fileData[1][0], fileData[1][1] # main_container.open_file(fileData[0], line, col, # ignore_checkers=ignore_checkers) # if current_file: # main_container.open_file(current_file) # if projects_explorer and projects: # projects_explorer.load_session_projects(projects) # def _set_editors_project_data(self): # self.__project_to_open -= 1 # if self.__project_to_open == 0: # self.disconnect(self.explorer, SIGNAL("projectOpened(QString)"), # self._set_editors_project_data) # self.mainContainer.update_editor_project() # def open_file(self, filename): # if filename: # self.mainContainer.open_file(filename) # def open_project(self, project): # if project: # self.actions.open_project(project) def __get_session(self): return self._session def __set_session(self, sessionName): self._session = sessionName if self._session is not None: self.setWindowTitle(translations.TR_SESSION_IDE_HEADER % {'session': self._session}) else: self.setWindowTitle( 'NINJA-IDE {Ninja-IDE Is Not Just Another IDE}') Session = property(__get_session, __set_session) def change_window_title(self, text=""): """Change the title of the Application display_name - [project] - {session} - NINJA-IDE """ title = [] main_container = self.get_service("main_container") neditor = main_container.get_current_editor() if neditor is not None: nfile = neditor.nfile title.append(nfile.display_name) nproject = self.get_current_project() if nproject is not None: title.append("[" + nproject.name + "]") session = self._session_manager.current_session if session is not None: title.append(translations.TR_SESSION_IDE_HEADER.format(session)) title.append("NINJA-IDE") formated_list = ["{}" for _ in title] self.setWindowTitle(" - ".join(formated_list).format(*title)) def wheelEvent(self, event): """Change the opacity of the application.""" if event.modifiers() == Qt.ShiftModifier: if event.delta() == 120 and self.opacity < settings.MAX_OPACITY: self.opacity += 0.1 elif event.delta() == -120 and self.opacity > settings.MIN_OPACITY: self.opacity -= 0.1 self.setWindowOpacity(self.opacity) event.ignore() else: QMainWindow.wheelEvent(self, event) @classmethod def ninja_settings(cls): qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) return qsettings @classmethod def editor_settings(cls): qsettings = nsettings.NSettings(resources.SETTINGS_PATH) main_container = cls.get_service("main_container") # Connect valueChanged signal to _editor_settings_changed slot qsettings.valueChanged.connect(main_container._editor_settings_changed) return qsettings @classmethod def data_settings(cls): qsettings = QSettings( resources.DATA_SETTINGS_PATH, QSettings.IniFormat) return qsettings # @classmethod # def ninja_settings(cls, qobject=None): # qsettings = nsettings.NSettings(resources.SETTINGS_PATH, qobject, # prefix="ns") # if cls.__created: # qsettings.valueChanged['PyQt_PyObject', # 'QString', # 'PyQt_PyObject'].connect( # cls.__instance._settings_value_changed) # return qsettings # @classmethod # def data_settings(cls): # qsettings = nsettings.NSettings(resources.DATA_SETTINGS_PATH, # prefix="ds") # if cls.__created: # qsettings.valueChanged['PyQt_PyObject', # 'QString', # 'PyQt_PyObject'].connect( # cls.__instance._settings_value_changed) # return qsettings # def _settings_value_changed(self, qobject, key, value): # signal_name = "%s(PyQt_PyObject)" % key.replace("/", "_") # signal_name = "%s" % key.replace("/", "_") # print(qobject, key) # callback = getattr(self, signal_name, None) # if hasattr(callback, "__call__"): # callback() # print(signal_name, value) # self.emit(SIGNAL(signal_name), value) # print("Falta emitir {}".format(signal_name)) def save_settings(self): """ Save the settings before the application is closed with QSettings. Info saved: files and projects opened, windows state(size and position). """ data_settings = IDE.data_settings() ninja_settings = IDE.ninja_settings() # Remove swap files # for editable in self.__neditables.values(): # if editable.swap_file is not None: # # A new file does not have a swap file # editable.swap_file._file_closed() if data_settings.value("ide/loadFiles", True): # Get opened projects projects_obj = self.filesystem.get_projects() projects = [projects_obj[project].path for project in projects_obj] data_settings.setValue("lastSession/projects", projects) # Opened files files_info = [] if self.opened_files: for nfile in self.opened_files: if nfile.is_new_file: continue editable = self.get_editable(nfile) files_info.append(( nfile.file_path, editable.editor.cursor_position)) data_settings.setValue("lastSession/openedFiles", files_info) main_container = self.get_service("main_container") neditor = main_container.get_current_editor() # Current opened file current_file = '' if neditor is not None: current_file = neditor.file_path data_settings.setValue('lastSession/currentFile', current_file) # Save toolbar visibility # ninja_settings.setValue('window/hide_toolbar', # not self.toolbar.isVisible()) # Save window state if self.isMaximized(): ninja_settings.setValue("window/maximized", True) else: ninja_settings.setValue("window/maximized", False) ninja_settings.setValue("window/size", self.size()) ninja_settings.setValue("window/pos", self.pos()) # qsettings = IDE.ninja_settings() # data_qsettings = IDE.data_settings() # main_container = self.get_service("main_container") # editor_widget = None # if main_container: # editor_widget = main_container.get_current_editor() # current_file = '' # if editor_widget is not None: # current_file = editor_widget.file_path # if qsettings.value('preferences/general/loadFiles', True, type=bool): # openedFiles = self.filesystem.get_files() # projects_obj = self.filesystem.get_projects() # projects = [projects_obj[proj].path for proj in projects_obj] # data_qsettings.setValue('lastSession/projects', projects) # files_info = [] # for path in openedFiles: # editable = self.__neditables.get(openedFiles[path]) # if editable is not None and editable.is_dirty: # stat_value = 0 # else: # stat_value = os.stat(path).st_mtime # files_info.append([path, # editable.editor.cursor_position, # stat_value]) # data_qsettings.setValue('lastSession/openedFiles', files_info) # if current_file is not None: # data_qsettings.setValue( # 'lastSession/currentFile', current_file) recent_files = main_container.last_opened_files data_settings.setValue("lastSession/recentFiles", recent_files) # "lastSession/recentFiles", list(main_container.last_opened_files)) # data_qsettings.setValue('lastSession/recentFiles', # settings.LAST_OPENED_FILES) # qsettings.setValue('preferences/editor/bookmarks', # settings.BOOKMARKS) # qsettings.setValue('preferences/editor/breakpoints', # settings.BREAKPOINTS) # Session # if self._session is not None: # val = QMessageBox.question( # self, # translations.TR_SESSION_ACTIVE_IDE_CLOSING_TITLE, # (translations.TR_SESSION_ACTIVE_IDE_CLOSING_BODY % # {'session': self.Session}), # QMessageBox.Yes, QMessageBox.No) # if val == QMessageBox.Yes: # session_manager.SessionsManager.save_session_data( # self.Session, self) # qsettings.setValue('preferences/general/toolbarArea', # self.toolBarArea(self.toolbar)) interpreter = self.interpreter.current.exec_path data_settings.setValue("ide/interpreter", interpreter) def activate_profile(self): """Show the Session Manager dialog.""" # profilesLoader = session_manager.SessionsManager(self) # profilesLoader.show() # pass self._session_manager.show() def deactivate_profile(self): """Close the Session Session.""" # self.Session = None pass def load_window_geometry(self): """Load from QSettings the window size of Ninja IDE""" qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) if qsettings.value("window/maximized", True, type=bool): self.setWindowState(Qt.WindowMaximized) else: self.resize(qsettings.value("window/size", QSizeF(800, 600))) self.move(qsettings.value("window/pos", QPointF(100, 100))) def _get_unsaved_files(self): """Return an array with the path of the unsaved files.""" unsaved = [] files = self.opened_files for f in files: editable = self.__neditables.get(f) if editable is not None and editable.editor.is_modified: unsaved.append(f) return unsaved def _save_unsaved_files(self, files): """Save the files from the paths in the array.""" for f in files: editable = self.get_or_create_editable(nfile=f) editable.ignore_checkers = True editable.save_content() def closeEvent(self, event): """Saves some global settings before closing.""" # if self.s_listener: # self.s_listener.close() # _unsaved_files = self._get_unsaved_files() # if settings.CONFIRM_EXIT and _unsaved_files: # dialog = unsaved_files.UnsavedFilesDialog(_unsaved_files, self) # if dialog.exec_() == QDialog.Rejected: # event.ignore() # return # else: # self._save_unsaved_files(_unsaved_files) self.save_settings() self.goingDown.emit() # close python documentation server (if running) # main_container.close_python_doc() # Shutdown PluginManager # self.plugin_manager.shutdown() # completion_daemon.shutdown_daemon() super(IDE, self).closeEvent(event) # def notify_plugin_errors(self): # # TODO: Check if the Plugin Error dialog can be improved # errors = self.plugin_manager.errors # if errors: # plugin_error_dialog = traceback_widget.PluginErrorDialog() # for err_tuple in errors: # plugin_error_dialog.add_traceback(err_tuple[0], err_tuple[1]) # # show the dialog # plugin_error_dialog.exec_() def show_message(self, message, duration=1800): """Show status message.""" self.notification.set_message(message, duration) self.notification.show() # def show_plugins_store(self): # """Open the Plugins Manager to install/uninstall plugins.""" # store = plugins_store.PluginsStore(self) # main_container = IDE.get_service("main_container") # if main_container: # main_container.show_dialog(store) # else: # store.show() # def show_languages(self): # """Open the Language Manager to install/uninstall languages.""" # manager = language_manager.LanguagesManagerWidget(self) # manager.show() # def show_schemes(self): # """Open the Schemes Manager to install/uninstall schemes.""" # manager = schemes_manager.SchemesManagerWidget(self) # manager.show() def show_about_qt(self): """Show About Qt Dialog.""" QMessageBox.aboutQt(self, translations.TR_ABOUT_QT) def show_about_ninja(self): """Show About NINJA-IDE Dialog.""" about = about_ninja.AboutNinja(self) about.show()
class SingleInstance: def __init__(self, application: QtApplication, files_to_open: Optional[List[str]]) -> None: self._application = application self._files_to_open = files_to_open self._single_instance_server = None # Starts a client that checks for a single instance server and sends the files that need to opened if the server # exists. Returns True if the single instance server is found, otherwise False. def startClient(self) -> bool: Logger.log("i", "Checking for the presence of an ready running Cura instance.") single_instance_socket = QLocalSocket(self._application) Logger.log("d", "Full single instance server name: %s", single_instance_socket.fullServerName()) single_instance_socket.connectToServer("ultimaker-cura") single_instance_socket.waitForConnected(msecs = 3000) # wait for 3 seconds if single_instance_socket.state() != QLocalSocket.ConnectedState: return False # We only send the files that need to be opened. if not self._files_to_open: Logger.log("i", "No file need to be opened, do nothing.") return True if single_instance_socket.state() == QLocalSocket.ConnectedState: Logger.log("i", "Connection has been made to the single-instance Cura socket.") # Protocol is one line of JSON terminated with a carriage return. # "command" field is required and holds the name of the command to execute. # Other fields depend on the command. payload = {"command": "clear-all"} single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii")) payload = {"command": "focus"} single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii")) for filename in self._files_to_open: payload = {"command": "open", "filePath": os.path.abspath(filename)} single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii")) payload = {"command": "close-connection"} single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii")) single_instance_socket.flush() single_instance_socket.waitForDisconnected() return True def startServer(self) -> None: self._single_instance_server = QLocalServer() if self._single_instance_server: self._single_instance_server.newConnection.connect(self._onClientConnected) self._single_instance_server.listen("ultimaker-cura") else: Logger.log("e", "Single instance server was not created.") def _onClientConnected(self) -> None: Logger.log("i", "New connection recevied on our single-instance server") connection = None #type: Optional[QLocalSocket] if self._single_instance_server: connection = self._single_instance_server.nextPendingConnection() if connection is not None: connection.readyRead.connect(lambda c = connection: self.__readCommands(c)) def __readCommands(self, connection: QLocalSocket) -> None: line = connection.readLine() while len(line) != 0: # There is also a .canReadLine() try: payload = json.loads(str(line, encoding = "ascii").strip()) command = payload["command"] # Command: Remove all models from the build plate. if command == "clear-all": self._application.callLater(lambda: self._application.deleteAll()) # Command: Load a model file elif command == "open": self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f)) # Command: Activate the window and bring it to the top. elif command == "focus": # Operating systems these days prevent windows from moving around by themselves. # 'alert' or flashing the icon in the taskbar is the best thing we do now. main_window = self._application.getMainWindow() if main_window is not None: self._application.callLater(lambda: main_window.alert(0)) # type: ignore # I don't know why MyPy complains here # Command: Close the socket connection. We're done. elif command == "close-connection": connection.close() else: Logger.log("w", "Received an unrecognized command " + str(command)) except json.decoder.JSONDecodeError as ex: Logger.log("w", "Unable to parse JSON command '%s': %s", line, repr(ex)) line = connection.readLine()
class _s_IDE(QMainWindow): ############################################################################### # SIGNALS # # goingDown() ############################################################################### goingDown = pyqtSignal() def __init__(self, start_server=False): super(_s_IDE, self).__init__() self.setWindowTitle('NINJA-IDE {Ninja-IDE Is Not Just Another IDE}') self.setMinimumSize(700, 500) #Load the size and the position of the main window self.load_window_geometry() self.__project_to_open = 0 #Start server if needed self.s_listener = None if start_server: self.s_listener = QLocalServer() self.s_listener.listen("ninja_ide") self.s_listener.newConnection.connect(self._process_connection) #Profile handler self.profile = None #Opacity self.opacity = settings.MAX_OPACITY #Define Actions object before the UI self.actions = actions.Actions() #StatusBar self.status = status_bar.StatusBar(self) self.status.hide() self.setStatusBar(self.status) #Main Widget - Create first than everything else self.central = central_widget.CentralWidget(self) self.load_ui(self.central) self.setCentralWidget(self.central) #ToolBar self.toolbar = QToolBar(self) self.toolbar.setToolTip(_translate("_s_IDE", "Press and Drag to Move")) self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly) self.addToolBar(settings.TOOLBAR_AREA, self.toolbar) if settings.HIDE_TOOLBAR: self.toolbar.hide() #Install Shortcuts after the UI has been initialized self.actions.install_shortcuts(self) self.mainContainer.currentTabChanged[str].connect(self.actions.update_explorer) #Menu menubar = self.menuBar() file_ = menubar.addMenu(_translate("_s_IDE", "&File")) edit = menubar.addMenu(_translate("_s_IDE", "&Edit")) view = menubar.addMenu(_translate("_s_IDE", "&View")) source = menubar.addMenu(_translate("_s_IDE", "&Source")) project = menubar.addMenu(_translate("_s_IDE", "&Project")) self.pluginsMenu = menubar.addMenu(_translate("_s_IDE", "&Addins")) about = menubar.addMenu(_translate("_s_IDE", "Abou&t")) #The order of the icons in the toolbar is defined by this calls self._menuFile = menu_file.MenuFile(file_, self.toolbar, self) self._menuView = menu_view.MenuView(view, self.toolbar, self) self._menuEdit = menu_edit.MenuEdit(edit, self.toolbar) self._menuSource = menu_source.MenuSource(source) self._menuProject = menu_project.MenuProject(project, self.toolbar) self._menuPlugins = menu_plugins.MenuPlugins(self.pluginsMenu) self._menuAbout = menu_about.MenuAbout(about) self.load_toolbar() #Plugin Manager services = { 'editor': plugin_services.MainService(), 'toolbar': plugin_services.ToolbarService(self.toolbar), 'menuApp': plugin_services.MenuAppService(self.pluginsMenu), 'explorer': plugin_services.ExplorerService(), 'misc': plugin_services.MiscContainerService(self.misc)} serviceLocator = plugin_manager.ServiceLocator(services) self.plugin_manager = plugin_manager.PluginManager(resources.PLUGINS, serviceLocator) self.plugin_manager.discover() #load all plugins! self.plugin_manager.load_all() #Tray Icon self.trayIcon = updates.TrayIconUpdates(self) self.trayIcon.show() self._menuFile.openFile[str].connect(self.mainContainer.open_file) self.mainContainer.fileSaved[str].connect(self.show_status_message) self.mainContainer.recentTabsModified[list].connect(self._menuFile.update_recent_files)#'QStringList' self.mainContainer.currentTabChanged[str].connect(self.actions.update_migration_tips) self.mainContainer.updateFileMetadata.connect(self.actions.update_migration_tips) self.mainContainer.migrationAnalyzed.connect(self.actions.update_migration_tips) def _process_connection(self): connection = self.s_listener.nextPendingConnection() connection.waitForReadyRead() data = connection.readAll() connection.close() if data: files, projects = str(data).split(ipc.project_delimiter, 1) files = [(x.split(':')[0], int(x.split(':')[1])) for x in files.split(ipc.file_delimiter)] projects = projects.split(ipc.project_delimiter) self.load_session_files_projects(files, [], projects, None) def load_toolbar(self): self.toolbar.clear() toolbar_items = {} toolbar_items.update(self._menuFile.toolbar_items) toolbar_items.update(self._menuView.toolbar_items) toolbar_items.update(self._menuEdit.toolbar_items) toolbar_items.update(self._menuSource.toolbar_items) toolbar_items.update(self._menuProject.toolbar_items) for item in settings.TOOLBAR_ITEMS: if item == 'separator': self.toolbar.addSeparator() else: tool_item = toolbar_items.get(item, None) if tool_item is not None: self.toolbar.addAction(tool_item) #load action added by plugins, This is a special case when reload #the toolbar after save the preferences widget for toolbar_action in settings.get_toolbar_item_for_plugins(): self.toolbar.addAction(toolbar_action) def load_external_plugins(self, paths): for path in paths: self.plugin_manager.add_plugin_dir(path) #load all plugins! self.plugin_manager.discover() self.plugin_manager.load_all() def show_status_message(self, message): self.status.showMessage(message, 2000) def load_ui(self, centralWidget): #Set Application Font for ToolTips QToolTip.setFont(QFont(settings.FONT_FAMILY, 10)) #Create Main Container to manage Tabs self.mainContainer = main_container.MainContainer(self) self.mainContainer.currentTabChanged[str].connect(self.change_window_title) self.mainContainer.locateFunction[str, str, bool].connect(self.actions.locate_function) self.mainContainer.navigateCode[bool, int].connect(self.actions.navigate_code_history) self.mainContainer.addBackItemNavigation.connect(self.actions.add_back_item_navigation) self.mainContainer.updateFileMetadata.connect(self.actions.update_explorer) self.mainContainer.updateLocator[str].connect(self.actions.update_explorer) self.mainContainer.openPreferences.connect(self._show_preferences) self.mainContainer.dontOpenStartPage.connect(self._dont_show_start_page_again) self.mainContainer.currentTabChanged[str].connect(self.status.handle_tab_changed) # When close the last tab cleanup self.mainContainer.allTabsClosed.connect(self._last_tab_closed) # Update symbols self.mainContainer.updateLocator[str].connect(self.status.explore_file_code) #Create Explorer Panel self.explorer = explorer_container.ExplorerContainer(self) self.central.splitterCentralRotated.connect(self.explorer.rotate_tab_position) self.explorer.updateLocator.connect(self.status.explore_code) self.explorer.goToDefinition[int].connect(self.actions.editor_go_to_line) self.explorer.projectClosed[str].connect(self.actions.close_files_from_project) #Create Misc Bottom Container self.misc = misc_container.MiscContainer(self) self.mainContainer.findOcurrences[str].connect(self.misc.show_find_occurrences) centralWidget.insert_central_container(self.mainContainer) centralWidget.insert_lateral_container(self.explorer) centralWidget.insert_bottom_container(self.misc) if self.explorer.count() == 0: centralWidget.change_explorer_visibility(force_hide=True) self.mainContainer.cursorPositionChange[int, int].connect(self.central.lateralPanel.update_line_col) # TODO: Change current symbol on move #self.connect(self.mainContainer, #SIGNAL("cursorPositionChange(int, int)"), #self.explorer.update_current_symbol) self.mainContainer.enabledFollowMode[bool].connect(self.central.enable_follow_mode_scrollbar) if settings.SHOW_START_PAGE: self.mainContainer.show_start_page() def _last_tab_closed(self): """ Called when the last tasb is closed """ self.explorer.cleanup_tabs() def _show_preferences(self): pref = preferences.PreferencesWidget(self.mainContainer) pref.show() def _dont_show_start_page_again(self): settings.SHOW_START_PAGE = False qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) qsettings.beginGroup('preferences') qsettings.beginGroup('general') qsettings.setValue('showStartPage', settings.SHOW_START_PAGE) qsettings.endGroup() qsettings.endGroup() self.mainContainer.actualTab.close_tab() def load_session_files_projects(self, filesTab1, filesTab2, projects, current_file, recent_files=None): self.__project_to_open = len(projects) self.explorer.projectOpened[str].connect(self._set_editors_project_data) self.explorer.open_session_projects(projects, notIDEStart=False) self.mainContainer.open_files(filesTab1, notIDEStart=False) self.mainContainer.open_files(filesTab2, mainTab=False, notIDEStart=False) if current_file: self.mainContainer.open_file(current_file, notStart=False) if recent_files is not None: self._menuFile.update_recent_files(recent_files) def _set_editors_project_data(self): self.__project_to_open -= 1 if self.__project_to_open == 0: self.explorer.projectOpened[str].disconnect(self._set_editors_project_data) self.mainContainer.update_editor_project() def open_file(self, filename): if filename: self.mainContainer.open_file(filename) def open_project(self, project): if project: self.actions.open_project(project) def __get_profile(self): return self.profile def __set_profile(self, profileName): self.profile = profileName if self.profile is not None: self.setWindowTitle('NINJA-IDE (PROFILE: %s)' % self.profile) else: self.setWindowTitle( 'NINJA-IDE {Ninja-IDE Is Not Just Another IDE}') Profile = property(__get_profile, __set_profile) def change_window_title(self, title): if self.profile is None: self.setWindowTitle('NINJA-IDE - %s' % title) else: self.setWindowTitle('NINJA-IDE (PROFILE: %s) - %s' % ( self.profile, title)) currentEditor = self.mainContainer.get_actual_editor() if currentEditor is not None: line = currentEditor.textCursor().blockNumber() + 1 col = currentEditor.textCursor().columnNumber() self.central.lateralPanel.update_line_col(line, col) def wheelEvent(self, event): if event.modifiers() == Qt.ShiftModifier: if event.delta() == 120 and self.opacity < settings.MAX_OPACITY: self.opacity += 0.1 elif event.delta() == -120 and self.opacity > settings.MIN_OPACITY: self.opacity -= 0.1 self.setWindowOpacity(self.opacity) event.ignore() else: QMainWindow.wheelEvent(self, event) def save_settings(self): """Save the settings before the application is closed with QSettings. Info saved: Tabs and projects opened, windows state(size and position). """ qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) editor_widget = self.mainContainer.get_actual_editor() current_file = '' if editor_widget is not None: current_file = editor_widget.ID if qsettings.value('preferences/general/loadFiles', True, type=bool): openedFiles = self.mainContainer.get_opened_documents() projects_obj = self.explorer.get_opened_projects() projects = [p.path for p in projects_obj] qsettings.setValue('openFiles/projects', projects) if len(openedFiles) > 0: qsettings.setValue('openFiles/mainTab', openedFiles[0]) if len(openedFiles) == 2: qsettings.setValue('openFiles/secondaryTab', openedFiles[1]) qsettings.setValue('openFiles/currentFile', current_file) qsettings.setValue('openFiles/recentFiles', self.mainContainer._tabMain.get_recent_files_list()) qsettings.setValue('preferences/editor/bookmarks', settings.BOOKMARKS) qsettings.setValue('preferences/editor/breakpoints', settings.BREAKPOINTS) qsettings.setValue('preferences/general/toolbarArea', self.toolBarArea(self.toolbar)) #Save if the windows state is maximixed if(self.isMaximized()): qsettings.setValue("window/maximized", True) else: qsettings.setValue("window/maximized", False) #Save the size and position of the mainwindow qsettings.setValue("window/size", self.size()) qsettings.setValue("window/pos", self.pos()) #Save the size of de splitters qsettings.setValue("window/central/areaSize", self.central.get_area_sizes()) qsettings.setValue("window/central/mainSize", self.central.get_main_sizes()) #Save the toolbar visibility if not self.toolbar.isVisible() and self.menuBar().isVisible(): qsettings.setValue("window/hide_toolbar", True) else: qsettings.setValue("window/hide_toolbar", False) #Save Misc state qsettings.setValue("window/show_misc", self.misc.isVisible()) #Save Profiles if self.profile is not None: self.actions.save_profile(self.profile) else: qsettings.setValue('ide/profiles', settings.PROFILES) def load_window_geometry(self): """Load from QSettings the window size of de Ninja IDE""" qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) if qsettings.value("window/maximized", True, type=bool): self.setWindowState(Qt.WindowMaximized) else: self.resize(qsettings.value("window/size", QSizeF(800, 600).toSize(), type='QSize')) self.move(qsettings.value("window/pos", QPointF(100, 100).toPoint(), type='QPoint')) def closeEvent(self, event): if self.s_listener: self.s_listener.close() if (settings.CONFIRM_EXIT and self.mainContainer.check_for_unsaved_tabs()): unsaved_files = self.mainContainer.get_unsaved_files() txt = '\n'.join(unsaved_files) val = QMessageBox.question(self, _translate("_s_IDE", "Some changes were not saved"), (_translate("_s_IDE", "%s\n\nDo you want to save them?") % txt), QMessageBox.Yes, QMessageBox.No, QMessageBox.Cancel) if val == QMessageBox.Yes: #Saves all open files self.mainContainer.save_all() if val == QMessageBox.Cancel: event.ignore() self.goingDown.emit() self.save_settings() completion_daemon.shutdown_daemon() #close python documentation server (if running) self.mainContainer.close_python_doc() #Shutdown PluginManager self.plugin_manager.shutdown() def notify_plugin_errors(self): errors = self.plugin_manager.errors if errors: plugin_error_dialog = traceback_widget.PluginErrorDialog() for err_tuple in errors: plugin_error_dialog.add_traceback(err_tuple[0], err_tuple[1]) #show the dialog plugin_error_dialog.exec_() def show_python_detection(self): suggested = settings.detect_python_path() if suggested: dialog = python_detect_dialog.PythonDetectDialog(suggested, self) dialog.show()
class QtSingleApplication(QApplication): """ Adapted from https://stackoverflow.com/a/12712362/11038610 Published by Johan Rade under 2-clause BSD license, opensource.org/licenses/BSD-2-Clause """ message_received_event = pyqtSignal(str) def __init__(self, id, *argv): super().__init__(*argv) self._id = id # Is there another instance running? self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._id) self._isRunning = self._outSocket.waitForConnected() if self._isRunning: # Yes, there is. self._outStream = QTextStream(self._outSocket) self._outStream.setCodec('UTF-8') else: # No, there isn't. self._outSocket = None self._outStream = None self._inSocket = None self._inStream = None self._server = QLocalServer() self._server.removeServer(self._id) self._server.listen(self._id) self._server.newConnection.connect(self._onNewConnection) def isRunning(self): return self._isRunning def id(self): return self._id def sendMessage(self, msg): if not self._outStream: return False self._outStream << msg << '\n' self._outStream.flush() return self._outSocket.waitForBytesWritten() def _onNewConnection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._onReadyRead) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QTextStream(self._inSocket) self._inStream.setCodec('UTF-8') self._inSocket.readyRead.connect(self._onReadyRead) def _onReadyRead(self): while True: msg = self._inStream.readLine() if not msg: break self.message_received_event.emit(msg)
class IDE(QMainWindow): """This class is like the Sauron's Ring: One ring to rule them all, One ring to find them, One ring to bring them all and in the darkness bind them. This Class knows all the containers, and its know by all the containers, but the containers don't need to know between each other, in this way we can keep a better api without the need to tie the behaviour between the widgets, and let them just consume the 'actions' they need.""" ############################################################################### # SIGNALS # # goingDown() ############################################################################### __IDESERVICES = {} __IDECONNECTIONS = {} __IDESHORTCUTS = {} __IDEBARCATEGORIES = {} __IDEMENUS = {} __IDETOOLBAR = {} # CONNECTIONS structure: # ({'target': service_name, 'signal_name': string, 'slot': function_obj},) # On modify add: {connected: True} __instance = None __created = False MessageStatusChanged = pyqtSignal(str) goingDown = pyqtSignal() # # ns_preferences_editor_font = pyqtSignal() # ns_preferences_editor_showTabsAndSpaces = pyqtSignal() # ns_preferences_editor_showIndentationGuide = pyqtSignal() # ns_preferences_editor_indent = pyqtSignal() # ns_preferences_editor_marginLine = pyqtSignal()#podría tener un argumento # ns_preferences_editor_showLineNumbers = pyqtSignal() # ns_preferences_editor_showMigrationTips = pyqtSignal() # ns_preferences_editor_checkStyle = pyqtSignal() # ns_preferences_editor_errors = pyqtSignal() # ds_lastSession_projects = pyqtSignal() # ds_lastSession_openedFiles = pyqtSignal() # ds_lastSession_currentFile = pyqtSignal() # ds_lastSession_recentFiles = pyqtSignal() # ns_preferences_editor_bookmarks = pyqtSignal() # ns_preferences_editor_breakpoints = pyqtSignal() # ns_window_maximized = pyqtSignal() # ns_preferences_general_loadFiles = pyqtSignal() # ns_preferences_general_activatePlugins = pyqtSignal() # ns_preferences_general_notifyUpdates = pyqtSignal() # ns_preferences_general_showStartPage = pyqtSignal(bool) # ns_preferences_general_confirmExit = pyqtSignal(bool) # ns_preferences_general_workspace = pyqtSignal() ns_preferences_general_supportedExtensions = pyqtSignal("QStringList") #ns_preferences_general_notification_position = pyqtSignal(int) #... ns_preferences_general_loadFiles = pyqtSignal(bool)# dato: 'True' ns_preferences_general_activatePlugins = pyqtSignal(bool)# dato: 'True' ns_preferences_general_notifyUpdates = pyqtSignal(bool)# dato: 'True' ns_preferences_general_showStartPage = pyqtSignal(bool)# dato: 'True' ns_preferences_general_confirmExit = pyqtSignal(bool)# dato: 'True' ns_preferences_general_workspace = pyqtSignal(str)# dato: '' #ns_preferences_general_supportedExtensions = pyqtSignal(list)# dato: '['.py', '.pyw', '.html', '.jpg','.png', '.ui', '.css', '.json', '.js', '.ini']' ns_preferences_general_notification_position = pyqtSignal(int)# dato: '0' ns_preferences_general_notification_color = pyqtSignal(str)# dato: '#000' ns_pythonPath = pyqtSignal(str)# dato: 'D:\Python34\python.exe' ns_executionOptions = pyqtSignal(str)# dato: '' ns_Show_Code_Nav = pyqtSignal(str)# dato: 'Ctrl+3' ns_Follow_mode = pyqtSignal(str)# dato: 'Ctrl+F10' ns_Change_Tab = pyqtSignal(str)# dato: 'Ctrl+PgDown' ns_Change_Tab_Reverse = pyqtSignal(str)# dato: 'Ctrl+PgUp' ns_Close_file = pyqtSignal(str)# dato: 'Ctrl+W' ns_Close_Split = pyqtSignal(str)# dato: 'Shift+F9' ns_Comment = pyqtSignal(str)# dato: 'Ctrl+G' ns_Complete_Declarations = pyqtSignal(str)# dato: 'Alt+Return' ns_copy = pyqtSignal(str)# dato: 'Ctrl+C' ns_History_Copy = pyqtSignal(str)# dato: 'Ctrl+Alt+C' ns_New_project = pyqtSignal(str)# dato: 'Ctrl+Shift+N' ns_New_file = pyqtSignal(str)# dato: 'Ctrl+N' ns_cut = pyqtSignal(str)# dato: 'Ctrl+X' ns_Debug = pyqtSignal(str)# dato: 'F7' ns_Duplicate = pyqtSignal(str)# dato: 'Ctrl+R' ns_Run_file = pyqtSignal(str)# dato: 'Ctrl+F6' ns_Run_project = pyqtSignal(str)# dato: 'F6' ns_expand_file_combo = pyqtSignal(str)# dato: 'Ctrl+Tab' ns_expand_symbol_combo = pyqtSignal(str)# dato: 'Ctrl+2' ns_Find = pyqtSignal(str)# dato: 'Ctrl+F' ns_Find_replace = pyqtSignal(str)# dato: 'Ctrl+H' ns_Find_in_files = pyqtSignal(str)# dato: 'Ctrl+L' ns_Find_next = pyqtSignal(str)# dato: 'Ctrl+F3' ns_Find_previous = pyqtSignal(str)# dato: 'Shift+F3' ns_Find_with_word = pyqtSignal(str)# dato: 'Ctrl+Shift+F' ns_Full_screen = pyqtSignal(str)# dato: 'Ctrl+F11' ns_Go_to_definition = pyqtSignal(str)# dato: 'Ctrl+Return' ns_Hide_all = pyqtSignal(str)# dato: 'F11' ns_Hide_editor = pyqtSignal(str)# dato: 'F3' ns_Hide_explorer = pyqtSignal(str)# dato: 'F2' ns_Hide_misc = pyqtSignal(str)# dato: 'F4' ns_Highlight_Word = pyqtSignal(str)# dato: 'Ctrl+Down' ns_Import = pyqtSignal(str)# dato: 'Ctrl+I' ns_Indent_less = pyqtSignal(str)# dato: 'Shift+Tab' ns_Indent_more = pyqtSignal(str)# dato: 'Tab' ns_Add_Bookmark_or_Breakpoint = pyqtSignal(str)# dato: 'Ctrl+B' ns_Title_comment = pyqtSignal(str)# dato: '' ns_Horizontal_line = pyqtSignal(str)# dato: '' ns_Move_down = pyqtSignal(str)# dato: 'Alt+Down' ns_Move_up = pyqtSignal(str)# dato: 'Alt+Up' ns_Move_Tab_to_left = pyqtSignal(str)# dato: 'Ctrl+Shift+9' ns_Move_Tab_to_right = pyqtSignal(str)# dato: 'Ctrl+Shift+0' ns_Navigate_back = pyqtSignal(str)# dato: 'Alt+Left' ns_Navigate_forward = pyqtSignal(str)# dato: 'Alt+Right' ns_Open_file = pyqtSignal(str)# dato: 'Ctrl+O' ns_Open_project = pyqtSignal(str)# dato: 'Ctrl+Shift+O' ns_Open_recent_closed = pyqtSignal(str)# dato: 'Ctrl+Shift+T' ns_paste = pyqtSignal(str)# dato: 'Ctrl+V' ns_History_Paste = pyqtSignal(str)# dato: 'Ctrl+Alt+V' ns_Print_file = pyqtSignal(str)# dato: 'Ctrl+P' ns_Redo = pyqtSignal(str)# dato: 'Ctrl+Y' ns_Reload_file = pyqtSignal(str)# dato: 'F5' ns_Remove_line = pyqtSignal(str)# dato: 'Ctrl+E' ns_Save_file = pyqtSignal(str)# dato: 'Ctrl+S' ns_Save_project = pyqtSignal(str)# dato: 'Ctrl+Shift+S' ns_Code_locator = pyqtSignal(str)# dato: 'Ctrl+K' ns_Show_Paste_History = pyqtSignal(str)# dato: 'Ctrl+4' ns_File_Opener = pyqtSignal(str)# dato: 'Ctrl+Alt+O' ns_Help = pyqtSignal(str)# dato: 'F1' ns_Show_Selector = pyqtSignal(str)# dato: 'Ctrl+`' ns_Split_assistance = pyqtSignal(str)# dato: 'F10' ns_change_tab_visibility = pyqtSignal(str)# dato: 'Shift+F1' ns_Split_horizontal = pyqtSignal(str)# dato: 'F9' ns_Split_vertical = pyqtSignal(str)# dato: 'Ctrl+F9' ns_Stop_execution = pyqtSignal(str)# dato: 'Ctrl+Shift+F6' ns_Uncomment = pyqtSignal(str)# dato: 'Ctrl+Shift+G' ns_undo = pyqtSignal(str)# dato: 'Ctrl+Z' ns_preferences_interface_showProjectExplorer = pyqtSignal(bool)# dato: 'True' ns_preferences_interface_showSymbolsList = pyqtSignal(bool)# dato: 'True' ns_preferences_interface_showWebInspector = pyqtSignal(bool)# dato: 'False' ns_preferences_interface_showErrorsList = pyqtSignal(bool)# dato: 'True' ns_preferences_interface_showMigrationList = pyqtSignal(bool)# dato: 'True' ns_preferences_interface_language = pyqtSignal(str)# dato: 'English' ns_preferences_editor_font = pyqtSignal(QFont)# dato: '<PyQt5.QtGui.QFont object at 0x089D32F0>' ns_preferences_editor_minimapMaxOpacity = pyqtSignal(float)# dato: '0.8' ns_preferences_editor_minimapMinOpacity = pyqtSignal(float)# dato: '0.1' ns_preferences_editor_minimapSizeProportion = pyqtSignal(float)# dato: '0.17' ns_preferences_editor_minimapShow = pyqtSignal(bool)# dato: 'False' ns_preferences_editor_scheme = pyqtSignal(str)# dato: 'default' ns_preferences_editor_useTabs = pyqtSignal(bool)# dato: 'False' ns_preferences_editor_marginLine = pyqtSignal(int)# dato: '80' ns_preferences_editor_showMarginLine = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_indent = pyqtSignal(int)# dato: '4' ns_preferences_editor_platformEndOfLine = pyqtSignal(bool)# dato: 'False' ns_preferences_editor_errorsUnderlineBackground = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_errors = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_errorsInLine = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_checkStyle = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_showMigrationTips = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_checkStyleInline = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_centerOnScroll = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_removeTrailingSpaces = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_allowWordWrap = pyqtSignal(bool)# dato: 'False' ns_preferences_editor_showTabsAndSpaces = pyqtSignal(bool)# dato: 'False' ns_preferences_editor_showIndentationGuide = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_checkForDocstrings = pyqtSignal(bool)# dato: 'False' ns_preferences_editor_showLineNumbers = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_parentheses = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_brackets = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_keys = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_simpleQuotes = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_doubleQuotes = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_codeCompletion = pyqtSignal(bool)# dato: 'True' ns_preferences_editor_completeDeclarations = pyqtSignal(bool)# dato: 'True' ns_preferences_theme_skin = pyqtSignal(str)# dato: 'Default' ds_lastSession_projects = pyqtSignal(list)# dato: '[]' ds_lastSession_openedFiles = pyqtSignal(list)# dato: '[]' ds_lastSession_currentFile = pyqtSignal(str)# dato: '' ds_lastSession_recentFiles = pyqtSignal(list)# dato: '[]' ns_preferences_editor_bookmarks = pyqtSignal(dict)# dato: '{}' ns_preferences_editor_breakpoints = pyqtSignal(dict)# dato: '{}' ns_window_maximized = pyqtSignal(bool)# dato: 'True' ns_window_central_baseSplitterSize = pyqtSignal(QByteArray)# dato: 'b'\x00\x00\x00\xff\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x03\x84\x00\x00\x00\xc8\x01\xff\xff\xff\xff\x01\x00\x00\x00\x01\x01'' ns_window_central_insideSplitterSize = pyqtSignal(QByteArray)# dato: 'b'\x00\x00\x00\xff\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x01B\x00\x00\x00\xa8\x01\xff\xff\xff\xff\x01\x00\x00\x00\x02\x01'' ns_window_central_lateralVisible = pyqtSignal(bool)# dato: 'True' ns_window_hide_toolbar = pyqtSignal(bool)# dato: 'False' ns_tools_dock_visible = pyqtSignal(bool)# dato: 'True' #... ds_recentProjects = pyqtSignal(dict) ns_window_size = pyqtSignal(QSize) ns_window_pos = pyqtSignal(QPoint) def __init__(self, start_server=False): super(IDE, self).__init__() self.setWindowTitle('NINJA-IDE {Ninja-IDE Is Not Just Another IDE}') self.setMinimumSize(750, 500) QToolTip.setFont(QFont(settings.FONT.family(), 10)) #Load the size and the position of the main window self.load_window_geometry() self.__project_to_open = 0 IDE.__instance = self wid = QWidget()#adjustSize wid.setContentsMargins(0, 0, 0, 0) box = QHBoxLayout(wid) box.setContentsMargins(0, 0, 0, 0) # l1 = QLabel("Info Here") # l1.setObjectName("Info") # l1.setStyleSheet("background-color: rgb(88, 255, 85);") # box.addWidget(l1) space = QSpacerItem(10,10, QSizePolicy.Expanding)#, QSizePolicy.Maximum) box.addSpacerItem(space) l2 = QLabel("Tab Size: "+str(settings.INDENT))#int(qsettings.value('preferences/editor/indent', 4, type=int)))) l2.setObjectName("Det1") font = l2.font() font.setPointSize(8) l2.setFont(font) box.addWidget(l2) box.addSpacing(50) l3 = QLabel("Python") l3.setObjectName("Det2") font.setPointSize(9) l3.setFont(font) box.addWidget(l3) box.addSpacing(30) status = self.statusBar() status.setMaximumHeight(20) status.addPermanentWidget(wid) # wid.show() # self.__wid = wid status.reformat() status.showMessage("Info Here") status.setStyleSheet("background-color: rgb(85, 85, 85);") #Editables self.__neditables = {} #Filesystem self.filesystem = nfilesystem.NVirtualFileSystem() #Sessions handler self._session = None #Opacity self.opacity = settings.MAX_OPACITY #ToolBar self.toolbar = QToolBar(self) if settings.IS_MAC_OS: self.toolbar.setIconSize(QSize(36, 36)) else: self.toolbar.setIconSize(QSize(24, 24)) self.toolbar.setToolTip(translations.TR_IDE_TOOLBAR_TOOLTIP) self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly) # Set toggleViewAction text and tooltip self.toggleView = self.toolbar.toggleViewAction() self.toggleView.setText(translations.TR_TOOLBAR_VISIBILITY) self.toggleView.setToolTip(translations.TR_TOOLBAR_VISIBILITY) self.addToolBar(settings.TOOLBAR_AREA, self.toolbar) if settings.HIDE_TOOLBAR: self.toolbar.hide() #Notificator self.notification = notification.Notification(self) self.statusBar().messageChanged[str].connect(self.MessageStatusChanged.emit) #Plugin Manager # CHECK ACTIVATE PLUGINS SETTING #services = { #'editor': plugin_services.MainService(), #'toolbar': plugin_services.ToolbarService(self.toolbar), ##'menuApp': plugin_services.MenuAppService(self.pluginsMenu), #'menuApp': plugin_services.MenuAppService(None), #'explorer': plugin_services.ExplorerService(), #'misc': plugin_services.MiscContainerService(self.misc)} #serviceLocator = plugin_manager.ServiceLocator(services) serviceLocator = plugin_manager.ServiceLocator(None) self.plugin_manager = plugin_manager.PluginManager(resources.PLUGINS, serviceLocator) self.plugin_manager.discover() #load all plugins! self.plugin_manager.load_all() #Tray Icon self.trayIcon = updates.TrayIconUpdates(self) self.trayIcon.closeTrayIcon.connect(self._close_tray_icon) self.trayIcon.show() key = Qt.Key_1 for i in range(10): if settings.IS_MAC_OS: short = ui_tools.TabShortcuts( QKeySequence(Qt.CTRL + Qt.ALT + key), self, i) else: short = ui_tools.TabShortcuts( QKeySequence(Qt.ALT + key), self, i) key += 1 short.activated.connect(self._change_tab_index) short = ui_tools.TabShortcuts(QKeySequence(Qt.ALT + Qt.Key_0), self, 10) short.activated.connect(self._change_tab_index) # Register menu categories IDE.register_bar_category(translations.TR_MENU_FILE, 100) IDE.register_bar_category(translations.TR_MENU_EDIT, 110) IDE.register_bar_category(translations.TR_MENU_VIEW, 120) IDE.register_bar_category(translations.TR_MENU_SOURCE, 130) IDE.register_bar_category(translations.TR_MENU_PROJECT, 140) IDE.register_bar_category(translations.TR_MENU_EXTENSIONS, 150) IDE.register_bar_category(translations.TR_MENU_ABOUT, 160) # Register General Menu Items ui_tools.install_shortcuts(self, actions.ACTIONS_GENERAL, self) self.register_service('ide', self) self.register_service('toolbar', self.toolbar) self.register_service('filesystem', self.filesystem) #Register signals connections connections = ( {'target': 'main_container', 'signal_name': 'fileSaved',#(QString) 'slot': self.show_message}, {'target': 'main_container', 'signal_name': 'currentEditorChanged',#(QString) 'slot': self.change_window_title}, {'target': 'main_container', 'signal_name': 'openPreferences',#() 'slot': self.show_preferences}, {'target': 'main_container', 'signal_name': 'allTabsClosed',#() 'slot': self._last_tab_closed}, {'target': 'explorer_container', 'signal_name': 'changeWindowTitle',#(QString) 'slot': self.change_window_title}, {'target': 'explorer_container', 'signal_name': 'projectClosed',#(QString) 'slot': self.close_project}, ) self.register_signals('ide', connections) # Central Widget MUST always exists self.central = IDE.get_service('central_container') print("self.central:", self.central) self.setCentralWidget(self.central) # Install Services for service_name in self.__IDESERVICES: self.install_service(service_name) IDE.__created = True # Place Status Bar main_container = IDE.get_service('main_container') status_bar = IDE.get_service('status_bar') main_container.add_status_bar(status_bar) # Load Menu Bar menu_bar = IDE.get_service('menu_bar') if menu_bar: menu_bar.load_menu(self) #These two are the same service, I think that's ok menu_bar.load_toolbar(self) #Start server if needed self.s_listener = None if start_server: self.s_listener = QLocalServer() self.s_listener.listen("ninja_ide") self.s_listener.newConnection.connect(self._process_connection) @classmethod def hasCreated(clss): return clss.__created @classmethod def getInstance(clss): return clss.__instance @classmethod def get_service(cls, service_name): """Return the instance of a registered service.""" return cls.__IDESERVICES.get(service_name, None) def get_menuitems(self): """Return a dictionary with the registered menu items.""" return IDE.__IDEMENUS def get_bar_categories(self): """Get the registered Categories for the Application menus.""" return IDE.__IDEBARCATEGORIES def get_toolbaritems(self): """Return a dictionary with the registered menu items.""" return IDE.__IDETOOLBAR @classmethod def register_service(cls, service_name, obj): """Register a service providing the service name and the instance.""" cls.__IDESERVICES[service_name] = obj if cls.hasCreated(): cls.getInstance().install_service(service_name) def install_service(self, service_name): """Activate the registered service.""" obj = IDE.__IDESERVICES.get(service_name, None) func = getattr(obj, 'install', None) if isinstance(func, collections.Callable): func() self._connect_signals() def place_me_on(self, name, obj, region="central", top=False): """Place a widget in some of the areas in the IDE. @name: id to access to that widget later if needed. @obj: the instance of the widget to be placed. @region: the area where to put the widget [central, lateral] @top: place the widget as the first item in the split.""" self.central.add_to_region(name, obj, region, top) @classmethod def register_signals(cls, service_name, connections): """Register all the signals that a particular service wants to be attached of. @service_name: id of the service @connections: list of dictionaries for the connection with: - 'target': 'the_other_service_name', - 'signal_name': 'name of the signal in the other service', - 'slot': function object in this service""" cls.__IDECONNECTIONS[service_name] = connections if cls.hasCreated(): cls.getInstance()._connect_signals() def _connect_signals(self): """Connect the signals between the different services.""" for service_name in IDE.__IDECONNECTIONS: connections = IDE.__IDECONNECTIONS[service_name] for connection in connections: if connection.get('connected', False): continue target = IDE.__IDESERVICES.get( connection['target'], None) slot = connection['slot'] signal_name = connection['signal_name'] if target and isinstance(slot, collections.Callable): getattr(target, signal_name).connect(slot) connection['connected'] = True @classmethod def register_shortcut(cls, shortcut_name, shortcut, action=None): """Register a shortcut and action.""" cls.__IDESHORTCUTS[shortcut_name] = (shortcut, action) @classmethod def register_menuitem(cls, menu_action, section, weight): """Register a QAction or QMenu in the IDE to be loaded later in the menubar using the section(string) to define where is going to be contained, and the weight define the order where is going to be placed. @menu_action: QAction or QMenu @section: String (name) @weight: int""" cls.__IDEMENUS[menu_action] = (section, weight) @classmethod def register_toolbar(cls, action, section, weight): """Register a QAction in the IDE to be loaded later in the toolbar using the section(string) to define where is going to be contained, and the weight define the order where is going to be placed. @action: QAction @section: String (name) @weight: int""" cls.__IDETOOLBAR[action] = (section, weight) @classmethod def register_bar_category(cls, category_name, weight): """Register a Menu Category to be created with the proper weight. @category_name: string @weight: int""" cls.__IDEBARCATEGORIES[category_name] = weight @classmethod def update_shortcut(cls, shortcut_name): """Update all the shortcuts of the application.""" short = resources.get_shortcut shortcut, action = cls.__IDESHORTCUTS.get(shortcut_name) if shortcut: shortcut.setKey(short(shortcut_name)) if action: action.setShortcut(short(shortcut_name)) def get_or_create_nfile(self, filename): """For convenience access to files from ide""" return self.filesystem.get_file(nfile_path=filename) def get_or_create_editable(self, filename="", nfile=None): if nfile is None: nfile = self.filesystem.get_file(nfile_path=filename) editable = self.__neditables.get(nfile) if editable is None: editable = neditable.NEditable(nfile) editable.fileClosing.connect(self._unload_neditable) self.__neditables[nfile] = editable return editable def _unload_neditable(self, editable): self.__neditables.pop(editable.nfile) editable.nfile.deleteLater() editable.editor.deleteLater() editable.deleteLater() @property def opened_files(self): return tuple(self.__neditables.keys()) def get_project_for_file(self, filename): project = None if filename: project = self.filesystem.get_project_for_file(filename) return project def create_project(self, path): nproj = nproject.NProject(path) self.filesystem.open_project(nproj) return nproj def close_project(self, project_path): self.filesystem.close_project(project_path) def get_projects(self): return self.filesystem.get_projects() def get_current_project(self): current_project = None projects = self.filesystem.get_projects() for project in projects: if projects[project].is_current: current_project = projects[project] break return current_project def showMessageStatus(self, msg): QTimer.singleShot(1, Qt.PreciseTimer, lambda: self.statusBar().showMessage(msg)) # self.statusBar().showMessage(msg) @classmethod def select_current(cls, widget): """Show the widget with a 4px lightblue border line.""" widget.setProperty("highlight", True) widget.style().unpolish(widget) widget.style().polish(widget) @classmethod def unselect_current(cls, widget): """Remove the 4px lightblue border line from the widget.""" widget.setProperty("highlight", False) widget.style().unpolish(widget) widget.style().polish(widget) def _close_tray_icon(self): """Close the System Tray Icon.""" self.trayIcon.hide() self.trayIcon.deleteLater() def _change_tab_index(self): """Change the tabs of the current TabWidget using alt+numbers.""" widget = QApplication.focusWidget() shortcut_index = getattr(widget, 'shortcut_index', None) if shortcut_index: obj = self.sender() shortcut_index(obj.index) def _process_connection(self): """Read the ipc input from another instance of ninja.""" connection = self.s_listener.nextPendingConnection() connection.waitForReadyRead() data = connection.readAll() connection.close() if data: files, projects = str(data).split(ipc.project_delimiter, 1) files = [(x.split(':')[0], int(x.split(':')[1])) for x in files.split(ipc.file_delimiter)] projects = projects.split(ipc.project_delimiter) self.load_session_files_projects(files, [], projects, None) def fullscreen_mode(self): """Change to fullscreen mode.""" if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def change_toolbar_visibility(self): """Switch the toolbar visibility""" if self.toolbar.isVisible(): self.toolbar.hide() else: self.toolbar.show() def load_external_plugins(self, paths): """Load external plugins, the ones added to ninja throw the cmd.""" for path in paths: self.plugin_manager.add_plugin_dir(path) #load all plugins! self.plugin_manager.discover() self.plugin_manager.load_all() def _last_tab_closed(self): """ Called when the last tasb is closed """ self.explorer.cleanup_tabs() def show_preferences(self): """Open the Preferences Dialog.""" pref = preferences.Preferences(self) main_container = IDE.get_service("main_container") print("\n\npreferences!!") if main_container: main_container.show_dialog(pref) print("\n\nmain_container---") else: pref.show() print("\n\nNONE---") def load_session_files_projects(self, files, projects, current_file, recent_files=None): """Load the files and projects from previous session.""" main_container = IDE.get_service('main_container') projects_explorer = IDE.get_service('projects_explorer') if main_container and files: for fileData in files: if file_manager.file_exists(fileData[0]): mtime = os.stat(fileData[0]).st_mtime ignore_checkers = (mtime == fileData[2]) line, col = fileData[1][0], fileData[1][1] main_container.open_file(fileData[0], line, col, ignore_checkers=ignore_checkers) #if current_file: #main_container.open_file(current_file) if projects_explorer and projects: projects_explorer.load_session_projects(projects) #if recent_files is not None: #menu_file = IDE.get_service('menu_file') #menu_file.update_recent_files(recent_files) #def _set_editors_project_data(self): #self.__project_to_open -= 1 #if self.__project_to_open == 0: #self.disconnect(self.explorer, SIGNAL("projectOpened(QString)"), #self._set_editors_project_data) #self.mainContainer.update_editor_project() #def open_file(self, filename): #if filename: #self.mainContainer.open_file(filename) #def open_project(self, project): #if project: #self.actions.open_project(project) def __get_session(self): return self._session def __set_session(self, sessionName): self._session = sessionName if self._session is not None: self.setWindowTitle(translations.TR_SESSION_IDE_HEADER % {'session': self._session}) else: self.setWindowTitle( 'NINJA-IDE {Ninja-IDE Is Not Just Another IDE}') Session = property(__get_session, __set_session) def change_window_title(self, title): """Change the title of the Application.""" if self._session is None: self.setWindowTitle('NINJA-IDE - %s' % title) else: self.setWindowTitle((translations.TR_SESSION_IDE_HEADER % {'session': self._session}) + ' - %s' % title) def wheelEvent(self, event): """Change the opacity of the application.""" if event.modifiers() == Qt.ShiftModifier: if event.delta() == 120 and self.opacity < settings.MAX_OPACITY: self.opacity += 0.1 elif event.delta() == -120 and self.opacity > settings.MIN_OPACITY: self.opacity -= 0.1 self.setWindowOpacity(self.opacity) event.ignore() else: super(IDE, self).wheelEvent(event) @classmethod def ninja_settings(cls): qsettings = nsettings.NSettings(resources.SETTINGS_PATH, prefix="ns") if cls.hasCreated(): qsettings.valueChanged.connect(cls.getInstance()._settings_value_changed) return qsettings @classmethod def data_settings(cls): qsettings = nsettings.NSettings(resources.DATA_SETTINGS_PATH, prefix="ds") if cls.hasCreated(): qsettings.valueChanged.connect(cls.getInstance()._settings_value_changed) return qsettings def _settings_value_changed(self, key, value): # signal_name = "%s(PyQt_PyObject)" % key.replace("/", "_") # self.emit(SIGNAL(signal_name), value) key = key.replace("/", "_").replace("-", "_") try: getattr(self, key).emit(value) except TypeError as reason: print("\n:::", key, value, type(value)) print("\n\nerrors:-:", reason) getattr(self, key).emit() except AttributeError: print("\n:::", key, value, type(value)) # if not value: # try: # getattr(self, key.replace("/", "_")).emit(value) # except TypeError: # getattr(self, key.replace("/", "_")).emit() # return # try: # getattr(self, key.replace("/", "_")).emit(value) # except TypeError as e: # print("\n\nerrors", e) # getattr(self, key.replace("/", "_")).emit() ##getattr(self, key.replace("/", "_").replace("-", "_")).emit(value) def save_settings(self): """Save the settings before the application is closed with QSettings. Info saved: Tabs and projects opened, windows state(size and position). """ qsettings = IDE.ninja_settings() data_qsettings = IDE.data_settings() main_container = self.get_service("main_container") editor_widget = None if main_container: editor_widget = main_container.get_current_editor() current_file = '' if editor_widget is not None: current_file = editor_widget.file_path if qsettings.value('preferences/general/loadFiles', True, type=bool): openedFiles = self.filesystem.get_files() projects_obj = self.filesystem.get_projects() projects = [projects_obj[proj].path for proj in projects_obj] data_qsettings.setValue('lastSession/projects', projects) files_info = [] for path in openedFiles: if not openedFiles[path]._exists(): print("\n\ncontinue", path) continue editable = self.__neditables.get(openedFiles[path]) if editable is not None and editable.is_dirty: stat_value = 0 else: stat_value = os.stat(path).st_mtime files_info.append([path, editable.editor.getCursorPosition(), stat_value]) data_qsettings.setValue('lastSession/openedFiles', files_info) if current_file is not None: data_qsettings.setValue('lastSession/currentFile', current_file) data_qsettings.setValue('lastSession/recentFiles', settings.LAST_OPENED_FILES) qsettings.setValue('preferences/editor/bookmarks', settings.BOOKMARKS) qsettings.setValue('preferences/editor/breakpoints', settings.BREAKPOINTS) # Session if self._session is not None: val = QMessageBox.question( self, translations.TR_SESSION_ACTIVE_IDE_CLOSING_TITLE, (translations.TR_SESSION_ACTIVE_IDE_CLOSING_BODY % {'session': self.Session}), QMessageBox.Yes, QMessageBox.No) if val == QMessageBox.Yes: session_manager.SessionsManager.save_session_data( self.Session, self) #qsettings.setValue('preferences/general/toolbarArea', #self.toolBarArea(self.toolbar)) #Save if the windows state is maximixed if(self.isMaximized()): qsettings.setValue("window/maximized", True) else: qsettings.setValue("window/maximized", False) #Save the size and position of the mainwindow qsettings.setValue("window/size", self.size()) qsettings.setValue("window/pos", self.pos()) self.central.save_configuration() #Save the toolbar visibility qsettings.setValue("window/hide_toolbar", not self.toolbar.isVisible()) #else: #qsettings.setValue("window/hide_toolbar", False) #Save Misc state #qsettings.setValue("window/show_region1", self.misc.isVisible()) #Save Profiles #if self.profile is not None: #self.actions.save_profile(self.profile) #else: #qsettings.setValue('ide/profiles', settings.PROFILES) def activate_profile(self): """Show the Session Manager dialog.""" profilesLoader = session_manager.SessionsManager(self) profilesLoader.show() def deactivate_profile(self): """Close the Session Session.""" self.Session = None def load_window_geometry(self): """Load from QSettings the window size of Ninja IDE""" qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) if qsettings.value("window/maximized", True, type=bool): self.setWindowState(Qt.WindowMaximized) else: self.resize(qsettings.value( "window/size", QSize(800, 600), type='QSize')) self.move(qsettings.value( "window/pos", QPoint(100, 100), type='QPoint')) def _get_unsaved_files(self): """Return an array with the path of the unsaved files.""" unsaved = [] files = self.opened_files for f in files: editable = self.__neditables.get(f) print("\n\neditable::", editable, getattr(editable, "editor", "-")) if editable is not None and editable.editor is not None and editable.editor.is_modified: unsaved.append(f) return unsaved def _save_unsaved_files(self, files): """Save the files from the paths in the array.""" for f in files: editable = self.get_or_create_editable(f) editable.ignore_checkers = True editable.save_content() def closeEvent(self, event): """Saves some global settings before closing.""" if self.s_listener: self.s_listener.close() main_container = self.get_service("main_container") unsaved_files = self._get_unsaved_files() if (settings.CONFIRM_EXIT and unsaved_files): txt = '\n'.join([nfile.file_name for nfile in unsaved_files]) val = QMessageBox.question( self, translations.TR_IDE_CONFIRM_EXIT_TITLE, (translations.TR_IDE_CONFIRM_EXIT_BODY % {'files': txt}), QMessageBox.Yes | QMessageBox.No, QMessageBox.Cancel) if val == QMessageBox.Yes: #Saves all open files self._save_unsaved_files(unsaved_files) if val == QMessageBox.Cancel: event.ignore() return self.save_settings() self.goingDown.emit() #close python documentation server (if running) main_container.close_python_doc() #Shutdown PluginManager self.plugin_manager.shutdown() #completion_daemon.shutdown_daemon() super(IDE, self).closeEvent(event) def notify_plugin_errors(self): #TODO: Check if the Plugin Error dialog can be improved errors = self.plugin_manager.errors if errors: plugin_error_dialog = traceback_widget.PluginErrorDialog() for err_tuple in errors: plugin_error_dialog.add_traceback(err_tuple[0], err_tuple[1]) #show the dialog plugin_error_dialog.exec_() def show_message(self, message, duration=3000): """Show status message.""" self.notification.set_message(message, duration) self.notification.show() def show_plugins_store(self): """Open the Plugins Manager to install/uninstall plugins.""" store = plugins_store.PluginsStore(self) main_container = IDE.get_service("main_container") print("\nshow_plugins_store") if main_container: print("\nshow_plugins_store::main_container") main_container.show_dialog(store) else: store.show() def show_languages(self): """Open the Language Manager to install/uninstall languages.""" manager = language_manager.LanguagesManagerWidget(self) manager.show() def show_schemes(self): """Open the Schemes Manager to install/uninstall schemes.""" manager = schemes_manager.SchemesManagerWidget(self) manager.show() def show_about_qt(self): """Show About Qt Dialog.""" QMessageBox.aboutQt(self, translations.TR_ABOUT_QT) def show_about_ninja(self): """Show About NINJA-IDE Dialog.""" about = about_ninja.AboutNinja(self) about.show() def show_python_detection(self): """Show Python detection dialog for windows.""" #TODO: Notify the user when no python version could be found suggested = settings.detect_python_path() if suggested: dialog = python_detect_dialog.PythonDetectDialog(suggested, self) dialog.show()
class IPCServer(QObject): """IPC server to which clients connect to. Attributes: ignored: Whether requests are ignored (in exception hook). _timer: A timer to handle timeouts. _server: A QLocalServer to accept new connections. _socket: The QLocalSocket we're currently connected to. _socketname: The socketname to use. _socketopts_ok: Set if using setSocketOptions is working with this OS/Qt version. _atime_timer: Timer to update the atime of the socket regularly. Signals: got_args: Emitted when there was an IPC connection and arguments were passed. got_args: Emitted with the raw data an IPC connection got. got_invalid_data: Emitted when there was invalid incoming data. """ got_args = pyqtSignal(list, str, str) got_raw = pyqtSignal(bytes) got_invalid_data = pyqtSignal() def __init__(self, socketname, parent=None): """Start the IPC server and listen to commands. Args: socketname: The socketname to use. parent: The parent to be used. """ super().__init__(parent) self.ignored = False self._socketname = socketname self._timer = usertypes.Timer(self, "ipc-timeout") self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) if os.name == "nt": # pragma: no coverage self._atime_timer = None else: self._atime_timer = usertypes.Timer(self, "ipc-atime") self._atime_timer.setInterval(ATIME_INTERVAL) self._atime_timer.timeout.connect(self.update_atime) self._atime_timer.setTimerType(Qt.VeryCoarseTimer) self._server = QLocalServer(self) self._server.newConnection.connect(self.handle_connection) self._socket = None self._socketopts_ok = os.name == "nt" if self._socketopts_ok: # pragma: no cover # If we use setSocketOptions on Unix with Qt < 5.4, we get a # NameError while listening... log.ipc.debug("Calling setSocketOptions") self._server.setSocketOptions(QLocalServer.UserAccessOption) else: # pragma: no cover log.ipc.debug("Not calling setSocketOptions") def _remove_server(self): """Remove an existing server.""" ok = QLocalServer.removeServer(self._socketname) if not ok: raise Error("Error while removing server {}!".format(self._socketname)) def listen(self): """Start listening on self._socketname.""" log.ipc.debug("Listening as {}".format(self._socketname)) if self._atime_timer is not None: # pragma: no branch self._atime_timer.start() self._remove_server() ok = self._server.listen(self._socketname) if not ok: if self._server.serverError() == QAbstractSocket.AddressInUseError: raise AddressInUseError(self._server) else: raise ListenError(self._server) if not self._socketopts_ok: # pragma: no cover # If we use setSocketOptions on Unix with Qt < 5.4, we get a # NameError while listening. # (see b135569d5c6e68c735ea83f42e4baf51f7972281) # # Also, we don't get an AddressInUseError with Qt 5.5: # https://bugreports.qt.io/browse/QTBUG-48635 # # This means we only use setSocketOption on Windows... os.chmod(self._server.fullServerName(), 0o700) @pyqtSlot(int) def on_error(self, err): """Raise SocketError on fatal errors.""" if self._socket is None: # Sometimes this gets called from stale sockets. log.ipc.debug("In on_error with None socket!") return self._timer.stop() log.ipc.debug("Socket error {}: {}".format(self._socket.error(), self._socket.errorString())) if err != QLocalSocket.PeerClosedError: raise SocketError("handling IPC connection", self._socket) @pyqtSlot() def handle_connection(self): """Handle a new connection to the server.""" if self.ignored: return if self._socket is not None: log.ipc.debug("Got new connection but ignoring it because we're " "still handling another one.") return socket = self._server.nextPendingConnection() if socket is None: log.ipc.debug("No new connection to handle.") return log.ipc.debug("Client connected.") self._timer.start() self._socket = socket socket.readyRead.connect(self.on_ready_read) if socket.canReadLine(): log.ipc.debug("We can read a line immediately.") self.on_ready_read() socket.error.connect(self.on_error) if socket.error() not in (QLocalSocket.UnknownSocketError, QLocalSocket.PeerClosedError): log.ipc.debug("We got an error immediately.") self.on_error(socket.error()) socket.disconnected.connect(self.on_disconnected) if socket.state() == QLocalSocket.UnconnectedState: log.ipc.debug("Socket was disconnected immediately.") self.on_disconnected() @pyqtSlot() def on_disconnected(self): """Clean up socket when the client disconnected.""" log.ipc.debug("Client disconnected.") self._timer.stop() if self._socket is None: log.ipc.debug("In on_disconnected with None socket!") else: self._socket.deleteLater() self._socket = None # Maybe another connection is waiting. self.handle_connection() def _handle_invalid_data(self): """Handle invalid data we got from a QLocalSocket.""" log.ipc.error("Ignoring invalid IPC data.") self.got_invalid_data.emit() self._socket.error.connect(self.on_error) self._socket.disconnectFromServer() @pyqtSlot() def on_ready_read(self): """Read json data from the client.""" if self._socket is None: # This happens when doing a connection while another one is already # active for some reason. log.ipc.warning("In on_ready_read with None socket!") return self._timer.start() while self._socket is not None and self._socket.canReadLine(): data = bytes(self._socket.readLine()) self.got_raw.emit(data) log.ipc.debug("Read from socket: {}".format(data)) try: decoded = data.decode("utf-8") except UnicodeDecodeError: log.ipc.error("invalid utf-8: {}".format(binascii.hexlify(data))) self._handle_invalid_data() return log.ipc.debug("Processing: {}".format(decoded)) try: json_data = json.loads(decoded) except ValueError: log.ipc.error("invalid json: {}".format(decoded.strip())) self._handle_invalid_data() return for name in ("args", "target_arg"): if name not in json_data: log.ipc.error("Missing {}: {}".format(name, decoded.strip())) self._handle_invalid_data() return try: protocol_version = int(json_data["protocol_version"]) except (KeyError, ValueError): log.ipc.error("invalid version: {}".format(decoded.strip())) self._handle_invalid_data() return if protocol_version != PROTOCOL_VERSION: log.ipc.error("incompatible version: expected {}, " "got {}".format(PROTOCOL_VERSION, protocol_version)) self._handle_invalid_data() return cwd = json_data.get("cwd", None) self.got_args.emit(json_data["args"], json_data["target_arg"], cwd) @pyqtSlot() def on_timeout(self): """Cancel the current connection if it was idle for too long.""" log.ipc.error("IPC connection timed out.") self._socket.disconnectFromServer() if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None self._socket.waitForDisconnected(CONNECT_TIMEOUT) if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None self._socket.abort() @pyqtSlot() def update_atime(self): """Update the atime of the socket file all few hours. From the XDG basedir spec: To ensure that your files are not removed, they should have their access time timestamp modified at least once every 6 hours of monotonic time or the 'sticky' bit should be set on the file. """ path = self._server.fullServerName() if not path: log.ipc.error("In update_atime with no server path!") return log.ipc.debug("Touching {}".format(path)) os.utime(path) def shutdown(self): """Shut down the IPC server cleanly.""" log.ipc.debug("Shutting down IPC") if self._socket is not None: self._socket.deleteLater() self._socket = None self._timer.stop() if self._atime_timer is not None: # pragma: no branch self._atime_timer.stop() try: self._atime_timer.timeout.disconnect(self.update_atime) except TypeError: pass self._server.close() self._server.deleteLater() self._remove_server()
class QSingleApplication(QApplication): messageReceived = pyqtSignal(str) def __init__(self, *args, **kwargs): super(QSingleApplication, self).__init__(*args, **kwargs) appid = QApplication.applicationFilePath().lower().split("/")[-1] self._socketName = "qtsingleapp-" + appid print("socketName", self._socketName) self._activationWindow = None self._activateOnMessage = False self._socketServer = None self._socketIn = None self._socketOut = None self._running = False # 先尝试连接 self._socketOut = QLocalSocket(self) self._socketOut.connectToServer(self._socketName) self._socketOut.error.connect(self.handleError) self._running = self._socketOut.waitForConnected() if not self._running: # 程序未运行 self._socketOut.close() del self._socketOut self._socketServer = QLocalServer(self) self._socketServer.listen(self._socketName) self._socketServer.newConnection.connect(self._onNewConnection) self.aboutToQuit.connect(self.removeServer) def handleError(self, message): print("handleError message: ", message) def isRunning(self): return self._running def activationWindow(self): return self._activationWindow def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage def activateWindow(self): if not self._activationWindow: return self._activationWindow.setWindowState( self._activationWindow.windowState() & ~Qt.WindowMinimized) self._activationWindow.raise_() self._activationWindow.activateWindow() def sendMessage(self, message, msecs=5000): if not self._socketOut: return False if not isinstance(message, bytes): message = str(message).encode() self._socketOut.write(message) if not self._socketOut.waitForBytesWritten(msecs): raise RuntimeError("Bytes not written within %ss" % (msecs / 1000.)) return True def _onNewConnection(self): if self._socketIn: self._socketIn.readyRead.disconnect(self._onReadyRead) self._socketIn = self._socketServer.nextPendingConnection() if not self._socketIn: return self._socketIn.readyRead.connect(self._onReadyRead) if self._activateOnMessage: self.activateWindow() def _onReadyRead(self): while 1: message = self._socketIn.readLine() if not message: break print("Message received: ", message) self.messageReceived.emit(message.data().decode()) def removeServer(self): self._socketServer.close() self._socketServer.removeServer(self._socketName)
class IPCServer(QObject): """IPC server to which clients connect to. Attributes: ignored: Whether requests are ignored (in exception hook). _timer: A timer to handle timeouts. _server: A QLocalServer to accept new connections. _socket: The QLocalSocket we're currently connected to. """ def __init__(self, parent=None): """Start the IPC server and listen to commands.""" super().__init__(parent) self.ignored = False self._remove_server() self._timer = usertypes.Timer(self, 'ipc-timeout') self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) self._server = QLocalServer(self) ok = self._server.listen(SOCKETNAME) if not ok: raise IPCError("Error while listening to IPC server: {} " "(error {})".format(self._server.errorString(), self._server.serverError())) self._server.newConnection.connect(self.handle_connection) self._socket = None def _remove_server(self): """Remove an existing server.""" ok = QLocalServer.removeServer(SOCKETNAME) if not ok: raise IPCError("Error while removing server {}!".format( SOCKETNAME)) @pyqtSlot(int) def on_error(self, error): """Convenience method which calls _socket_error on an error.""" self._timer.stop() log.ipc.debug("Socket error {}: {}".format( self._socket.error(), self._socket.errorString())) if error != QLocalSocket.PeerClosedError: _socket_error("handling IPC connection", self._socket) @pyqtSlot() def handle_connection(self): """Handle a new connection to the server.""" if self.ignored: return if self._socket is not None: log.ipc.debug("Got new connection but ignoring it because we're " "still handling another one.") return socket = self._server.nextPendingConnection() if socket is None: log.ipc.debug("No new connection to handle.") return log.ipc.debug("Client connected.") self._timer.start() self._socket = socket socket.readyRead.connect(self.on_ready_read) if socket.canReadLine(): log.ipc.debug("We can read a line immediately.") self.on_ready_read() socket.error.connect(self.on_error) if socket.error() not in (QLocalSocket.UnknownSocketError, QLocalSocket.PeerClosedError): log.ipc.debug("We got an error immediately.") self.on_error(socket.error()) socket.disconnected.connect(self.on_disconnected) if socket.state() == QLocalSocket.UnconnectedState: log.ipc.debug("Socket was disconnected immediately.") self.on_disconnected() @pyqtSlot() def on_disconnected(self): """Clean up socket when the client disconnected.""" log.ipc.debug("Client disconnected.") self._timer.stop() self._socket.deleteLater() self._socket = None # Maybe another connection is waiting. self.handle_connection() @pyqtSlot() def on_ready_read(self): """Read json data from the client.""" if self._socket is None: # this happened once and I don't know why log.ipc.warn("In on_ready_read with None socket!") return self._timer.start() while self._socket is not None and self._socket.canReadLine(): data = bytes(self._socket.readLine()) log.ipc.debug("Read from socket: {}".format(data)) try: decoded = data.decode('utf-8') except UnicodeDecodeError: log.ipc.error("Ignoring invalid IPC data.") log.ipc.debug("invalid data: {}".format( binascii.hexlify(data))) return log.ipc.debug("Processing: {}".format(decoded)) try: json_data = json.loads(decoded) except ValueError: log.ipc.error("Ignoring invalid IPC data.") log.ipc.debug("invalid json: {}".format(decoded.strip())) return try: args = json_data['args'] except KeyError: log.ipc.error("Ignoring invalid IPC data.") log.ipc.debug("no args: {}".format(decoded.strip())) return cwd = json_data.get('cwd', None) app = objreg.get('app') app.process_pos_args(args, via_ipc=True, cwd=cwd) @pyqtSlot() def on_timeout(self): """Cancel the current connection if it was idle for too long.""" log.ipc.error("IPC connection timed out.") self._socket.close() def shutdown(self): """Shut down the IPC server cleanly.""" if self._socket is not None: self._socket.deleteLater() self._socket = None self._timer.stop() self._server.close() self._server.deleteLater() self._remove_server()
class Eddy(QApplication): """ This class implements the main Qt application. """ messageReceived = pyqtSignal(str) def __init__(self, argv): """ Initialize Eddy. :type argv: list """ super().__init__(argv) parser = ArgumentParser() parser.add_argument('--nosplash', dest='nosplash', action='store_true') parser.add_argument('--tests', dest='tests', action='store_true') options, args = parser.parse_known_args(args=argv) self.inSocket = None self.inStream = None self.outSocket = QLocalSocket() self.outSocket.connectToServer(APPID) self.outStream = None self.isRunning = self.outSocket.waitForConnected() self.mainwindow = None self.pendingOpen = [] self.server = None # We do not initialize a new instance of Eddy if there is a process running # and we are not executing the tests suite: we'll create a socket instead so we can # exchange messages between the 2 processes (this one and the already running one). if self.isRunning and not options.tests: self.outStream = QTextStream(self.outSocket) self.outStream.setCodec('UTF-8') else: self.server = QLocalServer() self.server.listen(APPID) self.outSocket = None self.outStream = None connect(self.server.newConnection, self.newConnection) connect(self.messageReceived, self.readMessage) ############################################################################################################ # # # PERFORM EDDY INITIALIZATION # # # ############################################################################################################ # Draw the splashscreen. self.splashscreen = None if not options.nosplash: self.splashscreen = SplashScreen(min_splash_time=4) self.splashscreen.show() # Setup layout. self.setStyle(Clean('Fusion')) with open(expandPath('@eddy/ui/clean.qss')) as sheet: self.setStyleSheet(sheet.read()) # Create the main window. self.mainwindow = MainWindow() # Close the splashscreen. if self.splashscreen: self.splashscreen.wait(self.splashscreen.remaining) self.splashscreen.close() # Display the mainwindow. self.mainwindow.show() if Platform.identify() is Platform.Darwin: # On MacOS files being opened are handled as a QFileOpenEvent but since we don't # have a Main Window initialized we store them locally and we open them here. for filepath in self.pendingOpen: self.openFile(filepath) self.pendingOpen = [] else: # Perform document opening if files have been added to sys.argv. This is not # executed on Mac OS since this is already handled as a QFileOpenEvent instance. for filepath in argv: self.openFile(filepath) #################################################################################################################### # # # EVENTS # # # #################################################################################################################### def event(self, event): """ Executed when an event is received. :type event: T <= QEvent | QFileOpenEvent """ if event.type() == QEvent.FileOpen: self.pendingOpen = [event.file()] return True return super().event(event) #################################################################################################################### # # # INTERFACE # # # #################################################################################################################### def activate(self): """ Activate the application by raising the main window. """ if self.mainwindow: self.mainwindow.setWindowState((self.mainwindow.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.mainwindow.activateWindow() self.mainwindow.raise_() def openFile(self, filepath): """ Open the given file in the activation window. :type filepath: str :rtype: bool """ if self.mainwindow: if not isEmpty(filepath) and os.path.isfile(filepath) and filepath.endswith(Filetype.Graphol.extension): self.mainwindow.openFile(filepath) return True return False def sendMessage(self, message): """ Send a message to the other alive Eddy's process. :type message: str :rtype: bool """ if self.outStream: self.outStream = self.outStream << message << '\n' self.outStream.flush() return self.outSocket.waitForBytesWritten() return False #################################################################################################################### # # # SLOTS # # # #################################################################################################################### @pyqtSlot() def newConnection(self): """ Executed whenever a message is received. """ if self.inSocket: # Disconnect previously connected signal slot. disconnect(self.inSocket.readyRead, self.readyRead) # Create a new socket. self.inSocket = self.server.nextPendingConnection() if self.inSocket: self.inStream = QTextStream(self.inSocket) self.inStream.setCodec('UTF-8') connect(self.inSocket.readyRead, self.readyRead) self.activate() @pyqtSlot() def readyRead(self): """ Executed whenever we need to read a message. """ while True: message = self.inStream.readLine() if isEmpty(message): break self.messageReceived.emit(message) @pyqtSlot(str) def readMessage(self, message): """ Read a received message. :type message: str """ for filepath in message.split(' '): self.openFile(filepath)
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 SingleApplication(QtWidgets.QApplication): ''' Inheriting from QApplication, executing main App instead. Watching whether the app is already running. If so, quit befor execution. ''' messageReceived = QtCore.pyqtSignal(str) def __init__(self, id, *argv): super(SingleApplication, self).__init__(*argv) self._id = id self._activationWindow = None self._activateOnMessage = False # Check if another instance is running? self._outSocket = QLocalSocket() self._outSocket.connectToServer(self._id) self._isRunning = self._outSocket.waitForConnected() if self._isRunning: self._outStream = QtCore.QTextStream(self._outSocket) self._outStream.setCodec('UTF-8') else: self._outSocket = None self._outStream = None self._inSocket = None self._inStream = None self._server = QLocalServer() self._server.removeServer(self._id) # if existing after crash-exit self._server.listen(self._id) self._server.newConnection.connect(self._onNewConnection) def isRunning(self): return self._isRunning def id(self): return self._id def activationWindow(self): return self._activationWindow def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage def activateWindow(self): if not self._activationWindow: return self._activationWindow.setWindowState( self._activationWindow.windowState() & ~QtCore.Qt.WindowMinimized) self._activationWindow.show() self._activationWindow.activateWindow() def sendMessage(self, msg): if not self._outStream: return False self._outStream << msg << '\n' self._outStream.flush() return self._outSocket.waitForBytesWritten() def _onNewConnection(self): if self._inSocket: self._inSocket.readyRead.disconnect(self._onReadyRead) self._inSocket = self._server.nextPendingConnection() if not self._inSocket: return self._inStream = QtCore.QTextStream(self._inSocket) self._inStream.setCodec('UTF-8') self._inSocket.readyRead.connect(self._onReadyRead) if self._activateOnMessage: self.activateWindow() def _onReadyRead(self): while True: msg = self._inStream.readLine() if not msg: break self.messageReceived.emit(msg)