diff --git a/CHANGELOG.md b/CHANGELOG.md index abdb4d9b..81d3a192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - `MAP_OFFSET_W` and `MAP_OFFSET_H` (used to limit the maximum map size) are now read from the project. - The rendered area of the map border is now limited to the maximum player view distance (prior to this it included two extra rows on the top and bottom). - An error message will now be shown when Porymap is unable to save changes (e.g. if Porymap doesn't have write permissions for your project). +- Error and warning logs are now displayed in the status bar. This can be changed with a new setting. - A project may now be opened even if it has no maps or map groups. A minimum of one map layout is required. - The file extensions that are expected for `.png` and `.pal` data files and the extensions outputted when creating a new tileset can now be customized. - Miscellaneous performance improvements, especially for opening projects. @@ -123,6 +124,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - The Metatile / Tile usage counts in the Tileset Editor now update to reflect changes. - Fix regression that stopped the map zoom from centering on the cursor. - Fix `Open Map Scripts` not working on maps with a `shared_scripts_map` field. +- Fix the main window sometimes exceeding the screen size on first launch. ## [5.4.1] - 2024-03-21 ### Fixed diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index c605528d..996dff3b 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2867,6 +2867,13 @@ + + + + + + + @@ -2909,16 +2916,9 @@ - - - - - - - diff --git a/forms/paletteeditor.ui b/forms/paletteeditor.ui index c1a4ee7a..c98d49b6 100644 --- a/forms/paletteeditor.ui +++ b/forms/paletteeditor.ui @@ -120,6 +120,12 @@ 37 + + + File + + + Edit @@ -127,14 +133,8 @@ - - - Tools - - - + - diff --git a/forms/preferenceeditor.ui b/forms/preferenceeditor.ui index 83de6f18..0fcaf17e 100644 --- a/forms/preferenceeditor.ui +++ b/forms/preferenceeditor.ui @@ -80,6 +80,43 @@ + + + + Logging + + + + + + Status bar message types + + + + + + + Errors + + + + + + + Warnings + + + + + + + Information + + + + + + diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index c840d331..f732f335 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -39,7 +39,7 @@ 0 0 570 - 680 + 692 @@ -94,7 +94,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -164,7 +164,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -248,7 +248,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -467,37 +467,16 @@ 0 0 570 - 622 + 552 - + - Map Data Defaults + New Map Defaults - - - - Elevation - - - - - - - Width - - - - - - - <html><head/><body><p>The default elevation that will be used to fill new maps</p></body></html> - - - @@ -508,61 +487,6 @@ - - - - <html><head/><body><p>The default layout width for new maps</p></body></html> - - - 1 - - - - - - - <html><head/><body><p>The default layout height for new maps</p></body></html> - - - 1 - - - - - - - Metatile ID - - - - - - - Collision - - - - - - - Border Metatiles - - - - - - - <html><head/><body><p>The default metatile value that will be used to fill new maps</p></body></html> - - - - - - - <html><head/><body><p>The default collision that will be used to fill new maps</p></body></html> - - - @@ -634,6 +558,51 @@ + + + + + + Width + + + + + + + <html><head/><body><p>The default layout width for new maps</p></body></html> + + + 1 + + + + + + + Height + + + + + + + <html><head/><body><p>The default layout height for new maps</p></body></html> + + + 1 + + + + + + + + + Border Metatiles + + + @@ -649,13 +618,52 @@ 0 + + + + <html><head/><body><p>The default metatile value that will be used to fill new maps</p></body></html> + + + - - + + + + + + Elevation + + + + + + + Collision + + + + + + + <html><head/><body><p>The default collision that will be used to fill new maps</p></body></html> + + + + + + + <html><head/><body><p>The default elevation that will be used to fill new maps</p></body></html> + + + + + + + - Height + Metatile ID @@ -1197,7 +1205,7 @@ 0 0 570 - 927 + 915 @@ -1216,7 +1224,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1276,7 +1284,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1308,7 +1316,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1346,7 +1354,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1367,7 +1375,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1378,7 +1386,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1411,7 +1419,7 @@ ... - + :/icons/delete.ico:/icons/delete.ico @@ -1464,7 +1472,7 @@ ... - + :/icons/add.ico:/icons/add.ico @@ -1674,7 +1682,7 @@ 0 0 544 - 341 + 338 @@ -1703,7 +1711,7 @@ Add Global Constants File... - + :/icons/add.ico:/icons/add.ico @@ -1714,7 +1722,7 @@ ... - + :/icons/help.ico:/icons/help.ico @@ -1754,7 +1762,7 @@ ... - + :/icons/help.ico:/icons/help.ico @@ -1791,7 +1799,7 @@ 0 0 544 - 425 + 421 @@ -1820,7 +1828,7 @@ Add Global Constant... - + :/icons/add.ico:/icons/add.ico @@ -1886,6 +1894,8 @@
uintspinbox.h
- + + + diff --git a/forms/tileseteditor.ui b/forms/tileseteditor.ui index 1ff9d0fb..d1817859 100644 --- a/forms/tileseteditor.ui +++ b/forms/tileseteditor.ui @@ -633,32 +633,36 @@ - - - - Tools - + - - + + + Tools + + + + Edit + + + - - + + diff --git a/include/config.h b/include/config.h index 2e99dc3e..d261beb6 100644 --- a/include/config.h +++ b/include/config.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "events.h" #include "gridsettings.h" @@ -96,6 +97,7 @@ public: this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; this->shownInGameReloadMessage = false; this->gridSettings = GridSettings(); + this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; } void addRecentProject(QString project); void setRecentProjects(QStringList projects); @@ -161,6 +163,8 @@ public: QByteArray newLayoutDialogGeometry; bool shownInGameReloadMessage; GridSettings gridSettings; + // Prefer over QSet to prevent shuffling elements when writing the config file. + std::set statusBarLogTypes; protected: virtual QString getConfigFilepath() override; diff --git a/include/core/map.h b/include/core/map.h index 199968fd..ff6bc2d0 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -85,9 +85,12 @@ public: bool hasEvent(Event *) const; QStringList getScriptLabels(Event::Group group = Event::Group::None); - QString getScriptsFilePath() const; + QString getScriptsFilepath() const; void openScript(const QString &label); + static QString getJsonFilepath(const QString &mapName); + QString getJsonFilepath() const { return getJsonFilepath(m_name); } + void deleteConnections(); QList getConnections() const { return m_connections; } MapConnection* getConnection(const QString &direction) const; diff --git a/include/core/parseutil.h b/include/core/parseutil.h index 2edb9f78..c8ed3eb6 100644 --- a/include/core/parseutil.h +++ b/include/core/parseutil.h @@ -73,6 +73,8 @@ public: bool tryParseJsonFile(QJsonDocument *out, const QString &filepath, QString *error = nullptr); bool tryParseOrderedJsonFile(poryjson::Json::object *out, const QString &filepath, QString *error = nullptr); + static int getJsonLineNumber(const QString &filepath, const QString &searchText); + // 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); diff --git a/include/editor.h b/include/editor.h index 432084e8..40e0d1c5 100644 --- a/include/editor.h +++ b/include/editor.h @@ -184,6 +184,8 @@ public: void shouldReselectEvents(); void scaleMapView(int); static void openInTextEditor(const QString &path, int lineNum = 0); + void openMapJson(const QString &mapName) const; + void openLayoutJson(const QString &layoutId) const; void setCollisionGraphics(); enum ZValue { diff --git a/include/log.h b/include/log.h index 37571dae..abe85d35 100644 --- a/include/log.h +++ b/include/log.h @@ -8,6 +8,7 @@ #include #include #include +#include enum LogType { LOG_ERROR, @@ -15,12 +16,14 @@ enum LogType { LOG_INFO, }; -void logInfo(QString message); -void logWarn(QString message); -void logError(QString message); -void log(QString message, LogType type); +void logInit(); +void logInfo(const QString &message); +void logWarn(const QString &message); +void logError(const QString &message); +void log(const QString &message, LogType type); QString getLogPath(); QString getMostRecentError(); -bool cleanupLargeLog(); +void addLogStatusBar(QStatusBar *statusBar, const QSet &types = {}); +bool removeLogStatusBar(QStatusBar *statusBar); #endif // LOG_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 1efcc725..812253b3 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -412,6 +412,7 @@ private: void updateWindowTitle(); void initWindow(); + void initLogStatusBar(); void initCustomUI(); void initExtraSignals(); void initEditor(); diff --git a/include/project.h b/include/project.h index 30e84abc..570a2faa 100644 --- a/include/project.h +++ b/include/project.h @@ -215,7 +215,7 @@ public: static QString getScriptFileExtension(bool usePoryScript); QString getScriptDefaultString(bool usePoryScript, QString mapName) const; - QStringList getEventScriptsFilePaths() const; + QStringList getEventScriptsFilepaths() const; void insertGlobalScriptLabels(QStringList &scriptLabels) const; QString getDefaultPrimaryTilesetLabel() const; diff --git a/include/ui/maplisttoolbar.h b/include/ui/maplisttoolbar.h index 655ce414..77737db6 100644 --- a/include/ui/maplisttoolbar.h +++ b/include/ui/maplisttoolbar.h @@ -37,6 +37,9 @@ public: void setFilterLocked(bool locked) { m_filterLocked = locked; } bool isFilterLocked() const { return m_filterLocked; } + void setExpandListForSearch(bool expand) { m_expandListForSearch = expand; } + bool getExpandListForSearch() const { return m_expandListForSearch; } + void setSearchFocus(); signals: @@ -51,6 +54,7 @@ private: bool m_filterLocked = false; bool m_editsAllowed = false; bool m_emptyFoldersVisible = true; + bool m_expandListForSearch = true; }; #endif // MAPLISTTOOLBAR_H diff --git a/resources/icons/error.ico b/resources/icons/error.ico new file mode 100644 index 00000000..8b3611ac Binary files /dev/null and b/resources/icons/error.ico differ diff --git a/resources/icons/information.ico b/resources/icons/information.ico new file mode 100644 index 00000000..e93d00a9 Binary files /dev/null and b/resources/icons/information.ico differ diff --git a/resources/icons/warning.ico b/resources/icons/warning.ico new file mode 100644 index 00000000..a483781b Binary files /dev/null and b/resources/icons/warning.ico differ diff --git a/resources/images.qrc b/resources/images.qrc index 04253ed8..dd4d63b4 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -5,6 +5,7 @@ icons/collapse_all.ico icons/cursor.ico icons/delete.ico + icons/error.ico icons/expand_all.ico icons/file_add.ico icons/file_edit.ico @@ -20,6 +21,7 @@ icons/folder_map_opened.ico icons/folder_map.ico icons/folder.ico + icons/information.ico icons/lock_edit.ico icons/unlock_edit.ico icons/help.ico @@ -46,6 +48,7 @@ icons/sort_map.ico icons/sort_number.ico icons/tall_grass.ico + icons/warning.ico icons/minimap.ico icons/application_form_edit.ico icons/connections.ico diff --git a/src/config.cpp b/src/config.cpp index 43cc427c..2a893526 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -476,6 +476,13 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->gridSettings.style = GridSettings::getStyleFromName(value); } else if (key == "grid_color") { this->gridSettings.color = getConfigColor(key, value); + } else if (key == "status_bar_log_types") { + this->statusBarLogTypes.clear(); + auto typeStrings = value.split(",", Qt::SkipEmptyParts); + for (const auto &typeString : typeStrings) { + LogType type = static_cast(getConfigInteger(key, typeString, 0, 2)); + this->statusBarLogTypes.insert(type); + } } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } @@ -559,6 +566,12 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("grid_y", QString::number(this->gridSettings.offsetY)); map.insert("grid_style", GridSettings::getStyleName(this->gridSettings.style)); map.insert("grid_color", this->gridSettings.color.name().remove("#")); // Our text config treats '#' as the start of a comment. + + QStringList logTypesStrings; + for (const auto &type : this->statusBarLogTypes) { + logTypesStrings.append(QString::number(type)); + } + map.insert("status_bar_log_types", logTypesStrings.join(",")); return map; } diff --git a/src/core/map.cpp b/src/core/map.cpp index a006e47c..7754948f 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -148,13 +148,13 @@ void Map::invalidateScripts() { QStringList Map::getScriptLabels(Event::Group group) { if (!m_scriptsLoaded) { - const QString scriptsFilePath = getScriptsFilePath(); - m_scriptLabels = ParseUtil::getGlobalScriptLabels(scriptsFilePath); + const QString scriptsFilepath = getScriptsFilepath(); + m_scriptLabels = ParseUtil::getGlobalScriptLabels(scriptsFilepath); m_scriptsLoaded = true; // Track the scripts file for changes. Path may have changed, so stop tracking old files. m_scriptFileWatcher->removePaths(m_scriptFileWatcher->files()); - m_scriptFileWatcher->addPath(scriptsFilePath); + m_scriptFileWatcher->addPath(scriptsFilepath); } QStringList scriptLabels = m_scriptLabels; @@ -173,7 +173,7 @@ QStringList Map::getScriptLabels(Event::Group group) { return scriptLabels; } -QString Map::getScriptsFilePath() const { +QString Map::getScriptsFilepath() const { const bool usePoryscript = projectConfig.usePoryScript; auto path = QDir::cleanPath(QString("%1/%2/%3/scripts") .arg(projectConfig.projectDir) @@ -186,6 +186,13 @@ QString Map::getScriptsFilePath() const { return path; } +QString Map::getJsonFilepath(const QString &mapName) { + return QDir::cleanPath(QString("%1/%2/%3/map.json") + .arg(projectConfig.projectDir) + .arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)) + .arg(mapName)); +} + void Map::resetEvents() { m_events[Event::Group::Object].clear(); m_events[Event::Group::Warp].clear(); diff --git a/src/core/parseutil.cpp b/src/core/parseutil.cpp index 80252c13..27dc8c0d 100644 --- a/src/core/parseutil.cpp +++ b/src/core/parseutil.cpp @@ -819,14 +819,26 @@ bool ParseUtil::jsonToBool(const QJsonValue &value, bool * ok) { return false; } -int ParseUtil::getScriptLineNumber(const QString &filePath, const QString &scriptLabel) { +int ParseUtil::getJsonLineNumber(const QString &filepath, const QString &searchText) { + if (searchText.isEmpty()) + return 0; + + const QString text = readTextFile(filepath); + int index = text.indexOf(searchText); + if (index < 0) + return 0; + + return text.left(index).count('\n') + 1; +} + +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); + if (filepath.endsWith(".inc") || filepath.endsWith(".s")) + return getRawScriptLineNumber(readTextFile(filepath), scriptLabel); + else if (filepath.endsWith(".pory")) + return getPoryScriptLineNumber(readTextFile(filepath), scriptLabel); return 0; } diff --git a/src/editor.cpp b/src/editor.cpp index 6b8b9a09..d285fb08 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2272,13 +2272,13 @@ void Editor::deleteSelectedEvents() { } void Editor::openMapScripts() const { - openInTextEditor(map->getScriptsFilePath()); + openInTextEditor(map->getScriptsFilepath()); } void Editor::openScript(const QString &scriptLabel) const { // Find the location of scriptLabel. - QStringList scriptPaths(map->getScriptsFilePath()); - scriptPaths << project->getEventScriptsFilePaths(); + QStringList scriptPaths(map->getScriptsFilepath()); + scriptPaths << project->getEventScriptsFilepaths(); int lineNum = 0; QString scriptPath = scriptPaths.first(); for (const auto &path : scriptPaths) { @@ -2292,6 +2292,16 @@ void Editor::openScript(const QString &scriptLabel) const { openInTextEditor(scriptPath, lineNum); } +void Editor::openMapJson(const QString &mapName) const { + openInTextEditor(Map::getJsonFilepath(mapName)); +} + +void Editor::openLayoutJson(const QString &layoutId) const { + QString path = QDir::cleanPath(QString("%1/%2").arg(projectConfig.projectDir).arg(projectConfig.getFilePath(ProjectFilePath::json_layouts))); + QString idField = QString("\"id\": \"%1\",").arg(layoutId); + openInTextEditor(path, ParseUtil::getJsonLineNumber(path, idField)); +} + void Editor::openInTextEditor(const QString &path, int lineNum) { QString command = porymapConfig.textEditorGotoLine; if (command.isEmpty()) { diff --git a/src/log.cpp b/src/log.cpp index 64f6753a..75847372 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -3,6 +3,25 @@ #include #include #include +#include +#include +#include + +namespace Log { + static QString mostRecentError; + static QString path; + static QFile file; + static QTextStream textStream; + + struct Display { + QPointer statusBar; + QPointer message; + QPointer icon; + QSet acceptedTypes; + }; + static QList displays; + static QTimer displayClearTimer; +}; // Enabling this does not seem to be simple to color console output // on Windows for all CLIs without external libraries or extreme bloat. @@ -18,22 +37,20 @@ #define CLEAR_COLOR "\033[0m" #endif -void logInfo(QString message) { +void logInfo(const QString &message) { log(message, LogType::LOG_INFO); } -void logWarn(QString message) { +void logWarn(const QString &message) { log(message, LogType::LOG_WARN); } -static QString mostRecentError; - -void logError(QString message) { - mostRecentError = message; +void logError(const QString &message) { + Log::mostRecentError = message; log(message, LogType::LOG_ERROR); } -QString colorizeMessage(QString message, LogType type) { +QString colorizeMessage(const QString &message, LogType type) { QString colorized = message; switch (type) { @@ -50,50 +67,131 @@ QString colorizeMessage(QString message, LogType type) { return colorized; } -void log(QString message, LogType type) { +void addLogStatusBar(QStatusBar *statusBar, const QSet &acceptedTypes) { + if (!statusBar) return; + + static const QSet allTypes = {LOG_ERROR, LOG_WARN, LOG_INFO}; + + Log::Display display = { + .statusBar = statusBar, + .message = new QLabel(statusBar), + .icon = new QLabel(statusBar), + .acceptedTypes = acceptedTypes.isEmpty() ? allTypes : acceptedTypes, + }; + statusBar->addWidget(display.icon); + statusBar->addWidget(display.message); + Log::displays.append(display); +} + +void removeLogStatusBar(int index) { + Log::Display display = Log::displays.takeAt(index); + display.statusBar->removeWidget(display.icon); + display.statusBar->removeWidget(display.message); + delete display.icon; + delete display.message; +} + +bool removeLogStatusBar(QStatusBar *statusBar) { + if (!statusBar) return false; + + for (int i = 0; i < Log::displays.length(); i++) { + if (Log::displays.at(i).statusBar == statusBar) { + removeLogStatusBar(i); + return true; + } + } + return false; +} + +void updateLogDisplays(const QString &message, LogType type) { + static const QMap icons = { + {LogType::LOG_INFO, QPixmap(QStringLiteral(":/icons/information.ico"))}, + {LogType::LOG_WARN, QPixmap(QStringLiteral(":/icons/warning.ico"))}, + {LogType::LOG_ERROR, QPixmap(QStringLiteral(":/icons/error.ico"))}, + }; + + bool startTimer = false; + auto it = QMutableListIterator(Log::displays); + while (it.hasNext()) { + auto display = it.next(); + if (!display.statusBar) { + // Status bar was deleted externally, remove entry from the list. + it.remove(); + continue; + } + // Update the display, but only if it accepts this message type. + if (display.acceptedTypes.contains(type)) { + display.icon->setPixmap(icons.value(type)); + display.statusBar->clearMessage(); + display.message->setText(message); + startTimer = true; + } + } + + // Auto-hide status bar messages after a set period of time + if (startTimer) Log::displayClearTimer.start(5000); +} + +void clearLogDisplays() { + for (const auto &display : Log::displays) { + display.icon->setPixmap(QPixmap()); + display.message->setText(QString()); + } +} + +void log(const QString &message, LogType type) { QString now = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); QString typeString = ""; switch (type) { case LogType::LOG_INFO: - typeString = " [INFO]"; + typeString = QStringLiteral(" [INFO]"); break; case LogType::LOG_WARN: - typeString = " [WARN]"; + typeString = QStringLiteral(" [WARN]"); break; case LogType::LOG_ERROR: - typeString = "[ERROR]"; + typeString = QStringLiteral("[ERROR]"); break; } - message = QString("%1 %2 %3").arg(now).arg(typeString).arg(message); + updateLogDisplays(message, type); - qDebug().noquote() << colorizeMessage(message, type); - QFile outFile(getLogPath()); - outFile.open(QIODevice::WriteOnly | QIODevice::Append); - QTextStream ts(&outFile); - ts << message << Qt::endl; + QString fullMessage = QString("%1 %2 %3").arg(now).arg(typeString).arg(message); + + qDebug().noquote() << colorizeMessage(fullMessage, type); + + Log::textStream << fullMessage << Qt::endl; + Log::file.flush(); } QString getLogPath() { + return Log::path; +} + +QString getMostRecentError() { + return Log::mostRecentError; +} + +bool cleanupLargeLog() { + return Log::file.size() >= 20000000 && Log::file.resize(0); +} + +void logInit() { QString settingsPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(settingsPath); if (!dir.exists()) dir.mkpath(settingsPath); - return dir.absoluteFilePath("porymap.log"); -} + Log::path = dir.absoluteFilePath(QStringLiteral("porymap.log")); + Log::file.setFileName(Log::path); + Log::file.open(QIODevice::WriteOnly | QIODevice::Append); + Log::textStream.setDevice(&Log::file); -QString getMostRecentError() { - return mostRecentError; -} + QObject::connect(&Log::displayClearTimer, &QTimer::timeout, [=] { + clearLogDisplays(); + }); -bool cleanupLargeLog() { - QFile logFile(getLogPath()); - if (logFile.size() < 20000000) - return false; - - bool removed = logFile.remove(); - if (removed) - logWarn(QString("Previous log file %1 was cleared due to being over 20MB in size.").arg(getLogPath())); - return removed; + if (cleanupLargeLog()) { + logWarn(QString("Previous log file %1 was cleared due to being over 20MB in size.").arg(Log::path)); + } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1a76414e..7d1f177a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -73,7 +73,7 @@ MainWindow::MainWindow(QWidget *parent) : ui->setupUi(this); - cleanupLargeLog(); + logInit(); logInfo(QString("Launching Porymap v%1").arg(QCoreApplication::applicationVersion())); } @@ -142,6 +142,7 @@ void MainWindow::setWindowDisabled(bool disabled) { void MainWindow::initWindow() { porymapConfig.load(); + this->initLogStatusBar(); this->initCustomUI(); this->initExtraSignals(); this->initEditor(); @@ -237,6 +238,14 @@ void MainWindow::applyUserShortcuts() { shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut)); } +void MainWindow::initLogStatusBar() { + removeLogStatusBar(this->statusBar()); + QSet logTypes = QSet(porymapConfig.statusBarLogTypes.begin(), porymapConfig.statusBarLogTypes.end()); + if (!logTypes.isEmpty()) { + addLogStatusBar(this->statusBar(), logTypes); + } +} + void MainWindow::initCustomUI() { static const QMap mainTabNames = { {MainTab::Map, "Map"}, @@ -489,6 +498,10 @@ void MainWindow::initMapList() { ui->mapListToolBar_Locations->setEditsAllowedButtonVisible(false); ui->mapListToolBar_Layouts->setEditsAllowedButtonVisible(false); + // When searching the Layouts list, don't expand to show all the maps. If the user is searching + // this list then they're probably more interested in the layouts than the maps. + ui->mapListToolBar_Layouts->setExpandListForSearch(false); + // Initialize settings from config ui->mapListToolBar_Groups->setEditsAllowed(porymapConfig.mapListEditGroupsEnabled); for (auto i = porymapConfig.mapListHideEmptyEnabled.constBegin(); i != porymapConfig.mapListHideEmptyEnabled.constEnd(); i++) { @@ -627,13 +640,32 @@ void MainWindow::loadUserSettings() { } void MainWindow::restoreWindowState() { - logInfo("Restoring main window geometry from previous session."); QMap geometry = porymapConfig.getMainGeometry(); - this->restoreGeometry(geometry.value("main_window_geometry")); - this->restoreState(geometry.value("main_window_state")); - this->ui->splitter_map->restoreState(geometry.value("map_splitter_state")); - this->ui->splitter_main->restoreState(geometry.value("main_splitter_state")); - this->ui->splitter_Metatiles->restoreState(geometry.value("metatiles_splitter_state")); + const QByteArray mainWindowGeometry = geometry.value("main_window_geometry"); + if (!mainWindowGeometry.isEmpty()) { + logInfo("Restoring main window geometry from previous session."); + restoreGeometry(mainWindowGeometry); + restoreState(geometry.value("main_window_state")); + ui->splitter_map->restoreState(geometry.value("map_splitter_state")); + ui->splitter_main->restoreState(geometry.value("main_splitter_state")); + ui->splitter_Metatiles->restoreState(geometry.value("metatiles_splitter_state")); + } + + // Resize the window if it exceeds the available screen size. + auto screen = windowHandle() ? windowHandle()->screen() : QGuiApplication::primaryScreen(); + if (!screen) return; + const QRect screenGeometry = screen->availableGeometry(); + if (this->width() > screenGeometry.width() || this->height() > screenGeometry.height()) { + auto pixelRatio = screen->devicePixelRatio(); + logInfo(QString("Resizing main window. Dimensions of %1x%2 exceed available screen size of %3x%4") + .arg(qRound(this->width() * pixelRatio)) + .arg(qRound(this->height() * pixelRatio)) + .arg(qRound(screenGeometry.width() * pixelRatio)) + .arg(qRound(screenGeometry.height() * pixelRatio))); + resize(qMin(this->width(), screenGeometry.width()), + qMin(this->height(), screenGeometry.height())); + move(screenGeometry.center() - this->rect().center()); + } } void MainWindow::setTheme(QString theme) { @@ -662,7 +694,6 @@ bool MainWindow::openProject(QString dir, bool initial) { if (!QDir(dir).exists()) { const QString errorMsg = QString("Failed to open %1: No such directory").arg(projectString); - this->statusBar()->showMessage(errorMsg); if (initial) { // Graceful startup if recent project directory is missing logWarn(errorMsg); @@ -681,7 +712,6 @@ bool MainWindow::openProject(QString dir, bool initial) { } const QString openMessage = QString("Opening %1").arg(projectString); - this->statusBar()->showMessage(openMessage); logInfo(openMessage); porysplash->start(); @@ -718,7 +748,6 @@ bool MainWindow::openProject(QString dir, bool initial) { // Load the project if (!(loadProjectData() && setProjectUI() && setInitialMap())) { - this->statusBar()->showMessage(QString("Failed to open %1").arg(projectString)); showProjectOpenFailure(); delete this->editor->project; // TODO: Allow changing project settings at this point @@ -730,7 +759,6 @@ bool MainWindow::openProject(QString dir, bool initial) { this->editor->project->saveConfig(); updateWindowTitle(); - this->statusBar()->showMessage(QString("Opened %1").arg(projectString)); porymapConfig.projectManuallyClosed = false; porymapConfig.addRecentProject(dir); @@ -1104,12 +1132,20 @@ bool MainWindow::setMap(const QString &mapName) { // When editing in layout-only mode they are disabled. void MainWindow::setLayoutOnlyMode(bool layoutOnly) { bool mapEditingEnabled = !layoutOnly; - this->ui->mainTabBar->setTabEnabled(MainTab::Events, mapEditingEnabled); - this->ui->mainTabBar->setTabEnabled(MainTab::Header, mapEditingEnabled); - this->ui->mainTabBar->setTabEnabled(MainTab::Connections, mapEditingEnabled); - this->ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, mapEditingEnabled && editor->project->wildEncountersLoaded); + ui->mainTabBar->setTabEnabled(MainTab::Events, mapEditingEnabled); + ui->mainTabBar->setTabEnabled(MainTab::Header, mapEditingEnabled); + ui->mainTabBar->setTabEnabled(MainTab::Connections, mapEditingEnabled); + ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, mapEditingEnabled && this->editor->project->wildEncountersLoaded); - this->ui->comboBox_LayoutSelector->setEnabled(mapEditingEnabled); + // Set a tool tip to explain why the tabs are disabled. + static const QString disabledToolTip = Util::toHtmlParagraph("You are in layout-only mode. This tab is only enabled when a map is open."); + QString toolTip = mapEditingEnabled ? QString() : disabledToolTip; + ui->mainTabBar->setTabToolTip(MainTab::Events, toolTip); + ui->mainTabBar->setTabToolTip(MainTab::Header, toolTip); + ui->mainTabBar->setTabToolTip(MainTab::Connections, toolTip); + ui->mainTabBar->setTabToolTip(MainTab::WildPokemon, this->editor->project->wildEncountersLoaded ? toolTip : QString()); + + ui->comboBox_LayoutSelector->setEnabled(mapEditingEnabled); } // setLayout, but with a visible error message in case of failure. @@ -1480,6 +1516,9 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { if (itemType == "map_name") { // Right-clicking on a map. openItemAction = menu.addAction("Open Map"); + connect(menu.addAction("Open JSON file"), &QAction::triggered, [this, itemName] { + this->editor->openMapJson(itemName); + }); menu.addSeparator(); copyListNameAction = menu.addAction("Copy Map Name"); copyToolTipAction = menu.addAction("Copy Map ID"); @@ -1507,6 +1546,9 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { } else if (itemType == "map_layout") { // Right-clicking on a map layout openItemAction = menu.addAction("Open Layout"); + connect(menu.addAction("Open JSON file"), &QAction::triggered, [this, itemName] { + this->editor->openLayoutJson(itemName); + }); menu.addSeparator(); copyListNameAction = menu.addAction("Copy Layout Name"); copyToolTipAction = menu.addAction("Copy Layout ID"); @@ -3006,6 +3048,7 @@ void MainWindow::on_actionPreferences_triggered() { connect(preferenceEditor, &PreferenceEditor::themeChanged, this, &MainWindow::setTheme); connect(preferenceEditor, &PreferenceEditor::themeChanged, editor, &Editor::maskNonVisibleConnectionTiles); connect(preferenceEditor, &PreferenceEditor::preferencesSaved, this, &MainWindow::togglePreferenceSpecificUi); + connect(preferenceEditor, &PreferenceEditor::preferencesSaved, this, &MainWindow::initLogStatusBar); // Changes to porymapConfig.loadAllEventScripts or porymapConfig.eventSelectionShapeMode // require us to repopulate the EventFrames and redraw event pixmaps, respectively. connect(preferenceEditor, &PreferenceEditor::preferencesSaved, editor, &Editor::updateEvents); diff --git a/src/project.cpp b/src/project.cpp index 125f4722..c395ec92 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -367,7 +367,7 @@ QSet Project::getTopLevelMapFields() const { } QJsonDocument Project::readMapJson(const QString &mapName, QString *error) { - const QString mapFilepath = QString("%1%2/map.json").arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)).arg(mapName); + const QString mapFilepath = Map::getJsonFilepath(mapName); watchFile(mapFilepath); QJsonDocument doc; @@ -744,7 +744,11 @@ bool Project::saveMapLayouts() { } void Project::watchFile(const QString &filename) { - this->fileWatcher.addPath(QString("%1/%2").arg(this->root).arg(filename)); + if (!filename.startsWith(this->root)) { + this->fileWatcher.addPath(QString("%1/%2").arg(this->root).arg(filename)); + } else { + this->fileWatcher.addPath(filename); + } } void Project::watchFiles(const QStringList &filenames) { @@ -1234,7 +1238,7 @@ bool Project::saveMap(Map *map, bool skipLayout) { } // Create map.json for map data. - QString mapFilepath = fullPath + "/map.json"; + QString mapFilepath = map->getJsonFilepath(); QFile mapFile(mapFilepath); if (!mapFile.open(QIODevice::WriteOnly)) { logError(QString("Could not open '%1' for writing: %2").arg(mapFilepath).arg(mapFile.errorString())); @@ -2885,7 +2889,7 @@ bool Project::readEventScriptLabels() { this->globalScriptLabels.clear(); if (porymapConfig.loadAllEventScripts) { - for (const auto &filePath : getEventScriptsFilePaths()) + for (const auto &filePath : getEventScriptsFilepaths()) this->globalScriptLabels << ParseUtil::getGlobalScriptLabels(filePath); this->globalScriptLabels.sort(Qt::CaseInsensitive); @@ -2928,7 +2932,7 @@ QString Project::getScriptDefaultString(bool usePoryScript, QString mapName) con return QString("%1_MapScripts::\n\t.byte 0\n").arg(mapName); } -QStringList Project::getEventScriptsFilePaths() const { +QStringList Project::getEventScriptsFilepaths() const { QStringList filePaths(QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts))); const QString scriptsDir = QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_scripts_folders)); const QString mapsDir = QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_map_folders)); diff --git a/src/ui/colorpicker.cpp b/src/ui/colorpicker.cpp index 5d2d3992..6967c59a 100644 --- a/src/ui/colorpicker.cpp +++ b/src/ui/colorpicker.cpp @@ -43,11 +43,11 @@ ColorPicker::~ColorPicker() void ColorPicker::hover(const QPoint &pos) { QScreen *screen = QGuiApplication::screenAt(pos); if (!screen) { + // Try the screen the color picker is on, or the primary screen. const QWindow *window = windowHandle(); - if (window) screen = window->screen(); + screen = (window && window->screen()) ? window->screen() : QGuiApplication::primaryScreen(); + if (!screen) return; } - if (!screen) - return; // 15 X 15 box with 8x magnification = 120px square) QPixmap grab = screen->grabWindow(0, pos.x() - zoom_box_dimensions / 2, pos.y() - zoom_box_dimensions / 2, zoom_box_dimensions, zoom_box_dimensions); diff --git a/src/ui/maplisttoolbar.cpp b/src/ui/maplisttoolbar.cpp index f56be39b..7ef3213c 100644 --- a/src/ui/maplisttoolbar.cpp +++ b/src/ui/maplisttoolbar.cpp @@ -138,7 +138,7 @@ void MapListToolBar::applyFilter(const QString &filterText) { if (filterText.isEmpty()) { m_list->collapseAll(); emit filterCleared(m_list); - } else { + } else if (m_expandListForSearch) { m_list->expandToDepth(0); } } diff --git a/src/ui/preferenceeditor.cpp b/src/ui/preferenceeditor.cpp index 1289fe79..291c14ab 100644 --- a/src/ui/preferenceeditor.cpp +++ b/src/ui/preferenceeditor.cpp @@ -57,6 +57,11 @@ void PreferenceEditor::updateFields() { ui->checkBox_CheckForUpdates->setChecked(porymapConfig.checkForUpdates); ui->checkBox_DisableEventWarning->setChecked(porymapConfig.eventDeleteWarningDisabled); ui->checkBox_AutocompleteAllScripts->setChecked(porymapConfig.loadAllEventScripts); + + auto logTypeEnd = porymapConfig.statusBarLogTypes.end(); + ui->checkBox_StatusErrors->setChecked(porymapConfig.statusBarLogTypes.find(LogType::LOG_ERROR) != logTypeEnd); + ui->checkBox_StatusWarnings->setChecked(porymapConfig.statusBarLogTypes.find(LogType::LOG_WARN) != logTypeEnd); + ui->checkBox_StatusInformation->setChecked(porymapConfig.statusBarLogTypes.find(LogType::LOG_INFO) != logTypeEnd); } void PreferenceEditor::saveFields() { @@ -77,6 +82,12 @@ void PreferenceEditor::saveFields() { porymapConfig.reopenOnLaunch = ui->checkBox_OpenRecentProject->isChecked(); porymapConfig.checkForUpdates = ui->checkBox_CheckForUpdates->isChecked(); porymapConfig.eventDeleteWarningDisabled = ui->checkBox_DisableEventWarning->isChecked(); + + porymapConfig.statusBarLogTypes.clear(); + if (ui->checkBox_StatusErrors->isChecked()) porymapConfig.statusBarLogTypes.insert(LogType::LOG_ERROR); + if (ui->checkBox_StatusWarnings->isChecked()) porymapConfig.statusBarLogTypes.insert(LogType::LOG_WARN); + if (ui->checkBox_StatusInformation->isChecked()) porymapConfig.statusBarLogTypes.insert(LogType::LOG_INFO); + porymapConfig.save(); emit preferencesSaved();