File indexing completed on 2024-05-12 05:52:09

0001 /*
0002     SPDX-FileCopyrightText: 2022 Jiří Wolker <woljiri@gmail.com>
0003     SPDX-FileCopyrightText: 2022 Eugene Popov <popov895@ukr.net>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "welcomeview.h"
0009 
0010 #include "kateapp.h"
0011 #include "katemainwindow.h"
0012 #include "kateviewmanager.h"
0013 #include "recentitemsmodel.h"
0014 #include "savedsessionsmodel.h"
0015 
0016 #include <KAboutData>
0017 #include <KConfigGroup>
0018 #include <KIO/OpenFileManagerWindowJob>
0019 #include <KIconLoader>
0020 #include <KRecentFilesAction>
0021 #include <KSharedConfig>
0022 #include <QTimer>
0023 
0024 #include <QClipboard>
0025 #include <QDesktopServices>
0026 #include <QFileInfo>
0027 #include <QGraphicsOpacityEffect>
0028 #include <QLabel>
0029 #include <QMenu>
0030 
0031 class Placeholder : public QLabel
0032 {
0033 public:
0034     explicit Placeholder(QWidget *parent = nullptr)
0035         : QLabel(parent)
0036     {
0037         setAlignment(Qt::AlignCenter);
0038         setMargin(20);
0039         setWordWrap(true);
0040         // Match opacity of QML placeholder label component
0041         QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect;
0042         opacityEffect->setOpacity(0.5);
0043         setGraphicsEffect(opacityEffect);
0044     }
0045 };
0046 
0047 WelcomeView::WelcomeView(KateViewManager *viewManager, QWidget *parent)
0048     : QScrollArea(parent)
0049     , m_viewManager(viewManager)
0050 {
0051     setupUi(this);
0052 
0053     listViewRecentItems->setSelectionMode(QAbstractItemView::MultiSelection);
0054 
0055     const KAboutData aboutData = KAboutData::applicationData();
0056     labelTitle->setText(i18n("Welcome to %1", aboutData.displayName()));
0057     labelDescription->setText(aboutData.shortDescription());
0058     labelIcon->setPixmap(aboutData.programLogo().value<QIcon>().pixmap(KIconLoader::SizeEnormous));
0059 
0060     labelRecentItems->setText(KateApp::isKate() ? i18n("Recent Documents and Projects") : i18n("Recent Documents"));
0061 
0062     m_placeholderRecentItems = new Placeholder;
0063     m_placeholderRecentItems->setText(KateApp::isKate() ? i18n("No recent documents or projects") : i18n("No recent documents"));
0064 
0065     QVBoxLayout *layoutPlaceholderRecentItems = new QVBoxLayout;
0066     layoutPlaceholderRecentItems->addWidget(m_placeholderRecentItems);
0067     listViewRecentItems->setLayout(layoutPlaceholderRecentItems);
0068 
0069     m_recentItemsModel = new RecentItemsModel(this);
0070     connect(m_recentItemsModel, &RecentItemsModel::modelReset, this, [this]() {
0071         const bool noRecentItems = m_recentItemsModel->rowCount() == 0;
0072         buttonClearRecentItems->setDisabled(noRecentItems);
0073         m_placeholderRecentItems->setVisible(noRecentItems);
0074     });
0075 
0076     KRecentFilesAction *recentFilesAction = m_viewManager->mainWindow()->recentFilesAction();
0077     m_recentItemsModel->refresh(recentFilesAction->urls());
0078     recentFilesAction->menu()->installEventFilter(this);
0079 
0080     listViewRecentItems->setModel(m_recentItemsModel);
0081     connect(listViewRecentItems, &QListView::customContextMenuRequested, this, &WelcomeView::onRecentItemsContextMenuRequested);
0082     connect(listViewRecentItems, &QListView::activated, this, [this](const QModelIndex &index) {
0083         if (index.isValid()) {
0084             const QUrl url = m_recentItemsModel->url(index);
0085             Q_ASSERT(url.isValid());
0086             m_viewManager->openUrlOrProject(url);
0087         }
0088     });
0089 
0090     connect(buttonNewFile, &QPushButton::clicked, m_viewManager, &KateViewManager::slotDocumentNew);
0091     connect(buttonOpenFile, &QPushButton::clicked, m_viewManager, &KateViewManager::slotDocumentOpen);
0092     connect(buttonClearRecentItems, &QPushButton::clicked, this, [recentFilesAction]() {
0093         recentFilesAction->clear();
0094     });
0095 
0096     connect(labelHomepage, qOverload<>(&KUrlLabel::leftClickedUrl), this, [aboutData]() {
0097         QDesktopServices::openUrl(QUrl(aboutData.homepage()));
0098     });
0099     connect(labelContribute, qOverload<>(&KUrlLabel::leftClickedUrl), this, []() {
0100         QDesktopServices::openUrl(QUrl(QStringLiteral("https://kate-editor.org/join-us")));
0101     });
0102     connect(labelHandbook, qOverload<>(&KUrlLabel::leftClickedUrl), this, [this]() {
0103         m_viewManager->mainWindow()->appHelpActivated();
0104     });
0105 
0106     onPluginViewChanged();
0107 
0108     const KTextEditor::MainWindow *mainWindow = m_viewManager->mainWindow()->wrapper();
0109     connect(mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, &WelcomeView::onPluginViewChanged);
0110     connect(mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, &WelcomeView::onPluginViewChanged);
0111 
0112     if (KateApp::isKWrite()) {
0113         widgetSavedSessions->hide();
0114     } else {
0115         m_placeholderSavedSessions = new Placeholder;
0116         m_placeholderSavedSessions->setText(i18n("No saved sessions"));
0117 
0118         QVBoxLayout *layoutPlaceholderSavedSessions = new QVBoxLayout;
0119         layoutPlaceholderSavedSessions->addWidget(m_placeholderSavedSessions);
0120         listViewSavedSessions->setLayout(layoutPlaceholderSavedSessions);
0121 
0122         m_savedSessionsModel = new SavedSessionsModel(this);
0123         connect(m_savedSessionsModel, &SavedSessionsModel::modelReset, this, [this]() {
0124             const bool noSavedSession = m_savedSessionsModel->rowCount() == 0;
0125             m_placeholderSavedSessions->setVisible(noSavedSession);
0126         });
0127 
0128         KateSessionManager *sessionManager = KateApp::self()->sessionManager();
0129         m_savedSessionsModel->refresh(sessionManager->sessionList());
0130         connect(sessionManager, &KateSessionManager::sessionListChanged, this, [this, sessionManager]() {
0131             m_savedSessionsModel->refresh(sessionManager->sessionList());
0132         });
0133 
0134         listViewSavedSessions->setModel(m_savedSessionsModel);
0135         connect(listViewSavedSessions, &QListView::activated, this, [sessionManager](const QModelIndex &index) {
0136             if (index.isValid()) {
0137                 const QString sessionName = index.data().toString();
0138                 Q_ASSERT(!sessionName.isEmpty());
0139                 sessionManager->activateSession(sessionName);
0140             }
0141         });
0142 
0143         connect(buttonNewSession, &QPushButton::clicked, sessionManager, &KateSessionManager::sessionNew);
0144         connect(buttonManageSessions, &QPushButton::clicked, this, []() {
0145             KateSessionManager::sessionManage();
0146         });
0147     }
0148 
0149     static const char showForNewWindowKey[] = "Show welcome view for new window";
0150     KConfigGroup configGroup = KSharedConfig::openConfig()->group(QStringLiteral("General"));
0151     checkBoxShowForNewWindow->setChecked(configGroup.readEntry(showForNewWindowKey, true));
0152     connect(checkBoxShowForNewWindow, &QCheckBox::toggled, this, [configGroup](bool checked) mutable {
0153         configGroup.writeEntry(showForNewWindowKey, checked);
0154     });
0155 
0156     connect(KateApp::self(), &KateApp::configurationChanged, this, [this, configGroup]() {
0157         checkBoxShowForNewWindow->setChecked(configGroup.readEntry(showForNewWindowKey, true));
0158     });
0159 
0160     updateFonts();
0161     updateButtons();
0162 }
0163 
0164 bool WelcomeView::event(QEvent *event)
0165 {
0166     switch (event->type()) {
0167     case QEvent::FontChange:
0168         updateFonts();
0169         updateButtons();
0170         break;
0171     case QEvent::Resize:
0172         if (updateLayout()) {
0173             return true;
0174         }
0175         break;
0176     default:
0177         break;
0178     }
0179 
0180     return QScrollArea::event(event);
0181 }
0182 
0183 void WelcomeView::resizeEvent(QResizeEvent *event)
0184 {
0185     QScrollArea::resizeEvent(event);
0186 
0187     updateLayout();
0188 }
0189 
0190 bool WelcomeView::eventFilter(QObject *watched, QEvent *event)
0191 {
0192     KRecentFilesAction *recentFilesAction = m_viewManager->mainWindow()->recentFilesAction();
0193     if (watched == recentFilesAction->menu()) {
0194         switch (event->type()) {
0195         case QEvent::ActionAdded:
0196         case QEvent::ActionRemoved:
0197             // since the KRecentFilesAction doesn't notify about adding or
0198             // deleting items, we should use this dirty trick to find out
0199             // the KRecentFilesAction has changed
0200             QTimer::singleShot(0, this, [this, recentFilesAction]() {
0201                 m_recentItemsModel->refresh(recentFilesAction->urls());
0202             });
0203             break;
0204         default:
0205             break;
0206         }
0207     }
0208 
0209     return QScrollArea::eventFilter(watched, event);
0210 }
0211 
0212 void WelcomeView::onPluginViewChanged(const QString &pluginName)
0213 {
0214     static const QString projectPluginName = QStringLiteral("kateprojectplugin");
0215     if (pluginName.isEmpty() || pluginName == projectPluginName) {
0216         QObject *projectPluginView = m_viewManager->mainWindow()->pluginView(projectPluginName);
0217         if (projectPluginView) {
0218             connect(buttonOpenFolder, SIGNAL(clicked()), projectPluginView, SLOT(openDirectoryOrProject()));
0219             buttonOpenFolder->show();
0220         } else {
0221             buttonOpenFolder->hide();
0222         }
0223     }
0224 }
0225 
0226 void WelcomeView::onRecentItemsContextMenuRequested(const QPoint &pos)
0227 {
0228     const QModelIndex index = listViewRecentItems->indexAt(pos);
0229     if (!index.isValid()) {
0230         return;
0231     }
0232 
0233     const QUrl url = m_recentItemsModel->url(index);
0234     Q_ASSERT(url.isValid());
0235 
0236     QMenu contextMenu(listViewRecentItems);
0237 
0238     const auto selectedIndexes = listViewRecentItems->selectionModel()->selectedIndexes();
0239     auto allSelectedAreFiles = [this, selectedIndexes] {
0240         return std::all_of(selectedIndexes.begin(), selectedIndexes.end(), [model = m_recentItemsModel](const QModelIndex &index) {
0241             const QUrl url = model->url(index);
0242             return !url.isLocalFile() || QFileInfo(url.toLocalFile()).isFile();
0243         });
0244     };
0245     if (selectedIndexes.size() > 1 && allSelectedAreFiles()) {
0246         QAction *action = new QAction(i18n("Open Selected Files..."), this);
0247         action->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0248         connect(action, &QAction::triggered, this, [this, selectedIndexes]() {
0249             for (const auto &index : selectedIndexes) {
0250                 const auto url = m_recentItemsModel->url(index);
0251                 if (url.isValid()) {
0252                     m_viewManager->openUrl(url);
0253                 }
0254             }
0255         });
0256         contextMenu.addAction(action);
0257     }
0258 
0259     QAction *action = new QAction(i18n("Copy &Location"), this);
0260     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-path")));
0261     connect(action, &QAction::triggered, this, [url]() {
0262         qApp->clipboard()->setText(url.toString(QUrl::PreferLocalFile));
0263     });
0264     contextMenu.addAction(action);
0265 
0266     action = new QAction(i18n("&Open Containing Folder"), this);
0267     action->setEnabled(url.isLocalFile());
0268     action->setIcon(QIcon::fromTheme(QStringLiteral("document-open-folder")));
0269     connect(action, &QAction::triggered, this, [url]() {
0270         KIO::highlightInFileManager({url});
0271     });
0272     contextMenu.addAction(action);
0273 
0274     action = new QAction(i18n("&Remove"), this);
0275     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
0276     connect(action, &QAction::triggered, this, [this, url]() {
0277         KRecentFilesAction *recentFilesAction = m_viewManager->mainWindow()->recentFilesAction();
0278         recentFilesAction->removeUrl(url);
0279         m_recentItemsModel->refresh(recentFilesAction->urls());
0280     });
0281     contextMenu.addAction(action);
0282 
0283     contextMenu.exec(listViewRecentItems->mapToGlobal(pos));
0284 }
0285 
0286 void WelcomeView::updateButtons()
0287 {
0288     std::vector<QPushButton *> buttons{buttonNewFile, buttonOpenFile, buttonOpenFolder};
0289     if (KateApp::isKate()) {
0290         buttons.push_back(buttonNewSession);
0291         buttons.push_back(buttonManageSessions);
0292     }
0293     const int maxWidth = std::accumulate(buttons.cbegin(), buttons.cend(), 0, [](int maxWidth, const QPushButton *button) {
0294         return std::max(maxWidth, button->sizeHint().width());
0295     });
0296     for (QPushButton *button : std::as_const(buttons)) {
0297         button->setFixedWidth(maxWidth);
0298     }
0299 }
0300 
0301 void WelcomeView::updateFonts()
0302 {
0303     QFont titleFont = font();
0304     titleFont.setPointSize(titleFont.pointSize() + 6);
0305     titleFont.setWeight(QFont::Bold);
0306     labelTitle->setFont(titleFont);
0307 
0308     QFont panelTitleFont = font();
0309     panelTitleFont.setPointSize(panelTitleFont.pointSize() + 2);
0310     labelRecentItems->setFont(panelTitleFont);
0311     labelSavedSessions->setFont(panelTitleFont);
0312     labelHelp->setFont(panelTitleFont);
0313 
0314     QFont placeholderFont = font();
0315     placeholderFont.setPointSize(qRound(placeholderFont.pointSize() * 1.3));
0316     m_placeholderRecentItems->setFont(placeholderFont);
0317     if (m_placeholderSavedSessions) {
0318         m_placeholderSavedSessions->setFont(placeholderFont);
0319     }
0320 }
0321 
0322 bool WelcomeView::updateLayout()
0323 {
0324     // Align labelHelp with labelRecentItems
0325     labelHelp->setMinimumHeight(labelRecentItems->height());
0326 
0327     bool result = false;
0328 
0329     // show/hide widgetHeader depending on the view height
0330     if (widgetHeader->isVisible()) {
0331         if (height() <= frameContent->height()) {
0332             widgetHeader->hide();
0333             result = true;
0334         }
0335     } else {
0336         const int implicitHeight = frameContent->height() + widgetHeader->height() + layoutContent->spacing();
0337         if (height() > implicitHeight) {
0338             widgetHeader->show();
0339             result = true;
0340         }
0341     }
0342 
0343     // show/hide widgetHelp depending on the view height
0344     if (widgetHelp->isVisible()) {
0345         if (width() <= frameContent->width()) {
0346             widgetHelp->hide();
0347             result = true;
0348         }
0349     } else {
0350         const int implicitWidth = frameContent->width() + widgetHelp->width() + layoutPanels->horizontalSpacing();
0351         if (width() > implicitWidth) {
0352             widgetHelp->show();
0353             return true;
0354         }
0355     }
0356 
0357     return result;
0358 }
0359 
0360 #include "moc_welcomeview.cpp"