Merge place-without-drag (with master) into context-menu-placement

This commit is contained in:
ispyisail
2026-09-26 22:43:06 +12:00
14 changed files with 645 additions and 14 deletions
+2
View File
@@ -285,6 +285,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/recentfiles.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/shortcutmanager.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
+26 -4
View File
@@ -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));
}
+2
View File
@@ -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
+236
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "commandsearchpopup.h"
#include "shortcutmanager.h"
#include <QAction>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QScreen>
#include <QVBoxLayout>
#include <algorithm>
/**
@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<QPair<int, const Command *>> 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<void *>(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<QAction *>(item->data(Qt::UserRole).value<void *>());
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);
}
+65
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef COMMANDSEARCHPOPUP_H
#define COMMANDSEARCHPOPUP_H
#include <QFrame>
#include <QList>
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<Command> m_commands;
};
#endif // COMMANDSEARCHPOPUP_H
+92 -8
View File
@@ -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"
@@ -116,6 +117,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();
@@ -1258,6 +1260,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
@@ -1272,17 +1340,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);
@@ -1291,9 +1366,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();
@@ -1343,11 +1421,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)
+5 -2
View File
@@ -70,7 +70,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;
@@ -86,11 +87,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;
@@ -105,6 +107,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;
+5
View File
@@ -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);
+43
View File
@@ -22,6 +22,7 @@
#include <QCoreApplication>
#include <QToolButton>
#include "ElementsCollection/elementscollectionwidget.h"
#include "commandsearchpopup.h"
#include "QWidgetAnimation/qwidgetanimation.h"
#include "autoNum/ui/autonumberingdockwidget.h"
#include "conductornumexport.h"
@@ -541,6 +542,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);
@@ -822,6 +835,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);
m_delete_selection->setStatusTip( tr("Enlève les éléments sélectionnés du folio", "status bar tip"));
m_rotate_selection->setStatusTip( tr("Pivote les éléments et textes sélectionnés", "status bar tip"));
@@ -954,6 +987,13 @@ 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 found by the command
//search and bound in the Shortcuts page, like every other command.
for (QAction *action : m_add_item_actions_group.actions()) {
ShortcutManager::instance().registerAction(
action, "diagrameditor.add_" + action->data().toString(),
tr("Éditeur de schémas"), QKeySequence());
}
//Depth action
m_depth_action_group = QET::depthActionGroup(this);
@@ -1108,6 +1148,7 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_duplicate);
menu_edition -> addAction(m_configure_duplicate);
menu_edition -> addAction(m_insert_last_element);
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
@@ -1188,6 +1229,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());
@@ -2021,6 +2063,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);
+4
View File
@@ -45,6 +45,7 @@ class ElementsLocation;
class RecentFiles;
class DiagramPropertiesEditorDockWidget;
class ElementsCollectionWidget;
class CommandSearchPopup;
class AutoNumberingDockWidget;
class TerminalNumberingDialog;
@@ -217,6 +218,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
@@ -272,6 +274,8 @@ class QETDiagramEditor : public QETMainWindow
*m_qdw_elmt_collection,
*qdw_undo; /// Dock for the undo list
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;
+33
View File
@@ -197,3 +197,36 @@ bool ShortcutManager::trigger(const QString &id) const
}
return false;
}
/**
@return the QAction registered under @a id that belongs to @a owner --
that is, has @a owner among its ancestors -- or nullptr. Several windows
of the same kind each register their own action under one id, so a
window asking for "its" action has to say which window it is.
@param id
@param owner : the window, or nullptr for the first live action
*/
QAction *ShortcutManager::action(const QString &id, const QObject *owner) const
{
auto it = m_entries.find(id);
if (it == m_entries.end()) {
return nullptr;
}
for (const QPointer<QObject> &target : qAsConst(it->targets))
{
auto *action = qobject_cast<QAction *>(target.data());
if (!action) {
continue;
}
if (!owner) {
return action;
}
for (const QObject *o = action->parent(); o; o = o->parent()) {
if (o == owner) {
return action;
}
}
}
return nullptr;
}
+2
View File
@@ -26,6 +26,7 @@
#include <QStringList>
class QObject;
class QAction;
/**
@brief The ShortcutManager class
@@ -84,6 +85,7 @@ class ShortcutManager
/// multi-window case, not a guaranteed-correct dispatch.
/// @return whether a live target was found and triggered.
bool trigger(const QString &id) const;
QAction *action(const QString &id, const QObject *owner) const;
private:
ShortcutManager() = default;
+13
View File
@@ -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
+117
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "commandsearchpopup.h"
#include "shortcutmanager.h"
#include <QAction>
#include <QLineEdit>
#include <QListWidget>
#include <QSignalSpy>
#include <QTest>
/**
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<QListWidget *>();
for (int i = 0 ; i < list->count() ; ++i) {
texts << list->item(i)->text();
}
return texts;
}
private slots:
void fold_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("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<QLineEdit *>();
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<QLineEdit *>()->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"