diff --git a/src/ui/treewidget.cpp b/src/ui/treewidget.cpp index 943d33be..6ca5ea49 100644 --- a/src/ui/treewidget.cpp +++ b/src/ui/treewidget.cpp @@ -1,228 +1,247 @@ /* ui/treewidget.cpp This file is part of libkleopatra SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-libkleo.h> #include "treewidget.h" #include <KConfigGroup> #include <KLocalizedString> #include <KSharedConfig> #include <QContextMenuEvent> #include <QHeaderView> #include <QMenu> using namespace Kleo; class TreeWidget::Private { TreeWidget *q; public: QMenu *mHeaderPopup = nullptr; QList<QAction *> mColumnActions; QString mStateGroupName; + std::vector<bool> mColumnForcedHidden; Private(TreeWidget *qq) : q(qq) { } ~Private() { saveColumnLayout(); } void saveColumnLayout(); }; TreeWidget::TreeWidget(QWidget *parent) : QTreeWidget::QTreeWidget(parent) , d{new Private(this)} { header()->installEventFilter(this); } TreeWidget::~TreeWidget() = default; +void TreeWidget::forceColumnHidden(int column) +{ + if (column > columnCount()) { + return; + } + // ensure that the mColumnForcedHidden vector is initialized + d->mColumnForcedHidden.resize(columnCount(), false); + d->mColumnForcedHidden[column] = true; +} + void TreeWidget::Private::saveColumnLayout() { if (mStateGroupName.isEmpty()) { return; } auto config = KConfigGroup(KSharedConfig::openStateConfig(), mStateGroupName); auto header = q->header(); QVariantList columnVisibility; QVariantList columnOrder; QVariantList columnWidths; const int headerCount = header->count(); columnVisibility.reserve(headerCount); columnWidths.reserve(headerCount); columnOrder.reserve(headerCount); for (int i = 0; i < headerCount; ++i) { columnVisibility << QVariant(!q->isColumnHidden(i)); columnWidths << QVariant(header->sectionSize(i)); columnOrder << QVariant(header->visualIndex(i)); } config.writeEntry("ColumnVisibility", columnVisibility); config.writeEntry("ColumnOrder", columnOrder); config.writeEntry("ColumnWidths", columnWidths); config.writeEntry("SortAscending", (int)header->sortIndicatorOrder()); if (header->isSortIndicatorShown()) { config.writeEntry("SortColumn", header->sortIndicatorSection()); } else { config.writeEntry("SortColumn", -1); } config.sync(); } bool TreeWidget::restoreColumnLayout(const QString &stateGroupName) { if (stateGroupName.isEmpty()) { return false; } + // ensure that the mColumnForcedHidden vector is initialized + d->mColumnForcedHidden.resize(columnCount(), false); + d->mStateGroupName = stateGroupName; auto config = KConfigGroup(KSharedConfig::openStateConfig(), d->mStateGroupName); auto header = this->header(); QVariantList columnVisibility = config.readEntry("ColumnVisibility", QVariantList()); QVariantList columnOrder = config.readEntry("ColumnOrder", QVariantList()); QVariantList columnWidths = config.readEntry("ColumnWidths", QVariantList()); if (!columnVisibility.isEmpty() && !columnOrder.isEmpty() && !columnWidths.isEmpty()) { for (int i = 0; i < header->count(); ++i) { - if (i >= columnOrder.size() || i >= columnWidths.size() || i >= columnVisibility.size()) { - // An additional column that was not around last time we saved. - // We default to hidden. + if (d->mColumnForcedHidden[i] || i >= columnOrder.size() || i >= columnWidths.size() || i >= columnVisibility.size()) { + // Hide columns that are forced hidden and new columns that were not around the last time we saved hideColumn(i); continue; } bool visible = columnVisibility[i].toBool(); int width = columnWidths[i].toInt(); int order = columnOrder[i].toInt(); header->resizeSection(i, width ? width : 100); header->moveSection(header->visualIndex(i), order); if (!visible) { hideColumn(i); } } + } else { + for (int i = 0; i < header->count(); ++i) { + if (d->mColumnForcedHidden[i]) { + hideColumn(i); + } + } } int sortOrder = config.readEntry("SortAscending", (int)Qt::AscendingOrder); int sortColumn = config.readEntry("SortColumn", 0); if (sortColumn >= 0) { sortByColumn(sortColumn, (Qt::SortOrder)sortOrder); } connect(header, &QHeaderView::sectionResized, this, [this]() { d->saveColumnLayout(); }); connect(header, &QHeaderView::sectionMoved, this, [this]() { d->saveColumnLayout(); }); connect(header, &QHeaderView::sortIndicatorChanged, this, [this]() { d->saveColumnLayout(); }); return !columnVisibility.isEmpty() && !columnOrder.isEmpty() && !columnWidths.isEmpty(); } bool TreeWidget::eventFilter(QObject *watched, QEvent *event) { if ((watched == header()) && (event->type() == QEvent::ContextMenu)) { auto e = static_cast<QContextMenuEvent *>(event); if (!d->mHeaderPopup) { d->mHeaderPopup = new QMenu(this); d->mHeaderPopup->setTitle(i18nc("@title:menu", "View Columns")); for (int i = 0; i < model()->columnCount(); ++i) { QAction *tmp = d->mHeaderPopup->addAction(model()->headerData(i, Qt::Horizontal).toString()); tmp->setData(QVariant(i)); tmp->setCheckable(true); d->mColumnActions << tmp; } connect(d->mHeaderPopup, &QMenu::triggered, this, [this](QAction *action) { const int col = action->data().toInt(); if (action->isChecked()) { showColumn(col); } else { hideColumn(col); } if (action->isChecked()) { Q_EMIT columnEnabled(col); } else { Q_EMIT columnDisabled(col); } d->saveColumnLayout(); }); } for (QAction *action : std::as_const(d->mColumnActions)) { const int column = action->data().toInt(); action->setChecked(!isColumnHidden(column)); } d->mHeaderPopup->popup(mapToGlobal(e->pos())); return true; } return QTreeWidget::eventFilter(watched, event); } void TreeWidget::focusInEvent(QFocusEvent *event) { QTreeWidget::focusInEvent(event); // workaround for wrong order of accessible focus events emitted by Qt for QTreeWidget; // on first focusing of QTreeWidget, Qt sends focus event for current item before focus event for tree // so that orca doesn't announce the current item; // on re-focusing of QTreeWidget, Qt only sends focus event for tree auto forceAccessibleFocusEventForCurrentItem = [this]() { // force Qt to send a focus event for the current item to accessibility // tools; otherwise, the user has no idea which item is selected when the // list gets keyboard input focus const QModelIndex index = currentIndex(); if (index.isValid()) { currentChanged(index, QModelIndex{}); } }; // queue the invocation, so that it happens after the widget itself got focus QMetaObject::invokeMethod(this, forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); } QModelIndex TreeWidget::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { // make column by column keyboard navigation with Left/Right possible by switching // the selection behavior to SelectItems before calling the parent class's moveCursor, // because it ignores MoveLeft/MoveRight if the selection behavior is SelectRows; // moreover, temporarily disable exanding of items to prevent expanding/collapsing // on MoveLeft/MoveRight if ((cursorAction != MoveLeft) && (cursorAction != MoveRight)) { return QTreeWidget::moveCursor(cursorAction, modifiers); } const auto savedSelectionBehavior = selectionBehavior(); setSelectionBehavior(SelectItems); const auto savedItemsExpandable = itemsExpandable(); setItemsExpandable(false); const auto result = QTreeWidget::moveCursor(cursorAction, modifiers); setItemsExpandable(savedItemsExpandable); setSelectionBehavior(savedSelectionBehavior); return result; } #include "moc_treewidget.cpp" diff --git a/src/ui/treewidget.h b/src/ui/treewidget.h index 82817c4a..51fd28cc 100644 --- a/src/ui/treewidget.h +++ b/src/ui/treewidget.h @@ -1,59 +1,65 @@ /* ui/treewidget.h This file is part of libkleopatra SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include <QTreeWidget> namespace Kleo { /** * A tree widget that allows accessible column by column keyboard navigation * and that has customizable columns through a context menu in the header. * * This is the QTreeWidget-derived variant of TreeView. * * \sa TreeView */ class KLEO_EXPORT TreeWidget : public QTreeWidget { Q_OBJECT public: TreeWidget(QWidget *parent = nullptr); ~TreeWidget() override; + /** + * Hides the column with logical index @p column and doesn't allow the user + * to show it. + */ + void forceColumnHidden(int column); + /** * Restores the layout state under key @p stateGroupName and enables state * saving when the object is destroyed. Make sure that @p stateGroupName is * unique for each place the widget occurs. Returns true if some state was * restored. If false is returned, no state was restored and the caller should * apply the default configuration. */ bool restoreColumnLayout(const QString &stateGroupName); Q_SIGNALS: void columnEnabled(int column); void columnDisabled(int column); protected: bool eventFilter(QObject *watched, QEvent *event) override; void focusInEvent(QFocusEvent *event) override; QModelIndex moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; private: class Private; const std::unique_ptr<Private> d; }; }