diff --git a/plugins/scratchpad/CMakeLists.txt b/plugins/scratchpad/CMakeLists.txt index 83dfc0703d..31bd368800 100644 --- a/plugins/scratchpad/CMakeLists.txt +++ b/plugins/scratchpad/CMakeLists.txt @@ -1,25 +1,27 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevscratchpad\") set(scratchpad_SRCS scratchpad.cpp scratchpadview.cpp scratchpadjob.cpp ) ki18n_wrap_ui(scratchpad_SRCS scratchpadview.ui) +qt5_add_resources(scratchpad_SRCS kdevscratchpad.qrc) + declare_qt_logging_category(scratchpad_SRCS TYPE PLUGIN IDENTIFIER PLUGIN_SCRATCHPAD CATEGORY_BASENAME "scratchpad" ) kdevplatform_add_plugin(kdevscratchpad JSON scratchpad.json SOURCES ${scratchpad_SRCS} ) target_link_libraries(kdevscratchpad KDev::Interfaces KDev::Util KDev::OutputView ) diff --git a/plugins/scratchpad/kdevscratchpad.qrc b/plugins/scratchpad/kdevscratchpad.qrc new file mode 100644 index 0000000000..2b524eda97 --- /dev/null +++ b/plugins/scratchpad/kdevscratchpad.qrc @@ -0,0 +1,6 @@ + + + + kdevscratchpad.rc + + diff --git a/plugins/scratchpad/kdevscratchpad.rc b/plugins/scratchpad/kdevscratchpad.rc new file mode 100644 index 0000000000..6e015d7eda --- /dev/null +++ b/plugins/scratchpad/kdevscratchpad.rc @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/scratchpad/scratchpad.cpp b/plugins/scratchpad/scratchpad.cpp index 9257871d29..864d65f019 100644 --- a/plugins/scratchpad/scratchpad.cpp +++ b/plugins/scratchpad/scratchpad.cpp @@ -1,261 +1,280 @@ /* This file is part of KDevelop * * Copyright 2018 Amish K. Naidu * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "scratchpad.h" #include "scratchpadview.h" #include "scratchpadjob.h" #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(ScratchpadFactory, "scratchpad.json", registerPlugin(); ) class ScratchpadToolViewFactory : public KDevelop::IToolViewFactory { public: explicit ScratchpadToolViewFactory(Scratchpad* plugin) : m_plugin(plugin) {} QWidget* create(QWidget* parent = nullptr) override { return new ScratchpadView(parent, m_plugin); } Qt::DockWidgetArea defaultPosition() override { return Qt::LeftDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.scratchpad"); } private: Scratchpad* const m_plugin; }; namespace { KConfigGroup scratchCommands() { return KSharedConfig::openConfig()->group("Scratchpad").group("Commands"); } KConfigGroup mimeCommands() { return KSharedConfig::openConfig()->group("Scratchpad").group("Mime Commands"); } QString commandForScratch(const QFileInfo& file) { if (scratchCommands().hasKey(file.fileName())) { return scratchCommands().readEntry(file.fileName()); } const auto suffix = file.suffix(); if (mimeCommands().hasKey(suffix)) { return mimeCommands().readEntry(suffix); } const static QHash defaultCommands = { {QStringLiteral("cpp"), QStringLiteral("g++ -std=c++11 -o /tmp/a.out $f && /tmp/a.out")}, {QStringLiteral("py"), QStringLiteral("python $f")}, {QStringLiteral("js"), QStringLiteral("node $f")}, {QStringLiteral("c"), QStringLiteral("gcc -o /tmp/a.out $f && /tmp/a.out")}, }; return defaultCommands.value(suffix); } } Scratchpad::Scratchpad(QObject* parent, const QVariantList& args) : KDevelop::IPlugin(QStringLiteral("scratchpad"), parent) , m_factory(new ScratchpadToolViewFactory(this)) , m_model(new QStandardItemModel(this)) + , m_runAction(new QAction(this)) { Q_UNUSED(args); qCDebug(PLUGIN_SCRATCHPAD) << "Scratchpad plugin is loaded!"; core()->uiController()->addToolView(i18n("Scratchpad"), m_factory); const QDir dataDir(dataDirectory()); if (!dataDir.exists()) { qCDebug(PLUGIN_SCRATCHPAD) << "Creating directory" << dataDir; dataDir.mkpath(QStringLiteral(".")); } const QFileInfoList scratches = dataDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); for (const auto& fileInfo : scratches) { addFileToModel(fileInfo); // TODO if scratch is open (happens when restarting), set pretty name, below code doesn't work // auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); // if (document) { // document->setPrettyName(i18n("scratch:%1", fileInfo.fileName())); // } } } QStandardItemModel* Scratchpad::model() const { return m_model; } QString Scratchpad::dataDirectory() { const static QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kdevscratchpad/scratches/"); return dir; } void Scratchpad::openScratch(const QModelIndex& index) { const QUrl scratchUrl = QUrl::fromLocalFile(index.data(FullPathRole).toString()); auto* const document = core()->documentController()->openDocument(scratchUrl); document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString())); } void Scratchpad::runScratch(const QModelIndex& index) { qCDebug(PLUGIN_SCRATCHPAD) << "run" << index.data().toString(); auto command = index.data(RunCommandRole).toString(); command.replace(QLatin1String("$f"), index.data(FullPathRole).toString()); if (!command.isEmpty()) { auto* job = new ScratchpadJob(command, index.data().toString(), this); core()->runController()->registerJob(job); } } void Scratchpad::removeScratch(const QModelIndex& index) { const QString path = index.data(FullPathRole).toString(); if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(path))) { document->close(); } if (QFile::remove(path)) { qCDebug(PLUGIN_SCRATCHPAD) << "removed" << index.data(FullPathRole); scratchCommands().deleteEntry(index.data().toString()); m_model->removeRow(index.row()); } else { emit actionFailed(i18n("Failed to remove scratch: %1", index.data().toString())); } } void Scratchpad::createScratch(const QString& name) { if (!m_model->findItems(name).isEmpty()) { emit actionFailed(i18n("Failed to create scratch: Name already in use")); return; } QFile file(dataDirectory() + name); if (!file.exists() && file.open(QIODevice::WriteOnly)) { // create a new file if it doesn't exist file.close(); } if (file.exists()) { addFileToModel(file); } else { emit actionFailed(i18n("Failed to create new scratch")); } } void Scratchpad::renameScratch(const QModelIndex& index, const QString& previousName) { const QString newName = index.data().toString(); if (newName.contains(QDir::separator())) { m_model->setData(index, previousName); // undo emit actionFailed(i18n("Failed to rename scratch: Names must not include path seperator")); return; } const QString previousPath = dataDirectory() + previousName; const QString newPath = dataDirectory() + index.data().toString(); if (previousPath == newPath) { return; } if (QFile::rename(previousPath, newPath)) { qCDebug(PLUGIN_SCRATCHPAD) << "renamed" << previousPath << "to" << newPath; m_model->setData(index, newPath, Scratchpad::FullPathRole); m_model->itemFromIndex(index)->setIcon(m_iconProvider.icon(QFileInfo(newPath))); auto config = scratchCommands(); config.deleteEntry(previousName); config.writeEntry(newName, index.data(Scratchpad::RunCommandRole)); // close old and re-open the closed document if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(previousPath))) { // FIXME is there a better way ? this feels hacky document->close(); document = core()->documentController()->openDocument(QUrl::fromLocalFile(newPath)); document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString())); } } else { qCWarning(PLUGIN_SCRATCHPAD) << "failed renaming" << previousPath << "to" << newPath; // rollback m_model->setData(index, previousName); emit actionFailed(i18n("Failed renaming scratch.")); } } void Scratchpad::addFileToModel(const QFileInfo& fileInfo) { auto* const item = new QStandardItem(m_iconProvider.icon(fileInfo), fileInfo.fileName()); item->setData(fileInfo.absoluteFilePath(), FullPathRole); const auto command = commandForScratch(fileInfo); item->setData(command, RunCommandRole); scratchCommands().writeEntry(item->text(), item->data(RunCommandRole)); m_model->appendRow(item); } void Scratchpad::setCommand(const QModelIndex& index, const QString& command) { qCDebug(PLUGIN_SCRATCHPAD) << "set command" << index.data(); m_model->setData(index, command, RunCommandRole); scratchCommands().writeEntry(index.data().toString(), command); mimeCommands().writeEntry(QFileInfo(index.data().toString()).suffix(), command); } +QAction* Scratchpad::runAction() const +{ + return m_runAction; +} + +void Scratchpad::createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) +{ + Q_UNUSED(window); + + xmlFile = QStringLiteral("kdevscratchpad.rc"); + + // add to gui action collection, so that the shorcut is easily configurable + // action setup done in ScratchpadView + actions.addAction(QStringLiteral("run_scratch"), m_runAction); +} + + #include "scratchpad.moc" diff --git a/plugins/scratchpad/scratchpad.h b/plugins/scratchpad/scratchpad.h index 5598c96f11..3e8b9b9948 100644 --- a/plugins/scratchpad/scratchpad.h +++ b/plugins/scratchpad/scratchpad.h @@ -1,71 +1,78 @@ /* This file is part of KDevelop * * Copyright 2018 Amish K. Naidu * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef SCRATCHPAD_H #define SCRATCHPAD_H #include #include class ScratchpadToolViewFactory; class QStandardItemModel; class QModelIndex; class QFileInfo; class QString; +class QAction; class Scratchpad : public KDevelop::IPlugin { Q_OBJECT public: Scratchpad(QObject* parent, const QVariantList& args); QStandardItemModel* model() const; + QAction* runAction() const; + static QString dataDirectory(); + void createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) override; + enum ExtraRoles { FullPathRole = Qt::UserRole + 1, RunCommandRole, }; public Q_SLOTS: void openScratch(const QModelIndex& index); void runScratch(const QModelIndex& index); void removeScratch(const QModelIndex& index); void createScratch(const QString& name); void renameScratch(const QModelIndex& index, const QString& previousName); void setCommand(const QModelIndex& index, const QString& command); Q_SIGNALS: void actionFailed(const QString& message); private: void addFileToModel(const QFileInfo& fileInfo); ScratchpadToolViewFactory* m_factory; QStandardItemModel* m_model; QFileIconProvider m_iconProvider; + + QAction* const m_runAction; }; #endif // SCRATCHPAD_H diff --git a/plugins/scratchpad/scratchpadview.cpp b/plugins/scratchpad/scratchpadview.cpp index 8ca855a2d8..3835ad64ef 100644 --- a/plugins/scratchpad/scratchpadview.cpp +++ b/plugins/scratchpad/scratchpadview.cpp @@ -1,225 +1,227 @@ /* This file is part of KDevelop * * Copyright 2018 Amish K. Naidu * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "scratchpadview.h" #include "scratchpad.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Use a delegate because the dataChanged signal doesn't tell us the previous name class FileRenameDelegate : public QStyledItemDelegate { public: FileRenameDelegate(QObject* parent, Scratchpad* scratchpad) : QStyledItemDelegate(parent) , m_scratchpad(scratchpad) { } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override { const QString previousName = index.data().toString(); QStyledItemDelegate::setModelData(editor, model, index); const auto* proxyModel = static_cast(model); m_scratchpad->renameScratch(proxyModel->mapToSource(index), previousName); } private: Scratchpad* m_scratchpad; }; EmptyMessageListView::EmptyMessageListView(QWidget* parent) : QListView(parent) { } void EmptyMessageListView::paintEvent(QPaintEvent* event) { if (model() && model()->rowCount(rootIndex()) > 0) { QListView::paintEvent(event); } else { QPainter painter(viewport()); const auto margin = QMargins(parentWidget()->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 0, parentWidget()->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 0); painter.drawText(rect() - margin, Qt::AlignCenter | Qt::TextWordWrap, m_message); } } void EmptyMessageListView::setEmptyMessage(const QString& message) { m_message = message; } ScratchpadView::ScratchpadView(QWidget* parent, Scratchpad* scratchpad) : QWidget(parent) , m_scratchpad(scratchpad) { setupUi(this); setupActions(); setWindowTitle(i18n("Scratchpad")); setWindowIcon(QIcon::fromTheme(QStringLiteral("note"))); auto* const modelProxy = new QSortFilterProxyModel(this); modelProxy->setSourceModel(m_scratchpad->model()); modelProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); modelProxy->setSortCaseSensitivity(Qt::CaseInsensitive); modelProxy->setSortRole(Qt::DisplayRole); connect(m_filter, &QLineEdit::textEdited, modelProxy, &QSortFilterProxyModel::setFilterWildcard); scratchView->setModel(modelProxy); scratchView->setItemDelegate(new FileRenameDelegate(this, m_scratchpad)); 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.")); connect(scratchView, &QListView::activated, this, &ScratchpadView::scratchActivated); connect(m_scratchpad, &Scratchpad::actionFailed, [this](const QString& message) { KMessageBox::sorry(this, message); }); connect(commandWidget, &QLineEdit::returnPressed, this, &ScratchpadView::runSelectedScratch); connect(commandWidget, &QLineEdit::returnPressed, [this] { m_scratchpad->setCommand(proxyModel()->mapToSource(currentIndex()), commandWidget->text()); }); commandWidget->setToolTip(i18n("Command to run this scratch. $f will expand to the scratch path")); commandWidget->setPlaceholderText(commandWidget->toolTip()); // change active scratch when changing document connect(KDevelop::ICore::self()->documentController(), &KDevelop::IDocumentController::documentActivated, [this](const KDevelop::IDocument* document) { if (document->url().isLocalFile()) { const auto* model = scratchView->model(); const auto index = model->match(model->index(0, 0), Scratchpad::FullPathRole, document->url().toLocalFile()).value({}); if (index.isValid()) { scratchView->setCurrentIndex(index); } } }); validateItemActions(); } void ScratchpadView::setupActions() { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("New Scratch"), this); connect(action, &QAction::triggered, this, &ScratchpadView::createScratch); addAction(action); action = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove Scratch"), this); connect(action, &QAction::triggered, [this] { m_scratchpad->removeScratch(proxyModel()->mapToSource(currentIndex())); validateItemActions(); }); addAction(action); m_itemActions.push_back(action); action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Scratch"), this); connect(action, &QAction::triggered, [this] { scratchView->edit(scratchView->currentIndex()); }); addAction(action); m_itemActions.push_back(action); - action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Run Scratch"), this); + action = m_scratchpad->runAction(); + action->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); + action->setText(i18n("Run Scratch")); connect(action, &QAction::triggered, this, &ScratchpadView::runSelectedScratch); addAction(action); m_itemActions.push_back(action); m_filter = new QLineEdit(this); m_filter->setPlaceholderText(i18n("Filter...")); auto filterAction = new QWidgetAction(this); filterAction->setDefaultWidget(m_filter); addAction(filterAction); } void ScratchpadView::validateItemActions() { bool enable = currentIndex().isValid(); for (auto* action : m_itemActions) { action->setEnabled(enable); } commandWidget->setReadOnly(!enable); if (!enable) { commandWidget->clear(); } } void ScratchpadView::runSelectedScratch() { const auto sourceIndex = proxyModel()->mapToSource(currentIndex()); if (auto* document = KDevelop::ICore::self()->documentController()->documentForUrl( QUrl::fromLocalFile(sourceIndex.data(Scratchpad::FullPathRole).toString()))) { document->save(); } m_scratchpad->setCommand(sourceIndex, commandWidget->text()); m_scratchpad->runScratch(sourceIndex); } void ScratchpadView::scratchActivated(const QModelIndex& index) { validateItemActions(); m_scratchpad->openScratch(proxyModel()->mapToSource(index)); commandWidget->setText(index.data(Scratchpad::RunCommandRole).toString()); } void ScratchpadView::createScratch() { QString name = QInputDialog::getText(this, i18n("Create New Scratch"), i18n("Enter name for scratch file:"), QLineEdit::Normal, QStringLiteral("example.cpp")); if (!name.isEmpty()) { m_scratchpad->createScratch(name); } } QAbstractProxyModel* ScratchpadView::proxyModel() const { return static_cast(scratchView->model()); } QModelIndex ScratchpadView::currentIndex() const { return scratchView->currentIndex(); }