File indexing completed on 2024-05-05 04:40:55

0001 /*
0002     SPDX-FileCopyrightText: 2018 Amish K. Naidu <amhndu@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "scratchpadview.h"
0008 #include "scratchpad.h"
0009 
0010 #include <debug.h>
0011 
0012 #include <interfaces/icore.h>
0013 #include <interfaces/idocumentcontroller.h>
0014 #include <interfaces/iuicontroller.h>
0015 #include <interfaces/idocument.h>
0016 #include <sublime/message.h>
0017 
0018 #include <KLocalizedString>
0019 
0020 #include <QAction>
0021 #include <QStandardItemModel>
0022 #include <QSortFilterProxyModel>
0023 #include <QStyledItemDelegate>
0024 #include <QWidgetAction>
0025 #include <QLineEdit>
0026 #include <QInputDialog>
0027 #include <QPainter>
0028 #include <QAbstractItemView>
0029 
0030 // Use a delegate because the dataChanged signal doesn't tell us the previous name
0031 class FileRenameDelegate
0032     : public QStyledItemDelegate
0033 {
0034     Q_OBJECT
0035 
0036 public:
0037     FileRenameDelegate(QObject* parent, Scratchpad* scratchpad)
0038         : QStyledItemDelegate(parent)
0039         , m_scratchpad(scratchpad)
0040     {
0041     }
0042 
0043     void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override
0044     {
0045         const QString previousName = index.data().toString();
0046         QStyledItemDelegate::setModelData(editor, model, index);
0047         const auto* proxyModel = static_cast<QAbstractProxyModel*>(model);
0048         m_scratchpad->renameScratch(proxyModel->mapToSource(index), previousName);
0049     }
0050 
0051 private:
0052     Scratchpad* m_scratchpad;
0053 };
0054 
0055 EmptyMessageListView::EmptyMessageListView(QWidget* parent)
0056     : QListView(parent)
0057 {
0058 }
0059 
0060 void EmptyMessageListView::paintEvent(QPaintEvent* event)
0061 {
0062     if (model() && model()->rowCount(rootIndex()) > 0) {
0063         QListView::paintEvent(event);
0064     } else {
0065         QPainter painter(viewport());
0066         const auto margin =
0067             QMargins(parentWidget()->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 0,
0068                      parentWidget()->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 0);
0069         painter.drawText(rect() - margin, Qt::AlignCenter | Qt::TextWordWrap, m_message);
0070     }
0071 }
0072 
0073 void EmptyMessageListView::setEmptyMessage(const QString& message)
0074 {
0075     m_message = message;
0076 }
0077 
0078 
0079 ScratchpadView::ScratchpadView(QWidget* parent, Scratchpad* scratchpad)
0080     : QWidget(parent)
0081     , m_scratchpad(scratchpad)
0082 {
0083     setupUi(this);
0084 
0085     setupActions();
0086 
0087     setWindowTitle(i18nc("@title:window", "Scratchpad"));
0088     setWindowIcon(QIcon::fromTheme(QStringLiteral("note")));
0089 
0090     auto* const modelProxy = new QSortFilterProxyModel(this);
0091     modelProxy->setSourceModel(m_scratchpad->model());
0092     modelProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
0093     modelProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
0094     modelProxy->setSortRole(Qt::DisplayRole);
0095     connect(m_filter, &QLineEdit::textEdited,
0096             modelProxy, &QSortFilterProxyModel::setFilterWildcard);
0097 
0098     scratchView->setModel(modelProxy);
0099     scratchView->setItemDelegate(new FileRenameDelegate(this, m_scratchpad));
0100     scratchView->setEmptyMessage(i18n("Scratchpad lets you quickly run and experiment with code without a full project, and even store todos. Create a new scratch to start."));
0101 
0102     connect(scratchView, &QListView::activated, this, &ScratchpadView::scratchActivated);
0103 
0104     connect(m_scratchpad, &Scratchpad::actionFailed, this, [](const QString& messageText) {
0105         // TODO: could be also messagewidget inside toolview?
0106         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0107         KDevelop::ICore::self()->uiController()->postMessage(message);
0108     });
0109 
0110     connect(commandWidget, &QLineEdit::returnPressed, this, &ScratchpadView::runSelectedScratch);
0111     connect(commandWidget, &QLineEdit::returnPressed, this, [this] {
0112         m_scratchpad->setCommand(proxyModel()->mapToSource(currentIndex()), commandWidget->text());
0113     });
0114     commandWidget->setToolTip(i18nc("@info:tooltip", "Command to run this scratch. '$f' will expand to the scratch path."));
0115     commandWidget->setPlaceholderText(commandWidget->toolTip());
0116 
0117     // change active scratch when changing document
0118     connect(KDevelop::ICore::self()->documentController(), &KDevelop::IDocumentController::documentActivated, this,
0119         [this](const KDevelop::IDocument* document) {
0120         if (document->url().isLocalFile()) {
0121             const auto* model = scratchView->model();
0122             const auto index = model->match(model->index(0, 0), Scratchpad::FullPathRole,
0123                                             document->url().toLocalFile()).value({});
0124             if (index.isValid()) {
0125                 scratchView->setCurrentIndex(index);
0126             }
0127         }
0128     });
0129 
0130     connect(scratchView, &QAbstractItemView::pressed, this, &ScratchpadView::validateItemActions);
0131 
0132     validateItemActions();
0133 }
0134 
0135 void ScratchpadView::setupActions()
0136 {
0137     auto* action = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18nc("@action", "New Scratch"), this);
0138     connect(action, &QAction::triggered, this, &ScratchpadView::createScratch);
0139     addAction(action);
0140 
0141     action = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "Remove Scratch"), this);
0142     connect(action, &QAction::triggered, this, [this] {
0143         m_scratchpad->removeScratch(proxyModel()->mapToSource(currentIndex()));
0144         validateItemActions();
0145     });
0146     addAction(action);
0147     m_itemActions.push_back(action);
0148 
0149     action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18nc("@action", "Rename Scratch"), this);
0150     connect(action, &QAction::triggered, this, [this] {
0151         scratchView->edit(scratchView->currentIndex());
0152     });
0153     addAction(action);
0154     m_itemActions.push_back(action);
0155 
0156     action = m_scratchpad->runAction();
0157     action->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
0158     action->setText(i18nc("@action", "Run Scratch"));
0159     connect(action, &QAction::triggered, this, &ScratchpadView::runSelectedScratch);
0160     addAction(action);
0161     m_itemActions.push_back(action);
0162 
0163     m_filter = new QLineEdit(this);
0164     m_filter->setPlaceholderText(i18nc("@info:placeholder", "Filter..."));
0165     auto filterAction = new QWidgetAction(this);
0166     filterAction->setDefaultWidget(m_filter);
0167     addAction(filterAction);
0168 }
0169 
0170 void ScratchpadView::validateItemActions()
0171 {
0172     bool enable = currentIndex().isValid();
0173 
0174     for (auto* action : qAsConst(m_itemActions)) {
0175         action->setEnabled(enable);
0176     }
0177 
0178     commandWidget->setReadOnly(!enable);
0179     if (!enable) {
0180         commandWidget->clear();
0181     }
0182     commandWidget->setText(currentIndex().data(Scratchpad::RunCommandRole).toString());
0183 }
0184 
0185 void ScratchpadView::runSelectedScratch()
0186 {
0187     const auto sourceIndex = proxyModel()->mapToSource(currentIndex());
0188     if (auto* document = KDevelop::ICore::self()->documentController()->documentForUrl(
0189         QUrl::fromLocalFile(sourceIndex.data(Scratchpad::FullPathRole).toString()))) {
0190             document->save();
0191     }
0192     m_scratchpad->setCommand(sourceIndex, commandWidget->text());
0193     m_scratchpad->runScratch(sourceIndex);
0194 }
0195 
0196 void ScratchpadView::scratchActivated(const QModelIndex& index)
0197 {
0198     validateItemActions();
0199     m_scratchpad->openScratch(proxyModel()->mapToSource(index));
0200 }
0201 
0202 void ScratchpadView::createScratch()
0203 {
0204     QString name = QInputDialog::getText(this, i18nc("@title:window", "Create New Scratch"),
0205                                          i18nc("@label:textbox", "Name for scratch file:"),
0206                                          QLineEdit::Normal,
0207                                          QStringLiteral("example.cpp"));
0208     if (!name.isEmpty()) {
0209         m_scratchpad->createScratch(name);
0210     }
0211 }
0212 
0213 QAbstractProxyModel* ScratchpadView::proxyModel() const
0214 {
0215     return static_cast<QAbstractProxyModel*>(scratchView->model());
0216 }
0217 
0218 QModelIndex ScratchpadView::currentIndex() const
0219 {
0220     return scratchView->currentIndex();
0221 }
0222 
0223 #include "scratchpadview.moc"
0224 #include "moc_emptymessagelistview.cpp"
0225 #include "moc_scratchpadview.cpp"