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
23 changed files with 710 additions and 79 deletions

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();
}
}