Render the layers of each script separately
Some checks are pending
Build Porymap / build-linux (, 5.14.2) (push) Waiting to run
Build Porymap / build-linux (, 6.8.*) (push) Waiting to run
Build Porymap / build-linux (minimal, 5.14.2) (push) Waiting to run
Build Porymap / build-macos (macos-15-intel) (push) Waiting to run
Build Porymap / build-macos (macos-latest) (push) Waiting to run
Build Porymap / build-static-windows (push) Waiting to run

This commit is contained in:
GriffinR 2026-01-26 18:33:34 -05:00
parent 9728a851cf
commit f2bc3613ad
11 changed files with 176 additions and 134 deletions

View File

@ -48,7 +48,9 @@ public:
static void stop();
static void populateGlobalObject(MainWindow *mainWindow);
static QJSEngine *getEngine();
static QString getCurrentScriptHash();
static void invokeAction(int actionIndex);
static void setTimeout(QJSValue callback, int milliseconds);
static void cb_ProjectOpened(QString projectPath);
static void cb_ProjectClosed(QString projectPath);
@ -82,32 +84,33 @@ private:
MainWindow *mainWindow;
QJSEngine *engine;
class Script {
public:
Script() {};
class Script
{
public:
Script() {};
QString filepath() const { return m_filepath; }
QString fileName() const {
QFileInfo fileInfo(m_filepath);
return fileInfo.fileName();
QString filepath() const { return m_filepath; }
QString fileName() const {
QFileInfo fileInfo(m_filepath);
return fileInfo.fileName();
}
void setFilepath(const QString &filepath) { m_filepath = filepath; }
QJSValue module() const { return m_module; }
void setModule(const QJSValue &module) { m_module = module; }
QString hash() {
if (m_hash.isEmpty()) {
// We won't need to check whether most scripts are trusted,
// so only calculate the hash when it's first requested.
m_hash = Util::getFileHash(m_filepath);
}
void setFilepath(const QString &filepath) { m_filepath = filepath; }
QJSValue module() const { return m_module; }
void setModule(const QJSValue &module) { m_module = module; }
QString hash() {
if (m_hash.isEmpty()) {
// We won't need to check whether most scripts are trusted,
// so only calculate the hash when it's first requested.
m_hash = Util::getFileHash(m_filepath);
}
return m_hash;
}
private:
QString m_filepath;
QJSValue m_module;
QString m_hash;
return m_hash;
}
private:
QString m_filepath;
QJSValue m_module;
QString m_hash;
};
QList<QSharedPointer<Script>> scripts;
@ -119,14 +122,14 @@ private:
// track of multiple scripts executing at once.
QStack<QSharedPointer<Script>> scriptExecutionStack;
QSet<QTimer *> activeTimers;
QMap<QString, const QImage*> imageCache;
ScriptUtility *scriptUtility;
void loadScript(const QString &filepath);
void invokeCallback(CallbackType type, const QJSValueList &args);
void invokeCallback(QSharedPointer<const Script> script, CallbackType type, const QJSValueList &args);
bool invokeAction(QSharedPointer<const Script> script, const QString &functionName);
QSharedPointer<Script> getActiveScript() const;
QJSValue call(QSharedPointer<Script> script, QJSValue func, const QJSValueList &args = QJSValueList());
bool askForTrust(QSharedPointer<Script> script, const QString &reason);
};

View File

@ -75,13 +75,11 @@ public:
static bool validateMetatileLayerOrder(const QList<int> &order);
private:
void callTimeoutFunction(QJSValue callback);
void runMessageBox(QString text, QString informativeText, QString detailedText, QMessageBox::Icon icon);
QString detectProjectPath(const QString &path) const;
MainWindow *window;
QList<QAction *> registeredActions;
QSet<QTimer *> activeTimers;
QHash<int, QString> actionMap;
};

View File

