1 // Copyright 2018 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4 
5 #include "DolphinQt/Config/GameConfigEdit.h"
6 
7 #include <QAbstractItemView>
8 #include <QCompleter>
9 #include <QDesktopServices>
10 #include <QFile>
11 #include <QMenu>
12 #include <QMenuBar>
13 #include <QPushButton>
14 #include <QScrollBar>
15 #include <QStringListModel>
16 #include <QTextCursor>
17 #include <QTextEdit>
18 #include <QVBoxLayout>
19 #include <QWhatsThis>
20 
21 #include "DolphinQt/Config/GameConfigHighlighter.h"
22 #include "DolphinQt/QtUtils/ModalMessageBox.h"
23 
GameConfigEdit(QWidget * parent,QString path,bool read_only)24 GameConfigEdit::GameConfigEdit(QWidget* parent, QString path, bool read_only)
25     : QWidget{parent}, m_path(std::move(path)), m_read_only(read_only)
26 {
27   CreateWidgets();
28 
29   LoadFile();
30 
31   new GameConfigHighlighter(m_edit->document());
32 
33   AddDescription(QStringLiteral("Core"),
34                  tr("Section that contains most CPU and Hardware related settings."));
35 
36   AddDescription(QStringLiteral("CPUThread"), tr("Controls whether or not Dual Core should be "
37                                                  "enabled. Can improve performance but can also "
38                                                  "cause issues. Defaults to <b>True</b>"));
39 
40   AddDescription(QStringLiteral("FastDiscSpeed"),
41                  tr("Shortens loading times but may break some games. Can have negative effects on "
42                     "performance. Defaults to <b>False</b>"));
43 
44   AddDescription(QStringLiteral("MMU"), tr("Controls whether or not the Memory Management Unit "
45                                            "should be emulated fully. Few games require it."));
46 
47   AddDescription(
48       QStringLiteral("DSPHLE"),
49       tr("Controls whether to use high or low-level DSP emulation. Defaults to <b>True</b>"));
50 
51   AddDescription(
52       QStringLiteral("JITFollowBranch"),
53       tr("Tries to translate branches ahead of time, improving performance in most cases. Defaults "
54          "to <b>True</b>"));
55 
56   AddDescription(QStringLiteral("Gecko"), tr("Section that contains all Gecko cheat codes."));
57 
58   AddDescription(QStringLiteral("ActionReplay"),
59                  tr("Section that contains all Action Replay cheat codes."));
60 
61   AddDescription(QStringLiteral("Video_Settings"),
62                  tr("Section that contains all graphics related settings."));
63 
64   m_completer = new QCompleter(m_edit);
65 
66   auto* completion_model = new QStringListModel(m_completer);
67   completion_model->setStringList(m_completions);
68 
69   m_completer->setModel(completion_model);
70   m_completer->setModelSorting(QCompleter::UnsortedModel);
71   m_completer->setCompletionMode(QCompleter::PopupCompletion);
72   m_completer->setWidget(m_edit);
73 
74   AddMenubarOptions();
75   ConnectWidgets();
76 }
77 
CreateWidgets()78 void GameConfigEdit::CreateWidgets()
79 {
80   m_edit = new QTextEdit;
81   m_edit->setReadOnly(m_read_only);
82   m_edit->setAcceptRichText(false);
83 
84   auto* layout = new QVBoxLayout;
85 
86   auto* menu_button = new QPushButton;
87 
88   menu_button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
89   menu_button->setText(tr("Presets"));
90 
91   m_menu = new QMenu(menu_button);
92   menu_button->setMenu(m_menu);
93 
94   layout->addWidget(menu_button);
95   layout->addWidget(m_edit);
96 
97   setLayout(layout);
98 }
99 
AddDescription(const QString & keyword,const QString & description)100 void GameConfigEdit::AddDescription(const QString& keyword, const QString& description)
101 {
102   m_keyword_map[keyword] = description;
103   m_completions << keyword;
104 }
105 
LoadFile()106 void GameConfigEdit::LoadFile()
107 {
108   QFile file(m_path);
109   if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
110     return;
111 
112   m_edit->setPlainText(QString::fromStdString(file.readAll().toStdString()));
113 }
114 
SaveFile()115 void GameConfigEdit::SaveFile()
116 {
117   if (!isVisible() || m_read_only)
118     return;
119 
120   QFile file(m_path);
121 
122   if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
123   {
124     ModalMessageBox::warning(this, tr("Warning"), tr("Failed to open config file!"));
125     return;
126   }
127 
128   const QByteArray contents = m_edit->toPlainText().toUtf8();
129 
130   if (file.write(contents) == -1)
131     ModalMessageBox::warning(this, tr("Warning"), tr("Failed to write config file!"));
132 }
133 
ConnectWidgets()134 void GameConfigEdit::ConnectWidgets()
135 {
136   connect(m_edit, &QTextEdit::textChanged, this, &GameConfigEdit::SaveFile);
137   connect(m_edit, &QTextEdit::selectionChanged, this, &GameConfigEdit::OnSelectionChanged);
138   connect(m_completer, qOverload<const QString&>(&QCompleter::activated), this,
139           &GameConfigEdit::OnAutoComplete);
140 }
141 
OnSelectionChanged()142 void GameConfigEdit::OnSelectionChanged()
143 {
144   const QString& keyword = m_edit->textCursor().selectedText();
145 
146   if (m_keyword_map.count(keyword))
147     QWhatsThis::showText(QCursor::pos(), m_keyword_map[keyword], this);
148 }
149 
AddBoolOption(QMenu * menu,const QString & name,const QString & section,const QString & key)150 void GameConfigEdit::AddBoolOption(QMenu* menu, const QString& name, const QString& section,
151                                    const QString& key)
152 {
153   auto* option = menu->addMenu(name);
154 
155   option->addAction(tr("On"), this,
156                     [this, section, key] { SetOption(section, key, QStringLiteral("True")); });
157   option->addAction(tr("Off"), this,
158                     [this, section, key] { SetOption(section, key, QStringLiteral("False")); });
159 }
160 
SetOption(const QString & section,const QString & key,const QString & value)161 void GameConfigEdit::SetOption(const QString& section, const QString& key, const QString& value)
162 {
163   auto section_cursor =
164       m_edit->document()->find(QRegExp(QStringLiteral("^\\[%1\\]").arg(section)), 0);
165 
166   // Check if the section this belongs in can be found
167   if (section_cursor.isNull())
168   {
169     m_edit->append(QStringLiteral("[%1]\n\n%2 = %3\n").arg(section).arg(key).arg(value));
170   }
171   else
172   {
173     auto value_cursor =
174         m_edit->document()->find(QRegExp(QStringLiteral("^%1 = .*").arg(key)), section_cursor);
175 
176     const QString new_line = QStringLiteral("%1 = %2").arg(key).arg(value);
177 
178     // Check if the value that has to be set already exists
179     if (value_cursor.isNull())
180     {
181       section_cursor.clearSelection();
182       section_cursor.insertText(QLatin1Char{'\n'} + new_line);
183     }
184     else
185     {
186       value_cursor.insertText(new_line);
187     }
188   }
189 }
190 
GetTextUnderCursor()191 QString GameConfigEdit::GetTextUnderCursor()
192 {
193   QTextCursor tc = m_edit->textCursor();
194   tc.select(QTextCursor::WordUnderCursor);
195   return tc.selectedText();
196 }
197 
AddMenubarOptions()198 void GameConfigEdit::AddMenubarOptions()
199 {
200   auto* editor = m_menu->addMenu(tr("Editor"));
201 
202   editor->addAction(tr("Refresh"), this, &GameConfigEdit::LoadFile);
203   editor->addAction(tr("Open in External Editor"), this, &GameConfigEdit::OpenExternalEditor);
204 
205   if (!m_read_only)
206   {
207     m_menu->addSeparator();
208     auto* core_menubar = m_menu->addMenu(tr("Core"));
209 
210     AddBoolOption(core_menubar, tr("Dual Core"), QStringLiteral("Core"),
211                   QStringLiteral("CPUThread"));
212     AddBoolOption(core_menubar, tr("MMU"), QStringLiteral("Core"), QStringLiteral("MMU"));
213 
214     auto* video_menubar = m_menu->addMenu(tr("Video"));
215 
216     AddBoolOption(video_menubar, tr("Store EFB Copies to Texture Only"),
217                   QStringLiteral("Video_Hacks"), QStringLiteral("EFBToTextureEnable"));
218 
219     AddBoolOption(video_menubar, tr("Store XFB Copies to Texture Only"),
220                   QStringLiteral("Video_Hacks"), QStringLiteral("XFBToTextureEnable"));
221 
222     {
223       auto* texture_cache = video_menubar->addMenu(tr("Texture Cache"));
224       texture_cache->addAction(tr("Safe"), this, [this] {
225         SetOption(QStringLiteral("Video_Settings"), QStringLiteral("SafeTextureCacheColorSamples"),
226                   QStringLiteral("0"));
227       });
228       texture_cache->addAction(tr("Medium"), this, [this] {
229         SetOption(QStringLiteral("Video_Settings"), QStringLiteral("SafeTextureCacheColorSamples"),
230                   QStringLiteral("512"));
231       });
232       texture_cache->addAction(tr("Fast"), this, [this] {
233         SetOption(QStringLiteral("Video_Settings"), QStringLiteral("SafeTextureCacheColorSamples"),
234                   QStringLiteral("128"));
235       });
236     }
237   }
238 }
239 
OnAutoComplete(const QString & completion)240 void GameConfigEdit::OnAutoComplete(const QString& completion)
241 {
242   QTextCursor cursor = m_edit->textCursor();
243   int extra = completion.length() - m_completer->completionPrefix().length();
244   cursor.movePosition(QTextCursor::Left);
245   cursor.movePosition(QTextCursor::EndOfWord);
246   cursor.insertText(completion.right(extra));
247   m_edit->setTextCursor(cursor);
248 }
249 
OpenExternalEditor()250 void GameConfigEdit::OpenExternalEditor()
251 {
252   QFile file(m_path);
253 
254   if (!file.exists())
255   {
256     if (m_read_only)
257       return;
258 
259     file.open(QIODevice::WriteOnly);
260     file.close();
261   }
262 
263   if (!QDesktopServices::openUrl(QUrl::fromLocalFile(m_path)))
264   {
265     ModalMessageBox::warning(this, tr("Error"),
266                              tr("Failed to open file in external editor.\nMake sure there's an "
267                                 "application assigned to open INI files."));
268   }
269 }
270 
keyPressEvent(QKeyEvent * e)271 void GameConfigEdit::keyPressEvent(QKeyEvent* e)
272 {
273   if (m_completer->popup()->isVisible())
274   {
275     // The following keys are forwarded by the completer to the widget
276     switch (e->key())
277     {
278     case Qt::Key_Enter:
279     case Qt::Key_Return:
280     case Qt::Key_Escape:
281     case Qt::Key_Tab:
282     case Qt::Key_Backtab:
283       e->ignore();
284       return;  // let the completer do default behavior
285     default:
286       break;
287     }
288   }
289 
290   QWidget::keyPressEvent(e);
291 
292   const static QString end_of_word = QStringLiteral("~!@#$%^&*()_+{}|:\"<>?,./;'\\-=");
293 
294   QString completion_prefix = GetTextUnderCursor();
295 
296   if (e->text().isEmpty() || completion_prefix.length() < 2 ||
297       end_of_word.contains(e->text().right(1)))
298   {
299     m_completer->popup()->hide();
300     return;
301   }
302 
303   if (completion_prefix != m_completer->completionPrefix())
304   {
305     m_completer->setCompletionPrefix(completion_prefix);
306     m_completer->popup()->setCurrentIndex(m_completer->completionModel()->index(0, 0));
307   }
308   QRect cr = m_edit->cursorRect();
309   cr.setWidth(m_completer->popup()->sizeHintForColumn(0) +
310               m_completer->popup()->verticalScrollBar()->sizeHint().width());
311   m_completer->complete(cr);  // popup it up!
312 }
313 
focusInEvent(QFocusEvent * e)314 void GameConfigEdit::focusInEvent(QFocusEvent* e)
315 {
316   m_completer->setWidget(m_edit);
317   QWidget::focusInEvent(e);
318 }
319