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 }