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 + + +