Merge pull request #1055 from ispyisail/feature/context-toolbar

Add a command bar beside the selection after a click
This commit is contained in:
Laurent Trinques
2026-09-27 09:00:29 +02:00
committed by GitHub
10 changed files with 318 additions and 10 deletions
+164
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "diagramcontexttoolbar.h"
#include <QAction>
#include <QEvent>
#include <QGraphicsOpacityEffect>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QSettings>
#include <QToolButton>
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<QAction *> &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<QMouseEvent *>(event)->position().toPoint());
break;
case QEvent::MouseButtonPress:
case QEvent::Wheel:
case QEvent::KeyPress:
case QEvent::Leave:
hide();
break;
default:
break;
}
return false;
}
+60
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMCONTEXTTOOLBAR_H
#define DIAGRAMCONTEXTTOOLBAR_H
#include <QFrame>
#include <QList>
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<QAction *> &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
+53
View File
@@ -38,6 +38,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"
@@ -109,6 +112,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);
@@ -627,6 +637,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)
{
@@ -783,7 +797,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<QGraphicsItem *> 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<QAction *> 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);
}
/**
+4
View File
@@ -27,6 +27,7 @@
class CellRuler;
class Conductor;
class Diagram;
class DiagramContextToolbar;
class QETDiagramEditor;
class DVEventInterface;
class QInputEvent;
@@ -58,6 +59,8 @@ class DiagramView : public PaletteGraphicsView
QAction *m_create_template = nullptr;
QMenu *m_folio_report_menu = 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,
@@ -133,6 +136,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
+2 -10
View File
@@ -3194,16 +3194,8 @@ void QETDiagramEditor::showShortcutBar()
return;
}
const QList<QGraphicsItem *> 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);
}
+18
View File
@@ -17,9 +17,11 @@
*/
#include "shortcutbarsettings.h"
#include "qetgraphicsitem/conductor.h"
#include "shortcutmanager.h"
#include <QCoreApplication>
#include <algorithm>
#include <QSettings>
namespace {
@@ -46,6 +48,22 @@ QList<ShortcutBarSettings::Context> 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<QGraphicsItem *> &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
*/
+3
View File
@@ -19,6 +19,8 @@
#define SHORTCUTBARSETTINGS_H
#include <QList>
class QGraphicsItem;
#include <QString>
#include <QStringList>
@@ -41,6 +43,7 @@ class ShortcutBarSettings
};
static QList<Context> contexts();
static Context contextFor(const QList<QGraphicsItem *> &selection);
static QString title(Context context);
static QStringList ids(Context context);
static QStringList defaultIds(Context context);
@@ -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());
@@ -87,6 +87,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_context_toolbar_cb">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Afficher les commandes près de la sélection</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="guides_startup_cb">
<property name="text">