phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)

Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
This commit is contained in:
forbes
2026-02-13 14:03:58 -06:00
parent 5d81f8ac16
commit 87a0af0b0f
1566 changed files with 32071 additions and 6155 deletions

View File

@@ -0,0 +1,195 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* 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 "BreadcrumbToolBar.h"
#include "EditingContext.h"
#include <QColor>
#include <QLabel>
#include <QToolButton>
#include "Application.h"
#include "Document.h"
using namespace Gui;
// ---------------------------------------------------------------------------
// Stylesheet template for breadcrumb chip buttons
// ---------------------------------------------------------------------------
static QString chipStyle(const QString& bgHex)
{
// Darken background slightly for contrast, use white text
QColor bg(bgHex);
QColor textColor(QStringLiteral("#cdd6f4")); // Catppuccin Text
return QStringLiteral(
"QToolButton {"
" background-color: %1;"
" color: %2;"
" border: none;"
" border-radius: 4px;"
" padding: 2px 8px;"
" font-weight: bold;"
" font-size: 11px;"
"}"
"QToolButton:hover {"
" background-color: %3;"
"}"
"QToolButton:pressed {"
" background-color: %4;"
"}"
)
.arg(bg.name(), textColor.name(), bg.lighter(120).name(), bg.darker(120).name());
}
static QString separatorStyle()
{
return QStringLiteral(
"QLabel {"
" color: #6c7086;" // Catppuccin Overlay0
" font-size: 11px;"
" padding: 0 2px;"
"}"
);
}
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
BreadcrumbToolBar::BreadcrumbToolBar(QWidget* parent)
: QToolBar(parent)
{
setObjectName(QStringLiteral("BreadcrumbToolBar"));
setWindowTitle(tr("Editing Context"));
setMovable(false);
setFloatable(false);
setIconSize(QSize(16, 16));
// Thin toolbar with minimal margins
setStyleSheet(QStringLiteral(
"QToolBar {"
" background: #1e1e2e;" // Catppuccin Base
" border: none;"
" spacing: 2px;"
" padding: 2px 4px;"
" max-height: 24px;"
"}"
));
}
// ---------------------------------------------------------------------------
// Update display from context
// ---------------------------------------------------------------------------
void BreadcrumbToolBar::updateContext(const EditingContext& ctx)
{
clearSegments();
buildSegments(ctx);
}
// ---------------------------------------------------------------------------
// Build breadcrumb segments
// ---------------------------------------------------------------------------
void BreadcrumbToolBar::clearSegments()
{
for (auto* btn : _segments) {
removeAction(btn->defaultAction());
delete btn;
}
_segments.clear();
for (auto* sep : _separators) {
delete sep;
}
_separators.clear();
clear();
}
void BreadcrumbToolBar::buildSegments(const EditingContext& ctx)
{
if (ctx.breadcrumb.isEmpty()) {
return;
}
for (int i = 0; i < ctx.breadcrumb.size(); ++i) {
// Add separator between segments
if (i > 0) {
auto* sep = new QLabel(QStringLiteral("\u203A")); // character
sep->setStyleSheet(separatorStyle());
addWidget(sep);
_separators.append(sep);
}
auto* btn = new QToolButton(this);
btn->setText(ctx.breadcrumb[i]);
btn->setAutoRaise(true);
// Apply color from breadcrumb colors
QString color;
if (i < ctx.breadcrumbColors.size()) {
color = ctx.breadcrumbColors[i];
}
else {
color = ctx.color;
}
btn->setStyleSheet(chipStyle(color));
// Make clickable: clicking a parent segment navigates up
int segmentIndex = i;
connect(btn, &QToolButton::clicked, this, [this, segmentIndex]() {
segmentClicked(segmentIndex);
});
addWidget(btn);
_segments.append(btn);
}
}
// ---------------------------------------------------------------------------
// Navigation: clicking a breadcrumb segment
// ---------------------------------------------------------------------------
void BreadcrumbToolBar::segmentClicked(int index)
{
// Clicking the last segment does nothing (already there)
if (index >= _segments.size() - 1) {
return;
}
// Clicking a parent segment exits the current edit mode
auto* guiDoc = Application::Instance->activeDocument();
if (guiDoc && guiDoc->getInEdit()) {
guiDoc->resetEdit();
}
}
#include "moc_BreadcrumbToolBar.cpp"

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* 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_BREADCRUMBTOOLBAR_H
#define GUI_BREADCRUMBTOOLBAR_H
#include <QToolBar>
#include <QList>
#include <FCGlobal.h>
class QToolButton;
class QLabel;
namespace Gui
{
struct EditingContext;
/**
* A thin toolbar that displays the current editing context as color-coded
* breadcrumb segments. Each segment is a clickable QToolButton; clicking
* a parent segment navigates up (exits the current edit, etc.).
*/
class GuiExport BreadcrumbToolBar: public QToolBar
{
Q_OBJECT
public:
explicit BreadcrumbToolBar(QWidget* parent = nullptr);
public Q_SLOTS:
void updateContext(const EditingContext& ctx);
private Q_SLOTS:
void segmentClicked(int index);
private:
void clearSegments();
void buildSegments(const EditingContext& ctx);
QList<QToolButton*> _segments;
QList<QLabel*> _separators;
};
} // namespace Gui
#endif // GUI_BREADCRUMBTOOLBAR_H

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

614
src/Gui/EditingContext.cpp Normal file
View File

