diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index 5895397792..8000440ed6 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -54,6 +54,7 @@ #include "Workbench.h" #include "WorkbenchManager.h" #include "WorkbenchSelector.h" +#include "OriginSelectorWidget.h" #include "ShortcutManager.h" #include "Tools.h" @@ -1470,4 +1471,25 @@ void WindowAction::addTo(QWidget* widget) } } +// -------------------------------------------------------------------- + +OriginSelectorAction::OriginSelectorAction(Command* pcCmd, QObject* parent) + : Action(pcCmd, parent) +{} + +OriginSelectorAction::~OriginSelectorAction() = default; + +void OriginSelectorAction::addTo(QWidget* widget) +{ + if (widget->inherits("QToolBar")) { + auto* toolbar = static_cast(widget); + auto* selector = new OriginSelectorWidget(widget); + toolbar->addWidget(selector); + } + else { + // For menus, just add the action + widget->addAction(action()); + } +} + #include "moc_Action.cpp" diff --git a/src/Gui/Action.h b/src/Gui/Action.h index b52ab61076..01d02ed936 100644 --- a/src/Gui/Action.h +++ b/src/Gui/Action.h @@ -421,6 +421,25 @@ private: Q_DISABLE_COPY(WindowAction) }; +// -------------------------------------------------------------------- + +/** + * Action for origin selector widget in toolbars. + * Creates OriginSelectorWidget when added to a toolbar. + */ +class GuiExport OriginSelectorAction: public Action +{ + Q_OBJECT + +public: + explicit OriginSelectorAction(Command* pcCmd, QObject* parent = nullptr); + ~OriginSelectorAction() override; + void addTo(QWidget* widget) override; + +private: + Q_DISABLE_COPY(OriginSelectorAction) +}; + } // namespace Gui #endif // GUI_ACTION_H diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index da3115b7e8..c9519eed18 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1236,6 +1236,7 @@ SET(Widget_CPP_SRCS ElideCheckBox.cpp FontScaledSVG.cpp SplitButton.cpp + OriginSelectorWidget.cpp ) SET(Widget_HPP_SRCS ComboLinks.h @@ -1262,6 +1263,7 @@ SET(Widget_HPP_SRCS ElideCheckBox.h FontScaledSVG.h SplitButton.h + OriginSelectorWidget.h ) SET(Widget_SRCS ${Widget_CPP_SRCS} diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index 9f0d32bfb2..ec41692e5d 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -132,6 +132,45 @@ Action* StdCmdWorkbench::createAction() return pcAction; } +//=========================================================================== +// Std_Origin +//=========================================================================== + +DEF_STD_CMD_AC(StdCmdOrigin) + +StdCmdOrigin::StdCmdOrigin() + : Command("Std_Origin") +{ + sGroup = "File"; + sMenuText = QT_TR_NOOP("&Origin"); + sToolTipText = QT_TR_NOOP("Select file origin (Local Files, Silo, etc.)"); + sWhatsThis = "Std_Origin"; + sStatusTip = sToolTipText; + sPixmap = "folder"; + eType = 0; +} + +void StdCmdOrigin::activated(int /*iMsg*/) +{ + // Action is handled by OriginSelectorWidget +} + +bool StdCmdOrigin::isActive() +{ + return true; +} + +Action* StdCmdOrigin::createAction() +{ + Action* pcAction = new OriginSelectorAction(this, getMainWindow()); + pcAction->setShortcut(QString::fromLatin1(getAccel())); + applyCommandData(this->className(), pcAction); + if (getPixmap()) { + pcAction->setIcon(Gui::BitmapFactory().iconFromTheme(getPixmap())); + } + return pcAction; +} + //=========================================================================== // Std_RecentFiles //=========================================================================== @@ -1057,6 +1096,7 @@ void CreateStdCommands() rcCmdMgr.addCommand(new StdCmdDlgCustomize()); rcCmdMgr.addCommand(new StdCmdCommandLine()); rcCmdMgr.addCommand(new StdCmdWorkbench()); + rcCmdMgr.addCommand(new StdCmdOrigin()); rcCmdMgr.addCommand(new StdCmdRecentFiles()); rcCmdMgr.addCommand(new StdCmdRecentMacros()); rcCmdMgr.addCommand(new StdCmdWhatsThis()); diff --git a/src/Gui/OriginSelectorWidget.cpp b/src/Gui/OriginSelectorWidget.cpp new file mode 100644 index 0000000000..c8a90cb13e --- /dev/null +++ b/src/Gui/OriginSelectorWidget.cpp @@ -0,0 +1,265 @@ +/*************************************************************************** + * 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 + +#include "OriginSelectorWidget.h" +#include "OriginManager.h" +#include "FileOrigin.h" +#include "BitmapFactory.h" + + +namespace Gui { + +OriginSelectorWidget::OriginSelectorWidget(QWidget* parent) + : QToolButton(parent) + , m_menu(nullptr) + , m_originActions(nullptr) + , m_manageAction(nullptr) +{ + setupUi(); + connectSignals(); + rebuildMenu(); + updateDisplay(); +} + +OriginSelectorWidget::~OriginSelectorWidget() +{ + disconnectSignals(); +} + +void OriginSelectorWidget::setupUi() +{ + setPopupMode(QToolButton::InstantPopup); + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + setMinimumWidth(70); + setMaximumWidth(120); + + // Create menu + m_menu = new QMenu(this); + setMenu(m_menu); + + // Create action group for exclusive selection + m_originActions = new QActionGroup(this); + m_originActions->setExclusive(true); + + // Connect action group to selection handler + connect(m_originActions, &QActionGroup::triggered, + this, &OriginSelectorWidget::onOriginActionTriggered); +} + +void OriginSelectorWidget::connectSignals() +{ + auto* mgr = OriginManager::instance(); + + // Connect to OriginManager fastsignals + m_connRegistered = mgr->signalOriginRegistered.connect( + [this](const std::string& id) { onOriginRegistered(id); } + ); + m_connUnregistered = mgr->signalOriginUnregistered.connect( + [this](const std::string& id) { onOriginUnregistered(id); } + ); + m_connChanged = mgr->signalCurrentOriginChanged.connect( + [this](const std::string& id) { onCurrentOriginChanged(id); } + ); +} + +void OriginSelectorWidget::disconnectSignals() +{ + m_connRegistered.disconnect(); + m_connUnregistered.disconnect(); + m_connChanged.disconnect(); +} + +void OriginSelectorWidget::onOriginRegistered(const std::string& /*originId*/) +{ + // Rebuild menu to include new origin + rebuildMenu(); +} + +void OriginSelectorWidget::onOriginUnregistered(const std::string& /*originId*/) +{ + // Rebuild menu to remove origin + rebuildMenu(); +} + +void OriginSelectorWidget::onCurrentOriginChanged(const std::string& /*originId*/) +{ + // Update display and menu checkmarks + updateDisplay(); + + // Update checked state in menu + auto* mgr = OriginManager::instance(); + std::string currentId = mgr->currentOriginId(); + + for (QAction* action : m_originActions->actions()) { + std::string actionId = action->data().toString().toStdString(); + action->setChecked(actionId == currentId); + } +} + +void OriginSelectorWidget::onOriginActionTriggered(QAction* action) +{ + if (!action) { + return; + } + + std::string originId = action->data().toString().toStdString(); + auto* mgr = OriginManager::instance(); + + // Check if origin requires connection + FileOrigin* origin = mgr->getOrigin(originId); + if (origin && origin->requiresAuthentication()) { + ConnectionState state = origin->connectionState(); + if (state == ConnectionState::Disconnected || state == ConnectionState::Error) { + // Try to connect + if (!origin->connect()) { + // Connection failed - don't switch + // Revert the checkmark to current origin + std::string currentId = mgr->currentOriginId(); + for (QAction* a : m_originActions->actions()) { + a->setChecked(a->data().toString().toStdString() == currentId); + } + return; + } + } + } + + mgr->setCurrentOrigin(originId); +} + +void OriginSelectorWidget::onManageOriginsClicked() +{ + // TODO: Open origins management dialog (Issue #15) + // For now, this is a placeholder +} + +void OriginSelectorWidget::updateDisplay() +{ + FileOrigin* origin = OriginManager::instance()->currentOrigin(); + if (!origin) { + setText(tr("No Origin")); + setIcon(QIcon()); + setToolTip(QString()); + return; + } + + setText(QString::fromStdString(origin->nickname())); + setIcon(iconForOrigin(origin)); + setToolTip(QString::fromStdString(origin->name())); +} + +void OriginSelectorWidget::rebuildMenu() +{ + m_menu->clear(); + + // Remove old actions from action group + for (QAction* action : m_originActions->actions()) { + m_originActions->removeAction(action); + } + + auto* mgr = OriginManager::instance(); + std::string currentId = mgr->currentOriginId(); + + // Add origin entries + for (const std::string& originId : mgr->originIds()) { + FileOrigin* origin = mgr->getOrigin(originId); + if (!origin) { + continue; + } + + QAction* action = m_menu->addAction( + iconForOrigin(origin), + QString::fromStdString(origin->nickname()) + ); + action->setCheckable(true); + action->setChecked(originId == currentId); + action->setData(QString::fromStdString(originId)); + action->setToolTip(QString::fromStdString(origin->name())); + + m_originActions->addAction(action); + } + + // Add separator and manage action + m_menu->addSeparator(); + + m_manageAction = m_menu->addAction( + BitmapFactory().iconFromTheme("preferences-system"), + tr("Manage Origins...") + ); + connect(m_manageAction, &QAction::triggered, + this, &OriginSelectorWidget::onManageOriginsClicked); +} + +QIcon OriginSelectorWidget::iconForOrigin(FileOrigin* origin) const +{ + if (!origin) { + return QIcon(); + } + + QIcon baseIcon = origin->icon(); + + // For origins that require authentication, overlay connection status + if (origin->requiresAuthentication()) { + ConnectionState state = origin->connectionState(); + + switch (state) { + case ConnectionState::Connected: + // No overlay needed - use base icon + break; + + case ConnectionState::Connecting: + // TODO: Animated connecting indicator + break; + + case ConnectionState::Disconnected: + // Overlay disconnected indicator + { + QPixmap overlay = BitmapFactory().pixmapFromSvg( + "dagViewFail", QSizeF(8, 8)); + if (!overlay.isNull()) { + baseIcon = BitmapFactory::mergePixmap( + baseIcon, overlay, BitmapFactoryInst::BottomRight); + } + } + break; + + case ConnectionState::Error: + // Overlay error indicator + { + QPixmap overlay = BitmapFactory().pixmapFromSvg( + "Warning", QSizeF(8, 8)); + if (!overlay.isNull()) { + baseIcon = BitmapFactory::mergePixmap( + baseIcon, overlay, BitmapFactoryInst::BottomRight); + } + } + break; + } + } + + return baseIcon; +} + +} // namespace Gui diff --git a/src/Gui/OriginSelectorWidget.h b/src/Gui/OriginSelectorWidget.h new file mode 100644 index 0000000000..97e5dbb91b --- /dev/null +++ b/src/Gui/OriginSelectorWidget.h @@ -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 +#include +#include +#include +#include + +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 diff --git a/src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss b/src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss index e391a1102b..afbf474893 100644 --- a/src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss +++ b/src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss @@ -1142,6 +1142,28 @@ Gui--WorkbenchComboBox::drop-down { border-radius: 0 4px 4px 0; } +/* Origin Selector */ +Gui--OriginSelectorWidget { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px 8px; + min-width: 70px; + max-width: 120px; +} + +Gui--OriginSelectorWidget:hover { + border-color: #585b70; + background-color: #45475a; +} + +Gui--OriginSelectorWidget::menu-indicator { + subcontrol-origin: padding; + subcontrol-position: center right; + right: 4px; +} + /* Task Panel */ QSint--ActionGroup { background-color: #313244; diff --git a/src/Gui/Stylesheets/KindredCreate.qss b/src/Gui/Stylesheets/KindredCreate.qss index 2155ece5c5..2e3742e668 100644 --- a/src/Gui/Stylesheets/KindredCreate.qss +++ b/src/Gui/Stylesheets/KindredCreate.qss @@ -1163,6 +1163,28 @@ Gui--WorkbenchComboBox::drop-down { border-radius: 0 4px 4px 0; } +/* Origin Selector */ +Gui--OriginSelectorWidget { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px 8px; + min-width: 70px; + max-width: 120px; +} + +Gui--OriginSelectorWidget:hover { + border-color: #585b70; + background-color: #45475a; +} + +Gui--OriginSelectorWidget::menu-indicator { + subcontrol-origin: padding; + subcontrol-position: center right; + right: 4px; +} + /* Task Panel */ QSint--ActionGroup { background-color: #313244; diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 05964a37fd..96b393b58c 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -834,7 +834,7 @@ ToolBarItem* StdWorkbench::setupToolBars() const // File auto file = new ToolBarItem(root); file->setCommand("File"); - *file << "Std_New" << "Std_Open" << "Std_Save"; + *file << "Std_Origin" << "Std_New" << "Std_Open" << "Std_Save"; // Edit auto edit = new ToolBarItem(root);