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("&lt;"), QChar::fromLatin1('<'))
286                                          .replace(QStringLiteral("&gt;"), 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