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