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(
199 QRect(QPoint(viewport()->size().width() - m_margins.right(), 0), QSize(m_margins.right(), m_margins.top())),
200 Qt::AlignHCenter, tr("ASCII"));
201 for (int x = 0; x < 16; ++x) {
202 painter.drawText(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), 0), m_cellSize), Qt::AlignHCenter,
203 QString::number(x, 16).toUpper());
204 }
205 int height = (viewport()->size().height() - m_cellHeight) / m_cellHeight;
206 for (int y = 0; y < height; ++y) {
207 int yp = m_cellHeight * y + m_margins.top();
208 QString data = arg.arg((y + m_top) * 16 + m_base, 8, 16, c0).toUpper();
209 painter.drawText(QRectF(QPointF(0, yp), QSizeF(m_margins.left(), m_cellHeight)), Qt::AlignHCenter, data);
210 switch (m_align) {
211 case 2:
212 for (int x = 0; x < 16; x += 2) {
213 uint32_t address = (y + m_top) * 16 + x + m_base;
214 if (isInSelection(address)) {
215 painter.fillRect(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), yp),
216 QSizeF(m_cellSize.width() * 2, m_cellSize.height())),
217 palette.highlight());
218 painter.setPen(palette.color(QPalette::HighlightedText));
219 if (isEditing(address)) {
220 drawEditingText(
221 painter,
222 QPointF(m_cellSize.width() * (x + 1.0) - 2 * m_letterWidth + m_margins.left(), yp));
223 continue;
224 }
225 } else {
226 painter.setPen(palette.color(QPalette::WindowText));
227 }
228 uint16_t b = m_cpu->memory.load16(m_cpu, address, nullptr);
229 painter.drawStaticText(
230 QPointF(m_cellSize.width() * (x + 1.0) - 2 * m_letterWidth + m_margins.left(), yp),
231 m_staticNumbers[(b >> 8) & 0xFF]);
232 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 1.0) + m_margins.left(), yp),
233 m_staticNumbers[b & 0xFF]);
234 }
235 break;
236 case 4:
237 for (int x = 0; x < 16; x += 4) {
238 uint32_t address = (y + m_top) * 16 + x + m_base;
239 if (isInSelection(address)) {
240 painter.fillRect(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), yp),
241 QSizeF(m_cellSize.width() * 4, m_cellSize.height())),
242 palette.highlight());
243 painter.setPen(palette.color(QPalette::HighlightedText));
244 if (isEditing(address)) {
245 drawEditingText(
246 painter,
247 QPointF(m_cellSize.width() * (x + 2.0) - 4 * m_letterWidth + m_margins.left(), yp));
248 continue;
249 }
250 } else {
251 painter.setPen(palette.color(QPalette::WindowText));
252 }
253 uint32_t b = m_cpu->memory.load32(m_cpu, address, nullptr);
254 painter.drawStaticText(
255 QPointF(m_cellSize.width() * (x + 2.0) - 4 * m_letterWidth + m_margins.left(), yp),
256 m_staticNumbers[(b >> 24) & 0xFF]);
257 painter.drawStaticText(
258 QPointF(m_cellSize.width() * (x + 2.0) - 2 * m_letterWidth + m_margins.left(), yp),
259 m_staticNumbers[(b >> 16) & 0xFF]);
260 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 2.0) + m_margins.left(), yp),
261 m_staticNumbers[(b >> 8) & 0xFF]);
262 painter.drawStaticText(
263 QPointF(m_cellSize.width() * (x + 2.0) + 2 * m_letterWidth + m_margins.left(), yp),
264 m_staticNumbers[b & 0xFF]);
265 }
266 break;
267 case 1:
268 default:
269 for (int x = 0; x < 16; ++x) {
270 uint32_t address = (y + m_top) * 16 + x + m_base;
271 if (isInSelection(address)) {
272 painter.fillRect(QRectF(QPointF(m_cellSize.width() * x + m_margins.left(), yp), m_cellSize),
273 palette.highlight());
274 painter.setPen(palette.color(QPalette::HighlightedText));
275 if (isEditing(address)) {
276 drawEditingText(painter,
277 QPointF(m_cellSize.width() * (x + 0.5) - m_letterWidth + m_margins.left(), yp));
278 continue;
279 }
280 } else {
281 painter.setPen(palette.color(QPalette::WindowText));
282 }
283 uint8_t b = m_cpu->memory.load8(m_cpu, address, nullptr);
284 painter.drawStaticText(QPointF(m_cellSize.width() * (x + 0.5) - m_letterWidth + m_margins.left(), yp),
285 m_staticNumbers[b]);
286 }
287 break;
288 }
289 painter.setPen(palette.color(QPalette::WindowText));
290 for (int x = 0; x < 16; ++x) {
291 uint8_t b = m_cpu->memory.load8(m_cpu, (y + m_top) * 16 + x + m_base, nullptr);
292 painter.drawStaticText(
293 QPointF(viewport()->size().width() - (16 - x) * m_margins.right() / 17.0 - m_letterWidth * 0.5, yp),
294 b < 0x80 ? m_staticAscii[b] : m_staticAscii[0]);
295 }
296 }
297 painter.drawLine(m_margins.left(), 0, m_margins.left(), viewport()->size().height());
298 painter.drawLine(viewport()->size().width() - m_margins.right(), 0, viewport()->size().width() - m_margins.right(),
299 viewport()->size().height());
300 painter.drawLine(0, m_margins.top(), viewport()->size().width(), m_margins.top());
301}
302
303void MemoryModel::wheelEvent(QWheelEvent* event) {
304 m_top -= event->angleDelta().y() / 8;
305 boundsCheck();
306 event->accept();
307 verticalScrollBar()->setValue(m_top);
308 update();
309}
310
311void MemoryModel::mousePressEvent(QMouseEvent* event) {
312 if (event->x() < m_margins.left() || event->y() < m_margins.top() ||
313 event->x() > size().width() - m_margins.right()) {
314 m_selection = qMakePair(0, 0);
315 return;
316 }
317
318 QPoint position(event->pos() - QPoint(m_margins.left(), m_margins.top()));
319 uint32_t address = int(position.x() / m_cellSize.width()) +
320 (int(position.y() / m_cellSize.height()) + m_top) * 16 + m_base;
321 if (event->button() == Qt::RightButton && isInSelection(address)) {
322 return;
323 }
324 if (event->modifiers() & Qt::ShiftModifier) {
325 if ((address & ~(m_align - 1)) < m_selectionAnchor) {
326 m_selection = qMakePair(address & ~(m_align - 1), m_selectionAnchor + m_align);
327 } else {
328 m_selection = qMakePair(m_selectionAnchor, (address & ~(m_align - 1)) + m_align);
329 }
330 } else {
331 m_selectionAnchor = address & ~(m_align - 1);
332 m_selection = qMakePair(m_selectionAnchor, m_selectionAnchor + m_align);
333 }
334 m_buffer = 0;
335 m_bufferedNybbles = 0;
336 emit selectionChanged(m_selection.first, m_selection.second);
337 viewport()->update();
338}
339
340void MemoryModel::mouseMoveEvent(QMouseEvent* event) {
341 if (event->x() < m_margins.left() || event->y() < m_margins.top() ||
342 event->x() > size().width() - m_margins.right()) {
343 return;
344 }
345
346 QPoint position(event->pos() - QPoint(m_margins.left(), m_margins.top()));
347 uint32_t address = int(position.x() / m_cellSize.width()) +
348 (int(position.y() / m_cellSize.height()) + m_top) * 16 + m_base;
349 if ((address & ~(m_align - 1)) < m_selectionAnchor) {
350 m_selection = qMakePair(address & ~(m_align - 1), m_selectionAnchor + m_align);
351 } else {
352 m_selection = qMakePair(m_selectionAnchor, (address & ~(m_align - 1)) + m_align);
353 }
354 m_buffer = 0;
355 m_bufferedNybbles = 0;
356 emit selectionChanged(m_selection.first, m_selection.second);
357 viewport()->update();
358}
359
360void MemoryModel::keyPressEvent(QKeyEvent* event) {
361 if (m_selection.first >= m_selection.second) {
362 return;
363 }
364 int key = event->key();
365 uint8_t nybble = 0;
366 switch (key) {
367 case Qt::Key_0:
368 case Qt::Key_1:
369 case Qt::Key_2:
370 case Qt::Key_3:
371 case Qt::Key_4:
372 case Qt::Key_5:
373 case Qt::Key_6:
374 case Qt::Key_7:
375 case Qt::Key_8:
376 case Qt::Key_9:
377 nybble = key - Qt::Key_0;
378 break;
379 case Qt::Key_A:
380 case Qt::Key_B:
381 case Qt::Key_C:
382 case Qt::Key_D:
383 case Qt::Key_E:
384 case Qt::Key_F:
385 nybble = key - Qt::Key_A + 10;
386 break;
387 case Qt::Key_Left:
388 adjustCursor(-m_align, event->modifiers() & Qt::ShiftModifier);
389 return;
390 case Qt::Key_Right:
391 adjustCursor(m_align, event->modifiers() & Qt::ShiftModifier);
392 return;
393 case Qt::Key_Up:
394 adjustCursor(-16, event->modifiers() & Qt::ShiftModifier);
395 return;
396 case Qt::Key_Down:
397 adjustCursor(16, event->modifiers() & Qt::ShiftModifier);
398 return;
399 default:
400 return;
401 }
402 m_buffer <<= 4;
403 m_buffer |= nybble;
404 ++m_bufferedNybbles;
405 if (m_bufferedNybbles == m_align * 2) {
406 switch (m_align) {
407 case 1:
408 GBAPatch8(m_cpu, m_selection.first, m_buffer, nullptr);
409 break;
410 case 2:
411 GBAPatch16(m_cpu, m_selection.first, m_buffer, nullptr);
412 break;
413 case 4:
414 GBAPatch32(m_cpu, m_selection.first, m_buffer, nullptr);
415 break;
416 }
417 m_bufferedNybbles = 0;
418 m_buffer = 0;
419 m_selection.first += m_align;
420 if (m_selection.second <= m_selection.first) {
421 m_selection.second = m_selection.first + m_align;
422 }
423 emit selectionChanged(m_selection.first, m_selection.second);
424 }
425 viewport()->update();
426}
427
428void MemoryModel::boundsCheck() {
429 if (m_top < 0) {
430 m_top = 0;
431 } else if (m_top > (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight) {
432 m_top = (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight;
433 }
434}
435
436bool MemoryModel::isInSelection(uint32_t address) {
437 if (m_selection.first == m_selection.second) {
438 return false;
439 }
440 if (m_selection.second <= (address | (m_align - 1))) {
441 return false;
442 }
443 if (m_selection.first <= (address & ~(m_align - 1))) {
444 return true;
445 }
446 return false;
447}
448
449bool MemoryModel::isEditing(uint32_t address) {
450 return m_bufferedNybbles && m_selection.first == (address & ~(m_align - 1));
451}
452
453void MemoryModel::drawEditingText(QPainter& painter, const QPointF& origin) {
454 QPointF o(origin);
455 for (int nybbles = m_bufferedNybbles; nybbles > 0; nybbles -= 2) {
456 if (nybbles > 1) {
457 uint8_t b = m_buffer >> ((nybbles - 2) * 4);
458 painter.drawStaticText(o, m_staticNumbers[b]);
459 } else {
460 int b = m_buffer & 0xF;
461 if (b < 10) {
462 painter.drawStaticText(o, m_staticAscii[b + '0']);
463 } else {
464 painter.drawStaticText(o, m_staticAscii[b - 10 + 'A']);
465 }
466 }
467 o += QPointF(m_letterWidth * 2, 0);
468 }
469}
470
471void MemoryModel::adjustCursor(int adjust, bool shift) {
472 if (m_selection.first >= m_selection.second) {
473 return;
474 }
475 int cursorPosition = m_top;
476 if (shift) {
477 if (m_selectionAnchor == m_selection.first) {
478 if (adjust < 0 && m_base - adjust > m_selection.second) {
479 adjust = m_base - m_selection.second + m_align;
480 } else if (adjust > 0 && m_selection.second + adjust >= m_base + m_size) {
481 adjust = m_base + m_size - m_selection.second;
482 }
483 adjust += m_selection.second;
484 if (adjust <= m_selection.first) {
485 m_selection.second = m_selection.first + m_align;
486 m_selection.first = adjust - m_align;
487 cursorPosition = m_selection.first;
488 } else {
489 m_selection.second = adjust;
490 cursorPosition = m_selection.second - m_align;
491 }
492 } else {
493 if (adjust < 0 && m_base - adjust > m_selection.first) {
494 adjust = m_base - m_selection.first;
495 } else if (adjust > 0 && m_selection.first + adjust >= m_base + m_size) {
496 adjust = m_base + m_size - m_selection.first - m_align;
497 }
498 adjust += m_selection.first;
499 if (adjust >= m_selection.second) {
500 m_selection.first = m_selection.second - m_align;
501 m_selection.second = adjust + m_align;
502 cursorPosition = adjust;
503 } else {
504 m_selection.first = adjust;
505 cursorPosition = m_selection.first;
506 }
507 }
508 cursorPosition = (cursorPosition - m_base) / 16;
509 } else {
510 if (m_selectionAnchor == m_selection.first) {
511 m_selectionAnchor = m_selection.second - m_align;
512 } else {
513 m_selectionAnchor = m_selection.first;
514 }
515 if (adjust < 0 && m_base - adjust > m_selectionAnchor) {
516 m_selectionAnchor = m_base;
517 } else if (adjust > 0 && m_selectionAnchor + adjust >= m_base + m_size) {
518 m_selectionAnchor = m_base + m_size - m_align;
519 } else {
520 m_selectionAnchor += adjust;
521 }
522 m_selection.first = m_selectionAnchor;
523 m_selection.second = m_selection.first + m_align;
524 cursorPosition = (m_selectionAnchor - m_base) / 16;
525 }
526 if (cursorPosition < m_top) {
527 m_top = cursorPosition;
528 } else if (cursorPosition >= m_top + viewport()->size().height() / m_cellHeight - 1) {
529 m_top = cursorPosition - viewport()->size().height() / m_cellHeight + 2;
530 }
531 emit selectionChanged(m_selection.first, m_selection.second);
532 viewport()->update();
533}