diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 3d987cae90b4..04100f746596 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -30,6 +30,8 @@ add_executable(dolphin-emu Config/CheatWarningWidget.cpp Config/ControllersWindow.cpp Config/FilesystemWidget.cpp + Config/GameConfigEdit.cpp + Config/GameConfigHighlighter.cpp Config/GameConfigWidget.cpp Config/GeckoCodeWidget.cpp Config/Graphics/AdvancedWidget.cpp diff --git a/Source/Core/DolphinQt/Config/GameConfigEdit.cpp b/Source/Core/DolphinQt/Config/GameConfigEdit.cpp new file mode 100644 index 000000000000..762445e2c90b --- /dev/null +++ b/Source/Core/DolphinQt/Config/GameConfigEdit.cpp @@ -0,0 +1,311 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DolphinQt/Config/GameConfigEdit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DolphinQt/Config/GameConfigHighlighter.h" + +GameConfigEdit::GameConfigEdit(QWidget* parent, const QString& path, bool read_only) + : m_path(path), m_read_only(read_only) +{ + CreateWidgets(); + + LoadFile(); + + new GameConfigHighlighter(m_edit->document()); + + AddDescription(QStringLiteral("Core"), + tr("Section that contains most CPU and Hardware related settings.")); + + AddDescription(QStringLiteral("CPUThread"), tr("Controls whether or not Dual Core should be " + "enabled. Can improve performance but can also " + "cause issues. Defaults to True")); + + AddDescription(QStringLiteral("FastDiscSpeed"), + tr("Shortens loading times but may break some games. Can have negative effects on " + "performance. Defaults to False")); + + AddDescription(QStringLiteral("MMU"), tr("Controls whether or not the Memory Management Unit " + "should be emulated fully. Few games require it.")); + + AddDescription( + QStringLiteral("DSPHLE"), + tr("Controls whether to use high or low-level DSP emulation. Defaults to True")); + + AddDescription( + QStringLiteral("JITFollowBranch"), + tr("Tries to translate branches ahead of time, improving performance in most cases. Defaults " + "to True")); + + AddDescription(QStringLiteral("Gecko"), tr("Section that contains all Gecko cheat codes.")); + + AddDescription(QStringLiteral("ActionReplay"), + tr("Section that contains all Action Replay cheat codes.")); + + AddDescription(QStringLiteral("Video_Settings"), + tr("Section that contains all graphics related settings.")); + + m_completer = new QCompleter(m_edit); + + auto* completion_model = new QStringListModel; + completion_model->setStringList(m_completions); + + m_completer->setModel(completion_model); + m_completer->setModelSorting(QCompleter::UnsortedModel); + m_completer->setCompletionMode(QCompleter::PopupCompletion); + m_completer->setWidget(m_edit); + + AddMenubarOptions(); + ConnectWidgets(); +} + +void GameConfigEdit::CreateWidgets() +{ + m_menu = new QMenu; + + m_edit = new QTextEdit; + m_edit->setReadOnly(m_read_only); + m_edit->setAcceptRichText(false); + + auto* layout = new QVBoxLayout; + + auto* menu_button = new QToolButton; + + menu_button->setText(tr("Presets") + QStringLiteral(" ")); + menu_button->setMenu(m_menu); + + connect(menu_button, &QToolButton::pressed, [menu_button] { menu_button->showMenu(); }); + + layout->addWidget(menu_button); + layout->addWidget(m_edit); + + setLayout(layout); +} + +void GameConfigEdit::AddDescription(const QString& keyword, const QString& description) +{ + m_keyword_map[keyword] = description; + m_completions << keyword; +} + +void GameConfigEdit::LoadFile() +{ + QFile file(m_path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return; + + m_edit->setPlainText(QString::fromStdString(file.readAll().toStdString())); +} + +void GameConfigEdit::SaveFile() +{ + if (!isVisible() || m_read_only) + return; + + QFile file(m_path); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) + return; + + const QByteArray contents = m_edit->toPlainText().toUtf8(); + + if (!file.write(contents)) + QMessageBox::warning(this, tr("Warning"), tr("Failed to write config file!")); +} + +void GameConfigEdit::ConnectWidgets() +{ + connect(m_edit, &QTextEdit::textChanged, this, &GameConfigEdit::SaveFile); + connect(m_edit, &QTextEdit::selectionChanged, this, &GameConfigEdit::OnSelectionChanged); + connect(m_completer, static_cast(&QCompleter::activated), + this, &GameConfigEdit::OnAutoComplete); +} + +void GameConfigEdit::OnSelectionChanged() +{ + const QString& keyword = m_edit->textCursor().selectedText(); + + if (m_keyword_map.count(keyword)) + QWhatsThis::showText(QCursor::pos(), m_keyword_map[keyword], this); +} + +void GameConfigEdit::AddBoolOption(QMenu* menu, const QString& name, const QString& section, + const QString& key) +{ + auto* option = menu->addMenu(name); + + option->addAction(tr("On"), this, + [this, section, key] { SetOption(section, key, QStringLiteral("True")); }); + option->addAction(tr("Off"), this, + [this, section, key] { SetOption(section, key, QStringLiteral("False")); }); +} + +void GameConfigEdit::SetOption(const QString& section, const QString& key, const QString& value) +{ + auto section_cursor = + m_edit->document()->find(QRegExp(QStringLiteral("^\\[%1\\]").arg(section)), 0); + + // Check if the section this belongs in can be found + if (section_cursor.isNull()) + { + m_edit->append(QStringLiteral("[%1]\n\n%2 = %3\n").arg(section).arg(key).arg(value)); + } + else + { + auto value_cursor = + m_edit->document()->find(QRegExp(QStringLiteral("^%1 = .*").arg(key)), section_cursor); + + const QString new_line = QStringLiteral("%1 = %2").arg(key).arg(value); + + // Check if the value that has to be set already exists + if (value_cursor.isNull()) + { + section_cursor.clearSelection(); + section_cursor.insertText(QStringLiteral("\n") + new_line); + } + else + { + value_cursor.insertText(new_line); + } + } +} + +QString GameConfigEdit::GetTextUnderCursor() +{ + QTextCursor tc = m_edit->textCursor(); + tc.select(QTextCursor::WordUnderCursor); + return tc.selectedText(); +} + +void GameConfigEdit::AddMenubarOptions() +{ + auto* editor = m_menu->addMenu(tr("Editor")); + + editor->addAction(tr("Refresh"), this, &GameConfigEdit::LoadFile); + editor->addAction(tr("Open in External Editor"), this, &GameConfigEdit::OpenExternalEditor); + + if (!m_read_only) + { + m_menu->addSeparator(); + auto* core_menubar = m_menu->addMenu(tr("Core")); + + AddBoolOption(core_menubar, tr("Dual Core"), QStringLiteral("Core"), + QStringLiteral("CPUThread")); + AddBoolOption(core_menubar, tr("MMU"), QStringLiteral("Core"), QStringLiteral("MMU")); + + auto* video_menubar = m_menu->addMenu(tr("Video")); + + AddBoolOption(video_menubar, tr("Store EFB Copies to Texture Only"), + QStringLiteral("Video_Settings"), QStringLiteral("EFBToTextureEnable")); + + AddBoolOption(video_menubar, tr("Store XFB Copies to Texture Only"), + QStringLiteral("Video_Settings"), QStringLiteral("XFBToTextureEnable")); + + { + auto* texture_cache = video_menubar->addMenu(tr("Texture Cache")); + texture_cache->addAction(tr("Safe"), this, [this] { + SetOption(QStringLiteral("Video_Settings"), QStringLiteral("SafeTextureCacheColorSamples"), + QStringLiteral("0")); + }); + texture_cache->addAction(tr("Medium"), this, [this] { + SetOption(QStringLiteral("Video_Settings"), QStringLiteral("SafeTextureCacheColorSamples"), + QStringLiteral("512")); + }); + texture_cache->addAction(tr("Fast"), this, [this] { + SetOption(QStringLiteral("Video_Settings"), QStringLiteral("SafeTextureCacheColorSamples"), + QStringLiteral("128")); + }); + } + } +} + +void GameConfigEdit::OnAutoComplete(const QString& completion) +{ + QTextCursor cursor = m_edit->textCursor(); + int extra = completion.length() - m_completer->completionPrefix().length(); + cursor.movePosition(QTextCursor::Left); + cursor.movePosition(QTextCursor::EndOfWord); + cursor.insertText(completion.right(extra)); + m_edit->setTextCursor(cursor); +} + +void GameConfigEdit::OpenExternalEditor() +{ + QFile file(m_path); + + if (!file.exists()) + { + if (m_read_only) + return; + + file.open(QIODevice::WriteOnly); + file.close(); + } + + QDesktopServices::openUrl(QUrl::fromLocalFile(m_path)); +} + +void GameConfigEdit::keyPressEvent(QKeyEvent* e) +{ + if (m_completer->popup()->isVisible()) + { + // The following keys are forwarded by the completer to the widget + switch (e->key()) + { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Escape: + case Qt::Key_Tab: + case Qt::Key_Backtab: + e->ignore(); + return; // let the completer do default behavior + default: + break; + } + } + + QWidget::keyPressEvent(e); + + const static QString end_of_word = QStringLiteral("~!@#$%^&*()_+{}|:\"<>?,./;'\\-="); + + QString completion_prefix = GetTextUnderCursor(); + + if (e->text().isEmpty() || completion_prefix.length() < 2 || + end_of_word.contains(e->text().right(1))) + { + m_completer->popup()->hide(); + return; + } + + if (completion_prefix != m_completer->completionPrefix()) + { + m_completer->setCompletionPrefix(completion_prefix); + m_completer->popup()->setCurrentIndex(m_completer->completionModel()->index(0, 0)); + } + QRect cr = m_edit->cursorRect(); + cr.setWidth(m_completer->popup()->sizeHintForColumn(0) + + m_completer->popup()->verticalScrollBar()->sizeHint().width()); + m_completer->complete(cr); // popup it up! +} + +void GameConfigEdit::focusInEvent(QFocusEvent* e) +{ + m_completer->setWidget(m_edit); + QWidget::focusInEvent(e); +} diff --git a/Source/Core/DolphinQt/Config/GameConfigEdit.h b/Source/Core/DolphinQt/Config/GameConfigEdit.h new file mode 100644 index 000000000000..7eeca57b2da1 --- /dev/null +++ b/Source/Core/DolphinQt/Config/GameConfigEdit.h @@ -0,0 +1,57 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +class QCompleter; +class QMenu; +class QTextEdit; + +class GameConfigEdit : public QWidget +{ +public: + explicit GameConfigEdit(QWidget* parent, const QString& path, bool read_only); + +protected: + void keyPressEvent(QKeyEvent* e) override; + void focusInEvent(QFocusEvent* e) override; + +private: + void CreateWidgets(); + void ConnectWidgets(); + void AddMenubarOptions(); + + void LoadFile(); + void SaveFile(); + + void OnSelectionChanged(); + void OnAutoComplete(const QString& completion); + void OpenExternalEditor(); + + void SetReadOnly(bool read_only); + + QString GetTextUnderCursor(); + + void AddBoolOption(QMenu* menu, const QString& name, const QString& section, const QString& key); + + void SetOption(const QString& section, const QString& key, const QString& value); + + void AddDescription(const QString& keyword, const QString& description); + + QCompleter* m_completer; + QStringList m_completions; + QMenu* m_menu; + QTextEdit* m_edit; + + const QString m_path; + + bool m_read_only; + + QMap m_keyword_map; +}; diff --git a/Source/Core/DolphinQt/Config/GameConfigHighlighter.cpp b/Source/Core/DolphinQt/Config/GameConfigHighlighter.cpp new file mode 100644 index 000000000000..d9d2bb55bdf0 --- /dev/null +++ b/Source/Core/DolphinQt/Config/GameConfigHighlighter.cpp @@ -0,0 +1,63 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DolphinQt/Config/GameConfigHighlighter.h" + +struct HighlightingRule +{ + QRegularExpression pattern; + QTextCharFormat format; +}; + +GameConfigHighlighter::~GameConfigHighlighter() = default; + +GameConfigHighlighter::GameConfigHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) +{ + QTextCharFormat equal_format; + equal_format.setForeground(Qt::red); + + QTextCharFormat section_format; + section_format.setFontWeight(QFont::Bold); + + QTextCharFormat comment_format; + comment_format.setForeground(Qt::darkGreen); + comment_format.setFontItalic(true); + + QTextCharFormat const_format; + const_format.setFontWeight(QFont::Bold); + const_format.setForeground(Qt::blue); + + QTextCharFormat num_format; + num_format.setForeground(Qt::darkBlue); + + m_rules.emplace_back(HighlightingRule{QRegularExpression(QStringLiteral("=")), equal_format}); + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("^\\[.*?\\]")), section_format}); + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("\\bTrue\\b")), const_format}); + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("\\bFalse\\b")), const_format}); + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("\\b[0-9a-fA-F]+\\b")), num_format}); + + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("^#.*")), comment_format}); + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("^\\$.*")), comment_format}); + m_rules.emplace_back( + HighlightingRule{QRegularExpression(QStringLiteral("^\\*.*")), comment_format}); +} + +void GameConfigHighlighter::highlightBlock(const QString& text) +{ + for (const auto& rule : m_rules) + { + auto it = rule.pattern.globalMatch(text); + while (it.hasNext()) + { + auto match = it.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } +} diff --git a/Source/Core/DolphinQt/Config/GameConfigHighlighter.h b/Source/Core/DolphinQt/Config/GameConfigHighlighter.h new file mode 100644 index 000000000000..4b20291e932c --- /dev/null +++ b/Source/Core/DolphinQt/Config/GameConfigHighlighter.h @@ -0,0 +1,28 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +#include +#include +#include + +struct HighlightingRule; + +class GameConfigHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT + +public: + explicit GameConfigHighlighter(QTextDocument* parent = nullptr); + ~GameConfigHighlighter(); + +protected: + void highlightBlock(const QString& text) override; + +private: + std::vector m_rules; +}; diff --git a/Source/Core/DolphinQt/Config/GameConfigWidget.cpp b/Source/Core/DolphinQt/Config/GameConfigWidget.cpp index c626515ce0b4..c11ca30ebdd5 100644 --- a/Source/Core/DolphinQt/Config/GameConfigWidget.cpp +++ b/Source/Core/DolphinQt/Config/GameConfigWidget.cpp @@ -6,16 +6,12 @@ #include #include -#include -#include -#include #include -#include #include -#include #include +#include #include -#include +#include #include #include "Common/CommonPaths.h" @@ -24,6 +20,7 @@ #include "Core/ConfigLoaders/GameConfigLoader.h" #include "Core/ConfigManager.h" +#include "DolphinQt/Config/GameConfigEdit.h" #include "DolphinQt/Config/Graphics/GraphicsSlider.h" #include "UICommon/GameFile.h" @@ -38,22 +35,51 @@ constexpr const char* DETERMINISM_AUTO_STRING = "auto"; constexpr const char* DETERMINISM_NONE_STRING = "none"; constexpr const char* DETERMINISM_FAKE_COMPLETION_STRING = "fake-completion"; +static void PopulateTab(QTabWidget* tab, const std::string& path, std::string game_id, + bool read_only) +{ + while (!game_id.empty()) + { + const std::string ini_path = path + game_id + ".ini"; + if (File::Exists(ini_path)) + { + auto* edit = + new GameConfigEdit(nullptr, QString::fromStdString(path + game_id + ".ini"), read_only); + tab->addTab(edit, QString::fromStdString(game_id)); + } + + game_id = game_id.substr(0, game_id.size() - 1); + } +} + GameConfigWidget::GameConfigWidget(const UICommon::GameFile& game) : m_game(game) { m_game_id = m_game.GetGameID(); + m_gameini_local_path = QString::fromStdString(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini"); CreateWidgets(); LoadSettings(); ConnectWidgets(); + + PopulateTab(m_default_tab, File::GetSysDirectory() + "GameSettings/", m_game_id, true); + PopulateTab(m_local_tab, File::GetUserPath(D_GAMESETTINGS_IDX), m_game_id, false); + + // Always give the user the opportunity to create a new INI + if (m_local_tab->count() == 0) + { + auto* edit = new GameConfigEdit( + nullptr, QString::fromStdString(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini"), + false); + m_local_tab->addTab(edit, QString::fromStdString(m_game_id)); + } } void GameConfigWidget::CreateWidgets() { + // General m_refresh_config = new QPushButton(tr("Refresh")); - m_edit_user_config = new QPushButton(tr("Edit User Config")); - m_view_default_config = new QPushButton(tr("View Default Config")); // Core auto* core_box = new QGroupBox(tr("Core")); @@ -128,23 +154,51 @@ void GameConfigWidget::CreateWidgets() settings_layout->addWidget(core_box); settings_layout->addWidget(stereoscopy_box); - auto* layout = new QGridLayout; - - layout->addWidget(settings_box, 0, 0, 1, -1); - - auto* button_layout = new QHBoxLayout; - button_layout->setMargin(0); + auto* general_layout = new QGridLayout; - layout->addLayout(button_layout, 1, 0, 1, -1); + general_layout->addWidget(settings_box, 0, 0, 1, -1); - button_layout->addWidget(m_refresh_config); - button_layout->addWidget(m_edit_user_config); - button_layout->addWidget(m_view_default_config); + general_layout->addWidget(m_refresh_config, 1, 0, 1, -1); for (QCheckBox* item : {m_enable_dual_core, m_enable_mmu, m_enable_fprf, m_sync_gpu, m_enable_fast_disc, m_use_dsp_hle, m_use_monoscopic_shadows}) item->setTristate(true); + auto* general_widget = new QWidget; + general_widget->setLayout(general_layout); + + // Advanced + auto* advanced_layout = new QVBoxLayout; + + auto* default_group = new QGroupBox(tr("Default Config (Read Only)")); + auto* default_layout = new QVBoxLayout; + m_default_tab = new QTabWidget; + + default_group->setLayout(default_layout); + default_layout->addWidget(m_default_tab); + + auto* local_group = new QGroupBox(tr("User Config")); + auto* local_layout = new QVBoxLayout; + m_local_tab = new QTabWidget; + + local_group->setLayout(local_layout); + local_layout->addWidget(m_local_tab); + + advanced_layout->addWidget(default_group); + advanced_layout->addWidget(local_group); + + auto* advanced_widget = new QWidget; + + advanced_widget->setLayout(advanced_layout); + + auto* layout = new QVBoxLayout; + auto* tab_widget = new QTabWidget; + + tab_widget->addTab(general_widget, tr("General")); + tab_widget->addTab(advanced_widget, tr("Editor")); + + layout->addWidget(tab_widget); + setLayout(layout); } @@ -152,8 +206,6 @@ void GameConfigWidget::ConnectWidgets() { // Buttons connect(m_refresh_config, &QPushButton::pressed, this, &GameConfigWidget::LoadSettings); - connect(m_edit_user_config, &QPushButton::pressed, this, &GameConfigWidget::EditUserConfig); - connect(m_view_default_config, &QPushButton::pressed, this, &GameConfigWidget::ViewDefaultConfig); for (QCheckBox* box : {m_enable_dual_core, m_enable_mmu, m_enable_fprf, m_sync_gpu, m_enable_fast_disc, m_use_dsp_hle, m_use_monoscopic_shadows}) @@ -347,29 +399,3 @@ void GameConfigWidget::SaveSettings() if (success && File::GetSize(m_gameini_local_path.toStdString()) == 0) File::Delete(m_gameini_local_path.toStdString()); } - -void GameConfigWidget::EditUserConfig() -{ - QFile file(m_gameini_local_path); - - if (!file.exists()) - { - file.open(QIODevice::WriteOnly); - file.close(); - } - - QDesktopServices::openUrl(QUrl::fromLocalFile(m_gameini_local_path)); -} - -void GameConfigWidget::ViewDefaultConfig() -{ - for (const std::string& filename : - ConfigLoaders::GetGameIniFilenames(m_game_id, m_game.GetRevision())) - { - QString path = - QString::fromStdString(File::GetSysDirectory() + GAMESETTINGS_DIR DIR_SEP + filename); - - if (QFile(path).exists()) - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); - } -} diff --git a/Source/Core/DolphinQt/Config/GameConfigWidget.h b/Source/Core/DolphinQt/Config/GameConfigWidget.h index bb1c58d8030f..a8fba742cc0a 100644 --- a/Source/Core/DolphinQt/Config/GameConfigWidget.h +++ b/Source/Core/DolphinQt/Config/GameConfigWidget.h @@ -18,12 +18,10 @@ class GameFile; class QCheckBox; class QComboBox; -class QGroupBox; -class QLineEdit; class QPushButton; class QSlider; class QSpinBox; -class QVBoxLayout; +class QTabWidget; class GameConfigWidget : public QWidget { @@ -34,38 +32,40 @@ class GameConfigWidget : public QWidget private: void CreateWidgets(); void ConnectWidgets(); + void LoadSettings(); void SaveSettings(); void EditUserConfig(); - void ViewDefaultConfig(); - void LoadCheckBox(QCheckBox* checkbox, const std::string& section, const std::string& key); void SaveCheckBox(QCheckBox* checkbox, const std::string& section, const std::string& key); + void LoadCheckBox(QCheckBox* checkbox, const std::string& section, const std::string& key); - QPushButton* m_refresh_config; - QPushButton* m_edit_user_config; - QPushButton* m_view_default_config; + QString m_gameini_sys_path; + QString m_gameini_local_path; + + QTabWidget* m_default_tab; + QTabWidget* m_local_tab; - // Core QCheckBox* m_enable_dual_core; QCheckBox* m_enable_mmu; QCheckBox* m_enable_fprf; QCheckBox* m_sync_gpu; QCheckBox* m_enable_fast_disc; QCheckBox* m_use_dsp_hle; + QCheckBox* m_use_monoscopic_shadows; + + QPushButton* m_refresh_config; + QComboBox* m_deterministic_dual_core; - // Stereoscopy QSlider* m_depth_slider; + QSpinBox* m_convergence_spin; - QCheckBox* m_use_monoscopic_shadows; - QString m_gameini_local_path; + const UICommon::GameFile& m_game; + std::string m_game_id; IniFile m_gameini_local; IniFile m_gameini_default; - - const UICommon::GameFile& m_game; - std::string m_game_id; }; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 694f7b9f13e2..0dde566e5aa6 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -63,6 +63,8 @@ + + @@ -194,6 +196,8 @@ + + @@ -275,6 +279,8 @@ + +