Changeset View
Standalone View
plugins/scratchpad/scratchpad.cpp
- This file was added.
1 | /* This file is part of KDevelop | ||||
---|---|---|---|---|---|
2 | * | ||||
3 | * Copyright 2018 Amish K. Naidu <amhndu@gmail.com> | ||||
4 | * | ||||
5 | * This program is free software; you can redistribute it and/or | ||||
6 | * modify it under the terms of the GNU General Public License | ||||
7 | * as published by the Free Software Foundation; either version 2 | ||||
8 | * of the License, or (at your option) any later version. | ||||
9 | * | ||||
10 | * This program is distributed in the hope that it will be useful, | ||||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
13 | * GNU General Public License for more details. | ||||
14 | * | ||||
15 | * You should have received a copy of the GNU General Public License | ||||
16 | * along with this program; if not, write to the Free Software | ||||
17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | ||||
18 | * 02110-1301, USA. | ||||
19 | */ | ||||
20 | | ||||
21 | #include "scratchpad.h" | ||||
22 | #include "scratchpadview.h" | ||||
23 | #include "scratchpadjob.h" | ||||
24 | | ||||
25 | #include <debug.h> | ||||
26 | | ||||
27 | #include <interfaces/icore.h> | ||||
28 | #include <interfaces/iuicontroller.h> | ||||
29 | #include <interfaces/idocumentcontroller.h> | ||||
30 | #include <interfaces/iruncontroller.h> | ||||
31 | | ||||
32 | #include <KPluginFactory> | ||||
33 | #include <KLocalizedString> | ||||
34 | #include <KSharedConfig> | ||||
35 | #include <KConfigGroup> | ||||
36 | | ||||
37 | #include <QStandardItemModel> | ||||
38 | #include <QStandardPaths> | ||||
39 | #include <QDir> | ||||
40 | #include <QFileIconProvider> | ||||
41 | | ||||
42 | #include <algorithm> | ||||
43 | | ||||
44 | K_PLUGIN_FACTORY_WITH_JSON(ScratchpadFactory, "scratchpad.json", registerPlugin<Scratchpad>(); ) | ||||
45 | | ||||
46 | class ScratchpadToolViewFactory | ||||
47 | : public KDevelop::IToolViewFactory | ||||
48 | { | ||||
49 | public: | ||||
50 | explicit ScratchpadToolViewFactory(Scratchpad* plugin) | ||||
51 | : m_plugin(plugin) | ||||
52 | {} | ||||
53 | | ||||
54 | QWidget* create(QWidget* parent = nullptr) override | ||||
55 | { | ||||
56 | return new ScratchpadView(parent, m_plugin); | ||||
57 | } | ||||
58 | | ||||
59 | Qt::DockWidgetArea defaultPosition() override | ||||
60 | { | ||||
61 | return Qt::LeftDockWidgetArea; | ||||
62 | } | ||||
63 | | ||||
64 | QString id() const override | ||||
65 | { | ||||
66 | return QStringLiteral("org.kdevelop.scratchpad"); | ||||
67 | } | ||||
68 | | ||||
69 | private: | ||||
70 | Scratchpad* const m_plugin; | ||||
71 | }; | ||||
72 | | ||||
73 | namespace { | ||||
74 | KConfigGroup scratchCommands() | ||||
75 | { | ||||
76 | return KSharedConfig::openConfig()->group("Scratchpad").group("Commands"); | ||||
77 | } | ||||
78 | | ||||
79 | KConfigGroup mimeCommands() | ||||
80 | { | ||||
81 | return KSharedConfig::openConfig()->group("Scratchpad").group("Mime Commands"); | ||||
82 | } | ||||
83 | | ||||
84 | QString commandForScratch(const QFileInfo& file) | ||||
85 | { | ||||
86 | if (scratchCommands().hasKey(file.fileName())) { | ||||
87 | return scratchCommands().readEntry(file.fileName()); | ||||
88 | } | ||||
89 | | ||||
90 | const auto suffix = file.suffix(); | ||||
91 | if (mimeCommands().hasKey(suffix)) { | ||||
92 | return mimeCommands().readEntry(suffix); | ||||
93 | } | ||||
94 | | ||||
95 | const static QMap<QString, QString> defaultCommands = { | ||||
96 | {QStringLiteral("cpp"), QStringLiteral("g++ -std=c++11 -o /tmp/a.out $f && /tmp/a.out")}, | ||||
kfunk: Minor: `QHash` (unordered map) is sufficient here. | |||||
97 | {QStringLiteral("py"), QStringLiteral("python $f")}, | ||||
98 | {QStringLiteral("js"), QStringLiteral("node $f")}, | ||||
99 | {QStringLiteral("c"), QStringLiteral("gcc -o /tmp/a.out $f && /tmp/a.out")}, | ||||
100 | }; | ||||
101 | | ||||
102 | return defaultCommands.value(suffix); | ||||
103 | } | ||||
104 | } | ||||
105 | | ||||
106 | Scratchpad::Scratchpad(QObject* parent, const QVariantList& args) | ||||
107 | : KDevelop::IPlugin(QStringLiteral("scratchpad"), parent) | ||||
108 | , m_factory(new ScratchpadToolViewFactory(this)) | ||||
109 | , m_model(new QStandardItemModel(this)) | ||||
110 | { | ||||
111 | Q_UNUSED(args); | ||||
112 | | ||||
113 | qCDebug(PLUGIN_SCRATCHPAD) << "Scratchpad plugin is loaded!"; | ||||
114 | | ||||
115 | core()->uiController()->addToolView(i18n("Scratchpad"), m_factory); | ||||
116 | | ||||
117 | const QDir dataDir(dataDirectory()); | ||||
118 | if (!dataDir.exists()) { | ||||
119 | qCDebug(PLUGIN_SCRATCHPAD) << "Creating directory" << dataDir; | ||||
120 | dataDir.mkpath(QStringLiteral(".")); | ||||
121 | } | ||||
122 | | ||||
123 | const QFileInfoList scratches = dataDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); | ||||
124 | | ||||
125 | for (const auto& fileInfo : scratches) { | ||||
126 | addFileToModel(fileInfo); | ||||
127 | | ||||
128 | // TODO if scratch is open (happens when restarting), set pretty name, below code doesn't work | ||||
129 | // auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); | ||||
130 | // if (document) { | ||||
131 | // document->setPrettyName(i18n("scratch:%1", fileInfo.fileName())); | ||||
132 | // } | ||||
133 | } | ||||
134 | } | ||||
135 | | ||||
136 | QStandardItemModel* Scratchpad::model() | ||||
kfunk: `const` | |||||
Needs to be mutable because http://doc.qt.io/qt-5/qsortfilterproxymodel.html#setSourceModel, used in scratchpadview.cpp:101 amhndu: Needs to be mutable because http://doc.qt.io/qt-5/qsortfilterproxymodel.html#setSourceModel… | |||||
Heh, sorry, misunderstanding. The method should be const, not the return-type. kfunk: Heh, sorry, misunderstanding. The method should be const, not the return-type. | |||||
From what I understand reading https://isocpp.org/wiki/faq/const-correctness#return-const-ref-from-const-memfn, returning a non-const ref from a const member function is not recommended. amhndu: From what I understand reading https://isocpp.org/wiki/faq/const-correctness#return-const-ref… | |||||
Hey Amhndu. Sorry for replying late, but I've been busy last week. I agree that the source you're citing very much is in contrast to what I was saying. I'm trying to keep this short, but in the Qt world a const method implies that the function call has *no side-effects*. Here we're giving out a non-const pointer to an internal implementation detail (which the caller can then freely modify), but that is of no concern to us. We're only concerned about whether this is modified by that function call. Maybe the best explanation you can find is this one here: https://wiki.qt.io/API_Design_Principles#Constness (see section "Return values: pointers vs. const pointers") kfunk: Hey Amhndu.
Sorry for replying late, but I've been busy last week.
I agree that the source… | |||||
amhndu: Understood.
Should I then push after fixing this ? | |||||
It seems that the QStandardItemModel pointed to by m_model is owned by Scratchpad, so I wouldn't return a non-const pointer from a const method. But why not have both? QStandardItemModel* model(); const QStandardItemModel* model() const; I'm not sure why the Qt documentation suggests that const “falls apart” there. It works pretty well for the STL, see e.g. std::vector::data. The rule is not to generally not return non-const pointers/references from const functions, only when the returned pointer/reference points to something we consider a subobject. If it's a reference to something else that we don't own, the rule doesn't apply. aaronpuchert: It seems that the `QStandardItemModel` pointed to by `m_model` is owned by `Scratchpad`, so I… | |||||
Please let's not create additional confusion for newcomers. QStandardItemModel* model() const is the canonical way in the Qt world (cf. most other Qt API) if this method doesn't change the this object, so let's please stick to it. STL is a different beast. Regarding:
This clutters the class interface, to be honest. You might have to create two versions of a getter, for no real gain. kfunk: Please let's not create additional confusion for newcomers. `QStandardItemModel* model()… | |||||
137 | { | ||||
138 | return m_model; | ||||
139 | } | ||||
140 | | ||||
141 | QString Scratchpad::dataDirectory() | ||||
142 | { | ||||
brauch: "failed to remove" | |||||
143 | const static QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) | ||||
Ah, and we want QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevscratchpad/scratches/") here. Reasoning: Consistency with other plugins -- and this version indicates scratches can be shared amongst different users of kdevplatform. kfunk: Ah, and we want `QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral… | |||||
144 | + QLatin1String("/scratch/"); | ||||
145 | return dir; | ||||
146 | } | ||||
147 | | ||||
148 | void Scratchpad::openScratch(const QModelIndex& index) | ||||
brauch: This is too C++-specific for a general KDevelop plugin. | |||||
149 | { | ||||
150 | const QUrl scratchUrl = QUrl::fromLocalFile(index.data(FullPathRole).toString()); | ||||
151 | auto* const document = core()->documentController()->openDocument(scratchUrl); | ||||
152 | document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString())); | ||||
153 | } | ||||
154 | | ||||
155 | void Scratchpad::runScratch(const QModelIndex& index) | ||||
156 | { | ||||
brauch: else { // handle error } | |||||
157 | qCDebug(PLUGIN_SCRATCHPAD) << "run" << index.data().toString(); | ||||
158 | | ||||
159 | auto command = index.data(RunCommandRole).toString(); | ||||
160 | command.replace(QLatin1String("$f"), index.data(FullPathRole).toString()); | ||||
161 | if (!command.isEmpty()) { | ||||
162 | auto* job = new ScratchpadJob(command, index.data().toString(), this); | ||||
163 | core()->runController()->registerJob(job); | ||||
164 | } | ||||
165 | } | ||||
brauch: failed to rename | |||||
166 | | ||||
167 | void Scratchpad::removeScratch(const QModelIndex& index) | ||||
168 | { | ||||
169 | const QString path = index.data(FullPathRole).toString(); | ||||
170 | if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(path))) { | ||||
171 | document->close(); | ||||
172 | } | ||||
173 | | ||||
174 | if (QFile::remove(path)) { | ||||
175 | qCDebug(PLUGIN_SCRATCHPAD) << "removed" << index.data(FullPathRole); | ||||
176 | m_model->removeRow(index.row()); | ||||
177 | scratchCommands().deleteEntry(index.data().toString()); | ||||
178 | } else { | ||||
179 | emit actionFailed(i18n("Failed to remove scratch: %1", index.data().toString())); | ||||
180 | } | ||||
181 | } | ||||
182 | | ||||
183 | void Scratchpad::createScratch(const QString& name) | ||||
184 | { | ||||
185 | if (!m_model->findItems(name).isEmpty()) { | ||||
186 | emit actionFailed(i18n("Failed to create scratch: Name already in use")); | ||||
187 | return; | ||||
188 | } | ||||
189 | | ||||
190 | QFile file(dataDirectory() + name); | ||||
191 | if (file.open(QIODevice::NewOnly)) { // create a new file | ||||
192 | file.close(); | ||||
193 | } | ||||
194 | | ||||
195 | if (file.exists()) { | ||||
196 | addFileToModel(file); | ||||
197 | } else { | ||||
198 | emit actionFailed(i18n("Failed to create new scratch")); | ||||
199 | } | ||||
200 | } | ||||
201 | | ||||
202 | void Scratchpad::renameScratch(const QModelIndex& index, const QString& previousName) | ||||
203 | { | ||||
204 | const QString newName = index.data().toString(); | ||||
205 | if (newName.contains(QDir::separator())) { | ||||
206 | m_model->setData(index, previousName); // undo | ||||
207 | emit actionFailed(i18n("Failed to rename scratch: Names must not include path seperator")); | ||||
208 | return; | ||||
209 | } | ||||
210 | | ||||
211 | const QString previousPath = dataDirectory() + previousName; | ||||
212 | const QString newPath = dataDirectory() + index.data().toString(); | ||||
213 | if (previousPath == newPath) { | ||||
214 | return; | ||||
215 | } | ||||
216 | | ||||
217 | if (QFile::rename(previousPath, newPath)) { | ||||
218 | qCDebug(PLUGIN_SCRATCHPAD) << "renamed" << previousPath << "to" << newPath; | ||||
219 | | ||||
220 | m_model->setData(index, newPath, Scratchpad::FullPathRole); | ||||
221 | m_model->itemFromIndex(index)->setIcon(m_iconProvider.icon(QFileInfo(newPath))); | ||||
222 | auto config = scratchCommands(); | ||||
223 | config.deleteEntry(previousName); | ||||
224 | config.writeEntry(newName, index.data(Scratchpad::RunCommandRole)); | ||||
225 | | ||||
226 | // close old and re-open the closed document | ||||
227 | if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(previousPath))) { | ||||
228 | // FIXME is there a better way ? this feels hacky | ||||
229 | document->close(); | ||||
230 | document = core()->documentController()->openDocument(QUrl::fromLocalFile(newPath)); | ||||
231 | document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString())); | ||||
232 | } | ||||
233 | } else { | ||||
234 | qCWarning(PLUGIN_SCRATCHPAD) << "failed renaming" << previousPath << "to" << newPath; | ||||
235 | // rollback | ||||
236 | m_model->setData(index, previousName); | ||||
237 | emit actionFailed(i18n("Failed renaming scratch.")); | ||||
238 | } | ||||
239 | } | ||||
240 | | ||||
241 | void Scratchpad::addFileToModel(const QFileInfo& fileInfo) | ||||
242 | { | ||||
243 | auto* const item = new QStandardItem(m_iconProvider.icon(fileInfo), fileInfo.fileName()); | ||||
244 | item->setData(fileInfo.absoluteFilePath(), FullPathRole); | ||||
245 | const auto command = commandForScratch(fileInfo); | ||||
246 | item->setData(command, RunCommandRole); | ||||
247 | scratchCommands().writeEntry(item->text(), item->data(RunCommandRole)); | ||||
248 | m_model->appendRow(item); | ||||
249 | } | ||||
250 | | ||||
251 | void Scratchpad::setCommand(const QModelIndex& index, const QString& command) | ||||
252 | { | ||||
253 | qCDebug(PLUGIN_SCRATCHPAD) << "set command" << index.data(); | ||||
254 | m_model->setData(index, command, RunCommandRole); | ||||
255 | scratchCommands().writeEntry(index.data().toString(), command); | ||||
256 | | ||||
257 | mimeCommands().writeEntry(QFileInfo(index.data().toString()).suffix(), command); | ||||
258 | } | ||||
259 | | ||||
260 | #include "scratchpad.moc" |
Minor: QHash (unordered map) is sufficient here.