mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-20 15:24:14 +02:00
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:
@@ -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
@@ -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 *);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user