Libkleo

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

KDE's Doxygen guidelines are available online.