Merge pull request #325 from BigBahss/script-editing

Open map scripts directly to the script of the selected event
This commit is contained in:
huderlem 2020-12-13 17:22:47 -06:00 committed by GitHub
commit e16ec480e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 710 additions and 79 deletions

View file

@ -19,6 +19,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Add ruler tool for measuring metatile distance in events tab (Right-click to turn on/off, left-click to lock in place).
- Add delete button to wild pokemon encounters tab.
- Add shortcut customization via `Options -> Edit Shortcuts`.
- Add custom text editor commands in `Options -> Edit Preferences`, a tool-button next to the `Script` combo-box, and `Tools -> Open Project in Text Editor`. The tool-button will open the containing file to the cooresponding script.
### Changed
- Holding `shift` now toggles "Smart Path" drawing; when the "Smart Paths" checkbox is checked, holding `shift` will temporarily disable it.

View file

@ -225,11 +225,17 @@ Respawn NPC
Open Map Scripts
----------------
Clicking the ``Open Map Scripts`` button |open-map-scripts-button| will open the map's scripts file in your default text editor. If nothing happens when this button is clicked, you may need to associate a text editor with the `.inc` file extension.
Clicking the ``Open Map Scripts`` button |open-map-scripts-button| will open the map's scripts file in your default text editor. If nothing happens when this button is clicked, you may need to associate a text editor with the `.inc` file extension (or `.pory` if you're using Porycript).
Additionally, if you specify a ``Goto Line Command`` in *Options -> Edit Preferences* then a tool-button will appear next to the `Script` combo-box in the *Events* tab. Clicking this button will open the file that contains the script directly to the line number of that script. If the script cannot be found in the project then the current map's scripts file is opened.
|go-to-script-button|
.. |open-map-scripts-button|
image:: images/editing-map-events/open-map-scripts-button.png
.. |go-to-script-button|
image:: images/editing-map-events/go-to-script-button.png
Tool Buttons
------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -31,6 +31,8 @@ determined by this file.
``monitor_files``, 1, global, yes, Whether porymap will monitor changes to project files
``region_map_dimensions``, 32x20, global, yes, The dimensions of the region map tilemap
``theme``, default, global, yes, The color theme for porymap windows and widgets
``text_editor_goto_line``, , global, yes, The command that will be executed when clicking the button next the ``Script`` combo-box.
``text_editor_open_directory``, , global, yes, The command that will be executed when clicking ``Open Project in Text Editor``.
``base_game_version``, , project, no, The base pret repo for this project
``use_encounter_json``, 1, project, yes, Enables wild encounter table editing
``use_poryscript``, 0, project, yes, Whether to open .pory files for scripts

View file

@ -41,6 +41,7 @@ Main Window
Open New Tileset Dialog, ``Ctrl+Shift+N``
Open Tileset Editor, ``Ctrl+T``
Open Region Map Editor, ``Ctrl+M``
Edit Preferences, ``Ctrl+,``
.. csv-table::
:header: Map Editing

View file

@ -2624,8 +2624,6 @@
<addaction name="actionCursor_Tile_Outline"/>
<addaction name="actionPlayer_View_Rectangle"/>
<addaction name="actionBetter_Cursors"/>
<addaction name="separator"/>
<addaction name="actionThemes"/>
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
@ -2642,6 +2640,8 @@
<addaction name="actionNew_Tileset"/>
<addaction name="actionTileset_Editor"/>
<addaction name="actionRegion_Map_Editor"/>
<addaction name="separator"/>
<addaction name="actionOpen_Project_in_Text_Editor"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
@ -2657,6 +2657,7 @@
<addaction name="actionMonitor_Project_Files"/>
<addaction name="actionUse_Poryscript"/>
<addaction name="separator"/>
<addaction name="actionEdit_Preferences"/>
<addaction name="actionEdit_Shortcuts"/>
</widget>
<addaction name="menuFile"/>
@ -2914,16 +2915,24 @@
<string>Ctrl+Shift+N</string>
</property>
</action>
<action name="actionThemes">
<property name="text">
<string>Themes...</string>
</property>
</action>
<action name="actionExport_Stitched_Map_Image">
<property name="text">
<string>Export Map Stitch Image...</string>
</property>
</action>
<action name="actionEdit_Preferences">
<property name="text">
<string>Edit Preferences...</string>
</property>
<property name="shortcut">
<string>Ctrl+,</string>
</property>
</action>
<action name="actionOpen_Project_in_Text_Editor">
<property name="text">
<string>Open Project in Text Editor</string>
</property>
</action>
<action name="actionEdit_Shortcuts">
<property name="text">
<string>Edit Shortcuts...</string>

190
forms/preferenceeditor.ui Normal file
View file

