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