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 =
320 int(position.x() / m_cellSize.width()) + (int(position.y() / m_cellSize.height()) + m_top) * 16 + m_base;
321 if (event->button() == Qt::RightButton && isInSelection(address)) {
322 return;
323 }
324 m_selectionAnchor = address & ~(m_align - 1);
325 m_selection = qMakePair(m_selectionAnchor, m_selectionAnchor + m_align);
326 m_buffer = 0;
327 m_bufferedNybbles = 0;
328 emit selectionChanged(m_selection.first, m_selection.second);
329 viewport()->update();
330}
331
332void MemoryModel::mouseMoveEvent(QMouseEvent* event) {
333 if (event->x() < m_margins.left() || event->y() < m_margins.top() ||
334 event->x() > size().width() - m_margins.right()) {
335 return;
336 }
337
338 QPoint position(event->pos() - QPoint(m_margins.left(), m_margins.top()));
339 uint32_t address =
340 int(position.x() / m_cellSize.width()) + (int(position.y() / m_cellSize.height()) + m_top) * 16 + m_base;
341 if ((address & ~(m_align - 1)) < m_selectionAnchor) {
342 m_selection = qMakePair(address & ~(m_align - 1), m_selectionAnchor + m_align);
343 } else {
344 m_selection = qMakePair(m_selectionAnchor, (address & ~(m_align - 1)) + m_align);
345 }
346 m_buffer = 0;
347 m_bufferedNybbles = 0;
348 emit selectionChanged(m_selection.first, m_selection.second);
349 viewport()->update();
350}
351
352void MemoryModel::keyPressEvent(QKeyEvent* event) {
353 if (m_selection.first >= m_selection.second) {
354 return;
355 }
356 int key = event->key();
357 uint8_t nybble = 0;
358 switch (key) {
359 case Qt::Key_0:
360 case Qt::Key_1:
361 case Qt::Key_2:
362 case Qt::Key_3:
363 case Qt::Key_4:
364 case Qt::Key_5:
365 case Qt::Key_6:
366 case Qt::Key_7:
367 case Qt::Key_8:
368 case Qt::Key_9:
369 nybble = key - Qt::Key_0;
370 break;
371 case Qt::Key_A:
372 case Qt::Key_B:
373 case Qt::Key_C:
374 case Qt::Key_D:
375 case Qt::Key_E:
376 case Qt::Key_F:
377 nybble = key - Qt::Key_A + 10;
378 break;
379 case Qt::Key_Left:
380 adjustCursor(-m_align, event->modifiers() & Qt::ShiftModifier);
381 return;
382 case Qt::Key_Right:
383 adjustCursor(m_align, event->modifiers() & Qt::ShiftModifier);
384 return;
385 case Qt::Key_Up:
386 adjustCursor(-16, event->modifiers() & Qt::ShiftModifier);
387 return;
388 case Qt::Key_Down:
389 adjustCursor(16, event->modifiers() & Qt::ShiftModifier);
390 return;
391 default:
392 return;
393 }
394 m_buffer <<= 4;
395 m_buffer |= nybble;
396 ++m_bufferedNybbles;
397 if (m_bufferedNybbles == m_align * 2) {
398 switch (m_align) {
399 case 1:
400 GBAPatch8(m_cpu, m_selection.first, m_buffer, nullptr);
401 break;
402 case 2:
403 GBAPatch16(m_cpu, m_selection.first, m_buffer, nullptr);
404 break;
405 case 4:
406 GBAPatch32(m_cpu, m_selection.first, m_buffer, nullptr);
407 break;
408 }
409 m_bufferedNybbles = 0;
410 m_buffer = 0;
411 m_selection.first += m_align;
412 if (m_selection.second <= m_selection.first) {
413 m_selection.second = m_selection.first + m_align;
414 }
415 emit selectionChanged(m_selection.first, m_selection.second);
416 }
417 viewport()->update();
418}
419
420void MemoryModel::boundsCheck() {
421 if (m_top < 0) {
422 m_top = 0;
423 } else if (m_top > (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight) {
424 m_top = (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight;
425 }
426}
427
428bool MemoryModel::isInSelection(uint32_t address) {
429 if (m_selection.first == m_selection.second) {
430 return false;
431 }
432 if (m_selection.second <= (address | (m_align - 1))) {
433 return false;
434 }
435 if (m_selection.first <= (address & ~(m_align - 1))) {
436 return true;
437 }
438 return false;
439}
440
441bool MemoryModel::isEditing(uint32_t address) {
442 return m_bufferedNybbles && m_selection.first == (address & ~(m_align - 1));
443}
444
445void MemoryModel::drawEditingText(QPainter& painter, const QPointF& origin) {
446 QPointF o(origin);
447 for (int nybbles = m_bufferedNybbles; nybbles > 0; nybbles -= 2) {
448 if (nybbles > 1) {
449 uint8_t b = m_buffer >> ((nybbles - 2) * 4);
450 painter.drawStaticText(o, m_staticNumbers[b]);
451 } else {
452 int b = m_buffer & 0xF;
453 if (b < 10) {
454 painter.drawStaticText(o, m_staticAscii[b + '0']);
455 } else {
456 painter.drawStaticText(o, m_staticAscii[b - 10 + 'A']);
457 }
458 }
459 o += QPointF(m_letterWidth * 2, 0);
460 }
461}
462
463void MemoryModel::adjustCursor(int adjust, bool shift) {
464 if (m_selection.first >= m_selection.second) {
465 return;
466 }
467 int cursorPosition = m_top;
468 if (shift) {
469 if (m_selectionAnchor == m_selection.first) {
470 if (adjust < 0 && m_base - adjust > m_selection.second) {
471 adjust = m_base - m_selection.second + m_align;
472 } else if (adjust > 0 && m_selection.second + adjust >= m_base + m_size) {
473 adjust = m_base + m_size - m_selection.second;
474 }
475 adjust += m_selection.second;
476 if (adjust <= m_selection.first) {
477 m_selection.second = m_selection.first + m_align;
478 m_selection.first = adjust - m_align;
479 cursorPosition = m_selection.first;
480 } else {
481 m_selection.second = adjust;
482 cursorPosition = m_selection.second - m_align;
483 }
484 } else {
485 if (adjust < 0 && m_base - adjust > m_selection.first) {
486 adjust = m_base - m_selection.first;
487 } else if (adjust > 0 && m_selection.first + adjust >= m_base + m_size) {
488 adjust = m_base + m_size - m_selection.first - m_align;
489 }
490 adjust += m_selection.first;
491 if (adjust >= m_selection.second) {
492 m_selection.first = m_selection.second - m_align;
493 m_selection.second = adjust + m_align;
494 cursorPosition = adjust;
495 } else {
496 m_selection.first = adjust;
497 cursorPosition = m_selection.first;
498 }
499 }
500 cursorPosition = (cursorPosition - m_base) / 16;
501 } else {
502 if (m_selectionAnchor == m_selection.first) {
503 m_selectionAnchor = m_selection.second - m_align;
504 } else {
505 m_selectionAnchor = m_selection.first;
506 }
507 if (adjust < 0 && m_base - adjust > m_selectionAnchor) {
508 m_selectionAnchor = m_base;
509 } else if (adjust > 0 && m_selectionAnchor + adjust >= m_base + m_size) {
510 m_selectionAnchor = m_base + m_size - m_align;
511 } else {
512 m_selectionAnchor += adjust;
513 }
514 m_selection.first = m_selectionAnchor;
515 m_selection.second = m_selection.first + m_align;
516 cursorPosition = (m_selectionAnchor - m_base) / 16;
517 }
518 if (cursorPosition < m_top) {
519 m_top = cursorPosition;
520 } else if (cursorPosition >= m_top + viewport()->size().height() / m_cellHeight - 1) {
521 m_top = cursorPosition - viewport()->size().height() / m_cellHeight + 2;
522 }
523 emit selectionChanged(m_selection.first, m_selection.second);
524 viewport()->update();
525}