示例#1
0
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)
示例#2
0
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()
示例#3
0
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)
示例#4
0
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()
示例#5
0
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)
示例#6
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()
示例#7
0
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)
示例#8
0
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
示例#9
0
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 + '*')