1340 lines
69 KiB
Python
1340 lines
69 KiB
Python
#!/usr/bin/env python3
|
|
|
|
|
|
import hashlib
|
|
import re
|
|
import json
|
|
import platform
|
|
import sqlite3
|
|
import tempfile
|
|
import shutil
|
|
import os
|
|
import time
|
|
|
|
import magic
|
|
import lz4.block
|
|
import pandas as pd
|
|
import webbrowser
|
|
from typing import Union
|
|
from urllib import parse
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
from playwright.sync_api import sync_playwright, Error, TimeoutError
|
|
|
|
from PySide6 import QtWidgets, QtGui, QtCore
|
|
from Core.Interface import Stylesheets
|
|
from Core.Interface.Entity import BaseNode
|
|
|
|
|
|
class MenuBar(QtWidgets.QMenuBar):
|
|
|
|
def __init__(self, parent):
|
|
super().__init__(parent=parent)
|
|
|
|
fileMenu = self.addMenu("File")
|
|
fileMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
|
|
|
saveAction = QtGui.QAction("&Save",
|
|
self,
|
|
statusTip="Save The Project",
|
|
triggered=self.save)
|
|
saveAction.setShortcut("Ctrl+S")
|
|
fileMenu.addAction(saveAction)
|
|
|
|
saveAsAction = QtGui.QAction("Save As",
|
|
self,
|
|
statusTip="Save the Project in a new directory, under a new name.",
|
|
triggered=self.saveAs)
|
|
fileMenu.addAction(saveAsAction)
|
|
|
|
renameAction = QtGui.QAction("Rename Project",
|
|
self,
|
|
statusTip="Rename the Project.",
|
|
triggered=self.rename)
|
|
fileMenu.addAction(renameAction)
|
|
|
|
importMenu = self.addMenu("Import")
|
|
importMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
|
|
|
fromBrowserAction = QtGui.QAction("From Browser",
|
|
self,
|
|
statusTip="Import open tabs as Website and materials entities.",
|
|
triggered=self.importFromBrowser)
|
|
|
|
fromFileAction = QtGui.QAction("From File",
|
|
self,
|
|
statusTip="Import entities from a file.",
|
|
triggered=self.importFromFile)
|
|
|
|
graphMLCanvasAction = QtGui.QAction("GraphML - Canvas",
|
|
self,
|
|
statusTip="Import canvas from a GraphML file.",
|
|
triggered=self.parent().importCanvasFromGraphML)
|
|
|
|
graphMLDatabaseAction = QtGui.QAction("GraphML - Database",
|
|
self,
|
|
statusTip="Import database from a GraphML file.",
|
|
triggered=self.parent().importDatabaseFromGraphML)
|
|
importMenu.addAction(fromBrowserAction)
|
|
importMenu.addAction(fromFileAction)
|
|
importMenu.addAction(graphMLCanvasAction)
|
|
importMenu.addAction(graphMLDatabaseAction)
|
|
|
|
exportMenu = self.addMenu("Export")
|
|
exportMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
|
|
|
canvasPictureAction = QtGui.QAction("Save Picture of Canvas", self,
|
|
statusTip="Save a picture of your canvas",
|
|
triggered=self.savePic)
|
|
GraphMLCanvas = QtGui.QAction('GraphML - Canvas ',
|
|
self,
|
|
statusTip="Export Canvas to GraphML",
|
|
triggered=self.parent().exportCanvasToGraphML)
|
|
|
|
GraphMLDatabase = QtGui.QAction('GraphML - Database ',
|
|
self,
|
|
statusTip="Export Database to GraphML",
|
|
triggered=self.parent().exportDatabaseToGraphML)
|
|
exportMenu.addAction(canvasPictureAction)
|
|
exportMenu.addAction(GraphMLCanvas)
|
|
exportMenu.addAction(GraphMLDatabase)
|
|
|
|
editSettingsMenu = fileMenu.addMenu("Edit Settings")
|
|
editLog = QtGui.QAction('Logging Settings',
|
|
self,
|
|
statusTip="Edit Logging Settings",
|
|
triggered=self.editLogSettings)
|
|
|
|
editProject = QtGui.QAction('Project Settings',
|
|
self,
|
|
statusTip="Edit Project Settings",
|
|
triggered=self.editProjectSettings)
|
|
|
|
editResolution = QtGui.QAction('Resolutions Settings',
|
|
self,
|
|
statusTip="Edit Resolutions Settings",
|
|
triggered=self.editResolutionsSettings)
|
|
|
|
editSettingsMenu.addAction(editLog)
|
|
editSettingsMenu.addAction(editResolution)
|
|
editSettingsMenu.addAction(editProject)
|
|
|
|
exitAction = QtGui.QAction("Exit",
|
|
self,
|
|
statusTip="Save, Close Project and Exit",
|
|
triggered=self.exitSoftware)
|
|
fileMenu.addAction(exitAction)
|
|
|
|
viewMenu = self.addMenu("View")
|
|
viewMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
|
|
|
saveAction = QtGui.QAction("&Find",
|
|
self,
|
|
statusTip="Find Links or Entities",
|
|
triggered=self.findEntityOrLink)
|
|
saveAction.setShortcut("Ctrl+F")
|
|
viewMenu.addAction(saveAction)
|
|
|
|
runningResolutiosAction = QtGui.QAction("&Running Resolutions",
|
|
self,
|
|
statusTip="View Running Resolutions",
|
|
triggered=self.runningResolutions)
|
|
runningResolutiosAction.setShortcut("Ctrl+R")
|
|
viewMenu.addAction(runningResolutiosAction)
|
|
|
|
dockbarVisibilityMenu = viewMenu.addMenu("Toggle Dockbar Visibility")
|
|
|
|
self.dockbarOneVisibilityAction = QtGui.QAction("Toggle Dockbar One",
|
|
self,
|
|
statusTip="Toggle the visibility of Dockbar One",
|
|
triggered=self.toggleDockbarOneVisibility)
|
|
dockbarVisibilityMenu.addAction(self.dockbarOneVisibilityAction)
|
|
|
|
self.dockbarTwoVisibilityAction = QtGui.QAction("Toggle Dockbar Two",
|
|
self,
|
|
statusTip="Toggle the visibility of Dockbar Two",
|
|
triggered=self.toggleDockbarTwoVisibility)
|
|
dockbarVisibilityMenu.addAction(self.dockbarTwoVisibilityAction)
|
|
|
|
self.dockbarThreeVisibilityAction = QtGui.QAction("Toggle Dockbar Three",
|
|
self,
|
|
statusTip="Toggle the visibility of Dockbar Three",
|
|
triggered=self.toggleDockbarThreeVisibility)
|
|
dockbarVisibilityMenu.addAction(self.dockbarThreeVisibilityAction)
|
|
|
|
toolbarVisibilityMenu = viewMenu.addMenu("Toggle Toolbar Visibility")
|
|
self.primaryToolbarVisibilityAction = QtGui.QAction("Toggle Primary Toolbar",
|
|
self,
|
|
statusTip="Toggle the visibility of the Primary Toolbar",
|
|
triggered=self.togglePrimaryToolbarVisibility)
|
|
toolbarVisibilityMenu.addAction(self.primaryToolbarVisibilityAction)
|
|
|
|
modulesMenu = self.addMenu("Modules")
|
|
modulesMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
|
|
|
reloadModulesAction = QtGui.QAction("Reload Modules", self,
|
|
statusTip="Reload all Entities and Transforms from Modules",
|
|
triggered=self.reloadModules)
|
|
modulesMenu.addAction(reloadModulesAction)
|
|
|
|
serverMenu = self.addMenu("&Server")
|
|
serverMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
|
|
|
connectAction = QtGui.QAction("Connect", self,
|
|
statusTip="Connect to a Server",
|
|
triggered=self.serverConnectionWizard)
|
|
serverMenu.addAction(connectAction)
|
|
|
|
disconnectAction = QtGui.QAction("Disconnect", self,
|
|
statusTip="Disconnect from any connected server",
|
|
triggered=self.disconnectFromServer)
|
|
serverMenu.addAction(disconnectAction)
|
|
|
|
serverMenu.addSeparator()
|
|
|
|
createOrOpenProject = QtGui.QAction("Create or Open Project", self,
|
|
statusTip="Create or Open a new Server project.",
|
|
triggered=self.serverOpenOrCreateProject)
|
|
serverMenu.addAction(createOrOpenProject)
|
|
|
|
serverMenu.addSeparator()
|
|
|
|
syncCanvasAction = QtGui.QAction("Sync Current Canvas", self,
|
|
statusTip="Sync the selected canvas with the server's version, or create "
|
|
"it on the server if it does not exist.",
|
|
triggered=self.syncCurrentCanvas)
|
|
serverMenu.addAction(syncCanvasAction)
|
|
|
|
unSyncCanvasAction = QtGui.QAction("UnSync Current Canvas", self,
|
|
statusTip="UnSync the selected canvas from the server's version.",
|
|
triggered=self.unSyncCurrentCanvas)
|
|
serverMenu.addAction(unSyncCanvasAction)
|
|
|
|
serverMenu.addSeparator()
|
|
|
|
reSyncDatabase = QtGui.QAction("Force Re-Sync Database", self,
|
|
statusTip="Re-Sync the project's local database with the server's version.",
|
|
triggered=self.forceDatabaseSync)
|
|
serverMenu.addAction(reSyncDatabase)
|
|
|
|
reloadServerResolutionsAction = QtGui.QAction("Reload Server Resolutions", self,
|
|
statusTip="Reload resolutions from server",
|
|
triggered=self.reloadServerResolutions)
|
|
serverMenu.addAction(reloadServerResolutionsAction)
|
|
|
|
reloadServerProjectsAction = QtGui.QAction("Reload Server Projects List", self,
|
|
statusTip="Reload the list of projects made available "
|
|
"by the server. WARNING: Will disconnect from "
|
|
"current project, if any.",
|
|
triggered=self.reloadServerProjectsList)
|
|
serverMenu.addAction(reloadServerProjectsAction)
|
|
|
|
reloadServerProjectCanvasesAction = QtGui.QAction("Reload Server Project Canvases List", self,
|
|
statusTip="Reload the list of canvases for the current "
|
|
"server project. WARNING: Will unsync "
|
|
"currently synced canvases, if any.",
|
|
triggered=self.reloadServerProjectCanvasesList)
|
|
serverMenu.addAction(reloadServerProjectCanvasesAction)
|
|
|
|
reloadServerProjectFilesAction = QtGui.QAction("Reload Server Project Files List", self,
|
|
statusTip="Reload the list of files for the current "
|
|
"server project",
|
|
triggered=self.reloadServerProjectFilesList)
|
|
serverMenu.addAction(reloadServerProjectFilesAction)
|
|
|
|
serverMenu.addSeparator()
|
|
|
|
uploadFilesAction = QtGui.QAction("Upload Selected Files", self,
|
|
statusTip="Upload selected Materials entity files to server",
|
|
triggered=self.uploadFiles)
|
|
serverMenu.addAction(uploadFilesAction)
|
|
|
|
downloadFileAction = QtGui.QAction("Download Files", self,
|
|
statusTip="Download specified files from server",
|
|
triggered=self.downloadFile)
|
|
serverMenu.addAction(downloadFileAction)
|
|
|
|
# Use this function to change the labels of functions when necessary.
|
|
# Not very efficient, but much more intuitive than creating a ton of different menus
|
|
# and mashing them together.
|
|
def mousePressEvent(self, arg__1: QtGui.QMouseEvent) -> None:
|
|
super(MenuBar, self).mousePressEvent(arg__1)
|
|
self.dockbarOneVisibilityAction.setText("Hide Dockbar One" if self.parent().dockbarOne.isVisible() else
|
|
"Show Dockbar One")
|
|
self.dockbarTwoVisibilityAction.setText("Hide Dockbar Two" if self.parent().dockbarTwo.isVisible() else
|
|
"Show Dockbar Two")
|
|
self.dockbarThreeVisibilityAction.setText("Hide Dockbar Three" if self.parent().dockbarThree.isVisible() else
|
|
"Show Dockbar Three")
|
|
self.primaryToolbarVisibilityAction.setText("Hide Primary Toolbar"
|
|
if self.parent().primaryToolbar.isVisible() else
|
|
"Show Primary Toolbar")
|
|
|
|
def toggleDockbarOneVisibility(self) -> None:
|
|
if self.parent().dockbarOne.isVisible():
|
|
self.parent().dockbarOne.setVisible(False)
|
|
else:
|
|
self.parent().dockbarOne.setVisible(True)
|
|
|
|
def toggleDockbarTwoVisibility(self) -> None:
|
|
if self.parent().dockbarTwo.isVisible():
|
|
self.parent().dockbarTwo.setVisible(False)
|
|
else:
|
|
self.parent().dockbarTwo.setVisible(True)
|
|
|
|
def toggleDockbarThreeVisibility(self) -> None:
|
|
if self.parent().dockbarThree.isVisible():
|
|
self.parent().dockbarThree.setVisible(False)
|
|
else:
|
|
self.parent().dockbarThree.setVisible(True)
|
|
|
|
def togglePrimaryToolbarVisibility(self) -> None:
|
|
if self.parent().primaryToolbar.isVisible():
|
|
self.parent().primaryToolbar.setVisible(False)
|
|
else:
|
|
self.parent().primaryToolbar.setVisible(True)
|
|
|
|
def importFromFile(self) -> None:
|
|
importDialog = ImportFromFileDialog(self)
|
|
importDialogAccept = importDialog.exec_()
|
|
return_results = []
|
|
fileDirectory = importDialog.fileDirectory
|
|
if importDialogAccept and fileDirectory != '':
|
|
sceneToAddTo = None
|
|
if importDialog.importToCanvasCheckbox.isChecked():
|
|
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
|
|
importDialog.importToCanvasDropdown.currentText())
|
|
if importDialog.CSVFileChoice.isChecked():
|
|
try:
|
|
csvContents = pd.read_csv(fileDirectory, usecols=['EntityType', 'EntityField', 'EntityValue'])
|
|
for entity in range(len(csvContents['EntityType'])):
|
|
if csvContents['EntityType'][entity] in self.parent().RESOURCEHANDLER.getAllEntities():
|
|
if csvContents['EntityField'][entity] in list(
|
|
self.parent().RESOURCEHANDLER.getEntityJson(csvContents['EntityType'][entity])):
|
|
return_results.append(
|
|
[{csvContents['EntityField'][entity]: str(csvContents['EntityValue'][entity]),
|
|
'Entity Type': csvContents['EntityType'][entity]}])
|
|
else:
|
|
self.parent().MESSAGEHANDLER.warning(
|
|
'Please Check that the format of the csv provided is as described by '
|
|
f'the documentation. Malformed entity detected at line {entity + 1}. The '
|
|
f'field of the entity is Invalid', popUp=True)
|
|
else:
|
|
self.parent().MESSAGEHANDLER.warning(
|
|
"Please Check that the format of the csv provided is as described by "
|
|
f"the documentation. Malformed entity detected at line {entity + 1}. The "
|
|
f"entity type is Invalid", popUp=True)
|
|
except ValueError:
|
|
self.parent().MESSAGEHANDLER.warning("Please check that the format of the csv provided is as "
|
|
"described by the documentation. It should have 3 columns in "
|
|
"order of EntityType, EntityField, EntityValue. All the "
|
|
"'Values' should be filled accordingly", popUp=True)
|
|
return_results = []
|
|
elif importDialog.textFileChoice.isChecked():
|
|
importDialog = ImportFromTextFileDialog(self)
|
|
if importDialog.exec_():
|
|
txtFileContents = open(fileDirectory, 'r')
|
|
selectedEntityType = importDialog.importTypeDropdown.currentText()
|
|
jsonOfField = self.parent().RESOURCEHANDLER.getEntityJson(selectedEntityType)
|
|
primary_field = list(jsonOfField)[1]
|
|
for line in txtFileContents.readlines():
|
|
if re.match(r'\w', line):
|
|
return_results.append(
|
|
[{primary_field: line,
|
|
'Entity Type': selectedEntityType}])
|
|
newNodeUIDs = []
|
|
newLinks = []
|
|
for newNode in return_results:
|
|
newNodeJson = self.parent().LENTDB.addEntity(newNode[0])
|
|
if newNodeJson is not None:
|
|
newNodeUIDs.append(newNodeJson['uid'])
|
|
else:
|
|
newNodeUIDs.append(None)
|
|
|
|
self.parent().centralWidget().tabbedPane.linkAddHelper(newLinks)
|
|
|
|
if sceneToAddTo is not None:
|
|
for newNodeUID in newNodeUIDs:
|
|
if newNodeUID is not None:
|
|
sceneToAddTo.addNodeProgrammatic(newNodeUID)
|
|
sceneToAddTo.rearrangeGraph()
|
|
|
|
def savePic(self) -> None:
|
|
canvasSaveDialog = CanvasPictureDialog(self)
|
|
canvasSaveDialogAccept = canvasSaveDialog.exec_()
|
|
fileDirectory = canvasSaveDialog.fileDirectory
|
|
|
|
if canvasSaveDialogAccept and fileDirectory != '':
|
|
canvas = canvasSaveDialog.chosenCanvasDropdown.currentText()
|
|
transparentBackground = False
|
|
justViewport = True
|
|
if canvasSaveDialog.transparentChoice.isChecked():
|
|
transparentBackground = True
|
|
picture = self.parent().getPictureOfCanvas(canvas, justViewport, transparentBackground)
|
|
picture.save(fileDirectory, "PNG")
|
|
|
|
def save(self) -> None:
|
|
self.parent().saveProject()
|
|
|
|
def saveAs(self) -> None:
|
|
self.parent().saveAsProject()
|
|
|
|
def rename(self) -> None:
|
|
self.parent().renameProjectPromptName()
|
|
|
|
def editSettings(self) -> None:
|
|
self.parent().editSettings()
|
|
|
|
def editLogSettings(self) -> None:
|
|
self.parent().editLogSettings()
|
|
|
|
def editProjectSettings(self) -> None:
|
|
self.parent().editProjectSettings()
|
|
|
|
def editResolutionsSettings(self) -> None:
|
|
self.parent().editResolutionsSettings()
|
|
|
|
def exitSoftware(self) -> None:
|
|
self.parent().close()
|
|
|
|
def reloadModules(self) -> None:
|
|
self.parent().reloadModules()
|
|
|
|
def runningResolutions(self) -> None:
|
|
self.parent().cleanUpLocalFinishedResolutions()
|
|
runningResolutionsDialog = ViewAndStopResolutionsDialog(self.parent())
|
|
runningResolutionsDialog.exec()
|
|
|
|
# Server Operations
|
|
def serverConnectionWizard(self) -> None:
|
|
serverWizard = ServerConnectWizard(self)
|
|
serverWizard.exec_()
|
|
|
|
serverPassword = serverWizard.serverPasswordTextbox.text()
|
|
serverIP = serverWizard.serverIPTextbox.text()
|
|
serverPort = int(serverWizard.serverPortTextbox.text())
|
|
|
|
if not serverWizard.confirmConnect:
|
|
self.parent().MESSAGEHANDLER.debug("Cancelled connecting to server")
|
|
return
|
|
|
|
if isinstance(serverPassword, str) and \
|
|
isinstance(serverIP, str) and \
|
|
isinstance(serverPort, int) and \
|
|
0 < serverPort < 65535:
|
|
self.parent().connectToServer(serverPassword, serverIP, serverPort)
|
|
else:
|
|
self.parent().MESSAGEHANDLER.info("Invalid Information Provided",
|
|
popUp=True)
|
|
|
|
def disconnectFromServer(self) -> None:
|
|
self.parent().disconnectFromServer()
|
|
|
|
def serverOpenOrCreateProject(self) -> None:
|
|
if self.parent().FCOM.isConnected():
|
|
serverProjectDialog = ServerCreateOrOpenProject(self.parent())
|
|
createOrOpenProject = serverProjectDialog.exec()
|
|
|
|
if createOrOpenProject:
|
|
if serverProjectDialog.openProject:
|
|
self.parent().FCOM.openProject(serverProjectDialog.projectName,
|
|
serverProjectDialog.projectPass)
|
|
else:
|
|
self.parent().FCOM.createProject(serverProjectDialog.projectName,
|
|
serverProjectDialog.projectPass)
|
|
else:
|
|
self.parent().MESSAGEHANDLER.warning('Not Connected to Server!', popUp=True)
|
|
self.parent().setStatus('Must connect to a server before opening a project.')
|
|
|
|
# For canvas stuff, check if canvas name exists.
|
|
def syncCurrentCanvas(self) -> None:
|
|
self.parent().syncCanvasByName()
|
|
|
|
def unSyncCurrentCanvas(self) -> None:
|
|
self.parent().unSyncCurrentCanvas()
|
|
|
|
def reloadServerResolutions(self) -> None:
|
|
if self.parent().FCOM.isConnected():
|
|
self.parent().RESOLUTIONMANAGER.removeServerResolutions()
|
|
self.parent().dockbarOne.resolutionsPalette.loadAllResolutions()
|
|
self.parent().FCOM.askServerForResolutions()
|
|
|
|
def reloadServerProjectsList(self) -> None:
|
|
if self.parent().FCOM.isConnected():
|
|
if self.parent().SETTINGS.value("Project/Server/Project") != '':
|
|
self.parent().closeCurrentServerProject()
|
|
self.parent().receiveProjectsListListener([])
|
|
self.parent().FCOM.askProjectsList()
|
|
|
|
def reloadServerProjectCanvasesList(self) -> None:
|
|
if self.parent().FCOM.isConnected():
|
|
project_name = self.parent().SETTINGS.value("Project/Server/Project")
|
|
self.parent().unSyncCanvasByName(None)
|
|
self.parent().receiveProjectCanvasesListListener([])
|
|
self.parent().FCOM.askProjectCanvasesList(project_name)
|
|
|
|
def reloadServerProjectFilesList(self):
|
|
if self.parent().FCOM.isConnected():
|
|
project_name = self.parent().SETTINGS.value("Project/Server/Project")
|
|
self.parent().receiveFileListListener([])
|
|
self.parent().FCOM.askServerForFileList(project_name)
|
|
|
|
def forceDatabaseSync(self) -> None:
|
|
if self.parent().FCOM.isConnected():
|
|
project_name = self.parent().SETTINGS.value("Project/Server/Project")
|
|
with self.parent().LENTDB.dbLock:
|
|
self.parent().FCOM.syncDatabase(project_name, self.parent().LENTDB.database)
|
|
|
|
def uploadFiles(self) -> None:
|
|
self.parent().uploadFiles()
|
|
|
|
def downloadFile(self):
|
|
pass
|
|
|
|
def findEntityOrLink(self) -> None:
|
|
self.parent().findEntityOrLinkOnCanvas()
|
|
|
|
def openWebsite(self) -> None:
|
|
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
|
|
websites = [item.uid for item in currentScene.selectedItems()
|
|
if isinstance(item, BaseNode) and
|
|
self.parent().LENTDB.getEntity(item.uid)['Entity Type'] == 'Website']
|
|
for website in websites:
|
|
webbrowser.open(website[list(website)[1]], new=0, autoraise=True)
|
|
|
|
def importFromBrowser(self) -> None:
|
|
"""
|
|
Import session tabs to canvas. Optionally, also take a screenshot of them.
|
|
Assumes default browser profiles.
|
|
|
|
:return:
|
|
"""
|
|
if platform.system() != 'Linux' and platform.system() != 'Windows':
|
|
self.parent().setStatus('Importing tabs not supported on platforms other than Linux and Windows.')
|
|
self.parent().MESSAGEHANDLER.warning('Importing tabs not supported on platforms '
|
|
'other than Linux and Windows.', popUp=True)
|
|
return
|
|
|
|
importDialog = BrowserImportDialog(self)
|
|
|
|
if importDialog.exec_():
|
|
|
|
steps = 4
|
|
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
|
|
'Abort Import', 0, steps, self)
|
|
progress.setWindowModality(QtCore.Qt.WindowModal)
|
|
progress.setMinimumDuration(0)
|
|
|
|
returnResults = []
|
|
|
|
progress.setValue(1)
|
|
with sync_playwright() as p:
|
|
if importDialog.firefoxChoice.isChecked():
|
|
try:
|
|
browser = p.firefox.launch()
|
|
|
|
if platform.system() == 'Linux':
|
|
context = browser.new_context(
|
|
viewport={'width': 1920, 'height': 1080},
|
|
user_agent='Mozilla/5.0 (X11; Linux i686; rv:94.0) Gecko/20100101 Firefox/94.0'
|
|
)
|
|
urlPath = Path.home() / '.mozilla' / 'firefox'
|
|
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
|
|
context = browser.new_context(
|
|
viewport={'width': 1920, 'height': 1080},
|
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 '
|
|
'Firefox/94.0'
|
|
)
|
|
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
|
|
|
|
tabsFilePath = list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4'))
|
|
if len(tabsFilePath) == 0:
|
|
self.parent().MESSAGEHANDLER.warning('No Firefox session detected. Skipping importing from'
|
|
' Firefox.', popUp=True)
|
|
else:
|
|
tabsFilePath = tabsFilePath[0]
|
|
tabsToOpen = set()
|
|
|
|
tabsBytes = tabsFilePath.read_bytes()
|
|
if tabsBytes[:8] == b'mozLz40\0':
|
|
tabsBytes = lz4.block.decompress(tabsBytes[8:])
|
|
tabsJson = json.loads(tabsBytes)
|
|
for browserWindow in tabsJson['windows']:
|
|
for browserTab in browserWindow['tabs']:
|
|
if importDialog.firefoxSessionChoice.isChecked():
|
|
for browserEntry in browserTab['entries']:
|
|
url = browserEntry['url']
|
|
if not url.startswith('about:'):
|
|
tabsToOpen.add((url, browserEntry['title']))
|
|
else:
|
|
browserEntry = browserTab['entries'][-1]
|
|
url = browserEntry['url']
|
|
if not url.startswith('about:'):
|
|
tabsToOpen.add((url, browserEntry['title']))
|
|
|
|
cookiesDatabasePath = tabsFilePath.parent.parent / 'cookies.sqlite'
|
|
newCookiesDatabase = tempfile.mkstemp(suffix='.sqlite')
|
|
newCookiesDatabasePath = Path(newCookiesDatabase[1])
|
|
|
|
# Try to copy the database a few times, so we can access it
|
|
# with sqlite (original is locked)
|
|
copiedFile = False
|
|
for _ in range(5):
|
|
shutil.copyfile(cookiesDatabasePath, newCookiesDatabasePath)
|
|
originalDigest = self.cookieFileHashHelper(cookiesDatabasePath)
|
|
newDigest = self.cookieFileHashHelper(newCookiesDatabasePath)
|
|
if originalDigest == newDigest:
|
|
copiedFile = True
|
|
break
|
|
else:
|
|
time.sleep(0.2)
|
|
|
|
browserCookies = []
|
|
if not copiedFile:
|
|
self.parent().MESSAGEHANDLER.warning('Could not access Firefox cookies.', popUp=True)
|
|
else:
|
|
cookiesDB = sqlite3.connect(newCookiesDatabasePath)
|
|
for cookie in cookiesDB.execute('SELECT name,value,host,path,expiry,isSecure,'
|
|
'isHttpOnly,sameSite FROM moz_cookies'):
|
|
newCookie = {'name': cookie[0], 'value': cookie[1],
|
|
'domain': cookie[2], 'path': cookie[3],
|
|
'expires': cookie[4], 'secure': bool(cookie[5]),
|
|
'httpOnly': bool(cookie[6])}
|
|
if cookie[7] == 0:
|
|
newCookie['sameSite'] = "None"
|
|
elif cookie[7] == 1:
|
|
newCookie['sameSite'] = "Lax"
|
|
elif cookie[7] == 2:
|
|
newCookie['sameSite'] = "Strict"
|
|
browserCookies.append(newCookie)
|
|
cookiesDB.close()
|
|
newCookiesDatabasePath.unlink(missing_ok=True)
|
|
context.add_cookies(browserCookies)
|
|
page = context.new_page()
|
|
|
|
self.parent().MESSAGEHANDLER.debug('Tabs to open: ' + str(tabsToOpen))
|
|
projectFilesDir = Path(self.parent().SETTINGS.value("Project/FilesDir"))
|
|
|
|
for tabToOpen in tabsToOpen:
|
|
urlTitle = tabToOpen[1]
|
|
actualURL = tabToOpen[0]
|
|
decodedPath = parse.unquote(actualURL)
|
|
parsedURL = parse.urlparse(decodedPath)
|
|
urlPath = parsedURL.path
|
|
|
|
if parsedURL.scheme == 'file':
|
|
try:
|
|
mime = magic.Magic(mime=True)
|
|
pathType = mime.from_file(urlPath)
|
|
except FileNotFoundError:
|
|
continue
|
|
|
|
if 'application' in pathType:
|
|
returnResults.append([{'Document Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Document'}])
|
|
elif 'image' in pathType:
|
|
returnResults.append([{'Image Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Image'}])
|
|
elif 'video' in pathType:
|
|
returnResults.append([{'Video Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Video'}])
|
|
elif 'archive' in pathType:
|
|
returnResults.append([{'Archive Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Archive'}])
|
|
|
|
elif parsedURL.scheme.startswith('http'):
|
|
returnResults.append([{'URL': actualURL,
|
|
'Entity Type': 'Website'}])
|
|
|
|
if importDialog.importScreenshotsCheckbox.isChecked():
|
|
# If we time out, take a screenshot of the page as-is.
|
|
try:
|
|
page.goto(actualURL)
|
|
except TimeoutError:
|
|
pass
|
|
|
|
urlSaveDir = projectFilesDir / urlTitle
|
|
|
|
try:
|
|
if not urlSaveDir.exists():
|
|
urlSaveDir.mkdir(mode=0o700)
|
|
except OSError:
|
|
urlSaveDir = None
|
|
|
|
if urlSaveDir is not None:
|
|
timeNow = str(datetime.now().timestamp() * 1000000).split('.')[0]
|
|
screenshotSavePath = str(urlSaveDir / (
|
|
actualURL.replace('/', '+') + ' ' + timeNow + ' screenshot.png'))
|
|
page.screenshot(path=screenshotSavePath, full_page=True)
|
|
|
|
returnResults.append(
|
|
[{'Image Name': decodedPath + ' Screenshot ' + timeNow,
|
|
'File Path': screenshotSavePath,
|
|
'Entity Type': 'Image'},
|
|
{len(returnResults) - 1: {'Resolution': 'Screenshot of Tab',
|
|
'Notes': ''}}])
|
|
|
|
browser.close()
|
|
except Error as e:
|
|
self.parent().MESSAGEHANDLER.warning('Cannot import tabs from Firefox: ' + str(repr(e)),
|
|
popUp=True)
|
|
|
|
progress.setValue(2)
|
|
if importDialog.chromeChoice.isChecked() and not progress.wasCanceled():
|
|
try:
|
|
browser = p.chromium.launch()
|
|
|
|
# NOTE: Cookies are not obtained for chromium based browsers.
|
|
|
|
if platform.system() == 'Linux':
|
|
context = browser.new_context(
|
|
viewport={'width': 1920, 'height': 1080},
|
|
user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
"Chrome/96.0.4664.45 Safari/537.36"
|
|
)
|
|
sessionFilePath = Path.home() / '.config' / 'google-chrome' / 'Default' / 'Sessions'
|
|
if not sessionFilePath.exists():
|
|
sessionFilePath = Path.home() / 'snap' / 'chromium' / 'common' / 'chromium' / \
|
|
'Default' / 'Sessions'
|
|
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
|
|
context = browser.new_context(
|
|
viewport={'width': 1920, 'height': 1080},
|
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
"(KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"
|
|
)
|
|
sessionFilePath = Path.home() / 'AppData' / 'Local' / 'Google' / 'Chrome' / 'User Data' / \
|
|
'Default'
|
|
|
|
lastSessionOpenTabs = set()
|
|
|
|
# Get the session file with the largest timestamp (i.e. most recent session)
|
|
# Need the while loop to prevent race conditions
|
|
chromeSessionFileContents = None
|
|
if sessionFilePath.exists():
|
|
while True:
|
|
try:
|
|
latestTimestamp = max([int(sessionFile.split("Session_", 1)[1])
|
|
for sessionFile in os.listdir(sessionFilePath)
|
|
if 'Session_' in sessionFile])
|
|
sessionFilePath = sessionFilePath.joinpath("Session_" + str(latestTimestamp))
|
|
chromeSessionFileContents = sessionFilePath.read_bytes()
|
|
break
|
|
except (FileNotFoundError, IndexError):
|
|
pass
|
|
except ValueError:
|
|
# No session files means we can't do anything for chrome.
|
|
break
|
|
|
|
if chromeSessionFileContents is None:
|
|
self.parent().MESSAGEHANDLER.warning('Chrome / Chromium session file does not exist or is'
|
|
' inaccessible. Cannot import tabs from Chrome /'
|
|
' Chromium.', popUp=True)
|
|
else:
|
|
projectFilesDir = Path(self.parent().SETTINGS.value("Project/FilesDir"))
|
|
|
|
"""
|
|
WARNING: This is a hackish solution that does not always work. Documentation and tools to work
|
|
with chrome and chromium session files are lacking last I checked,
|
|
and most of the existing ones have been broken since the last update.
|
|
This is a best-effort solution for now, which should mostly work.
|
|
|
|
As for how it works, it's just pattern matching after some observations about the
|
|
file structure of chrome session files.
|
|
|
|
This will probably be revised in the future to make it actually parse chrome session
|
|
files.
|
|
"""
|
|
httpTabs = re.split(b'http', chromeSessionFileContents)
|
|
for httpTab in httpTabs[1:]:
|
|
try:
|
|
tabURL = re.split(b'\x00|\x0b', httpTab)[0].decode()
|
|
except UnicodeDecodeError:
|
|
continue
|
|
if tabURL.startswith('s://'):
|
|
tabURL = 'https://' + tabURL[4:]
|
|
else:
|
|
tabURL = 'http://' + tabURL[3:]
|
|
if tabURL[-1] == '/':
|
|
tabURL = tabURL[:-1]
|
|
lastSessionOpenTabs.add(tabURL)
|
|
fileTabs = re.split(b'file', chromeSessionFileContents)
|
|
for fileTab in fileTabs[1:]:
|
|
try:
|
|
tabURL = 'file://' + re.split(b'\x00|\x0b', fileTab)[0].decode()[3:]
|
|
except UnicodeDecodeError:
|
|
continue
|
|
lastSessionOpenTabs.add(tabURL)
|
|
|
|
page = context.new_page()
|
|
for tabURL in lastSessionOpenTabs:
|
|
decodedPath = parse.unquote(tabURL)
|
|
parsedURL = parse.urlparse(decodedPath)
|
|
urlPath = parsedURL.path
|
|
urlTitle = parsedURL.netloc
|
|
|
|
if parsedURL.scheme == 'file':
|
|
urlTitle = Path(urlPath).name
|
|
try:
|
|
mime = magic.Magic(mime=True)
|
|
pathType = mime.from_file(urlPath)
|
|
except FileNotFoundError:
|
|
continue
|
|
|
|
if 'application' in pathType:
|
|
returnResults.append([{'Document Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Document'}])
|
|
elif 'image' in pathType:
|
|
returnResults.append([{'Image Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Image'}])
|
|
elif 'video' in pathType:
|
|
returnResults.append([{'Video Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Video'}])
|
|
elif 'archive' in pathType:
|
|
returnResults.append([{'Archive Name': urlTitle,
|
|
'File Path': urlPath,
|
|
'Entity Type': 'Archive'}])
|
|
|
|
elif parsedURL.scheme.startswith('http'):
|
|
returnResults.append([{'URL': tabURL,
|
|
'Entity Type': 'Website'}])
|
|
|
|
if importDialog.importScreenshotsCheckbox.isChecked():
|
|
# If we time out, take a screenshot of the page as-is.
|
|
try:
|
|
page.goto(tabURL)
|
|
except TimeoutError:
|
|
pass
|
|
urlSaveDir = projectFilesDir / urlTitle
|
|
|
|
try:
|
|
if not urlSaveDir.exists():
|
|
urlSaveDir.mkdir(mode=0o700)
|
|
except OSError:
|
|
urlSaveDir = None
|
|
|
|
if urlSaveDir is not None:
|
|
timeNow = str(datetime.now().timestamp() * 1000000).split('.')[0]
|
|
screenshotSavePath = str(urlSaveDir / (
|
|
tabURL.replace('/', '+') + ' ' + timeNow + ' screenshot.png'))
|
|
page.screenshot(path=screenshotSavePath, full_page=True)
|
|
|
|
returnResults.append(
|
|
[{'Image Name': decodedPath + ' Screenshot ' + timeNow,
|
|
'File Path': screenshotSavePath,
|
|
'Entity Type': 'Image'},
|
|
{len(returnResults) - 1: {'Resolution': 'Screenshot of Tab',
|
|
'Notes': ''}}])
|
|
|
|
browser.close()
|
|
except Error:
|
|
self.parent().MESSAGEHANDLER.warning('Chrome / Chromium executable is not installed. Cannot '
|
|
'import tabs from Chrome / Chromium.', popUp=True)
|
|
|
|
progress.setValue(3)
|
|
if progress.wasCanceled():
|
|
progress.setValue(4)
|
|
self.parent().setStatus('Cancelled importing entities from Browser.')
|
|
return
|
|
if returnResults:
|
|
self.importBrowserTabsFindings(returnResults, importDialog.importToCanvasCheckbox.isChecked(),
|
|
importDialog.importToCanvasDropdown.currentText())
|
|
progress.setValue(4)
|
|
|
|
def cookieFileHashHelper(self, filePath):
|
|
cookieHash = hashlib.md5() # nosec
|
|
with open(filePath, 'rb') as cookieFile:
|
|
for chunk in iter(lambda: cookieFile.read(4096), b""):
|
|
cookieHash.update(chunk)
|
|
return cookieHash.digest()
|
|
|
|
def importBrowserTabsFindings(self, resolution_result: list, importToCanvas: Union[bool, None] = None,
|
|
canvasToImportTo: Union[str, None] = None) -> None:
|
|
# See the function 'facilitateResolution' in CentralPane for guidance.
|
|
|
|
# Get all the entities, then split it into several lists, to make searching & iterating through them faster.
|
|
allEntities = [(entity['uid'], (entity[list(entity)[1]], entity['Entity Type']))
|
|
for entity in self.parent().LENTDB.getAllEntities()]
|
|
if allEntities:
|
|
allEntityUIDs, allEntityPrimaryFieldsAndTypes = map(list, zip(*allEntities))
|
|
else:
|
|
allEntityUIDs = []
|
|
allEntityPrimaryFieldsAndTypes = []
|
|
allLinks = [linkUID['uid'] for linkUID in self.parent().LENTDB.getAllLinks()]
|
|
links = []
|
|
newNodeUIDs = []
|
|
for resultList in resolution_result:
|
|
newNodeJSON = resultList[0]
|
|
newNodeEntityType = newNodeJSON['Entity Type']
|
|
# Cannot assume proper order of dicts sent over the net.
|
|
newNodePrimaryFieldKey = self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(newNodeEntityType)
|
|
newNodePrimaryField = newNodeJSON[newNodePrimaryFieldKey]
|
|
|
|
try:
|
|
# Attempt to get the index of an existing entity that shares primary field and type with the new
|
|
# entity. Those two entities are considered to be referring to the same thing.
|
|
newNodeExistsIndex = allEntityPrimaryFieldsAndTypes.index((newNodePrimaryField, newNodeEntityType))
|
|
# If entity already exists, update the fields and re-add
|
|
newNodeExistingUID = allEntityUIDs[newNodeExistsIndex]
|
|
existingEntityJSON = self.parent().LENTDB.getEntity(newNodeExistingUID)
|
|
# Remove primary field and entity type, since those are duplicates. Primary field is the first element.
|
|
del newNodeJSON['Entity Type']
|
|
del newNodeJSON[newNodePrimaryFieldKey]
|
|
try:
|
|
notesField = newNodeJSON.pop('Notes')
|
|
existingEntityJSON['Notes'] += '\n' + notesField
|
|
except KeyError:
|
|
# If no new field was actually added to the entity, don't re-add to the database
|
|
if len(newNodeJSON) == 0:
|
|
newNodeUIDs.append(newNodeExistingUID)
|
|
continue
|
|
# Update old values to new ones, and add new ones where applicable.
|
|
existingEntityJSON.update(dict((newNodeKey, newNodeJSON[newNodeKey]) for newNodeKey in newNodeJSON))
|
|
self.parent().LENTDB.addEntity(existingEntityJSON, fromServer=True, updateTimeline=False)
|
|
newNodeUIDs.append(newNodeExistingUID)
|
|
except ValueError:
|
|
# If there is no index for which the primary field and entity type of the new node match one of the
|
|
# existing ones, the node must indeed be new. We add it here.
|
|
entityJson = self.parent().LENTDB.addEntity(newNodeJSON, fromServer=True, updateTimeline=False)
|
|
newNodeUIDs.append(entityJson['uid'])
|
|
# Ensure that different entities involved in the resolution can't independently
|
|
# create the same new entities.
|
|
allEntityUIDs.append(entityJson['uid'])
|
|
allEntityPrimaryFieldsAndTypes.append((newNodePrimaryField, newNodeEntityType))
|
|
|
|
for resultListIndex in range(len(resolution_result)):
|
|
if len(resolution_result[resultListIndex]) > 1:
|
|
outputEntityUID = newNodeUIDs[resultListIndex]
|
|
parentsDict = resolution_result[resultListIndex][1]
|
|
for parentID in parentsDict:
|
|
parentUID = parentID
|
|
if isinstance(parentUID, int):
|
|
parentUID = newNodeUIDs[parentUID]
|
|
resolutionName = parentsDict[parentID]['Resolution']
|
|
newLinkUID = (parentUID, outputEntityUID)
|
|
# Avoid creating more links between the same two entities.
|
|
if newLinkUID in allLinks:
|
|
linkJson = self.parent().LENTDB.getLinkIfExists(newLinkUID)
|
|
if resolutionName not in linkJson['Notes']:
|
|
linkJson['Notes'] += '\nConnection also produced by Resolution: ' + resolutionName
|
|
self.parent().LENTDB.addLink(linkJson, fromServer=True)
|
|
else:
|
|
self.parent().LENTDB.addLink({'uid': newLinkUID, 'Resolution': resolutionName,
|
|
'Notes': parentsDict[parentID]['Notes']}, fromServer=True)
|
|
links.append((parentUID, outputEntityUID, resolutionName))
|
|
allLinks.append(newLinkUID)
|
|
|
|
self.parent().syncDatabase()
|
|
if importToCanvas:
|
|
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(canvasToImportTo)
|
|
for newNodeUID in newNodeUIDs:
|
|
if newNodeUID is not None and newNodeUID not in sceneToAddTo.sceneGraph.nodes:
|
|
sceneToAddTo.addNodeProgrammatic(newNodeUID)
|
|
sceneToAddTo.rearrangeGraph()
|
|
self.parent().centralWidget().tabbedPane.addLinksToTabs(links, "Browser Import")
|
|
self.parent().LENTDB.resetTimeline()
|
|
self.parent().saveProject()
|
|
self.parent().MESSAGEHANDLER.info('Imported tabs from browser successfully.')
|
|
|
|
|
|
class BrowserImportDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent):
|
|
super(BrowserImportDialog, self).__init__(parent=parent)
|
|
self.setWindowTitle('Import Browser Tabs')
|
|
self.setModal(True)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
descriptionLabel = QtWidgets.QLabel('Choose open browser(s) to import tabs from. Note that support'
|
|
' for chromium-based browsers is experimental, and may not'
|
|
' always import all open tabs.\n\nAlso note that session files are used '
|
|
'to read session data, so please ensure that you\'ve waited about half '
|
|
'a minute or so before running this.')
|
|
descriptionLabel.setWordWrap(True)
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
|
|
|
firefoxGroup = QtWidgets.QGroupBox('Firefox Options')
|
|
firefoxGroupLayout = QtWidgets.QVBoxLayout()
|
|
firefoxGroup.setLayout(firefoxGroupLayout)
|
|
self.firefoxChoice = QtWidgets.QCheckBox('Get tabs from Firefox')
|
|
self.firefoxChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
self.firefoxSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
|
|
self.firefoxSessionChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
firefoxGroupLayout.addWidget(self.firefoxChoice)
|
|
firefoxGroupLayout.addWidget(self.firefoxSessionChoice)
|
|
|
|
chromeGroup = QtWidgets.QGroupBox('Chrome Options')
|
|
chromeGroupLayout = QtWidgets.QVBoxLayout()
|
|
chromeGroup.setLayout(chromeGroupLayout)
|
|
self.chromeChoice = QtWidgets.QCheckBox('Get tabs from Chrome / Chromium (Experimental)')
|
|
self.chromeChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
chromeGroupLayout.addWidget(self.chromeChoice)
|
|
|
|
dialogLayout.addWidget(firefoxGroup, 1, 0, 1, 2)
|
|
dialogLayout.addWidget(chromeGroup, 2, 0, 1, 2)
|
|
|
|
self.importScreenshotsCheckbox = QtWidgets.QCheckBox('Take screenshots of sites')
|
|
self.importScreenshotsCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
dialogLayout.addWidget(self.importScreenshotsCheckbox, 3, 0, 1, 2)
|
|
|
|
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
|
|
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
self.importToCanvasDropdown = QtWidgets.QComboBox()
|
|
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
|
self.importToCanvasDropdown.setEditable(False)
|
|
self.importToCanvasDropdown.setDisabled(True)
|
|
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
|
|
self.importToCanvasDropdown.isEnabled()))
|
|
dialogLayout.addWidget(self.importToCanvasCheckbox, 4, 0, 1, 1)
|
|
dialogLayout.addWidget(self.importToCanvasDropdown, 4, 1, 1, 1)
|
|
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
acceptButton.setAutoDefault(True)
|
|
acceptButton.setDefault(True)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
acceptButton.setFocus()
|
|
|
|
self.setMaximumWidth(450)
|
|
self.setMinimumWidth(450)
|
|
self.setMaximumHeight(400)
|
|
self.setMinimumHeight(400)
|
|
|
|
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
|
|
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
|
|
|
|
|
|
class ServerConnectWizard(QtWidgets.QDialog):
|
|
"""
|
|
Dialog Window that lets the user input the details of the server that
|
|
they want to connect to.
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
super().__init__(parent=parent)
|
|
self.setLayout(QtWidgets.QFormLayout())
|
|
self.setWindowTitle("Server Connection Wizard")
|
|
# self.setMinimumSize(550,200)
|
|
|
|
serverIPLabel = QtWidgets.QLabel("Server IP:")
|
|
self.serverIPTextbox = QtWidgets.QLineEdit()
|
|
self.layout().addRow(serverIPLabel, self.serverIPTextbox)
|
|
|
|
serverPortLabel = QtWidgets.QLabel("Server Port:")
|
|
self.serverPortTextbox = QtWidgets.QLineEdit("3777")
|
|
self.layout().addRow(serverPortLabel, self.serverPortTextbox)
|
|
|
|
serverPasswordLabel = QtWidgets.QLabel("Server Password:")
|
|
self.serverPasswordTextbox = QtWidgets.QLineEdit()
|
|
self.serverPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.Password)
|
|
self.layout().addRow(serverPasswordLabel, self.serverPasswordTextbox)
|
|
|
|
self.confirmConnect = False
|
|
confirmButton = QtWidgets.QPushButton("Confirm")
|
|
cancelButton = QtWidgets.QPushButton("Cancel")
|
|
self.layout().addRow(cancelButton, confirmButton)
|
|
|
|
confirmButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
|
|
def accept(self):
|
|
self.confirmConnect = True
|
|
super().accept()
|
|
|
|
|
|
class ServerCreateOrOpenProject(QtWidgets.QDialog):
|
|
|
|
def __init__(self, mainWindow):
|
|
super(ServerCreateOrOpenProject, self).__init__(parent=mainWindow)
|
|
self.setModal(True)
|
|
|
|
with mainWindow.serverProjectsLock:
|
|
serverProjects = mainWindow.serverProjects
|
|
|
|
self.setLayout(QtWidgets.QVBoxLayout())
|
|
self.openProject = True
|
|
self.projectName = ''
|
|
self.projectPass = ''
|
|
|
|
openProjectWidget = QtWidgets.QWidget()
|
|
openProjectLayout = QtWidgets.QFormLayout()
|
|
openProjectWidget.setLayout(openProjectLayout)
|
|
self.openProjectDropdown = QtWidgets.QComboBox()
|
|
self.openProjectDropdown.setToolTip('Select the name of the server project to work on.')
|
|
self.openProjectDropdown.setEditable(False)
|
|
self.openProjectDropdown.addItems(serverProjects)
|
|
self.openProjectPassword = QtWidgets.QLineEdit('')
|
|
self.openProjectPassword.setToolTip('Enter the password of the selected server project.')
|
|
self.openProjectPassword.setEchoMode(QtWidgets.QLineEdit.Password)
|
|
openProjectLayout.addRow('Open Project:', self.openProjectDropdown)
|
|
openProjectLayout.addRow('Password:', self.openProjectPassword)
|
|
openProjectButton = QtWidgets.QPushButton('Open Project')
|
|
|
|
self.layout().addWidget(openProjectWidget)
|
|
self.layout().addWidget(openProjectButton)
|
|
|
|
createProjectWidget = QtWidgets.QWidget()
|
|
createProjectLayout = QtWidgets.QFormLayout()
|
|
createProjectWidget.setLayout(createProjectLayout)
|
|
self.createProjectNameTextbox = QtWidgets.QLineEdit('')
|
|
self.createProjectNameTextbox.setToolTip('Specify the name of the server project. Must be unique.')
|
|
self.createProjectPasswordTextbox = QtWidgets.QLineEdit('')
|
|
self.createProjectPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.Password)
|
|
self.createProjectPasswordTextbox.setToolTip('Specify a password to be entered'
|
|
' in order to access this project.')
|
|
|
|
createProjectLayout.addRow('Project Name:', self.createProjectNameTextbox)
|
|
createProjectLayout.addRow('Project Password:', self.createProjectPasswordTextbox)
|
|
createProjectButton = QtWidgets.QPushButton('Create Server Project')
|
|
|
|
self.layout().addWidget(createProjectWidget)
|
|
self.layout().addWidget(createProjectButton)
|
|
|
|
openProjectButton.clicked.connect(self.openExistingProject)
|
|
createProjectButton.clicked.connect(self.createNewProject)
|
|
|
|
def createNewProject(self) -> None:
|
|
self.projectName = self.createProjectNameTextbox.text()
|
|
if self.projectName == '':
|
|
self.parent().MESSAGEHANDLER.warning('Project name cannot be blank.', popUp=True)
|
|
return
|
|
self.projectPass = self.createProjectPasswordTextbox.text()
|
|
self.openProject = False
|
|
self.accept()
|
|
|
|
def openExistingProject(self) -> None:
|
|
self.projectName = self.openProjectDropdown.currentText()
|
|
if self.projectName == '':
|
|
self.parent().MESSAGEHANDLER.warning('Project name cannot be blank.', popUp=True)
|
|
return
|
|
self.projectPass = self.openProjectPassword.text()
|
|
self.openProject = True
|
|
self.accept()
|
|
|
|
|
|
class ViewAndStopResolutionsDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, mainWindowObject):
|
|
super(ViewAndStopResolutionsDialog, self).__init__()
|
|
self.setModal(True)
|
|
self.setLayout(QtWidgets.QVBoxLayout())
|
|
self.mainWindowObject = mainWindowObject
|
|
|
|
resolutionsLabel = QtWidgets.QLabel('Running Resolutions:')
|
|
|
|
resolutionsLabel.setAlignment(QtCore.Qt.AlignCenter)
|
|
self.layout().addWidget(resolutionsLabel)
|
|
|
|
scrollArea = QtWidgets.QScrollArea()
|
|
scrollAreaLayout = QtWidgets.QFormLayout()
|
|
scrollArea.setLayout(scrollAreaLayout)
|
|
|
|
for resolution in mainWindowObject.resolutions:
|
|
if resolution[1]:
|
|
resolutionName = 'Server Resolution: ' + resolution[0].resolution
|
|
stopResolutionButton = ViewAndStopResolutionsDialogOption(resolution[0],
|
|
resolution[1], mainWindowObject)
|
|
scrollAreaLayout.addRow(QtWidgets.QLabel(resolutionName), stopResolutionButton)
|
|
elif not resolution[0].done:
|
|
resolutionName = 'Local Resolution: ' + resolution[0].resolution
|
|
stopResolutionButton = ViewAndStopResolutionsDialogOption(resolution[0],
|
|
resolution[1], mainWindowObject)
|
|
scrollAreaLayout.addRow(QtWidgets.QLabel(resolutionName), stopResolutionButton)
|
|
|
|
self.layout().addWidget(scrollArea)
|
|
self.setMinimumWidth(scrollArea.width())
|
|
self.resize(scrollArea.width(), 400)
|
|
|
|
|
|
class ViewAndStopResolutionsDialogOption(QtWidgets.QPushButton):
|
|
|
|
def __init__(self, resolutionThread, fromServer, mainWindowObject):
|
|
super(ViewAndStopResolutionsDialogOption, self).__init__()
|
|
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
|
self.mainWindowObject = mainWindowObject
|
|
self.resolutionThread = resolutionThread
|
|
self.fromServer = fromServer
|
|
self.setText('Stop Resolution')
|
|
|
|
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
if self.fromServer:
|
|
self.mainWindowObject.FCOM.abortResolution(self.resolutionThread.resolution, self.resolutionThread.uid)
|
|
else:
|
|
self.resolutionThread.return_results = False
|
|
self.resolutionThread.exit()
|
|
self.mainWindowObject.cleanUpLocalFinishedResolutions()
|
|
# Remove from parent's layout
|
|
self.parent().layout().removeRow(self)
|
|
|
|
|
|
class ImportFromFileDialog(QtWidgets.QDialog):
|
|
def popupFileDialog(self):
|
|
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select file to import',
|
|
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
|
filter="CSV or txt (*.csv *.txt)")[0]
|
|
if self.fileDirectory != '':
|
|
self.fileDirectoryLine.setText(self.fileDirectory)
|
|
|
|
def __init__(self, parent):
|
|
super(ImportFromFileDialog, self).__init__(parent=parent)
|
|
self.fileDirectory = ''
|
|
self.setWindowTitle('Import From File')
|
|
self.setModal(True)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
descriptionLabel = QtWidgets.QLabel('Choose the filetype you selected')
|
|
descriptionLabel.setWordWrap(True)
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
|
|
|
self.fileDirectoryButton = QtWidgets.QPushButton("Select file...")
|
|
self.fileDirectoryLine = QtWidgets.QLineEdit()
|
|
self.fileDirectoryLine.setReadOnly(True)
|
|
self.textFileChoice = QtWidgets.QRadioButton('Text file')
|
|
self.textFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
|
self.textFileChoice.setChecked(True)
|
|
self.CSVFileChoice = QtWidgets.QRadioButton('CSV (Please see documentation for formatting)')
|
|
self.CSVFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
|
|
|
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
|
|
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
|
|
dialogLayout.addWidget(self.textFileChoice, 3, 0, 1, 2)
|
|
dialogLayout.addWidget(self.CSVFileChoice, 4, 0, 1, 2)
|
|
|
|
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
|
|
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
|
self.importToCanvasDropdown = QtWidgets.QComboBox()
|
|
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
|
self.importToCanvasDropdown.setEditable(False)
|
|
self.importToCanvasDropdown.setDisabled(True)
|
|
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
|
|
self.importToCanvasDropdown.isEnabled()))
|
|
dialogLayout.addWidget(self.importToCanvasCheckbox, 5, 0, 1, 1)
|
|
dialogLayout.addWidget(self.importToCanvasDropdown, 5, 1, 1, 1)
|
|
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
acceptButton.setAutoDefault(True)
|
|
acceptButton.setDefault(True)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
self.fileDirectoryButton.clicked.connect(self.popupFileDialog)
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
acceptButton.setFocus()
|
|
|
|
self.setMaximumWidth(500)
|
|
self.setMinimumWidth(300)
|
|
self.setMaximumHeight(300)
|
|
self.setMinimumHeight(300)
|
|
|
|
dialogLayout.addWidget(cancelButton, 6, 0, 1, 1)
|
|
dialogLayout.addWidget(acceptButton, 6, 1, 1, 1)
|
|
|
|
|
|
class ImportFromTextFileDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent):
|
|
super(ImportFromTextFileDialog, self).__init__(parent=parent)
|
|
self.setWindowTitle('Import From Text File')
|
|
self.setModal(True)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
descriptionLabel = QtWidgets.QLabel('Choose what type the entities will be imported as')
|
|
descriptionLabel.setWordWrap(True)
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
|
|
|
self.importTypeDropdown = QtWidgets.QComboBox()
|
|
self.importTypeDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
|
|
self.importTypeDropdown.setEditable(False)
|
|
|
|
dialogLayout.addWidget(self.importTypeDropdown, 3, 1, 1, 1)
|
|
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
acceptButton.setAutoDefault(True)
|
|
acceptButton.setDefault(True)
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
acceptButton.setFocus()
|
|
|
|
self.setMaximumWidth(450)
|
|
self.setMinimumWidth(300)
|
|
self.setMaximumHeight(300)
|
|
self.setMinimumHeight(300)
|
|
|
|
dialogLayout.addWidget(cancelButton, 4, 0, 1, 1)
|
|
dialogLayout.addWidget(acceptButton, 4, 1, 1, 1)
|
|
|
|
|
|
class CanvasPictureDialog(QtWidgets.QDialog):
|
|
|
|
def __init__(self, parent):
|
|
super(CanvasPictureDialog, self).__init__(parent=parent)
|
|
self.fileDirectory = ""
|
|
self.setWindowTitle('Save Canvas Picture')
|
|
self.setModal(True)
|
|
|
|
dialogLayout = QtWidgets.QGridLayout()
|
|
self.setLayout(dialogLayout)
|
|
descriptionLabel = QtWidgets.QLabel('Choose the directory to save the Picture to:')
|
|
descriptionLabel.setWordWrap(True)
|
|
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
|
|
|
self.fileDirectoryButton = QtWidgets.QPushButton("Select directory...")
|
|
self.fileDirectoryLine = QtWidgets.QLineEdit()
|
|
self.fileDirectoryLine.setReadOnly(True)
|
|
self.transparentChoice = QtWidgets.QRadioButton('Transparent')
|
|
self.transparentChoice.setChecked(True)
|
|
self.withBackgroundChoice = QtWidgets.QRadioButton('With Background')
|
|
|
|
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
|
|
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
|
|
dialogLayout.addWidget(self.transparentChoice, 3, 0, 1, 1)
|
|
dialogLayout.addWidget(self.withBackgroundChoice, 3, 1, 1, 1)
|
|
|
|
canvasLabel = QtWidgets.QLabel('Canvas:')
|
|
canvasLabel.setWordWrap(False)
|
|
dialogLayout.addWidget(canvasLabel, 5, 0, 1, 1)
|
|
self.chosenCanvasDropdown = QtWidgets.QComboBox()
|
|
self.chosenCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
|
self.chosenCanvasDropdown.setEditable(False)
|
|
dialogLayout.addWidget(self.chosenCanvasDropdown, 5, 1, 1, 1)
|
|
|
|
acceptButton = QtWidgets.QPushButton('Accept')
|
|
acceptButton.setAutoDefault(True)
|
|
acceptButton.setDefault(True)
|
|
acceptButton.setFocus()
|
|
cancelButton = QtWidgets.QPushButton('Cancel')
|
|
self.fileDirectoryButton.clicked.connect(self.popupFileDialog)
|
|
acceptButton.clicked.connect(self.accept)
|
|
cancelButton.clicked.connect(self.reject)
|
|
|
|
self.setMaximumWidth(500)
|
|
self.setMinimumWidth(300)
|
|
self.setMaximumHeight(300)
|
|
self.setMinimumHeight(300)
|
|
|
|
dialogLayout.addWidget(cancelButton, 6, 0, 1, 1)
|
|
dialogLayout.addWidget(acceptButton, 6, 1, 1, 1)
|
|
|
|
def popupFileDialog(self):
|
|
saveAsDialog = QtWidgets.QFileDialog()
|
|
saveAsDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
|
saveAsDialog.setViewMode(QtWidgets.QFileDialog.List)
|
|
saveAsDialog.setNameFilter("Image (*.png)")
|
|
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
|
saveAsDialog.exec()
|
|
self.fileDirectory = saveAsDialog.selectedFiles()[0]
|
|
if self.fileDirectory != '':
|
|
if Path(self.fileDirectory).suffix != 'png':
|
|
self.fileDirectory = str(Path(self.fileDirectory).with_suffix('.png'))
|
|
self.fileDirectoryLine.setText(self.fileDirectory)
|