feat(gui): add OriginSelectorWidget for file origin selection (#13)
Some checks failed
Build and Test / build (push) Has been cancelled

- Create OriginSelectorWidget class (QToolButton with dropdown menu)
- Add OriginSelectorAction to create widget in toolbars
- Add Std_Origin command registered in CommandStd.cpp
- Add widget to File toolbar (before New/Open/Save)
- Connect to OriginManager fastsignals for origin changes
- Add Catppuccin Mocha styling for the widget
- Widget shows current origin name/icon with connection status overlay

This implements Issue #13: Origin selector toolbar widget
This commit is contained in:
2026-02-05 14:47:18 -06:00
parent 103fc28bc6
commit deeb6376f7
9 changed files with 488 additions and 1 deletions

View File

@@ -54,6 +54,7 @@
#include "Workbench.h"
#include "WorkbenchManager.h"
#include "WorkbenchSelector.h"
#include "OriginSelectorWidget.h"
#include "ShortcutManager.h"
#include "Tools.h"
@@ -1470,4 +1471,25 @@ void WindowAction::addTo(QWidget* widget)
}
}
// --------------------------------------------------------------------
OriginSelectorAction::OriginSelectorAction(Command* pcCmd, QObject* parent)
: Action(pcCmd, parent)
{}
OriginSelectorAction::~OriginSelectorAction() = default;
void OriginSelectorAction::addTo(QWidget* widget)
{
if (widget->inherits("QToolBar")) {
auto* toolbar = static_cast<QToolBar*>(widget);
auto* selector = new OriginSelectorWidget(widget);
toolbar->addWidget(selector);
}
else {
// For menus, just add the action
widget->addAction(action());
}
}
#include "moc_Action.cpp"

View File

@@ -421,6 +421,25 @@ private:
Q_DISABLE_COPY(WindowAction)
};
// --------------------------------------------------------------------
/**
* Action for origin selector widget in toolbars.
* Creates OriginSelectorWidget when added to a toolbar.
*/
class GuiExport OriginSelectorAction: public Action
{
Q_OBJECT
public:
explicit OriginSelectorAction(Command* pcCmd, QObject* parent = nullptr);
~OriginSelectorAction() override;
void addTo(QWidget* widget) override;
private:
Q_DISABLE_COPY(OriginSelectorAction)
};
} // namespace Gui
#endif // GUI_ACTION_H

View File

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

View File

@@ -132,6 +132,45 @@ Action* StdCmdWorkbench::createAction()
return pcAction;
}
//===========================================================================
// Std_Origin
//===========================================================================
DEF_STD_CMD_AC(StdCmdOrigin)
StdCmdOrigin::StdCmdOrigin()
: Command("Std_Origin")
{
sGroup = "File";
sMenuText = QT_TR_NOOP("&Origin");
sToolTipText = QT_TR_NOOP("Select file origin (Local Files, Silo, etc.)");
sWhatsThis = "Std_Origin";
sStatusTip = sToolTipText;
sPixmap = "folder";
eType = 0;
}
void StdCmdOrigin::activated(int /*iMsg*/)
{
// Action is handled by OriginSelectorWidget
}
bool StdCmdOrigin::isActive()
{
return true;
}
Action* StdCmdOrigin::createAction()
{
Action* pcAction = new OriginSelectorAction(this, getMainWindow());
pcAction->setShortcut(QString::fromLatin1(getAccel()));
applyCommandData(this->className(), pcAction);
if (getPixmap()) {
pcAction->setIcon(Gui::BitmapFactory().iconFromTheme(getPixmap()));
}
return pcAction;
}
//===========================================================================
// Std_RecentFiles
//===========================================================================
@@ -1057,6 +1096,7 @@ void CreateStdCommands()
rcCmdMgr.addCommand(new StdCmdDlgCustomize());
rcCmdMgr.addCommand(new StdCmdCommandLine());
rcCmdMgr.addCommand(new StdCmdWorkbench());
rcCmdMgr.addCommand(new StdCmdOrigin());
rcCmdMgr.addCommand(new StdCmdRecentFiles());
rcCmdMgr.addCommand(new StdCmdRecentMacros());
rcCmdMgr.addCommand(new StdCmdWhatsThis());

