src/platform/qt/MemoryModel.cpp (view raw)
1/* Copyright (c) 2013-2015 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6#include "MemoryModel.h"
7
8#include "GBAApp.h"
9#include "GameController.h"
10
11#include <QAction>
12#include <QApplication>
13#include <QClipboard>
14#include <QFontMetrics>
15#include <QPainter>
16#include <QScrollBar>
17#include <QSlider>
18#include <QWheelEvent>
19
20extern "C" {
21#include "gba/memory.h"
22}
23
24using namespace QGBA;
25
26MemoryModel::MemoryModel(QWidget* parent)
27 : QAbstractScrollArea(parent)
28 , m_cpu(nullptr)
29 , m_top(0)
30 , m_align(1)
31 , m_selection(0, 0)
32 , m_selectionAnchor(0)
33{
34 m_font.setFamily("Source Code Pro");
35 m_font.setStyleHint(QFont::Monospace);
36 m_font.setPointSize(12);
37 QFontMetrics metrics(m_font);
38 m_cellHeight = metrics.height();
39 m_letterWidth = metrics.averageCharWidth();
40
41 setFocusPolicy(Qt::StrongFocus);
42 setContextMenuPolicy(Qt::ActionsContextMenu);
43
44 QAction* copy = new QAction(tr("Copy selection"), this);
45 copy->setShortcut(QKeySequence::Copy);
46 connect(copy, SIGNAL(triggered()), this, SLOT(copy()));
47 addAction(copy);
48
49 QAction* save = new QAction(tr("Save selection"), this);
50 save->setShortcut(QKeySequence::Save);
51 connect(save, SIGNAL(triggered()), this, SLOT(save()));
52 addAction(save);
53
54 static QString arg("%0");
55 for (int i = 0; i < 256; ++i) {
56 QStaticText str(arg.arg(i, 2, 16, QChar('0')).toUpper());
57 str.prepare(QTransform(), m_font);
58 m_staticNumbers.append(str);
59 }
60
61 for (int i = 0; i < 128; ++i) {
62 QChar c(i);
63 if (!c.isPrint()) {
64 c = '.';
65 }
66 QStaticText str = QStaticText(QString(c));
67 str.prepare(QTransform(), m_font);
68 m_staticAscii.append(str);
69 }
70
71 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
72 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
73 m_margins = QMargins(metrics.width("0FFFFFF0 ") + 3, m_cellHeight + 1, metrics.width(" AAAAAAAAAAAAAAAA") + 3, 0);
74 m_cellSize = QSizeF((viewport()->size().width() - (m_margins.left() + m_margins.right())) / 16.0, m_cellHeight);
75
76 connect(verticalScrollBar(), &QSlider::sliderMoved, [this](int position) {
77 m_top = position;
78 update();
79 });
80
81 setRegion(0, 0x10000000, tr("All"));
82}
83
84void MemoryModel::setController(GameController* controller) {
85 m_cpu = controller->thread()->cpu;
86}
87
88void MemoryModel::setRegion(uint32_t base, uint32_t size, const QString& name) {
89 m_top = 0;
90 m_base = base;
91 m_size = size;
92 m_regionName = name;
93 m_regionName.prepare(QTransform(), m_font);
94 verticalScrollBar()->setRange(0, (size >> 4) + 1 - viewport()->size().height() / m_cellHeight);
95 verticalScrollBar()->setValue(0);
96 viewport()->update();
97}
98
99void MemoryModel::setAlignment(int width) {
100 if (width != 1 && width != 2 && width != 4) {
101 return;
102 }
103 m_align = width;
104 m_buffer = 0;
105 m_bufferedNybbles = 0;
106 viewport()->update();
107}
108
109void MemoryModel::jumpToAddress(const QString& hex) {
110 bool ok = false;
111 uint32_t i = hex.toInt(&ok, 16);
112 if (ok) {
113 jumpToAddress(i);
114 }
115}
116
117void MemoryModel::jumpToAddress(uint32_t address) {
118 if (address >= 0x10000000) {
119 return;
120 }
121 if (address < m_base || address >= m_base + m_size) {
122 setRegion(0, 0x10000000, tr("All"));
123 }
124 m_top = (address - m_base) / 16;
125 boundsCheck();
126 verticalScrollBar()->setValue(m_top);
127 m_buffer = 0;
128 m_bufferedNybbles = 0;
129}
130
131void MemoryModel::copy() {
132 QClipboard* clipboard = QApplication::clipboard();
133 if (!clipboard) {
134 return;
135 }
136 QByteArray bytestring;
137 QDataStream stream(&bytestring, QIODevice::WriteOnly);
138 serialize(&stream);
139 QString string;
140 string.reserve(bytestring.size() * 2);
141 static QString arg("%0");
142 static QChar c0('0');
143 for (uchar c : bytestring) {
144 string.append(arg.arg(c, 2, 16, c0).toUpper());
145 }
146 clipboard->setText(string);
147}
148
149void MemoryModel::save() {
150 QString filename = GBAApp::app()->getSaveFileName(this, tr("Save selected memory"));
151 if (filename.isNull()) {
152 return;
153 }
154 QFile outfile(filename);
155 if (!outfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
156 // TODO: Log
157 return;
158 }
159 QDataStream stream(&outfile);
160 serialize(&stream);
161}
162
163void MemoryModel::serialize(QDataStream* stream) {
164 switch (m_align) {
165 case 1:
166 for (uint32_t i = m_selection.first; i < m_selection.second; i += m_align) {
167 *stream << (quint8) m_cpu->memory.load8(m_cpu, i, nullptr);
168 }
169 break;
170 case 2:
171 for (uint32_t i = m_selection.first; i < m_selection.second; i += m_align) {
172 *stream << (quint16) m_cpu->memory.load16(m_cpu, i, nullptr);
173 }
174 break;
175 case 4:
176 for (uint32_t i = m_selection.first; i < m_selection.second; i += m_align) {
177 *stream << (quint32) m_cpu->memory.load32(m_cpu, i, nullptr);
178 }
179 break;
180 }
181}
182
183void MemoryModel::resizeEvent(QResizeEvent*) {
184 m_cellSize = QSizeF((viewport()->size().width() - (m_margins.left() + m_margins.right())) / 16.0, m_cellHeight);
185 verticalScrollBar()->setRange(0, (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight);
186 boundsCheck();
187}
188
189void MemoryModel::paintEvent(QPaintEvent* event) {
190 QPainter painter(viewport());
191 QPalette palette;
192 painter.setFont(m_font);
193 painter.setPen(palette.color(QPalette::WindowText));
194 static QChar c0('0');
195 static QString arg("%0");
196 QSizeF letterSize = QSizeF(m_letterWidth, m_cellHeight);
197 painter.drawStaticText(QPointF((m_margins.left() - m_regionName.size().width() - 1) / 2.0, 0), m_regionName);
198 painter.drawText(QRect(QPoint(viewport()->size().width() - m_margins.right(), 0), QSize(m_margins.right(), m_margins.top())), Qt::AlignHCenter, tr("ASCII"));
199 for (int x = 0; x < 16; ++x) {
200 painter.drawText(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), 0), m_cellSize), Qt::AlignHCenter, QString::number(x, 16).toUpper());
201 }
202 int height = (viewport()->size().height() - m_cellHeight) / m_cellHeight;
203 for (int y = 0; y < height; ++y) {
204 int yp = m_cellHeight * y + m_margins.top();
205 QString data = arg.arg((y + m_top) * 16 + m_base, 8, 16, c0).toUpper();
206 painter.drawText(QRectF(QPointF(0, yp), QSizeF(m_margins.left(), m_cellHeight)), Qt::AlignHCenter, data);
207 switch (m_align) {
208 case 2:
209 for (int x = 0; x < 16; x += 2) {
210 uint32_t address = (y + m_top) * 16 + x + m_base;
211 if (isInSelection(address)) {
212 painter.fillRect(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), yp), QSizeF(m_cellSize.width() * 2, m_cellSize.height())), palette.highlight());
213 painter.setPen(palette.color(QPalette::HighlightedText));
214 if (isEditing(address)) {
215 drawEditingText(painter, QPointF(m_cellSize.width() * (x + 1.0) - 2 * m_letterWidth + m_margins.left(), yp));
216 continue;
217 }
218 } else {
219 painter.setPen(palette.color(QPalette::WindowText));
220 }
221 uint16_t b = m_cpu->memory.load16(m_cpu, address, nullptr);
222 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 1.0) - 2 * m_letterWidth + m_margins.left(), yp), m_staticNumbers[(b >> 8) & 0xFF]);
223 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 1.0) + m_margins.left(), yp), m_staticNumbers[b & 0xFF]);
224 }
225 break;
226 case 4:
227 for (int x = 0; x < 16; x += 4) {
228 uint32_t address = (y + m_top) * 16 + x + m_base;
229 if (isInSelection(address)) {
230 painter.fillRect(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), yp), QSizeF(m_cellSize.width() * 4, m_cellSize.height())), palette.highlight());
231 painter.setPen(palette.color(QPalette::HighlightedText));
232 if (isEditing(address)) {
233 drawEditingText(painter, QPointF(m_cellSize.width() * (x + 2.0) - 4 * m_letterWidth + m_margins.left(), yp));
234 continue;
235 }
236 } else {
237 painter.setPen(palette.color(QPalette::WindowText));
238 }
239 uint32_t b = m_cpu->memory.load32(m_cpu, address, nullptr);
240 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 2.0) - 4 * m_letterWidth + m_margins.left(), yp), m_staticNumbers[(b >> 24) & 0xFF]);
241 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 2.0) - 2 * m_letterWidth + m_margins.left(), yp), m_staticNumbers[(b >> 16) & 0xFF]);
242 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 2.0) + m_margins.left(), yp), m_staticNumbers[(b >> 8) & 0xFF]);
243 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 2.0) + 2 * m_letterWidth + m_margins.left(), yp), m_staticNumbers[b & 0xFF]);
244 }
245 break;
246 case 1:
247 default:
248 for (int x = 0; x < 16; ++x) {
249 uint32_t address = (y + m_top) * 16 + x + m_base;
250 if (isInSelection(address)) {
251 painter.fillRect(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), yp), m_cellSize), palette.highlight());
252 painter.setPen(palette.color(QPalette::HighlightedText));
253 if (isEditing(address)) {
254 drawEditingText(painter, QPointF(m_cellSize.width() * (x + 0.5) - m_letterWidth + m_margins.left(), yp));
255 continue;
256 }
257 } else {
258 painter.setPen(palette.color(QPalette::WindowText));
259 }
260 uint8_t b = m_cpu->memory.load8(m_cpu, address, nullptr);
261 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 0.5) - m_letterWidth + m_margins.left(), yp), m_staticNumbers[b]);
262 }
263 break;
264 }
265 painter.setPen(palette.color(QPalette::WindowText));
266 for (int x = 0; x < 16; ++x) {
267 uint8_t b = m_cpu->memory.load8(m_cpu, (y + m_top) * 16 + x + m_base, nullptr);
268 painter.drawStaticText(QPointF(viewport()->size().width() - (16 - x) * m_margins.right() / 17.0 - m_letterWidth * 0.5, yp), b < 0x80 ? m_staticAscii[b] : m_staticAscii[0]);
269 }
270 }
271 painter.drawLine(m_margins.left(), 0, m_margins.left(), viewport()->size().height());
272 painter.drawLine(viewport()->size().width() - m_margins.right(), 0, viewport()->size().width() - m_margins.right(), viewport()->size().height());
273 painter.drawLine(0, m_margins.top(), viewport()->size().width(), m_margins.top());
274}
275
276void MemoryModel::wheelEvent(QWheelEvent* event) {
277 m_top -= event->angleDelta().y() / 8;
278 boundsCheck();
279 event->accept();
280 verticalScrollBar()->setValue(m_top);
281 update();
282}
283
284void MemoryModel::mousePressEvent(QMouseEvent* event) {
285 if (event->x() < m_margins.left() || event->y() < m_margins.top() || event->x() > size().width() - m_margins.right()) {
286 m_selection = qMakePair(0, 0);
287 return;
288 }
289
290 QPoint position(event->pos() - QPoint(m_margins.left(), m_margins.top()));
291 uint32_t address = int(position.x() / m_cellSize.width()) + (int(position.y() / m_cellSize.height()) + m_top) * 16 + m_base;
292 if (event->button() == Qt::RightButton && isInSelection(address)) {
293 return;
294 }
295 m_selectionAnchor = address & ~(m_align - 1);
296 m_selection = qMakePair(m_selectionAnchor, m_selectionAnchor + m_align);
297 m_buffer = 0;
298 m_bufferedNybbles = 0;
299 emit selectionChanged(m_selection.first, m_selection.second);
300 viewport()->update();
301}
302
303void MemoryModel::mouseMoveEvent(QMouseEvent* event) {
304 if (event->x() < m_margins.left() || event->y() < m_margins.top() || event->x() > size().width() - m_margins.right()) {
305 return;
306 }
307
308 QPoint position(event->pos() - QPoint(m_margins.left(), m_margins.top()));
309 uint32_t address = int(position.x() / m_cellSize.width()) + (int(position.y() / m_cellSize.height()) + m_top) * 16 + m_base;
310 if ((address & ~(m_align - 1)) < m_selectionAnchor) {
311 m_selection = qMakePair(address & ~(m_align - 1), m_selectionAnchor + m_align);
312 } else {
313 m_selection = qMakePair(m_selectionAnchor, (address & ~(m_align - 1)) + m_align);
314 }
315 m_buffer = 0;
316 m_bufferedNybbles = 0;
317 emit selectionChanged(m_selection.first, m_selection.second);
318 viewport()->update();
319}
320
321void MemoryModel::keyPressEvent(QKeyEvent* event) {
322 if (m_selection.first >= m_selection.second) {
323 return;
324 }
325 int key = event->key();
326 uint8_t nybble = 0;
327 switch (key) {
328 case Qt::Key_0:
329 case Qt::Key_1:
330 case Qt::Key_2:
331 case Qt::Key_3:
332 case Qt::Key_4:
333 case Qt::Key_5:
334 case Qt::Key_6:
335 case Qt::Key_7:
336 case Qt::Key_8:
337 case Qt::Key_9:
338 nybble = key - Qt::Key_0;
339 break;
340 case Qt::Key_A:
341 case Qt::Key_B:
342 case Qt::Key_C:
343 case Qt::Key_D:
344 case Qt::Key_E:
345 case Qt::Key_F:
346 nybble = key - Qt::Key_A + 10;
347 break;
348 case Qt::Key_Left:
349 adjustCursor(-m_align, event->modifiers() & Qt::ShiftModifier);
350 return;
351 case Qt::Key_Right:
352 adjustCursor(m_align, event->modifiers() & Qt::ShiftModifier);
353 return;
354 case Qt::Key_Up:
355 adjustCursor(-16, event->modifiers() & Qt::ShiftModifier);
356 return;
357 case Qt::Key_Down:
358 adjustCursor(16, event->modifiers() & Qt::ShiftModifier);
359 return;
360 default:
361 return;
362 }
363 m_buffer <<= 4;
364 m_buffer |= nybble;
365 ++m_bufferedNybbles;
366 if (m_bufferedNybbles == m_align * 2) {
367 switch (m_align) {
368 case 1:
369 GBAPatch8(m_cpu, m_selection.first, m_buffer, nullptr);
370 break;
371 case 2:
372 GBAPatch16(m_cpu, m_selection.first, m_buffer, nullptr);
373 break;
374 case 4:
375 GBAPatch32(m_cpu, m_selection.first, m_buffer, nullptr);
376 break;
377 }
378 m_bufferedNybbles = 0;
379 m_buffer = 0;
380 m_selection.first += m_align;
381 if (m_selection.second <= m_selection.first) {
382 m_selection.second = m_selection.first + m_align;
383 }
384 emit selectionChanged(m_selection.first, m_selection.second);
385 }
386 viewport()->update();
387}
388
389void MemoryModel::boundsCheck() {
390 if (m_top < 0) {
391 m_top = 0;
392 } else if (m_top > (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight) {
393 m_top = (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight;
394 }
395}
396
397bool MemoryModel::isInSelection(uint32_t address) {
398 if (m_selection.first == m_selection.second) {
399 return false;
400 }
401 if (m_selection.second <= (address | (m_align - 1))) {
402 return false;
403 }
404 if (m_selection.first <= (address & ~(m_align - 1))) {
405 return true;
406 }
407 return false;
408}
409
410bool MemoryModel::isEditing(uint32_t address) {
411 return m_bufferedNybbles && m_selection.first == (address & ~(m_align - 1));
412}
413
414void MemoryModel::drawEditingText(QPainter& painter, const QPointF& origin) {
415 QPointF o(origin);
416 for (int nybbles = m_bufferedNybbles; nybbles > 0; nybbles -= 2) {
417 if (nybbles > 1) {
418 uint8_t b = m_buffer >> ((nybbles - 2) * 4);
419 painter.drawStaticText(o, m_staticNumbers[b]);
420 } else {
421 int b = m_buffer & 0xF;
422 painter.drawStaticText(o, m_staticAscii[b + '0']);
423 }
424 o += QPointF(m_letterWidth * 2, 0);
425 }
426}
427
428void MemoryModel::adjustCursor(int adjust, bool shift) {
429 if (m_selection.first >= m_selection.second) {
430 return;
431 }
432 int cursorPosition = m_top;
433 if (shift) {
434 if (m_selectionAnchor == m_selection.first) {
435 if (adjust < 0 && m_base - adjust > m_selection.second) {
436 adjust = m_base - m_selection.second + m_align;
437 } else if (adjust > 0 && m_selection.second + adjust >= m_base + m_size) {
438 adjust = m_base + m_size - m_selection.second;
439 }
440 adjust += m_selection.second;
441 if (adjust <= m_selection.first) {
442 m_selection.second = m_selection.first + m_align;
443 m_selection.first = adjust - m_align;
444 cursorPosition = m_selection.first;
445 } else {
446 m_selection.second = adjust;
447 cursorPosition = m_selection.second - m_align;
448 }
449 } else {
450 if (adjust < 0 && m_base - adjust > m_selection.first) {
451 adjust = m_base - m_selection.first;
452 } else if (adjust > 0 && m_selection.first + adjust >= m_base + m_size) {
453 adjust = m_base + m_size - m_selection.first - m_align;
454 }
455 adjust += m_selection.first;
456 if (adjust >= m_selection.second) {
457 m_selection.first = m_selection.second - m_align;
458 m_selection.second = adjust + m_align;
459 cursorPosition = adjust;
460 } else {
461 m_selection.first = adjust;
462 cursorPosition = m_selection.first;
463 }
464 }
465 cursorPosition = (cursorPosition - m_base) / 16;
466 } else {
467 if (m_selectionAnchor == m_selection.first) {
468 m_selectionAnchor = m_selection.second - m_align;
469 } else {
470 m_selectionAnchor = m_selection.first;
471 }
472 if (adjust < 0 && m_base - adjust > m_selectionAnchor) {
473 m_selectionAnchor = m_base;
474 } else if (adjust > 0 && m_selectionAnchor + adjust >= m_base + m_size) {
475 m_selectionAnchor = m_base + m_size - m_align;
476 } else {
477 m_selectionAnchor += adjust;
478 }
479 m_selection.first = m_selectionAnchor;
480 m_selection.second = m_selection.first + m_align;
481 cursorPosition = (m_selectionAnchor - m_base) / 16;
482 }
483 if (cursorPosition < m_top) {
484 m_top = cursorPosition;
485 } else if (cursorPosition >= m_top + viewport()->size().height() / m_cellHeight - 1) {
486 m_top = cursorPosition - viewport()->size().height() / m_cellHeight + 2;
487 }
488 emit selectionChanged(m_selection.first, m_selection.second);
489 viewport()->update();
490}