Finish implementing Markdown editor.
Dockbar Two now has 'Notes' tab that allows investigators to take canvas-specific notes. Allows Markdown formatting. Markdown editor now used for editing & displaying notes for entities and links as well. Breaking change: Canvases file renamed from canvasTabs.lscanvas to CanvasTabs.lscanvas.
This commit is contained in:
@@ -18,7 +18,9 @@ from PySide6 import QtWidgets, QtGui, QtCore
|
||||
from PySide6.QtWidgets import QGraphicsPixmapItem
|
||||
from PySide6.QtSvgWidgets import QGraphicsSvgItem
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
from Core.Interface import Entity, Stylesheets
|
||||
from Core.ResourceHandler import RichNotesEditor
|
||||
|
||||
|
||||
class WorkspaceWidget(QtWidgets.QWidget):
|
||||
@@ -188,6 +190,13 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
self.syncedTabs = []
|
||||
self.nodeCreationThreads = []
|
||||
|
||||
self.canvasDBPath = Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "CanvasTabs.lscanvas"
|
||||
self.tabsNotesPath = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("CanvasNotes.lsnotes")
|
||||
self.tabsNotesDict = {}
|
||||
self.previousTab = None
|
||||
|
||||
self.currentChanged.connect(self.currentTabChangedListener)
|
||||
|
||||
def addCanvas(self, canvasName='New Graph', graph=None, positions=None, a=0, b=0, c=0, d=0) -> bool:
|
||||
if not self.isCanvasNameAvailable(canvasName):
|
||||
return False
|
||||
@@ -257,11 +266,20 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
return self.canvasTabs[self.tabText(index)]
|
||||
|
||||
def closeTab(self, tabName: str) -> None:
|
||||
"""
|
||||
Delete tab with name tabName.
|
||||
@param tabName:
|
||||
@return:
|
||||
"""
|
||||
for tabIndex in range(self.count()):
|
||||
if self.tabText(tabIndex) == tabName:
|
||||
self.removeTab(tabIndex)
|
||||
break
|
||||
self.canvasTabs.pop(tabName)
|
||||
try:
|
||||
self.tabsNotesDict.pop(tabName)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def hideTab(self, index) -> None:
|
||||
self.removeTab(index)
|
||||
@@ -569,25 +587,50 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
def save(self) -> None:
|
||||
if len(self.canvasTabs) == 0:
|
||||
return
|
||||
canvasDBPath = Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "canvasTabs.lscanvas"
|
||||
canvasDBPathTmp = canvasDBPath.with_suffix(canvasDBPath.suffix + '.tmp')
|
||||
|
||||
canvasDBFile = open(canvasDBPathTmp, "wb")
|
||||
saveJson = {}
|
||||
for canvasName in self.canvasTabs:
|
||||
saveJson[canvasName] = [
|
||||
self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
|
||||
self.canvasTabs[canvasName].scene().scenePos]
|
||||
dump(saveJson, canvasDBFile)
|
||||
canvasDBFile.close()
|
||||
move(canvasDBPathTmp, canvasDBPath)
|
||||
canvasDBPathTmp = self.canvasDBPath.with_suffix(self.canvasDBPath.suffix + '.tmp')
|
||||
canvasNotesPathTmp = self.tabsNotesPath.with_suffix(self.tabsNotesPath.suffix + '.tmp')
|
||||
|
||||
# Save canvases
|
||||
with open(canvasDBPathTmp, "wb") as canvasDBFile:
|
||||
saveJson = {}
|
||||
for canvasName in self.canvasTabs:
|
||||
saveJson[canvasName] = [
|
||||
self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
|
||||
self.canvasTabs[canvasName].scene().scenePos]
|
||||
dump(saveJson, canvasDBFile)
|
||||
move(canvasDBPathTmp, self.canvasDBPath)
|
||||
|
||||
# Save canvas notes
|
||||
currIndex = self.currentIndex()
|
||||
if currIndex != -1:
|
||||
canvasTabName = self.tabText(currIndex)
|
||||
if canvasTabName in self.canvasTabs:
|
||||
self.tabsNotesDict[canvasTabName] = self.mainWindow.dockbarTwo.getNotesText()
|
||||
with open(canvasNotesPathTmp, "wb") as canvasNotesFileTmp:
|
||||
dump(self.tabsNotesDict, canvasNotesFileTmp)
|
||||
move(canvasNotesPathTmp, self.tabsNotesPath)
|
||||
|
||||
def open(self) -> None:
|
||||
canvasDBPath = Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "canvasTabs.lscanvas"
|
||||
|
||||
if Path(canvasDBPath).exists():
|
||||
try:
|
||||
with open(self.tabsNotesPath, 'rb') as notesFile:
|
||||
# Load the content into the Notes pane.
|
||||
self.tabsNotesDict = load(notesFile)
|
||||
except ValueError:
|
||||
# If the Tab Notes file is empty or contains invalid input, ignore it.
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
# Create new placeholder notes file if it doesn't exist.
|
||||
try:
|
||||
with open(canvasDBPath, "rb") as canvasDBFile:
|
||||
self.tabsNotesPath.touch(0o700, exist_ok=False)
|
||||
self.mainWindow.MESSAGEHANDLER.info('Created new Tab Notes file.')
|
||||
except FileExistsError:
|
||||
self.mainWindow.MESSAGEHANDLER.error('Race condition occurred while trying to create '
|
||||
'Tab Notes file.')
|
||||
|
||||
if Path(self.canvasDBPath).exists():
|
||||
try:
|
||||
with open(self.canvasDBPath, "rb") as canvasDBFile:
|
||||
savedJson = load(canvasDBFile)
|
||||
for canvasName in savedJson:
|
||||
self.addCanvas(canvasName,
|
||||
@@ -597,6 +640,20 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
self.messageHandler.error("Exception occurred when opening tabs: " + str(exc) +
|
||||
"\nSkipping opening tabs.", popUp=True)
|
||||
|
||||
def currentTabChangedListener(self, newIndex: int) -> None:
|
||||
if self.previousTab in self.canvasTabs:
|
||||
# Will never have 'None' canvas, or consider deleted tabs.
|
||||
self.tabsNotesDict[self.previousTab] = self.mainWindow.dockbarTwo.getNotesText()
|
||||
|
||||
if newIndex != -1:
|
||||
canvasName = self.tabText(newIndex)
|
||||
tabNotes = self.tabsNotesDict.get(canvasName, '#### Type notes here.\n')
|
||||
self.mainWindow.dockbarTwo.setNotesText(tabNotes)
|
||||
self.previousTab = canvasName
|
||||
else:
|
||||
self.mainWindow.dockbarTwo.setNotesText("")
|
||||
self.previousTab = None
|
||||
|
||||
|
||||
class DocWorldPane(QtWidgets.QWidget):
|
||||
|
||||
@@ -1948,7 +2005,7 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
continue
|
||||
keyField = QtWidgets.QLabel(key)
|
||||
if key == "Notes":
|
||||
valueField = QtWidgets.QPlainTextEdit(objectJson[key])
|
||||
valueField = RichNotesEditor(self, objectJson["Notes"])
|
||||
elif key == 'Icon':
|
||||
valueField = PropertiesEditorIconField(objectJson[key], objectJson['uid'], self.canvas)
|
||||
elif key == 'File Path':
|
||||
@@ -1974,7 +2031,7 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
row, self.itemProperties.LabelRole).widget().text()
|
||||
if key == "Notes":
|
||||
value = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.FieldRole).widget().toPlainText()
|
||||
row, self.itemProperties.FieldRole).widget().toMarkdown()
|
||||
elif key == 'Icon':
|
||||
value = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.FieldRole).widget().pictureByteArray
|
||||
|
||||
@@ -4,9 +4,7 @@ from pathlib import Path
|
||||
import magic
|
||||
from PySide6 import QtWidgets, QtCore, QtGui
|
||||
from Core.Interface import Stylesheets
|
||||
from Core.ResourceHandler import MinSizeStackedLayout
|
||||
|
||||
from typing import Union
|
||||
from Core.ResourceHandler import MinSizeStackedLayout, RichNotesEditor
|
||||
|
||||
|
||||
class DockBarTwo(QtWidgets.QDockWidget):
|
||||
@@ -51,6 +49,22 @@ class DockBarTwo(QtWidgets.QDockWidget):
|
||||
|
||||
self.initialiseLayout()
|
||||
|
||||
def setNotesText(self, newText: str) -> None:
|
||||
if self.tabNotes.textEditor.isReadOnly():
|
||||
self.tabNotes.textEditor.contents = newText
|
||||
self.tabNotes.textEditor.setMarkdown(newText)
|
||||
else:
|
||||
self.tabNotes.textEditor.setReadOnly(True)
|
||||
self.tabNotes.textEditor.contents = newText
|
||||
self.tabNotes.textEditor.setMarkdown(newText)
|
||||
|
||||
def getNotesText(self) -> str:
|
||||
"""
|
||||
Get text with the Markdown control characters.
|
||||
@return:
|
||||
"""
|
||||
return self.tabNotes.textEditor.toMarkdown()
|
||||
|
||||
|
||||
class TabNotesPanel(QtWidgets.QWidget):
|
||||
"""
|
||||
@@ -62,62 +76,12 @@ class TabNotesPanel(QtWidgets.QWidget):
|
||||
super(TabNotesPanel, self).__init__(parent=parent)
|
||||
tabNotesLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
self.textEditor = TabNotesEditor(self)
|
||||
self.textEditor = RichNotesEditor(self)
|
||||
|
||||
tabNotesLayout.addWidget(self.textEditor)
|
||||
self.setLayout(tabNotesLayout)
|
||||
|
||||
|
||||
class TabNotesEditor(QtWidgets.QTextBrowser):
|
||||
|
||||
def __init__(self, parent):
|
||||
super(TabNotesEditor, self).__init__(parent=parent)
|
||||
self.setReadOnly(True)
|
||||
self.setUndoRedoEnabled(True)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction |
|
||||
QtCore.Qt.TextSelectableByKeyboard)
|
||||
|
||||
self.contents = '#### Type notes here.\n'
|
||||
self.setMarkdown(self.contents)
|
||||
self.textFormat = self.currentCharFormat()
|
||||
|
||||
def startEditing(self) -> None:
|
||||
if self.isReadOnly():
|
||||
# Reset char format to plain text.
|
||||
self.setCurrentCharFormat(self.textFormat)
|
||||
self.setPlainText(self.contents)
|
||||
self.setReadOnly(False)
|
||||
|
||||
def stopEditing(self) -> None:
|
||||
if not self.isReadOnly():
|
||||
self.contents = self.toPlainText()
|
||||
self.setMarkdown(self.contents)
|
||||
self.setReadOnly(True)
|
||||
|
||||
def dropEvent(self, e: QtGui.QDropEvent) -> None:
|
||||
editingBefore = self.isReadOnly()
|
||||
self.startEditing()
|
||||
super(TabNotesEditor, self).dropEvent(e)
|
||||
if editingBefore:
|
||||
self.stopEditing()
|
||||
|
||||
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
|
||||
potentialLink = self.anchorAt(ev.pos())
|
||||
if not potentialLink:
|
||||
if ev.button() == QtGui.Qt.LeftButton:
|
||||
self.startEditing()
|
||||
super(TabNotesEditor, self).mousePressEvent(ev)
|
||||
|
||||
def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None:
|
||||
if not self.underMouse():
|
||||
if self.isActiveWindow():
|
||||
self.stopEditing()
|
||||
super(TabNotesEditor, self).focusOutEvent(ev)
|
||||
|
||||
def doSetSource(self, name: Union[QtCore.QUrl, str], resourceType: QtGui.QTextDocument.ResourceType = ...) -> None:
|
||||
QtGui.QDesktopServices.openUrl(name)
|
||||
|
||||
|
||||
class EntityDetails(QtWidgets.QWidget):
|
||||
"""
|
||||
This class is used to display a detailed view of the attributes of
|
||||
@@ -318,8 +282,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
if key == "uid" or key == "Child UIDs" or key == "Icon":
|
||||
continue
|
||||
elif key == "Notes":
|
||||
notesTextArea = QtWidgets.QPlainTextEdit(jsonDict[key])
|
||||
notesTextArea.setReadOnly(True)
|
||||
notesTextArea = RichNotesEditor(self, jsonDict[key], False)
|
||||
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel(key), rowCount, 0)
|
||||
self.detailsLayoutOneNode.addWidget(notesTextArea, rowCount, 1, 10, 1)
|
||||
rowCount += 9
|
||||
|
||||
@@ -14,7 +14,7 @@ from ast import literal_eval
|
||||
from base64 import b64decode
|
||||
from dateutil import parser
|
||||
|
||||
from PySide6.QtCore import QByteArray, QSize
|
||||
from PySide6.QtCore import QByteArray, QSize, QUrl, Qt
|
||||
from PySide6 import QtWidgets, QtGui
|
||||
|
||||
from Core.Interface import Stylesheets
|
||||
@@ -529,3 +529,60 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
|
||||
|
||||
def minimumSize(self) -> QSize:
|
||||
return self.currentWidget().minimumSize()
|
||||
|
||||
|
||||
class RichNotesEditor(QtWidgets.QTextBrowser):
|
||||
|
||||
def __init__(self, parent=None, currentText: str = '#### Type notes here.\n', allowEditing: bool = True):
|
||||
super(RichNotesEditor, self).__init__(parent=parent)
|
||||
self.allowEditing = allowEditing
|
||||
self.setReadOnly(True)
|
||||
if allowEditing:
|
||||
self.setUndoRedoEnabled(True)
|
||||
self.setTextInteractionFlags(Qt.TextBrowserInteraction | Qt.TextSelectableByKeyboard)
|
||||
else:
|
||||
self.setUndoRedoEnabled(False)
|
||||
self.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard
|
||||
| Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
|
||||
self.contents = currentText
|
||||
self.setMarkdown(self.contents)
|
||||
self.textFormat = self.currentCharFormat()
|
||||
|
||||
def startEditing(self) -> None:
|
||||
if self.allowEditing:
|
||||
if self.isReadOnly():
|
||||
# Reset char format to plain text.
|
||||
self.setCurrentCharFormat(self.textFormat)
|
||||
self.setPlainText(self.contents)
|
||||
self.setReadOnly(False)
|
||||
|
||||
def stopEditing(self) -> None:
|
||||
if not self.isReadOnly():
|
||||
self.contents = self.toPlainText()
|
||||
self.setMarkdown(self.contents)
|
||||
self.setReadOnly(True)
|
||||
|
||||
def dropEvent(self, e: QtGui.QDropEvent) -> None:
|
||||
if self.allowEditing:
|
||||
editingBefore = self.isReadOnly()
|
||||
self.startEditing()
|
||||
super(RichNotesEditor, self).dropEvent(e)
|
||||
if editingBefore:
|
||||
self.stopEditing()
|
||||
|
||||
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
|
||||
potentialLink = self.anchorAt(ev.pos())
|
||||
if not potentialLink:
|
||||
if ev.button() == QtGui.Qt.LeftButton:
|
||||
self.startEditing()
|
||||
super(RichNotesEditor, self).mousePressEvent(ev)
|
||||
|
||||
def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None:
|
||||
if not self.underMouse():
|
||||
if self.isActiveWindow():
|
||||
self.stopEditing()
|
||||
super(RichNotesEditor, self).focusOutEvent(ev)
|
||||
|
||||
def doSetSource(self, name: Union[QUrl, str], resourceType: QtGui.QTextDocument.ResourceType = ...) -> None:
|
||||
QtGui.QDesktopServices.openUrl(name)
|
||||
|
||||
Reference in New Issue
Block a user