1 /*
2 * This file is part of Krita
3 *
4 * Copyright (c) 2020 L. E. Segovia <amy@amyspark.me>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20
21 #include <KisDialogStateSaver.h>
22 #include <KoColor.h>
23 #include <KoResourceServer.h>
24 #include <KoResourceServerProvider.h>
25 #include <KSeExprUI/ErrorMessages.h>
26 #include <filter/kis_filter_configuration.h>
27 #include <kis_icon.h>
28 #include <kis_config.h>
29
30 #include "SeExprExpressionContext.h"
31 #include "generator.h"
32 #include "kis_wdg_seexpr.h"
33 #include "ui_wdgseexpr.h"
34
KisWdgSeExpr(QWidget * parent)35 KisWdgSeExpr::KisWdgSeExpr(QWidget *parent)
36 : KisConfigWidget(parent)
37 , updateCompressor(1000, KisSignalCompressor::Mode::POSTPONE)
38 , m_currentPreset(new KisSeExprScript(i18n("Untitled")))
39 , m_saveDialog(new KisWdgSeExprPresetsSave(this))
40 , m_isCreatingPresetFromScratch(true)
41 {
42 m_widget = new Ui_WdgSeExpr();
43 m_widget->setupUi(this);
44 m_widget->txtEditor->setControlCollectionWidget(m_widget->wdgControls);
45
46 m_widget->renameBrushPresetButton->setIcon(KisIconUtils::loadIcon("dirty-preset")); // edit icon
47
48 m_widget->reloadPresetButton->setIcon(KisIconUtils::loadIcon("updateColorize"));
49 m_widget->reloadPresetButton->setToolTip(i18n("Reload the preset"));
50 m_widget->dirtyPresetIndicatorButton->setIcon(KisIconUtils::loadIcon("warning"));
51 m_widget->dirtyPresetIndicatorButton->setToolTip(i18n("The settings for this preset have changed from their default."));
52
53 KisDialogStateSaver::restoreState(m_widget->txtEditor, "krita/generators/seexpr");
54 // Manually restore SeExpr state. KisDialogStateSaver uses setPlainText, not text itself
55 m_widget->txtEditor->setExpr(m_widget->txtEditor->exprTe->toPlainText());
56
57 m_widget->txtEditor->registerExtraVariable("$u", i18nc("SeExpr variable", "Normalized X axis coordinate of the image from its top-left corner"));
58 m_widget->txtEditor->registerExtraVariable("$v", i18nc("SeExpr variable", "Normalized Y axis coordinate of the image from its top-left corner"));
59 m_widget->txtEditor->registerExtraVariable("$w", i18nc("SeExpr variable", "Image width"));
60 m_widget->txtEditor->registerExtraVariable("$h", i18nc("SeExpr variable", "Image height"));
61
62 m_widget->txtEditor->updateCompleter();
63
64 m_widget->txtEditor->exprTe->setFont(QFontDatabase().systemFont(QFontDatabase::FixedFont));
65
66 connect(m_widget->scriptSelectorWidget, SIGNAL(resourceSelected(KoResource*)), this, SLOT(slotResourceSelected(KoResource*)));
67 connect(m_saveDialog, SIGNAL(resourceSelected(KoResource*)), this, SLOT(slotResourceSaved(KoResource*)));
68
69 connect(m_widget->renameBrushPresetButton, SIGNAL(clicked(bool)),
70 this, SLOT(slotRenamePresetActivated()));
71 connect(m_widget->cancelBrushNameUpdateButton, SIGNAL(clicked(bool)),
72 this, SLOT(slotRenamePresetDeactivated()));
73 connect(m_widget->updateBrushNameButton, SIGNAL(clicked(bool)),
74 this, SLOT(slotSaveRenameCurrentPreset()));
75 connect(m_widget->renameBrushNameTextField, SIGNAL(returnPressed()),
76 this, SLOT(slotSaveRenameCurrentPreset()));
77
78 connect(m_widget->saveBrushPresetButton, SIGNAL(clicked()),
79 this, SLOT(slotSaveBrushPreset()));
80 connect(m_widget->saveNewBrushPresetButton, SIGNAL(clicked()),
81 this, SLOT(slotSaveNewBrushPreset()));
82
83 connect(m_widget->reloadPresetButton, SIGNAL(clicked()),
84 this, SLOT(slotReloadPresetClicked()));
85
86 connect(m_widget->txtEditor, SIGNAL(apply()),
87 &updateCompressor, SLOT(start()));
88 connect(m_widget->txtEditor, SIGNAL(preview()),
89 &updateCompressor, SLOT(start()));
90
91 connect(&updateCompressor, SIGNAL(timeout()), this, SLOT(isValid()));
92
93 togglePresetRenameUIActive(false); // reset the UI state of renaming a preset if we are changing presets
94 slotUpdatePresetSettings(); // disable everything until a preset is selected
95
96 m_widget->splitter->restoreState(KisConfig(true).readEntry("seExpr/splitLayoutState", QByteArray())); // restore splitter state
97 m_widget->tabWidget->setCurrentIndex(KisConfig(true).readEntry("seExpr/selectedTab", -1)); // save currently selected tab
98 }
99
~KisWdgSeExpr()100 KisWdgSeExpr::~KisWdgSeExpr()
101 {
102 KisDialogStateSaver::saveState(m_widget->txtEditor, "krita/generators/seexpr");
103 KisConfig(false).writeEntry("seExpr/splitLayoutState", m_widget->splitter->saveState()); // save splitter state
104 KisConfig(false).writeEntry("seExpr/selectedTab", m_widget->tabWidget->currentIndex()); // save currently selected tab
105
106 delete m_saveDialog;
107 delete m_widget;
108 }
109
widget() const110 inline const Ui_WdgSeExpr *KisWdgSeExpr::widget() const
111 {
112 return m_widget;
113 }
114
setConfiguration(const KisPropertiesConfigurationSP config)115 void KisWdgSeExpr::setConfiguration(const KisPropertiesConfigurationSP config)
116 {
117 auto rserver = KoResourceServerProvider::instance()->seExprScriptServer();
118 auto name = config->getString("pattern", "Disney_noisecolor2");
119 auto pattern = rserver->resourceByName(name);
120 if (pattern) {
121 m_widget->scriptSelectorWidget->setCurrentScript(pattern);
122 }
123
124 QString script = config->getString("script");
125
126 if (!script.isNull()) {
127 m_widget->txtEditor->setExpr(script, true);
128 }
129 }
130
configuration() const131 KisPropertiesConfigurationSP KisWdgSeExpr::configuration() const
132 {
133 KisFilterConfigurationSP config = new KisFilterConfiguration("seexpr", 1);
134
135 if (m_widget->scriptSelectorWidget->currentResource()) {
136 QVariant v;
137 v.setValue(m_widget->scriptSelectorWidget->currentResource()->name());
138 config->setProperty("pattern", v);
139 }
140 config->setProperty("script", QVariant(m_widget->txtEditor->getExpr()));
141
142 return config;
143 }
144
slotResourceSaved(KoResource * r)145 void KisWdgSeExpr::slotResourceSaved(KoResource *r)
146 {
147 KisSeExprScript *g = static_cast<KisSeExprScript *>(r);
148
149 if (g) {
150 m_widget->scriptSelectorWidget->setCurrentScript(r);
151 slotResourceSelected(r);
152 }
153 }
154
slotResourceSelected(KoResource * r)155 void KisWdgSeExpr::slotResourceSelected(KoResource *r)
156 {
157 KisSeExprScript *g = static_cast<KisSeExprScript *>(r);
158 if (g) {
159 // ALWAYS have a manageable copy of the preset
160 // this is required for detecting dirty presets and reloading
161 m_currentPreset = g->clone();
162
163 m_isCreatingPresetFromScratch = false;
164
165 m_widget->txtEditor->setExpr(g->script(), true);
166
167 QString formattedBrushName = g->name().replace("_", " ");
168 m_widget->currentBrushNameLabel->setText(formattedBrushName);
169 m_widget->renameBrushNameTextField->setText(g->name());
170 // get the preset image and pop it into the thumbnail area on the top of the brush editor
171 QSize thumbSize = QSize(55, 55)*devicePixelRatioF();
172 QPixmap thumbnail = QPixmap::fromImage(g->image().scaled(thumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
173 thumbnail.setDevicePixelRatio(devicePixelRatioF());
174 m_widget->presetThumbnailicon->setPixmap(thumbnail);
175
176 togglePresetRenameUIActive(false); // reset the UI state of renaming a brush if we are changing brush presets
177 slotUpdatePresetSettings(); // check to see if the dirty preset icon needs to be shown
178
179 updateCompressor.start();
180 }
181 }
182
slotRenamePresetActivated()183 void KisWdgSeExpr::slotRenamePresetActivated()
184 {
185 togglePresetRenameUIActive(true);
186 }
187
slotRenamePresetDeactivated()188 void KisWdgSeExpr::slotRenamePresetDeactivated()
189 {
190 togglePresetRenameUIActive(false);
191 }
192
togglePresetRenameUIActive(bool isRenaming)193 void KisWdgSeExpr::togglePresetRenameUIActive(bool isRenaming)
194 {
195 // This function doesn't really do anything except get the UI in a state to rename a brush preset
196 m_widget->renameBrushNameTextField->setVisible(isRenaming);
197 m_widget->updateBrushNameButton->setVisible(isRenaming);
198 m_widget->cancelBrushNameUpdateButton->setVisible(isRenaming);
199
200 // hide these below areas while renaming
201 m_widget->currentBrushNameLabel->setVisible(!isRenaming);
202 m_widget->renameBrushPresetButton->setVisible(!isRenaming);
203 m_widget->saveBrushPresetButton->setEnabled(!isRenaming);
204 m_widget->saveBrushPresetButton->setVisible(!isRenaming);
205 m_widget->saveNewBrushPresetButton->setEnabled(!isRenaming);
206 m_widget->saveNewBrushPresetButton->setVisible(!isRenaming);
207 }
208
slotSaveRenameCurrentPreset()209 void KisWdgSeExpr::slotSaveRenameCurrentPreset()
210 {
211 slotReloadPresetClicked();
212
213 KisSeExprScript *curPreset = m_currentPreset;
214
215 if (!curPreset)
216 return;
217
218 KoResourceServer<KisSeExprScript> *rServer = KoResourceServerProvider::instance()->seExprScriptServer();
219 QString saveLocation = rServer->saveLocation();
220
221 QString originalPresetName = curPreset->name();
222 QString renamedPresetName = m_widget->renameBrushNameTextField->text();
223 QString originalPresetPathAndFile = saveLocation + originalPresetName + curPreset->defaultFileExtension();
224 QString renamedPresetPathAndFile = saveLocation + renamedPresetName + curPreset->defaultFileExtension();
225
226 KisSeExprScript *newPreset = curPreset->clone();
227 newPreset->setFilename(renamedPresetPathAndFile); // this also contains the path
228 newPreset->setName(renamedPresetName);
229 newPreset->setImage(curPreset->image()); // use existing thumbnail (might not need to do this)
230 newPreset->setDirty(false);
231 rServer->addResource(newPreset);
232
233 slotResourceSelected(newPreset); // refresh and select our freshly renamed resource
234
235 // Now blacklist the original file
236 if (rServer->resourceByName(originalPresetName)) {
237 rServer->removeResourceAndBlacklist(curPreset);
238 }
239
240 togglePresetRenameUIActive(false); // this returns the UI to its original state after saving
241
242 slotUpdatePresetSettings(); // update visibility of dirty preset and icon
243 }
244
slotUpdatePresetSettings()245 void KisWdgSeExpr::slotUpdatePresetSettings()
246 {
247 // hide options on UI if we are creating a brush preset from scratch to prevent confusion
248 if (m_isCreatingPresetFromScratch) {
249 m_widget->presetThumbnailicon->setVisible(false);
250 m_widget->dirtyPresetIndicatorButton->setVisible(false);
251 m_widget->reloadPresetButton->setVisible(false);
252 m_widget->saveBrushPresetButton->setVisible(false);
253 m_widget->saveNewBrushPresetButton->setEnabled(false);
254 m_widget->renameBrushPresetButton->setVisible(false);
255 } else {
256 // In SeExpr's case, there is never a default preset -- amyspark
257 if (!m_currentPreset) {
258 return;
259 }
260
261 bool isPresetDirty = m_currentPreset->isDirty();
262
263 m_widget->presetThumbnailicon->setVisible(true);
264 // don't need to reload or overwrite a clean preset
265 m_widget->dirtyPresetIndicatorButton->setVisible(isPresetDirty);
266 m_widget->reloadPresetButton->setVisible(isPresetDirty);
267 m_widget->saveBrushPresetButton->setEnabled(isPresetDirty);
268 m_widget->saveNewBrushPresetButton->setEnabled(true);
269 m_widget->renameBrushPresetButton->setVisible(true);
270 }
271 }
272
slotSaveBrushPreset()273 void KisWdgSeExpr::slotSaveBrushPreset()
274 {
275 KisFilterConfigurationSP currentConfiguration = static_cast<KisFilterConfiguration *>(configuration().data());
276
277 m_saveDialog->useNewPresetDialog(false); // this mostly just makes sure we keep the existing brush preset name when saving
278 m_saveDialog->setCurrentPreset(m_currentPreset);
279 m_saveDialog->setCurrentRenderConfiguration(currentConfiguration);
280 m_saveDialog->loadExistingThumbnail(); // This makes sure we use the existing preset icon when updating the existing brush preset
281 m_saveDialog->savePreset();
282
283 // refresh the view settings so the brush doesn't appear dirty
284 slotUpdatePresetSettings();
285 }
286
slotSaveNewBrushPreset()287 void KisWdgSeExpr::slotSaveNewBrushPreset()
288 {
289 KisFilterConfigurationSP currentConfiguration = static_cast<KisFilterConfiguration *>(configuration().data());
290
291 m_saveDialog->useNewPresetDialog(true);
292 m_saveDialog->setCurrentPreset(m_currentPreset);
293 m_saveDialog->setCurrentRenderConfiguration(currentConfiguration);
294 m_saveDialog->showDialog();
295 }
296
slotReloadPresetClicked()297 void KisWdgSeExpr::slotReloadPresetClicked()
298 {
299 auto *rserver = KoResourceServerProvider::instance()->seExprScriptServer();
300 auto preset = rserver->resourceByName(m_currentPreset->name());
301 if (preset) {
302 preset->load();
303
304 KIS_ASSERT(!preset->isDirty());
305
306 slotResourceSelected(preset);
307 }
308 }
309
isValid()310 void KisWdgSeExpr::isValid()
311 {
312 QString script = m_widget->txtEditor->getExpr();
313 SeExprExpressionContext expression(script);
314
315 expression.setDesiredReturnType(KSeExpr::ExprType().FP(3));
316
317 expression.m_vars["u"] = new SeExprVariable();
318 expression.m_vars["v"] = new SeExprVariable();
319 expression.m_vars["w"] = new SeExprVariable();
320 expression.m_vars["h"] = new SeExprVariable();
321
322 m_widget->txtEditor->clearErrors();
323
324 if (!expression.isValid()) {
325 auto errors = expression.getErrors();
326
327 for (auto occurrence : errors) {
328 QString message = ErrorMessages::message(occurrence.error);
329 for (auto arg : occurrence.ids) {
330 message = message.arg(QString::fromStdString(arg));
331 }
332 m_widget->txtEditor->addError(occurrence.startPos, occurrence.endPos, message);
333 }
334
335 m_widget->saveBrushPresetButton->setEnabled(false);
336 m_widget->saveNewBrushPresetButton->setEnabled(false);
337 }
338 // Should not happen now, but I've left it for completeness's sake
339 else if (!expression.returnType().isFP(3)) {
340 QString type = QString::fromStdString(expression.returnType().toString());
341 m_widget->txtEditor->addError(1, 1, tr2i18n("Expected this script to output color, got '%1'").arg(type));
342
343 m_widget->saveBrushPresetButton->setEnabled(false);
344 m_widget->saveNewBrushPresetButton->setEnabled(false);
345 } else {
346 m_widget->txtEditor->clearErrors();
347 emit sigConfigurationUpdated();
348
349 if (m_currentPreset) {
350 if (m_currentPreset->script() != m_widget->txtEditor->getExpr()) {
351 m_currentPreset->setScript(m_widget->txtEditor->getExpr());
352 m_currentPreset->setDirty(true);
353 }
354 slotUpdatePresetSettings();
355 }
356 }
357 }
358