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