class PFSNet(QWidget): changed = pyqtSignal() def __init__(self, id: str, window, tempName=None): super(QWidget, self).__init__() self._filename = None self._filepath = None self._tempName = tempName self._id = id layout = QHBoxLayout() self._tab = QTabWidget() self._tab.currentChanged.connect(self.changeTab) self._tab.setTabsClosable(True) self._tab.tabCloseRequested.connect(self.closeTab) layout.addWidget(self._tab) self.setLayout(layout) self._prop = QTableWidget(20, 2) self._prop.itemChanged.connect(self.propertiesItemChanged) self._prop.verticalHeader().hide() self._prop.setColumnWidth(1, 180) self._prop.setMaximumWidth(300) lv = QVBoxLayout() lv.addWidget(self._prop) self._tree = QTreeWidget() self._tree.itemClicked.connect(self.treeItemClicked) self._tree.setMaximumWidth(300) lv.addWidget(self._tree) layout.addLayout(lv) self._pages = [] self._idPage = 0 self._sm = window._sm self._window = window self._distributorId = 0 self._activityId = 0 self._relationId = 0 self._otherId = 0 self._pageId = 0 self._page = None self._elements = {} self.undoStack = QUndoStack(self) self.undoAction = self.undoStack.createUndoAction(self, "Desfazer") self.undoAction.setShortcuts(QKeySequence.Undo) self.undoAction.setIcon( QIcon.fromTheme("edit-undo", QIcon("icons/edit-undo.svg"))) self.redoAction = self.undoStack.createRedoAction(self, "Refazer") self.redoAction.setShortcuts(QKeySequence.Redo) self.redoAction.setIcon( QIcon.fromTheme("edit-redo", QIcon("icons/edit-redo.svg"))) self._pasteList = [] def tree(self): tree = QTreeWidgetItem(self._tree, ["Net " + self._id], 0) child = self._page.tree(tree) self._tree.expandAll() return tree def prepareTree(self): self._tree.clear() self.tree() def showPage(self, widget): if widget in self._pages: self._tab.setCurrentWidget(widget) else: self._tab.addTab(widget, widget.name()) self._pages.append(widget) self._tab.setCurrentWidget(widget) def removeTabWidget(self, widget): for i in range(self._tab.count()): if self._tab.widget(i) == widget: self._tab.removeTab(i) self._pages.remove(widget) def propertiesItemChanged(self, item: PFSTableValueText): if item.comparePrevious(): item.edited.emit(item) def getAllPages(self): ans = [] ans.append(self._page) aux = self._page.getAllSubPages() if len(aux) > 0: ans = ans + aux return ans def generateXml(self, xml: QXmlStreamWriter): xml.writeStartDocument() xml.writeStartElement("PetriNetDoc") xml.writeStartElement("net") xml.writeAttribute("id", self._id) pages = self.getAllPages() for p in pages: p.generateXml(xml) xml.writeEndElement() xml.writeEndElement() xml.writeEndDocument() def treeItemClicked(self, item, col): if isinstance(item, PFSTreeItem): item.clicked.emit() def createFromXml(doc: QDomDocument, window): el = doc.documentElement() nodes = el.childNodes() nets = [] for i in range(nodes.count()): node = nodes.at(i) if node.nodeName() != "net": continue if not (node.hasAttributes() and node.attributes().contains("id")): continue id = node.attributes().namedItem("id").nodeValue() net = PFSNet(id, window) nodesPage = node.childNodes() pages = [] for j in range(nodesPage.count()): nodePage = nodesPage.at(j) if nodePage.nodeName() != "page": continue page = PFSPage.createFromXml(nodePage) if page is not None: pages.append(page) if len(pages) == 0: continue aux = {} for page in pages: p = PFSPage.createFromContent(page, window._sm, net) if p is not None: i = page._ref if i is None or not i: i = "main" aux[i] = p if "main" not in aux.keys(): continue for indice, page in aux.items(): ids = page.getMaxIds() if net._activityId < ids[0] + 1: net._activityId = ids[0] + 1 if net._distributorId < ids[1] + 1: net._distributorId = ids[1] + 1 if net._relationId < ids[2] + 1: net._relationId = ids[2] + 1 if net._otherId < ids[3] + 1: net._otherId = ids[3] + 1 if net._pageId < int(page._id[1:]) + 1: net._pageId = int(page._id[1:]) + 1 if indice == "main": net._tab.blockSignals(True) net._tab.addTab(page, page.name()) net._tab.blockSignals(False) for indice2, page2 in aux.items(): elem = page2.getElementById(indice) if elem is not None: page._subRef = elem elem.setSubPage(page) page.setName("Ref_" + elem._id) break net._page = aux["main"] net._pages.append(aux["main"]) net.tree() nets.append(net) return nets def getTabName(self) -> str: if self._filename is None and self._tempName is None: ans = "New model" elif self._filename is None: ans = self._tempName else: ans = self._filename if self.undoStack.isClean(): return ans return ans + "*" def newNet(id, window, tempName="newmodel.xml"): ans = PFSNet(id, window, tempName) page = PFSPage.newPage(ans.requestId(PFSPage), window._sm, ans) ans._page = page ans._pages.append(page) ans._tab.addTab(page, page.name()) return ans def openPage(self, element): if isinstance(element, PFSPage): page = element elif isinstance(element, PFSActivity): page = element.subPage() else: return if page not in self._pages: self._tab.addTab(page, page.name()) self._pages.append(page) self._tab.setCurrentWidget(page) def createPage(self, element=None): page = PFSPage.newPage(self.requestId(PFSPage), self._sm, self, 600, 120) if element is not None and element.setSubPage(page): page.setName("Ref_" + element._id) page._subRef = element openac = PFSOpenActivity(self.requestId(PFSOpenActivity), 20, 10, 100) self.addItemNoUndo(openac, page) closeac = PFSCloseActivity(self.requestId(PFSCloseActivity), page._scene.sceneRect().width() - 20, 10, 100) self.addItemNoUndo(closeac, page) self._idPage = self._idPage + 1 self._sm.fixTransitions(page._scene) return page return None def deleteElements(self): if len(self._pages) == 0: return scene = self._tab.currentWidget()._scene itemsSeleted = scene.selectedItems() if len(itemsSeleted) == 0: if self._tab.currentWidget() == self._page: return x = PFSUndoDeletePage(self._tab.currentWidget()) self.undoStack.push(x) return itemsDeleted = [] for item in itemsSeleted: if not item.canDelete(): continue if isinstance(item, PFSNode): item.deleted.emit() itemsDeleted.append(item) if len(itemsDeleted) > 0: x = PFSUndoDelete(itemsDeleted) self.undoStack.push(x) def pasteElements(self, elements): self._pasteList = elements def pasteItems(self, pos): ans = [] aux = {} for elem in self._pasteList: if isinstance(elem, PFSRelationContent) or isinstance( elem, PFSSecondaryFlowContent): continue oldId = elem._id id = self.requestId(elem) if isinstance(elem, PFSActivityContent): e = PFSActivity.paste(elem, id, pos.x(), pos.y()) ans.append(e) aux[oldId] = e elif isinstance(elem, PFSDistributorContent): e = PFSDistributor.paste(elem, id, pos.x(), pos.y()) ans.append(e) aux[oldId] = e for elem in self._pasteList: if isinstance(elem, PFSRelationContent): oldId = elem._id id = self.requestId(elem) e = PFSRelation.paste(elem, id, pos.x(), pos.y(), aux) ans.append(e) elif isinstance(elem, PFSSecondaryFlowContent): oldId = elem._id id = self.requestId(elem) e = PFSSecondaryFlow.paste(elem, id, pos.x(), pos.y(), aux) ans.append(e) x = PFSUndoAdd(ans, self._tab.currentWidget()._scene) self.undoStack.push(x) def export(self, filename): if len(self._pages) > 1: scene = self._tab.currentWidget()._scene elif len(self._pages) == 1: scene = self._pages[0]._scene else: return if filename.endswith(".png"): PFSImage.gravaPng(scene, filename) else: PFSImage.gravaSvg(scene, filename) def addItem(self, element, page: PFSPage): if isinstance(element, PFSRelation): if isinstance(element._source, PFSActive) and isinstance( element._target, PFSActive): return False if isinstance(element._source, PFSPassive) and isinstance( element._target, PFSPassive): return False x = PFSUndoAdd([element], page._scene) self.undoStack.push(x) return True def addItemNoUndo(self, element, page: PFSPage): if isinstance(element, PFSRelation): if isinstance(element._source, PFSActive) and isinstance( element._target, PFSActive): return False if isinstance(element._source, PFSPassive) and isinstance( element._target, PFSPassive): return False page._scene.addItem(element) page._scene.update() return True def requestId(self, element): if element == PFSActivity or isinstance(element, PFSActivityContent): ans = "A" + str(self._activityId) self._activityId = self._activityId + 1 elif element == PFSDistributor or isinstance(element, PFSDistributorContent): ans = "D" + str(self._distributorId) self._distributorId = self._distributorId + 1 elif element == PFSRelation: ans = "R" + str(self._relationId) self._relationId = self._relationId + 1 elif element == PFSPage: ans = "P" + str(self._pageId) self._pageId = self._pageId + 1 else: ans = "O" + str(self._otherId) self._otherId = self._otherId + 1 return ans def changeTab(self, index: int): self._prop.clear() self._tree.clear() self.tree() if index < 0: return self._tab.widget(index)._scene.clearSelection() self._window._main.tabChanged.emit() def fillProperties(self, props): if len(props) > 0: self._prop.setRowCount(0) self._prop.setRowCount(len(props)) i = 0 for line in props: if isinstance(line[0], QTableWidgetItem): self._prop.setItem(i, 0, line[0]) else: self._prop.setCellWidget(i, 0, line[0]) if isinstance(line[1], QTableWidgetItem): self._prop.setItem(i, 1, line[1]) else: self._prop.setCellWidget(i, 1, line[1]) if isinstance(line[0], PFSTableLabelTags): self._prop.setRowHeight(i, 100) i = i + 1 def closeTab(self, ind): w = self._tab.widget(ind) self._pages.remove(w) self._tab.removeTab(ind)
class MainGUI(QMainWindow): def __init__(self, parent=None): super(MainGUI, self).__init__(parent) self.parent = parent self.screen_size = QDesktopWidget().screenGeometry(-1) self.window_size = (self.screen_size.width(), self.screen_size.height()) self.menu_bar = self.menuBar() # Edit File self.fileMenu = None self.saveAct = None self.openAct = None # Recent sub menu self.recentMenu = None self.compareAct = None # Import sub menu self.importMenu = None self.importAct = None self.import_csvAct = None # Export sub menu self.exportMenu = None self.exportAct = None self.export_csvAct = None self.exitAct = None # Edit Menu self.editMenu = None self.undoAct = None self.redoAct = None self.clearAct = None # ViewMenu self.viewMenu = None self.timelabelAct = None self.daylabelAct = None self.fullscrenAct = None # Theme Menu self.styleMenu = None self.dark_styleAct = None self.white_styleAct = None # Theme self._style = "Dark" self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) # Save self._save_week_list = [] self._num_save = 0 # Undo framework self.undoStack = QUndoStack() # Main grid self.grid = None self.initUI() def initMenuBar(self): # saveAct = QAction(QIcon('save.png'), '&Save', self) self.saveAct = QAction('&Save', self) self.saveAct.setShortcut('Ctrl+S') self.saveAct.setStatusTip('Save the grid') self.saveAct.triggered.connect(self.save) # openAct = QAction(QIcon('open.png'), '&Open', self) self.openAct = QAction('&Open', self) self.openAct.setShortcut('Ctrl+O') self.openAct.setStatusTip('Open file') self.openAct.triggered.connect(self.open) # self.importMenu = QAction(QIcon('recent.png'), '&Open Recent', self) self.recentMenu = QMenu('&Open Recent', self) # compareAct = QAction(QIcon('compare.png'), '&Compare', self) self.compareAct = QAction('&Compare', self) self.compareAct.setShortcut('Ctrl+B') self.compareAct.setStatusTip('Compare two grid') self.compareAct.triggered.connect(self.compare) self.importMenu = QMenu('&Import', self) self.importMenu.setStatusTip('Import file') # self.importAct = QAction(QIcon('import.png'), '&Import', self) self.importAct = QAction('&Import', self) self.importAct.setShortcut('Ctrl+I') self.importAct.setStatusTip('Import file') # self.import_csvAct = QAction(QIcon('import.png'), '&csv', self) self.import_csvAct = QAction('&csv', self) self.import_csvAct.setStatusTip('Import csv') # self.exportMenu = QAction(QIcon('csv.png'), '&Exit', self) self.exportMenu = QMenu('&Export', self) self.exportMenu.setStatusTip('Export file') # self.exportAct = QAction(QIcon('import.png'), '&Export', self) self.exportAct = QAction('&Export', self) self.exportAct.setShortcut('Ctrl+E') self.importAct.setStatusTip('Import file') # self.import_csvAct = QAction(QIcon('csv.png'), '&csv', self) self.export_csvAct = QAction('&csv', self) self.export_csvAct.setStatusTip('Import csv') self.export_csvAct.triggered.connect(self.export_csv) # exitAct = QAction(QIcon('exit.png'), '&Exit', self) self.exitAct = QAction('&Exit', self) self.exitAct.setShortcut('Ctrl+Q') self.exitAct.setStatusTip('Exit application') self.exitAct.triggered.connect(qApp.quit) # self.undoAct = QAction(QIcon("icons/undo.png"),"Undo last action",self) self.undoAct = QAction('&Undo', self) self.undoAct.setStatusTip("Undo last action") self.undoAct.setShortcut("Ctrl+Z") self.undoAct.triggered.connect(self.undo) # self.redoAct = QtWidgets.QAction(QtGui.QIcon("icons/redo.png"),"Redo last undone thing",self) self.redoAct = QAction('&Redo', self) self.redoAct.setStatusTip("Redo last undone thing") self.redoAct.setShortcut("Ctrl+Y") self.redoAct.triggered.connect(self.redo) # self.clearAct = QAction(QIcon('clear.png'), '&Exit', self) self.clearAct = QAction('&Clear', self) self.clearAct.setShortcut('Ctrl+D') self.clearAct.setStatusTip('Clear calender') self.clearAct.triggered.connect(self.clear) # self.daylabelAct = QAction(QIcon('daylabel.png'), '&Toggle day Label', self) self.daylabelAct = QAction('&Toggle Day Label', self) self.daylabelAct.setCheckable(True) self.daylabelAct.setChecked(True) self.daylabelAct.setShortcut('Ctrl+G') self.daylabelAct.setStatusTip('View the day label') self.daylabelAct.triggered[bool].connect(self.daylabel) # self.timelabelAct = QAction(QIcon('timelabel.png'), '&Toggle Time Label', self) self.timelabelAct = QAction('&Toggle Time Label', self) self.timelabelAct.setCheckable(True) self.timelabelAct.setChecked(False) self.timelabelAct.setShortcut('Ctrl+T') self.timelabelAct.setStatusTip('View the time label') self.timelabelAct.triggered[bool].connect(self.timelabel) # self.fullscreenAct = QAction(QIcon('fullscreen.png'), '&FullScreen', self) self.fullscreenAct = QAction('&Fullscreen', self) self.fullscreenAct.setCheckable(True) self.fullscreenAct.setShortcut('F11') self.fullscreenAct.setStatusTip('Fullscreen') self.fullscreenAct.triggered.connect(self.fullscreen) # self.styleMenu = QAction(QIcon('theme.png'), '&Theme', self) self.styleMenu = QMenu('&Theme', self) # self.dark_styleAct = QAction(QIcon('darktheme.png'), '&Dark', self) self.dark_styleAct = QAction('&Dark ', self) self.dark_styleAct.setStatusTip('Dark') self.dark_styleAct.triggered.connect(self.dark_mode) # self.white_styleAct = QAction(QIcon('whitetheme.png'), '&White', self) self.white_styleAct = QAction('&White ', self) self.white_styleAct.setStatusTip('White theme') self.white_styleAct.triggered.connect(self.white_mode) def save(self): save = SaveCommand('save%s' % self._num_save, self.grid.week, self._num_save) new_file, filename = tempfile.mkstemp() outfile = open(filename, 'wb') serial = pickle.dump(self.grid.week, outfile) self.tmp_filename = filename saveAct = QAction(save.nickname, self) saveAct.triggered.connect(self.open_recent) self.recentMenu.addAction(saveAct) self._save_week_list.append(save) self._num_save = self._num_save + 1 def open(self): filename = QFileDialog.getOpenFileName(self, 'Open File', ".", "(*.writer)")[0] if filename: with open(self.filename, "rt") as file: pass def open_recent(self): week = pickle.load(open(self.tmp_filename, 'rb')) self.grid.week = week def compare(self): cmp_dialog = CompareDialog(parent=self) def clear(self): clear_week = ClearCommand(grid=self.grid) self.undoStack.push(clear_week) def import_csv(self): ImportFile(self.grid.week, 'FreeTime.csv').csv() def export_csv(self): ExportFile(self.grid.week, 'FreeTime.csv').csv() def undo(self): self.undoStack.undo() def redo(self): self.undoStack.redo() def daylabel(self, state): if state: self.grid.grid_date.visibility_day = True else: self.grid.grid_date.visibility_day = False def timelabel(self, state): if state: self.grid.grid_date.visibility_time = True else: self.grid.grid_date.visibility_time = False def fullscreen(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() def dark_mode(self): self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) self._style = "Dark" self.grid.grid_date.update_theme() def white_mode(self): self.setStyleSheet("") self._style = "White" self.grid.grid_date.update_theme() def initToolbar(self): self.fileMenu = self.menu_bar.addMenu('&File') self.fileMenu.addAction(self.openAct) self.fileMenu.addMenu(self.recentMenu) self.fileMenu.addAction(self.saveAct) self.fileMenu.addAction(self.compareAct) self.fileMenu.addSeparator() self.fileMenu.addMenu(self.importMenu) self.importMenu.addAction(self.import_csvAct) self.fileMenu.addMenu(self.exportMenu) self.exportMenu.addAction(self.export_csvAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.editMenu = self.menu_bar.addMenu('&Edit') self.editMenu.addAction(self.undoAct) self.editMenu.addAction(self.redoAct) self.editMenu.addSeparator() self.editMenu.addAction(self.clearAct) self.viewMenu = self.menu_bar.addMenu('&View') self.viewMenu.addMenu(self.styleMenu) self.styleMenu.addAction(self.dark_styleAct) self.styleMenu.addAction(self.white_styleAct) self.viewMenu.addSeparator() self.viewMenu.addAction(self.fullscreenAct) self.viewMenu.addSeparator() self.viewMenu.addAction(self.daylabelAct) self.viewMenu.addAction(self.timelabelAct) @property def style(self): return self._style def initUI(self): undoAction = self.undoStack.createUndoAction(self, self.tr("&Undo")) undoAction.setShortcuts(QKeySequence.Undo) redoAction = self.undoStack.createRedoAction(self, self.tr("&Redo")) redoAction.setShortcuts(QKeySequence.Redo) # Windows properties self.setWindowTitle('FreeTime') # self.setWindowIcon(QIcon('image\icon.png')) self.resize(self.window_size[0], self.window_size[1]) self.move(0, 0) # Menu self.statusBar() self.initMenuBar() self.initToolbar() # GridLayout self.grid = MainGrid(parent=self) self.setCentralWidget(self.grid) self.show()
class CustomScene(QGraphicsScene): """ Extends QGraphicsScene with undo-redo functionality """ labelAdded = pyqtSignal(shapes.QGraphicsItem) def __init__(self, *args, parent=None): super(CustomScene, self).__init__(*args, parent=parent) self.undoStack = QUndoStack(self) #Used to store undo-redo moves self.createActions( ) #creates necessary actions that need to be called for undo-redo def createActions(self): # helper function to create delete, undo and redo shortcuts self.deleteAction = QAction("Delete Item", self) self.deleteAction.setShortcut(Qt.Key_Delete) self.deleteAction.triggered.connect(self.deleteItem) self.undoAction = self.undoStack.createUndoAction(self, "Undo") self.undoAction.setShortcut(QKeySequence.Undo) self.redoAction = self.undoStack.createRedoAction(self, "Redo") self.redoAction.setShortcut(QKeySequence.Redo) def createUndoView(self, parent): # creates an undo stack view for current QGraphicsScene undoView = QUndoView(self.undoStack, parent) showUndoDialog(undoView, parent) def deleteItem(self): # (slot) used to delete all selected items, and add undo action for each of them if self.selectedItems(): for item in self.selectedItems(): self.undoStack.push(deleteCommand(item, self)) def itemMoved(self, movedItem, lastPos): #item move event, checks if item is moved self.undoStack.push(moveCommand(movedItem, lastPos)) self.advance() def addItemPlus(self, item): # extended add item method, so that a corresponding undo action is also pushed self.undoStack.push(addCommand(item, self)) def mousePressEvent(self, event): # overloaded mouse press event to check if an item was moved bdsp = event.buttonDownScenePos(Qt.LeftButton) #get click pos point = QPointF(bdsp.x(), bdsp.y()) #create a Qpoint from click pos itemList = self.items(point) #get items at said point self.movingItem = itemList[ 0] if itemList else None #set first item in list as moving item if self.movingItem and event.button() == Qt.LeftButton: self.oldPos = self.movingItem.pos( ) #if left click is held, then store old pos self.clearSelection() #clears selected items return super(CustomScene, self).mousePressEvent(event) def mouseReleaseEvent(self, event): # overloaded mouse release event to check if an item was moved if self.movingItem and event.button() == Qt.LeftButton: if self.oldPos != self.movingItem.pos(): #if item pos had changed, when mouse was realeased, emit itemMoved signal self.itemMoved(self.movingItem, self.oldPos) self.movingItem = None #clear movingitem reference return super(CustomScene, self).mouseReleaseEvent(event)
class mylabel(QLabel): print("into mylabel") def __init__(self, parent): super(mylabel, self).__init__(parent) self.image = QImage() self.drawing = True self.lastPoint = QPoint() self.modified = False self.scribbling = False self.eraserSize = 5 #橡皮擦初始值 self.fontSize = 12 #字形初始值 self.setAcceptDrops(True) self.savetextedit = [] self.text = [] #紀錄文字 self.eraserPos = [] self.temp_img = 1 #紀錄圖片編號 self.numstack = [] self.i = 0 # 紀錄textedit self.eraserClicked = False self.textClicked = False self.clearClicked = False self.undoStack = QUndoStack() self.m_undoaction = self.undoStack.createUndoAction(self, self.tr("&Undo")) # self.m_undoaction.setShortcut('Ctrl+Z') self.addAction(self.m_undoaction) self.m_redoaction = self.undoStack.createRedoAction(self, self.tr("&Redo")) # self.m_redoaction.setShortcut('Ctrl+Y') self.addAction(self.m_redoaction) #開啟圖片 def openimage(self, filename): self.image = filename self.scaredPixmap = self.image.scaled(self.width(), self.height(), aspectRatioMode=Qt.KeepAspectRatio) self.image = self.scaredPixmap self.setAlignment(Qt.AlignLeft) self.update() #獲得橡皮擦大小 def getErasersize(self, esize): self.eraserSize = int(esize) print(self.eraserSize) #獲得字體大小 def getFontsize(self, fsize): self.fontSize = int(fsize) print(self.fontSize) def mousePressEvent(self, event): print("left press") super().mousePressEvent(event) if event.button() == Qt.LeftButton and self.eraserClicked: print("start erase action") self.drawing = True self.eraserPos.clear() self.lastPoint = event.pos() self.eraserPos.append(self.lastPoint) elif event.button() == Qt.LeftButton and self.textClicked: print("add textedit") self.item = self.childAt(event.x(), event.y()) if not self.childAt(event.x(), event.y()): # 如果不是點在文字框上的話 # 文字框 self.textedit = textedit1(self) self.textedit.setObjectName("textedit{}".format(self.i)) self.textedit.setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid black;\n"); # 設定邊框 self.textedit.move(event.pos().x(), event.pos().y()) self.savetextedit.append(self.textedit) print("record textedit", self.savetextedit) # 設定文字框格式 self.textedit.setFont(QFont("Roman times", self.fontSize)) self.textedit.setLineWrapMode(QTextEdit.FixedColumnWidth) # 自動斷行 self.textedit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 隱藏滾動軸 self.textedit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 隱藏滾動軸 self.textedit.show() print(self.textedit) self.item = self.childAt(event.x(), event.y()) print("choose item", self.item) command = storeCommand(self.textedit) self.undoStack.push(command) print("undostack count", self.undoStack.count()) print("into undostack", self.textedit) if self.textedit: # 將文字框設為不能寫 x = 0 for self.savetextedit[x] in self.savetextedit: self.savetextedit[x].setReadOnly(True) self.savetextedit[x].setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid black;\n"); # 設定邊框 x = x + 1 else: self.item.setReadOnly(False) self.doc = self.item.document() self.doc.contentsChanged.connect(self.textAreaChanged) # 隨著輸入的文字改變文字框的大小 elif event.button() == Qt.RightButton: self.item = self.childAt(event.x(), event.y()) print("choose textedit", self.item) # 有存到stack裡,可是沒辦法redo,沒辦法傳進去移動過後的位置 try: if self.item: self.item.setReadOnly(True) print("moveCmd", self.item, self.item.pos()) # self.undoStack.push(moveCommand(self.item, self.item.pos())) self.undoStack.push(moveCommand(self.item, self.item.pos())) print("moveundostack count", self.undoStack.count()) except Exception as e: print(e) def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: print("delete textedit", self.item) self.item.deleteLater() self.savetextedit.remove(self.item) # 隨著輸入的文字改變文字框大小 def textAreaChanged(self): self.doc.adjustSize() newWidth = self.doc.size().width() + 20 newHeight = self.doc.size().height() + 20 if newWidth != self.item.width(): self.item.setFixedWidth(newWidth) if newHeight != self.item.height(): self.item.setFixedHeight(newHeight) def dragEnterEvent(self, e): print("drag") e.accept() def dropEvent(self, e): print("drop item") mime = e.mimeData().text() x, y = map(int, mime.split(',')) print(x,y) position = e.pos() print(position) e.source().setReadOnly(True) self.undoStack.push(moveCommand(e.source(), e.source().pos())) e.source().move(position - QPoint(x,y)) print("move item to", self.item.pos()) e.setDropAction(Qt.MoveAction) e.accept() def mouseMoveEvent(self, event): if (event.buttons() and Qt.LeftButton) and self.drawing and self.eraserClicked: print("start painting") self.path = QPainterPath() self.path.moveTo(self.lastPoint) self.path.lineTo(event.pos()) painter = QPainter(self.image) x = self.width() - self.image.width() y = self.height() - self.image.height() painter.translate(x / 2 - x, y / 2 - y) #因為將圖片置中,所以要重設置畫筆原點 painter.setPen(QPen(Qt.white, self.eraserSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawPath(self.path) print(self.path) self.lastPoint = event.pos() self.eraserPos.append(self.lastPoint) self.update() def mouseReleaseEvent(self, event): print("release mouse") if event.button() == Qt.LeftButton: self.drawing = False #儲存圖片佔存,供undo redo使用 if self.eraserClicked == True: self.imagename = "img{}.PNG".format(self.temp_img) print(self.imagename) self.image.save("./saveload/{}".format(self.imagename)) self.temp_img = self.temp_img + 1 self.numstack.append(self.imagename) def paintEvent(self, event): canvasPainter = QPainter(self) self.x = (self.width() - self.image.width())/2 self.y = (self.height() - self.image.height())/2 canvasPainter.translate(self.x, self.y) canvasPainter.drawImage(event.rect(), self.image, event.rect()) if not self.textClicked: x = 0 self.undoStack.clear() for self.savetextedit[x] in self.savetextedit: self.savetextedit[x].setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid transparent;\n"); # 設定邊框 if self.savetextedit[x].toPlainText() == "": self.savetextedit[x].deleteLater() self.savetextedit.remove(self.savetextedit[x]) x = x + 1 self.update()
class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) # undo # <http://doc.qt.io/qt-5/qtwidgets-tools-undoframework-example.html> self.undoStack = QUndoStack() self.ui.menuStyles.addSeparator() self.undoAction = self.undoStack.createUndoAction(self) self.undoAction.setShortcuts(QKeySequence.Undo) self.ui.menuStyles.addAction(self.undoAction) self.redoAction = self.undoStack.createRedoAction(self) self.redoAction.setShortcuts(QKeySequence.Redo) self.ui.menuStyles.addAction(self.redoAction) # constants self.chartPatternFilter = self.tr("ChartPattern (*.chartpattern)") try: self.XL = win32com.client.Dispatch("Excel.Application") except pythoncom.com_error: QMessageBox.warning(self, self.tr("Error"), self.tr("Excel isn't installed.")) sys.exit(0) # model self.pickedModel = models.ChartStyleModel(self) self.pickedDelegate = models.ChartPatternDelegate(self) self.ui.treeView.setModel(self.pickedModel) self.ui.treeView.setItemDelegate(self.pickedDelegate) if QT_VERSION_STR.startswith("5."): self.ui.treeView.header().setSectionsMovable(False) else: self.ui.treeView.header().setMovable(False) self.targetModel = QStringListModel([ self.tr("ActiveBook"), self.tr("ActiveSheet"), ], self) self.typeModel = QStringListModel([ self.tr("All Chart"), self.tr("Type of ActiveChart"), ], self) self.ui.comboBoxTarget.setModel(self.targetModel) self.ui.comboBoxType.setModel(self.typeModel) self.pickedModel.setUndoStack(self.undoStack) # general_progress self.general_progress = QProgressBar() self.ui.statusbar.addWidget(self.general_progress) self.general_progress.setVisible(False) # progress & progObj self.progress = QProgressBar() self.ui.statusbar.addWidget(self.progress) self.progress.setVisible(False) self.progObj = QtProgressObject() self.progObj.initialized.connect(self.on_progress_initialized) self.progObj.updated.connect(self.on_progress_updated) self.progObj.finished.connect(self.on_progress_finished) def getOpenFileName(self, filter=''): return QFileDialog.getOpenFileName(self, "", "", filter)[0] def getSaveFileName(self, filter=''): return QFileDialog.getSaveFileName(self, "", "", filter)[0] def getActiveChart(self): ActiveChart = None try: ActiveChart = self.XL.ActiveChart except AttributeError: pass if ActiveChart is None: QMessageBox.warning(self, self.tr("Error"), self.tr("Chart is not active currently.")) return None return ActiveChart @pyqtSlot() def on_actionNewStyle_triggered(self): self.pickedModel.insertRow(self.pickedModel.rowCount(), QModelIndex()) self.pickedModel.fetchMore() @pyqtSlot() def on_actionRemoveStyle_triggered(self): current = self.ui.treeView.currentIndex() if not current.isValid(): return self.pickedModel.removeRow(current.row(), QModelIndex()) @pyqtSlot() def on_actionUpStyle_triggered(self): current = self.ui.treeView.currentIndex() if not current.isValid(): return self.pickedModel.upRow(current.row()) @pyqtSlot() def on_actionDownStyle_triggered(self): current = self.ui.treeView.currentIndex() if not current.isValid(): return self.pickedModel.downRow(current.row()) @pyqtSlot() def on_actionReplicate_triggered(self): current = self.ui.treeView.currentIndex() if not current.isValid(): return self.pickedModel.replicate(current.row()) @pyqtSlot() def on_actionLoad_triggered(self): filename = self.getOpenFileName(self.chartPatternFilter) if not filename: return try: data = json.load(open(six.text_type(filename))) self.pickedModel.setStyles( [excel.Style.from_dump(x) for x in data]) except: raise QMessageBox.warning(self, self.tr("Load error"), self.tr("Failed to load the chart pattern.")) @pyqtSlot() def on_actionSave_triggered(self): filename = self.getSaveFileName(self.chartPatternFilter) if not filename: return data = [x.dump() for x in self.pickedModel.styles()] fp = open(six.text_type(filename), "w", encoding="utf-8") json.dump(data, fp, indent=1) @pyqtSlot() def on_pushButtonPickup_clicked(self): try: chart = self.getActiveChart() if chart is None: return styles = excel.collect_styles(chart, prog=self.progObj) self.pickedModel.setStyles(styles) except pythoncom.com_error: QMessageBox.warning( self, self.tr("Collect error"), self.tr("Failed collecting from the ActiveChart.")) @pyqtSlot() def on_pushButtonApplyChart_clicked(self): styles = self.pickedModel.styles() try: chart = self.getActiveChart() if chart is None: return excel.apply_styles(chart, styles, prog=self.progObj) except pythoncom.com_error: QMessageBox.warning(self, self.tr("Apply error"), self.tr("Failed applied in the ActiveChart.")) @pyqtSlot() def on_pushButtonApplyBook_clicked(self): styles = self.pickedModel.styles() target_mode = self.ui.comboBoxTarget.currentIndex() target_type = self.ui.comboBoxType.currentIndex() self.general_progress.setValue(0) self.general_progress.setRange(0, 0) self.general_progress.setVisible(True) try: type_filter = None # default: All Chart if target_type == 1: # ActiveChart Type chart = self.getActiveChart() if chart is None: return type_filter = chart.ChartType book = self.XL.ActiveWorkbook if book is None: raise pythoncom.com_error sheets = [] # ActiveBook if target_mode == 0: for i in excel.com_range(book.Worksheets.Count): sheets.append(book.Worksheets(i)) # ActiveSheet elif target_mode == 1: sheets = [self.XL.ActiveSheet] # calc chart count self.on_progress_initialized(len(sheets)) charts = [] for c, sheet in enumerate(sheets, 1): self.on_progress_updated(c) for i in excel.com_range(sheet.ChartObjects().Count): chart = sheet.ChartObjects(i).Chart if type_filter is not None and chart.ChartType != type_filter: continue charts.append(chart) self.general_progress.setRange(0, len(charts)) aborted = False for i, chart in enumerate(charts): if aborted: break self.general_progress.setValue(i) try: excel.apply_styles(chart, styles, prog=self.progObj) except pythoncom.com_error: chart.Parent.Activate() buttons = QMessageBox.Ok | QMessageBox.Abort ret = QMessageBox.warning( self, self.tr("Apply error"), self.tr("Failed applied in the ActiveChart."), buttons) if ret == QMessageBox.Abort: aborted = True except pythoncom.com_error: QMessageBox.warning(self, self.tr("Apply error"), self.tr("Failed applied.")) self.general_progress.setVisible(False) @pyqtSlot(QModelIndex) def on_treeView_doubleClicked(self, index): if index.column() not in [1, 2, 4]: return color = QColor(index.data(Qt.EditRole)) dialog = QColorDialog(self) dialog.setCurrentColor(color) if dialog.exec_() == QDialog.Accepted: self.pickedModel.setData(index, dialog.currentColor()) @pyqtSlot(int) def on_progress_initialized(self, maximum): self.progress.setValue(0) self.progress.setRange(0, maximum) self.progress.setVisible(True) @pyqtSlot(int) def on_progress_updated(self, value): self.progress.setValue(value) @pyqtSlot() def on_progress_finished(self): self.progress.setVisible(False) self.progress.setValue(0)
class GMPage: @property def document(self): return self._doc @property def index(self): return self._index @property def page_image(self): return self._page_image @property def drawing_image(self): return self._drawing_image @property def command_count(self): return self._undo_stack.count() @property def can_undo(self): return self._undo_stack.canUndo() @property def can_redo(self): return self._undo_stack.canRedo() def __init__(self, document, mu_page, index): assert isinstance(document, GMDoc) assert isinstance(mu_page, fitz.Page) assert type(index) is int self._doc = document self._index = index self._mu_page = mu_page # zoom in to get higher resolution pixmap # todo: add this param to preferences dialog zoom = 2 matrix = fitz.Matrix(zoom, zoom) page_data = self._mu_page.getPixmap(matrix=matrix, alpha=False).getImageData() self._undo_stack = QUndoStack() self._page_image = QPixmap() self._page_image.loadFromData(page_data) self._drawing_image = generateDrawingPixmap(self._page_image) self._last_drawing_hash = None self.captureState() def getData(self, alpha=False): return self._mu_page.getPixmap(alpha=alpha).getImageData() def getSize(self): return self._page_image.size() def setDrawingPixmap(self, pixmap): self._drawing_image = pixmap def getMergedPixmap(self): result = QPixmap(self.getSize()) result.fill(QColor(255, 255, 255)) painter = QPainter(result) painter.setRenderHint(QPainter.HighQualityAntialiasing, True) painter.drawPixmap(QPoint(0, 0), self._page_image) painter.drawPixmap(QPoint(0, 0), self._drawing_image) painter.end() return result def getUndoAction(self, parent): return self._undo_stack.createUndoAction(parent) def getRedoAction(self, parent): return self._undo_stack.createRedoAction(parent) def pushCommand(self, command): assert isinstance(command, QUndoCommand) self._undo_stack.push(command) def captureState(self): self._last_drawing_hash = self._drawing_image.cacheKey() self._undo_stack.setClean() def changed(self): if self._undo_stack.isClean(): return False return self._undo_stack.count() > 0 def undo(self): self._undo_stack.undo() def redo(self): self._undo_stack.redo()
class jide(QMainWindow): """This is the primary class which serves as the glue for JIDE. This class interfaces between the various canvases, pixel and color palettes, centralized data source, and data output routines. """ def __init__(self): """jide constructor """ super().__init__() self.setupWindow() self.setupTabs() self.setupDocks() self.setupToolbar() self.setupActions() self.setupStatusBar() self.setupPrefs() def setupWindow(self): """Entry point to set up primary window attributes """ self.setWindowTitle("JIDE") self.sprite_view = QGraphicsView() self.tile_view = QGraphicsView() self.sprite_view.setStyleSheet("background-color: #494949;") self.tile_view.setStyleSheet("background-color: #494949;") def setupDocks(self): """Set up pixel palette, color palette, and tile map docks """ self.sprite_color_palette_dock = ColorPaletteDock(Source.SPRITE, self) self.sprite_pixel_palette_dock = PixelPaletteDock(Source.SPRITE, self) self.tile_color_palette_dock = ColorPaletteDock(Source.TILE, self) self.tile_pixel_palette_dock = PixelPaletteDock(Source.TILE, self) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_color_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_pixel_palette_dock) self.removeDockWidget(self.tile_color_palette_dock) self.removeDockWidget(self.tile_pixel_palette_dock) def setupToolbar(self): """Set up graphics tools toolbar """ self.canvas_toolbar = QToolBar() self.addToolBar(Qt.LeftToolBarArea, self.canvas_toolbar) self.tool_actions = QActionGroup(self) self.select_tool = QAction(QIcon(":/icons/select_tool.png"), "&Select tool", self.tool_actions) self.select_tool.setShortcut("S") self.pen_tool = QAction(QIcon(":/icons/pencil_tool.png"), "&Pen tool", self.tool_actions) self.pen_tool.setShortcut("P") self.fill_tool = QAction(QIcon(":/icons/fill_tool.png"), "&Fill tool", self.tool_actions) self.fill_tool.setShortcut("G") self.line_tool = QAction(QIcon(":/icons/line_tool.png"), "&Line tool", self.tool_actions) self.line_tool.setShortcut("L") self.rect_tool = QAction( QIcon(":/icons/rect_tool.png"), "&Rectangle tool", self.tool_actions, ) self.rect_tool.setShortcut("R") self.ellipse_tool = QAction( QIcon(":/icons/ellipse_tool.png"), "&Ellipse tool", self.tool_actions, ) self.ellipse_tool.setShortcut("E") self.tools = [ self.select_tool, self.pen_tool, self.fill_tool, self.line_tool, self.rect_tool, self.ellipse_tool, ] for tool in self.tools: tool.setCheckable(True) tool.setEnabled(False) self.canvas_toolbar.addAction(tool) def setupTabs(self): """Set up main window sprite/tile/tile map tabs """ self.canvas_tabs = QTabWidget() self.canvas_tabs.addTab(self.sprite_view, "Sprites") self.canvas_tabs.addTab(self.tile_view, "Tiles") self.canvas_tabs.setTabEnabled(0, False) self.canvas_tabs.setTabEnabled(1, False) self.setCentralWidget(self.canvas_tabs) def setupActions(self): """Set up main menu actions """ # Exit exit_act = QAction("&Exit", self) exit_act.setShortcut("Ctrl+Q") exit_act.setStatusTip("Exit application") exit_act.triggered.connect(qApp.quit) # Open file open_file = QAction("&Open", self) open_file.setShortcut("Ctrl+O") open_file.setStatusTip("Open file") open_file.triggered.connect(self.selectFile) # Open preferences open_prefs = QAction("&Preferences", self) open_prefs.setStatusTip("Edit preferences") open_prefs.triggered.connect(self.openPrefs) # Undo/redo self.undo_stack = QUndoStack(self) undo_act = self.undo_stack.createUndoAction(self, "&Undo") undo_act.setShortcut(QKeySequence.Undo) redo_act = self.undo_stack.createRedoAction(self, "&Redo") redo_act.setShortcut(QKeySequence.Redo) # Copy/paste self.copy_act = QAction("&Copy", self) self.copy_act.setShortcut("Ctrl+C") self.copy_act.setStatusTip("Copy") self.copy_act.setEnabled(False) self.paste_act = QAction("&Paste", self) self.paste_act.setShortcut("Ctrl+V") self.paste_act.setStatusTip("Paste") self.paste_act.setEnabled(False) # JCAP compile/load self.gendat_act = QAction("&Generate DAT Files", self) self.gendat_act.setShortcut("Ctrl+D") self.gendat_act.setStatusTip("Generate DAT Files") self.gendat_act.triggered.connect(self.genDATFiles) self.gendat_act.setEnabled(False) self.load_jcap = QAction("&Load JCAP System", self) self.load_jcap.setShortcut("Ctrl+L") self.load_jcap.setStatusTip("Load JCAP System") self.load_jcap.setEnabled(False) self.load_jcap.triggered.connect(self.loadJCAP) # Build menu bar menu_bar = self.menuBar() file_menu = menu_bar.addMenu("&File") file_menu.addAction(open_file) file_menu.addSeparator() file_menu.addAction(open_prefs) file_menu.addAction(exit_act) edit_menu = menu_bar.addMenu("&Edit") edit_menu.addAction(undo_act) edit_menu.addAction(redo_act) edit_menu.addAction(self.copy_act) edit_menu.addAction(self.paste_act) jcap_menu = menu_bar.addMenu("&JCAP") jcap_menu.addAction(self.gendat_act) jcap_menu.addAction(self.load_jcap) def setupStatusBar(self): """Set up bottom status bar """ self.statusBar = self.statusBar() def setupPrefs(self): QCoreApplication.setOrganizationName("Connor Spangler") QCoreApplication.setOrganizationDomain("https://github.com/cspang1") QCoreApplication.setApplicationName("JIDE") self.prefs = QSettings() self.prefs.setValue("test", 69) @pyqtSlot(bool) def setCopyActive(self, active): """Set whether the copy action is available :param active: Variable representing whether copy action should be set to available or unavailable :type active: bool """ self.copy_act.isEnabled(active) @pyqtSlot(bool) def setPasteActive(self, active): """Set whether the paste action is available :param active: Variable representing whether paste action should be set to available or unavailable :type active: bool """ self.paste_act.isEnabled(active) def selectFile(self): """Open file action to hand file handle to GameData """ file_name, _ = QFileDialog.getOpenFileName( self, "Open file", "", "JCAP Resource File (*.jrf)") self.loadProject(file_name) def loadProject(self, file_name): """Load project file data and populate UI elements/set up signals and slots """ if file_name: try: self.data = GameData.fromFilename(file_name, self) except KeyError: QMessageBox( QMessageBox.Critical, "Error", "Unable to load project due to malformed data", ).exec() return except OSError: QMessageBox( QMessageBox.Critical, "Error", "Unable to open project file", ).exec() return else: return self.setWindowTitle("JIDE - " + self.data.getGameName()) self.gendat_act.setEnabled(True) self.load_jcap.setEnabled(True) self.data.setUndoStack(self.undo_stack) self.sprite_scene = GraphicsScene(self.data, Source.SPRITE, self) self.tile_scene = GraphicsScene(self.data, Source.TILE, self) self.sprite_view = GraphicsView(self.sprite_scene, self) self.tile_view = GraphicsView(self.tile_scene, self) self.sprite_view.setStyleSheet("background-color: #494949;") self.tile_view.setStyleSheet("background-color: #494949;") sprite_pixel_palette = self.sprite_pixel_palette_dock.pixel_palette tile_pixel_palette = self.tile_pixel_palette_dock.pixel_palette sprite_color_palette = self.sprite_color_palette_dock.color_palette tile_color_palette = self.tile_color_palette_dock.color_palette sprite_pixel_palette.subject_selected.connect( self.sprite_scene.setSubject) self.sprite_scene.set_color_switch_enabled.connect( sprite_color_palette.color_preview.setColorSwitchEnabled) self.sprite_color_palette_dock.palette_updated.connect( self.sprite_scene.setColorPalette) self.sprite_color_palette_dock.palette_updated.connect( self.sprite_pixel_palette_dock.palette_updated) sprite_color_palette.color_selected.connect( self.sprite_scene.setPrimaryColor) tile_pixel_palette.subject_selected.connect(self.tile_scene.setSubject) self.tile_scene.set_color_switch_enabled.connect( tile_color_palette.color_preview.setColorSwitchEnabled) self.tile_color_palette_dock.palette_updated.connect( self.tile_scene.setColorPalette) self.tile_color_palette_dock.palette_updated.connect( self.tile_pixel_palette_dock.palette_updated) tile_color_palette.color_selected.connect( self.tile_scene.setPrimaryColor) self.sprite_color_palette_dock.setup(self.data) self.tile_color_palette_dock.setup(self.data) self.sprite_pixel_palette_dock.setup(self.data) self.tile_pixel_palette_dock.setup(self.data) self.canvas_tabs = QTabWidget() self.canvas_tabs.addTab(self.sprite_view, "Sprites") self.canvas_tabs.addTab(self.tile_view, "Tiles") self.canvas_tabs.setTabEnabled(0, True) self.canvas_tabs.setTabEnabled(1, True) self.setCentralWidget(self.canvas_tabs) self.canvas_tabs.currentChanged.connect(self.setCanvas) self.setCanvas(0) self.data.col_pal_updated.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.col_pal_renamed.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.col_pal_added.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.col_pal_removed.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.pix_batch_updated.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.row_count_updated.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.select_tool.triggered.connect( lambda checked, tool=Tools.SELECT: self.sprite_scene.setTool(tool)) self.select_tool.triggered.connect( lambda checked, tool=Tools.SELECT: self.tile_scene.setTool(tool)) self.pen_tool.triggered.connect( lambda checked, tool=Tools.PEN: self.sprite_scene.setTool(tool)) self.pen_tool.triggered.connect( lambda checked, tool=Tools.PEN: self.tile_scene.setTool(tool)) self.fill_tool.triggered.connect(lambda checked, tool=Tools.FLOODFILL: self.sprite_scene.setTool(tool)) self.fill_tool.triggered.connect(lambda checked, tool=Tools.FLOODFILL: self.tile_scene.setTool(tool)) self.line_tool.triggered.connect( lambda checked, tool=Tools.LINE: self.sprite_scene.setTool(tool)) self.line_tool.triggered.connect( lambda checked, tool=Tools.LINE: self.tile_scene.setTool(tool)) self.rect_tool.triggered.connect(lambda checked, tool=Tools.RECTANGLE: self.sprite_scene.setTool(tool)) self.rect_tool.triggered.connect(lambda checked, tool=Tools.RECTANGLE: self.tile_scene.setTool(tool)) self.ellipse_tool.triggered.connect(lambda checked, tool=Tools.ELLIPSE: self.sprite_scene.setTool(tool)) self.ellipse_tool.triggered.connect( lambda checked, tool=Tools.ELLIPSE: self.tile_scene.setTool(tool)) for tool in self.tools: tool.setEnabled(True) self.pen_tool.setChecked(True) self.pen_tool.triggered.emit(True) def setCanvas(self, index): """Set the dock and signal/slot layout to switch between sprite/tile/ tile map tabs :param index: Index of canvas tab :type index: int """ self.paste_act.triggered.disconnect() self.copy_act.triggered.disconnect() if index == 0: self.copy_act.triggered.connect(self.sprite_scene.copy) self.paste_act.triggered.connect(self.sprite_scene.startPasting) self.tile_color_palette_dock.hide() self.tile_pixel_palette_dock.hide() self.sprite_color_palette_dock.show() self.sprite_pixel_palette_dock.show() self.removeDockWidget(self.tile_color_palette_dock) self.removeDockWidget(self.tile_pixel_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_color_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_pixel_palette_dock) self.copy_act.triggered.connect(self.sprite_scene.copy) self.paste_act.triggered.connect(self.sprite_scene.startPasting) self.sprite_scene.region_copied.connect(self.paste_act.setEnabled) self.sprite_scene.region_selected.connect(self.copy_act.setEnabled) elif index == 1: self.copy_act.triggered.connect(self.tile_scene.copy) self.paste_act.triggered.connect(self.tile_scene.startPasting) self.sprite_color_palette_dock.hide() self.sprite_pixel_palette_dock.hide() self.tile_color_palette_dock.show() self.tile_pixel_palette_dock.show() self.removeDockWidget(self.sprite_color_palette_dock) self.removeDockWidget(self.sprite_pixel_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.tile_color_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.tile_pixel_palette_dock) self.copy_act.triggered.connect(self.tile_scene.copy) self.paste_act.triggered.connect(self.tile_scene.startPasting) self.tile_scene.region_copied.connect(self.paste_act.setEnabled) self.tile_scene.region_selected.connect(self.copy_act.setEnabled) def openPrefs(self): prefs = Preferences() prefs.exec() def genDATFiles(self): """Generate .dat files from project for use by JCAP """ dat_path = Path(__file__).parents[1] / "data" / "DAT Files" dat_path.mkdir(exist_ok=True) tcp_path = dat_path / "tile_color_palettes.dat" tpp_path = dat_path / "tiles.dat" scp_path = dat_path / "sprite_color_palettes.dat" spp_path = dat_path / "sprites.dat" tile_pixel_data = self.data.getPixelPalettes(Source.TILE) tile_color_data = self.data.getColPals(Source.TILE) sprite_pixel_data = self.data.getPixelPalettes(Source.SPRITE) sprite_color_data = self.data.getColPals(Source.SPRITE) self.genPixelDATFile(tile_pixel_data, tpp_path) self.genColorDATFile(tile_color_data, tcp_path) self.genPixelDATFile(sprite_pixel_data, spp_path) self.genColorDATFile(sprite_color_data, scp_path) def genPixelDATFile(self, source, path): """Generate sprite/tile pixel palette .dat file :param source: List containing sprite/tile pixel data :type source: list :param path: File path to .dat :type path: str """ with path.open("wb") as dat_file: for element in source: for line in element: total = 0 for pixel in line: total = (total << 4) + pixel dat_file.write(total.to_bytes(4, byteorder="big")[::-1]) def genColorDATFile(self, source, path): """Generate sprite/tile color palette .dat file :param source: List containing sprite/tile color data :type source: list :param path: File path to .dat :type path: str """ with path.open("wb") as dat_file: for palette in source: for color in palette: r, g, b = downsample(color.red(), color.green(), color.blue()) rgb = (r << 5) | (g << 2) | (b) dat_file.write(bytes([rgb])) def loadJCAP(self): """Generate .dat files and execute command-line serial loading of JCAP """ self.statusBar.showMessage("Loading JCAP...") self.genDATFiles() dat_path = Path(__file__).parents[1] / "data" / "DAT Files" jcap_path = Path(__file__).parents[2] / "jcap" / "dev" / "software" sysload_path = jcap_path / "sysload.sh" for dat_file in dat_path.glob("**/*"): shutil.copy(str(dat_file), str(jcap_path)) self.prefs.beginGroup("ports") if not self.prefs.contains("cpu_port") or not self.prefs.contains( "gpu_port"): # Popup error self.openPrefs() return cpu_port = self.prefs.value("cpu_port") gpu_port = self.prefs.value("gpu_port") self.prefs.endGroup() result = subprocess.run( ["bash.exe", str(sysload_path), "-c", cpu_port, "-g", gpu_port], capture_output=True, ) print(result.stderr) self.statusBar.showMessage("JCAP Loaded!", 5000)
class SchmereoMainWindow(QtWidgets.QMainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ui = uic.loadUi( uifile=pkg_resources.resource_stream("schmereo", "schmereo.ui"), baseinstance=self, ) # Platform-specific semantic keyboard shortcuts cannot be set in Qt Designer self.ui.actionNew.setShortcut(QKeySequence.New) self.ui.actionOpen.setShortcut(QKeySequence.Open) self.ui.actionQuit.setShortcut( QKeySequence.Quit) # no effect on Windows self.ui.actionSave.setShortcut(QKeySequence.Save) self.ui.actionSave_Project_As.setShortcut(QKeySequence.SaveAs) self.ui.actionZoom_In.setShortcuts( [QKeySequence.ZoomIn, "Ctrl+="]) # '=' so I don't need to press SHIFT self.ui.actionZoom_Out.setShortcut(QKeySequence.ZoomOut) # self.recent_files = RecentFileList( open_file_slot=self.load_file, settings_key="recent_files", menu=self.ui.menuRecent_Files, ) # Link views self.shared_camera = Camera() self.ui.leftImageWidget.camera = self.shared_camera self.ui.rightImageWidget.camera = self.shared_camera # self.ui.leftImageWidget.file_dropped.connect(self.load_left_file) self.ui.rightImageWidget.file_dropped.connect(self.load_right_file) # self.ui.leftImageWidget.image.transform.center = FractionalImagePos( -0.5, 0) self.ui.rightImageWidget.image.transform.center = FractionalImagePos( +0.5, 0) # for w in (self.ui.leftImageWidget, self.ui.rightImageWidget): w.messageSent.connect(self.ui.statusbar.showMessage) # self.marker_set = list() self.zoom_increment = 1.10 self.image_saver = ImageSaver(self.ui.leftImageWidget, self.ui.rightImageWidget) # TODO: object for AddMarker tool button tb = self.ui.addMarkerToolButton tb.setDefaultAction(self.ui.actionAdd_Marker) sz = 32 tb.setFixedSize(sz, sz) tb.setIconSize(QtCore.QSize(sz, sz)) hb = self.ui.handModeToolButton hb.setDefaultAction(self.ui.actionHand_Mode) hb.setFixedSize(sz, sz) hb.setIconSize(QtCore.QSize(sz, sz)) _set_action_icon( self.ui.actionAdd_Marker, "schmereo.marker", "crosshair64.png", "crosshair64blue.png", ) _set_action_icon(self.ui.actionHand_Mode, "schmereo", "cursor-openhand20.png") # tb.setDragEnabled(True) # TODO: drag tool button to place marker self.marker_manager = MarkerManager(self) self.aligner = Aligner(self) self.project_file_name = None # self.undo_stack = QUndoStack(self) undo_action = self.undo_stack.createUndoAction(self, '&Undo') undo_action.setShortcuts(QKeySequence.Undo) redo_action = self.undo_stack.createRedoAction(self, '&Redo') redo_action.setShortcuts(QKeySequence.Redo) self.undo_stack.cleanChanged.connect(self.on_undoStack_cleanChanged) # self.ui.menuEdit.insertAction(self.ui.actionAlign_Now, undo_action) self.ui.menuEdit.insertAction(self.ui.actionAlign_Now, redo_action) self.ui.menuEdit.insertSeparator(self.ui.actionAlign_Now) self.clip_box = ClipBox(parent=self, camera=self.shared_camera, images=[i.image for i in self.eye_widgets()]) self.ui.actionResolve_Clip_Box.triggered.connect( self.recenter_clip_box) for w in self.eye_widgets(): w.undo_stack = self.undo_stack w.clip_box = self.clip_box self.clip_box.changed.connect(w.update) self.project_folder = None def check_save(self) -> bool: if self.undo_stack.isClean(): return True # OK to do whatever now result = QMessageBox.warning( self, "The project has been modified.", "The project has been modified.\n" "Do you want to save your changes?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save, ) if result == QMessageBox.Save: if self.project_file_name is None: return self.on_actionSave_Project_As_triggered() else: return self.save_project_file(self.project_file_name) elif result == QMessageBox.Discard: return True # OK to do whatever now elif result == QMessageBox.Cancel: return False else: # Unexpected to get here? return False # cancel / abort def closeEvent(self, event: QCloseEvent): if self.check_save(): event.accept() else: event.ignore() def eye_widgets(self): for w in (self.ui.leftImageWidget, self.ui.rightImageWidget): yield w def keyReleaseEvent(self, event: QtGui.QKeyEvent): if event.key() == Qt.Key_Escape: self.marker_manager.set_marker_mode(False) def load_left_file(self, file_name: str) -> None: self.load_file(file_name) def load_right_file(self, file_name: str) -> None: self.load_file(file_name) @QtCore.pyqtSlot(str) def load_file(self, file_name: str) -> bool: result = False self.log_message(f"Loading file {file_name}...") try: image = Image.open(file_name) except OSError: return self.load_project(file_name) result = self.ui.leftImageWidget.load_image(file_name) if result: result = self.ui.rightImageWidget.load_image(file_name) if result: self.ui.leftImageWidget.update() self.ui.rightImageWidget.update() self.recent_files.add_file(file_name) self.project_folder = os.path.dirname(file_name) else: self.log_message(f"ERROR: Image load failed.") return result def load_project(self, file_name): with open(file_name, "r") as fh: data = json.load(fh) self.from_dict(data) for w in self.eye_widgets(): w.update() self.recent_files.add_file(file_name) self.project_file_name = file_name self.project_folder = os.path.dirname(file_name) self.setWindowFilePath(self.project_file_name) self.undo_stack.clear() self.undo_stack.setClean() return True def log_message(self, message: str) -> None: self.ui.statusbar.showMessage(message) @QtCore.pyqtSlot() def on_actionAbout_Schmereo_triggered(self): QtWidgets.QMessageBox.about( self, "About Schmereo", inspect.cleandoc(f""" Schmereo stereograph restoration application Version: {__version__} Author: Christopher M. Bruns Code: https://github.com/cmbruns/schmereo """), ) @QtCore.pyqtSlot() def on_actionAlign_Now_triggered(self): self.clip_box.recenter() self.undo_stack.push(AlignNowCommand(self)) @QtCore.pyqtSlot() def on_actionClear_Markers_triggered(self): self.undo_stack.push(ClearMarkersCommand(*self.eye_widgets())) @QtCore.pyqtSlot() def on_actionNew_triggered(self): if not self.check_save(): return self.project_folder = None self.project_file_name = None self.setWindowFilePath("untitled") self.shared_camera.reset() for w in self.eye_widgets(): w.image.transform.reset() self.undo_stack.clear() self.undo_stack.setClean() @QtCore.pyqtSlot() def on_actionOpen_triggered(self): folder = None if folder is None: folder = self.project_folder if folder is None: folder = "" file_name, file_type = QtWidgets.QFileDialog.getOpenFileName( parent=self, caption="Load Image", directory=folder, filter="Projects and Images (*.json *.tif);;All Files (*)", ) if file_name is None: return if len(file_name) < 1: return self.load_file(file_name) @QtCore.pyqtSlot() def on_actionQuit_triggered(self): if self.check_save(): QtCore.QCoreApplication.quit() @QtCore.pyqtSlot() def on_actionReport_a_Problem_triggered(self): url = QtCore.QUrl("https://github.com/cmbruns/schmereo/issues") QtGui.QDesktopServices.openUrl(url) @QtCore.pyqtSlot() def on_actionSave_triggered(self): if self.project_file_name is None: return self.clip_box.recenter() self.save_project_file(self.project_file_name) @QtCore.pyqtSlot() def on_actionSave_Images_triggered(self): if not self.image_saver.can_save(): return self.clip_box.recenter() path = "" if self.project_file_name is not None: path = f"{os.path.splitext(self.project_file_name)[0]}.pns" elif self.project_folder is not None: path = self.project_folder file_name, file_type = QtWidgets.QFileDialog.getSaveFileName( parent=self, caption="Save File(s)", directory=path, filter="3D Images (*.pns *.jps)", ) if file_name is None: return if len(file_name) < 1: return bs = self.clip_box.size self.image_saver.eye_size = (int(bs[0]), int(bs[1])) self.image_saver.save_image(file_name, file_type) if self.project_folder is None: self.project_folder = os.path.dirname(file_name) @QtCore.pyqtSlot() def on_actionSave_Project_As_triggered(self) -> bool: path = "" if self.project_file_name is not None: path = os.path.dirname(self.project_file_name) elif self.project_folder is not None: path = self.project_folder file_name, file_type = QtWidgets.QFileDialog.getSaveFileName( parent=self, caption="Save Project", directory=path, filter="Schmereo Projects (*.json);;All Files (*)", ) if file_name is None: return False if len(file_name) < 1: return False return self.save_project_file(file_name) @QtCore.pyqtSlot() def on_actionZoom_In_triggered(self): self.zoom(amount=self.zoom_increment) @QtCore.pyqtSlot() def on_actionZoom_Out_triggered(self): self.zoom(amount=1.0 / self.zoom_increment) @QtCore.pyqtSlot(bool) def on_undoStack_cleanChanged(self, is_clean: bool): self.ui.actionSave.setEnabled(not is_clean) doc_title = "untitled" if self.project_file_name is not None: doc_title = self.project_file_name if not is_clean: doc_title = f"{doc_title}*" self.setWindowFilePath(doc_title) def recenter_clip_box(self): self.clip_box.recenter() self.clip_box.notify() self.camera.notify() def save_project_file(self, file_name) -> bool: with open(file_name, "w") as fh: self.clip_box.recenter() json.dump(self.to_dict(), fh, indent=2) self.recent_files.add_file(file_name) self.setWindowFilePath(file_name) self.project_file_name = file_name self.project_folder = os.path.dirname(file_name) self.undo_stack.setClean() return True return False def to_dict(self): self.clip_box.recenter() # Normalize values before serialization return { "app": { "name": "schmereo", "version": __version__ }, "clip_box": self.clip_box.to_dict(), "left": self.ui.leftImageWidget.to_dict(), "right": self.ui.rightImageWidget.to_dict(), } def from_dict(self, data): self.ui.leftImageWidget.from_dict(data["left"]) self.ui.rightImageWidget.from_dict(data["right"]) if "clip_box" in data: self.clip_box.from_dict(data["clip_box"]) @QtCore.pyqtSlot() def zoom(self, amount: float): # In case the zoom is not linked between the two image widgets... widgets = (self.ui.leftImageWidget, self.ui.rightImageWidget) # store zoom values in case the cameras are all the same zooms = [w.camera.zoom for w in widgets] for idx, w in enumerate(widgets): w.camera.zoom = zooms[idx] * amount for w in widgets: w.camera.notify() # repaint now
class MainWindow(QMainWindow, Ui_MainWindow): """Main window for a sequencing project""" def __init__(self, parent=None): """Constructor. parent -- Parent QObject. """ QMainWindow.__init__(self, parent) self.setupUi(self) # Set up menu actions and undo stack. self._undo_stack = QUndoStack(self) self.actionUndo.triggered.connect( self._undo_stack.createUndoAction(self).trigger) self.actionRedo.triggered.connect( self._undo_stack.createRedoAction(self).trigger) self._undo_stack.canRedoChanged.connect(self._set_can_redo) self._undo_stack.canUndoChanged.connect(self._set_can_undo) self._undo_stack.cleanChanged.connect(self._set_undo_stack_clean) # TODO: Create default project or load project. For now, a test project is created instead. project_tree_root = DirProjectTreeNode('root') child_dir_0 = DirProjectTreeNode('childDir0', project_tree_root) DirProjectTreeNode('childDir1', project_tree_root) grandchild_seq = SequenceProjectTreeNode('grandchildSequence', child_dir_0) child_note = NonInstancedSequenceComponentNode( 'childNote', grandchild_seq, component_type=NoteSequenceComponentType) child_note.parent = grandchild_seq.root_seq_component_node NonInstancedSequenceComponentNode('childAutomation', grandchild_seq, component_type=AutomationSequenceComponentType).parent = \ grandchild_seq.root_seq_component_node NonInstancedSequenceComponentNode('childWaveTable', grandchild_seq, component_type=WaveTableSequenceComponentType).parent = \ grandchild_seq.root_seq_component_node child_note.set_custom_prop( 'prop0', CustomSequenceComponentPropValue('val0', False)) child_note.set_custom_prop( 'prop1', CustomSequenceComponentPropValue('[1, 2]', True)) child_note.set_custom_prop( 'prop2', CustomSequenceComponentPropValue('val2', False)) # Set up project change controllers. project_tree_controller = ProjectTreeController(self) seq_component_tree_controller = SequenceComponentTreeController(self) seq_component_node_controller = SequenceComponentNodeController(self) # Set up Qt model objects. project_tree_qt_model = ProjectTreeQtModel(project_tree_root, self._undo_stack, project_tree_controller, self) seq_component_tree_qt_model = SequenceComponentTreeQtModel( self._undo_stack, project_tree_controller, seq_component_tree_controller, seq_component_node_controller, self) seq_component_custom_props_qt_model = SequenceComponentCustomPropsQtModel( self._undo_stack, seq_component_tree_controller, seq_component_node_controller, self) # Set up GUI. project_tree_widget = ProjectTreeWidget(self._undo_stack, project_tree_qt_model, project_tree_controller, self) self.projectTreePlaceholder.addWidget(project_tree_widget) seq_component_tree_widget = SequenceComponentTreeWidget( self._undo_stack, seq_component_tree_qt_model, project_tree_controller, seq_component_tree_controller, seq_component_node_controller, self) self.seqComponentTreePlaceholder.addWidget(seq_component_tree_widget) seq_component_props_widget = SequenceComponentPropsWidget( self._undo_stack, seq_component_custom_props_qt_model, self) self.seqComponentPropsPlaceholder.addWidget(seq_component_props_widget) # Give project change controllers the necessary references to Qt model objects. project_tree_controller.project_tree_qt_model = project_tree_qt_model project_tree_controller.seq_component_tree_qt_model = seq_component_tree_qt_model seq_component_tree_controller.seq_component_tree_qt_model = seq_component_tree_qt_model seq_component_tree_controller.seq_component_custom_props_qt_model = seq_component_custom_props_qt_model def _set_can_redo(self, can_redo): """Called when the status of whether we have an action to redo changes. can_redo -- Boolean indicating whether we can currently redo. """ self.actionRedo.setEnabled(can_redo) redo_action_text = QCoreApplication.translate('MainWindow', '&Redo') self.actionRedo.setText(redo_action_text + ' {}'.format(self._undo_stack.redoText()) if can_redo else redo_action_text) def _set_can_undo(self, can_undo): """Called when the status of whether we have an action to undo changes. can_undo -- Boolean indicating whether we can currently undo. """ self.actionUndo.setEnabled(can_undo) undo_action_text = QCoreApplication.translate('MainWindow', '&Undo') self.actionUndo.setText(undo_action_text + ' {}'.format(self._undo_stack.undoText()) if can_undo else undo_action_text) def _set_undo_stack_clean(self, is_clean): """Called when the undo stack changes clean status. is_clean -- Boolean indicating whether the undo stack is currently in a clean state. """ self.actionSaveProject.setEnabled(not is_clean) window_title = QCoreApplication.translate('MainWindow', 'ScriptASeq') self.setWindowTitle(window_title if is_clean else window_title + '*')