@ -125,7 +125,14 @@ signals:
void pressedDelete();
protected:
virtual void keyPressEvent(QKeyEvent *event) override;
virtual void keyPressEvent(QKeyEvent *event) override {
if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
emit pressedDelete();
event->accept();
} else {
QGraphicsView::keyPressEvent(event);
}
}
};
#endif // GRAPHICSVIEW_H

View File

@ -23,8 +23,8 @@ public:
Editor *editor;
Overlay * getOverlay(int layer);
void clearOverlayMap();
void clearOverlay();
void renderOverlay(QPainter*);
// Overlay scripting API
#ifdef QT_QML_LIB
@ -80,6 +80,7 @@ public:
Q_INVOKABLE void addTileImage(int x, int y, int tileId, bool xflip, bool yflip, int paletteId, bool setTransparency = false, int layer = 0);
Q_INVOKABLE void addTileImage(int x, int y, QJSValue tileObj, bool setTransparency = false, int layer = 0);
Q_INVOKABLE void addMetatileImage(int x, int y, int metatileId, bool setTransparency = false, int layer = 0);
Q_INVOKABLE void setRenderPriority(int priority);
#endif // QT_QML_LIB
protected:
@ -87,10 +88,34 @@ protected:
virtual void keyPressEvent(QKeyEvent*) override;
virtual void moveEvent(QMoveEvent *event) override;
private:
QMap<int, Overlay*> overlayMap;
#ifdef QT_QML_LIB
// Maps a layer number to the overlay items.
// We specifically use a QMap because we guarantee a sorted
// order for the layers so that users can predict rendering.
typedef QMap<int, Overlay*> OverlayLayers;
// Maps a script's file hash to a priority value.
// This priority value is the order in which all layers
// belonging to that script will be rendered relative to
// the layers belonging to other scripts.
// These are normally unique and automatically assigned,
// but users may set this value manually to render scripts
// in an explicit order relative to one another, or even
// to have multiple scripts share layers.
// This is a QMap vs a QHash solely for performance,
// given most users will have relatively few scripts.
QMap<QString, int> hashToPriority;
// Maps a render priority provided by 'hashToPriority'
// to the corresponding overlay layers.
// A QMap is used once again to guarantee render order.
QMap<int, OverlayLayers> overlayMap;
OverlayLayers* getCurrentScriptLayers();
Overlay * getOverlay(int layer);
void updateScene();
void addTileImage(int x, int y, const Tile &tile, bool setTransparency, int layer = 0);
#endif
};
#endif // GRAPHICSVIEW_H

View File

@ -125,17 +125,6 @@ private:
QRectF *clippingRect;
};
#else
class Overlay
{
public:
Overlay() {}
~Overlay() {}
void renderItems(QPainter *) {}
};
#endif // QT_QML_LIB
#endif // OVERLAY_H

View File

@ -108,7 +108,7 @@ SOURCES += src/core/advancemapparser.cpp \
src/ui/maplistmodels.cpp \
src/ui/maplisttoolbar.cpp \
src/ui/message.cpp \
src/ui/graphicsview.cpp \
src/ui/mapview.cpp \
src/ui/imageproviders.cpp \
src/ui/layoutpixmapitem.cpp \
src/ui/prefabcreationdialog.cpp \

View File

