From 0af40d11dc30d9598b00f0e7d99f2556cea79911 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 25 Jan 2026 12:28:24 -0500 Subject: [PATCH] Add file I/O to scripting API --- CHANGELOG.md | 3 ++ docsrc/manual/scripting-capabilities.rst | 68 ++++++++++++++++++++++++ include/scripting.h | 1 + include/scriptutility.h | 6 +++ src/scriptapi/apiutility.cpp | 44 +++++++++++++++ src/scriptapi/scripting.cpp | 7 +++ 6 files changed, 129 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 329ec577..ed516256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project somewhat adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The MAJOR version number is bumped when there are **"Breaking Changes"** in the pret projects. For more on this, see [the manual page on breaking changes](https://huderlem.github.io/porymap/manual/breaking-changes.html). ## [Unreleased] +### Added +- Add API functions for reading and writing text files. + ### Fixed - Fix custom top-level data in the `encounters` object of `wild_encounters.json` being discarded if no `fields` data is present. diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index e18bd8e3..c21f3ea6 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -2012,6 +2012,74 @@ All utility functions are callable via the global ``utility`` object. :returns: ``true`` if ``Yes`` was selected, ``false`` if ``No`` was selected or if the window was closed without selection :rtype: boolean +.. |describe-file-dialog-caption| + replace:: Title bar text for the dialog. Not shown on all platforms. Defaults to ``""``. + +.. |describe-file-dialog-dir| + replace:: The dialog's working directory. If empty, the dialog will open to the most recent file dialog directory. Defaults to ``""``. + +.. |describe-file-dialog-filter| + replace:: A list of options for file types to use. For example, ``["Text files (*.txt *.md)", "JSON files (*.json)"]``. If empty, any file type can be used. Defaults to ``[]``. + +.. js:function:: utility.showOpenFileDialog(caption = "", dir = "", filter = []) + + Displays a native file dialog for choosing an existing file. + + :param caption: |describe-file-dialog-caption| + :type caption: string + :param dir: |describe-file-dialog-dir| + :type dir: string + :param filter: |describe-file-dialog-filter| + :type filter: array + :returns: The path of the selected file, or an empty string if the dialog was closed without selection. + :rtype: string + +.. js:function:: utility.showSaveFileDialog(caption = "", dir = "", filter = []) + + Displays a native file dialog for saving a file. + + :param caption: |describe-file-dialog-caption| + :type caption: string + :param dir: |describe-file-dialog-dir| + :type dir: string + :param filter: |describe-file-dialog-filter| + :type filter: array + :returns: The path of the selected file, or an empty string if the dialog was closed without selection. + :rtype: string + +.. js:function:: utility.showOpenDirectoryDialog(caption = "", dir = "") + + Displays a native file dialog for choosing an existing directory. + + :param caption: |describe-file-dialog-caption| + :type caption: string + :param dir: |describe-file-dialog-dir| + :type dir: string + :returns: The path of the selected directory, or an empty string if the dialog was closed without selection. + :rtype: string + +.. js:function:: utility.readTextFile(path) + + Reads the contents of the file specified by ``path``. + + :param path: The path of the file to read. The path may either be relative to the project directory or absolute. + :type path: string + :returns: If reading the file was successful ``content`` will contain the contents of the file and ``error`` will be empty. Otherwise ``content`` will be empty and ``error`` will contain an error message. + :rtype: object (``{content, error}``) + +.. js:function:: utility.writeTextFile(path, content, append = false) + + Writes ``content`` to the file specified by ``path``. The file is created if it does not already exist. + + :param path: The path of the file to write. The path to an existing file may either be relative to the project directory or absolute. If the file does not already exist the path must be absolute. + :type path: string + :param content: The text to write to the file. + :type content: string + :param append: If ``true``, content is added to the end of the existing file content. If ``false``, the old file contents are replaced. Defaults to ``false``. + :type append: boolean + :returns: An error message, or empty string if no error occurred. + :rtype: string + .. js:function:: utility.getInputText(title, label, default) Displays a text input dialog with an ``OK`` and a ``Cancel`` button. Execution stops while the window is open. diff --git a/include/scripting.h b/include/scripting.h index d54292b4..40903c09 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -72,6 +72,7 @@ public: static QJSValue position(int x, int y); static const QImage * getImage(const QString &filepath, bool useCache); static QJSValue dialogInput(QJSValue input, bool selectedOk); + static QJSValue fileResponse(const QString &s, bool isError); private: MainWindow *mainWindow; diff --git a/include/scriptutility.h b/include/scriptutility.h index ae60d316..23d42d99 100644 --- a/include/scriptutility.h +++ b/include/scriptutility.h @@ -33,6 +33,11 @@ public: Q_INVOKABLE void showWarning(QString text, QString informativeText = "", QString detailedText = ""); Q_INVOKABLE void showError(QString text, QString informativeText = "", QString detailedText = ""); Q_INVOKABLE bool showQuestion(QString text, QString informativeText = "", QString detailedText = ""); + Q_INVOKABLE QString showOpenFileDialog(const QString &caption = QString(), const QString &dir = QString(), const QStringList &filters = QStringList()) const; + Q_INVOKABLE QString showSaveFileDialog(const QString &caption = QString(), const QString &dir = QString(), const QStringList &filters = QStringList()) const; + Q_INVOKABLE QString showOpenDirectoryDialog(const QString &caption = QString(), const QString &dir = QString()) const; + Q_INVOKABLE QJSValue readTextFile(const QString &path) const; + Q_INVOKABLE QString writeTextFile(const QString &path, const QString &content, bool append = false) const; Q_INVOKABLE QJSValue getInputText(QString title, QString label, QString defaultValue = ""); Q_INVOKABLE QJSValue getInputNumber(QString title, QString label, double defaultValue = 0, double min = INT_MIN, double max = INT_MAX, int decimals = 0, double step = 1); Q_INVOKABLE QJSValue getInputItem(QString title, QString label, QStringList items, int defaultValue = 0, bool editable = false); @@ -72,6 +77,7 @@ public: 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 registeredActions; diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index e18d7110..15252d36 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -4,6 +4,7 @@ #include "ui_mainwindow.h" #include "scripting.h" #include "config.h" +#include "filedialog.h" ScriptUtility::~ScriptUtility() { if (window && window->ui && window->ui->menuTools) { @@ -122,6 +123,49 @@ bool ScriptUtility::showQuestion(QString text, QString informativeText, QString return messageBox.exec() == QMessageBox::Yes; } +QString ScriptUtility::showOpenFileDialog(const QString &caption, const QString &dir, const QStringList &filters) const { + const QString filter = filters.join(";;"); + return FileDialog::getOpenFileName(window, caption, dir, filter); +} + +QString ScriptUtility::showSaveFileDialog(const QString &caption, const QString &dir, const QStringList &filters) const { + const QString filter = filters.join(";;"); + return FileDialog::getSaveFileName(window, caption, dir, filter); +} + +QString ScriptUtility::showOpenDirectoryDialog(const QString &caption, const QString &dir) const { + return FileDialog::getExistingDirectory(window, caption, dir); +} + +QString ScriptUtility::detectProjectPath(const QString &path) const { + if (!window || !window->editor || !window->editor->project) return path; + const QString projectPath = window->editor->project->getExistingFilepath(path); + return projectPath.isEmpty() ? path : projectPath; +} + +QJSValue ScriptUtility::readTextFile(const QString &path) const { + QFile file(detectProjectPath(path)); + if (!file.exists()) { + return Scripting::fileResponse(QString("Failed to open file '%1' for reading: No such file.").arg(path), true); + } + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return Scripting::fileResponse(QString("Failed to open file '%1' for reading.").arg(path), true); + } + return Scripting::fileResponse(QString(file.readAll()), false); +} + +QString ScriptUtility::writeTextFile(const QString &path, const QString &content, bool append) const { + QFile file(detectProjectPath(path)); + QIODeviceBase::OpenMode flags = QIODevice::WriteOnly | QIODevice::Text; + if (append) flags |= QIODeviceBase::Append; + if (!file.open(flags)) { + return QString("Failed to open file '%1' for writing.").arg(path); + } + QTextStream out(&file); + out << content; + return QString(); +} + QJSValue ScriptUtility::getInputText(QString title, QString label, QString defaultValue) { bool ok; QString input = QInputDialog::getText(window, title, label, QLineEdit::Normal, defaultValue, &ok); diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 7ae0af85..f405f63c 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -405,6 +405,13 @@ QJSValue Scripting::dialogInput(QJSValue input, bool selectedOk) { return obj; } +QJSValue Scripting::fileResponse(const QString &s, bool isError) { + QJSValue obj = instance->engine->newObject(); + obj.setProperty("content", isError ? QString() : s); + obj.setProperty("error", isError ? s : QString()); + return obj; +} + QJSEngine *Scripting::getEngine() { return instance->engine; }