File indexing completed on 2024-06-16 04:16:50

0001 /*
0002  * This file is part of Krita
0003  *
0004  * SPDX-FileCopyrightText: 2020 L. E. Segovia <amy@amyspark.me>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include <QCheckBox>
0010 
0011 #include <KSeExprUI/ErrorMessages.h>
0012 #include <KisDialogStateSaver.h>
0013 #include <KisGlobalResourcesInterface.h>
0014 #include <KisResourceUserOperations.h>
0015 #include <KoColor.h>
0016 #include <KoResourceServer.h>
0017 #include <KoResourceServerProvider.h>
0018 
0019 #include <filter/kis_filter_configuration.h>
0020 #include <kis_assert.h>
0021 #include <kis_config.h>
0022 #include <kis_debug.h>
0023 #include <kis_icon.h>
0024 #include <kis_signals_blocker.h>
0025 
0026 #include "SeExprExpressionContext.h"
0027 #include "generator.h"
0028 #include "kis_wdg_seexpr.h"
0029 #include "ui_wdgseexpr.h"
0030 
0031 KisWdgSeExpr::KisWdgSeExpr(QWidget *parent)
0032     : KisConfigWidget(parent)
0033     , updateCompressor(1000, KisSignalCompressor::Mode::POSTPONE)
0034     , m_currentPreset(new KisSeExprScript(i18n("Untitled")))
0035     , m_saveDialog(new KisWdgSeExprPresetsSave(this))
0036     , m_isCreatingPresetFromScratch(true)
0037 {
0038     m_widget = new Ui_WdgSeExpr();
0039     m_widget->setupUi(this);
0040     m_widget->txtEditor->setControlCollectionWidget(m_widget->wdgControls);
0041 
0042     m_widget->renameBrushPresetButton->setIcon(KisIconUtils::loadIcon("document-edit"));
0043 
0044     m_widget->reloadPresetButton->setIcon(KisIconUtils::loadIcon("reload-preset-16"));
0045     m_widget->reloadPresetButton->setToolTip(i18n("Reload the preset"));
0046     m_widget->dirtyPresetIndicatorButton->setIcon(KisIconUtils::loadIcon("warning"));
0047     m_widget->dirtyPresetIndicatorButton->setToolTip(i18n("The settings for this preset have changed from their default."));
0048 
0049     KisDialogStateSaver::restoreState(m_widget->txtEditor, "krita/generators/seexpr");
0050     // Manually restore SeExpr state. KisDialogStateSaver uses setPlainText, not text itself
0051     m_widget->txtEditor->setExpr(m_widget->txtEditor->exprTe->toPlainText());
0052 
0053     m_widget->txtEditor->registerExtraVariable("$u", i18nc("SeExpr variable", "Normalized X axis coordinate of the image from its top-left corner"));
0054     m_widget->txtEditor->registerExtraVariable("$v", i18nc("SeExpr variable", "Normalized Y axis coordinate of the image from its top-left corner"));
0055     m_widget->txtEditor->registerExtraVariable("$w", i18nc("SeExpr variable", "Image width"));
0056     m_widget->txtEditor->registerExtraVariable("$h", i18nc("SeExpr variable", "Image height"));
0057 
0058     m_widget->txtEditor->updateCompleter();
0059 
0060     m_widget->txtEditor->exprTe->setFont(QFontDatabase().systemFont(QFontDatabase::FixedFont));
0061     const QFontMetricsF fntTe(m_widget->txtEditor->exprTe->fontMetrics());
0062     m_widget->txtEditor->exprTe->setTabStopDistance(fntTe.horizontalAdvance("    "));
0063 
0064     connect(m_widget->scriptSelectorWidget, SIGNAL(resourceSelected(KoResourceSP)), this, SLOT(slotResourceSelected(KoResourceSP)));
0065     connect(m_saveDialog, SIGNAL(resourceSelected(KoResourceSP)), this, SLOT(slotResourceSaved(KoResourceSP)));
0066 
0067     connect(m_widget->renameBrushPresetButton, SIGNAL(clicked(bool)),
0068             this, SLOT(slotRenamePresetActivated()));
0069     connect(m_widget->cancelBrushNameUpdateButton, SIGNAL(clicked(bool)),
0070             this, SLOT(slotRenamePresetDeactivated()));
0071     connect(m_widget->updateBrushNameButton, SIGNAL(clicked(bool)),
0072             this, SLOT(slotSaveRenameCurrentPreset()));
0073     connect(m_widget->renameBrushNameTextField, SIGNAL(returnPressed()),
0074             this, SLOT(slotSaveRenameCurrentPreset()));
0075 
0076     connect(m_widget->saveBrushPresetButton, SIGNAL(clicked()),
0077         this, SLOT(slotSaveBrushPreset()));
0078     connect(m_widget->saveNewBrushPresetButton, SIGNAL(clicked()),
0079         this, SLOT(slotSaveNewBrushPreset()));
0080 
0081     connect(m_widget->reloadPresetButton, SIGNAL(clicked()),
0082         this, SLOT(slotReloadPresetClicked()));
0083 
0084     connect(m_widget->txtEditor, SIGNAL(apply()),
0085             &updateCompressor, SLOT(start()));
0086     connect(m_widget->txtEditor, SIGNAL(preview()),
0087             &updateCompressor, SLOT(start()));
0088     connect(m_widget->txtEditor, &ExprEditor::preview, this, &KisWdgSeExpr::slotHideCheckboxes); // HACK: hide disney checkboxes
0089 
0090     connect(&updateCompressor, SIGNAL(timeout()), this, SLOT(isValid()));
0091 
0092     togglePresetRenameUIActive(false); // reset the UI state of renaming a preset if we are changing presets
0093     slotUpdatePresetSettings();        // disable everything until a preset is selected
0094 
0095     m_widget->splitter->restoreState(KisConfig(true).readEntry("seExpr/splitLayoutState", QByteArray())); // restore splitter state
0096     m_widget->tabWidget->setCurrentIndex(KisConfig(true).readEntry("seExpr/selectedTab",  -1));               // save currently selected tab
0097 }
0098 
0099 KisWdgSeExpr::~KisWdgSeExpr()
0100 {
0101     KisDialogStateSaver::saveState(m_widget->txtEditor, "krita/generators/seexpr");
0102     KisConfig(false).writeEntry("seExpr/splitLayoutState", m_widget->splitter->saveState()); // save splitter state
0103     KisConfig(false).writeEntry("seExpr/selectedTab", m_widget->tabWidget->currentIndex()); // save currently selected tab
0104 
0105     delete m_saveDialog;
0106     delete m_widget;
0107 }
0108 
0109 inline const Ui_WdgSeExpr *KisWdgSeExpr::widget() const
0110 {
0111     return m_widget;
0112 }
0113 
0114 void KisWdgSeExpr::setConfiguration(const KisPropertiesConfigurationSP config)
0115 {
0116     auto rserver = KoResourceServerProvider::instance()->seExprScriptServer();
0117     auto name = config->getString("seexpr", "Disney_noisecolor2");
0118     auto pattern = rserver->resource("", "", name);
0119     if (pattern) {
0120         m_widget->scriptSelectorWidget->setCurrentScript(pattern);
0121     }
0122 
0123     QString script = config->getString("script");
0124 
0125     if (!script.isNull()) {
0126         m_widget->txtEditor->setExpr(script, true);
0127     }
0128 }
0129 
0130 KisPropertiesConfigurationSP KisWdgSeExpr::configuration() const
0131 {
0132     KisFilterConfigurationSP config = new KisFilterConfiguration("seexpr", 1, KisGlobalResourcesInterface::instance());
0133 
0134     if (m_widget->scriptSelectorWidget->currentResource()) {
0135         QVariant v;
0136         v.setValue(m_widget->scriptSelectorWidget->currentResource()->name());
0137         config->setProperty("pattern", v);
0138     }
0139     config->setProperty("script", QVariant(m_widget->txtEditor->getExpr()));
0140 
0141     return config;
0142 }
0143 
0144 void KisWdgSeExpr::slotResourceSaved(KoResourceSP resource)
0145 {
0146     if (resource) {
0147         m_widget->scriptSelectorWidget->setCurrentScript(resource);
0148         slotResourceSelected(resource);
0149     }
0150 }
0151 
0152 void KisWdgSeExpr::slotResourceSelected(KoResourceSP resource)
0153 {
0154     KisSeExprScriptSP preset = resource.dynamicCast<KisSeExprScript>();
0155     if (preset) {
0156         m_currentPreset = preset;
0157 
0158         m_isCreatingPresetFromScratch = false;
0159 
0160         m_widget->txtEditor->setExpr(m_currentPreset->script(), true);
0161 
0162         QString formattedBrushName = m_currentPreset->name().replace("_", " ");
0163         m_widget->currentBrushNameLabel->setText(formattedBrushName);
0164         m_widget->renameBrushNameTextField->setText(formattedBrushName);
0165         // get the preset image and pop it into the thumbnail area on the top of the brush editor
0166         QSize thumbSize = QSize(55, 55)*devicePixelRatioF();
0167         QPixmap thumbnail = QPixmap::fromImage(m_currentPreset->image().scaled(thumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
0168         thumbnail.setDevicePixelRatio(devicePixelRatioF());
0169         m_widget->presetThumbnailicon->setPixmap(thumbnail);
0170 
0171         togglePresetRenameUIActive(false); // reset the UI state of renaming a brush if we are changing brush presets
0172         slotUpdatePresetSettings();        // check to see if the dirty preset icon needs to be shown
0173 
0174         updateCompressor.start();
0175     }
0176 }
0177 
0178 void KisWdgSeExpr::slotRenamePresetActivated()
0179 {
0180     togglePresetRenameUIActive(true);
0181 }
0182 
0183 void KisWdgSeExpr::slotRenamePresetDeactivated()
0184 {
0185     togglePresetRenameUIActive(false);
0186 }
0187 
0188 void KisWdgSeExpr::togglePresetRenameUIActive(bool isRenaming)
0189 {
0190     // This function doesn't really do anything except get the UI in a state to rename a brush preset
0191     m_widget->renameBrushNameTextField->setVisible(isRenaming);
0192     m_widget->updateBrushNameButton->setVisible(isRenaming);
0193     m_widget->cancelBrushNameUpdateButton->setVisible(isRenaming);
0194 
0195     // hide these below areas while renaming
0196     m_widget->currentBrushNameLabel->setVisible(!isRenaming);
0197     m_widget->renameBrushPresetButton->setVisible(!isRenaming);
0198     m_widget->saveBrushPresetButton->setEnabled(!isRenaming);
0199     m_widget->saveBrushPresetButton->setVisible(!isRenaming);
0200     m_widget->saveNewBrushPresetButton->setEnabled(!isRenaming);
0201     m_widget->saveNewBrushPresetButton->setVisible(!isRenaming);
0202 }
0203 
0204 void KisWdgSeExpr::slotSaveRenameCurrentPreset()
0205 {
0206     KIS_ASSERT_RECOVER_RETURN(m_currentPreset);
0207 
0208     // if you are renaming a brush, that is different than updating the settings
0209     // make sure we are in a clean state before renaming. This logic might change,
0210     // but that is what we are going with for now
0211     const auto prevScript = m_currentPreset->script();
0212     bool isDirty = m_currentPreset->isDirty();
0213 
0214     // this returns the UI to its original state after saving
0215     togglePresetRenameUIActive(false);
0216     slotUpdatePresetSettings(); // update visibility of dirty preset and icon
0217 
0218     // in case the preset is dirty, we need an id to get the actual non-dirty preset to save just the name change
0219     // into the database
0220     int currentPresetResourceId = m_currentPreset->resourceId();
0221 
0222     QString renamedPresetName = m_widget->renameBrushNameTextField->text();
0223 
0224     // If the id < 0, this is a new preset that hasn't been added to the storage and the database yet.
0225     if (currentPresetResourceId < 0) {
0226         m_currentPreset->setName(renamedPresetName);
0227         slotUpdatePresetSettings(); // update visibility of dirty preset and icon
0228         return;
0229     }
0230 
0231     slotReloadPresetClicked();
0232 
0233     // create a new brush preset with the name specified and add to resource provider
0234     KisResourceModel model(ResourceType::SeExprScripts);
0235     KoResourceSP properCleanResource = model.resourceForId(currentPresetResourceId);
0236     const bool success = KisResourceUserOperations::renameResourceWithUserInput(this, properCleanResource, renamedPresetName);
0237 
0238     if (isDirty) {
0239         properCleanResource.dynamicCast<KisSeExprScript>()->setScript(prevScript);
0240         properCleanResource.dynamicCast<KisSeExprScript>()->setDirty(isDirty);
0241     }
0242 
0243     // refresh and select our freshly renamed resource
0244     if (success) slotResourceSelected(properCleanResource);
0245 
0246 
0247     slotUpdatePresetSettings(); // update visibility of dirty preset and icon
0248 }
0249 
0250 void KisWdgSeExpr::slotUpdatePresetSettings()
0251 {
0252     // hide options on UI if we are creating a brush preset from scratch to prevent confusion
0253     if (m_isCreatingPresetFromScratch) {
0254         m_widget->presetThumbnailicon->setVisible(false);
0255         m_widget->dirtyPresetIndicatorButton->setVisible(false);
0256         m_widget->reloadPresetButton->setVisible(false);
0257         m_widget->saveBrushPresetButton->setVisible(false);
0258         m_widget->saveNewBrushPresetButton->setEnabled(false);
0259         m_widget->renameBrushPresetButton->setVisible(false);
0260     } else {
0261         // In SeExpr's case, there is never a default preset -- amyspark
0262         if (!m_currentPreset) {
0263             return;
0264         }
0265 
0266         bool isPresetDirty = m_currentPreset->isDirty();
0267 
0268         m_widget->presetThumbnailicon->setVisible(true);
0269         // don't need to reload or overwrite a clean preset
0270         m_widget->dirtyPresetIndicatorButton->setVisible(isPresetDirty);
0271         m_widget->reloadPresetButton->setVisible(isPresetDirty);
0272         m_widget->saveBrushPresetButton->setEnabled(isPresetDirty);
0273         m_widget->saveNewBrushPresetButton->setEnabled(true);
0274         m_widget->renameBrushPresetButton->setVisible(true);
0275     }
0276 }
0277 
0278 void KisWdgSeExpr::slotSaveBrushPreset()
0279 {
0280     KisFilterConfigurationSP currentConfiguration = static_cast<KisFilterConfiguration *>(configuration().data());
0281 
0282     m_saveDialog->useNewPresetDialog(false); // this mostly just makes sure we keep the existing brush preset name when saving
0283     m_saveDialog->setCurrentPreset(m_currentPreset);
0284     m_saveDialog->setCurrentRenderConfiguration(currentConfiguration);
0285     m_saveDialog->loadExistingThumbnail(); // This makes sure we use the existing preset icon when updating the existing brush preset
0286     m_saveDialog->showDialog(); // apply tiar's suggestion and let the user decide
0287 }
0288 
0289 void KisWdgSeExpr::slotSaveNewBrushPreset()
0290 {
0291     KisFilterConfigurationSP currentConfiguration = static_cast<KisFilterConfiguration *>(configuration().data());
0292 
0293     m_saveDialog->useNewPresetDialog(true);
0294     m_saveDialog->setCurrentPreset(m_currentPreset);
0295     m_saveDialog->setCurrentRenderConfiguration(currentConfiguration);
0296     m_saveDialog->loadExistingThumbnail();
0297     m_saveDialog->showDialog();
0298 }
0299 
0300 void KisWdgSeExpr::slotReloadPresetClicked()
0301 {
0302     KisSignalsBlocker blocker(this);
0303 
0304     KisResourceModel model(ResourceType::SeExprScripts);
0305     const bool success = model.reloadResource(m_currentPreset);
0306 
0307     KIS_SAFE_ASSERT_RECOVER_NOOP(success && "couldn't reload preset");
0308 
0309     warnPlugins << "resourceSelected: preset" << m_currentPreset
0310                 << (m_currentPreset ? QString("%1").arg(m_currentPreset->valid()) : "");
0311 
0312     KIS_ASSERT(!m_currentPreset->isDirty());
0313 
0314     // refresh and select our freshly renamed resource
0315     if (success) slotResourceSelected(m_currentPreset);
0316 }
0317 
0318 void KisWdgSeExpr::slotHideCheckboxes()
0319 {
0320     for (auto controls : m_widget->wdgControls->findChildren<ExprControl *>()) {
0321         for (auto wdg : controls->findChildren<QCheckBox *>(QString(), Qt::FindDirectChildrenOnly)) {
0322             wdg->setCheckState(Qt::Unchecked);
0323             wdg->setVisible(false);
0324         }
0325     }
0326 }
0327 
0328 void KisWdgSeExpr::isValid()
0329 {
0330     QString script = m_widget->txtEditor->getExpr();
0331     SeExprExpressionContext expression(script);
0332 
0333     expression.setDesiredReturnType(KSeExpr::ExprType().FP(3));
0334 
0335     expression.m_vars["u"] = new SeExprVariable();
0336     expression.m_vars["v"] = new SeExprVariable();
0337     expression.m_vars["w"] = new SeExprVariable();
0338     expression.m_vars["h"] = new SeExprVariable();
0339 
0340     m_widget->txtEditor->clearErrors();
0341 
0342     if (!expression.isValid()) {
0343         const auto &errors = expression.getErrors();
0344 
0345         for (const auto &occurrence : errors) {
0346             QString message = ErrorMessages::message(occurrence.error);
0347             for (const auto &arg : occurrence.ids) {
0348                 message = message.arg(QString::fromStdString(arg));
0349             }
0350             m_widget->txtEditor->addError(occurrence.startPos, occurrence.endPos, message);
0351         }
0352 
0353         m_widget->saveBrushPresetButton->setEnabled(false);
0354         m_widget->saveNewBrushPresetButton->setEnabled(false);
0355     }
0356     // Should not happen now, but I've left it for completeness's sake
0357     else if (!expression.returnType().isFP(3)) {
0358         QString type = QString::fromStdString(expression.returnType().toString());
0359         m_widget->txtEditor->addError(1, 1, tr2i18n("Expected this script to output color, got '%1'").arg(type));
0360 
0361         m_widget->saveBrushPresetButton->setEnabled(false);
0362         m_widget->saveNewBrushPresetButton->setEnabled(false);
0363     } else {
0364         m_widget->txtEditor->clearErrors();
0365         emit sigConfigurationItemChanged();
0366 
0367         if (m_currentPreset->script() != m_widget->txtEditor->getExpr()) {
0368             m_currentPreset->setScript(m_widget->txtEditor->getExpr());
0369             m_currentPreset->setDirty(true);
0370         }
0371 
0372         slotUpdatePresetSettings();
0373 
0374         // Override preset settings with the following
0375         if (m_isCreatingPresetFromScratch) {
0376             m_widget->saveNewBrushPresetButton->setEnabled(true);
0377         }
0378     }
0379 }