Diagram: Tab/Shift+Tab item-selection cycling + select-all-conductors/text-fields (#574)

Implements the second pillar of #574: keyboard-driven selection on the
diagram canvas.

Tab / Shift+Tab select the next / previous item on the current
diagram, cycling through items() (z-order) and wrapping at either
end. If nothing is selected, Tab selects the first item and
Shift+Tab the last. Skipped while a text item has focus, for the
same reason arrow-key movement already guards on !focusItem().
Candidates use the same "what counts as a real selectable diagram
item" filter (QetGraphicsItem / DiagramTextItem / Conductor) already
established by Diagram::invertSelection(), so the cycling order
always matches what a user could reach by clicking.

Getting Tab to actually reach the scene needed two separate fixes,
each independently discovered by empirical testing rather than
assumption:

- QWidget (DiagramView) intercepts Tab/Backtab for widget focus-chain
  traversal before generating a key event at all. Overriding
  DiagramView::focusNextPrevChild() to return false disables that.
- QGraphicsScene (Diagram) has its own, separate item-focus-chain
  traversal, checked before keyPressEvent() is ever reached. The
  obvious fix -- overriding Diagram::focusNextPrevChild() the same
  way -- silently does nothing on Qt 5, because
  QGraphicsScene::focusNextPrevChild() only becomes virtual in Qt 6
  (guarded by the QT6_VIRTUAL macro); a compile error surfaced this
  immediately when attempted directly, rather than shipping a fix
  that worked on Qt 6 and silently no-opped on Qt 5. Intercepting
  QEvent::KeyPress in Diagram::event() instead is virtual on every Qt
  version and sidesteps the scene's internal traversal entirely.

Also adds Diagram::selectAllConductors() / selectAllTextFields(),
wired up as two new actions in the existing select_all /
select_nothing / select_invert action group in
qetdiagrameditor.cpp, so they appear in the Edit menu and go through
the same QAction -> data() -> selectGroupTriggered() dispatch as the
existing selection commands.

Verified end-to-end in a real running session (Xvfb + xdotool) with
a multi-transistor schematic: Tab/Shift+Tab correctly move a single
selection forward/backward through elements and text fields
(confirmed via the properties panel updating to each new item and
the visual selection box moving on canvas); Tab/Shift+Tab from no
selection correctly select the first/last item; "Select all
conductors" and "Select all text fields" each correctly select every
matching item and deselect everything else.

See discussion #574.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-08-01 00:10:37 +12:00
parent 199444b6db
commit 88823ea35f
5 changed files with 141 additions and 1 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 *);
+17
View File
@@ -702,6 +702,23 @@ 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)
{
Q_UNUSED(next)
return false;
}
/**
@brief DiagramView::keyPressEvent
Handles "key press" events. Reimplemented here to switch to visualisation
+1
View File
@@ -80,6 +80,7 @@ class DiagramView : public QGraphicsView
void keyPressEvent(QKeyEvent *) override;
void keyReleaseEvent(QKeyEvent *) override;
bool event(QEvent *) override;
bool focusNextPrevChild(bool next) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
+10
View File
@@ -688,6 +688,8 @@ 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);
@@ -696,10 +698,14 @@ void QETDiagramEditor::setUpActions()
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);
@@ -1584,6 +1590,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();
}
/**