mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-27 20:44:13 +02:00
Merge place-without-drag (with master) into context-menu-placement
This commit is contained in:
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user