diff options
Diffstat (limited to 'src')
30 files changed, 1411 insertions, 0 deletions
diff --git a/src/build/settings/base.json b/src/build/settings/base.json new file mode 100644 index 0000000..e6321bb --- /dev/null +++ b/src/build/settings/base.json @@ -0,0 +1,6 @@ +{ + "app_name": "PFD-Tool", + "author": "FOSSEE", + "main_module": "src/main/python/main.py", + "version": "0.0.0" +}
\ No newline at end of file diff --git a/src/build/settings/linux.json b/src/build/settings/linux.json new file mode 100644 index 0000000..7a64c95 --- /dev/null +++ b/src/build/settings/linux.json @@ -0,0 +1,6 @@ +{ + "categories": "Utility;", + "description": "", + "author_email": "", + "url": "" +}
\ No newline at end of file diff --git a/src/build/settings/mac.json b/src/build/settings/mac.json new file mode 100644 index 0000000..f7bd610 --- /dev/null +++ b/src/build/settings/mac.json @@ -0,0 +1,3 @@ +{ + "mac_bundle_identifier": "" +}
\ No newline at end of file diff --git a/src/main/icons/Icon.ico b/src/main/icons/Icon.ico Binary files differnew file mode 100644 index 0000000..3312d86 --- /dev/null +++ b/src/main/icons/Icon.ico diff --git a/src/main/icons/README.md b/src/main/icons/README.md new file mode 100644 index 0000000..c6c4194 --- /dev/null +++ b/src/main/icons/README.md @@ -0,0 +1,11 @@ +![Sample app icon](linux/128.png) + +This directory contains the icons that are displayed for your app. Feel free to +change them. + +The difference between the icons on Mac and the other platforms is that on Mac, +they contain a ~5% transparent margin. This is because otherwise they look too +big (eg. in the Dock or in the app switcher). + +You can create Icon.ico from the .png files with +[an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
\ No newline at end of file diff --git a/src/main/icons/base/16.png b/src/main/icons/base/16.png Binary files differnew file mode 100644 index 0000000..f7d02dc --- /dev/null +++ b/src/main/icons/base/16.png diff --git a/src/main/icons/base/24.png b/src/main/icons/base/24.png Binary files differnew file mode 100644 index 0000000..faa6710 --- /dev/null +++ b/src/main/icons/base/24.png diff --git a/src/main/icons/base/32.png b/src/main/icons/base/32.png Binary files differnew file mode 100644 index 0000000..36b25e8 --- /dev/null +++ b/src/main/icons/base/32.png diff --git a/src/main/icons/base/48.png b/src/main/icons/base/48.png Binary files differnew file mode 100644 index 0000000..4a5dcbd --- /dev/null +++ b/src/main/icons/base/48.png diff --git a/src/main/icons/base/64.png b/src/main/icons/base/64.png Binary files differnew file mode 100644 index 0000000..4b0a423 --- /dev/null +++ b/src/main/icons/base/64.png diff --git a/src/main/icons/linux/1024.png b/src/main/icons/linux/1024.png Binary files differnew file mode 100644 index 0000000..2248377 --- /dev/null +++ b/src/main/icons/linux/1024.png diff --git a/src/main/icons/linux/128.png b/src/main/icons/linux/128.png Binary files differnew file mode 100644 index 0000000..05b2b35 --- /dev/null +++ b/src/main/icons/linux/128.png diff --git a/src/main/icons/linux/256.png b/src/main/icons/linux/256.png Binary files differnew file mode 100644 index 0000000..578fdc7 --- /dev/null +++ b/src/main/icons/linux/256.png diff --git a/src/main/icons/linux/512.png b/src/main/icons/linux/512.png Binary files differnew file mode 100644 index 0000000..0fbac4f --- /dev/null +++ b/src/main/icons/linux/512.png diff --git a/src/main/icons/mac/1024.png b/src/main/icons/mac/1024.png Binary files differnew file mode 100644 index 0000000..c1c8691 --- /dev/null +++ b/src/main/icons/mac/1024.png diff --git a/src/main/icons/mac/128.png b/src/main/icons/mac/128.png Binary files differnew file mode 100644 index 0000000..de9bee6 --- /dev/null +++ b/src/main/icons/mac/128.png diff --git a/src/main/icons/mac/256.png b/src/main/icons/mac/256.png Binary files differnew file mode 100644 index 0000000..c3a68b9 --- /dev/null +++ b/src/main/icons/mac/256.png diff --git a/src/main/icons/mac/512.png b/src/main/icons/mac/512.png Binary files differnew file mode 100644 index 0000000..b2fc07e --- /dev/null +++ b/src/main/icons/mac/512.png diff --git a/src/main/python/main.py b/src/main/python/main.py new file mode 100644 index 0000000..7d72bae --- /dev/null +++ b/src/main/python/main.py @@ -0,0 +1,195 @@ +import pickle +import sys + +from fbs_runtime.application_context.PyQt5 import ApplicationContext +from PyQt5.QtCore import QObject, Qt, pyqtSignal +from PyQt5.QtGui import QBrush, QColor, QImage, QPainter, QPalette, QPen +from PyQt5.QtWidgets import (QComboBox, QFileDialog, QFormLayout, QVBoxLayout, + QHBoxLayout, QLabel, QMainWindow, QMenu, + QPushButton, QWidget, QMdiArea, QSplitter, QGraphicsItem) +from PyQt5 import QtWidgets + +from utils.canvas import canvas +from utils.fileWindow import fileWindow +from utils.data import ppiList, sheetDimensionList +from utils import dialogs +from utils.toolbar import toolbar + +class appWindow(QMainWindow): + """ + Application entry point, subclasses QMainWindow and implements the main widget, + sets necessary window behaviour etc. + """ + def __init__(self, parent=None): + super(appWindow, self).__init__(parent) + + #create the menu bar + titleMenu = self.menuBar() #fetch reference to current menu bar + # self.mainWidget.setObjectName("Main Widget") + + self.menuFile = titleMenu.addMenu('File') #File Menu + self.menuFile.addAction("New", self.newProject) + self.menuFile.addAction("Open", self.openProject) + self.menuFile.addAction("Save", self.saveProject) + + self.menuGenerate = titleMenu.addMenu('Generate') #Generate menu + self.menuGenerate.addAction("Image", self.saveImage) + self.menuGenerate.addAction("Report", self.generateReport) + + self.mdi = QMdiArea(self) #create area for files to be displayed + self.mdi.setObjectName('mdi area') + + #create toolbar and add the toolbar plus mdi to layout + self.createToolbar() + + #set flags so that window doesnt look weird + self.mdi.setOption(QMdiArea.DontMaximizeSubWindowOnActivation, True) + self.mdi.setTabsClosable(True) + self.mdi.setTabsMovable(True) + self.mdi.setDocumentMode(False) + + #declare main window layout + # self.mainWidget.setLayout(mainLayout) + self.setCentralWidget(self.mdi) + self.resize(1280, 720) #set collapse dim + self.mdi.subWindowActivated.connect(self.tabSwitched) + + + def createToolbar(self): + #place holder for toolbar with fixed width, layout may change + self.toolbar = toolbar(self) + self.toolbar.setObjectName("Toolbar") + # self.addToolBar(Qt.LeftToolBarArea, self.toolbar) + self.addDockWidget(Qt.LeftDockWidgetArea, self.toolbar) + self.toolbar.toolbuttonClicked.connect(self.toolButtonClicked) + self.toolbar.populateToolbar(self.toolbar.toolbarItemList) + + def toolButtonClicked(self, object): + currentDiagram = self.mdi.currentSubWindow().tabber.currentWidget().painter + if currentDiagram: + graphic = getattr(QtWidgets, object['object'])(*object['args']) + graphic.setPen(QPen(Qt.black, 2)) + graphic.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable) + currentDiagram.addItem(graphic) + + def newProject(self): + #call to create a new file inside mdi area + project = fileWindow(self.mdi) + project.setObjectName("New Project") + self.mdi.addSubWindow(project) + if not project.tabList: # important when unpickling a file instead + project.newDiagram() #create a new tab in the new file + project.resizeHandler() + project.fileCloseEvent.connect(self.fileClosed) #closed file signal to switch to sub window view + if self.count > 1: #switch to tab view if needed + self.mdi.setViewMode(QMdiArea.TabbedView) + project.show() + + def openProject(self): + #show the open file dialog to open a saved file, then unpickle it. + name = QFileDialog.getOpenFileNames(self, 'Open File(s)', '', 'Process Flow Diagram (*pfd)') + if name: + for files in name[0]: + with open(files,'rb') as file: + project = pickle.load(file) + self.mdi.addSubWindow(project) + project.show() + project.resizeHandler() + project.fileCloseEvent.connect(self.fileClosed) + if self.count > 1: + # self.tabSpace.setVisible(True) + self.mdi.setViewMode(QMdiArea.TabbedView) + + def saveProject(self): + #pickle all files in mdi area + for j, i in enumerate(self.activeFiles): #get list of all windows with atleast one tab + if i.tabCount: + name = QFileDialog.getSaveFileName(self, 'Save File', f'New Diagram {j}', 'Process Flow Diagram (*.pfd)') + i.saveProject(name) + else: + return False + return True + + def saveImage(self): + #place holder for future implementaion + pass + + def generateReport(self): + #place holder for future implementaion + pass + + def tabSwitched(self, window): + #handle window switched edge case + if window: + window.resizeHandler() + + def resizeEvent(self, event): + #overload resize to also handle resize on file windows inside + for i in self.mdi.subWindowList(): + i.resizeHandler() + self.toolbar.resize() + super(appWindow, self).resizeEvent(event) + + def closeEvent(self, event): + #save alert on window close + if len(self.activeFiles) and not dialogs.saveEvent(self): + event.ignore() + else: + event.accept() + + def fileClosed(self, index): + #checks if the file tab menu needs to be removed + if self.count <= 2 : + self.mdi.setViewMode(QMdiArea.SubWindowView) + + @property + def activeFiles(self): + return [i for i in self.mdi.subWindowList() if i.tabCount] + + @property + def count(self): + return len(self.mdi.subWindowList()) + + #Key input handler + def keyPressEvent(self, event): + #overload key press event for custom keyboard shortcuts + if event.modifiers() and Qt.ControlModifier: + if event.key() == Qt.Key_N: + self.newProject() + + elif event.key() == Qt.Key_S: + self.saveProject() + + elif event.key() == Qt.Key_O: + self.openProject() + + elif event.key() == Qt.Key_W: + self.close() + + elif event.key() == Qt.Key_P: + if Qt.AltModifier: + self.saveImage() + else: + self.generateReport() + + elif event.key() == Qt.Key_A: + #todo implement selectAll + for item in self.mdi.activeSubWindow().tabber.currentWidget().items: + item.setSelected(True) + + #todo copy, paste, undo redo + + + elif event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace: + for item in self.mdi.activeSubWindow().tabber.currentWidget().painter.selectedItems(): + item.setEnabled(False) + #donot delete, to manage undo redo + + event.accept() + +if __name__ == '__main__': + app = ApplicationContext() # 1. Instantiate ApplicationContext + main = appWindow() + main.show() + exit_code = app.app.exec_() # 2. Invoke appctxt.app.exec_() + sys.exit(exit_code) diff --git a/src/main/python/utils/__init__.py b/src/main/python/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/main/python/utils/__init__.py diff --git a/src/main/python/utils/canvas.py b/src/main/python/utils/canvas.py new file mode 100644 index 0000000..2332087 --- /dev/null +++ b/src/main/python/utils/canvas.py @@ -0,0 +1,144 @@ +import pickle + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QBrush, QPalette +from PyQt5.QtWidgets import (QFileDialog, QApplication, QHBoxLayout, QMenu, + QTabWidget, QWidget, QSpacerItem, QStyle) + +from . import dialogs +from .graphics import customView, customScene +from .data import paperSizes, ppiList, sheetDimensionList + +class canvas(QWidget): + """ + Defines the work area for a single sheet. Contains a QGraphicScene along with necessary properties + for context menu and dialogs. + """ + + def __init__(self, parent=None, size= 'A4', ppi= '72'): + super(canvas, self).__init__(parent) + + #Store values for the canvas dimensions for ease of access, these are here just to be + # manipulated by the setters and getters + self._ppi = ppi + self._canvasSize = size + # self.setFixedSize(parent.size()) + #Create area for the graphic items to be placed, this is just here right now for the future + # when we will draw items on this, this might be changed if QGraphicScene is subclassed. + + #set layout and background color + self.painter = customScene() + self.painter.setBackgroundBrush(QBrush(Qt.white)) #set white background + + self.view = customView(self.painter, self) #create a viewport for the canvas board + + self.layout = QHBoxLayout(self) #create the layout of the canvas, the canvas could just subclass QGView instead + self.layout.addWidget(self.view, alignment=Qt.AlignCenter) + self.layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self.layout) + + #set initial paper size for the scene + self.painter.setSceneRect(0, 0, *paperSizes[self.canvasSize][self.ppi]) + + #set pointers to necessary parents for ease of reference + self.parentMdiArea = self.parent().parentWidget().parentWidget().parentWidget().parentWidget() + self.parentFileWindow = self.parent().parentWidget().parentWidget() + + def resizeView(self, w, h): + #helper function to resize canvas + self.painter.setSceneRect(0, 0, w, h) + + def adjustView(self): + #utitily to adjust current diagram view + width, height = self.dimensions + frameWidth = self.view.frameWidth() + #update view size + self.view.setSceneRect(0, 0, width - frameWidth*2, height) + + # use the available mdi area, also add padding + prect = self.parentMdiArea.rect() + width = width + 20 + height = height + 60 + + # add scrollbar size to width and height if they are visible, avoids clipping + if self.view.verticalScrollBar().isVisible(): + width += self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + if self.view.horizontalScrollBar().isVisible(): + height += self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + + #if view is visible use half of available width + factor = 2 if self.parentFileWindow.sideViewTab is not None else 1 + #use minimum width required to fit the view + width = min((prect.width() - 40)//factor, width) + height = min(prect.height() - 80, height) + #set view dims + self.view.setFixedWidth(width) + self.view.setFixedHeight(height) + + def resizeEvent(self, event): + #overloaded function to also view size on window update + self.adjustView() + + def setCanvasSize(self, size): + """ + extended setter for dialog box + """ + self.canvasSize = size + + def setCanvasPPI(self, ppi): + """ + extended setter for dialog box + """ + self.ppi = ppi + + @property + def dimensions(self): + #returns the dimension of the current scene + return self.painter.sceneRect().width(), self.painter.sceneRect().height() + @property + def items(self): + # generator to filter out certain items + for i in self.painter.items(): + yield i + + @property + def canvasSize(self): + return self._canvasSize + @property + def ppi(self): + return self._ppi + + @canvasSize.setter + def canvasSize(self, size): + self._canvasSize = size + if self.painter: + self.resizeView(*paperSizes[self.canvasSize][self.ppi]) + + @ppi.setter + def ppi(self, ppi): + self._ppi = ppi + if self.painter: + self.resizeView(*paperSizes[self.canvasSize][self.ppi]) + + #following 2 methods are defined for correct pickling of the scene. may be changed to json or xml later so as + # to not have a binary file. + def __getstate__(self) -> dict: + return { + "_classname_": self.__class__.__name__, + "ppi": self._ppi, + "canvasSize": self._canvasSize, + "ObjectName": self.objectName(), + "items": [i.__getState__() for i in self.painter.items()] + } + + def __setstate__(self, dict): + self.__init__() + self._ppi = dict['ppi'] + self._canvasSize = dict['canvasSize'] + self.setObjectName(dict['ObjectName']) + for item in dict['items']: + graphic = getattr(graphics, dict['_classname_']) + graphic.__setstate__(item) + self.painter.addItem(graphic) + +
\ No newline at end of file diff --git a/src/main/python/utils/data.py b/src/main/python/utils/data.py new file mode 100644 index 0000000..593cb1b --- /dev/null +++ b/src/main/python/utils/data.py @@ -0,0 +1,311 @@ +paperSizes = { + "A0": { + "72": [2384, 3370], + "96": [3179, 4494], + "150": [4967, 7022], + "300": [9933, 14043] + }, + "A1": { + "72": [1684, 2384], + "96": [2245, 3179], + "150": [3508, 4967], + "300": [7016, 9933] + }, + "A2": { + "72": [1191, 1684], + "96": [1587, 2245], + "150": [2480, 3508], + "300": [4960, 7016] + }, + "A3": { + "72": [842, 1191], + "96": [1123, 1587], + "150": [1754, 2480], + "300": [3508, 4960] + }, + "A4": { + "72": [595, 842], + "96": [794, 1123], + "150": [1240, 1754], + "300": [2480, 3508] + } +} + +sheetDimensionList = [f'A{i}' for i in range(5)] + +ppiList = ["72", "96", "150", "300"] + +toolbarItems = { + 'Ellipse': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse2': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse3': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse4': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse5': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse6': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse7': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse8': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse9': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse11': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse12': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse13': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse14': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse15': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse16': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse17': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse18': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse19': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse20': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse21': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse22': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse23': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse24': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse25': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse26': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse27': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse28': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse29': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse30': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse31': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse32': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse33': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse34': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse35': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse36': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse37': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse38': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse39': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse40': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse41': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse42': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse43': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse44': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse45': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, + 'Ellipse46': { + 'name': 'Ellipse', + 'icon': 'ellipse.png', + 'object': 'QGraphicsEllipseItem', + 'args': [20, 20, 300, 300] + }, +} + +defaultToolbarItems = toolbarItems.keys()
\ No newline at end of file diff --git a/src/main/python/utils/dialogs.py b/src/main/python/utils/dialogs.py new file mode 100644 index 0000000..e008aa5 --- /dev/null +++ b/src/main/python/utils/dialogs.py @@ -0,0 +1,102 @@ +from PyQt5.QtWidgets import QDialog, QPushButton, QFormLayout, QComboBox, QLabel, QMessageBox, QDialogButtonBox +from .data import sheetDimensionList, ppiList + +class paperDims(QDialog): + """ + Utility dialog box to adjust the current canvas's dimensions, might return just dimensions later + so that sizes do not need to be imported in every other module. + """ + def __init__(self, parent=None, size='A4', ppi='72', name='Canvas Size'): + super(paperDims, self).__init__(parent) + + #store initial values to show currently set value, also updated when changed. these are returned at EOL + self.returnCanvasSize = size + self.returnCanvasPPI = ppi + + self.setWindowTitle(name+" :Canvas Size") #Set Window Title + #init layout + dialogBoxLayout = QFormLayout(self) + + sizeComboBox = QComboBox() #combo box for paper sizes + sizeComboBox.addItems(sheetDimensionList) + sizeComboBox.setCurrentIndex(4) + sizeComboBox.activated[str].connect(lambda size: setattr(self, "returnCanvasSize", size)) + sizeLabel = QLabel("Canvas Size") + sizeLabel.setBuddy(sizeComboBox) # label for the above combo box + sizeComboBox.setCurrentIndex(sheetDimensionList.index(self.returnCanvasSize)) #set index to current value of canvas + dialogBoxLayout.setWidget(0, QFormLayout.LabelRole, sizeLabel) + dialogBoxLayout.setWidget(0, QFormLayout.FieldRole, sizeComboBox) + + ppiComboBox = QComboBox() #combo box for ppis + ppiComboBox.addItems(ppiList) + ppiComboBox.activated[str].connect(lambda ppi: setattr(self, "returnCanvasPPI", ppi)) + ppiLabel = QLabel("Canvas ppi") + ppiLabel.setBuddy(ppiComboBox) # label for the above combo box + ppiComboBox.setCurrentIndex(ppiList.index(self.returnCanvasPPI)) #set index to current value of canvas + dialogBoxLayout.setWidget(1, QFormLayout.LabelRole, ppiLabel) + dialogBoxLayout.setWidget(1, QFormLayout.FieldRole, ppiComboBox) + + # add ok and cancel buttons + buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.reject) + + dialogBoxLayout.addWidget(buttonBox) + self.setLayout(dialogBoxLayout) + self.resize(300,100) #resize to a certain size + + def exec_(self): + #overload exec_ to add return values and delete itself(currently being tested) + super(paperDims, self).exec_() + self.deleteLater() #remove from memory + #if ok was pressed return value else return None + return (self.returnCanvasSize, self.returnCanvasPPI) if self.result() else None + +class sideViewSwitchDialog(QDialog): + """ + Custom dialog box to show, all available tabs to set the side view to. + Also has accept reject events. Structure is similar to paperDims dialog box. + """ + def __init__(self, parent=None, tabList = None, initial = None): + super(sideViewSwitchDialog, self).__init__(parent=parent) + self.tabList = tabList + self.returnVal = initial + self.initial = initial + + dialogBoxLayout = QFormLayout(self) + tabListComboBox = QComboBox() + tabListComboBox.addItems(self.tabList) + tabListComboBox.activated[str].connect(lambda x: setattr(self, 'returnVal', self.tabList.index(x))) + tabLabel = QLabel("Change Side View") + tabLabel.setBuddy(tabListComboBox) # label for the above combo box + tabListComboBox.setCurrentIndex(self.returnVal) + dialogBoxLayout.setWidget(1, QFormLayout.LabelRole, tabLabel) + dialogBoxLayout.setWidget(1, QFormLayout.FieldRole, tabListComboBox) + + # add ok and cancel buttons + buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.reject) + + dialogBoxLayout.addWidget(buttonBox) + self.setLayout(dialogBoxLayout) + self.resize(300,100) #resize to a certain size + + def exec_(self): + #overload exec_ to add return values and delete itself(currently being tested) + super(sideViewSwitchDialog, self).exec_() + self.deleteLater() #remove from memory + #if ok was pressed return value else return None + return self.returnVal if self.result() else self.initial + +def saveEvent(parent = None): + #utility function to generate a Qt alert window requesting the user to save the file, returns user intention on window close + alert = QMessageBox.question(parent, parent.objectName(), "All unsaved progress will be LOST!", + QMessageBox.StandardButtons(QMessageBox.Save|QMessageBox.Ignore|QMessageBox.Cancel), QMessageBox.Save) + if alert == QMessageBox.Cancel: + return False + else: + if alert == QMessageBox.Save: + if not parent.saveProject(): #the parent's saveProject method is called which returns false if saving was cancelled by the user + return False + return True
\ No newline at end of file diff --git a/src/main/python/utils/fileWindow.py b/src/main/python/utils/fileWindow.py new file mode 100644 index 0000000..e549568 --- /dev/null +++ b/src/main/python/utils/fileWindow.py @@ -0,0 +1,255 @@ +import pickle + +from PyQt5.QtCore import Qt, pyqtSignal, QPoint +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import (QFileDialog, QHBoxLayout, + QMdiSubWindow, QMenu, QPushButton, QSizePolicy, + QSplitter, QWidget, QStyle) + +from . import dialogs +from .graphics import customView +from .canvas import canvas +from .tabs import customTabWidget + + +class fileWindow(QMdiSubWindow): + """ + This defines a single file, inside the application, consisting of multiple tabs that contain + canvases. Pre-Defined so that a file can be instantly created without defining the structure again. + """ + fileCloseEvent = pyqtSignal(int) + + def __init__(self, parent = None, title = 'New Project', size = 'A4', ppi = '72'): + super(fileWindow, self).__init__(parent) + self._sideViewTab = None + self.index = None + + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + #Uses a custom QTabWidget that houses a custom new Tab Button, used to house the seperate + # diagrams inside a file + self.tabber = customTabWidget(self) + self.tabber.setObjectName(title) #store title as object name for pickling + self.tabber.tabCloseRequested.connect(self.closeTab) # Show save alert on tab close + self.tabber.currentChanged.connect(self.changeTab) # placeholder just to detect tab change + self.tabber.plusClicked.connect(self.newDiagram) #connect the new tab button to add a new tab + + #assign layout to widget + self.mainWidget = QWidget(self) + layout = QHBoxLayout(self.mainWidget) + self.createSideViewArea() #create the side view objects + layout.addWidget(self.tabber) + layout.addWidget(self.splitter) + layout.addWidget(self.sideView) + self.mainWidget.setLayout(layout) + self.setWidget(self.mainWidget) + self.setWindowTitle(title) + + #This is done so that a right click menu is shown on right click + self.tabber.setContextMenuPolicy(Qt.CustomContextMenu) + self.tabber.customContextMenuRequested.connect(self.contextMenu) + + # self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setWindowFlag(Qt.CustomizeWindowHint, True) + self.setWindowFlag(Qt.WindowMinimizeButtonHint, False) + self.setWindowFlag(Qt.WindowMaximizeButtonHint, False) + + def createSideViewArea(self): + #creates the side view widgets and sets them to invisible + self.splitter = QSplitter(Qt.Vertical ,self) + self.sideView = customView(parent = self) + self.sideView.setInteractive(False) + self.sideViewCloseButton = QPushButton('×', self.sideView) + self.sideViewCloseButton.setFlat(True) + self.sideViewCloseButton.setStyleSheet("""QPushButton{ + background: rgba(214, 54, 40, 50%); + border: 1px groove white; + border-radius: 2px; + font-size: 18px; + font-weight: Bold; + padding: 1px 2px 3px 3px; + color: rgba(255, 255, 255, 50%); + } + QPushButton:Hover{ + background: rgba(214, 54, 40, 90%); + color: rgba(255, 255, 255, 90%); + } + """) + self.sideViewCloseButton.setFixedSize(20, 20) + self.moveSideViewCloseButton() + self.sideViewCloseButton.clicked.connect(lambda: setattr(self, 'sideViewTab', None)) + self.splitter.setVisible(False) + self.sideView.setVisible(False) + self.sideView.setContextMenuPolicy(Qt.CustomContextMenu) + self.sideView.customContextMenuRequested.connect(self.sideViewContextMenu) + + def resizeHandler(self): + # resize Handler to handle resize cases. + parentRect = self.mdiArea().size() + current = self.tabber.currentWidget() + width, height = current.dimensions + + # if side view is visible, set width to maximum possible, else use minimum requirement + if self.sideViewTab: + width = parentRect.width() + height = parentRect.height() + self.moveSideViewCloseButton() + + else: + width = min(parentRect.width(), width + 100) + height = min(parentRect.height(), height + 200) + + if len(self.parent().parent().subWindowList()) > 1: + height -= 20 + + # set element dimensions + self.setFixedSize(width, height) + self.tabber.resize(width, height) + self.tabber.currentWidget().adjustView() + + def contextMenu(self, point): + #function to display the right click menu at point of right click + menu = QMenu("Context Menu", self) + menu.addAction("Adjust Canvas", self.adjustCanvasDialog) + menu.addAction("Remove Side View" if self.sideViewTab == self.tabber.currentWidget() else "View Side-By-Side", + self.sideViewMode) + menu.addAction("Reset Zoom", lambda : setattr(self.tabber.currentWidget().view, 'zoom', 1)) + menu.exec_(self.tabber.mapToGlobal(point)) + + def sideViewMode(self): + #helper context menu function to toggle side view + self.sideViewTab = self.tabber.currentWidget() + + def adjustCanvasDialog(self): + #helper context menu function to the context menu dialog box + currentTab = self.tabber.currentWidget() + result = dialogs.paperDims(self, currentTab._canvasSize, currentTab._ppi, currentTab.objectName()).exec_() + if result is not None: + currentTab.canvasSize, currentTab.ppi = result + return self.resizeHandler() + else: + return None + + def sideViewToggle(self): + #Function checks if current side view tab is set, and toggles view as required + if self.sideViewTab: + self.splitter.setVisible(True) + self.sideView.setVisible(True) + self.sideView.setScene(self.tabber.currentWidget().painter) + self.moveSideViewCloseButton() + self.resizeHandler() + return True + else: + self.splitter.setVisible(False) + self.sideView.setVisible(False) + self.resizeHandler() + return False + + def moveSideViewCloseButton(self): + # used to place side view close button at appropriate position + self.sideViewCloseButton.move(5, 5) + + def sideViewContextMenu(self, point): + # context menu for side view + menu = QMenu("Context Menu", self.sideView) + menu.addAction("Reset Zoom", lambda : setattr(self.sideView, 'zoom', 1)) + menu.addSection('Change Side View Tab') + if self.tabCount > 5: + # just show switch dialog box, if there are 6 or more tabs open + menu.addAction("Show switch menu", self.sideViewSwitchTab) + else: + # enumerate all tabs from side view. + for i in range(self.tabCount): + j = self.tabber.widget(i) + if j == self.sideViewTab: + continue + # evaluate i as index, weird lambda behaviour + #see https://stackoverflow.com/a/33984811/7799568 + menu.addAction(f'{i}. {j.objectName()}', lambda index=i: self.sideViewSwitchCMenu(index)) + menu.addAction("Remove side view", lambda : setattr(self, 'sideViewTab', None)) + menu.exec_(self.sideView.mapToGlobal(point)) + + def sideViewSwitchCMenu(self, index): + self.sideViewTab = self.tabber.widget(index) + + def sideViewSwitchTab(self): + # displays a side view switch dialog box + tabList = [f'{i}. {j.objectName()}' for i, j in enumerate(self.tabList)] #names and index of all tabs + initial = self.tabList.index(self.sideViewTab) # current side view tab + result = dialogs.sideViewSwitchDialog(self.tabber, tabList, initial).exec_() #call dialog box + if result != initial: + self.sideViewTab = self.tabber.widget(result) if result<self.tabCount else None + + @property + def sideViewTab(self): + #returns current active if sideViewTab otherwise None + return self._sideViewTab + + @property + def tabList(self): + #returns a list of tabs in the given window + return [self.tabber.widget(i) for i in range(self.tabCount)] + + @property + def tabCount(self): + #returns the number of tabs in the given window only + return self.tabber.count() + + @sideViewTab.setter + def sideViewTab(self, tab): + #setter for side view. Also toggles view as necessary + self._sideViewTab = None if tab == self.sideViewTab else tab + return self.sideViewToggle() + + def changeTab(self, currentIndex): + #placeholder function to detect tab change + self.resizeHandler() + + def closeTab(self, currentIndex): + #show save alert on tab close + if dialogs.saveEvent(self): + self.tabber.widget(currentIndex).deleteLater() + self.tabber.removeTab(currentIndex) + + def newDiagram(self): + # helper function to add a new tab on pressing new tab button, using the add tab method on QTabWidget + diagram = canvas(self.tabber) + diagram.setObjectName("New") + index = self.tabber.addTab(diagram, "New") + self.tabber.setCurrentIndex(index) + + def saveProject(self, name = None): + # called by dialog.saveEvent, saves the current file + name = QFileDialog.getSaveFileName(self, 'Save File', f'New Diagram', 'Process Flow Diagram (*.pfd)') if not name else name + if name[0]: + with open(name[0],'wb') as file: + pickle.dump(self, file) + return True + else: + return False + + def closeEvent(self, event): + # handle save alert on file close, check if current file has no tabs aswell. + if self.tabCount==0 or dialogs.saveEvent(self): + event.accept() + self.deleteLater() + self.fileCloseEvent.emit(self.index) + else: + event.ignore() + + #following 2 methods are defined for correct pickling of the scene. may be changed to json or xml later so as + # to not have a binary file. + def __getstate__(self) -> dict: + return { + "_classname_": self.__class__.__name__, + "ObjectName": self.objectName(), + "ppi": self._ppi, + "canvasSize": self._canvasSize, + "tabs": [i.__getstate__() for i in self.tabList] + } + + def __setstate__(self, dict): + self.__init__(title = dict['ObjectName']) + for i in dict['tabs']: + diagram = canvas(self.tabber, size = dict['canvasSize'], ppi = dict['ppi'], fileWindow = self) + diagram.__setstate__(i) + self.tabber.addTab(diagram, i['ObjectName']) diff --git a/src/main/python/utils/funcs.py b/src/main/python/utils/funcs.py new file mode 100644 index 0000000..7796ece --- /dev/null +++ b/src/main/python/utils/funcs.py @@ -0,0 +1,5 @@ +from itertools import zip_longest + +def grouper(n, iterable, fillvalue=None): + args = [iter(iterable)] * n + return zip_longest(fillvalue=fillvalue, *args)
\ No newline at end of file diff --git a/src/main/python/utils/graphics.py b/src/main/python/utils/graphics.py new file mode 100644 index 0000000..0fe0030 --- /dev/null +++ b/src/main/python/utils/graphics.py @@ -0,0 +1,77 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPen +from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsProxyWidget, QGraphicsItem +from PyQt5 import QtWidgets + +class customView(QGraphicsView): + """ + Defines custom QGraphicsView with zoom features and drag-drop accept event, overriding wheel event + """ + def __init__(self, scene = None, parent=None): + if scene is not None: #overloaded constructor + super(customView, self).__init__(scene, parent) + else: + super(customView, self).__init__(parent) + self._zoom = 1 + self.setDragMode(True) #sets pannable using mouse + self.setAcceptDrops(True) #sets ability to accept drops + + #following four functions are required to be overridden for drag-drop functionality + def dragEnterEvent(self, QDragEnterEvent): + #defines acceptable drop items + if QDragEnterEvent.mimeData().hasText(): + QDragEnterEvent.acceptProposedAction() + + def dragMoveEvent(self, QDragMoveEvent): + #defines acceptable drop items + if QDragMoveEvent.mimeData().hasText(): + QDragMoveEvent.acceptProposedAction() + + def dragLeaveEvent(self, QDragLeaveEvent): + #accept any drag leave event, avoid unnecessary logging + QDragLeaveEvent.accept() + + def dropEvent(self, QDropEvent): + #defines item drop, fetches text, creates corresponding QGraphicItem and adds it to scene + if QDropEvent.mimeData().hasText(): + #QDropEvent.mimeData().text() defines intended drop item, the pos values define position + graphic = getattr(QtWidgets, QDropEvent.mimeData().text())(QDropEvent.pos().x()-150, QDropEvent.pos().y()-150, 300, 300) + graphic.setPen(QPen(Qt.black, 2)) + graphic.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable) + self.scene().addItem(graphic) + QDropEvent.acceptProposedAction() + + def wheelEvent(self, QWheelEvent): + #overload wheelevent, to zoom if control is pressed, else scroll normally + if Qt.ControlModifier: #check if control is pressed + if QWheelEvent.source() == Qt.MouseEventNotSynthesized: #check if precision mouse(mac) + # angle delta is 1/8th of a degree per scroll unit + if self.zoom + QWheelEvent.angleDelta().y()/2880 > 0.1: # hit and trial value (2880) + self.zoom += QWheelEvent.angleDelta().y()/2880 + else: + # precision delta is exactly equal to amount to scroll + if self.zoom + QWheelEvent.pixelDelta().y() > 0.1: + self.zoom += QWheelEvent.angleDelta().y() + QWheelEvent.accept() # accept event so that scrolling doesnt happen simultaneously + else: + return super().wheelEvent(self, QWheelEvent) # scroll if ctrl not pressed + + @property + def zoom(self): + # property for zoom + return self._zoom + + @zoom.setter + def zoom(self, value): + # set scale according to zoom value being set + temp = self.zoom + self._zoom = value + self.scale(self.zoom / temp, self.zoom / temp) + +class customScene(QGraphicsScene): + """ + re-implement QGraphicsScene for future functionality + hint: QUndoFramework + """ + pass +
\ No newline at end of file diff --git a/src/main/python/utils/layout.py b/src/main/python/utils/layout.py new file mode 100644 index 0000000..6781249 --- /dev/null +++ b/src/main/python/utils/layout.py @@ -0,0 +1,87 @@ +from PyQt5.QtCore import Qt, QRect, QPoint, QSize +from PyQt5.QtWidgets import QLayout, QSizePolicy + +class flowLayout(QLayout): + def __init__(self, parent=None, margin=0, spacing=-1): + super(flowLayout, self).__init__(parent) + + if parent is not None: + self.setContentsMargins(margin, margin, margin, margin) + + self.setSpacing(spacing) + self.itemList = [] + + def __del__(self): + item = self.takeAt(0) + while item: + item = self.takeAt(0) + + def addItem(self, item): + self.itemList.append(item) + + def count(self): + return len(self.itemList) + + def itemAt(self, index): + if index >= 0 and index < len(self.itemList): + return self.itemList[index] + + return None + + def takeAt(self, index): + if index >= 0 and index < len(self.itemList): + return self.itemList.pop(index) + + return None + + def expandingDirections(self): + return Qt.Orientations(Qt.Orientation(0)) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + height = self.doLayout(QRect(0, 0, width, 0), True) + return height + + def setGeometry(self, rect): + super(flowLayout, self).setGeometry(rect) + self.doLayout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QSize() + + for item in self.itemList: + size = size.expandedTo(item.minimumSize()) + + margin, _, _, _ = self.getContentsMargins() + + size += QSize(2 * margin, 2 * margin) + return size + + def doLayout(self, rect, testOnly): + x = rect.x() + y = rect.y() + lineHeight = 0 + + for item in self.itemList: + wid = item.widget() + spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.ToolButton, QSizePolicy.ToolButton, Qt.Horizontal) + spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.ToolButton, QSizePolicy.ToolButton, Qt.Vertical) + nextX = x + item.sizeHint().width() + spaceX + if nextX - spaceX > rect.right() and lineHeight > 0: + x = rect.x() + y = y + lineHeight + spaceY + nextX = x + item.sizeHint().width() + spaceX + lineHeight = 0 + + if not testOnly: + item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) + + x = nextX + lineHeight = max(lineHeight, item.sizeHint().height()) + + return y + lineHeight - rect.y() diff --git a/src/main/python/utils/tabs.py b/src/main/python/utils/tabs.py new file mode 100644 index 0000000..eab056a --- /dev/null +++ b/src/main/python/utils/tabs.py @@ -0,0 +1,71 @@ +from PyQt5.QtWidgets import QTabBar, QPushButton, QTabWidget +from PyQt5.QtCore import pyqtSignal, QSize + +class tabBarPlus(QTabBar): + """ + Just implemented to overload resize and layout change to emit a signal + """ + layoutChanged = pyqtSignal() + def resizeEvent(self, event): + super().resizeEvent(event) + self.layoutChanged.emit() + + def tabLayoutChange(self): + super().tabLayoutChange() + self.layoutChanged.emit() + + +class customTabWidget(QTabWidget): + """ + QTabWidget with a new tab button, also catches layoutChange signal by + the tabBarPlus to dynamically move the button to the correct location + """ + plusClicked = pyqtSignal() + def __init__(self, parent=None): + super(customTabWidget, self).__init__(parent) + + self.tab = tabBarPlus() + self.setTabBar(self.tab) #set tabBar to our custom tabBarPlus + + self.plusButton = QPushButton('+', self) #create the new tab button + #style the new tab button + self.plusButton.setFlat(True) + self.plusButton.setStyleSheet(""" + QPushButton{ + background: rgba(230, 230, 227, 0%); + padding: 1px; + border: 0px solid #E6E6E3; + + } + QPushButton:hover{ + background: rgba(230, 230, 227, 60%); + }""") + + #and parent it to the widget to add it at 0, 0 + self.plusButton.setFixedSize(35, 25) #set dimensions + self.plusButton.clicked.connect(self.plusClicked.emit) #emit signal on click + #set flags + self.setMovable(True) + self.setTabsClosable(True) + + self.tab.layoutChanged.connect(self.movePlusButton) #connect layout change + # to dynamically move the button. + + #set custom stylesheet for the widget area + self.setStyleSheet("""QTabWidget::pane { + margin: 0px,1px,1px,1px; + border: 2px solid #E6E6E3; + border-radius: 7px; + padding: 1px; + background-color: #E6E6E3;}""") + + def movePlusButton(self): + #move the new tab button to correct location + size = sum([self.tab.tabRect(i).width() for i in range(self.tab.count())]) + # calculate width of all tabs + h = max(self.tab.geometry().bottom() - self.plusButton.height(), 0) #align with bottom of tabbar + w = self.tab.width() + if size > w: #if all the tabs do not overflow the tab bar, add at the end + self.plusButton.move(w-self.plusButton.width(), h) + else: + self.plusButton.move(size-3, h)
\ No newline at end of file diff --git a/src/main/python/utils/toolbar.py b/src/main/python/utils/toolbar.py new file mode 100644 index 0000000..ec3b6a8 --- /dev/null +++ b/src/main/python/utils/toolbar.py @@ -0,0 +1,138 @@ +from fbs_runtime.application_context.PyQt5 import ApplicationContext +from PyQt5.QtCore import QSize, Qt, pyqtSignal, QMimeData +from PyQt5.QtGui import QIcon, QDrag +from PyQt5.QtWidgets import (QBoxLayout, QDockWidget, QGridLayout, QLineEdit, + QScrollArea, QToolButton, QWidget, QApplication) +from re import search, IGNORECASE + +from .data import toolbarItems +from .funcs import grouper +from .layout import flowLayout + +resourceManager = ApplicationContext() #Used to load images, mainly toolbar icons + +class toolbar(QDockWidget): + """ + Defines the right side toolbar, using QDockWidget. + """ + toolbuttonClicked = pyqtSignal(dict) #signal for any object button pressed + + def __init__(self, parent = None): + super(toolbar, self).__init__(parent) + self.toolbarButtonDict = dict() #initializes empty dict to store toolbar buttons + self.toolbarItems(toolbarItems.keys()) #creates all necessary buttons + + self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable) + #mainly used to disable closeability of QDockWidget + + #declare main widget and layout + self.widget = QWidget(self) + self.layout = QBoxLayout(QBoxLayout.TopToBottom, self.widget) + self.setAllowedAreas(Qt.AllDockWidgetAreas) + + self.searchBox = QLineEdit(self.widget) #search box to search through componenets + + #connect signal to filter slot, add searchbar to toolbar + self.searchBox.textChanged.connect(self.searchQuery) + self.layout.addWidget(self.searchBox, alignment=Qt.AlignHCenter) + + #create a scrollable area to house all buttons + self.diagArea = QScrollArea(self) + self.layout.addWidget(self.diagArea) + self.diagAreaWidget = QWidget(self.diagArea) #inner widget for scroll area + #custom layout for inner widget + self.diagAreaLayout = flowLayout(self.diagAreaWidget) + + # self.diagArea.setWidget() #set inner widget to scroll area + self.setWidget(self.widget) #set main widget to dockwidget + + def clearLayout(self): + # used to clear all items from toolbar, by parenting it to the toolbar instead + # this works because changing parents moves widgets to be the child of the new + # parent, setting it to none, would have qt delete them to free memory + for i in reversed(range(self.diagAreaLayout.count())): + # since changing parent would effect indexing, its important to go in reverse + self.diagAreaLayout.itemAt(i).widget().setParent(self) + + def populateToolbar(self, list): + #called everytime the button box needs to be updated(incase of a filter) + self.clearLayout() #clears layout + for item in list: + self.diagAreaLayout.addWidget(self.toolbarButtonDict[item]) + + def searchQuery(self): + # shorten toolbaritems list with search items + # self.populateToolbar() # populate with toolbar items + text = self.searchBox.text() #get text + if text == '': + self.populateToolbar(self.toolbarItemList) # restore everything on empty string + else: + # use regex to search filter through button list and add the remainder to toolbar + self.populateToolbar(filter(lambda x: search(text, x, IGNORECASE), self.toolbarItemList)) + + def resize(self): + # called when main window resizes, overloading resizeEvent caused issues. + parent = self.parentWidget() #used to get parent dimensions + self.layout.setDirection(QBoxLayout.TopToBottom) # here so that a horizontal toolbar can be implemented later + self.setFixedWidth(.12*parent.width()) #12% of parent width + self.setFixedHeight(self.height()) #span available height + width = .12*parent.width() #12% of parent width + self.diagAreaWidget.setFixedWidth(width) #set inner widget width + # the following line, sets the required height for the current width, so that blank space doesnt occur + self.diagAreaWidget.setFixedHeight(self.diagAreaLayout.heightForWidth(width)) + + def toolbarItems(self, items): + #helper functions to create required buttons + for item in items: + obj = toolbarItems[item] + button = toolbarButton(self, obj) + button.clicked.connect(lambda : self.toolbuttonClicked.emit(obj)) + self.toolbarButtonDict[item] = button + + @property + def toolbarItemList(self): + #generator to iterate over all buttons + for i in self.toolbarButtonDict.keys(): + yield i + +class toolbarButton(QToolButton): + """ + Custom buttons for components that implements drag and drop functionality + item -> dict from toolbarItems dict, had 4 properties, name, object, icon and default arguments. + """ + def __init__(self, parent = None, item = None): + super(toolbarButton, self).__init__(parent) + #uses fbs resource manager to get icons + self.setIcon(QIcon(resourceManager.get_resource(f'toolbar/{item["icon"]}'))) + self.setIconSize(QSize(40, 40)) #unecessary but left for future references + self.dragStartPosition = None #intialize value for drag event + self.itemObject = item['object'] #refer current item object, to handle drag mime + self.setText(item["name"]) #button text + self.setToolTip(item["name"]) #button tooltip + + def mousePressEvent(self, event): + #check if button was pressed or there was a drag intent + super(toolbarButton, self).mousePressEvent(event) + if event.button() == Qt.LeftButton: + self.dragStartPosition = event.pos() #set dragstart position + + def mouseMoveEvent(self, event): + #handles drag + if not (event.buttons() and Qt.LeftButton): + return #ignore if left click is not held + if (event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance(): + return #check if mouse was dragged enough, manhattan length is a rough and quick method in qt + + drag = QDrag(self) #create drag object + mimeData = QMimeData() #create drag mime + mimeData.setText(self.itemObject) # set mime value for view to accept + drag.setMimeData(mimeData) # attach mime to drag + drag.exec(Qt.CopyAction) #execute drag + + def sizeHint(self): + #defines button size + return self.minimumSizeHint() + + def minimumSizeHint(self): + #defines button size + return QSize(30, 30)
\ No newline at end of file diff --git a/src/main/resources/base/toolbar/ellipse.png b/src/main/resources/base/toolbar/ellipse.png Binary files differnew file mode 100644 index 0000000..e708bdd --- /dev/null +++ b/src/main/resources/base/toolbar/ellipse.png |