2940 lines
135 KiB
Python
2940 lines
135 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Load modules
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
import networkx as nx
|
|
from ast import literal_eval
|
|
from uuid import uuid4
|
|
from shutil import move
|
|
from inspect import getsourcefile
|
|
from os import listdir, access, R_OK, W_OK
|
|
from os.path import abspath, dirname
|
|
from msgpack import load
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Union
|
|
from PySide6 import QtWidgets, QtGui, QtCore
|
|
|
|
from Core import MessageHandler, SettingsObject
|
|
from Core import ResourceHandler
|
|
from Core import ReportGeneration
|
|
from Core import EntityDB
|
|
from Core import ResolutionManager
|
|
from Core import URLManager
|
|
from Core import FrontendCommunicationsHandler
|
|
from Core.Interface import CentralPane
|
|
from Core.Interface import DockBarOne, DockBarTwo, DockBarThree
|
|
from Core.Interface import ToolBarOne
|
|
from Core.Interface import MenuBar
|
|
from Core.Interface import Stylesheets
|
|
from Core.Interface.Entity import BaseNode, BaseConnector, GroupNode
|
|
from Core.PathHelper import is_path_exists_or_creatable_portable
|
|
|
|
|
|
# Main Window of Application
|
|
class MainWindow(QtWidgets.QMainWindow):
|
|
|
|
# Redefining the function to adjust its signature.
|
|
def centralWidget(self) -> Union[QtWidgets.QWidget, QtWidgets.QWidget, CentralPane.WorkspaceWidget]:
|
|
return super(MainWindow, self).centralWidget()
|
|
|
|
def getSettings(self) -> SettingsObject.SettingsObject:
|
|
return self.SETTINGS
|
|
|
|
# What happens when the software is closed
|
|
def closeEvent(self, event) -> None:
|
|
self.dockbarThree.logViewerUpdateThread.endLogging = True
|
|
# Save the window settings
|
|
self.SETTINGS.setValue("MainWindow/Geometry", self.saveGeometry().data())
|
|
self.SETTINGS.setValue("MainWindow/WindowState", self.saveState().data())
|
|
if self.FCOM.isConnected():
|
|
self.FCOM.close()
|
|
self.SETTINGS.setValue("Project/Server/Project", "")
|
|
self.saveProject()
|
|
# Wait just a little for the logging thread to close.
|
|
# We don't _have_ to do this, but it stops errors from popping up due to threads being rudely interrupted.
|
|
while not self.dockbarThree.logViewerUpdateThread.isFinished():
|
|
time.sleep(0.01)
|
|
super(MainWindow, self).closeEvent(event)
|
|
|
|
def saveProject(self) -> None:
|
|
# noinspection PyBroadException
|
|
try:
|
|
self.SETTINGS.save()
|
|
self.LENTDB.save()
|
|
self.centralWidget().tabbedPane.save()
|
|
self.setStatus("Project Saved.", 3000)
|
|
self.MESSAGEHANDLER.info('Project Saved')
|
|
except Exception as e:
|
|
errorMessage = "Could not Save Project: " + str(repr(e))
|
|
self.MESSAGEHANDLER.error(errorMessage, exc_info=True)
|
|
self.setStatus("Failed Saving Project.", 3000)
|
|
self.MESSAGEHANDLER.info("Failed Saving Project " + self.SETTINGS.value("Project/Name", 'Untitled'))
|
|
raise e
|
|
|
|
def saveAsProject(self) -> None:
|
|
if len(self.resolutions) > 0:
|
|
self.MESSAGEHANDLER.warning('Cannot Save As project while resolutions are running. Running resolutions: '
|
|
+ str(self.resolutions), popUp=True)
|
|
return
|
|
# Native file dialogs (at least on Ubuntu) return sandboxed paths in some occasions, which messes with the
|
|
# saving of the project, since we need to create a directory to save the project in.
|
|
saveAsDialog = QtWidgets.QFileDialog()
|
|
saveAsDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
saveAsDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
saveAsDialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
|
|
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
|
saveAsExec = saveAsDialog.exec()
|
|
if not saveAsExec:
|
|
self.setStatus('Save As operation cancelled.')
|
|
return
|
|
fileName = saveAsDialog.selectedFiles()[0]
|
|
newProjectPath = Path(fileName)
|
|
# There is a limit to how long path names can be. This will not prevent all edge cases with nested files,
|
|
# since users can have files with absurdly long names, but it should be a reasonable precaution.
|
|
if not is_path_exists_or_creatable_portable(str(newProjectPath)):
|
|
self.MESSAGEHANDLER.error(
|
|
'Invalid project name or path to save at.', popUp=True, exc_info=False)
|
|
return
|
|
|
|
try:
|
|
newProjectPath.mkdir(0o700, parents=False, exist_ok=False)
|
|
except FileExistsError:
|
|
self.MESSAGEHANDLER.error('Cannot save project to an existing directory. Please choose a unique name.',
|
|
popUp=True, exc_info=False)
|
|
return
|
|
except FileNotFoundError:
|
|
self.MESSAGEHANDLER.error('Cannot save project into a non-existing parent directory.'
|
|
'Please create the required parent directories and try again.',
|
|
popUp=True, exc_info=False)
|
|
return
|
|
|
|
oldName = self.SETTINGS.value("Project/Name")
|
|
oldBaseDir = self.SETTINGS.value("Project/BaseDir")
|
|
oldFilesDir = self.SETTINGS.value("Project/FilesDir")
|
|
|
|
self.SETTINGS.setValue("Project/BaseDir", str(newProjectPath))
|
|
self.SETTINGS.setValue("Project/FilesDir",
|
|
str(Path(self.SETTINGS.value("Project/BaseDir")).joinpath("Project Files")))
|
|
self.SETTINGS.setValue("Project/Name", newProjectPath.name)
|
|
|
|
try:
|
|
Path(self.SETTINGS.value("Project/FilesDir")).mkdir(0o700, parents=False, exist_ok=False)
|
|
except FileExistsError:
|
|
self.MESSAGEHANDLER.error('Cannot save project to an existing directory. Please choose a unique name.',
|
|
popUp=True, exc_info=False)
|
|
self.SETTINGS.setValue("Project/BaseDir", oldBaseDir)
|
|
self.SETTINGS.setValue("Project/FilesDir", oldFilesDir)
|
|
self.SETTINGS.setValue("Project/Name", oldName)
|
|
return
|
|
except FileNotFoundError:
|
|
self.MESSAGEHANDLER.error('Cannot save project into a non-existing parent directory.'
|
|
'Please create the required parent directories and try again.',
|
|
popUp=True, exc_info=False)
|
|
self.SETTINGS.setValue("Project/BaseDir", oldBaseDir)
|
|
self.SETTINGS.setValue("Project/FilesDir", oldFilesDir)
|
|
self.SETTINGS.setValue("Project/Name", oldName)
|
|
return
|
|
|
|
self.setWindowTitle("LinkScope - " + self.SETTINGS.get('Project/Name', 'Untitled'))
|
|
self.saveProject()
|
|
self.setStatus('Project Saved As: ' + newProjectPath.name)
|
|
|
|
# https://networkx.org/documentation/stable/reference/readwrite/graphml.html
|
|
def exportCanvasToGraphML(self):
|
|
currentCanvasGraph = self.centralWidget().tabbedPane.getCurrentScene().sceneGraph
|
|
saveAsDialog = QtWidgets.QFileDialog()
|
|
saveAsDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
saveAsDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
saveAsDialog.setNameFilter("GraphML (*.xml)")
|
|
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
|
if saveAsDialog.exec():
|
|
try:
|
|
filePath = saveAsDialog.selectedFiles()[0]
|
|
if Path(filePath).suffix != '.xml':
|
|
filePath += '.xml'
|
|
nx.write_graphml(currentCanvasGraph, filePath)
|
|
self.setStatus('Canvas exported successfully.')
|
|
except Exception as exc:
|
|
self.MESSAGEHANDLER.error("Could not export canvas to file.", popUp=True)
|
|
self.setStatus('Canvas export failed.')
|
|
|
|
def importCanvasFromGraphML(self):
|
|
openDialog = QtWidgets.QFileDialog()
|
|
openDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
openDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
openDialog.setNameFilter("GraphML (*.xml)")
|
|
if openDialog.exec():
|
|
filePath = openDialog.selectedFiles()[0]
|
|
try:
|
|
read_graphml = nx.read_graphml(filePath)
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
for node in read_graphml.nodes():
|
|
entity = self.LENTDB.getEntity(node)
|
|
if node not in currentScene.nodesDict:
|
|
if entity['Entity Type'] == 'EntityGroup':
|
|
currentScene.addNodeProgrammatic(node, entity['Child UIDs'])
|
|
else:
|
|
currentScene.addNodeProgrammatic(node)
|
|
currentScene.rearrangeGraph()
|
|
self.setStatus('Canvas imported successfully.')
|
|
except KeyError:
|
|
self.MESSAGEHANDLER.error("Cannot import canvas: One or more nodes in the graph "
|
|
"do not exist in the database.", popUp=True)
|
|
self.setStatus('Canvas import failed.')
|
|
except Exception as exc:
|
|
self.MESSAGEHANDLER.error("Cannot import canvas: " + str(exc), popUp=True)
|
|
self.setStatus('Canvas import failed.')
|
|
|
|
def exportDatabaseToGraphML(self):
|
|
# Need to create a new database to remove the icons
|
|
self.LENTDB.dbLock.acquire()
|
|
currentDatabase = nx.DiGraph(self.LENTDB.database)
|
|
self.LENTDB.dbLock.release()
|
|
|
|
for node in currentDatabase.nodes:
|
|
del currentDatabase.nodes[node]['Icon']
|
|
|
|
for edge in currentDatabase.edges:
|
|
currentDatabase.edges[edge]['uid'] = str(currentDatabase.edges[edge]['uid'])
|
|
|
|
saveAsDialog = QtWidgets.QFileDialog()
|
|
saveAsDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
saveAsDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
saveAsDialog.setNameFilter("GraphML (*.xml)")
|
|
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
|
if saveAsDialog.exec():
|
|
try:
|
|
filePath = saveAsDialog.selectedFiles()[0]
|
|
if Path(filePath).suffix != '.xml':
|
|
filePath += '.xml'
|
|
nx.write_graphml(currentDatabase, filePath)
|
|
self.setStatus('Database exported successfully.')
|
|
except Exception as exc:
|
|
self.MESSAGEHANDLER.error("Could not export database to file.", popUp=True)
|
|
self.setStatus('Database export failed.')
|
|
|
|
def importDatabaseFromGraphML(self):
|
|
openDialog = QtWidgets.QFileDialog()
|
|
openDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
openDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
openDialog.setNameFilter("GraphML (*.xml)")
|
|
if openDialog.exec():
|
|
try:
|
|
filePath = openDialog.selectedFiles()[0]
|
|
read_graphml = nx.read_graphml(filePath)
|
|
for node in read_graphml.nodes:
|
|
read_graphml.nodes[node]['Icon'] = self.RESOURCEHANDLER.getEntityDefaultPicture(
|
|
read_graphml.nodes[node]['Entity Type'])
|
|
for edge in read_graphml.edges:
|
|
read_graphml.edges[edge]['uid'] = literal_eval(read_graphml.edges[edge]['uid'])
|
|
self.LENTDB.mergeDatabases(read_graphml, fromServer=False)
|
|
self.dockbarOne.existingEntitiesPalette.loadEntities()
|
|
self.LENTDB.resetTimeline()
|
|
self.setStatus('Database imported successfully.')
|
|
except Exception as exc:
|
|
self.MESSAGEHANDLER.error("Could not import database from file.", popUp=True)
|
|
self.setStatus('Database import failed.')
|
|
|
|
def generateReport(self):
|
|
wizard = ReportWizard(self)
|
|
wizard.show()
|
|
|
|
def renameProjectPromptName(self) -> None:
|
|
newName, confirm = QtWidgets.QInputDialog.getText(self,
|
|
'Rename Project',
|
|
'New Name:',
|
|
QtWidgets.QLineEdit.Normal,
|
|
text=self.SETTINGS.value("Project/Name"))
|
|
if confirm:
|
|
if newName == self.SETTINGS.value("Project/Name"):
|
|
self.MESSAGEHANDLER.warning(
|
|
"New name must be different than current name.", popUp=True)
|
|
return
|
|
if newName == '':
|
|
self.MESSAGEHANDLER.warning("New name cannot be blank.", popUp=True)
|
|
return
|
|
self.renameProject(newName)
|
|
|
|
def renameProject(self, newName: str) -> None:
|
|
if len(self.resolutions) > 0:
|
|
self.MESSAGEHANDLER.warning('Cannot Rename project while resolutions are running. Running resolutions: '
|
|
+ str(self.resolutions), popUp=True)
|
|
return
|
|
oldName = self.SETTINGS.value("Project/Name")
|
|
oldBaseDir = self.SETTINGS.value("Project/BaseDir")
|
|
|
|
newBaseDir = Path(oldBaseDir).parent.joinpath(newName)
|
|
|
|
if newBaseDir.exists():
|
|
self.MESSAGEHANDLER.error('Path already exists.', popUp=True, exc_info=False)
|
|
return
|
|
|
|
if not is_path_exists_or_creatable_portable(str(newBaseDir)):
|
|
self.MESSAGEHANDLER.error(
|
|
'Invalid project name or path to save at.', popUp=True, exc_info=False)
|
|
return
|
|
|
|
self.SETTINGS.setValue("Project/BaseDir", str(newBaseDir))
|
|
self.SETTINGS.setValue("Project/FilesDir",
|
|
str(Path(self.SETTINGS.value("Project/BaseDir")).joinpath("Project Files")))
|
|
self.SETTINGS.setValue("Project/Name", newName)
|
|
|
|
move(oldBaseDir, self.SETTINGS.value("Project/BaseDir"))
|
|
oldProjectFile = newBaseDir.joinpath(oldName + '.linkscope')
|
|
oldProjectFile.unlink(missing_ok=True)
|
|
|
|
self.setWindowTitle("LinkScope - " + self.SETTINGS.get('Project/Name', 'Untitled'))
|
|
self.saveProject()
|
|
statusMessage = 'Project Renamed to: ' + newName
|
|
self.setStatus(statusMessage)
|
|
self.MESSAGEHANDLER.info(statusMessage)
|
|
|
|
def addCanvas(self) -> None:
|
|
# Create or open canvas
|
|
connected = False
|
|
if self.FCOM.isConnected():
|
|
connected = True
|
|
|
|
with self.syncedCanvasesLock:
|
|
availableSyncedCanvases = self.syncedCanvases
|
|
newCanvasPopup = CreateOrOpenCanvas(self, connected, availableSyncedCanvases)
|
|
if newCanvasPopup.exec():
|
|
self.MESSAGEHANDLER.info("New Canvas added: " + newCanvasPopup.canvasName)
|
|
|
|
def toggleWorldDoc(self) -> None:
|
|
if self.centralWidget() is not None:
|
|
self.centralWidget().toggleLayout()
|
|
|
|
def toggleLinkingMode(self) -> None:
|
|
if self.centralWidget().tabbedPane.getCurrentScene().appendingToGroup:
|
|
self.setStatus('Cannot create manual link at this moment: Currently adding entities to group.')
|
|
return
|
|
if self.linkingNodes:
|
|
self.centralWidget().tabbedPane.enableAllTabs()
|
|
self.centralWidget().tabbedPane.getCurrentScene().linking = False
|
|
self.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
|
self.linkingNodes = False
|
|
else:
|
|
self.centralWidget().tabbedPane.disableAllTabsExceptCurrent()
|
|
self.centralWidget().tabbedPane.getCurrentScene().linking = True
|
|
self.centralWidget().tabbedPane.getCurrentScene().itemsToLink = []
|
|
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
self.setCursor(QtGui.QCursor(QtCore.Qt.CrossCursor))
|
|
self.linkingNodes = True
|
|
|
|
def deleteSpecificEntity(self, itemUID: str) -> None:
|
|
for canvas in self.centralWidget().tabbedPane.canvasTabs:
|
|
scene = self.centralWidget().tabbedPane.canvasTabs[canvas].scene()
|
|
if itemUID in scene.nodesDict:
|
|
scene.removeNode(scene.nodesDict[itemUID])
|
|
self.LENTDB.removeEntity(itemUID)
|
|
self.MESSAGEHANDLER.info("Deleted node: " + itemUID)
|
|
|
|
def deleteSpecificLink(self, linkUIDs: set) -> None:
|
|
"""
|
|
Remove a set of connections between two nodes. This takes as an argument the set of link UIDs to remove
|
|
(i.e. {(uid, uid), ...}) and it removes them from all canvases.
|
|
:param linkUIDs: A list of link uid tuples to delete.
|
|
:return:
|
|
"""
|
|
linkUIDs = list(linkUIDs)
|
|
for canvas in self.centralWidget().tabbedPane.canvasTabs:
|
|
scene = self.centralWidget().tabbedPane.canvasTabs[canvas].scene()
|
|
for linkUID in linkUIDs:
|
|
scene.removeUIDFromLink(linkUID)
|
|
for linkUID in linkUIDs:
|
|
self.LENTDB.removeLink(linkUID)
|
|
self.MESSAGEHANDLER.info("Deleted link: " + str(linkUID))
|
|
|
|
def setGroupAppendMode(self, enable: bool) -> None:
|
|
if self.centralWidget().tabbedPane.getCurrentScene().linking:
|
|
return
|
|
if enable:
|
|
self.centralWidget().tabbedPane.disableAllTabsExceptCurrent()
|
|
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
|
|
else:
|
|
self.centralWidget().tabbedPane.enableAllTabs()
|
|
self.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
|
|
|
def selectLeafNodes(self) -> None:
|
|
"""
|
|
Select the nodes with at least one link coming into them, and no outgoing links.
|
|
(i.e. They are a child node in at least 1 relationship, but are not a parent node in any relationship.)
|
|
:return:
|
|
"""
|
|
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
currentCanvasGraph = self.centralWidget().tabbedPane.getCurrentScene().sceneGraph
|
|
leaves = [x for x in currentCanvasGraph.nodes()
|
|
if currentCanvasGraph.out_degree(x) == 0 and currentCanvasGraph.in_degree(x) >= 1]
|
|
for item in [node for node in self.centralWidget().tabbedPane.getCurrentScene().items()
|
|
if isinstance(node, BaseNode)]:
|
|
if item.uid in leaves:
|
|
item.setSelected(True)
|
|
|
|
def selectRootNodes(self) -> None:
|
|
"""
|
|
Select all the nodes that have at least one link going out of them, and no incoming links.
|
|
(i.e. They are a parent node in at least 1 relationship, but are not a child node in any relationship.)
|
|
:return:
|
|
"""
|
|
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
currentCanvasGraph = self.centralWidget().tabbedPane.getCurrentScene().sceneGraph
|
|
roots = [x for x in currentCanvasGraph.nodes()
|
|
if currentCanvasGraph.out_degree(x) >= 1 and currentCanvasGraph.in_degree(x) == 0]
|
|
for item in [node for node in self.centralWidget().tabbedPane.getCurrentScene().items()
|
|
if isinstance(node, BaseNode)]:
|
|
if item.uid in roots:
|
|
item.setSelected(True)
|
|
|
|
def selectIsolatedNodes(self) -> None:
|
|
"""
|
|
Select all the nodes that have no links coming into or going out of them.
|
|
:return:
|
|
"""
|
|
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
currentCanvasGraph = self.centralWidget().tabbedPane.getCurrentScene().sceneGraph
|
|
nodes = [x for x in currentCanvasGraph.nodes()
|
|
if currentCanvasGraph.out_degree(x) == 0 and currentCanvasGraph.in_degree(x) == 0]
|
|
for item in [node for node in self.centralWidget().tabbedPane.getCurrentScene().items()
|
|
if isinstance(node, BaseNode)]:
|
|
if item.uid in nodes:
|
|
item.setSelected(True)
|
|
|
|
def selectNonIsolatedNodes(self) -> None:
|
|
"""
|
|
Select all nodes with at least one link going into or out of them.
|
|
:return:
|
|
"""
|
|
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
currentCanvasGraph = self.centralWidget().tabbedPane.getCurrentScene().sceneGraph
|
|
nodes = [x for x in currentCanvasGraph.nodes()
|
|
if currentCanvasGraph.out_degree(x) > 0 or currentCanvasGraph.in_degree(x) > 0]
|
|
for item in [node for node in self.centralWidget().tabbedPane.getCurrentScene().items()
|
|
if isinstance(node, BaseNode)]:
|
|
if item.uid in nodes:
|
|
item.setSelected(True)
|
|
|
|
def findShortestPath(self) -> None:
|
|
"""
|
|
Find the shortest path between two nodes, if it exists.
|
|
Exactly two nodes must be selected.
|
|
:return:
|
|
"""
|
|
endPoints = [item.uid for item in self.centralWidget().tabbedPane.getCurrentScene().selectedItems()
|
|
if isinstance(item, BaseNode)]
|
|
if len(endPoints) != 2:
|
|
self.MESSAGEHANDLER.warning('Exactly two entities must be selected for the Shortest Path function to work.',
|
|
popUp=True)
|
|
return
|
|
currentCanvasGraph = self.centralWidget().tabbedPane.getCurrentScene().sceneGraph
|
|
try:
|
|
shortestPath = nx.shortest_path(currentCanvasGraph, endPoints[0], endPoints[1])
|
|
except nx.NetworkXNoPath:
|
|
try:
|
|
shortestPath = nx.shortest_path(currentCanvasGraph, endPoints[1], endPoints[0])
|
|
except nx.NetworkXNoPath:
|
|
shortestPath = None
|
|
|
|
if shortestPath is None:
|
|
self.setStatus('No path between the selected nodes was found.')
|
|
else:
|
|
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
for item in [node for node in self.centralWidget().tabbedPane.getCurrentScene().items()
|
|
if isinstance(node, BaseNode)]:
|
|
if item.uid in shortestPath:
|
|
item.setSelected(True)
|
|
linksToSelect = [(a, b) for a, b in zip(shortestPath, shortestPath[1:])]
|
|
for linkItem in [link for link in self.centralWidget().tabbedPane.getCurrentScene().items()
|
|
if isinstance(link, BaseConnector)]:
|
|
if linkItem.uid in linksToSelect:
|
|
linkItem.setSelected(True)
|
|
self.setStatus('Shortest path found.')
|
|
|
|
def findEntityOrLinkOnCanvas(self) -> None:
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
currentUIDs = [item.uid for item in currentScene.items() if isinstance(item, BaseNode)
|
|
or isinstance(item, BaseConnector)]
|
|
entityPrimaryFields = []
|
|
for uid in currentUIDs:
|
|
item = self.LENTDB.getEntity(uid)
|
|
if item is None:
|
|
item = self.LENTDB.getLink(uid)
|
|
if item is None:
|
|
continue
|
|
entityPrimaryFields.append(item[list(item)[1]])
|
|
matchedPrimaryFieldsAndUIDs = [item for item in zip(currentUIDs, entityPrimaryFields)]
|
|
# Remove duplicates
|
|
entityPrimaryFields = list(set(entityPrimaryFields))
|
|
matchedPrimaryFieldsAndUIDs = list(set(matchedPrimaryFieldsAndUIDs))
|
|
findPrompt = FindEntityOnCanvasDialog(entityPrimaryFields)
|
|
promptReturnCode = findPrompt.exec()
|
|
|
|
if promptReturnCode:
|
|
uidsToSelect = []
|
|
findText = findPrompt.findInput.text()
|
|
for item in matchedPrimaryFieldsAndUIDs:
|
|
if item[1].startswith(findText):
|
|
uidsToSelect.append(item[0])
|
|
|
|
currentScene.clearSelection()
|
|
for item in [linkOrEntity for linkOrEntity in currentScene.items()
|
|
if isinstance(linkOrEntity, BaseNode) or isinstance(linkOrEntity, BaseConnector)]:
|
|
if item.uid in uidsToSelect:
|
|
item.setSelected(True)
|
|
if len(uidsToSelect) == 1:
|
|
self.centralWidget().tabbedPane.getCurrentView().centerViewportOnNode(uidsToSelect[0])
|
|
|
|
def mergeEntities(self) -> None:
|
|
"""
|
|
Show table of entities w/ primary fields, and incoming / outgoing links.
|
|
Let user choose which entity should be the primary one. For all the rest:
|
|
Get all links to and from them, and add them to the primary one.
|
|
Add their fields to the primary one, if they are not the same.
|
|
Delete them when done.
|
|
:return:
|
|
"""
|
|
entitiesToMerge = [self.LENTDB.getEntity(item.uid)
|
|
for item in self.centralWidget().tabbedPane.getCurrentScene().selectedItems()
|
|
if isinstance(item, BaseNode) and not isinstance(item, GroupNode)]
|
|
if len(entitiesToMerge) < 2:
|
|
self.MESSAGEHANDLER.info('Not enough valid entities to merge selected! Please choose at least two'
|
|
' non-Meta entities.', popUp=True)
|
|
return
|
|
mergeDialog = MergeEntitiesDialog(self, entitiesToMerge)
|
|
|
|
if mergeDialog.exec():
|
|
primaryEntityUID = mergeDialog.primaryEntityUID
|
|
primaryEntity = [entity for entity in entitiesToMerge if entity['uid'] == primaryEntityUID][0]
|
|
otherEntitiesUIDs = mergeDialog.otherEntitiesUIDs # First entity is written first. No overwrites.
|
|
allParentsPrimary = [link[0] for link in self.LENTDB.getIncomingLinks(primaryEntityUID)]
|
|
allChildrenPrimary = [link[1] for link in self.LENTDB.getOutgoingLinks(primaryEntityUID)]
|
|
# Do not want links pointing to itself.
|
|
allParentsPrimary.append(primaryEntityUID)
|
|
allChildrenPrimary.append(primaryEntityUID)
|
|
linksToAdd = []
|
|
|
|
for entityUID in otherEntitiesUIDs:
|
|
otherEntity = [otherEntity for otherEntity in entitiesToMerge if otherEntity['uid'] == entityUID][0]
|
|
# Do not include links to / from entities where such links exist already on the primary entity.
|
|
# Also do not include links to / from other entities that are being merged.
|
|
allIncomingLinks = [link for link in self.LENTDB.getIncomingLinks(entityUID)
|
|
if link[0] not in allParentsPrimary and
|
|
link[0] not in otherEntitiesUIDs]
|
|
allOutgoingLinks = [link for link in self.LENTDB.getOutgoingLinks(entityUID)
|
|
if link[1] not in allChildrenPrimary
|
|
and link[1] not in otherEntitiesUIDs]
|
|
for field in otherEntity:
|
|
# Check if field does not exist, or if it does, but contains 'None' value.
|
|
if str(primaryEntity.get(field)) == 'None':
|
|
primaryEntity[field] = otherEntity[field]
|
|
for incomingLink in allIncomingLinks:
|
|
linkJson = self.LENTDB.getLink(incomingLink)
|
|
linksToAdd.append([incomingLink[0], primaryEntity['uid'], linkJson['Resolution'],
|
|
linkJson['Notes']])
|
|
for outgoingLink in allOutgoingLinks:
|
|
linkJson = self.LENTDB.getLink(outgoingLink)
|
|
linksToAdd.append([primaryEntity['uid'], outgoingLink[1], linkJson['Resolution'],
|
|
linkJson['Notes']])
|
|
self.deleteSpecificEntity(entityUID)
|
|
self.centralWidget().tabbedPane.linkAddHelper(linksToAdd)
|
|
|
|
def splitEntity(self) -> None:
|
|
entityToSplit = [self.LENTDB.getEntity(item.uid)
|
|
for item in self.centralWidget().tabbedPane.getCurrentScene().selectedItems()
|
|
if isinstance(item, BaseNode)]
|
|
validEntityToSplit = [entity for entity in entityToSplit
|
|
if entity['Entity Type'] != 'EntityGroup']
|
|
if len(validEntityToSplit) == 0:
|
|
self.MESSAGEHANDLER.info('No valid entities to split selected! Please choose at least one non-Meta entity.',
|
|
popUp=True)
|
|
return
|
|
elif len(validEntityToSplit) > 1:
|
|
self.MESSAGEHANDLER.info('Multiple entities selected. Please pick one valid entity to split.',
|
|
popUp=True)
|
|
return
|
|
|
|
entityToSplit = validEntityToSplit[0]
|
|
entityToSplitPrimaryFieldKey = list(entityToSplit)[1]
|
|
entityToSplitUID = entityToSplit['uid']
|
|
splitDialog = SplitEntitiesDialog(self, entityToSplit)
|
|
|
|
if splitDialog.exec():
|
|
canvasTabs = self.centralWidget().tabbedPane.canvasTabs
|
|
allScenesWithNode = [canvasTabs[view].scene() for view in canvasTabs
|
|
if entityToSplitUID in canvasTabs[view].scene().sceneGraph.nodes()]
|
|
for newEntityWithLinks in splitDialog.splitEntitiesWithLinks:
|
|
newEntity = {entityToSplitPrimaryFieldKey: newEntityWithLinks[0]}
|
|
for field in entityToSplit:
|
|
if field != 'uid' and field != entityToSplitPrimaryFieldKey:
|
|
newEntity[field] = entityToSplit[field]
|
|
newEntity = self.LENTDB.addEntity(newEntity)
|
|
|
|
for link in newEntityWithLinks[1]:
|
|
newLink = {}
|
|
for field in link:
|
|
newLink[field] = link[field]
|
|
if newLink['uid'][0] == entityToSplitUID:
|
|
newLink['uid'] = (newEntity['uid'], newLink['uid'][1])
|
|
else:
|
|
newLink['uid'] = (newLink['uid'][0], newEntity['uid'])
|
|
self.LENTDB.addLink(newLink)
|
|
for scene in allScenesWithNode:
|
|
scene.addNodeProgrammatic(newEntity['uid'])
|
|
self.deleteSpecificEntity(entityToSplitUID)
|
|
for scene in allScenesWithNode:
|
|
scene.rearrangeGraph()
|
|
|
|
def handleGroupNodeUpdateAfterEntityDeletion(self, entityUID) -> None:
|
|
for canvas in self.centralWidget().tabbedPane.canvasTabs:
|
|
self.centralWidget().tabbedPane.canvasTabs[canvas].cleanDeletedNodeFromGroupsIfExists(entityUID)
|
|
|
|
def editProjectSettings(self) -> None:
|
|
settingsDialog = ProjectEditDialog(self.SETTINGS)
|
|
settingsConfirm = settingsDialog.exec()
|
|
|
|
if settingsConfirm:
|
|
# Save new settings
|
|
newSettings = settingsDialog.newSettings
|
|
for key in newSettings:
|
|
newSettingValue = newSettings[key]
|
|
if newSettingValue[1]:
|
|
# Delete key
|
|
self.SETTINGS.pop(key)
|
|
elif newSettingValue[0] != '':
|
|
# Do not allow blank settings.
|
|
if key == 'Project/Resolution Result Grouping Threshold' or \
|
|
key == 'Project/Number of Answers Returned' or \
|
|
key == 'Project/Question Answering Retriever Value' or \
|
|
key == 'Project/Question Answering Reader Value':
|
|
try:
|
|
int(newSettingValue[1])
|
|
self.SETTINGS.setValue(key, newSettingValue[0])
|
|
except ValueError:
|
|
pass
|
|
elif newSettingValue[0] == 'Copy' or newSettingValue[0] == 'Symlink':
|
|
self.SETTINGS.setValue(key, newSettingValue[0])
|
|
|
|
self.saveProject()
|
|
|
|
def editResolutionsSettings(self) -> None:
|
|
settingsDialog = ResolutionsEditDialog(self.SETTINGS)
|
|
settingsConfirm = settingsDialog.exec()
|
|
|
|
if settingsConfirm:
|
|
# Save new settings
|
|
newSettings = settingsDialog.newSettings
|
|
for key in newSettings:
|
|
newSettingValue = newSettings[key]
|
|
if newSettingValue[1]:
|
|
# Delete key
|
|
self.SETTINGS.pop(key)
|
|
elif newSettingValue[0] != '':
|
|
# Do not allow blank settings.
|
|
self.SETTINGS.setValue(key, newSettingValue[0])
|
|
|
|
self.saveProject()
|
|
|
|
def editLogSettings(self) -> None:
|
|
settingsDialog = LoggingSettingsDialog(self.SETTINGS)
|
|
settingsConfirm = settingsDialog.exec()
|
|
|
|
if settingsConfirm:
|
|
# Save new settings
|
|
newSettings = settingsDialog.newSettings
|
|
for key in newSettings:
|
|
newSettingValue = newSettings[key]
|
|
if newSettingValue[1]:
|
|
# Delete key
|
|
self.SETTINGS.pop(key)
|
|
elif newSettingValue[0] != '':
|
|
# Do not allow blank settings.
|
|
self.SETTINGS.setValue(key, newSettingValue[0])
|
|
|
|
self.MESSAGEHANDLER.setSeverityLevel(self.SETTINGS.value('Logging/Severity'))
|
|
self.MESSAGEHANDLER.changeLogfile(self.SETTINGS.value('Logging/Logfile'))
|
|
self.saveProject()
|
|
|
|
def loadModules(self) -> None:
|
|
"""
|
|
Loads user-defined modules from the Modules folder in the installation directory.
|
|
:return:
|
|
"""
|
|
self.RESOURCEHANDLER.loadModuleEntities()
|
|
modulesBasePath = Path(self.SETTINGS.value("Program/BaseDir")).joinpath("Modules")
|
|
for module in listdir(modulesBasePath):
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromDir(modulesBasePath / module)
|
|
self.setStatus('Loaded Modules.')
|
|
|
|
def reloadModules(self) -> None:
|
|
"""
|
|
Same as loadModules, except this one updates the GUI to show newly loaded entities and resolutions.
|
|
This is meant to be ran after the application started, in case the user wants to load a module without
|
|
closing and reopening the application.
|
|
:return:
|
|
"""
|
|
self.RESOURCEHANDLER.loadModuleEntities()
|
|
modulesBasePath = Path(self.SETTINGS.value("Program/BaseDir")).joinpath("Modules")
|
|
for module in listdir(modulesBasePath):
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromDir(modulesBasePath / module)
|
|
self.dockbarOne.existingEntitiesPalette.loadEntities()
|
|
self.dockbarOne.resolutionsPalette.loadAllResolutions()
|
|
self.dockbarOne.nodesPalette.loadEntities()
|
|
self.setStatus('Reloaded Modules.')
|
|
|
|
def setStatus(self, message: str, timeout: int = 5000) -> None:
|
|
"""
|
|
Show the message provided in the status bar.
|
|
Timeout dictates how long the message is shown, in milliseconds.
|
|
|
|
Translates the message automatically, no need to call 'tr' on the message parameter used.
|
|
"""
|
|
|
|
self.statusBar().showMessage(self.tr(message), timeout)
|
|
|
|
def getPictureOfCanvas(self, canvasName: str, justViewport: bool = True,
|
|
transparentBackground: bool = False) -> Union[QtGui.QPicture, None]:
|
|
view = self.centralWidget().tabbedPane.canvasTabs.get(canvasName)
|
|
if view is None:
|
|
return None
|
|
return view.takePictureOfView(justViewport, transparentBackground)
|
|
|
|
def resetTimeline(self, graph: nx.DiGraph) -> None:
|
|
self.dockbarThree.timeWidget.resetTimeline(graph, True)
|
|
|
|
def updateTimeline(self, node, added: bool = True, updateGraph: bool = True) -> None:
|
|
self.dockbarThree.timeWidget.updateTimeline(node, added, updateGraph)
|
|
|
|
def timelineSelectMatchingEntities(self, timescale: list) -> None:
|
|
if not timescale: # i.e. if timescale == []
|
|
return
|
|
scene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
scene.clearSelection()
|
|
for uid in scene.nodesDict:
|
|
if self.LENTDB.isNode(uid):
|
|
createdDate = datetime.fromisoformat(self.LENTDB.getEntity(uid)['Date Created'])
|
|
try:
|
|
year = timescale[0]
|
|
except IndexError:
|
|
year = None
|
|
try:
|
|
month = timescale[1]
|
|
except IndexError:
|
|
month = None
|
|
try:
|
|
day = timescale[2]
|
|
except IndexError:
|
|
day = None
|
|
try:
|
|
hour = timescale[3]
|
|
except IndexError:
|
|
hour = None
|
|
try:
|
|
minute = timescale[4]
|
|
except IndexError:
|
|
minute = None
|
|
|
|
if minute is not None and createdDate.minute != minute:
|
|
continue
|
|
if hour is not None and createdDate.hour != hour:
|
|
continue
|
|
if day is not None and createdDate.day != day:
|
|
continue
|
|
if month is not None and createdDate.month != month:
|
|
continue
|
|
if year is not None and createdDate.year != year:
|
|
continue
|
|
|
|
scene.nodesDict[uid].setSelected(True)
|
|
|
|
def setCurrentCanvasSelection(self, uidList: list) -> None:
|
|
currScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
currScene.clearSelection()
|
|
for item in uidList:
|
|
if isinstance(item, str):
|
|
nodeItem = currScene.getVisibleNodeForUID(item)
|
|
if nodeItem is not None:
|
|
nodeItem.setSelected(True)
|
|
else:
|
|
for entityItem in item:
|
|
nodeItem = currScene.getVisibleNodeForUID(entityItem)
|
|
if nodeItem is not None:
|
|
nodeItem.setSelected(True)
|
|
if len(uidList) == 1 and isinstance(uidList[0], str):
|
|
self.centralWidget().tabbedPane.getCurrentView().centerViewportOnNode(uidList[0])
|
|
|
|
def populateDetailsWidget(self, uids) -> None:
|
|
eJson = []
|
|
for uid in uids:
|
|
# Connectors give the list if edge UIDs they represent
|
|
if isinstance(uid, set):
|
|
for linkUID in uid:
|
|
eJson.append(self.LENTDB.getLink(linkUID))
|
|
else:
|
|
eJson.append(self.LENTDB.getEntity(uid))
|
|
|
|
self.dockbarTwo.entDetails.displayWidgetDetails(eJson)
|
|
|
|
def updateEntityNodeLabelsOnCanvases(self, uid: str, label: str) -> None:
|
|
"""
|
|
If an entity is updated (i.e. re-added) to the database, the labels
|
|
for each node on each canvas need to be updated as well.
|
|
|
|
:param label:
|
|
:param uid:
|
|
:return:
|
|
"""
|
|
for tab in self.centralWidget().tabbedPane.canvasTabs:
|
|
scene = self.centralWidget().tabbedPane.canvasTabs[tab].scene()
|
|
try:
|
|
scene.nodesDict[uid].updateLabel(label)
|
|
except KeyError:
|
|
pass
|
|
|
|
def updateLinkLabelsOnCanvases(self, uid: str, label: str) -> None:
|
|
"""
|
|
If a link is updated (i.e. re-added) to the database, the labels
|
|
for each link label on each canvas need to be updated as well.
|
|
|
|
:param label:
|
|
:param uid:
|
|
:return:
|
|
"""
|
|
for tab in self.centralWidget().tabbedPane.canvasTabs:
|
|
scene = self.centralWidget().tabbedPane.canvasTabs[tab].scene()
|
|
try:
|
|
scene.linksDict[uid].updateLabel(label)
|
|
except KeyError:
|
|
pass
|
|
|
|
def populateEntitiesWidget(self, eJson: dict, add: bool) -> None:
|
|
if add:
|
|
self.dockbarOne.existingEntitiesPalette.addEntity(eJson)
|
|
else:
|
|
self.dockbarOne.existingEntitiesPalette.removeEntity(eJson)
|
|
|
|
def populateResolutionsWidget(self, selected) -> None:
|
|
self.dockbarOne.resolutionsPalette.loadResolutionsForSelected(selected)
|
|
|
|
def runResolution(self, resolution) -> None:
|
|
"""
|
|
Runs the specified resolution in another thread.
|
|
"""
|
|
self.setStatus("Running Resolution: " + resolution)
|
|
try:
|
|
category, resolution = resolution.split('/')
|
|
except ValueError:
|
|
self.MESSAGEHANDLER.error('Category name or resolution name should not contain slashes: ' + resolution)
|
|
return
|
|
|
|
scene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
items = scene.selectedItems()
|
|
resArgument = []
|
|
for item in items:
|
|
if not isinstance(item, BaseNode):
|
|
continue
|
|
resArgument.append(self.LENTDB.getEntity(item.uid))
|
|
|
|
parameters = self.RESOLUTIONMANAGER.getResolutionParameters(category, resolution)
|
|
if parameters is None:
|
|
message = 'Resolution parameters not found for resolution: ' + resolution
|
|
self.MESSAGEHANDLER.error(message, popUp=True, exc_info=False)
|
|
self.setStatus(message)
|
|
return
|
|
|
|
resolutionParameterValues = {}
|
|
resolutionUnspecifiedParameterValues = {}
|
|
for parameter in parameters:
|
|
if parameters[parameter].get('global') is True:
|
|
# Extra slash in the middle to ensure that resolutions cannot overwrite these accidentally,
|
|
# since slashes are not allowed by default on Linux or Windows.
|
|
savedParameterValue = self.SETTINGS.value('Resolutions/Global/Parameters/' + parameter)
|
|
else:
|
|
savedParameterValue = self.SETTINGS.value('Resolutions/' + resolution + '/' + parameter)
|
|
if savedParameterValue is not None:
|
|
resolutionParameterValues[parameter] = savedParameterValue
|
|
else:
|
|
resolutionUnspecifiedParameterValues[parameter] = parameters[parameter]
|
|
|
|
# Show Resolution wizard if there are any parameters required that aren't saved in settings, or
|
|
# if the user did not select any items to run the resolution on.
|
|
if len(items) == 0 or (0 < len(parameters) and 0 < len(resolutionUnspecifiedParameterValues)):
|
|
selectEntityList = None
|
|
uidAndPrimaryFields: list = []
|
|
acceptableOriginTypes = None
|
|
if len(items) == 0:
|
|
acceptableOriginTypes = self.RESOLUTIONMANAGER.getResolutionOriginTypes(resolution)
|
|
uidAndPrimaryFields = [(entity['uid'], entity[list(entity)[1]])
|
|
for entity in self.LENTDB.getAllEntities()
|
|
if entity['Entity Type'] in acceptableOriginTypes]
|
|
selectEntityList = [entity[1] for entity in uidAndPrimaryFields]
|
|
|
|
parameterSelector = ResolutionParametersSelector(
|
|
resolutionUnspecifiedParameterValues, selectEntityList, acceptableOriginTypes)
|
|
parameterSelectorConfirm = parameterSelector.exec()
|
|
|
|
if not parameterSelectorConfirm:
|
|
self.setStatus('Resolution ' + resolution + ' aborted.')
|
|
return
|
|
else:
|
|
resolutionParameterValues.update(parameterSelector.chosenParameters)
|
|
|
|
newParametersToSave = parameterSelector.parametersToRemember
|
|
for parameterToRemember in newParametersToSave:
|
|
# No need to update settings objects here - these values are only relevant to the mainWindow,
|
|
# since this function is where all the resolutions are ran from.
|
|
if parameters[parameterToRemember].get('global') is True:
|
|
self.SETTINGS.setValue('Resolutions/Global/Parameters/' + parameterToRemember,
|
|
newParametersToSave[parameterToRemember])
|
|
else:
|
|
self.SETTINGS.setValue('Resolutions/' + resolution + '/' + parameterToRemember,
|
|
newParametersToSave[parameterToRemember])
|
|
|
|
if len(items) == 0:
|
|
selectedEntities = parameterSelector.entitySelector.selectedItems()
|
|
if len(selectedEntities) == 0:
|
|
self.setStatus('Resolution ' + resolution +
|
|
' did not run: No entities selected.')
|
|
return
|
|
|
|
for selectedEntity in selectedEntities:
|
|
uid = uidAndPrimaryFields[selectEntityList.index(selectedEntity.text())][0]
|
|
resArgument.append(self.LENTDB.getEntity(uid))
|
|
|
|
resolutionParameterValues['Project Files Directory'] = self.SETTINGS.value("Project/FilesDir")
|
|
resolutionUID = str(uuid4())
|
|
resolutionThread = ResolutionExecutorThread(
|
|
resolution, resArgument, resolutionParameterValues, self, resolutionUID)
|
|
resolutionThread.sig.connect(self.resolutionSignalListener)
|
|
self.MESSAGEHANDLER.info('Running Resolution: ' + resolution)
|
|
resolutionThread.start()
|
|
self.resolutions.append((resolutionThread, category == 'Server Resolutions'))
|
|
|
|
def resolutionSignalListener(self, resolution_name: str, resolution_result: Union[list, str]) -> None:
|
|
"""
|
|
Is called by the threads created by runResolution to handle the
|
|
result, i.e. run the function that adds nodes and links.
|
|
"""
|
|
if isinstance(resolution_result, str):
|
|
self.MESSAGEHANDLER.info(f"Resolution {resolution_name} finished with status: {resolution_result}",
|
|
popUp=True)
|
|
elif len(resolution_result) == 0:
|
|
self.MESSAGEHANDLER.info(f"Resolution {resolution_name} returned no results.", popUp=True)
|
|
else:
|
|
self.centralWidget().tabbedPane.facilitateResolution(resolution_name, resolution_result)
|
|
|
|
self.cleanUpLocalFinishedResolutions()
|
|
self.setStatus("Resolution: " + resolution_name + " completed.")
|
|
|
|
def cleanUpLocalFinishedResolutions(self) -> None:
|
|
"""
|
|
Clean out old non-server resolutions.
|
|
:return:
|
|
"""
|
|
for resolutionThread in self.resolutions:
|
|
if resolutionThread[0].isFinished() and resolutionThread[1] is False:
|
|
self.resolutions.remove(resolutionThread)
|
|
|
|
# Server functions
|
|
def statusMessageListener(self, message: str, showPopup: bool = True) -> None:
|
|
if showPopup:
|
|
self.MESSAGEHANDLER.info(message, popUp=True)
|
|
self.setStatus(message)
|
|
|
|
def connectedToServerListener(self, server: str) -> None:
|
|
self.setStatus("Connected to server: " + server)
|
|
self.dockbarThree.serverStatus.updateStatus("Connected to server: " + server)
|
|
self.SETTINGS.setValue("Project/Server", server)
|
|
self.MESSAGEHANDLER.info("Communications successfully initialized with the server.", popUp=True)
|
|
|
|
def connectToServer(self, password: str, server: str, port: int = 3777) -> None:
|
|
self.setStatus("Connecting to server...")
|
|
if self.FCOM.isConnected():
|
|
self.disconnectFromServer()
|
|
|
|
self.MESSAGEHANDLER.info("Connecting to server: " + server)
|
|
try:
|
|
if self.FCOM.beginCommunications(password=password, server=server, port=port):
|
|
status = "Getting Resolutions..."
|
|
self.MESSAGEHANDLER.info(status)
|
|
self.setStatus(status)
|
|
self.FCOM.askServerForResolutions()
|
|
status = "Getting server projects list..."
|
|
self.MESSAGEHANDLER.info(status)
|
|
self.setStatus(status)
|
|
self.FCOM.askProjectsList()
|
|
else:
|
|
self.setStatus('Failed to connect to server.')
|
|
except ConnectionRefusedError:
|
|
self.MESSAGEHANDLER.info("Connection Refused.", popUp=True)
|
|
except Exception as exception:
|
|
self.MESSAGEHANDLER.error(
|
|
"Exception occurred while connecting to server: " + repr(exception))
|
|
self.disconnectFromServer()
|
|
return
|
|
|
|
def disconnectFromServer(self) -> None:
|
|
self.setStatus("Disconnecting from server...")
|
|
if self.FCOM.isConnected():
|
|
self.MESSAGEHANDLER.info("Closing existing connection...")
|
|
self.FCOM.close()
|
|
self.RESOLUTIONMANAGER.removeServerResolutions()
|
|
self.dockbarOne.resolutionsPalette.loadAllResolutions()
|
|
self.closeServerProjectListener()
|
|
self.setStatus("Disconnected from server.")
|
|
self.dockbarThree.serverStatus.updateStatus("Not connected to a server")
|
|
|
|
def addResolutionsFromServerListener(self, resolutions) -> None:
|
|
self.setStatus("Adding Resolutions from Server...")
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromServer(resolutions)
|
|
self.dockbarOne.resolutionsPalette.loadAllResolutions()
|
|
|
|
def executeRemoteResolution(self, resolution_name: str, resolution_entities: list, resolution_parameters: dict,
|
|
resolution_uid: str):
|
|
self.FCOM.runRemoteResolution(resolution_name, resolution_entities, resolution_parameters, resolution_uid)
|
|
|
|
def cleanServerResolutionListener(self, resolution_uid: str) -> None:
|
|
for resolutionThread in self.resolutions:
|
|
if resolutionThread[0].uid == resolution_uid and resolutionThread[1] is True:
|
|
if resolutionThread[0].isFinished():
|
|
self.resolutions.remove(resolutionThread)
|
|
break
|
|
|
|
def receiveProjectsListListener(self, projects: list) -> None:
|
|
with self.serverProjectsLock:
|
|
self.serverProjects = projects
|
|
|
|
def receiveProjectCanvasesListListener(self, canvases: list) -> None:
|
|
with self.syncedCanvasesLock:
|
|
self.syncedCanvases = canvases
|
|
|
|
def openServerProjectListener(self, project_name: str) -> None:
|
|
self.SETTINGS.setValue("Project/Server/Project", project_name)
|
|
self.setStatus("Opened Server Project: " + project_name)
|
|
self.dockbarThree.serverStatus.updateStatus("Connected to server: " +
|
|
self.SETTINGS.value("Project/Server") + " Project: " + project_name)
|
|
self.syncDatabase()
|
|
self.FCOM.askProjectCanvasesList(project_name)
|
|
self.FCOM.askServerForFileList(project_name)
|
|
self.MESSAGEHANDLER.info("Opened Server Project: " + project_name, popUp=True)
|
|
|
|
def closeCurrentServerProject(self) -> None:
|
|
current_project = self.SETTINGS.value("Project/Server/Project")
|
|
if current_project != "":
|
|
self.FCOM.closeProject(current_project)
|
|
|
|
def closeServerProjectListener(self) -> None:
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
self.SETTINGS.setValue("Project/Server/Project", "")
|
|
self.FCOM.receiveFileAbortAll(project_name)
|
|
self.FCOM.sendFileAbortAll(project_name)
|
|
self.unSyncCanvasByName()
|
|
self.setStatus("Closed Server project: " + project_name)
|
|
self.dockbarThree.serverStatus.updateStatus("Connected to server: " +
|
|
self.SETTINGS.value("Project/Server"))
|
|
|
|
self.dockbarOne.documentsList.updateFileListFromServer(None)
|
|
with self.serverProjectsLock:
|
|
self.serverProjects = []
|
|
with self.syncedCanvasesLock:
|
|
self.syncedCanvases = []
|
|
|
|
def openServerCanvasListener(self, canvas_name: str) -> None:
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
self.setStatus("Opened Server Canvas: " + canvas_name + " on project: " + project_name)
|
|
self.dockbarThree.serverStatus.updateStatus("Connected to server: " +
|
|
self.SETTINGS.value("Project/Server") + " Project: " +
|
|
project_name + " Canvas: " + canvas_name)
|
|
|
|
def closeServerCanvasListener(self, canvas_name: str) -> None:
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
self.centralWidget().tabbedPane.unmarkSyncedCanvasesByName(canvas_name)
|
|
self.setStatus("Closed Server Canvas: " + canvas_name + " on project: " + project_name)
|
|
self.dockbarThree.serverStatus.updateStatus("Connected to server: " +
|
|
self.SETTINGS.value("Project/Server") + " Project: " +
|
|
project_name)
|
|
|
|
def syncDatabase(self):
|
|
if self.FCOM.isConnected():
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != "":
|
|
with self.LENTDB.dbLock:
|
|
self.FCOM.syncDatabase(project_name, self.LENTDB.database)
|
|
self.MESSAGEHANDLER.info('Database Synced for project: ' + project_name)
|
|
|
|
def syncCanvasByName(self, canvasName: str = None) -> None:
|
|
"""
|
|
If canvasName is None, syncs the current canvas.
|
|
:param canvasName:
|
|
:return:
|
|
"""
|
|
if self.FCOM.isConnected():
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
canvas_name, canvas_graph = self.centralWidget().tabbedPane.markCanvasAsSyncedByName(canvasName)
|
|
if canvas_name is not None:
|
|
self.FCOM.syncCanvasSend(project_name, canvas_name, canvas_graph)
|
|
self.MESSAGEHANDLER.info('Syncing Canvas: ' + canvas_name)
|
|
else:
|
|
self.setStatus('No canvas to sync!')
|
|
else:
|
|
self.setStatus('Must Create or Open a project on the Server before syncing canvases.')
|
|
else:
|
|
self.setStatus("Not connected to server.")
|
|
|
|
def unSyncCurrentCanvas(self) -> None:
|
|
currentCanvasName = self.centralWidget().tabbedPane.getCurrentView().name
|
|
self.unSyncCanvasByName(currentCanvasName)
|
|
|
|
def unSyncCanvasByName(self, canvasName: str = None) -> None:
|
|
"""
|
|
If canvasName is None, unsyncs ALL canvases.
|
|
Use 'unSyncCurrentCanvas' to unsync the current canvas.
|
|
:param canvasName:
|
|
:return:
|
|
"""
|
|
self.centralWidget().tabbedPane.unmarkSyncedCanvasesByName(canvasName)
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
self.FCOM.closeCanvas(project_name, canvasName)
|
|
if canvasName is not None:
|
|
self.setStatus('Stopped syncing Canvas: ' + canvasName)
|
|
self.MESSAGEHANDLER.info('Stopped syncing Canvas: ' + canvasName)
|
|
|
|
def receiveSyncCanvasListener(self, canvas_name: str, canvas_nodes: dict, canvas_edges: dict) -> None:
|
|
if canvas_name in self.centralWidget().tabbedPane.canvasTabs:
|
|
canvasToSync = self.centralWidget().tabbedPane.canvasTabs[canvas_name]
|
|
if canvasToSync.synced:
|
|
canvasToSync.scene().syncCanvas(canvas_nodes, canvas_edges)
|
|
self.MESSAGEHANDLER.debug('Canvas ' + canvas_name + ' synced.')
|
|
|
|
def receiveSyncDatabaseListener(self, database_nodes: dict, database_edges: dict) -> None:
|
|
"""
|
|
Handles received Database Sync events sent from the server.
|
|
"""
|
|
self.LENTDB.mergeDatabases(database_nodes, database_edges, fromServer=True)
|
|
self.MESSAGEHANDLER.debug('Project database synced.')
|
|
|
|
def sendLocalCanvasUpdateToServer(self, canvas_name: str, entity_or_link_uid: Union[str, tuple]) -> None:
|
|
if not self.FCOM.isConnected():
|
|
return
|
|
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
self.FCOM.sendCanvasUpdateEvent(project_name, canvas_name, entity_or_link_uid)
|
|
self.MESSAGEHANDLER.debug('Project canvas ' + canvas_name + ' send update for entity / link: ' +
|
|
str(entity_or_link_uid))
|
|
|
|
def receiveServerCanvasUpdate(self, canvas_name: str, entity_or_link_uid: Union[str, tuple]) -> None:
|
|
scene = self.centralWidget().tabbedPane.getSceneByName(canvas_name)
|
|
if scene is not None:
|
|
# Check that the argument in the update is not empty.
|
|
if entity_or_link_uid:
|
|
if isinstance(entity_or_link_uid, str):
|
|
# Add Entity
|
|
if entity_or_link_uid not in scene.nodesDict:
|
|
scene.addNodeProgrammatic(entity_or_link_uid, fromServer=True)
|
|
scene.rearrangeGraph()
|
|
else:
|
|
# Add Link
|
|
if entity_or_link_uid not in scene.linksDict:
|
|
scene.addLinkProgrammatic(entity_or_link_uid, fromServer=True)
|
|
scene.rearrangeGraph()
|
|
|
|
self.MESSAGEHANDLER.debug('Received update to canvas: ' + canvas_name + ' for entity / link: ' +
|
|
str(entity_or_link_uid))
|
|
|
|
def sendLocalDatabaseUpdateToServer(self, entityJson: dict, add: bool) -> None:
|
|
"""
|
|
Called by the database when a local item event occurs.
|
|
If connected to a server, the event is propagated to all other connected clients.
|
|
"""
|
|
if not self.FCOM.isConnected():
|
|
return
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
self.FCOM.sendDatabaseUpdateEvent(project_name, entityJson, add)
|
|
self.MESSAGEHANDLER.debug('Sent database update to server: ' + str(entityJson) + ' - Operation: ' +
|
|
str(add))
|
|
|
|
def receiveServerDatabaseUpdate(self, entityJson, add) -> None:
|
|
# Check that the JSON received is not empty.
|
|
if entityJson:
|
|
uid = entityJson['uid']
|
|
if add:
|
|
# Add item
|
|
if isinstance(uid, str):
|
|
# Add Entity
|
|
self.LENTDB.addEntity(entityJson, fromServer=True)
|
|
else:
|
|
# Add Link
|
|
self.centralWidget().tabbedPane.serverLinkAddHelper(entityJson)
|
|
else:
|
|
# Remove item
|
|
if isinstance(uid, str):
|
|
# Remove Entity
|
|
self.centralWidget().tabbedPane.nodeRemoveAllHelper(uid)
|
|
self.LENTDB.removeEntity(uid, fromServer=True)
|
|
else:
|
|
# Remove Link
|
|
self.centralWidget().tabbedPane.linkRemoveAllHelper(uid)
|
|
self.LENTDB.removeLink(uid, fromServer=True)
|
|
|
|
self.MESSAGEHANDLER.debug('Received database update from server: ' + str(entityJson) + ' - Operation: ' +
|
|
str(add))
|
|
|
|
def uploadFiles(self, items=None) -> None:
|
|
if not self.FCOM.isConnected():
|
|
self.setStatus("Not Connected to Server.")
|
|
return
|
|
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name == "":
|
|
self.setStatus("No Open Project, cannot upload files.")
|
|
self.MESSAGEHANDLER.warning("Must create or open a Server Project before uploading files.", popUp=True)
|
|
return
|
|
|
|
self.setStatus('Uploading...')
|
|
|
|
itemsToUpload = []
|
|
if items is not None:
|
|
itemsToUpload = items
|
|
else:
|
|
materialsEntities = self.RESOURCEHANDLER.getAllEntitiesInCategory('Materials')
|
|
for item in self.centralWidget().tabbedPane.getCurrentScene().selectedItems():
|
|
itemJson = self.LENTDB.getEntity(item.uid)
|
|
if itemJson.get('Entity Type') in materialsEntities and \
|
|
itemJson['File Path'] != "None":
|
|
itemsToUpload.append(itemJson)
|
|
|
|
if not itemsToUpload:
|
|
self.setStatus('No Materials entities selected to upload to Server.')
|
|
self.MESSAGEHANDLER.info('To send files to the server, please select any number of entities on the '
|
|
'current canvas. The type of each selected entity has to be one of the types '
|
|
'in the Materials category.', popUp=True)
|
|
return
|
|
|
|
for item in itemsToUpload:
|
|
fileDir = self.SETTINGS.value("Project/FilesDir") / Path(item['File Path'])
|
|
file_name = item[list(item)[1]]
|
|
if file_name in [uploadingFileName.getFileName()
|
|
for uploadingFileName in self.dockbarOne.documentsList.uploadingFileWidgets]:
|
|
continue
|
|
if file_name in [uploadedFileName.getFileName()
|
|
for uploadedFileName in self.dockbarOne.documentsList.uploadedFileWidgets]:
|
|
continue
|
|
if fileDir.exists():
|
|
self.dockbarOne.documentsList.addUploadingFileToList(item[list(item)[1]])
|
|
self.FCOM.sendFile(project_name, file_name, fileDir)
|
|
|
|
def receiveAbortUploadOfFiles(self, file_name: Union[str, None]):
|
|
"""
|
|
Remove file_name from documents widget on dockbar one.
|
|
If None is passed instead of a string, all currently uploading files are removed.
|
|
|
|
:param file_name:
|
|
:return:
|
|
"""
|
|
if file_name is not None:
|
|
self.dockbarOne.documentsList.finishUploadingFile(file_name)
|
|
else:
|
|
for uploadingFile in self.dockbarOne.documentsList.uploadingFileWidgets:
|
|
self.dockbarOne.documentsList.takeTopLevelItem(
|
|
self.dockbarOne.documentsList.indexOfTopLevelItem(uploadingFile))
|
|
self.dockbarOne.documentsList.uploadingFileWidgets.remove(uploadingFile)
|
|
|
|
def receiveFileListListener(self, fileList: list) -> None:
|
|
self.MESSAGEHANDLER.debug('Received file list from server: ' + str(fileList))
|
|
self.dockbarOne.documentsList.updateFileListFromServer(fileList)
|
|
|
|
def fileUploadFinishedListener(self, file_name: str) -> None:
|
|
self.MESSAGEHANDLER.debug('File upload finished:' + file_name)
|
|
self.dockbarOne.documentsList.finishUploadingFile(file_name)
|
|
|
|
def abortUpload(self, file_name: str) -> None:
|
|
if self.FCOM.isConnected():
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
self.FCOM.sendFileAbort(project_name, file_name)
|
|
|
|
def downloadFile(self, file_name: str) -> None:
|
|
saveDir = Path(self.SETTINGS.value("Project/FilesDir")) / file_name
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if not self.FCOM.isConnected():
|
|
self.setStatus("Not Connected to a Server.")
|
|
self.MESSAGEHANDLER.info('Cannot Download file: Not connected to a Server.', popUp=True)
|
|
if project_name != '':
|
|
if saveDir.exists():
|
|
# Do not overwrite local files.
|
|
return
|
|
self.FCOM.receiveFile(project_name, file_name)
|
|
else:
|
|
self.setStatus("No currently open Server Project.")
|
|
self.MESSAGEHANDLER.info('Cannot Download file: Not working on a Server Project.', popUp=True)
|
|
|
|
def abortDownload(self, file_name: str) -> None:
|
|
if self.FCOM.isConnected():
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
self.FCOM.receiveFileAbort(project_name, file_name)
|
|
|
|
def getSummaryOfDocument(self, document_name: Union[str, None]) -> None:
|
|
if self.FCOM.isConnected():
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name != '':
|
|
if document_name is None:
|
|
selectedDocuments = self.dockbarOne.documentsList.selectedItems()
|
|
if len(selectedDocuments) > 0:
|
|
document_name = selectedDocuments[0].getFileName()
|
|
else:
|
|
self.setStatus("Must upload and select document before obtaining summary.")
|
|
self.MESSAGEHANDLER.warning("Must upload and select document from 'Files Loaded' section "
|
|
"before getting summary.", popUp=True)
|
|
self.FCOM.askServerForFileSummary(project_name, document_name)
|
|
else:
|
|
self.setStatus("Not Connected to Server.")
|
|
self.MESSAGEHANDLER.info('Cannot get summary of documents: Not working on a Server Project.',
|
|
popUp=True)
|
|
else:
|
|
self.setStatus("No currently open Server Project.")
|
|
|
|
self.MESSAGEHANDLER.info('Cannot get summary of documents: Not connected to a Server.', popUp=True)
|
|
|
|
def receiveSummaryOfDocument(self, document_name: str, summary: str):
|
|
self.centralWidget().setDocTitleAndContents(document_name, summary)
|
|
|
|
def askQuestion(self) -> None:
|
|
question = self.dockbarTwo.oracle.questionSection.text()
|
|
if not self.FCOM.isConnected():
|
|
self.setStatus("Not Connected to Server.")
|
|
self.dockbarTwo.oracle.answerSection.setPlainText(
|
|
"Not connected to Server - Question Answering is disabled."
|
|
)
|
|
else:
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name == "":
|
|
self.setStatus("No Open Project, Question Answering system cannot be used.")
|
|
self.dockbarTwo.oracle.answerSection.setPlainText(
|
|
"No open server Project - Question Answering is disabled."
|
|
)
|
|
else:
|
|
self.dockbarTwo.oracle.answerSection.setPlainText("Calculating Answer...")
|
|
self.MESSAGEHANDLER.info('Asking question: ' + question)
|
|
self.FCOM.askQuestion(project_name, question,
|
|
int(self.SETTINGS.value("Project/Question Answering Reader Value")),
|
|
int(self.SETTINGS.value("Project/Question Answering Retriever Value")),
|
|
int(self.SETTINGS.value("Project/Number of Answers Returned")))
|
|
self.setStatus("Asked Question")
|
|
|
|
def questionAnswerListener(self, response: list) -> None:
|
|
textAns = "No Answer."
|
|
answerCount = len(response)
|
|
if answerCount != 0:
|
|
textAns = ""
|
|
for answerIndex in range(answerCount):
|
|
answer = response[answerIndex]
|
|
if answer['answer']:
|
|
textAns += "Answer " + str(answerIndex + 1) + ": " + answer['answer'] + "\n\n"
|
|
textAns += "Context: ..." + answer['context'] + "...\n\n"
|
|
textAns += "Document Used: " + answer['doc']
|
|
textAns += "\n\n"
|
|
else:
|
|
textAns += "Answer " + str(answerIndex + 1) + ": No Answer\n\n"
|
|
|
|
self.dockbarTwo.oracle.answerSection.setPlainText(textAns)
|
|
self.setStatus("Answered Question")
|
|
|
|
def sendChatMessage(self, chat_message: str) -> None:
|
|
if not self.FCOM.isConnected():
|
|
self.setStatus("Not Connected to Server.")
|
|
else:
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
if project_name == "":
|
|
self.setStatus("No open Project.")
|
|
else:
|
|
self.FCOM.sendTextMessage(project_name, chat_message)
|
|
|
|
def receiveChatMessage(self, message: str) -> None:
|
|
self.dockbarThree.chatBox.receiveMessage(message)
|
|
|
|
def initializeLayout(self) -> None:
|
|
|
|
self.restoreGeometry(QtCore.QByteArray(self.SETTINGS.value("MainWindow/Geometry")))
|
|
self.restoreState(QtCore.QByteArray(self.SETTINGS.value("MainWindow/WindowState")))
|
|
|
|
self.addDockWidget(QtCore.Qt.LeftDockWidgetArea,
|
|
self.dockbarOne)
|
|
self.addDockWidget(QtCore.Qt.RightDockWidgetArea,
|
|
self.dockbarTwo)
|
|
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea,
|
|
self.dockbarThree)
|
|
|
|
self.addToolBar(self.primaryToolbar)
|
|
self.setMenuBar(MenuBar.MenuBar(self))
|
|
|
|
# Set the main window title and show it to the user.
|
|
self.setWindowTitle("LinkScope - " + self.SETTINGS.get('Project/Name', 'Untitled'))
|
|
|
|
self.dockbarOne.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.dockbarTwo.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.dockbarThree.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
self.show()
|
|
|
|
# Moved this here so the software doesn't crash if there are a ton of nodes.
|
|
self.centralWidget().tabbedPane.open()
|
|
# Creating default 'Home' tab, if no tabs exist.
|
|
if len(self.centralWidget().tabbedPane.canvasTabs) == 0:
|
|
self.centralWidget().tabbedPane.createHomeTab()
|
|
|
|
self.setStatus("Ready")
|
|
self.MESSAGEHANDLER.info('Project opened, ready to work.')
|
|
|
|
def __init__(self):
|
|
super(MainWindow, self).__init__()
|
|
self.linkingNodes = False
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
# Create or open project
|
|
nW = NewOrOpenWidget(self)
|
|
nW.exec()
|
|
|
|
if not nW.createProject and nW.openProject is None:
|
|
sys.exit()
|
|
|
|
self.SETTINGS = SettingsObject.SettingsObject()
|
|
if nW.createProject:
|
|
self.SETTINGS.setValue(
|
|
"Project/BaseDir", str(Path(nW.pDir.text()).joinpath(nW.pName.text())))
|
|
self.SETTINGS.setValue("Project/FilesDir",
|
|
str(Path(self.SETTINGS.value("Project/BaseDir")).joinpath("Project Files")))
|
|
self.SETTINGS.setValue("Project/Name", nW.pName.text())
|
|
self.SETTINGS.setValue("Program/BaseDir", dirname(abspath(getsourcefile(lambda: 0))))
|
|
|
|
try:
|
|
Path(self.SETTINGS.value("Project/BaseDir")
|
|
).mkdir(0o700, parents=False, exist_ok=False)
|
|
Path(self.SETTINGS.value("Project/FilesDir")
|
|
).mkdir(0o700, parents=False, exist_ok=False)
|
|
except FileExistsError:
|
|
QtWidgets.QMessageBox.warning(self, 'Cannot create project',
|
|
'Cannot save project to an existing directory. '
|
|
'Please choose a unique name.',
|
|
QtWidgets.QMessageBox.Ok)
|
|
return
|
|
except FileNotFoundError:
|
|
QtWidgets.QMessageBox.warning(self, 'Cannot create project',
|
|
'Cannot save project into a non-existing parent directory.'
|
|
'Please create the required parent directories and try again.',
|
|
QtWidgets.QMessageBox.Ok)
|
|
return
|
|
|
|
elif nW.openProject is not None:
|
|
projectDir = Path(nW.openProject).parent.absolute()
|
|
projectDirFile = open(nW.openProject, "rb")
|
|
self.SETTINGS.load(load(projectDirFile))
|
|
projectDirFile.close()
|
|
|
|
# Re-set file path related settings in case the project or the software was moved.
|
|
self.SETTINGS.setValue("Program/BaseDir", dirname(abspath(getsourcefile(lambda: 0))))
|
|
self.SETTINGS.setValue("Project/BaseDir", str(projectDir))
|
|
self.SETTINGS.setValue("Project/FilesDir", str(projectDir / "Project Files"))
|
|
|
|
self.MESSAGEHANDLER = MessageHandler.MessageHandler(self)
|
|
self.RESOURCEHANDLER = ResourceHandler.ResourceHandler(self, self.MESSAGEHANDLER)
|
|
self.dockbarThree = DockBarThree.DockBarThree(self)
|
|
self.LENTDB = EntityDB.EntitiesDB(self, self.MESSAGEHANDLER, self.RESOURCEHANDLER)
|
|
self.URLMANAGER = URLManager.URLManager(self)
|
|
self.RESOLUTIONMANAGER = ResolutionManager.ResolutionManager(self, self.MESSAGEHANDLER)
|
|
self.FCOM = FrontendCommunicationsHandler.CommunicationsHandler(self)
|
|
|
|
self.syncedCanvases = []
|
|
self.syncedCanvasesLock = threading.Lock()
|
|
self.serverProjects = []
|
|
self.serverProjectsLock = threading.Lock()
|
|
self.resolutions = []
|
|
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromDir(
|
|
Path(self.SETTINGS.value("Program/BaseDir")) / "Core" / "Resolutions" / "Core")
|
|
|
|
self.setCentralWidget(CentralPane.WorkspaceWidget(self,
|
|
self.MESSAGEHANDLER,
|
|
self.URLMANAGER,
|
|
self.LENTDB,
|
|
self.RESOURCEHANDLER))
|
|
|
|
self.loadModules()
|
|
|
|
self.dockbarOne = DockBarOne.DockBarOne(
|
|
self,
|
|
self.RESOLUTIONMANAGER,
|
|
self.RESOURCEHANDLER,
|
|
self.LENTDB)
|
|
|
|
self.dockbarTwo = DockBarTwo.DockBarTwo(self,
|
|
self.RESOURCEHANDLER,
|
|
self.LENTDB)
|
|
|
|
self.primaryToolbar = ToolBarOne.ToolBarOne('Primary Toolbar', self)
|
|
|
|
self.initializeLayout()
|
|
|
|
|
|
class ReportWizard(QtWidgets.QWizard):
|
|
def __init__(self, parent):
|
|
super(ReportWizard, self).__init__(parent=parent)
|
|
|
|
self.primaryFieldsList = []
|
|
|
|
self.addPage(InitialConfigPage(self))
|
|
self.addPage(TitlePage(self))
|
|
|
|
self.addPage(SummaryPage(self))
|
|
|
|
self.selectedNodes = [entity for entity in
|
|
self.parent().centralWidget().tabbedPane.getCurrentScene().selectedItems()
|
|
if isinstance(entity, BaseNode)]
|
|
for selectedNode in self.selectedNodes:
|
|
# used in wizard
|
|
self.primaryField = selectedNode.labelItem.text()
|
|
self.uid = selectedNode.uid
|
|
|
|
# used in report generation
|
|
self.primaryFieldsList.append(self.primaryField)
|
|
self.addPage(EntityPage(self))
|
|
|
|
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
|
self.setWindowTitle("Generate Report Wizard")
|
|
|
|
self.button(QtWidgets.QWizard.FinishButton).clicked.connect(self.onFinish)
|
|
|
|
def onFinish(self):
|
|
reportData = []
|
|
|
|
outgoingEntitiesForEachEntity = []
|
|
incomingEntitiesForEachEntity = []
|
|
outgoingEntityPrimaryFieldsForEachEntity = []
|
|
incomingEntityPrimaryFieldsForEachEntity = []
|
|
|
|
entityList = []
|
|
for pageID in self.pageIds():
|
|
pageObject = self.page(pageID)
|
|
reportData.append(pageObject.getData())
|
|
|
|
for selectedNode in self.selectedNodes:
|
|
uid = selectedNode.uid
|
|
entityList.append(self.parent().LENTDB.getEntity(uid))
|
|
outgoing = self.parent().LENTDB.getOutgoingLinks(uid)
|
|
incoming = self.parent().LENTDB.getIncomingLinks(uid)
|
|
|
|
outgoingEntities = []
|
|
incomingEntities = []
|
|
outgoingNames = []
|
|
incomingNames = []
|
|
|
|
for out in outgoing:
|
|
outLink = self.parent().LENTDB.getLink(out)
|
|
outgoingEntities.append(outLink)
|
|
outgoingEntityJson = self.parent().LENTDB.getEntity(outLink['uid'][1])
|
|
outgoingNames.append(outgoingEntityJson[list(outgoingEntityJson)[1]])
|
|
|
|
for inc in incoming:
|
|
inLink = self.parent().LENTDB.getLink(inc)
|
|
incomingEntities.append(inLink)
|
|
incomingEntityJson = self.parent().LENTDB.getEntity(inLink['uid'][0])
|
|
|
|
incomingNames.append(incomingEntityJson[list(incomingEntityJson)[1]])
|
|
|
|
outgoingEntityPrimaryFieldsForEachEntity.append(outgoingNames)
|
|
incomingEntityPrimaryFieldsForEachEntity.append(incomingNames)
|
|
outgoingEntitiesForEachEntity.append(outgoingEntities)
|
|
incomingEntitiesForEachEntity.append(incomingEntities)
|
|
|
|
path = Path(reportData[0].get('SavePath'))
|
|
|
|
canvas = reportData[2].get('CanvasName')
|
|
viewPortBool = reportData[2].get('ViewPort')
|
|
|
|
canvasPicture = self.parent().getPictureOfCanvas(canvas, viewPortBool, True)
|
|
temp_dir = tempfile.TemporaryDirectory()
|
|
canvasImagePath = Path(temp_dir.name) / 'canvas.png'
|
|
canvasPicture.save(str(canvasImagePath), "PNG")
|
|
|
|
timelinePicture = self.parent().dockbarThree.timeWidget.takePictureOfView(False)
|
|
timelineImagePath = Path(temp_dir.name) / 'timeline.png'
|
|
timelinePicture.save(str(timelineImagePath), "PNG")
|
|
|
|
ReportGeneration.PDFReport(str(path), reportData, outgoingEntitiesForEachEntity, incomingEntitiesForEachEntity,
|
|
entityList, canvasImagePath, timelineImagePath, self.primaryFieldsList,
|
|
incomingEntityPrimaryFieldsForEachEntity, outgoingEntityPrimaryFieldsForEachEntity)
|
|
|
|
timelineImagePath.unlink(missing_ok=True)
|
|
self.parent().MESSAGEHANDLER.info(reportData)
|
|
|
|
|
|
class InitialConfigPage(QtWidgets.QWizardPage):
|
|
def __init__(self, parent=None):
|
|
super(InitialConfigPage, self).__init__(parent)
|
|
self.subtitleLabel = QtWidgets.QLabel("Path to save the report at: ")
|
|
self.savePathEdit = QtWidgets.QLineEdit()
|
|
self.setTitle(self.tr("Initial Configuration Wizard"))
|
|
|
|
pDirButton = QtWidgets.QPushButton("Save Report As...")
|
|
pDirButton.clicked.connect(self.editPath)
|
|
|
|
hLayout = QtWidgets.QVBoxLayout()
|
|
hLayout.addWidget(self.subtitleLabel)
|
|
hLayout.addWidget(self.savePathEdit)
|
|
hLayout.addWidget(pDirButton)
|
|
|
|
layout = QtWidgets.QVBoxLayout()
|
|
layout.addLayout(hLayout)
|
|
self.setLayout(layout)
|
|
|
|
def editPath(self):
|
|
selectedPath = QtWidgets.QFileDialog.getSaveFileName(self,
|
|
"File Name to Save As",
|
|
str(Path.home()),
|
|
filter="PDF Files (*.pdf)",
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
selectedPath = selectedPath[0]
|
|
if selectedPath != '':
|
|
self.savePathEdit.setText(str(Path(selectedPath).absolute()))
|
|
|
|
def getData(self):
|
|
data = {'SavePath': self.savePathEdit.text()}
|
|
return data
|
|
|
|
|
|
class TitlePage(QtWidgets.QWizardPage):
|
|
def __init__(self, parent=None):
|
|
super(TitlePage, self).__init__(parent)
|
|
self.inputTitleEdit = QtWidgets.QLineEdit()
|
|
self.inputSubtitleEdit = QtWidgets.QLineEdit()
|
|
self.inputAuthorsEdit = QtWidgets.QLineEdit()
|
|
self.setTitle(self.tr("Title Page Wizard"))
|
|
|
|
titleLabel = QtWidgets.QLabel("Title: ")
|
|
subtitleLabel = QtWidgets.QLabel("Subtitle: ")
|
|
authorsLabel = QtWidgets.QLabel("Authors: ")
|
|
|
|
hLayout = QtWidgets.QVBoxLayout()
|
|
hLayout.addWidget(titleLabel)
|
|
hLayout.addWidget(self.inputTitleEdit)
|
|
hLayout.addWidget(subtitleLabel)
|
|
hLayout.addWidget(self.inputSubtitleEdit)
|
|
hLayout.addWidget(authorsLabel)
|
|
hLayout.addWidget(self.inputAuthorsEdit)
|
|
|
|
layout = QtWidgets.QVBoxLayout()
|
|
layout.addLayout(hLayout)
|
|
self.setLayout(layout)
|
|
|
|
def getData(self):
|
|
data = {'Title': self.inputTitleEdit.text(), 'Subtitle': self.inputSubtitleEdit.text(),
|
|
'Authors': self.inputAuthorsEdit.text()}
|
|
return data
|
|
|
|
|
|
class SummaryPage(QtWidgets.QWizardPage):
|
|
def __init__(self, parent):
|
|
super(SummaryPage, self).__init__(parent=parent.parent())
|
|
self.setTitle(self.tr("Summary Page Wizard"))
|
|
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
|
|
self.canvasDropDownMenu = QtWidgets.QComboBox()
|
|
self.viewPortCheckBox = QtWidgets.QCheckBox('ViewPort Only')
|
|
self.viewPortCheckBox.setChecked(False)
|
|
self.canvasNames = list(self.parent().centralWidget().tabbedPane.canvasTabs.keys())
|
|
|
|
summaryLabel = QtWidgets.QLabel("Summary Notes: ")
|
|
|
|
canvasLabel = QtWidgets.QLabel("Select canvas to be displayed: ")
|
|
for canvas in self.canvasNames:
|
|
self.canvasDropDownMenu.addItem(canvas)
|
|
|
|
hLayout = QtWidgets.QVBoxLayout()
|
|
hLayout.addWidget(summaryLabel)
|
|
hLayout.addWidget(self.inputNotesEdit)
|
|
hLayout.addWidget(canvasLabel)
|
|
hLayout.addWidget(self.viewPortCheckBox)
|
|
hLayout.addWidget(self.canvasDropDownMenu)
|
|
layout = QtWidgets.QVBoxLayout()
|
|
layout.addLayout(hLayout)
|
|
self.setLayout(layout)
|
|
|
|
def getData(self):
|
|
data = {'SummaryNotes': self.inputNotesEdit.toPlainText(), 'CanvasName': self.canvasDropDownMenu.currentText(),
|
|
'ViewPort': self.viewPortCheckBox.isChecked()}
|
|
return data
|
|
|
|
|
|
class EntityPage(QtWidgets.QWizardPage):
|
|
def __init__(self, parent: ReportWizard):
|
|
super(EntityPage, self).__init__(parent=parent.parent())
|
|
self.inputAppendixImageEdit = QtWidgets.QLineEdit()
|
|
self.appendixWidget = QtWidgets.QWidget()
|
|
self.appendixLayout = QtWidgets.QVBoxLayout()
|
|
|
|
self.setTitle(self.tr(f"Entity Page Wizard"))
|
|
self.setMinimumSize(300, 700)
|
|
|
|
self.entityName = parent.primaryField
|
|
self.uidPicture = parent.uid
|
|
|
|
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
|
|
self.inputImageEdit = QtWidgets.QLineEdit()
|
|
self.button = QtWidgets.QPushButton("Add...")
|
|
|
|
self.scrolllayout = QtWidgets.QVBoxLayout()
|
|
self.scrollwidget = QtWidgets.QWidget()
|
|
|
|
self.defaultpic = self.parent().LENTDB.getEntity(self.uidPicture).get('Icon')
|
|
|
|
summaryLabel = QtWidgets.QLabel(f"Entity {self.entityName} Notes: ")
|
|
|
|
imageLabel = QtWidgets.QLabel("Image Path: ")
|
|
pDirButton = QtWidgets.QPushButton("Select Image...")
|
|
pDirButton.clicked.connect(self.editPath)
|
|
pDirButton.setDisabled(True)
|
|
imageCheckBox = QtWidgets.QCheckBox('Add Custom Entity Image')
|
|
imageCheckBox.setChecked(False)
|
|
imageCheckBox.toggled.connect(pDirButton.setEnabled)
|
|
|
|
self.button.clicked.connect(self.addSection)
|
|
|
|
self.button.setDisabled(True)
|
|
|
|
appendixCheckBox = QtWidgets.QCheckBox('Add Appendix')
|
|
appendixCheckBox.setChecked(False)
|
|
appendixCheckBox.toggled.connect(self.button.setEnabled)
|
|
|
|
self.scrollwidget.setLayout(self.scrolllayout)
|
|
|
|
scroll = QtWidgets.QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setWidget(self.scrollwidget)
|
|
|
|
hLayout = QtWidgets.QVBoxLayout()
|
|
hLayout.addWidget(summaryLabel)
|
|
hLayout.addWidget(self.inputNotesEdit)
|
|
|
|
hLayout.addWidget(imageCheckBox)
|
|
hLayout.addWidget(imageLabel)
|
|
hLayout.addWidget(self.inputImageEdit)
|
|
hLayout.addWidget(pDirButton)
|
|
|
|
hLayout.addWidget(appendixCheckBox)
|
|
hLayout.addWidget(self.button)
|
|
|
|
layout = QtWidgets.QVBoxLayout()
|
|
layout.addLayout(hLayout)
|
|
layout.addWidget(scroll)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def editPath(self):
|
|
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
|
filter="Image Files (*.png *.jpg)")[0]
|
|
if selectedPath != '':
|
|
self.inputImageEdit.setText(str(Path(selectedPath).absolute()))
|
|
|
|
def editAppendixPath(self):
|
|
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
|
filter="Image Files (*.png *.jpg)")[0]
|
|
if selectedPath != '':
|
|
self.inputAppendixImageEdit.setText(str(Path(selectedPath).absolute()))
|
|
|
|
def addSection(self):
|
|
appendixLabelNotes = QtWidgets.QLabel("Entity Notes: ")
|
|
inputAppendixNotesEdit = QtWidgets.QPlainTextEdit()
|
|
imageAppendixLabel = QtWidgets.QLabel("Image Path: ")
|
|
appendixButton = QtWidgets.QPushButton("Select Image...")
|
|
appendixButton.clicked.connect(self.editAppendixPath)
|
|
self.appendixLayout.addWidget(appendixLabelNotes)
|
|
self.appendixLayout.addWidget(inputAppendixNotesEdit)
|
|
self.appendixLayout.addWidget(imageAppendixLabel)
|
|
self.appendixLayout.addWidget(self.inputAppendixImageEdit)
|
|
self.appendixLayout.addWidget(appendixButton)
|
|
self.appendixWidget.setLayout(self.appendixLayout)
|
|
self.scrolllayout.addWidget(self.appendixWidget)
|
|
self.button.setDisabled(True)
|
|
|
|
def getData(self):
|
|
import re
|
|
from svglib.svglib import svg2rlg
|
|
|
|
appendixNotes = []
|
|
if self.inputImageEdit.text() != '':
|
|
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': self.inputImageEdit.text()}
|
|
else:
|
|
if 'svg' in str(self.defaultpic):
|
|
contents = bytearray(self.defaultpic)
|
|
widthRegex = re.compile(b' width="\d*" ')
|
|
fileContents = ''
|
|
for widthMatches in widthRegex.findall(self.defaultpic):
|
|
fileContents = contents.replace(widthMatches, b' ')
|
|
heightRegex = re.compile(b' height="\d*" ')
|
|
for heightMatches in heightRegex.findall(self.defaultpic):
|
|
fileContents = contents.replace(heightMatches, b' ')
|
|
fileContents = fileContents.replace(b'<svg ', b'<svg height="150" width="150" ')
|
|
|
|
temp_dir = tempfile.TemporaryDirectory()
|
|
imagePath = Path(temp_dir.name) / 'entity.svg'
|
|
with open(imagePath, 'wb') as tempFile:
|
|
tempFile.write(bytearray(fileContents))
|
|
|
|
image = svg2rlg(imagePath)
|
|
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': image}
|
|
elif 'PNG' in str(self.defaultpic):
|
|
temp_dir = tempfile.TemporaryDirectory()
|
|
imagePath = Path(temp_dir.name) / 'entity.png'
|
|
with open(imagePath, 'wb') as tempFile:
|
|
tempFile.write(bytearray(self.defaultpic.data()))
|
|
|
|
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': str(imagePath)}
|
|
else:
|
|
self.defaultpic = self.parent().RESOURCEHANDLER.getEntityDefaultPicture(
|
|
self.parent().LENTDB.getEntity(self.uidPicture)['Entity Type'])
|
|
# Default picture is an SVG.
|
|
contents = bytearray(self.defaultpic)
|
|
widthRegex = re.compile(b' width="\d*" ')
|
|
fileContents = ''
|
|
for widthMatches in widthRegex.findall(self.defaultpic):
|
|
fileContents = contents.replace(widthMatches, b' ')
|
|
heightRegex = re.compile(b' height="\d*" ')
|
|
for heightMatches in heightRegex.findall(self.defaultpic):
|
|
fileContents = contents.replace(heightMatches, b' ')
|
|
fileContents = fileContents.replace(b'<svg ', b'<svg height="150" width="150" ')
|
|
|
|
temp_dir = tempfile.TemporaryDirectory()
|
|
imagePath = Path(temp_dir.name) / 'entity.svg'
|
|
with open(imagePath, 'wb') as tempFile:
|
|
tempFile.write(bytearray(fileContents))
|
|
|
|
image = svg2rlg(imagePath)
|
|
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': image}
|
|
|
|
qPlainTextNote = self.appendixWidget.findChildren(QtWidgets.QPlainTextEdit)
|
|
qlineEdits = self.appendixWidget.findChildren(QtWidgets.QLineEdit)
|
|
for i in range(len(qPlainTextNote)):
|
|
appendixDict = {'AppendixEntityNotes': qPlainTextNote[i].toPlainText(),
|
|
'AppendixEntityImage': qlineEdits[i].text()}
|
|
appendixNotes.append(appendixDict)
|
|
|
|
return data, appendixNotes
|
|
|
|
|
|
class CreateOrOpenCanvas(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent, isConnectedToNetwork=False, syncedCanvases=None):
|
|
super().__init__(parent=parent)
|
|
if syncedCanvases is None:
|
|
syncedCanvases = []
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
self.setWindowTitle("Create New Canvas Or Open Existing")
|
|
self.setMinimumSize(400, 200)
|
|
|
|
createCanvasTitleLabel = QtWidgets.QLabel("Create New Canvas")
|
|
createCanvasTitleLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
|
|
|
createCanvasTitleLabel.setFont(QtGui.QFont("Times", 13, QtGui.QFont.Bold))
|
|
|
|
createCanvasTitleLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
createCanvasNameLabel = QtWidgets.QLabel("New Canvas Name:")
|
|
self.createCanvasTextbox = QtWidgets.QLineEdit("")
|
|
createCanvasButton = QtWidgets.QPushButton("Create New Canvas")
|
|
createCanvasButton.clicked.connect(self.confirmCreateCanvas)
|
|
|
|
dialogLayout.addWidget(createCanvasTitleLabel, 0, 0, 1, 4)
|
|
dialogLayout.addWidget(createCanvasNameLabel, 1, 0, 1, 2)
|
|
dialogLayout.addWidget(self.createCanvasTextbox, 1, 2, 1, 2)
|
|
dialogLayout.addWidget(createCanvasButton, 2, 1, 1, 2)
|
|
# Add some space in the layout
|
|
spacer = QtWidgets.QLabel()
|
|
spacer.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
|
|
dialogLayout.addWidget(spacer, 3, 1)
|
|
|
|
openExistingCanvasTitleLabel = QtWidgets.QLabel("Open Existing Canvas")
|
|
openExistingCanvasTitleLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
|
|
|
openExistingCanvasTitleLabel.setFont(QtGui.QFont("Times", 13, QtGui.QFont.Bold))
|
|
|
|
openExistingCanvasTitleLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
openExistingCanvasNameLabel = QtWidgets.QLabel("Canvas Name:")
|
|
self.openExistingCanvasDropdown = QtWidgets.QComboBox()
|
|
self.openExistingCanvasDropdown.setEditable(False)
|
|
self.openExistingCanvasDropdown.SizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
|
tabbedPane = self.parent().centralWidget().tabbedPane
|
|
canvasTabs = tabbedPane.canvasTabs
|
|
existingTabNames = []
|
|
for tabIndex in range(tabbedPane.count()):
|
|
existingTabNames.append(tabbedPane.tabText(tabIndex))
|
|
|
|
openExistingCanvasButton = QtWidgets.QPushButton('Open Existing Canvas')
|
|
openExistingCanvasButton.clicked.connect(self.confirmOpenExistingCanvas)
|
|
|
|
canvasesToOpen = [tabName for tabName in canvasTabs if tabName not in existingTabNames]
|
|
self.openExistingCanvasDropdown.addItems(canvasesToOpen)
|
|
|
|
if len(canvasesToOpen) == 0:
|
|
self.openExistingCanvasDropdown.setEnabled(False)
|
|
openExistingCanvasButton.setEnabled(False)
|
|
|
|
dialogLayout.addWidget(openExistingCanvasTitleLabel, 4, 0, 1, 4)
|
|
dialogLayout.addWidget(openExistingCanvasNameLabel, 5, 0, 1, 2)
|
|
dialogLayout.addWidget(self.openExistingCanvasDropdown, 5, 2, 1, 2)
|
|
dialogLayout.addWidget(openExistingCanvasButton, 6, 1, 1, 2)
|
|
# Add some space in the layout
|
|
spacer = QtWidgets.QLabel()
|
|
spacer.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
|
|
dialogLayout.addWidget(spacer, 7, 1)
|
|
|
|
openServerCanvasTitleLabel = QtWidgets.QLabel("Open Canvas From Server")
|
|
openServerCanvasTitleLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
|
|
|
openServerCanvasTitleLabel.setFont(QtGui.QFont("Times", 13, QtGui.QFont.Bold))
|
|
|
|
openServerCanvasTitleLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
openServerCanvasNameLabel = QtWidgets.QLabel("Canvas Name:")
|
|
self.openServerCanvasDropdown = QtWidgets.QComboBox()
|
|
self.openServerCanvasDropdown.setEditable(False)
|
|
self.openServerCanvasDropdown.SizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
|
self.openServerCanvasDropdown.addItems(syncedCanvases)
|
|
openServerCanvasButton = QtWidgets.QPushButton("Open Canvas from Server")
|
|
openServerCanvasButton.clicked.connect(self.confirmOpenServerCanvas)
|
|
|
|
if not isConnectedToNetwork or syncedCanvases == []:
|
|
self.openServerCanvasDropdown.setEnabled(False)
|
|
openServerCanvasButton.setEnabled(False)
|
|
|
|
dialogLayout.addWidget(openServerCanvasTitleLabel, 8, 0, 1, 4)
|
|
dialogLayout.addWidget(openServerCanvasNameLabel, 9, 0, 1, 2)
|
|
dialogLayout.addWidget(self.openServerCanvasDropdown, 9, 2, 1, 2)
|
|
dialogLayout.addWidget(openServerCanvasButton, 10, 1, 1, 2)
|
|
self.canvasName = ""
|
|
|
|
def confirmCreateCanvas(self):
|
|
self.canvasName = self.createCanvasTextbox.text()
|
|
createStatus = self.parent().centralWidget().tabbedPane.addCanvas(self.canvasName)
|
|
if createStatus:
|
|
self.accept()
|
|
else:
|
|
self.parent().MESSAGEHANDLER.warning("A Canvas with that name already exists!", popUp=True)
|
|
|
|
def confirmOpenServerCanvas(self):
|
|
self.canvasName = self.openServerCanvasDropdown.currentText()
|
|
createStatus = self.parent().centralWidget().tabbedPane.addCanvas(self.canvasName)
|
|
if createStatus:
|
|
self.parent().syncCanvasByName(self.canvasName)
|
|
self.accept()
|
|
else:
|
|
self.parent().MESSAGEHANDLER.warning("A Canvas with that name already exists!", popUp=True)
|
|
|
|
def confirmOpenExistingCanvas(self):
|
|
self.canvasName = self.openExistingCanvasDropdown.currentText()
|
|
self.parent().centralWidget().tabbedPane.showTab(self.canvasName)
|
|
self.accept()
|
|
|
|
|
|
class NewOrOpenWidget(QtWidgets.QDialog):
|
|
"""
|
|
If self.openProject is not None, then the user opened a project.
|
|
If it is none, check self.pName (Project Name) and self.pDir
|
|
(Project Directory).
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
super().__init__(parent=parent)
|
|
|
|
newOrOpenLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(newOrOpenLayout)
|
|
self.setWindowTitle("Create New Project Or Open Existing")
|
|
self.setMinimumSize(700, 300)
|
|
self.setObjectName("settingsWidget")
|
|
self.setStyleSheet(Stylesheets.SETTINGS_WIDGET_STYLESHEET)
|
|
|
|
# New Project
|
|
self.createProject = False
|
|
newProjectNameLabel = QtWidgets.QLabel("New Project")
|
|
newProjectNameLabel.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
newProjectNameLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
|
|
pProjectPathLabel = QtWidgets.QLabel("Project Path:")
|
|
pProjectPathLabel.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
pDirLabel = QtWidgets.QLabel("Directory")
|
|
pDirLabel.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
pDirLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
pNameLabel = QtWidgets.QLabel("Name")
|
|
pNameLabel.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
pNameLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
pSlashLabel = QtWidgets.QLabel("/")
|
|
pSlashLabel.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
pSlashLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
|
|
self.pName = QtWidgets.QLineEdit("Untitled")
|
|
self.pName.setStyleSheet(Stylesheets.PATH_INPUT_STYLESHEET)
|
|
self.pDir = QtWidgets.QLineEdit("")
|
|
self.pDir.setStyleSheet(Stylesheets.PATH_INPUT_STYLESHEET)
|
|
self.pDir.setMinimumSize(400, 10)
|
|
self.pDir.setReadOnly(True)
|
|
|
|
pDirButton = QtWidgets.QPushButton("Select Directory...")
|
|
pDirButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
|
pDirButton.clicked.connect(self.selectProjectDirectory)
|
|
newProjectConfirm = QtWidgets.QPushButton("Create")
|
|
newProjectConfirm.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
|
|
|
newProjectConfirm.clicked.connect(self.createNewProject)
|
|
|
|
newOrOpenLayout.addWidget(newProjectNameLabel, 0, 0, 1, 4)
|
|
newOrOpenLayout.addWidget(pDirLabel, 1, 1)
|
|
newOrOpenLayout.addWidget(pNameLabel, 1, 3)
|
|
newOrOpenLayout.addWidget(pProjectPathLabel, 2, 0)
|
|
newOrOpenLayout.addWidget(pSlashLabel, 2, 2)
|
|
|
|
newOrOpenLayout.addWidget(self.pDir, 2, 1)
|
|
newOrOpenLayout.addWidget(self.pName, 2, 3)
|
|
newOrOpenLayout.addWidget(pDirButton, 3, 1)
|
|
newOrOpenLayout.addWidget(newProjectConfirm, 3, 3)
|
|
|
|
# Existing
|
|
self.openProject = None
|
|
openProjectFileLabel = QtWidgets.QLabel("Open Project File")
|
|
openProjectFileLabel.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
openProjectFileLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
openProjectButton = QtWidgets.QPushButton("Open...")
|
|
openProjectButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
|
openProjectButton.clicked.connect(self.openFilename)
|
|
newOrOpenLayout.addWidget(openProjectFileLabel, 5, 0, 1, 4)
|
|
newOrOpenLayout.addWidget(openProjectButton, 6, 0, 1, 4)
|
|
|
|
def createNewProject(self):
|
|
errorMessage = ""
|
|
newProjectDir = Path(self.pDir.text()) / self.pName.text()
|
|
if not access(self.pDir.text(), R_OK | W_OK):
|
|
errorMessage += "Chosen Directory is not Readable and Writeable!\n"
|
|
if newProjectDir.exists():
|
|
errorMessage += "Chosen Project Path already exists!\n"
|
|
if errorMessage != "":
|
|
showError = QtWidgets.QMessageBox(self)
|
|
showError.setText(errorMessage)
|
|
showError.setStandardButtons(QtWidgets.QMessageBox().Ok)
|
|
showError.setDefaultButton(QtWidgets.QMessageBox().Ok)
|
|
showError.exec()
|
|
|
|
else:
|
|
self.createProject = True
|
|
self.close()
|
|
|
|
def selectProjectDirectory(self):
|
|
|
|
self.setStyleSheet(Stylesheets.SELECT_PROJECT_STYLESHEET)
|
|
|
|
filename = QtWidgets.QFileDialog.getExistingDirectory(self,
|
|
"Select Project Directory",
|
|
str(Path.home()),
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
if access(filename, R_OK | W_OK):
|
|
self.pDir.setText(filename)
|
|
|
|
def openFilename(self):
|
|
|
|
self.setStyleSheet(Stylesheets.SELECT_PROJECT_STYLESHEET)
|
|
|
|
filename = QtWidgets.QFileDialog.getOpenFileName(self,
|
|
"Open Project File",
|
|
str(Path.home()),
|
|
"LinkScope Projects(*.linkscope)",
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
|
|
if filename[0].endswith('.linkscope') and access(filename[0], R_OK | W_OK):
|
|
self.openProject = filename[0]
|
|
self.close()
|
|
|
|
|
|
class ResolutionExecutorThread(QtCore.QThread):
|
|
sig = QtCore.Signal(str, dict)
|
|
|
|
def __init__(self, resolution: str, resolutionArgument: list, resolutionParameters: dict,
|
|
mainWindowObject: MainWindow, uid: str):
|
|
super().__init__()
|
|
self.resolution = resolution
|
|
self.resolutionArgument = resolutionArgument
|
|
self.resolutionParameters = resolutionParameters
|
|
self.mainWindow = mainWindowObject
|
|
self.return_results = True
|
|
self.uid = uid
|
|
self.done = False
|
|
|
|
def run(self):
|
|
try:
|
|
ret = self.mainWindow.RESOLUTIONMANAGER.executeResolution(self.resolution,
|
|
self.resolutionArgument,
|
|
self.resolutionParameters,
|
|
self.uid)
|
|
if ret is None:
|
|
self.mainWindow.MESSAGEHANDLER.error('Resolution ' + self.resolution + ' failed during run.',
|
|
popUp=True)
|
|
elif isinstance(ret, bool):
|
|
# Resolution is running on the server, we do not have results right now.
|
|
ret = None
|
|
except Exception as e:
|
|
self.mainWindow.MESSAGEHANDLER.error('Resolution ' + self.resolution + ' failed during run: ' +
|
|
str(e), popUp=True)
|
|
ret = None
|
|
|
|
# If the resolution is ran on the server or there is a problem, don't emit signal.
|
|
if ret is not None and self.return_results:
|
|
self.sig.emit(self.resolution, ret)
|
|
self.done = True
|
|
|
|
|
|
class ResolutionParametersSelector(QtWidgets.QDialog):
|
|
|
|
def __init__(self, properties: dict, includeEntitySelector: list = None, originTypes: list = None):
|
|
super(ResolutionParametersSelector, self).__init__()
|
|
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.setModal(True)
|
|
self.setWindowTitle('Resolution Wizard')
|
|
self.parametersList = []
|
|
# Have two separate dicts for readability.
|
|
self.chosenParameters = {}
|
|
self.parametersToRemember = {}
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
self.childWidget = QtWidgets.QTabWidget()
|
|
dialogLayout.addWidget(self.childWidget, 0, 0, 4, 2)
|
|
dialogLayout.setRowStretch(0, 1)
|
|
dialogLayout.setColumnStretch(0, 1)
|
|
|
|
if includeEntitySelector is not None and originTypes is not None:
|
|
entitySelectTab = QtWidgets.QWidget()
|
|
entitySelectTab.setLayout(QtWidgets.QVBoxLayout())
|
|
entitySelectTabLabel = QtWidgets.QLabel('Select the entities to use for this resolution. Accepted Origin '
|
|
'Types: ' + ', '.join(originTypes))
|
|
entitySelectTabLabel.setWordWrap(True)
|
|
entitySelectTabLabel.setMaximumWidth(600)
|
|
|
|
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
entitySelectTab.layout().addWidget(entitySelectTabLabel)
|
|
|
|
self.entitySelector = QtWidgets.QListWidget()
|
|
self.entitySelector.addItems(includeEntitySelector)
|
|
|
|
self.entitySelector.setSelectionMode(self.entitySelector.MultiSelection)
|
|
entitySelectTab.layout().addWidget(self.entitySelector)
|
|
|
|
self.childWidget.addTab(entitySelectTab, 'Entities')
|
|
|
|
for key in properties:
|
|
propertyWidget = QtWidgets.QWidget()
|
|
propertyKeyLayout = QtWidgets.QVBoxLayout()
|
|
propertyWidget.setLayout(propertyKeyLayout)
|
|
|
|
propertyLabel = QtWidgets.QLabel(properties[key].get('description'))
|
|
propertyLabel.setWordWrap(True)
|
|
propertyLabel.setMaximumWidth(600)
|
|
|
|
propertyLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
propertyKeyLayout.addWidget(propertyLabel)
|
|
|
|
propertyType = properties[key].get('type')
|
|
propertyValue = properties[key].get('value')
|
|
propertyDefaultValue = properties[key].get('default')
|
|
|
|
if propertyType == 'String':
|
|
propertyInputField = StringPropertyInput(propertyValue, propertyDefaultValue)
|
|
elif propertyType == 'File':
|
|
propertyInputField = FilePropertyInput(propertyValue, propertyDefaultValue)
|
|
elif propertyType == 'SingleChoice':
|
|
propertyInputField = SingleChoicePropertyInput(propertyValue, propertyDefaultValue)
|
|
elif propertyType == 'MultiChoice':
|
|
propertyInputField = MultiChoicePropertyInput(propertyValue, propertyDefaultValue)
|
|
else:
|
|
# If value has invalid type, skip to the next property.
|
|
propertyInputField = None
|
|
|
|
if propertyInputField is not None:
|
|
propertyKeyLayout.addWidget(propertyInputField)
|
|
propertyInputField.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
|
|
rememberChoiceCheckbox = QtWidgets.QCheckBox('Remember Choice')
|
|
rememberChoiceCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
rememberChoiceCheckbox.setChecked(False)
|
|
propertyKeyLayout.addWidget(rememberChoiceCheckbox)
|
|
propertyKeyLayout.setStretch(1, 1)
|
|
|
|
self.childWidget.addTab(propertyWidget, key)
|
|
self.parametersList.append((key, propertyInputField, rememberChoiceCheckbox))
|
|
|
|
nextButton = QtWidgets.QPushButton('Next')
|
|
nextButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
nextButton.clicked.connect(self.nextTab)
|
|
previousButton = QtWidgets.QPushButton('Previous')
|
|
previousButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
previousButton.clicked.connect(self.previousTab)
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
acceptButton.setAutoDefault(True)
|
|
acceptButton.setDefault(True)
|
|
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
cancelButton.clicked.connect(self.reject)
|
|
|
|
dialogLayout.addWidget(previousButton, 4, 0, 1, 1)
|
|
dialogLayout.addWidget(nextButton, 4, 1, 1, 1)
|
|
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
|
|
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
|
|
|
|
def nextTab(self):
|
|
currentIndex = self.childWidget.currentIndex()
|
|
if currentIndex < self.childWidget.count():
|
|
self.childWidget.setCurrentIndex(currentIndex + 1)
|
|
|
|
def previousTab(self):
|
|
currentIndex = self.childWidget.currentIndex()
|
|
if currentIndex > 0:
|
|
self.childWidget.setCurrentIndex(currentIndex - 1)
|
|
|
|
def accept(self) -> None:
|
|
for resolutionParameterName, resolutionParameterInput, resolutionParameterRemember in self.parametersList:
|
|
value = resolutionParameterInput.getValue()
|
|
if value == '':
|
|
msgBox = QtWidgets.QMessageBox()
|
|
msgBox.setModal(True)
|
|
QtWidgets.QMessageBox.warning(msgBox,
|
|
"Not all parameters filled in",
|
|
"Some of the required parameters for the resolution have been left blank."
|
|
" Please fill them in before proceeding.")
|
|
return
|
|
self.chosenParameters[resolutionParameterName] = value
|
|
|
|
if resolutionParameterRemember.isChecked():
|
|
self.parametersToRemember[resolutionParameterName] = value
|
|
|
|
super(ResolutionParametersSelector, self).accept()
|
|
|
|
|
|
class StringPropertyInput(QtWidgets.QLineEdit):
|
|
|
|
def __init__(self, placeholderText, defaultText):
|
|
super(StringPropertyInput, self).__init__()
|
|
self.setPlaceholderText(placeholderText)
|
|
if defaultText is not None:
|
|
self.setText(defaultText)
|
|
|
|
def getValue(self):
|
|
return self.text()
|
|
|
|
|
|
class FilePropertyInput(QtWidgets.QLineEdit):
|
|
|
|
def __init__(self, placeholderText, defaultText):
|
|
super(FilePropertyInput, self).__init__()
|
|
self.setPlaceholderText(placeholderText)
|
|
if defaultText is not None:
|
|
self.setText(defaultText)
|
|
self.fileDialog = QtWidgets.QFileDialog()
|
|
|
|
def getValue(self):
|
|
return self.text()
|
|
|
|
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
fileChosen = self.fileDialog.getOpenFileName(self,
|
|
"Open File",
|
|
str(Path.home()),
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
self.setText(fileChosen[0])
|
|
|
|
|
|
class SingleChoicePropertyInput(QtWidgets.QGroupBox):
|
|
|
|
def __init__(self, optionsSet: set, defaultOption):
|
|
# Ensure that the options given are an actual set (i.e. each one is unique)
|
|
enforceOptionsSet = set(optionsSet)
|
|
super(SingleChoicePropertyInput, self).__init__(title='Option Selection')
|
|
vboxLayout = QtWidgets.QVBoxLayout()
|
|
self.setLayout(vboxLayout)
|
|
|
|
self.options = []
|
|
if defaultOption is None:
|
|
defaultOption = ''
|
|
|
|
for option in enforceOptionsSet:
|
|
radioButton = QtWidgets.QRadioButton(option)
|
|
radioButton.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
|
if option == defaultOption:
|
|
radioButton.setChecked(True)
|
|
else:
|
|
radioButton.setChecked(False)
|
|
self.options.append(radioButton)
|
|
vboxLayout.addWidget(radioButton)
|
|
|
|
def getValue(self):
|
|
for option in self.options:
|
|
if option.isChecked():
|
|
return option.text()
|
|
|
|
return ''
|
|
|
|
|
|
class MultiChoicePropertyInput(QtWidgets.QGroupBox):
|
|
|
|
def __init__(self, optionsSet: set, defaultOptions):
|
|
# Ensure that the options given are an actual set (i.e. each one is unique)
|
|
enforceOptionsSet = set(optionsSet)
|
|
super(MultiChoicePropertyInput, self).__init__(title='Option Selection')
|
|
vboxLayout = QtWidgets.QVBoxLayout()
|
|
self.setLayout(vboxLayout)
|
|
|
|
self.options = []
|
|
if defaultOptions is None:
|
|
defaultOptions = []
|
|
|
|
for option in enforceOptionsSet:
|
|
checkBox = QtWidgets.QCheckBox(option)
|
|
checkBox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
if option in defaultOptions:
|
|
checkBox.setChecked(True)
|
|
else:
|
|
checkBox.setChecked(False)
|
|
self.options.append(checkBox)
|
|
vboxLayout.addWidget(checkBox)
|
|
|
|
def getValue(self):
|
|
valuesSelected = []
|
|
for option in self.options:
|
|
if option.isChecked():
|
|
valuesSelected.append(option.text())
|
|
|
|
return valuesSelected
|
|
|
|
|
|
class ResolutionsEditDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, settingsObject):
|
|
super(ResolutionsEditDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.setMaximumWidth(850)
|
|
self.setMinimumWidth(600)
|
|
self.setMaximumHeight(600)
|
|
self.setMinimumHeight(400)
|
|
self.settings = settingsObject
|
|
self.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
|
|
resolutionsEditDialog = QtWidgets.QGridLayout()
|
|
self.setLayout(resolutionsEditDialog)
|
|
scrollArea = QtWidgets.QScrollArea()
|
|
scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
|
scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
|
scrollArea.setWidgetResizable(True)
|
|
scrollContainer = QtWidgets.QWidget()
|
|
scrollLayout = QtWidgets.QVBoxLayout()
|
|
scrollContainer.setLayout(scrollLayout)
|
|
scrollArea.setWidget(scrollContainer)
|
|
resolutionsEditDialog.addWidget(scrollArea, 0, 0, 2, 2)
|
|
|
|
resolutionCategoryWidget = QtWidgets.QWidget()
|
|
self.resolutionCategoryLayout = SettingsCategoryLayout()
|
|
resolutionCategoryWidget.setLayout(self.resolutionCategoryLayout)
|
|
resolutionCategoryLabel = QtWidgets.QLabel('Resolutions Settings')
|
|
|
|
resolutionCategoryLabel.setFont(QtGui.QFont("Times", 13, QtGui.QFont.Bold))
|
|
resolutionCategoryLabel.setFrameStyle(QtWidgets.QFrame.Raised | QtWidgets.QFrame.Panel)
|
|
|
|
resolutionCategoryLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
scrollLayout.addWidget(resolutionCategoryLabel)
|
|
scrollLayout.addWidget(resolutionCategoryWidget)
|
|
|
|
confirmButton = QtWidgets.QPushButton('Confirm')
|
|
confirmButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
confirmButton.clicked.connect(self.accept)
|
|
resolutionsEditDialog.addWidget(confirmButton, 2, 1, 1, 1)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
cancelButton.clicked.connect(self.reject)
|
|
resolutionsEditDialog.addWidget(cancelButton, 2, 0, 1, 1)
|
|
|
|
self.settingsTextboxes = []
|
|
self.newSettings = {}
|
|
|
|
for setting in self.settings:
|
|
settingTextbox = SettingsEditTextBox(self.settings.value(setting), setting)
|
|
self.settingsTextboxes.append(settingTextbox)
|
|
|
|
if setting.startswith('Resolutions/'):
|
|
keyName = setting.split('Resolutions/', 1)[1]
|
|
self.resolutionCategoryLayout.addRow(keyName, settingTextbox)
|
|
|
|
def accept(self) -> None:
|
|
for settingTextbox in self.settingsTextboxes:
|
|
key = settingTextbox.settingsKey
|
|
value = settingTextbox.text()
|
|
isDeleted = settingTextbox.keyDeleted
|
|
self.newSettings[key] = (value, isDeleted)
|
|
|
|
super(ResolutionsEditDialog, self).accept()
|
|
|
|
|
|
class LoggingSettingsDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, settingsObject):
|
|
super(LoggingSettingsDialog, self).__init__()
|
|
|
|
self.setModal(True)
|
|
self.setMaximumWidth(850)
|
|
self.setMinimumWidth(600)
|
|
self.setMaximumHeight(600)
|
|
self.setMinimumHeight(400)
|
|
self.settings = settingsObject
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
loggingSettingsLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(loggingSettingsLayout)
|
|
scrollArea = QtWidgets.QScrollArea()
|
|
scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
|
scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
|
scrollArea.setWidgetResizable(True)
|
|
scrollContainer = QtWidgets.QWidget()
|
|
scrollLayout = QtWidgets.QVBoxLayout()
|
|
scrollContainer.setLayout(scrollLayout)
|
|
scrollArea.setWidget(scrollContainer)
|
|
loggingSettingsLayout.addWidget(scrollArea, 0, 0, 2, 2)
|
|
|
|
# Settings Categories: Program, Logging, Project, Resolution, Other.
|
|
|
|
loggingCategoryWidget = QtWidgets.QWidget()
|
|
self.loggingCategoryLayout = SettingsCategoryLayout(supportsDeletion=False)
|
|
loggingCategoryWidget.setLayout(self.loggingCategoryLayout)
|
|
loggingCategoryLabel = QtWidgets.QLabel('Logging Settings')
|
|
|
|
loggingCategoryLabel.setFont(QtGui.QFont("Times", 13, QtGui.QFont.Bold))
|
|
loggingCategoryLabel.setFrameStyle(QtWidgets.QFrame.Raised | QtWidgets.QFrame.Panel)
|
|
|
|
loggingCategoryLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
scrollLayout.addWidget(loggingCategoryLabel)
|
|
scrollLayout.addWidget(loggingCategoryWidget)
|
|
|
|
confirmButton = QtWidgets.QPushButton('Confirm')
|
|
confirmButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
confirmButton.clicked.connect(self.accept)
|
|
loggingSettingsLayout.addWidget(confirmButton, 2, 1, 1, 1)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
cancelButton.clicked.connect(self.reject)
|
|
loggingSettingsLayout.addWidget(cancelButton, 2, 0, 1, 1)
|
|
|
|
self.settingsTextboxes = []
|
|
self.newSettings = {}
|
|
|
|
for setting in self.settings:
|
|
settingTextbox = SettingsEditTextBox(self.settings.value(setting), setting)
|
|
settingTextbox.setStyleSheet(Stylesheets.TEXT_BOX_STYLESHEET)
|
|
self.settingsTextboxes.append(settingTextbox)
|
|
if setting.startswith('Logging/'):
|
|
keyName = setting.split('Logging/', 1)[1]
|
|
self.loggingCategoryLayout.addRow(keyName, settingTextbox)
|
|
|
|
def accept(self) -> None:
|
|
for settingTextbox in self.settingsTextboxes:
|
|
key = settingTextbox.settingsKey
|
|
value = settingTextbox.text()
|
|
isDeleted = settingTextbox.keyDeleted
|
|
self.newSettings[key] = (value, isDeleted)
|
|
|
|
super(LoggingSettingsDialog, self).accept()
|
|
|
|
|
|
class ProjectEditDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, settingsObject):
|
|
super(ProjectEditDialog, self).__init__()
|
|
|
|
self.setModal(True)
|
|
self.setMaximumWidth(850)
|
|
self.setMinimumWidth(600)
|
|
self.setMaximumHeight(600)
|
|
self.setMinimumHeight(400)
|
|
self.settings = settingsObject
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
editDialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(editDialogLayout)
|
|
scrollArea = QtWidgets.QScrollArea()
|
|
scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
|
scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
|
scrollArea.setWidgetResizable(True)
|
|
scrollContainer = QtWidgets.QWidget()
|
|
scrollLayout = QtWidgets.QVBoxLayout()
|
|
scrollContainer.setLayout(scrollLayout)
|
|
scrollArea.setWidget(scrollContainer)
|
|
editDialogLayout.addWidget(scrollArea, 0, 0, 2, 2)
|
|
|
|
resolutionCategoryWidget = QtWidgets.QWidget()
|
|
self.resolutionCategoryLayout = SettingsCategoryLayout(supportsDeletion=False)
|
|
resolutionCategoryWidget.setLayout(self.resolutionCategoryLayout)
|
|
resolutionCategoryLabel = QtWidgets.QLabel('Project Settings')
|
|
|
|
resolutionCategoryLabel.setFont(QtGui.QFont("Times", 13, QtGui.QFont.Bold))
|
|
resolutionCategoryLabel.setFrameStyle(QtWidgets.QFrame.Raised | QtWidgets.QFrame.Panel)
|
|
|
|
resolutionCategoryLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
scrollLayout.addWidget(resolutionCategoryLabel)
|
|
scrollLayout.addWidget(resolutionCategoryWidget)
|
|
|
|
confirmButton = QtWidgets.QPushButton('Confirm')
|
|
confirmButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
confirmButton.clicked.connect(self.accept)
|
|
editDialogLayout.addWidget(confirmButton, 2, 1, 1, 1)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
cancelButton.clicked.connect(self.reject)
|
|
editDialogLayout.addWidget(cancelButton, 2, 0, 1, 1)
|
|
|
|
self.settingsTextboxes = []
|
|
self.settingsSingleChoice = []
|
|
self.newSettings = {}
|
|
|
|
for setting in self.settings:
|
|
if setting == 'Project/Resolution Result Grouping Threshold' or \
|
|
setting == 'Project/Number of Answers Returned' or \
|
|
setting == 'Project/Question Answering Retriever Value' or \
|
|
setting == 'Project/Question Answering Reader Value':
|
|
keyName = setting.split('Project/', 1)[1]
|
|
settingTextbox = SettingsEditTextBox(self.settings.value(setting), setting)
|
|
settingTextbox.setStyleSheet(Stylesheets.TEXT_BOX_STYLESHEET)
|
|
self.settingsTextboxes.append(settingTextbox)
|
|
self.resolutionCategoryLayout.addRow(keyName, settingTextbox)
|
|
|
|
elif setting == 'Project/Symlink or Copy Materials':
|
|
keyName = setting.split('Project/', 1)[1]
|
|
settingSingleChoice = SettingsEditSingleChoice(['Symlink', 'Copy'], self.settings.value(setting),
|
|
setting)
|
|
settingSingleChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
|
self.settingsSingleChoice.append(settingSingleChoice)
|
|
self.resolutionCategoryLayout.addRow(keyName, settingSingleChoice)
|
|
|
|
def accept(self) -> None:
|
|
for settingTextbox in self.settingsTextboxes:
|
|
key = settingTextbox.settingsKey
|
|
value = settingTextbox.text()
|
|
isDeleted = settingTextbox.keyDeleted
|
|
self.newSettings[key] = (value, isDeleted)
|
|
for settingSingleChoice in self.settingsSingleChoice:
|
|
key = settingSingleChoice.settingsKey
|
|
value = [option.text() for option in settingSingleChoice.options if option.isChecked()][0]
|
|
isDeleted = settingSingleChoice.keyDeleted
|
|
self.newSettings[key] = (value, isDeleted)
|
|
|
|
super(ProjectEditDialog, self).accept()
|
|
|
|
|
|
class SettingsEditTextBox(QtWidgets.QLineEdit):
|
|
|
|
def __init__(self, contents, settingsKey):
|
|
super(SettingsEditTextBox, self).__init__(str(contents))
|
|
self.settingsKey = settingsKey
|
|
self.keyDeleted = False
|
|
self.setToolTip("Edit the contents to change the setting's value.")
|
|
|
|
|
|
class SettingsEditSingleChoice(QtWidgets.QWidget):
|
|
|
|
def __init__(self, contents, currentSettingValue, settingsKey):
|
|
super(SettingsEditSingleChoice, self).__init__()
|
|
self.settingsKey = settingsKey
|
|
self.keyDeleted = False
|
|
self.setLayout(QtWidgets.QVBoxLayout())
|
|
self.layout().setAlignment(QtCore.Qt.AlignCenter)
|
|
|
|
self.options = []
|
|
|
|
for userChoice in contents:
|
|
radioButton = QtWidgets.QRadioButton(userChoice)
|
|
self.options.append(radioButton)
|
|
if userChoice == currentSettingValue:
|
|
radioButton.setChecked(True)
|
|
self.layout().addWidget(radioButton)
|
|
|
|
|
|
class SettingsCategoryLayout(QtWidgets.QVBoxLayout):
|
|
|
|
def __init__(self, supportsDeletion: bool = True):
|
|
super(SettingsCategoryLayout, self).__init__()
|
|
self.supportsDeletion = supportsDeletion
|
|
|
|
def addRow(self, labelText: str, valueWidget):
|
|
rowWidget = QtWidgets.QWidget()
|
|
rowWidgetLayout = QtWidgets.QVBoxLayout()
|
|
rowWidget.setLayout(rowWidgetLayout)
|
|
keyLabel = QtWidgets.QLabel(labelText)
|
|
|
|
keyLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
rowWidgetLayout.addWidget(keyLabel)
|
|
rowWidgetLayout.addWidget(valueWidget)
|
|
if self.supportsDeletion:
|
|
deleteKeyButton = SettingsDeleteKeyButton('Delete', valueWidget, rowWidget)
|
|
rowWidgetLayout.addWidget(deleteKeyButton)
|
|
self.addWidget(rowWidget)
|
|
|
|
|
|
class SettingsDeleteKeyButton(QtWidgets.QPushButton):
|
|
|
|
def __init__(self, text: str, settingsItem, rowItem: QtWidgets.QWidget):
|
|
super(SettingsDeleteKeyButton, self).__init__(text)
|
|
self.settingsItem = settingsItem
|
|
self.rowItem = rowItem
|
|
|
|
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
self.settingsItem.keyDeleted = True
|
|
self.rowItem.hide()
|
|
|
|
|
|
class FindEntityOnCanvasDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, primaryFieldsList: list):
|
|
super(FindEntityOnCanvasDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.setMinimumWidth(400)
|
|
self.setWindowTitle('Find Entity')
|
|
self.setStyleSheet(Stylesheets.MENUS_STYLESHEET)
|
|
|
|
findLabel = QtWidgets.QLabel('Find:')
|
|
|
|
findLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
self.findInput = QtWidgets.QLineEdit('')
|
|
self.findInput.setPlaceholderText('Type the primary field value to search for')
|
|
|
|
confirmButton = QtWidgets.QPushButton('Confirm')
|
|
confirmButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
confirmButton.clicked.connect(self.accept)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
cancelButton.clicked.connect(self.reject)
|
|
|
|
autoCompleter = QtWidgets.QCompleter(primaryFieldsList)
|
|
autoCompleter.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
self.findInput.setCompleter(autoCompleter)
|
|
|
|
findLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(findLayout)
|
|
|
|
findLayout.addWidget(findLabel, 0, 0, 1, 1)
|
|
findLayout.addWidget(self.findInput, 0, 1, 1, 1)
|
|
findLayout.addWidget(cancelButton, 1, 0, 1, 1)
|
|
findLayout.addWidget(confirmButton, 1, 1, 1, 1)
|
|
|
|
|
|
class MergeEntitiesDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent: MainWindow, entitiesToMerge: list):
|
|
super(MergeEntitiesDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.setWindowTitle('Merge Entities')
|
|
self.parent = parent
|
|
self.entitiesToMerge = entitiesToMerge
|
|
self.primaryEntityUID = None
|
|
self.otherEntitiesUIDs = []
|
|
self.setStyleSheet(Stylesheets.MERGE_STYLESHEET)
|
|
|
|
descriptionLabel = QtWidgets.QLabel("Select the primary entity onto which the fields of the "
|
|
"other entities will be merged. The order in which the "
|
|
"entities are in the table is the order in which they will "
|
|
"be merged. The first entity is written first. Fields with "
|
|
"non 'None' values are not overwritten.")
|
|
descriptionLabel.setWordWrap(True)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
self.entitiesTable = MergeTableWidget(0, 5, self)
|
|
self.entitiesTable.setHorizontalHeaderLabels(['Entity', 'Entity Type', 'Incoming\nLinks',
|
|
'Outgoing\nLinks', 'Shift\nPriority'])
|
|
|
|
self.radioButtonGroup = QtWidgets.QButtonGroup()
|
|
|
|
for entity in entitiesToMerge:
|
|
self.insertRow(entity)
|
|
|
|
self.entitiesTable.cellWidget(0, 0).setChecked(True)
|
|
self.entitiesTable.setColumnWidth(0, 150)
|
|
self.entitiesTable.setColumnWidth(1, 150)
|
|
self.entitiesTable.setMinimumWidth(self.entitiesTable.width())
|
|
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 2, 2)
|
|
dialogLayout.addWidget(self.entitiesTable, 2, 0, 2, 2)
|
|
dialogLayout.setRowStretch(3, 1)
|
|
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
dialogLayout.addWidget(cancelButton, 4, 0, 1, 1)
|
|
dialogLayout.addWidget(acceptButton, 4, 1, 1, 1)
|
|
|
|
self.adjustSize()
|
|
self.setFixedWidth(self.entitiesTable.width() + 19)
|
|
self.setMaximumHeight(700)
|
|
|
|
def insertRow(self, entityJSON):
|
|
newRowIndex = self.entitiesTable.rowCount()
|
|
self.entitiesTable.insertRow(newRowIndex)
|
|
self.entitiesTable.setRowHeight(newRowIndex, 70)
|
|
isPrimaryRadioButton = QtWidgets.QRadioButton()
|
|
isPrimaryRadioButton.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
|
isPrimaryRadioButton.setText(entityJSON[list(entityJSON)[1]])
|
|
|
|
self.entitiesTable.setCellWidget(newRowIndex, 0, isPrimaryRadioButton)
|
|
|
|
pixmapLabel = QtWidgets.QLabel()
|
|
entityPixmap = QtGui.QPixmap()
|
|
entityPixmap.loadFromData(entityJSON.get('Icon'))
|
|
pixmapLabel.setPixmap(entityPixmap)
|
|
|
|
pixmapLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
entityTypeWidget = QtWidgets.QWidget()
|
|
entityTypeWidgetLayout = QtWidgets.QHBoxLayout()
|
|
entityTypeWidget.setLayout(entityTypeWidgetLayout)
|
|
entityTypeWidgetLayout.addWidget(pixmapLabel)
|
|
entityTypeWidgetLayout.addWidget(QtWidgets.QLabel(entityJSON['Entity Type']))
|
|
|
|
self.entitiesTable.setCellWidget(newRowIndex, 1, entityTypeWidget)
|
|
|
|
incomingLinks = QtWidgets.QLabel(str(len(self.parent.LENTDB.getIncomingLinks(entityJSON['uid']))))
|
|
|
|
incomingLinks.setAlignment(QtCore.Qt.AlignCenter)
|
|
self.entitiesTable.setCellWidget(newRowIndex, 2, incomingLinks)
|
|
outgoingLinks = QtWidgets.QLabel(str(len(self.parent.LENTDB.getOutgoingLinks(entityJSON['uid']))))
|
|
|
|
outgoingLinks.setAlignment(QtCore.Qt.AlignCenter)
|
|
self.entitiesTable.setCellWidget(newRowIndex, 3, outgoingLinks)
|
|
|
|
upDownButtons = MergeTableShiftRowUpDownButtons(self.entitiesTable, entityJSON['uid'])
|
|
|
|
self.entitiesTable.setCellWidget(newRowIndex, 4, upDownButtons)
|
|
|
|
def accept(self) -> None:
|
|
for rowIndex in range(0, self.entitiesTable.rowCount()):
|
|
if self.entitiesTable.cellWidget(rowIndex, 0).isChecked():
|
|
self.primaryEntityUID = self.entitiesTable.cellWidget(rowIndex, 4).uid
|
|
else:
|
|
self.otherEntitiesUIDs.append(self.entitiesTable.cellWidget(rowIndex, 4).uid)
|
|
super(MergeEntitiesDialog, self).accept()
|
|
|
|
|
|
class MergeTableWidget(QtWidgets.QTableWidget):
|
|
"""
|
|
Table that presents the user the selected entities for them to merge.
|
|
|
|
The user will select a primary entity, and reorder the table such that the most important
|
|
entity values to preserve will be in entities at the top of the table.
|
|
|
|
The table has 5 columns, and indices for columns go from 0 to 4.
|
|
"""
|
|
|
|
def findRowOfShiftingWidget(self, widget: QtWidgets.QWidget):
|
|
# Always in column with index 4.
|
|
for row in range(self.rowCount()):
|
|
if self.cellWidget(row, 4) == widget:
|
|
return row
|
|
|
|
def shiftRowUp(self, widget: QtWidgets.QWidget):
|
|
rowIndex = self.findRowOfShiftingWidget(widget)
|
|
if rowIndex == 0:
|
|
return rowIndex
|
|
widgetsToShift = []
|
|
for widgetColumnIndex in range(5):
|
|
widgetsToShift.append(self.cellWidget(rowIndex, widgetColumnIndex))
|
|
self.insertRow(rowIndex - 1)
|
|
self.setRowHeight(rowIndex - 1, 70)
|
|
for widgetColumnIndex in range(5):
|
|
self.setCellWidget(rowIndex - 1, widgetColumnIndex, widgetsToShift[widgetColumnIndex])
|
|
self.removeRow(rowIndex + 1)
|
|
|
|
def shiftRowDown(self, widget: QtWidgets.QWidget) -> None:
|
|
rowIndex = self.findRowOfShiftingWidget(widget)
|
|
if rowIndex == self.rowCount() - 1:
|
|
return
|
|
widgetsToShift = []
|
|
for widgetColumnIndex in range(5):
|
|
widgetsToShift.append(self.cellWidget(rowIndex, widgetColumnIndex))
|
|
self.insertRow(rowIndex + 2)
|
|
self.setRowHeight(rowIndex + 2, 70)
|
|
for widgetColumnIndex in range(5):
|
|
self.setCellWidget(rowIndex + 2, widgetColumnIndex, widgetsToShift[widgetColumnIndex])
|
|
self.removeRow(rowIndex)
|
|
|
|
|
|
class MergeTableShiftRowUpDownButtons(QtWidgets.QWidget):
|
|
|
|
def __init__(self, parent, uid) -> None:
|
|
super(MergeTableShiftRowUpDownButtons, self).__init__(parent=parent)
|
|
self.setLayout(QtWidgets.QVBoxLayout())
|
|
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
self.uid = uid
|
|
|
|
upButton = QtWidgets.QPushButton('^')
|
|
upButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
|
|
|
downButton = QtWidgets.QPushButton('v')
|
|
downButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
|
|
|
upButton.clicked.connect(lambda: self.shiftRow(True))
|
|
downButton.clicked.connect(lambda: self.shiftRow(False))
|
|
|
|
self.layout().addWidget(upButton)
|
|
self.layout().addWidget(downButton)
|
|
|
|
def shiftRow(self, shiftUp: bool) -> None:
|
|
if shiftUp:
|
|
self.parent().parent().shiftRowUp(self)
|
|
else:
|
|
self.parent().parent().shiftRowDown(self)
|
|
|
|
|
|
class SplitEntitiesDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent: MainWindow, entityToSplit: dict) -> None:
|
|
super(SplitEntitiesDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.parent = parent
|
|
self.entityToSplitPrimaryField = entityToSplit[list(entityToSplit)[1]]
|
|
self.setWindowTitle('Split Entities')
|
|
# One is used to store primary fields, the other primary fields + the links that the user selected
|
|
# for that primary field. This takes up more memory, but less processing time.
|
|
self.splitEntities = []
|
|
self.splitEntitiesWithLinks = []
|
|
self.setStyleSheet(Stylesheets.MERGE_STYLESHEET)
|
|
|
|
incomingLinks = list(parent.LENTDB.getIncomingLinks(entityToSplit['uid']))
|
|
outgoingLinks = list(parent.LENTDB.getOutgoingLinks(entityToSplit['uid']))
|
|
self.allLinks = [parent.LENTDB.getLink(linkUID) for linkUID in incomingLinks + outgoingLinks]
|
|
|
|
descriptionLabel = QtWidgets.QLabel("Use the '+' and '-' buttons to create and remove entities. For each "
|
|
"resolution column in each entity row, set the checkbox to selected "
|
|
"if you want the corresponding entity to have the resolution on the "
|
|
"selected column.")
|
|
descriptionLabel.setWordWrap(True)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
self.entitiesTable = QtWidgets.QTableWidget(0, len(self.allLinks) + 1, self)
|
|
# Resize columns to give a bit more space to each column.
|
|
for columnIndex in range(self.entitiesTable.columnCount()):
|
|
self.entitiesTable.setColumnWidth(columnIndex, 150)
|
|
|
|
horizontalHeaderLabels = []
|
|
for incomingLink in incomingLinks:
|
|
linkNode = parent.LENTDB.getEntity(incomingLink[0])
|
|
linkNodeName = linkNode[list(linkNode)[1]]
|
|
horizontalHeaderLabels.append('Incoming From:\n' + linkNodeName)
|
|
|
|
for outgoingLink in outgoingLinks:
|
|
linkNode = parent.LENTDB.getEntity(outgoingLink[1])
|
|
linkNodeName = linkNode[list(linkNode)[1]]
|
|
horizontalHeaderLabels.append('Outgoing To:\n' + linkNodeName)
|
|
|
|
self.entitiesTable.setHorizontalHeaderLabels(['Entity Name:'] + horizontalHeaderLabels)
|
|
|
|
self.insertRow(self.entityToSplitPrimaryField)
|
|
|
|
self.entitiesTable.setMinimumWidth(self.entitiesTable.width())
|
|
|
|
self.addSplitEntity = QtWidgets.QPushButton('+')
|
|
self.addSplitEntity.clicked.connect(self.insertRow)
|
|
self.removeSplitEntity = QtWidgets.QPushButton('-')
|
|
self.removeSplitEntity.clicked.connect(self.removeRow)
|
|
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 2, 4)
|
|
dialogLayout.addWidget(self.entitiesTable, 2, 1, 4, 3)
|
|
dialogLayout.addWidget(self.addSplitEntity, 2, 0, 1, 1)
|
|
dialogLayout.addWidget(self.removeSplitEntity, 3, 0, 1, 1)
|
|
dialogLayout.setRowStretch(4, 1)
|
|
dialogLayout.setColumnStretch(1, 1)
|
|
dialogLayout.setColumnStretch(2, 1)
|
|
dialogLayout.setColumnStretch(3, 1)
|
|
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
dialogLayout.addWidget(cancelButton, 6, 0, 1, 2)
|
|
dialogLayout.addWidget(acceptButton, 6, 2, 1, 2)
|
|
|
|
self.adjustSize()
|
|
self.setMaximumHeight(700)
|
|
self.setMinimumWidth(min(self.entitiesTable.width(), 700))
|
|
|
|
def insertRow(self, entityPrimaryField: str = '') -> None:
|
|
# Do not allow splitting into more than 5 entities at a time for performance reasons.
|
|
if self.entitiesTable.rowCount() == 5:
|
|
self.parent.MESSAGEHANDLER.info('Maximum number of entities to split into reached.')
|
|
return
|
|
newRowIndex = self.entitiesTable.rowCount()
|
|
self.entitiesTable.insertRow(newRowIndex)
|
|
|
|
self.entitiesTable.setItem(newRowIndex, 0, QtWidgets.QTableWidgetItem(entityPrimaryField))
|
|
self.entitiesTable.setFocus()
|
|
|
|
count = 1
|
|
for link in self.allLinks:
|
|
selectResolution = QtWidgets.QCheckBox(link['Resolution'])
|
|
selectResolution.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
selectResolution.linkUID = link['uid']
|
|
self.entitiesTable.setCellWidget(newRowIndex, count, selectResolution)
|
|
count += 1
|
|
|
|
def removeRow(self) -> None:
|
|
self.entitiesTable.removeRow(self.entitiesTable.rowCount() - 1)
|
|
|
|
def accept(self) -> None:
|
|
for newEntityRow in range(self.entitiesTable.rowCount()):
|
|
entityName = self.entitiesTable.item(newEntityRow, 0).text()
|
|
if entityName == '':
|
|
self.parent.MESSAGEHANDLER.info('Cannot split into entities with blank primary fields.',
|
|
popUp=True)
|
|
self.splitEntities = []
|
|
self.splitEntitiesWithLinks = []
|
|
return
|
|
elif self.parent.LENTDB.doesEntityExist(entityName) and entityName != self.entityToSplitPrimaryField:
|
|
self.parent.MESSAGEHANDLER.info("Entity primary field value specified already exists:\n" + entityName,
|
|
popUp=True)
|
|
self.splitEntities = []
|
|
self.splitEntitiesWithLinks = []
|
|
return
|
|
elif entityName in self.splitEntities:
|
|
self.parent.MESSAGEHANDLER.info("Duplicate primary field value specified:\n" + entityName,
|
|
popUp=True)
|
|
self.splitEntities = []
|
|
self.splitEntitiesWithLinks = []
|
|
return
|
|
self.splitEntities.append(entityName)
|
|
|
|
allLinkUIDsForEntity = []
|
|
for columnIndex in range(1, self.entitiesTable.columnCount()):
|
|
if self.entitiesTable.cellWidget(newEntityRow, columnIndex).isChecked():
|
|
allLinkUIDsForEntity.append(self.allLinks[columnIndex - 1])
|
|
self.splitEntitiesWithLinks.append((entityName, allLinkUIDsForEntity))
|
|
|
|
# Clear out this list, no more need for it
|
|
self.splitEntities = []
|
|
super(SplitEntitiesDialog, self).accept()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Create a graphical application
|
|
application = QtWidgets.QApplication(sys.argv)
|
|
mainWindow = MainWindow()
|
|
sys.exit(application.exec())
|