1 // Copyright 2017 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "DolphinQt/Config/GeckoCodeWidget.h"
6
7 #include <QCursor>
8 #include <QFontDatabase>
9 #include <QFormLayout>
10 #include <QHBoxLayout>
11 #include <QLabel>
12 #include <QListWidget>
13 #include <QMenu>
14 #include <QPushButton>
15 #include <QTextEdit>
16 #include <QVBoxLayout>
17
18 #include "Common/FileUtil.h"
19 #include "Common/IniFile.h"
20
21 #include "Core/ConfigManager.h"
22 #include "Core/GeckoCode.h"
23 #include "Core/GeckoCodeConfig.h"
24
25 #include "DolphinQt/Config/CheatCodeEditor.h"
26 #include "DolphinQt/Config/CheatWarningWidget.h"
27 #include "DolphinQt/QtUtils/ModalMessageBox.h"
28
29 #include "UICommon/GameFile.h"
30
GeckoCodeWidget(const UICommon::GameFile & game,bool restart_required)31 GeckoCodeWidget::GeckoCodeWidget(const UICommon::GameFile& game, bool restart_required)
32 : m_game(game), m_game_id(game.GetGameID()), m_gametdb_id(game.GetGameTDBID()),
33 m_game_revision(game.GetRevision()), m_restart_required(restart_required)
34 {
35 CreateWidgets();
36 ConnectWidgets();
37
38 IniFile game_ini_local;
39
40 // We don't use LoadLocalGameIni() here because user cheat codes that are installed via the UI
41 // will always be stored in GS/${GAMEID}.ini
42 game_ini_local.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini");
43
44 const IniFile game_ini_default = SConfig::LoadDefaultGameIni(m_game_id, m_game_revision);
45 m_gecko_codes = Gecko::LoadCodes(game_ini_default, game_ini_local);
46
47 UpdateList();
48 }
49
50 GeckoCodeWidget::~GeckoCodeWidget() = default;
51
CreateWidgets()52 void GeckoCodeWidget::CreateWidgets()
53 {
54 m_warning = new CheatWarningWidget(m_game_id, m_restart_required, this);
55 m_code_list = new QListWidget;
56 m_name_label = new QLabel;
57 m_creator_label = new QLabel;
58
59 m_code_list->setContextMenuPolicy(Qt::CustomContextMenu);
60
61 QFont monospace(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
62
63 const auto line_height = QFontMetrics(font()).lineSpacing();
64
65 m_code_description = new QTextEdit;
66 m_code_description->setFont(monospace);
67 m_code_description->setReadOnly(true);
68 m_code_description->setFixedHeight(line_height * 5);
69
70 m_code_view = new QTextEdit;
71 m_code_view->setFont(monospace);
72 m_code_view->setReadOnly(true);
73 m_code_view->setFixedHeight(line_height * 10);
74
75 m_add_code = new QPushButton(tr("&Add New Code..."));
76 m_edit_code = new QPushButton(tr("&Edit Code..."));
77 m_remove_code = new QPushButton(tr("&Remove Code"));
78 m_download_codes = new QPushButton(tr("Download Codes"));
79
80 m_download_codes->setToolTip(tr("Download Codes from the WiiRD Database"));
81
82 m_download_codes->setEnabled(!m_game_id.empty());
83 m_edit_code->setEnabled(false);
84 m_remove_code->setEnabled(false);
85
86 auto* layout = new QVBoxLayout;
87
88 layout->addWidget(m_warning);
89 layout->addWidget(m_code_list);
90
91 auto* info_layout = new QFormLayout;
92
93 info_layout->addRow(tr("Name:"), m_name_label);
94 info_layout->addRow(tr("Creator:"), m_creator_label);
95 info_layout->addRow(tr("Description:"), static_cast<QWidget*>(nullptr));
96
97 info_layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop);
98
99 for (QLabel* label : {m_name_label, m_creator_label})
100 {
101 label->setTextInteractionFlags(Qt::TextSelectableByMouse);
102 label->setCursor(Qt::IBeamCursor);
103 }
104
105 layout->addLayout(info_layout);
106 layout->addWidget(m_code_description);
107 layout->addWidget(m_code_view);
108
109 QHBoxLayout* btn_layout = new QHBoxLayout;
110
111 btn_layout->addWidget(m_add_code);
112 btn_layout->addWidget(m_edit_code);
113 btn_layout->addWidget(m_remove_code);
114 btn_layout->addWidget(m_download_codes);
115
116 layout->addLayout(btn_layout);
117
118 setLayout(layout);
119 }
120
ConnectWidgets()121 void GeckoCodeWidget::ConnectWidgets()
122 {
123 connect(m_code_list, &QListWidget::itemSelectionChanged, this,
124 &GeckoCodeWidget::OnSelectionChanged);
125 connect(m_code_list, &QListWidget::itemChanged, this, &GeckoCodeWidget::OnItemChanged);
126 connect(m_code_list->model(), &QAbstractItemModel::rowsMoved, this,
127 &GeckoCodeWidget::OnListReordered);
128 connect(m_code_list, &QListWidget::customContextMenuRequested, this,
129 &GeckoCodeWidget::OnContextMenuRequested);
130
131 connect(m_add_code, &QPushButton::clicked, this, &GeckoCodeWidget::AddCode);
132 connect(m_remove_code, &QPushButton::clicked, this, &GeckoCodeWidget::RemoveCode);
133 connect(m_edit_code, &QPushButton::clicked, this, &GeckoCodeWidget::EditCode);
134 connect(m_download_codes, &QPushButton::clicked, this, &GeckoCodeWidget::DownloadCodes);
135 connect(m_warning, &CheatWarningWidget::OpenCheatEnableSettings, this,
136 &GeckoCodeWidget::OpenGeneralSettings);
137 }
138
OnSelectionChanged()139 void GeckoCodeWidget::OnSelectionChanged()
140 {
141 auto items = m_code_list->selectedItems();
142
143 const bool empty = items.empty();
144
145 m_edit_code->setEnabled(!empty);
146 m_remove_code->setEnabled(!empty);
147
148 if (items.empty())
149 return;
150
151 auto selected = items[0];
152
153 const int index = selected->data(Qt::UserRole).toInt();
154
155 const auto& code = m_gecko_codes[index];
156
157 m_name_label->setText(QString::fromStdString(code.name));
158 m_creator_label->setText(QString::fromStdString(code.creator));
159
160 m_code_description->clear();
161
162 for (const auto& line : code.notes)
163 m_code_description->append(QString::fromStdString(line));
164
165 m_code_view->clear();
166
167 for (const auto& c : code.codes)
168 {
169 m_code_view->append(QStringLiteral("%1 %2")
170 .arg(c.address, 8, 16, QLatin1Char('0'))
171 .arg(c.data, 8, 16, QLatin1Char('0')));
172 }
173 }
174
OnItemChanged(QListWidgetItem * item)175 void GeckoCodeWidget::OnItemChanged(QListWidgetItem* item)
176 {
177 const int index = item->data(Qt::UserRole).toInt();
178 m_gecko_codes[index].enabled = (item->checkState() == Qt::Checked);
179
180 if (!m_restart_required)
181 Gecko::SetActiveCodes(m_gecko_codes);
182
183 SaveCodes();
184 }
185
AddCode()186 void GeckoCodeWidget::AddCode()
187 {
188 Gecko::GeckoCode code;
189 code.enabled = true;
190
191 CheatCodeEditor ed(this);
192 ed.SetGeckoCode(&code);
193 if (ed.exec() == QDialog::Rejected)
194 return;
195
196 m_gecko_codes.push_back(std::move(code));
197 SaveCodes();
198 UpdateList();
199 }
200
EditCode()201 void GeckoCodeWidget::EditCode()
202 {
203 const auto* item = m_code_list->currentItem();
204 if (item == nullptr)
205 return;
206
207 const int index = item->data(Qt::UserRole).toInt();
208
209 CheatCodeEditor ed(this);
210 ed.SetGeckoCode(&m_gecko_codes[index]);
211 if (ed.exec() == QDialog::Rejected)
212 return;
213
214 SaveCodes();
215 UpdateList();
216 }
217
RemoveCode()218 void GeckoCodeWidget::RemoveCode()
219 {
220 const auto* item = m_code_list->currentItem();
221
222 if (item == nullptr)
223 return;
224
225 m_gecko_codes.erase(m_gecko_codes.begin() + item->data(Qt::UserRole).toInt());
226
227 UpdateList();
228 SaveCodes();
229 }
230
SaveCodes()231 void GeckoCodeWidget::SaveCodes()
232 {
233 const auto ini_path =
234 std::string(File::GetUserPath(D_GAMESETTINGS_IDX)).append(m_game_id).append(".ini");
235
236 IniFile game_ini_local;
237 game_ini_local.Load(ini_path);
238 Gecko::SaveCodes(game_ini_local, m_gecko_codes);
239 game_ini_local.Save(ini_path);
240 }
241
OnContextMenuRequested()242 void GeckoCodeWidget::OnContextMenuRequested()
243 {
244 QMenu menu;
245
246 menu.addAction(tr("Sort Alphabetically"), this, &GeckoCodeWidget::SortAlphabetically);
247
248 menu.exec(QCursor::pos());
249 }
250
SortAlphabetically()251 void GeckoCodeWidget::SortAlphabetically()
252 {
253 m_code_list->sortItems();
254 OnListReordered();
255 }
256
OnListReordered()257 void GeckoCodeWidget::OnListReordered()
258 {
259 // Reorder codes based on the indices of table item
260 std::vector<Gecko::GeckoCode> codes;
261 codes.reserve(m_gecko_codes.size());
262
263 for (int i = 0; i < m_code_list->count(); i++)
264 {
265 const int index = m_code_list->item(i)->data(Qt::UserRole).toInt();
266
267 codes.push_back(std::move(m_gecko_codes[index]));
268 }
269
270 m_gecko_codes = std::move(codes);
271
272 UpdateList();
273 SaveCodes();
274 }
275
UpdateList()276 void GeckoCodeWidget::UpdateList()
277 {
278 m_code_list->clear();
279
280 for (size_t i = 0; i < m_gecko_codes.size(); i++)
281 {
282 const auto& code = m_gecko_codes[i];
283
284 auto* item = new QListWidgetItem(QString::fromStdString(code.name)
285 .replace(QStringLiteral("<"), QChar::fromLatin1('<'))
286 .replace(QStringLiteral(">"), QChar::fromLatin1('>')));
287
288 item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable |
289 Qt::ItemIsDragEnabled);
290 item->setCheckState(code.enabled ? Qt::Checked : Qt::Unchecked);
291 item->setData(Qt::UserRole, static_cast<int>(i));
292
293 m_code_list->addItem(item);
294 }
295
296 m_code_list->setDragDropMode(QAbstractItemView::InternalMove);
297 }
298
DownloadCodes()299 void GeckoCodeWidget::DownloadCodes()
300 {
301 bool success;
302
303 std::vector<Gecko::GeckoCode> codes = Gecko::DownloadCodes(m_gametdb_id, &success);
304
305 if (!success)
306 {
307 ModalMessageBox::critical(this, tr("Error"), tr("Failed to download codes."));
308 return;
309 }
310
311 if (codes.empty())
312 {
313 ModalMessageBox::critical(this, tr("Error"), tr("File contained no codes."));
314 return;
315 }
316
317 size_t added_count = 0;
318
319 for (const auto& code : codes)
320 {
321 auto it = std::find(m_gecko_codes.begin(), m_gecko_codes.end(), code);
322
323 if (it == m_gecko_codes.end())
324 {
325 m_gecko_codes.push_back(code);
326 added_count++;
327 }
328 }
329
330 UpdateList();
331 SaveCodes();
332
333 ModalMessageBox::information(
334 this, tr("Download complete"),
335 tr("Downloaded %1 codes. (added %2)")
336 .arg(QString::number(codes.size()), QString::number(added_count)));
337 }
338