Feat: vis.js Layout-Switcher im XSL-Abhängigkeitsgraph (v1.2.0)
Layout-Umschaltung zwischen barnesHut, ForceAtlas2, Repulsion und hierarchischem Layout mit konfigurierbaren Parametern pro Layout. Einstellungen werden persistent in AppSettings gespeichert und beim Öffnen des Dialogs wiederhergestellt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<!-- Paket-Definition (ersetzt Product in v4) -->
|
<!-- Paket-Definition (ersetzt Product in v4) -->
|
||||||
<Package
|
<Package
|
||||||
Name="DocuMentor"
|
Name="DocuMentor"
|
||||||
Version="1.1.1"
|
Version="1.2.0"
|
||||||
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
|
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
|
||||||
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
|
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
|
||||||
Language="1031"
|
Language="1031"
|
||||||
|
|||||||
@@ -253,5 +253,5 @@ HINWEISE
|
|||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
Stand: März 2026
|
Stand: März 2026
|
||||||
Erstellt für: DocuMentor v1.1.1
|
Erstellt für: DocuMentor v1.2.0
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
; Build-Befehl: iscc installer.iss
|
; Build-Befehl: iscc installer.iss
|
||||||
|
|
||||||
#define MyAppName "DocuMentor"
|
#define MyAppName "DocuMentor"
|
||||||
#define MyAppVersion "1.1.1"
|
#define MyAppVersion "1.2.0"
|
||||||
#define MyAppPublisher "Ihr Name/Organisation"
|
#define MyAppPublisher "Ihr Name/Organisation"
|
||||||
#define MyAppURL "https://github.com/yourusername/xsl-validator"
|
#define MyAppURL "https://github.com/yourusername/xsl-validator"
|
||||||
#define MyAppExeName "DocuMentor.exe"
|
#define MyAppExeName "DocuMentor.exe"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "DocuMentor"
|
name = "DocuMentor"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
+60
@@ -80,6 +80,65 @@ class XsltVersion(str, Enum):
|
|||||||
XSLT_2_0_3_0 = "2.0/3.0" # s9api (XSLT 2.0 und 3.0)
|
XSLT_2_0_3_0 = "2.0/3.0" # s9api (XSLT 2.0 und 3.0)
|
||||||
|
|
||||||
|
|
||||||
|
class GraphLayout(str, Enum):
|
||||||
|
"""vis.js Physics-Solver / Layout-Modus."""
|
||||||
|
|
||||||
|
BARNES_HUT = "barnesHut"
|
||||||
|
FORCE_ATLAS2 = "forceAtlas2Based"
|
||||||
|
REPULSION = "repulsion"
|
||||||
|
HIERARCHICAL = "hierarchical"
|
||||||
|
|
||||||
|
|
||||||
|
class HierarchicalDirection(str, Enum):
|
||||||
|
"""Richtung für hierarchisches Layout."""
|
||||||
|
|
||||||
|
UD = "UD"
|
||||||
|
DU = "DU"
|
||||||
|
LR = "LR"
|
||||||
|
RL = "RL"
|
||||||
|
|
||||||
|
|
||||||
|
class HierarchicalSortMethod(str, Enum):
|
||||||
|
"""Sortiermethode für hierarchisches Layout."""
|
||||||
|
|
||||||
|
HUBSIZE = "hubsize"
|
||||||
|
DIRECTED = "directed"
|
||||||
|
|
||||||
|
|
||||||
|
class GraphLayoutSettings(BaseModel):
|
||||||
|
"""Persistierte vis.js Layout-Einstellungen für den XSL-Abhängigkeitsgraph."""
|
||||||
|
|
||||||
|
layout: GraphLayout = GraphLayout.BARNES_HUT
|
||||||
|
|
||||||
|
# barnesHut
|
||||||
|
bh_gravitational_constant: int = -3000
|
||||||
|
bh_central_gravity: float = 0.3
|
||||||
|
bh_spring_length: int = 150
|
||||||
|
bh_spring_constant: float = 0.04
|
||||||
|
bh_damping: float = 0.09
|
||||||
|
|
||||||
|
# forceAtlas2Based
|
||||||
|
fa_gravitational_constant: int = -50
|
||||||
|
fa_central_gravity: float = 0.01
|
||||||
|
fa_spring_length: int = 100
|
||||||
|
fa_spring_constant: float = 0.08
|
||||||
|
fa_damping: float = 0.4
|
||||||
|
|
||||||
|
# repulsion
|
||||||
|
re_node_distance: int = 120
|
||||||
|
re_central_gravity: float = 0.0
|
||||||
|
re_spring_length: int = 200
|
||||||
|
re_spring_constant: float = 0.05
|
||||||
|
re_damping: float = 0.09
|
||||||
|
|
||||||
|
# hierarchical
|
||||||
|
hi_direction: HierarchicalDirection = HierarchicalDirection.UD
|
||||||
|
hi_sort_method: HierarchicalSortMethod = HierarchicalSortMethod.HUBSIZE
|
||||||
|
hi_level_separation: int = 150
|
||||||
|
hi_node_spacing: int = 100
|
||||||
|
hi_tree_spacing: int = 200
|
||||||
|
|
||||||
|
|
||||||
class PostgreSqlDb(BaseModel):
|
class PostgreSqlDb(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
@@ -147,6 +206,7 @@ class AppSettings(BaseSettings):
|
|||||||
window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height)
|
window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height)
|
||||||
splitter_sizes: list[int] | None = None # Splitter-Positionen
|
splitter_sizes: list[int] | None = None # Splitter-Positionen
|
||||||
tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten
|
tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten
|
||||||
|
graph_layout_settings: GraphLayoutSettings = Field(default_factory=GraphLayoutSettings)
|
||||||
|
|
||||||
model_config = SettingsConfigDict(json_file=config_path)
|
model_config = SettingsConfigDict(json_file=config_path)
|
||||||
|
|
||||||
|
|||||||
+381
-24
@@ -13,13 +13,103 @@ from pathlib import Path
|
|||||||
|
|
||||||
from PySide6.QtCore import QUrl, Qt
|
from PySide6.QtCore import QUrl, Qt
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
from PySide6.QtWidgets import QDialog, QTreeWidgetItem
|
from PySide6.QtWidgets import (
|
||||||
|
QComboBox,
|
||||||
|
QDialog,
|
||||||
|
QDoubleSpinBox,
|
||||||
|
QFormLayout,
|
||||||
|
QLabel,
|
||||||
|
QSpinBox,
|
||||||
|
QStackedWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from conf import (
|
||||||
|
GraphLayout,
|
||||||
|
GraphLayoutSettings,
|
||||||
|
HierarchicalDirection,
|
||||||
|
HierarchicalSortMethod,
|
||||||
|
app_settings,
|
||||||
|
)
|
||||||
from ui.XslDependencyDialog_ui import Ui_XslDependencyDialog
|
from ui.XslDependencyDialog_ui import Ui_XslDependencyDialog
|
||||||
from xsl_dependencies import XslDependencyGraph
|
from xsl_dependencies import XslDependencyGraph
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_physics_options_dict(s: GraphLayoutSettings) -> str:
|
||||||
|
"""
|
||||||
|
Generiert den vis.js Physics/Layout-Optionsblock als JSON-String.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s: Aktuelle GraphLayoutSettings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-String für network.setOptions()
|
||||||
|
"""
|
||||||
|
if s.layout == GraphLayout.HIERARCHICAL:
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"physics": {"enabled": False},
|
||||||
|
"layout": {
|
||||||
|
"hierarchical": {
|
||||||
|
"enabled": True,
|
||||||
|
"direction": s.hi_direction.value,
|
||||||
|
"sortMethod": s.hi_sort_method.value,
|
||||||
|
"levelSeparation": s.hi_level_separation,
|
||||||
|
"nodeSpacing": s.hi_node_spacing,
|
||||||
|
"treeSpacing": s.hi_tree_spacing,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
physics_params: dict = {"solver": s.layout.value}
|
||||||
|
|
||||||
|
if s.layout == GraphLayout.BARNES_HUT:
|
||||||
|
physics_params["barnesHut"] = {
|
||||||
|
"gravitationalConstant": s.bh_gravitational_constant,
|
||||||
|
"centralGravity": s.bh_central_gravity,
|
||||||
|
"springLength": s.bh_spring_length,
|
||||||
|
"springConstant": s.bh_spring_constant,
|
||||||
|
"damping": s.bh_damping,
|
||||||
|
}
|
||||||
|
elif s.layout == GraphLayout.FORCE_ATLAS2:
|
||||||
|
physics_params["forceAtlas2Based"] = {
|
||||||
|
"gravitationalConstant": s.fa_gravitational_constant,
|
||||||
|
"centralGravity": s.fa_central_gravity,
|
||||||
|
"springLength": s.fa_spring_length,
|
||||||
|
"springConstant": s.fa_spring_constant,
|
||||||
|
"damping": s.fa_damping,
|
||||||
|
}
|
||||||
|
elif s.layout == GraphLayout.REPULSION:
|
||||||
|
physics_params["repulsion"] = {
|
||||||
|
"nodeDistance": s.re_node_distance,
|
||||||
|
"centralGravity": s.re_central_gravity,
|
||||||
|
"springLength": s.re_spring_length,
|
||||||
|
"springConstant": s.re_spring_constant,
|
||||||
|
"damping": s.re_damping,
|
||||||
|
}
|
||||||
|
|
||||||
|
physics_params["stabilization"] = {"enabled": True, "iterations": 200, "updateInterval": 25}
|
||||||
|
|
||||||
|
return json.dumps({"physics": physics_params, "layout": {"hierarchical": {"enabled": False}}})
|
||||||
|
|
||||||
|
|
||||||
|
def _build_set_options_js(s: GraphLayoutSettings) -> str:
|
||||||
|
"""
|
||||||
|
Generiert den JavaScript-Aufruf network.setOptions(...) als String.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s: Aktuelle GraphLayoutSettings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JavaScript-Code für runJavaScript()
|
||||||
|
"""
|
||||||
|
return f"network.setOptions({_build_physics_options_dict(s)});"
|
||||||
|
|
||||||
|
|
||||||
# Prüfe ob QWebEngineView verfügbar ist
|
# Prüfe ob QWebEngineView verfügbar ist
|
||||||
try:
|
try:
|
||||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
@@ -80,12 +170,15 @@ class XslDependencyDialog(QDialog):
|
|||||||
self.ui.fileTree.currentItemChanged.connect(self._on_file_selected)
|
self.ui.fileTree.currentItemChanged.connect(self._on_file_selected)
|
||||||
self.ui.searchEdit.textChanged.connect(self._on_search_changed)
|
self.ui.searchEdit.textChanged.connect(self._on_search_changed)
|
||||||
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
|
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
|
||||||
self.ui.graphFilterConnectedCheck.toggled.connect(
|
self.ui.graphFilterConnectedCheck.toggled.connect(lambda: self._on_search_changed(self.ui.searchEdit.text()))
|
||||||
lambda: self._on_search_changed(self.ui.searchEdit.text())
|
|
||||||
)
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
# Layout-Controls in graphSettingsPage einfügen und Einstellungen wiederherstellen
|
||||||
"""Räumt temporäre Dateien und QWebEngineView auf."""
|
self._build_layout_controls()
|
||||||
|
self._restore_graph_layout_settings()
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
"""Speichert Layout-Einstellungen und räumt temporäre Dateien und QWebEngineView auf."""
|
||||||
|
self._save_graph_layout_settings()
|
||||||
if self._web_view is not None:
|
if self._web_view is not None:
|
||||||
self._web_view.setUrl(QUrl("about:blank"))
|
self._web_view.setUrl(QUrl("about:blank"))
|
||||||
self._web_view.deleteLater()
|
self._web_view.deleteLater()
|
||||||
@@ -96,7 +189,7 @@ class XslDependencyDialog(QDialog):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._temp_html_file = None
|
self._temp_html_file = None
|
||||||
super().closeEvent(event)
|
super().reject()
|
||||||
|
|
||||||
def _build_reverse_map(self) -> dict[Path, set[Path]]:
|
def _build_reverse_map(self) -> dict[Path, set[Path]]:
|
||||||
"""Baut eine Reverse-Map auf: Welche Dateien importieren eine gegebene Datei?"""
|
"""Baut eine Reverse-Map auf: Welche Dateien importieren eine gegebene Datei?"""
|
||||||
@@ -241,6 +334,276 @@ class XslDependencyDialog(QDialog):
|
|||||||
self.ui.mainSplitter.setSizes([1000, 0])
|
self.ui.mainSplitter.setSizes([1000, 0])
|
||||||
self.ui.sidebarWidget.setVisible(False)
|
self.ui.sidebarWidget.setVisible(False)
|
||||||
|
|
||||||
|
# === Graph-Layout-Einstellungen ===
|
||||||
|
|
||||||
|
def _build_layout_controls(self):
|
||||||
|
"""Erstellt programmatisch alle Layout-Steuerelemente in graphSettingsPage."""
|
||||||
|
layout = self.ui.graphSettingsLayout
|
||||||
|
|
||||||
|
# Spacer am Ende entfernen (wird am Schluss wieder angehängt)
|
||||||
|
layout.removeItem(self.ui.graphSettingsSpacer)
|
||||||
|
|
||||||
|
# ComboBox für Layout-Auswahl
|
||||||
|
layout.addWidget(QLabel("Layout-Algorithmus:"))
|
||||||
|
|
||||||
|
self._layout_combo = QComboBox()
|
||||||
|
self._layout_combo.addItem("Barnes-Hut", GraphLayout.BARNES_HUT)
|
||||||
|
self._layout_combo.addItem("ForceAtlas2", GraphLayout.FORCE_ATLAS2)
|
||||||
|
self._layout_combo.addItem("Repulsion", GraphLayout.REPULSION)
|
||||||
|
self._layout_combo.addItem("Hierarchisch", GraphLayout.HIERARCHICAL)
|
||||||
|
layout.addWidget(self._layout_combo)
|
||||||
|
|
||||||
|
# StackedWidget für layout-spezifische Parameter
|
||||||
|
self._params_stack = QStackedWidget()
|
||||||
|
layout.addWidget(self._params_stack)
|
||||||
|
|
||||||
|
# Seite 0: barnesHut
|
||||||
|
self._params_stack.addWidget(self._build_barnes_hut_page())
|
||||||
|
|
||||||
|
# Seite 1: forceAtlas2Based
|
||||||
|
self._params_stack.addWidget(self._build_force_atlas2_page())
|
||||||
|
|
||||||
|
# Seite 2: repulsion
|
||||||
|
self._params_stack.addWidget(self._build_repulsion_page())
|
||||||
|
|
||||||
|
# Seite 3: hierarchical
|
||||||
|
self._params_stack.addWidget(self._build_hierarchical_page())
|
||||||
|
|
||||||
|
# Spacer wieder an den Schluss
|
||||||
|
layout.addItem(self.ui.graphSettingsSpacer)
|
||||||
|
|
||||||
|
# Signale verbinden — live Update bei jeder Änderung
|
||||||
|
self._layout_combo.currentIndexChanged.connect(self._on_layout_changed)
|
||||||
|
for spinbox in (
|
||||||
|
self._bh_gravitational,
|
||||||
|
self._bh_spring_length,
|
||||||
|
self._fa_gravitational,
|
||||||
|
self._fa_spring_length,
|
||||||
|
self._re_node_distance,
|
||||||
|
self._re_spring_length,
|
||||||
|
self._hi_level_separation,
|
||||||
|
self._hi_node_spacing,
|
||||||
|
self._hi_tree_spacing,
|
||||||
|
):
|
||||||
|
spinbox.valueChanged.connect(self._apply_layout_to_graph)
|
||||||
|
for dspinbox in (
|
||||||
|
self._bh_central_gravity,
|
||||||
|
self._bh_spring_constant,
|
||||||
|
self._bh_damping,
|
||||||
|
self._fa_central_gravity,
|
||||||
|
self._fa_spring_constant,
|
||||||
|
self._fa_damping,
|
||||||
|
self._re_central_gravity,
|
||||||
|
self._re_spring_constant,
|
||||||
|
self._re_damping,
|
||||||
|
):
|
||||||
|
dspinbox.valueChanged.connect(self._apply_layout_to_graph)
|
||||||
|
self._hi_direction.currentIndexChanged.connect(self._apply_layout_to_graph)
|
||||||
|
self._hi_sort_method.currentIndexChanged.connect(self._apply_layout_to_graph)
|
||||||
|
|
||||||
|
def _build_barnes_hut_page(self) -> QWidget:
|
||||||
|
"""Erstellt die Parameter-Seite für das Barnes-Hut-Layout."""
|
||||||
|
page = QWidget()
|
||||||
|
form = QFormLayout(page)
|
||||||
|
self._bh_gravitational = QSpinBox()
|
||||||
|
self._bh_gravitational.setRange(-30000, 0)
|
||||||
|
self._bh_gravitational.setSingleStep(100)
|
||||||
|
self._bh_central_gravity = QDoubleSpinBox()
|
||||||
|
self._bh_central_gravity.setRange(0.0, 5.0)
|
||||||
|
self._bh_central_gravity.setSingleStep(0.1)
|
||||||
|
self._bh_central_gravity.setDecimals(2)
|
||||||
|
self._bh_spring_length = QSpinBox()
|
||||||
|
self._bh_spring_length.setRange(1, 1000)
|
||||||
|
self._bh_spring_constant = QDoubleSpinBox()
|
||||||
|
self._bh_spring_constant.setRange(0.0, 1.0)
|
||||||
|
self._bh_spring_constant.setSingleStep(0.01)
|
||||||
|
self._bh_spring_constant.setDecimals(3)
|
||||||
|
self._bh_damping = QDoubleSpinBox()
|
||||||
|
self._bh_damping.setRange(0.0, 1.0)
|
||||||
|
self._bh_damping.setSingleStep(0.01)
|
||||||
|
self._bh_damping.setDecimals(2)
|
||||||
|
form.addRow("Gravitation:", self._bh_gravitational)
|
||||||
|
form.addRow("Zentrale Gravitation:", self._bh_central_gravity)
|
||||||
|
form.addRow("Federlänge:", self._bh_spring_length)
|
||||||
|
form.addRow("Federkonstante:", self._bh_spring_constant)
|
||||||
|
form.addRow("Dämpfung:", self._bh_damping)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def _build_force_atlas2_page(self) -> QWidget:
|
||||||
|
"""Erstellt die Parameter-Seite für das ForceAtlas2-Layout."""
|
||||||
|
page = QWidget()
|
||||||
|
form = QFormLayout(page)
|
||||||
|
self._fa_gravitational = QSpinBox()
|
||||||
|
self._fa_gravitational.setRange(-10000, 0)
|
||||||
|
self._fa_gravitational.setSingleStep(10)
|
||||||
|
self._fa_central_gravity = QDoubleSpinBox()
|
||||||
|
self._fa_central_gravity.setRange(0.0, 5.0)
|
||||||
|
self._fa_central_gravity.setSingleStep(0.01)
|
||||||
|
self._fa_central_gravity.setDecimals(3)
|
||||||
|
self._fa_spring_length = QSpinBox()
|
||||||
|
self._fa_spring_length.setRange(1, 1000)
|
||||||
|
self._fa_spring_constant = QDoubleSpinBox()
|
||||||
|
self._fa_spring_constant.setRange(0.0, 1.0)
|
||||||
|
self._fa_spring_constant.setSingleStep(0.01)
|
||||||
|
self._fa_spring_constant.setDecimals(3)
|
||||||
|
self._fa_damping = QDoubleSpinBox()
|
||||||
|
self._fa_damping.setRange(0.0, 1.0)
|
||||||
|
self._fa_damping.setSingleStep(0.01)
|
||||||
|
self._fa_damping.setDecimals(2)
|
||||||
|
form.addRow("Gravitation:", self._fa_gravitational)
|
||||||
|
form.addRow("Zentrale Gravitation:", self._fa_central_gravity)
|
||||||
|
form.addRow("Federlänge:", self._fa_spring_length)
|
||||||
|
form.addRow("Federkonstante:", self._fa_spring_constant)
|
||||||
|
form.addRow("Dämpfung:", self._fa_damping)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def _build_repulsion_page(self) -> QWidget:
|
||||||
|
"""Erstellt die Parameter-Seite für das Repulsion-Layout."""
|
||||||
|
page = QWidget()
|
||||||
|
form = QFormLayout(page)
|
||||||
|
self._re_node_distance = QSpinBox()
|
||||||
|
self._re_node_distance.setRange(1, 1000)
|
||||||
|
self._re_central_gravity = QDoubleSpinBox()
|
||||||
|
self._re_central_gravity.setRange(0.0, 5.0)
|
||||||
|
self._re_central_gravity.setSingleStep(0.1)
|
||||||
|
self._re_central_gravity.setDecimals(2)
|
||||||
|
self._re_spring_length = QSpinBox()
|
||||||
|
self._re_spring_length.setRange(1, 1000)
|
||||||
|
self._re_spring_constant = QDoubleSpinBox()
|
||||||
|
self._re_spring_constant.setRange(0.0, 1.0)
|
||||||
|
self._re_spring_constant.setSingleStep(0.01)
|
||||||
|
self._re_spring_constant.setDecimals(3)
|
||||||
|
self._re_damping = QDoubleSpinBox()
|
||||||
|
self._re_damping.setRange(0.0, 1.0)
|
||||||
|
self._re_damping.setSingleStep(0.01)
|
||||||
|
self._re_damping.setDecimals(2)
|
||||||
|
form.addRow("Knotenabstand:", self._re_node_distance)
|
||||||
|
form.addRow("Zentrale Gravitation:", self._re_central_gravity)
|
||||||
|
form.addRow("Federlänge:", self._re_spring_length)
|
||||||
|
form.addRow("Federkonstante:", self._re_spring_constant)
|
||||||
|
form.addRow("Dämpfung:", self._re_damping)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def _build_hierarchical_page(self) -> QWidget:
|
||||||
|
"""Erstellt die Parameter-Seite für das hierarchische Layout."""
|
||||||
|
page = QWidget()
|
||||||
|
form = QFormLayout(page)
|
||||||
|
self._hi_direction = QComboBox()
|
||||||
|
self._hi_direction.addItem("Oben → Unten", HierarchicalDirection.UD)
|
||||||
|
self._hi_direction.addItem("Unten → Oben", HierarchicalDirection.DU)
|
||||||
|
self._hi_direction.addItem("Links → Rechts", HierarchicalDirection.LR)
|
||||||
|
self._hi_direction.addItem("Rechts → Links", HierarchicalDirection.RL)
|
||||||
|
self._hi_sort_method = QComboBox()
|
||||||
|
self._hi_sort_method.addItem("Hubsize", HierarchicalSortMethod.HUBSIZE)
|
||||||
|
self._hi_sort_method.addItem("Gerichtet", HierarchicalSortMethod.DIRECTED)
|
||||||
|
self._hi_level_separation = QSpinBox()
|
||||||
|
self._hi_level_separation.setRange(1, 1000)
|
||||||
|
self._hi_node_spacing = QSpinBox()
|
||||||
|
self._hi_node_spacing.setRange(1, 1000)
|
||||||
|
self._hi_tree_spacing = QSpinBox()
|
||||||
|
self._hi_tree_spacing.setRange(1, 1000)
|
||||||
|
form.addRow("Richtung:", self._hi_direction)
|
||||||
|
form.addRow("Sortierung:", self._hi_sort_method)
|
||||||
|
form.addRow("Ebenenabstand:", self._hi_level_separation)
|
||||||
|
form.addRow("Knotenabstand:", self._hi_node_spacing)
|
||||||
|
form.addRow("Baumabstand:", self._hi_tree_spacing)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def _on_layout_changed(self, index: int):
|
||||||
|
"""Wechselt die sichtbare Parameter-Seite und wendet das Layout sofort an."""
|
||||||
|
self._params_stack.setCurrentIndex(index)
|
||||||
|
self._apply_layout_to_graph()
|
||||||
|
|
||||||
|
def _current_layout_settings(self) -> GraphLayoutSettings:
|
||||||
|
"""Liest alle aktuellen Steuerelement-Werte aus und gibt ein GraphLayoutSettings-Objekt zurück."""
|
||||||
|
return GraphLayoutSettings(
|
||||||
|
layout=self._layout_combo.currentData(),
|
||||||
|
bh_gravitational_constant=self._bh_gravitational.value(),
|
||||||
|
bh_central_gravity=self._bh_central_gravity.value(),
|
||||||
|
bh_spring_length=self._bh_spring_length.value(),
|
||||||
|
bh_spring_constant=self._bh_spring_constant.value(),
|
||||||
|
bh_damping=self._bh_damping.value(),
|
||||||
|
fa_gravitational_constant=self._fa_gravitational.value(),
|
||||||
|
fa_central_gravity=self._fa_central_gravity.value(),
|
||||||
|
fa_spring_length=self._fa_spring_length.value(),
|
||||||
|
fa_spring_constant=self._fa_spring_constant.value(),
|
||||||
|
fa_damping=self._fa_damping.value(),
|
||||||
|
re_node_distance=self._re_node_distance.value(),
|
||||||
|
re_central_gravity=self._re_central_gravity.value(),
|
||||||
|
re_spring_length=self._re_spring_length.value(),
|
||||||
|
re_spring_constant=self._re_spring_constant.value(),
|
||||||
|
re_damping=self._re_damping.value(),
|
||||||
|
hi_direction=self._hi_direction.currentData(),
|
||||||
|
hi_sort_method=self._hi_sort_method.currentData(),
|
||||||
|
hi_level_separation=self._hi_level_separation.value(),
|
||||||
|
hi_node_spacing=self._hi_node_spacing.value(),
|
||||||
|
hi_tree_spacing=self._hi_tree_spacing.value(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _restore_graph_layout_settings(self):
|
||||||
|
"""Stellt die gespeicherten Layout-Einstellungen aus app_settings wieder her."""
|
||||||
|
s = app_settings.graph_layout_settings
|
||||||
|
|
||||||
|
# ComboBox: Layout auswählen (Signale blockieren, um Mehrfach-Updates zu vermeiden)
|
||||||
|
self._layout_combo.blockSignals(True)
|
||||||
|
for i in range(self._layout_combo.count()):
|
||||||
|
if self._layout_combo.itemData(i) == s.layout:
|
||||||
|
self._layout_combo.setCurrentIndex(i)
|
||||||
|
self._params_stack.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
self._layout_combo.blockSignals(False)
|
||||||
|
|
||||||
|
# barnesHut
|
||||||
|
self._bh_gravitational.setValue(s.bh_gravitational_constant)
|
||||||
|
self._bh_central_gravity.setValue(s.bh_central_gravity)
|
||||||
|
self._bh_spring_length.setValue(s.bh_spring_length)
|
||||||
|
self._bh_spring_constant.setValue(s.bh_spring_constant)
|
||||||
|
self._bh_damping.setValue(s.bh_damping)
|
||||||
|
|
||||||
|
# forceAtlas2Based
|
||||||
|
self._fa_gravitational.setValue(s.fa_gravitational_constant)
|
||||||
|
self._fa_central_gravity.setValue(s.fa_central_gravity)
|
||||||
|
self._fa_spring_length.setValue(s.fa_spring_length)
|
||||||
|
self._fa_spring_constant.setValue(s.fa_spring_constant)
|
||||||
|
self._fa_damping.setValue(s.fa_damping)
|
||||||
|
|
||||||
|
# repulsion
|
||||||
|
self._re_node_distance.setValue(s.re_node_distance)
|
||||||
|
self._re_central_gravity.setValue(s.re_central_gravity)
|
||||||
|
self._re_spring_length.setValue(s.re_spring_length)
|
||||||
|
self._re_spring_constant.setValue(s.re_spring_constant)
|
||||||
|
self._re_damping.setValue(s.re_damping)
|
||||||
|
|
||||||
|
# hierarchical
|
||||||
|
for i in range(self._hi_direction.count()):
|
||||||
|
if self._hi_direction.itemData(i) == s.hi_direction:
|
||||||
|
self._hi_direction.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
for i in range(self._hi_sort_method.count()):
|
||||||
|
if self._hi_sort_method.itemData(i) == s.hi_sort_method:
|
||||||
|
self._hi_sort_method.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
self._hi_level_separation.setValue(s.hi_level_separation)
|
||||||
|
self._hi_node_spacing.setValue(s.hi_node_spacing)
|
||||||
|
self._hi_tree_spacing.setValue(s.hi_tree_spacing)
|
||||||
|
|
||||||
|
logger.debug("Graph-Layout-Einstellungen wiederhergestellt: %s", s.layout)
|
||||||
|
|
||||||
|
def _apply_layout_to_graph(self):
|
||||||
|
"""Überträgt die aktuellen Layout-Einstellungen per JavaScript an den vis.js Graph."""
|
||||||
|
if not self._graph_loaded or self._web_view is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
s = self._current_layout_settings()
|
||||||
|
self._web_view.page().runJavaScript(_build_set_options_js(s))
|
||||||
|
logger.debug("Layout angewendet: %s", s.layout)
|
||||||
|
|
||||||
|
def _save_graph_layout_settings(self):
|
||||||
|
"""Speichert die aktuellen Layout-Einstellungen in app_settings und persistiert sie."""
|
||||||
|
app_settings.graph_layout_settings = self._current_layout_settings()
|
||||||
|
app_settings.save()
|
||||||
|
logger.info("Graph-Layout-Einstellungen gespeichert: %s", app_settings.graph_layout_settings.layout)
|
||||||
|
|
||||||
# === Netzwerkgraph (vis.js) ===
|
# === Netzwerkgraph (vis.js) ===
|
||||||
|
|
||||||
def _on_tab_changed(self, index: int):
|
def _on_tab_changed(self, index: int):
|
||||||
@@ -274,10 +637,16 @@ class XslDependencyDialog(QDialog):
|
|||||||
self._temp_html_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", encoding="utf-8", delete=False)
|
self._temp_html_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", encoding="utf-8", delete=False)
|
||||||
self._temp_html_file.write(html)
|
self._temp_html_file.write(html)
|
||||||
self._temp_html_file.close()
|
self._temp_html_file.close()
|
||||||
|
self._web_view.loadFinished.connect(self._on_graph_load_finished)
|
||||||
self._web_view.setUrl(QUrl.fromLocalFile(self._temp_html_file.name))
|
self._web_view.setUrl(QUrl.fromLocalFile(self._temp_html_file.name))
|
||||||
self._graph_loaded = True
|
|
||||||
|
|
||||||
logger.info("Netzwerkgraph geladen")
|
def _on_graph_load_finished(self, ok: bool):
|
||||||
|
"""Wird aufgerufen, wenn die vis.js-Seite vollständig geladen ist."""
|
||||||
|
if ok:
|
||||||
|
self._graph_loaded = True
|
||||||
|
logger.info("Netzwerkgraph vollständig geladen")
|
||||||
|
else:
|
||||||
|
logger.error("Netzwerkgraph konnte nicht geladen werden")
|
||||||
|
|
||||||
def _build_graph_data(self) -> tuple[str, str]:
|
def _build_graph_data(self) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
@@ -428,21 +797,6 @@ class XslDependencyDialog(QDialog):
|
|||||||
}},
|
}},
|
||||||
smooth: {{ type: 'continuous' }}
|
smooth: {{ type: 'continuous' }}
|
||||||
}},
|
}},
|
||||||
physics: {{
|
|
||||||
solver: 'barnesHut',
|
|
||||||
barnesHut: {{
|
|
||||||
gravitationalConstant: -3000,
|
|
||||||
centralGravity: 0.3,
|
|
||||||
springLength: 150,
|
|
||||||
springConstant: 0.04,
|
|
||||||
damping: 0.09
|
|
||||||
}},
|
|
||||||
stabilization: {{
|
|
||||||
enabled: true,
|
|
||||||
iterations: 200,
|
|
||||||
updateInterval: 25
|
|
||||||
}}
|
|
||||||
}},
|
|
||||||
interaction: {{
|
interaction: {{
|
||||||
hover: true,
|
hover: true,
|
||||||
tooltipDelay: 200,
|
tooltipDelay: 200,
|
||||||
@@ -451,6 +805,9 @@ class XslDependencyDialog(QDialog):
|
|||||||
}}
|
}}
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
// Layout-Einstellungen VOR Netzwerk-Erstellung in options einmischen
|
||||||
|
Object.assign(options, {_build_physics_options_dict(app_settings.graph_layout_settings)});
|
||||||
|
|
||||||
var network = new vis.Network(container, data, options);
|
var network = new vis.Network(container, data, options);
|
||||||
|
|
||||||
// Nachbar-Hervorhebung bei Klick
|
// Nachbar-Hervorhebung bei Klick
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "documentor"
|
name = "documentor"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "connectorx" },
|
{ name = "connectorx" },
|
||||||
|
|||||||
Reference in New Issue
Block a user