feat(gui): add OriginSelectorWidget for file origin selection (#13)
Some checks failed
Build and Test / build (push) Has been cancelled
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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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());
|
||||
|
||||
265
src/Gui/OriginSelectorWidget.cpp
Normal file
265
src/Gui/OriginSelectorWidget.cpp
Normal 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
|
||||
95
src/Gui/OriginSelectorWidget.h
Normal file
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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user