diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index 8673a4a6..648a0256 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -15,24 +15,24 @@ Porymap is extensible via scripting capabilities. This allows the user to write If you are compiling Porymap yourself, these features will only be available if Qt's ``qml`` module is installed. -Custom Scripts Editor ---------------------- +Plugin Editor +------------- -Your custom scripts can be managed with the Custom Scripts Editor accessible under ``Options -> Custom Scripts...``. +Your plugins can be managed with the Plugin Editor accessible under ``Options -> Plugins...``. .. figure:: images/scripting-capabilities/custom-scripts-editor.png - :alt: Custom Scripts Editor + :alt: Plugins Editor :width: 60% :align: center - Custom Scripts Editor + Plugins Editor At the top there are three basic buttons for managing your scripts: - |button-create| Opens a prompt to create a new script file, which will be populated with a basic template. - |button-load| Lets you add an existing script file to Porymap that you've already created or downloaded from elsewhere. - |button-refresh| Any edits made to your scripts while Porymap is already open will not be reflected until you select this button. -Below these buttons is a list of all the custom scripts you have loaded for your project. Each entry will have a text box showing the path of the script file. This path can be freely updated, or you can choose a new path with the |button-folder| button next to it. The |button-edit| button will open the script file in your default text editor, and the |button-remove| button will remove it from the list. The check box to the left of the filepath indicates whether your script should be running. If you'd like to temporarily disable a script you can uncheck this box. +Below these buttons is a list of all the plugins you have loaded for your project. Each entry will have a text box showing the path of the script file. This path can be freely updated, or you can choose a new path with the |button-folder| button next to it. The |button-edit| button will open the script file in your default text editor, and the |button-remove| button will remove it from the list. The check box to the left of the filepath indicates whether your script should be running. If you'd like to temporarily disable a script you can uncheck this box. .. |button-create| image:: images/scripting-capabilities/button-create.png :height: 24 @@ -51,12 +51,12 @@ Below these buttons is a list of all the custom scripts you have loaded for your :height: 24 -Writing a Custom Script +Writing a Plugin Script ----------------------- -Let's write a custom script that will randomize grass patterns when the user is editing the map. This is useful, since it's cumbersome to manually add randomness to grass patches. With the custom script, it will happen automatically. Whenever the user paints a grass tile onto the map, the script will overwrite the tile with a random grass tile instead. +Let's write a plugin script that will randomize grass patterns when the user is editing the map. This is useful, since it's cumbersome to manually add randomness to grass patches. With the plugin script, it will happen automatically. Whenever the user paints a grass tile onto the map, the script will overwrite the tile with a random grass tile instead. -First, open the ``Options -> Custom Scripts...`` window and select the |button-create| button. This will open a file save prompt; let's name our new script file ``my_script.js`` and save it. We've successfully added a new script! We can now see it listed in the editor. +First, open the ``Options -> Plugins...`` window and select the |button-create| button. This will open a file save prompt; let's name our new script file ``my_script.js`` and save it. We've successfully added a new script! We can now see it listed in the editor. .. figure:: images/scripting-capabilities/new-script.png :alt: Our New Script @@ -117,7 +117,7 @@ Now let's test our script! If we try to paint grass on the map, we should see ou Registering Script Actions -------------------------- -The grass-randomizer script above happens implicitly when the user paints on the map. However, other times we probably want to call the custom script on demand. One of the API functions Porymap provides is the ability to trigger scripting functions from the ``Tools`` menu, or a keyboard shortcut. To do this, we will usually want to register the action when the project loads. Here is an example script where some custom actions are registered. +The grass-randomizer script above happens implicitly when the user paints on the map. However, other times we probably want to call the plugin script on demand. One of the API functions Porymap provides is the ability to trigger scripting functions from the ``Tools`` menu, or a keyboard shortcut. To do this, we will usually want to register the action when the project loads. Here is an example script where some custom actions are registered. .. code-block:: js @@ -1829,11 +1829,11 @@ All settings functions are callable via the global ``utility`` object. :param enabled: smart paths enabled :type enabled: boolean -.. js:function:: utility.getCustomScripts() +.. js:function:: utility.getPluginScripts() - Gets the list of paths to custom scripts. + Gets the list of paths to plugin scripts. - :returns: string array of custom scripts paths + :returns: string array of plugin script paths :rtype: array .. js:function:: utility.getMainTab() @@ -1908,7 +1908,7 @@ All settings functions are callable via the global ``utility`` object. Utility Functions ^^^^^^^^^^^^^^^^^ -These are some miscellaneous functions that can be very useful when building custom scripts. +These are some miscellaneous functions that can be very useful when building plugins. All utility functions are callable via the global ``utility`` object. @@ -1947,7 +1947,7 @@ All utility functions are callable via the global ``utility`` object. .. js:function:: utility.log(message) - Logs a message to the Porymap log file with the prefix ``[INFO]``. This is useful for debugging custom scripts. + Logs a message to the Porymap log file with the prefix ``[INFO]``. This is useful for debugging. :param message: the message to log :type message: string diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index b9f7b366..a13aef07 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2919,7 +2919,7 @@ - + @@ -3161,9 +3161,9 @@ Project Settings... - + - Custom Scripts... + Plugins... diff --git a/forms/customscriptseditor.ui b/forms/plugineditor.ui similarity index 88% rename from forms/customscriptseditor.ui rename to forms/plugineditor.ui index 7db3b208..10420929 100644 --- a/forms/customscriptseditor.ui +++ b/forms/plugineditor.ui @@ -1,7 +1,7 @@ - CustomScriptsEditor - + PluginEditor + 0 @@ -11,7 +11,7 @@ - Custom Scripts Editor + Plugin Editor @@ -58,12 +58,12 @@ 6 - + - <html><head/><body><p>Create a new Porymap script file with a default template</p></body></html> + <html><head/><body><p>Create a new Porymap plugin script file with a default template</p></body></html> - Create New Script... + Create New Plugin... @@ -72,12 +72,12 @@ - + - <html><head/><body><p>Add an existing script file to the list below</p></body></html> + <html><head/><body><p>Add an existing plugin script file to the list below</p></body></html> - Load Script... + Load Plugin... @@ -86,12 +86,12 @@ - + - <html><head/><body><p>Refresh all loaded scripts to account for any recent edits</p></body></html> + <html><head/><body><p>Refresh all loaded plugins to account for any recent edits</p></body></html> - Refresh Scripts + Refresh Plugins @@ -142,7 +142,7 @@ - Scripts + Plugins diff --git a/forms/customscriptslistitem.ui b/forms/pluginlistitem.ui similarity index 88% rename from forms/customscriptslistitem.ui rename to forms/pluginlistitem.ui index 3a34e954..09b10a98 100644 --- a/forms/customscriptslistitem.ui +++ b/forms/pluginlistitem.ui @@ -1,7 +1,7 @@ - CustomScriptsListItem - + PluginListItem + 0 @@ -26,7 +26,7 @@ - If unchecked this script will be ignored + If unchecked this plugin will be ignored @@ -49,7 +49,7 @@ - Choose a new filepath for this script + Choose a new filepath for this plugin ... @@ -63,7 +63,7 @@ - Open this script file + Open this plugin's script file ... @@ -77,7 +77,7 @@ - Remove this script + Remove this plugin ... diff --git a/include/api/pluginsettings.h b/include/api/pluginsettings.h new file mode 100644 index 00000000..1bba2cee --- /dev/null +++ b/include/api/pluginsettings.h @@ -0,0 +1,27 @@ +#pragma once +#ifndef PLUGINSETTINGS_H +#define PLUGINSETTINGS_H + +#include +#include + +// Holds the basic user-provided information about a plugin. +struct PluginSettings { + QString path; + bool enabled = true; + + // Plugins can either be specific to the project, or specific to the user. + // This allows projects to send plugin scripts downstream to their users, + // while still allowing them to use their own personal plugins. + bool userOnly = true; + + static QStringList filter(const QList& plugins) { + QStringList paths; + for (auto& plugin : plugins) { + if (plugin.enabled) paths.append(plugin.path); + } + return paths; + } +}; + +#endif // PLUGINSETTINGS_H diff --git a/include/scripting.h b/include/api/scripting.h similarity index 98% rename from include/scripting.h rename to include/api/scripting.h index e9788e96..d73cb22c 100644 --- a/include/scripting.h +++ b/include/api/scripting.h @@ -21,7 +21,7 @@ class MainWindow; #ifdef QT_QML_LIB // !! New callback functions or changes to existing callback function names/arguments -// should be synced to resources/text/script_template.txt and docsrc/manual/scripting-capabilities.rst +// should be synced to resources/text/plugin_template.txt and docsrc/manual/scripting-capabilities.rst class Scripting : public QObject { Q_OBJECT diff --git a/include/scriptutility.h b/include/api/scriptutility.h similarity index 95% rename from include/scriptutility.h rename to include/api/scriptutility.h index 33b9658e..f9a33192 100644 --- a/include/scriptutility.h +++ b/include/api/scriptutility.h @@ -50,7 +50,8 @@ public: Q_INVOKABLE bool getBorderVisibility(); Q_INVOKABLE void setSmartPathsEnabled(bool visible); Q_INVOKABLE bool getSmartPathsEnabled(); - static Q_INVOKABLE QList getCustomScripts(); + static Q_INVOKABLE QList getPluginScripts(); + static Q_INVOKABLE QList getCustomScripts() {return getPluginScripts();} // Alias for backwards-compatibility Q_INVOKABLE QList getMetatileLayerOrder(); Q_INVOKABLE void setMetatileLayerOrder(const QList &order); Q_INVOKABLE QList getMetatileLayerOpacity(); diff --git a/include/config/projectconfig.h b/include/config/projectconfig.h index 62a671c3..d05ccfdf 100644 --- a/include/config/projectconfig.h +++ b/include/config/projectconfig.h @@ -196,7 +196,7 @@ public: int metatileSelectorWidth = 8; QStringList globalConstantsFilepaths; QMap globalConstants; - QList customScripts; + QList plugins; QMap eventIconPaths; QMap pokemonIconPaths; QVersionNumber minimumVersion; @@ -248,7 +248,7 @@ public: m_fm->addField(&this->metatileSelectorWidth, "metatile_selector_width", 1, INT_MAX); m_fm->addField(&this->globalConstantsFilepaths, "global_constants_filepaths"); m_fm->addField(&this->globalConstants, "global_constants"); - m_fm->addField(&this->customScripts, "custom_scripts"); + m_fm->addField(&this->plugins, "plugins"); m_fm->addField(&this->eventIconPaths, "event_icon_paths"); m_fm->addField(&this->pokemonIconPaths, "pokemon_icon_paths"); m_fm->addField(&this->minimumVersion, "minimum_version"); diff --git a/include/config/userconfig.h b/include/config/userconfig.h index 01433fc1..6c0051ba 100644 --- a/include/config/userconfig.h +++ b/include/config/userconfig.h @@ -19,7 +19,7 @@ public: QString prefabsFilepath; bool prefabsImportPrompted = false; bool useEncounterJson = true; - QList customScripts; + QList plugins; protected: virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; @@ -32,7 +32,7 @@ protected: m_fm->addField(&this->prefabsFilepath, "prefabs_filepath"); m_fm->addField(&this->prefabsImportPrompted, "prefabs_import_prompted"); m_fm->addField(&this->useEncounterJson, "use_encounter_json"); - m_fm->addField(&this->customScripts, "custom_scripts"); + m_fm->addField(&this->plugins, "plugins"); } return m_fm.get(); } diff --git a/include/core/converter.h b/include/core/converter.h index 5a5e86b5..198dea4d 100644 --- a/include/core/converter.h +++ b/include/core/converter.h @@ -10,7 +10,7 @@ #include "magic_enum.hpp" #include "orderedset.h" -#include "scriptsettings.h" +#include "pluginsettings.h" #include "gridsettings.h" #include "basegame.h" @@ -360,16 +360,16 @@ struct Converter : DefaultConverter { }; template <> -struct Converter : DefaultConverter { - static QJsonValue toJson(const ScriptSettings& value) { +struct Converter : DefaultConverter { + static QJsonValue toJson(const PluginSettings& value) { QJsonObject obj; obj["path"] = value.path; obj["enabled"] = value.enabled; return obj; } - static ScriptSettings fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + static PluginSettings fromJson(const QJsonValue& json, QStringList* errors = nullptr) { const auto obj = Converter::fromJson(json, errors); - ScriptSettings settings; + PluginSettings settings; settings.path = obj.value("path").toString(); settings.enabled = obj.value("enabled").toBool(); return settings; diff --git a/include/core/scriptsettings.h b/include/core/scriptsettings.h deleted file mode 100644 index 6c8c9d6f..00000000 --- a/include/core/scriptsettings.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#ifndef SCRIPTSETTINGS_H -#define SCRIPTSETTINGS_H - -#include -#include - -// Holds the basic user-provided information about a plug-in script. -struct ScriptSettings { - QString path; - bool enabled = true; - - // Scripts can either be specific to the project, or specific to the user. - // This allows projects to send scripts downstream to their users, - // while still allowing them to use their own personal scripts. - bool userOnly = true; - - static QStringList filter(const QList& scripts) { - QStringList paths; - for (auto& script : scripts) { - if (script.enabled) paths.append(script.path); - } - return paths; - } -}; - -#endif // SCRIPTSETTINGS_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 84bfef1b..69125f11 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -16,7 +16,7 @@ #include "preferenceeditor.h" #include "projectsettingseditor.h" #include "gridsettings.h" -#include "customscriptseditor.h" +#include "plugineditor.h" #include "wildmonchart.h" #include "wildmonsearch.h" #include "updatepromoter.h" @@ -303,7 +303,7 @@ private slots: void on_actionCheck_for_Updates_triggered(); void togglePreferenceSpecificUi(); void on_actionProject_Settings_triggered(); - void on_actionCustom_Scripts_triggered(); + void on_actionPlugins_triggered(); void reloadScriptEngine(); void on_actionShow_Grid_triggered(); void on_actionGrid_Settings_triggered(); @@ -326,7 +326,7 @@ private: QPointer preferenceEditor = nullptr; QPointer projectSettingsEditor = nullptr; QPointer gridSettingsDialog = nullptr; - QPointer customScriptsEditor = nullptr; + QPointer pluginEditor = nullptr; QPointer groupListProxyModel = nullptr; QPointer mapGroupModel = nullptr; @@ -454,7 +454,7 @@ private: bool initRegionMapEditor(bool silent = false); bool askToFixRegionMapEditor(); void initShortcutsEditor(); - void initCustomScriptsEditor(); + void initPluginEditor(); void connectSubEditorsToShortcutsEditor(); void openProjectSettingsEditor(int tab); bool isProjectOpen(); diff --git a/include/ui/customscriptseditor.h b/include/ui/customscriptseditor.h deleted file mode 100644 index 141ebae8..00000000 --- a/include/ui/customscriptseditor.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef CUSTOMSCRIPTSEDITOR_H -#define CUSTOMSCRIPTSEDITOR_H - -#include -#include -#include -#include - -#include "customscriptslistitem.h" - -namespace Ui { -class CustomScriptsEditor; -} - - -class CustomScriptsEditor : public QMainWindow -{ - Q_OBJECT - -public: - explicit CustomScriptsEditor(QWidget *parent = nullptr); - ~CustomScriptsEditor(); - -signals: - void reloadScriptEngine(); - -public slots: - void applyUserShortcuts(); - -private: - Ui::CustomScriptsEditor *ui; - - bool hasUnsavedChanges = false; - const QString baseDir; - - void displayScript(const ScriptSettings &settings); - void displayNewScript(QString filepath); - QString chooseScript(QString dir); - void removeScript(QListWidgetItem * item); - void replaceScript(QListWidgetItem * item); - void openScript(QListWidgetItem * item); - QString getScriptFilepath(QListWidgetItem * item, bool absolutePath = true) const; - void setScriptFilepath(QListWidgetItem * item, QString filepath) const; - bool getScriptEnabled(QListWidgetItem * item) const; - void markEdited(); - int prompt(const QString &text, QMessageBox::StandardButton defaultButton); - void save(); - void closeEvent(QCloseEvent*); - void initShortcuts(); - QObjectList shortcutableObjects() const; - void openManual(); - -private slots: - void dialogButtonClicked(QAbstractButton *button); - void createNewScript(); - void loadScript(); - bool refreshScripts(); - void userRefreshScripts(); - void removeSelectedScripts(); - void openSelectedScripts(); -}; - -#endif // CUSTOMSCRIPTSEDITOR_H diff --git a/include/ui/customscriptslistitem.h b/include/ui/customscriptslistitem.h deleted file mode 100644 index 9aa7be7d..00000000 --- a/include/ui/customscriptslistitem.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef CUSTOMSCRIPTSLISTITEM_H -#define CUSTOMSCRIPTSLISTITEM_H - -#include -#include "scriptsettings.h" - -namespace Ui { -class CustomScriptsListItem; -} - -class CustomScriptsListItem : public QFrame -{ - Q_OBJECT - -public: - explicit CustomScriptsListItem(QWidget *parent = nullptr); - explicit CustomScriptsListItem(const ScriptSettings& settings, QWidget *parent = nullptr); - ~CustomScriptsListItem(); - - void setSettings(const ScriptSettings& settings); - ScriptSettings getSettings() const; - - void setPath(const QString& text); - QString path() const; - - void setScriptEnabled(bool enabled); - bool scriptEnabled() const; - -signals: - void clickedChooseScript(); - void clickedEditScript(); - void clickedDeleteScript(); - void toggledEnable(bool checked); - void pathEdited(const QString& text); - -private: - Ui::CustomScriptsListItem *ui; -}; - -#endif // CUSTOMSCRIPTSLISTITEM_H diff --git a/include/ui/plugineditor.h b/include/ui/plugineditor.h new file mode 100644 index 00000000..d5cd0024 --- /dev/null +++ b/include/ui/plugineditor.h @@ -0,0 +1,63 @@ +#ifndef PLUGINEDITOR_H +#define PLUGINEDITOR_H + +#include +#include +#include +#include + +#include "pluginlistitem.h" + +namespace Ui { +class PluginEditor; +} + + +class PluginEditor : public QMainWindow +{ + Q_OBJECT + +public: + explicit PluginEditor(QWidget *parent = nullptr); + ~PluginEditor(); + +signals: + void reloadScriptEngine(); + +public slots: + void applyUserShortcuts(); + +private: + Ui::PluginEditor *ui; + + bool hasUnsavedChanges = false; + const QString baseDir; + + void displayPlugin(const PluginSettings &settings); + void displayNewPlugin(QString filepath); + QString choosePlugin(QString dir); + void removePlugin(QListWidgetItem * item); + void replacePlugin(QListWidgetItem * item); + void openPlugin(QListWidgetItem * item); + QString getPluginFilepath(QListWidgetItem * item, bool absolutePath = true) const; + void setPluginFilepath(QListWidgetItem * item, QString filepath) const; + bool getPluginEnabled(QListWidgetItem * item) const; + void markEdited(); + int prompt(const QString &text, QMessageBox::StandardButton defaultButton); + void save(); + void closeEvent(QCloseEvent*); + void initShortcuts(); + QObjectList shortcutableObjects() const; + void openManual(); + +private slots: + void dialogButtonClicked(QAbstractButton *button); + void createNewPlugin(); + void loadPlugin(); + bool refreshPlugins(); + void userRefreshPlugins(); + void removeSelectedPlugins(); + void openSelectedPlugins(); +}; + +#endif // PLUGINEDITOR_H diff --git a/include/ui/pluginlistitem.h b/include/ui/pluginlistitem.h new file mode 100644 index 00000000..c6bfd298 --- /dev/null +++ b/include/ui/pluginlistitem.h @@ -0,0 +1,40 @@ +#ifndef PLUGINLISTITEM_H +#define PLUGINLISTITEM_H + +#include +#include "pluginsettings.h" + +namespace Ui { +class PluginListItem; +} + +class PluginListItem : public QFrame +{ + Q_OBJECT + +public: + explicit PluginListItem(QWidget *parent = nullptr); + explicit PluginListItem(const PluginSettings& settings, QWidget *parent = nullptr); + ~PluginListItem(); + + void setSettings(const PluginSettings& settings); + PluginSettings getSettings() const; + + void setPath(const QString& text); + QString path() const; + + void setPluginEnabled(bool enabled); + bool pluginEnabled() const; + +signals: + void clickedChoosePlugin(); + void clickedEditPlugin(); + void clickedDeletePlugin(); + void toggledEnable(bool checked); + void pathEdited(const QString& text); + +private: + Ui::PluginListItem *ui; +}; + +#endif // PLUGINLISTITEM_H diff --git a/porymap.pro b/porymap.pro index 2cf2251d..67d1195f 100644 --- a/porymap.pro +++ b/porymap.pro @@ -81,10 +81,10 @@ SOURCES += src/config/keyvalueconfigbase.cpp \ src/lib/collapsiblesection.cpp \ src/lib/orderedjson.cpp \ src/core/regionmapeditcommands.cpp \ - src/scriptapi/apimap.cpp \ - src/scriptapi/apioverlay.cpp \ - src/scriptapi/apiutility.cpp \ - src/scriptapi/scripting.cpp \ + src/api/apimap.cpp \ + src/api/apioverlay.cpp \ + src/api/apiutility.cpp \ + src/api/scripting.cpp \ src/ui/aboutporymap.cpp \ src/ui/checkeredbgscene.cpp \ src/ui/colorinputwidget.cpp \ @@ -92,8 +92,8 @@ SOURCES += src/config/keyvalueconfigbase.cpp \ src/ui/connectionslistitem.cpp \ src/ui/customattributesdialog.cpp \ src/ui/customattributestable.cpp \ - src/ui/customscriptseditor.cpp \ - src/ui/customscriptslistitem.cpp \ + src/ui/plugineditor.cpp \ + src/ui/pluginlistitem.cpp \ src/ui/divingmappixmapitem.cpp \ src/ui/eventpixmapitem.cpp \ src/ui/bordermetatilespixmapitem.cpp \ @@ -215,8 +215,8 @@ HEADERS += include/config/keyvalueconfigbase.h \ include/ui/connectionslistitem.h \ include/ui/customattributesdialog.h \ include/ui/customattributestable.h \ - include/ui/customscriptseditor.h \ - include/ui/customscriptslistitem.h \ + include/ui/plugineditor.h \ + include/ui/pluginlistitem.h \ include/ui/divingmappixmapitem.h \ include/ui/edithistoryspinbox.h \ include/ui/eventcombobox.h \ @@ -289,8 +289,8 @@ HEADERS += include/config/keyvalueconfigbase.h \ include/editor.h \ include/mainwindow.h \ include/project.h \ - include/scripting.h \ - include/scriptutility.h \ + include/api/scripting.h \ + include/api/scriptutility.h \ include/settings.h \ include/log.h \ include/ui/uintspinbox.h \ @@ -329,8 +329,8 @@ FORMS += forms/mainwindow.ui \ forms/regionmappropertiesdialog.ui \ forms/colorpicker.ui \ forms/projectsettingseditor.ui \ - forms/customscriptseditor.ui \ - forms/customscriptslistitem.ui \ + forms/plugineditor.ui \ + forms/pluginlistitem.ui \ forms/customattributesdialog.ui \ forms/updatepromoter.ui \ forms/wildmonchart.ui \ @@ -343,6 +343,7 @@ RESOURCES += \ resources/text.qrc INCLUDEPATH += include +INCLUDEPATH += include/api INCLUDEPATH += include/config INCLUDEPATH += include/core INCLUDEPATH += include/ui diff --git a/resources/text.qrc b/resources/text.qrc index 687a2711..e8694a77 100644 --- a/resources/text.qrc +++ b/resources/text.qrc @@ -6,7 +6,7 @@ text/prefabs_default_emerald.json text/prefabs_default_firered.json text/prefabs_default_ruby.json - text/script_template.txt + text/plugin_template.txt ../CHANGELOG.md diff --git a/resources/text/script_template.txt b/resources/text/plugin_template.txt similarity index 100% rename from resources/text/script_template.txt rename to resources/text/plugin_template.txt diff --git a/src/scriptapi/apimap.cpp b/src/api/apimap.cpp similarity index 100% rename from src/scriptapi/apimap.cpp rename to src/api/apimap.cpp diff --git a/src/scriptapi/apioverlay.cpp b/src/api/apioverlay.cpp similarity index 100% rename from src/scriptapi/apioverlay.cpp rename to src/api/apioverlay.cpp diff --git a/src/scriptapi/apiutility.cpp b/src/api/apiutility.cpp similarity index 97% rename from src/scriptapi/apiutility.cpp rename to src/api/apiutility.cpp index 8a196e6c..9d53d1bf 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/api/apiutility.cpp @@ -197,12 +197,12 @@ bool ScriptUtility::getSmartPathsEnabled() { return window->ui->checkBox_smartPaths->isChecked(); } -QList ScriptUtility::getCustomScripts() { +QList ScriptUtility::getPluginScripts() { QList paths; - for (const auto& settings : userConfig.customScripts) - paths.append(settings.path); - for (const auto& settings : projectConfig.customScripts) - paths.append(settings.path); + for (const auto& plugin : userConfig.plugins) + paths.append(plugin.path); + for (const auto& plugin : projectConfig.plugins) + paths.append(plugin.path); return paths; } diff --git a/src/scriptapi/scripting.cpp b/src/api/scripting.cpp similarity index 98% rename from src/scriptapi/scripting.cpp rename to src/api/scripting.cpp index 866a65d5..23bfa8a2 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/api/scripting.cpp @@ -24,8 +24,8 @@ Scripting::Scripting(MainWindow *mainWindow) { this->engine->installExtensions(QJSEngine::ConsoleExtension); - const QStringList paths = ScriptSettings::filter(userConfig.customScripts) - + ScriptSettings::filter(projectConfig.customScripts); + const QStringList paths = PluginSettings::filter(userConfig.plugins) + + PluginSettings::filter(projectConfig.plugins); for (const auto& path : paths) { loadScript(path); } @@ -61,7 +61,7 @@ void Scripting::loadScript(const QString &filepath) { if (script->filepath().isEmpty() || tryErrorJS(script->module())) { QMessageBox messageBox(this->mainWindow); messageBox.setText("Failed to load script"); - messageBox.setInformativeText(QString("An error occurred while loading custom script file '%1'").arg(filepath)); + messageBox.setInformativeText(QString("An error occurred while loading plugin script file '%1'").arg(filepath)); messageBox.setDetailedText(getMostRecentError()); messageBox.setIcon(QMessageBox::Warning); messageBox.addButton(QMessageBox::Ok); @@ -69,7 +69,7 @@ void Scripting::loadScript(const QString &filepath) { return; } - logInfo(QString("Successfully loaded custom script file '%1'").arg(filepath)); + logInfo(QString("Successfully loaded plugin script file '%1'").arg(filepath)); this->scripts.append(script); } @@ -145,7 +145,7 @@ bool Scripting::tryErrorJS(QJSValue js) { QString fileErrStr = fileName == "undefined" ? "" : QString(" '%1'").arg(fileName); QString lineErrStr = lineNumber == "undefined" ? "" : QString(" at line %1").arg(lineNumber); - logError(QString("Error in custom script%1%2: '%3'") + logError(QString("Error in plugin script%1%2: '%3'") .arg(fileErrStr) .arg(lineErrStr) .arg(errStr)); diff --git a/src/config/legacy.cpp b/src/config/legacy.cpp index 2b72bc8c..767fcaec 100644 --- a/src/config/legacy.cpp +++ b/src/config/legacy.cpp @@ -367,7 +367,7 @@ bool ProjectConfig::parseLegacyKeyValue(const QString &key, const QString &value // The format is a comma-separated list of paths. Each path can be followed (before the comma) // by a :0 or :1 to indicate whether it should be disabled or enabled, respectively. If neither // follow, it's assumed the script should be enabled. -QList parseCustomScripts(const QString &input) { +QList parseCustomScripts(const QString &input) { QMap customScripts; const QList paths = input.split(",", Qt::SkipEmptyParts); for (QString path : paths) { @@ -381,7 +381,7 @@ QList parseCustomScripts(const QString &input) { customScripts.insert(path, enabled); } } - QList settingsList; + QList settingsList; for (auto it = customScripts.begin(); it != customScripts.end(); it++) { settingsList.append({ .path = it.key(), @@ -398,7 +398,7 @@ bool UserConfig::parseLegacyKeyValue(const QString &key, const QString &value) { } else if (key == "use_encounter_json") { this->useEncounterJson = toBool(value); } else if (key == "custom_scripts") { - this->customScripts = parseCustomScripts(value); + this->plugins = parseCustomScripts(value); } else { return false; } diff --git a/src/config/projectconfig.cpp b/src/config/projectconfig.cpp index b2a62892..13328e49 100644 --- a/src/config/projectconfig.cpp +++ b/src/config/projectconfig.cpp @@ -239,8 +239,8 @@ void ProjectConfig::loadFromJson(const QJsonObject& obj_) { KeyValueConfigBase::loadFromJson(obj); - // Enforce this setting for projectConfig's custom scripts - for (auto& settings : this->customScripts) settings.userOnly = false; + // Enforce this setting for projectConfig's plugins + for (auto& settings : this->plugins) settings.userOnly = false; } QJsonObject ProjectConfig::getDefaultJson() const { diff --git a/src/config/userconfig.cpp b/src/config/userconfig.cpp index c0f24bef..075c2d8b 100644 --- a/src/config/userconfig.cpp +++ b/src/config/userconfig.cpp @@ -6,8 +6,8 @@ UserConfig userConfig; void UserConfig::loadFromJson(const QJsonObject& obj) { KeyValueConfigBase::loadFromJson(obj); - // Enforce this setting for userConfig's custom scripts - for (auto& settings : this->customScripts) settings.userOnly = true; + // Enforce this setting for userConfig's plugins + for (auto& settings : this->plugins) settings.userOnly = true; } QJsonObject UserConfig::getDefaultJson() const { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 87863da2..ec97bcb6 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -212,7 +212,7 @@ void MainWindow::initWindow() { #endif #ifndef QT_QML_LIB - ui->actionCustom_Scripts->setVisible(false); + ui->actionPlugins->setVisible(false); missingModules.append(" 'qml'"); #endif @@ -806,7 +806,7 @@ bool MainWindow::openProject(QString dir, bool initial) { return false; } - porysplash->showLoadingMessage("custom scripts"); + porysplash->showLoadingMessage("plugin scripts"); Scripting::init(this); // Create the project @@ -2405,12 +2405,12 @@ void MainWindow::connectSubEditorsToShortcutsEditor() { this->regionMapEditor, &RegionMapEditor::applyUserShortcuts); } - if (!this->customScriptsEditor) { - initCustomScriptsEditor(); + if (!this->pluginEditor) { + initPluginEditor(); } - if (this->customScriptsEditor) { + if (this->pluginEditor) { connect(this->shortcutsEditor, &ShortcutsEditor::shortcutsSaved, - this->customScriptsEditor, &CustomScriptsEditor::applyUserShortcuts); + this->pluginEditor, &PluginEditor::applyUserShortcuts); } } @@ -3107,19 +3107,19 @@ void MainWindow::onWarpBehaviorWarningClicked() { msgBox->open(); } -void MainWindow::on_actionCustom_Scripts_triggered() { - if (!this->customScriptsEditor) { - initCustomScriptsEditor(); +void MainWindow::on_actionPlugins_triggered() { + if (!this->pluginEditor) { + initPluginEditor(); } - if (this->customScriptsEditor) { - Util::show(this->customScriptsEditor); + if (this->pluginEditor) { + Util::show(this->pluginEditor); } } -void MainWindow::initCustomScriptsEditor() { +void MainWindow::initPluginEditor() { #ifdef QT_QML_LIB - this->customScriptsEditor = new CustomScriptsEditor(this); - connect(this->customScriptsEditor, &CustomScriptsEditor::reloadScriptEngine, + this->pluginEditor = new PluginEditor(this); + connect(this->pluginEditor, &PluginEditor::reloadScriptEngine, this, &MainWindow::reloadScriptEngine); #endif } diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp deleted file mode 100644 index f2cf8f59..00000000 --- a/src/ui/customscriptseditor.cpp +++ /dev/null @@ -1,296 +0,0 @@ -#include "customscriptseditor.h" -#include "ui_customscriptseditor.h" -#include "ui_customscriptslistitem.h" -#include "config.h" -#include "editor.h" -#include "shortcut.h" -#include "filedialog.h" -#include "eventfilters.h" - -#include -#include -#include - -CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) : - QMainWindow(parent), - ui(new Ui::CustomScriptsEditor), - baseDir(userConfig.projectDir() + "/") -{ - ui->setupUi(this); - setAttribute(Qt::WA_DeleteOnClose); - // This property seems to be reset if we don't set it programmatically - ui->list->setDragDropMode(QAbstractItemView::NoDragDrop); - - for (const auto& settings : projectConfig.customScripts) - displayScript(settings); - for (const auto& settings : userConfig.customScripts) - displayScript(settings); - - connect(ui->button_Help, &QAbstractButton::clicked, this, &CustomScriptsEditor::openManual); - connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript); - connect(ui->button_LoadScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::loadScript); - connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::userRefreshScripts); - connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CustomScriptsEditor::dialogButtonClicked); - - initShortcuts(); - installEventFilter(new GeometrySaver(this)); -} - -CustomScriptsEditor::~CustomScriptsEditor() -{ - ui->list->clear(); - delete ui; -} - -void CustomScriptsEditor::initShortcuts() { - auto *shortcut_remove = new Shortcut({QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(removeSelectedScripts())); - shortcut_remove->setObjectName("shortcut_remove"); - shortcut_remove->setWhatsThis("Remove Selected Scripts"); - - auto *shortcut_open = new Shortcut(QKeySequence(), this, SLOT(openSelectedScripts())); - shortcut_open->setObjectName("shortcut_open"); - shortcut_open->setWhatsThis("Open Selected Scripts"); - - auto *shortcut_createNew = new Shortcut(QKeySequence(), this, SLOT(createNewScript())); - shortcut_createNew->setObjectName("shortcut_createNew"); - shortcut_createNew->setWhatsThis("Create New Script..."); - - auto *shortcut_load = new Shortcut(QKeySequence(), this, SLOT(loadScript())); - shortcut_load->setObjectName("shortcut_load"); - shortcut_load->setWhatsThis("Load Script..."); - - auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(userRefreshScripts())); - shortcut_refresh->setObjectName("shortcut_refresh"); - shortcut_refresh->setWhatsThis("Refresh Scripts"); - - shortcutsConfig.setDefaultShortcuts(shortcutableObjects()); - applyUserShortcuts(); -} - -QObjectList CustomScriptsEditor::shortcutableObjects() const { - QObjectList shortcutable_objects; - - for (auto *action : findChildren()) - if (!action->objectName().isEmpty()) - shortcutable_objects.append(qobject_cast(action)); - for (auto *shortcut : findChildren()) - if (!shortcut->objectName().isEmpty()) - shortcutable_objects.append(qobject_cast(shortcut)); - - return shortcutable_objects; -} - -void CustomScriptsEditor::applyUserShortcuts() { - for (auto *action : findChildren()) - if (!action->objectName().isEmpty()) - action->setShortcuts(shortcutsConfig.userShortcuts(action)); - for (auto *shortcut : findChildren()) - if (!shortcut->objectName().isEmpty()) - shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut)); -} - -void CustomScriptsEditor::displayScript(const ScriptSettings& settings) { - auto item = new QListWidgetItem(ui->list); - auto widget = new CustomScriptsListItem(settings, ui->list); - item->setSizeHint(widget->sizeHint()); - - connect(widget, &CustomScriptsListItem::clickedChooseScript, [this, item] { this->replaceScript(item); }); - connect(widget, &CustomScriptsListItem::clickedEditScript, [this, item] { this->openScript(item); }); - connect(widget, &CustomScriptsListItem::clickedDeleteScript, [this, item] { this->removeScript(item); }); - connect(widget, &CustomScriptsListItem::toggledEnable, this, &CustomScriptsEditor::markEdited); - connect(widget, &CustomScriptsListItem::pathEdited, this, &CustomScriptsEditor::markEdited); - - // Per the Qt manual, for performance reasons QListWidget::setItemWidget shouldn't be used with non-static items. - // There's an assumption here that users won't have enough scripts for that to be a problem. - ui->list->addItem(item); - ui->list->setItemWidget(item, widget); -} - -void CustomScriptsEditor::markEdited() { - this->hasUnsavedChanges = true; -} - -QString CustomScriptsEditor::getScriptFilepath(QListWidgetItem * item, bool absolutePath) const { - auto widget = dynamic_cast(ui->list->itemWidget(item)); - if (!widget) return QString(); - - QString path = widget->path(); - if (absolutePath) { - QFileInfo fileInfo(path); - if (fileInfo.isRelative()) - path.prepend(this->baseDir); - } - return path; -} - -void CustomScriptsEditor::setScriptFilepath(QListWidgetItem * item, QString filepath) const { - auto widget = dynamic_cast(ui->list->itemWidget(item)); - if (widget) { - widget->setPath(Util::stripPrefix(filepath, this->baseDir)); - } -} - -bool CustomScriptsEditor::getScriptEnabled(QListWidgetItem * item) const { - auto widget = dynamic_cast(ui->list->itemWidget(item)); - return widget && widget->scriptEnabled(); -} - -QString CustomScriptsEditor::chooseScript(QString dir) { - return FileDialog::getOpenFileName(this, "Choose Custom Script File", dir, "JavaScript Files (*.js)"); -} - -void CustomScriptsEditor::createNewScript() { - const QString filepath = FileDialog::getSaveFileName(this, "Create New Script File", FileDialog::getDirectory() + "/new_script.js", "JavaScript Files (*.js)"); - if (filepath.isEmpty()) - return; - - QFile scriptFile(filepath); - if (!scriptFile.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(filepath)); - QMessageBox messageBox(this); - messageBox.setText("Failed to create new script file!"); - messageBox.setInformativeText(QString("Could not open \"%1\" for writing").arg(filepath)); - messageBox.setIcon(QMessageBox::Warning); - messageBox.exec(); - return; - } - ParseUtil parser; - scriptFile.write(parser.readTextFile(":/text/script_template.txt").toUtf8()); - scriptFile.close(); - - this->displayNewScript(filepath); -} - -void CustomScriptsEditor::loadScript() { - QString filepath = this->chooseScript(FileDialog::getDirectory()); - if (filepath.isEmpty()) - return; - this->displayNewScript(filepath); -} - -void CustomScriptsEditor::displayNewScript(QString filepath) { - filepath = Util::stripPrefix(filepath, this->baseDir); - - // Verify new script path is not already in list - for (int i = 0; i < ui->list->count(); i++) { - if (filepath == this->getScriptFilepath(ui->list->item(i), false)) { - QMessageBox::information(this, QApplication::applicationName(), QString("The script '%1' is already loaded").arg(filepath)); - return; - } - } - - ScriptSettings settings; - settings.path = filepath; - this->displayScript(settings); - this->markEdited(); -} - -void CustomScriptsEditor::removeScript(QListWidgetItem * item) { - ui->list->takeItem(ui->list->row(item)); - this->markEdited(); -} - -void CustomScriptsEditor::removeSelectedScripts() { - QList items = ui->list->selectedItems(); - if (items.length() == 0) - return; - for (auto item : items) - this->removeScript(item); -} - -void CustomScriptsEditor::replaceScript(QListWidgetItem * item) { - const QString filepath = this->chooseScript(this->getScriptFilepath(item)); - if (filepath.isEmpty()) - return; - this->setScriptFilepath(item, filepath); - this->markEdited(); -} - -void CustomScriptsEditor::openScript(QListWidgetItem * item) { - const QString path = this->getScriptFilepath(item); - QFileInfo fileInfo(path); - if (!fileInfo.exists() || !fileInfo.isFile()){ - QMessageBox::warning(this, QApplication::applicationName(), QString("Failed to open script '%1'").arg(path)); - return; - } - Editor::openInTextEditor(path); -} - -void CustomScriptsEditor::openSelectedScripts() { - for (auto item : ui->list->selectedItems()) - this->openScript(item); -} - -void CustomScriptsEditor::openManual() { - static const QUrl url("https://huderlem.github.io/porymap/manual/scripting-capabilities.html"); - QDesktopServices::openUrl(url); -} - -// When the user refreshes the scripts we show a little tooltip as feedback. -// We don't want this tooltip to display when we refresh programmatically, like when changes are saved. -void CustomScriptsEditor::userRefreshScripts() { - if (refreshScripts()) - QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!"); -} - -bool CustomScriptsEditor::refreshScripts() { - if (this->hasUnsavedChanges) { - if (this->prompt("Scripts have been modified, save changes and reload the script engine?", QMessageBox::Yes) == QMessageBox::No) - return false; - this->save(); - } - emit reloadScriptEngine(); - return true; -} - -void CustomScriptsEditor::save() { - if (!this->hasUnsavedChanges) - return; - - QList userScripts; - QList projectScripts; - for (int i = 0; i < ui->list->count(); i++) { - auto item = ui->list->item(i); - auto widget = dynamic_cast(ui->list->itemWidget(item)); - if (!widget) continue; - const ScriptSettings settings = widget->getSettings(); - if (settings.userOnly) userScripts.append(settings); - else projectScripts.append(settings); - } - - userConfig.customScripts = userScripts; - userConfig.save(); - - projectConfig.customScripts = projectScripts; - projectConfig.save(); - - this->hasUnsavedChanges = false; - this->refreshScripts(); -} - -int CustomScriptsEditor::prompt(const QString &text, QMessageBox::StandardButton defaultButton) { - QMessageBox messageBox(this); - messageBox.setText(text); - messageBox.setIcon(QMessageBox::Question); - messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No | defaultButton); - messageBox.setDefaultButton(defaultButton); - return messageBox.exec(); -} - -void CustomScriptsEditor::dialogButtonClicked(QAbstractButton *button) { - if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) - this->save(); - close(); // All buttons (OK and Cancel) close the window -} - -void CustomScriptsEditor::closeEvent(QCloseEvent* event) { - if (this->hasUnsavedChanges) { - int result = this->prompt("Scripts have been modified, save changes?", QMessageBox::Cancel); - if (result == QMessageBox::Cancel) { - event->ignore(); - return; - } - if (result == QMessageBox::Yes) - this->save(); - } -} diff --git a/src/ui/customscriptslistitem.cpp b/src/ui/customscriptslistitem.cpp deleted file mode 100644 index ab00ef0b..00000000 --- a/src/ui/customscriptslistitem.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "customscriptslistitem.h" -#include "ui_customscriptslistitem.h" - -CustomScriptsListItem::CustomScriptsListItem(QWidget *parent) : - QFrame(parent), - ui(new Ui::CustomScriptsListItem) -{ - ui->setupUi(this); - - connect(ui->b_Choose, &QAbstractButton::clicked, this, &CustomScriptsListItem::clickedChooseScript); - connect(ui->b_Edit, &QAbstractButton::clicked, this, &CustomScriptsListItem::clickedEditScript); - connect(ui->b_Delete, &QAbstractButton::clicked, this, &CustomScriptsListItem::clickedDeleteScript); - connect(ui->checkBox_Enable, &QCheckBox::toggled, this, &CustomScriptsListItem::toggledEnable); - connect(ui->lineEdit_filepath, &QLineEdit::textEdited, this, &CustomScriptsListItem::pathEdited); -} - -CustomScriptsListItem::CustomScriptsListItem(const ScriptSettings& settings, QWidget *parent) : - CustomScriptsListItem(parent) -{ - setSettings(settings); -} - -CustomScriptsListItem::~CustomScriptsListItem() { - delete ui; -} - -void CustomScriptsListItem::setPath(const QString& text) { - ui->lineEdit_filepath->setText(text); -} - -QString CustomScriptsListItem::path() const { - return ui->lineEdit_filepath->text(); -} - -void CustomScriptsListItem::setScriptEnabled(bool enabled) { - ui->checkBox_Enable->setChecked(enabled); -} - -bool CustomScriptsListItem::scriptEnabled() const { - return ui->checkBox_Enable->isChecked(); -} - -// TODO: The two functions below should read/write from/to the UI whether the script belongs to the project or user. -void CustomScriptsListItem::setSettings(const ScriptSettings& settings) { - setPath(settings.path); - setScriptEnabled(settings.enabled); -} - -ScriptSettings CustomScriptsListItem::getSettings() const { - return { - .path = path(), - .enabled = scriptEnabled(), - .userOnly = true, - }; -} diff --git a/src/ui/plugineditor.cpp b/src/ui/plugineditor.cpp new file mode 100644 index 00000000..4b34f891 --- /dev/null +++ b/src/ui/plugineditor.cpp @@ -0,0 +1,295 @@ +#include "plugineditor.h" +#include "ui_plugineditor.h" +#include "config.h" +#include "editor.h" +#include "shortcut.h" +#include "filedialog.h" +#include "eventfilters.h" + +#include +#include +#include + +PluginEditor::PluginEditor(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::PluginEditor), + baseDir(userConfig.projectDir() + "/") +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + // This property seems to be reset if we don't set it programmatically + ui->list->setDragDropMode(QAbstractItemView::NoDragDrop); + + for (const auto& plugin : projectConfig.plugins) + displayPlugin(plugin); + for (const auto& plugin : userConfig.plugins) + displayPlugin(plugin); + + connect(ui->button_Help, &QAbstractButton::clicked, this, &PluginEditor::openManual); + connect(ui->button_CreateNewPlugin, &QAbstractButton::clicked, this, &PluginEditor::createNewPlugin); + connect(ui->button_LoadPlugin, &QAbstractButton::clicked, this, &PluginEditor::loadPlugin); + connect(ui->button_RefreshPlugins, &QAbstractButton::clicked, this, &PluginEditor::userRefreshPlugins); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &PluginEditor::dialogButtonClicked); + + initShortcuts(); + installEventFilter(new GeometrySaver(this)); +} + +PluginEditor::~PluginEditor() +{ + ui->list->clear(); + delete ui; +} + +void PluginEditor::initShortcuts() { + auto *shortcut_remove = new Shortcut({QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(removeSelectedPlugins())); + shortcut_remove->setObjectName("shortcut_remove"); + shortcut_remove->setWhatsThis("Remove Selected Plugins"); + + auto *shortcut_open = new Shortcut(QKeySequence(), this, SLOT(openSelectedPlugins())); + shortcut_open->setObjectName("shortcut_open"); + shortcut_open->setWhatsThis("Open Selected Plugins"); + + auto *shortcut_createNew = new Shortcut(QKeySequence(), this, SLOT(createNewPlugin())); + shortcut_createNew->setObjectName("shortcut_createNew"); + shortcut_createNew->setWhatsThis("Create New Plugin..."); + + auto *shortcut_load = new Shortcut(QKeySequence(), this, SLOT(loadPlugin())); + shortcut_load->setObjectName("shortcut_load"); + shortcut_load->setWhatsThis("Load Plugin..."); + + auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(userRefreshPlugins())); + shortcut_refresh->setObjectName("shortcut_refresh"); + shortcut_refresh->setWhatsThis("Refresh Plugins"); + + shortcutsConfig.setDefaultShortcuts(shortcutableObjects()); + applyUserShortcuts(); +} + +QObjectList PluginEditor::shortcutableObjects() const { + QObjectList shortcutable_objects; + + for (auto *action : findChildren()) + if (!action->objectName().isEmpty()) + shortcutable_objects.append(qobject_cast(action)); + for (auto *shortcut : findChildren()) + if (!shortcut->objectName().isEmpty()) + shortcutable_objects.append(qobject_cast(shortcut)); + + return shortcutable_objects; +} + +void PluginEditor::applyUserShortcuts() { + for (auto *action : findChildren()) + if (!action->objectName().isEmpty()) + action->setShortcuts(shortcutsConfig.userShortcuts(action)); + for (auto *shortcut : findChildren()) + if (!shortcut->objectName().isEmpty()) + shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut)); +} + +void PluginEditor::displayPlugin(const PluginSettings& settings) { + auto item = new QListWidgetItem(ui->list); + auto widget = new PluginListItem(settings, ui->list); + item->setSizeHint(widget->sizeHint()); + + connect(widget, &PluginListItem::clickedChoosePlugin, [this, item] { this->replacePlugin(item); }); + connect(widget, &PluginListItem::clickedEditPlugin, [this, item] { this->openPlugin(item); }); + connect(widget, &PluginListItem::clickedDeletePlugin, [this, item] { this->removePlugin(item); }); + connect(widget, &PluginListItem::toggledEnable, this, &PluginEditor::markEdited); + connect(widget, &PluginListItem::pathEdited, this, &PluginEditor::markEdited); + + // Per the Qt manual, for performance reasons QListWidget::setItemWidget shouldn't be used with non-static items. + // There's an assumption here that users won't have enough plugins for that to be a problem. + ui->list->addItem(item); + ui->list->setItemWidget(item, widget); +} + +void PluginEditor::markEdited() { + this->hasUnsavedChanges = true; +} + +QString PluginEditor::getPluginFilepath(QListWidgetItem * item, bool absolutePath) const { + auto widget = dynamic_cast(ui->list->itemWidget(item)); + if (!widget) return QString(); + + QString path = widget->path(); + if (absolutePath) { + QFileInfo fileInfo(path); + if (fileInfo.isRelative()) + path.prepend(this->baseDir); + } + return path; +} + +void PluginEditor::setPluginFilepath(QListWidgetItem * item, QString filepath) const { + auto widget = dynamic_cast(ui->list->itemWidget(item)); + if (widget) { + widget->setPath(Util::stripPrefix(filepath, this->baseDir)); + } +} + +bool PluginEditor::getPluginEnabled(QListWidgetItem * item) const { + auto widget = dynamic_cast(ui->list->itemWidget(item)); + return widget && widget->pluginEnabled(); +} + +QString PluginEditor::choosePlugin(QString dir) { + return FileDialog::getOpenFileName(this, "Choose Plugin", dir, "JavaScript Files (*.js)"); +} + +void PluginEditor::createNewPlugin() { + const QString filepath = FileDialog::getSaveFileName(this, "Create New Plugin", FileDialog::getDirectory() + "/new_plugin.js", "JavaScript Files (*.js)"); + if (filepath.isEmpty()) + return; + + QFile pluginFile(filepath); + if (!pluginFile.open(QIODevice::WriteOnly)) { + logError(QString("Error: Could not open %1 for writing").arg(filepath)); + QMessageBox messageBox(this); + messageBox.setText("Failed to create new plugin file!"); + messageBox.setInformativeText(QString("Could not open \"%1\" for writing").arg(filepath)); + messageBox.setIcon(QMessageBox::Warning); + messageBox.exec(); + return; + } + ParseUtil parser; + pluginFile.write(parser.readTextFile(":/text/plugin_template.txt").toUtf8()); + pluginFile.close(); + + this->displayNewPlugin(filepath); +} + +void PluginEditor::loadPlugin() { + QString filepath = this->choosePlugin(FileDialog::getDirectory()); + if (filepath.isEmpty()) + return; + this->displayNewPlugin(filepath); +} + +void PluginEditor::displayNewPlugin(QString filepath) { + filepath = Util::stripPrefix(filepath, this->baseDir); + + // Verify new plugin path is not already in list + for (int i = 0; i < ui->list->count(); i++) { + if (filepath == this->getPluginFilepath(ui->list->item(i), false)) { + QMessageBox::information(this, QApplication::applicationName(), QString("The plugin '%1' is already loaded").arg(filepath)); + return; + } + } + + PluginSettings settings; + settings.path = filepath; + this->displayPlugin(settings); + this->markEdited(); +} + +void PluginEditor::removePlugin(QListWidgetItem * item) { + ui->list->takeItem(ui->list->row(item)); + this->markEdited(); +} + +void PluginEditor::removeSelectedPlugins() { + QList items = ui->list->selectedItems(); + if (items.length() == 0) + return; + for (auto item : items) + this->removePlugin(item); +} + +void PluginEditor::replacePlugin(QListWidgetItem * item) { + const QString filepath = this->choosePlugin(this->getPluginFilepath(item)); + if (filepath.isEmpty()) + return; + this->setPluginFilepath(item, filepath); + this->markEdited(); +} + +void PluginEditor::openPlugin(QListWidgetItem * item) { + const QString path = this->getPluginFilepath(item); + QFileInfo fileInfo(path); + if (!fileInfo.exists() || !fileInfo.isFile()){ + QMessageBox::warning(this, QApplication::applicationName(), QString("Failed to open plugin '%1'").arg(path)); + return; + } + Editor::openInTextEditor(path); +} + +void PluginEditor::openSelectedPlugins() { + for (auto item : ui->list->selectedItems()) + this->openPlugin(item); +} + +void PluginEditor::openManual() { + static const QUrl url("https://huderlem.github.io/porymap/manual/scripting-capabilities.html"); + QDesktopServices::openUrl(url); +} + +// When the user refreshes the plugins we show a little tooltip as feedback. +// We don't want this tooltip to display when we refresh programmatically, like when changes are saved. +void PluginEditor::userRefreshPlugins() { + if (refreshPlugins()) + QToolTip::showText(ui->button_RefreshPlugins->mapToGlobal(QPoint(0, 0)), "Refreshed!"); +} + +bool PluginEditor::refreshPlugins() { + if (this->hasUnsavedChanges) { + if (this->prompt("Plugins have been modified, save changes and reload plugins?", QMessageBox::Yes) == QMessageBox::No) + return false; + this->save(); + } + emit reloadScriptEngine(); + return true; +} + +void PluginEditor::save() { + if (!this->hasUnsavedChanges) + return; + + QList userPlugins; + QList projectPlugins; + for (int i = 0; i < ui->list->count(); i++) { + auto item = ui->list->item(i); + auto widget = dynamic_cast(ui->list->itemWidget(item)); + if (!widget) continue; + const PluginSettings settings = widget->getSettings(); + if (settings.userOnly) userPlugins.append(settings); + else projectPlugins.append(settings); + } + + userConfig.plugins = userPlugins; + userConfig.save(); + + projectConfig.plugins = projectPlugins; + projectConfig.save(); + + this->hasUnsavedChanges = false; + this->refreshPlugins(); +} + +int PluginEditor::prompt(const QString &text, QMessageBox::StandardButton defaultButton) { + QMessageBox messageBox(this); + messageBox.setText(text); + messageBox.setIcon(QMessageBox::Question); + messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No | defaultButton); + messageBox.setDefaultButton(defaultButton); + return messageBox.exec(); +} + +void PluginEditor::dialogButtonClicked(QAbstractButton *button) { + if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) + this->save(); + close(); // All buttons (OK and Cancel) close the window +} + +void PluginEditor::closeEvent(QCloseEvent* event) { + if (this->hasUnsavedChanges) { + int result = this->prompt("Plugins have been modified, save changes?", QMessageBox::Cancel); + if (result == QMessageBox::Cancel) { + event->ignore(); + return; + } + if (result == QMessageBox::Yes) + this->save(); + } +} diff --git a/src/ui/pluginlistitem.cpp b/src/ui/pluginlistitem.cpp new file mode 100644 index 00000000..cad23309 --- /dev/null +++ b/src/ui/pluginlistitem.cpp @@ -0,0 +1,55 @@ +#include "pluginlistitem.h" +#include "ui_pluginlistitem.h" + +PluginListItem::PluginListItem(QWidget *parent) : + QFrame(parent), + ui(new Ui::PluginListItem) +{ + ui->setupUi(this); + + connect(ui->b_Choose, &QAbstractButton::clicked, this, &PluginListItem::clickedChoosePlugin); + connect(ui->b_Edit, &QAbstractButton::clicked, this, &PluginListItem::clickedEditPlugin); + connect(ui->b_Delete, &QAbstractButton::clicked, this, &PluginListItem::clickedDeletePlugin); + connect(ui->checkBox_Enable, &QCheckBox::toggled, this, &PluginListItem::toggledEnable); + connect(ui->lineEdit_filepath, &QLineEdit::textEdited, this, &PluginListItem::pathEdited); +} + +PluginListItem::PluginListItem(const PluginSettings& settings, QWidget *parent) : + PluginListItem(parent) +{ + setSettings(settings); +} + +PluginListItem::~PluginListItem() { + delete ui; +} + +void PluginListItem::setPath(const QString& text) { + ui->lineEdit_filepath->setText(text); +} + +QString PluginListItem::path() const { + return ui->lineEdit_filepath->text(); +} + +void PluginListItem::setPluginEnabled(bool enabled) { + ui->checkBox_Enable->setChecked(enabled); +} + +bool PluginListItem::pluginEnabled() const { + return ui->checkBox_Enable->isChecked(); +} + +// TODO: The two functions below should read/write from/to the UI whether the plugin belongs to the project or user. +void PluginListItem::setSettings(const PluginSettings& settings) { + setPath(settings.path); + setPluginEnabled(settings.enabled); +} + +PluginSettings PluginListItem::getSettings() const { + return { + .path = path(), + .enabled = pluginEnabled(), + .userOnly = true, + }; +}