1 // Copyright 2017 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "DolphinQt/Debugger/WatchWidget.h"
6
7 #include <QHeaderView>
8 #include <QMenu>
9 #include <QTableWidget>
10 #include <QToolBar>
11 #include <QVBoxLayout>
12
13 #include "Common/FileUtil.h"
14 #include "Common/IniFile.h"
15 #include "Core/ConfigManager.h"
16 #include "Core/Core.h"
17 #include "Core/PowerPC/MMU.h"
18 #include "Core/PowerPC/PowerPC.h"
19
20 #include "DolphinQt/Host.h"
21 #include "DolphinQt/QtUtils/ModalMessageBox.h"
22 #include "DolphinQt/Resources.h"
23 #include "DolphinQt/Settings.h"
24
WatchWidget(QWidget * parent)25 WatchWidget::WatchWidget(QWidget* parent) : QDockWidget(parent)
26 {
27 // i18n: This kind of "watch" is used for watching emulated memory.
28 // It's not related to timekeeping devices.
29 setWindowTitle(tr("Watch"));
30 setObjectName(QStringLiteral("watch"));
31
32 setHidden(!Settings::Instance().IsWatchVisible() || !Settings::Instance().IsDebugModeEnabled());
33
34 setAllowedAreas(Qt::AllDockWidgetAreas);
35
36 CreateWidgets();
37
38 auto& settings = Settings::GetQSettings();
39
40 restoreGeometry(settings.value(QStringLiteral("watchwidget/geometry")).toByteArray());
41 // macOS: setHidden() needs to be evaluated before setFloating() for proper window presentation
42 // according to Settings
43 setFloating(settings.value(QStringLiteral("watchwidget/floating")).toBool());
44
45 ConnectWidgets();
46
47 connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
48 UpdateButtonsEnabled();
49 if (state != Core::State::Starting)
50 Update();
51 });
52
53 connect(Host::GetInstance(), &Host::UpdateDisasmDialog, this, &WatchWidget::Update);
54
55 connect(&Settings::Instance(), &Settings::WatchVisibilityChanged, this,
56 [this](bool visible) { setHidden(!visible); });
57
58 connect(&Settings::Instance(), &Settings::DebugModeToggled, this,
59 [this](bool enabled) { setHidden(!enabled || !Settings::Instance().IsWatchVisible()); });
60
61 connect(&Settings::Instance(), &Settings::ThemeChanged, this, &WatchWidget::UpdateIcons);
62 UpdateIcons();
63 }
64
~WatchWidget()65 WatchWidget::~WatchWidget()
66 {
67 auto& settings = Settings::GetQSettings();
68
69 settings.setValue(QStringLiteral("watchwidget/geometry"), saveGeometry());
70 settings.setValue(QStringLiteral("watchwidget/floating"), isFloating());
71 }
72
CreateWidgets()73 void WatchWidget::CreateWidgets()
74 {
75 m_toolbar = new QToolBar;
76 m_toolbar->setContentsMargins(0, 0, 0, 0);
77 m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
78
79 m_table = new QTableWidget;
80 m_table->setTabKeyNavigation(false);
81
82 m_table->setContentsMargins(0, 0, 0, 0);
83 m_table->setColumnCount(NUM_COLUMNS);
84 m_table->verticalHeader()->setHidden(true);
85 m_table->setContextMenuPolicy(Qt::CustomContextMenu);
86 m_table->setSelectionMode(QAbstractItemView::SingleSelection);
87
88 m_load = m_toolbar->addAction(tr("Load"), this, &WatchWidget::OnLoad);
89 m_save = m_toolbar->addAction(tr("Save"), this, &WatchWidget::OnSave);
90
91 m_load->setEnabled(false);
92 m_save->setEnabled(false);
93
94 auto* layout = new QVBoxLayout;
95 layout->setContentsMargins(2, 2, 2, 2);
96 layout->setSpacing(0);
97 layout->addWidget(m_toolbar);
98 layout->addWidget(m_table);
99
100 QWidget* widget = new QWidget;
101 widget->setLayout(layout);
102
103 setWidget(widget);
104 }
105
ConnectWidgets()106 void WatchWidget::ConnectWidgets()
107 {
108 connect(m_table, &QTableWidget::customContextMenuRequested, this, &WatchWidget::ShowContextMenu);
109 connect(m_table, &QTableWidget::itemChanged, this, &WatchWidget::OnItemChanged);
110 }
111
UpdateIcons()112 void WatchWidget::UpdateIcons()
113 {
114 m_load->setIcon(Resources::GetScaledThemeIcon("debugger_load"));
115 m_save->setIcon(Resources::GetScaledThemeIcon("debugger_save"));
116 }
117
UpdateButtonsEnabled()118 void WatchWidget::UpdateButtonsEnabled()
119 {
120 if (!isVisible())
121 return;
122
123 m_load->setEnabled(Core::IsRunning());
124 m_save->setEnabled(Core::IsRunning());
125 }
126
Update()127 void WatchWidget::Update()
128 {
129 if (!isVisible())
130 return;
131
132 m_updating = true;
133
134 m_table->clear();
135
136 int size = static_cast<int>(PowerPC::debug_interface.GetWatches().size());
137
138 m_table->setRowCount(size + 1);
139
140 m_table->setHorizontalHeaderLabels(
141 {tr("Label"), tr("Address"), tr("Hexadecimal"),
142 // i18n: The base 10 numeral system. Not related to non-integer numbers
143 tr("Decimal"),
144 // i18n: Data type used in computing
145 tr("String"),
146 // i18n: Floating-point (non-integer) number
147 tr("Float")});
148
149 for (int i = 0; i < size; i++)
150 {
151 auto entry = PowerPC::debug_interface.GetWatch(i);
152
153 auto* label = new QTableWidgetItem(QString::fromStdString(entry.name));
154 auto* address =
155 new QTableWidgetItem(QStringLiteral("%1").arg(entry.address, 8, 16, QLatin1Char('0')));
156 auto* hex = new QTableWidgetItem;
157 auto* decimal = new QTableWidgetItem;
158 auto* string = new QTableWidgetItem;
159 auto* floatValue = new QTableWidgetItem;
160
161 std::array<QTableWidgetItem*, NUM_COLUMNS> items = {label, address, hex,
162 decimal, string, floatValue};
163
164 QBrush brush = QPalette().brush(QPalette::Text);
165
166 if (!Core::IsRunning() || !PowerPC::HostIsRAMAddress(entry.address))
167 brush.setColor(Qt::red);
168
169 if (Core::IsRunning())
170 {
171 if (PowerPC::HostIsRAMAddress(entry.address))
172 {
173 hex->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U32(entry.address), 8, 16,
174 QLatin1Char('0')));
175 decimal->setText(QString::number(PowerPC::HostRead_U32(entry.address)));
176 string->setText(QString::fromStdString(PowerPC::HostGetString(entry.address, 32)));
177 floatValue->setText(QString::number(PowerPC::HostRead_F32(entry.address)));
178 }
179 }
180
181 address->setForeground(brush);
182 string->setFlags(Qt::ItemIsEnabled);
183
184 for (int column = 0; column < NUM_COLUMNS; column++)
185 {
186 auto* item = items[column];
187 item->setData(Qt::UserRole, i);
188 item->setData(Qt::UserRole + 1, column);
189 m_table->setItem(i, column, item);
190 }
191 }
192
193 auto* label = new QTableWidgetItem;
194 label->setData(Qt::UserRole, -1);
195
196 m_table->setItem(size, 0, label);
197
198 for (int i = 1; i < NUM_COLUMNS; i++)
199 {
200 auto* no_edit = new QTableWidgetItem;
201 no_edit->setFlags(Qt::ItemIsEnabled);
202 m_table->setItem(size, i, no_edit);
203 }
204
205 m_updating = false;
206 }
207
closeEvent(QCloseEvent *)208 void WatchWidget::closeEvent(QCloseEvent*)
209 {
210 Settings::Instance().SetWatchVisible(false);
211 }
212
showEvent(QShowEvent * event)213 void WatchWidget::showEvent(QShowEvent* event)
214 {
215 UpdateButtonsEnabled();
216 Update();
217 }
218
OnLoad()219 void WatchWidget::OnLoad()
220 {
221 IniFile ini;
222
223 std::vector<std::string> watches;
224
225 if (!ini.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + SConfig::GetInstance().GetGameID() + ".ini",
226 false))
227 {
228 return;
229 }
230
231 if (ini.GetLines("Watches", &watches, false))
232 {
233 PowerPC::debug_interface.ClearWatches();
234 PowerPC::debug_interface.LoadWatchesFromStrings(watches);
235 }
236
237 Update();
238 }
239
OnSave()240 void WatchWidget::OnSave()
241 {
242 IniFile ini;
243 ini.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + SConfig::GetInstance().GetGameID() + ".ini",
244 false);
245 ini.SetLines("Watches", PowerPC::debug_interface.SaveWatchesToStrings());
246 ini.Save(File::GetUserPath(D_GAMESETTINGS_IDX) + SConfig::GetInstance().GetGameID() + ".ini");
247 }
248
ShowContextMenu()249 void WatchWidget::ShowContextMenu()
250 {
251 QMenu* menu = new QMenu(this);
252
253 if (!m_table->selectedItems().empty())
254 {
255 auto row_variant = m_table->selectedItems()[0]->data(Qt::UserRole);
256
257 if (!row_variant.isNull())
258 {
259 int row = row_variant.toInt();
260
261 if (row >= 0)
262 {
263 // i18n: This kind of "watch" is used for watching emulated memory.
264 // It's not related to timekeeping devices.
265 menu->addAction(tr("&Delete Watch"), this, [this, row] { DeleteWatch(row); });
266 menu->addAction(tr("&Add Memory Breakpoint"), this,
267 [this, row] { AddWatchBreakpoint(row); });
268 }
269 }
270 }
271
272 menu->addSeparator();
273
274 menu->addAction(tr("Update"), this, &WatchWidget::Update);
275
276 menu->exec(QCursor::pos());
277 }
278
OnItemChanged(QTableWidgetItem * item)279 void WatchWidget::OnItemChanged(QTableWidgetItem* item)
280 {
281 if (m_updating || item->data(Qt::UserRole).isNull())
282 return;
283
284 int row = item->data(Qt::UserRole).toInt();
285 int column = item->data(Qt::UserRole + 1).toInt();
286
287 if (row == -1)
288 {
289 if (!item->text().isEmpty())
290 {
291 AddWatch(item->text(), 0);
292
293 Update();
294 return;
295 }
296 }
297 else
298 {
299 switch (column)
300 {
301 // Label
302 case 0:
303 if (item->text().isEmpty())
304 DeleteWatch(row);
305 else
306 PowerPC::debug_interface.UpdateWatchName(row, item->text().toStdString());
307 break;
308 // Address
309 // Hexadecimal
310 // Decimal
311 case 1:
312 case 2:
313 case 3:
314 {
315 bool good;
316 quint32 value = item->text().toUInt(&good, column < 3 ? 16 : 10);
317
318 if (good)
319 {
320 if (column == 1)
321 PowerPC::debug_interface.UpdateWatchAddress(row, value);
322 else
323 PowerPC::HostWrite_U32(value, PowerPC::debug_interface.GetWatch(row).address);
324 }
325 else
326 {
327 ModalMessageBox::critical(this, tr("Error"), tr("Invalid input provided"));
328 }
329 break;
330 }
331 }
332
333 Update();
334 }
335 }
336
DeleteWatch(int row)337 void WatchWidget::DeleteWatch(int row)
338 {
339 PowerPC::debug_interface.RemoveWatch(row);
340 Update();
341 }
342
AddWatchBreakpoint(int row)343 void WatchWidget::AddWatchBreakpoint(int row)
344 {
345 emit RequestMemoryBreakpoint(PowerPC::debug_interface.GetWatch(row).address);
346 }
347
AddWatch(QString name,u32 addr)348 void WatchWidget::AddWatch(QString name, u32 addr)
349 {
350 PowerPC::debug_interface.SetWatch(addr, name.toStdString());
351 Update();
352 }
353