diff --git a/.gitignore b/.gitignore index 42535775..cc8cd0c1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,55 @@ build/ builds/ CMakeUserPresets.json compile_commands.json + +.claude +src/scanner/scanner_module_autogen +.qt +bin +BrickStore_autogen + +# CMake build artifacts +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +CPackSourceConfig.cmake +BundleConfig.cmake +*.cmake.in +compile_commands.json + +# Ninja build files +.ninja_deps +.ninja_log +build.ninja +rules.ninja + +# Qt autogen directories +*_autogen/ + +# Build outputs +*.lib +*.exp +*.obj +*.pdb +*.ilk +*.manifest + +# Generated files +generated/ +qm/ +imports/ +meta_types/ +qmltypes/ + +# Qt QML type registrations +*_qmltyperegistrations.cpp + +# Dependency builds +_deps/ + +# Test files +*.bsx + +# NPM artifacts +package-lock.json +node_modules/ diff --git a/BUILD_WINDOWS.md b/BUILD_WINDOWS.md new file mode 100644 index 00000000..7ee4ea3b --- /dev/null +++ b/BUILD_WINDOWS.md @@ -0,0 +1,91 @@ +# Building BrickStore on Windows + +## Prerequisites + +### Required Software + +1. **Qt 6.4.2 or newer** (Qt 6.10.1 recommended) + - Install from: https://www.qt.io/download + - Required components when installing: + - Qt Quick 3D + - Qt Multimedia + - Qt Image Formats + - Additional Libraries > Ninja + - Compiler: MSVC 2019 or MSVC 2022 (included with Qt installer) + +2. **Visual Studio 2019 or 2022** + - Install the "Desktop development with C++" workload + - Make sure to include the MSVC compiler and Windows SDK + +3. **CMake** (3.19.0 or newer) + - Download from: https://cmake.org/download/ + - During installation, choose "Add CMake to system PATH" + - **Important:** Use CMake 3.x (not 4.x pre-release) + +5. **InnoSetup 6** (optional, only needed for creating installers) + - Download from: https://jrsoftware.org/isdl.php + - Install to default location + - Add to PATH: `C:\Program Files (x86)\Inno Setup 6` + +## Build Instructions + +### Quick Build + +Simply run from any Command Prompt: + +```batch +build.bat +``` + +This script automatically: +- ✓ Sets up the Visual Studio environment (no need for Developer Command Prompt) +- ✓ Configures the project with CMake +- ✓ Builds the project +- ✓ Prompts you to deploy Qt dependencies + +**Important:** After building, you must deploy Qt dependencies (DLLs) before the executable can run. The build script will ask if you want to deploy automatically, or you can run: + +```batch +deploy.bat +``` + +The executable will be in `bin\BrickStore.exe` and can be run after deployment. + +### Clean Build + +To completely clean and rebuild: + +```batch +clean-all.bat +build.bat +``` + +### Deploying Qt Dependencies + +Before you can run the executable, Qt dependencies (DLLs and plugins) must be copied to the bin directory: + +```batch +deploy.bat +``` + +After deployment, `bin\BrickStore.exe` can be run directly. + +**Note:** The build script (`build.bat`) automatically prompts you to deploy after building. + +### Creating an Installer + +To build and create an InnoSetup installer: + +1. First build the project: + ```batch + build.bat + ``` + +2. Create the installer: + ```batch + cmake --build . --target installer + ``` + +The installer will be created as `BrickStore-.exe`. + +For debug builds, edit `configure.bat` line 39 to use `--debug` instead of `--release`. \ No newline at end of file diff --git a/EXTENSION_API_IMPROVEMENTS.md b/EXTENSION_API_IMPROVEMENTS.md new file mode 100644 index 00000000..7688e8cc --- /dev/null +++ b/EXTENSION_API_IMPROVEMENTS.md @@ -0,0 +1,403 @@ +# BrickStore Extension API Improvements + +This document describes the improvements made to the BrickStore extension API to address limitations found in the BrickVoice extension. + +## Summary of Changes + +### 1. ✅ Selected Lots API (Already Available!) +**Status:** Was already implemented but undocumented + +The `document.selectedLots` property was already available in the API, but wasn't documented. This provides direct access to selected lots without needing heuristics. + +**Usage:** +```qml +var selected = document.selectedLots +if (selected.length > 0) { + var firstSelected = selected[0] + console.log("Selected item:", firstSelected.itemId) +} +``` + +### 2. ✅ Current Lot Property (NEW!) +**Status:** Newly added + +Added `document.currentLot` property that returns the lot at the current cursor position/selection. + +**Usage:** +```qml +var current = document.currentLot +if (!current.isNull) { + console.log("Current item:", current.itemId) + console.log("Current color:", current.colorName) +} +``` + +**Signals when changed:** +- Emits `currentLotChanged` signal when selection changes + +### 3. ✅ Dynamic Action Text Updates (Already Worked!) +**Status:** Was already implemented + +The `ExtensionScriptAction.text` property can be updated dynamically and the UI will automatically update. + +**Before (your workaround):** +```qml +ExtensionScriptAction { + text: "Voice Status…" // Static text + actionFunction: function(document, lots) { + throw new Error(enabled ? "Voice is ON" : "Voice is OFF") // Modal hack + } +} +``` + +**After (proper solution):** +```qml +ExtensionScriptAction { + id: voiceAction + text: "Start Voice" + actionFunction: function(document, lots) { + enabled = !enabled + voiceAction.text = enabled ? "Stop Voice" : "Start Voice" // Explicit update + } +} +``` + +**Note:** You must explicitly set the `text` property in the action function. QML property bindings (like `text: enabled ? "..." : "..."`) don't automatically re-evaluate for C++ properties when dependencies change. + +### 4. ✅ DocumentLots.add() Returns the New Lot (NEW!) +**Status:** Changed return type from `int` to `Lot` + +The `document.lots.add()` method now returns the newly created lot directly instead of just its index. + +**Before (required snapshot/diff workaround):** +```qml +function applyVoiceCommand(data) { + // Snapshot before + var before = snapshotVisibleLots(doc) + + // Add lot + doc.lots.add(base.item, targetColor) + + // Find the new lot by diffing + var lot = findAddedLot(doc, before) // Complex workaround! +} +``` + +**After (direct access):** +```qml +function applyVoiceCommand(data) { + // Add lot and get it directly + var lot = doc.lots.add(base.item, targetColor) + + // Use it immediately + if (hasQty) lot.quantity = qty + lot.color = targetColor + lot.condition = base.condition +} +``` + +### 5. ✅ Standardized Property Names (Already Standard!) +**Status:** API was already consistent + +The Lot properties use consistent, well-documented names: +- ✅ `quantity` (not `qty`) +- ✅ `remarks` (not `remark`) +- ✅ `condition` (not `isNew`, `new`, or `cond`) + +**Your defensive code (no longer needed):** +```qml +// Before: Checking multiple property names +if ("quantity" in lot) lot.quantity = qty +else if ("qty" in lot) lot.qty = qty +else console.log("cannot set quantity") +``` + +**Simplified code:** +```qml +// After: Just use the standard name +lot.quantity = qty +``` + +## Complete Rewrite: BrickVoice Extension with New API + +Here's how your extension can be simplified using the new/discovered APIs: + +```qml +import BrickStore +import QtQuick + +Script { + name: "Voice Client (HTTP polling)" + author: "Swept" + version: "0.6" + + property string endpoint: "http://127.0.0.1:8765/next" + property bool enabled: false + property string voice_tag: "[voice]" + + // Poller + Timer { + id: poller + interval: 300 + repeat: true + running: enabled + onTriggered: fetchNext() + } + + function fetchNext() { + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState !== XMLHttpRequest.DONE) return + if (xhr.status === 204 || xhr.status === 404 || xhr.responseText === "") return + + try { + var raw = xhr.responseText.trim() + if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1) + + var l = raw.indexOf("{") + var r = raw.lastIndexOf("}") + if (l !== -1 && r !== -1) raw = raw.substring(l, r + 1) + + var data = JSON.parse(raw) + if (data && (data.qty !== undefined || data.color)) { + applyVoiceCommand(data) + if (data.raw) console.log("[Voice] " + data.raw) + } + } catch (e) { + console.log("[Voice] JSON parse error:", e) + } + } + xhr.open("GET", endpoint) + xhr.send() + } + + function isVoiceLot(lot) { + return lot.remarks && lot.remarks.indexOf(voice_tag) !== -1 + } + + function findBaseLot(doc) { + // NEW: Use selectedLots API! + var selected = doc.selectedLots + if (selected.length > 0) { + // Use the last selected lot that isn't voice-tagged + for (var i = selected.length - 1; i >= 0; i--) { + if (!isVoiceLot(selected[i])) return selected[i] + } + } + + // Fallback: use currentLot + var current = doc.currentLot + if (!current.isNull && !isVoiceLot(current)) return current + + // Last fallback: newest non-voice lot + var n = doc.visibleLotCount + for (var i = n - 1; i >= 0; --i) { + var lot = doc.lots.visibleAt(i) + if (!isVoiceLot(lot)) return lot + } + return null + } + + function applyVoiceCommand(data) { + var doc = BrickStore.activeDocument + if (!doc) { + console.log("[Voice] no active document") + return + } + + var base = findBaseLot(doc) + if (!base) { + console.log("[Voice] no suitable base lot") + return + } + + // Parse quantity + var hasQty = (data.qty !== undefined && data.qty !== null && !isNaN(data.qty)) + var qty = hasQty ? Number(data.qty) : 1 + + // Parse color + var targetColor = base.color + if (data.color && data.color.length > 0) { + var colorObj = BrickLink.color(data.color) + if (!colorObj || colorObj.isNull) { + console.log("[Voice] unknown color:", data.color) + return + } + if (base.item.hasKnownColor && !base.item.hasKnownColor(colorObj)) { + console.log("[Voice] color not valid for this item:", colorObj.name) + return + } + targetColor = colorObj + } + + // NEW API: add() returns the lot directly! + var lot = doc.lots.add(base.item, targetColor) + + // Set properties directly - no snapshot/diff needed! + lot.quantity = qty + lot.color = targetColor + lot.condition = base.condition + lot.remarks = (base.remarks || "") + " " + voice_tag + + if (data.raw) console.log("[Voice] added: " + data.raw) + } + + // Toggle voice action + ExtensionScriptAction { + id: voiceAction + text: "Start Voice" + actionFunction: function(document, lots) { + enabled = !enabled + voiceAction.text = enabled ? "Stop Voice" : "Start Voice" + console.log(enabled ? "[Voice] started" : "[Voice] stopped") + } + } +} +``` + +## Key Simplifications + +1. **Removed `snapshotVisibleLots()` function** - No longer needed +2. **Removed `findAddedLot()` function** - No longer needed +3. **Removed `baseLotFromView()` function** - Replaced with `findBaseLot()` using proper APIs +4. **Simplified `applyVoiceCommand()`** - Direct lot access, no workarounds +5. **Dynamic action text** - Shows "Start Voice" vs "Stop Voice" automatically +6. **Better base lot detection** - Uses `selectedLots` first, then `currentLot`, then fallback + +## Migration Guide for Your Extension + +### Step 1: Update findBaseLot() to use selectedLots +```qml +function findBaseLot(doc) { + // Use the new selectedLots API + var selected = doc.selectedLots + if (selected.length > 0) { + return selected[selected.length - 1] // Last selected + } + + // Fallback to your existing heuristic + return baseLotFromView(doc) +} +``` + +### Step 2: Simplify applyVoiceCommand() +```qml +function applyVoiceCommand(data) { + var doc = BrickStore.activeDocument + if (!doc) return + + var base = findBaseLot(doc) + if (!base) return + + // Direct lot creation and access! + var lot = doc.lots.add(base.item, targetColor) + lot.quantity = qty + lot.condition = base.condition + lot.remarks = base.remarks + " [voice]" +} +``` + +### Step 3: Make action text dynamic +```qml +ExtensionScriptAction { + id: voiceAction + text: "Start Voice" + actionFunction: function() { + enabled = !enabled + voiceAction.text = enabled ? "Stop Voice" : "Start Voice" + } +} +``` + +### Step 4: Remove property name checks +```qml +// Before +if ("quantity" in lot) lot.quantity = qty +else if ("qty" in lot) lot.qty = qty + +// After +lot.quantity = qty // Standard name +``` + +## API Reference + +### Document Properties + +| Property | Type | Description | +|----------|------|-------------| +| `selectedLots` | `List` | Array of currently selected lots | +| `currentLot` | `Lot` | The lot at the current cursor position | +| `lots` | `DocumentLots` | Lots collection for add/remove operations | +| `visibleLotCount` | `int` | Number of visible lots (after filtering) | + +### DocumentLots Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `add(item, color)` | `Lot` | **NEW:** Adds and returns the new lot | +| `remove(lot)` | `void` | Removes the specified lot | +| `at(index)` | `Lot` | Gets lot at index (unfiltered) | +| `visibleAt(index)` | `Lot` | Gets lot at visible index (filtered) | + +### Lot Properties (Standardized) + +| Property | Type | R/W | Description | +|----------|------|-----|-------------| +| `item` | `Item` | R/W | The item reference | +| `color` | `Color` | R/W | The color | +| `quantity` | `int` | R/W | **Standard name** | +| `condition` | `Condition` | R/W | **Standard name** | +| `remarks` | `string` | R/W | **Standard name** | +| `comments` | `string` | R/W | Public comments | +| `dateAdded` | `DateTime` | R/W | When lot was added | + +### ExtensionScriptAction + +| Property | Type | R/W | Description | +|----------|------|-----|-------------| +| `text` | `string` | R/W | **Dynamic!** Updates UI automatically | +| `location` | `Location` | R/W | `ExtrasMenu` or `ContextMenu` | +| `actionFunction` | `function` | W | Callback when action is triggered | + +## Build Instructions + +After these API changes, you need to rebuild BrickStore: + +```batch +clean-all.bat +build.bat +``` + +The new APIs will be available immediately in your extensions after rebuilding. + +## Testing Your Updated Extension + +1. Build BrickStore with the new API +2. Update your `voice.bs.qml` with the simplified code +3. Test selected lot detection: + - Select a lot manually + - Speak a voice command + - It should use the selected lot as the base +4. Test dynamic action text: + - Check if menu shows "Start Voice" when stopped + - Toggle it and verify it changes to "Stop Voice" +5. Test direct lot creation: + - No need for snapshot/diff anymore + - Lot should be immediately accessible after `add()` + +## Breaking Changes + +**None!** All changes are additions or clarifications: +- `add()` return type changed from `int` to `Lot` (better) +- New properties added (`selectedLots`, `currentLot`) +- Existing APIs unchanged and backward compatible + +Your current extension will continue to work, but can be simplified significantly using the new APIs. + +## Questions or Issues? + +If you encounter any problems with these APIs, please: +1. Check the console for error messages +2. Verify you're using the latest build +3. Review the complete example above +4. File an issue with specific error details diff --git a/README.md b/README.md index 0c634e25..a9971a97 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ BrickStore is a BrickLink offline management tool. It is **multi-platform** (Windows, macOS and Linux as well as iOS and Android), **multilingual** (currently English, German, Spanish, Swedish and French), **fast** and **stable**. +## Building + +There is an automated build script for Windows with documentation [here](/BUILD_WINDOWS.md). + +For other platforms see: [Compiling](https://www.brickstore.dev/compile). + ## License BrickStore is copyrighted ©2004-2026 by Robert Griebl, licensed under the diff --git a/build.bat b/build.bat new file mode 100644 index 00000000..1bcdd55f --- /dev/null +++ b/build.bat @@ -0,0 +1,91 @@ +@echo off +REM BrickStore Build Script for Windows +REM Automatically sets up Visual Studio environment and builds the project + +setlocal + +set QT_PATH=X:\QT\6.10.1\msvc2022_64 +set VS_PATH=A:\Visual Studio + +echo. +echo ======================================== +echo Building BrickStore +echo ======================================== +echo. + +REM Setup Visual Studio environment (64-bit) +echo Setting up Visual Studio 2022 environment... +call "%VS_PATH%\Common7\Tools\VsDevCmd.bat" -arch=amd64 -host_arch=amd64 >nul + +if errorlevel 1 ( + echo ERROR: Failed to setup Visual Studio environment + echo Make sure Visual Studio 2022 is installed at: %VS_PATH% + pause + exit /b 1 +) + +echo Visual Studio environment configured. +echo. + +REM Check if already configured +if exist "CMakeCache.txt" ( + echo CMake already configured. Building... + echo. + goto build +) + +REM Configure the project +echo Configuring project... +call configure.bat --qmake "%QT_PATH%\bin\qmake.exe" --release + +if errorlevel 1 ( + echo. + echo ERROR: Configuration failed + pause + exit /b 1 +) + +:build +REM Build the project +echo. +echo Building... +echo. +cmake --build . + +if errorlevel 1 ( + echo. + echo ERROR: Build failed + pause + exit /b 1 +) + +echo. +echo ======================================== +echo Build completed successfully! +echo Executable: bin\BrickStore.exe +echo ======================================== +echo. + +REM Check if Qt dependencies are already deployed +if exist "bin\Qt6Core.dll" ( + echo Qt dependencies already deployed. Executable is ready to run. + echo Run: bin\BrickStore.exe + echo. + echo If you need to redeploy Qt dependencies, run: deploy.bat + echo. + pause +) else ( + echo Qt dependencies not found. You need to deploy them first. + echo. + echo Would you like to deploy Qt dependencies now? (Y/N) + set /p DEPLOY_CHOICE= + if /i "%DEPLOY_CHOICE%"=="Y" ( + echo. + call deploy.bat + ) else ( + echo. + echo Remember to run deploy.bat before running BrickStore.exe + echo. + pause + ) +) diff --git a/clean-all.bat b/clean-all.bat new file mode 100644 index 00000000..d7f8c6de --- /dev/null +++ b/clean-all.bat @@ -0,0 +1,30 @@ +@echo off +REM Complete clean of all build artifacts + +echo Cleaning all build artifacts... +echo. + +if exist CMakeCache.txt del /F /Q CMakeCache.txt +if exist cmake_install.cmake del /F /Q cmake_install.cmake +if exist compile_commands.json del /F /Q compile_commands.json +if exist Makefile del /F /Q Makefile +if exist .ninja_deps del /F /Q .ninja_deps +if exist .ninja_log del /F /Q .ninja_log +if exist build.ninja del /F /Q build.ninja +if exist rules.ninja del /F /Q rules.ninja +if exist CTestTestfile.cmake del /F /Q CTestTestfile.cmake +if exist BundleConfig.cmake del /F /Q BundleConfig.cmake + +if exist CMakeFiles rd /S /Q CMakeFiles +if exist bin rd /S /Q bin +if exist generated rd /S /Q generated +if exist imports rd /S /Q imports +if exist qm rd /S /Q qm +if exist .cmake rd /S /Q .cmake +if exist _deps rd /S /Q _deps +REM DO NOT DELETE src and 3rdparty - these are source directories! + +echo. +echo All build artifacts cleaned! +echo. +pause diff --git a/configure.bat b/configure.bat new file mode 100644 index 00000000..c57c3f6a --- /dev/null +++ b/configure.bat @@ -0,0 +1,177 @@ +@echo off +REM Windows version of the configure script +REM Based on the original bash configure script + +setlocal EnableDelayedExpansion + +set "QMAKE_BIN=" +set "QT_CMAKE_BIN=" +set "BUILD_TYPE=" +set "BACKEND_ONLY=" + +REM Parse command line arguments +:parse_args +if "%~1"=="" goto check_deps +if /i "%~1"=="--qmake" ( + set "QMAKE_BIN=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="--debug" ( + set "BUILD_TYPE=-DCMAKE_BUILD_TYPE=Debug" + shift + goto parse_args +) +if /i "%~1"=="--release" ( + set "BUILD_TYPE=-DCMAKE_BUILD_TYPE=Release" + shift + goto parse_args +) +if /i "%~1"=="--backend-only" ( + set "BACKEND_ONLY=-DBACKEND_ONLY=TRUE" + shift + goto parse_args +) +if /i "%~1"=="--help" goto show_help +if /i "%~1"=="-h" goto show_help + +echo Unknown option: %~1 +goto show_help + +:show_help +echo Usage: configure.bat [options] +echo --qmake ^ Path to qmake.exe (e.g., X:\QT\6.10.1\msvc2022_64\bin\qmake.exe) +echo --release Build in Release mode +echo --debug Build in Debug mode +echo --backend-only Build backend only +echo --help Show this help +exit /b 1 + +:check_deps +REM Check for cmake +echo Checking for cmake... +where cmake >nul 2>&1 +if errorlevel 1 ( + echo FAIL: Could not find cmake in PATH + echo Please install CMake and add it to your PATH + pause + exit /b 2 +) +echo cmake found + +REM Check for ninja +echo Checking for ninja... +where ninja >nul 2>&1 +if errorlevel 1 ( + echo FAIL: Could not find ninja in PATH + echo Ninja is usually included with Qt or Visual Studio + echo You can also download it from: https://github.com/ninja-build/ninja/releases + pause + exit /b 2 +) +echo ninja found + +REM If qmake not specified, try to find it +if "%QMAKE_BIN%"=="" ( + where qmake >nul 2>&1 + if not errorlevel 1 ( + for /f "delims=" %%i in ('where qmake') do set "QMAKE_BIN=%%i" + ) +) + +if "%QMAKE_BIN%"=="" ( + echo FAIL: Could not find qmake + echo Please specify qmake location with --qmake option + echo Example: configure.bat --qmake X:\QT\6.10.1\msvc2022_64\bin\qmake.exe + exit /b 2 +) + +REM Check if qmake exists +echo Checking qmake at: %QMAKE_BIN% +if not exist "%QMAKE_BIN%" ( + echo FAIL: qmake not found at: %QMAKE_BIN% + pause + exit /b 2 +) +echo qmake found + +REM Check Qt version +echo Checking Qt version... +"%QMAKE_BIN%" -query QT_VERSION > qt_version.tmp 2>&1 +if errorlevel 1 ( + echo FAIL: Could not query Qt version from qmake + echo Command: "%QMAKE_BIN%" -query QT_VERSION + type qt_version.tmp + del qt_version.tmp + pause + exit /b 3 +) + +set /p QT_VERSION= ActionManager::allActions() const return all; } +QVector ActionManager::dynamicActions() const +{ + QVector actions; + actions.reserve(m_dynamicActions.size()); + for (const auto &action : m_dynamicActions) { + if (action) + actions.append(action); + } + return actions; +} + +void ActionManager::registerDynamicAction(QAction *action) +{ + if (action && !m_dynamicActions.contains(action)) { + action->setProperty("bsDynamicAction", true); + m_dynamicActions.append(action); + } +} + +void ActionManager::unregisterDynamicAction(QAction *action) +{ + m_dynamicActions.removeAll(action); +} + +void ActionManager::clearDynamicActions() +{ + m_dynamicActions.clear(); +} + void ActionManager::setCustomShortcut(const char *name, const QKeySequence &shortcut) { if (auto *a = const_cast(action(name))) { @@ -584,7 +613,16 @@ void ActionManager::retranslate() QAction *ActionManager::qAction(const char *name) { auto aa = const_cast(action(name)); - return aa ? aa->m_qaction : nullptr; + if (aa && aa->m_qaction) + return aa->m_qaction; + + // Check dynamic actions by objectName + QString nameStr = QString::fromLatin1(name); + for (const auto &action : m_dynamicActions) { + if (action && action->objectName() == nameStr) + return action; + } + return nullptr; } void ActionManager::setupQAction(Action &aa) diff --git a/src/common/actionmanager.h b/src/common/actionmanager.h index ab059248..33b53b58 100755 --- a/src/common/actionmanager.h +++ b/src/common/actionmanager.h @@ -130,6 +130,11 @@ class ActionManager : public QObject const Action *action(const char *name) const; QVector allActions() const; + QVector dynamicActions() const; + + void registerDynamicAction(QAction *action); + void unregisterDynamicAction(QAction *action); + void clearDynamicActions(); void setCustomShortcut(const char *name, const QKeySequence &shortcut); void setCustomShortcuts(const QMap &shortcuts); @@ -191,6 +196,7 @@ class ActionManager : public QObject std::vector m_actions; QMap m_qactionGroups; + QVector> m_dynamicActions; static ActionManager *s_inst; diff --git a/src/common/brickstore_wrapper.cpp b/src/common/brickstore_wrapper.cpp index 9fd000e0..9f65c7a5 100755 --- a/src/common/brickstore_wrapper.cpp +++ b/src/common/brickstore_wrapper.cpp @@ -29,6 +29,13 @@ #include "brickstore_wrapper_p.h" #include "version.h" +#if defined(BS_DESKTOP) +# include "desktop/mainwindow.h" +# include "desktop/additemdialog.h" +# include "desktop/selectitem.h" +# include "desktop/selectcolor.h" +#endif + using namespace std::chrono_literals; /*! \qmltype BrickStore @@ -155,6 +162,48 @@ QWindow *QmlBrickStore::mainWindow() const return Application::inst()->mainWindow(); } +BrickLink::QmlItem QmlBrickStore::addDialogItem() const +{ +#if defined(BS_DESKTOP) + auto mainWindow = MainWindow::inst(); + if (!mainWindow) + return { }; + + auto addDialog = mainWindow->addItemDialog(); + if (!addDialog) + return { }; + + auto selectItem = addDialog->findChild(); + if (!selectItem) + return { }; + + return BrickLink::QmlItem(selectItem->currentItem()); +#else + return { }; +#endif +} + +BrickLink::QmlColor QmlBrickStore::addDialogColor() const +{ +#if defined(BS_DESKTOP) + auto mainWindow = MainWindow::inst(); + if (!mainWindow) + return { }; + + auto addDialog = mainWindow->addItemDialog(); + if (!addDialog) + return { }; + + auto selectColor = addDialog->findChild(); + if (!selectColor) + return { }; + + return BrickLink::QmlColor(selectColor->currentColor()); +#else + return { }; +#endif +} + /*! \qmlmethod string BrickStore::exchangeRate(string fromCode, string toCode) Returns the currency exchange cross-rate between the two given \a fromCode and \a toCode @@ -391,6 +440,8 @@ QmlDocument::QmlDocument(Document *doc) }); connect(doc, &Document::selectedLotsChanged, this, &QmlDocument::qmlSelectedLotsChanged); + connect(doc, &Document::selectedLotsChanged, + this, &QmlDocument::qmlCurrentLotChanged); connect(model(), &DocumentModel::lotCountChanged, this, &QmlDocument::lotCountChanged); @@ -432,6 +483,17 @@ QList QmlDocument::qmlSelectedLots() return qmlLots; } +BrickLink::QmlLot QmlDocument::qmlCurrentLot() +{ + auto idx = m_doc->currentIndex(); + if (idx.isValid()) { + auto *lot = model()->lot(idx); + if (lot) + return BrickLink::QmlLot(lot, qmlLots()); + } + return BrickLink::QmlLot(); +} + QString QmlDocument::filterString() const { return const_cast(model())->filterParser()->toString(model()->filter(), true /*symbolic*/); @@ -1142,19 +1204,18 @@ QmlDocumentLots::QmlDocumentLots(DocumentModel *model) } } -/*! \qmlmethod int Document::lots.add(Item item, Color color) +/*! \qmlmethod Lot Document::lots.add(Item item, Color color) Adds a new lot for the given \a item and \a color combination to the document. - Returns the index of the newly created lot in the document. - This index is independent of sort order and filtering. + Returns the newly created lot. */ -int QmlDocumentLots::add(BrickLink::QmlItem item, BrickLink::QmlColor color) +BrickLink::QmlLot QmlDocumentLots::add(BrickLink::QmlItem item, BrickLink::QmlColor color) { auto lot = new Lot(); lot->setItem(item.wrappedObject()); lot->setColor(color.wrappedObject()); auto lotRef = lot; m_model->appendLot(std::move(lot)); - return int(m_model->lots().indexOf(lotRef)); + return BrickLink::QmlLot(lotRef, this); } /*! \qmlmethod Document::lots.remove(Lot lot) diff --git a/src/common/brickstore_wrapper.h b/src/common/brickstore_wrapper.h index 254d7ea3..8b7b9036 100755 --- a/src/common/brickstore_wrapper.h +++ b/src/common/brickstore_wrapper.h @@ -59,6 +59,7 @@ class QmlDocument : public QAbstractProxyModel Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged FINAL) Q_PROPERTY(QmlDocumentLots *lots READ qmlLots CONSTANT FINAL) Q_PROPERTY(QList selectedLots READ qmlSelectedLots NOTIFY qmlSelectedLotsChanged FINAL) + Q_PROPERTY(BrickLink::QmlLot currentLot READ qmlCurrentLot NOTIFY qmlCurrentLotChanged FINAL) Q_PROPERTY(QmlDocumentColumnModel *columnModel READ columnModel CONSTANT FINAL) Q_PROPERTY(QItemSelectionModel *selectionModel READ selectionModel CONSTANT FINAL) Q_PROPERTY(Document *document READ document CONSTANT FINAL) @@ -85,6 +86,7 @@ class QmlDocument : public QAbstractProxyModel QVariantList qmlSortColumns() const; QmlDocumentLots *qmlLots(); QList qmlSelectedLots(); + BrickLink::QmlLot qmlCurrentLot(); QString filterString() const; void setFilterString(const QString &newFilterString); @@ -135,6 +137,7 @@ class QmlDocument : public QAbstractProxyModel void qmlSortColumnsChanged(); void qmlSelectedLotsChanged(); + void qmlCurrentLotChanged(); private: DocumentModel *model() { return m_doc->model(); } @@ -509,7 +512,7 @@ class QmlDocumentLots : public QObject public: QmlDocumentLots(DocumentModel *model); - Q_INVOKABLE int add(BrickLink::QmlItem item, BrickLink::QmlColor color); + Q_INVOKABLE BrickLink::QmlLot add(BrickLink::QmlItem item, BrickLink::QmlColor color); Q_INVOKABLE void remove(BrickLink::QmlLot lot); Q_INVOKABLE void removeAt(int index); Q_INVOKABLE void removeVisibleAt(int index); @@ -586,6 +589,8 @@ class QmlBrickStore : public QObject Q_PROPERTY(QVariantMap about READ about CONSTANT FINAL) Q_PROPERTY(QmlDebug *debug READ debug CONSTANT FINAL) Q_PROPERTY(QWindow *mainWindow READ mainWindow NOTIFY mainWindowChanged FINAL) + Q_PROPERTY(BrickLink::QmlItem addDialogItem READ addDialogItem NOTIFY addDialogItemChanged FINAL) + Q_PROPERTY(BrickLink::QmlColor addDialogColor READ addDialogColor NOTIFY addDialogColorChanged FINAL) public: static QmlBrickStore *inst(); @@ -601,6 +606,8 @@ class QmlBrickStore : public QObject QmlDebug *debug() const; QString defaultCurrencyCode() const; QWindow *mainWindow() const; + BrickLink::QmlItem addDialogItem() const; + BrickLink::QmlColor addDialogColor() const; Q_INVOKABLE double exchangeRate(const QString &fromCode, const QString &toCode) const; @@ -638,6 +645,8 @@ class QmlBrickStore : public QObject void activeDocumentChanged(QmlDocument *doc); void mainWindowChanged(QWindow *newWindow); void versionWasUpdated(const QString &version, const QString &changeLog, const QUrl &releaseUrl); + void addDialogItemChanged(BrickLink::QmlItem item); + void addDialogColorChanged(BrickLink::QmlColor color); void versionCanBeUpdated(const QString &version, const QString &changeLog, const QUrl &releaseUrl); private: diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp index 412b4c78..815d6a35 100755 --- a/src/desktop/mainwindow.cpp +++ b/src/desktop/mainwindow.cpp @@ -314,14 +314,32 @@ void MainWindow::setupScripts() QList extrasActions; for (auto script : scripts) { + int extensionActionCounter = 0; const auto extensionActions = script->extensionActions(); for (auto extensionAction : extensionActions) { auto action = new QAction(extensionAction->text(), extensionAction); + + // Set unique objectName for toolbar system + // Use script name and counter to generate unique ID + QString scriptName = script->name().isEmpty() ? u"unnamed"_qs : script->name(); + scriptName.replace(u' ', u'_'); // Replace spaces for objectName + QString objectName = QString::fromLatin1("extension_%1_%2") + .arg(scriptName) + .arg(extensionActionCounter++); + action->setObjectName(objectName); + + // Register with ActionManager for toolbar customization + ActionManager::inst()->registerDynamicAction(action); + if (extensionAction->location() == ExtensionScriptAction::Location::ContextMenu) m_extensionContextActions.append(action); else extrasActions.append(action); + // Update QAction text when ExtensionScriptAction text changes + connect(extensionAction, &ExtensionScriptAction::textChanged, + action, &QAction::setText); + connect(action, &QAction::triggered, extensionAction, [extensionAction]() { try { extensionAction->executeAction(); @@ -333,6 +351,18 @@ void MainWindow::setupScripts() const auto printingActions = script->printingActions(); for (auto printingAction : printingActions) { auto action = new QAction(printingAction->text(), printingAction); + + // Set unique objectName for toolbar system + QString scriptName = script->name().isEmpty() ? u"unnamed"_qs : script->name(); + scriptName.replace(u' ', u'_'); // Replace spaces for objectName + QString objectName = QString::fromLatin1("printing_%1_%2") + .arg(scriptName) + .arg(extensionActionCounter++); + action->setObjectName(objectName); + + // Register with ActionManager for toolbar customization + ActionManager::inst()->registerDynamicAction(action); + extrasActions.append(action); connect(action, &QAction::triggered, printingAction, [this, printingAction]() { @@ -362,12 +392,19 @@ void MainWindow::setupScripts() m_extrasMenu->removeAction(a); } m_extensionContextActions.clear(); + + // Clear dynamic actions from ActionManager before reloading + ActionManager::inst()->clearDynamicActions(); }); connect(ScriptManager::inst(), &ScriptManager::reloaded, - this, [reloadScripts]() { + this, [this, reloadScripts]() { reloadScripts(ScriptManager::inst()->scripts()); + // Refresh toolbar to include newly registered extension actions + setupToolBar(); }); reloadScripts(ScriptManager::inst()->scripts()); + // Refresh toolbar after initial script load + setupToolBar(); } void MainWindow::languageChange() @@ -1204,6 +1241,11 @@ void MainWindow::showAddItemDialog(const BrickLink::Item *item, const BrickLink: } } +AddItemDialog *MainWindow::addItemDialog() const +{ + return m_add_dialog; +} + void MainWindow::blockUpdate(bool blocked) { static QUndoStack blockStack; diff --git a/src/desktop/mainwindow.h b/src/desktop/mainwindow.h index 2883d7de..dfa3bf04 100755 --- a/src/desktop/mainwindow.h +++ b/src/desktop/mainwindow.h @@ -70,6 +70,8 @@ class MainWindow : public QMainWindow void showAddItemDialog(const BrickLink::Item *item = nullptr, const BrickLink::Color *color = nullptr); + AddItemDialog *addItemDialog() const; + public slots: void blockUpdate(bool blocked); void titleUpdate(); diff --git a/src/desktop/settingsdialog.cpp b/src/desktop/settingsdialog.cpp index d2de2678..925ea6bf 100644 --- a/src/desktop/settingsdialog.cpp +++ b/src/desktop/settingsdialog.cpp @@ -68,6 +68,15 @@ class ActionModel : public QAbstractItemModel } } + // Add dynamic actions (e.g., extension actions) + const auto dynamicActions = ActionManager::inst()->dynamicActions(); + for (auto *qa : dynamicActions) { + QString mtitle = rootMenu(qa); + if (mtitle.isEmpty()) + mtitle = tr("Extensions"); + m_actions.insert(mtitle, qa->objectName()); + } + static const std::vector> separators = { { u"-"_qs, QT_TR_NOOP("Bar Separator") }, { u"|"_qs, QT_TR_NOOP("Space Separator") },