Merge remote-tracking branch 'origin/master' into fix-crossref-textfield

This commit is contained in:
Kellermorph
2026-09-16 11:49:55 +02:00
24 changed files with 2892 additions and 1778 deletions
+107
View File
@@ -450,6 +450,34 @@ void Diagram::wheelEvent(QGraphicsSceneWheelEvent *event)
QGraphicsScene::wheelEvent(event);
}
/**
@brief Diagram::event
QGraphicsScene has its own Tab/Shift+Tab item-focus-chain traversal
(mirroring QWidget's), checked before keyPressEvent() is ever reached:
by default it would silently consume Tab/Backtab to move focus among
the scene's own focusable items. Intercepting the key press here,
ahead of that, is the only way to reliably override it: unlike
QWidget::focusNextPrevChild(), QGraphicsScene::focusNextPrevChild() is
only virtual starting in Qt 6 (guarded by the QT6_VIRTUAL macro), so a
Diagram:: override of it would silently do nothing on a Qt 5 build.
@param event
*/
bool Diagram::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *key_event = static_cast<QKeyEvent *>(event);
if ((key_event->key() == Qt::Key_Tab || key_event->key() == Qt::Key_Backtab)
&& !isReadOnly() && !focusItem()) {
bool forward = key_event->key() == Qt::Key_Tab
&& !(key_event->modifiers() & Qt::ShiftModifier);
selectNextItem(forward);
event->accept();
return true;
}
}
return QGraphicsScene::event(event);
}
/**
@brief Diagram::keyPressEvent
This event is managed by diagram event interface if any.
@@ -1902,6 +1930,85 @@ void Diagram::invertSelection()
emit selectionChanged();
}
/**
@brief Diagram::selectAllConductors
Select every conductor on this diagram, deselecting anything else.
*/
void Diagram::selectAllConductors()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<Conductor *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectAllTextFields
Select every text field on this diagram (independent/static text,
conductor labels, and dynamic element texts), deselecting anything else.
*/
void Diagram::selectAllTextFields()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<DiagramTextItem *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectNextItem
Select the next (or, if @a forward is false, the previous) selectable
item on this diagram, cycling through items() (z-order) and wrapping
around at either end. If nothing is currently selected, selects the
first (or last) item. Uses the same "what counts as a real selectable
diagram item" filter as invertSelection(), so the candidate list and
its order always match what the user could reach by clicking.
@param forward true to select the next item, false for the previous one
*/
void Diagram::selectNextItem(bool forward)
{
QList<QGraphicsItem *> candidates;
for (auto item : items()) {
if (dynamic_cast<QetGraphicsItem *>(item) ||
dynamic_cast<DiagramTextItem *>(item) ||
dynamic_cast<Conductor *>(item)) {
candidates << item;
}
}
if (candidates.isEmpty())
return;
int current_index = -1;
for (int i = 0; i < candidates.size(); ++i) {
if (candidates.at(i) -> isSelected()) {
current_index = i;
break;
}
}
int next_index;
if (current_index == -1) {
next_index = forward ? 0 : candidates.size() - 1;
} else {
next_index = forward
? (current_index + 1) % candidates.size()
: (current_index - 1 + candidates.size()) % candidates.size();
}
clearSelection();
QGraphicsItem *next_item = candidates.at(next_index);
next_item -> setSelected(true);
next_item -> ensureVisible();
}
/**
@brief Diagram::insertFolioSeqHash
This class inserts a stringlist containing all
+6 -1
View File
@@ -150,8 +150,11 @@ class Diagram : public QGraphicsScene
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *) override;
bool event(QEvent *event) override;
private:
void selectNextItem(bool forward);
public:
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
@@ -287,6 +290,8 @@ class Diagram : public QGraphicsScene
void selectAll();
void deselectAll();
void invertSelection();
void selectAllConductors();
void selectAllTextFields();
signals:
void showDiagram (Diagram *);
@@ -0,0 +1,241 @@
/*
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 "diagrameventaddpaste.h"
#include "../diagram.h"
#include "../diagramcommands.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/conductor.h"
#include <QApplication>
#include <QClipboard>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QStatusBar>
/**
@brief DiagramEventAddPaste::DiagramEventAddPaste
@param diagram : diagram to paste into
@param start_pos : where the pasted items first appear, in scene
coordinates -- normally the cursor
*/
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
DiagramEventInterface(diagram)
{
//DiagramEventInterface::init() is called by Diagram::setEventInterface
//only when it is replacing an earlier interface, so call it here as
//DiagramEventAddMacro does.
init();
const QString clipboard_text = QApplication::clipboard()->text();
if (clipboard_text.isEmpty()) return;
QDomDocument document_xml;
if (!document_xml.setContent(clipboard_text)) return;
m_diagram->fromXml(document_xml, Diagram::snapToGrid(start_pos), false, &m_content);
if (!m_content.count()) return;
//Remember where each item sits relative to the group's top left, so a
//move is one assignment per item rather than an accumulated delta.
QRectF group_rect;
const QList<QGraphicsItem *> movable = m_content.items(MovableItems);
for (auto *item : movable) {
group_rect = group_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
}
const QPointF top_left = group_rect.topLeft();
for (auto *item : movable) {
m_relative_pos.insert(item, item->pos() - top_left);
}
m_diagram->clearSelection();
for (auto *item : movable) {
item->setSelected(true);
}
if (!m_diagram->views().isEmpty()) {
if (const auto qde = QETApp::diagramEditorAncestorOf(m_diagram->views().at(0))) {
m_status_bar = qde->statusBar();
}
}
showHint();
m_running = true;
}
/**
@brief DiagramEventAddPaste::~DiagramEventAddPaste
If the placement never finished -- the editor closed, or another tool took
over -- the items are still on the folio with nothing on the undo stack to
account for them, so take them away.
*/
DiagramEventAddPaste::~DiagramEventAddPaste()
{
if (!m_finished && m_diagram) {
cancel();
}
if (m_status_bar) {
m_status_bar->clearMessage();
}
}
/**
@brief DiagramEventAddPaste::clipboardHasDiagram
@return true if the clipboard holds a diagram fragment
*/
bool DiagramEventAddPaste::clipboardHasDiagram()
{
return Diagram::clipboardMayContainDiagram();
}
/**
@brief DiagramEventAddPaste::init
Suppress the context menu while placing, so a right click can cancel
instead of opening a menu over the items being positioned.
*/
void DiagramEventAddPaste::init()
{
if (!m_diagram) return;
const auto views = m_diagram->views();
for (auto *view : views) {
view->setContextMenuPolicy(Qt::NoContextMenu);
}
}
void DiagramEventAddPaste::showHint()
{
if (m_status_bar) {
m_status_bar->showMessage(
tr("Cliquez pour poser le collage, Échap ou clic droit pour annuler",
"status bar tip while positioning a paste"));
}
}
/**
@brief DiagramEventAddPaste::moveTo
Put the group's top left corner at @a scene_pos, snapped to the grid.
*/
void DiagramEventAddPaste::moveTo(const QPointF &scene_pos)
{
const QPointF anchor = Diagram::snapToGrid(scene_pos);
for (auto it = m_relative_pos.constBegin() ; it != m_relative_pos.constEnd() ; ++it) {
if (it.key()) {
it.key()->setPos(anchor + it.value());
}
}
}
void DiagramEventAddPaste::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_running) return;
moveTo(event->scenePos());
event->setAccepted(true);
}
void DiagramEventAddPaste::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_running) return;
//Swallowed so the press cannot start a rubber band or drag an item
//out of the group; the release is what decides.
event->setAccepted(true);
}
void DiagramEventAddPaste::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_running) return;
if (event->button() == Qt::LeftButton) {
moveTo(event->scenePos());
commit();
} else if (event->button() == Qt::RightButton) {
cancel();
}
event->setAccepted(true);
}
void DiagramEventAddPaste::keyPressEvent(QKeyEvent *event)
{
if (!m_running) return;
switch (event->key()) {
case Qt::Key_Escape:
cancel();
event->setAccepted(true);
break;
//Return and Enter drop the paste where it stands, so the whole
//operation can be completed without a mouse.
case Qt::Key_Return:
case Qt::Key_Enter:
commit();
event->setAccepted(true);
break;
default:
break;
}
}
/**
@brief DiagramEventAddPaste::commit
Hand the items to the undo stack where they stand.
PasteDiagramCommand's first redo() does not add the items to the scene --
it assumes they are already there, which is what Diagram::fromXml did when
this started. So pushing it here adopts them rather than duplicating them.
*/
void DiagramEventAddPaste::commit()
{
if (m_finished || !m_diagram) return;
m_finished = true;
m_running = false;
m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_content));
emit finish();
}
/**
@brief DiagramEventAddPaste::cancel
Take the items back off the folio. Nothing was pushed to the undo stack,
so there is nothing to undo afterwards.
*/
void DiagramEventAddPaste::cancel()
{
if (m_finished || !m_diagram) return;
m_finished = true;
m_running = false;
//Conductors first: they hold pointers to the terminals of the
//elements below, so removing an element out from under one would
//leave it pointing at freed memory for as long as it is still in the
//scene.
const QList<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
for (auto *conductor : conductors) {
m_diagram->removeItem(conductor);
delete conductor;
}
const QList<QGraphicsItem *> rest = m_content.items(MovableItems);
for (auto *item : rest) {
m_diagram->removeItem(item);
delete item;
}
m_content.clear();
m_relative_pos.clear();
emit finish();
}
@@ -0,0 +1,85 @@
/*
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 DIAGRAMEVENTADDPASTE_H
#define DIAGRAMEVENTADDPASTE_H
#include "diagrameventinterface.h"
#include "../diagramcontent.h"
#include <QHash>
#include <QPointer>
class QStatusBar;
/**
@brief The DiagramEventAddPaste class
Paste the clipboard as a placement you can still move.
The pasted items are added straight away and follow the cursor until a
left click drops them; Escape or a right click takes them away again.
This is the same interaction as placing a new element, so a paste behaves
like every other "put something on the folio" action.
The items are the real ones from the start, not a preview: Diagram::fromXml
creates them, this class moves them, and PasteDiagramCommand is pushed only
once they are dropped. That keeps one copy of the paste logic rather than
two, and means a cancelled paste leaves nothing on the undo stack.
*/
class DiagramEventAddPaste : public DiagramEventInterface
{
Q_OBJECT
public:
///Items with a position of their own. Conductors are left out
///deliberately: they are drawn from their terminals, so they
///follow when the elements they attach to move.
static const int MovableItems =
DiagramContent::Elements
| DiagramContent::TextFields
| DiagramContent::Images
| DiagramContent::Shapes
| DiagramContent::Tables
| DiagramContent::TerminalStrip;
DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos);
~DiagramEventAddPaste() override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void init() override;
///@return true if the clipboard holds something this can paste.
static bool clipboardHasDiagram();
private:
void moveTo(const QPointF &scene_pos);
void commit();
void cancel();
void showHint();
DiagramContent m_content;
///Each movable item's position relative to the group's top left,
///taken once so repeated moves cannot accumulate rounding drift.
QHash<QGraphicsItem *, QPointF> m_relative_pos;
QPointer<QStatusBar> m_status_bar;
bool m_finished{false};
};
#endif // DIAGRAMEVENTADDPASTE_H
+89 -15
View File
@@ -702,6 +702,27 @@ void DiagramView::focusInEvent(QFocusEvent *e) {
}
}
/**
@brief DiagramView::focusNextPrevChild
By default, QWidget intercepts Tab/Shift+Tab to move keyboard focus to
the next/previous widget before a key press event is ever generated,
which would silently swallow the diagram's Tab-based item-selection
cycling (see Diagram::event()). Returning false here disables that
automatic focus-chain traversal for this view, so Tab/Shift+Tab reach
keyPressEvent() (and from there, the scene) as ordinary key presses
instead.
@return always false
*/
bool DiagramView::focusNextPrevChild(bool next)
{
//Escape asked for focus to leave; allow exactly this one traversal.
if (m_releasing_focus) {
m_releasing_focus = false;
return QGraphicsView::focusNextPrevChild(next);
}
return false;
}
/**
@brief DiagramView::keyPressEvent
Handles "key press" events. Reimplemented here to switch to visualisation
@@ -717,6 +738,19 @@ void DiagramView::keyPressEvent(QKeyEvent *e)
DiagramContent dc(m_diagram);
switch(e -> key())
{
case Qt::Key_Escape:
//Tab cycles the folio's items rather than moving focus (see
//focusNextPrevChild above), so without this there would be no
//way off the canvas for someone working without a mouse.
//Escape steps back out: first it drops the selection, then it
//hands focus to the next widget.
if (m_diagram && !m_diagram->selectedItems().isEmpty()) {
m_diagram->clearSelection();
} else {
m_releasing_focus = true;
focusNextChild();
}
return;
case Qt::Key_PageUp:
current_project->changeTabUp();
return;
@@ -1218,30 +1252,70 @@ QList<QAction *> DiagramView::contextMenuActions() const
*/
void DiagramView::contextMenuEvent(QContextMenuEvent *e)
{
QGraphicsView::contextMenuEvent(e);
if(e->isAccepted())
return;
QPoint menu_pos = e->pos();
QPoint menu_global_pos = e->globalPos();
//A context menu raised from the keyboard (the Menu key, or
//Shift+F10) carries no useful position: Qt does not aim it at the
//selection. Two things then went wrong. QGraphicsView handed the
//event to whichever item held focus, which answered with its own
//generic Undo/Cut/Copy menu and accepted it, so the folio's real
//menu was never built; and had it got past that, itemAt() below
//would have looked up an unrelated point.
//
//So a keyboard-raised menu is built here directly rather than being
//offered to the items first, and aimed at the selection when there
//is one. The keyboard then gets the folio's menu, which is what a
//right-click gets.
const bool from_keyboard = e->reason() == QContextMenuEvent::Keyboard;
if (auto qgi = m_diagram->itemAt(mapToScene(e->pos()), transform()))
if (from_keyboard)
{
if (!qgi->isSelected()) {
m_diagram->clearSelection();
//Aim at the selection when there is one, so the menu appears
//beside what it acts on. With nothing selected there is nothing
//to aim at, so use the middle of the view -- the folio's own
//menu is still the right menu to show.
const auto selection = m_diagram->selectedItems();
if (!selection.isEmpty())
{
QRectF selection_rect;
for (auto *item : selection) {
selection_rect |= item->sceneBoundingRect();
}
menu_pos = mapFromScene(selection_rect.center());
}
else
{
menu_pos = viewport()->rect().center();
}
menu_global_pos = viewport()->mapToGlobal(menu_pos);
}
else
{
QGraphicsView::contextMenuEvent(e);
if(e->isAccepted())
return;
// At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem.
// When we call clearSelection the parent item of the handler
// is deselected and so delete all handlers, in this case,
// qgi become a dangling pointer.
// we need to call again itemAt.
if (auto item_ = m_diagram->itemAt(mapToScene(e->pos()), transform())) {
item_->setSelected(true);
if (auto qgi = m_diagram->itemAt(mapToScene(menu_pos), transform()))
{
if (!qgi->isSelected()) {
m_diagram->clearSelection();
}
// At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem.
// When we call clearSelection the parent item of the handler
// is deselected and so delete all handlers, in this case,
// qgi become a dangling pointer.
// we need to call again itemAt.
if (auto item_ = m_diagram->itemAt(mapToScene(menu_pos), transform())) {
item_->setSelected(true);
}
}
}
if (m_diagram->selectedItems().isEmpty())
{
m_paste_here_pos = e->pos();
m_paste_here_pos = menu_pos;
m_paste_here->setEnabled(Diagram::clipboardMayContainDiagram());
}
@@ -1250,7 +1324,7 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
{
QMenu *context_menu = new QMenu(this);
context_menu->addActions(list);
context_menu->popup(e->globalPos());
context_menu->popup(menu_global_pos);
e->accept();
}
}
+3
View File
@@ -80,6 +80,9 @@ class DiagramView : public QGraphicsView
void keyPressEvent(QKeyEvent *) override;
void keyReleaseEvent(QKeyEvent *) override;
bool event(QEvent *) override;
bool focusNextPrevChild(bool next) override;
///Set for one call only, by the Escape handler, to let focus leave the view.
bool m_releasing_focus = false;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
+33 -5
View File
@@ -546,12 +546,40 @@ QString QET::joinWithSpaces(const QStringList &string_list) {
QStringList QET::splitWithSpaces(const QString &string) {
// les chaines sont separees par des espaces non echappes
// = avec un nombre nul ou pair de backslashes devant
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),Qt::SkipEmptyParts);
//
// This was a QRegularExpression("[^\\]?(?:\\\\)* ") split, which never
// worked: "[^\]" opens a character class whose "\]" is an escaped
// bracket, so the class is never closed and the pattern is invalid.
// QRegularExpression::isValid() was false, QString::split() warned
// "invalid QRegularExpression object" and returned an EMPTY list for
// every input -- so a second instance's file arguments were always
// dropped (bugtracker #248), not just ones containing spaces.
//
// A correct pattern is not expressible here either: the separator is a
// space preceded by an even-length run of backslashes, and PCRE2 has no
// variable-length lookbehind. Scanning explicitly is both correct and
// easier to read than the alternatives.
QStringList returned_list;
foreach(QString escaped_string, escaped_strings) {
returned_list << QET::unescapeSpaces(escaped_string);
QString current;
int backslashes = 0;
for (const QChar &c : string) {
if (c == QLatin1Char('\\')) {
++backslashes;
current += c;
continue;
}
if (c == QLatin1Char(' ') && backslashes % 2 == 0) {
if (!current.isEmpty()) {
returned_list << QET::unescapeSpaces(current);
}
current.clear();
} else {
current += c;
}
backslashes = 0;
}
if (!current.isEmpty()) {
returned_list << QET::unescapeSpaces(current);
}
return(returned_list);
}
+25 -1
View File
@@ -1659,7 +1659,31 @@ void QETApp::receiveMessage(int instanceId, QByteArray message)
{
QString my_message(str.mid(20));
QStringList args_list = QET::splitWithSpaces(my_message);
openFiles(QETArguments(args_list));
// Deferred, not called directly.
//
// This slot runs inside SingleApplication's readyRead handling:
// SingleApplicationPrivate::slotDataAvailable() emits
// receivedMessage() synchronously from the socket's readyRead
// lambda. openFiles() then loads a project -- seconds of work on
// a large one -- and openAndAddProject() puts up a modal
// BackupDialog, whose exec() runs a nested event loop while the
// socket handler is still on the stack.
//
// During that nested loop the secondary instance exits, the
// connection closes and the QLocalSocket is deleted. When the
// dialog is dismissed and the stack unwinds, QMetaObject::
// activate() continues emitting on the freed sender and the
// process dies. Reported with a backtrace on PR #861;
// reproduced on Qt 6.10.2 by dismissing the dialog, which is the
// step that makes it fail -- leaving it open never unwinds.
//
// A zero-timer returns to the event loop first, so the socket
// stack is fully unwound before any of this runs.
const QETArguments deferred_args{args_list};
QTimer::singleShot(0, this, [this, deferred_args]() {
openFiles(deferred_args);
});
}
}
+42 -2
View File
@@ -29,6 +29,7 @@
#include "diagramevent/diagrameventaddshape.h"
#include "diagramevent/diagrameventaddpath.h"
#include "diagramevent/diagrameventaddtext.h"
#include "diagramevent/diagrameventaddpaste.h"
#include "diagramview.h"
#include "elementspanelwidget.h"
#include "factory/qetgraphicstablefactory.h"
@@ -349,8 +350,21 @@ void QETDiagramEditor::setUpActions()
currentDiagramView()->copy();
});
connect(m_paste, &QAction::triggered, [this]() {
if(currentDiagramView())
currentDiagramView()->paste();
auto *dv = currentDiagramView();
if (!dv || !dv->diagram()) return;
//Paste as a placement rather than dropping the items straight
//down. Pasting in place put the copy exactly on top of the
//original, where it was easy to miss entirely; now it appears
//under the cursor and follows it until a click, Return, or Escape
//to cancel -- the same interaction as placing a new element.
const QPoint view_pos = dv->viewport()->mapFromGlobal(QCursor::pos());
const QPointF start_pos = dv->viewport()->rect().contains(view_pos)
? dv->mapToScene(view_pos)
: dv->mapToScene(dv->viewport()->rect().center());
dv->diagram()->setEventInterface(
new DiagramEventAddPaste(dv->diagram(), start_pos));
});
//Reset conductor path
@@ -688,18 +702,28 @@ void QETDiagramEditor::setUpActions()
QAction *select_all = m_select_actions_group.addAction( QET::Icons::EditSelectAll, tr("Tout sélectionner") );
QAction *select_nothing = m_select_actions_group.addAction( QET::Icons::EditSelectNone, tr("Désélectionner tout") );
QAction *select_invert = m_select_actions_group.addAction( QET::Icons::EditSelectInvert, tr("Inverser la sélection") );
QAction *select_all_conductors = m_select_actions_group.addAction( QET::Icons::Conductor, tr("Sélectionner tous les conducteurs") );
QAction *select_all_text_fields = m_select_actions_group.addAction( QET::Icons::PartTextField, tr("Sélectionner tous les champs de texte") );
ShortcutManager::instance().registerAction(select_all, "diagrameditor.select_all", tr("Éditeur de schémas"), QKeySequence::SelectAll);
ShortcutManager::instance().registerAction(select_nothing, "diagrameditor.select_nothing", tr("Éditeur de schémas"), QKeySequence::Deselect);
ShortcutManager::instance().registerAction(select_invert, "diagrameditor.select_invert", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_I);
//No default sequence for these two: they are menu actions, and the
//point of registering them is so a user can bind one if they want.
ShortcutManager::instance().registerAction(select_all_conductors, "diagrameditor.select_all_conductors", tr("Éditeur de schémas"), QKeySequence());
ShortcutManager::instance().registerAction(select_all_text_fields, "diagrameditor.select_all_text_fields", tr("Éditeur de schémas"), QKeySequence());
select_all ->setStatusTip( tr("Sélectionne tous les éléments du folio", "status bar tip") );
select_nothing->setStatusTip( tr("Désélectionne tous les éléments du folio", "status bar tip") );
select_invert ->setStatusTip( tr("Désélectionne les éléments sélectionnés et sélectionne les éléments non sélectionnés", "status bar tip") );
select_all_conductors ->setStatusTip( tr("Sélectionne tous les conducteurs du folio, désélectionne le reste", "status bar tip") );
select_all_text_fields->setStatusTip( tr("Sélectionne tous les champs de texte du folio, désélectionne le reste", "status bar tip") );
select_all ->setData("select_all");
select_nothing->setData("deselect");
select_invert ->setData("invert_selection");
select_all_conductors ->setData("select_all_conductors");
select_all_text_fields->setData("select_all_text_fields");
connect(&m_select_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::selectGroupTriggered);
@@ -934,6 +958,15 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_cut);
menu_edition -> addAction(m_copy);
menu_edition -> addAction(m_paste);
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
//toolbar button has no key, so text fields, images and every drawing
//shape simply could not be added. m_depth_action_group below has
//always been in both places; this brings these into line with it.
QMenu *menu_add_item = menu_edition -> addMenu(tr("A&jouter"));
menu_add_item -> setIcon(QET::Icons::Add);
menu_add_item -> addActions(m_add_item_actions_group.actions());
menu_edition -> addSeparator();
menu_edition -> addActions(m_select_actions_group.actions());
menu_edition -> addSeparator();
@@ -952,6 +985,9 @@ void QETDiagramEditor::setUpMenu()
// menu Projet
menu_project -> addAction(m_project_edit_properties);
menu_project -> addAction(m_auto_conductor);
//Sits beside m_auto_conductor, the setting it pairs with. It was
//toolbar-only and so had no keyboard route at all.
menu_project -> addAction(m_auto_break_conductor);
menu_project -> addSeparator();
menu_project -> addAction(m_project_add_diagram);
menu_project -> addAction(m_remove_diagram_from_project);
@@ -1584,6 +1620,10 @@ void QETDiagramEditor::selectGroupTriggered(QAction *action)
diagram->deselectAll();
else if (value == "invert_selection")
diagram->invertSelection();
else if (value == "select_all_conductors")
diagram->selectAllConductors();
else if (value == "select_all_text_fields")
diagram->selectAllTextFields();
}
/**
+16 -1
View File
@@ -574,8 +574,23 @@ void Conductor::paint(QPainter *painter, const QStyleOptionGraphicsItem *options
painter -> setPen(final_conductor_pen);
painter -> setBrush(junction_brush);
painter -> setRenderHint(QPainter::Antialiasing, true);
// The junction dot has to read as a dot on top of the conductor
// that carries it, so it scales with the conductor width instead
// of being a fixed 3.0 across: on a wide conductor a 3.0 dot is
// narrower than the line and simply disappears (bugtracker #108).
//
// Floored at the historic 3.0 so nothing changes for the default
// width of 1.0 or anything thinner -- only the wide conductors
// the report is about are affected. m_properties.cond_size is
// used rather than the pen, whose width is inflated by 4 while
// the mouse is over the conductor.
const qreal junction_diameter = qMax(3.0, 3.0 * m_properties.cond_size);
const qreal junction_radius = junction_diameter / 2.0;
foreach(QPointF point, junctions_list) {
painter -> drawEllipse(QRectF(point.x() - 1.5, point.y() - 1.5, 3.0, 3.0));
painter -> drawEllipse(QRectF(point.x() - junction_radius,
point.y() - junction_radius,
junction_diameter,
junction_diameter));
}
}
+38
View File
@@ -19,6 +19,7 @@
#include <QWhatsThis>
#include <QMenu>
#include <QMenuBar>
#include <QShortcut>
#include <QDragEnterEvent>
#include <QDesktopServices>
@@ -41,6 +42,14 @@ QETMainWindow::QETMainWindow(QWidget *widget, Qt::WindowFlags flags) :
initCommonMenus();
setAcceptDrops(true);
//A shortcut rather than a key handler: a key press goes to the
//focused child widget, so a keyPressEvent() here would never see F10
//while the canvas or a panel holds focus.
QShortcut *menu_bar_shortcut = new QShortcut(QKeySequence(Qt::Key_F10), this);
menu_bar_shortcut -> setContext(Qt::WindowShortcut);
connect(menu_bar_shortcut, &QShortcut::activated,
this, &QETMainWindow::activateMenuBar);
}
/**
@@ -251,6 +260,35 @@ void QETMainWindow::checkToolbarsmenu()
/**
Handle the \a e event.
*/
/**
@brief QETMainWindow::activateMenuBar
Open the first usable menu, as pressing Alt and a menu's letter would.
F10 is what most applications use for this, and QMenuBar does not handle
it: given the key directly it leaves it unaccepted, and sent to the window
it never reaches the menu bar at all, because a key press goes to the
focused child widget. So the press fell through to whichever widget had
focus and looked like nothing happening.
This is convenience, not access. Qt already provides two keyboard routes
into the menus and both work: a bare Alt tap focuses the bar, and Alt with
a menu's letter opens it. This adds the key people reach for out of habit.
A shortcut rather than a keyPressEvent() override, for the reason above --
the window never sees the key while a child holds focus.
*/
void QETMainWindow::activateMenuBar() {
QMenuBar *bar = menuBar();
if (!bar) return;
for (QAction *action : bar -> actions()) {
if (action -> isVisible() && action -> isEnabled() && action -> menu()) {
bar -> setActiveAction(action);
return;
}
}
}
bool QETMainWindow::event(QEvent *e) {
if (e -> type() == QEvent::WindowStateChange) {
updateFullScreenAction();
+1
View File
@@ -39,6 +39,7 @@ class QETMainWindow : public QMainWindow {
QAction *actionForMenu(QMenu *);
protected:
void activateMenuBar();
bool event(QEvent *) override;
void dragEnterEvent(QDragEnterEvent *e) override;
void dropEvent(QDropEvent *e) override;
+77 -32
View File
@@ -29,6 +29,7 @@
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QCheckBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -36,6 +37,7 @@
#include <QAction>
#include <QFont>
#include <QTimer>
#include <QSettings>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -56,12 +58,16 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
main_layout->addWidget(m_unlink_pb, 0, 1);
main_layout->addWidget(m_show_this_pb, 0, 2);
// Row 1: Search field
// Row 1: Hide linked elements checkbox
m_hide_linked_cb = new QCheckBox(tr("Masquer les éléments connectés"), this);
main_layout->addWidget(m_hide_linked_cb, 1, 0, 1, 3);
// Row 2: Search field
m_search_field = new QLineEdit(this);
m_search_field->setPlaceholderText(tr("Recherche"));
main_layout->addWidget(m_search_field, 1, 0, 1, 3);
main_layout->addWidget(m_search_field, 2, 0, 1, 3);
// Row 2: Tree widget
// Row 3: Tree widget
m_tree_widget = new QTreeWidget(this);
m_tree_widget->setHeaderLabels({
tr("Label"), tr("Type"), tr("Adresse"),
@@ -78,9 +84,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
main_layout->addWidget(m_tree_widget, 2, 0, 1, 3);
main_layout->addWidget(m_tree_widget, 3, 0, 1, 3);
// Row 3: Hidden masters note
// Row 4: Hidden masters note
m_hidden_masters_label = new QLabel(
tr("Remarque : les éléments maîtres ayant atteint leur nombre maximal "
"d'esclaves sont masqués."), this);
@@ -89,9 +95,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
italic_font.setItalic(true);
m_hidden_masters_label->setFont(italic_font);
m_hidden_masters_label->hide();
main_layout->addWidget(m_hidden_masters_label, 3, 0, 1, 3);
main_layout->addWidget(m_hidden_masters_label, 4, 0, 1, 3);
main_layout->setRowStretch(2, 1);
main_layout->setRowStretch(3, 1);
setMinimumWidth(500);
@@ -104,6 +110,12 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
this, &PlcLinkWidget::on_m_search_field_textEdited);
connect(m_tree_widget, &QTreeWidget::customContextMenuRequested,
this, &PlcLinkWidget::on_m_tree_widget_customContextMenuRequested);
connect(m_hide_linked_cb, &QCheckBox::toggled,
this, &PlcLinkWidget::on_m_hide_linked_cb_toggled);
QSettings settings;
m_hide_linked_cb->setChecked(
settings.value(QStringLiteral("plclinkwidget/hideLinked"), false).toBool());
if (elmt)
setElement(elmt);
@@ -163,6 +175,7 @@ void PlcLinkWidget::buildPlcTree()
{
m_tree_widget->clear();
m_io_entry_hash.clear();
m_linked_children.clear();
if (!m_element || !m_element->diagram() || !m_element->diagram()->project())
return;
@@ -216,6 +229,7 @@ void PlcLinkWidget::buildPlcTree()
parent_item->setExpanded(false);
// Add child items for each IO entry
bool all_children_hidden = true;
for (int i = 0; i < plc_data.ios.size(); ++i) {
const auto &io = plc_data.ios.at(i);
auto *child_item = new QTreeWidgetItem(parent_item);
@@ -233,36 +247,54 @@ void PlcLinkWidget::buildPlcTree()
entry.ioIndex = i;
m_io_entry_hash.insert(child_item, entry);
// If this IO is already linked to a slave, grey it out and strike through
// If this IO is already linked to a slave
if (used_io_indices.contains(i)) {
QFont strike_font = child_item->font(0);
strike_font.setStrikeOut(true);
child_item->setFont(0, strike_font);
child_item->setFont(1, strike_font);
child_item->setFont(2, strike_font);
child_item->setFont(3, strike_font);
child_item->setFont(4, strike_font);
child_item->setFont(5, strike_font);
m_linked_children.insert(child_item);
if (m_hide_linked_cb->isChecked()) {
child_item->setHidden(true);
} else {
all_children_hidden = false;
QFont strike_font = child_item->font(0);
strike_font.setStrikeOut(true);
child_item->setFont(0, strike_font);
child_item->setFont(1, strike_font);
child_item->setFont(2, strike_font);
child_item->setFont(3, strike_font);
child_item->setFont(4, strike_font);
child_item->setFont(5, strike_font);
QBrush grey_brush(Qt::gray);
for (int col = 0; col < 6; ++col)
child_item->setForeground(col, grey_brush);
QBrush grey_brush(Qt::gray);
for (int col = 0; col < 6; ++col)
child_item->setForeground(col, grey_brush);
// Show which slave is linked
for (Element *linked : elmt->linkedElements()) {
if (elmt->groupIndexForElement(linked) == i) {
child_item->setToolTip(0,
tr("Lié à: %1").arg(linked->actualLabel()));
break;
// Show which slave is linked
for (Element *linked : elmt->linkedElements()) {
if (elmt->groupIndexForElement(linked) == i) {
child_item->setToolTip(0,
tr("Lié à: %1").arg(linked->actualLabel()));
break;
}
}
}
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
}
} else {
all_children_hidden = false;
}
}
// If checkbox is on and every child is linked (hidden), hide the master too
if (m_hide_linked_cb->isChecked() && all_children_hidden) {
parent_item->setHidden(true);
}
}
}
bool PlcLinkWidget::isChildLinked(QTreeWidgetItem *child) const
{
return m_linked_children.contains(child);
}
void PlcLinkWidget::hideButtons()
{
m_label->hide();
@@ -296,15 +328,21 @@ void PlcLinkWidget::on_m_search_field_textEdited(const QString &text)
}
}
}
child->setHidden(!match);
if (match) any_child_visible = true;
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
child->setHidden(!match || hidden);
if (match && !hidden) any_child_visible = true;
}
// Also check if parent label matches
if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) {
any_child_visible = true;
for (int j = 0; j < parent->childCount(); ++j)
parent->child(j)->setHidden(false);
for (int j = 0; j < parent->childCount(); ++j) {
QTreeWidgetItem *child = parent->child(j);
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
if (!hidden) {
any_child_visible = true;
child->setHidden(false);
}
}
}
parent->setHidden(!any_child_visible);
@@ -383,3 +421,10 @@ void PlcLinkWidget::on_m_show_this_pb_clicked()
m_element->diagram()->showMe();
m_element->setHighlighted(true);
}
void PlcLinkWidget::on_m_hide_linked_cb_toggled(bool checked)
{
QSettings settings;
settings.setValue(QStringLiteral("plclinkwidget/hideLinked"), checked);
buildPlcTree();
}
+5
View File
@@ -29,6 +29,7 @@ class QTreeWidget;
class QLineEdit;
class QPushButton;
class QLabel;
class QCheckBox;
class Element;
/**
@@ -58,17 +59,20 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
void buildPlcTree();
void hideButtons();
void showButtons();
bool isChildLinked(QTreeWidgetItem *child) const;
private slots:
void on_m_search_field_textEdited(const QString &text);
void on_m_tree_widget_customContextMenuRequested(const QPoint &pos);
void on_m_unlink_pb_clicked();
void on_m_show_this_pb_clicked();
void on_m_hide_linked_cb_toggled(bool checked);
private:
QLabel *m_label{nullptr};
QPushButton *m_unlink_pb{nullptr};
QPushButton *m_show_this_pb{nullptr};
QCheckBox *m_hide_linked_cb{nullptr};
QLineEdit *m_search_field{nullptr};
QTreeWidget *m_tree_widget{nullptr};
QLabel *m_hidden_masters_label{nullptr};
@@ -79,6 +83,7 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
int ioIndex = -1;
};
QHash<QTreeWidgetItem*, PlcIoEntry> m_io_entry_hash;
QSet<QTreeWidgetItem*> m_linked_children;
Element *m_element_to_link = nullptr;
int m_pending_io_index = -1;