File indexing completed on 2024-11-24 04:34:18

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2021 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "basicfileview.h"
0021 
0022 #include <QHeaderView>
0023 #include <QScrollBar>
0024 #include <QKeyEvent>
0025 #include <QAction>
0026 #include <QMenu>
0027 #include <QTimer>
0028 
0029 #include <KLocalizedString>
0030 
0031 #include <BibTeXFields>
0032 #include <models/FileModel>
0033 #include "file/sortfilterfilemodel.h"
0034 #include "logging_gui.h"
0035 
0036 class BasicFileView::Private
0037 {
0038 private:
0039     BasicFileView *p;
0040 
0041 public:
0042     const QString name;
0043     bool automaticBalancing;
0044     FileModel *fileModel;
0045     QSortFilterProxyModel *sortFilterProxyModel;
0046 
0047     Private(const QString &n, BasicFileView *parent)
0048             : p(parent), name(n), automaticBalancing(true), fileModel(nullptr), sortFilterProxyModel(nullptr) {
0049         /// nothing
0050     }
0051 
0052     ~Private() {
0053         saveColumnProperties();
0054     }
0055 
0056     void balanceColumns() {
0057         QSignalBlocker headerViewSignalBlocker(p->header());
0058 
0059         if (p->header()->count() != BibTeXFields::instance().count()) {
0060             qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count();
0061             return;
0062         } else if (!automaticBalancing) {
0063             qCWarning(LOG_KBIBTEX_GUI) << "Will not automaticlly balance columns if automatic balancing is disabled";
0064             return;
0065         }
0066 
0067         /// Automatic balancing of columns is enabled
0068         int defaultWidthSumVisible = 0;
0069         int col = 0;
0070         for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0071             if (!p->header()->isSectionHidden(col))
0072                 defaultWidthSumVisible += fd.defaultWidth;
0073             ++col;
0074         }
0075 
0076         if (defaultWidthSumVisible == 0) return;
0077 
0078         col = 0;
0079         for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0080             if (!p->header()->isSectionHidden(col))
0081                 p->header()->resizeSection(col, p->header()->width() * fd.defaultWidth / defaultWidthSumVisible);
0082             ++col;
0083         }
0084     }
0085 
0086     void enableAutomaticBalancing()
0087     {
0088         automaticBalancing = true;
0089         p->header()->setSectionsMovable(false);
0090         p->header()->setSectionResizeMode(QHeaderView::Fixed);
0091         int col = 0;
0092         for (BibTeXFields::Iterator it = BibTeXFields::instance().begin(), endIt = BibTeXFields::instance().end(); it != endIt; ++it) {
0093             auto &fd = *it;
0094             fd.visible.remove(name);
0095             const bool visibility = fd.defaultVisible;
0096             p->header()->setSectionHidden(col, !visibility);
0097 
0098             /// Later, when loading config, Width of 0 for all fields means 'enable auto-balancing'
0099             fd.width[name] = 0;
0100 
0101             /// Move columns in their original order, i.e. visual index == logical index
0102             const int vi = p->header()->visualIndex(col);
0103             fd.visualIndex[name] = col;
0104             if (vi != col) p->header()->moveSection(vi, col);
0105 
0106             ++col;
0107         }
0108 
0109         balanceColumns();
0110     }
0111 
0112     enum class ColumnSizingOrigin { FromCurrentLayout, FromStoredSettings };
0113 
0114     void enableManualColumnSizing(const ColumnSizingOrigin &columnSizingOrigin)
0115     {
0116         const int columnCount = p->header()->count();
0117         if (columnCount != BibTeXFields::instance().count()) {
0118             qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count();
0119             return;
0120         }
0121 
0122         automaticBalancing = false;
0123 
0124         p->header()->setSectionsMovable(true);
0125         p->header()->setSectionResizeMode(QHeaderView::Interactive);
0126 
0127         if (columnSizingOrigin == ColumnSizingOrigin::FromCurrentLayout) {
0128             /// Memorize each columns current width (for future reference)
0129             for (int logicalIndex = 0; logicalIndex < columnCount; ++logicalIndex)
0130                 BibTeXFields::instance()[logicalIndex].width[name] = p->header()->sectionSize(logicalIndex);
0131         } else if (columnSizingOrigin == ColumnSizingOrigin::FromStoredSettings) {
0132             QSignalBlocker headerViewSignalBlocker(p->header());
0133 
0134             /// Manual columns widths are to be restored
0135             int col = 0;
0136             for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0137                 /// Take column width from stored settings, but as a fall-back/default value
0138                 /// take the default width (usually <10) and multiply it by the average character
0139                 /// width and 4 (magic constant)
0140                 p->header()->resizeSection(col, fd.width.value(name, fd.defaultWidth * 4 * p->fontMetrics().averageCharWidth()));
0141                 ++col;
0142             }
0143         }
0144     }
0145 
0146     void resetColumnProperties() {
0147         enableAutomaticBalancing();
0148         BibTeXFields::instance().save();
0149     }
0150 
0151     void loadColumnProperties() {
0152         const int columnCount = p->header()->count();
0153         if (columnCount != BibTeXFields::instance().count()) {
0154             qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count();
0155             return;
0156         }
0157 
0158         QSignalBlocker headerViewSignalBlocker(p->header());
0159 
0160         if (p->header()->count() != BibTeXFields::instance().count()) {
0161             qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count();
0162             return;
0163         }
0164         int col = 0;
0165         automaticBalancing = true;
0166         for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0167             const bool visibility = fd.visible.value(name, fd.defaultVisible);
0168             /// Width of 0 for all fields means 'enable auto-balancing'
0169             automaticBalancing &= fd.width.value(name, 0) == 0;
0170             p->header()->setSectionHidden(col, !visibility);
0171             ++col;
0172         }
0173         if (automaticBalancing)
0174             enableAutomaticBalancing();
0175         else {
0176             enableManualColumnSizing(ColumnSizingOrigin::FromStoredSettings);
0177 
0178             /// In case no automatic balancing was set, move columns to their positions from previous session
0179             /// (columns' widths got already restored in 'enableManualColumnSizingPositioning')
0180             col = 0;
0181             QHash<int, int> moveTarget;
0182             for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0183                 moveTarget[fd.visualIndex[name]] = col;
0184                 ++col;
0185             }
0186             for (int visualIndex = 0; visualIndex < columnCount; ++visualIndex)
0187                 if (moveTarget[visualIndex] != visualIndex) p->header()->moveSection(moveTarget[visualIndex], visualIndex);
0188         }
0189     }
0190 
0191     void saveColumnProperties() {
0192         if (p->header()->count() != BibTeXFields::instance().count()) {
0193             qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count();
0194             return;
0195         }
0196         int col = 0;
0197         for (BibTeXFields::Iterator it = BibTeXFields::instance().begin(), endIt = BibTeXFields::instance().end(); it != endIt; ++it) {
0198             auto &fd = *it;
0199             fd.visible[name] = !p->header()->isSectionHidden(col);
0200             /// Width of 0 for all fields means 'enable auto-balancing'
0201             fd.width[name] = automaticBalancing ? 0 : p->header()->sectionSize(col);
0202             fd.visualIndex[name] = automaticBalancing ? col : p->header()->visualIndex(col);
0203             ++col;
0204         }
0205         BibTeXFields::instance().save();
0206     }
0207 };
0208 
0209 BasicFileView::BasicFileView(const QString &name, QWidget *parent)
0210         : QTreeView(parent), d(new Private(name, this))
0211 {
0212     /// general visual appearance and behaviour
0213     setSelectionMode(QAbstractItemView::ExtendedSelection);
0214     setSelectionBehavior(QAbstractItemView::SelectRows);
0215     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
0216     setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
0217     setFrameStyle(QFrame::NoFrame);
0218     setAlternatingRowColors(true);
0219     setAllColumnsShowFocus(true);
0220     setRootIsDecorated(false);
0221 
0222     /// header appearance and behaviour
0223     header()->setSectionsClickable(true);
0224     header()->setSortIndicatorShown(true);
0225     header()->setSortIndicator(-1, Qt::AscendingOrder);
0226     connect(header(), &QHeaderView::sortIndicatorChanged, this, &BasicFileView::sort);
0227     header()->setContextMenuPolicy(Qt::CustomContextMenu);
0228     connect(header(), &QHeaderView::customContextMenuRequested, this, &BasicFileView::showHeaderContextMenu);
0229     QTimer::singleShot(250, this, [this]() {
0230         /// Delayed initialization to prevent early triggering of the following slot
0231         connect(header(), &QHeaderView::sectionResized, this, [this](int logicalIndex, int oldSize, int newSize) {
0232             Q_UNUSED(oldSize)
0233             if (!d->automaticBalancing && !d->name.isEmpty() && newSize > 0 && logicalIndex >= 0 && logicalIndex < BibTeXFields::instance().count()) {
0234                 BibTeXFields::instance()[logicalIndex].width[d->name] = newSize;
0235             }
0236         });
0237     });
0238 }
0239 
0240 BasicFileView::~BasicFileView()
0241 {
0242     delete d;
0243 }
0244 
0245 void BasicFileView::setModel(QAbstractItemModel *model)
0246 {
0247     /// Using a lambda context variable (a few lines later to be set in the 'connect' invocation)
0248     /// takes care that at most one connect between any given model and the lambda function exists
0249     static QObject *lambdaContext = nullptr;
0250     if (d->fileModel != nullptr)
0251         delete lambdaContext;
0252 
0253     d->sortFilterProxyModel = nullptr;
0254     d->fileModel = dynamic_cast<FileModel *>(model);
0255     if (d->fileModel == nullptr) {
0256         d->sortFilterProxyModel = qobject_cast<QSortFilterProxyModel *>(model);
0257         if (d->sortFilterProxyModel == nullptr)
0258             qCWarning(LOG_KBIBTEX_GUI) << "Failed to dynamically cast model to QSortFilterProxyModel*";
0259         else
0260             d->fileModel = dynamic_cast<FileModel *>(d->sortFilterProxyModel->sourceModel());
0261     }
0262     if (d->fileModel == nullptr)
0263         qCWarning(LOG_KBIBTEX_GUI) << "Failed to dynamically cast model to FileModel*";
0264     else {
0265         connect(d->fileModel, &FileModel::bibliographySystemChanged, lambdaContext = new QObject(this), [this]() {
0266             /// When the bibliography system is switched (e.g. from BibTeX to BibLaTeX),
0267             /// re-load the column properties, e.g. to hide or rebalance columns
0268             d->loadColumnProperties();
0269         });
0270     }
0271 
0272     QTreeView::setModel(model);
0273 
0274     connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection &selected, const QItemSelection &) {
0275         const bool hasSelection = !selected.empty();
0276         Q_EMIT this->hasSelectionChanged(hasSelection);
0277     });
0278 
0279     /// sort according to session
0280     if (header()->isSortIndicatorShown())
0281         sort(header()->sortIndicatorSection(), header()->sortIndicatorOrder());
0282 
0283     d->loadColumnProperties();
0284 }
0285 
0286 FileModel *BasicFileView::fileModel()
0287 {
0288     return d->fileModel;
0289 }
0290 
0291 QSortFilterProxyModel *BasicFileView::sortFilterProxyModel()
0292 {
0293     return d->sortFilterProxyModel;
0294 }
0295 
0296 void BasicFileView::keyPressEvent(QKeyEvent *event)
0297 {
0298     if (event->modifiers() == Qt::NoModifier) {
0299         if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && currentIndex() != QModelIndex()) {
0300             Q_EMIT doubleClicked(currentIndex());
0301             event->accept();
0302         } else if (!event->text().isEmpty() && event->text().at(0).isLetterOrNumber()) {
0303             Q_EMIT searchFor(event->text());
0304             event->accept();
0305         }
0306     }
0307     QTreeView::keyPressEvent(event);
0308 }
0309 
0310 void BasicFileView::resizeEvent(QResizeEvent *event) {
0311     const int w = qMax(event->size().width() - verticalScrollBar()->width(), 0);
0312     header()->setMinimumWidth(w);
0313     if (d->automaticBalancing) {
0314         header()->setMaximumWidth(w);
0315         d->balanceColumns();
0316     } else
0317         header()->setMaximumWidth(w * 3);
0318 }
0319 
0320 void BasicFileView::headerColumnVisibilityToggled()
0321 {
0322     QAction *action = qobject_cast<QAction *>(sender());
0323     if (action == nullptr) return;
0324     bool ok = false;
0325     const int col = action->data().toInt(&ok);
0326     if (!ok) return;
0327 
0328     if (header()->hiddenSectionCount() + 1 >= header()->count() && !header()->isSectionHidden(col)) {
0329         /// If only one last column is visible and the current action likes to hide
0330         /// this column, abort so that the column cannot be hidden by the user
0331         qCWarning(LOG_KBIBTEX_GUI) << "Already too many columns hidden, won't hide more";
0332         return;
0333     }
0334 
0335     header()->setSectionHidden(col, !header()->isSectionHidden(col));
0336     if (d->automaticBalancing)
0337         d->balanceColumns();
0338 }
0339 
0340 void BasicFileView::sort(int t, Qt::SortOrder s)
0341 {
0342     SortFilterFileModel *sortedModel = qobject_cast<SortFilterFileModel *>(model());
0343     if (sortedModel != nullptr)
0344         sortedModel->sort(t, s);
0345 }
0346 
0347 void BasicFileView::noSorting()
0348 {
0349     SortFilterFileModel *sortedModel = qobject_cast<SortFilterFileModel *>(model());
0350     if (sortedModel != nullptr) {
0351         sortedModel->sort(-1);
0352         header()->setSortIndicator(-1, Qt::AscendingOrder);
0353     }
0354 }
0355 
0356 void BasicFileView::showHeaderContextMenu(const QPoint &pos)
0357 {
0358     const QPoint globalPos = viewport()->mapToGlobal(pos);
0359     QMenu menu(this);
0360 
0361     int col = 0;
0362     const bool onlyOneLastColumnVisible = header()->hiddenSectionCount() + 1 >= header()->count();
0363     for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0364         QAction *action = new QAction(fd.label, &menu);
0365         action->setData(col);
0366         action->setCheckable(true);
0367         action->setChecked(!header()->isSectionHidden(col));
0368         if (onlyOneLastColumnVisible && action->isChecked()) {
0369             /// If only one last column is visible and the current field is this column,
0370             /// disable action so that the column cannot be hidden by the user
0371             action->setEnabled(false);
0372         }
0373         connect(action, &QAction::triggered, this, &BasicFileView::headerColumnVisibilityToggled);
0374         menu.addAction(action);
0375         ++col;
0376     }
0377 
0378     /// Add separator to header's context menu
0379     QAction *action = new QAction(&menu);
0380     action->setSeparator(true);
0381     menu.addAction(action);
0382 
0383     /// Add action to reset to defaults (regarding column visibility) to header's context menu
0384     action = new QAction(i18n("Reset to defaults"), &menu);
0385     connect(action, &QAction::triggered, this, [this]() {
0386         d->resetColumnProperties();
0387     });
0388     menu.addAction(action);
0389 
0390     /// Add action to allow manual resizing of columns
0391     action = new QAction(i18n("Allow manual column resizing/positioning"), &menu);
0392     action->setCheckable(true);
0393     action->setChecked(!d->automaticBalancing);
0394     connect(action, &QAction::triggered, this, [this, action]() {
0395         if (action->isChecked())
0396             d->enableManualColumnSizing(BasicFileView::Private::ColumnSizingOrigin::FromCurrentLayout);
0397         else
0398             d->enableAutomaticBalancing();
0399     });
0400     menu.addAction(action);
0401 
0402     /// Add separator to header's context menu
0403     action = new QAction(&menu);
0404     action->setSeparator(true);
0405     menu.addAction(action);
0406 
0407     /// Add action to disable any sorting
0408     action = new QAction(i18n("No sorting"), &menu);
0409     connect(action, &QAction::triggered, this, &BasicFileView::noSorting);
0410     menu.addAction(action);
0411 
0412     menu.exec(globalPos);
0413 }