@@ -0,0 +1,614 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* 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 "EditingContext.h"
#include <algorithm>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <Base/Type.h>
#include "Application.h"
#include "Document.h"
#include "MDIView.h"
#include "ToolBarManager.h"
#include "ViewProvider.h"
#include "ViewProviderDocumentObject.h"
using namespace Gui;
// Catppuccin Mocha palette
namespace CatppuccinMocha
{
constexpr const char* Surface0 = "#313244";
constexpr const char* Surface1 = "#45475a";
constexpr const char* Mauve = "#cba6f7";
constexpr const char* Green = "#a6e3a1";
constexpr const char* Blue = "#89b4fa";
constexpr const char* Yellow = "#f9e2af";
constexpr const char* Teal = "#94e2d5";
constexpr const char* Red = "#f38ba8";
constexpr const char* Peach = "#fab387";
constexpr const char* Text = "#cdd6f4";
} // namespace CatppuccinMocha
// ---------------------------------------------------------------------------
// Private data
// ---------------------------------------------------------------------------
struct EditingContextResolver::Private
{
QList<ContextDefinition> contexts; // sorted by descending priority
QList<OverlayDefinition> overlays;
EditingContext current;
// Additional commands injected into context toolbars
// Key: contextId -> toolbarName -> list of command names
QMap<QString, QMap<QString, QStringList>> injections;
void sortContexts()
{
std::sort(
contexts.begin(),
contexts.end(),
[](const ContextDefinition& a, const ContextDefinition& b) {
return a.priority > b.priority;
}
);
}
};
// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------
EditingContextResolver* EditingContextResolver::_instance = nullptr;
EditingContextResolver* EditingContextResolver::instance()
{
if (!_instance) {
_instance = new EditingContextResolver();
}
return _instance;
}
void EditingContextResolver::destruct()
{
delete _instance;
_instance = nullptr;
}
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
EditingContextResolver::EditingContextResolver()
: QObject(nullptr)
, d(new Private)
{
registerBuiltinContexts();
// Connect to application signals
auto& app = *Application::Instance;
app.signalInEdit.connect([this](const ViewProviderDocumentObject& vp) { onInEdit(vp); });
app.signalResetEdit.connect([this](const ViewProviderDocumentObject& vp) { onResetEdit(vp); });
app.signalActiveDocument.connect([this](const Document& doc) { onActiveDocument(doc); });
app.signalActivateView.connect([this](const MDIView* view) { onActivateView(view); });
app.signalActivateWorkbench.connect([this](const char*) { refresh(); });
}
EditingContextResolver::~EditingContextResolver()
{
delete d;
}
// ---------------------------------------------------------------------------
// Helper: check if an App::DocumentObject's type name matches (by string)
// ---------------------------------------------------------------------------
static bool objectIsDerivedFrom(App::DocumentObject* obj, const char* typeName)
{
if (!obj) {
return false;
}
Base::Type target = Base::Type::fromName(typeName);
if (target.isBad()) {
return false;
}
return obj->getTypeId().isDerivedFrom(target);
}
static bool vpObjectIsDerivedFrom(ViewProvider* vp, const char* typeName)
{
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (!vpd) {
return false;
}
return objectIsDerivedFrom(vpd->getObject(), typeName);
}
// ---------------------------------------------------------------------------
// Helper: get the active "part" object from the active view
// ---------------------------------------------------------------------------
static App::DocumentObject* getActivePartObject()
{
auto* guiDoc = Application::Instance->activeDocument();
if (!guiDoc) {
return nullptr;
}
auto* view = guiDoc->getActiveView();
if (!view) {
return nullptr;
}
return view->getActiveObject<App::DocumentObject*>("part");
}
// ---------------------------------------------------------------------------
// Helper: get the label of the active "part" object
// ---------------------------------------------------------------------------
static QString getActivePartLabel()
{
auto* obj = getActivePartObject();
if (!obj) {
return {};
}
return QString::fromUtf8(obj->Label.getValue());
}
// ---------------------------------------------------------------------------
// Helper: get label of the object currently in edit
// ---------------------------------------------------------------------------
static QString getInEditLabel()
{
auto* guiDoc = Application::Instance->activeDocument();
if (!guiDoc) {
return {};
}
auto* vp = guiDoc->getInEdit();
if (!vp) {
return {};
}
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (!vpd || !vpd->getObject()) {
return {};
}
return QString::fromUtf8(vpd->getObject()->Label.getValue());
}
// ---------------------------------------------------------------------------
// Built-in context registrations
// ---------------------------------------------------------------------------
void EditingContextResolver::registerBuiltinContexts()
{
// --- Sketcher edit (highest priority — VP in edit) ---
registerContext({
/*.id =*/QStringLiteral("sketcher.edit"),
/*.labelTemplate =*/QStringLiteral("Sketcher: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Green),
/*.toolbars =*/
{QStringLiteral("Edit Mode"),
QStringLiteral("Geometries"),
QStringLiteral("Constraints"),
QStringLiteral("Sketcher Tools"),
QStringLiteral("B-Spline Tools"),
QStringLiteral("Visual Helpers")},
/*.priority =*/90,
/*.match =*/
[]() {
auto* doc = Application::Instance->activeDocument();
if (!doc) {
return false;
}
auto* vp = doc->getInEdit();
return vp && vpObjectIsDerivedFrom(vp, "Sketcher::SketchObject");
},
});
// --- Assembly edit (VP in edit) ---
registerContext({
/*.id =*/QStringLiteral("assembly.edit"),
/*.labelTemplate =*/QStringLiteral("Assembly: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Blue),
/*.toolbars =*/
{QStringLiteral("Assembly"),
QStringLiteral("Assembly Joints"),
QStringLiteral("Assembly Management")},
/*.priority =*/90,
/*.match =*/
[]() {
auto* doc = Application::Instance->activeDocument();
if (!doc) {
return false;
}
auto* vp = doc->getInEdit();
return vp && vpObjectIsDerivedFrom(vp, "Assembly::AssemblyObject");
},
});
// --- PartDesign with features (active body has children) ---
registerContext({
/*.id =*/QStringLiteral("partdesign.feature"),
/*.labelTemplate =*/QStringLiteral("Body: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Mauve),
/*.toolbars =*/
{QStringLiteral("Part Design Helper Features"),
QStringLiteral("Part Design Modeling Features"),
QStringLiteral("Part Design Dress-Up Features"),
QStringLiteral("Part Design Transformation Features")},
/*.priority =*/40,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
if (!obj || !objectIsDerivedFrom(obj, "PartDesign::Body")) {
return false;
}
// Body has at least one child beyond the origin
auto children = obj->getOutList();
for (auto* child : children) {
if (child && !objectIsDerivedFrom(child, "App::Origin")) {
return true;
}
}
return false;
},
});
// --- PartDesign body (active body, empty or origin-only) ---
registerContext({
/*.id =*/QStringLiteral("partdesign.body"),
/*.labelTemplate =*/QStringLiteral("Body: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Mauve),
/*.toolbars =*/
{QStringLiteral("Part Design Helper Features"), QStringLiteral("Sketcher")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
return obj && objectIsDerivedFrom(obj, "PartDesign::Body");
},
});
// --- Assembly idle (assembly exists, active, but not in edit) ---
registerContext({
/*.id =*/QStringLiteral("assembly.idle"),
/*.labelTemplate =*/QStringLiteral("Assembly: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Blue),
/*.toolbars =*/
{QStringLiteral("Assembly")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
return obj && objectIsDerivedFrom(obj, "Assembly::AssemblyObject");
},
});
// --- Spreadsheet ---
registerContext({
/*.id =*/QStringLiteral("spreadsheet"),
/*.labelTemplate =*/QStringLiteral("Spreadsheet: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Yellow),
/*.toolbars =*/
{QStringLiteral("Spreadsheet")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
return obj && objectIsDerivedFrom(obj, "Spreadsheet::Sheet");
},
});
// --- Empty document ---
registerContext({
/*.id =*/QStringLiteral("empty_document"),
/*.labelTemplate =*/QStringLiteral("New Document"),
/*.color =*/QLatin1String(CatppuccinMocha::Surface1),
/*.toolbars =*/
{QStringLiteral("Structure")},
/*.priority =*/10,
/*.match =*/
[]() { return Application::Instance->activeDocument() != nullptr; },
});
// --- No document ---
registerContext({
/*.id =*/QStringLiteral("no_document"),
/*.labelTemplate =*/QStringLiteral("Kindred Create"),
/*.color =*/QLatin1String(CatppuccinMocha::Surface0),
/*.toolbars =*/ {},
/*.priority =*/0,
/*.match =*/[]() { return true; },
});
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
void EditingContextResolver::registerContext(const ContextDefinition& def)
{
// Remove any existing context with the same id
unregisterContext(def.id);
d->contexts.append(def);
d->sortContexts();
}
void EditingContextResolver::unregisterContext(const QString& id)
{
d->contexts.erase(
std::remove_if(
d->contexts.begin(),
d->contexts.end(),
[&](const ContextDefinition& c) { return c.id == id; }
),
d->contexts.end()
);
}
void EditingContextResolver::registerOverlay(const OverlayDefinition& def)
{
unregisterOverlay(def.id);
d->overlays.append(def);
}
void EditingContextResolver::unregisterOverlay(const QString& id)
{
d->overlays.erase(
std::remove_if(
d->overlays.begin(),
d->overlays.end(),
[&](const OverlayDefinition& o) { return o.id == id; }
),
d->overlays.end()
);
}
void EditingContextResolver::injectCommands(
const QString& contextId,
const QString& toolbarName,
const QStringList& commands
)
{
d->injections[contextId][toolbarName].append(commands);
}
// ---------------------------------------------------------------------------
// Resolution
// ---------------------------------------------------------------------------
EditingContext EditingContextResolver::resolve() const
{
EditingContext ctx;
// Find the first matching primary context
for (const auto& def : d->contexts) {
if (def.match && def.match()) {
ctx.id = def.id;
ctx.color = def.color;
ctx.toolbars = def.toolbars;
// Expand label template
QString label = def.labelTemplate;
if (label.contains(QStringLiteral("{name}"))) {
// For edit-mode contexts, use the in-edit object name
QString name = getInEditLabel();
if (name.isEmpty()) {
name = getActivePartLabel();
}
if (name.isEmpty()) {
name = QStringLiteral("?");
}
label.replace(QStringLiteral("{name}"), name);
}
ctx.label = label;
break;
}
}
// Append overlay toolbars
for (const auto& overlay : d->overlays) {
if (overlay.match && overlay.match()) {
for (const auto& tb : overlay.toolbars) {
if (!ctx.toolbars.contains(tb)) {
ctx.toolbars.append(tb);
}
}
}
}
// Append any injected toolbar names that aren't already in the context
auto injIt = d->injections.find(ctx.id);
if (injIt != d->injections.end()) {
for (auto it = injIt->begin(); it != injIt->end(); ++it) {
if (!ctx.toolbars.contains(it.key())) {
ctx.toolbars.append(it.key());
}
}
}
// Build breadcrumb
ctx.breadcrumb = buildBreadcrumb(ctx);
ctx.breadcrumbColors = buildBreadcrumbColors(ctx);
return ctx;
}
EditingContext EditingContextResolver::currentContext() const
{
return d->current;
}
// ---------------------------------------------------------------------------
// Breadcrumb building
// ---------------------------------------------------------------------------
QStringList EditingContextResolver::buildBreadcrumb(const EditingContext& ctx) const
{
QStringList crumbs;
if (ctx.id == QStringLiteral("no_document") || ctx.id == QStringLiteral("empty_document")) {
crumbs << ctx.label;
return crumbs;
}
// Always start with the active part/body/assembly label
QString partLabel = getActivePartLabel();
if (!partLabel.isEmpty()) {
crumbs << partLabel;
}
// If in edit mode, add the edited object
auto* guiDoc = Application::Instance->activeDocument();
if (guiDoc) {
auto* vp = guiDoc->getInEdit();
if (vp) {
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (vpd && vpd->getObject()) {
QString editLabel = QString::fromUtf8(vpd->getObject()->Label.getValue());
// Don't duplicate if the part label IS the edited object
if (editLabel != partLabel) {
crumbs << editLabel;
}
crumbs << QStringLiteral("[editing]");
}
}
}
if (crumbs.isEmpty()) {
crumbs << ctx.label;
}
return crumbs;
}
QStringList EditingContextResolver::buildBreadcrumbColors(const EditingContext& ctx) const
{
QStringList colors;
if (ctx.breadcrumb.size() <= 1) {
colors << ctx.color;
return colors;
}
// For multi-segment breadcrumbs:
// - First segments (parent) use the parent context color
// - Last segments (active edit) use the current context color
// e.g., Body (mauve) > Sketch001 (green) > [editing] (green)
// Determine parent color
QString parentColor = QLatin1String(CatppuccinMocha::Mauve); // default for Body
auto* partObj = getActivePartObject();
if (partObj) {
if (objectIsDerivedFrom(partObj, "Assembly::AssemblyObject")) {
parentColor = QLatin1String(CatppuccinMocha::Blue);
}
}
for (int i = 0; i < ctx.breadcrumb.size(); ++i) {
if (i == 0 && ctx.breadcrumb.size() > 1) {
colors << parentColor;
}
else {
colors << ctx.color;
}
}
return colors;
}
// ---------------------------------------------------------------------------
// Apply context → toolbar state changes
// ---------------------------------------------------------------------------
void EditingContextResolver::applyContext(const EditingContext& ctx)
{
if (ctx.id == d->current.id && ctx.toolbars == d->current.toolbars) {
return; // No change
}
auto* tbm = ToolBarManager::getInstance();
if (!tbm) {
return;
}
// Hide previously active context toolbars
if (!d->current.toolbars.isEmpty()) {
tbm->setState(d->current.toolbars, ToolBarManager::State::RestoreDefault);
}
// Show new context toolbars
if (!ctx.toolbars.isEmpty()) {
tbm->setState(ctx.toolbars, ToolBarManager::State::ForceAvailable);
}
d->current = ctx;
Q_EMIT contextChanged(ctx);
}
// ---------------------------------------------------------------------------
// Signal handlers
// ---------------------------------------------------------------------------
void EditingContextResolver::onInEdit(const ViewProviderDocumentObject& /*vp*/)
{
refresh();
}
void EditingContextResolver::onResetEdit(const ViewProviderDocumentObject& /*vp*/)
{
refresh();
}
void EditingContextResolver::onActiveDocument(const Document& /*doc*/)
{
refresh();
}
void EditingContextResolver::onActivateView(const MDIView* /*view*/)
{
refresh();
}
void EditingContextResolver::refresh()
{
applyContext(resolve());
}
#include "moc_EditingContext.cpp"

134
src/Gui/EditingContext.h Normal file
View File

@@ -0,0 +1,134 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* 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_EDITINGCONTEXT_H
#define GUI_EDITINGCONTEXT_H
#include <functional>
#include <QObject>
#include <QString>
#include <QStringList>
#include <FCGlobal.h>
namespace Gui
{
class Document;
class MDIView;
class ViewProviderDocumentObject;
/// Snapshot of the resolved editing context.
struct GuiExport EditingContext
{
QString id; // e.g. "sketcher.edit", "partdesign.body"
QString label; // e.g. "Sketcher: Sketch001"
QString color; // Catppuccin Mocha hex, e.g. "#a6e3a1"
QStringList toolbars; // toolbar names to ForceAvailable
QStringList breadcrumb; // e.g. ["Body", "Sketch001", "[editing]"]
QStringList breadcrumbColors; // per-segment color hex values
};
/// Definition used when registering a context.
struct GuiExport ContextDefinition
{
QString id;
QString labelTemplate; // supports {name} placeholder
QString color;
QStringList toolbars;
int priority = 0; // higher = checked first during resolve
/// Return true if this context matches the current application state.
std::function<bool()> match;
};
/// Definition for an overlay context (additive, not exclusive).
struct GuiExport OverlayDefinition
{
QString id;
QStringList toolbars;
/// Return true if the overlay should be active.
std::function<bool()> match;
};
/**
* Singleton that resolves the current editing context from application state
* and drives toolbar visibility and the breadcrumb display.
*
* Built-in contexts (PartDesign, Sketcher, Assembly, Spreadsheet) are registered
* at construction. Third-party modules register via registerContext() /
* registerOverlay() from Python or C++.
*/
class GuiExport EditingContextResolver: public QObject
{
Q_OBJECT
public:
static EditingContextResolver* instance();
static void destruct();
void registerContext(const ContextDefinition& def);
void unregisterContext(const QString& id);
void registerOverlay(const OverlayDefinition& def);
void unregisterOverlay(const QString& id);
/// Inject additional commands into a context's toolbar.
void injectCommands(const QString& contextId, const QString& toolbarName, const QStringList& commands);
/// Force a re-resolve (e.g. after workbench switch).
void refresh();
EditingContext currentContext() const;
Q_SIGNALS:
void contextChanged(const EditingContext& ctx);
private:
EditingContextResolver();
~EditingContextResolver() override;
EditingContext resolve() const;
void applyContext(const EditingContext& ctx);
QStringList buildBreadcrumb(const EditingContext& ctx) const;
QStringList buildBreadcrumbColors(const EditingContext& ctx) const;
// Signal handlers
void onInEdit(const ViewProviderDocumentObject& vp);
void onResetEdit(const ViewProviderDocumentObject& vp);
void onActiveDocument(const Document& doc);
void onActivateView(const MDIView* view);
void registerBuiltinContexts();
struct Private;
Private* d;
static EditingContextResolver* _instance;
};
} // namespace Gui
#endif // GUI_EDITINGCONTEXT_H

231
src/Gui/FileOrigin.cpp Normal file
View File

@@ -0,0 +1,231 @@
/***************************************************************************
* 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 <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 "FileDialog.h"
#include "MainWindow.h"
namespace Gui {
// Property name used by PLM origins (Silo) to mark tracked documents
static const char* SILO_ITEM_ID_PROP = "SiloItemId";
//===========================================================================
// LocalFileOrigin
//===========================================================================
LocalFileOrigin::LocalFileOrigin()
{
}
QIcon LocalFileOrigin::icon() const
{
return BitmapFactory().iconFromTheme("document-new");
}
std::string LocalFileOrigin::documentIdentity(App::Document* doc) const
{
if (!doc || !ownsDocument(doc)) {
return {};
}
return doc->FileName.getValue();
}
std::string LocalFileOrigin::documentDisplayId(App::Document* doc) const
{
// For local files, identity and display ID are the same (file path)
return documentIdentity(doc);
}
bool LocalFileOrigin::ownsDocument(App::Document* doc) const
{
if (!doc) {
return false;
}
// Local origin owns documents that do NOT have PLM tracking properties.
// Check all objects for SiloItemId property - if any have it,
// this document is owned by a PLM origin, not local.
for (auto* obj : doc->getObjects()) {
if (obj->getPropertyByName(SILO_ITEM_ID_PROP)) {
return false;
}
}
return true;
}
App::Document* LocalFileOrigin::newDocument(const std::string& name)
{
std::string docName = name.empty() ? "Unnamed" : name;
return App::GetApplication().newDocument(docName.c_str(), docName.c_str());
}
App::Document* LocalFileOrigin::openDocument(const std::string& identity)
{
if (identity.empty()) {
return nullptr;
}
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) {
return false;
}
// If document has never been saved, we need a path
const char* fileName = doc->FileName.getValue();
if (!fileName || fileName[0] == '\0') {
// No file name set - would need UI interaction for Save As
// This will be handled by the command layer
return false;
}
return doc->save();
}
bool LocalFileOrigin::saveDocumentAs(App::Document* doc, const std::string& newIdentity)
{
if (!doc || newIdentity.empty()) {
return false;
}
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

276
src/Gui/FileOrigin.h Normal file
View File

@@ -0,0 +1,276 @@
/***************************************************************************
* 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_FILEORIGIN_H
#define GUI_FILEORIGIN_H
#include <string>
#include <QIcon>
#include <FCGlobal.h>
#include <fastsignals/signal.h>
namespace App {
class Document;
}
namespace Gui {
/**
* @brief Classification of origin types
*/
enum class OriginType {
Local, ///< Local filesystem storage
PLM, ///< Product Lifecycle Management system (e.g., Silo)
Cloud, ///< Generic cloud storage
Custom ///< User-defined origin type
};
/**
* @brief Connection state for origins that require network access
*/
enum class ConnectionState {
Disconnected, ///< Not connected
Connecting, ///< Connection in progress
Connected, ///< Successfully connected
Error ///< Connection error occurred
};
/**
* @brief Abstract base class for document origin handlers
*
* FileOrigin provides an interface for different storage backends
* that can handle FreeCAD documents. Each origin defines workflows
* for creating, opening, saving, and managing documents.
*
* Key insight: Origins don't change where files are stored - all documents
* are always saved locally. Origins change the workflow and identity model:
* - Local: Document identity = file path, no external tracking
* - PLM: Document identity = database UUID, syncs with external system
*/
class GuiExport FileOrigin
{
public:
virtual ~FileOrigin() = default;
///@name Identity Methods
//@{
/** Unique identifier for this origin instance */
virtual std::string id() const = 0;
/** Display name for UI */
virtual std::string name() const = 0;
/** Short nickname for compact UI elements (e.g., toolbar) */
virtual std::string nickname() const = 0;
/** Icon for UI representation */
virtual QIcon icon() const = 0;
/** Origin type classification */
virtual OriginType type() const = 0;
//@}
///@name Workflow Characteristics
//@{
/** Whether this origin tracks documents externally (e.g., in a database) */
virtual bool tracksExternally() const = 0;
/** Whether this origin requires user authentication */
virtual bool requiresAuthentication() const = 0;
//@}
///@name Capability Queries
//@{
/** Whether this origin supports revision history */
virtual bool supportsRevisions() const { return false; }
/** Whether this origin supports Bill of Materials */
virtual bool supportsBOM() const { return false; }
/** Whether this origin supports part numbers */
virtual bool supportsPartNumbers() const { return false; }
/** Whether this origin supports assemblies natively */
virtual bool supportsAssemblies() const { return false; }
//@}
///@name Connection State
//@{
/** Get current connection state */
virtual ConnectionState connectionState() const { return ConnectionState::Connected; }
/** Attempt to connect/authenticate */
virtual bool connect() { return true; }
/** Disconnect from origin */
virtual void disconnect() {}
/** Signal emitted when connection state changes */
fastsignals::signal<void(ConnectionState)> signalConnectionStateChanged;
//@}
///@name Document Identity
//@{
/**
* Get document identity string (path for local, UUID for PLM)
* This is the immutable tracking key for the document.
* @param doc The App document to get identity for
* @return Identity string or empty if not owned by this origin
*/
virtual std::string documentIdentity(App::Document* doc) const = 0;
/**
* Get human-readable document identity (path for local, part number for PLM)
* This is for display purposes in the UI.
* @param doc The App document
* @return Display identity string or empty if not owned
*/
virtual std::string documentDisplayId(App::Document* doc) const = 0;
/**
* Check if this origin owns the given document.
* Ownership is determined by document properties, not file path.
* @param doc The document to check
* @return true if this origin owns the document
*/
virtual bool ownsDocument(App::Document* doc) const = 0;
//@}
///@name Property Synchronization
//@{
/**
* Sync document properties to the origin backend.
* For local origin this is a no-op. For PLM origins this pushes
* property changes to the database.
* @param doc The document to sync
* @return true if sync succeeded
*/
virtual bool syncProperties(App::Document* doc) { (void)doc; return true; }
//@}
///@name Core Document Operations
//@{
/**
* Create a new document managed by this origin.
* Local: Creates empty document
* PLM: Shows part creation form
* @param name Optional document name
* @return The created document or nullptr on failure
*/
virtual App::Document* newDocument(const std::string& name = "") = 0;
/**
* 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)
* @return The opened document or nullptr on failure
*/
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 (if path known)
* PLM: Saves to disk and syncs with external system
* @param doc The document to save
* @return true if save succeeded
*/
virtual bool saveDocument(App::Document* doc) = 0;
/**
* 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)
//@{
/** Commit document changes to external system */
virtual bool commitDocument(App::Document* doc) { (void)doc; return false; }
/** Pull latest changes from external system */
virtual bool pullDocument(App::Document* doc) { (void)doc; return false; }
/** Push local changes to external system */
virtual bool pushDocument(App::Document* doc) { (void)doc; return false; }
/** Show document info dialog */
virtual void showInfo(App::Document* doc) { (void)doc; }
/** Show Bill of Materials dialog */
virtual void showBOM(App::Document* doc) { (void)doc; }
//@}
protected:
FileOrigin() = default;
// Non-copyable
FileOrigin(const FileOrigin&) = delete;
FileOrigin& operator=(const FileOrigin&) = delete;
};
/**
* @brief Local filesystem origin - default origin for local files
*
* This is the default origin that handles documents stored on the
* local filesystem without any external tracking or synchronization.
*/
class GuiExport LocalFileOrigin : public FileOrigin
{
public:
LocalFileOrigin();
~LocalFileOrigin() override = default;
// Identity
std::string id() const override { return "local"; }
std::string name() const override { return "Local Files"; }
std::string nickname() const override { return "Local"; }
QIcon icon() const override;
OriginType type() const override { return OriginType::Local; }
// Characteristics
bool tracksExternally() const override { return false; }
bool requiresAuthentication() const override { return false; }
// Document identity
std::string documentIdentity(App::Document* doc) const override;
std::string documentDisplayId(App::Document* doc) const override;
bool ownsDocument(App::Document* doc) const override;
// 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
#endif // GUI_FILEORIGIN_H

View File

@@ -0,0 +1,624 @@
/***************************************************************************
* 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 <App/Application.h>
#include <App/Document.h>
#include <App/DocumentPy.h>
#include <Base/Console.h>
#include <Base/Interpreter.h>
#include <Base/PyObjectBase.h>
#include "FileOriginPython.h"
#include "OriginManager.h"
#include "BitmapFactory.h"
namespace Gui {
std::vector<FileOriginPython*> FileOriginPython::_instances;
void FileOriginPython::addOrigin(const Py::Object& obj)
{
// Check if already registered
if (findOrigin(obj)) {
Base::Console().warning("FileOriginPython: Origin already registered\n");
return;
}
auto* origin = new FileOriginPython(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");
delete origin;
return;
}
_instances.push_back(origin);
// Register with OriginManager
if (!OriginManager::instance()->registerOrigin(origin)) {
// Registration failed - remove from our instances list
// (registerOrigin already deleted the origin)
_instances.pop_back();
}
}
void FileOriginPython::removeOrigin(const Py::Object& obj)
{
FileOriginPython* origin = findOrigin(obj);
if (!origin) {
return;
}
std::string originId = origin->_cachedId;
// Remove from instances list
auto it = std::find(_instances.begin(), _instances.end(), origin);
if (it != _instances.end()) {
_instances.erase(it);
}
// Unregister from OriginManager (this will delete the origin)
OriginManager::instance()->unregisterOrigin(originId);
}
FileOriginPython* FileOriginPython::findOrigin(const Py::Object& obj)
{
for (auto* instance : _instances) {
if (instance->_inst == obj) {
return instance;
}
}
return nullptr;
}
FileOriginPython::FileOriginPython(const Py::Object& obj)
: _inst(obj)
{
}
FileOriginPython::~FileOriginPython() = default;
Py::Object FileOriginPython::callMethod(const char* method) const
{
return callMethod(method, Py::Tuple());
}
Py::Object FileOriginPython::callMethod(const char* method, const Py::Tuple& args) const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr(method)) {
Py::Callable func(_inst.getAttr(method));
return func.apply(args);
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return Py::None();
}
bool FileOriginPython::callBoolMethod(const char* method, bool defaultValue) const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr(method)) {
Py::Callable func(_inst.getAttr(method));
Py::Object result = func.apply(Py::Tuple());
if (result.isBoolean()) {
return Py::Boolean(result);
}
if (result.isNumeric()) {
return Py::Long(result) != 0;
}
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return defaultValue;
}
std::string FileOriginPython::callStringMethod(const char* method, const std::string& defaultValue) const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr(method)) {
Py::Callable func(_inst.getAttr(method));
Py::Object result = func.apply(Py::Tuple());
if (result.isString()) {
return Py::String(result).as_std_string();
}
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return defaultValue;
}
Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
{
if (!doc) {
return Py::None();
}
return Py::asObject(doc->getPyObject());
}
// Identity methods
std::string FileOriginPython::id() const
{
return _cachedId.empty() ? callStringMethod("id") : _cachedId;
}
std::string FileOriginPython::name() const
{
return callStringMethod("name", id());
}
std::string FileOriginPython::nickname() const
{
return callStringMethod("nickname", name());
}
QIcon FileOriginPython::icon() const
{
std::string iconName = callStringMethod("icon", "document-new");
return BitmapFactory().iconFromTheme(iconName.c_str());
}
OriginType FileOriginPython::type() const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("type")) {
Py::Callable func(_inst.getAttr("type"));
Py::Object result = func.apply(Py::Tuple());
if (result.isNumeric()) {
int t = static_cast<int>(Py::Long(result));
if (t >= 0 && t <= static_cast<int>(OriginType::Custom)) {
return static_cast<OriginType>(t);
}
}
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return OriginType::Custom;
}
// Workflow characteristics
bool FileOriginPython::tracksExternally() const
{
return callBoolMethod("tracksExternally", false);
}
bool FileOriginPython::requiresAuthentication() const
{
return callBoolMethod("requiresAuthentication", false);
}
// Capability queries
bool FileOriginPython::supportsRevisions() const
{
return callBoolMethod("supportsRevisions", false);
}
bool FileOriginPython::supportsBOM() const
{
return callBoolMethod("supportsBOM", false);
}
bool FileOriginPython::supportsPartNumbers() const
{
return callBoolMethod("supportsPartNumbers", false);
}
bool FileOriginPython::supportsAssemblies() const
{
return callBoolMethod("supportsAssemblies", false);
}
// Connection state
ConnectionState FileOriginPython::connectionState() const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("connectionState")) {
Py::Callable func(_inst.getAttr("connectionState"));
Py::Object result = func.apply(Py::Tuple());
if (result.isNumeric()) {
int s = static_cast<int>(Py::Long(result));
if (s >= 0 && s <= static_cast<int>(ConnectionState::Error)) {
return static_cast<ConnectionState>(s);
}
}
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return ConnectionState::Connected;
}
bool FileOriginPython::connect()
{
return callBoolMethod("connect", true);
}
void FileOriginPython::disconnect()
{
callMethod("disconnect");
}
// Document identity
std::string FileOriginPython::documentIdentity(App::Document* doc) const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("documentIdentity")) {
Py::Callable func(_inst.getAttr("documentIdentity"));
Py::Tuple args(1);
args.setItem(0, getDocPyObject(doc));
Py::Object result = func.apply(args);
if (result.isString()) {
return Py::String(result).as_std_string();
}
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return {};
}
std::string FileOriginPython::documentDisplayId(App::Document* doc) const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("documentDisplayId")) {
Py::Callable func(_inst.getAttr("documentDisplayId"));
Py::Tuple args(1);
args.setItem(0, getDocPyObject(doc));
Py::Object result = func.apply(args);
if (result.isString()) {
return Py::String(result).as_std_string();
}
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
return documentIdentity(doc);
}
bool FileOriginPython::ownsDocument(App::Document* doc) const
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("ownsDocument")) {
Py::Callable func(_inst.getAttr("ownsDocument"));
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;
}
bool FileOriginPython::syncProperties(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("syncProperties")) {
Py::Callable func(_inst.getAttr("syncProperties"));
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 true;
}
// Core document operations
App::Document* FileOriginPython::newDocument(const std::string& name)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("newDocument")) {
Py::Callable func(_inst.getAttr("newDocument"));
Py::Tuple args(1);
args.setItem(0, Py::String(name));
Py::Object result = func.apply(args);
if (!result.isNone()) {
// Extract App::Document* from Python object
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;
}
App::Document* FileOriginPython::openDocument(const std::string& identity)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("openDocument")) {
Py::Callable func(_inst.getAttr("openDocument"));
Py::Tuple args(1);
args.setItem(0, Py::String(identity));
Py::Object result = func.apply(args);
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::saveDocument(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("saveDocument")) {
Py::Callable func(_inst.getAttr("saveDocument"));
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;
}
bool FileOriginPython::saveDocumentAs(App::Document* doc, const std::string& newIdentity)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("saveDocumentAs")) {
Py::Callable func(_inst.getAttr("saveDocumentAs"));
Py::Tuple args(2);
args.setItem(0, getDocPyObject(doc));
args.setItem(1, Py::String(newIdentity));
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;
}
// Extended operations
bool FileOriginPython::commitDocument(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("commitDocument")) {
Py::Callable func(_inst.getAttr("commitDocument"));
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;
}
bool FileOriginPython::pullDocument(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("pullDocument")) {
Py::Callable func(_inst.getAttr("pullDocument"));
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;
}
bool FileOriginPython::pushDocument(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("pushDocument")) {
Py::Callable func(_inst.getAttr("pushDocument"));
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;
}
void FileOriginPython::showInfo(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("showInfo")) {
Py::Callable func(_inst.getAttr("showInfo"));
Py::Tuple args(1);
args.setItem(0, getDocPyObject(doc));
func.apply(args);
}
}
catch (Py::Exception&) {
Base::PyException e;
e.reportException();
}
}
void FileOriginPython::showBOM(App::Document* doc)
{
Base::PyGILStateLocker lock;
try {
if (_inst.hasAttr("showBOM")) {
Py::Callable func(_inst.getAttr("showBOM"));
Py::Tuple args(1);
args.setItem(0, getDocPyObject(doc));
func.apply(args);
}
}
catch (Py::Exception&) {
Base::PyException e;
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

148
src/Gui/FileOriginPython.h Normal file
View File

@@ -0,0 +1,148 @@
/***************************************************************************
* 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_FILEORIGINPYTHON_H
#define GUI_FILEORIGINPYTHON_H
#include <vector>
#include <FCGlobal.h>
#include <CXX/Objects.hxx>
#include "FileOrigin.h"
namespace Gui {
/**
* @brief Wrapper that adapts a Python object to the FileOrigin interface
*
* This allows Python addons (like Silo) to implement origins in Python
* while integrating with the C++ OriginManager.
*
* The Python object should implement the following methods:
* - id() -> str
* - name() -> str
* - nickname() -> str
* - icon() -> str (icon name for BitmapFactory)
* - type() -> int (OriginType enum value)
* - tracksExternally() -> bool
* - requiresAuthentication() -> bool
* - ownsDocument(doc) -> bool
* - documentIdentity(doc) -> str
* - documentDisplayId(doc) -> str
*
* Optional methods:
* - supportsRevisions() -> bool
* - supportsBOM() -> bool
* - supportsPartNumbers() -> bool
* - supportsAssemblies() -> bool
* - connectionState() -> int
* - connect() -> bool
* - disconnect() -> None
* - syncProperties(doc) -> bool
* - newDocument(name) -> Document
* - openDocument(identity) -> Document
* - saveDocument(doc) -> bool
* - saveDocumentAs(doc, newIdentity) -> bool
* - commitDocument(doc) -> bool
* - pullDocument(doc) -> bool
* - pushDocument(doc) -> bool
* - showInfo(doc) -> None
* - showBOM(doc) -> None
*/
class GuiExport FileOriginPython : public FileOrigin
{
public:
/**
* Register a Python object as an origin.
* The Python object should implement the FileOrigin interface methods.
* @param obj The Python object implementing the origin interface
*/
static void addOrigin(const Py::Object& obj);
/**
* Unregister a Python origin by its Python object.
* @param obj The Python object to unregister
*/
static void removeOrigin(const Py::Object& obj);
/**
* Find a registered Python origin by its Python object.
* @param obj The Python object to find
* @return The FileOriginPython wrapper or nullptr
*/
static FileOriginPython* findOrigin(const Py::Object& obj);
// FileOrigin interface - delegates to Python
std::string id() const override;
std::string name() const override;
std::string nickname() const override;
QIcon icon() const override;
OriginType type() const override;
bool tracksExternally() const override;
bool requiresAuthentication() const override;
bool supportsRevisions() const override;
bool supportsBOM() const override;
bool supportsPartNumbers() const override;
bool supportsAssemblies() const override;
ConnectionState connectionState() const override;
bool connect() override;
void disconnect() override;
std::string documentIdentity(App::Document* doc) const override;
std::string documentDisplayId(App::Document* doc) const override;
bool ownsDocument(App::Document* doc) const override;
bool syncProperties(App::Document* doc) override;
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;
bool pushDocument(App::Document* doc) override;
void showInfo(App::Document* doc) override;
void showBOM(App::Document* doc) override;
private:
explicit FileOriginPython(const Py::Object& obj);
~FileOriginPython() override;
// Helper to call Python methods safely
Py::Object callMethod(const char* method) const;
Py::Object callMethod(const char* method, const Py::Tuple& args) const;
bool callBoolMethod(const char* method, bool defaultValue = false) const;
std::string callStringMethod(const char* method, const std::string& defaultValue = "") const;
Py::Object getDocPyObject(App::Document* doc) const;
Py::Object _inst;
std::string _cachedId; // Cache the ID since it's used for registration
static std::vector<FileOriginPython*> _instances;
};
} // namespace Gui
#endif // GUI_FILEORIGINPYTHON_H

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1028"
height="1028"
viewBox="0 0 271.99167 271.99167"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (2aeb623e1d, 2025-05-12)"
sodipodi:docname="kindred-logo.svg"
inkscape:export-filename="../3290ed6b/kindred-logo-blue-baack.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
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">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="1.036062"
inkscape:cx="397.6596"
inkscape:cy="478.25323"
inkscape:window-width="2494"
inkscape:window-height="1371"
inkscape:window-x="1146"
inkscape:window-y="1112"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:export-bgcolor="#79c0c500">
<inkscape:grid
type="axonomgrid"
id="grid6"
units="mm"
originx="0"
originy="0"
spacingx="0.99999998"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 123.49166 : 1"
inkscape:vp_y="0 : 999.99998 : 0"
inkscape:vp_z="210.00001 : 123.49166 : 1"
inkscape:persp3d-origin="105 : 73.991665 : 1"
id="perspective1" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#7c4a82;fill-opacity:1;stroke:#12101c;stroke-width:5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path6-81-5"
inkscape:flatsided="true"
sodipodi:sides="6"
sodipodi:cx="61.574867"
sodipodi:cy="103.99491"
sodipodi:r1="25.000006"
sodipodi:r2="22.404818"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-1.0471974"
inkscape:rounded="0.77946499"
inkscape:randomized="0"
d="m 61.574868,78.994905 c 19.486629,10e-7 11.907325,-4.375912 21.65064,12.500004 9.743314,16.875911 9.743314,8.12409 -1e-6,25.000001 -9.743315,16.87592 -2.164011,12.50001 -21.65064,12.50001 -19.486629,0 -11.907326,4.37591 -21.65064,-12.50001 -9.743314,-16.875912 -9.743314,-8.12409 0,-25.000002 9.743315,-16.875916 2.164012,-12.500003 21.650641,-12.500003 z"
transform="matrix(1.9704344,0,0,1.8525167,-28.510585,-40.025402)" />
<path
sodipodi:type="star"
style="fill:#ff9701;fill-opacity:1;stroke:#12101c;stroke-width:5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path6-81-5-6"
inkscape:flatsided="true"
sodipodi:sides="6"
sodipodi:cx="61.574867"
sodipodi:cy="103.99491"
sodipodi:r1="25.000006"
sodipodi:r2="22.404818"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-1.0471974"
inkscape:rounded="0.77946499"
inkscape:randomized="0"
d="m 61.574868,78.994905 c 19.486629,10e-7 11.907325,-4.375912 21.65064,12.500004 9.743314,16.875921 9.743314,8.12409 -1e-6,25.000001 -9.743315,16.87592 -2.164011,12.50001 -21.65064,12.50001 -19.48663,0 -11.907326,4.37591 -21.65064,-12.50001 -9.743314,-16.875913 -9.743315,-8.12409 10e-7,-25.000002 9.743315,-16.875916 2.164011,-12.500003 21.65064,-12.500003 z"
transform="matrix(1.9704344,0,0,1.8525167,56.811738,-86.338327)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View 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

View 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

View 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

View 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

View 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

296
src/Gui/OriginManager.cpp Normal file
View File

@@ -0,0 +1,296 @@
/***************************************************************************
* 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 <App/Application.h>
#include <Base/Console.h>
#include "OriginManager.h"
#include "FileOrigin.h"
namespace Gui {
// Preferences path for origin settings
static const char* PREF_PATH = "User parameter:BaseApp/Preferences/General/Origin";
static const char* PREF_CURRENT_ORIGIN = "CurrentOriginId";
// Built-in origin ID that cannot be unregistered
static const char* LOCAL_ORIGIN_ID = "local";
OriginManager* OriginManager::_instance = nullptr;
OriginManager* OriginManager::instance()
{
if (!_instance) {
_instance = new OriginManager();
}
return _instance;
}
void OriginManager::destruct()
{
delete _instance;
_instance = nullptr;
}
OriginManager::OriginManager()
{
ensureLocalOrigin();
loadPreferences();
}
OriginManager::~OriginManager()
{
savePreferences();
_origins.clear();
}
void OriginManager::ensureLocalOrigin()
{
// Create the built-in local filesystem origin
auto localOrigin = std::make_unique<LocalFileOrigin>();
_origins[LOCAL_ORIGIN_ID] = std::move(localOrigin);
_currentOriginId = LOCAL_ORIGIN_ID;
}
void OriginManager::loadPreferences()
{
try {
auto hGrp = App::GetApplication().GetParameterGroupByPath(PREF_PATH);
std::string savedOriginId = hGrp->GetASCII(PREF_CURRENT_ORIGIN, LOCAL_ORIGIN_ID);
// Only use saved origin if it's registered
if (_origins.find(savedOriginId) != _origins.end()) {
_currentOriginId = savedOriginId;
}
}
catch (...) {
// Ignore preference loading errors
_currentOriginId = LOCAL_ORIGIN_ID;
}
}
void OriginManager::savePreferences()
{
try {
auto hGrp = App::GetApplication().GetParameterGroupByPath(PREF_PATH);
hGrp->SetASCII(PREF_CURRENT_ORIGIN, _currentOriginId.c_str());
}
catch (...) {
// Ignore preference saving errors
}
}
bool OriginManager::registerOrigin(FileOrigin* origin)
{
if (!origin) {
return false;
}
std::string originId = origin->id();
if (originId.empty()) {
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());
delete origin;
return false;
}
_origins[originId] = std::unique_ptr<FileOrigin>(origin);
Base::Console().log("OriginManager: Registered origin '%s'\n", originId.c_str());
signalOriginRegistered(originId);
return true;
}
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");
return false;
}
auto it = _origins.find(id);
if (it == _origins.end()) {
return false;
}
// If unregistering the current origin, switch to local
if (_currentOriginId == id) {
_currentOriginId = LOCAL_ORIGIN_ID;
signalCurrentOriginChanged(_currentOriginId);
}
_origins.erase(it);
Base::Console().log("OriginManager: Unregistered origin '%s'\n", id.c_str());
signalOriginUnregistered(id);
return true;
}
std::vector<std::string> OriginManager::originIds() const
{
std::vector<std::string> ids;
ids.reserve(_origins.size());
for (const auto& pair : _origins) {
ids.push_back(pair.first);
}
return ids;
}
FileOrigin* OriginManager::getOrigin(const std::string& id) const
{
auto it = _origins.find(id);
if (it != _origins.end()) {
return it->second.get();
}
return nullptr;
}
FileOrigin* OriginManager::currentOrigin() const
{
auto it = _origins.find(_currentOriginId);
if (it != _origins.end()) {
return it->second.get();
}
// Fallback to local (should never happen)
return _origins.at(LOCAL_ORIGIN_ID).get();
}
std::string OriginManager::currentOriginId() const
{
return _currentOriginId;
}
bool OriginManager::setCurrentOrigin(const std::string& id)
{
if (_origins.find(id) == _origins.end()) {
return false;
}
if (_currentOriginId != id) {
_currentOriginId = id;
savePreferences();
signalCurrentOriginChanged(_currentOriginId);
}
return true;
}
FileOrigin* OriginManager::findOwningOrigin(App::Document* doc) const
{
if (!doc) {
return nullptr;
}
// Check each origin to see if it owns this document
// Start with non-local origins since they have specific ownership criteria
for (const auto& pair : _origins) {
if (pair.first == LOCAL_ORIGIN_ID) {
continue; // Check local last as fallback
}
if (pair.second->ownsDocument(doc)) {
return pair.second.get();
}
}
// If no PLM origin claims it, check local
auto localIt = _origins.find(LOCAL_ORIGIN_ID);
if (localIt != _origins.end() && localIt->second->ownsDocument(doc)) {
return localIt->second.get();
}
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();
}
} // namespace Gui

184
src/Gui/OriginManager.h Normal file
View File

@@ -0,0 +1,184 @@
/***************************************************************************
* 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_ORIGINMANAGER_H
#define GUI_ORIGINMANAGER_H
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <FCGlobal.h>
#include <fastsignals/signal.h>
namespace App {
class Document;
}
namespace Gui {
class FileOrigin;
/**
* @brief Singleton manager for document origins
*
* OriginManager tracks all registered FileOrigin instances and maintains
* the current origin selection. It provides lookup methods and signals
* for UI updates.
*
* The manager always has at least one origin: the local filesystem origin,
* which is created automatically on initialization.
*/
class GuiExport OriginManager
{
public:
/** Get the singleton instance */
static OriginManager* instance();
/** Destroy the singleton instance */
static void destruct();
///@name Origin Registration
//@{
/**
* Register an origin with the manager.
* The manager takes ownership of the origin.
* @param origin The origin to register
* @return true if successfully registered (ID not already in use)
*/
bool registerOrigin(FileOrigin* origin);
/**
* Unregister and delete an origin.
* Cannot unregister the built-in "local" origin.
* @param id The origin ID to unregister
* @return true if successfully unregistered
*/
bool unregisterOrigin(const std::string& id);
/**
* Get all registered origin IDs.
* @return Vector of origin ID strings
*/
std::vector<std::string> originIds() const;
/**
* Get origin by ID.
* @param id The origin ID
* @return The origin or nullptr if not found
*/
FileOrigin* getOrigin(const std::string& id) const;
//@}
///@name Current Origin Selection
//@{
/**
* Get the currently selected origin.
* @return The current origin (never nullptr)
*/
FileOrigin* currentOrigin() const;
/**
* Get the current origin ID.
* @return The current origin's ID
*/
std::string currentOriginId() const;
/**
* Set the current origin by ID.
* @param id The origin ID to select
* @return true if origin was found and selected
*/
bool setCurrentOrigin(const std::string& id);
//@}
///@name Document Origin Resolution
//@{
/**
* Find which origin owns a document.
* Iterates through all origins to find one that claims ownership
* based on document properties.
* @param doc The document to check
* @return The owning origin or nullptr if unowned
*/
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.
* @return The origin to use for new documents
*/
FileOrigin* originForNewDocument() const;
//@}
///@name Signals
//@{
/** Emitted when an origin is registered */
fastsignals::signal<void(const std::string&)> signalOriginRegistered;
/** Emitted when an origin is unregistered */
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:
OriginManager();
~OriginManager();
private:
void loadPreferences();
void savePreferences();
void ensureLocalOrigin();
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
#endif // GUI_ORIGINMANAGER_H

View File

@@ -0,0 +1,247 @@
/***************************************************************************
* 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"
#include "Application.h"
#include "Command.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()
{
Application::Instance->commandManager().runCommandByName("Silo_Settings");
}
void OriginManagerDialog::onEditOrigin()
{
FileOrigin* origin = selectedOrigin();
if (!origin || origin->id() == "local") {
return;
}
Application::Instance->commandManager().runCommandByName("Silo_Settings");
}
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,271 @@
/***************************************************************************
* 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(80);
setMaximumWidth(160);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
// 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

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

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<FCParameters>
<FCParamGroup Name="Root">
<FCParamGroup Name="BaseApp">
<FCParamGroup Name="Preferences">
<FCParamGroup Name="Editor">
<FCUInt Name="Text" Value="3453416703" />
<FCUInt Name="Bookmark" Value="3032415999" />
<FCUInt Name="Breakpoint" Value="4086016255" />
<FCUInt Name="Keyword" Value="3416717311" />
<FCUInt Name="Comment" Value="2139095295" />
<FCUInt Name="Block comment" Value="2139095295" />
<FCUInt Name="Number" Value="4206069759" />
<FCUInt Name="String" Value="2799935999" />
<FCUInt Name="Character" Value="4073902335" />
<FCUInt Name="Class name" Value="2310339327" />
<FCUInt Name="Define name" Value="2310339327" />
<FCUInt Name="Operator" Value="2312199935" />
<FCUInt Name="Python output" Value="2796290303" />
<FCUInt Name="Python error" Value="4086016255" />
<FCUInt Name="Current line highlight" Value="1162304255" />
</FCParamGroup>
<FCParamGroup Name="OutputWindow">
<FCUInt Name="colorText" Value="3453416703" />
<FCUInt Name="colorLogging" Value="2497893887" />
<FCUInt Name="colorWarning" Value="4192382975" />
<FCUInt Name="colorError" Value="4086016255" />
<FCBool Name="checkError" Value="1" />
<FCBool Name="checkLogging" Value="1" />
<FCBool Name="checkShowReportViewOnError" Value="1" />
<FCBool Name="checkShowReportViewOnWarning" Value="1" />
</FCParamGroup>
<FCParamGroup Name="View">
<FCUInt Name="BackgroundColor" Value="505294591" />
<FCUInt Name="BackgroundColor2" Value="286333951" />
<FCUInt Name="BackgroundColor3" Value="404235775" />
<FCUInt Name="BackgroundColor4" Value="825378047" />
<FCBool Name="Simple" Value="0" />
<FCBool Name="Gradient" Value="1" />
<FCBool Name="UseBackgroundColorMid" Value="0" />
<FCUInt Name="HighlightColor" Value="3416717311" />
<FCUInt Name="SelectionColor" Value="3032415999" />
<FCUInt Name="PreselectColor" Value="2497893887" />
<FCUInt Name="DefaultShapeColor" Value="1482387711" />
<FCBool Name="RandomColor" Value="0" />
<FCUInt Name="DefaultShapeLineColor" Value="2470768383" />
<FCUInt Name="DefaultShapeVertexColor" Value="2470768383" />
<FCUInt Name="BoundingBoxColor" Value="1819509759" />
<FCUInt Name="AnnotationTextColor" Value="3453416703" />
<FCUInt Name="SketchEdgeColor" Value="3453416703" />
<FCUInt Name="SketchVertexColor" Value="3453416703" />
<FCUInt Name="EditedEdgeColor" Value="3416717311" />
<FCUInt Name="EditedVertexColor" Value="4123402495" />
<FCUInt Name="ConstructionColor" Value="4206069759" />
<FCUInt Name="ExternalColor" Value="4192382975" />
<FCUInt Name="FullyConstrainedColor" Value="2799935999" />
<FCUInt Name="InternalAlignedGeoColor" Value="1959907071" />
<FCUInt Name="FullyConstraintElementColor" Value="2799935999" />
<FCUInt
Name="FullyConstraintConstructionElementColor"
Value="2497893887"
/>
<FCUInt
Name="FullyConstraintInternalAlignmentColor"
Value="2312199935"
/>
<FCUInt
Name="FullyConstraintConstructionPointColor"
Value="2799935999"
/>
<FCUInt Name="ConstrainedIcoColor" Value="2310339327" />
<FCUInt Name="NonDrivingConstrDimColor" Value="2139095295" />
<FCUInt Name="ConstrainedDimColor" Value="3416717311" />
<FCUInt Name="ExprBasedConstrDimColor" Value="4206069759" />
<FCUInt Name="DeactivatedConstrDimColor" Value="1819509759" />
<FCUInt Name="CursorTextColor" Value="3453416703" />
<FCUInt Name="CursorCrosshairColor" Value="3416717311" />
<FCUInt Name="CreateLineColor" Value="2799935999" />
<FCUInt Name="ShadowLightColor" Value="2470768128" />
<FCUInt Name="ShadowGroundColor" Value="286333952" />
<FCUInt Name="HiddenLineColor" Value="825378047" />
<FCUInt Name="HiddenLineFaceColor" Value="505294591" />
<FCUInt Name="HiddenLineBackground" Value="505294591" />
<FCBool Name="EnableBacklight" Value="1" />
<FCUInt Name="BacklightColor" Value="1162304255" />
<FCFloat Name="BacklightIntensity" Value="0.30" />
</FCParamGroup>
<FCParamGroup Name="Document">
<FCInt Name="MaxUndoSize" Value="50" />
<FCInt Name="AutoSaveTimeout" Value="5" />
<FCInt Name="CountBackupFiles" Value="3" />
<FCInt Name="prefLicenseType" Value="19" />
<FCText Name="prefLicenseUrl"></FCText>
</FCParamGroup>
<FCParamGroup Name="TreeView">
<FCUInt Name="TreeEditColor" Value="3416717311" />
<FCUInt Name="TreeActiveColor" Value="2799935999" />
<FCBool Name="PreSelection" Value="1" />
<FCBool Name="SyncView" Value="1" />
<FCBool Name="SyncSelection" Value="1" />
</FCParamGroup>
<FCParamGroup Name="NotificationArea">
<FCInt Name="MaxWidgetMessages" Value="100" />
<FCInt Name="MaxOpenNotifications" Value="3" />
<FCInt Name="NotificiationWidth" Value="400" />
<FCInt Name="NotificationTime" Value="10" />
<FCInt Name="MinimumOnScreenTime" Value="3" />
</FCParamGroup>
<FCParamGroup Name="General">
<FCText Name="AutoloadModule">PartDesignWorkbench</FCText>
</FCParamGroup>
<FCParamGroup Name="MainWindow">
<FCText Name="StyleSheet">KindredCreate.qss</FCText>
<FCText Name="Theme">KindredCreate</FCText>
</FCParamGroup>
<FCParamGroup Name="Mod">
<FCParamGroup Name="Start">
<FCUInt Name="BackgroundColor1" Value="404235775" />
<FCUInt Name="BackgroundTextColor" Value="3453416703" />
<FCUInt Name="PageColor" Value="505294591" />
<FCUInt Name="PageTextColor" Value="3453416703" />
<FCUInt Name="BoxColor" Value="825378047" />
<FCUInt Name="LinkColor" Value="2310339327" />
<FCUInt Name="BackgroundColor2" Value="286333951" />
</FCParamGroup>
<FCParamGroup Name="Part">
<FCUInt Name="VertexColor" Value="3032415999" />
<FCUInt Name="EdgeColor" Value="2310339327" />
</FCParamGroup>
<FCParamGroup Name="PartDesign">
<FCUInt Name="DefaultDatumColor" Value="3416717311" />
</FCParamGroup>
<FCParamGroup Name="Draft">
<FCUInt Name="snapcolor" Value="2799935999" />
</FCParamGroup>
<FCParamGroup Name="Sketcher">
<FCUInt Name="GridLineColor" Value="1162304255" />
</FCParamGroup>
</FCParamGroup>
</FCParamGroup>
</FCParamGroup>
</FCParamGroup>
</FCParameters>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path d="M 3,1 7,5 3,9" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -1,55 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1083.5663,336.59799 v 8 c 0,1.5 1,2.5 2.5,2.5 h 8"
id="path1536" />
<g
transform="translate(7.0000156,-5.4999999)"
id="layer1-4">
<path
id="path2474"
d="m 1075.0663,348.59798 3,3.50001 -3,3.49999"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.34901961;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.34901961" />
</g>
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- L-junction with closed arrow: └▸ -->
<line x1="10" y1="0" x2="10" y2="10" stroke="#585b70" stroke-width="1"/>
<line x1="10" y1="10" x2="14" y2="10" stroke="#585b70" stroke-width="1"/>
<path d="M 14,6 18,10 14,14" fill="none" stroke="#a6adc8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -1,47 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1083.5663,336.59799 v 8 c 0,1.5 1,2.5 2.5,2.5 h 8"
id="path1536" />
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- L-junction (last sibling): └ -->
<line x1="10" y1="0" x2="10" y2="10" stroke="#585b70" stroke-width="1"/>
<line x1="10" y1="10" x2="20" y2="10" stroke="#585b70" stroke-width="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -1,55 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1083.5663,336.59799 v 8 c 0,1.5 1,2.5 2.5,2.5 h 8"
id="path1536" />
<g
transform="rotate(90,1082.5663,353.098)"
id="layer1-4">
<path
id="path2474"
d="m 1075.0663,348.59798 3,3.50001 -3,3.49999"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.34901961;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.34901961" />
</g>
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- L-junction with open (down) arrow: └▾ -->
<line x1="10" y1="0" x2="10" y2="10" stroke="#585b70" stroke-width="1"/>
<line x1="10" y1="10" x2="14" y2="10" stroke="#585b70" stroke-width="1"/>
<path d="M 13,8 17,12 13,16" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -1,55 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1094.0663,347.09799 h -10"
id="path1730" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1083.5663,336.59799 v 20"
id="path1728" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.34901961;stroke:#000000;stroke-width:1.99999996;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.34901961"
d="m 1082.0663,343.09798 3,3.50001 -3,3.49999"
id="path2474" />
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- T-junction with closed arrow: ├▸ -->
<line x1="10" y1="0" x2="10" y2="20" stroke="#585b70" stroke-width="1"/>
<line x1="10" y1="10" x2="14" y2="10" stroke="#585b70" stroke-width="1"/>
<path d="M 14,6 18,10 14,14" fill="none" stroke="#a6adc8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -1,51 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1094.0663,347.09799 h -10"
id="path1730" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:0.99999998;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1083.5663,336.59799 v 20"
id="path1728" />
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- T-junction: ├ -->
<line x1="10" y1="0" x2="10" y2="20" stroke="#585b70" stroke-width="1"/>
<line x1="10" y1="10" x2="20" y2="10" stroke="#585b70" stroke-width="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -1,63 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<g
transform="rotate(90,1082.5663,353.098)"
id="layer1-4">
<path
id="path2474"
d="m 1075.0663,348.59798 3,3.50001 -3,3.49999"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.34901961;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.34901961" />
</g>
<g
transform="translate(1.5420313e-5)"
id="layer1-6">
<path
id="path1730"
d="m 1094.0663,347.09799 h -10"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706" />
<path
id="path1728"
d="m 1083.5663,336.59799 v 20"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706" />
</g>
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- T-junction with open (down) arrow: ├▾ -->
<line x1="10" y1="0" x2="10" y2="20" stroke="#585b70" stroke-width="1"/>
<line x1="10" y1="10" x2="14" y2="10" stroke="#585b70" stroke-width="1"/>
<path d="M 13,8 17,12 13,16" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path d="M 1,3 5,7 9,3" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -1,47 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000002"
id="svg2"
version="1.1"
viewBox="0 0 20.000002 20.000001"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Pablo Gil</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>SVG</rdf:li>
<rdf:li>template</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-1074.0663,-336.59799)">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:0.60093898;stroke:#000000;stroke-width:0.99999998;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.11764706"
d="m 1083.5663,336.59799 v 20"
id="path1728" />
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<!-- Vertical line through center: │ -->
<line x1="10" y1="0" x2="10" y2="20" stroke="#585b70" stroke-width="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 250 B

View File

@@ -0,0 +1,54 @@
# Kindred Create core module
# Handles auto-loading of ztools and Silo addons
# Generate version.py from template with Kindred Create version
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/version.py.in
${CMAKE_CURRENT_BINARY_DIR}/version.py
@ONLY
)
# Install Python init files
install(
FILES
Init.py
InitGui.py
update_checker.py
${CMAKE_CURRENT_BINARY_DIR}/version.py
DESTINATION
Mod/Create
)
# Install ztools addon
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/ztools/ztools
DESTINATION
mods/ztools
)
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/ztools/CatppuccinMocha
DESTINATION
mods/ztools
)
install(
FILES
${CMAKE_SOURCE_DIR}/mods/ztools/package.xml
DESTINATION
mods/ztools
)
# Install Silo addon
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/silo/freecad/
DESTINATION
mods/silo/freecad
)
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/silo/silo-client/
DESTINATION
mods/silo/silo-client
)

48
src/Mod/Create/Init.py Normal file
View File

@@ -0,0 +1,48 @@
# Kindred Create - Core Module
# Console initialization - loads ztools and Silo addons
import os
import sys
import FreeCAD
def setup_kindred_addons():
"""Add Kindred Create addon paths and load their Init.py files."""
# Get the FreeCAD home directory (where src/Mod/Create is installed)
home = FreeCAD.getHomePath()
mods_dir = os.path.join(home, "mods")
# Define built-in addons with their paths relative to mods/
addons = [
("ztools", "ztools/ztools"), # mods/ztools/ztools/
("silo", "silo/freecad"), # mods/silo/freecad/
]
for name, subpath in addons:
addon_path = os.path.join(mods_dir, subpath)
if os.path.isdir(addon_path):
# Add to sys.path if not already present
if addon_path not in sys.path:
sys.path.insert(0, addon_path)
# Execute Init.py if it exists
init_file = os.path.join(addon_path, "Init.py")
if os.path.isfile(init_file):
try:
with open(init_file) as f:
exec_globals = globals().copy()
exec_globals["__file__"] = init_file
exec_globals["__name__"] = name
exec(compile(f.read(), init_file, "exec"), exec_globals)
FreeCAD.Console.PrintLog(f"Create: Loaded {name} Init.py\n")
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Create: Failed to load {name}: {e}\n"
)
else:
FreeCAD.Console.PrintLog(f"Create: Addon path not found: {addon_path}\n")
setup_kindred_addons()
FreeCAD.Console.PrintLog("Create module initialized\n")

171
src/Mod/Create/InitGui.py Normal file
View File

@@ -0,0 +1,171 @@
# Kindred Create - Core Module
# GUI initialization - loads ztools and Silo workbenches
import os
import sys
import FreeCAD
import FreeCADGui
def setup_kindred_workbenches():
"""Load Kindred Create addon workbenches."""
home = FreeCAD.getHomePath()
mods_dir = os.path.join(home, "mods")
addons = [
("ztools", "ztools/ztools"),
("silo", "silo/freecad"),
]
for name, subpath in addons:
addon_path = os.path.join(mods_dir, subpath)
if os.path.isdir(addon_path):
# Ensure path is in sys.path
if addon_path not in sys.path:
sys.path.insert(0, addon_path)
# Execute InitGui.py if it exists
init_gui_file = os.path.join(addon_path, "InitGui.py")
if os.path.isfile(init_gui_file):
try:
with open(init_gui_file) as f:
exec_globals = globals().copy()
exec_globals["__file__"] = init_gui_file
exec_globals["__name__"] = name
exec(
compile(f.read(), init_gui_file, "exec"),
exec_globals,
)
FreeCAD.Console.PrintLog(f"Create: Loaded {name} workbench\n")
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Create: Failed to load {name} GUI: {e}\n"
)
setup_kindred_workbenches()
FreeCAD.Console.PrintLog("Create GUI module initialized\n")
# ---------------------------------------------------------------------------
# Silo integration enhancements
# ---------------------------------------------------------------------------
def _check_silo_first_start():
"""Show Silo settings dialog on first startup if not yet configured."""
try:
param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/KindredSilo")
if not param.GetBool("FirstStartChecked", False):
param.SetBool("FirstStartChecked", True)
if not param.GetString("ApiUrl", ""):
FreeCADGui.runCommand("Silo_Settings")
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo first-start check skipped: {e}\n")
def _register_silo_origin():
"""Register Silo as a file origin so the origin selector can offer it."""
try:
import silo_commands # noqa: F401 - registers Silo commands
import silo_origin
silo_origin.register_silo_origin()
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo origin registration skipped: {e}\n")
def _setup_silo_auth_panel():
"""Dock the Silo authentication panel in the right-hand side panel."""
try:
from PySide import QtCore, QtWidgets
mw = FreeCADGui.getMainWindow()
if mw is None:
return
# Don't create duplicate panels
if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseAuth"):
return
import silo_commands
auth = silo_commands.SiloAuthDockWidget()
panel = QtWidgets.QDockWidget("Database Auth", mw)
panel.setObjectName("SiloDatabaseAuth")
panel.setWidget(auth.widget)
# Keep the auth object alive so its QTimer isn't destroyed while running
panel._auth = auth
mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel)
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo auth panel skipped: {e}\n")
def _setup_silo_activity_panel():
"""Show a dock widget with recent Silo database activity."""
try:
from PySide import QtCore, QtWidgets
mw = FreeCADGui.getMainWindow()
if mw is None:
return
# Don't create duplicate panels
if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseActivity"):
return
panel = QtWidgets.QDockWidget("Database Activity", mw)
panel.setObjectName("SiloDatabaseActivity")
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
activity_list = QtWidgets.QListWidget()
layout.addWidget(activity_list)
try:
import silo_commands
items = silo_commands._client.list_items()
if isinstance(items, list):
for item in items[:20]:
pn = item.get("part_number", "")
desc = item.get("description", "")
updated = item.get("updated_at", "")
if updated:
updated = updated[:10]
activity_list.addItem(f"{pn} - {desc} - {updated}")
if activity_list.count() == 0:
activity_list.addItem("(No items in database)")
except Exception:
activity_list.addItem("(Unable to connect to Silo database)")
panel.setWidget(widget)
mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel)
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo activity panel skipped: {e}\n")
def _check_for_updates():
"""Check for application updates in the background."""
try:
from update_checker import _run_update_check
_run_update_check()
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Update check skipped: {e}\n")
# Defer enhancements until the GUI event loop is running
try:
from PySide.QtCore import QTimer
QTimer.singleShot(1500, _register_silo_origin)
QTimer.singleShot(2000, _setup_silo_auth_panel)
QTimer.singleShot(3000, _check_silo_first_start)
QTimer.singleShot(4000, _setup_silo_activity_panel)
QTimer.singleShot(10000, _check_for_updates)
except Exception:
pass

View File

@@ -0,0 +1,165 @@
"""Kindred Create update checker.
Queries the Gitea releases API to determine if a newer version is
available. Designed to run in the background on startup without
blocking the UI.
"""
import json
import re
import urllib.request
from datetime import datetime, timezone
import FreeCAD
_RELEASES_URL = "https://git.kindred-systems.com/api/v1/repos/kindred/create/releases"
_PREF_PATH = "User parameter:BaseApp/Preferences/Mod/KindredCreate/Update"
_TIMEOUT = 5
def _parse_version(tag):
"""Parse a version tag like 'v0.1.3' into a comparable tuple.
Returns None if the tag doesn't match the expected pattern.
"""
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)$", tag)
if not m:
return None
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
def check_for_update(current_version):
"""Check if a newer release is available on Gitea.
Args:
current_version: Version string like "0.1.3".
Returns:
Dict with update info if a newer version exists, None otherwise.
Dict keys: version, tag, release_url, assets, body.
"""
current = _parse_version(current_version)
if current is None:
return None
req = urllib.request.Request(
f"{_RELEASES_URL}?limit=10",
headers={"Accept": "application/json"},
)
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
releases = json.loads(resp.read())
best = None
best_version = current
for release in releases:
if release.get("draft"):
continue
if release.get("prerelease"):
continue
tag = release.get("tag_name", "")
# Skip the rolling 'latest' tag
if tag == "latest":
continue
ver = _parse_version(tag)
if ver is None:
continue
if ver > best_version:
best_version = ver
best = release
if best is None:
return None
assets = []
for asset in best.get("assets", []):
assets.append(
{
"name": asset.get("name", ""),
"url": asset.get("browser_download_url", ""),
"size": asset.get("size", 0),
}
)
return {
"version": ".".join(str(x) for x in best_version),
"tag": best["tag_name"],
"release_url": best.get("html_url", ""),
"assets": assets,
"body": best.get("body", ""),
}
def _should_check(param):
"""Determine whether an update check should run now.
Args:
param: FreeCAD parameter group for update preferences.
Returns:
True if a check should be performed.
"""
if not param.GetBool("CheckEnabled", True):
return False
last_check = param.GetString("LastCheckTimestamp", "")
if not last_check:
return True
interval_days = param.GetInt("CheckIntervalDays", 1)
if interval_days <= 0:
return True
try:
last_dt = datetime.fromisoformat(last_check)
now = datetime.now(timezone.utc)
elapsed = (now - last_dt).total_seconds()
return elapsed >= interval_days * 86400
except (ValueError, TypeError):
return True
def _run_update_check():
"""Entry point called from the deferred startup timer."""
param = FreeCAD.ParamGet(_PREF_PATH)
if not _should_check(param):
return
try:
from version import VERSION
except ImportError:
FreeCAD.Console.PrintLog(
"Create: update check skipped — version module not available\n"
)
return
try:
result = check_for_update(VERSION)
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: update check failed: {e}\n")
return
# Record that we checked
param.SetString(
"LastCheckTimestamp",
datetime.now(timezone.utc).isoformat(),
)
if result is None:
FreeCAD.Console.PrintLog("Create: application is up to date\n")
return
skipped = param.GetString("SkippedVersion", "")
if result["version"] == skipped:
FreeCAD.Console.PrintLog(
f"Create: update {result['version']} available but skipped by user\n"
)
return
FreeCAD.Console.PrintMessage(
f"Kindred Create {result['version']} is available (current: {VERSION})\n"
)

View File

@@ -0,0 +1 @@
VERSION = "@KINDRED_CREATE_VERSION@"