From b80d6e7cd384f258ae4fbf9468c450bb45ce37f8 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 26 Sep 2026 13:18:34 +1200 Subject: [PATCH] Show the selection's commands beside the cursor after a click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a click that selects something on a folio, a small row of commands appears just above and to the right of the cursor, as the SolidWorks context toolbar does. It fades as the mouse moves away and is gone past 200 pixels; a click elsewhere, the wheel, a key press or an emptied selection hide it too. Clicking a command leaves it up, so rotate can be clicked again. The commands are the shortcut bar's for that selection -- elements or conductors -- at most eight, so customising the bar customises this as well. It is a child of the view's viewport, never a window, and never takes the focus. It is not shown after a drag (moving items, a rubber band), while placing or drawing (Diagram::eventInterfaceIsRunning()), on a read-only folio, or when switched off with the new General option "Afficher les commandes près de la sélection" (diagrameditor/ context_toolbar, on by default). ShortcutBarSettings::contextFor() now decides the context for both the bar and this toolbar. Discussion #1033. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG (cherry picked from commit 78f3c1a2bc5c3cf1f7beae4bd60aefec20847d6d) --- cmake/qet_compilation_vars.cmake | 2 + sources/diagramcontexttoolbar.cpp | 164 ++++++++++++++++++ sources/diagramcontexttoolbar.h | 60 +++++++ sources/diagramview.cpp | 53 ++++++ sources/diagramview.h | 4 + sources/qetdiagrameditor.cpp | 12 +- sources/shortcutbarsettings.cpp | 18 ++ sources/shortcutbarsettings.h | 3 + .../configpage/generalconfigurationpage.cpp | 2 + .../ui/configpage/generalconfigurationpage.ui | 10 ++ 10 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 sources/diagramcontexttoolbar.cpp create mode 100644 sources/diagramcontexttoolbar.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index ddc1ba0e0..b3d973e29 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -287,6 +287,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/shortcutmanager.h ${QET_DIR}/sources/shortcutbarsettings.cpp ${QET_DIR}/sources/shortcutbarsettings.h + ${QET_DIR}/sources/diagramcontexttoolbar.cpp + ${QET_DIR}/sources/diagramcontexttoolbar.h ${QET_DIR}/sources/titleblockcell.cpp ${QET_DIR}/sources/titleblockcell.h ${QET_DIR}/sources/titleblockproperties.cpp diff --git a/sources/diagramcontexttoolbar.cpp b/sources/diagramcontexttoolbar.cpp new file mode 100644 index 000000000..c8e8ab3be --- /dev/null +++ b/sources/diagramcontexttoolbar.cpp @@ -0,0 +1,164 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "diagramcontexttoolbar.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + /// Fully opaque within this distance of the toolbar, in pixels... +const int opaque_distance = 30; + /// ...fading to hidden at this one +const int hide_distance = 200; +} + +/** + @brief DiagramContextToolbar::DiagramContextToolbar + @param viewport : the view's viewport, which it is drawn on and watches +*/ +DiagramContextToolbar::DiagramContextToolbar(QWidget *viewport) : + QFrame(viewport) +{ + setFrameShape(QFrame::StyledPanel); + setAutoFillBackground(true); + setFocusPolicy(Qt::NoFocus); + + m_layout = new QHBoxLayout(this); + m_layout->setContentsMargins(2, 2, 2, 2); + m_layout->setSpacing(1); + + m_opacity = new QGraphicsOpacityEffect(this); + setGraphicsEffect(m_opacity); + hide(); + + viewport->installEventFilter(this); + //Key presses go to the view, not its viewport + if (viewport->parentWidget()) { + viewport->parentWidget()->installEventFilter(this); + } +} + +/** + @return whether the toolbar should be shown at all (a preference) +*/ +bool DiagramContextToolbar::isEnabled() +{ + return QSettings().value(QStringLiteral("diagrameditor/context_toolbar"), + true).toBool(); +} + +/** + @brief DiagramContextToolbar::showAt + Show @a actions just above and to the right of @a viewport_pos, kept + inside the viewport. Clicking a button triggers the action and leaves + the toolbar up, so a command such as rotate can be clicked again. +*/ +void DiagramContextToolbar::showAt(const QPoint &viewport_pos, + const QList &actions) +{ + while (QLayoutItem *item = m_layout->takeAt(0)) { + delete item->widget(); + delete item; + } + for (QAction *action : actions.mid(0, max_actions)) + { + auto *button = new QToolButton(this); + button->setAutoRaise(true); + button->setFocusPolicy(Qt::NoFocus); + button->setIconSize(QSize(20, 20)); + const QString text = action->text().remove(QLatin1Char('&')); + if (action->icon().isNull()) { + button->setText(text); + } else { + button->setIcon(action->icon()); + } + button->setToolTip(text); + button->setEnabled(action->isEnabled()); + connect(button, &QToolButton::clicked, action, &QAction::trigger); + m_layout->addWidget(button); + } + if (actions.isEmpty()) { + hide(); + return; + } + + adjustSize(); + const QRect area = parentWidget()->rect(); + QPoint pos = viewport_pos + QPoint(16, -height() - 16); + pos.setX(qBound(area.left(), pos.x(), area.right() - width())); + pos.setY(qBound(area.top(), pos.y(), area.bottom() - height())); + move(pos); + m_opacity->setOpacity(1.0); + show(); + raise(); +} + +/** + @brief DiagramContextToolbar::followMouse + Fade with the mouse's distance from the toolbar, and hide past + hide_distance. +*/ +void DiagramContextToolbar::followMouse(const QPoint &viewport_pos) +{ + const QRect r = geometry(); + const int dx = qMax(qMax(r.left() - viewport_pos.x(), 0), viewport_pos.x() - r.right()); + const int dy = qMax(qMax(r.top() - viewport_pos.y(), 0), viewport_pos.y() - r.bottom()); + const int distance = qMax(dx, dy); + + if (distance >= hide_distance) { + hide(); + return; + } + const qreal fade = qreal(distance - opaque_distance) + / (hide_distance - opaque_distance); + m_opacity->setOpacity(qBound(0.15, 1.0 - fade, 1.0)); +} + +/** + @brief DiagramContextToolbar::eventFilter + Watch the viewport and the view while shown: follow the mouse, and hide + on a press elsewhere, a wheel turn or a key press. +*/ +bool DiagramContextToolbar::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched) + if (!isVisible()) { + return false; + } + + switch (event->type()) + { + case QEvent::MouseMove: + followMouse(static_cast(event)->position().toPoint()); + break; + case QEvent::MouseButtonPress: + case QEvent::Wheel: + case QEvent::KeyPress: + case QEvent::Leave: + hide(); + break; + default: + break; + } + return false; +} diff --git a/sources/diagramcontexttoolbar.h b/sources/diagramcontexttoolbar.h new file mode 100644 index 000000000..5f550a93f --- /dev/null +++ b/sources/diagramcontexttoolbar.h @@ -0,0 +1,60 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef DIAGRAMCONTEXTTOOLBAR_H +#define DIAGRAMCONTEXTTOOLBAR_H + +#include +#include + +class QAction; +class QGraphicsOpacityEffect; +class QHBoxLayout; + +/** + @brief A small row of commands shown beside the cursor after a click + selects something on a folio, as SolidWorks does. + + It lives on the view's viewport, so it never takes the focus or opens a + window. It fades as the mouse moves away and hides once the mouse is far + enough, on a click elsewhere, a wheel turn or a key press. The commands + are the shortcut bar's for the selection (ShortcutBarSettings). +*/ +class DiagramContextToolbar : public QFrame +{ + Q_OBJECT + + public: + explicit DiagramContextToolbar(QWidget *viewport); + + void showAt(const QPoint &viewport_pos, const QList &actions); + static bool isEnabled(); + + /// At most this many commands, so it stays small + static const int max_actions = 8; + + protected: + bool eventFilter(QObject *watched, QEvent *event) override; + + private: + void followMouse(const QPoint &viewport_pos); + + QHBoxLayout *m_layout = nullptr; + QGraphicsOpacityEffect *m_opacity = nullptr; +}; + +#endif // DIAGRAMCONTEXTTOOLBAR_H diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 9d9f3edf7..39feac21c 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -37,6 +37,9 @@ #include "utils/conductorcreator.h" #include "undocommand/addgraphicsobjectcommand.h" #include "diagram.h" +#include "diagramcontexttoolbar.h" +#include "shortcutbarsettings.h" +#include "shortcutmanager.h" #include "ElementsCollection/xmlelementcollection.h" #include "NameList/nameslist.h" #include "elementdialog.h" @@ -104,6 +107,13 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) : m_separators.last()->setSeparator(true); } + m_context_toolbar = new DiagramContextToolbar(viewport()); + connect(m_diagram, &QGraphicsScene::selectionChanged, this, [this]() { + if (m_diagram->selectedItems().isEmpty()) { + m_context_toolbar->hide(); + } + }); + connect(m_diagram, &Diagram::showDiagram, this, &DiagramView::showDiagram); connect(m_diagram, &QGraphicsScene::sceneRectChanged, this, &DiagramView::adjustSceneRect); connect(&(m_diagram -> border_and_titleblock), &BorderTitleBlock::informationChanged, this, &DiagramView::updateWindowTitle); @@ -621,6 +631,10 @@ void DiagramView::mousePressEvent(QMouseEvent *e) if (m_event_interface && m_event_interface->mousePressEvent(e)) return; + if (e->button() == Qt::LeftButton) { + m_press_pos = e->position().toPoint(); + } + //Start drag view when hold the middle button if (e->button() == Qt::MiddleButton) { @@ -777,7 +791,46 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e) e->accept(); } else + { QGraphicsView::mouseReleaseEvent(e); + + //A click, not a drag: moving items or a rubber band selection + //should not be followed by a toolbar under the mouse. + const QPoint pos = e->position().toPoint(); + if (e->button() == Qt::LeftButton + && (pos - m_press_pos).manhattanLength() < QApplication::startDragDistance()) { + showContextToolbar(pos); + } + } +} + +/** + @brief DiagramView::showContextToolbar + After a click that leaves something selected, show the shortcut bar's + commands for that selection beside the cursor. Not while placing or + drawing, nor on a read-only folio, nor when switched off in the + configuration. + @param viewport_pos : where the click was +*/ +void DiagramView::showContextToolbar(const QPoint &viewport_pos) +{ + const QList selection = m_diagram->selectedItems(); + QETDiagramEditor *qde = diagramEditor(); + if (selection.isEmpty() || !qde + || m_diagram->isReadOnly() || m_diagram->eventInterfaceIsRunning() + || !DiagramContextToolbar::isEnabled()) { + m_context_toolbar->hide(); + return; + } + + QList actions; + const auto context = ShortcutBarSettings::contextFor(selection); + for (const QString &id : ShortcutBarSettings::ids(context)) { + if (QAction *action = ShortcutManager::instance().action(id, qde)) { + actions << action; + } + } + m_context_toolbar->showAt(viewport_pos, actions); } /** diff --git a/sources/diagramview.h b/sources/diagramview.h index 03eb067d5..a0c311cd1 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -27,6 +27,7 @@ class CellRuler; class Conductor; class Diagram; +class DiagramContextToolbar; class QETDiagramEditor; class DVEventInterface; class QInputEvent; @@ -56,6 +57,8 @@ class DiagramView : public PaletteGraphicsView QAction *m_multi_paste = nullptr; QAction *m_create_template = nullptr; QPoint m_paste_here_pos; + QPoint m_press_pos; + DiagramContextToolbar *m_context_toolbar = nullptr; QPoint m_last_mouse_pos = QPoint(-1, -1); QPointF m_drag_last_pos; bool m_fresh_focus_in, @@ -127,6 +130,7 @@ class DiagramView : public PaletteGraphicsView bool gestures() const; void updateCellRulers(); void placeCellRulers(); + void showContextToolbar(const QPoint &viewport_pos); /// Lowest and highest allowed value of the view transform scale (m11). /// Prevents wheel-zoom from driving the transform to overflow, which diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index e5f5f81cb..516d13c16 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -3157,16 +3157,8 @@ void QETDiagramEditor::showShortcutBar() return; } - const QList selection = dv->diagram()->selectedItems(); - ShortcutBarSettings::Context context = ShortcutBarSettings::Canvas; - if (!selection.isEmpty()) - { - const bool only_conductors = std::all_of( - selection.cbegin(), selection.cend(), - [](QGraphicsItem *item) { return item->type() == Conductor::Type; }); - context = only_conductors ? ShortcutBarSettings::Conductor - : ShortcutBarSettings::Selection; - } + const ShortcutBarSettings::Context context = + ShortcutBarSettings::contextFor(dv->diagram()->selectedItems()); elementPicker()->popUpShortcutBar(QCursor::pos(), context); } diff --git a/sources/shortcutbarsettings.cpp b/sources/shortcutbarsettings.cpp index fa66300a3..7832c6ee5 100644 --- a/sources/shortcutbarsettings.cpp +++ b/sources/shortcutbarsettings.cpp @@ -17,9 +17,11 @@ */ #include "shortcutbarsettings.h" +#include "qetgraphicsitem/conductor.h" #include "shortcutmanager.h" #include +#include #include namespace { @@ -46,6 +48,22 @@ QList ShortcutBarSettings::contexts() return {Canvas, Selection, Conductor}; } +/** + @return the context for @a selection: Canvas when empty, Conductor when + it holds only conductors, Selection otherwise +*/ +ShortcutBarSettings::Context ShortcutBarSettings::contextFor( + const QList &selection) +{ + if (selection.isEmpty()) { + return Canvas; + } + const bool only_conductors = std::all_of( + selection.cbegin(), selection.cend(), + [](QGraphicsItem *item) { return item->type() == Conductor::Type; }); + return only_conductors ? Conductor : Selection; +} + /** @return the name of @a context, for the configuration page */ diff --git a/sources/shortcutbarsettings.h b/sources/shortcutbarsettings.h index 8e83b9232..adf533359 100644 --- a/sources/shortcutbarsettings.h +++ b/sources/shortcutbarsettings.h @@ -19,6 +19,8 @@ #define SHORTCUTBARSETTINGS_H #include + +class QGraphicsItem; #include #include @@ -41,6 +43,7 @@ class ShortcutBarSettings }; static QList contexts(); + static Context contextFor(const QList &selection); static QString title(Context context); static QStringList ids(Context context); static QStringList defaultIds(Context context); diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index c59390c24..bd58a2e2a 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -70,6 +70,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : //Stored as "inserts" but presented as "edits", so the default (insert) //is the unchecked state -- a preference reads better as an opt-out. ui->m_collection_dblclick_edits->setChecked(!settings.value("elementscollection/double-click-inserts", true).toBool()); + ui->m_context_toolbar_cb->setChecked(settings.value("diagrameditor/context_toolbar", true).toBool()); ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt()); ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt()); for (const qreal divisor : TextGrid::divisors) @@ -298,6 +299,7 @@ void GeneralConfigurationPage::applyConf() settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked()); settings.setValue("diagrameditor/guides_display_startup", ui->guides_startup_cb->isChecked()); settings.setValue("elementscollection/double-click-inserts", !ui->m_collection_dblclick_edits->isChecked()); + settings.setValue("diagrameditor/context_toolbar", ui->m_context_toolbar_cb->isChecked()); //Grid step and key navigation settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value()); settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 2ab58cdc2..de4b7445c 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -87,6 +87,16 @@ + + + + Après un clic qui sélectionne un élément ou un conducteur, les commandes de la barre de raccourcis apparaissent près du curseur et s'effacent quand la souris s'éloigne. + + + Afficher les commandes près de la sélection + + +