4552 lines
214 KiB
Python
4552 lines
214 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Load modules
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import csv
|
|
import statistics
|
|
import itertools
|
|
|
|
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, QtCharts
|
|
|
|
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.ResourceHandler import StringPropertyInput, FilePropertyInput, SingleChoicePropertyInput, \
|
|
MultiChoicePropertyInput
|
|
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.LQL import LQLQueryBuilder
|
|
from Core.PathHelper import is_path_exists_or_creatable_portable
|
|
|
|
|
|
# Main Window of Application
|
|
class MainWindow(QtWidgets.QMainWindow):
|
|
facilitateResolutionSignalListener = QtCore.Signal(str, list)
|
|
notifyUserSignalListener = QtCore.Signal(str, str, bool)
|
|
|
|
# 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
|
|
|
|
# Duplicate and alter a group entity to make it
|
|
def copyGroupEntity(self, existingGroupUID: str, targetCanvas: CentralPane.CanvasScene) -> Union[dict, None]:
|
|
# Have to make a new group entity, so that ungrouping in one canvas doesn't delete the entity
|
|
# group in another.
|
|
entityJSON = self.LENTDB.getEntity(existingGroupUID)
|
|
# Dereference the list, so we don't have issues w/ the original Group Node.
|
|
newChildren = [childUID for childUID in list(entityJSON['Child UIDs'])
|
|
if childUID not in targetCanvas.sceneGraph.nodes]
|
|
# Don't create the entity if all the nodes in it already exist on the target canvas.
|
|
if len(newChildren) == 0:
|
|
return None
|
|
newEntity = self.LENTDB.addEntity(
|
|
{'Group Name': entityJSON['Group Name'] + ' Copy',
|
|
'Child UIDs': newChildren,
|
|
'Entity Type': 'EntityGroup'})
|
|
newUID = newEntity['uid']
|
|
|
|
# Need to re-create the links too
|
|
for linkUID in self.LENTDB.getOutgoingLinks(entityJSON['uid']):
|
|
newLinkJSON = dict(self.LENTDB.getLink(linkUID))
|
|
newLinkJSON['uid'] = (newUID, linkUID[1])
|
|
self.LENTDB.addLink(newLinkJSON)
|
|
for linkUID in self.LENTDB.getIncomingLinks(entityJSON['uid']):
|
|
newLinkJSON = dict(self.LENTDB.getLink(linkUID))
|
|
newLinkJSON['uid'] = (linkUID[0], newUID)
|
|
self.LENTDB.addLink(newLinkJSON)
|
|
return newEntity
|
|
|
|
# What happens when the software is closed
|
|
def closeEvent(self, event) -> None:
|
|
self.dockbarThree.logViewerUpdateThread.endLogging = True
|
|
self.saveTimer.stop()
|
|
# 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:
|
|
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'))
|
|
|
|
def autoSaveProject(self):
|
|
try:
|
|
self.SETTINGS.save()
|
|
self.LENTDB.save()
|
|
self.centralWidget().tabbedPane.save()
|
|
self.setStatus("Project Autosaved.", 3000)
|
|
except Exception:
|
|
self.setStatus("Failed Autosaving Project " + self.SETTINGS.value("Project/Name", 'Untitled'))
|
|
|
|
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)
|
|
saveAsDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
saveAsDialog.setDirectory(str(Path.home()))
|
|
|
|
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)
|
|
saveAsDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
saveAsDialog.setDirectory(str(Path.home()))
|
|
|
|
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: " + str(exc), 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)")
|
|
openDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
openDialog.setDirectory(str(Path.home()))
|
|
|
|
if openDialog.exec():
|
|
filePath = openDialog.selectedFiles()[0]
|
|
try:
|
|
read_graphml = nx.read_graphml(filePath)
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
# Deduplication, just in case.
|
|
graphMLNodes = set(read_graphml.nodes())
|
|
nodesToReadFirst = [self.LENTDB.getEntity(node) for node in graphMLNodes
|
|
if not read_graphml.nodes[node].get('groupID')]
|
|
nodesToReadFirst = [entity for entity in nodesToReadFirst if entity is not None and
|
|
entity['Entity Type'] == 'EntityGroup']
|
|
for entity in nodesToReadFirst:
|
|
# Create new group entity so we don't mess with the contents of the original.
|
|
if entity['uid'] not in currentScene.sceneGraph.nodes:
|
|
newGroupEntity = self.copyGroupEntity(entity['uid'], currentScene)
|
|
if newGroupEntity is not None:
|
|
currentScene.addNodeProgrammatic(newGroupEntity['uid'], newGroupEntity['Child UIDs'])
|
|
graphMLNodes.remove(entity['uid'])
|
|
for node in graphMLNodes:
|
|
# Need to check as existing group nodes could have been updated with new nodes after export
|
|
# but before the import.
|
|
if node not in currentScene.sceneGraph.nodes:
|
|
currentScene.addNodeProgrammatic(node)
|
|
|
|
currentScene.rearrangeGraph()
|
|
self.setStatus('Canvas imported successfully.')
|
|
except KeyError:
|
|
self.MESSAGEHANDLER.error("Aborted canvas import: One or more nodes in the graph "
|
|
"do not exist in the database.", popUp=True)
|
|
self.setStatus('Canvas import aborted.')
|
|
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:
|
|
# Remove icons. Will reset custom icons to default, but saves space.
|
|
del currentDatabase.nodes[node]['Icon']
|
|
if currentDatabase.nodes[node].get('Child UIDs'):
|
|
currentDatabase.nodes[node]['Child UIDs'] = str(currentDatabase.nodes[node]['Child UIDs'])
|
|
|
|
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)
|
|
saveAsDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
saveAsDialog.setDirectory(str(Path.home()))
|
|
|
|
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: " + str(exc), 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)")
|
|
openDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
openDialog.setDirectory(str(Path.home()))
|
|
|
|
if openDialog.exec():
|
|
try:
|
|
filePath = openDialog.selectedFiles()[0]
|
|
read_graphml = nx.read_graphml(filePath)
|
|
read_graphml_nodes = {key: read_graphml.nodes[key] for key in read_graphml.nodes}
|
|
read_graphml_edges = {key: read_graphml.edges[key] for key in read_graphml.edges}
|
|
for node in read_graphml_nodes:
|
|
read_graphml_nodes[node]['Icon'] = self.RESOURCEHANDLER.getEntityDefaultPicture(
|
|
read_graphml_nodes[node]['Entity Type'])
|
|
if read_graphml_nodes[node].get('Child UIDs'):
|
|
read_graphml_nodes[node]['Child UIDs'] = literal_eval(read_graphml_nodes[node]['Child UIDs'])
|
|
for edge in read_graphml_edges:
|
|
read_graphml_edges[edge]['uid'] = literal_eval(read_graphml_edges[edge]['uid'])
|
|
self.LENTDB.mergeDatabases(read_graphml_nodes, read_graphml_edges, 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: " + str(exc), 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:
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
if currentScene.appendingToGroup:
|
|
message = 'Cannot create manual link at this moment: Currently adding entities to group.'
|
|
self.MESSAGEHANDLER.info(message, popUp=True)
|
|
self.setStatus(message)
|
|
return
|
|
if self.linkingNodes:
|
|
self.centralWidget().tabbedPane.enableAllTabs()
|
|
currentScene.linking = False
|
|
self.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
|
self.linkingNodes = False
|
|
else:
|
|
self.centralWidget().tabbedPane.disableAllTabsExceptCurrent()
|
|
currentScene.linking = True
|
|
currentScene.itemsToLink = []
|
|
currentScene.clearSelection()
|
|
self.setCursor(QtGui.QCursor(QtCore.Qt.CrossCursor))
|
|
self.linkingNodes = True
|
|
|
|
def deleteSpecificEntity(self, itemUID: str) -> None:
|
|
self.centralWidget().tabbedPane.nodeRemoveAllHelper(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:
|
|
message = 'Cannot append to group: Currently creating new link.'
|
|
self.setStatus(message)
|
|
self.MESSAGEHANDLER.info(message, popUp=True)
|
|
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:
|
|
"""
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
currentScene.clearSelection()
|
|
for isolatedNodeUID in nx.isolates(currentScene.sceneGraph):
|
|
currentScene.nodesDict[isolatedNodeUID].setSelected(True)
|
|
|
|
def selectNonIsolatedNodes(self) -> None:
|
|
"""
|
|
Select all nodes with at least one link going into or out of them.
|
|
:return:
|
|
"""
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
currentScene.clearSelection()
|
|
isolatedNodes = list(nx.isolates(currentScene.sceneGraph))
|
|
for node in currentScene.nodesDict:
|
|
if node not in isolatedNodes:
|
|
currentScene.nodesDict[node].setSelected(True)
|
|
|
|
def findShortestPath(self) -> None:
|
|
"""
|
|
Find the shortest path between two nodes, if it exists.
|
|
Exactly two nodes must be selected.
|
|
:return:
|
|
"""
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
endPoints = [item.uid for item in currentScene.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 = currentScene.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:
|
|
messagePathNotFound = 'No path found connecting the selected nodes: ' + str(endPoints)
|
|
self.setStatus(messagePathNotFound)
|
|
self.MESSAGEHANDLER.info(messagePathNotFound, popUp=True)
|
|
else:
|
|
currentScene.clearSelection()
|
|
|
|
for itemUID in currentScene.nodesDict:
|
|
if itemUID in shortestPath:
|
|
currentScene.nodesDict[itemUID].setSelected(True)
|
|
|
|
linksToSelect = [(a, b) for a, b in zip(shortestPath, shortestPath[1:])]
|
|
for linkItem in [link for link in currentScene.items() if isinstance(link, BaseConnector)]:
|
|
if linkItem.uid.intersection(linksToSelect):
|
|
linkItem.setSelected(True)
|
|
self.setStatus('Shortest path found.')
|
|
|
|
def extractCycles(self) -> None:
|
|
"""
|
|
Find cycles in the graph, optionally involving selected nodes.
|
|
:return:
|
|
"""
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
canvasName = currentScene.getSelfName()
|
|
endPoints = [item.uid for item in currentScene.selectedItems()
|
|
if isinstance(item, BaseNode)]
|
|
currentCanvasGraph = currentScene.sceneGraph
|
|
tempGraph = currentCanvasGraph.copy()
|
|
# Just in case.
|
|
tempGraph.remove_edges_from(nx.selfloop_edges(currentCanvasGraph))
|
|
|
|
newCyclesThread = ExtractCyclesThread(tempGraph, endPoints, canvasName)
|
|
newCyclesThread.cyclesSignal.connect(self.extractCyclesResultHandler)
|
|
self.MESSAGEHANDLER.info('Extracting Cycles from Canvas: ' + canvasName)
|
|
newCyclesThread.start()
|
|
self.cycleExtractionThreads.append(newCyclesThread)
|
|
|
|
def extractCyclesResultHandler(self, results: list, canvasName: str) -> None:
|
|
if not results:
|
|
self.MESSAGEHANDLER.info('No Cycles in Canvas: ' + canvasName)
|
|
else:
|
|
count = 0
|
|
while True:
|
|
newCanvasName = canvasName + ' Cycles #' + str(count)
|
|
if self.centralWidget().tabbedPane.addCanvas(newCanvasName):
|
|
break
|
|
else:
|
|
count += 1
|
|
|
|
nodesToAdd = results[0]
|
|
groupsToMake = results[1]
|
|
newCanvas = self.centralWidget().tabbedPane.canvasTabs[newCanvasName].scene()
|
|
|
|
for node in nodesToAdd:
|
|
newCanvas.addNodeProgrammatic(node)
|
|
|
|
count = 1
|
|
entitiesAlreadyInGroups = set()
|
|
for group in groupsToMake:
|
|
newGroup = set()
|
|
for groupEntity in group:
|
|
if groupEntity not in entitiesAlreadyInGroups:
|
|
newGroup.add(groupEntity)
|
|
entitiesAlreadyInGroups.add(groupEntity)
|
|
newCanvas.groupItemsProgrammatic(newGroup, 'Group ' + str(count))
|
|
count += 1
|
|
newCanvas.rearrangeGraph('neato')
|
|
|
|
for cycleThread in list(self.cycleExtractionThreads):
|
|
if cycleThread.isFinished():
|
|
cycleThread.deleteLater()
|
|
self.cycleExtractionThreads.remove(cycleThread)
|
|
|
|
def findEntityOrLinkOnCanvas(self, regex: bool = False) -> 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:
|
|
if isinstance(uid, str):
|
|
item = self.LENTDB.getEntity(uid)
|
|
if item is not None:
|
|
if not entityPrimaryFields.get(item[list(item)[1]]):
|
|
entityPrimaryFields[item[list(item)[1]]] = set()
|
|
entityPrimaryFields[item[list(item)[1]]].add(uid)
|
|
|
|
elif isinstance(uid, set):
|
|
for potentialLinkItem in uid:
|
|
item = self.LENTDB.getLink(potentialLinkItem)
|
|
if item is not None:
|
|
if not entityPrimaryFields.get(item['Resolution']):
|
|
entityPrimaryFields[item['Resolution']] = set()
|
|
entityPrimaryFields[item['Resolution']].add(str(uid))
|
|
findPrompt = FindEntityOnCanvasDialog(list(entityPrimaryFields), regex)
|
|
|
|
if findPrompt.exec():
|
|
uidsToSelect = []
|
|
findText = findPrompt.findInput.text()
|
|
if findText != "":
|
|
try:
|
|
if regex:
|
|
expression = re.compile(findText)
|
|
for item in entityPrimaryFields:
|
|
if expression.match(item):
|
|
# Add the elements in each index to uidsToSelect instead of the sets themselves.
|
|
uidsToSelect.extend(entityPrimaryFields[item])
|
|
|
|
else:
|
|
for item in entityPrimaryFields:
|
|
if item.startswith(findText):
|
|
# Add the elements in each index to uidsToSelect instead of the sets themselves.
|
|
uidsToSelect.extend(entityPrimaryFields[item])
|
|
|
|
currentScene.clearSelection()
|
|
for item in [linkOrEntity for linkOrEntity in currentScene.items()
|
|
if isinstance(linkOrEntity, BaseNode) or isinstance(linkOrEntity, BaseConnector)]:
|
|
if str(item.uid) in uidsToSelect:
|
|
item.setSelected(True)
|
|
if len(uidsToSelect) == 1 and ',' not in uidsToSelect[0]:
|
|
self.centralWidget().tabbedPane.getCurrentView().centerViewportOnNode(uidsToSelect[0])
|
|
except re.error:
|
|
self.MESSAGEHANDLER.error('Invalid Regex Specified!', popUp=True, exc_info=False)
|
|
|
|
def findEntityOfTypeOnCanvas(self, regex: bool = False) -> None:
|
|
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
|
currentUIDs = [item.uid for item in currentScene.items() if isinstance(item, BaseNode)]
|
|
# Keys: Entity Types. Values: dicts, where the key is the primary field and the value is the UID.
|
|
entityTypesOnCanvas = {}
|
|
for uid in currentUIDs:
|
|
item = self.LENTDB.getEntity(uid)
|
|
if item is not None:
|
|
itemType = item.get('Entity Type')
|
|
itemPrimaryFieldValue = item[self.RESOURCEHANDLER.getPrimaryFieldForEntityType(itemType)]
|
|
if itemType not in entityTypesOnCanvas:
|
|
entityTypesOnCanvas[itemType] = {}
|
|
if itemPrimaryFieldValue not in entityTypesOnCanvas[itemType]:
|
|
entityTypesOnCanvas[itemType][itemPrimaryFieldValue] = set()
|
|
entityTypesOnCanvas[itemType][itemPrimaryFieldValue].add(uid)
|
|
|
|
findPrompt = FindEntityOfTypeOnCanvasDialog(entityTypesOnCanvas, regex)
|
|
|
|
if findPrompt.exec():
|
|
uidsToSelect = []
|
|
findText = findPrompt.findInput.text()
|
|
findType = findPrompt.typeInput.currentText()
|
|
if findText != "":
|
|
try:
|
|
if regex:
|
|
expression = re.compile(findText)
|
|
for item in entityTypesOnCanvas[findType]:
|
|
if expression.match(item):
|
|
# Add the elements in each index to uidsToSelect instead of the sets themselves.
|
|
uidsToSelect.extend(entityTypesOnCanvas[findType][item])
|
|
|
|
else:
|
|
for item in entityTypesOnCanvas[findType]:
|
|
if item.startswith(findText):
|
|
# Add the elements in each index to uidsToSelect instead of the sets themselves.
|
|
uidsToSelect.extend(entityTypesOnCanvas[findType][item])
|
|
|
|
currentScene.clearSelection()
|
|
for item in [sceneEntity for sceneEntity in currentScene.items()
|
|
if isinstance(sceneEntity, BaseNode)]:
|
|
if item.uid in uidsToSelect:
|
|
item.setSelected(True)
|
|
if len(uidsToSelect) == 1:
|
|
self.centralWidget().tabbedPane.getCurrentView().centerViewportOnNode(uidsToSelect[0])
|
|
except re.error:
|
|
self.MESSAGEHANDLER.error('Invalid Regex Specified!', popUp=True, exc_info=False)
|
|
|
|
def findResolution(self) -> None:
|
|
# Dereference the entities and resolutions, just to be safe.
|
|
findDialog = FindResolutionDialog(self, list(self.RESOURCEHANDLER.getAllEntities()),
|
|
dict(self.RESOLUTIONMANAGER.resolutions))
|
|
findDialog.exec()
|
|
|
|
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 launchQueryWizard(self):
|
|
queryWizard = QueryBuilderWizard(self)
|
|
queryWizard.exec()
|
|
|
|
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 editProgramSettings(self) -> None:
|
|
settingsDialog = ProgramEditDialog(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 changeGraphics(self) -> None:
|
|
settingsDialog = GraphicsEditDialog(self.SETTINGS, self.RESOURCEHANDLER)
|
|
settingsConfirm = settingsDialog.exec()
|
|
|
|
if settingsConfirm:
|
|
newSettings = settingsDialog.newSettings
|
|
try:
|
|
etfVal = int(newSettings["ETF"])
|
|
self.entityTextFont.setPointSize(etfVal)
|
|
self.SETTINGS.setValue("Program/Graphics/EntityTextFontSize", str(newSettings["ETF"]))
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
ltfVal = int(newSettings["LTF"])
|
|
self.linkTextFont.setPointSize(ltfVal)
|
|
self.SETTINGS.setValue("Program/Graphics/LinkTextFontSize", str(newSettings["LTF"]))
|
|
except ValueError:
|
|
pass
|
|
|
|
etcVal = newSettings["ETC"]
|
|
newEtcColor = QtGui.QColor(etcVal)
|
|
if newEtcColor.isValid():
|
|
self.entityTextBrush.setColor(newEtcColor)
|
|
self.SETTINGS.setValue("Program/Graphics/EntityTextColor", newEtcColor.name())
|
|
ltcVal = newSettings["LTC"]
|
|
newLtcColor = QtGui.QColor(ltcVal)
|
|
if newLtcColor.isValid():
|
|
self.linkTextBrush.setColor(newLtcColor)
|
|
self.SETTINGS.setValue("Program/Graphics/LinkTextColor", newLtcColor.name())
|
|
|
|
for viewKey in self.centralWidget().tabbedPane.canvasTabs:
|
|
scene = self.centralWidget().tabbedPane.canvasTabs[viewKey].scene()
|
|
scene.updateNodeGraphics(self.entityTextFont, self.entityTextBrush, self.linkTextFont,
|
|
self.linkTextBrush)
|
|
|
|
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):
|
|
try:
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromDir(modulesBasePath / module)
|
|
except TypeError:
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromDir(modulesBasePath / module.decode('utf-8'))
|
|
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 notifyUser(self, message: str, title: str = "LinkScope Notification", beep: bool = True,
|
|
icon: QtGui.QIcon = None) -> None:
|
|
if icon is not None:
|
|
self.trayIcon.showMessage(self.tr(title), self.tr(message), icon)
|
|
else:
|
|
self.trayIcon.showMessage(self.tr(title), self.tr(message))
|
|
if beep:
|
|
application.beep()
|
|
|
|
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:
|
|
linkItem = currScene.getVisibleLinkForUID(item)
|
|
if linkItem is not None:
|
|
linkItem.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
|
|
resolutionDescription = 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]
|
|
resolutionDescription = self.RESOLUTIONMANAGER.getResolutionDescription(resolution)
|
|
|
|
parameterSelector = ResolutionParametersSelector(
|
|
resolutionUnspecifiedParameterValues, selectEntityList, acceptableOriginTypes, resolutionDescription)
|
|
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)
|
|
resolutionThread.sigError.connect(self.resolutionErrorSignalListener)
|
|
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 resolutionErrorSignalListener(self, error_message: str):
|
|
self.MESSAGEHANDLER.error(error_message, popUp=True)
|
|
|
|
def cleanUpLocalFinishedResolutions(self) -> None:
|
|
"""
|
|
Clean out old non-server resolutions.
|
|
:return:
|
|
"""
|
|
for resolutionThread in list(self.resolutions):
|
|
if resolutionThread[0].isFinished() and resolutionThread[1] is False:
|
|
resolutionThread[0].deleteLater()
|
|
self.resolutions.remove(resolutionThread)
|
|
|
|
def getClientCollectors(self) -> dict:
|
|
with self.serverCollectorsLock:
|
|
try:
|
|
clientCollectorUIDs = literal_eval(self.SETTINGS.value('Project/Server/Collectors'))
|
|
if not isinstance(clientCollectorUIDs, dict):
|
|
raise ValueError('Collectors were not saved in the correct format.')
|
|
except Exception as e:
|
|
self.MESSAGEHANDLER.error('Unable to load Collectors from Settings file.',
|
|
popUp=False,
|
|
exc_info=False)
|
|
self.MESSAGEHANDLER.debug('Cannot eval Project/Server/Collectors setting: ' + str(e))
|
|
clientCollectorUIDs = {}
|
|
return clientCollectorUIDs
|
|
|
|
def setClientCollectors(self, newClientCollectorsDict: dict) -> None:
|
|
if not isinstance(newClientCollectorsDict, dict):
|
|
self.MESSAGEHANDLER.error('Unable to save Collectors to Settings: Invalid format.')
|
|
self.MESSAGEHANDLER.debug('Collectors argument: ' + str(newClientCollectorsDict))
|
|
self.MESSAGEHANDLER.debug('Collectors format: ' + str(type(newClientCollectorsDict)))
|
|
else:
|
|
with self.serverCollectorsLock:
|
|
self.SETTINGS.setValue('Project/Server/Collectors', str(newClientCollectorsDict))
|
|
self.SETTINGS.save()
|
|
self.MESSAGEHANDLER.info('Client Collectors state updated.')
|
|
|
|
def receiveCollectorResultListener(self, collector_name: str, collector_uid: str, timestamp: str, results: list):
|
|
# Signal ints are 4 bytes long and signed, so we use strings to communicate timestamps.
|
|
currentCollectors = self.getClientCollectors()
|
|
currentCollectors[collector_uid] = int(timestamp)
|
|
self.setClientCollectors(currentCollectors)
|
|
self.centralWidget().tabbedPane.facilitateResolution('Collector ' + str(collector_uid), results)
|
|
self.notifyUser("New entities discovered by collector: " + str(collector_name), "Collector Update")
|
|
|
|
# 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 Collectors..."
|
|
self.MESSAGEHANDLER.info(status)
|
|
self.setStatus(status)
|
|
self.FCOM.askServerForCollectors(self.getClientCollectors())
|
|
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()
|
|
with self.serverProjectsLock:
|
|
self.serverProjects = []
|
|
with self.serverCollectorsLock:
|
|
self.collectors = {}
|
|
self.runningCollectors = {}
|
|
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 list(self.resolutions):
|
|
if resolutionThread[0].uid == resolution_uid and resolutionThread[1] is True:
|
|
if resolutionThread[0].isFinished():
|
|
resolutionThread[0].deleteLater()
|
|
self.resolutions.remove(resolutionThread)
|
|
break
|
|
|
|
def addCollectorsFromServerListener(self, server_collectors: dict, continuing_collectors_info: dict) -> None:
|
|
with self.serverCollectorsLock:
|
|
self.collectors = server_collectors
|
|
self.runningCollectors = continuing_collectors_info
|
|
|
|
def startNewCollectorListener(self, collector_category: str, collector_name: str, collector_uid: str,
|
|
collector_entities: list, collector_parameters: dict):
|
|
with self.serverCollectorsLock:
|
|
if collector_category not in self.runningCollectors:
|
|
self.runningCollectors[collector_category] = {}
|
|
if collector_name not in self.runningCollectors[collector_category]:
|
|
self.runningCollectors[collector_category][collector_name] = []
|
|
|
|
# Re-running a collector would generate duplicate info; we don't want that.
|
|
duplicateExists = False
|
|
for collectorInstance in self.runningCollectors[collector_category][collector_name]:
|
|
if collectorInstance['uid'] == collector_uid:
|
|
duplicateExists = True
|
|
break
|
|
if not duplicateExists:
|
|
self.runningCollectors[collector_category][collector_name].append({'uid': collector_uid,
|
|
'entities': collector_entities,
|
|
'parameters': collector_parameters})
|
|
currentCollectors = self.getClientCollectors()
|
|
currentCollectors[collector_uid] = time.time_ns() // 1000
|
|
self.setClientCollectors(currentCollectors)
|
|
|
|
def stopRunningCollector(self, collectorUID: str):
|
|
"""
|
|
Stops collector. Need to be ran before setClientCollectors.
|
|
"""
|
|
with self.serverCollectorsLock:
|
|
for collectorCategory in self.runningCollectors:
|
|
for collectorName in self.runningCollectors[collectorCategory]:
|
|
for collector in self.runningCollectors[collectorCategory][collectorName]:
|
|
if collector['uid'] == collectorUID:
|
|
self.runningCollectors[collectorCategory][collectorName].remove(collector)
|
|
|
|
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 receiveProjectDeleteListener(self, deleted_project: str) -> None:
|
|
with self.serverProjectsLock:
|
|
try:
|
|
self.serverProjects.remove(deleted_project)
|
|
self.closeServerProjectListener()
|
|
except ValueError:
|
|
# If the client doesn't know about the deleted project, nothing to do.
|
|
pass
|
|
|
|
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:
|
|
if self.FCOM.isConnected():
|
|
current_project = self.SETTINGS.value("Project/Server/Project")
|
|
if current_project != "":
|
|
self.FCOM.closeProject(current_project)
|
|
else:
|
|
self.MESSAGEHANDLER.warning('No Server Project to close.', popUp=True)
|
|
else:
|
|
self.MESSAGEHANDLER.warning('Not connected to a Server.', popUp=True)
|
|
|
|
def closeServerProjectListener(self) -> None:
|
|
project_name = self.SETTINGS.value("Project/Server/Project")
|
|
server = self.SETTINGS.value("Project/Server")
|
|
self.SETTINGS.setValue("Project/Server/Project", "")
|
|
self.FCOM.receiveFileAbortAll(project_name)
|
|
self.FCOM.sendFileAbortAll(project_name)
|
|
self.unSyncCanvasByName()
|
|
if server is not None:
|
|
self.dockbarThree.serverStatus.updateStatus("Connected to server: " + server)
|
|
if project_name != "":
|
|
statusMessage = "Closed Server project: " + str(project_name)
|
|
self.MESSAGEHANDLER.info(statusMessage)
|
|
self.setStatus(statusMessage)
|
|
|
|
self.dockbarOne.documentsList.updateFileListFromServer(None)
|
|
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:
|
|
statusMessage = 'Stopped syncing Canvas: ' + canvasName
|
|
self.setStatus(statusMessage)
|
|
self.MESSAGEHANDLER.info(statusMessage)
|
|
else:
|
|
statusMessage = 'Stopped syncing all Canvases.'
|
|
self.setStatus(statusMessage)
|
|
self.MESSAGEHANDLER.info(statusMessage)
|
|
|
|
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: int) -> 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: dict, add: int) -> None:
|
|
# Check that the JSON received is not empty.
|
|
if entityJson:
|
|
uid = entityJson['uid']
|
|
if add == 1:
|
|
# Add item
|
|
if isinstance(uid, str):
|
|
# Add Entity
|
|
self.LENTDB.addEntity(entityJson, fromServer=True)
|
|
else:
|
|
# Add Link
|
|
self.centralWidget().tabbedPane.serverLinkAddHelper(entityJson)
|
|
elif add == 2:
|
|
# 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)
|
|
elif add == 3:
|
|
# Overwrite existing link
|
|
self.centralWidget().tabbedPane.serverLinkAddHelper(entityJson, overwrite=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')
|
|
projectFilesPath = Path(self.SETTINGS.value("Project/FilesDir"))
|
|
for item in self.centralWidget().tabbedPane.getCurrentScene().selectedItems():
|
|
itemJson = self.LENTDB.getEntity(item.uid)
|
|
itemPath = projectFilesPath / itemJson['File Path']
|
|
if itemJson.get('Entity Type') in materialsEntities and itemPath.exists() and itemPath.is_file():
|
|
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 = Path(self.SETTINGS.value("Project/FilesDir")) / item['File Path']
|
|
file_name = item[self.RESOURCEHANDLER.getPrimaryFieldForEntityType(item['Entity Type'])]
|
|
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
|
|
|
|
self.dockbarOne.documentsList.addUploadingFileToList(file_name)
|
|
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 list(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 downloadFiles(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 Server Project, cannot download files.")
|
|
self.MESSAGEHANDLER.warning("Must create or open a Server Project before downloading files.", popUp=True)
|
|
return
|
|
|
|
self.setStatus('Downloading...')
|
|
|
|
itemsToDownload = []
|
|
if items is not None:
|
|
itemsToDownload = items
|
|
else:
|
|
materialsEntities = self.RESOURCEHANDLER.getAllEntitiesInCategory('Materials')
|
|
projectFilesPath = Path(self.SETTINGS.value("Project/FilesDir"))
|
|
for item in self.centralWidget().tabbedPane.getCurrentScene().selectedItems():
|
|
itemJson = self.LENTDB.getEntity(item.uid)
|
|
itemPath = projectFilesPath / itemJson['File Path']
|
|
# Do not download files that already exist.
|
|
if itemJson.get('Entity Type') in materialsEntities and not itemPath.exists():
|
|
itemsToDownload.append(itemJson)
|
|
|
|
if not itemsToDownload:
|
|
self.setStatus('No Materials Entities selected to download from Server.')
|
|
self.MESSAGEHANDLER.info('To download files from 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 itemsToDownload:
|
|
fileDir = Path(self.SETTINGS.value("Project/FilesDir")) / item['File Path']
|
|
file_name = item[self.RESOURCEHANDLER.getPrimaryFieldForEntityType(item['Entity Type'])]
|
|
self.FCOM.receiveFile(project_name, file_name, fileDir)
|
|
|
|
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 1.3 - " + self.SETTINGS.get('Project/Name', 'Untitled'))
|
|
iconPath = Path(self.SETTINGS.get('Program/BaseDir')) / 'Icon.ico'
|
|
appIcon = QtGui.QIcon(str(iconPath))
|
|
self.setWindowIcon(appIcon)
|
|
self.trayIcon = QtWidgets.QSystemTrayIcon(appIcon, self)
|
|
# Whether the icon is shown or not depends on the Desktop environment.
|
|
self.trayIcon.show()
|
|
|
|
self.dockbarOne.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.dockbarTwo.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.dockbarThree.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
# Autosave approximately once every ten minutes.
|
|
# Margin of error: 500 ms.
|
|
self.saveTimer.start(600000)
|
|
|
|
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.LQLWIZARD = LQLQueryBuilder(self)
|
|
|
|
# Have the project auto-save on regular intervals by default.
|
|
self.saveTimer = QtCore.QTimer(self)
|
|
self.saveTimer.timeout.connect(self.autoSaveProject)
|
|
self.saveTimer.setSingleShot(False)
|
|
self.saveTimer.setTimerType(QtCore.Qt.VeryCoarseTimer)
|
|
|
|
self.syncedCanvases = []
|
|
self.syncedCanvasesLock = threading.Lock()
|
|
self.serverProjects = []
|
|
self.serverProjectsLock = threading.Lock()
|
|
self.resolutions = []
|
|
self.serverCollectorsLock = threading.Lock()
|
|
self.collectors = {}
|
|
self.runningCollectors = {}
|
|
self.cycleExtractionThreads = []
|
|
|
|
self.RESOLUTIONMANAGER.loadResolutionsFromDir(
|
|
Path(self.SETTINGS.value("Program/BaseDir")) / "Core" / "Resolutions" / "Core")
|
|
|
|
self.entityTextFont = QtGui.QFont(self.SETTINGS.value("Program/Graphics/EntityTextFontType"),
|
|
int(self.SETTINGS.value("Program/Graphics/EntityTextFontSize")),
|
|
int(self.SETTINGS.value("Program/Graphics/EntityTextFontBoldness")))
|
|
self.entityTextBrush = QtGui.QBrush(self.SETTINGS.value("Program/Graphics/EntityTextColor"))
|
|
self.linkTextFont = QtGui.QFont(self.SETTINGS.value("Program/Graphics/LinkTextFontType"),
|
|
int(self.SETTINGS.value("Program/Graphics/LinkTextFontSize")),
|
|
int(self.SETTINGS.value("Program/Graphics/LinkTextFontBoldness")))
|
|
self.linkTextBrush = QtGui.QBrush(self.SETTINGS.value("Program/Graphics/LinkTextColor"))
|
|
|
|
self.setCentralWidget(CentralPane.WorkspaceWidget(self,
|
|
self.MESSAGEHANDLER,
|
|
self.URLMANAGER,
|
|
self.LENTDB,
|
|
self.RESOURCEHANDLER,
|
|
self.entityTextFont,
|
|
self.entityTextBrush,
|
|
self.linkTextFont,
|
|
self.linkTextBrush))
|
|
|
|
self.facilitateResolutionSignalListener.connect(self.centralWidget().tabbedPane.facilitateResolution)
|
|
|
|
self.loadModules()
|
|
|
|
# Sort the resolutions alphabetically.
|
|
self.RESOLUTIONMANAGER.resolutions = dict(sorted(self.RESOLUTIONMANAGER.resolutions.items()))
|
|
|
|
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.trayIcon = None
|
|
# Cannot specify icon if notification spawned from signal.
|
|
self.notifyUserSignalListener.connect(self.notifyUser)
|
|
|
|
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")
|
|
|
|
savePath = Path(reportData[0]['SavePath']).absolute()
|
|
|
|
try:
|
|
ReportGeneration.PDFReport(str(path), reportData, outgoingEntitiesForEachEntity,
|
|
incomingEntitiesForEachEntity, entityList, canvasImagePath, None, # <timelinePic
|
|
self.primaryFieldsList, incomingEntityPrimaryFieldsForEachEntity,
|
|
outgoingEntityPrimaryFieldsForEachEntity)
|
|
|
|
self.parent().MESSAGEHANDLER.debug(reportData)
|
|
self.parent().MESSAGEHANDLER.info("Saved Report at: " + str(savePath), popUp=True)
|
|
except PermissionError:
|
|
self.parent().MESSAGEHANDLER.error("Could not generate report. No permission to save at the chosen "
|
|
"location: " + str(savePath), popUp=True, exc_info=False)
|
|
except Exception as exc:
|
|
self.parent().MESSAGEHANDLER.error("Could not generate report: " + str(exc), popUp=True, exc_info=True)
|
|
finally:
|
|
# Technically not necessary as the temp directory is deleted.
|
|
canvasImagePath.unlink(missing_ok=True)
|
|
|
|
|
|
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',
|
|
dir=str(Path.home()),
|
|
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',
|
|
dir=str(Path.home()),
|
|
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)
|
|
sigError = QtCore.Signal(str)
|
|
|
|
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) -> None:
|
|
try:
|
|
ret = self.mainWindow.RESOLUTIONMANAGER.executeResolution(self.resolution,
|
|
self.resolutionArgument,
|
|
self.resolutionParameters,
|
|
self.uid)
|
|
if ret is None:
|
|
self.sigError.emit('Resolution ' + self.resolution + ' failed during run.')
|
|
elif isinstance(ret, bool):
|
|
# Resolution is running on the server, we do not have results right now.
|
|
ret = None
|
|
except Exception as e:
|
|
self.sigError.emit('Resolution ' + self.resolution + ' failed during run: ' + str(e))
|
|
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,
|
|
resolutionDescription: str = None) -> 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())
|
|
labelText = ""
|
|
if resolutionDescription is not None:
|
|
labelText += resolutionDescription + "\n\n"
|
|
labelText += 'Select the entities to use for this resolution.\nAccepted Origin Types: ' + \
|
|
', '.join(originTypes)
|
|
entitySelectTabLabel = QtWidgets.QLabel(labelText)
|
|
entitySelectTabLabel.setWordWrap(True)
|
|
entitySelectTabLabel.setMaximumWidth(600)
|
|
|
|
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
entitySelectTab.layout().addWidget(entitySelectTabLabel)
|
|
|
|
self.entitySelector = QtWidgets.QListWidget()
|
|
self.entitySelector.setSortingEnabled(True)
|
|
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 were 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 GraphicsEditDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, settingsObject, resourceHandler):
|
|
super(GraphicsEditDialog, 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.ScrollBarAsNeeded)
|
|
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('Graphics 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.settingsValueTextboxes = []
|
|
self.newSettings = {}
|
|
|
|
etfSettingTextbox = SettingsIntegerEditTextBox(int(self.settings.value("Program/Graphics/EntityTextFontSize")),
|
|
"ETF",
|
|
50, 5)
|
|
etfSettingTextbox.setStyleSheet(Stylesheets.TEXT_BOX_STYLESHEET)
|
|
self.settingsValueTextboxes.append(etfSettingTextbox)
|
|
self.resolutionCategoryLayout.addRow("Entity Text Font Size", etfSettingTextbox)
|
|
|
|
ltfSettingTextbox = SettingsIntegerEditTextBox(int(self.settings.value("Program/Graphics/LinkTextFontSize")),
|
|
"LTF",
|
|
50, 5)
|
|
ltfSettingTextbox.setStyleSheet(Stylesheets.TEXT_BOX_STYLESHEET)
|
|
self.settingsValueTextboxes.append(ltfSettingTextbox)
|
|
self.resolutionCategoryLayout.addRow("Link Text Font Size", ltfSettingTextbox)
|
|
|
|
self.colorPicker = QtWidgets.QColorDialog()
|
|
self.colorPicker.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.colorPicker.setOption(QtWidgets.QColorDialog.DontUseNativeDialog, True)
|
|
|
|
etcSettingWidget = QtWidgets.QWidget()
|
|
etcSettingLayout = QtWidgets.QHBoxLayout()
|
|
etcSettingWidget.setLayout(etcSettingLayout)
|
|
|
|
etcSettingTextbox = SettingsEditTextBox(self.settings.value("Program/Graphics/EntityTextColor"), "ETC")
|
|
etcSettingTextbox.setReadOnly(True)
|
|
etcSettingTextbox.setStyleSheet(Stylesheets.TEXT_BOX_STYLESHEET)
|
|
etcSettingLayout.addWidget(etcSettingTextbox, 5)
|
|
|
|
etcSettingPalettePrompt = QtWidgets.QPushButton(QtGui.QIcon(resourceHandler.getIcon("colorPicker")),
|
|
"Pick Colour")
|
|
etcSettingPalettePrompt.clicked.connect(self.runEntityColorPicker)
|
|
etcSettingLayout.addWidget(etcSettingPalettePrompt)
|
|
|
|
self.settingsTextboxes.append(etcSettingTextbox)
|
|
self.resolutionCategoryLayout.addRow("Entity Text Color", etcSettingWidget)
|
|
|
|
ltcSettingWidget = QtWidgets.QWidget()
|
|
ltcSettingLayout = QtWidgets.QHBoxLayout()
|
|
ltcSettingWidget.setLayout(ltcSettingLayout)
|
|
|
|
ltcSettingTextbox = SettingsEditTextBox(self.settings.value("Program/Graphics/LinkTextColor"), "LTC")
|
|
ltcSettingTextbox.setReadOnly(True)
|
|
ltcSettingTextbox.setStyleSheet(Stylesheets.TEXT_BOX_STYLESHEET)
|
|
ltcSettingLayout.addWidget(ltcSettingTextbox, 5)
|
|
|
|
ltcSettingPalettePrompt = QtWidgets.QPushButton(QtGui.QIcon(resourceHandler.getIcon("colorPicker")),
|
|
"Pick Colour")
|
|
ltcSettingPalettePrompt.clicked.connect(self.runLinkColorPicker)
|
|
ltcSettingLayout.addWidget(ltcSettingPalettePrompt)
|
|
|
|
self.settingsTextboxes.append(ltcSettingTextbox)
|
|
self.resolutionCategoryLayout.addRow("Link Text Color", ltcSettingWidget)
|
|
|
|
def runEntityColorPicker(self):
|
|
color = self.colorPicker.getColor(QtGui.QColor(self.settings.value("Program/Graphics/EntityTextColor")),
|
|
title="Select New Entity Text Color")
|
|
if color.isValid():
|
|
self.settingsTextboxes[0].setText(color.name())
|
|
|
|
def runLinkColorPicker(self):
|
|
color = self.colorPicker.getColor(QtGui.QColor(self.settings.value("Program/Graphics/LinkTextColor")),
|
|
title="Select New Link Text Color")
|
|
if color.isValid():
|
|
self.settingsTextboxes[1].setText(color.name())
|
|
|
|
def accept(self) -> None:
|
|
# Cannot delete these values.
|
|
for settingTextbox in self.settingsTextboxes:
|
|
key = settingTextbox.settingsKey
|
|
value = settingTextbox.text()
|
|
self.newSettings[key] = value
|
|
for settingsValueTextbox in self.settingsValueTextboxes:
|
|
key = settingsValueTextbox.settingsKey
|
|
value = settingsValueTextbox.value()
|
|
self.newSettings[key] = value
|
|
|
|
super(GraphicsEditDialog, self).accept()
|
|
|
|
|
|
class ProgramEditDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, settingsObject):
|
|
super(ProgramEditDialog, 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.ScrollBarAsNeeded)
|
|
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(supportsDeletion=False)
|
|
resolutionCategoryWidget.setLayout(self.resolutionCategoryLayout)
|
|
resolutionCategoryLabel = QtWidgets.QLabel('Program 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 = {}
|
|
self.settingsSingleChoice = []
|
|
|
|
for setting in self.settings:
|
|
if setting.startswith('Program/'):
|
|
keyName = setting.split('Program/', 1)[1]
|
|
if keyName != "BaseDir" and len(setting.split('/')) == 2: # Don't allow users to mess with these.
|
|
# A bit redundant to do it this way, but it'll be cleaner if / when more settings are added.
|
|
if keyName == "GraphLayout":
|
|
settingSingleChoice = SettingsEditSingleChoice(['dot', 'sfdp', 'neato'],
|
|
self.settings.value(setting),
|
|
setting)
|
|
settingSingleChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
|
self.settingsSingleChoice.append(settingSingleChoice)
|
|
self.resolutionCategoryLayout.addRow(keyName, settingSingleChoice)
|
|
else:
|
|
settingTextbox = SettingsEditTextBox(self.settings.value(setting), setting)
|
|
self.settingsTextboxes.append(settingTextbox)
|
|
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)
|
|
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(ProgramEditDialog, self).accept()
|
|
|
|
|
|
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.ScrollBarAsNeeded)
|
|
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.ScrollBarAsNeeded)
|
|
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.ScrollBarAsNeeded)
|
|
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: str, settingsKey: str):
|
|
super(SettingsEditTextBox, self).__init__(str(contents))
|
|
self.settingsKey = settingsKey
|
|
self.keyDeleted = False
|
|
self.setToolTip("Edit the contents to change the setting's value.")
|
|
|
|
|
|
class SettingsIntegerEditTextBox(QtWidgets.QSpinBox):
|
|
|
|
def __init__(self, contents: int, settingsKey: str, maxVal: int, minVal: int):
|
|
super(SettingsIntegerEditTextBox, self).__init__()
|
|
self.setMaximum(maxVal)
|
|
self.setMinimum(minVal)
|
|
self.setValue(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, regex: bool):
|
|
super(FindEntityOnCanvasDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.setMinimumWidth(400)
|
|
if regex:
|
|
self.setWindowTitle('Regex Find Entity or Link')
|
|
else:
|
|
self.setWindowTitle('Find Entity or Link')
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_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)
|
|
if regex:
|
|
# Doesn't actually work in python, as far as I can see.
|
|
# Throws error: Unhandled QCompleter::filterMode flag is used.
|
|
# autoCompleter.setFilterMode(QtGui.Qt.MatchRegularExpression)
|
|
pass
|
|
else:
|
|
autoCompleter.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
autoCompleter.setFilterMode(QtCore.Qt.MatchContains)
|
|
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)
|
|
# Adding the confirm button first so that it's what is activated when someone presses Enter.
|
|
findLayout.addWidget(confirmButton, 1, 1, 1, 1)
|
|
findLayout.addWidget(cancelButton, 1, 0, 1, 1)
|
|
|
|
|
|
class FindEntityOfTypeOnCanvasDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, entityTypesDict: dict, regex: bool):
|
|
super(FindEntityOfTypeOnCanvasDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.setMinimumWidth(400)
|
|
if regex:
|
|
self.setWindowTitle('Regex Find Entity Of Type')
|
|
else:
|
|
self.setWindowTitle('Find Entity Of Type')
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
typeLabel = QtWidgets.QLabel('Entity Type:')
|
|
self.typeInput = QtWidgets.QComboBox()
|
|
self.typeInput.setEditable(False)
|
|
self.typeInput.addItems(list(entityTypesDict))
|
|
self.typeInput.currentIndexChanged.connect(self.changeSelectedType)
|
|
findLabel = QtWidgets.QLabel('Find Entity:')
|
|
|
|
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)
|
|
|
|
self.autoCompleters = {}
|
|
for entityType in entityTypesDict:
|
|
autoCompleter = QtWidgets.QCompleter(list(entityTypesDict[entityType]))
|
|
self.autoCompleters[entityType] = autoCompleter
|
|
if regex:
|
|
# Doesn't actually work in python, as far as I can see.
|
|
# Throws error: Unhandled QCompleter::filterMode flag is used.
|
|
# autoCompleter.setFilterMode(QtGui.Qt.MatchRegularExpression)
|
|
pass
|
|
else:
|
|
autoCompleter.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
autoCompleter.setFilterMode(QtCore.Qt.MatchContains)
|
|
|
|
findLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(findLayout)
|
|
|
|
findLayout.addWidget(typeLabel, 0, 0, 1, 1)
|
|
findLayout.addWidget(self.typeInput, 0, 1, 1, 1)
|
|
findLayout.addWidget(findLabel, 1, 0, 1, 1)
|
|
findLayout.addWidget(self.findInput, 1, 1, 1, 1)
|
|
# Adding the confirm button first so that it's what is activated when someone presses Enter.
|
|
findLayout.addWidget(confirmButton, 2, 1, 1, 1)
|
|
findLayout.addWidget(cancelButton, 2, 0, 1, 1)
|
|
|
|
self.changeSelectedType()
|
|
|
|
def changeSelectedType(self):
|
|
self.findInput.setCompleter(self.autoCompleters[self.typeInput.currentText()])
|
|
|
|
|
|
class ResolutionSearchResultsList(QtWidgets.QListWidget):
|
|
|
|
def __init__(self, mainWindowObject: MainWindow):
|
|
super(ResolutionSearchResultsList, self).__init__()
|
|
self.mainWindow = mainWindowObject
|
|
self.setSortingEnabled(True)
|
|
|
|
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
super(ResolutionSearchResultsList, self).mouseDoubleClickEvent(event)
|
|
resItem = self.itemAt(event.pos())
|
|
if resItem is None or '/' not in resItem.text():
|
|
return
|
|
self.mainWindow.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
|
self.mainWindow.runResolution(resItem.text())
|
|
|
|
|
|
class FindResolutionDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent: MainWindow, entityList: list, resolutionDict: dict):
|
|
super(FindResolutionDialog, self).__init__()
|
|
self.entities = entityList
|
|
self.resolutions = resolutionDict
|
|
self.setModal(True)
|
|
self.setWindowTitle('Find Resolutions')
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
|
|
descriptionLabel = QtWidgets.QLabel("Find Resolutions based on their parameters.")
|
|
descriptionLabel.setWordWrap(True)
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
|
|
|
originLabel = QtWidgets.QLabel("Origin Entity:")
|
|
self.originDropDown = QtWidgets.QComboBox()
|
|
self.originDropDown.addItem('Any')
|
|
self.originDropDown.addItems(entityList)
|
|
self.originDropDown.addItem('*')
|
|
dialogLayout.addWidget(originLabel, 1, 0, 1, 1)
|
|
dialogLayout.addWidget(self.originDropDown, 1, 1, 1, 1)
|
|
|
|
targetLabel = QtWidgets.QLabel("Target Entity:")
|
|
self.targetDropDown = QtWidgets.QComboBox()
|
|
self.targetDropDown.addItem('Any')
|
|
self.targetDropDown.addItems(entityList)
|
|
self.targetDropDown.addItem('*')
|
|
dialogLayout.addWidget(targetLabel, 2, 0, 1, 1)
|
|
dialogLayout.addWidget(self.targetDropDown, 2, 1, 1, 1)
|
|
|
|
keywordsLabel = QtWidgets.QLabel("Keywords:")
|
|
self.keywordsWidget = QtWidgets.QLineEdit()
|
|
self.keywordsWidget.setToolTip("Add keywords separated by spaces.\nKeywords are checked against the "
|
|
"resolutions' titles and descriptions.")
|
|
dialogLayout.addWidget(keywordsLabel, 3, 0, 1, 2)
|
|
dialogLayout.addWidget(self.keywordsWidget, 4, 0, 1, 2)
|
|
|
|
resultsLabel = QtWidgets.QLabel("Matches:")
|
|
self.resultsWidget = ResolutionSearchResultsList(parent)
|
|
self.resultsWidget.addItem('Click "Search" to display results')
|
|
dialogLayout.addWidget(resultsLabel, 5, 0, 1, 2)
|
|
dialogLayout.addWidget(self.resultsWidget, 6, 0, 2, 2)
|
|
|
|
self.searchButton = QtWidgets.QPushButton("Search")
|
|
self.searchButton.clicked.connect(self.search)
|
|
self.closeButton = QtWidgets.QPushButton("Close")
|
|
self.closeButton.clicked.connect(self.accept)
|
|
dialogLayout.addWidget(self.closeButton, 8, 0, 1, 1)
|
|
dialogLayout.addWidget(self.searchButton, 8, 1, 1, 1)
|
|
|
|
def search(self):
|
|
self.resultsWidget.clear()
|
|
target = self.targetDropDown.currentText()
|
|
validResolutions = []
|
|
if target != 'Any':
|
|
for category in self.resolutions:
|
|
for resolution in self.resolutions[category]:
|
|
if target in self.resolutions[category][resolution]['originTypes']:
|
|
validResolutions.append(category + '/' + resolution)
|
|
else:
|
|
for category in self.resolutions:
|
|
for resolution in self.resolutions[category]:
|
|
validResolutions.append(category + '/' + resolution)
|
|
|
|
origin = self.originDropDown.currentText()
|
|
if origin != 'Any':
|
|
for category in self.resolutions:
|
|
for resolution in self.resolutions[category]:
|
|
if origin not in self.resolutions[category][resolution]['originTypes']:
|
|
try:
|
|
validResolutions.remove(str(category) + '/' + str(resolution))
|
|
except KeyError:
|
|
# Python's philosophy of asking for forgiveness instead of asking for permission
|
|
# is not one to live your life by. When in Rome, though.
|
|
pass
|
|
|
|
# Try to see if any of the keywords are a substring of the name or description of any resolution.
|
|
keywordFilter = self.keywordsWidget.text().strip()
|
|
if keywordFilter != '':
|
|
wordsToFind = keywordFilter.split(' ')
|
|
for category in self.resolutions:
|
|
for resolution in self.resolutions[category]:
|
|
titleText = self.resolutions[category][resolution]['name']
|
|
descriptionText = self.resolutions[category][resolution]['description']
|
|
for keyword in wordsToFind:
|
|
if keyword not in titleText and keyword not in descriptionText:
|
|
try:
|
|
validResolutions.remove(str(category) + '/' + str(resolution))
|
|
except KeyError:
|
|
pass
|
|
|
|
for result in validResolutions:
|
|
self.resultsWidget.addItem(result)
|
|
|
|
|
|
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()
|
|
|
|
|
|
class QueryBuilderWizard(QtWidgets.QDialog):
|
|
|
|
def __init__(self, mainWindowObject: MainWindow):
|
|
super(QueryBuilderWizard, self).__init__()
|
|
self.mainWindowObject = mainWindowObject
|
|
self.setModal(True)
|
|
self.setWindowTitle('LQL Query Wizard')
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
|
|
# Add a tab to create a new query, and a tab to re-run old queries.
|
|
self.queryNewOrHistory = QtWidgets.QTabWidget()
|
|
self.queryTabbedPane = QtWidgets.QTabWidget()
|
|
self.queryNewOrHistory.addTab(self.queryTabbedPane, 'New Query')
|
|
dialogLayout.addWidget(self.queryNewOrHistory)
|
|
|
|
buttonsWidget = QtWidgets.QWidget()
|
|
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
|
|
buttonsWidget.setLayout(buttonsWidgetLayout)
|
|
exitButton = QtWidgets.QPushButton('Close')
|
|
exitButton.clicked.connect(self.accept)
|
|
self.runButton = QtWidgets.QPushButton('Run Query')
|
|
self.runButton.clicked.connect(self.runButtonPressed)
|
|
buttonsWidgetLayout.addWidget(exitButton)
|
|
buttonsWidgetLayout.addWidget(self.runButton)
|
|
dialogLayout.addWidget(buttonsWidget)
|
|
|
|
#### SELECT
|
|
selectPane = QtWidgets.QWidget()
|
|
selectPaneLayout = QtWidgets.QGridLayout()
|
|
selectPane.setLayout(selectPaneLayout)
|
|
self.selectStatementPicker = QtWidgets.QComboBox()
|
|
self.selectStatementPicker.addItems(['SELECT', 'RSELECT'])
|
|
self.selectStatementPicker.setEditable(False) # Default, but it's nice to be explicit.
|
|
self.selectStatementPicker.currentIndexChanged.connect(
|
|
lambda newIndex: self.selectStatementValuePickerLayout.setCurrentIndex(newIndex))
|
|
selectStatementValuePickerWidget = QtWidgets.QWidget()
|
|
self.selectStatementValuePickerLayout = QtWidgets.QStackedLayout()
|
|
selectStatementValuePickerWidget.setLayout(self.selectStatementValuePickerLayout)
|
|
self.selectStatementList = QtWidgets.QListWidget()
|
|
self.selectStatementList.setSortingEnabled(True)
|
|
self.selectStatementList.setSelectionMode(QtWidgets.QListWidget.ExtendedSelection)
|
|
self.selectStatementList.setMinimumHeight(125)
|
|
self.selectStatementList.setToolTip('Highlight all the fields you wish to select.')
|
|
self.selectStatementTextbox = QtWidgets.QLineEdit('')
|
|
self.selectStatementTextbox.setFixedHeight(26)
|
|
self.selectStatementTextbox.setToolTip('Type the regex you want to use to specify the fields to select.')
|
|
self.selectStatementValuePickerLayout.addWidget(self.selectStatementList)
|
|
self.selectStatementValuePickerLayout.addWidget(self.selectStatementTextbox)
|
|
|
|
selectPaneLayout.addWidget(QtWidgets.QLabel('Selection mode: '), 0, 0, 1, 1)
|
|
selectPaneLayout.addWidget(self.selectStatementPicker, 0, 1, 1, 1)
|
|
selectPaneLayout.addWidget(QtWidgets.QLabel('Field selection:'), 1, 0, 1, 2)
|
|
selectPaneLayout.addWidget(selectStatementValuePickerWidget, 2, 0, 2, 2)
|
|
|
|
self.queryTabbedPane.addTab(selectPane, 'Selection')
|
|
####
|
|
|
|
#### SOURCE
|
|
sourcePane = QtWidgets.QWidget()
|
|
sourcePaneLayout = QtWidgets.QGridLayout()
|
|
sourcePane.setLayout(sourcePaneLayout)
|
|
|
|
self.sourceStatementPicker = QtWidgets.QComboBox()
|
|
self.sourceStatementPicker.addItems(['FROMDB', 'FROM'])
|
|
self.sourceStatementPicker.setEditable(False)
|
|
self.sourceStatementPicker.currentTextChanged.connect(self.sourceModeSwitch)
|
|
|
|
self.sourceValues = []
|
|
self.sourceValuesArea = QtWidgets.QScrollArea()
|
|
self.sourceValuesArea.setWidgetResizable(True)
|
|
self.sourceValuesArea.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
|
|
|
sourceValuesAreaWidget = QtWidgets.QWidget()
|
|
self.sourceValuesAreaWidgetLayout = QtWidgets.QVBoxLayout()
|
|
sourceValuesAreaWidget.setLayout(self.sourceValuesAreaWidgetLayout)
|
|
self.sourceValuesArea.setWidget(sourceValuesAreaWidget)
|
|
self.sourceValuesArea.setEnabled(False)
|
|
self.sourceValuesArea.setDisabled(True)
|
|
self.sourceValuesArea.setHidden(True)
|
|
|
|
sourceButtonsWidget = QtWidgets.QWidget()
|
|
sourceButtonsWidgetLayout = QtWidgets.QHBoxLayout()
|
|
sourceButtonsWidget.setLayout(sourceButtonsWidgetLayout)
|
|
sourceAddStatementButton = QtWidgets.QPushButton('Add Clause')
|
|
sourceRemoveStatementButton = QtWidgets.QPushButton('Remove Last Clause')
|
|
sourceAddStatementButton.clicked.connect(self.addSourceClause)
|
|
sourceRemoveStatementButton.clicked.connect(self.removeSourceClause)
|
|
sourceButtonsWidgetLayout.addWidget(sourceRemoveStatementButton)
|
|
sourceButtonsWidgetLayout.addWidget(sourceAddStatementButton)
|
|
|
|
self.sourceValuesAreaWidgetLayout.addWidget(sourceButtonsWidget)
|
|
|
|
sourcePaneLayout.addWidget(QtWidgets.QLabel('Source: '), 0, 0, 1, 1)
|
|
sourcePaneLayout.addWidget(self.sourceStatementPicker, 0, 1, 1, 1)
|
|
sourcePaneLayout.addWidget(self.sourceValuesArea, 1, 0, 2, 2)
|
|
|
|
self.queryTabbedPane.addTab(sourcePane, 'Source')
|
|
####
|
|
|
|
#### CONDITIONS
|
|
conditionsPane = QtWidgets.QWidget()
|
|
conditionsPaneLayout = QtWidgets.QGridLayout()
|
|
conditionsPane.setLayout(conditionsPaneLayout)
|
|
|
|
self.conditionValues = []
|
|
self.conditionValuesArea = QtWidgets.QScrollArea()
|
|
self.conditionValuesArea.setWidgetResizable(True)
|
|
conditionValuesAreaWidget = QtWidgets.QWidget()
|
|
self.conditionValuesAreaWidgetLayout = QtWidgets.QVBoxLayout()
|
|
conditionValuesAreaWidget.setLayout(self.conditionValuesAreaWidgetLayout)
|
|
self.conditionValuesArea.setWidget(conditionValuesAreaWidget)
|
|
self.conditionValuesArea.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
|
|
|
conditionsButtonsWidget = QtWidgets.QWidget()
|
|
conditionsButtonsWidgetLayout = QtWidgets.QHBoxLayout()
|
|
conditionsButtonsWidget.setLayout(conditionsButtonsWidgetLayout)
|
|
conditionsAddStatementButton = QtWidgets.QPushButton('Add Condition')
|
|
conditionsRemoveStatementButton = QtWidgets.QPushButton('Remove Last Condition')
|
|
conditionsAddStatementButton.clicked.connect(self.addConditionClause)
|
|
conditionsRemoveStatementButton.clicked.connect(self.removeConditionClause)
|
|
conditionsButtonsWidgetLayout.addWidget(conditionsRemoveStatementButton)
|
|
conditionsButtonsWidgetLayout.addWidget(conditionsAddStatementButton)
|
|
|
|
self.conditionValuesAreaWidgetLayout.addWidget(conditionsButtonsWidget)
|
|
|
|
conditionsPaneLayout.addWidget(QtWidgets.QLabel('Conditions: '), 0, 0, 1, 1)
|
|
conditionsPaneLayout.addWidget(self.conditionValuesArea, 0, 0, 2, 2)
|
|
|
|
self.queryTabbedPane.addTab(conditionsPane, 'Conditions')
|
|
####
|
|
|
|
#### MODIFY
|
|
modificationsPane = QtWidgets.QWidget()
|
|
modificationsPaneLayout = QtWidgets.QGridLayout()
|
|
modificationsPane.setLayout(modificationsPaneLayout)
|
|
|
|
self.modificationValues = []
|
|
self.modificationValuesArea = QtWidgets.QScrollArea()
|
|
self.modificationValuesArea.setWidgetResizable(True)
|
|
self.modificationValuesArea.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
|
|
|
modificationValuesAreaWidget = QtWidgets.QWidget()
|
|
self.modificationValuesAreaWidgetLayout = QtWidgets.QVBoxLayout()
|
|
modificationValuesAreaWidget.setLayout(self.modificationValuesAreaWidgetLayout)
|
|
self.modificationValuesArea.setWidget(modificationValuesAreaWidget)
|
|
|
|
modificationsButtonsWidget = QtWidgets.QWidget()
|
|
modificationsButtonsWidgetLayout = QtWidgets.QHBoxLayout()
|
|
modificationsButtonsWidget.setLayout(modificationsButtonsWidgetLayout)
|
|
modificationsAddStatementButton = QtWidgets.QPushButton('Add Modification')
|
|
modificationsRemoveStatementButton = QtWidgets.QPushButton('Remove Last Modification')
|
|
modificationsAddStatementButton.clicked.connect(self.addModificationClause)
|
|
modificationsRemoveStatementButton.clicked.connect(self.removeModificationClause)
|
|
modificationsButtonsWidgetLayout.addWidget(modificationsRemoveStatementButton)
|
|
modificationsButtonsWidgetLayout.addWidget(modificationsAddStatementButton)
|
|
|
|
self.modificationValuesAreaWidgetLayout.addWidget(modificationsButtonsWidget)
|
|
|
|
modificationsPaneLayout.addWidget(QtWidgets.QLabel('Modifications: '), 0, 0, 1, 1)
|
|
modificationsPaneLayout.addWidget(self.modificationValuesArea, 0, 0, 2, 2)
|
|
|
|
self.queryTabbedPane.addTab(modificationsPane, 'Modifications')
|
|
####
|
|
|
|
self.entityDropdownTriplets = []
|
|
|
|
self.historyTable = QtWidgets.QTableWidget(0, 7, self)
|
|
self.historyTable.setSelectionBehavior(self.historyTable.SelectRows)
|
|
self.historyTable.setSelectionMode(self.historyTable.SingleSelection)
|
|
self.historyTable.setAcceptDrops(False)
|
|
self.historyTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
|
self.historyTable.verticalHeader().setCascadingSectionResizes(True)
|
|
self.historyTable.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
|
self.historyTable.setHorizontalHeaderLabels(['Query UID', 'Select Clause', 'Select Value(s)', 'Source Clause',
|
|
'Source Value(s)', 'Condition Clause(s)', 'Modification Values'])
|
|
self.queryNewOrHistory.addTab(self.historyTable, 'History')
|
|
|
|
self.updateValues()
|
|
self.resize(1000, 600)
|
|
|
|
def sourceModeSwitch(self, newText: str):
|
|
if newText == 'FROMDB':
|
|
self.sourceValuesArea.setEnabled(False)
|
|
self.sourceValuesArea.setDisabled(True)
|
|
self.sourceValuesArea.setHidden(True)
|
|
else:
|
|
self.sourceValuesArea.setEnabled(True)
|
|
self.sourceValuesArea.setDisabled(False)
|
|
self.sourceValuesArea.setHidden(False)
|
|
|
|
def addSourceClause(self):
|
|
|
|
clauseWidget = QtWidgets.QFrame()
|
|
clauseWidgetLayout = QtWidgets.QVBoxLayout()
|
|
clauseWidget.setLayout(clauseWidgetLayout)
|
|
clauseWidget.setFrameStyle(clauseWidget.Panel | clauseWidget.Raised)
|
|
clauseWidget.setLineWidth(3)
|
|
|
|
andOrClause = QtWidgets.QComboBox()
|
|
andOrClause.addItems(['OR', 'AND'])
|
|
specifier = QtWidgets.QComboBox()
|
|
specifier.addItems(['CANVAS', 'RCANVAS'])
|
|
negation = QtWidgets.QComboBox()
|
|
negation.addItems(['MATCHES', 'DOES NOT MATCH'])
|
|
inputDropdown = QtWidgets.QListWidget()
|
|
inputDropdown.setSortingEnabled(True)
|
|
inputDropdown.setSelectionMode(QtWidgets.QListWidget.SingleSelection)
|
|
inputDropdown.setMinimumHeight(125)
|
|
inputDropdown.addItems(self.mainWindowObject.LQLWIZARD.allCanvases)
|
|
inputText = QtWidgets.QLineEdit('')
|
|
inputText.setFixedHeight(26)
|
|
inputWidget = QtWidgets.QWidget()
|
|
inputWidgetLayout = QtWidgets.QStackedLayout()
|
|
inputWidget.setLayout(inputWidgetLayout)
|
|
inputWidgetLayout.addWidget(inputDropdown)
|
|
inputWidgetLayout.addWidget(inputText)
|
|
specifier.currentIndexChanged.connect(lambda newIndex: inputWidgetLayout.setCurrentIndex(newIndex))
|
|
|
|
clauseWidgetLayout.addWidget(andOrClause)
|
|
clauseWidgetLayout.addWidget(specifier)
|
|
clauseWidgetLayout.addWidget(negation)
|
|
clauseWidgetLayout.addWidget(inputWidget)
|
|
|
|
if self.sourceValuesAreaWidgetLayout.count() == 1:
|
|
andOrClause.setDisabled(True)
|
|
andOrClause.setToolTip('Cannot edit the set modifier of the first source clause.')
|
|
|
|
self.sourceValues.append(clauseWidget)
|
|
self.sourceValuesAreaWidgetLayout.insertWidget(self.sourceValuesAreaWidgetLayout.count() - 1, clauseWidget)
|
|
|
|
def removeSourceClause(self):
|
|
if self.sourceValuesAreaWidgetLayout.count() != 1:
|
|
# Remove clause that was added last.
|
|
itemToDel = self.sourceValuesAreaWidgetLayout.takeAt(self.sourceValuesAreaWidgetLayout.count() - 2)
|
|
itemToDel.widget().deleteLater()
|
|
widgetToDel = self.sourceValues.pop()
|
|
widgetToDel.deleteLater()
|
|
|
|
def addConditionClause(self):
|
|
|
|
clauseWidget = ConditionClauseWidget(self)
|
|
|
|
if self.conditionValuesAreaWidgetLayout.count() == 1:
|
|
clauseWidget.andOrClause.setDisabled(True)
|
|
clauseWidget.andOrClause.setToolTip('Cannot edit the set modifier of the first condition clause.')
|
|
self.conditionValues.append(clauseWidget)
|
|
self.conditionValuesAreaWidgetLayout.insertWidget(self.conditionValuesAreaWidgetLayout.count() - 1,
|
|
clauseWidget)
|
|
|
|
def removeConditionClause(self):
|
|
if self.conditionValuesAreaWidgetLayout.count() != 1:
|
|
# Remove clause that was added last.
|
|
itemToDel = self.conditionValuesAreaWidgetLayout.takeAt(self.conditionValuesAreaWidgetLayout.count() - 2)
|
|
itemToDel.widget().deleteLater()
|
|
widgetToDel = self.conditionValues.pop()
|
|
widgetToDel.deleteLater()
|
|
|
|
def addModificationClause(self):
|
|
|
|
clauseWidget = QtWidgets.QFrame()
|
|
clauseWidgetLayout = QtWidgets.QVBoxLayout()
|
|
clauseWidget.setLayout(clauseWidgetLayout)
|
|
clauseWidget.setFrameStyle(clauseWidget.Panel | clauseWidget.Raised)
|
|
clauseWidget.setLineWidth(3)
|
|
|
|
andOrClause = QtWidgets.QLabel('AND')
|
|
specifier = QtWidgets.QComboBox()
|
|
specifier.addItems(['MODIFY', 'RMODIFY'])
|
|
|
|
modifyOption = QtWidgets.QComboBox()
|
|
modifyOption.addItems(['NUMIFY', 'UPPERCASE', 'LOWERCASE'])
|
|
|
|
inputWidget = QtWidgets.QWidget()
|
|
inputWidgetLayout = QtWidgets.QStackedLayout()
|
|
|
|
inputDropdown = QtWidgets.QListWidget()
|
|
inputDropdown.setSortingEnabled(True)
|
|
inputDropdown.setSelectionMode(QtWidgets.QListWidget.SingleSelection)
|
|
inputDropdown.addItems(self.mainWindowObject.LQLWIZARD.allEntityFields)
|
|
inputDropdown.setMinimumHeight(125)
|
|
inputText = QtWidgets.QLineEdit('')
|
|
inputText.setFixedHeight(26)
|
|
inputWidget.setLayout(inputWidgetLayout)
|
|
inputWidgetLayout.addWidget(inputDropdown)
|
|
inputWidgetLayout.addWidget(inputText)
|
|
specifier.currentIndexChanged.connect(lambda newIndex: inputWidgetLayout.setCurrentIndex(newIndex))
|
|
|
|
clauseWidgetLayout.addWidget(andOrClause)
|
|
clauseWidgetLayout.addWidget(specifier)
|
|
clauseWidgetLayout.addWidget(inputWidget)
|
|
clauseWidgetLayout.addWidget(modifyOption)
|
|
|
|
if self.modificationValuesAreaWidgetLayout.count() == 1:
|
|
andOrClause.setDisabled(True)
|
|
andOrClause.setToolTip('Cannot edit the set modifier of the first source clause.')
|
|
self.modificationValues.append(clauseWidget)
|
|
self.modificationValuesAreaWidgetLayout.insertWidget(self.modificationValuesAreaWidgetLayout.count() - 1,
|
|
clauseWidget)
|
|
|
|
def removeModificationClause(self):
|
|
if self.modificationValuesAreaWidgetLayout.count() != 1:
|
|
# Remove clause that was added last.
|
|
itemToDel = self.modificationValuesAreaWidgetLayout.takeAt(
|
|
self.modificationValuesAreaWidgetLayout.count() - 2)
|
|
itemToDel.widget().deleteLater()
|
|
widgetToDel = self.modificationValues.pop()
|
|
widgetToDel.deleteLater()
|
|
|
|
def runButtonPressed(self):
|
|
if self.runButton.text() == 'Run Query':
|
|
self.runButton.setText('Reset Query Wizard')
|
|
self.runQuery()
|
|
else:
|
|
self.updateValues()
|
|
self.runButton.setText('Run Query')
|
|
|
|
def updateValues(self):
|
|
self.mainWindowObject.LQLWIZARD.takeSnapshot()
|
|
|
|
self.selectStatementList.clear()
|
|
self.selectStatementList.addItems(self.mainWindowObject.LQLWIZARD.allEntityFields)
|
|
self.selectStatementTextbox.setText('')
|
|
|
|
for _ in range(len(self.sourceValues)):
|
|
widgetToDel = self.sourceValues.pop()
|
|
widgetToDel.deleteLater()
|
|
|
|
for _ in range(self.sourceValuesAreaWidgetLayout.count() - 1):
|
|
itemToDel = self.sourceValuesAreaWidgetLayout.takeAt(0)
|
|
itemToDel.widget().deleteLater()
|
|
|
|
for _ in range(len(self.conditionValues)):
|
|
widgetToDel = self.conditionValues.pop()
|
|
widgetToDel.deleteLater()
|
|
|
|
for _ in range(self.conditionValuesAreaWidgetLayout.count() - 1):
|
|
itemToDel = self.conditionValuesAreaWidgetLayout.takeAt(0)
|
|
itemToDel.widget().deleteLater()
|
|
|
|
for _ in range(self.modificationValuesAreaWidgetLayout.count() - 1):
|
|
itemToDel = self.modificationValuesAreaWidgetLayout.takeAt(0)
|
|
itemToDel.widget().deleteLater()
|
|
|
|
for _ in range(len(self.modificationValues)):
|
|
widgetToDel = self.modificationValues.pop()
|
|
widgetToDel.deleteLater()
|
|
|
|
self.entityDropdownTriplets.clear()
|
|
for entityUID in self.mainWindowObject.LQLWIZARD.databaseSnapshot.nodes:
|
|
nodeDetails = self.mainWindowObject.LQLWIZARD.databaseSnapshot.nodes[entityUID]
|
|
pixmapIcon = QtGui.QPixmap()
|
|
pixmapIcon.loadFromData(nodeDetails['Icon'])
|
|
self.entityDropdownTriplets.append((nodeDetails[list(nodeDetails)[1]], entityUID, pixmapIcon))
|
|
|
|
for _ in range(self.historyTable.rowCount()):
|
|
self.historyTable.removeRow(0)
|
|
|
|
for oldQueryUID in self.mainWindowObject.LQLWIZARD.QUERIES_HISTORY:
|
|
self.historyTable.insertRow(0)
|
|
self.historyTable.setItem(0, 0, QtWidgets.QTableWidgetItem(str(oldQueryUID)))
|
|
for valueIndex in range(6):
|
|
self.historyTable.setItem(0, valueIndex + 1, QtWidgets.QTableWidgetItem(
|
|
str(self.mainWindowObject.LQLWIZARD.QUERIES_HISTORY[oldQueryUID][valueIndex])))
|
|
|
|
def runQuery(self):
|
|
# Results
|
|
if self.queryNewOrHistory.currentIndex() == 0:
|
|
sourceResults = []
|
|
for sourceValue in self.sourceValues:
|
|
sourceResult = [sourceValue.layout().itemAt(0).widget().currentText(),
|
|
sourceValue.layout().itemAt(1).widget().currentText(),
|
|
False if sourceValue.layout().itemAt(2).widget().currentText() == 'MATCHES' else True]
|
|
if sourceValue.layout().itemAt(3).widget().layout().currentIndex() == 0:
|
|
try:
|
|
sourceResult.append(
|
|
sourceValue.layout().itemAt(3).widget().layout().itemAt(0).widget().selectedItems()[
|
|
0].text())
|
|
except IndexError:
|
|
continue
|
|
else:
|
|
sourceResult.append(sourceValue.layout().itemAt(3).widget().layout().itemAt(1).widget().text())
|
|
sourceResults.append(sourceResult)
|
|
|
|
conditionResults = []
|
|
for conditionValue in self.conditionValues:
|
|
conditionResult = conditionValue.getValue()
|
|
if conditionResult is not None:
|
|
conditionResults.append(conditionResult)
|
|
if not conditionResults:
|
|
conditionResults = None
|
|
|
|
modificationResults = []
|
|
for modificationValue in self.modificationValues:
|
|
modificationResult = []
|
|
specifierText = modificationValue.layout().itemAt(1).widget().currentText()
|
|
modificationResult.append(specifierText)
|
|
if specifierText == 'MODIFY':
|
|
try:
|
|
modificationResult.append(
|
|
modificationValue.layout().itemAt(2).widget().layout().itemAt(0).widget().selectedItems()[
|
|
0].text())
|
|
except IndexError:
|
|
continue
|
|
else:
|
|
modificationResult.append(
|
|
modificationValue.layout().itemAt(2).widget().layout().itemAt(1).widget().text())
|
|
modificationResult.append(modificationValue.layout().itemAt(3).widget().currentText())
|
|
modificationResults.append(modificationResult)
|
|
if not modificationResults:
|
|
modificationResults = None
|
|
|
|
currentSelectStatement = self.selectStatementPicker.currentText()
|
|
if currentSelectStatement == 'SELECT':
|
|
selectedFields = []
|
|
for item in self.selectStatementList.selectedItems():
|
|
selectedFields.append(item.text())
|
|
else:
|
|
selectedFields = self.selectStatementTextbox.text()
|
|
sourceStatement = self.sourceStatementPicker.currentText()
|
|
sourceListOrNone = None if sourceStatement == 'FROMDB' else sourceResults
|
|
|
|
resultsSet, modificationsSet = self.mainWindowObject.LQLWIZARD.parseQuery(currentSelectStatement,
|
|
selectedFields, sourceStatement,
|
|
sourceListOrNone,
|
|
conditionResults,
|
|
modificationResults)
|
|
else:
|
|
try:
|
|
selectedHistoryUID = self.historyTable.selectedItems()[0].text()
|
|
resultsSet, modificationsSet = self.mainWindowObject.LQLWIZARD.parseQuery(
|
|
*self.mainWindowObject.LQLWIZARD.QUERIES_HISTORY[selectedHistoryUID])
|
|
except IndexError:
|
|
self.mainWindowObject.MESSAGEHANDLER.error('No Query selected from history.', popUp=True)
|
|
return
|
|
|
|
self.showResults(resultsSet, modificationsSet)
|
|
|
|
def showResults(self, resultsSet, modificationsSet):
|
|
if not resultsSet:
|
|
self.mainWindowObject.MESSAGEHANDLER.warning('Query returned no results.', popUp=True)
|
|
return
|
|
|
|
if modificationsSet:
|
|
numified = modificationsSet[1]
|
|
else:
|
|
numified = None
|
|
|
|
qResultsViewer = QueryResultsViewer(self.mainWindowObject, self.mainWindowObject.LQLWIZARD.allEntities,
|
|
resultsSet[0], resultsSet[1], numified)
|
|
qResultsViewer.exec()
|
|
|
|
|
|
class QueryResultsViewer(QtWidgets.QDialog):
|
|
|
|
def __init__(self, mainWindowObject: MainWindow, entitiesDict: dict, selectedUIDs: set, selectedFields: set,
|
|
numifiedFields: set):
|
|
super(QueryResultsViewer, self).__init__()
|
|
self.mainWindowObject = mainWindowObject
|
|
self.setModal(True)
|
|
self.setWindowTitle('Query Results')
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
|
|
self.resultsTabbedPane = QtWidgets.QTabWidget(self)
|
|
dialogLayout.addWidget(self.resultsTabbedPane, 0, 0, 2, 2)
|
|
|
|
self.headerFields = list(selectedFields)
|
|
try:
|
|
self.headerFields.remove('uid')
|
|
except ValueError:
|
|
pass
|
|
self.headerFields.insert(0, 'uid')
|
|
|
|
self.resultsTable = QtWidgets.QTableWidget(0, len(self.headerFields), self)
|
|
self.resultsTable.setHorizontalHeaderLabels(self.headerFields)
|
|
self.resultsTable.setAcceptDrops(False)
|
|
self.resultsTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
|
self.resultsTable.verticalHeader().setCascadingSectionResizes(True)
|
|
for index in range(1, len(self.headerFields)):
|
|
self.resultsTable.horizontalHeader().setSectionResizeMode(index, QtWidgets.QHeaderView.Stretch)
|
|
|
|
count = 0
|
|
for uid in selectedUIDs:
|
|
self.resultsTable.insertRow(count)
|
|
for index, field in enumerate(self.headerFields):
|
|
self.resultsTable.setItem(count, index, QtWidgets.QTableWidgetItem(
|
|
str(entitiesDict[uid].get(field, 'None'))))
|
|
count += 1
|
|
|
|
self.resultsTabbedPane.addTab(self.resultsTable, 'Table')
|
|
|
|
self.charts = {}
|
|
for headerField in self.headerFields[1:]:
|
|
values = {}
|
|
for entity in entitiesDict:
|
|
entityValue = str(entitiesDict[entity].get(headerField))
|
|
if entityValue not in values:
|
|
values[entityValue] = 1
|
|
else:
|
|
values[entityValue] += 1
|
|
|
|
if not values:
|
|
# Do not make charts if there are no values to make charts out of.
|
|
continue
|
|
|
|
fieldChart = QtCharts.QChart()
|
|
chartTitle = headerField + " Chart"
|
|
fieldChart.setTitle(chartTitle)
|
|
fieldChart.setTheme(QtCharts.QChart.ChartThemeBlueCerulean)
|
|
fieldChart.setMargins(QtCore.QMargins(0, 0, 0, 0))
|
|
chartView = QtCharts.QChartView(fieldChart)
|
|
chartView.setRubberBand(QtCharts.QChartView.NoRubberBand)
|
|
chartView.setRenderHint(QtGui.QPainter.Antialiasing)
|
|
fieldChart.setAnimationOptions(QtCharts.QChart.AllAnimations)
|
|
fieldChart.setAnimationDuration(250)
|
|
fieldChart.legend().setVisible(True)
|
|
fieldChart.legend().setAlignment(QtCore.Qt.AlignBottom)
|
|
|
|
self.charts[headerField] = (fieldChart, chartView)
|
|
self.resultsTabbedPane.addTab(chartView, chartTitle)
|
|
|
|
barSeries = QtCharts.QBarSeries()
|
|
barSeries.setName(headerField)
|
|
for barValue in values:
|
|
barSet = QtCharts.QBarSet(barValue)
|
|
barSet.append(values[barValue])
|
|
barSeries.append(barSet)
|
|
|
|
fieldChart.addSeries(barSeries)
|
|
|
|
xAxis = QtCharts.QBarCategoryAxis()
|
|
xAxis.append([headerField])
|
|
fieldChart.addAxis(xAxis, QtCore.Qt.AlignBottom)
|
|
barSeries.attachAxis(xAxis)
|
|
|
|
yAxis = QtCharts.QValueAxis()
|
|
yAxis.setRange(0, max(values.values()) + 1)
|
|
yAxis.applyNiceNumbers()
|
|
fieldChart.addAxis(yAxis, QtCore.Qt.AlignLeft)
|
|
barSeries.attachAxis(yAxis)
|
|
|
|
if numifiedFields:
|
|
for field in numifiedFields:
|
|
fieldValues = []
|
|
for entity in entitiesDict:
|
|
try:
|
|
entityValue = float(entitiesDict[entity].get(field))
|
|
except TypeError:
|
|
continue
|
|
fieldValues.append(entityValue)
|
|
if not fieldValues:
|
|
continue
|
|
numValues = len(fieldValues)
|
|
maxValue = max(fieldValues)
|
|
minValue = min(fieldValues)
|
|
meanValue = statistics.fmean(fieldValues)
|
|
medianValue = statistics.median(fieldValues)
|
|
modeValue = statistics.mode(fieldValues)
|
|
sumValue = sum(fieldValues)
|
|
rangeValue = maxValue - minValue
|
|
varianceValue = statistics.pvariance(fieldValues)
|
|
standardDeviationValue = statistics.pstdev(fieldValues)
|
|
|
|
numifiedFieldWidget = QtWidgets.QWidget()
|
|
numifiedFieldWidgetLayout = QtWidgets.QVBoxLayout()
|
|
numifiedFieldWidget.setLayout(numifiedFieldWidgetLayout)
|
|
fieldLabel = QtWidgets.QLabel('Numerical Information for field: ' + field)
|
|
numifiedValuesWidget = QtWidgets.QWidget()
|
|
numifiedValuesWidgetLayout = QtWidgets.QFormLayout()
|
|
numifiedValuesWidget.setLayout(numifiedValuesWidgetLayout)
|
|
|
|
numifiedValuesWidgetLayout.addRow('Number of Values: ', QtWidgets.QLabel(str(numValues)))
|
|
numifiedValuesWidgetLayout.addRow('Biggest Value: ', QtWidgets.QLabel(str(maxValue)))
|
|
numifiedValuesWidgetLayout.addRow('Smallest Value: ', QtWidgets.QLabel(str(minValue)))
|
|
numifiedValuesWidgetLayout.addRow('Mean Value: ', QtWidgets.QLabel(str(meanValue)))
|
|
numifiedValuesWidgetLayout.addRow('Median Value: ', QtWidgets.QLabel(str(medianValue)))
|
|
numifiedValuesWidgetLayout.addRow('Mode Value: ', QtWidgets.QLabel(str(modeValue)))
|
|
numifiedValuesWidgetLayout.addRow('Sum of Values: ', QtWidgets.QLabel(str(sumValue)))
|
|
numifiedValuesWidgetLayout.addRow('Range of Values: ', QtWidgets.QLabel(str(rangeValue)))
|
|
numifiedValuesWidgetLayout.addRow('Variance of Values: ', QtWidgets.QLabel(str(varianceValue)))
|
|
numifiedValuesWidgetLayout.addRow('Standard Deviation of Values: ',
|
|
QtWidgets.QLabel(str(standardDeviationValue)))
|
|
|
|
numifiedFieldWidgetLayout.addWidget(fieldLabel, 0)
|
|
numifiedFieldWidgetLayout.addWidget(numifiedValuesWidget, 1)
|
|
|
|
self.resultsTabbedPane.addTab(numifiedFieldWidget, field + ' Field Values Information')
|
|
|
|
closeButton = QtWidgets.QPushButton('Close')
|
|
closeButton.clicked.connect(self.accept)
|
|
exportButton = QtWidgets.QPushButton('Export Table')
|
|
exportButton.clicked.connect(self.exportData)
|
|
|
|
dialogLayout.addWidget(closeButton, 3, 0, 1, 1)
|
|
dialogLayout.addWidget(exportButton, 3, 1, 1, 1)
|
|
|
|
def exportData(self):
|
|
exportDialog = QtWidgets.QFileDialog()
|
|
exportDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
exportDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
exportDialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
|
|
exportDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
|
exportDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
exportDialog.setDirectory(str(Path.home()))
|
|
|
|
exportExec = exportDialog.exec()
|
|
if not exportExec:
|
|
self.mainWindowObject.setStatus('Export operation cancelled.')
|
|
return False
|
|
fileName = exportDialog.selectedFiles()[0]
|
|
exportFilePath = Path(fileName)
|
|
if not is_path_exists_or_creatable_portable(str(exportFilePath)):
|
|
self.mainWindowObject.MESSAGEHANDLER.error(
|
|
'Invalid export file name or path to save at.', popUp=True, exc_info=False)
|
|
return False
|
|
|
|
try:
|
|
with open(exportFilePath, 'w') as fileToWrite:
|
|
csvWriter = csv.writer(fileToWrite, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
|
csvWriter.writerow(self.headerFields)
|
|
for rowIndex in range(self.resultsTable.rowCount()):
|
|
currColumnValues = []
|
|
for columnIndex in range(self.resultsTable.columnCount()):
|
|
currColumnValues.append(self.resultsTable.item(rowIndex, columnIndex).text())
|
|
csvWriter.writerow(currColumnValues)
|
|
except FileNotFoundError:
|
|
self.mainWindowObject.MESSAGEHANDLER.error('Cannot write file into a non-existing parent directory. '
|
|
'Please create the required parent directories and try again.',
|
|
popUp=True, exc_info=False)
|
|
return False
|
|
|
|
self.mainWindowObject.MESSAGEHANDLER.info('Table exported successfully.', popUp=True, exc_info=False)
|
|
return True
|
|
|
|
|
|
class ConditionClauseWidget(QtWidgets.QFrame):
|
|
|
|
def __init__(self, parentWizard: QueryBuilderWizard):
|
|
super(ConditionClauseWidget, self).__init__()
|
|
clauseWidgetLayout = QtWidgets.QVBoxLayout()
|
|
self.setLayout(clauseWidgetLayout)
|
|
self.setFrameStyle(self.Panel | self.Raised)
|
|
self.setLineWidth(3)
|
|
|
|
self.andOrClause = QtWidgets.QComboBox()
|
|
self.andOrClause.addItems(['OR', 'AND'])
|
|
self.specifier = QtWidgets.QComboBox()
|
|
self.specifier.addItems(['Value Condition', 'Graph Condition'])
|
|
self.negation = QtWidgets.QComboBox()
|
|
self.negation.addItems(['MATCHES', 'DOES NOT MATCH'])
|
|
|
|
inputWidget = QtWidgets.QWidget()
|
|
inputWidgetLayout = QtWidgets.QStackedLayout()
|
|
inputWidget.setLayout(inputWidgetLayout)
|
|
|
|
vcWidget = QtWidgets.QFrame()
|
|
vcWidgetLayout = QtWidgets.QVBoxLayout()
|
|
vcWidget.setLayout(vcWidgetLayout)
|
|
|
|
valueInputDropdownAttr = QtWidgets.QComboBox()
|
|
valueInputDropdownAttr.addItems(["ATTRIBUTE", "RATTRIBUTE"])
|
|
userInputDropdownAttr = QtWidgets.QLineEdit('')
|
|
userInputDropdownAttr.setFixedHeight(26)
|
|
valueInputDropdownCondition = QtWidgets.QComboBox()
|
|
valueInputDropdownCondition.addItems(["EQ", "CONTAINS", "STARTSWITH", "ENDSWITH", "RMATCH"])
|
|
userInputDropdownCondition = QtWidgets.QLineEdit('')
|
|
userInputDropdownCondition.setFixedHeight(26)
|
|
|
|
vcWidgetLayout.addWidget(valueInputDropdownAttr)
|
|
vcWidgetLayout.addWidget(userInputDropdownAttr)
|
|
vcWidgetLayout.addWidget(valueInputDropdownCondition)
|
|
vcWidgetLayout.addWidget(userInputDropdownCondition)
|
|
|
|
gcWidget = QtWidgets.QFrame()
|
|
gcWidgetLayout = QtWidgets.QVBoxLayout()
|
|
gcWidget.setLayout(gcWidgetLayout)
|
|
|
|
graphDropdownCondition = QtWidgets.QComboBox()
|
|
graphDropdownCondition.addItems(['CHILDOF', 'DESCENDANTOF', 'PARENTOF', 'ANCESTOROF', 'CONNECTEDTO',
|
|
'NUMCHILDREN', 'NUMPARENTS', 'ISOLATED', 'ISROOT', 'ISLEAF'])
|
|
gcWidgetLayout.addWidget(graphDropdownCondition)
|
|
|
|
gcSecondaryInput = QtWidgets.QWidget()
|
|
self.gcSecondaryInputLayout = QtWidgets.QStackedLayout()
|
|
gcSecondaryInput.setLayout(self.gcSecondaryInputLayout)
|
|
|
|
graphEntitiesDropdown = QtWidgets.QTreeWidget()
|
|
graphEntitiesDropdown.setHeaderLabels(['Primary Field', 'UID', 'Icon'])
|
|
graphEntitiesDropdown.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
|
for entityDropdownTriplet in parentWizard.entityDropdownTriplets:
|
|
newItem = QtWidgets.QTreeWidgetItem()
|
|
newItem.setText(0, entityDropdownTriplet[0])
|
|
newItem.setText(1, entityDropdownTriplet[1])
|
|
newItem.setIcon(2, entityDropdownTriplet[2])
|
|
graphEntitiesDropdown.addTopLevelItem(newItem)
|
|
|
|
self.gcSecondaryInputLayout.addWidget(graphEntitiesDropdown)
|
|
|
|
graphNumComparisonsWidget = QtWidgets.QWidget()
|
|
graphNumComparisonsWidgetLayout = QtWidgets.QHBoxLayout()
|
|
graphNumComparisonsWidget.setLayout(graphNumComparisonsWidgetLayout)
|
|
|
|
graphNumComparisonDropdown = QtWidgets.QComboBox()
|
|
graphNumComparisonDropdown.addItems(['<', '<=', '>', '>=', '=='])
|
|
graphNumInput = QtWidgets.QSpinBox()
|
|
graphNumInput.setMinimum(0)
|
|
graphNumInput.setMaximum(1000000) # Can be adjusted higher if need be.
|
|
graphNumInput.setValue(0)
|
|
graphNumComparisonsWidgetLayout.addWidget(graphNumComparisonDropdown)
|
|
graphNumComparisonsWidgetLayout.addWidget(graphNumInput)
|
|
|
|
self.gcSecondaryInputLayout.addWidget(graphNumComparisonsWidget)
|
|
|
|
emptyWidget = QtWidgets.QWidget()
|
|
self.gcSecondaryInputLayout.addWidget(emptyWidget)
|
|
|
|
gcWidgetLayout.addWidget(gcSecondaryInput)
|
|
|
|
inputWidgetLayout.addWidget(vcWidget)
|
|
inputWidgetLayout.addWidget(gcWidget)
|
|
self.specifier.currentIndexChanged.connect(lambda newIndex: inputWidgetLayout.setCurrentIndex(newIndex))
|
|
graphDropdownCondition.currentIndexChanged.connect(self.determineSecondaryInput)
|
|
|
|
clauseWidgetLayout.addWidget(self.andOrClause)
|
|
clauseWidgetLayout.addWidget(self.specifier)
|
|
clauseWidgetLayout.addWidget(self.negation)
|
|
clauseWidgetLayout.addWidget(inputWidget)
|
|
|
|
def determineSecondaryInput(self, conditionIndex: int):
|
|
if conditionIndex < 5:
|
|
self.gcSecondaryInputLayout.setCurrentIndex(0)
|
|
elif conditionIndex < 7:
|
|
self.gcSecondaryInputLayout.setCurrentIndex(1)
|
|
else:
|
|
self.gcSecondaryInputLayout.setCurrentIndex(2)
|
|
|
|
def getValue(self):
|
|
specifierValue = self.specifier.currentText()
|
|
returnValues = [self.andOrClause.currentText(),
|
|
specifierValue]
|
|
if self.negation.currentText() == 'MATCHES':
|
|
returnValues.append(False)
|
|
else:
|
|
returnValues.append(True)
|
|
conditionValue = []
|
|
|
|
if specifierValue == 'Value Condition':
|
|
conditionValue.append(
|
|
self.layout().itemAt(3).widget().layout().itemAt(0).widget().layout().itemAt(0).widget().currentText())
|
|
conditionValue.append(
|
|
self.layout().itemAt(3).widget().layout().itemAt(0).widget().layout().itemAt(1).widget().text())
|
|
conditionValue.append(
|
|
self.layout().itemAt(3).widget().layout().itemAt(0).widget().layout().itemAt(2).widget().currentText())
|
|
conditionValue.append(
|
|
self.layout().itemAt(3).widget().layout().itemAt(0).widget().layout().itemAt(3).widget().text())
|
|
else:
|
|
conditionValue.append(
|
|
self.layout().itemAt(3).widget().layout().itemAt(1).widget().layout().itemAt(0).widget().currentText())
|
|
if self.gcSecondaryInputLayout.currentIndex() == 0:
|
|
try:
|
|
conditionValue.append(self.gcSecondaryInputLayout.itemAt(0).widget().selectedItems()[0].text(1))
|
|
except IndexError:
|
|
return None
|
|
elif self.gcSecondaryInputLayout.currentIndex() == 1:
|
|
conditionValue.append(
|
|
self.gcSecondaryInputLayout.itemAt(1).widget().layout().itemAt(0).widget().currentText())
|
|
conditionValue.append(
|
|
self.gcSecondaryInputLayout.itemAt(1).widget().layout().itemAt(1).widget().value())
|
|
|
|
returnValues.append(conditionValue)
|
|
|
|
return returnValues
|
|
|
|
|
|
class ExtractCyclesThread(QtCore.QThread):
|
|
cyclesSignal = QtCore.Signal(list, str)
|
|
|
|
def __init__(self, tempGraph: nx.DiGraph, nodesList: list, canvasName: str):
|
|
super().__init__()
|
|
self.tempGraph = tempGraph
|
|
self.nodesList = nodesList
|
|
self.canvasName = canvasName
|
|
|
|
def run(self) -> None:
|
|
allCycles = list(nx.simple_cycles(self.tempGraph))
|
|
if not allCycles:
|
|
self.cyclesSignal.emit([], self.canvasName)
|
|
|
|
if self.nodesList:
|
|
startNode = self.nodesList[0]
|
|
else:
|
|
mostCommonDict = {}
|
|
for cycle in allCycles:
|
|
for node in cycle:
|
|
mostCommonDict[node] = mostCommonDict.get(node, 0) + 1
|
|
startNode = max(mostCommonDict, key=mostCommonDict.get)
|
|
|
|
reorderedCycles = []
|
|
for cycle in allCycles:
|
|
try:
|
|
reorderedCycles.append(cycle[cycle.index(startNode):] + cycle[:cycle.index(startNode)])
|
|
except ValueError:
|
|
reorderedCycles.append(cycle)
|
|
|
|
groups = []
|
|
allElements = set()
|
|
skipGrouping = True
|
|
for cycle in list(itertools.zip_longest(*reorderedCycles)):
|
|
cycleSet = set()
|
|
for cycleElement in cycle:
|
|
if cycleElement is not None:
|
|
cycleSet.add(cycleElement)
|
|
allElements.add(cycleElement)
|
|
if len(cycleSet) > 1 and not skipGrouping:
|
|
groups.append(cycleSet)
|
|
elif skipGrouping:
|
|
skipGrouping = False
|
|
|
|
self.cyclesSignal.emit([allElements, groups], self.canvasName)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Create a graphical application
|
|
application = QtWidgets.QApplication(sys.argv)
|
|
mainWindow = MainWindow()
|
|
sys.exit(application.exec())
|