@ -0,0 +1,190 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PreferenceEditor</class>
<widget class="QMainWindow" name="PreferenceEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
<string>Preferences</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>9</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_Themes">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Application Theme</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_TextEditor">
<property name="title">
<string>Preferred Text Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="2" column="0">
<widget class="QScrollArea" name="scrollArea_TextEditor">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_TextEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>582</width>
<height>372</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="label_TextEditorGotoLineHelp">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When this command is set a button will appear next to the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Script&lt;/span&gt; combo-box in the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Events&lt;/span&gt; tab which executes this command.&lt;span style=&quot; font-weight:600;&quot;&gt; %F&lt;/span&gt; will be substituted with the file path of the script and &lt;span style=&quot; font-weight:600;&quot;&gt;%L&lt;/span&gt; will be substituted with the line number of the script in that file. &lt;span style=&quot; font-weight:600;&quot;&gt;%F &lt;/span&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;must&lt;/span&gt; be given if &lt;span style=&quot; font-weight:600;&quot;&gt;%L&lt;/span&gt; is given. If &lt;span style=&quot; font-weight:600;&quot;&gt;%F&lt;/span&gt; is &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; given then the script's file path will be added to the end of the command. If the script can't be found then the current map's scripts file is opened.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_TextEditorGotoLine">
<property name="text">
<string>Goto Line Command</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_TextEditorOpenFolder">
<property name="toolTip">
<string>The shell command for your preferred text editor (possibly an absolute path if the program doesn't exist in your PATH).</string>
</property>
<property name="placeholderText">
<string>e.g. code %D</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_TextEditorOpenFolderHelp">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is the command that is executed when clicking &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Open Project in Text Editor&lt;/span&gt; in the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Tools&lt;/span&gt; menu. &lt;span style=&quot; font-weight:600;&quot;&gt;%D&lt;/span&gt; will be substituted with the project's root directory. If &lt;span style=&quot; font-weight:600;&quot;&gt;%D&lt;/span&gt; is &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; specified then the project directory will be added to the end of the command.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_TextEditorOpenFolder">
<property name="text">
<string>Open Directory Command</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_TextEditorGotoLine">
<property name="toolTip">
<string>The shell command for your preferred text editor to open a file to a specific line number (possibly an absolute path if the program doesn't exist in your PATH).</string>
</property>
<property name="placeholderText">
<string>e.g. code --goto %F:%L</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -47,6 +47,8 @@ public:
this->monitorFiles = true;
this->regionMapDimensions = QSize(32, 20);
this->theme = "default";
this->textEditorOpenFolder = "";
this->textEditorGotoLine = "";
}
void setRecentProject(QString project);
void setRecentMap(QString map);
@ -63,6 +65,8 @@ public:
void setMonitorFiles(bool monitor);
void setRegionMapDimensions(int width, int height);
void setTheme(QString theme);
void setTextEditorOpenFolder(const QString &command);
void setTextEditorGotoLine(const QString &command);
QString getRecentProject();
QString getRecentMap();
MapSortOrder getMapSortOrder();
@ -78,6 +82,8 @@ public:
bool getMonitorFiles();
QSize getRegionMapDimensions();
QString getTheme();
QString getTextEditorOpenFolder();
QString getTextEditorGotoLine();
protected:
virtual QString getConfigFilepath() override;
virtual void parseConfigKeyValue(QString key, QString value) override;
@ -109,6 +115,8 @@ private:
bool monitorFiles;
QSize regionMapDimensions;
QString theme;
QString textEditorOpenFolder;
QString textEditorGotoLine;
};
extern PorymapConfig porymapConfig;

View file

