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 }