mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat(qt6): visual workflow canvas with infinite pan/zoom, Bezier connections, 152-node palette
Replace list-based WorkflowEditor with spatial infinite canvas: - NodeRegistry C++ class loads 152 node types from node-registry.json - 5000x5000 Flickable canvas with grid, Scale transform zoom (0.25x-2x) - Draggable nodes with group-colored headers, input/output ports - Bezier connection drawing via Canvas 2D (n8n adjacency map format) - Interactive port-to-port connection creation with drag preview - Searchable/filterable node palette with group chips - Properties panel with schema-driven parameter editing - Full n8n-style JSON: nodes, connections, variables, meta, tags - DBAL integration preserved with mock workflow fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,30 @@
|
||||
# AUTO-GENERATED by generate_cmake.py — do not edit manually
|
||||
# Generated from cmake_config.json | 91 QML files, 6 C++ sources, 22 SVGs, 1 audio assets
|
||||
#
|
||||
# Discovered packages:
|
||||
# analytics v1.0.0 - Analytics Studio
|
||||
# blog v1.0.0 - Blog
|
||||
# breakout v1.0.0 - Breakout
|
||||
# connection_hub v1.0.0 - Connection Hub
|
||||
# escape_room v1.0.0 - Escape Room
|
||||
# forum v1.0.0 - Community Forum
|
||||
# frontpage v1.0.0 - Frontpage Experience
|
||||
# gallery v1.0.0 - Gallery
|
||||
# god_panel v1.0.0 - God Panel
|
||||
# guestbook v1.0.0 - Guestbook
|
||||
# login v1.0.0 - Login Shell
|
||||
# marketplace v1.0.0 - Marketplace
|
||||
# microthread v1.0.0 - MicroThread
|
||||
# music_player v1.0.0 - Music Player
|
||||
# package_manager v1.0.0 - Package Manager
|
||||
# profile_page v1.0.0 - Profile Page
|
||||
# retro_games v1.0.0 - Retro Arcade
|
||||
# snake_game v1.0.0 - Snake Game
|
||||
# storybook v1.0.0 - Storybook Showcase
|
||||
# supergod_panel v1.0.0 - SuperGod Panel
|
||||
# user_settings v1.0.0 - User Settings
|
||||
# watchtower v1.0.0 - Watchtower
|
||||
|
||||
cmake_minimum_required(VERSION 3.27)
|
||||
project(dbal_qml VERSION 0.1 LANGUAGES CXX)
|
||||
|
||||
@@ -18,54 +45,118 @@ qt_policy(SET QTP0001 NEW)
|
||||
|
||||
qt_add_executable(dbal-qml
|
||||
main.cpp
|
||||
src/PackageRegistry.cpp
|
||||
src/ModPlayer.cpp
|
||||
src/DBALClient.cpp
|
||||
src/ModPlayer.cpp
|
||||
src/NodeRegistry.cpp
|
||||
src/PackageLoader.cpp
|
||||
src/PackageRegistry.cpp
|
||||
)
|
||||
|
||||
# Pass source dir so the binary can find shared QML components at runtime
|
||||
target_compile_definitions(dbal-qml PRIVATE SRCDIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
qt_add_qml_module(dbal-qml
|
||||
URI DBALObservatory
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
App.qml
|
||||
FrontPage.qml
|
||||
LoginView.qml
|
||||
DashboardView.qml
|
||||
AdminView.qml
|
||||
PackageViewLoader.qml
|
||||
ProfileView.qml
|
||||
App.qml
|
||||
CommentsView.qml
|
||||
GodPanel.qml
|
||||
SchemaEditor.qml
|
||||
WorkflowEditor.qml
|
||||
LuaEditor.qml
|
||||
DatabaseManager.qml
|
||||
PageRoutesManager.qml
|
||||
ComponentHierarchyEditor.qml
|
||||
CssClassManager.qml
|
||||
DashboardView.qml
|
||||
DatabaseManager.qml
|
||||
DropdownConfigManager.qml
|
||||
UserManagement.qml
|
||||
ThemeEditor.qml
|
||||
SMTPConfigEditor.qml
|
||||
SuperGodPanel.qml
|
||||
FrontPage.qml
|
||||
GodPanel.qml
|
||||
LoginView.qml
|
||||
LuaEditor.qml
|
||||
MaterialLanding.qml
|
||||
ModPlayerPanel.qml
|
||||
PackageManager.qml
|
||||
Storybook.qml
|
||||
MediaServicePanel.qml
|
||||
ModPlayerPanel.qml
|
||||
NotificationsPanel.qml
|
||||
PackageManager.qml
|
||||
PackageViewLoader.qml
|
||||
PageRoutesManager.qml
|
||||
ProfileView.qml
|
||||
SMTPConfigEditor.qml
|
||||
SchemaEditor.qml
|
||||
SettingsView.qml
|
||||
Storybook.qml
|
||||
SuperGodPanel.qml
|
||||
ThemeEditor.qml
|
||||
UserManagement.qml
|
||||
WorkflowEditor.qml
|
||||
qmllib/Material/MaterialAccordion.qml
|
||||
qmllib/Material/MaterialAlert.qml
|
||||
qmllib/Material/MaterialAppBar.qml
|
||||
qmllib/Material/MaterialAvatar.qml
|
||||
qmllib/Material/MaterialBadge.qml
|
||||
qmllib/Material/MaterialBox.qml
|
||||
qmllib/Material/MaterialButton.qml
|
||||
qmllib/Material/MaterialCard.qml
|
||||
qmllib/Material/MaterialCheckbox.qml
|
||||
qmllib/Material/MaterialChip.qml
|
||||
qmllib/Material/MaterialCircularProgress.qml
|
||||
qmllib/Material/MaterialCollapse.qml
|
||||
qmllib/Material/MaterialContainer.qml
|
||||
qmllib/Material/MaterialDialog.qml
|
||||
qmllib/Material/MaterialDivider.qml
|
||||
qmllib/Material/MaterialDividerProps.qml
|
||||
qmllib/Material/MaterialGrid.qml
|
||||
qmllib/Material/MaterialIconButton.qml
|
||||
qmllib/Material/MaterialLinearProgress.qml
|
||||
qmllib/Material/MaterialLink.qml
|
||||
qmllib/Material/MaterialMenu.qml
|
||||
qmllib/Material/MaterialMenuItem.qml
|
||||
qmllib/Material/MaterialMenuProps.qml
|
||||
qmllib/Material/MaterialPalette.qml
|
||||
qmllib/Material/MaterialPaper.qml
|
||||
qmllib/Material/MaterialPopover.qml
|
||||
qmllib/Material/MaterialPopoverProps.qml
|
||||
qmllib/Material/MaterialSkeleton.qml
|
||||
qmllib/Material/MaterialSnackbar.qml
|
||||
qmllib/Material/MaterialSurface.qml
|
||||
qmllib/Material/MaterialSwitch.qml
|
||||
qmllib/Material/MaterialTextField.qml
|
||||
qmllib/Material/MaterialToolbar.qml
|
||||
qmllib/Material/MaterialTypography.qml
|
||||
qmllib/MetaBuilder/ContactForm.qml
|
||||
qmllib/MetaBuilder/FeatureCard.qml
|
||||
qmllib/MetaBuilder/HeroSection.qml
|
||||
qmllib/MetaBuilder/NavBar.qml
|
||||
qmllib/MetaBuilder/StatusCard.qml
|
||||
qmllib/MetaBuilder/WorkflowNode.qml
|
||||
qmllib/dbal/DBALProvider.qml
|
||||
packages/analytics/PackageView.qml
|
||||
packages/blog/PackageView.qml
|
||||
packages/breakout/PackageView.qml
|
||||
packages/connection-hub/PackageView.qml
|
||||
packages/escape-room/PackageView.qml
|
||||
packages/forum/PackageView.qml
|
||||
packages/frontpage/PackageView.qml
|
||||
packages/gallery/PackageView.qml
|
||||
packages/god-panel/PackageView.qml
|
||||
packages/guestbook/PackageView.qml
|
||||
packages/login/PackageView.qml
|
||||
packages/marketplace/PackageView.qml
|
||||
packages/microthread/PackageView.qml
|
||||
packages/music-player/PackageView.qml
|
||||
packages/package-manager/PackageView.qml
|
||||
packages/profile-page/PackageView.qml
|
||||
packages/retro-games/PackageView.qml
|
||||
packages/snake-game/PackageView.qml
|
||||
packages/storybook/PackageView.qml
|
||||
packages/supergod-panel/PackageView.qml
|
||||
packages/user-settings/PackageView.qml
|
||||
packages/watchtower/PackageView.qml
|
||||
RESOURCES
|
||||
assets/audio/retro-gaming.mod
|
||||
qmllib/Material/qmldir
|
||||
qmllib/MetaBuilder/qmldir
|
||||
qmllib/dbal/qmldir
|
||||
)
|
||||
|
||||
# Add SVG assets
|
||||
# SVG assets
|
||||
file(GLOB SVG_ASSETS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} assets/svg/*.svg)
|
||||
qt_add_resources(dbal-qml "svg_assets"
|
||||
PREFIX "/"
|
||||
@@ -76,9 +167,7 @@ target_link_libraries(dbal-qml PRIVATE
|
||||
Qt6::Core Qt6::Gui Qt6::Quick Qt6::Qml Qt6::Network
|
||||
)
|
||||
|
||||
# Conan Qt recipe adds module-specific include dirs to CMAKE_INCLUDE_PATH
|
||||
# but MSVC needs them as actual compiler include paths.
|
||||
# Add the Qt root include + all module subdirs from CMAKE_INCLUDE_PATH.
|
||||
# Conan Qt recipe: propagate CMAKE_INCLUDE_PATH entries for MSVC
|
||||
foreach(_inc ${CMAKE_INCLUDE_PATH})
|
||||
target_include_directories(dbal-qml PRIVATE "${_inc}")
|
||||
endforeach()
|
||||
|
||||
107
frontends/qt6/PLAN.md
Normal file
107
frontends/qt6/PLAN.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Phase 9: Visual Workflow Canvas + Frontend Parity
|
||||
|
||||
**Status**: In Progress
|
||||
**Started**: 2026-03-19
|
||||
|
||||
---
|
||||
|
||||
## Part A: QML Workflow Canvas (PRIMARY)
|
||||
|
||||
Replace the list-based WorkflowEditor with a spatial infinite canvas.
|
||||
|
||||
### Steps
|
||||
|
||||
- [x] **Step 1: NodeRegistry C++ class** ✓
|
||||
- New `src/NodeRegistry.h` + `src/NodeRegistry.cpp`
|
||||
- Reads `workflow/plugins/registry/node-registry.json`
|
||||
- Exposes `nodeTypes()`, `groups()`, `nodeType(name)`, `nodesByGroup()`, `searchNodes()` to QML
|
||||
- Registered as context property `NodeRegistry` in `main.cpp`
|
||||
- Auto-discovered by `generate_cmake.py` (6 C++ sources total)
|
||||
|
||||
- [x] **Step 2: Canvas Infrastructure** ✓
|
||||
- `Flickable` 5000x5000 with grid background
|
||||
- `Scale` transform for zoom (0.25x–2x)
|
||||
- Mousewheel zoom, zoom overlay with +/- buttons
|
||||
- `Canvas` 2D connection layer + `Repeater` node layer
|
||||
|
||||
- [x] **Step 3: WorkflowNode Component** ✓
|
||||
- `qmllib/MetaBuilder/WorkflowNode.qml` with DragHandler
|
||||
- Colored header by group (prefix-based color mapping)
|
||||
- Input ports (left, blue), output ports (right, green)
|
||||
- Click-to-select with visual highlight
|
||||
- Inline node delegates in WorkflowEditor for tight integration
|
||||
|
||||
- [x] **Step 4: Bezier Connection Drawing** ✓
|
||||
- QML `Canvas` with `context.bezierCurveTo()`
|
||||
- Parses n8n-style `connections` adjacency map
|
||||
- Control points offset 40% of horizontal distance
|
||||
- Arrow heads at destination ports
|
||||
- Dashed line for connection being drawn
|
||||
- `requestPaint()` on node drag and connection changes
|
||||
|
||||
- [x] **Step 5: Node Palette (Left Sidebar)** ✓
|
||||
- ListView from `NodeRegistry.nodeTypes` filtered by search + group
|
||||
- Group filter chips (All, core, logic, transform, integration, etc.)
|
||||
- Double-click to add at canvas center
|
||||
- `Drag.active` + `DropArea` for drag-to-canvas
|
||||
|
||||
- [x] **Step 6: Properties Panel (Right Sidebar)** ✓
|
||||
- Animated slide-in panel (300px)
|
||||
- Name (editable), Type (badge with group color)
|
||||
- Parameters from `NodeRegistry.nodeType()` schema
|
||||
- Dynamic text fields and dropdowns for property options
|
||||
- Input/output port display with chips
|
||||
- Workflow variables display
|
||||
- Position readout, Delete button
|
||||
|
||||
- [x] **Step 7: Workflow I/O** ✓
|
||||
- Full n8n-style JSON: name, active, settings, tags, meta, variables, nodes, connections
|
||||
- DBAL load/save via `DBALProvider.list/create/update/remove`
|
||||
- Mock workflows with realistic graph layouts as fallback
|
||||
- Add/remove nodes + connections with proper cleanup
|
||||
|
||||
### Files
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `WorkflowEditor.qml` | Full rewrite — spatial canvas |
|
||||
| `qmllib/MetaBuilder/WorkflowNode.qml` | New — draggable node component |
|
||||
| `src/NodeRegistry.h` | New — C++ node type loader |
|
||||
| `src/NodeRegistry.cpp` | New — implementation |
|
||||
| `main.cpp` | Register NodeRegistry context property |
|
||||
| `CMakeLists.txt` | Add NodeRegistry to sources |
|
||||
|
||||
### Verification
|
||||
|
||||
1. Build compiles clean
|
||||
2. God Panel → Workflows tab shows canvas
|
||||
3. Pan (scroll) + zoom (Ctrl+scroll) works
|
||||
4. Load `seed_game.json` (58 nodes) renders correctly
|
||||
5. Drag from palette → node on canvas
|
||||
6. Port-to-port connection drawing
|
||||
7. Properties panel for selected node
|
||||
8. Save roundtrip to DBAL
|
||||
|
||||
---
|
||||
|
||||
## Part B: Next.js Frontend Alignment (LATER)
|
||||
|
||||
- [ ] Level-based layout (5 levels matching `old/`)
|
||||
- [ ] Sidebar navigation matching Qt6
|
||||
- [ ] God Panel tabs
|
||||
- [ ] Wire in workflowui editor
|
||||
|
||||
## Part C: CLI Feature Parity (LATER)
|
||||
|
||||
- [ ] `workflow list/get/run/create` commands
|
||||
- [ ] `package list/install/uninstall` commands
|
||||
|
||||
---
|
||||
|
||||
## Reusable Code
|
||||
|
||||
- `DBALProvider.qml` — DBAL REST client (keep as-is)
|
||||
- `PackageLoader` C++ pattern — template for NodeRegistry
|
||||
- `node-registry.json` — 152 node types
|
||||
- `components/workflow-editor/ConnectionLine.tsx` — Bezier math reference
|
||||
- `gameengine/packages/seed/workflows/*.json` — test data
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
#include "src/ModPlayer.h"
|
||||
#include "src/DBALClient.h"
|
||||
#include "src/PackageLoader.h"
|
||||
#include "src/NodeRegistry.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QGuiApplication app(argc, argv);
|
||||
@@ -16,15 +17,20 @@ int main(int argc, char *argv[]) {
|
||||
// Add shared QML component library path
|
||||
// Resolves: import QmlComponents 1.0
|
||||
const auto appDir = QCoreApplication::applicationDirPath();
|
||||
const QStringList qmlPaths = {
|
||||
appDir + "/../../qml",
|
||||
appDir + "/../../../qml",
|
||||
appDir + "/../../../../qml",
|
||||
QDir::cleanPath(QStringLiteral(SRCDIR) + "/../../qml")
|
||||
// Qt6 resolves "import QmlComponents" by looking for a QmlComponents/ dir
|
||||
// inside each import path. We symlink or reference the parent of qml/.
|
||||
const QStringList qmlParentPaths = {
|
||||
appDir + "/../../",
|
||||
appDir + "/../../../",
|
||||
appDir + "/../../../../",
|
||||
QDir::cleanPath(QStringLiteral(SRCDIR) + "/../..")
|
||||
};
|
||||
for (const auto &path : qmlPaths) {
|
||||
if (QDir(path).exists()) {
|
||||
engine.addImportPath(QDir(path).absolutePath());
|
||||
for (const auto &path : qmlParentPaths) {
|
||||
const QString candidate = QDir(path).absolutePath();
|
||||
// Check if QmlComponents symlink or qml/ dir with qmldir exists
|
||||
if (QDir(candidate + "/QmlComponents").exists()
|
||||
|| QDir(candidate + "/qml").exists()) {
|
||||
engine.addImportPath(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -33,16 +39,24 @@ int main(int argc, char *argv[]) {
|
||||
ModPlayer modPlayer;
|
||||
DBALClient dbalClient;
|
||||
PackageLoader packageLoader;
|
||||
NodeRegistry nodeRegistry;
|
||||
registry.loadPackage("frontpage");
|
||||
packageLoader.setPackagesDir(QDir(QStringLiteral(SRCDIR) + QStringLiteral("/packages")).absolutePath());
|
||||
packageLoader.scan();
|
||||
packageLoader.setWatching(true);
|
||||
|
||||
// Load workflow node type registry
|
||||
const QString registryPath = QDir::cleanPath(
|
||||
QStringLiteral(SRCDIR) + QStringLiteral("/../../workflow/plugins/registry/node-registry.json"));
|
||||
nodeRegistry.loadRegistry(registryPath);
|
||||
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("PackageRegistry"), ®istry);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("ModPlayer"), &modPlayer);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("DBALClient"), &dbalClient);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("PackageLoader"), &packageLoader);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("NodeRegistry"), &nodeRegistry);
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/DBALObservatory/App.qml"));
|
||||
const QUrl url(QStringLiteral("qrc:/qt/qml/DBALObservatory/App.qml"));
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||
&app, [url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && objUrl == url)
|
||||
|
||||
214
frontends/qt6/qmllib/MetaBuilder/WorkflowNode.qml
Normal file
214
frontends/qt6/qmllib/MetaBuilder/WorkflowNode.qml
Normal file
@@ -0,0 +1,214 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: nodeRoot
|
||||
|
||||
property string nodeId: ""
|
||||
property string nodeName: "Node"
|
||||
property string nodeType: "action"
|
||||
property string displayName: ""
|
||||
property var nodeInputs: []
|
||||
property var nodeOutputs: []
|
||||
property var parameters: ({})
|
||||
property real nodeX: 0
|
||||
property real nodeY: 0
|
||||
property bool selected: false
|
||||
property real zoom: 1.0
|
||||
|
||||
// Port geometry constants
|
||||
readonly property int portRadius: 6
|
||||
readonly property int portSpacing: 24
|
||||
readonly property int headerHeight: 32
|
||||
readonly property int minWidth: 180
|
||||
readonly property int minHeight: headerHeight + Math.max(nodeInputs.length, nodeOutputs.length) * portSpacing + 16
|
||||
|
||||
signal moved(string id, real newX, real newY)
|
||||
signal clicked(string id)
|
||||
signal doubleClicked(string id)
|
||||
signal portPressed(string nodeId, string portName, string portType, bool isOutput, real globalX, real globalY)
|
||||
signal portReleased(string nodeId, string portName, string portType, bool isOutput, real globalX, real globalY)
|
||||
|
||||
x: nodeX
|
||||
y: nodeY
|
||||
width: minWidth
|
||||
height: minHeight
|
||||
radius: 8
|
||||
color: selected ? Qt.lighter(Theme.paper, 1.1) : Theme.paper
|
||||
border.color: selected ? groupColor() : Theme.border
|
||||
border.width: selected ? 2 : 1
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: null
|
||||
|
||||
function groupColor() {
|
||||
switch (nodeType.split(".")[0]) {
|
||||
case "metabuilder": return Theme.success
|
||||
case "logic": return Theme.warning
|
||||
case "transform":
|
||||
case "packagerepo": return "#FF9800"
|
||||
case "sdl":
|
||||
case "graphics": return "#2196F3"
|
||||
case "integration": return "#9C27B0"
|
||||
case "io": return "#00BCD4"
|
||||
default: return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
// Header bar
|
||||
Rectangle {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: headerHeight
|
||||
radius: 8
|
||||
color: groupColor()
|
||||
|
||||
// Square off bottom corners
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: parent.radius
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
text: displayName || nodeName
|
||||
color: "#FFFFFF"
|
||||
variant: "body2"
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 16
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Drag handler on the header
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
target: nodeRoot
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
nodeRoot.moved(nodeId, nodeRoot.x, nodeRoot.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click handler
|
||||
TapHandler {
|
||||
onTapped: nodeRoot.clicked(nodeId)
|
||||
onDoubleTapped: nodeRoot.doubleClicked(nodeId)
|
||||
}
|
||||
|
||||
// Input ports (left side)
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: -portRadius
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 8
|
||||
spacing: portSpacing - portRadius * 2
|
||||
|
||||
Repeater {
|
||||
model: nodeInputs
|
||||
delegate: Item {
|
||||
width: portRadius * 2 + 60
|
||||
height: portRadius * 2
|
||||
|
||||
Rectangle {
|
||||
id: inputPort
|
||||
width: portRadius * 2
|
||||
height: portRadius * 2
|
||||
radius: portRadius
|
||||
color: Theme.primary
|
||||
border.color: Theme.background
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.CrossCursor
|
||||
|
||||
onPressed: function(mouse) {
|
||||
var global = inputPort.mapToItem(null, portRadius, portRadius)
|
||||
nodeRoot.portPressed(nodeId, modelData.name, modelData.type, false, global.x, global.y)
|
||||
}
|
||||
onReleased: function(mouse) {
|
||||
var global = inputPort.mapToItem(null, portRadius, portRadius)
|
||||
nodeRoot.portReleased(nodeId, modelData.name, modelData.type, false, global.x, global.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
anchors.left: inputPort.right
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: inputPort.verticalCenter
|
||||
text: modelData.displayName || modelData.name || "in"
|
||||
variant: "caption"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output ports (right side)
|
||||
Column {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: -portRadius
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 8
|
||||
spacing: portSpacing - portRadius * 2
|
||||
|
||||
Repeater {
|
||||
model: nodeOutputs
|
||||
delegate: Item {
|
||||
width: portRadius * 2 + 60
|
||||
height: portRadius * 2
|
||||
layoutDirection: Qt.RightToLeft
|
||||
|
||||
CText {
|
||||
anchors.right: outputPort.left
|
||||
anchors.rightMargin: 4
|
||||
anchors.verticalCenter: outputPort.verticalCenter
|
||||
text: modelData.displayName || modelData.name || "out"
|
||||
variant: "caption"
|
||||
font.pixelSize: 10
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: outputPort
|
||||
anchors.right: parent.right
|
||||
width: portRadius * 2
|
||||
height: portRadius * 2
|
||||
radius: portRadius
|
||||
color: Theme.success
|
||||
border.color: Theme.background
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.CrossCursor
|
||||
|
||||
onPressed: function(mouse) {
|
||||
var global = outputPort.mapToItem(null, portRadius, portRadius)
|
||||
nodeRoot.portPressed(nodeId, modelData.name, modelData.type, true, global.x, global.y)
|
||||
}
|
||||
onReleased: function(mouse) {
|
||||
var global = outputPort.mapToItem(null, portRadius, portRadius)
|
||||
nodeRoot.portReleased(nodeId, modelData.name, modelData.type, true, global.x, global.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@ HeroSection 1.0 HeroSection.qml
|
||||
FeatureCard 1.0 FeatureCard.qml
|
||||
StatusCard 1.0 StatusCard.qml
|
||||
ContactForm 1.0 ContactForm.qml
|
||||
WorkflowNode 1.0 WorkflowNode.qml
|
||||
|
||||
111
frontends/qt6/src/NodeRegistry.cpp
Normal file
111
frontends/qt6/src/NodeRegistry.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "NodeRegistry.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
|
||||
NodeRegistry::NodeRegistry(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVariantList NodeRegistry::nodeTypes() const
|
||||
{
|
||||
QVariantList list;
|
||||
for (auto it = m_nodeTypes.constBegin(); it != m_nodeTypes.constEnd(); ++it) {
|
||||
list.append(it.value().toVariantMap());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void NodeRegistry::loadRegistry(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
m_lastError = QStringLiteral("Cannot open registry file: ") + path;
|
||||
qWarning() << "NodeRegistry:" << m_lastError;
|
||||
emit errorOccurred(m_lastError);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError parseErr;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseErr);
|
||||
file.close();
|
||||
|
||||
if (parseErr.error != QJsonParseError::NoError) {
|
||||
m_lastError = QStringLiteral("JSON parse error: ") + parseErr.errorString();
|
||||
qWarning() << "NodeRegistry:" << m_lastError;
|
||||
emit errorOccurred(m_lastError);
|
||||
return;
|
||||
}
|
||||
|
||||
parseRegistry(doc.object());
|
||||
|
||||
m_loaded = true;
|
||||
emit loadedChanged();
|
||||
emit nodeTypesChanged();
|
||||
qDebug() << "NodeRegistry: loaded" << m_nodeTypes.count() << "node types in"
|
||||
<< m_groups.count() << "groups from" << path;
|
||||
}
|
||||
|
||||
void NodeRegistry::parseRegistry(const QJsonObject &root)
|
||||
{
|
||||
m_nodeTypes.clear();
|
||||
m_groups.clear();
|
||||
|
||||
const QJsonArray types = root.value(QStringLiteral("nodeTypes")).toArray();
|
||||
QSet<QString> groupSet;
|
||||
|
||||
for (const QJsonValue &val : types) {
|
||||
const QJsonObject obj = val.toObject();
|
||||
const QString name = obj.value(QStringLiteral("name")).toString();
|
||||
if (name.isEmpty())
|
||||
continue;
|
||||
|
||||
m_nodeTypes.insert(name, obj);
|
||||
|
||||
const QString group = obj.value(QStringLiteral("group")).toString();
|
||||
if (!group.isEmpty())
|
||||
groupSet.insert(group);
|
||||
}
|
||||
|
||||
m_groups = groupSet.values();
|
||||
m_groups.sort();
|
||||
}
|
||||
|
||||
QVariantMap NodeRegistry::nodeType(const QString &name) const
|
||||
{
|
||||
if (!m_nodeTypes.contains(name))
|
||||
return {};
|
||||
return m_nodeTypes.value(name).toVariantMap();
|
||||
}
|
||||
|
||||
QVariantList NodeRegistry::nodesByGroup(const QString &group) const
|
||||
{
|
||||
QVariantList list;
|
||||
for (auto it = m_nodeTypes.constBegin(); it != m_nodeTypes.constEnd(); ++it) {
|
||||
if (it.value().value(QStringLiteral("group")).toString() == group)
|
||||
list.append(it.value().toVariantMap());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
QVariantList NodeRegistry::searchNodes(const QString &query) const
|
||||
{
|
||||
if (query.isEmpty())
|
||||
return nodeTypes();
|
||||
|
||||
const QString lower = query.toLower();
|
||||
QVariantList list;
|
||||
for (auto it = m_nodeTypes.constBegin(); it != m_nodeTypes.constEnd(); ++it) {
|
||||
const QJsonObject &obj = it.value();
|
||||
const QString name = obj.value(QStringLiteral("name")).toString().toLower();
|
||||
const QString displayName = obj.value(QStringLiteral("displayName")).toString().toLower();
|
||||
const QString desc = obj.value(QStringLiteral("description")).toString().toLower();
|
||||
|
||||
if (name.contains(lower) || displayName.contains(lower) || desc.contains(lower))
|
||||
list.append(obj.toVariantMap());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
50
frontends/qt6/src/NodeRegistry.h
Normal file
50
frontends/qt6/src/NodeRegistry.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef NODEREGISTRY_H
|
||||
#define NODEREGISTRY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QMap>
|
||||
#include <QVariantList>
|
||||
|
||||
class NodeRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QVariantList nodeTypes READ nodeTypes NOTIFY nodeTypesChanged)
|
||||
Q_PROPERTY(QStringList groups READ groups NOTIFY nodeTypesChanged)
|
||||
Q_PROPERTY(int nodeCount READ nodeCount NOTIFY nodeTypesChanged)
|
||||
Q_PROPERTY(bool loaded READ isLoaded NOTIFY loadedChanged)
|
||||
Q_PROPERTY(QString lastError READ lastError NOTIFY errorOccurred)
|
||||
|
||||
public:
|
||||
explicit NodeRegistry(QObject *parent = nullptr);
|
||||
|
||||
QVariantList nodeTypes() const;
|
||||
QStringList groups() const { return m_groups; }
|
||||
int nodeCount() const { return m_nodeTypes.count(); }
|
||||
bool isLoaded() const { return m_loaded; }
|
||||
QString lastError() const { return m_lastError; }
|
||||
|
||||
public slots:
|
||||
void loadRegistry(const QString &path);
|
||||
QVariantMap nodeType(const QString &name) const;
|
||||
QVariantList nodesByGroup(const QString &group) const;
|
||||
QVariantList searchNodes(const QString &query) const;
|
||||
|
||||
signals:
|
||||
void nodeTypesChanged();
|
||||
void loadedChanged();
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
private:
|
||||
void parseRegistry(const QJsonObject &root);
|
||||
|
||||
QMap<QString, QJsonObject> m_nodeTypes;
|
||||
QStringList m_groups;
|
||||
bool m_loaded = false;
|
||||
QString m_lastError;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user