Libkleo

treeview.cpp
1/*
2 ui/treeview.cpp
3
4 This file is part of libkleopatra
5 SPDX-FileCopyrightText: 2022 g10 Code GmbH
6 SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10
11#include <config-libkleo.h>
12
13#include "treeview.h"
14
15#include <models/keylist.h>
16
17#include <KConfigGroup>
18#include <KLocalizedString>
19#include <KSharedConfig>
20
21#include <QClipboard>
22#include <QContextMenuEvent>
23#include <QGuiApplication>
24#include <QHeaderView>
25#include <QMenu>
26
27using namespace Kleo;
28
29static const int MAX_AUTOMATIC_COLUMN_WIDTH = 400;
30
31class TreeView::Private
32{
33 TreeView *q;
34
35public:
36 QMenu *mHeaderPopup = nullptr;
37 QList<QAction *> mColumnActions;
38 QString mStateGroupName;
39
40 Private(TreeView *qq)
41 : q(qq)
42 {
43 }
44
45 ~Private()
46 {
47 saveColumnLayout();
48 }
49 void saveColumnLayout();
50};
51
52TreeView::TreeView(QWidget *parent)
53 : QTreeView::QTreeView(parent)
54 , d{new Private(this)}
55{
56 header()->installEventFilter(this);
57}
58
59TreeView::~TreeView() = default;
60
61bool TreeView::eventFilter(QObject *watched, QEvent *event)
62{
63 Q_UNUSED(watched)
64 if (event->type() == QEvent::ContextMenu) {
65 auto e = static_cast<QContextMenuEvent *>(event);
66
67 if (!d->mHeaderPopup) {
68 d->mHeaderPopup = new QMenu(this);
69 d->mHeaderPopup->setTitle(i18nc("@title:menu", "View Columns"));
70 for (int i = 0; i < model()->columnCount(); ++i) {
71 QAction *tmp = d->mHeaderPopup->addAction(model()->headerData(i, Qt::Horizontal).toString());
72 tmp->setData(QVariant(i));
73 tmp->setCheckable(true);
74 d->mColumnActions << tmp;
75 }
76
77 connect(d->mHeaderPopup, &QMenu::triggered, this, [this](QAction *action) {
78 const int col = action->data().toInt();
79 if (action->isChecked()) {
80 showColumn(col);
81 if (columnWidth(col) == 0 || columnWidth(col) == header()->defaultSectionSize()) {
82 resizeColumnToContents(col);
83 setColumnWidth(col, std::min(columnWidth(col), MAX_AUTOMATIC_COLUMN_WIDTH));
84 }
85 } else {
86 hideColumn(col);
87 }
88
89 if (action->isChecked()) {
90 Q_EMIT columnEnabled(col);
91 } else {
92 Q_EMIT columnDisabled(col);
93 }
94 d->saveColumnLayout();
95 });
96 }
97
98 for (QAction *action : std::as_const(d->mColumnActions)) {
99 const int column = action->data().toInt();
100 action->setChecked(!isColumnHidden(column));
101 }
102
103 auto numVisibleColumns = std::count_if(d->mColumnActions.cbegin(), d->mColumnActions.cend(), [](const auto &action) {
104 return action->isChecked();
105 });
106
107 for (auto action : std::as_const(d->mColumnActions)) {
108 action->setEnabled(numVisibleColumns != 1 || !action->isChecked());
109 }
110
111 d->mHeaderPopup->popup(mapToGlobal(e->pos()));
112 return true;
113 }
114
115 return false;
116}
117
118void TreeView::Private::saveColumnLayout()
119{
120 if (mStateGroupName.isEmpty()) {
121 return;
122 }
123 auto config = KConfigGroup(KSharedConfig::openStateConfig(), mStateGroupName);
124 auto header = q->header();
125
126 QVariantList columnVisibility;
127 QVariantList columnOrder;
128 QVariantList columnWidths;
129 const int headerCount = header->count();
130 columnVisibility.reserve(headerCount);
131 columnWidths.reserve(headerCount);
132 columnOrder.reserve(headerCount);
133 for (int i = 0; i < headerCount; ++i) {
134 columnVisibility << QVariant(!q->isColumnHidden(i));
135 columnWidths << QVariant(header->sectionSize(i));
136 columnOrder << QVariant(header->visualIndex(i));
137 }
138
139 config.writeEntry("ColumnVisibility", columnVisibility);
140 config.writeEntry("ColumnOrder", columnOrder);
141 config.writeEntry("ColumnWidths", columnWidths);
142
143 config.writeEntry("SortAscending", (int)header->sortIndicatorOrder());
145 config.writeEntry("SortColumn", header->sortIndicatorSection());
146 } else {
147 config.writeEntry("SortColumn", -1);
148 }
149 config.sync();
150}
151
152bool TreeView::restoreColumnLayout(const QString &stateGroupName)
153{
154 if (stateGroupName.isEmpty()) {
155 return false;
156 }
157 d->mStateGroupName = stateGroupName;
158 auto config = KConfigGroup(KSharedConfig::openStateConfig(), d->mStateGroupName);
159 auto header = this->header();
160
161 QVariantList columnVisibility = config.readEntry("ColumnVisibility", QVariantList());
162 QVariantList columnOrder = config.readEntry("ColumnOrder", QVariantList());
163 QVariantList columnWidths = config.readEntry("ColumnWidths", QVariantList());
164
165 if (!columnVisibility.isEmpty() && !columnOrder.isEmpty() && !columnWidths.isEmpty()) {
166 for (int i = 0; i < header->count(); ++i) {
167 if (i >= columnOrder.size() || i >= columnWidths.size() || i >= columnVisibility.size()) {
168 // An additional column that was not around last time we saved.
169 // We default to hidden.
170 hideColumn(i);
171 continue;
172 }
173 bool visible = columnVisibility[i].toBool();
174 int width = columnWidths[i].toInt();
175 int order = columnOrder[i].toInt();
176
179
180 if (!visible) {
181 hideColumn(i);
182 }
183 }
184 }
185
186 int sortOrder = config.readEntry("SortAscending", (int)Qt::AscendingOrder);
187 int sortColumn = config.readEntry("SortColumn", isSortingEnabled() ? 0 : -1);
188 if (sortColumn >= 0) {
189 sortByColumn(sortColumn, (Qt::SortOrder)sortOrder);
190 }
191
192 connect(header, &QHeaderView::sectionResized, this, [this]() {
193 d->saveColumnLayout();
194 });
195 connect(header, &QHeaderView::sectionMoved, this, [this]() {
196 d->saveColumnLayout();
197 });
199 d->saveColumnLayout();
200 });
201 return !columnVisibility.isEmpty() && !columnOrder.isEmpty() && !columnWidths.isEmpty();
202}
203
204void TreeView::focusInEvent(QFocusEvent *event)
205{
207 // workaround for wrong order of accessible focus events emitted by Qt for QTreeView;
208 // on first focusing of QTreeView, Qt sends focus event for current item before focus event for tree
209 // so that orca doesn't announce the current item;
210 // on re-focusing of QTreeView, Qt only sends focus event for tree
211 auto forceAccessibleFocusEventForCurrentItem = [this]() {
212 // force Qt to send a focus event for the current item to accessibility
213 // tools; otherwise, the user has no idea which item is selected when the
214 // list gets keyboard input focus
215 const QModelIndex index = currentIndex();
216 if (index.isValid()) {
217 currentChanged(index, QModelIndex{});
218 }
219 };
220 // queue the invocation, so that it happens after the widget itself got focus
221 QMetaObject::invokeMethod(this, forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection);
222}
223
224void TreeView::keyPressEvent(QKeyEvent *event)
225{
226 if (event == QKeySequence::Copy) {
227 const QModelIndex index = currentIndex();
228 if (index.isValid() && model()) {
229 QVariant variant = model()->data(index, Kleo::ClipboardRole);
230 if (!variant.isValid()) {
231 variant = model()->data(index, Qt::DisplayRole);
232 }
233 if (variant.canConvert<QString>()) {
235 }
236 }
237 event->accept();
238 return;
239 }
240
242}
243
244QModelIndex TreeView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
245{
246 // make column by column keyboard navigation with Left/Right possible by switching
247 // the selection behavior to SelectItems before calling the parent class's moveCursor,
248 // because it ignores MoveLeft/MoveRight if the selection behavior is SelectRows;
249 // moreover, temporarily disable exanding of items to prevent expanding/collapsing
250 // on MoveLeft/MoveRight
251 if ((cursorAction != MoveLeft) && (cursorAction != MoveRight)) {
252 return QTreeView::moveCursor(cursorAction, modifiers);
253 }
254
255 const auto savedSelectionBehavior = selectionBehavior();
257 const auto savedItemsExpandable = itemsExpandable();
258 setItemsExpandable(false);
259
260 const auto result = QTreeView::moveCursor(cursorAction, modifiers);
261
262 setItemsExpandable(savedItemsExpandable);
263 setSelectionBehavior(savedSelectionBehavior);
264
265 return result;
266}
267
268void TreeView::saveColumnLayout(const QString &stateGroupName)
269{
270 d->mStateGroupName = stateGroupName;
271 d->saveColumnLayout();
272}
273
274void TreeView::resizeToContentsLimited()
275{
276 for (int i = 0; i < model()->columnCount(); i++) {
278 setColumnWidth(i, std::min(columnWidth(i), MAX_AUTOMATIC_COLUMN_WIDTH));
279 }
280}
281
282#include "moc_treeview.cpp"
static KSharedConfig::Ptr openStateConfig(const QString &fileName=QString())
A tree view that allows accessible column by column keyboard navigation and that has customizable col...
Definition treeview.h:40
void saveColumnLayout(const QString &stateGroupName)
Set the state config group name to use for saving the state.
Definition treeview.cpp:268
bool restoreColumnLayout(const QString &stateGroupName)
Restores the layout state under key stateGroupName and enables state saving when the object is destro...
Definition treeview.cpp:152
QString i18nc(const char *context, const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
virtual int columnCount(const QModelIndex &parent) const const=0
virtual QVariant data(const QModelIndex &index, int role) const const=0
QModelIndex currentIndex() const const
virtual bool event(QEvent *event) override
virtual void focusInEvent(QFocusEvent *event) override
QAbstractItemModel * model() const const
void setCheckable(bool)
bool isChecked() const const
void setData(const QVariant &data)
void setText(const QString &text, Mode mode)
QClipboard * clipboard()
int count() const const
void moveSection(int from, int to)
void resizeSection(int logicalIndex, int size)
void sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
void sectionResized(int logicalIndex, int oldSize, int newSize)
int sectionSize(int logicalIndex) const const
bool isSortIndicatorShown() const const
void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
Qt::SortOrder sortIndicatorOrder() const const
int sortIndicatorSection() const const
int visualIndex(int logicalIndex) const const
void triggered(QAction *action)
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
bool isValid() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void installEventFilter(QObject *filterObj)
bool isEmpty() const const
QueuedConnection
DisplayRole
typedef KeyboardModifiers
Horizontal
AscendingOrder
int columnWidth(int column) const const
virtual void currentChanged(const QModelIndex &current, const QModelIndex &previous) override
QHeaderView * header() const const
void hideColumn(int column)
bool isColumnHidden(int column) const const
virtual void keyPressEvent(QKeyEvent *event) override
virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override
void resizeColumnToContents(int column)
void setColumnWidth(int column, int width)
void sortByColumn(int column, Qt::SortOrder order)
bool isSortingEnabled() const const
bool canConvert() const const
bool isValid() const const
QString toString() const const
QPoint mapToGlobal(const QPoint &pos) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:11:58 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.