@ -31,10 +31,10 @@ public:
Event(const Event&);
Event(QJsonObject, QString);
public:
int x() {
int x() const {
return getInt("x");
}
int y() {
int y() const {
return getInt("y");
}
int elevation() {
@ -46,16 +46,16 @@ public:
void setY(int y) {
put("y", y);
}
QString get(QString key) {
QString get(const QString &key) const {
return values.value(key);
}
int getInt(QString key) {
int getInt(const QString &key) const {
return values.value(key).toInt(nullptr, 0);
}
uint16_t getU16(QString key) {
uint16_t getU16(const QString &key) const {
return values.value(key).toUShort(nullptr, 0);
}
int16_t getS16(QString key) {
int16_t getS16(const QString &key) const {
return values.value(key).toShort(nullptr, 0);
}
void put(QString key, int value) {

View file

@ -83,7 +83,8 @@ public:
void floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation);
void _floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation);
void magicFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation);
QList<Event*> getAllEvents();
QList<Event*> getAllEvents() const;
QStringList eventScriptLabels(const QString &event_group_type = QString()) const;
void removeEvent(Event*);
void addEvent(Event*);
QPixmap renderConnection(MapConnection, MapLayout *);

View file

@ -40,7 +40,7 @@ class ParseUtil
public:
ParseUtil();
void set_root(QString);
QString readTextFile(QString);
static QString readTextFile(QString);
void strip_comment(QString*);
QList<QStringList>* parseAsm(QString);
int evaluateDefine(QString, QMap<QString, int>*);
@ -54,6 +54,17 @@ public:
bool tryParseJsonFile(QJsonDocument *out, QString filepath);
bool ensureFieldsExist(QJsonObject obj, QList<QString> fields);
// Returns the 1-indexed line number for the definition of scriptLabel in the scripts file at filePath.
// Returns 0 if a definition for scriptLabel cannot be found.
static int getScriptLineNumber(const QString &filePath, const QString &scriptLabel);
static int getRawScriptLineNumber(QString text, const QString &scriptLabel);
static int getPoryScriptLineNumber(QString text, const QString &scriptLabel);
static QString &removeStringLiterals(QString &text);
static QString &removeLineComments(QString &text, const QString &commentSymbol);
static QString &removeLineComments(QString &text, const QStringList &commentSymbols);
static QStringList splitShellCommand(QStringView command);
private:
QString root;
QString text;

View file

@ -65,7 +65,6 @@ public:
void displayMapBorder();
void displayMapGrid();
void displayWildMonTables();
void maskNonVisibleConnectionTiles();
void updateMapBorder();
void updateMapConnections();
@ -155,6 +154,12 @@ public:
void shouldReselectEvents();
void scaleMapView(int);
public slots:
void openMapScripts() const;
void openScript(const QString &scriptLabel) const;
void openProjectInTextEditor() const;
void maskNonVisibleConnectionTiles();
private:
void setConnectionItemsVisible(bool);
void setBorderItemsVisible(bool, qreal = 1);
@ -182,6 +187,10 @@ private:
QString getMovementPermissionText(uint16_t collision, uint16_t elevation);
QString getMetatileDisplayMessage(uint16_t metatileId);
bool eventLimitReached(Map *, QString);
void openInTextEditor(const QString &path, int lineNum = 0) const;
bool startDetachedProcess(const QString &command,
const QString &workingDirectory = QString(),
qint64 *pid = nullptr) const;
private slots:
void onMapStartPaint(QGraphicsSceneMouseEvent *event, MapPixmapItem *item);

View file

@ -22,6 +22,7 @@
#include "newmappopup.h"
#include "newtilesetdialog.h"
#include "shortcutseditor.h"
#include "preferenceeditor.h"
namespace Ui {
class MainWindow;
@ -119,8 +120,6 @@ private slots:
void duplicate();
void openInTextEditor();
void onLoadMapRequested(QString, QString);
void onMapChanged(Map *map);
void onMapNeedsRedrawing();
@ -167,7 +166,6 @@ private slots:
void on_actionMap_Shift_triggered();
void on_toolButton_deleteObject_clicked();
void on_toolButton_Open_Scripts_clicked();
void addNewEvent(QString);
void updateSelectedObjects();
@ -222,7 +220,6 @@ private slots:
void on_toolButton_ExpandAll_clicked();
void on_toolButton_CollapseAll_clicked();
void on_actionAbout_Porymap_triggered();
void on_actionThemes_triggered();
void on_pushButton_AddCustomHeaderField_clicked();
void on_pushButton_DeleteCustomHeaderField_clicked();
void on_tableWidget_CustomHeaderFields_cellChanged(int row, int column);
@ -232,6 +229,8 @@ private slots:
void on_pushButton_ConfigureEncountersJSON_clicked();
void on_actionRegion_Map_Editor_triggered();
void on_actionEdit_Preferences_triggered();
void togglePreferenceSpecificUi();
private:
Ui::MainWindow *ui;
@ -242,6 +241,7 @@ private:
MapImageExporter *mapImageExporter = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
NewMapPopup *newmapprompt = nullptr;
PreferenceEditor *preferenceEditor = nullptr;
QStandardItemModel *mapListModel;
QList<QStandardItem*> *mapGroupItemsList;
QMap<QString, QModelIndex> mapListIndexes;
@ -267,6 +267,7 @@ private:
DraggablePixmapItem *selectedHealspot;
QList<QAction *> registeredActions;
QVector<QToolButton *> openScriptButtons;
bool isProgrammaticEventTabChange;
bool projectHasUnsavedChanges;

View file

@ -172,8 +172,10 @@ public:
QString fixPalettePath(QString path);
QString fixGraphicPath(QString path);
QString getScriptFileExtension(bool usePoryScript);
QString getScriptDefaultString(bool usePoryScript, QString mapName);
QString getScriptFileExtension(bool usePoryScript) const;
QString getScriptDefaultString(bool usePoryScript, QString mapName) const;
QString getMapScriptsFilePath(const QString &mapName) const;
QStringList getEventScriptsFilePaths() const;
bool loadMapBorder(Map *map);

View file

@ -0,0 +1,37 @@
#ifndef PREFERENCES_H
#define PREFERENCES_H
#include <QMainWindow>
class NoScrollComboBox;
class QAbstractButton;
namespace Ui {
class PreferenceEditor;
}
class PreferenceEditor : public QMainWindow
{
Q_OBJECT
public:
explicit PreferenceEditor(QWidget *parent = nullptr);
~PreferenceEditor();
signals:
void preferencesSaved();
void themeChanged(const QString &theme);
private:
Ui::PreferenceEditor *ui;
NoScrollComboBox *themeSelector;
void populateFields();
void saveFields();
private slots:
void dialogButtonClicked(QAbstractButton *button);
};
#endif // PREFERENCES_H

View file

@ -74,6 +74,7 @@ SOURCES += src/core/block.cpp \
src/ui/shortcut.cpp \
src/ui/shortcutseditor.cpp \
src/ui/multikeyedit.cpp \
src/ui/preferenceeditor.cpp \
src/config.cpp \
src/editor.cpp \
src/main.cpp \
@ -146,6 +147,7 @@ HEADERS += include/core/block.h \
include/ui/shortcut.h \
include/ui/shortcutseditor.h \
include/ui/multikeyedit.h \
include/ui/preferenceeditor.h \
include/config.h \
include/editor.h \
include/mainwindow.h \
@ -163,7 +165,8 @@ FORMS += forms/mainwindow.ui \
forms/aboutporymap.ui \
forms/newtilesetdialog.ui \
forms/mapimageexporter.ui \
forms/shortcutseditor.ui
forms/shortcutseditor.ui \
forms/preferenceeditor.ui
RESOURCES += \
resources/images.qrc \

View file

@ -190,6 +190,10 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
}
} else if (key == "theme") {
this->theme = value;
} else if (key == "text_editor_open_directory") {
this->textEditorOpenFolder = value;
} else if (key == "text_editor_goto_line") {
this->textEditorGotoLine = value;
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
@ -219,6 +223,8 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("region_map_dimensions", QString("%1x%2").arg(this->regionMapDimensions.width())
.arg(this->regionMapDimensions.height()));
map.insert("theme", this->theme);
map.insert("text_editor_open_directory", this->textEditorOpenFolder);
map.insert("text_editor_goto_line", this->textEditorGotoLine);
return map;
}
@ -319,6 +325,16 @@ void PorymapConfig::setTheme(QString theme) {
this->theme = theme;
}
void PorymapConfig::setTextEditorOpenFolder(const QString &command) {
this->textEditorOpenFolder = command;
this->save();
}
void PorymapConfig::setTextEditorGotoLine(const QString &command) {
this->textEditorGotoLine = command;
this->save();
}
QString PorymapConfig::getRecentProject() {
return this->recentProject;
}
@ -401,6 +417,14 @@ QString PorymapConfig::getTheme() {
return this->theme;
}
QString PorymapConfig::getTextEditorOpenFolder() {
return this->textEditorOpenFolder;
}
QString PorymapConfig::getTextEditorGotoLine() {
return this->textEditorGotoLine;
}
const QMap<BaseGameVersion, QString> baseGameVersionMap = {
{BaseGameVersion::pokeruby, "pokeruby"},
{BaseGameVersion::pokefirered, "pokefirered"},

View file

@ -450,12 +450,32 @@ void Map::magicFillCollisionElevation(int initialX, int initialY, uint16_t colli
}
}
QList<Event *> Map::getAllEvents() {
QList<Event*> all;
for (QList<Event*> list : events.values()) {
all += list;
QList<Event *> Map::getAllEvents() const {
QList<Event *> all_events;
for (const auto &event_list : events) {
all_events << event_list;
}
return all;
return all_events;
}
QStringList Map::eventScriptLabels(const QString &event_group_type) const {
QStringList scriptLabels;
if (event_group_type.isEmpty()) {
for (const auto *event : getAllEvents())
scriptLabels << event->get("script_label");
} else {
for (const auto *event : events.value(event_group_type))
scriptLabels << event->get("script_label");
}
scriptLabels.removeDuplicates();
scriptLabels.removeAll(QString());
if (scriptLabels.contains("0x0"))
scriptLabels.move(scriptLabels.indexOf("0x0"), scriptLabels.count() - 1);
if (scriptLabels.contains("NULL"))
scriptLabels.move(scriptLabels.indexOf("NULL"), scriptLabels.count() - 1);
return scriptLabels;
}
void Map::removeEvent(Event *event) {

View file

@ -420,3 +420,118 @@ bool ParseUtil::ensureFieldsExist(QJsonObject obj, QList<QString> fields) {
}
return true;
}
int ParseUtil::getScriptLineNumber(const QString &filePath, const QString &scriptLabel) {
if (scriptLabel.isEmpty())
return 0;
if (filePath.endsWith(".inc") || filePath.endsWith(".s"))
return getRawScriptLineNumber(readTextFile(filePath), scriptLabel);
else if (filePath.endsWith(".pory"))
return getPoryScriptLineNumber(readTextFile(filePath), scriptLabel);
return 0;
}
int ParseUtil::getRawScriptLineNumber(QString text, const QString &scriptLabel) {
removeStringLiterals(text);
removeLineComments(text, "@");
static const QRegularExpression re_incScriptLabel("\\b(?<label>[\\w_][\\w\\d_]*):{1,2}");
QRegularExpressionMatchIterator it = re_incScriptLabel.globalMatch(text);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
if (match.captured("label") == scriptLabel)
return text.left(match.capturedStart("label")).count('\n') + 1;
}
return 0;
}
int ParseUtil::getPoryScriptLineNumber(QString text, const QString &scriptLabel) {
removeStringLiterals(text);
removeLineComments(text, {"//", "#"});
static const QRegularExpression re_poryScriptLabel("\\b(script)(\\((global|local)\\))?\\s*\\b(?<label>[\\w_][\\w\\d_]*)");
QRegularExpressionMatchIterator it = re_poryScriptLabel.globalMatch(text);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
if (match.captured("label") == scriptLabel)
return text.left(match.capturedStart("label")).count('\n') + 1;
}
static const QRegularExpression re_poryRawSection("\\b(raw)\\s*`(?<raw_script>[^`]*)");
QRegularExpressionMatchIterator raw_it = re_poryRawSection.globalMatch(text);
while (raw_it.hasNext()) {
const QRegularExpressionMatch match = raw_it.next();
const int relativelineNum = getRawScriptLineNumber(match.captured("raw_script"), scriptLabel);
if (relativelineNum)
return text.left(match.capturedStart("raw_script")).count('\n') + relativelineNum;
}
return 0;
}
QString &ParseUtil::removeStringLiterals(QString &text) {
static const QRegularExpression re_string("\".*\"");
return text.remove(re_string);
}
QString &ParseUtil::removeLineComments(QString &text, const QString &commentSymbol) {
const QRegularExpression re_lineComment(commentSymbol + "+.*");
return text.remove(re_lineComment);
}
QString &ParseUtil::removeLineComments(QString &text, const QStringList &commentSymbols) {
for (const auto &commentSymbol : commentSymbols)
removeLineComments(text, commentSymbol);
return text;
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
#include <QProcess>
QStringList ParseUtil::splitShellCommand(QStringView command) {
return QProcess::splitCommand(command);
}
#else
// The source for QProcess::splitCommand() as of Qt 5.15.2
QStringList ParseUtil::splitShellCommand(QStringView command) {
QStringList args;
QString tmp;
int quoteCount = 0;
bool inQuote = false;
// handle quoting. tokens can be surrounded by double quotes
// "hello world". three consecutive double quotes represent
// the quote character itself.
for (int i = 0; i < command.size(); ++i) {
if (command.at(i) == QLatin1Char('"')) {
++quoteCount;
if (quoteCount == 3) {
// third consecutive quote
quoteCount = 0;
tmp += command.at(i);
}
continue;
}
if (quoteCount) {
if (quoteCount == 1)
inQuote = !inQuote;
quoteCount = 0;
}
if (!inQuote && command.at(i).isSpace()) {
if (!tmp.isEmpty()) {
args += tmp;
tmp.clear();
}
} else {
tmp += command.at(i);
}
}
if (!tmp.isEmpty())
args += tmp;
return args;
}
#endif

View file

@ -9,10 +9,12 @@
#include "metatile.h"
#include "montabwidget.h"
#include "editcommands.h"
#include "config.h"
#include <QCheckBox>
#include <QPainter>
#include <QMouseEvent>
#include <QDir>
#include <QProcess>
#include <math.h>
static bool selectNewEvents = false;
@ -2024,6 +2026,72 @@ void Editor::deleteEvent(Event *event) {
//updateSelectedObjects();
}
void Editor::openMapScripts() const {
const QString scriptPath = project->getMapScriptsFilePath(map->name);
openInTextEditor(scriptPath);
}
void Editor::openScript(const QString &scriptLabel) const {
// Find the location of scriptLabel.
QStringList scriptPaths(project->getMapScriptsFilePath(map->name));
scriptPaths << project->getEventScriptsFilePaths();
int lineNum = 0;
QString scriptPath = scriptPaths.first();
for (const auto &path : scriptPaths) {
lineNum = ParseUtil::getScriptLineNumber(path, scriptLabel);
if (lineNum != 0) {
scriptPath = path;
break;
}
}
openInTextEditor(scriptPath, lineNum);
}
void Editor::openInTextEditor(const QString &path, int lineNum) const {
QString command = porymapConfig.getTextEditorGotoLine();
if (command.isEmpty()) {
// Open map scripts in the system's default editor.
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
} else {
if (command.contains("%F")) {
if (command.contains("%L"))
command.replace("%L", QString::number(lineNum));
command.replace("%F", path);
} else {
command += ' ' + path;
}
startDetachedProcess(command);
}
}
void Editor::openProjectInTextEditor() const {
QString command = porymapConfig.getTextEditorOpenFolder();
if (command.contains("%D"))
command.replace("%D", project->root);
else
command += ' ' + project->root;
startDetachedProcess(command);
}
bool Editor::startDetachedProcess(const QString &command, const QString &workingDirectory, qint64 *pid) const {
static QProcess process;
logInfo("Executing command: " + command);
#ifdef Q_OS_WIN
// On Windows, a QProcess command must be wrapped in a cmd.exe command.
process.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
process.setProgram(process.processEnvironment().value("COMSPEC"));
process.setNativeArguments("/c " + command);
process.setWorkingDirectory(workingDirectory);
#else
QStringList arguments = ParseUtil::splitShellCommand(command);
process.setProgram(arguments.takeFirst());
process.setArguments(arguments);
process.setWorkingDirectory(workingDirectory);
#endif
return process.startDetached(pid);
}
// It doesn't seem to be possible to prevent the mousePress event
// from triggering both event's DraggablePixmapItem and the background mousePress.
// Since the DraggablePixmapItem's event fires first, we can set a temp

View file

@ -29,7 +29,6 @@
#include <QDialogButtonBox>
#include <QScroller>
#include <math.h>
#include <QProcess>
#include <QSysInfo>
#include <QDesktopServices>
#include <QTransform>
@ -84,6 +83,8 @@ void MainWindow::setWindowDisabled(bool disabled) {
ui->action_Exit->setDisabled(false);
ui->menuHelp->setDisabled(false);
ui->actionAbout_Porymap->setDisabled(false);
if (!disabled)
togglePreferenceSpecificUi();
}
void MainWindow::initWindow() {
@ -143,9 +144,9 @@ void MainWindow::initExtraShortcuts() {
shortcutCollapse_All->setObjectName("shortcutCollapse_All");
shortcutCollapse_All->setWhatsThis("Map List: Collapse all folders");
auto *shortcutNew_Event = new Shortcut(QKeySequence(), this, SLOT(on_toolButton_Open_Scripts_clicked()));
shortcutNew_Event->setObjectName("shortcut_Open_Scripts");
shortcutNew_Event->setWhatsThis("Open Map Scripts");
auto *shortcut_Open_Scripts = new Shortcut(QKeySequence(), ui->toolButton_Open_Scripts, SLOT(click()));
shortcut_Open_Scripts->setObjectName("shortcut_Open_Scripts");
shortcut_Open_Scripts->setWhatsThis("Open Map Scripts");
}
QObjectList MainWindow::shortcutableObjects() const {
@ -230,6 +231,8 @@ void MainWindow::initEditor() {
connect(this->editor, SIGNAL(currentMetatilesSelectionChanged()), this, SLOT(currentMetatilesSelectionChanged()));
connect(this->editor, SIGNAL(wildMonDataChanged()), this, SLOT(onWildMonDataChanged()));
connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged);
connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts);
connect(ui->actionOpen_Project_in_Text_Editor, &QAction::triggered, this->editor, &Editor::openProjectInTextEditor);
this->loadUserSettings();
@ -1372,16 +1375,6 @@ void MainWindow::duplicate() {
editor->duplicateSelectedEvents();
}
// Open current map scripts in system default editor for .inc files
void MainWindow::openInTextEditor() {
bool usePoryscript = projectConfig.getUsePoryScript();
QString path = QDir::cleanPath("file://" + editor->project->root + QDir::separator() + "data/maps/" + editor->map->name + "/scripts");
// Try opening scripts file, if opening .pory failed try again with .inc
if (!QDesktopServices::openUrl(QUrl(path + editor->project->getScriptFileExtension(usePoryscript))) && usePoryscript)
QDesktopServices::openUrl(QUrl(path + editor->project->getScriptFileExtension(false)));
}
void MainWindow::on_action_Save_triggered() {
editor->save();
updateMapList();
@ -1658,6 +1651,10 @@ void MainWindow::updateSelectedObjects() {
}
}
for (auto *button : openScriptButtons)
delete button;
openScriptButtons.clear();
QMap<QString, int> event_obj_gfx_constants = editor->project->getEventObjGfxConstants();
QList<EventPropertiesFrame *> frames;
@ -1917,6 +1914,7 @@ void MainWindow::updateSelectedObjects() {
"normal movement behavior actions.");
combo->setMinimumContentsLength(4);
} else if (key == "script_label") {
combo->addItems(editor->map->eventScriptLabels());
combo->setToolTip("The script which is executed with this event.");
} else if (key == "trainer_type") {
combo->addItems(*editor->project->trainerTypes);
@ -1979,7 +1977,26 @@ void MainWindow::updateSelectedObjects() {
} else {
combo->setCurrentText(value);
fl->addRow(new QLabel(field_labels[key], widget), combo);
if (key == "script_label") {
// Add button next to combo which opens combo's current script.
auto *hl = new QHBoxLayout();
hl->setSpacing(3);
auto *openScriptButton = new QToolButton(widget);
openScriptButtons << openScriptButton;
openScriptButton->setFixedSize(combo->height(), combo->height());
openScriptButton->setIcon(QFileIconProvider().icon(QFileIconProvider::File));
openScriptButton->setToolTip("Go to this script definition in text editor.");
connect(openScriptButton, &QToolButton::clicked,
[this, combo]() { this->editor->openScript(combo->currentText()); });
hl->addWidget(combo);
hl->addWidget(openScriptButton);
fl->addRow(new QLabel(field_labels[key], widget), hl);
if (porymapConfig.getTextEditorGotoLine().isEmpty())
openScriptButton->hide();
} else {
fl->addRow(new QLabel(field_labels[key], widget), combo);
}
widget->setLayout(fl);
frame->layout()->addWidget(widget);
@ -2214,11 +2231,6 @@ void MainWindow::on_toolButton_deleteObject_clicked() {
}
}
void MainWindow::on_toolButton_Open_Scripts_clicked()
{
openInTextEditor();
}
void MainWindow::on_toolButton_Paint_clicked()
{
if (ui->mainTabBar->currentIndex() == 0)
@ -2672,38 +2684,40 @@ void MainWindow::on_actionAbout_Porymap_triggered()
window->show();
}
void MainWindow::on_actionThemes_triggered()
{
QStringList themes;
QRegularExpression re(":/themes/([A-z0-9_-]+).qss");
themes.append("default");
QDirIterator it(":/themes", QDirIterator::Subdirectories);
while (it.hasNext()) {
QString themeName = re.match(it.next()).captured(1);
themes.append(themeName);
void MainWindow::on_actionEdit_Preferences_triggered() {
if (!preferenceEditor) {
preferenceEditor = new PreferenceEditor(this);
connect(preferenceEditor, &PreferenceEditor::themeChanged,
this, &MainWindow::setTheme);
connect(preferenceEditor, &PreferenceEditor::themeChanged,
editor, &Editor::maskNonVisibleConnectionTiles);
connect(preferenceEditor, &PreferenceEditor::preferencesSaved,
this, &MainWindow::togglePreferenceSpecificUi);
connect(preferenceEditor, &QObject::destroyed, [=](QObject *) { preferenceEditor = nullptr; });
}
QDialog themeSelectorWindow(this);
QFormLayout form(&themeSelectorWindow);
if (!preferenceEditor->isVisible()) {
preferenceEditor->show();
} else if (preferenceEditor->isMinimized()) {
preferenceEditor->showNormal();
} else {
preferenceEditor->activateWindow();
}
}
NoScrollComboBox *themeSelector = new NoScrollComboBox();
themeSelector->addItems(themes);
themeSelector->setCurrentText(porymapConfig.getTheme());
form.addRow(new QLabel("Themes"), themeSelector);
void MainWindow::togglePreferenceSpecificUi() {
if (porymapConfig.getTextEditorGotoLine().isEmpty()) {
for (auto *button : openScriptButtons)
button->hide();
} else {
for (auto *button : openScriptButtons)
button->show();
}
QDialogButtonBox buttonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close, Qt::Horizontal, &themeSelectorWindow);
form.addRow(&buttonBox);
connect(&buttonBox, &QDialogButtonBox::clicked, [&buttonBox, themeSelector, this](QAbstractButton *button){
if (button == buttonBox.button(QDialogButtonBox::Apply)) {
QString theme = themeSelector->currentText();
porymapConfig.setTheme(theme);
this->setTheme(theme);
editor->maskNonVisibleConnectionTiles();
}
});
connect(&buttonBox, SIGNAL(rejected()), &themeSelectorWindow, SLOT(reject()));
themeSelectorWindow.exec();
if (porymapConfig.getTextEditorOpenFolder().isEmpty())
ui->actionOpen_Project_in_Text_Editor->setEnabled(false);
else
ui->actionOpen_Project_in_Text_Editor->setEnabled(true);
}
void MainWindow::on_pushButton_AddCustomHeaderField_clicked()

View file

@ -2410,7 +2410,7 @@ QString Project::fixGraphicPath(QString path) {
return path;
}
QString Project::getScriptFileExtension(bool usePoryScript) {
QString Project::getScriptFileExtension(bool usePoryScript) const {
if(usePoryScript) {
return ".pory";
} else {
@ -2418,13 +2418,50 @@ QString Project::getScriptFileExtension(bool usePoryScript) {
}
}
QString Project::getScriptDefaultString(bool usePoryScript, QString mapName) {
QString Project::getScriptDefaultString(bool usePoryScript, QString mapName) const {
if(usePoryScript)
return QString("mapscripts %1_MapScripts {}").arg(mapName);
else
return QString("%1_MapScripts::\n\t.byte 0\n").arg(mapName);
}
QString Project::getMapScriptsFilePath(const QString &mapName) const {
const bool usePoryscript = projectConfig.getUsePoryScript();
auto path = QDir::cleanPath(root + "/data/maps/" + mapName + "/scripts");
auto extension = getScriptFileExtension(usePoryscript);
if (usePoryscript && !QFile::exists(path + extension))
extension = getScriptFileExtension(false);
path += extension;
return path;
}
QStringList Project::getEventScriptsFilePaths() const {
QStringList filePaths(QDir::cleanPath(root + "/data/event_scripts.s"));
const QString scriptsDir = QDir::cleanPath(root + "/data/scripts");
const QString mapsDir = QDir::cleanPath(root + "/data/maps");
const bool usePoryscript = projectConfig.getUsePoryScript();
if (usePoryscript) {
QDirIterator it_pory_shared(scriptsDir, {"*.pory"}, QDir::Files);
while (it_pory_shared.hasNext())
filePaths << it_pory_shared.next();
QDirIterator it_pory_maps(mapsDir, {"scripts.pory"}, QDir::Files, QDirIterator::Subdirectories);
while (it_pory_maps.hasNext())
filePaths << it_pory_maps.next();
}
QDirIterator it_inc_shared(scriptsDir, {"*.inc"}, QDir::Files);
while (it_inc_shared.hasNext())
filePaths << it_inc_shared.next();
QDirIterator it_inc_maps(mapsDir, {"scripts.inc"}, QDir::Files, QDirIterator::Subdirectories);
while (it_inc_maps.hasNext())
filePaths << it_inc_maps.next();
return filePaths;
}
void Project::loadEventPixmaps(QList<Event*> objects) {
bool needs_update = false;
for (Event *object : objects) {

View file

@ -0,0 +1,72 @@
#include "preferenceeditor.h"
#include "ui_preferenceeditor.h"
#include "config.h"
#include "noscrollcombobox.h"
#include <QAbstractButton>
#include <QRegularExpression>
#include <QDirIterator>
#include <QFormLayout>
PreferenceEditor::PreferenceEditor(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::PreferenceEditor),
themeSelector(nullptr)
{
ui->setupUi(this);
auto *formLayout = new QFormLayout(ui->groupBox_Themes);
themeSelector = new NoScrollComboBox(ui->groupBox_Themes);
formLayout->addRow("Themes", themeSelector);
setAttribute(Qt::WA_DeleteOnClose);
connect(ui->buttonBox, &QDialogButtonBox::clicked,
this, &PreferenceEditor::dialogButtonClicked);
populateFields();
}
PreferenceEditor::~PreferenceEditor()
{
delete ui;
}
void PreferenceEditor::populateFields() {
QStringList themes = { "default" };
QRegularExpression re(":/themes/([A-z0-9_-]+).qss");
QDirIterator it(":/themes", QDirIterator::Subdirectories);
while (it.hasNext()) {
QString themeName = re.match(it.next()).captured(1);
themes.append(themeName);
}
themeSelector->addItems(themes);
themeSelector->setCurrentText(porymapConfig.getTheme());
ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.getTextEditorOpenFolder());
ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.getTextEditorGotoLine());
}
void PreferenceEditor::saveFields() {
if (themeSelector->currentText() != porymapConfig.getTheme()) {
const auto theme = themeSelector->currentText();
porymapConfig.setTheme(theme);
emit themeChanged(theme);
}
porymapConfig.setTextEditorOpenFolder(ui->lineEdit_TextEditorOpenFolder->text());
porymapConfig.setTextEditorGotoLine(ui->lineEdit_TextEditorGotoLine->text());
emit preferencesSaved();
}
void PreferenceEditor::dialogButtonClicked(QAbstractButton *button) {
auto buttonRole = ui->buttonBox->buttonRole(button);
if (buttonRole == QDialogButtonBox::AcceptRole) {
saveFields();
close();
} else if (buttonRole == QDialogButtonBox::ApplyRole) {
saveFields();
} else if (buttonRole == QDialogButtonBox::RejectRole) {
close();
}
}