Skip to content

Commit b081934

Browse files
dogboydogbjorn
andauthored
Allow JS expressions in number inputs (#4234)
Replaced QSpinBox and QDoubleSpinBox throughout the app, including in script dialogs and properties view, with subclasses that allows setting a value using a JavaScript expression. Any prefix or suffix is temporarily hidden while a spin box has focus because it gets in the way or is automatically added when it shouldn't. This way we also don't need to parse them out before evaluating the text. QSpinBox::setValue and QDoubleSpinBox::setValue cause the text to get reset to the current value, but we don't want to do this while the user is still typing. So in the Properties view we now compare values first to not call setValue needlessly. We don't evaluate the expression in the validate implementation because this is called a bit too much to be executing JS. It also gets called when plain number values are set on the spin box, for example during initialization. Co-authored-by: Thorbjørn Lindeijer <[email protected]>
1 parent 9ba0a5c commit b081934

15 files changed

+365
-94
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Unreleased
22

3+
* Allow changing the values of number inputs using expressions (with dogboydog, #4234)
34
* Added support for SVG 1.2 / CSS blending modes to layers (#3932)
45
* Added button to toggle Terrain Brush to full tile mode (by Finlay Pearson, #3407)
56
* Added export plugin for Remixed Dungeon (by Mikhael Danilov, #4158)

src/tiled/expressionspinbox.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* expressionspinbox.cpp
3+
* Copyright 2025, dogboydog
4+
*
5+
* This file is part of Tiled.
6+
*
7+
* This program is free software; you can redistribute it and/or modify it
8+
* under the terms of the GNU General Public License as published by the Free
9+
* Software Foundation; either version 2 of the License, or (at your option)
10+
* any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15+
* more details.
16+
*
17+
* You should have received a copy of the GNU General Public License along with
18+
* this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
#include "expressionspinbox.h"
22+
23+
#include <QJSEngine>
24+
25+
namespace Tiled {
26+
27+
QJSEngine *ExpressionEvaluator::mEngine;
28+
29+
void ExpressionEvaluator::deleteInstance()
30+
{
31+
delete mEngine;
32+
mEngine = nullptr;
33+
}
34+
35+
QJSValue ExpressionEvaluator::evaluate(const QString &program)
36+
{
37+
if (!mEngine)
38+
mEngine = new QJSEngine;
39+
return mEngine->evaluate(program);
40+
}
41+
42+
43+
// ExpressionSpinBox
44+
45+
ExpressionSpinBox::ExpressionSpinBox(QWidget *parent)
46+
: QSpinBox(parent)
47+
{}
48+
49+
int ExpressionSpinBox::valueFromText(const QString &text) const
50+
{
51+
const QJSValue result = ExpressionEvaluator::evaluate(text);
52+
if (result.isNumber())
53+
return result.toNumber();
54+
55+
return value();
56+
}
57+
58+
QValidator::State ExpressionSpinBox::validate(QString &/*text*/, int &/*pos*/) const
59+
{
60+
return QValidator::Acceptable;
61+
}
62+
63+
void ExpressionSpinBox::focusInEvent(QFocusEvent *event)
64+
{
65+
// Remember current prefix/suffix and remove them
66+
mPrefix = prefix();
67+
mSuffix = suffix();
68+
if (!mPrefix.isEmpty())
69+
setPrefix(QString());
70+
if (!mSuffix.isEmpty())
71+
setSuffix(QString());
72+
73+
QSpinBox::focusInEvent(event);
74+
}
75+
76+
void ExpressionSpinBox::focusOutEvent(QFocusEvent *event)
77+
{
78+
QSpinBox::focusOutEvent(event);
79+
80+
// Restore any previously removed prefix/suffix
81+
if (!mPrefix.isEmpty())
82+
setPrefix(mPrefix);
83+
if (!mSuffix.isEmpty())
84+
setSuffix(mSuffix);
85+
mPrefix.clear();
86+
mSuffix.clear();
87+
}
88+
89+
// ExpressionDoubleSpinBox
90+
91+
ExpressionDoubleSpinBox::ExpressionDoubleSpinBox(QWidget *parent)
92+
: QDoubleSpinBox(parent)
93+
{}
94+
95+
double ExpressionDoubleSpinBox::valueFromText(const QString &text) const
96+
{
97+
const QJSValue result = ExpressionEvaluator::evaluate(text);
98+
if (result.isNumber())
99+
return result.toNumber();
100+
101+
return value();
102+
}
103+
104+
QValidator::State ExpressionDoubleSpinBox::validate(QString &/*text*/, int &/*pos*/) const
105+
{
106+
return QValidator::Acceptable;
107+
}
108+
109+
void ExpressionDoubleSpinBox::focusInEvent(QFocusEvent *event)
110+
{
111+
// Remember current prefix/suffix and remove them
112+
mPrefix = prefix();
113+
mSuffix = suffix();
114+
if (!mPrefix.isEmpty())
115+
setPrefix(QString());
116+
if (!mSuffix.isEmpty())
117+
setSuffix(QString());
118+
119+
QDoubleSpinBox::focusInEvent(event);
120+
}
121+
122+
void ExpressionDoubleSpinBox::focusOutEvent(QFocusEvent *event)
123+
{
124+
QDoubleSpinBox::focusOutEvent(event);
125+
126+
// Restore any previously removed prefix/suffix
127+
if (!mPrefix.isEmpty())
128+
setPrefix(mPrefix);
129+
if (!mSuffix.isEmpty())
130+
setSuffix(mSuffix);
131+
mPrefix.clear();
132+
mSuffix.clear();
133+
}
134+
135+
} // namespace Tiled
136+
137+
#include "moc_expressionspinbox.cpp"

src/tiled/expressionspinbox.h

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* expressionspinbox.h
3+
* Copyright 2025, dogboydog
4+
*
5+
* This file is part of Tiled.
6+
*
7+
* This program is free software; you can redistribute it and/or modify it
8+
* under the terms of the GNU General Public License as published by the Free
9+
* Software Foundation; either version 2 of the License, or (at your option)
10+
* any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15+
* more details.
16+
*
17+
* You should have received a copy of the GNU General Public License along with
18+
* this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
#pragma once
22+
23+
#include <QJSValue>
24+
#include <QSpinBox>
25+
26+
namespace Tiled {
27+
28+
/**
29+
* @brief The ExpressionEvaluator class can evaluate simple Javascript expressions
30+
* that do not require access to the Tiled scripting API, nor the Tiled project.
31+
*/
32+
class ExpressionEvaluator
33+
{
34+
public:
35+
static void deleteInstance();
36+
static QJSValue evaluate(const QString &program);
37+
38+
private:
39+
static QJSEngine *mEngine;
40+
};
41+
42+
43+
class ExpressionSpinBox : public QSpinBox
44+
{
45+
Q_OBJECT
46+
47+
public:
48+
ExpressionSpinBox(QWidget *parent);
49+
50+
protected:
51+
int valueFromText(const QString &text) const override;
52+
QValidator::State validate(QString &text, int &pos) const override;
53+
54+
void focusInEvent(QFocusEvent *event) override;
55+
void focusOutEvent(QFocusEvent *event) override;
56+
57+
private:
58+
QString mPrefix;
59+
QString mSuffix;
60+
};
61+
62+
63+
class ExpressionDoubleSpinBox : public QDoubleSpinBox
64+
{
65+
Q_OBJECT
66+
67+
public:
68+
ExpressionDoubleSpinBox(QWidget *parent);
69+
70+
protected:
71+
double valueFromText(const QString &text) const override;
72+
QValidator::State validate(QString &text, int &pos) const override;
73+
74+
void focusInEvent(QFocusEvent *event) override;
75+
void focusOutEvent(QFocusEvent *event) override;
76+
77+
private:
78+
QString mPrefix;
79+
QString mSuffix;
80+
};
81+
82+
} // namespace Tiled

src/tiled/libtilededitor.qbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ DynamicLibrary {
247247
"exportasimagedialog.ui",
248248
"exporthelper.cpp",
249249
"exporthelper.h",
250+
"expressionspinbox.cpp",
251+
"expressionspinbox.h",
250252
"filechangedwarning.cpp",
251253
"filechangedwarning.h",
252254
"fileedit.cpp",

src/tiled/newmapdialog.ui

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</property>
1616
<layout class="QGridLayout" name="gridLayout_3">
1717
<property name="sizeConstraint">
18-
<enum>QLayout::SetFixedSize</enum>
18+
<enum>QLayout::SizeConstraint::SetFixedSize</enum>
1919
</property>
2020
<item row="0" column="0" colspan="2">
2121
<widget class="QGroupBox" name="groupBox">
@@ -91,7 +91,7 @@
9191
<item>
9292
<layout class="QGridLayout" name="gridLayout_2">
9393
<item row="1" column="2">
94-
<widget class="QSpinBox" name="mapHeight">
94+
<widget class="Tiled::ExpressionSpinBox" name="mapHeight">
9595
<property name="suffix">
9696
<string> tiles</string>
9797
</property>
@@ -117,7 +117,7 @@
117117
</widget>
118118
</item>
119119
<item row="0" column="2">
120-
<widget class="QSpinBox" name="mapWidth">
120+
<widget class="Tiled::ExpressionSpinBox" name="mapWidth">
121121
<property name="suffix">
122122
<string extracomment="Remember starting with a space."> tiles</string>
123123
</property>
@@ -152,10 +152,10 @@
152152
<item row="1" column="0">
153153
<spacer name="fixedSizeSpacer">
154154
<property name="orientation">
155-
<enum>Qt::Horizontal</enum>
155+
<enum>Qt::Orientation::Horizontal</enum>
156156
</property>
157157
<property name="sizeType">
158-
<enum>QSizePolicy::Fixed</enum>
158+
<enum>QSizePolicy::Policy::Fixed</enum>
159159
</property>
160160
<property name="sizeHint" stdset="0">
161161
<size>
@@ -194,7 +194,7 @@
194194
</widget>
195195
</item>
196196
<item row="0" column="1">
197-
<widget class="QSpinBox" name="tileWidth">
197+
<widget class="Tiled::ExpressionSpinBox" name="tileWidth">
198198
<property name="suffix">
199199
<string extracomment="Remember starting with a space."> px</string>
200200
</property>
@@ -220,7 +220,7 @@
220220
</widget>
221221
</item>
222222
<item row="1" column="1">
223-
<widget class="QSpinBox" name="tileHeight">
223+
<widget class="Tiled::ExpressionSpinBox" name="tileHeight">
224224
<property name="suffix">
225225
<string> px</string>
226226
</property>
@@ -238,7 +238,7 @@
238238
<item row="2" column="0" colspan="2">
239239
<spacer name="verticalSpacer_2">
240240
<property name="orientation">
241-
<enum>Qt::Vertical</enum>
241+
<enum>Qt::Orientation::Vertical</enum>
242242
</property>
243243
<property name="sizeHint" stdset="0">
244244
<size>
@@ -254,7 +254,7 @@
254254
<item row="2" column="0">
255255
<spacer name="verticalSpacer">
256256
<property name="orientation">
257-
<enum>Qt::Vertical</enum>
257+
<enum>Qt::Orientation::Vertical</enum>
258258
</property>
259259
<property name="sizeHint" stdset="0">
260260
<size>
@@ -267,15 +267,22 @@
267267
<item row="3" column="0" colspan="2">
268268
<widget class="QDialogButtonBox" name="buttonBox">
269269
<property name="orientation">
270-
<enum>Qt::Horizontal</enum>
270+
<enum>Qt::Orientation::Horizontal</enum>
271271
</property>
272272
<property name="standardButtons">
273-
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
273+
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
274274
</property>
275275
</widget>
276276
</item>
277277
</layout>
278278
</widget>
279+
<customwidgets>
280+
<customwidget>
281+
<class>Tiled::ExpressionSpinBox</class>
282+
<extends>QSpinBox</extends>
283+
<header>expressionspinbox.h</header>
284+
</customwidget>
285+
</customwidgets>
279286
<tabstops>
280287
<tabstop>orientation</tabstop>
281288
<tabstop>layerFormat</tabstop>

0 commit comments

Comments
 (0)