File indexing completed on 2024-05-19 05:05:50

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 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 "documentlist.h"
0021 
0022 #include <QStringListModel>
0023 #include <QTimer>
0024 #include <QGridLayout>
0025 #include <QLabel>
0026 #include <QPainter>
0027 #include <QPushButton>
0028 #include <QApplication>
0029 #include <QIcon>
0030 #include <QAction>
0031 
0032 #include <kio_version.h>
0033 #include <kwidgetsaddons_version.h>
0034 #include <KIconLoader>
0035 #include <KLocalizedString>
0036 #include <KActionMenu>
0037 #include <KDirOperator>
0038 #include <KService>
0039 #include <KMessageBox>
0040 
0041 #include <KBibTeX>
0042 
0043 class DirOperatorWidget : public QWidget
0044 {
0045     Q_OBJECT
0046 
0047 public:
0048     KDirOperator *dirOperator;
0049 
0050     DirOperatorWidget(QWidget *parent)
0051             : QWidget(parent) {
0052         QGridLayout *layout = new QGridLayout(this);
0053         layout->setContentsMargins(0, 0, 0, 0);
0054         layout->setColumnStretch(0, 0);
0055         layout->setColumnStretch(1, 0);
0056         layout->setColumnStretch(2, 1);
0057 
0058         QPushButton *buttonUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), this);
0059         buttonUp->setToolTip(i18n("One level up"));
0060         layout->addWidget(buttonUp, 0, 0, 1, 1);
0061 
0062         QPushButton *buttonHome = new QPushButton(QIcon::fromTheme(QStringLiteral("user-home")), QString(), this);
0063         buttonHome->setToolTip(i18n("Go to Home folder"));
0064         layout->addWidget(buttonHome, 0, 1, 1, 1);
0065 
0066         dirOperator = new KDirOperator(QUrl(QStringLiteral("file:") + QDir::homePath()), this);
0067         layout->addWidget(dirOperator, 1, 0, 1, 3);
0068 #if KIO_VERSION < QT_VERSION_CHECK(5, 100, 0)
0069         dirOperator->setView(KFile::Detail);
0070 #else // >= 5.100.0
0071         dirOperator->setViewMode(KFile::Detail);
0072 #endif // KIO_VERSION < QT_VERSION_CHECK(5, 100, 0)
0073 
0074         connect(buttonUp, &QPushButton::clicked, dirOperator, &KDirOperator::cdUp);
0075         connect(buttonHome, &QPushButton::clicked, dirOperator, &KDirOperator::home);
0076     }
0077 };
0078 
0079 DocumentListDelegate::DocumentListDelegate(QObject *parent)
0080         : QStyledItemDelegate(parent)
0081 {
0082     /// nothing
0083 }
0084 
0085 void DocumentListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0086 {
0087     int height = option.rect.height();
0088 
0089     QStyle *style = QApplication::style();
0090     style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr);
0091 
0092     painter->save();
0093 
0094     if (option.state & QStyle::State_Selected) {
0095         painter->setPen(QPen(option.palette.highlightedText().color()));
0096     } else {
0097         painter->setPen(QPen(option.palette.text().color()));
0098     }
0099 
0100     OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(index.data(Qt::UserRole));
0101 
0102     if (OpenFileInfoManager::instance().currentFile() == ofi) {
0103         /// for the currently open file, use a bold font to write file name
0104         QFont font = painter->font();
0105         font.setBold(true);
0106         painter->setFont(font);
0107     }
0108 
0109     QRect textRect = option.rect;
0110     textRect.setLeft(textRect.left() + height + 4);
0111     textRect.setHeight(height / 2);
0112     painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, index.data(Qt::DisplayRole).toString());
0113 
0114     textRect = option.rect;
0115     textRect.setLeft(textRect.left() + height + 4);
0116     textRect.setTop(textRect.top() + height / 2);
0117     textRect.setHeight(height * 3 / 8);
0118     QFont font = painter->font();
0119     font.setPointSize(font.pointSize() * 7 / 8);
0120     painter->setFont(font);
0121     painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, ofi->fullCaption());
0122 
0123     QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
0124     painter->drawPixmap(option.rect.left() + 1, option.rect.top() + 1, height - 2, height - 2, icon.pixmap(height - 2, height - 2));
0125 
0126     painter->restore();
0127 
0128 }
0129 
0130 QSize DocumentListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0131 {
0132     QSize size = QStyledItemDelegate::sizeHint(option, index);
0133     size.setHeight(size.height() * 9 / 4);
0134     return size;
0135 }
0136 
0137 class DocumentListModel::DocumentListModelPrivate
0138 {
0139 public:
0140     OpenFileInfo::StatusFlag sf;
0141     OpenFileInfoManager::OpenFileInfoList ofiList;
0142 
0143 public:
0144     DocumentListModelPrivate(OpenFileInfo::StatusFlag statusFlag, DocumentListModel *parent)
0145             : sf(statusFlag)
0146     {
0147         Q_UNUSED(parent)
0148     }
0149 };
0150 
0151 DocumentListModel::DocumentListModel(OpenFileInfo::StatusFlag statusFlag, QObject *parent)
0152         : QAbstractListModel(parent), d(new DocumentListModel::DocumentListModelPrivate(statusFlag, this))
0153 {
0154     listsChanged(d->sf);
0155 
0156     connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &DocumentListModel::listsChanged);
0157 }
0158 
0159 DocumentListModel::~DocumentListModel()
0160 {
0161     delete d;
0162 }
0163 
0164 int DocumentListModel::rowCount(const QModelIndex &parent) const
0165 {
0166     if (parent != QModelIndex()) return 0;
0167     return d->ofiList.count();
0168 }
0169 
0170 QVariant DocumentListModel::data(const QModelIndex &index, int role) const
0171 {
0172     if (index.row() < 0 || index.row() >= rowCount()) return QVariant();
0173 
0174     OpenFileInfo *openFileInfo = d->ofiList[index.row()];
0175     const QString iconName = openFileInfo->mimeType().replace(QLatin1Char('/'), QLatin1Char('-'));
0176 
0177     switch (role) {
0178     case Qt::DisplayRole: return openFileInfo->shortCaption();
0179     case Qt::DecorationRole: {
0180         /// determine mime type-based icon and overlays (e.g. for modified files)
0181         QStringList overlays;
0182         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Favorite))
0183             overlays << QStringLiteral("favorites");
0184         else
0185             overlays << QString();
0186         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::RecentlyUsed))
0187             overlays << QStringLiteral("document-open-recent");
0188         else
0189             overlays << QString();
0190         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Open))
0191             overlays << QStringLiteral("folder-open");
0192         else
0193             overlays << QString();
0194         if (openFileInfo->isModified())
0195             overlays << QStringLiteral("document-save");
0196         else
0197             overlays << QString();
0198         return KDE::icon(iconName, overlays, nullptr);
0199     }
0200     case Qt::ToolTipRole: {
0201         QString htmlText(QString(QStringLiteral("<qt><img src=\"%1\"> <b>%2</b>")).arg(KIconLoader::global()->iconPath(iconName, KIconLoader::Small), openFileInfo->shortCaption()));
0202         const QUrl url = openFileInfo->url();
0203         if (url.isValid()) {
0204             QString path(QFileInfo(url.path()).path());
0205             if (!path.endsWith(QLatin1Char('/')))
0206                 path.append(QLatin1Char('/'));
0207             htmlText.append(i18n("<br/><small>located in <b>%1</b></small>", path));
0208         }
0209         QStringList flagListItems;
0210         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Favorite))
0211             flagListItems << i18n("Favorite");
0212         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::RecentlyUsed))
0213             flagListItems << i18n("Recently Used");
0214         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Open))
0215             flagListItems << i18n("Open");
0216         if (openFileInfo->isModified())
0217             flagListItems << i18n("Modified");
0218         if (!flagListItems.empty()) {
0219             htmlText.append(QStringLiteral("<ul>"));
0220             for (const QString &flagListItem : const_cast<const QStringList &>(flagListItems)) {
0221                 htmlText.append(QString(QStringLiteral("<li>%1</li>")).arg(flagListItem));
0222             }
0223             htmlText.append(QStringLiteral("</ul>"));
0224         }
0225         htmlText.append(QStringLiteral("</qt>"));
0226         return htmlText;
0227     }
0228     case Qt::UserRole: return QVariant::fromValue(openFileInfo);
0229     default: return QVariant();
0230     }
0231 }
0232 
0233 QVariant DocumentListModel::headerData(int section, Qt::Orientation orientation, int role) const
0234 {
0235     if (orientation != Qt::Horizontal || section != 0 || role != Qt::DisplayRole) return QVariant();
0236     return QVariant(QStringLiteral("List of Files"));
0237 }
0238 
0239 void DocumentListModel::listsChanged(OpenFileInfo::StatusFlags statusFlags)
0240 {
0241     if (statusFlags.testFlag(d->sf)) {
0242         beginResetModel();
0243         d->ofiList = OpenFileInfoManager::instance().filteredItems(d->sf);
0244         endResetModel();
0245     }
0246 }
0247 
0248 class DocumentListView::DocumentListViewPrivate
0249 {
0250 private:
0251     DocumentListView *p;
0252 
0253 public:
0254     QAction *actionAddToFav, *actionRemFromFav;
0255     QAction *actionCloseFile, *actionOpenFile;
0256     KActionMenu *actionOpenMenu;
0257     QList<QAction *> openMenuActions;
0258 
0259     DocumentListViewPrivate(DocumentListView *parent)
0260             : p(parent), actionAddToFav(nullptr), actionRemFromFav(nullptr), actionCloseFile(nullptr), actionOpenFile(nullptr), actionOpenMenu(nullptr)
0261     {
0262         /// nothing
0263     }
0264 
0265     void openFileWithService(const KPluginMetaData &service)
0266     {
0267         const QModelIndex modelIndex = p->currentIndex();
0268         if (modelIndex != QModelIndex()) {
0269             OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole));
0270             if (!ofi->isModified() || (
0271 #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
0272                         KMessageBox::questionYesNo(p, i18n("The current document has to be saved before switching the viewer/editor component."), i18n("Save before switching?"), KGuiItem(i18n("Save document"), QIcon::fromTheme(QStringLiteral("document-save"))), KGuiItem(i18n("Do not switch"), QIcon::fromTheme(QStringLiteral("dialog-cancel")))) == KMessageBox::Yes
0273 #else // >= 5.100.0
0274                         KMessageBox::questionTwoActions(p, i18n("The current document has to be saved before switching the viewer/editor component."), i18n("Save before switching?"), KGuiItem(i18n("Save document"), QIcon::fromTheme(QStringLiteral("document-save"))), KGuiItem(i18n("Do not switch"), QIcon::fromTheme(QStringLiteral("dialog-cancel")))) == KMessageBox::PrimaryAction
0275 #endif // KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
0276                         && ofi->save()))
0277                 OpenFileInfoManager::instance().setCurrentFile(ofi, service);
0278         }
0279     }
0280 };
0281 
0282 DocumentListView::DocumentListView(OpenFileInfo::StatusFlag statusFlag, QWidget *parent)
0283         : QListView(parent), d(new DocumentListViewPrivate(this))
0284 {
0285     setContextMenuPolicy(Qt::ActionsContextMenu);
0286     setItemDelegate(new DocumentListDelegate(this));
0287 
0288     if (statusFlag == OpenFileInfo::StatusFlag::Open) {
0289         d->actionCloseFile = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Close File"), this);
0290         connect(d->actionCloseFile, &QAction::triggered, this, &DocumentListView::closeFile);
0291         addAction(d->actionCloseFile);
0292     } else {
0293         d->actionOpenFile = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open File"), this);
0294         connect(d->actionOpenFile, &QAction::triggered, this, &DocumentListView::openFile);
0295         addAction(d->actionOpenFile);
0296     }
0297 
0298     d->actionOpenMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open with"), this);
0299     addAction(d->actionOpenMenu);
0300 
0301     if (statusFlag == OpenFileInfo::StatusFlag::Favorite) {
0302         d->actionRemFromFav = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Remove from Favorites"), this);
0303         connect(d->actionRemFromFav, &QAction::triggered, this, &DocumentListView::removeFromFavorites);
0304         addAction(d->actionRemFromFav);
0305     } else {
0306         d->actionAddToFav = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Add to Favorites"), this);
0307         connect(d->actionAddToFav, &QAction::triggered, this, &DocumentListView::addToFavorites);
0308         addAction(d->actionAddToFav);
0309     }
0310 
0311     connect(this, &DocumentListView::activated, this, &DocumentListView::openFile);
0312 
0313     currentChanged(QModelIndex(), QModelIndex());
0314 }
0315 
0316 DocumentListView::~DocumentListView()
0317 {
0318     delete d;
0319 }
0320 
0321 void DocumentListView::addToFavorites()
0322 {
0323     QModelIndex modelIndex = currentIndex();
0324     if (modelIndex != QModelIndex()) {
0325         OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole));
0326         ofi->addFlags(OpenFileInfo::StatusFlag::Favorite);
0327     }
0328 }
0329 
0330 void DocumentListView::removeFromFavorites()
0331 {
0332     QModelIndex modelIndex = currentIndex();
0333     if (modelIndex != QModelIndex()) {
0334         OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole));
0335         ofi->removeFlags(OpenFileInfo::StatusFlag::Favorite);
0336     }
0337 }
0338 
0339 void DocumentListView::openFile()
0340 {
0341     QModelIndex modelIndex = currentIndex();
0342     if (modelIndex != QModelIndex()) {
0343         OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole));
0344         OpenFileInfoManager::instance().setCurrentFile(ofi);
0345     }
0346 }
0347 
0348 void DocumentListView::closeFile()
0349 {
0350     QModelIndex modelIndex = currentIndex();
0351     if (modelIndex != QModelIndex()) {
0352         OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole));
0353         OpenFileInfoManager::instance().close(ofi);
0354     }
0355 }
0356 
0357 void DocumentListView::currentChanged(const QModelIndex &current, const QModelIndex &)
0358 {
0359     bool hasCurrent = current != QModelIndex();
0360     OpenFileInfo *ofi = hasCurrent ? qvariant_cast<OpenFileInfo *>(current.data(Qt::UserRole)) : nullptr;
0361     bool isOpen = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::StatusFlag::Open) : false;
0362     bool isFavorite = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::StatusFlag::Favorite) : false;
0363     bool hasName = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::StatusFlag::HasName) : false;
0364 
0365     if (d->actionOpenFile != nullptr)
0366         d->actionOpenFile->setEnabled(hasCurrent && !isOpen);
0367     if (d->actionCloseFile != nullptr)
0368         d->actionCloseFile->setEnabled(hasCurrent && isOpen);
0369     if (d->actionAddToFav != nullptr)
0370         d->actionAddToFav->setEnabled(hasCurrent && !isFavorite && hasName);
0371     if (d->actionRemFromFav != nullptr)
0372         d->actionRemFromFav->setEnabled(hasCurrent && isFavorite);
0373 
0374     for (QAction *action : const_cast<const QList<QAction *> &>(d->openMenuActions)) {
0375         d->actionOpenMenu->removeAction(action);
0376     }
0377     if (ofi != nullptr) {
0378         const QVector<KPluginMetaData> services = ofi->listOfServices();
0379         for (const KPluginMetaData &service : services) {
0380             QAction *menuItem = new QAction(QIcon::fromTheme(service.iconName()), service.name(), this);
0381             d->actionOpenMenu->addAction(menuItem);
0382             d->openMenuActions << menuItem;
0383 
0384             connect(menuItem, &QAction::triggered, this, [this, service]() {
0385                 d->openFileWithService(service);
0386             });
0387         }
0388     }
0389     d->actionOpenMenu->setEnabled(!d->openMenuActions.isEmpty());
0390 }
0391 
0392 class DocumentList::DocumentListPrivate
0393 {
0394 public:
0395     DocumentListView *listOpenFiles;
0396     DocumentListView *listRecentFiles;
0397     DocumentListView *listFavorites;
0398     DirOperatorWidget *dirOperator;
0399 
0400     DocumentListPrivate(DocumentList *p) {
0401         listOpenFiles = new DocumentListView(OpenFileInfo::StatusFlag::Open, p);
0402         DocumentListModel *model = new DocumentListModel(OpenFileInfo::StatusFlag::Open, listOpenFiles);
0403         listOpenFiles->setModel(model);
0404         p->addTab(listOpenFiles, QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Files"));
0405 
0406         listRecentFiles = new DocumentListView(OpenFileInfo::StatusFlag::RecentlyUsed, p);
0407         model = new DocumentListModel(OpenFileInfo::StatusFlag::RecentlyUsed, listRecentFiles);
0408         listRecentFiles->setModel(model);
0409         p->addTab(listRecentFiles, QIcon::fromTheme(QStringLiteral("document-open-recent")), i18n("Recently Used"));
0410 
0411         listFavorites = new DocumentListView(OpenFileInfo::StatusFlag::Favorite, p);
0412         model = new DocumentListModel(OpenFileInfo::StatusFlag::Favorite, listFavorites);
0413         listFavorites->setModel(model);
0414         p->addTab(listFavorites, QIcon::fromTheme(QStringLiteral("favorites")), i18n("Favorites"));
0415 
0416         dirOperator = new DirOperatorWidget(p);
0417         p->addTab(dirOperator,  QIcon::fromTheme(QStringLiteral("system-file-manager")), i18n("Filesystem Browser"));
0418         connect(dirOperator->dirOperator, &KDirOperator::fileSelected, p, &DocumentList::fileSelected);
0419     }
0420 };
0421 
0422 DocumentList::DocumentList(QWidget *parent)
0423         : QTabWidget(parent), d(new DocumentListPrivate(this))
0424 {
0425     setDocumentMode(true);
0426 }
0427 
0428 DocumentList::~DocumentList()
0429 {
0430     delete d;
0431 }
0432 
0433 void DocumentList::fileSelected(const KFileItem &item)
0434 {
0435     if (item.isFile() && item.isReadable())
0436         Q_EMIT openFile(item.url());
0437 }
0438 
0439 #include "documentlist.moc"