@ -3240,7 +3240,7 @@ bool MainWindow::askToFixRegionMapEditor() {
void MainWindow::clearOverlay() {
if (ui->graphicsView_Map)
ui->graphicsView_Map->clearOverlayMap();
ui->graphicsView_Map->clearOverlay();
}
bool MainWindow::closeSupplementaryWindows() {

View File

@ -4,6 +4,39 @@
#include "imageproviders.h"
#include "editor.h"
void MapView::setRenderPriority(int priority) {
const QString hash = Scripting::getCurrentScriptHash();
if (!hash.isEmpty()) this->hashToPriority[hash] = priority;
}
MapView::OverlayLayers* MapView::getCurrentScriptLayers() {
int priority = 1;
const QString hash = Scripting::getCurrentScriptHash();
auto it = this->hashToPriority.find(hash);
if (it == this->hashToPriority.end()) {
// This script does not have a priority, which means
// the user has not assigned it one and we haven't
// retrieved layers for it before.
// Assign it a new unique priority, starting from 1.
QList<int> renderIds = this->hashToPriority.values();
while (renderIds.contains(priority)) priority++;
this->hashToPriority[hash] = priority;
} else {
priority = *it;
}
return &this->overlayMap[priority];
}
Overlay* MapView::getOverlay(int layer) {
OverlayLayers *layers = getCurrentScriptLayers();
Overlay *overlay = layers->value(layer, nullptr);
if (!overlay) {
overlay = new Overlay();
layers->insert(layer, overlay);
}
return overlay;
}
void MapView::updateScene() {
if (this->scene()) {
this->scene()->update();
@ -17,7 +50,7 @@ void MapView::clear(int layer) {
// Overload. No layer provided, clear all layers
void MapView::clear() {
this->clearOverlayMap();
this->clearOverlay();
this->updateScene();
}
@ -50,7 +83,7 @@ void MapView::setVisibility(bool visible, int layer) {
// Overload. No layer provided, set visibility of all layers
void MapView::setVisibility(bool visible) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setHidden(!visible);
this->updateScene();
}
@ -70,7 +103,7 @@ void MapView::setX(int x, int layer) {
// Overload. No layer provided, set x of all layers
void MapView::setX(int x) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setX(x);
this->updateScene();
}
@ -82,7 +115,7 @@ void MapView::setY(int y, int layer) {
// Overload. No layer provided, set y of all layers
void MapView::setY(int y) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setY(y);
this->updateScene();
}
@ -94,7 +127,7 @@ void MapView::setClippingRect(int x, int y, int width, int height, int layer) {
void MapView::setClippingRect(int x, int y, int width, int height) {
QRectF rect = QRectF(x, y, width, height);
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setClippingRect(rect);
this->updateScene();
}
@ -105,7 +138,7 @@ void MapView::clearClippingRect(int layer) {
}
void MapView::clearClippingRect() {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->clearClippingRect();
this->updateScene();
}
@ -122,7 +155,7 @@ void MapView::setPosition(int x, int y, int layer) {
// Overload. No layer provided, set position of all layers
void MapView::setPosition(int x, int y) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setPosition(x, y);
this->updateScene();
}
@ -134,7 +167,7 @@ void MapView::move(int deltaX, int deltaY, int layer) {
// Overload. No layer provided, move all layers
void MapView::move(int deltaX, int deltaY) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->move(deltaX, deltaY);
this->updateScene();
}
@ -150,7 +183,7 @@ void MapView::setOpacity(int opacity, int layer) {
// Overload. No layer provided, set opacity of all layers
void MapView::setOpacity(int opacity) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setOpacity(opacity);
this->updateScene();
}
@ -170,7 +203,7 @@ void MapView::setHorizontalScale(qreal scale, int layer) {
// Overload. No layer provided, set horizontal scale of all layers
void MapView::setHorizontalScale(qreal scale) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setHScale(scale);
this->updateScene();
}
@ -182,7 +215,7 @@ void MapView::setVerticalScale(qreal scale, int layer) {
// Overload. No layer provided, set vertical scale of all layers
void MapView::setVerticalScale(qreal scale) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setVScale(scale);
this->updateScene();
}
@ -194,7 +227,7 @@ void MapView::setScale(qreal hScale, qreal vScale, int layer) {
// Overload. No layer provided, set scale of all layers
void MapView::setScale(qreal hScale, qreal vScale) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setScale(hScale, vScale);
this->updateScene();
}
@ -210,7 +243,7 @@ void MapView::setRotation(int angle, int layer) {
// Overload. No layer provided, set rotation of all layers
void MapView::setRotation(int angle) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->setRotation(angle);
this->updateScene();
}
@ -222,7 +255,7 @@ void MapView::rotate(int degrees, int layer) {
// Overload. No layer provided, rotate all layers
void MapView::rotate(int degrees) {
foreach (Overlay * layer, this->overlayMap)
for (const auto& layer : *getCurrentScriptLayers())
layer->rotate(degrees);
this->updateScene();
}

View File

@ -12,10 +12,6 @@ ScriptUtility::~ScriptUtility() {
window->ui->menuTools->removeAction(action);
}
}
for (auto timer : this->activeTimers) {
timer->stop();
delete timer;
}
}
bool ScriptUtility::registerAction(QString functionName, QString actionName, QString shortcut) {
@ -60,24 +56,7 @@ QString ScriptUtility::getActionFunctionName(int actionIndex) {
}
void ScriptUtility::setTimeout(QJSValue callback, int milliseconds) {
if (!callback.isCallable() || milliseconds < 0)
return;
QTimer *timer = new QTimer();
connect(timer, &QTimer::timeout, [=](){
if (this->activeTimers.remove(timer)) {
this->callTimeoutFunction(callback);
timer->deleteLater();
}
});
this->activeTimers.insert(timer);
timer->setSingleShot(true);
timer->start(milliseconds);
}
void ScriptUtility::callTimeoutFunction(QJSValue callback) {
Scripting::tryErrorJS(callback.call());
Scripting::setTimeout(callback, milliseconds);
}
void ScriptUtility::log(QString message) {

View File

@ -50,8 +50,12 @@ Scripting::Scripting(MainWindow *mainWindow) {
}
Scripting::~Scripting() {
if (mainWindow) mainWindow->clearOverlay();
this->engine->setInterrupted(true);
for (auto timer : this->activeTimers) {
timer->stop();
delete timer;
}
if (mainWindow) mainWindow->clearOverlay();
qDeleteAll(this->imageCache);
this->scripts.clear();
this->scriptExecutionStack.clear();
@ -159,19 +163,21 @@ bool Scripting::tryErrorJS(QJSValue js) {
return true;
}
void Scripting::invokeCallback(CallbackType type, const QJSValueList &args) {
for (const auto& script : this->scripts) {
this->scriptExecutionStack.push(script);
invokeCallback(script, type, args);
this->scriptExecutionStack.pop();
}
QJSValue Scripting::call(QSharedPointer<Script> script, QJSValue func, const QJSValueList &args) {
this->scriptExecutionStack.push(script);
QJSValue result = func.call(args);
tryErrorJS(result);
this->scriptExecutionStack.pop();
return result;
}
void Scripting::invokeCallback(QSharedPointer<const Script> script, CallbackType type, const QJSValueList &args) {
QString functionName = callbackFunctions[type];
QJSValue callbackFunction = script->module().property(functionName);
if (tryErrorJS(callbackFunction)) return;
tryErrorJS(callbackFunction.call(args));
void Scripting::invokeCallback(CallbackType type, const QJSValueList &args) {
for (const auto& script : this->scripts) {
QString functionName = callbackFunctions[type];
QJSValue callbackFunction = script->module().property(functionName);
if (tryErrorJS(callbackFunction)) return;
call(script, callbackFunction, args);
}
}
void Scripting::invokeAction(int actionIndex) {
@ -181,9 +187,11 @@ void Scripting::invokeAction(int actionIndex) {
bool foundFunction = false;
for (const auto& script : instance->scripts) {
instance->scriptExecutionStack.push(script);
if (instance->invokeAction(script, functionName)) foundFunction = true;
instance->scriptExecutionStack.pop();
QJSValue callbackFunction = script->module().property(functionName);
if (callbackFunction.isUndefined() || !callbackFunction.isCallable()) continue;
foundFunction = true;
if (tryErrorJS(callbackFunction)) continue;
instance->call(script, callbackFunction);
}
if (!foundFunction) {
logError(QString("Unknown custom script function '%1'").arg(functionName));
@ -196,19 +204,22 @@ void Scripting::invokeAction(int actionIndex) {
}
}
// Returns true if the script had the specified function and tried to execute it (whether successful or not),
// returns false if the script did not have the specified function.
// TODO: Now that we can individually identify scripts, this should never happen.
// We can always call the function using the script that registered this action.
bool Scripting::invokeAction(QSharedPointer<const Script> script, const QString &functionName) {
QJSValue callbackFunction = script->module().property(functionName);
if (callbackFunction.isUndefined() || !callbackFunction.isCallable())
return false;
if (tryErrorJS(callbackFunction)) return true;
void Scripting::setTimeout(QJSValue callback, int milliseconds) {
if (!instance || !callback.isCallable() || milliseconds < 0)
return;
QJSValue result = callbackFunction.call(QJSValueList());
tryErrorJS(result);
return true;
auto script = instance->getActiveScript();
QTimer *timer = new QTimer();
QObject::connect(timer, &QTimer::timeout, [=](){
if (instance->activeTimers.remove(timer)) {
instance->call(script, callback);
timer->deleteLater();
}
});
instance->activeTimers.insert(timer);
timer->setSingleShot(true);
timer->start(milliseconds);
}
void Scripting::cb_ProjectOpened(QString projectPath) {
@ -460,6 +471,12 @@ bool Scripting::askForTrust(QSharedPointer<Script> script, const QString &reason
return false;
}
QString Scripting::getCurrentScriptHash() {
if (!instance) return QString();
auto script = instance->getActiveScript();
return script ? script->hash() : QString();
}
QJSEngine *Scripting::getEngine() {
return instance->engine;
}

View File

@ -1,4 +1,3 @@
#include "graphicsview.h"
#include "mapview.h"
#include "editor.h"
@ -18,12 +17,10 @@ void MapView::keyPressEvent(QKeyEvent *event) {
}
void MapView::drawForeground(QPainter *painter, const QRectF&) {
for (auto i = this->overlayMap.constBegin(); i != this->overlayMap.constEnd(); i++) {
i.value()->renderItems(painter);
}
if (!editor) return;
renderOverlay(painter);
QStyleOptionGraphicsItem option;
// Draw elements of the map view that should always render on top of anything added by the user with the scripting API.
@ -49,27 +46,21 @@ void MapView::drawForeground(QPainter *painter, const QRectF&) {
editor->cursorMapTileRect->paint(painter, &option, this);
}
void MapView::clearOverlayMap() {
for (auto i = this->overlayMap.constBegin(); i != this->overlayMap.constEnd(); i++) {
delete i.value();
void MapView::renderOverlay(QPainter *painter) {
#ifdef QT_QML_LIB
for (auto outerIt = this->overlayMap.constBegin(); outerIt != this->overlayMap.constEnd(); outerIt++)
for (auto innerIt = outerIt.value().constBegin(); innerIt != outerIt.value().constEnd(); innerIt++) {
innerIt.value()->renderItems(painter);
}
#endif
}
void MapView::clearOverlay() {
#ifdef QT_QML_LIB
for (auto outerIt = this->overlayMap.constBegin(); outerIt != this->overlayMap.constEnd(); outerIt++)
for (auto innerIt = outerIt.value().constBegin(); innerIt != outerIt.value().constEnd(); innerIt++) {
delete innerIt.value();
}
this->overlayMap.clear();
}
Overlay * MapView::getOverlay(int layer) {
Overlay * overlay = this->overlayMap.value(layer, nullptr);
if (!overlay) {
overlay = new Overlay();
this->overlayMap.insert(layer, overlay);
}
return overlay;
}
void ConnectionsView::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
emit pressedDelete();
event->accept();
} else {
QGraphicsView::keyPressEvent(event);
}
#endif
}