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
195
src/Gui/BreadcrumbToolBar.cpp
Normal 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"
|
||||
68
src/Gui/BreadcrumbToolBar.h
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
624
src/Gui/FileOriginPython.cpp
Normal 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
@@ -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
|
||||
106
src/Gui/Icons/kindred-create.svg
Normal 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 |
BIN
src/Gui/Icons/kindredcreateabout.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/Gui/Icons/kindredcreatesplash.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/Gui/Icons/kindredcreatesplash_2x.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
12
src/Gui/Icons/silo-bom.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Outer box -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" fill="#313244"/>
|
||||
<!-- List lines (BOM rows) -->
|
||||
<line x1="8" y1="8" x2="18" y2="8" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<line x1="8" y1="12" x2="18" y2="12" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<line x1="8" y1="16" x2="18" y2="16" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<!-- Hierarchy dots -->
|
||||
<circle cx="6" cy="8" r="1" fill="#cba6f7"/>
|
||||
<circle cx="6" cy="12" r="1" fill="#cba6f7"/>
|
||||
<circle cx="6" cy="16" r="1" fill="#cba6f7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
8
src/Gui/Icons/silo-commit.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Git commit style -->
|
||||
<circle cx="12" cy="12" r="4" fill="#313244" stroke="#a6e3a1"/>
|
||||
<line x1="12" y1="2" x2="12" y2="8" stroke="#cba6f7"/>
|
||||
<line x1="12" y1="16" x2="12" y2="22" stroke="#cba6f7"/>
|
||||
<!-- Checkmark inside -->
|
||||
<polyline points="9.5 12 11 13.5 14.5 10" stroke="#a6e3a1" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
6
src/Gui/Icons/silo-info.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Info circle -->
|
||||
<circle cx="12" cy="12" r="10" fill="#313244"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12" stroke="#89dceb" stroke-width="2"/>
|
||||
<circle cx="12" cy="8" r="0.5" fill="#89dceb" stroke="#89dceb"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
7
src/Gui/Icons/silo-pull.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Cloud -->
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||
<!-- Download arrow -->
|
||||
<path d="M12 13v5m0 0l-2-2m2 2l2-2" stroke="#89b4fa" stroke-width="2"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13" stroke="#89b4fa" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
7
src/Gui/Icons/silo-push.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Cloud -->
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||
<!-- Upload arrow -->
|
||||
<path d="M12 18v-5m0 0l-2 2m2-2l2 2" stroke="#a6e3a1" stroke-width="2"/>
|
||||
<line x1="12" y1="13" x2="12" y2="9" stroke="#a6e3a1" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
296
src/Gui/OriginManager.cpp
Normal 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
@@ -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
|
||||
247
src/Gui/OriginManagerDialog.cpp
Normal 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
|
||||
74
src/Gui/OriginManagerDialog.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_ORIGINMANAGERDIALOG_H
|
||||
#define GUI_ORIGINMANAGERDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QDialogButtonBox>
|
||||
#include <FCGlobal.h>
|
||||
|
||||
namespace Gui {
|
||||
|
||||
class FileOrigin;
|
||||
|
||||
/**
|
||||
* @brief Dialog for managing file origins
|
||||
*
|
||||
* This dialog allows users to view, add, edit, and remove file origins
|
||||
* (Silo instances). The local filesystem origin cannot be removed.
|
||||
*/
|
||||
class GuiExport OriginManagerDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OriginManagerDialog(QWidget* parent = nullptr);
|
||||
~OriginManagerDialog() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void onAddSilo();
|
||||
void onEditOrigin();
|
||||
void onRemoveOrigin();
|
||||
void onSetDefault();
|
||||
void onOriginSelectionChanged();
|
||||
void onOriginDoubleClicked(QListWidgetItem* item);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateOriginList();
|
||||
void updateButtonStates();
|
||||
FileOrigin* selectedOrigin() const;
|
||||
|
||||
QListWidget* m_originList;
|
||||
QPushButton* m_addButton;
|
||||
QPushButton* m_editButton;
|
||||
QPushButton* m_removeButton;
|
||||
QPushButton* m_defaultButton;
|
||||
QDialogButtonBox* m_buttonBox;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_ORIGINMANAGERDIALOG_H
|
||||
271
src/Gui/OriginSelectorWidget.cpp
Normal 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
|
||||
95
src/Gui/OriginSelectorWidget.h
Normal file
@@ -0,0 +1,95 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2025 Kindred Systems *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_ORIGINSELECTORWIDGET_H
|
||||
#define GUI_ORIGINSELECTORWIDGET_H
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QMenu>
|
||||
#include <QActionGroup>
|
||||
#include <fastsignals/signal.h>
|
||||
#include <FCGlobal.h>
|
||||
|
||||
namespace Gui {
|
||||
|
||||
class FileOrigin;
|
||||
|
||||
/**
|
||||
* @brief Toolbar widget for selecting the current file origin
|
||||
*
|
||||
* OriginSelectorWidget displays the currently selected origin and provides
|
||||
* a dropdown menu to switch between available origins (Local Files, Silo
|
||||
* instances, etc.).
|
||||
*
|
||||
* Visual design:
|
||||
* Collapsed (toolbar state):
|
||||
* ┌──────────────────┐
|
||||
* │ ☁️ Work ▼ │ ~70-100px wide
|
||||
* └──────────────────┘
|
||||
*
|
||||
* Expanded (dropdown open):
|
||||
* ┌──────────────────┐
|
||||
* │ ✓ ☁️ Work │ ← Current selection (checkmark)
|
||||
* │ ☁️ Prod │
|
||||
* │ 📁 Local │
|
||||
* ├──────────────────┤
|
||||
* │ ⚙️ Manage... │ ← Opens config dialog
|
||||
* └──────────────────┘
|
||||
*/
|
||||
class GuiExport OriginSelectorWidget : public QToolButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OriginSelectorWidget(QWidget* parent = nullptr);
|
||||
~OriginSelectorWidget() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void onOriginActionTriggered(QAction* action);
|
||||
void onManageOriginsClicked();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void connectSignals();
|
||||
void disconnectSignals();
|
||||
|
||||
void onOriginRegistered(const std::string& originId);
|
||||
void onOriginUnregistered(const std::string& originId);
|
||||
void onCurrentOriginChanged(const std::string& originId);
|
||||
|
||||
void updateDisplay();
|
||||
void rebuildMenu();
|
||||
QIcon iconForOrigin(FileOrigin* origin) const;
|
||||
|
||||
QMenu* m_menu;
|
||||
QActionGroup* m_originActions;
|
||||
QAction* m_manageAction;
|
||||
|
||||
// Signal connections
|
||||
fastsignals::scoped_connection m_connRegistered;
|
||||
fastsignals::scoped_connection m_connUnregistered;
|
||||
fastsignals::scoped_connection m_connChanged;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_ORIGINSELECTORWIDGET_H
|
||||
143
src/Gui/PreferencePacks/KindredCreate/KindredCreate.cfg
Normal 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>
|
||||
1367
src/Gui/Stylesheets/KindredCreate.qss
Normal file
4
src/Gui/Stylesheets/images_dark-light/branch_closed.svg
Normal 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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
4
src/Gui/Stylesheets/images_dark-light/branch_open.svg
Normal 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 |
@@ -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 |
54
src/Mod/Create/CMakeLists.txt
Normal 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
@@ -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
@@ -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
|
||||
165
src/Mod/Create/update_checker.py
Normal 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"
|
||||
)
|
||||
1
src/Mod/Create/version.py.in
Normal file
@@ -0,0 +1 @@
|
||||
VERSION = "@KINDRED_CREATE_VERSION@"
|
||||