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