Compare commits

...

9 Commits

Author SHA1 Message Date
forbes
84b69b935b fix(build): remove non-existent SelectModule.h include in FileOrigin.cpp
Some checks failed
Build and Test / build (push) Failing after 19m45s
Release Build / publish-release (push) Has been skipped
Release Build / build-linux (push) Failing after 31m20s
SelectModule is declared in FileDialog.h which was already included.
The separate #include "SelectModule.h" referenced a header that
doesn't exist, causing a fatal build error.
2026-02-05 16:53:14 -06:00
a6e84552da feat(gui): add cross-origin detection in StdCmdSaveAs (#17)
Some checks failed
Build and Test / build (push) Failing after 12m50s
- Detect document's origin vs current (target) origin
- Route to appropriate workflow:
  - Same origin: standard SaveAs
  - Local → PLM: migration (handled by PLM origin)
  - PLM → Local: export (handled by local origin)
  - PLM → PLM: transfer (handled by target PLM origin)

This provides the infrastructure for Issue #17: Mixed origin workflows.
The actual migration/export dialogs will be implemented in the
respective origin adapters (SiloOrigin, LocalFileOrigin).
2026-02-05 14:54:36 -06:00
015df38328 feat(gui): add document-origin tracking and display in window title (#16)
Some checks failed
Build and Test / build (push) Has been cancelled
- Add originForDocument(), setDocumentOrigin(), clearDocumentOrigin()
  methods to OriginManager
- Add signalDocumentOriginChanged signal for UI updates
- Add document-to-origin tracking map in OriginManager
- Update MDIView::buildWindowTitle() to append origin suffix for
  non-local origins (e.g., 'Part001 [Silo]')

This implements Issue #16: Document origin tracking and display
2026-02-05 14:53:45 -06:00
db85277f26 feat(gui): add OriginManagerDialog for managing file origins (#15)
Some checks failed
Build and Test / build (push) Has been cancelled
- Create OriginManagerDialog with list of configured origins
- Show connection status for remote origins
- Allow setting default origin
- Add/Edit/Remove buttons (Add/Edit show placeholder for now)
- Wire up 'Manage Origins...' button in OriginSelectorWidget
- Prevent removal of built-in local origin

Part of Issue #15: Multi-instance Silo configuration UI
2026-02-05 14:51:30 -06:00
679aaec6d4 feat(gui): add unified origin commands for PLM operations (#14)
Some checks are pending
Build and Test / build (push) Has started running
- Create CommandOrigin.cpp with Origin_Commit, Origin_Pull, Origin_Push,
  Origin_Info, Origin_BOM commands
- Commands delegate to current origin's extended operations
- Commands auto-disable based on origin capabilities (supportsRevisions,
  supportsBOM, supportsPartNumbers)
- Add 'Origin Tools' toolbar with PLM operations
- Add origin commands to File menu
- Register commands via CreateOriginCommands() in Application startup

This implements Issue #14: Dynamic toolbar extension for Silo commands
2026-02-05 14:49:22 -06:00
deeb6376f7 feat(gui): add OriginSelectorWidget for file origin selection (#13)
Some checks failed
Build and Test / build (push) Has been cancelled
- Create OriginSelectorWidget class (QToolButton with dropdown menu)
- Add OriginSelectorAction to create widget in toolbars
- Add Std_Origin command registered in CommandStd.cpp
- Add widget to File toolbar (before New/Open/Save)
- Connect to OriginManager fastsignals for origin changes
- Add Catppuccin Mocha styling for the widget
- Widget shows current origin name/icon with connection status overlay

This implements Issue #13: Origin selector toolbar widget
2026-02-05 14:47:18 -06:00
103fc28bc6 ci: retrigger after pushing silo submodule
Some checks failed
Build and Test / build (push) Has been cancelled
2026-02-05 14:35:59 -06:00
79c85ed2e5 fix(gui): add interactive methods to FileOriginPython and fix Console API calls
Some checks failed
Build and Test / build (push) Failing after 2m13s
- Add openDocumentInteractive() and saveDocumentAsInteractive() to FileOriginPython
- Fix Console API: Warning -> warning, Error -> error in OriginManager.cpp
- These methods bridge Python origins to the new interactive document operations
2026-02-05 14:25:21 -06:00
38358e431d feat(gui): implement issues #10 and #12 - LocalFileOrigin and Std_* delegation
Issue #10: Local filesystem origin implementation
- Add openDocumentInteractive() method to FileOrigin interface for UI-based
  file opening (shows file dialog)
- Add saveDocumentAsInteractive() method for UI-based save as
- Implement LocalFileOrigin::openDocumentInteractive() with full file dialog
  support, filter list building, and module handler integration
- Implement LocalFileOrigin::saveDocumentAsInteractive() delegating to
  Gui::Document::saveAs()

Issue #12: Modify Std_* commands to delegate to current origin
- StdCmdNew::activated() now delegates to origin->newDocument() and sets
  up view orientation for the new document
- StdCmdOpen::activated() delegates to origin->openDocumentInteractive()
  with connection state checking for authenticated origins
- StdCmdSave::activated() uses document's owning origin (via findOwningOrigin)
  for save, falling back to saveDocumentAsInteractive if no filename
- StdCmdSaveAs::activated() delegates to origin->saveDocumentAsInteractive()
- Updated isActive() methods to check for active document

The Std_* commands now work seamlessly with both LocalFileOrigin and
SiloOrigin. When Local origin is selected, standard file dialogs appear.
When Silo origin is selected, Silo's search/creation dialogs appear.

Import and Export commands are left unchanged as they operate on document
content rather than document lifecycle.

Closes #10, Closes #12
2026-02-05 14:02:26 -06:00
31 changed files with 2022 additions and 162 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -1022,6 +1022,7 @@ void Application::createStandardOperations()
Gui::CreateStructureCommands();
Gui::CreateTestCommands();
Gui::CreateLinkCommands();
Gui::CreateOriginCommands();
}
void Application::slotNewDocument(const App::Document& Doc, bool isMainDoc)

View File

@@ -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}

View File

@@ -247,6 +247,7 @@ void CreateWindowStdCommands();
void CreateStructureCommands();
void CreateTestCommands();
void CreateLinkCommands();
void CreateOriginCommands();
/** The CommandBase class

View File

@@ -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,27 @@ 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
Gui::Document* guiDoc = Application::Instance->getDocument(doc);
if (guiDoc) {
auto views = guiDoc->getMDIViewsOfType(View3DInventor::getClassTypeId());
for (auto* view : views) {
auto view3d = static_cast<View3DInventor*>(view);
view3d->getViewer()->viewDefaultOrientation();
}
}
ParameterGrp::handle hViewGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/View"
@@ -749,12 +712,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 +762,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
View 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

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -111,14 +111,14 @@ 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;
}
@@ -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;
}
@@ -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();

View File

@@ -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

View 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

View 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

View 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 = BitmapFactory::mergePixmap(
baseIcon, overlay, BitmapFactoryInst::BottomRight);
}
}
break;
case ConnectionState::Error:
// Overlay error indicator
{
QPixmap overlay = BitmapFactory().pixmapFromSvg(
"Warning", QSizeF(8, 8));
if (!overlay.isNull()) {
baseIcon = BitmapFactory::mergePixmap(
baseIcon, overlay, BitmapFactoryInst::BottomRight);
}
}
break;
}
}
return baseIcon;
}
} // namespace Gui

View 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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);