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
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) {
152 }
153 connect(header, &QHeaderView::sectionResized, this, [this]() {
154 d->saveColumnLayout();
155 });
156 connect(header, &QHeaderView::sectionMoved, this, [this]() {
157 d->saveColumnLayout();
158 });
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())
bool restoreColumnLayout(const QString &stateGroupName)
Restores the layout state under key stateGroupName and enables state saving when the object is destro...
void forceColumnHidden(int column)
Hides the column with logical index column and doesn't allow the user to show it.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
bool isEmpty() const const
QueuedConnection
DisplayRole
typedef KeyboardModifiers
Horizontal
AscendingOrder
QHeaderView * header() const const
void hideColumn(int column)
void sortByColumn(int column, Qt::SortOrder order)
bool isSortingEnabled() const const
int sortColumn() const const
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-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:51:58 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.