diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index ddc1ba0e0..1d8d7812b 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/commandsearchpopup.cpp
+ ${QET_DIR}/sources/commandsearchpopup.h
${QET_DIR}/sources/titleblockcell.cpp
${QET_DIR}/sources/titleblockcell.h
${QET_DIR}/sources/titleblockproperties.cpp
diff --git a/sources/cellruler.cpp b/sources/cellruler.cpp
index 299d70eb8..b9055e713 100644
--- a/sources/cellruler.cpp
+++ b/sources/cellruler.cpp
@@ -73,7 +73,7 @@ void CellRuler::paintEvent(QPaintEvent *event)
Q_UNUSED(event)
QPainter painter(this);
- painter.fillRect(rect(), palette().color(QPalette::Button));
+ painter.fillRect(rect(), background());
const bool horizontal = m_orientation == Qt::Horizontal;
const int length = horizontal ? width() : height();
@@ -92,12 +92,15 @@ void CellRuler::paintEvent(QPaintEvent *event)
return;
}
const BorderTitleBlock &border = diagram->border_and_titleblock;
- const QRectF inside = border.insideBorderRect();
const QTransform transform = m_view->viewportTransform();
const int count = horizontal ? border.columnsCount() : border.rowsCount();
const qreal cell_size = horizontal ? border.columnsWidth() : border.rowsHeight();
- const qreal first = horizontal ? inside.left() : inside.top();
+ //Where BorderTitleBlock::draw() puts the first cell: after the
+ //other header's room even when that header is hidden, which
+ //insideBorderRect() does not account for
+ const qreal first = Diagram::margin
+ + (horizontal ? border.rowsHeaderWidth() : border.columnsHeaderHeight());
const qreal scale = horizontal ? transform.m11() : transform.m22();
const qreal offset = (horizontal ? transform.dx() : transform.dy()) + m_leading_space;
const qreal cell_pixels = cell_size * scale;
@@ -165,6 +168,25 @@ void CellRuler::paintEvent(QPaintEvent *event)
if (m_leading_space > 0) {
painter.fillRect(horizontal ? QRect(0, 0, m_leading_space, depth - 1)
: QRect(0, 0, depth - 1, m_leading_space),
- palette().color(QPalette::Button));
+ background());
}
}
+
+/**
+ @brief CellRuler::background
+ @return the button colour laid over the window colour, always opaque.
+ The Windows 11 style gives buttons a translucent colour; filled with it
+ as is, a ruler (painted with Qt::WA_OpaquePaintEvent, so never cleared
+ first) would let every previous frame show through, and zooming would
+ leave a shadow of the old labels behind the new ones.
+*/
+QColor CellRuler::background() const
+{
+ const QColor window = palette().color(QPalette::Window);
+ const QColor button = palette().color(QPalette::Button);
+ const qreal alpha = button.alphaF();
+ return QColor::fromRgbF(
+ button.redF() * alpha + window.redF() * (1 - alpha),
+ button.greenF() * alpha + window.greenF() * (1 - alpha),
+ button.blueF() * alpha + window.blueF() * (1 - alpha));
+}
diff --git a/sources/cellruler.h b/sources/cellruler.h
index 619b2d52e..586918210 100644
--- a/sources/cellruler.h
+++ b/sources/cellruler.h
@@ -44,6 +44,8 @@ class CellRuler : public QWidget
void paintEvent(QPaintEvent *event) override;
private:
+ QColor background() const;
+
Qt::Orientation m_orientation;
DiagramView *m_view;
/// Pixels before the viewport starts, left empty: the corner the
diff --git a/sources/commandsearchpopup.cpp b/sources/commandsearchpopup.cpp
new file mode 100644
index 000000000..57c1efbd1
--- /dev/null
+++ b/sources/commandsearchpopup.cpp
@@ -0,0 +1,236 @@
+/*
+ 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 "commandsearchpopup.h"
+
+#include "shortcutmanager.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+/**
+ @brief CommandSearchPopup::CommandSearchPopup
+ @param owner : the window whose commands are searched; also the parent
+*/
+CommandSearchPopup::CommandSearchPopup(QWidget *owner) :
+ QFrame(owner, Qt::Popup)
+{
+ setFrameShape(QFrame::StyledPanel);
+ setFrameShadow(QFrame::Raised);
+ setMinimumWidth(380);
+
+ m_search = new QLineEdit(this);
+ m_search->setPlaceholderText(tr("Rechercher une commande…"));
+ m_search->setClearButtonEnabled(true);
+ m_list = new QListWidget(this);
+ m_list->setIconSize(QSize(20, 20));
+ m_list->setMinimumHeight(280);
+ m_list->setFocusPolicy(Qt::NoFocus);
+
+ auto *hint = new QLabel(tr("Entrée pour lancer · Échap pour fermer"), this);
+ hint->setEnabled(false);
+
+ auto *layout = new QVBoxLayout(this);
+ layout->setContentsMargins(6, 6, 6, 6);
+ layout->setSpacing(4);
+ layout->addWidget(m_search);
+ layout->addWidget(m_list);
+ layout->addWidget(hint);
+
+ connect(m_search, &QLineEdit::textChanged, this, &CommandSearchPopup::filter);
+ connect(m_list, &QListWidget::itemClicked, this, [this]() { runCurrent(); });
+}
+
+/**
+ @brief CommandSearchPopup::fold
+ @return @a text lower-cased, without accents and without the "&" of
+ mnemonics, for matching
+*/
+QString CommandSearchPopup::fold(const QString &text)
+{
+ QString out;
+ const QString decomposed = text.normalized(QString::NormalizationForm_D);
+ out.reserve(decomposed.size());
+ for (const QChar c : decomposed) {
+ if (c.category() != QChar::Mark_NonSpacing && c != QLatin1Char('&')) {
+ out.append(c.toLower());
+ }
+ }
+ return out;
+}
+
+/**
+ @brief CommandSearchPopup::popUpAt
+ Rebuild the command list -- actions come and go with windows, and their
+ enabled state changes -- then show at @a global_pos, kept on screen.
+*/
+void CommandSearchPopup::popUpAt(const QPoint &global_pos)
+{
+ collect();
+ m_search->clear();
+ filter();
+
+ adjustSize();
+ QPoint pos = global_pos;
+ if (QScreen *screen = QGuiApplication::screenAt(global_pos)) {
+ const QRect avail = screen->availableGeometry();
+ pos.setX(qBound(avail.left(), pos.x(), avail.right() - width()));
+ pos.setY(qBound(avail.top(), pos.y(), avail.bottom() - height()));
+ }
+ move(pos);
+ show();
+ m_search->setFocus();
+}
+
+/**
+ @brief CommandSearchPopup::collect
+ Every command registered by the owning window, except this search.
+*/
+void CommandSearchPopup::collect()
+{
+ m_commands.clear();
+ for (const ShortcutManager::ShortcutInfo &info :
+ ShortcutManager::instance().allShortcuts())
+ {
+ if (info.id == QLatin1String("diagrameditor.command_search")) {
+ continue;
+ }
+ QAction *action = ShortcutManager::instance().action(info.id, parentWidget());
+ if (!action || !action->isVisible()) {
+ continue;
+ }
+ const QString text = action->text().remove(QLatin1Char('&'));
+ if (text.isEmpty()) {
+ continue;
+ }
+ m_commands.append({action, text, fold(text)});
+ }
+}
+
+/**
+ @brief CommandSearchPopup::filter
+ Show the commands matching the search text, best first: a name starting
+ with it, then a word starting with it, then containing it. An empty
+ search lists everything, alphabetically.
+*/
+void CommandSearchPopup::filter()
+{
+ const QString needle = fold(m_search->text().trimmed());
+
+ QList> hits;
+ for (const Command &command : std::as_const(m_commands))
+ {
+ int score = 0;
+ if (needle.isEmpty()) {
+ score = 1;
+ } else if (command.folded.startsWith(needle)) {
+ score = 3;
+ } else if (command.folded.contains(QLatin1Char(' ') + needle)) {
+ score = 2;
+ } else if (command.folded.contains(needle)) {
+ score = 1;
+ }
+ if (score) {
+ hits.append({score, &command});
+ }
+ }
+ std::stable_sort(hits.begin(), hits.end(), [](const auto &a, const auto &b) {
+ if (a.first != b.first) {
+ return a.first > b.first;
+ }
+ return a.second->folded < b.second->folded;
+ });
+
+ m_list->clear();
+ for (const auto &hit : std::as_const(hits))
+ {
+ QAction *action = hit.second->action;
+ const QKeySequence key = action->shortcut();
+ auto *item = new QListWidgetItem(
+ action->icon(),
+ key.isEmpty() ? hit.second->text
+ : QStringLiteral("%1 (%2)").arg(hit.second->text,
+ key.toString(QKeySequence::NativeText)));
+ item->setData(Qt::UserRole, QVariant::fromValue(static_cast(action)));
+ if (!action->isEnabled()) {
+ item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
+ }
+ m_list->addItem(item);
+ }
+
+ //Preselect the first command that can run, so Enter works at once
+ for (int i = 0 ; i < m_list->count() ; ++i) {
+ if (m_list->item(i)->flags() & Qt::ItemIsEnabled) {
+ m_list->setCurrentRow(i);
+ break;
+ }
+ }
+}
+
+/**
+ @brief CommandSearchPopup::runCurrent
+ Close, then trigger the highlighted command: it may open a dialog or
+ start a tool on the folio, which need the focus the popup holds.
+*/
+void CommandSearchPopup::runCurrent()
+{
+ QListWidgetItem *item = m_list->currentItem();
+ if (!item || !(item->flags() & Qt::ItemIsEnabled)) {
+ return;
+ }
+ auto *action = static_cast(item->data(Qt::UserRole).value());
+ hide();
+ if (action) {
+ action->trigger();
+ }
+}
+
+/**
+ @brief CommandSearchPopup::keyPressEvent
+ Up and Down move through the list while typing goes on in the search
+ field; Enter runs, Esc closes.
+*/
+void CommandSearchPopup::keyPressEvent(QKeyEvent *event)
+{
+ switch (event->key())
+ {
+ case Qt::Key_Escape:
+ hide();
+ return;
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ runCurrent();
+ return;
+ case Qt::Key_Down:
+ case Qt::Key_Up:
+ case Qt::Key_PageDown:
+ case Qt::Key_PageUp:
+ QCoreApplication::sendEvent(m_list, event);
+ return;
+ default:
+ break;
+ }
+ QFrame::keyPressEvent(event);
+}
diff --git a/sources/commandsearchpopup.h b/sources/commandsearchpopup.h
new file mode 100644
index 000000000..ff71ea667
--- /dev/null
+++ b/sources/commandsearchpopup.h
@@ -0,0 +1,65 @@
+/*
+ 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 COMMANDSEARCHPOPUP_H
+#define COMMANDSEARCHPOPUP_H
+
+#include
+#include
+
+class QAction;
+class QLineEdit;
+class QListWidget;
+
+/**
+ @brief Type part of a command's name, press Enter to run it.
+
+ Lists every command the owning window registered with ShortcutManager,
+ best match first, with its key if it has one. Matching ignores case and
+ accents, so "editer" finds "Éditer". Disabled commands are listed,
+ greyed, but cannot be run.
+*/
+class CommandSearchPopup : public QFrame
+{
+ Q_OBJECT
+
+ public:
+ explicit CommandSearchPopup(QWidget *owner);
+
+ void popUpAt(const QPoint &global_pos);
+ static QString fold(const QString &text);
+
+ protected:
+ void keyPressEvent(QKeyEvent *event) override;
+
+ private:
+ void collect();
+ void filter();
+ void runCurrent();
+
+ struct Command {
+ QAction *action;
+ QString text; ///< as shown
+ QString folded; ///< for matching
+ };
+
+ QLineEdit *m_search = nullptr;
+ QListWidget *m_list = nullptr;
+ QList m_commands;
+};
+
+#endif // COMMANDSEARCHPOPUP_H
diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp
index 9d9f3edf7..534166c9c 100644
--- a/sources/diagramview.cpp
+++ b/sources/diagramview.cpp
@@ -30,6 +30,7 @@
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/independenttextitem.h"
#include "qeticons.h"
+#include "qetpalette.h"
#include "titleblock/integrationmovetemplateshandler.h"
#include "ui/diagrampropertiesdialog.h"
#include "ui/multipastedialog.h"
@@ -112,6 +113,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
m_top_ruler = new CellRuler(Qt::Horizontal, this);
m_side_ruler = new CellRuler(Qt::Vertical, this);
m_cell_rulers_shown = QSettings().value("diagrameditor/cell_rulers", false).toBool();
+ m_cell_lines_shown = QSettings().value("diagrameditor/cell_lines", false).toBool();
connect(&m_diagram->border_and_titleblock, &BorderTitleBlock::borderChanged, this, &DiagramView::updateCellRulers);
connect(&m_diagram->border_and_titleblock, &BorderTitleBlock::displayChanged, this, &DiagramView::updateCellRulers);
updateCellRulers();
@@ -1254,6 +1256,72 @@ void DiagramView::paintingInverted(bool inverted)
m_diagram->setInvertedLightness(inverted);
}
+/**
+ @brief DiagramView::setCellLinesShown
+ Show or hide the lines that mark the columns and the rows of the folio
+ border across the drawing, in this view only: printing and exporting
+ never draw them.
+ @param shown
+*/
+void DiagramView::setCellLinesShown(bool shown)
+{
+ m_cell_lines_shown = shown;
+ viewport()->update();
+}
+
+/**
+ @brief DiagramView::drawBackground
+ Reimplemented from PaletteGraphicsView: over the folio background, the
+ cell lines when they are shown. Dashed and faint, so they do not read
+ as conductors, and under every item.
+ @param painter
+ @param rect
+*/
+void DiagramView::drawBackground(QPainter *painter, const QRectF &rect)
+{
+ PaletteGraphicsView::drawBackground(painter, rect);
+
+ const BorderTitleBlock &border = m_diagram->border_and_titleblock;
+ if (!m_cell_lines_shown || !border.borderIsDisplayed()) {
+ return;
+ }
+
+ //Where the border draws its cells, whether or not the other
+ //header is displayed
+ const QPointF origin(Diagram::margin + border.rowsHeaderWidth(),
+ Diagram::margin + border.columnsHeaderHeight());
+ const qreal right = origin.x() + border.columnsCount() * border.columnsWidth();
+ const qreal bottom = origin.y() + border.rowsCount() * border.rowsHeight();
+
+ QPainter *p = scenePainter(painter);
+ p->save();
+ p->setRenderHint(QPainter::Antialiasing, false);
+ QColor color = QET::Palette::gridDotColor(Diagram::background_color,
+ invertsLightness());
+ color.setAlpha(70);
+ QPen pen(color, 1, Qt::DashLine);
+ pen.setCosmetic(true);
+ p->setPen(pen);
+
+ if (border.columnsAreDisplayed()) {
+ for (int i = 1 ; i < border.columnsCount() ; ++i) {
+ const qreal x = origin.x() + i * border.columnsWidth();
+ if (x >= rect.left() && x <= rect.right()) {
+ p->drawLine(QPointF(x, origin.y()), QPointF(x, bottom));
+ }
+ }
+ }
+ if (border.rowsAreDisplayed()) {
+ for (int i = 1 ; i < border.rowsCount() ; ++i) {
+ const qreal y = origin.y() + i * border.rowsHeight();
+ if (y >= rect.top() && y <= rect.bottom()) {
+ p->drawLine(QPointF(origin.x(), y), QPointF(right, y));
+ }
+ }
+ }
+ p->restore();
+}
+
/**
@brief DiagramView::setCellRulersShown
Show or hide the rulers that keep the column numbers and the row
@@ -1268,17 +1336,24 @@ void DiagramView::setCellRulersShown(bool shown)
/**
@brief DiagramView::updateCellRulers
- Show each ruler when the rulers are wanted and the folio shows the
- matching header, and give it room in the margins of the view. The
- part of the folio in sight stays in sight when the viewport resizes.
+ Show each ruler when the rulers are wanted, the folio shows the
+ matching header and that header is not already wholly in sight, and
+ give it room in the margins of the view. The drawing does not move on
+ screen when a ruler comes or goes: the ruler covers or uncovers the
+ edge of the viewport, as if it lay over it.
*/
void DiagramView::updateCellRulers()
{
const BorderTitleBlock &border = m_diagram->border_and_titleblock;
+ const QRectF in_sight = mapToScene(viewport()->rect()).boundingRect();
+ const QRectF columns = border.columnsRect();
+ const QRectF rows = border.rowsRect();
const bool top = m_cell_rulers_shown
- && border.borderIsDisplayed() && border.columnsAreDisplayed();
+ && border.borderIsDisplayed() && border.columnsAreDisplayed()
+ && (columns.top() < in_sight.top() || columns.bottom() > in_sight.bottom());
const bool side = m_cell_rulers_shown
- && border.borderIsDisplayed() && border.rowsAreDisplayed();
+ && border.borderIsDisplayed() && border.rowsAreDisplayed()
+ && (rows.left() < in_sight.left() || rows.right() > in_sight.right());
const int thickness = m_top_ruler->thickness();
m_top_ruler->setVisible(top);
@@ -1287,9 +1362,12 @@ void DiagramView::updateCellRulers()
const QMargins margins(side ? thickness : 0, top ? thickness : 0, 0, 0);
if (margins != viewportMargins()) {
- const QPointF centre = mapToScene(viewport()->rect().center());
+ const QPointF origin = mapToScene(viewport()->rect().center());
+ const QPoint before = viewport()->mapToGlobal(mapFromScene(origin));
setViewportMargins(margins);
- centerOn(centre);
+ const QPoint moved = viewport()->mapToGlobal(mapFromScene(origin)) - before;
+ horizontalScrollBar()->setValue(horizontalScrollBar()->value() + moved.x());
+ verticalScrollBar()->setValue(verticalScrollBar()->value() + moved.y());
}
placeCellRulers();
m_top_ruler->update();
@@ -1339,11 +1417,17 @@ void DiagramView::paintEvent(QPaintEvent *event)
{
PaletteGraphicsView::paintEvent(event);
- //Scrolling and zooming both repaint the viewport: follow them
+ //Scrolling and zooming both repaint the viewport: follow them.
+ //Showing or hiding a ruler resizes the viewport, which cannot be
+ //done while it paints.
if (viewportTransform() != m_rulers_transform) {
m_rulers_transform = viewportTransform();
m_top_ruler->update();
m_side_ruler->update();
+ if (m_cell_rulers_shown) {
+ QMetaObject::invokeMethod(this, &DiagramView::updateCellRulers,
+ Qt::QueuedConnection);
+ }
}
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
diff --git a/sources/diagramview.h b/sources/diagramview.h
index 03eb067d5..a9e76547f 100644
--- a/sources/diagramview.h
+++ b/sources/diagramview.h
@@ -68,7 +68,8 @@ class DiagramView : public PaletteGraphicsView
bool m_cell_rulers_shown = false;
/// Last viewport transform the rulers were painted for
QTransform m_rulers_transform;
-
+ bool m_cell_lines_shown = false;
+
public:
QString title() const;
@@ -84,11 +85,12 @@ class DiagramView : public PaletteGraphicsView
/// cursor query (QCursor::pos()/setPos() are silently ignored by
/// several window managers and compositors, Wayland included).
QPoint lastMousePos() const { return m_last_mouse_pos; }
- void setCellRulersShown(bool shown);
bool startElementPlacement(const ElementsLocation &location,
const QPointF &scene_pos);
QPointF defaultPlacementPos() const;
+ void setCellRulersShown(bool shown);
+ void setCellLinesShown(bool shown);
protected:
void mouseDoubleClickEvent(QMouseEvent *) override;
@@ -103,6 +105,7 @@ class DiagramView : public PaletteGraphicsView
bool m_releasing_focus = false;
void paintEvent(QPaintEvent *event) override;
bool viewportEvent(QEvent *event) override;
+ void drawBackground(QPainter *painter, const QRectF &rect) override;
void paintingInverted(bool inverted) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
diff --git a/sources/palettegraphicsview.h b/sources/palettegraphicsview.h
index 080897545..96d2ace36 100644
--- a/sources/palettegraphicsview.h
+++ b/sources/palettegraphicsview.h
@@ -80,6 +80,11 @@ class PaletteGraphicsView : public QGraphicsView
nothing by default.
*/
virtual void paintingInverted(bool inverted);
+ /// @return the painter the scene really paints with: the
+ /// off-screen image's while painting inverted, else \a painter.
+ /// For subclasses that draw more in drawBackground().
+ QPainter *scenePainter(QPainter *painter)
+ { return m_inverting ? &m_buffer_painter : painter; }
private:
void paintInverted(QPaintEvent *event);
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index e5f5f81cb..0aa2c9cac 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -25,6 +25,7 @@
#include "ElementsCollection/elementpickerpopup.h"
#include "shortcutbarsettings.h"
#include "qetgraphicsitem/conductor.h"
+#include "commandsearchpopup.h"
#include "QWidgetAnimation/qwidgetanimation.h"
#include "autoNum/ui/autonumberingdockwidget.h"
#include "conductornumexport.h"
@@ -545,6 +546,18 @@ void QETDiagramEditor::setUpActions()
dv->setCellRulersShown(checked);
});
+ //Draw the limits of the folio columns and rows across the drawing
+ m_cell_lines = new QAction(tr("Afficher les limites des cases"), this);
+ m_cell_lines->setStatusTip(tr("Trace les limites des colonnes et des lignes du folio sur le schéma, à l'écran seulement"));
+ m_cell_lines->setCheckable(true);
+ m_cell_lines->setChecked(settings.value("diagrameditor/cell_lines", false).toBool());
+ connect(m_cell_lines, &QAction::triggered, [this](bool checked) {
+ QSettings().setValue("diagrameditor/cell_lines", checked);
+ foreach (ProjectView *prjv, this->openedProjects())
+ foreach (DiagramView *dv, prjv->diagram_views())
+ dv->setCellLinesShown(checked);
+ });
+
//Edit current diagram properties
m_edit_diagram_properties = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
ShortcutManager::instance().registerAction(m_edit_diagram_properties, "diagrameditor.edit_diagram_properties", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_L);
@@ -826,6 +839,26 @@ void QETDiagramEditor::setUpActions()
connect(m_insert_last_element, &QAction::triggered,
this, &QETDiagramEditor::insertLastElement);
addAction(m_insert_last_element);
+ //Type to find and run any command, as SolidWorks' "Search Commands"
+ //and the command palette of many editors. Ctrl+Shift+P, the key those
+ //editors use, is taken by the autonumbering dock; M for "menu".
+ m_command_search = new QAction(tr("Rechercher une commande…"), this);
+ m_command_search->setStatusTip(
+ tr("Tapez une partie du nom d'une commande et appuyez sur Entrée pour la lancer",
+ "status bar tip"));
+ ShortcutManager::instance().registerAction(
+ m_command_search, "diagrameditor.command_search",
+ tr("Éditeur de schémas"), Qt::CTRL | Qt::SHIFT | Qt::Key_M);
+ connect(m_command_search, &QAction::triggered, this, [this]() {
+ if (!m_command_search_popup) {
+ m_command_search_popup = new CommandSearchPopup(this);
+ }
+ const QRect area = geometry();
+ m_command_search_popup->popUpAt(
+ area.contains(QCursor::pos()) ? QCursor::pos()
+ : area.center());
+ });
+ addAction(m_command_search);
//Cursor-anchored picker. Insert is unbound anywhere in the tree and
//reads correctly for the action, which keeps A free for the far more
@@ -988,8 +1021,9 @@ void QETDiagramEditor::setUpActions()
add_path->setCheckable(true);
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
- //No default key, but an id: they can then be bound in the Shortcuts
- //page and placed on the shortcut bar, like every other command.
+ //No default key, but an id: they can then be found by the command
+ //search, bound in the Shortcuts page and placed on the shortcut bar,
+ //like every other command.
for (QAction *action : m_add_item_actions_group.actions()) {
ShortcutManager::instance().registerAction(
action, "diagrameditor.add_" + action->data().toString(),
@@ -1151,6 +1185,7 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_insert_last_element);
menu_edition -> addAction(m_show_element_picker);
menu_edition -> addAction(m_show_shortcut_bar);
+ menu_edition -> addAction(m_command_search);
menu_edition -> addSeparator();
//The same actions the "Ajouter" toolbar holds. They were toolbar-only,
//which left them unreachable for anyone working without a mouse: a
@@ -1231,6 +1266,7 @@ void QETDiagramEditor::setUpMenu()
menu_affichage -> addMenu(m_text_grid_menu);
menu_affichage -> addAction(m_draw_guides);
menu_affichage -> addAction(m_cell_rulers);
+ menu_affichage -> addAction(m_cell_lines);
menu_affichage -> addMenu(m_background_color_button->menu());
menu_affichage -> addSeparator();
menu_affichage -> addActions(m_zoom_actions_group.actions());
@@ -2066,6 +2102,7 @@ void QETDiagramEditor::slot_updateActions()
m_draw_grid-> setEnabled(opened_diagram);
m_draw_guides-> setEnabled(opened_diagram);
m_cell_rulers-> setEnabled(opened_diagram);
+ m_cell_lines-> setEnabled(opened_diagram);
//Project menu
m_project_edit_properties -> setEnabled(opened_project);
diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h
index 7a6abc53a..f04c3184e 100644
--- a/sources/qetdiagrameditor.h
+++ b/sources/qetdiagrameditor.h
@@ -46,6 +46,7 @@ class RecentFiles;
class DiagramPropertiesEditorDockWidget;
class ElementsCollectionWidget;
class ElementPickerPopup;
+class CommandSearchPopup;
class AutoNumberingDockWidget;
class TerminalNumberingDialog;
@@ -219,6 +220,7 @@ class QETDiagramEditor : public QETMainWindow
*m_draw_grid, ///< Switch the background grid display or not
*m_draw_guides = nullptr, ///< Switch the custom guides display or not
*m_cell_rulers = nullptr, ///< Keep the folio column/row headers in sight or not
+ *m_cell_lines = nullptr, ///< Draw the folio column/row limits across the drawing or not
*m_project_edit_properties, ///< Edit the properties of the current project.
*m_project_add_diagram, ///< Add a diagram to the current project.
*m_remove_diagram_from_project, ///< Delete a diagram from the current project
@@ -278,6 +280,8 @@ class QETDiagramEditor : public QETMainWindow
QAction *m_show_element_picker = nullptr;
QAction *m_show_shortcut_bar = nullptr;
ElementPickerPopup *m_element_picker = nullptr; ///< Built on first use
+ QAction *m_command_search = nullptr;
+ CommandSearchPopup *m_command_search_popup = nullptr; ///< Built on first use
ElementsCollectionWidget *m_element_collection_widget;
/// Last element placed from the collection, for "insert last"
ElementsLocation m_last_inserted_element;
diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt
index 3296bc016..98d3a17fa 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -156,6 +156,19 @@ add_test(NAME tst_qetstrings COMMAND tst_qetstrings)
target_include_directories(tst_qetstrings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_qetstrings PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
+# CommandSearchPopup: accent-blind matching, ranking, and that Enter runs
+# only the owning window's highlighted, enabled command.
+add_executable(
+ tst_commandsearch
+ tst_commandsearch.cpp
+ ${QET_DIR}/sources/commandsearchpopup.cpp
+ ${QET_DIR}/sources/commandsearchpopup.h
+ ${QET_DIR}/sources/shortcutmanager.cpp)
+add_test(NAME tst_commandsearch COMMAND tst_commandsearch)
+set_tests_properties(tst_commandsearch PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
+target_include_directories(tst_commandsearch PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_commandsearch PRIVATE Qt::Test Qt::Widgets)
+
# QETSql::isSingleReadOnlyStatement() -- read-only enforcement for every
# project-database query, including the ones a .qet file carries. Compiles
# sqlreadonly.cpp alone against its own in-memory SQLite, so the security
diff --git a/tests/qttest/tst_commandsearch.cpp b/tests/qttest/tst_commandsearch.cpp
new file mode 100644
index 000000000..b4acca186
--- /dev/null
+++ b/tests/qttest/tst_commandsearch.cpp
@@ -0,0 +1,117 @@
+/*
+ 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 "commandsearchpopup.h"
+#include "shortcutmanager.h"
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ CommandSearchPopup: accent- and case-blind matching, ranking, and that
+ Enter runs the highlighted command of the owning window only.
+*/
+class tst_commandsearch : public QObject
+{
+ Q_OBJECT
+
+private:
+ QAction *add(QWidget *owner, const QString &id, const QString &text)
+ {
+ auto *action = new QAction(text, owner);
+ ShortcutManager::instance().registerAction(action, id, QStringLiteral("test"), QKeySequence());
+ return action;
+ }
+
+ QStringList shown(CommandSearchPopup &popup)
+ {
+ QStringList texts;
+ auto *list = popup.findChild();
+ for (int i = 0 ; i < list->count() ; ++i) {
+ texts << list->item(i)->text();
+ }
+ return texts;
+ }
+
+private slots:
+ void fold_data()
+ {
+ QTest::addColumn("input");
+ QTest::addColumn("expected");
+ QTest::newRow("accents") << QStringLiteral("Éditer l'élément") << QStringLiteral("editer l'element");
+ QTest::newRow("mnemonic") << QStringLiteral("&Fichier") << QStringLiteral("fichier");
+ QTest::newRow("cedilla") << QStringLiteral("Façade") << QStringLiteral("facade");
+ QTest::newRow("plain") << QStringLiteral("zoom") << QStringLiteral("zoom");
+ }
+
+ void fold()
+ {
+ QFETCH(QString, input);
+ QFETCH(QString, expected);
+ QCOMPARE(CommandSearchPopup::fold(input), expected);
+ }
+
+ void ranksAndRunsTheBestMatch()
+ {
+ QWidget owner;
+ QWidget other;
+ QAction *rotate = add(&owner, QStringLiteral("t.rotate"), QStringLiteral("Pivoter"));
+ add(&owner, QStringLiteral("t.rotate_texts"), QStringLiteral("Orienter les textes"));
+ QAction *edit = add(&owner, QStringLiteral("t.edit"), QStringLiteral("Éditer l'item sélectionné"));
+ add(&owner, QStringLiteral("t.text"), QStringLiteral("Ajouter un champ de texte"));
+ //Same id family, another window: must not be listed
+ add(&other, QStringLiteral("t.other"), QStringLiteral("Texte d'une autre fenêtre"));
+
+ CommandSearchPopup popup(&owner);
+ popup.popUpAt(QPoint(0, 0));
+ auto *search = popup.findChild();
+
+ search->setText(QStringLiteral("texte"));
+ //word start ("…textes") before a contained match; other window absent
+ QCOMPARE(shown(popup), (QStringList{QStringLiteral("Ajouter un champ de texte"),
+ QStringLiteral("Orienter les textes")}));
+
+ search->setText(QStringLiteral("editer"));
+ QCOMPARE(shown(popup).value(0), QStringLiteral("Éditer l'item sélectionné"));
+ QSignalSpy edited(edit, &QAction::triggered);
+ QSignalSpy rotated(rotate, &QAction::triggered);
+ QTest::keyClick(&popup, Qt::Key_Return);
+ QCOMPARE(edited.count(), 1);
+ QCOMPARE(rotated.count(), 0);
+ QVERIFY(!popup.isVisible());
+ }
+
+ void disabledCommandsCannotRun()
+ {
+ QWidget owner;
+ QAction *paste = add(&owner, QStringLiteral("u.paste"), QStringLiteral("Coller"));
+ paste->setEnabled(false);
+ CommandSearchPopup popup(&owner);
+ popup.popUpAt(QPoint(0, 0));
+ popup.findChild()->setText(QStringLiteral("coller"));
+ QCOMPARE(shown(popup), QStringList{QStringLiteral("Coller")});
+ QSignalSpy pasted(paste, &QAction::triggered);
+ QTest::keyClick(&popup, Qt::Key_Return);
+ QCOMPARE(pasted.count(), 0);
+ }
+};
+
+QTEST_MAIN(tst_commandsearch)
+#include "tst_commandsearch.moc"