src/platform/qt/FrameView.cpp (view raw)
1/* Copyright (c) 2013-2019 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 "FrameView.h"
7
8#include <QMouseEvent>
9#include <QPainter>
10#include <QPalette>
11
12#include <array>
13#include <cmath>
14
15#include "CoreController.h"
16
17#ifdef M_CORE_GBA
18#include <mgba/internal/gba/gba.h>
19#include <mgba/internal/gba/io.h>
20#include <mgba/internal/gba/memory.h>
21#include <mgba/internal/gba/video.h>
22#endif
23#ifdef M_CORE_GB
24#include <mgba/internal/gb/gb.h>
25#include <mgba/internal/gb/memory.h>
26#endif
27
28using namespace QGBA;
29
30FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent)
31 : AssetView(controller, parent)
32{
33 m_ui.setupUi(this);
34
35 m_glowTimer.setInterval(33);
36 connect(&m_glowTimer, &QTimer::timeout, this, [this]() {
37 ++m_glowFrame;
38 invalidateQueue();
39 });
40 m_glowTimer.start();
41
42 m_ui.renderedView->installEventFilter(this);
43 m_ui.compositedView->installEventFilter(this);
44
45 connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) {
46 Layer& layer = m_queue[item->data(Qt::UserRole).toInt()];
47 layer.enabled = item->checkState() == Qt::Checked;
48 if (layer.enabled) {
49 m_disabled.remove(layer.id);
50 } else {
51 m_disabled.insert(layer.id);
52 }
53 invalidateQueue();
54 });
55 connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) {
56 if (item) {
57 m_active = m_queue[item->data(Qt::UserRole).toInt()].id;
58 } else {
59 m_active = {};
60 }
61 invalidateQueue();
62 });
63 connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() {
64 invalidateQueue();
65
66 QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value());
67 m_ui.renderedView->setPixmap(rendered);
68 });
69}
70
71bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) {
72 for (Layer& layer : m_queue) {
73 if (!layer.enabled || m_disabled.contains(layer.id)) {
74 continue;
75 }
76 QPointF location = layer.location;
77 QSizeF layerDims(layer.image.width(), layer.image.height());
78 QRegion region;
79 if (layer.repeats) {
80 if (location.x() + layerDims.width() < 0) {
81 location.setX(std::fmod(location.x(), layerDims.width()));
82 }
83 if (location.y() + layerDims.height() < 0) {
84 location.setY(std::fmod(location.y(), layerDims.height()));
85 }
86
87 region += layer.mask.translated(location.x(), location.y());
88 region += layer.mask.translated(location.x() + layerDims.width(), location.y());
89 region += layer.mask.translated(location.x(), location.y() + layerDims.height());
90 region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
91 } else {
92 region = layer.mask.translated(location.x(), location.y());
93 }
94
95 if (region.contains(QPoint(coord.x(), coord.y()))) {
96 out = &layer;
97 return true;
98 }
99 }
100 return false;
101}
102
103void FrameView::selectLayer(const QPointF& coord) {
104 Layer* layer;
105 if (!lookupLayer(coord, layer)) {
106 return;
107 }
108 if (layer->id == m_active) {
109 m_active = {};
110 } else {
111 m_active = layer->id;
112 }
113 m_glowFrame = 0;
114}
115
116void FrameView::disableLayer(const QPointF& coord) {
117 Layer* layer;
118 if (!lookupLayer(coord, layer)) {
119 return;
120 }
121 layer->enabled = false;
122 m_disabled.insert(layer->id);
123}
124
125void FrameView::updateTilesGBA(bool force) {
126 if (m_ui.freeze->checkState() == Qt::Checked) {
127 return;
128 }
129 m_queue.clear();
130 {
131 CoreController::Interrupter interrupter(m_controller);
132 updateRendered();
133
134 uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
135 QRgb backdrop = M_RGB5_TO_RGB8(static_cast<GBA*>(m_controller->thread()->core->board)->video.palette[0]);
136 int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]);
137
138 std::array<bool, 4> enabled{
139 bool(GBARegisterDISPCNTIsBg0Enable(io[REG_DISPCNT >> 1])),
140 bool(GBARegisterDISPCNTIsBg1Enable(io[REG_DISPCNT >> 1])),
141 bool(GBARegisterDISPCNTIsBg2Enable(io[REG_DISPCNT >> 1])),
142 bool(GBARegisterDISPCNTIsBg3Enable(io[REG_DISPCNT >> 1])),
143 };
144
145 for (int priority = 0; priority < 4; ++priority) {
146 for (int sprite = 0; sprite < 128; ++sprite) {
147 ObjInfo info;
148 lookupObj(sprite, &info);
149
150 if (!info.enabled || info.priority != priority) {
151 continue;
152 }
153
154 QPointF offset(info.x, info.y);
155 QImage obj(compositeObj(info));
156 if (info.hflip || info.vflip) {
157 obj = obj.mirrored(info.hflip, info.vflip);
158 }
159 m_queue.append({
160 { LayerId::SPRITE, sprite },
161 !m_disabled.contains({ LayerId::SPRITE, sprite }),
162 QPixmap::fromImage(obj),
163 {}, offset, false
164 });
165 if (m_queue.back().image.hasAlpha()) {
166 m_queue.back().mask = QRegion(m_queue.back().image.mask());
167 } else {
168 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
169 }
170 }
171
172 for (int bg = 0; bg < 4; ++bg) {
173 if (!enabled[bg]) {
174 continue;
175 }
176 if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) {
177 continue;
178 }
179
180 QPointF offset;
181 if (mode == 0) {
182 offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF));
183 offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF));
184 };
185 m_queue.append({
186 { LayerId::BACKGROUND, bg },
187 !m_disabled.contains({ LayerId::BACKGROUND, bg }),
188 QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])),
189 {}, offset, true
190 });
191 if (m_queue.back().image.hasAlpha()) {
192 m_queue.back().mask = QRegion(m_queue.back().image.mask());
193 } else {
194 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
195 }
196 }
197 }
198 QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono);
199 backdropImage.fill(1);
200 backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 });
201 m_queue.append({
202 { LayerId::BACKDROP },
203 !m_disabled.contains({ LayerId::BACKDROP }),
204 QPixmap::fromImage(backdropImage),
205 {}, {0, 0}, false
206 });
207 }
208 invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
209}
210
211void FrameView::updateTilesGB(bool force) {
212 if (m_ui.freeze->checkState() == Qt::Checked) {
213 return;
214 }
215 m_queue.clear();
216 {
217 CoreController::Interrupter interrupter(m_controller);
218 updateRendered();
219 }
220 invalidateQueue(m_controller->screenDimensions());
221}
222
223void FrameView::invalidateQueue(const QSize& dims) {
224 if (dims.isValid()) {
225 m_dims = dims;
226 }
227 bool blockSignals = m_ui.queue->blockSignals(true);
228 QPixmap composited(m_dims);
229
230 QPainter painter(&composited);
231 QPalette palette;
232 QColor activeColor = palette.color(QPalette::HighlightedText);
233 activeColor.setAlpha(sin(m_glowFrame * M_PI / 60) * 16 + 96);
234
235 QRectF rect(0, 0, m_dims.width(), m_dims.height());
236 painter.setCompositionMode(QPainter::CompositionMode_Source);
237 painter.fillRect(rect, QColor(0, 0, 0, 0));
238
239 painter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
240 for (int i = 0; i < m_queue.count(); ++i) {
241 const Layer& layer = m_queue[i];
242 QListWidgetItem* item;
243 if (i >= m_ui.queue->count()) {
244 item = new QListWidgetItem;
245 m_ui.queue->addItem(item);
246 } else {
247 item = m_ui.queue->item(i);
248 }
249 item->setText(layer.id.readable());
250 item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
251 item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked);
252 item->setData(Qt::UserRole, i);
253 item->setSelected(layer.id == m_active);
254
255 if (!layer.enabled) {
256 continue;
257 }
258
259 QPointF location = layer.location;
260 QSizeF layerDims(layer.image.width(), layer.image.height());
261 QRegion region;
262 if (layer.repeats) {
263 if (location.x() + layerDims.width() < 0) {
264 location.setX(std::fmod(location.x(), layerDims.width()));
265 }
266 if (location.y() + layerDims.height() < 0) {
267 location.setY(std::fmod(location.y(), layerDims.height()));
268 }
269
270 if (layer.id == m_active) {
271 region = layer.mask.translated(location.x(), location.y());
272 region += layer.mask.translated(location.x() + layerDims.width(), location.y());
273 region += layer.mask.translated(location.x(), location.y() + layerDims.height());
274 region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
275 }
276 } else {
277 QRectF layerRect(location, layerDims);
278 if (!rect.intersects(layerRect)) {
279 continue;
280 }
281 if (layer.id == m_active) {
282 region = layer.mask.translated(location.x(), location.y());
283 }
284 }
285
286 if (layer.id == m_active) {
287 painter.setClipping(true);
288 painter.setClipRegion(region);
289 painter.fillRect(rect, activeColor);
290 painter.setClipping(false);
291 }
292
293 if (layer.repeats) {
294 painter.drawPixmap(location, layer.image);
295 painter.drawPixmap(location + QPointF(layerDims.width(), 0), layer.image);
296 painter.drawPixmap(location + QPointF(0, layerDims.height()), layer.image);
297 painter.drawPixmap(location + QPointF(layerDims.width(), layerDims.height()), layer.image);
298 } else {
299 painter.drawPixmap(location, layer.image);
300 }
301 }
302 painter.end();
303
304 while (m_ui.queue->count() > m_queue.count()) {
305 delete m_ui.queue->takeItem(m_queue.count());
306 }
307 m_ui.queue->blockSignals(blockSignals);
308
309 m_composited = composited.scaled(m_dims * m_ui.magnification->value());
310 m_ui.compositedView->setPixmap(m_composited);
311}
312
313void FrameView::updateRendered() {
314 if (m_ui.freeze->checkState() == Qt::Checked) {
315 return;
316 }
317 m_rendered.convertFromImage(m_controller->getPixels());
318 QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value());
319 m_ui.renderedView->setPixmap(rendered);
320}
321
322bool FrameView::eventFilter(QObject* obj, QEvent* event) {
323 QPointF pos;
324 switch (event->type()) {
325 case QEvent::MouseButtonPress:
326 pos = static_cast<QMouseEvent*>(event)->localPos();
327 pos /= m_ui.magnification->value();
328 selectLayer(pos);
329 return true;
330 case QEvent::MouseButtonDblClick:
331 pos = static_cast<QMouseEvent*>(event)->localPos();
332 pos /= m_ui.magnification->value();
333 disableLayer(pos);
334 return true;
335 }
336 return false;
337}
338
339QString FrameView::LayerId::readable() const {
340 QString typeStr;
341 switch (type) {
342 case NONE:
343 return tr("None");
344 case BACKGROUND:
345 typeStr = tr("Background");
346 break;
347 case WINDOW:
348 typeStr = tr("Window");
349 break;
350 case SPRITE:
351 typeStr = tr("Sprite");
352 break;
353 case BACKDROP:
354 typeStr = tr("Backdrop");
355 break;
356 }
357 if (index < 0) {
358 return typeStr;
359 }
360 return tr("%1 %2").arg(typeStr).arg(index);
361}