View File

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

View File

@@ -0,0 +1,95 @@
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#ifndef GUI_ORIGINSELECTORWIDGET_H
#define GUI_ORIGINSELECTORWIDGET_H
#include <QToolButton>
#include <QMenu>
#include <QActionGroup>
#include <fastsignals/signal.h>
#include <FCGlobal.h>
namespace Gui {
class FileOrigin;
/**
* @brief Toolbar widget for selecting the current file origin
*
* OriginSelectorWidget displays the currently selected origin and provides
* a dropdown menu to switch between available origins (Local Files, Silo
* instances, etc.).
*
* Visual design:
* Collapsed (toolbar state):
* ┌──────────────────┐
* │ ☁️ Work ▼ │ ~70-100px wide
* └──────────────────┘
*
* Expanded (dropdown open):
* ┌──────────────────┐
* │ ✓ ☁️ Work │ ← Current selection (checkmark)
* │ ☁️ Prod │
* │ 📁 Local │
* ├──────────────────┤
* │ ⚙️ Manage... │ ← Opens config dialog
* └──────────────────┘
*/
class GuiExport OriginSelectorWidget : public QToolButton
{
Q_OBJECT
public:
explicit OriginSelectorWidget(QWidget* parent = nullptr);
~OriginSelectorWidget() override;
private Q_SLOTS:
void onOriginActionTriggered(QAction* action);
void onManageOriginsClicked();
private:
void setupUi();
void connectSignals();
void disconnectSignals();
void onOriginRegistered(const std::string& originId);
void onOriginUnregistered(const std::string& originId);
void onCurrentOriginChanged(const std::string& originId);
void updateDisplay();
void rebuildMenu();
QIcon iconForOrigin(FileOrigin* origin) const;
QMenu* m_menu;
QActionGroup* m_originActions;
QAction* m_manageAction;
// Signal connections
fastsignals::scoped_connection m_connRegistered;
fastsignals::scoped_connection m_connUnregistered;
fastsignals::scoped_connection m_connChanged;
};
} // namespace Gui
#endif // GUI_ORIGINSELECTORWIDGET_H

View File

@@ -1142,6 +1142,28 @@ Gui--WorkbenchComboBox::drop-down {
border-radius: 0 4px 4px 0;
}
/* Origin Selector */
Gui--OriginSelectorWidget {
background-color: #313244;
color: #cdd6f4;
border: 1px solid #45475a;
border-radius: 4px;
padding: 4px 8px;
min-width: 70px;
max-width: 120px;
}
Gui--OriginSelectorWidget:hover {
border-color: #585b70;
background-color: #45475a;
}
Gui--OriginSelectorWidget::menu-indicator {
subcontrol-origin: padding;
subcontrol-position: center right;
right: 4px;
}
/* Task Panel */
QSint--ActionGroup {
background-color: #313244;

View File

@@ -1163,6 +1163,28 @@ Gui--WorkbenchComboBox::drop-down {
border-radius: 0 4px 4px 0;
}
/* Origin Selector */
Gui--OriginSelectorWidget {
background-color: #313244;
color: #cdd6f4;
border: 1px solid #45475a;
border-radius: 4px;
padding: 4px 8px;
min-width: 70px;
max-width: 120px;
}
Gui--OriginSelectorWidget:hover {
border-color: #585b70;
background-color: #45475a;
}
Gui--OriginSelectorWidget::menu-indicator {
subcontrol-origin: padding;
subcontrol-position: center right;
right: 4px;
}
/* Task Panel */
QSint--ActionGroup {
background-color: #313244;

View File

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