mirror of
https://github.com/logos-co/logos-dev-boost.git
synced 2026-08-27 16:11:11 +00:00
fix scaffold of UI apps
This commit is contained in:
@@ -73,6 +73,11 @@
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/logos-dev-boost";
|
||||
};
|
||||
# Alias for `nix run .#app` (same as default)
|
||||
app = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/logos-dev-boost";
|
||||
};
|
||||
mcp-server = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/logos-dev-boost-mcp";
|
||||
|
||||
+138
-42
@@ -2,15 +2,28 @@
|
||||
|
||||
## IComponent Pattern
|
||||
|
||||
UI Apps are Qt plugins loaded directly by Basecamp. They implement `IComponent`:
|
||||
UI apps are Qt plugins loaded directly by Basecamp. They implement `IComponent`, which is **vendored locally** in every project (not provided by the SDK):
|
||||
|
||||
`interfaces/IComponent.h`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QtPlugin>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class IComponent {
|
||||
public:
|
||||
virtual ~IComponent() = default;
|
||||
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
|
||||
virtual void destroyWidget(QWidget* widget) = 0;
|
||||
};
|
||||
|
||||
#define IComponent_iid "com.logos.component.IComponent"
|
||||
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)
|
||||
```
|
||||
|
||||
The plugin class inherits both `QObject` and `IComponent`, and uses `Q_PLUGIN_METADATA`:
|
||||
@@ -19,86 +32,162 @@ The plugin class inherits both `QObject` and `IComponent`, and uses `Q_PLUGIN_ME
|
||||
class MyPlugin : public QObject, public IComponent {
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(IComponent)
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "../metadata.json")
|
||||
public:
|
||||
QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
|
||||
void destroyWidget(QWidget* widget) override;
|
||||
};
|
||||
```
|
||||
|
||||
## CMakeLists.txt Requirements
|
||||
|
||||
```cmake
|
||||
set(CMAKE_AUTOMOC ON) # not AUTORCC
|
||||
|
||||
logos_module(
|
||||
NAME my_app
|
||||
SOURCES ...
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/interfaces # required for #include <IComponent.h>
|
||||
)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets QuickControls2)
|
||||
|
||||
qt_add_resources(my_app_module_plugin ui_qml_resources
|
||||
PREFIX "/"
|
||||
FILES src/qml/Main.qml
|
||||
)
|
||||
```
|
||||
|
||||
QML is embedded via `qt_add_resources` (no `.qrc` file needed). The embedded path is `qrc:/src/qml/Main.qml`.
|
||||
|
||||
## C++/QML Boundary Rules
|
||||
|
||||
This is the most important convention for UI apps. Every piece of logic must go in the right layer:
|
||||
Every piece of logic must go in the right layer:
|
||||
|
||||
| Concern | C++ (backend class) | QML |
|
||||
|---------|---------------------|-----|
|
||||
| Data models, state | `Q_PROPERTY` on `QObject` | Bind to `backend.property` |
|
||||
| Business logic | Methods on backend class | Never — no JS business logic |
|
||||
| Module calls | `LogosAPI::callModule()` | `logos.callModule()` (thin wrapper) |
|
||||
| File I/O, networking | Always C++ | Never |
|
||||
| UI layout, styling | Never | Always use `Logos.Theme`, `Logos.Controls` |
|
||||
| User interactions | `Q_INVOKABLE` slots | `onClicked: backend.doThing()` |
|
||||
| Plugin lifecycle | `IComponent::createWidget/destroyWidget` | N/A |
|
||||
|
||||
| Concern | C++ (backend class) | QML |
|
||||
| -------------------- | ---------------------------------------- | -------------------------------------- |
|
||||
| Data models, state | `Q_PROPERTY` on `QObject` | Bind to `backend.property` |
|
||||
| Business logic | Methods on backend class | Never — no JS business logic |
|
||||
| Module calls | Via `LogosAPI*` | Never |
|
||||
| File I/O, networking | Always C++ | Never |
|
||||
| UI layout, styling | Never | QML; use `Logos.Theme` inside Basecamp |
|
||||
| User interactions | `Q_INVOKABLE` slots | `onClicked: backend.doThing()` |
|
||||
| Plugin lifecycle | `IComponent::createWidget/destroyWidget` | N/A |
|
||||
|
||||
|
||||
## Plugin `createWidget()` Pattern
|
||||
|
||||
```cpp
|
||||
QWidget* MyPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
QQuickStyle::setStyle("Basic"); // consistent cross-platform rendering
|
||||
|
||||
auto* quickWidget = new QQuickWidget();
|
||||
quickWidget->setMinimumSize(800, 600);
|
||||
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
auto* backend = new MyBackend(logosAPI, quickWidget);
|
||||
quickWidget->rootContext()->setContextProperty("backend", backend);
|
||||
|
||||
// Dev mode: export QML_PATH=$PWD/src/qml to load Main.qml from disk
|
||||
const QString devSource = QString::fromUtf8(qgetenv("QML_PATH"));
|
||||
const QUrl qmlUrl = devSource.isEmpty()
|
||||
? QUrl("qrc:/src/qml/Main.qml")
|
||||
: QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml"));
|
||||
|
||||
quickWidget->setSource(qmlUrl);
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
qWarning() << "MyPlugin: failed to load QML from" << qmlUrl;
|
||||
for (const auto& e : quickWidget->errors())
|
||||
qWarning() << e.toString();
|
||||
}
|
||||
|
||||
return quickWidget;
|
||||
}
|
||||
```
|
||||
|
||||
## Backend Class Pattern
|
||||
|
||||
The backend is a `QObject` subclass exposed to QML as a context property:
|
||||
|
||||
```cpp
|
||||
class MyBackend : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged)
|
||||
Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged)
|
||||
Q_PROPERTY(int itemCount READ itemCount NOTIFY itemCountChanged)
|
||||
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
|
||||
public:
|
||||
explicit MyBackend(LogosAPI* api, QObject* parent = nullptr);
|
||||
QVariantList items() const;
|
||||
|
||||
Q_INVOKABLE void addItem(const QString& name);
|
||||
QVariantList items() const;
|
||||
int itemCount() const;
|
||||
QString statusMessage() const;
|
||||
|
||||
Q_INVOKABLE void addItem(const QString& text);
|
||||
Q_INVOKABLE void removeItem(int index);
|
||||
|
||||
signals:
|
||||
void itemsChanged();
|
||||
void itemCountChanged();
|
||||
void statusMessageChanged();
|
||||
|
||||
private:
|
||||
LogosAPI* m_api;
|
||||
QVariantList m_items;
|
||||
QString m_statusMessage;
|
||||
};
|
||||
```
|
||||
|
||||
In `createWidget()`, set the backend as a context property on the QML engine:
|
||||
|
||||
```cpp
|
||||
QWidget* MyPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
auto* widget = new QQuickWidget;
|
||||
auto* backend = new MyBackend(logosAPI, widget);
|
||||
widget->rootContext()->setContextProperty("backend", backend);
|
||||
widget->setSource(QUrl("qrc:/qml/Main.qml"));
|
||||
return widget;
|
||||
}
|
||||
```
|
||||
- Expose every QML-bound value with a `Q_PROPERTY` + NOTIFY signal
|
||||
- `statusMessage` is useful for feedback displayed in a status bar
|
||||
- Actions go in `Q_INVOKABLE` methods
|
||||
- Call other modules via `LogosAPI*` inside the backend
|
||||
|
||||
## QML Conventions
|
||||
|
||||
- Entry point is always `Main.qml`
|
||||
- Use `Logos.Theme` for all colors: `Logos.Theme.backgroundColor`, `Logos.Theme.textColor`
|
||||
- Use `Logos.Controls` for interactive elements: `LogosButton`, `LogosText`
|
||||
- Never hardcode colors — always use theme properties
|
||||
- Access the backend via the `backend` context property
|
||||
- Use declarative bindings over imperative JavaScript
|
||||
- Root element should use `anchors.fill: parent`
|
||||
- Entry point is always `src/qml/Main.qml`, embedded at `qrc:/src/qml/Main.qml`
|
||||
- Root element: `Rectangle { anchors.fill: parent }`
|
||||
- React to backend signals via `Connections { target: backend }`
|
||||
- Inside Basecamp: use `Logos.Theme` for colors, `Logos.Controls` for components
|
||||
- When running standalone (`nix run .`): use plain QtQuick; `Logos.Theme` is not available
|
||||
- Never hardcode UI logic in JavaScript
|
||||
|
||||
## Dev Mode
|
||||
|
||||
```bash
|
||||
export QML_PATH=$PWD/src/qml
|
||||
nix run . # loads Main.qml from disk; restart app to pick up QML changes
|
||||
```
|
||||
|
||||
C++ changes always require `nix build`.
|
||||
|
||||
## Standalone Test
|
||||
|
||||
```bash
|
||||
nix build
|
||||
nix run . # or: nix run .#app
|
||||
```
|
||||
|
||||
`mkLogosModule` with `"type": "ui"` automatically wires up `apps.default` — no manual flake setup needed.
|
||||
|
||||
## Calling Logos Modules
|
||||
|
||||
From C++ backend:
|
||||
|
||||
```cpp
|
||||
QVariant result = m_api->callModule("storage", "save", {key, value});
|
||||
auto* client = m_logosAPI->getClient("storage_module");
|
||||
QVariant result = client->invokeRemoteMethod("storage_module", "save", key, value);
|
||||
```
|
||||
|
||||
From QML (via LogosQmlBridge):
|
||||
```qml
|
||||
logos.callModule("storage", "save", [key, value])
|
||||
Or with generated typed wrappers (recommended when available):
|
||||
|
||||
```cpp
|
||||
#include "logos_sdk.h" // generated at build time from metadata.json dependencies
|
||||
LogosModules logos(m_logosAPI);
|
||||
logos.storage_module.save(key, value);
|
||||
```
|
||||
|
||||
Always declare module dependencies in `metadata.json` so they are loaded before the UI app.
|
||||
Always declare module dependencies in `metadata.json` `"dependencies"` so they are loaded before the UI app.
|
||||
|
||||
## metadata.json for UI Apps
|
||||
|
||||
@@ -108,8 +197,15 @@ Always declare module dependencies in `metadata.json` so they are loaded before
|
||||
"type": "ui",
|
||||
"version": "1.0.0",
|
||||
"description": "My UI application",
|
||||
"icon": "icon.png",
|
||||
"icon": "icons/my_app.png",
|
||||
"category": "tools",
|
||||
"dependencies": ["storage_module"]
|
||||
"main": "my_app_plugin",
|
||||
"dependencies": ["storage_module"],
|
||||
"nix": {
|
||||
"packages": { "build": [], "runtime": ["qt6.qtdeclarative"] },
|
||||
"external_libraries": [],
|
||||
"cmake": { "find_packages": [], "extra_sources": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+446
-35
@@ -270,6 +270,11 @@ function createUiApp(
|
||||
filesCreated: string[]
|
||||
) {
|
||||
const pascal = toPascalCase(name);
|
||||
const qmlEscDescription = description
|
||||
.replace(/\r?\n/g, " ")
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"');
|
||||
const flakeEscDescription = description.replace(/"/g, '\\"').replace(/\r?\n/g, " ");
|
||||
|
||||
writeFile(
|
||||
path.join(dir, "metadata.json"),
|
||||
@@ -279,9 +284,17 @@ function createUiApp(
|
||||
version: "1.0.0",
|
||||
description,
|
||||
type: "ui",
|
||||
category: "tools",
|
||||
category: "ui",
|
||||
main: `${name}_plugin`,
|
||||
dependencies: [],
|
||||
nix: {
|
||||
packages: {
|
||||
build: [],
|
||||
runtime: ["qt6.qtdeclarative"],
|
||||
},
|
||||
external_libraries: [],
|
||||
cmake: { find_packages: [], extra_sources: [] },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
@@ -290,19 +303,56 @@ function createUiApp(
|
||||
);
|
||||
|
||||
writeFile(
|
||||
path.join(dir, `src/${pascal}Plugin.h`),
|
||||
path.join(dir, "interfaces/IComponent.h"),
|
||||
`#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QtPlugin>
|
||||
#include "IComponent.h"
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class IComponent {
|
||||
public:
|
||||
virtual ~IComponent() = default;
|
||||
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
|
||||
virtual void destroyWidget(QWidget* widget) = 0;
|
||||
};
|
||||
|
||||
#define IComponent_iid "com.logos.component.IComponent"
|
||||
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)
|
||||
`,
|
||||
filesCreated
|
||||
);
|
||||
|
||||
writeFile(
|
||||
path.join(dir, ".gitignore"),
|
||||
`.DS_Store
|
||||
result
|
||||
build/
|
||||
`,
|
||||
filesCreated
|
||||
);
|
||||
|
||||
writeFile(
|
||||
path.join(dir, `src/${name}_plugin.h`),
|
||||
`#pragma once
|
||||
|
||||
#include <IComponent.h>
|
||||
#include <QObject>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class ${pascal}Plugin : public QObject, public IComponent {
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(IComponent)
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "../metadata.json")
|
||||
|
||||
public:
|
||||
QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
|
||||
explicit ${pascal}Plugin(QObject* parent = nullptr);
|
||||
~${pascal}Plugin();
|
||||
|
||||
Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
|
||||
void destroyWidget(QWidget* widget) override;
|
||||
};
|
||||
`,
|
||||
@@ -310,19 +360,48 @@ public:
|
||||
);
|
||||
|
||||
writeFile(
|
||||
path.join(dir, `src/${pascal}Plugin.cpp`),
|
||||
`#include "${pascal}Plugin.h"
|
||||
path.join(dir, `src/${name}_plugin.cpp`),
|
||||
`#include "${name}_plugin.h"
|
||||
#include "${pascal}Backend.h"
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
#include <QQuickWidget>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <QUrl>
|
||||
|
||||
${pascal}Plugin::${pascal}Plugin(QObject* parent) : QObject(parent) {}
|
||||
${pascal}Plugin::~${pascal}Plugin() {}
|
||||
|
||||
QWidget* ${pascal}Plugin::createWidget(LogosAPI* logosAPI) {
|
||||
auto* widget = new QQuickWidget;
|
||||
auto* backend = new ${pascal}Backend(logosAPI, widget);
|
||||
widget->rootContext()->setContextProperty("backend", backend);
|
||||
widget->setSource(QUrl("qrc:/qml/Main.qml"));
|
||||
widget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
return widget;
|
||||
QQuickStyle::setStyle("Basic");
|
||||
|
||||
auto* quickWidget = new QQuickWidget();
|
||||
quickWidget->setMinimumSize(800, 600);
|
||||
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
auto* backend = new ${pascal}Backend(logosAPI, quickWidget);
|
||||
quickWidget->rootContext()->setContextProperty("backend", backend);
|
||||
|
||||
// Dev mode: set QML_PATH to the directory containing Main.qml to load from disk without rebuilding.
|
||||
// Example: export QML_PATH=$PWD/src/qml
|
||||
const QString devSource = QString::fromUtf8(qgetenv("QML_PATH"));
|
||||
const QUrl qmlUrl = devSource.isEmpty()
|
||||
? QUrl("qrc:/src/qml/Main.qml")
|
||||
: QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml"));
|
||||
|
||||
quickWidget->setSource(qmlUrl);
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
qWarning() << "${pascal}Plugin: failed to load QML from" << qmlUrl;
|
||||
for (const auto& e : quickWidget->errors()) {
|
||||
qWarning() << e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return quickWidget;
|
||||
}
|
||||
|
||||
void ${pascal}Plugin::destroyWidget(QWidget* widget) {
|
||||
@@ -335,7 +414,9 @@ void ${pascal}Plugin::destroyWidget(QWidget* widget) {
|
||||
writeFile(
|
||||
path.join(dir, `src/${pascal}Backend.h`),
|
||||
`#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
class LogosAPI;
|
||||
@@ -343,19 +424,34 @@ class LogosAPI;
|
||||
class ${pascal}Backend : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged)
|
||||
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
|
||||
Q_PROPERTY(int itemCount READ itemCount NOTIFY itemCountChanged)
|
||||
|
||||
public:
|
||||
explicit ${pascal}Backend(LogosAPI* api, QObject* parent = nullptr);
|
||||
QVariantList items() const;
|
||||
|
||||
Q_INVOKABLE void addItem(const QString& name);
|
||||
QVariantList items() const;
|
||||
QString statusMessage() const;
|
||||
int itemCount() const;
|
||||
|
||||
Q_INVOKABLE void addNote(const QString& text);
|
||||
Q_INVOKABLE void removeItem(int index);
|
||||
Q_INVOKABLE void clearAll();
|
||||
|
||||
signals:
|
||||
void itemsChanged();
|
||||
void statusMessageChanged();
|
||||
void itemCountChanged();
|
||||
void noteAdded(int index, const QString& text);
|
||||
void noteRemoved(int index);
|
||||
|
||||
private:
|
||||
void setStatusMessage(const QString& message);
|
||||
void bumpCounts();
|
||||
|
||||
LogosAPI* m_api;
|
||||
QVariantList m_items;
|
||||
QString m_statusMessage;
|
||||
};
|
||||
`,
|
||||
filesCreated
|
||||
@@ -365,23 +461,80 @@ private:
|
||||
path.join(dir, `src/${pascal}Backend.cpp`),
|
||||
`#include "${pascal}Backend.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
${pascal}Backend::${pascal}Backend(LogosAPI* api, QObject* parent)
|
||||
: QObject(parent), m_api(api) {}
|
||||
: QObject(parent)
|
||||
, m_api(api)
|
||||
{
|
||||
Q_UNUSED(m_api);
|
||||
setStatusMessage("Ready. Add a note below.");
|
||||
}
|
||||
|
||||
QVariantList ${pascal}Backend::items() const { return m_items; }
|
||||
QVariantList ${pascal}Backend::items() const {
|
||||
return m_items;
|
||||
}
|
||||
|
||||
void ${pascal}Backend::addItem(const QString& name) {
|
||||
QVariantMap item;
|
||||
item["name"] = name;
|
||||
m_items.append(item);
|
||||
QString ${pascal}Backend::statusMessage() const {
|
||||
return m_statusMessage;
|
||||
}
|
||||
|
||||
int ${pascal}Backend::itemCount() const {
|
||||
return m_items.size();
|
||||
}
|
||||
|
||||
void ${pascal}Backend::setStatusMessage(const QString& message) {
|
||||
if (m_statusMessage == message) {
|
||||
return;
|
||||
}
|
||||
m_statusMessage = message;
|
||||
emit statusMessageChanged();
|
||||
}
|
||||
|
||||
void ${pascal}Backend::bumpCounts() {
|
||||
emit itemsChanged();
|
||||
emit itemCountChanged();
|
||||
}
|
||||
|
||||
void ${pascal}Backend::addNote(const QString& text) {
|
||||
const QString trimmed = text.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
setStatusMessage("Enter some text before adding a note.");
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap row;
|
||||
row["title"] = trimmed;
|
||||
row["created"] = QDateTime::currentDateTime().toString(Qt::ISODate);
|
||||
|
||||
const int index = m_items.size();
|
||||
m_items.append(row);
|
||||
bumpCounts();
|
||||
emit noteAdded(index, trimmed);
|
||||
setStatusMessage(QString("Added note (%1 total).").arg(m_items.size()));
|
||||
}
|
||||
|
||||
void ${pascal}Backend::removeItem(int index) {
|
||||
if (index >= 0 && index < m_items.size()) {
|
||||
m_items.removeAt(index);
|
||||
emit itemsChanged();
|
||||
if (index < 0 || index >= m_items.size()) {
|
||||
setStatusMessage("Invalid note index.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_items.removeAt(index);
|
||||
bumpCounts();
|
||||
emit noteRemoved(index);
|
||||
setStatusMessage(m_items.isEmpty() ? "All notes cleared." : QString("Removed note (%1 left).").arg(m_items.size()));
|
||||
}
|
||||
|
||||
void ${pascal}Backend::clearAll() {
|
||||
if (m_items.isEmpty()) {
|
||||
setStatusMessage("Nothing to clear.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_items.clear();
|
||||
bumpCounts();
|
||||
setStatusMessage("Cleared all notes.");
|
||||
}
|
||||
`,
|
||||
filesCreated
|
||||
@@ -394,33 +547,291 @@ import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
id: root
|
||||
color: "#1e1e1e"
|
||||
|
||||
Connections {
|
||||
target: backend
|
||||
|
||||
function onNoteAdded(index, text) {
|
||||
noteField.clear()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
anchors.margins: 24
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "${pascal}"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: "#ffffff"
|
||||
}
|
||||
|
||||
ListView {
|
||||
Text {
|
||||
text: "${qmlEscDescription}"
|
||||
font.pixelSize: 14
|
||||
color: "#a0a0a0"
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: backend.items
|
||||
delegate: Text {
|
||||
text: modelData.name
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
color: "#2d2d2d"
|
||||
radius: 4
|
||||
border.color: "#444444"
|
||||
border.width: 1
|
||||
|
||||
TextField {
|
||||
id: noteField
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
placeholderText: "Write a note…"
|
||||
color: "#ffffff"
|
||||
selectionColor: "#4A90E2"
|
||||
background: Rectangle { color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Add Item"
|
||||
onClicked: backend.addItem("New Item")
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
|
||||
Button {
|
||||
text: "Add note"
|
||||
onClicked: backend.addNote(noteField.text)
|
||||
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
font.pixelSize: 13
|
||||
color: parent.enabled ? "#ffffff" : "#808080"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 110
|
||||
implicitHeight: 32
|
||||
color: parent.enabled ? (parent.pressed ? "#1a7f37" : "#238636") : "#2d2d2d"
|
||||
radius: 4
|
||||
border.color: parent.enabled ? "#2ea043" : "#3d3d3d"
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Clear all"
|
||||
enabled: backend.itemCount > 0
|
||||
onClicked: backend.clearAll()
|
||||
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
font.pixelSize: 13
|
||||
color: parent.enabled ? "#ffffff" : "#808080"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: 32
|
||||
color: parent.enabled ? (parent.pressed ? "#5c1a1a" : "#7a2a2a") : "#2d2d2d"
|
||||
radius: 4
|
||||
border.color: parent.enabled ? "#c62828" : "#3d3d3d"
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "#252526"
|
||||
radius: 6
|
||||
border.color: "#333333"
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: backend.itemCount === 0
|
||||
text: "No notes yet.\\nAdd one with the field above."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#808080"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: noteList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
clip: true
|
||||
visible: backend.itemCount > 0
|
||||
model: backend.items
|
||||
spacing: 6
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 56
|
||||
color: index % 2 === 0 ? "#2d2d2d" : "#333333"
|
||||
radius: 4
|
||||
border.color: mouseArea.containsMouse ? "#4A90E2" : "#444444"
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 12
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: modelData.title
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.created || ""
|
||||
color: "#a0a0a0"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Delete"
|
||||
onClicked: backend.removeItem(index)
|
||||
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
font.pixelSize: 12
|
||||
color: parent.enabled ? "#ffffff" : "#808080"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 72
|
||||
implicitHeight: 28
|
||||
color: parent.pressed ? "#5c1a1a" : "#7a2a2a"
|
||||
radius: 4
|
||||
border.color: "#c62828"
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
color: "#2d2d2d"
|
||||
radius: 4
|
||||
border.color: "#444444"
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 10
|
||||
text: backend.statusMessage
|
||||
color: "#c0c0c0"
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
filesCreated
|
||||
);
|
||||
|
||||
writeFile(
|
||||
path.join(dir, "CMakeLists.txt"),
|
||||
`cmake_minimum_required(VERSION 3.14)
|
||||
project(${pascal}Plugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
||||
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
||||
else()
|
||||
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
|
||||
endif()
|
||||
|
||||
logos_module(
|
||||
NAME ${name}
|
||||
SOURCES
|
||||
src/${name}_plugin.h
|
||||
src/${name}_plugin.cpp
|
||||
src/${pascal}Backend.h
|
||||
src/${pascal}Backend.cpp
|
||||
INCLUDE_DIRS
|
||||
\${CMAKE_CURRENT_SOURCE_DIR}/interfaces
|
||||
)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets QuickControls2)
|
||||
|
||||
qt_add_resources(${name}_module_plugin ui_qml_resources
|
||||
PREFIX "/"
|
||||
FILES
|
||||
src/qml/Main.qml
|
||||
)
|
||||
|
||||
target_link_libraries(${name}_module_plugin PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::Quick
|
||||
Qt6::QuickWidgets
|
||||
Qt6::QuickControls2
|
||||
)
|
||||
`,
|
||||
filesCreated
|
||||
);
|
||||
|
||||
writeFile(
|
||||
path.join(dir, "flake.nix"),
|
||||
`{
|
||||
description = "${flakeEscDescription}";
|
||||
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
let
|
||||
base = logos-module-builder.lib.mkLogosModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
};
|
||||
in
|
||||
base // (
|
||||
if base ? apps then {
|
||||
apps = builtins.mapAttrs (_system: apps:
|
||||
apps // { app = apps.default; }
|
||||
) base.apps;
|
||||
} else {}
|
||||
);
|
||||
}
|
||||
`,
|
||||
filesCreated
|
||||
);
|
||||
|
||||
+205
-55
@@ -1,35 +1,77 @@
|
||||
---
|
||||
name: create-ui-app
|
||||
description: Activate when creating a Logos Basecamp UI app with IComponent, C++ backend, and QML frontend. Covers the plugin class, QObject backend, QML entry point, and the C++/QML boundary.
|
||||
---
|
||||
|
||||
## name: create-ui-app
|
||||
|
||||
description: Activate when creating a Logos Basecamp UI app with IComponent, C++backend, and QML frontend. Covers the plugin class, QObject backend, QML entry point, and the C++/QML boundary.
|
||||
|
||||
# Create a UI App for Logos Basecamp
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Creating an application with a graphical interface for Basecamp
|
||||
- Building an `IComponent` plugin with `createWidget` / `destroyWidget`
|
||||
- The app has a C++ backend and QML frontend
|
||||
|
||||
## Step 1: Create Project Structure
|
||||
## Fastest path: scaffold with logos-dev-boost
|
||||
|
||||
```bash
|
||||
nix run github:logos-co/logos-dev-boost -- init my_app --type ui-app
|
||||
cd logos-my-app
|
||||
git init && git add -A
|
||||
nix build
|
||||
nix run . # standalone app (same as nix run .#app)
|
||||
```
|
||||
|
||||
This generates the full structure below, compiles cleanly, and runs standalone via `nix run`.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Project Structure
|
||||
|
||||
```
|
||||
my_app/
|
||||
logos-my-app/
|
||||
├── interfaces/
|
||||
│ └── IComponent.h ← vendored locally (every UI repo does this)
|
||||
├── src/
|
||||
│ ├── MyAppPlugin.h
|
||||
│ ├── MyAppPlugin.cpp
|
||||
│ ├── my_app_plugin.h
|
||||
│ ├── my_app_plugin.cpp
|
||||
│ ├── MyAppBackend.h
|
||||
│ ├── MyAppBackend.cpp
|
||||
│ └── qml/
|
||||
│ ├── Main.qml
|
||||
│ └── resources.qrc
|
||||
│ └── Main.qml ← QML entry point (embedded via qt_add_resources)
|
||||
├── metadata.json
|
||||
├── CMakeLists.txt
|
||||
└── flake.nix
|
||||
├── flake.nix
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
## Step 2: Create metadata.json
|
||||
`interfaces/IComponent.h` must be present — it is **not** provided by the SDK, every UI repo vendors it locally.
|
||||
|
||||
## Step 2: `interfaces/IComponent.h`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QtPlugin>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class IComponent {
|
||||
public:
|
||||
virtual ~IComponent() = default;
|
||||
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
|
||||
virtual void destroyWidget(QWidget* widget) = 0;
|
||||
};
|
||||
|
||||
#define IComponent_iid "com.logos.component.IComponent"
|
||||
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)
|
||||
```
|
||||
|
||||
## Step 3: `metadata.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -39,48 +81,131 @@ my_app/
|
||||
"type": "ui",
|
||||
"category": "tools",
|
||||
"main": "my_app_plugin",
|
||||
"dependencies": []
|
||||
"dependencies": [],
|
||||
"nix": {
|
||||
"packages": { "build": [], "runtime": ["qt6.qtdeclarative"] },
|
||||
"external_libraries": [],
|
||||
"cmake": { "find_packages": [], "extra_sources": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: UI apps do NOT use `"interface": "universal"`. They are hand-written Qt plugins.
|
||||
|
||||
## Step 3: Create the Plugin Class
|
||||
## Step 4: `CMakeLists.txt`
|
||||
|
||||
`src/MyAppPlugin.h`:
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(MyAppPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
||||
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
||||
else()
|
||||
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
|
||||
endif()
|
||||
|
||||
logos_module(
|
||||
NAME my_app
|
||||
SOURCES
|
||||
src/my_app_plugin.h
|
||||
src/my_app_plugin.cpp
|
||||
src/MyAppBackend.h
|
||||
src/MyAppBackend.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/interfaces # so #include <IComponent.h> resolves
|
||||
)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets QuickControls2)
|
||||
|
||||
qt_add_resources(my_app_module_plugin ui_qml_resources
|
||||
PREFIX "/"
|
||||
FILES
|
||||
src/qml/Main.qml
|
||||
)
|
||||
|
||||
target_link_libraries(my_app_module_plugin PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::Quick
|
||||
Qt6::QuickWidgets
|
||||
Qt6::QuickControls2
|
||||
)
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- `CMAKE_AUTOMOC ON` (not AUTORCC — resources go through `qt_add_resources`)
|
||||
- `INCLUDE_DIRS` points at `interfaces/` so `<IComponent.h>` resolves
|
||||
- `qt_add_resources` embeds QML at `qrc:/src/qml/Main.qml`
|
||||
|
||||
## Step 5: Plugin Class
|
||||
|
||||
`src/my_app_plugin.h`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <IComponent.h>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QtPlugin>
|
||||
#include "IComponent.h"
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class MyAppPlugin : public QObject, public IComponent {
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(IComponent)
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "../metadata.json")
|
||||
|
||||
public:
|
||||
QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
|
||||
explicit MyAppPlugin(QObject* parent = nullptr);
|
||||
~MyAppPlugin();
|
||||
|
||||
Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
|
||||
void destroyWidget(QWidget* widget) override;
|
||||
};
|
||||
```
|
||||
|
||||
`src/MyAppPlugin.cpp`:
|
||||
`src/my_app_plugin.cpp`:
|
||||
|
||||
```cpp
|
||||
#include "MyAppPlugin.h"
|
||||
#include "my_app_plugin.h"
|
||||
#include "MyAppBackend.h"
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QString>
|
||||
#include <QQuickWidget>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <QUrl>
|
||||
|
||||
MyAppPlugin::MyAppPlugin(QObject* parent) : QObject(parent) {}
|
||||
MyAppPlugin::~MyAppPlugin() {}
|
||||
|
||||
QWidget* MyAppPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
auto* widget = new QQuickWidget;
|
||||
auto* backend = new MyAppBackend(logosAPI, widget);
|
||||
widget->rootContext()->setContextProperty("backend", backend);
|
||||
widget->setSource(QUrl("qrc:/qml/Main.qml"));
|
||||
widget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
return widget;
|
||||
QQuickStyle::setStyle("Basic");
|
||||
|
||||
auto* quickWidget = new QQuickWidget();
|
||||
quickWidget->setMinimumSize(800, 600);
|
||||
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
auto* backend = new MyAppBackend(logosAPI, quickWidget);
|
||||
quickWidget->rootContext()->setContextProperty("backend", backend);
|
||||
|
||||
// Dev mode: export QML_PATH=$PWD/src/qml to load from disk without rebuilding
|
||||
const QString devSource = QString::fromUtf8(qgetenv("QML_PATH"));
|
||||
const QUrl qmlUrl = devSource.isEmpty()
|
||||
? QUrl("qrc:/src/qml/Main.qml")
|
||||
: QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml"));
|
||||
|
||||
quickWidget->setSource(qmlUrl);
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
qWarning() << "MyAppPlugin: failed to load QML from" << qmlUrl;
|
||||
for (const auto& e : quickWidget->errors())
|
||||
qWarning() << e.toString();
|
||||
}
|
||||
|
||||
return quickWidget;
|
||||
}
|
||||
|
||||
void MyAppPlugin::destroyWidget(QWidget* widget) {
|
||||
@@ -88,103 +213,129 @@ void MyAppPlugin::destroyWidget(QWidget* widget) {
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Create the Backend Class
|
||||
## Step 6: Backend Class
|
||||
|
||||
`src/MyAppBackend.h`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class MyAppBackend : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged)
|
||||
Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged)
|
||||
Q_PROPERTY(int itemCount READ itemCount NOTIFY itemCountChanged)
|
||||
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
|
||||
|
||||
public:
|
||||
explicit MyAppBackend(LogosAPI* api, QObject* parent = nullptr);
|
||||
QVariantList items() const;
|
||||
|
||||
Q_INVOKABLE void addItem(const QString& name);
|
||||
QVariantList items() const;
|
||||
int itemCount() const;
|
||||
QString statusMessage() const;
|
||||
|
||||
Q_INVOKABLE void addItem(const QString& text);
|
||||
Q_INVOKABLE void removeItem(int index);
|
||||
Q_INVOKABLE void clearAll();
|
||||
|
||||
signals:
|
||||
void itemsChanged();
|
||||
void itemCountChanged();
|
||||
void statusMessageChanged();
|
||||
|
||||
private:
|
||||
LogosAPI* m_api;
|
||||
QVariantList m_items;
|
||||
QString m_statusMessage;
|
||||
};
|
||||
```
|
||||
|
||||
**Rules for the backend:**
|
||||
|
||||
- All business logic lives here, not in QML
|
||||
- Expose data via `Q_PROPERTY` with NOTIFY signals
|
||||
- Expose actions via `Q_INVOKABLE` methods
|
||||
- Call other modules via `LogosAPI::callModule()` here
|
||||
- Call other modules via `LogosAPI`* here (never from QML JS)
|
||||
- `statusMessage` pattern is useful for showing feedback in the status bar
|
||||
|
||||
## Step 5: Create Main.qml
|
||||
|
||||
`src/qml/Main.qml`:
|
||||
## Step 7: `src/qml/Main.qml`
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Logos.Theme.backgroundColor
|
||||
color: "#1e1e1e" // when inside Basecamp use Logos.Theme.backgroundColor
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
LogosText {
|
||||
Text {
|
||||
text: "My App"
|
||||
font.pixelSize: 24
|
||||
color: "#ffffff"
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: backend.items
|
||||
delegate: LogosText {
|
||||
delegate: Text {
|
||||
text: modelData.name
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Button {
|
||||
text: "Add Item"
|
||||
onClicked: backend.addItem("New Item")
|
||||
}
|
||||
|
||||
Text {
|
||||
text: backend.statusMessage
|
||||
color: "#a0a0a0"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**QML Rules:**
|
||||
- Use `Logos.Theme` for all colors
|
||||
- Use `Logos.Controls` for interactive elements
|
||||
- Access C++ backend via `backend` context property
|
||||
|
||||
- Access C++ backend via `backend` context property (set in `createWidget`)
|
||||
- Bind to `Q_PROPERTY` values; react to signals via `Connections { target: backend }`
|
||||
- When running inside Basecamp you can use `Logos.Theme` for colors and `Logos.Controls` for styled components
|
||||
- When running standalone (`nix run .`) use plain QtQuick (no Logos.Theme available)
|
||||
- Never put business logic in JavaScript
|
||||
|
||||
## Step 6: Create resources.qrc
|
||||
## Step 8: Dev Mode (QML changes without rebuild)
|
||||
|
||||
`src/qml/resources.qrc`:
|
||||
|
||||
```xml
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>qml/Main.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
```bash
|
||||
export QML_PATH=$PWD/src/qml
|
||||
nix run . # loads Main.qml from disk
|
||||
# Edit src/qml/Main.qml, then restart — no nix build needed
|
||||
```
|
||||
|
||||
## Step 7: Test in Basecamp
|
||||
C++ changes (`.h`, `.cpp`, `CMakeLists.txt`, `metadata.json`) always require `nix build`.
|
||||
|
||||
## Step 9: Build and Test
|
||||
|
||||
```bash
|
||||
git init && git add -A # nix needs files tracked
|
||||
nix build # compiles the plugin
|
||||
nix run . # launches standalone app (nix run .#app also works)
|
||||
```
|
||||
|
||||
## Step 10: Load in Basecamp
|
||||
|
||||
```bash
|
||||
nix build
|
||||
@@ -192,4 +343,3 @@ cp -r result/* ~/.local/share/Logos/LogosBasecampDev/plugins/my_app/
|
||||
# Launch Basecamp — find my_app in sidebar
|
||||
```
|
||||
|
||||
QML changes hot-reload when running in dev mode (`./run-dev.sh`). C++ changes require a rebuild.
|
||||
|
||||
+20
-12
@@ -1,19 +1,27 @@
|
||||
# UI App Template
|
||||
|
||||
This template is used by `logos-dev-boost init <name> --type ui-app` to scaffold a new Basecamp UI app.
|
||||
This template documents what `logos-dev-boost init <name> --type ui-app` scaffolds (implemented in `mcp-server/tools/scaffold.ts`).
|
||||
|
||||
## Generated files
|
||||
|
||||
- `src/<Name>Plugin.h/cpp` — IComponent implementation with createWidget/destroyWidget
|
||||
- `src/<Name>Backend.h/cpp` — QObject backend exposed to QML
|
||||
- `src/qml/Main.qml` — QML entry point
|
||||
- `metadata.json` — `"type": "ui"`
|
||||
- `CMakeLists.txt` — Qt6 Quick/QuickWidgets dependencies
|
||||
- `flake.nix` — Standard module builder config
|
||||
- `interfaces/IComponent.h` — vendored `IComponent` interface (same pattern as `logos-package-manager-ui` and other Basecamp UI plugins)
|
||||
- `src/<name>_plugin.h` / `src/<name>_plugin.cpp` — `IComponent` implementation: `QQuickWidget`, `QQuickStyle::setStyle("Basic")`, optional `QML_PATH` dev mode, loads `qrc:/src/qml/Main.qml` or `Main.qml` from `QML_PATH`
|
||||
- `src/<Pascal>Backend.h` / `src/<Pascal>Backend.cpp` — QObject backend with `Q_PROPERTY` / `Q_INVOKABLE` / signals (sample notes list + status line)
|
||||
- `src/qml/Main.qml` — dark-themed QML (toolbar, list, status bar)
|
||||
- `metadata.json` — `"type": "ui"`, `main`: `<name>_plugin`
|
||||
- `CMakeLists.txt` — `INCLUDE_DIRS` for `interfaces/`, `qt_add_resources` for QML, Qt6 Widgets/Quick/QuickWidgets/QuickControls2
|
||||
- `flake.nix` — `mkLogosModule` with `nix-bundle-lgx` input; adds `apps.<system>.app` as an alias of `default` so `nix run .#app` works
|
||||
- `.gitignore` — `result`, `build/`, `.DS_Store`
|
||||
|
||||
## C++/QML Boundary
|
||||
## Dev mode (QML without rebuild)
|
||||
|
||||
The generated code establishes the correct C++/QML boundary:
|
||||
- Business logic: C++ backend class (Q_PROPERTY + Q_INVOKABLE)
|
||||
- UI layout: QML with Logos.Theme and Logos.Controls
|
||||
- Bridge: Backend set as context property on QQuickWidget
|
||||
```bash
|
||||
export QML_PATH=$PWD/src/qml
|
||||
nix run .
|
||||
```
|
||||
|
||||
## C++/QML boundary
|
||||
|
||||
- Business logic: C++ backend (`Q_PROPERTY`, `Q_INVOKABLE`, signals)
|
||||
- UI: QML (`QtQuick` / `Controls` / `Layouts`); inside Basecamp you can also use `Logos.Theme` and `Logos.Controls`
|
||||
- Bridge: `backend` context property on the `QQuickWidget` root context
|
||||
|
||||
Reference in New Issue
Block a user