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