Compare commits
14 Commits
v0.1.1
...
docs/split
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2200b4042 | ||
|
|
c858706d48 | ||
|
|
724440dcb7 | ||
|
|
2f594dac0a | ||
|
|
939b81385e | ||
|
|
84b69b935b | ||
| a6e84552da | |||
| 015df38328 | |||
| db85277f26 | |||
| 679aaec6d4 | |||
| deeb6376f7 | |||
| 103fc28bc6 | |||
| 79c85ed2e5 | |||
| 38358e431d |
61
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Architecture
|
||||
|
||||
## Bootstrap flow
|
||||
|
||||
```
|
||||
FreeCAD startup
|
||||
└─ src/Mod/Create/Init.py
|
||||
└─ setup_kindred_addons()
|
||||
├─ exec(mods/ztools/ztools/Init.py)
|
||||
└─ exec(mods/silo/pkg/freecad/Init.py)
|
||||
|
||||
└─ src/Mod/Create/InitGui.py
|
||||
├─ setup_kindred_workbenches()
|
||||
│ ├─ exec(mods/ztools/ztools/InitGui.py)
|
||||
│ │ ├─ registers ZToolsWorkbench
|
||||
│ │ └─ installs _ZToolsPartDesignManipulator (global)
|
||||
│ └─ exec(mods/silo/pkg/freecad/InitGui.py)
|
||||
│ └─ registers SiloWorkbench
|
||||
└─ Deferred setup (QTimer):
|
||||
├─ 1500ms: _setup_silo_auth_panel() → "Database Auth" dock
|
||||
├─ 2000ms: _setup_silo_menu() → SiloMenuManipulator
|
||||
├─ 3000ms: _check_silo_first_start() → settings prompt
|
||||
└─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock
|
||||
```
|
||||
|
||||
## Key source layout
|
||||
|
||||
```
|
||||
src/Mod/Create/ Kindred bootstrap module (Python)
|
||||
├── Init.py Adds mods/ addon paths, loads Init.py files
|
||||
└── InitGui.py Loads workbenches, installs Silo manipulators
|
||||
|
||||
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin
|
||||
src/Gui/CommandOrigin.cpp Origin_Commit/Pull/Push/Info/BOM commands
|
||||
src/Gui/OriginManager.h/.cpp Origin lifecycle management
|
||||
src/Gui/OriginSelectorWidget.h/.cpp UI for origin selection
|
||||
|
||||
mods/ztools/ [submodule] ztools workbench
|
||||
├── ztools/InitGui.py ZToolsWorkbench + PartDesign manipulator
|
||||
├── ztools/ztools/
|
||||
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
|
||||
│ ├── datums/core.py Datum creation via Part::AttachExtension
|
||||
│ └── resources/ Icons, theme utilities
|
||||
└── CatppuccinMocha/ Theme preference pack (QSS)
|
||||
|
||||
mods/silo/ [submodule] Silo parts database
|
||||
├── cmd/ Go server entry points
|
||||
├── internal/ Go API, database, auth, storage packages
|
||||
├── pkg/freecad/ FreeCAD workbench (Python)
|
||||
│ ├── InitGui.py SiloWorkbench
|
||||
│ ├── silo_commands.py Commands + SiloClient API
|
||||
│ └── silo_origin.py FileOrigin backend for Silo
|
||||
├── pkg/calc/ LibreOffice Calc extension (Python)
|
||||
├── deployments/ Docker compose configuration
|
||||
└── migrations/ PostgreSQL schema migrations (001–010)
|
||||
|
||||
src/Gui/Stylesheets/ QSS themes and SVG assets
|
||||
resources/preferences/ Canonical preference pack (KindredCreate)
|
||||
```
|
||||
|
||||
See [INTEGRATION_PLAN.md](INTEGRATION_PLAN.md) for architecture layers and phase status.
|
||||
110
docs/COMPONENTS.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Components
|
||||
|
||||
## ztools workbench
|
||||
|
||||
**Registered commands (9):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) |
|
||||
| `ZTools_DatumManager` | Manage existing datum objects |
|
||||
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) |
|
||||
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation |
|
||||
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly |
|
||||
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis |
|
||||
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles |
|
||||
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment |
|
||||
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation |
|
||||
|
||||
**PartDesign integration** via `_ZToolsPartDesignManipulator`:
|
||||
- `ZTools_DatumCreator`, `ZTools_DatumManager` → "Part Design Helper Features" toolbar
|
||||
- `ZTools_EnhancedPocket` → "Part Design Modeling Features" toolbar
|
||||
- `ZTools_RotatedLinearPattern` → "Part Design Transformation Features" toolbar
|
||||
- Same commands inserted into Part Design menu after `PartDesign_Boolean`
|
||||
|
||||
**Datum types (7):** offset_from_face, offset_from_plane, midplane, 3_points, normal_to_edge, angled, tangent_to_cylinder. All except tangent_to_cylinder use `Part::AttachExtension` for automatic parametric updates.
|
||||
|
||||
---
|
||||
|
||||
## Origin commands (C++)
|
||||
|
||||
The Origin abstraction (`src/Gui/FileOrigin.h`) provides a backend-agnostic interface for document storage. Commands delegate to the active `FileOrigin` implementation (currently `LocalFileOrigin` for local files, `SiloOrigin` via `mods/silo/pkg/freecad/silo_origin.py` for Silo-tracked documents).
|
||||
|
||||
**Registered commands (5):**
|
||||
|
||||
| Command | Function | Icon |
|
||||
|---------|----------|------|
|
||||
| `Origin_Commit` | Commit changes as a new revision | `silo-commit` |
|
||||
| `Origin_Pull` | Pull a specific revision from the origin | `silo-pull` |
|
||||
| `Origin_Push` | Push local changes to the origin | `silo-push` |
|
||||
| `Origin_Info` | Show document information from origin | `silo-info` |
|
||||
| `Origin_BOM` | Show Bill of Materials for this document | `silo-bom` |
|
||||
|
||||
These appear in the File menu and "Origin Tools" toolbar across all workbenches (see `src/Gui/Workbench.cpp`).
|
||||
|
||||
---
|
||||
|
||||
## Silo workbench
|
||||
|
||||
**Registered commands (14):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `Silo_New` | Create new Silo-tracked document |
|
||||
| `Silo_Open` | Open file from Silo database |
|
||||
| `Silo_Save` | Save to Silo (create revision) |
|
||||
| `Silo_Commit` | Commit current revision |
|
||||
| `Silo_Pull` | Pull latest revision from server |
|
||||
| `Silo_Push` | Push local changes to server |
|
||||
| `Silo_Info` | View item metadata and history |
|
||||
| `Silo_BOM` | Bill of materials dialog (BOM + Where Used) |
|
||||
| `Silo_TagProjects` | Assign project tags |
|
||||
| `Silo_Rollback` | Rollback to previous revision |
|
||||
| `Silo_SetStatus` | Set revision status (draft/review/released/obsolete) |
|
||||
| `Silo_Settings` | Configure API URL, projects dir, SSL certificates |
|
||||
| `Silo_ToggleMode` | Swap Ctrl+O/S/N between FreeCAD and Silo commands |
|
||||
| `Silo_Auth` | Login/logout authentication panel |
|
||||
|
||||
**Global integration** via `SiloMenuManipulator` in `src/Mod/Create/InitGui.py`:
|
||||
- File menu: Silo_New, Silo_Open, Silo_Save, Silo_Commit, Silo_Pull, Silo_Push, Silo_BOM
|
||||
- File toolbar: Silo_ToggleMode button
|
||||
|
||||
**Server architecture:** Go REST API (38+ routes) + PostgreSQL + MinIO S3. Authentication via local (bcrypt), LDAP, or OIDC backends. See `mods/silo/docs/` for server documentation.
|
||||
|
||||
**LibreOffice Calc extension** (`mods/silo/pkg/calc/`): BOM management, item creation, and AI-assisted descriptions via OpenRouter API. Shares the same Silo REST API and auth token system.
|
||||
|
||||
---
|
||||
|
||||
## Theme
|
||||
|
||||
**Canonical source:** `resources/preferences/KindredCreate/KindredCreate.qss`
|
||||
|
||||
Four copies must stay in sync:
|
||||
1. `resources/preferences/KindredCreate/KindredCreate.qss` (canonical)
|
||||
2. `src/Gui/Stylesheets/KindredCreate.qss`
|
||||
3. `src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`
|
||||
4. `mods/ztools/CatppuccinMocha/CatppuccinMocha.qss`
|
||||
|
||||
---
|
||||
|
||||
## Icon infrastructure
|
||||
|
||||
### Qt resource icons (`src/Gui/Icons/`)
|
||||
|
||||
5 `silo-*` SVGs registered in `resource.qrc`, used by C++ Origin commands:
|
||||
|
||||
`silo-bom.svg`, `silo-commit.svg`, `silo-info.svg`, `silo-pull.svg`, `silo-push.svg`
|
||||
|
||||
### Silo module icons (`mods/silo/pkg/freecad/resources/icons/`)
|
||||
|
||||
10 SVGs loaded at runtime by the `_icon()` function in `silo_commands.py`:
|
||||
|
||||
`silo-auth.svg`, `silo-bom.svg`, `silo-commit.svg`, `silo-info.svg`, `silo-new.svg`, `silo-open.svg`, `silo-pull.svg`, `silo-push.svg`, `silo-save.svg`, `silo.svg`
|
||||
|
||||
### Missing icons
|
||||
|
||||
3 command icon names have no corresponding SVG file: `silo-tag`, `silo-rollback`, `silo-status`. The `_icon()` function returns an empty string for these, so `Silo_TagProjects`, `Silo_Rollback`, and `Silo_SetStatus` render without toolbar icons.
|
||||
|
||||
### Palette
|
||||
|
||||
All silo-* icons use the Catppuccin Mocha color scheme. See `kindred-icons/README.md` for palette specification and icon design standards.
|
||||
75
docs/KNOWN_ISSUES.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Known Issues
|
||||
|
||||
## Issues
|
||||
|
||||
### Critical
|
||||
|
||||
1. **QSS duplication.** Four copies of the stylesheet must be kept in sync manually. A build step or symlinks should eliminate this.
|
||||
|
||||
2. **WorkbenchManipulator timing.** The `_ZToolsPartDesignManipulator` appends commands by name. If ZToolsWorkbench hasn't been activated when the user switches to PartDesign, the commands may not be registered. The manipulator API tolerates missing commands silently, but buttons won't appear.
|
||||
|
||||
3. **Silo shortcut persistence.** `Silo_ToggleMode` stores original shortcuts in a module-level dict. If FreeCAD crashes with Silo mode on, original shortcuts are lost on next launch.
|
||||
|
||||
### High
|
||||
|
||||
4. **Silo authentication not production-hardened.** Local auth (bcrypt) works end-to-end. LDAP (FreeIPA) and OIDC (Keycloak) backends are coded but depend on infrastructure not yet deployed. FreeCAD client has `Silo_Auth` dock panel for login and API token management. Server has session middleware (`alexedwards/scs`), CSRF protection (`nosurf`), and role-based access control (admin/editor/viewer). Migration `009_auth.sql` adds users, api_tokens, and sessions tables.
|
||||
|
||||
5. **No unit tests.** Zero test coverage for ztools and Silo FreeCAD commands. Silo Go backend also lacks tests.
|
||||
|
||||
6. **Assembly solver datum handling is minimal.** The `findPlacement()` fix in `src/Mod/Assembly/UtilsAssembly.py` extracts placement from `obj.Shape.Faces[0]` for `PartDesign::Plane` and from shape vertex for `PartDesign::Point`. Does not handle empty shapes or non-planar datum objects.
|
||||
|
||||
### Medium
|
||||
|
||||
7. **`Silo_BOM` requires Silo-tracked document.** Depends on `SiloPartNumber` property. Unregistered documents show a warning with no registration path.
|
||||
|
||||
8. **PartDesign menu insertion fragility.** `_ZToolsPartDesignManipulator.modifyMenuBar()` inserts after `PartDesign_Boolean`. If upstream renames this command, insertions silently fail.
|
||||
|
||||
9. **tangent_to_cylinder falls back to manual placement.** TangentPlane MapMode requires a vertex reference not collected by the current UI.
|
||||
|
||||
10. **`delete_bom_entry()` bypasses error normalization.** Uses raw `urllib.request` instead of `SiloClient._request()`.
|
||||
|
||||
11. **Missing Silo icons.** Three commands reference icons that don't exist: `silo-tag.svg` (`Silo_TagProjects`), `silo-rollback.svg` (`Silo_Rollback`), `silo-status.svg` (`Silo_SetStatus`). The `_icon()` function returns an empty string, so these commands render without toolbar icons.
|
||||
|
||||
### Fixed (retain for reference)
|
||||
|
||||
12. **OndselSolver Newton-Raphson convergence.** `NewtonRaphson::isConvergedToNumericalLimit()` compared `dxNorms->at(iterNo)` to itself instead of `dxNorms->at(iterNo - 1)`. This prevented convergence detection on complex assemblies, causing solver exhaustion and "grounded object moved" warnings. Fixed in Kindred fork (`src/3rdParty/OndselSolver`). Needs upstreaming to `FreeCAD/OndselSolver`.
|
||||
|
||||
13. **Assembly solver crash on document restore.** `AssemblyObject::onChanged()` called `updateSolveStatus()` when the Group property changed during document restore, triggering the solver while child objects were still deserializing (SIGSEGV). Fixed with `isRestoring()` and `isPerformingTransaction()` guards at `src/Mod/Assembly/App/AssemblyObject.cpp:143`.
|
||||
|
||||
---
|
||||
|
||||
## Incomplete features
|
||||
|
||||
### Silo
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Authentication | Local auth complete | LDAP/OIDC backends coded, pending infrastructure |
|
||||
| CSRF protection | Implemented | `nosurf` library on web form routes |
|
||||
| File locking | Not implemented | Needed to prevent concurrent edits |
|
||||
| Odoo ERP integration | Stub only | Returns "not yet implemented" |
|
||||
| Part number date segments | Broken | `formatDate()` returns error |
|
||||
| Location/inventory APIs | Tables exist, no handlers | |
|
||||
| CSV import rollback | Not implemented | `bom_handlers.go` |
|
||||
|
||||
### ztools
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Tangent-to-cylinder attachment | Manual fallback | No vertex ref in UI |
|
||||
| Angled datum live editing | Incomplete | AttachmentOffset not updated in panel |
|
||||
| Assembly pattern undo | Not implemented | |
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
1. **Authentication hardening** -- Deploy FreeIPA and Keycloak infrastructure. End-to-end test LDAP and OIDC flows. Harden token rotation and session expiry.
|
||||
|
||||
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save.
|
||||
|
||||
3. **File locking** -- Pessimistic locks on `Silo_Open` to prevent concurrent edits. Requires server-side lock table and client-side lock display.
|
||||
|
||||
4. **Build system** -- CMake install rules for `mods/` submodules so packages include ztools and Silo without manual steps.
|
||||
|
||||
5. **Test coverage** -- Unit tests for ztools datum creation, Silo FreeCAD commands, and Go API endpoints.
|
||||
29
docs/OVERVIEW.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Kindred Create
|
||||
|
||||
**Last updated:** 2026-02-06
|
||||
**Branch:** main @ `c858706d480`
|
||||
**Kindred Create:** v0.1.0
|
||||
**FreeCAD base:** v1.0.0
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Contents |
|
||||
|----------|----------|
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Bootstrap flow, source layout, submodules |
|
||||
| [COMPONENTS.md](COMPONENTS.md) | ztools, Silo, Origin commands, theme, icons |
|
||||
| [KNOWN_ISSUES.md](KNOWN_ISSUES.md) | Bugs, incomplete features, next steps |
|
||||
| [INTEGRATION_PLAN.md](INTEGRATION_PLAN.md) | Architecture layers, integration phases |
|
||||
| [CI_CD.md](CI_CD.md) | Build and release workflows |
|
||||
|
||||
## Submodules
|
||||
|
||||
| Submodule | Path | Source | Pinned commit |
|
||||
|-----------|------|--------|---------------|
|
||||
| ztools | `mods/ztools` | `gitea.kindred.internal/kindred/ztools-0065` | `d2f94c3` |
|
||||
| silo | `mods/silo` | `gitea.kindred.internal/kindred/silo-0062` | `27e112e` |
|
||||
| OndselSolver | `src/3rdParty/OndselSolver` | `gitea.kindred.internal/kindred/ondsel` | `5d1988b` |
|
||||
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` | `756c91a` |
|
||||
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | `01e242e` |
|
||||
| googletest | `tests/lib` | `github.com/google/googletest` | `56efe39` |
|
||||
|
||||
OndselSolver is forked from `github.com/FreeCAD/OndselSolver` to carry a Newton-Raphson convergence fix (see [KNOWN_ISSUES.md](KNOWN_ISSUES.md#12)).
|
||||
@@ -1,215 +0,0 @@
|
||||
# Repository State
|
||||
|
||||
**Last updated:** 2026-02-03
|
||||
**Branch:** main @ `0ef9ffcf51`
|
||||
**Kindred Create:** v0.1.0
|
||||
**FreeCAD base:** v1.0.0
|
||||
|
||||
## Submodules
|
||||
|
||||
| Submodule | Path | Source | Pinned commit |
|
||||
|-----------|------|--------|---------------|
|
||||
| ztools | `mods/ztools` | `gitea.kindred.internal/kindred/ztools-0065` | `d2f94c3` |
|
||||
| silo | `mods/silo` | `gitea.kindred.internal/kindred/silo-0062` | `17a10ab` |
|
||||
| OndselSolver | `src/3rdParty/OndselSolver` | `gitea.kindred.internal/kindred/ondsel` | `e32c9cd` |
|
||||
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` | `756c91a` |
|
||||
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | `01e242e` |
|
||||
| googletest | `tests/lib` | `github.com/google/googletest` | `56efe39` |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Bootstrap flow
|
||||
|
||||
```
|
||||
FreeCAD startup
|
||||
└─ src/Mod/Create/Init.py
|
||||
└─ setup_kindred_addons()
|
||||
├─ exec(mods/ztools/ztools/Init.py)
|
||||
└─ exec(mods/silo/pkg/freecad/Init.py)
|
||||
|
||||
└─ src/Mod/Create/InitGui.py
|
||||
├─ setup_kindred_workbenches()
|
||||
│ ├─ exec(mods/ztools/ztools/InitGui.py)
|
||||
│ │ ├─ registers ZToolsWorkbench
|
||||
│ │ └─ installs _ZToolsPartDesignManipulator (global)
|
||||
│ └─ exec(mods/silo/pkg/freecad/InitGui.py)
|
||||
│ └─ registers SiloWorkbench
|
||||
└─ Deferred setup (QTimer):
|
||||
├─ 1500ms: _setup_silo_auth_panel() → "Database Auth" dock
|
||||
├─ 2000ms: _setup_silo_menu() → SiloMenuManipulator
|
||||
├─ 3000ms: _check_silo_first_start() → settings prompt
|
||||
└─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock
|
||||
```
|
||||
|
||||
### Key source layout
|
||||
|
||||
```
|
||||
src/Mod/Create/ Kindred bootstrap module (Python)
|
||||
├── Init.py Adds mods/ addon paths, loads Init.py files
|
||||
└── InitGui.py Loads workbenches, installs Silo manipulators
|
||||
|
||||
mods/ztools/ [submodule] ztools workbench
|
||||
├── ztools/InitGui.py ZToolsWorkbench + PartDesign manipulator
|
||||
├── ztools/ztools/
|
||||
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
|
||||
│ ├── datums/core.py Datum creation via Part::AttachExtension
|
||||
│ └── resources/ Icons, theme utilities
|
||||
└── CatppuccinMocha/ Theme preference pack (QSS)
|
||||
|
||||
mods/silo/ [submodule] Silo parts database
|
||||
├── cmd/ Go server entry points
|
||||
├── internal/ Go API, database, storage packages
|
||||
├── pkg/freecad/ FreeCAD workbench (Python)
|
||||
│ ├── InitGui.py SiloWorkbench
|
||||
│ └── silo_commands.py Commands + SiloClient API
|
||||
├── deployments/ Docker compose configuration
|
||||
└── migrations/ PostgreSQL schema migrations
|
||||
|
||||
src/Gui/Stylesheets/ QSS themes and SVG assets
|
||||
resources/preferences/ Canonical preference pack (KindredCreate)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component status
|
||||
|
||||
### ztools workbench
|
||||
|
||||
**Registered commands (9):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) |
|
||||
| `ZTools_DatumManager` | Manage existing datum objects |
|
||||
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) |
|
||||
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation |
|
||||
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly |
|
||||
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis |
|
||||
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles |
|
||||
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment |
|
||||
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation |
|
||||
|
||||
**PartDesign integration** via `_ZToolsPartDesignManipulator`:
|
||||
- `ZTools_DatumCreator`, `ZTools_DatumManager` → "Part Design Helper Features" toolbar
|
||||
- `ZTools_EnhancedPocket` → "Part Design Modeling Features" toolbar
|
||||
- `ZTools_RotatedLinearPattern` → "Part Design Transformation Features" toolbar
|
||||
- Same commands inserted into Part Design menu after `PartDesign_Boolean`
|
||||
|
||||
**Datum types (7):** offset_from_face, offset_from_plane, midplane, 3_points, normal_to_edge, angled, tangent_to_cylinder. All except tangent_to_cylinder use `Part::AttachExtension` for automatic parametric updates.
|
||||
|
||||
### Silo workbench
|
||||
|
||||
**Registered commands (13):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `Silo_New` | Create new Silo-tracked document |
|
||||
| `Silo_Open` | Open file from Silo database |
|
||||
| `Silo_Save` | Save to Silo (create revision) |
|
||||
| `Silo_Commit` | Commit current revision |
|
||||
| `Silo_Pull` | Pull latest revision from server |
|
||||
| `Silo_Push` | Push local changes to server |
|
||||
| `Silo_Info` | View item metadata and history |
|
||||
| `Silo_BOM` | Bill of materials dialog (BOM + Where Used) |
|
||||
| `Silo_TagProjects` | Assign project tags |
|
||||
| `Silo_Rollback` | Rollback to previous revision |
|
||||
| `Silo_SetStatus` | Set revision status (draft/review/released/obsolete) |
|
||||
| `Silo_Settings` | Configure API URL, projects dir, SSL certificates |
|
||||
| `Silo_ToggleMode` | Swap Ctrl+O/S/N between FreeCAD and Silo commands |
|
||||
|
||||
**Global integration** via `SiloMenuManipulator` in `src/Mod/Create/InitGui.py`:
|
||||
- File menu: Silo_New, Silo_Open, Silo_Save, Silo_Commit, Silo_Pull, Silo_Push, Silo_BOM
|
||||
- File toolbar: Silo_ToggleMode button
|
||||
|
||||
**Server architecture:** Go REST API (38 routes) + PostgreSQL + MinIO. See `mods/silo/docs/REPOSITORY_STATUS.md` for route details.
|
||||
|
||||
### Theme
|
||||
|
||||
**Canonical source:** `resources/preferences/KindredCreate/KindredCreate.qss`
|
||||
|
||||
Four copies must stay in sync:
|
||||
1. `resources/preferences/KindredCreate/KindredCreate.qss` (canonical)
|
||||
2. `src/Gui/Stylesheets/KindredCreate.qss`
|
||||
3. `src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`
|
||||
4. `mods/ztools/CatppuccinMocha/CatppuccinMocha.qss`
|
||||
|
||||
---
|
||||
|
||||
## Known issues
|
||||
|
||||
### Critical
|
||||
|
||||
1. **QSS duplication.** Four copies of the stylesheet must be kept in sync manually. A build step or symlinks should eliminate this.
|
||||
|
||||
2. **WorkbenchManipulator timing.** The `_ZToolsPartDesignManipulator` appends commands by name. If ZToolsWorkbench hasn't been activated when the user switches to PartDesign, the commands may not be registered. The manipulator API tolerates missing commands silently, but buttons won't appear.
|
||||
|
||||
3. **Silo shortcut persistence.** `Silo_ToggleMode` stores original shortcuts in a module-level dict. If FreeCAD crashes with Silo mode on, original shortcuts are lost on next launch.
|
||||
|
||||
### High
|
||||
|
||||
4. **No authentication on Silo server.** All API endpoints are publicly accessible. Required before multi-user deployment.
|
||||
|
||||
5. **No unit tests.** Zero test coverage for ztools and Silo FreeCAD commands. Silo Go backend also lacks tests.
|
||||
|
||||
6. **Assembly solver datum handling is minimal.** The `findPlacement()` fix extracts placement from `obj.Shape.Faces[0]` for `PartDesign::Plane`. Does not handle empty shapes or non-planar datum objects.
|
||||
|
||||
### Medium
|
||||
|
||||
7. **`Silo_BOM` requires Silo-tracked document.** Depends on `SiloPartNumber` property. Unregistered documents show a warning with no registration path.
|
||||
|
||||
8. **PartDesign menu insertion fragility.** `_ZToolsPartDesignManipulator.modifyMenuBar()` inserts after `PartDesign_Boolean`. If upstream renames this command, insertions silently fail.
|
||||
|
||||
9. **tangent_to_cylinder falls back to manual placement.** TangentPlane MapMode requires a vertex reference not collected by the current UI.
|
||||
|
||||
10. **`delete_bom_entry()` bypasses error normalization.** Uses raw `urllib.request` instead of `SiloClient._request()`.
|
||||
|
||||
---
|
||||
|
||||
## Incomplete features
|
||||
|
||||
### Silo
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Authentication/authorization | Not implemented | Required for multi-user |
|
||||
| File locking | Not implemented | Needed to prevent concurrent edits |
|
||||
| Odoo ERP integration | Stub only | Returns "not yet implemented" |
|
||||
| Part number date segments | Broken | `formatDate()` returns error |
|
||||
| Location/inventory APIs | Tables exist, no handlers | |
|
||||
| CSRF protection | Not implemented | Web UI only |
|
||||
| CSV import rollback | Not implemented | `bom_handlers.go` |
|
||||
|
||||
### ztools
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Tangent-to-cylinder attachment | Manual fallback | No vertex ref in UI |
|
||||
| Angled datum live editing | Incomplete | AttachmentOffset not updated in panel |
|
||||
| Assembly pattern undo | Not implemented | |
|
||||
|
||||
### Integration plan
|
||||
|
||||
| Phase | Feature | Status |
|
||||
|-------|---------|--------|
|
||||
| 1 | Addon auto-loading | Done |
|
||||
| 2 | Enhanced Pocket as C++ feature | Not started |
|
||||
| 3 | Datum C++ helpers | Not started (Python approach used) |
|
||||
| 4 | Theme moved to Create module | Partial (QSS synced, not relocated) |
|
||||
| 5 | Silo deep integration | Done |
|
||||
| 6 | Build system install rules for mods/ | Partial (CI/CD done, CMake install rules pending) |
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
1. **Authentication** -- LDAP/FreeIPA integration for Silo multi-user deployment. Server needs auth middleware; FreeCAD client needs credential storage.
|
||||
|
||||
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save.
|
||||
|
||||
3. **File locking** -- Pessimistic locks on `Silo_Open` to prevent concurrent edits. Requires server-side lock table and client-side lock display.
|
||||
|
||||
4. **Build system** -- CMake install rules for `mods/` submodules so packages include ztools and Silo without manual steps.
|
||||
|
||||
5. **Test coverage** -- Unit tests for ztools datum creation, Silo FreeCAD commands, and Go API endpoints.
|
||||
@@ -1,6 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="4" fill="#313244"/>
|
||||
<path d="M12 20 L8 20 A4 4 0 0 1 8 12 L12 12" fill="none" stroke="#89b4fa" stroke-width="2"/>
|
||||
<path d="M20 12 L24 12 A4 4 0 0 1 24 20 L20 20" fill="none" stroke="#74c7ec" stroke-width="2"/>
|
||||
<line x1="12" y1="16" x2="20" y2="16" stroke="#89b4fa" stroke-width="2"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
sodipodi:docname="Link.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="25.632621"
|
||||
inkscape:cx="14.005591"
|
||||
inkscape:cy="15.839192"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3" />
|
||||
<rect
|
||||
width="32"
|
||||
height="32"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<path
|
||||
d="M 12.013793,20 H 8.0137931 a 4,4 0 0 1 0,-8 h 3.9999999"
|
||||
fill="none"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 20.013793,12 h 4 a 4,4 0 0 1 0,8 h -4"
|
||||
fill="none"
|
||||
stroke="#74c7ec"
|
||||
stroke-width="1.5"
|
||||
id="path2"
|
||||
style="stroke:#89b4fa;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#89b4fa;stroke-width:1.77369;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 9.6965515,16 H 22.303449"
|
||||
id="path3" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +1,69 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="4" fill="#313244"/>
|
||||
<path d="M10 18 L6 18 A4 4 0 0 1 6 10 L10 10" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<path d="M18 10 L22 10 A4 4 0 0 1 22 18 L18 18" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<path d="M24 22 L24 28 L16 24 Z" fill="#a6e3a1"/>
|
||||
<line x1="24" y1="28" x2="24" y2="20" stroke="#a6e3a1" stroke-width="2"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
sodipodi:docname="LinkImport.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="25.632621"
|
||||
inkscape:cx="14.005591"
|
||||
inkscape:cy="15.800179"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3" />
|
||||
<rect
|
||||
width="32"
|
||||
height="32"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<path
|
||||
d="M 12.013793,20 H 8.0137931 a 4,4 0 0 1 0,-8 h 3.9999999"
|
||||
fill="none"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 20.013793,12 h 4 a 4,4 0 0 1 0,8 h -4"
|
||||
fill="none"
|
||||
stroke="#74c7ec"
|
||||
stroke-width="1.5"
|
||||
id="path2"
|
||||
style="stroke:#89b4fa;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#89b4fa;stroke-width:1.77369;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 9.6965515,16 H 22.303449"
|
||||
id="path3" />
|
||||
<circle
|
||||
cx="25.513792"
|
||||
cy="-9.2321157"
|
||||
fill="#a6e3a1"
|
||||
id="circle2"
|
||||
style="stroke-width:0.999999"
|
||||
transform="scale(1,-1)"
|
||||
r="5" />
|
||||
<path
|
||||
d="M 25.51379,11.732116 V 6.7321179 m -2.499999,2.4999988 h 5"
|
||||
stroke="#1e1e2e"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
id="path3-7" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 1.9 KiB |
@@ -1,7 +1,62 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="4" fill="#313244"/>
|
||||
<path d="M12 18 L8 18 A4 4 0 0 1 8 10 L12 10" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<path d="M18 10 L22 10 A4 4 0 0 1 22 18 L18 18" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<line x1="12" y1="14" x2="18" y2="14" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<rect x="14" y="20" width="12" height="8" rx="1" fill="none" stroke="#f9e2af" stroke-width="1.5" stroke-dasharray="2,2"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
sodipodi:docname="LinkSelect.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="25.632621"
|
||||
inkscape:cx="14.005591"
|
||||
inkscape:cy="15.800179"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3" />
|
||||
<rect
|
||||
width="32"
|
||||
height="32"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<path
|
||||
d="M 12.013793,20 H 8.0137931 a 4,4 0 0 1 0,-8 h 3.9999999"
|
||||
fill="none"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 20.013793,12 h 4 a 4,4 0 0 1 0,8 h -4"
|
||||
fill="none"
|
||||
stroke="#74c7ec"
|
||||
stroke-width="1.5"
|
||||
id="path2"
|
||||
style="stroke:#89b4fa;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#89b4fa;stroke-width:1.77369;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 9.6965515,16 H 22.303449"
|
||||
id="path3" />
|
||||
<rect
|
||||
style="fill:none;stroke:#fab387;stroke-width:1.056;stroke-dasharray:1.05599999, 2.11199999;stroke-linejoin:round;stroke-linecap:round;stroke-dashoffset:15.41759968;stroke-opacity:1"
|
||||
id="rect2"
|
||||
width="28.12822"
|
||||
height="14.785847"
|
||||
x="1.9358902"
|
||||
y="8.6070766" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 523 B After Width: | Height: | Size: 1.8 KiB |
@@ -1,12 +1,78 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
|
||||
<!-- Block with rounded edge -->
|
||||
<path d="M6 24 L6 10 L16 6 L26 10 L26 24 L16 28 Z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="#89b4fa" stroke-width="1.5" fill="none"/>
|
||||
<path d="M16 14 L16 28" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<!-- Fillet radius on edge -->
|
||||
<path d="M6 10 Q10 10 12 14" stroke="#a6e3a1" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Radius indicator -->
|
||||
<path d="M6 10 L9 12" stroke="#cdd6f4" stroke-width="1" stroke-dasharray="2,1"/>
|
||||
<text x="4" y="8" font-family="sans-serif" font-size="6" fill="#a6e3a1">R</text>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="PartDesign_Fillet.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs5" />
|
||||
<sodipodi:namedview
|
||||
id="namedview5"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="2.8284271"
|
||||
inkscape:cy="17.412504"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg5" />
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="28"
|
||||
height="28"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<!-- Block with chamfered edge -->
|
||||
<path
|
||||
d="M 6,24 6.1584709,15.885723 10.508095,8.6005883 16,6 26,10 V 24 L 16,28 Z M 20.923535,12.327039 26.006643,9.9805673 Z"
|
||||
fill="#45475a"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="ccccccccccc" />
|
||||
<path
|
||||
d="M 16,19.88 V 28"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5"
|
||||
id="path3"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
d="M 5.721457,16 10.416889,7.8973687 21,12 l -3.708852,3.943415 -1.158565,4.20175 z"
|
||||
fill="#a6e3a1"
|
||||
fill-opacity="0.3"
|
||||
id="path5"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
style="opacity:1;fill:#a6e3a1;fill-opacity:1" />
|
||||
<!-- Chamfer cut on edge -->
|
||||
<!-- Chamfer face -->
|
||||
<path
|
||||
d="m 25.554002,10.020735 c 0,0 -2.839088,1.13525 -4.950437,2.544214 -1.652011,1.102435 -2.601422,2.423855 -3.100562,3.159378 -1.011406,1.490386 -1.47202,3.667857 -1.538841,4.165681 -0.0066,0.04949 -0.01392,7.861307 -0.01392,7.861307 m 7.653323,-14.186366"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5847"
|
||||
id="path3-0"
|
||||
sodipodi:nodetypes="csssc"
|
||||
style="fill:none" />
|
||||
<path
|
||||
d="m 16.23145,6.006509 c 0,0 -3.524098,1.0247646 -5.635447,2.4337286 -1.6520107,1.1024351 -2.6014217,2.4238554 -3.1005617,3.1593784 -1.011406,1.490386 -1.47202,3.667857 -1.538841,4.165681 -0.0066,0.04949 0.2070509,8.612608 0.2070509,8.612608"
|
||||
stroke="#89b4fa"
|
||||
stroke-width="1.5847"
|
||||
id="path3-0-6"
|
||||
sodipodi:nodetypes="csssc"
|
||||
style="fill:none" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 753 B After Width: | Height: | Size: 2.6 KiB |
@@ -1,13 +1,64 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
sodipodi:docname="PartDesign_NewSketch.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="36.25"
|
||||
inkscape:cx="16"
|
||||
inkscape:cy="16"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3" />
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="28"
|
||||
height="28"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<!-- Sketch plane -->
|
||||
<path d="M4 20 L16 12 L28 20 L16 28 Z" fill="#45475a" stroke="#f9e2af" stroke-width="1.5"/>
|
||||
<path
|
||||
d="m 4,17.158621 12,-8.0000003 12,8.0000003 -12,8 z"
|
||||
fill="#45475a"
|
||||
stroke="#f9e2af"
|
||||
stroke-width="1.5"
|
||||
id="path1" />
|
||||
<!-- Grid on plane -->
|
||||
<line x1="10" y1="20" x2="22" y2="20" stroke="#6c7086" stroke-width="0.75"/>
|
||||
<line x1="16" y1="16" x2="16" y2="24" stroke="#6c7086" stroke-width="0.75"/>
|
||||
<!-- Sketch geometry -->
|
||||
<path d="M12 20 L16 16 L20 20 L16 22 Z" fill="none" stroke="#fab387" stroke-width="1.5"/>
|
||||
<!-- Plus sign for "new" -->
|
||||
<circle cx="24" cy="8" r="5" fill="#a6e3a1"/>
|
||||
<path d="M24 5.5 L24 10.5 M21.5 8 L26.5 8" stroke="#1e1e2e" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle
|
||||
cx="23.092838"
|
||||
cy="-10.553846"
|
||||
fill="#a6e3a1"
|
||||
id="circle2"
|
||||
style="stroke-width:0.999999"
|
||||
transform="scale(1,-1)"
|
||||
r="5" />
|
||||
<path
|
||||
d="M 23.092837,13.053846 V 8.0538483 m -2.499999,2.4999987 h 5"
|
||||
stroke="#1e1e2e"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
id="path3-7" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 1.7 KiB |
@@ -1,9 +1,98 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="4" fill="#313244"/>
|
||||
<circle cx="16" cy="13" r="7" fill="none" stroke="#f9e2af" stroke-width="1.5"/>
|
||||
<path d="M12 20 L12 24 L20 24 L20 20" fill="none" stroke="#fab387" stroke-width="1.5"/>
|
||||
<line x1="13" y1="27" x2="19" y2="27" stroke="#fab387" stroke-width="1.5"/>
|
||||
<line x1="16" y1="6" x2="16" y2="4" stroke="#f9e2af" stroke-width="1.5"/>
|
||||
<line x1="8" y1="8" x2="6" y2="6" stroke="#f9e2af" stroke-width="1.5"/>
|
||||
<line x1="24" y1="8" x2="26" y2="6" stroke="#f9e2af" stroke-width="1.5"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="bulb.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="18.125"
|
||||
inkscape:cx="12.634483"
|
||||
inkscape:cy="17.048276"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<rect
|
||||
width="32"
|
||||
height="32"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<path
|
||||
d="m 13.398145,19.664706 v 4.206463 h 5.120952 v -4.206463"
|
||||
fill="none"
|
||||
stroke="#fab387"
|
||||
stroke-width="1.23069"
|
||||
id="path1" />
|
||||
<line
|
||||
x1="12.95862"
|
||||
y1="26.492428"
|
||||
x2="18.958622"
|
||||
y2="26.492428"
|
||||
stroke="#fab387"
|
||||
stroke-width="1.5"
|
||||
id="line1"
|
||||
style="stroke-width:1.3;stroke-dasharray:none" />
|
||||
<line
|
||||
x1="12.95862"
|
||||
y1="28.743103"
|
||||
x2="18.958622"
|
||||
y2="28.743103"
|
||||
stroke="#fab387"
|
||||
stroke-width="1.5"
|
||||
id="line1-2"
|
||||
style="stroke-width:1.2;stroke-dasharray:none" />
|
||||
<line
|
||||
x1="16"
|
||||
y1="8.2896547"
|
||||
x2="16"
|
||||
y2="4"
|
||||
stroke="#f9e2af"
|
||||
stroke-width="2.19678"
|
||||
id="line2"
|
||||
style="stroke-width:1.45;stroke-dasharray:none" />
|
||||
<line
|
||||
x1="21.842089"
|
||||
y1="9.2134304"
|
||||
x2="24.801268"
|
||||
y2="6.1714916"
|
||||
stroke="#f9e2af"
|
||||
stroke-width="2.25021"
|
||||
id="line4"
|
||||
style="stroke-width:1.45;stroke-dasharray:none" />
|
||||
<line
|
||||
x1="9.5786028"
|
||||
y1="9.2134304"
|
||||
x2="6.6194234"
|
||||
y2="6.1714916"
|
||||
stroke="#f9e2af"
|
||||
stroke-width="2.25021"
|
||||
id="line4-5"
|
||||
style="stroke-width:1.45;stroke-dasharray:none" />
|
||||
<ellipse
|
||||
cx="15.710345"
|
||||
cy="15.234483"
|
||||
fill="none"
|
||||
stroke="#f9e2af"
|
||||
stroke-width="1.07682"
|
||||
id="circle1"
|
||||
rx="5.0736599"
|
||||
ry="4.977108" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 2.4 KiB |
@@ -1,5 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="4" fill="#313244"/>
|
||||
<path d="M22 10 A8 8 0 1 1 10 10" fill="none" stroke="#cba6f7" stroke-width="2"/>
|
||||
<path d="M22 6 L22 14 L14 10 Z" fill="#b4befe"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="button_rotate.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="25.632621"
|
||||
inkscape:cx="7.8025576"
|
||||
inkscape:cy="17.282665"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<rect
|
||||
width="32"
|
||||
height="32"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<path
|
||||
d="M22 10 A8 8 0 1 1 10 10"
|
||||
fill="none"
|
||||
stroke="#cba6f7"
|
||||
stroke-width="2"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 26.440057,9.4371459 19.892123,14.033292 18.569944,5.1872849 Z"
|
||||
fill="#b4befe"
|
||||
id="path2" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 1.3 KiB |
@@ -1,10 +1,64 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="help-browser.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="36.25"
|
||||
inkscape:cx="16"
|
||||
inkscape:cy="16"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="28"
|
||||
height="28"
|
||||
rx="4"
|
||||
fill="#313244"
|
||||
id="rect1" />
|
||||
<!-- Circle background -->
|
||||
<circle cx="16" cy="16" r="10" fill="#45475a" stroke="#cba6f7" stroke-width="2"/>
|
||||
<circle
|
||||
cx="16.020691"
|
||||
cy="15.889656"
|
||||
r="10"
|
||||
fill="#45475a"
|
||||
stroke="#cba6f7"
|
||||
stroke-width="2"
|
||||
id="circle1" />
|
||||
<!-- Question mark -->
|
||||
<path d="M13 12 C13 9 15 8 17 8 C19 8 21 9.5 21 12 C21 14 19 14.5 17 15.5 L17 18"
|
||||
stroke="#b4befe" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||
<path
|
||||
d="m 12.000002,12.965517 c 0,-2.9999996 2,-3.9999998 4,-3.9999998 2,0 4,1.4999998 4,3.9999998 0,2 -2,2.5 -4,3.5 v 2.5"
|
||||
stroke="#b4befe"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
fill="none"
|
||||
id="path1" />
|
||||
<!-- Dot -->
|
||||
<circle cx="17" cy="22" r="1.5" fill="#b4befe"/>
|
||||
<circle
|
||||
cx="16"
|
||||
cy="22.965519"
|
||||
r="1.5"
|
||||
fill="#b4befe"
|
||||
id="circle2" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 1.6 KiB |
@@ -22,7 +22,7 @@ requirements:
|
||||
- noqt5
|
||||
- python>=3.11,<3.12
|
||||
- qt6-main>=6.8,<6.9
|
||||
- swig
|
||||
- swig >=4.0,<4.4
|
||||
|
||||
- if: linux and x86_64
|
||||
then:
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "Workbench.h"
|
||||
#include "WorkbenchManager.h"
|
||||
#include "WorkbenchSelector.h"
|
||||
#include "OriginSelectorWidget.h"
|
||||
#include "ShortcutManager.h"
|
||||
#include "Tools.h"
|
||||
|
||||
@@ -1470,4 +1471,25 @@ void WindowAction::addTo(QWidget* widget)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
OriginSelectorAction::OriginSelectorAction(Command* pcCmd, QObject* parent)
|
||||
: Action(pcCmd, parent)
|
||||
{}
|
||||
|
||||
OriginSelectorAction::~OriginSelectorAction() = default;
|
||||
|
||||
void OriginSelectorAction::addTo(QWidget* widget)
|
||||
{
|
||||
if (widget->inherits("QToolBar")) {
|
||||
auto* toolbar = static_cast<QToolBar*>(widget);
|
||||
auto* selector = new OriginSelectorWidget(widget);
|
||||
toolbar->addWidget(selector);
|
||||
}
|
||||
else {
|
||||
// For menus, just add the action
|
||||
widget->addAction(action());
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_Action.cpp"
|
||||
|
||||
@@ -421,6 +421,25 @@ private:
|
||||
Q_DISABLE_COPY(WindowAction)
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Action for origin selector widget in toolbars.
|
||||
* Creates OriginSelectorWidget when added to a toolbar.
|
||||
*/
|
||||
class GuiExport OriginSelectorAction: public Action
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OriginSelectorAction(Command* pcCmd, QObject* parent = nullptr);
|
||||
~OriginSelectorAction() override;
|
||||
void addTo(QWidget* widget) override;
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(OriginSelectorAction)
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_ACTION_H
|
||||
|
||||
@@ -1022,6 +1022,7 @@ void Application::createStandardOperations()
|
||||
Gui::CreateStructureCommands();
|
||||
Gui::CreateTestCommands();
|
||||
Gui::CreateLinkCommands();
|
||||
Gui::CreateOriginCommands();
|
||||
}
|
||||
|
||||
void Application::slotNewDocument(const App::Document& Doc, bool isMainDoc)
|
||||
|
||||
@@ -529,6 +529,7 @@ SET(Command_CPP_SRCS
|
||||
CommandFeat.cpp
|
||||
CommandMacro.cpp
|
||||
CommandStd.cpp
|
||||
CommandOrigin.cpp
|
||||
CommandWindow.cpp
|
||||
CommandTest.cpp
|
||||
CommandView.cpp
|
||||
@@ -592,6 +593,7 @@ SET(Dialog_CPP_SRCS
|
||||
Dialogs/DlgObjectSelection.cpp
|
||||
Dialogs/DlgAddProperty.cpp
|
||||
VectorListEditor.cpp
|
||||
OriginManagerDialog.cpp
|
||||
)
|
||||
|
||||
SET(Dialog_HPP_SRCS
|
||||
@@ -634,6 +636,7 @@ SET(Dialog_HPP_SRCS
|
||||
Dialogs/DlgObjectSelection.h
|
||||
Dialogs/DlgAddProperty.h
|
||||
VectorListEditor.h
|
||||
OriginManagerDialog.h
|
||||
)
|
||||
|
||||
SET(Dialog_SRCS
|
||||
@@ -1236,6 +1239,7 @@ SET(Widget_CPP_SRCS
|
||||
ElideCheckBox.cpp
|
||||
FontScaledSVG.cpp
|
||||
SplitButton.cpp
|
||||
OriginSelectorWidget.cpp
|
||||
)
|
||||
SET(Widget_HPP_SRCS
|
||||
ComboLinks.h
|
||||
@@ -1262,6 +1266,7 @@ SET(Widget_HPP_SRCS
|
||||
ElideCheckBox.h
|
||||
FontScaledSVG.h
|
||||
SplitButton.h
|
||||
OriginSelectorWidget.h
|
||||
)
|
||||
SET(Widget_SRCS
|
||||
${Widget_CPP_SRCS}
|
||||
|
||||
@@ -247,6 +247,7 @@ void CreateWindowStdCommands();
|
||||
void CreateStructureCommands();
|
||||
void CreateTestCommands();
|
||||
void CreateLinkCommands();
|
||||
void CreateOriginCommands();
|
||||
|
||||
|
||||
/** The CommandBase class
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
#include "Control.h"
|
||||
#include "DockWindowManager.h"
|
||||
#include "FileDialog.h"
|
||||
#include "FileOrigin.h"
|
||||
#include "MainWindow.h"
|
||||
#include "OriginManager.h"
|
||||
#include "Selection.h"
|
||||
#include "Dialogs/DlgObjectSelection.h"
|
||||
#include "Dialogs/DlgProjectInformationImp.h"
|
||||
@@ -95,81 +97,25 @@ void StdCmdOpen::activated(int iMsg)
|
||||
{
|
||||
Q_UNUSED(iMsg);
|
||||
|
||||
// fill the list of registered endings
|
||||
QString formatList;
|
||||
const char* supported = QT_TR_NOOP("Supported formats");
|
||||
const char* allFiles = QT_TR_NOOP("All files (*.*)");
|
||||
formatList = QObject::tr(supported);
|
||||
formatList += QLatin1String(" (");
|
||||
|
||||
std::vector<std::string> filetypes = App::GetApplication().getImportTypes();
|
||||
// Make sure FCStd is the very first fileformat
|
||||
auto it = std::ranges::find(filetypes, "FCStd");
|
||||
if (it != filetypes.end()) {
|
||||
filetypes.erase(it);
|
||||
filetypes.insert(filetypes.begin(), "FCStd");
|
||||
}
|
||||
for (it = filetypes.begin(); it != filetypes.end(); ++it) {
|
||||
formatList += QLatin1String(" *.");
|
||||
formatList += QLatin1String(it->c_str());
|
||||
}
|
||||
|
||||
formatList += QLatin1String(");;");
|
||||
|
||||
std::map<std::string, std::string> FilterList = App::GetApplication().getImportFilters();
|
||||
std::map<std::string, std::string>::iterator jt;
|
||||
// Make sure the format name for FCStd is the very first in the list
|
||||
for (jt = FilterList.begin(); jt != FilterList.end(); ++jt) {
|
||||
if (jt->first.find("*.FCStd") != std::string::npos) {
|
||||
formatList += QLatin1String(jt->first.c_str());
|
||||
formatList += QLatin1String(";;");
|
||||
FilterList.erase(jt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (jt = FilterList.begin(); jt != FilterList.end(); ++jt) {
|
||||
formatList += QLatin1String(jt->first.c_str());
|
||||
formatList += QLatin1String(";;");
|
||||
}
|
||||
formatList += QObject::tr(allFiles);
|
||||
|
||||
QString selectedFilter;
|
||||
QStringList fileList = FileDialog::getOpenFileNames(
|
||||
getMainWindow(),
|
||||
QObject::tr("Open Document"),
|
||||
QString(),
|
||||
formatList,
|
||||
&selectedFilter
|
||||
);
|
||||
if (fileList.isEmpty()) {
|
||||
// Delegate to current origin
|
||||
FileOrigin* origin = OriginManager::instance()->currentOrigin();
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// load the files with the associated modules
|
||||
SelectModule::Dict dict = SelectModule::importHandler(fileList, selectedFilter);
|
||||
if (dict.isEmpty()) {
|
||||
QMessageBox::critical(
|
||||
// Check connection for origins that require authentication
|
||||
if (origin->requiresAuthentication() &&
|
||||
origin->connectionState() != ConnectionState::Connected) {
|
||||
QMessageBox::warning(
|
||||
getMainWindow(),
|
||||
qApp->translate("StdCmdOpen", "Cannot Open File"),
|
||||
qApp->translate("StdCmdOpen", "Loading the file %1 is not supported").arg(fileList.front())
|
||||
qApp->translate("StdCmdOpen", "Not Connected"),
|
||||
qApp->translate("StdCmdOpen", "Please connect to %1 before opening files.")
|
||||
.arg(QString::fromStdString(origin->name()))
|
||||
);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
for (SelectModule::Dict::iterator it = dict.begin(); it != dict.end(); ++it) {
|
||||
|
||||
// Set flag indicating that this load/restore has been initiated by the user (not by a macro)
|
||||
getGuiApplication()->setStatus(Gui::Application::UserInitiatedOpenDocument, true);
|
||||
|
||||
getGuiApplication()->open(it.key().toUtf8(), it.value().toLatin1());
|
||||
|
||||
getGuiApplication()->setStatus(Gui::Application::UserInitiatedOpenDocument, false);
|
||||
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
|
||||
getGuiApplication()->checkPartialRestore(doc);
|
||||
getGuiApplication()->checkRestoreError(doc);
|
||||
}
|
||||
}
|
||||
origin->openDocumentInteractive();
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
@@ -715,10 +661,28 @@ StdCmdNew::StdCmdNew()
|
||||
void StdCmdNew::activated(int iMsg)
|
||||
{
|
||||
Q_UNUSED(iMsg);
|
||||
QString cmd;
|
||||
cmd = QStringLiteral("App.newDocument()");
|
||||
runCommand(Command::Doc, cmd.toUtf8());
|
||||
doCommand(Command::Gui, "Gui.activeDocument().activeView().viewDefaultOrientation()");
|
||||
|
||||
// Delegate to current origin
|
||||
FileOrigin* origin = OriginManager::instance()->currentOrigin();
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
App::Document* doc = origin->newDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default view orientation for the new document
|
||||
auto hGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/View"
|
||||
);
|
||||
std::string default_view = hGrp->GetASCII("NewDocumentCameraOrientation", "Top");
|
||||
doCommand(
|
||||
Command::Gui,
|
||||
"Gui.activeDocument().activeView().viewDefaultOrientation('%s',0)",
|
||||
default_view.c_str()
|
||||
);
|
||||
|
||||
ParameterGrp::handle hViewGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/View"
|
||||
@@ -749,12 +713,33 @@ StdCmdSave::StdCmdSave()
|
||||
void StdCmdSave::activated(int iMsg)
|
||||
{
|
||||
Q_UNUSED(iMsg);
|
||||
doCommand(Command::Gui, "Gui.SendMsgToActiveView(\"Save\")");
|
||||
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use document's origin for save, not current origin
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
if (!origin) {
|
||||
// Document has no origin yet - use current origin for first save
|
||||
origin = OriginManager::instance()->currentOrigin();
|
||||
}
|
||||
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to save the document
|
||||
if (!origin->saveDocument(doc)) {
|
||||
// If save failed (e.g., no filename), try SaveAs
|
||||
origin->saveDocumentAsInteractive(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool StdCmdSave::isActive()
|
||||
{
|
||||
return getGuiApplication()->sendHasMsgToActiveView("Save");
|
||||
return App::GetApplication().getActiveDocument() != nullptr;
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
@@ -778,12 +763,51 @@ StdCmdSaveAs::StdCmdSaveAs()
|
||||
void StdCmdSaveAs::activated(int iMsg)
|
||||
{
|
||||
Q_UNUSED(iMsg);
|
||||
doCommand(Command::Gui, "Gui.SendMsgToActiveView(\"SaveAs\")");
|
||||
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* mgr = OriginManager::instance();
|
||||
FileOrigin* currentOrigin = mgr->currentOrigin();
|
||||
FileOrigin* docOrigin = mgr->originForDocument(doc);
|
||||
|
||||
if (!currentOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine workflow based on document and target origins
|
||||
OriginType currentType = currentOrigin->type();
|
||||
OriginType docType = docOrigin ? docOrigin->type() : OriginType::Local;
|
||||
|
||||
if (docOrigin == currentOrigin || !docOrigin) {
|
||||
// Same origin or new document - standard SaveAs
|
||||
currentOrigin->saveDocumentAsInteractive(doc);
|
||||
}
|
||||
else if (currentType == OriginType::PLM && docType == OriginType::Local) {
|
||||
// Local → PLM: Migration workflow
|
||||
// The PLM origin's saveDocumentAsInteractive should handle this
|
||||
currentOrigin->saveDocumentAsInteractive(doc);
|
||||
}
|
||||
else if (currentType == OriginType::Local && docType == OriginType::PLM) {
|
||||
// PLM → Local: Export workflow
|
||||
// Use local origin to save without PLM tracking
|
||||
currentOrigin->saveDocumentAsInteractive(doc);
|
||||
}
|
||||
else if (currentType == OriginType::PLM && docType == OriginType::PLM) {
|
||||
// PLM → Different PLM: Transfer workflow
|
||||
currentOrigin->saveDocumentAsInteractive(doc);
|
||||
}
|
||||
else {
|
||||
// Default: use current origin
|
||||
currentOrigin->saveDocumentAsInteractive(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool StdCmdSaveAs::isActive()
|
||||
{
|
||||
return getGuiApplication()->sendHasMsgToActiveView("SaveAs");
|
||||
return App::GetApplication().getActiveDocument() != nullptr;
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
|
||||
277
src/Gui/CommandOrigin.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
/**
|
||||
* @file CommandOrigin.cpp
|
||||
* @brief Unified origin commands that work with the current origin
|
||||
*
|
||||
* These commands delegate to the current FileOrigin's extended operations.
|
||||
* They are only active when the current origin supports the required capability.
|
||||
*/
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
|
||||
#include "Application.h"
|
||||
#include "BitmapFactory.h"
|
||||
#include "Command.h"
|
||||
#include "Document.h"
|
||||
#include "FileOrigin.h"
|
||||
#include "MainWindow.h"
|
||||
#include "OriginManager.h"
|
||||
|
||||
|
||||
using namespace Gui;
|
||||
|
||||
//===========================================================================
|
||||
// Origin_Commit
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_A(OriginCmdCommit)
|
||||
|
||||
OriginCmdCommit::OriginCmdCommit()
|
||||
: Command("Origin_Commit")
|
||||
{
|
||||
sGroup = "File";
|
||||
sMenuText = QT_TR_NOOP("&Commit");
|
||||
sToolTipText = QT_TR_NOOP("Commit changes as a new revision");
|
||||
sWhatsThis = "Origin_Commit";
|
||||
sStatusTip = sToolTipText;
|
||||
sPixmap = "silo-commit";
|
||||
sAccel = "Ctrl+Shift+C";
|
||||
eType = AlterDoc;
|
||||
}
|
||||
|
||||
void OriginCmdCommit::activated(int /*iMsg*/)
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
if (origin && origin->supportsRevisions()) {
|
||||
origin->commitDocument(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool OriginCmdCommit::isActive()
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
return origin && origin->supportsRevisions();
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// Origin_Pull
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_A(OriginCmdPull)
|
||||
|
||||
OriginCmdPull::OriginCmdPull()
|
||||
: Command("Origin_Pull")
|
||||
{
|
||||
sGroup = "File";
|
||||
sMenuText = QT_TR_NOOP("&Pull");
|
||||
sToolTipText = QT_TR_NOOP("Pull a specific revision from the origin");
|
||||
sWhatsThis = "Origin_Pull";
|
||||
sStatusTip = sToolTipText;
|
||||
sPixmap = "silo-pull";
|
||||
sAccel = "Ctrl+Shift+P";
|
||||
eType = AlterDoc;
|
||||
}
|
||||
|
||||
void OriginCmdPull::activated(int /*iMsg*/)
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
if (origin && origin->supportsRevisions()) {
|
||||
origin->pullDocument(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool OriginCmdPull::isActive()
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
return origin && origin->supportsRevisions();
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// Origin_Push
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_A(OriginCmdPush)
|
||||
|
||||
OriginCmdPush::OriginCmdPush()
|
||||
: Command("Origin_Push")
|
||||
{
|
||||
sGroup = "File";
|
||||
sMenuText = QT_TR_NOOP("Pu&sh");
|
||||
sToolTipText = QT_TR_NOOP("Push local changes to the origin");
|
||||
sWhatsThis = "Origin_Push";
|
||||
sStatusTip = sToolTipText;
|
||||
sPixmap = "silo-push";
|
||||
sAccel = "Ctrl+Shift+U";
|
||||
eType = AlterDoc;
|
||||
}
|
||||
|
||||
void OriginCmdPush::activated(int /*iMsg*/)
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
if (origin && origin->supportsRevisions()) {
|
||||
origin->pushDocument(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool OriginCmdPush::isActive()
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
return origin && origin->supportsRevisions();
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// Origin_Info
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_A(OriginCmdInfo)
|
||||
|
||||
OriginCmdInfo::OriginCmdInfo()
|
||||
: Command("Origin_Info")
|
||||
{
|
||||
sGroup = "File";
|
||||
sMenuText = QT_TR_NOOP("&Info");
|
||||
sToolTipText = QT_TR_NOOP("Show document information from origin");
|
||||
sWhatsThis = "Origin_Info";
|
||||
sStatusTip = sToolTipText;
|
||||
sPixmap = "silo-info";
|
||||
eType = 0;
|
||||
}
|
||||
|
||||
void OriginCmdInfo::activated(int /*iMsg*/)
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
if (origin && origin->supportsPartNumbers()) {
|
||||
origin->showInfo(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool OriginCmdInfo::isActive()
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
return origin && origin->supportsPartNumbers();
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// Origin_BOM
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_A(OriginCmdBOM)
|
||||
|
||||
OriginCmdBOM::OriginCmdBOM()
|
||||
: Command("Origin_BOM")
|
||||
{
|
||||
sGroup = "File";
|
||||
sMenuText = QT_TR_NOOP("&Bill of Materials");
|
||||
sToolTipText = QT_TR_NOOP("Show Bill of Materials for this document");
|
||||
sWhatsThis = "Origin_BOM";
|
||||
sStatusTip = sToolTipText;
|
||||
sPixmap = "silo-bom";
|
||||
eType = 0;
|
||||
}
|
||||
|
||||
void OriginCmdBOM::activated(int /*iMsg*/)
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
if (origin && origin->supportsBOM()) {
|
||||
origin->showBOM(doc);
|
||||
}
|
||||
}
|
||||
|
||||
bool OriginCmdBOM::isActive()
|
||||
{
|
||||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileOrigin* origin = OriginManager::instance()->findOwningOrigin(doc);
|
||||
return origin && origin->supportsBOM();
|
||||
}
|
||||
|
||||
|
||||
//===========================================================================
|
||||
// Command Registration
|
||||
//===========================================================================
|
||||
|
||||
namespace Gui {
|
||||
|
||||
void CreateOriginCommands()
|
||||
{
|
||||
CommandManager& rcCmdMgr = Application::Instance->commandManager();
|
||||
|
||||
rcCmdMgr.addCommand(new OriginCmdCommit());
|
||||
rcCmdMgr.addCommand(new OriginCmdPull());
|
||||
rcCmdMgr.addCommand(new OriginCmdPush());
|
||||
rcCmdMgr.addCommand(new OriginCmdInfo());
|
||||
rcCmdMgr.addCommand(new OriginCmdBOM());
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
@@ -132,6 +132,45 @@ Action* StdCmdWorkbench::createAction()
|
||||
return pcAction;
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// Std_Origin
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_AC(StdCmdOrigin)
|
||||
|
||||
StdCmdOrigin::StdCmdOrigin()
|
||||
: Command("Std_Origin")
|
||||
{
|
||||
sGroup = "File";
|
||||
sMenuText = QT_TR_NOOP("&Origin");
|
||||
sToolTipText = QT_TR_NOOP("Select file origin (Local Files, Silo, etc.)");
|
||||
sWhatsThis = "Std_Origin";
|
||||
sStatusTip = sToolTipText;
|
||||
sPixmap = "folder";
|
||||
eType = 0;
|
||||
}
|
||||
|
||||
void StdCmdOrigin::activated(int /*iMsg*/)
|
||||
{
|
||||
// Action is handled by OriginSelectorWidget
|
||||
}
|
||||
|
||||
bool StdCmdOrigin::isActive()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Action* StdCmdOrigin::createAction()
|
||||
{
|
||||
Action* pcAction = new OriginSelectorAction(this, getMainWindow());
|
||||
pcAction->setShortcut(QString::fromLatin1(getAccel()));
|
||||
applyCommandData(this->className(), pcAction);
|
||||
if (getPixmap()) {
|
||||
pcAction->setIcon(Gui::BitmapFactory().iconFromTheme(getPixmap()));
|
||||
}
|
||||
return pcAction;
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// Std_RecentFiles
|
||||
//===========================================================================
|
||||
@@ -1057,6 +1096,7 @@ void CreateStdCommands()
|
||||
rcCmdMgr.addCommand(new StdCmdDlgCustomize());
|
||||
rcCmdMgr.addCommand(new StdCmdCommandLine());
|
||||
rcCmdMgr.addCommand(new StdCmdWorkbench());
|
||||
rcCmdMgr.addCommand(new StdCmdOrigin());
|
||||
rcCmdMgr.addCommand(new StdCmdRecentFiles());
|
||||
rcCmdMgr.addCommand(new StdCmdRecentMacros());
|
||||
rcCmdMgr.addCommand(new StdCmdWhatsThis());
|
||||
|
||||
@@ -22,15 +22,22 @@
|
||||
|
||||
#include "PreCompiled.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
#include <App/DocumentObject.h>
|
||||
#include <App/PropertyStandard.h>
|
||||
|
||||
#include "FileOrigin.h"
|
||||
#include "Application.h"
|
||||
#include "BitmapFactory.h"
|
||||
#include "Document.h"
|
||||
#include "Application.h"
|
||||
#include "FileDialog.h"
|
||||
#include "MainWindow.h"
|
||||
|
||||
|
||||
namespace Gui {
|
||||
@@ -99,6 +106,86 @@ App::Document* LocalFileOrigin::openDocument(const std::string& identity)
|
||||
return App::GetApplication().openDocument(identity.c_str());
|
||||
}
|
||||
|
||||
App::Document* LocalFileOrigin::openDocumentInteractive()
|
||||
{
|
||||
// Build file filter list for Open dialog
|
||||
QString formatList;
|
||||
const char* supported = QT_TR_NOOP("Supported formats");
|
||||
const char* allFiles = QT_TR_NOOP("All files (*.*)");
|
||||
formatList = QObject::tr(supported);
|
||||
formatList += QLatin1String(" (");
|
||||
|
||||
std::vector<std::string> filetypes = App::GetApplication().getImportTypes();
|
||||
// Make sure FCStd is the very first fileformat
|
||||
auto it = std::find(filetypes.begin(), filetypes.end(), "FCStd");
|
||||
if (it != filetypes.end()) {
|
||||
filetypes.erase(it);
|
||||
filetypes.insert(filetypes.begin(), "FCStd");
|
||||
}
|
||||
for (it = filetypes.begin(); it != filetypes.end(); ++it) {
|
||||
formatList += QLatin1String(" *.");
|
||||
formatList += QLatin1String(it->c_str());
|
||||
}
|
||||
|
||||
formatList += QLatin1String(");;");
|
||||
|
||||
std::map<std::string, std::string> FilterList = App::GetApplication().getImportFilters();
|
||||
// Make sure the format name for FCStd is the very first in the list
|
||||
for (auto jt = FilterList.begin(); jt != FilterList.end(); ++jt) {
|
||||
if (jt->first.find("*.FCStd") != std::string::npos) {
|
||||
formatList += QLatin1String(jt->first.c_str());
|
||||
formatList += QLatin1String(";;");
|
||||
FilterList.erase(jt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto& filter : FilterList) {
|
||||
formatList += QLatin1String(filter.first.c_str());
|
||||
formatList += QLatin1String(";;");
|
||||
}
|
||||
formatList += QObject::tr(allFiles);
|
||||
|
||||
QString selectedFilter;
|
||||
QStringList fileList = FileDialog::getOpenFileNames(
|
||||
getMainWindow(),
|
||||
QObject::tr("Open Document"),
|
||||
QString(),
|
||||
formatList,
|
||||
&selectedFilter
|
||||
);
|
||||
if (fileList.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Load the files with the associated modules
|
||||
SelectModule::Dict dict = SelectModule::importHandler(fileList, selectedFilter);
|
||||
if (dict.isEmpty()) {
|
||||
QMessageBox::critical(
|
||||
getMainWindow(),
|
||||
qApp->translate("StdCmdOpen", "Cannot Open File"),
|
||||
qApp->translate("StdCmdOpen", "Loading the file %1 is not supported").arg(fileList.front())
|
||||
);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
App::Document* lastDoc = nullptr;
|
||||
for (SelectModule::Dict::iterator it = dict.begin(); it != dict.end(); ++it) {
|
||||
// Set flag indicating that this load/restore has been initiated by the user
|
||||
Application::Instance->setStatus(Gui::Application::UserInitiatedOpenDocument, true);
|
||||
|
||||
Application::Instance->open(it.key().toUtf8(), it.value().toLatin1());
|
||||
|
||||
Application::Instance->setStatus(Gui::Application::UserInitiatedOpenDocument, false);
|
||||
|
||||
lastDoc = App::GetApplication().getActiveDocument();
|
||||
|
||||
Application::Instance->checkPartialRestore(lastDoc);
|
||||
Application::Instance->checkRestoreError(lastDoc);
|
||||
}
|
||||
|
||||
return lastDoc;
|
||||
}
|
||||
|
||||
bool LocalFileOrigin::saveDocument(App::Document* doc)
|
||||
{
|
||||
if (!doc) {
|
||||
@@ -125,4 +212,20 @@ bool LocalFileOrigin::saveDocumentAs(App::Document* doc, const std::string& newI
|
||||
return doc->saveAs(newIdentity.c_str());
|
||||
}
|
||||
|
||||
bool LocalFileOrigin::saveDocumentAsInteractive(App::Document* doc)
|
||||
{
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get Gui document for save dialog
|
||||
Gui::Document* guiDoc = Application::Instance->getDocument(doc);
|
||||
if (!guiDoc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use Gui::Document::saveAs() which handles the file dialog
|
||||
return guiDoc->saveAs();
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
@@ -168,7 +168,7 @@ public:
|
||||
virtual App::Document* newDocument(const std::string& name = "") = 0;
|
||||
|
||||
/**
|
||||
* Open a document by identity.
|
||||
* Open a document by identity (non-interactive).
|
||||
* Local: Opens file at path
|
||||
* PLM: Opens document by UUID (downloads if needed)
|
||||
* @param identity Document identity (path or UUID)
|
||||
@@ -176,9 +176,17 @@ public:
|
||||
*/
|
||||
virtual App::Document* openDocument(const std::string& identity) = 0;
|
||||
|
||||
/**
|
||||
* Open a document interactively (shows dialog).
|
||||
* Local: Shows file picker dialog
|
||||
* PLM: Shows search/browse dialog
|
||||
* @return The opened document or nullptr if cancelled/failed
|
||||
*/
|
||||
virtual App::Document* openDocumentInteractive() = 0;
|
||||
|
||||
/**
|
||||
* Save the document.
|
||||
* Local: Saves to disk
|
||||
* Local: Saves to disk (if path known)
|
||||
* PLM: Saves to disk and syncs with external system
|
||||
* @param doc The document to save
|
||||
* @return true if save succeeded
|
||||
@@ -186,14 +194,21 @@ public:
|
||||
virtual bool saveDocument(App::Document* doc) = 0;
|
||||
|
||||
/**
|
||||
* Save document with new identity.
|
||||
* Local: File picker for new path
|
||||
* PLM: Migration or copy workflow
|
||||
* Save document with new identity (non-interactive).
|
||||
* @param doc The document to save
|
||||
* @param newIdentity New identity (path or part number)
|
||||
* @return true if save succeeded
|
||||
*/
|
||||
virtual bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) = 0;
|
||||
|
||||
/**
|
||||
* Save document interactively (shows dialog).
|
||||
* Local: Shows file picker for new path
|
||||
* PLM: Shows migration or copy workflow dialog
|
||||
* @param doc The document to save
|
||||
* @return true if save succeeded
|
||||
*/
|
||||
virtual bool saveDocumentAsInteractive(App::Document* doc) = 0;
|
||||
//@}
|
||||
|
||||
///@name Extended Operations (PLM-specific, default to no-op)
|
||||
@@ -250,8 +265,10 @@ public:
|
||||
// Document operations
|
||||
App::Document* newDocument(const std::string& name = "") override;
|
||||
App::Document* openDocument(const std::string& identity) override;
|
||||
App::Document* openDocumentInteractive() override;
|
||||
bool saveDocument(App::Document* doc) override;
|
||||
bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) override;
|
||||
bool saveDocumentAsInteractive(App::Document* doc) override;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
#include <App/DocumentPy.h>
|
||||
#include <Base/Console.h>
|
||||
#include <Base/Interpreter.h>
|
||||
#include <Base/PyObjectBase.h>
|
||||
@@ -41,7 +42,7 @@ void FileOriginPython::addOrigin(const Py::Object& obj)
|
||||
{
|
||||
// Check if already registered
|
||||
if (findOrigin(obj)) {
|
||||
Base::Console().Warning("FileOriginPython: Origin already registered\n");
|
||||
Base::Console().warning("FileOriginPython: Origin already registered\n");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ void FileOriginPython::addOrigin(const Py::Object& obj)
|
||||
// Cache the ID immediately for registration
|
||||
origin->_cachedId = origin->callStringMethod("id");
|
||||
if (origin->_cachedId.empty()) {
|
||||
Base::Console().Error("FileOriginPython: Origin must have non-empty id()\n");
|
||||
Base::Console().error("FileOriginPython: Origin must have non-empty id()\n");
|
||||
delete origin;
|
||||
return;
|
||||
}
|
||||
@@ -117,7 +118,7 @@ Py::Object FileOriginPython::callMethod(const char* method, const Py::Tuple& arg
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return Py::None();
|
||||
}
|
||||
@@ -139,7 +140,7 @@ bool FileOriginPython::callBoolMethod(const char* method, bool defaultValue) con
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -158,7 +159,7 @@ std::string FileOriginPython::callStringMethod(const char* method, const std::st
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -210,7 +211,7 @@ OriginType FileOriginPython::type() const
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return OriginType::Custom;
|
||||
}
|
||||
@@ -265,7 +266,7 @@ ConnectionState FileOriginPython::connectionState() const
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return ConnectionState::Connected;
|
||||
}
|
||||
@@ -297,7 +298,7 @@ std::string FileOriginPython::documentIdentity(App::Document* doc) const
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -318,7 +319,7 @@ std::string FileOriginPython::documentDisplayId(App::Document* doc) const
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return documentIdentity(doc);
|
||||
}
|
||||
@@ -342,7 +343,7 @@ bool FileOriginPython::ownsDocument(App::Document* doc) const
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -366,7 +367,7 @@ bool FileOriginPython::syncProperties(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -391,7 +392,7 @@ App::Document* FileOriginPython::newDocument(const std::string& name)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -414,7 +415,7 @@ App::Document* FileOriginPython::openDocument(const std::string& identity)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -438,7 +439,7 @@ bool FileOriginPython::saveDocument(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -463,7 +464,7 @@ bool FileOriginPython::saveDocumentAs(App::Document* doc, const std::string& new
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -488,7 +489,7 @@ bool FileOriginPython::commitDocument(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -512,7 +513,7 @@ bool FileOriginPython::pullDocument(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -536,7 +537,7 @@ bool FileOriginPython::pushDocument(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -554,7 +555,7 @@ void FileOriginPython::showInfo(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,8 +572,53 @@ void FileOriginPython::showBOM(App::Document* doc)
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
e.reportException();
|
||||
}
|
||||
}
|
||||
|
||||
App::Document* FileOriginPython::openDocumentInteractive()
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("openDocumentInteractive")) {
|
||||
Py::Callable func(_inst.getAttr("openDocumentInteractive"));
|
||||
Py::Object result = func.apply(Py::Tuple());
|
||||
if (!result.isNone()) {
|
||||
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
|
||||
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.reportException();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FileOriginPython::saveDocumentAsInteractive(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("saveDocumentAsInteractive")) {
|
||||
Py::Callable func(_inst.getAttr("saveDocumentAsInteractive"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.reportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
@@ -115,8 +115,10 @@ public:
|
||||
|
||||
App::Document* newDocument(const std::string& name = "") override;
|
||||
App::Document* openDocument(const std::string& identity) override;
|
||||
App::Document* openDocumentInteractive() override;
|
||||
bool saveDocument(App::Document* doc) override;
|
||||
bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) override;
|
||||
bool saveDocumentAsInteractive(App::Document* doc) override;
|
||||
|
||||
bool commitDocument(App::Document* doc) override;
|
||||
bool pullDocument(App::Document* doc) override;
|
||||
|
||||
@@ -185,6 +185,11 @@
|
||||
<file>sel-bbox.svg</file>
|
||||
<file>sel-forward.svg</file>
|
||||
<file>sel-instance.svg</file>
|
||||
<file>silo-bom.svg</file>
|
||||
<file>silo-commit.svg</file>
|
||||
<file>silo-info.svg</file>
|
||||
<file>silo-pull.svg</file>
|
||||
<file>silo-push.svg</file>
|
||||
<file>spaceball_button.svg</file>
|
||||
<file>SpNav-PanLR.svg</file>
|
||||
<file>SpNav-PanUD.svg</file>
|
||||
|
||||
12
src/Gui/Icons/silo-bom.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Outer box -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" fill="#313244"/>
|
||||
<!-- List lines (BOM rows) -->
|
||||
<line x1="8" y1="8" x2="18" y2="8" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<line x1="8" y1="12" x2="18" y2="12" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<line x1="8" y1="16" x2="18" y2="16" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<!-- Hierarchy dots -->
|
||||
<circle cx="6" cy="8" r="1" fill="#cba6f7"/>
|
||||
<circle cx="6" cy="12" r="1" fill="#cba6f7"/>
|
||||
<circle cx="6" cy="16" r="1" fill="#cba6f7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
8
src/Gui/Icons/silo-commit.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Git commit style -->
|
||||
<circle cx="12" cy="12" r="4" fill="#313244" stroke="#a6e3a1"/>
|
||||
<line x1="12" y1="2" x2="12" y2="8" stroke="#cba6f7"/>
|
||||
<line x1="12" y1="16" x2="12" y2="22" stroke="#cba6f7"/>
|
||||
<!-- Checkmark inside -->
|
||||
<polyline points="9.5 12 11 13.5 14.5 10" stroke="#a6e3a1" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
6
src/Gui/Icons/silo-info.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Info circle -->
|
||||
<circle cx="12" cy="12" r="10" fill="#313244"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12" stroke="#89dceb" stroke-width="2"/>
|
||||
<circle cx="12" cy="8" r="0.5" fill="#89dceb" stroke="#89dceb"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
7
src/Gui/Icons/silo-pull.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Cloud -->
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||
<!-- Download arrow -->
|
||||
<path d="M12 13v5m0 0l-2-2m2 2l2-2" stroke="#89b4fa" stroke-width="2"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13" stroke="#89b4fa" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
7
src/Gui/Icons/silo-push.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Cloud -->
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||
<!-- Upload arrow -->
|
||||
<path d="M12 18v-5m0 0l-2 2m2-2l2 2" stroke="#a6e3a1" stroke-width="2"/>
|
||||
<line x1="12" y1="13" x2="12" y2="9" stroke="#a6e3a1" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -44,7 +44,9 @@
|
||||
#include "Application.h"
|
||||
#include "Document.h"
|
||||
#include "FileDialog.h"
|
||||
#include "FileOrigin.h"
|
||||
#include "MainWindow.h"
|
||||
#include "OriginManager.h"
|
||||
#include "ViewProviderDocumentObject.h"
|
||||
|
||||
|
||||
@@ -522,6 +524,13 @@ QString MDIView::buildWindowTitle() const
|
||||
QString windowTitle;
|
||||
if (auto document = getAppDocument()) {
|
||||
windowTitle.append(QString::fromStdString(document->Label.getStrValue()));
|
||||
|
||||
// Append origin suffix for non-local origins
|
||||
FileOrigin* origin = OriginManager::instance()->originForDocument(document);
|
||||
if (origin && origin->type() != OriginType::Local) {
|
||||
windowTitle.append(QStringLiteral(" [%1]")
|
||||
.arg(QString::fromStdString(origin->nickname())));
|
||||
}
|
||||
}
|
||||
|
||||
return windowTitle;
|
||||
|
||||
@@ -111,20 +111,20 @@ bool OriginManager::registerOrigin(FileOrigin* origin)
|
||||
|
||||
std::string originId = origin->id();
|
||||
if (originId.empty()) {
|
||||
Base::Console().Warning("OriginManager: Cannot register origin with empty ID\n");
|
||||
Base::Console().warning("OriginManager: Cannot register origin with empty ID\n");
|
||||
delete origin;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if ID already in use
|
||||
if (_origins.find(originId) != _origins.end()) {
|
||||
Base::Console().Warning("OriginManager: Origin '%s' already registered\n", originId.c_str());
|
||||
Base::Console().warning("OriginManager: Origin '%s' already registered\n", originId.c_str());
|
||||
delete origin;
|
||||
return false;
|
||||
}
|
||||
|
||||
_origins[originId] = std::unique_ptr<FileOrigin>(origin);
|
||||
Base::Console().Log("OriginManager: Registered origin '%s'\n", originId.c_str());
|
||||
Base::Console().log("OriginManager: Registered origin '%s'\n", originId.c_str());
|
||||
|
||||
signalOriginRegistered(originId);
|
||||
return true;
|
||||
@@ -134,7 +134,7 @@ bool OriginManager::unregisterOrigin(const std::string& id)
|
||||
{
|
||||
// Cannot unregister the built-in local origin
|
||||
if (id == LOCAL_ORIGIN_ID) {
|
||||
Base::Console().Warning("OriginManager: Cannot unregister built-in local origin\n");
|
||||
Base::Console().warning("OriginManager: Cannot unregister built-in local origin\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ bool OriginManager::unregisterOrigin(const std::string& id)
|
||||
}
|
||||
|
||||
_origins.erase(it);
|
||||
Base::Console().Log("OriginManager: Unregistered origin '%s'\n", id.c_str());
|
||||
Base::Console().log("OriginManager: Unregistered origin '%s'\n", id.c_str());
|
||||
|
||||
signalOriginUnregistered(id);
|
||||
return true;
|
||||
@@ -231,6 +231,63 @@ FileOrigin* OriginManager::findOwningOrigin(App::Document* doc) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FileOrigin* OriginManager::originForDocument(App::Document* doc) const
|
||||
{
|
||||
if (!doc) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check explicit association first
|
||||
auto it = _documentOrigins.find(doc);
|
||||
if (it != _documentOrigins.end()) {
|
||||
FileOrigin* origin = getOrigin(it->second);
|
||||
if (origin) {
|
||||
return origin;
|
||||
}
|
||||
// Origin was unregistered, clear stale association
|
||||
_documentOrigins.erase(it);
|
||||
}
|
||||
|
||||
// Fall back to ownership detection
|
||||
FileOrigin* owner = findOwningOrigin(doc);
|
||||
if (owner) {
|
||||
// Cache the result
|
||||
_documentOrigins[doc] = owner->id();
|
||||
return owner;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void OriginManager::setDocumentOrigin(App::Document* doc, FileOrigin* origin)
|
||||
{
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string originId = origin ? origin->id() : "";
|
||||
|
||||
if (origin) {
|
||||
_documentOrigins[doc] = originId;
|
||||
} else {
|
||||
_documentOrigins.erase(doc);
|
||||
}
|
||||
|
||||
signalDocumentOriginChanged(doc, originId);
|
||||
}
|
||||
|
||||
void OriginManager::clearDocumentOrigin(App::Document* doc)
|
||||
{
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = _documentOrigins.find(doc);
|
||||
if (it != _documentOrigins.end()) {
|
||||
_documentOrigins.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
FileOrigin* OriginManager::originForNewDocument() const
|
||||
{
|
||||
return currentOrigin();
|
||||
|
||||
@@ -121,6 +121,27 @@ public:
|
||||
*/
|
||||
FileOrigin* findOwningOrigin(App::Document* doc) const;
|
||||
|
||||
/**
|
||||
* Get the origin associated with a document.
|
||||
* First checks explicit association, then uses findOwningOrigin().
|
||||
* @param doc The document to check
|
||||
* @return The document's origin or nullptr if unknown
|
||||
*/
|
||||
FileOrigin* originForDocument(App::Document* doc) const;
|
||||
|
||||
/**
|
||||
* Associate a document with an origin.
|
||||
* @param doc The document
|
||||
* @param origin The origin to associate (nullptr to clear)
|
||||
*/
|
||||
void setDocumentOrigin(App::Document* doc, FileOrigin* origin);
|
||||
|
||||
/**
|
||||
* Clear document origin association (called when document closes).
|
||||
* @param doc The document being closed
|
||||
*/
|
||||
void clearDocumentOrigin(App::Document* doc);
|
||||
|
||||
/**
|
||||
* Get the appropriate origin for a new document.
|
||||
* Returns the current origin.
|
||||
@@ -137,6 +158,8 @@ public:
|
||||
fastsignals::signal<void(const std::string&)> signalOriginUnregistered;
|
||||
/** Emitted when current origin changes */
|
||||
fastsignals::signal<void(const std::string&)> signalCurrentOriginChanged;
|
||||
/** Emitted when a document's origin association changes */
|
||||
fastsignals::signal<void(App::Document*, const std::string&)> signalDocumentOriginChanged;
|
||||
//@}
|
||||
|
||||
protected:
|
||||
@@ -151,6 +174,9 @@ private:
|
||||
static OriginManager* _instance;
|
||||
std::map<std::string, std::unique_ptr<FileOrigin>> _origins;
|
||||
std::string _currentOriginId;
|
||||
|
||||
// Document-to-origin associations (doc -> origin ID)
|
||||
mutable std::map<App::Document*, std::string> _documentOrigins;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
251
src/Gui/OriginManagerDialog.cpp
Normal file
@@ -0,0 +1,251 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "OriginManagerDialog.h"
|
||||
#include "OriginManager.h"
|
||||
#include "FileOrigin.h"
|
||||
#include "BitmapFactory.h"
|
||||
|
||||
|
||||
namespace Gui {
|
||||
|
||||
OriginManagerDialog::OriginManagerDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setupUi();
|
||||
populateOriginList();
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
OriginManagerDialog::~OriginManagerDialog() = default;
|
||||
|
||||
void OriginManagerDialog::setupUi()
|
||||
{
|
||||
setWindowTitle(tr("Manage File Origins"));
|
||||
setMinimumSize(450, 350);
|
||||
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// Description
|
||||
auto* descLabel = new QLabel(tr("Configure file origins for storing and retrieving documents."));
|
||||
descLabel->setWordWrap(true);
|
||||
mainLayout->addWidget(descLabel);
|
||||
|
||||
// Origin list
|
||||
m_originList = new QListWidget(this);
|
||||
m_originList->setIconSize(QSize(24, 24));
|
||||
m_originList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
connect(m_originList, &QListWidget::itemSelectionChanged,
|
||||
this, &OriginManagerDialog::onOriginSelectionChanged);
|
||||
connect(m_originList, &QListWidget::itemDoubleClicked,
|
||||
this, &OriginManagerDialog::onOriginDoubleClicked);
|
||||
mainLayout->addWidget(m_originList);
|
||||
|
||||
// Action buttons
|
||||
auto* actionLayout = new QHBoxLayout();
|
||||
|
||||
m_addButton = new QPushButton(tr("Add Silo..."));
|
||||
m_addButton->setIcon(BitmapFactory().iconFromTheme("list-add"));
|
||||
connect(m_addButton, &QPushButton::clicked, this, &OriginManagerDialog::onAddSilo);
|
||||
actionLayout->addWidget(m_addButton);
|
||||
|
||||
m_editButton = new QPushButton(tr("Edit..."));
|
||||
m_editButton->setIcon(BitmapFactory().iconFromTheme("document-edit"));
|
||||
connect(m_editButton, &QPushButton::clicked, this, &OriginManagerDialog::onEditOrigin);
|
||||
actionLayout->addWidget(m_editButton);
|
||||
|
||||
m_removeButton = new QPushButton(tr("Remove"));
|
||||
m_removeButton->setIcon(BitmapFactory().iconFromTheme("list-remove"));
|
||||
connect(m_removeButton, &QPushButton::clicked, this, &OriginManagerDialog::onRemoveOrigin);
|
||||
actionLayout->addWidget(m_removeButton);
|
||||
|
||||
actionLayout->addStretch();
|
||||
|
||||
m_defaultButton = new QPushButton(tr("Set as Default"));
|
||||
connect(m_defaultButton, &QPushButton::clicked, this, &OriginManagerDialog::onSetDefault);
|
||||
actionLayout->addWidget(m_defaultButton);
|
||||
|
||||
mainLayout->addLayout(actionLayout);
|
||||
|
||||
// Dialog buttons
|
||||
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||
connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(m_buttonBox);
|
||||
}
|
||||
|
||||
void OriginManagerDialog::populateOriginList()
|
||||
{
|
||||
m_originList->clear();
|
||||
|
||||
auto* mgr = OriginManager::instance();
|
||||
std::string currentId = mgr->currentOriginId();
|
||||
|
||||
for (const std::string& originId : mgr->originIds()) {
|
||||
FileOrigin* origin = mgr->getOrigin(originId);
|
||||
if (!origin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* item = new QListWidgetItem(m_originList);
|
||||
item->setIcon(origin->icon());
|
||||
|
||||
QString displayText = QString::fromStdString(origin->name());
|
||||
|
||||
// Add connection status for remote origins
|
||||
if (origin->requiresAuthentication()) {
|
||||
ConnectionState state = origin->connectionState();
|
||||
switch (state) {
|
||||
case ConnectionState::Connected:
|
||||
displayText += tr(" [Connected]");
|
||||
break;
|
||||
case ConnectionState::Connecting:
|
||||
displayText += tr(" [Connecting...]");
|
||||
break;
|
||||
case ConnectionState::Disconnected:
|
||||
displayText += tr(" [Disconnected]");
|
||||
break;
|
||||
case ConnectionState::Error:
|
||||
displayText += tr(" [Error]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark default origin
|
||||
if (originId == currentId) {
|
||||
displayText += tr(" (Default)");
|
||||
QFont font = item->font();
|
||||
font.setBold(true);
|
||||
item->setFont(font);
|
||||
}
|
||||
|
||||
item->setText(displayText);
|
||||
item->setData(Qt::UserRole, QString::fromStdString(originId));
|
||||
item->setToolTip(QString::fromStdString(origin->name()));
|
||||
}
|
||||
}
|
||||
|
||||
void OriginManagerDialog::updateButtonStates()
|
||||
{
|
||||
FileOrigin* origin = selectedOrigin();
|
||||
bool hasSelection = (origin != nullptr);
|
||||
bool isLocal = hasSelection && (origin->id() == "local");
|
||||
bool isDefault = hasSelection && (origin->id() == OriginManager::instance()->currentOriginId());
|
||||
|
||||
// Can't edit or remove local origin
|
||||
m_editButton->setEnabled(hasSelection && !isLocal);
|
||||
m_removeButton->setEnabled(hasSelection && !isLocal);
|
||||
m_defaultButton->setEnabled(hasSelection && !isDefault);
|
||||
}
|
||||
|
||||
FileOrigin* OriginManagerDialog::selectedOrigin() const
|
||||
{
|
||||
QListWidgetItem* item = m_originList->currentItem();
|
||||
if (!item) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string originId = item->data(Qt::UserRole).toString().toStdString();
|
||||
return OriginManager::instance()->getOrigin(originId);
|
||||
}
|
||||
|
||||
void OriginManagerDialog::onAddSilo()
|
||||
{
|
||||
// TODO: Open SiloConfigDialog for adding new instance
|
||||
QMessageBox::information(this, tr("Add Silo"),
|
||||
tr("Silo configuration dialog not yet implemented.\n\n"
|
||||
"To add a Silo instance, configure it in the Silo workbench preferences."));
|
||||
}
|
||||
|
||||
void OriginManagerDialog::onEditOrigin()
|
||||
{
|
||||
FileOrigin* origin = selectedOrigin();
|
||||
if (!origin || origin->id() == "local") {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Open SiloConfigDialog for editing
|
||||
QMessageBox::information(this, tr("Edit Origin"),
|
||||
tr("Origin editing not yet implemented.\n\n"
|
||||
"To edit this origin, modify settings in the Silo workbench preferences."));
|
||||
}
|
||||
|
||||
void OriginManagerDialog::onRemoveOrigin()
|
||||
{
|
||||
FileOrigin* origin = selectedOrigin();
|
||||
if (!origin || origin->id() == "local") {
|
||||
return;
|
||||
}
|
||||
|
||||
QString name = QString::fromStdString(origin->name());
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(this,
|
||||
tr("Remove Origin"),
|
||||
tr("Are you sure you want to remove '%1'?\n\n"
|
||||
"This will not delete any files, but you will need to reconfigure "
|
||||
"the connection to use this origin again.").arg(name),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
std::string originId = origin->id();
|
||||
OriginManager::instance()->unregisterOrigin(originId);
|
||||
populateOriginList();
|
||||
updateButtonStates();
|
||||
}
|
||||
}
|
||||
|
||||
void OriginManagerDialog::onSetDefault()
|
||||
{
|
||||
FileOrigin* origin = selectedOrigin();
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
OriginManager::instance()->setCurrentOrigin(origin->id());
|
||||
populateOriginList();
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
void OriginManagerDialog::onOriginSelectionChanged()
|
||||
{
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
void OriginManagerDialog::onOriginDoubleClicked(QListWidgetItem* item)
|
||||
{
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string originId = item->data(Qt::UserRole).toString().toStdString();
|
||||
if (originId != "local") {
|
||||
onEditOrigin();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
74
src/Gui/OriginManagerDialog.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_ORIGINMANAGERDIALOG_H
|
||||
#define GUI_ORIGINMANAGERDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QDialogButtonBox>
|
||||
#include <FCGlobal.h>
|
||||
|
||||
namespace Gui {
|
||||
|
||||
class FileOrigin;
|
||||
|
||||
/**
|
||||
* @brief Dialog for managing file origins
|
||||
*
|
||||
* This dialog allows users to view, add, edit, and remove file origins
|
||||
* (Silo instances). The local filesystem origin cannot be removed.
|
||||
*/
|
||||
class GuiExport OriginManagerDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OriginManagerDialog(QWidget* parent = nullptr);
|
||||
~OriginManagerDialog() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void onAddSilo();
|
||||
void onEditOrigin();
|
||||
void onRemoveOrigin();
|
||||
void onSetDefault();
|
||||
void onOriginSelectionChanged();
|
||||
void onOriginDoubleClicked(QListWidgetItem* item);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateOriginList();
|
||||
void updateButtonStates();
|
||||
FileOrigin* selectedOrigin() const;
|
||||
|
||||
QListWidget* m_originList;
|
||||
QPushButton* m_addButton;
|
||||
QPushButton* m_editButton;
|
||||
QPushButton* m_removeButton;
|
||||
QPushButton* m_defaultButton;
|
||||
QDialogButtonBox* m_buttonBox;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_ORIGINMANAGERDIALOG_H
|
||||
270
src/Gui/OriginSelectorWidget.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#include "OriginSelectorWidget.h"
|
||||
#include "OriginManager.h"
|
||||
#include "OriginManagerDialog.h"
|
||||
#include "FileOrigin.h"
|
||||
#include "BitmapFactory.h"
|
||||
|
||||
|
||||
namespace Gui {
|
||||
|
||||
OriginSelectorWidget::OriginSelectorWidget(QWidget* parent)
|
||||
: QToolButton(parent)
|
||||
, m_menu(nullptr)
|
||||
, m_originActions(nullptr)
|
||||
, m_manageAction(nullptr)
|
||||
{
|
||||
setupUi();
|
||||
connectSignals();
|
||||
rebuildMenu();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
OriginSelectorWidget::~OriginSelectorWidget()
|
||||
{
|
||||
disconnectSignals();
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::setupUi()
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
setMinimumWidth(70);
|
||||
setMaximumWidth(120);
|
||||
|
||||
// Create menu
|
||||
m_menu = new QMenu(this);
|
||||
setMenu(m_menu);
|
||||
|
||||
// Create action group for exclusive selection
|
||||
m_originActions = new QActionGroup(this);
|
||||
m_originActions->setExclusive(true);
|
||||
|
||||
// Connect action group to selection handler
|
||||
connect(m_originActions, &QActionGroup::triggered,
|
||||
this, &OriginSelectorWidget::onOriginActionTriggered);
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::connectSignals()
|
||||
{
|
||||
auto* mgr = OriginManager::instance();
|
||||
|
||||
// Connect to OriginManager fastsignals
|
||||
m_connRegistered = mgr->signalOriginRegistered.connect(
|
||||
[this](const std::string& id) { onOriginRegistered(id); }
|
||||
);
|
||||
m_connUnregistered = mgr->signalOriginUnregistered.connect(
|
||||
[this](const std::string& id) { onOriginUnregistered(id); }
|
||||
);
|
||||
m_connChanged = mgr->signalCurrentOriginChanged.connect(
|
||||
[this](const std::string& id) { onCurrentOriginChanged(id); }
|
||||
);
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::disconnectSignals()
|
||||
{
|
||||
m_connRegistered.disconnect();
|
||||
m_connUnregistered.disconnect();
|
||||
m_connChanged.disconnect();
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::onOriginRegistered(const std::string& /*originId*/)
|
||||
{
|
||||
// Rebuild menu to include new origin
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::onOriginUnregistered(const std::string& /*originId*/)
|
||||
{
|
||||
// Rebuild menu to remove origin
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::onCurrentOriginChanged(const std::string& /*originId*/)
|
||||
{
|
||||
// Update display and menu checkmarks
|
||||
updateDisplay();
|
||||
|
||||
// Update checked state in menu
|
||||
auto* mgr = OriginManager::instance();
|
||||
std::string currentId = mgr->currentOriginId();
|
||||
|
||||
for (QAction* action : m_originActions->actions()) {
|
||||
std::string actionId = action->data().toString().toStdString();
|
||||
action->setChecked(actionId == currentId);
|
||||
}
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::onOriginActionTriggered(QAction* action)
|
||||
{
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string originId = action->data().toString().toStdString();
|
||||
auto* mgr = OriginManager::instance();
|
||||
|
||||
// Check if origin requires connection
|
||||
FileOrigin* origin = mgr->getOrigin(originId);
|
||||
if (origin && origin->requiresAuthentication()) {
|
||||
ConnectionState state = origin->connectionState();
|
||||
if (state == ConnectionState::Disconnected || state == ConnectionState::Error) {
|
||||
// Try to connect
|
||||
if (!origin->connect()) {
|
||||
// Connection failed - don't switch
|
||||
// Revert the checkmark to current origin
|
||||
std::string currentId = mgr->currentOriginId();
|
||||
for (QAction* a : m_originActions->actions()) {
|
||||
a->setChecked(a->data().toString().toStdString() == currentId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mgr->setCurrentOrigin(originId);
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::onManageOriginsClicked()
|
||||
{
|
||||
OriginManagerDialog dialog(this);
|
||||
dialog.exec();
|
||||
|
||||
// Refresh the menu in case origins changed
|
||||
rebuildMenu();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::updateDisplay()
|
||||
{
|
||||
FileOrigin* origin = OriginManager::instance()->currentOrigin();
|
||||
if (!origin) {
|
||||
setText(tr("No Origin"));
|
||||
setIcon(QIcon());
|
||||
setToolTip(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
setText(QString::fromStdString(origin->nickname()));
|
||||
setIcon(iconForOrigin(origin));
|
||||
setToolTip(QString::fromStdString(origin->name()));
|
||||
}
|
||||
|
||||
void OriginSelectorWidget::rebuildMenu()
|
||||
{
|
||||
m_menu->clear();
|
||||
|
||||
// Remove old actions from action group
|
||||
for (QAction* action : m_originActions->actions()) {
|
||||
m_originActions->removeAction(action);
|
||||
}
|
||||
|
||||
auto* mgr = OriginManager::instance();
|
||||
std::string currentId = mgr->currentOriginId();
|
||||
|
||||
// Add origin entries
|
||||
for (const std::string& originId : mgr->originIds()) {
|
||||
FileOrigin* origin = mgr->getOrigin(originId);
|
||||
if (!origin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QAction* action = m_menu->addAction(
|
||||
iconForOrigin(origin),
|
||||
QString::fromStdString(origin->nickname())
|
||||
);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(originId == currentId);
|
||||
action->setData(QString::fromStdString(originId));
|
||||
action->setToolTip(QString::fromStdString(origin->name()));
|
||||
|
||||
m_originActions->addAction(action);
|
||||
}
|
||||
|
||||
// Add separator and manage action
|
||||
m_menu->addSeparator();
|
||||
|
||||
m_manageAction = m_menu->addAction(
|
||||
BitmapFactory().iconFromTheme("preferences-system"),
|
||||
tr("Manage Origins...")
|
||||
);
|
||||
connect(m_manageAction, &QAction::triggered,
|
||||
this, &OriginSelectorWidget::onManageOriginsClicked);
|
||||
}
|
||||
|
||||
QIcon OriginSelectorWidget::iconForOrigin(FileOrigin* origin) const
|
||||
{
|
||||
if (!origin) {
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QIcon baseIcon = origin->icon();
|
||||
|
||||
// For origins that require authentication, overlay connection status
|
||||
if (origin->requiresAuthentication()) {
|
||||
ConnectionState state = origin->connectionState();
|
||||
|
||||
switch (state) {
|
||||
case ConnectionState::Connected:
|
||||
// No overlay needed - use base icon
|
||||
break;
|
||||
|
||||
case ConnectionState::Connecting:
|
||||
// TODO: Animated connecting indicator
|
||||
break;
|
||||
|
||||
case ConnectionState::Disconnected:
|
||||
// Overlay disconnected indicator
|
||||
{
|
||||
QPixmap overlay = BitmapFactory().pixmapFromSvg(
|
||||
"dagViewFail", QSizeF(8, 8));
|
||||
if (!overlay.isNull()) {
|
||||
baseIcon = BitmapFactoryInst::mergePixmap(
|
||||
baseIcon, overlay, BitmapFactoryInst::BottomRight);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ConnectionState::Error:
|
||||
// Overlay error indicator
|
||||
{
|
||||
QPixmap overlay = BitmapFactory().pixmapFromSvg(
|
||||
"Warning", QSizeF(8, 8));
|
||||
if (!overlay.isNull()) {
|
||||
baseIcon = BitmapFactoryInst::mergePixmap(
|
||||
baseIcon, overlay, BitmapFactoryInst::BottomRight);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return baseIcon;
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
95
src/Gui/OriginSelectorWidget.h
Normal file
@@ -0,0 +1,95 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_ORIGINSELECTORWIDGET_H
|
||||
#define GUI_ORIGINSELECTORWIDGET_H
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QMenu>
|
||||
#include <QActionGroup>
|
||||
#include <fastsignals/signal.h>
|
||||
#include <FCGlobal.h>
|
||||
|
||||
namespace Gui {
|
||||
|
||||
class FileOrigin;
|
||||
|
||||
/**
|
||||
* @brief Toolbar widget for selecting the current file origin
|
||||
*
|
||||
* OriginSelectorWidget displays the currently selected origin and provides
|
||||
* a dropdown menu to switch between available origins (Local Files, Silo
|
||||
* instances, etc.).
|
||||
*
|
||||
* Visual design:
|
||||
* Collapsed (toolbar state):
|
||||
* ┌──────────────────┐
|
||||
* │ ☁️ Work ▼ │ ~70-100px wide
|
||||
* └──────────────────┘
|
||||
*
|
||||
* Expanded (dropdown open):
|
||||
* ┌──────────────────┐
|
||||
* │ ✓ ☁️ Work │ ← Current selection (checkmark)
|
||||
* │ ☁️ Prod │
|
||||
* │ 📁 Local │
|
||||
* ├──────────────────┤
|
||||
* │ ⚙️ Manage... │ ← Opens config dialog
|
||||
* └──────────────────┘
|
||||
*/
|
||||
class GuiExport OriginSelectorWidget : public QToolButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OriginSelectorWidget(QWidget* parent = nullptr);
|
||||
~OriginSelectorWidget() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void onOriginActionTriggered(QAction* action);
|
||||
void onManageOriginsClicked();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void connectSignals();
|
||||
void disconnectSignals();
|
||||
|
||||
void onOriginRegistered(const std::string& originId);
|
||||
void onOriginUnregistered(const std::string& originId);
|
||||
void onCurrentOriginChanged(const std::string& originId);
|
||||
|
||||
void updateDisplay();
|
||||
void rebuildMenu();
|
||||
QIcon iconForOrigin(FileOrigin* origin) const;
|
||||
|
||||
QMenu* m_menu;
|
||||
QActionGroup* m_originActions;
|
||||
QAction* m_manageAction;
|
||||
|
||||
// Signal connections
|
||||
fastsignals::scoped_connection m_connRegistered;
|
||||
fastsignals::scoped_connection m_connUnregistered;
|
||||
fastsignals::scoped_connection m_connChanged;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_ORIGINSELECTORWIDGET_H
|
||||
@@ -1142,6 +1142,28 @@ Gui--WorkbenchComboBox::drop-down {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Origin Selector */
|
||||
Gui--OriginSelectorWidget {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
min-width: 70px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
Gui--OriginSelectorWidget:hover {
|
||||
border-color: #585b70;
|
||||
background-color: #45475a;
|
||||
}
|
||||
|
||||
Gui--OriginSelectorWidget::menu-indicator {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
/* Task Panel */
|
||||
QSint--ActionGroup {
|
||||
background-color: #313244;
|
||||
|
||||
@@ -1163,6 +1163,28 @@ Gui--WorkbenchComboBox::drop-down {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Origin Selector */
|
||||
Gui--OriginSelectorWidget {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
min-width: 70px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
Gui--OriginSelectorWidget:hover {
|
||||
border-color: #585b70;
|
||||
background-color: #45475a;
|
||||
}
|
||||
|
||||
Gui--OriginSelectorWidget::menu-indicator {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
/* Task Panel */
|
||||
QSint--ActionGroup {
|
||||
background-color: #313244;
|
||||
|
||||
@@ -682,7 +682,10 @@ MenuItem* StdWorkbench::setupMenuBar() const
|
||||
file->setCommand("&File");
|
||||
*file << "Std_New" << "Std_Open" << "Std_RecentFiles" << "Separator" << "Std_CloseActiveWindow"
|
||||
<< "Std_CloseAllWindows" << "Separator" << "Std_Save" << "Std_SaveAs"
|
||||
<< "Std_SaveCopy" << "Std_SaveAll" << "Std_Revert" << "Separator" << "Std_Import"
|
||||
<< "Std_SaveCopy" << "Std_SaveAll" << "Std_Revert"
|
||||
<< "Separator" << "Origin_Commit" << "Origin_Pull" << "Origin_Push"
|
||||
<< "Origin_Info" << "Origin_BOM"
|
||||
<< "Separator" << "Std_Import"
|
||||
<< "Std_Export" << "Std_MergeProjects" << "Std_ProjectInfo"
|
||||
<< "Separator" << "Std_Print" << "Std_PrintPreview" << "Std_PrintPdf"
|
||||
<< "Separator" << "Std_Quit";
|
||||
@@ -834,7 +837,13 @@ ToolBarItem* StdWorkbench::setupToolBars() const
|
||||
// File
|
||||
auto file = new ToolBarItem(root);
|
||||
file->setCommand("File");
|
||||
*file << "Std_New" << "Std_Open" << "Std_Save";
|
||||
*file << "Std_Origin" << "Std_New" << "Std_Open" << "Std_Save";
|
||||
|
||||
// Origin Tools (PLM operations - commands auto-disable when not applicable)
|
||||
auto originTools = new ToolBarItem(root);
|
||||
originTools->setCommand("Origin Tools");
|
||||
*originTools << "Origin_Commit" << "Origin_Pull" << "Origin_Push"
|
||||
<< "Separator" << "Origin_Info" << "Origin_BOM";
|
||||
|
||||
// Edit
|
||||
auto edit = new ToolBarItem(root);
|
||||
|
||||