def __init__(self, parent=None): super(PyFlow, self).__init__(parent=parent) self.setupUi(self) self.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png")) self._tools = set() self.currentTempDir = "" self.preferencesWindow = PreferencesWindow(self) self.graphManager = GraphManager() self.canvasWidget = Canvas(self.graphManager, self) self.canvasWidget.requestFillProperties.connect( self.onRequestFillProperties) self.canvasWidget.requestClearProperties.connect( self.onRequestClearProperties) self.graphManager.graphChanged.connect(self.updateGraphTreeLocation) self.updateGraphTreeLocation() self.SceneLayout.addWidget(self.canvasWidget) rxLettersAndNumbers = QtCore.QRegExp('^[a-zA-Z0-9]*$') nameValidator = QtGui.QRegExpValidator(rxLettersAndNumbers, self.leCompoundName) self.leCompoundName.setValidator(nameValidator) self.leCompoundName.returnPressed.connect( self.onActiveCompoundNameAccepted) rxLetters = QtCore.QRegExp('^[a-zA-Z]*$') categoryValidator = QtGui.QRegExpValidator(rxLetters, self.leCompoundCategory) self.leCompoundCategory.setValidator(categoryValidator) self.leCompoundCategory.returnPressed.connect( self.onActiveCompoundCategoryAccepted) self.setMouseTracking(True) self._lastClock = 0.0 self.fps = EDITOR_TARGET_FPS self.tick_timer = QtCore.QTimer() self._current_file_name = 'Untitled' self.populateMenu()
def run(filePath): app = QApplication(sys.argv) app.setStyle(QStyleFactory.create("plastique")) app.setStyleSheet(editableStyleSheet().getStyleSheet()) msg = QMessageBox() msg.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png")) msg.setIcon(QMessageBox.Critical) if os.path.exists(filePath): with open(filePath, 'r') as f: data = json.load(f) # Window to display inputs prop = QDialog() prop.setLayout(QVBoxLayout()) prop.setWindowTitle(filePath) prop.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png")) # Initalize packages try: INITIALIZE() man = GraphManager() man.deserialize(data) grph = man.findRootGraph() inputs = grph.getNodesByClassName("graphInputs") # If no GraphInput Nodes Exit propgram if len(inputs) > 0: for inp in inputs: uiNode = getUINodeInstance(inp) uiNodeJsonTemplate = inp.serialize() uiNodeJsonTemplate["wrapper"] = inp.wrapperJsonData uiNode.postCreate(uiNodeJsonTemplate) cat = uiNode.createOutputWidgets(prop.layout(), inp.name) prop.show() def programLoop(): while True: man.Tick(deltaTime=0.02) time.sleep(0.02) if man.terminationRequested: break t = threading.Thread(target=programLoop) t.start() def quitEvent(): man.terminationRequested = True t.join() app.aboutToQuit.connect(quitEvent) else: msg.setInformativeText(filePath) msg.setDetailedText( "The file doesn't containt graphInputs nodes") msg.setWindowTitle("PyFlow Ui Graph Parser") msg.setStandardButtons(QMessageBox.Ok) msg.show() except Exception as e: msg.setText("Error reading Graph") msg.setInformativeText(filePath) msg.setDetailedText(str(e)) msg.setWindowTitle("PyFlow Ui Graph Parser") msg.setStandardButtons(QMessageBox.Ok) msg.show() else: msg.setText("File Not Found") msg.setInformativeText(filePath) msg.setWindowTitle("PyFlow Ui Graph Parser") msg.setStandardButtons(QMessageBox.Ok) msg.show() try: sys.exit(app.exec_()) except Exception as e: print(e)
class PyFlow(QMainWindow, GraphEditor_ui.Ui_MainWindow): newFileExecuted = QtCore.Signal(bool) def __init__(self, parent=None): super(PyFlow, self).__init__(parent=parent) self.setupUi(self) self.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png")) self._tools = set() self.currentTempDir = "" self.preferencesWindow = PreferencesWindow(self) self.graphManager = GraphManager() self.canvasWidget = Canvas(self.graphManager, self) self.canvasWidget.requestFillProperties.connect( self.onRequestFillProperties) self.canvasWidget.requestClearProperties.connect( self.onRequestClearProperties) self.graphManager.graphChanged.connect(self.updateGraphTreeLocation) self.updateGraphTreeLocation() self.SceneLayout.addWidget(self.canvasWidget) rxLettersAndNumbers = QtCore.QRegExp('^[a-zA-Z0-9]*$') nameValidator = QtGui.QRegExpValidator(rxLettersAndNumbers, self.leCompoundName) self.leCompoundName.setValidator(nameValidator) self.leCompoundName.returnPressed.connect( self.onActiveCompoundNameAccepted) rxLetters = QtCore.QRegExp('^[a-zA-Z]*$') categoryValidator = QtGui.QRegExpValidator(rxLetters, self.leCompoundCategory) self.leCompoundCategory.setValidator(categoryValidator) self.leCompoundCategory.returnPressed.connect( self.onActiveCompoundCategoryAccepted) self.setMouseTracking(True) self._lastClock = 0.0 self.fps = EDITOR_TARGET_FPS self.tick_timer = QtCore.QTimer() self._current_file_name = 'Untitled' self.populateMenu() def getTempDirectory(self): """Returns unique temp directory for application instance. This folder and all it's content will be removed from disc on application shutdown. """ return self.currentTempDir def populateMenu(self): fileMenu = self.menuBar.addMenu("File") newFileAction = fileMenu.addAction("New file") newFileAction.setIcon(QtGui.QIcon(":/new_file_icon.png")) newFileAction.triggered.connect(self.newFile) loadAction = fileMenu.addAction("Load") loadAction.setIcon(QtGui.QIcon(":/folder_open_icon.png")) loadAction.triggered.connect(self.load) saveAction = fileMenu.addAction("Save") saveAction.setIcon(QtGui.QIcon(":/save_icon.png")) saveAction.triggered.connect(self.save) saveAsAction = fileMenu.addAction("Save as") saveAsAction.setIcon(QtGui.QIcon(":/save_as_icon.png")) saveAsAction.triggered.connect(lambda: self.save(True)) editMenu = self.menuBar.addMenu("Edit") preferencesAction = editMenu.addAction("Preferences") preferencesAction.setIcon(QtGui.QIcon(":/options_icon.png")) preferencesAction.triggered.connect(self.showPreferencesWindow) helpMenu = self.menuBar.addMenu("Help") shortcutsAction = helpMenu.addAction("Shortcuts") shortcutsAction.setIcon(QtGui.QIcon(":/shortcuts_icon.png")) shortcutsAction.triggered.connect(self.shortcuts_info) def showPreferencesWindow(self): self.preferencesWindow.show() def registerToolInstance(self, instance): """Registers tool instance reference This needed to prevent classes from being garbage collected and to save widgets state Args: instance (ToolBase): Tool to be registered """ self._tools.add(instance) def unregisterToolInstance(self, instance): if instance in self._tools: self._tools.remove(instance) def onRequestFillProperties(self, propertiesFillDelegate): for toolInstance in self._tools: if isinstance(toolInstance, PropertiesTool): toolInstance.clear() toolInstance.assignPropertiesWidget(propertiesFillDelegate) def onRequestClearProperties(self): for toolInstance in self._tools: if isinstance(toolInstance, PropertiesTool): toolInstance.clear() def getToolbar(self): return self.toolBar def getCanvas(self): return self.canvasWidget def setCompoundPropertiesWidgetVisible(self, bVisible): if bVisible: self.CompoundPropertiesWidget.show() self.leCompoundName.setText(self.graphManager.activeGraph().name) self.leCompoundCategory.setText( self.graphManager.activeGraph().category) else: self.CompoundPropertiesWidget.hide() def keyPressEvent(self, event): modifiers = event.modifiers() currentInputAction = InputAction(name="temp", actionType=InputActionType.Keyboard, key=event.key(), modifiers=modifiers) actionSaveVariants = InputManager()["App.Save"] actionNewFileVariants = InputManager()["App.NewFile"] actionLoadVariants = InputManager()["App.Load"] actionSaveAsVariants = InputManager()["App.SaveAs"] if currentInputAction in actionNewFileVariants: self.newFile() if currentInputAction in actionSaveVariants: self.save() if currentInputAction in actionLoadVariants: self.load() if currentInputAction in actionSaveAsVariants: self.save_as() def loadFromData(self, data, fpath=""): self.newFile(keepRoot=False) # load raw data self.graphManager.deserialize(data) # create ui nodes for graph in self.graphManager.getAllGraphs(): self.canvasWidget.createWrappersForGraph(graph) self.graphManager.selectRootGraph() def load(self): name_filter = "Graph files (*.json)" savepath = QFileDialog.getOpenFileName(filter=name_filter) if type(savepath) in [tuple, list]: fpath = savepath[0] else: fpath = savepath if not fpath == '': with open(fpath, 'r') as f: data = json.load(f) self.loadFromData(data, fpath) def save(self, save_as=False): if save_as: name_filter = "Graph files (*.json)" savepath = QFileDialog.getSaveFileName(filter=name_filter) if type(savepath) in [tuple, list]: pth = savepath[0] else: pth = savepath if not pth == '': self._current_file_name = pth else: self._current_file_name = "Untitled" else: if not os.path.isfile(self._current_file_name): name_filter = "Graph files (*.json)" savepath = QFileDialog.getSaveFileName(filter=name_filter) if type(savepath) in [tuple, list]: pth = savepath[0] else: pth = savepath if not pth == '': self._current_file_name = pth else: self._current_file_name = "Untitled" if self._current_file_name in ["", "Untitled"]: return if not self._current_file_name == '': with open(self._current_file_name, 'w') as f: saveData = self.graphManager.serialize() json.dump(saveData, f, indent=4) print(str("// saved: '{0}'".format(self._current_file_name))) def newFile(self, keepRoot=True): self.tick_timer.stop() self.tick_timer.timeout.disconnect() # broadcast self.graphManager.clear(keepRoot=keepRoot) self.newFileExecuted.emit(keepRoot) self._current_file_name = 'Untitled' self.onRequestClearProperties() self.startMainLoop() def updateGraphTreeLocation(self, *args, **kwargs): location = self.canvasWidget.location() clearLayout(self.layoutGraphPath) spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.layoutGraphPath.addItem(spacerItem) for folderName in location: index = self.layoutGraphPath.count() - 1 btn = QPushButton(folderName) def onClicked(checked, name=None): self.canvasWidget.stepToCompound(name) btn.clicked.connect( lambda chk=False, name=folderName: onClicked(chk, name)) self.layoutGraphPath.insertWidget(index, btn) self.setCompoundPropertiesWidgetVisible( self.graphManager.activeGraph().depth() > 1) def onActiveCompoundNameAccepted(self): newName = self.graphManager.getUniqName(self.leCompoundName.text()) self.graphManager.activeGraph().name = newName self.leCompoundName.blockSignals(True) self.leCompoundName.setText(newName) self.leCompoundName.blockSignals(False) self.updateGraphTreeLocation() def onActiveCompoundCategoryAccepted(self): newCategoryName = self.leCompoundCategory.text() self.graphManager.activeGraph().category = newCategoryName def startMainLoop(self): self.tick_timer.timeout.connect(self.mainLoop) self.tick_timer.start(1000 / EDITOR_TARGET_FPS) def stopMainLoop(self): self.tick_timer.stop() self.tick_timer.timeout.disconnect() def mainLoop(self): deltaTime = clock() - self._lastClock ds = (deltaTime * 1000.0) if ds > 0: self.fps = int(1000.0 / ds) # Tick all graphs # each graph will tick owning raw nodes # each raw node will tick it's ui wrapper if it exists self.graphManager.Tick(deltaTime) # Tick canvas. Update ui only stuff such animation etc. self.canvasWidget.Tick(deltaTime) self._lastClock = clock() def createPopupMenu(self): pass def newPlugin(self, pluginType): name, result = QInputDialog.getText(self, 'Plugin name', 'Enter plugin name') if result: _implementPlugin(name, pluginType) def getToolClassByName(self, packageName, toolName, toolClass=DockTool): registeredTools = GET_TOOLS() for ToolClass in registeredTools[packageName]: if issubclass(ToolClass, toolClass): if ToolClass.name() == toolName: return ToolClass return None def createToolInstanceByClass(self, packageName, toolName, toolClass=DockTool): registeredTools = GET_TOOLS() for ToolClass in registeredTools[packageName]: if issubclass(ToolClass, toolClass): if ToolClass.name() == toolName: return ToolClass() return None def invokeDockToolByName(self, packageName, name, settings=None): # invokeDockToolByName Invokes dock tool by tool name and package name # If settings provided QMainWindow::restoreDockWidget will be called instead QMainWindow::addDockWidget toolClass = self.getToolClassByName(packageName, name, DockTool) isSingleton = toolClass.isSingleton() if isSingleton: # check if already registered if name in [t.name() for t in self._tools]: for tool in self._tools: if tool.name() == name: # Highlight window print("highlight", tool.uniqueName()) return ToolInstance = self.createToolInstanceByClass(packageName, name, DockTool) if ToolInstance: self.registerToolInstance(ToolInstance) if settings is not None: ToolInstance.restoreState(settings) if not self.restoreDockWidget(ToolInstance): # handle if ui state was not restored pass else: self.addDockWidget(ToolInstance.defaultDockArea(), ToolInstance) ToolInstance.setCanvas(self.canvasWidget) ToolInstance.onShow() return ToolInstance def closeEvent(self, event): self.tick_timer.stop() self.tick_timer.timeout.disconnect() self.canvasWidget.shoutDown() # save editor config settings = QtCore.QSettings(ConfigManager().APP_SETTINGS_PATH, QtCore.QSettings.IniFormat, self) # clear file each time to capture opened dock tools settings.clear() settings.sync() settings.beginGroup('Editor') settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) settings.endGroup() # save tools state settings.beginGroup('Tools') for tool in self._tools: if isinstance(tool, ShelfTool): settings.beginGroup("ShelfTools") settings.beginGroup(tool.name()) tool.saveState(settings) settings.endGroup() settings.endGroup() if isinstance(tool, DockTool): settings.beginGroup("DockTools") settings.beginGroup(tool.uniqueName()) tool.saveState(settings) settings.endGroup() settings.endGroup() tool.onDestroy() settings.endGroup() settings.sync() # remove temp directory shutil.rmtree(self.currentTempDir, ignore_errors=True) QMainWindow.closeEvent(self, event) def shortcuts_info(self): data = "Tab - togle node box\n" data += "Ctrl+N - new file\n" data += "Ctrl+S - save\n" data += "Ctrl+Shift+S - save as\n" data += "Ctrl+O - open file\n" data += "F - frame selected\n" data += "H - frame all\n" data += "C - comment selected nodes\n" data += "Delete - kill selected nodes\n" data += "Ctrl+C - Copy\n" data += "Ctrl+V - Paste\n" data += "Alt+Drag - Duplicate\n" data += "Ctrl+Z - Undo\n" data += "Ctrl+Y - Redo\n" data += "Alt+Click - Disconnect Pin\n" data += "Ctrl+Shift+ArrowLeft - Align left\n" data += "Ctrl+Shift+ArrowUp - Align Up\n" data += "Ctrl+Shift+ArrowRight - Align right\n" data += "Ctrl+Shift+ArrowBottom - Align Bottom\n" QMessageBox.information(self, "Shortcuts", data) @staticmethod def instance(parent=None): instance = PyFlow(parent) instance.startMainLoop() INITIALIZE() # create app folder in documents # random string used for cases when multiple instances of app are running in the same time prefs = QtCore.QSettings(ConfigManager().PREFERENCES_CONFIG_PATH, QtCore.QSettings.IniFormat) tempDirPath = prefs.value("Preferences/General/TempFilesDir") if tempDirPath == None: tempDirPath = "/tmp/pyflow" if tempDirPath[-1:] in ('/', '\\'): tempDirPath = tempDirPath[:-1] instance.currentTempDir = "{0}_{1}".format(tempDirPath, generateRandomString()) if not os.path.exists(instance.currentTempDir): os.makedirs(instance.currentTempDir) # populate tools canvas = instance.getCanvas() toolbar = instance.getToolbar() settings = QtCore.QSettings(ConfigManager().APP_SETTINGS_PATH, QtCore.QSettings.IniFormat) geo = settings.value('Editor/geometry') if geo is not None: instance.restoreGeometry(geo) state = settings.value('Editor/state') if state is not None: instance.restoreState(state) settings.beginGroup("Tools") for packageName, registeredToolSet in GET_TOOLS().items(): for ToolClass in registeredToolSet: if issubclass(ToolClass, ShelfTool): ToolInstance = ToolClass() # prevent to be garbage collected instance.registerToolInstance(ToolInstance) ToolInstance.setCanvas(canvas) action = QAction(instance) action.setIcon(ToolInstance.getIcon()) action.setText(ToolInstance.name()) action.setToolTip(ToolInstance.toolTip()) action.setObjectName(ToolInstance.name()) action.triggered.connect(ToolInstance.do) # check if context menu data available menuBuilder = ToolInstance.contextMenuBuilder() if menuBuilder: menuGenerator = ContextMenuGenerator(menuBuilder) menu = menuGenerator.generate() action.setMenu(menu) toolbar.addAction(action) # step to ShelfTools/ToolName group and pass settings inside settings.beginGroup("ShelfTools") settings.beginGroup(ToolClass.name()) ToolInstance.restoreState(settings) settings.endGroup() settings.endGroup() if issubclass(ToolClass, DockTool): menus = instance.menuBar.findChildren(QMenu) helpMenuAction = [m for m in menus if m.title() == "Help"][0].menuAction() toolsMenu = getOrCreateMenu(instance.menuBar, "Tools") instance.menuBar.insertMenu(helpMenuAction, toolsMenu) packageSubMenu = getOrCreateMenu(toolsMenu, packageName) toolsMenu.addMenu(packageSubMenu) showToolAction = packageSubMenu.addAction(ToolClass.name()) icon = ToolClass.getIcon() if icon: showToolAction.setIcon(icon) showToolAction.triggered.connect( lambda pkgName=packageName, toolName=ToolClass.name( ): instance.invokeDockToolByName(pkgName, toolName)) settings.beginGroup("DockTools") childGroups = settings.childGroups() for dockToolGroupName in childGroups: # This dock tool data been saved on last shutdown settings.beginGroup(dockToolGroupName) if dockToolGroupName in [ t.uniqueName() for t in instance._tools ]: continue toolName = dockToolGroupName.split("::")[0] ToolInstance = instance.invokeDockToolByName( packageName, toolName, settings) settings.endGroup() settings.endGroup() return instance
if not fpath == '': with open(fpath, 'r') as f: data = json.load(f) # Window to display Inputs prop = QDialog() prop.setLayout(QVBoxLayout()) prop.setWindowTitle(fpath) prop.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png")) msg = QMessageBox() msg.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png")) msg.setIcon(QMessageBox.Critical) # Initalize Packages try: INITIALIZE() man = GraphManager() man.deserialize(data) grph = man.findRootGraph() inputs = grph.getNodesByClassName("graphInputs") # If no GraphInput Nodes Exit propgram if len(inputs) > 0: for inp in inputs: uiNode = getUINodeInstance(inp) uiNodeJsonTemplate = inp.serialize() uiNodeJsonTemplate["wrapper"] = inp.wrapperJsonData uiNode.postCreate(uiNodeJsonTemplate) cat = uiNode.createOutputWidgets(prop.layout(), inp.name) prop.show() else: msg.setInformativeText(fpath) msg.setDetailedText(