summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/build/settings/base.json6
-rw-r--r--src/build/settings/linux.json6
-rw-r--r--src/build/settings/mac.json3
-rw-r--r--src/main/icons/Icon.icobin0 -> 168229 bytes
-rw-r--r--src/main/icons/README.md11
-rw-r--r--src/main/icons/base/16.pngbin0 -> 544 bytes
-rw-r--r--src/main/icons/base/24.pngbin0 -> 783 bytes
-rw-r--r--src/main/icons/base/32.pngbin0 -> 912 bytes
-rw-r--r--src/main/icons/base/48.pngbin0 -> 1497 bytes
-rw-r--r--src/main/icons/base/64.pngbin0 -> 1657 bytes
-rw-r--r--src/main/icons/linux/1024.pngbin0 -> 26841 bytes
-rw-r--r--src/main/icons/linux/128.pngbin0 -> 3177 bytes
-rw-r--r--src/main/icons/linux/256.pngbin0 -> 5641 bytes
-rw-r--r--src/main/icons/linux/512.pngbin0 -> 11653 bytes
-rw-r--r--src/main/icons/mac/1024.pngbin0 -> 47311 bytes
-rw-r--r--src/main/icons/mac/128.pngbin0 -> 4978 bytes
-rw-r--r--src/main/icons/mac/256.pngbin0 -> 10278 bytes
-rw-r--r--src/main/icons/mac/512.pngbin0 -> 21699 bytes
-rw-r--r--src/main/python/main.py195
-rw-r--r--src/main/python/utils/__init__.py0
-rw-r--r--src/main/python/utils/canvas.py144
-rw-r--r--src/main/python/utils/data.py311
-rw-r--r--src/main/python/utils/dialogs.py102
-rw-r--r--src/main/python/utils/fileWindow.py255
-rw-r--r--src/main/python/utils/funcs.py5
-rw-r--r--src/main/python/utils/graphics.py77
-rw-r--r--src/main/python/utils/layout.py87
-rw-r--r--src/main/python/utils/tabs.py71
-rw-r--r--src/main/python/utils/toolbar.py138
-rw-r--r--src/main/resources/base/toolbar/ellipse.pngbin0 -> 37162 bytes
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
new file mode 100644
index 0000000..3312d86
--- /dev/null
+++ b/src/main/icons/Icon.ico
Binary files differ
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
new file mode 100644
index 0000000..f7d02dc
--- /dev/null
+++ b/src/main/icons/base/16.png
Binary files differ
diff --git a/src/main/icons/base/24.png b/src/main/icons/base/24.png
new file mode 100644
index 0000000..faa6710
--- /dev/null
+++ b/src/main/icons/base/24.png
Binary files differ
diff --git a/src/main/icons/base/32.png b/src/main/icons/base/32.png
new file mode 100644
index 0000000..36b25e8
--- /dev/null
+++ b/src/main/icons/base/32.png
Binary files differ
diff --git a/src/main/icons/base/48.png b/src/main/icons/base/48.png
new file mode 100644
index 0000000..4a5dcbd
--- /dev/null
+++ b/src/main/icons/base/48.png
Binary files differ
diff --git a/src/main/icons/base/64.png b/src/main/icons/base/64.png
new file mode 100644
index 0000000..4b0a423
--- /dev/null
+++ b/src/main/icons/base/64.png
Binary files differ
diff --git a/src/main/icons/linux/1024.png b/src/main/icons/linux/1024.png
new file mode 100644
index 0000000..2248377
--- /dev/null
+++ b/src/main/icons/linux/1024.png
Binary files differ
diff --git a/src/main/icons/linux/128.png b/src/main/icons/linux/128.png
new file mode 100644
index 0000000..05b2b35
--- /dev/null
+++ b/src/main/icons/linux/128.png
Binary files differ
diff --git a/src/main/icons/linux/256.png b/src/main/icons/linux/256.png
new file mode 100644
index 0000000..578fdc7
--- /dev/null
+++ b/src/main/icons/linux/256.png
Binary files differ
diff --git a/src/main/icons/linux/512.png b/src/main/icons/linux/512.png
new file mode 100644
index 0000000..0fbac4f
--- /dev/null
+++ b/src/main/icons/linux/512.png
Binary files differ
diff --git a/src/main/icons/mac/1024.png b/src/main/icons/mac/1024.png
new file mode 100644
index 0000000..c1c8691
--- /dev/null
+++ b/src/main/icons/mac/1024.png
Binary files differ
diff --git a/src/main/icons/mac/128.png b/src/main/icons/mac/128.png
new file mode 100644
index 0000000..de9bee6
--- /dev/null
+++ b/src/main/icons/mac/128.png
Binary files differ
diff --git a/src/main/icons/mac/256.png b/src/main/icons/mac/256.png
new file mode 100644
index 0000000..c3a68b9
--- /dev/null
+++ b/src/main/icons/mac/256.png
Binary files differ
diff --git a/src/main/icons/mac/512.png b/src/main/icons/mac/512.png
new file mode 100644
index 0000000..b2fc07e
--- /dev/null
+++ b/src/main/icons/mac/512.png
Binary files differ
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
new file mode 100644
index 0000000..e708bdd
--- /dev/null
+++ b/src/main/resources/base/toolbar/ellipse.png
Binary files differ