Line data Source code
1 : // SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief DeselectableTreeView: a QTreeView that clears its selection when
6 : * empty space or an already-selected row is clicked.
7 : */
8 : #ifndef SRC_DESELECTABLETREEVIEW_H_
9 : #define SRC_DESELECTABLETREEVIEW_H_
10 :
11 : #include <QGuiApplication>
12 : #include <QMouseEvent>
13 : #include <QStyleHints>
14 : #include <QTimer>
15 : #include <QTreeView>
16 :
17 : /**
18 : * @class DeselectableTreeView
19 : * @ingroup ui
20 : * @brief A QTreeView that clears its selection, and emits emptyClicked(),
21 : * when the user clicks empty space or an already-selected item.
22 : *
23 : * The press records whether the row under the pointer was selected, the
24 : * release handles selection immediately and arms a single-shot timer for the
25 : * platform's double-click interval, and only when no double-click arrives in
26 : * that time does deselect() clear the selection. Opening an entry by
27 : * double-click therefore always wins over deselecting it.
28 : */
29 : class DeselectableTreeView : public QTreeView {
30 : Q_OBJECT
31 :
32 : public:
33 : /**
34 : * @brief Construct the view and arm the single-shot deselect timer.
35 : * @param parent Parent widget forwarded to QTreeView.
36 : */
37 131 : explicit DeselectableTreeView(QWidget *parent) : QTreeView(parent) {
38 131 : deselectTimer.setSingleShot(true);
39 131 : connect(&deselectTimer, &QTimer::timeout, this,
40 131 : &DeselectableTreeView::deselect);
41 131 : }
42 : /**
43 : * @brief Default destructor.
44 : */
45 262 : ~DeselectableTreeView() override = default;
46 :
47 : signals:
48 : /**
49 : * @brief Emitted after deselect() has cleared the selection because of a
50 : * single click on empty space or on a selected row; MainWindow
51 : * clears the password panel in response.
52 : */
53 : void emptyClicked();
54 :
55 : private:
56 : /**
57 : * @brief Set by mouseDoubleClickEvent() so a deselect() that fires after a
58 : * double-click's first release does nothing.
59 : */
60 : bool doubleClickHappened = false;
61 : /**
62 : * @brief Whether the row under the last press was already selected; read
63 : * and reset by the matching release.
64 : */
65 : bool clickSelected = false;
66 : /**
67 : * @brief Single-shot timer, started by a qualifying release for one
68 : * double-click interval; its timeout runs deselect().
69 : */
70 : QTimer deselectTimer;
71 :
72 : /**
73 : * @brief Remember whether the pressed row was already selected and cancel
74 : * any pending deselect from the previous click.
75 : * @param event The mouse press; forwarded to QTreeView.
76 : */
77 6 : void mousePressEvent(QMouseEvent *event) override {
78 : // A new press belongs to a new click: whatever the previous release left
79 : // pending must not clear the selection this press is about to make.
80 6 : deselectTimer.stop();
81 6 : clickSelected =
82 6 : selectionModel()->isSelected(indexAt(event->position().toPoint()));
83 6 : QTreeView::mousePressEvent(event);
84 6 : }
85 :
86 : /**
87 : * @brief Let QTreeView handle the release at once, then, if the press was
88 : * on a selected row and the release is over empty space or a still
89 : * selected row, start deselectTimer.
90 : * @param event The mouse release; forwarded to QTreeView first.
91 : */
92 6 : void mouseReleaseEvent(QMouseEvent *event) override {
93 6 : const QModelIndex item = indexAt(event->position().toPoint());
94 6 : const bool pressWasOnSelection = clickSelected;
95 6 : clickSelected = false;
96 :
97 : // Handle the release immediately: waiting here would delay selection —
98 : // and therefore every decrypt — by the double-click interval.
99 6 : QTreeView::mouseReleaseEvent(event);
100 :
101 : const bool emptyOrSelected =
102 6 : !item.isValid() || selectionModel()->isSelected(item);
103 6 : if (!pressWasOnSelection || !emptyOrSelected)
104 2 : return;
105 :
106 : // Deselecting is the one decision that must wait: a double-click starts
107 : // with a release, and opening the editor must win over clearing the
108 : // selection. Ask the platform how long a double-click may take instead
109 : // of assuming 200 ms.
110 4 : doubleClickHappened = false;
111 4 : deselectTimer.start(
112 : QGuiApplication::styleHints()->mouseDoubleClickInterval());
113 : }
114 :
115 : /**
116 : * @brief Timer slot: clear the selection and current index and emit
117 : * emptyClicked(), unless a double-click happened in the meantime.
118 : */
119 0 : void deselect() {
120 0 : if (doubleClickHappened)
121 : return;
122 0 : clearSelection();
123 0 : selectionModel()->setCurrentIndex(QModelIndex(),
124 : QItemSelectionModel::Select);
125 0 : emit emptyClicked();
126 : }
127 :
128 : /**
129 : * @brief Cancel the pending deselect so the double-click opens the entry
130 : * instead of clearing the selection.
131 : * @param event The double-click; forwarded to QTreeView.
132 : */
133 1 : void mouseDoubleClickEvent(QMouseEvent *event) override {
134 1 : doubleClickHappened = true;
135 1 : deselectTimer.stop();
136 1 : QTreeView::mouseDoubleClickEvent(event);
137 1 : }
138 : };
139 :
140 : #endif // SRC_DESELECTABLETREEVIEW_H_
|