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