mirror of
https://github.com/devinacker/bsnes-plus.git
synced 2025-04-02 10:52:46 -04:00
407 lines
13 KiB
C++
407 lines
13 KiB
C++
#include "tile-viewer.moc"
|
|
|
|
TileViewer *tileViewer;
|
|
|
|
const char* TileViewer::VramBaseText[6] = {
|
|
"BG1:", "BG2:", "BG3:", "BG4:",
|
|
"OAM1:", "OAM2:"
|
|
};
|
|
|
|
TileViewer::TileViewer() {
|
|
setObjectName("tile-viewer");
|
|
setWindowTitle("Tile Viewer");
|
|
setGeometryString(&config().geometry.tileViewer);
|
|
application.windowList.append(this);
|
|
|
|
inUpdateFormCall = false;
|
|
inExportClickedCall = false;
|
|
|
|
layout = new QHBoxLayout;
|
|
layout->setSizeConstraint(QLayout::SetMinimumSize);
|
|
layout->setAlignment(Qt::AlignLeft);
|
|
layout->setMargin(Style::WindowMargin);
|
|
layout->setSpacing(Style::WidgetSpacing);
|
|
setLayout(layout);
|
|
|
|
sidebarLayout = new QFormLayout;
|
|
sidebarLayout->setSizeConstraint(QLayout::SetMinimumSize);
|
|
sidebarLayout->setRowWrapPolicy(QFormLayout::DontWrapRows);
|
|
sidebarLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
|
sidebarLayout->setFormAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
|
sidebarLayout->setLabelAlignment(Qt::AlignLeft);
|
|
layout->addLayout(sidebarLayout);
|
|
|
|
|
|
zoomCombo = new QComboBox;
|
|
zoomCombo->addItem("1x", QVariant(1));
|
|
zoomCombo->addItem("2x", QVariant(2));
|
|
zoomCombo->addItem("3x", QVariant(3));
|
|
zoomCombo->addItem("4x", QVariant(4));
|
|
zoomCombo->addItem("5x", QVariant(5));
|
|
zoomCombo->addItem("6x", QVariant(6));
|
|
zoomCombo->addItem("7x", QVariant(7));
|
|
zoomCombo->addItem("8x", QVariant(8));
|
|
zoomCombo->addItem("9x", QVariant(9));
|
|
|
|
showGrid = new QCheckBox("Show Grid");
|
|
sidebarLayout->addRow(zoomCombo, showGrid);
|
|
|
|
autoUpdateBox = new QCheckBox("Auto update");
|
|
sidebarLayout->addRow("", autoUpdateBox);
|
|
|
|
|
|
buttonLayout = new QHBoxLayout;
|
|
|
|
exportButton = new QPushButton("Export");
|
|
buttonLayout->addWidget(exportButton);
|
|
|
|
refreshButton = new QPushButton("Refresh");
|
|
buttonLayout->addWidget(refreshButton);
|
|
|
|
sidebarLayout->addRow(buttonLayout);
|
|
|
|
|
|
sidebarLayout->addRow(new QWidget);
|
|
|
|
source = new QComboBox;
|
|
source->addItem("VRAM", QVariant(TileRenderer::VRAM));
|
|
source->addItem("S-CPU Bus", QVariant(TileRenderer::CPU_BUS));
|
|
source->addItem("Cartridge ROM", QVariant(TileRenderer::CART_ROM));
|
|
source->addItem("Cartridge RAM", QVariant(TileRenderer::CART_RAM));
|
|
source->addItem("SA1 Bus", QVariant(TileRenderer::SA1_BUS));
|
|
source->addItem("SFX Bus", QVariant(TileRenderer::SFX_BUS));
|
|
sidebarLayout->addRow("Source:", source);
|
|
|
|
|
|
addressLayout = new QHBoxLayout;
|
|
|
|
prevAddressButton = new QToolButton;
|
|
prevAddressButton->setToolTip("Previous");
|
|
prevAddressButton->setIcon(QIcon(":16x16/mem-prev-unknown.png"));
|
|
addressLayout->addWidget(prevAddressButton);
|
|
|
|
address = new QLineEdit;
|
|
address->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
address->setMinimumWidth(7 * address->fontMetrics().horizontalAdvance('0'));
|
|
addressLayout->addWidget(address, 1);
|
|
|
|
nextAddressButton = new QToolButton;
|
|
nextAddressButton->setToolTip("Next");
|
|
nextAddressButton->setIcon(QIcon(":16x16/mem-next-unknown.png"));
|
|
addressLayout->addWidget(nextAddressButton);
|
|
|
|
sidebarLayout->addRow("Address:", addressLayout);
|
|
|
|
|
|
bitDepth = new QComboBox;
|
|
bitDepth->addItem("2bpp", QVariant(TileRenderer::BPP2));
|
|
bitDepth->addItem("4bpp", QVariant(TileRenderer::BPP4));
|
|
bitDepth->addItem("8bpp", QVariant(TileRenderer::BPP8));
|
|
bitDepth->addItem("Mode 7", QVariant(TileRenderer::MODE7));
|
|
bitDepth->addItem("Mode 7 EXTBG", QVariant(TileRenderer::MODE7_EXTBG));
|
|
sidebarLayout->addRow("Bit Depth:", bitDepth);
|
|
|
|
widthSpinBox = new QSpinBox;
|
|
widthSpinBox->setMinimum(8);
|
|
widthSpinBox->setMaximum(64);
|
|
widthSpinBox->setValue(16);
|
|
sidebarLayout->addRow("Width:", widthSpinBox);
|
|
|
|
|
|
sidebarLayout->addRow(new QWidget);
|
|
|
|
overrideBackgroundColor = new QCheckBox("Override Background Color");
|
|
sidebarLayout->addRow(overrideBackgroundColor);
|
|
|
|
customBgColorCombo = new QComboBox;
|
|
customBgColorCombo->addItem("Transparent", QVariant(qRgba(0, 0, 0, 0)));
|
|
customBgColorCombo->addItem("Magenta", QVariant(qRgb(255, 0, 255)));
|
|
customBgColorCombo->addItem("Cyan", QVariant(qRgb(0, 255, 255)));
|
|
customBgColorCombo->addItem("White", QVariant(qRgb(255, 255, 255)));
|
|
customBgColorCombo->addItem("Black", QVariant(qRgb(0, 0, 0)));
|
|
sidebarLayout->addRow("BG Color:", customBgColorCombo);
|
|
|
|
|
|
sidebarLayout->addRow(new QWidget);
|
|
|
|
useCgram = new QCheckBox("Use CGRAM");
|
|
sidebarLayout->addRow(useCgram);
|
|
|
|
cgramWidget = new CgramWidget;
|
|
cgramWidget->setScale(12);
|
|
sidebarLayout->addRow(cgramWidget);
|
|
|
|
|
|
sidebarLayout->addRow(new QWidget);
|
|
|
|
sidebarLayout->addRow(new QLabel("Base Tile Addresses:"));
|
|
|
|
vramBaseLayout = new QGridLayout;
|
|
vramBaseLayout->setColumnStretch(0, 1);
|
|
sidebarLayout->addRow(vramBaseLayout);
|
|
|
|
vramBaseButtonGroup = new QButtonGroup(this);
|
|
|
|
for(unsigned i = 0; i < N_VRAM_BASE_ITEMS; i++) {
|
|
QLabel* label = new QLabel(VramBaseText[i]);
|
|
vramBaseLayout->addWidget(label, i, 1);
|
|
|
|
vramBaseAddress[i] = new QLineEdit;
|
|
vramBaseAddress[i]->setReadOnly(true);
|
|
vramBaseAddress[i]->setFixedWidth(9 * vramBaseAddress[i]->fontMetrics().horizontalAdvance('0'));
|
|
vramBaseLayout->addWidget(vramBaseAddress[i], i, 2);
|
|
|
|
vramBaseButton[i] = new QToolButton;
|
|
vramBaseButton[i]->setText("goto");
|
|
vramBaseLayout->addWidget(vramBaseButton[i], i, 3);
|
|
|
|
vramBaseButtonGroup->addButton(vramBaseButton[i], i);
|
|
}
|
|
|
|
|
|
sidebarLayout->addRow(new QWidget);
|
|
|
|
tileInfo = new QLabel;
|
|
sidebarLayout->addRow(tileInfo);
|
|
|
|
|
|
imageGridWidget = new ImageGridWidget();
|
|
imageGridWidget->setMinimumSize(256, 256);
|
|
imageGridWidget->setGridSize(8);
|
|
imageGridWidget->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
|
layout->addWidget(imageGridWidget, 10);
|
|
|
|
|
|
zoomCombo->setCurrentIndex(3 - 1);
|
|
onZoomChanged(3 - 1);
|
|
updateForm();
|
|
|
|
connect(exportButton, SIGNAL(clicked(bool)), this, SLOT(onExportClicked()));
|
|
connect(refreshButton, SIGNAL(released()), this, SLOT(refresh()));
|
|
connect(zoomCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onZoomChanged(int)));
|
|
connect(showGrid, SIGNAL(clicked(bool)), imageGridWidget, SLOT(setShowGrid(bool)));
|
|
|
|
connect(source, SIGNAL(activated(int)), this, SLOT(refresh()));
|
|
connect(address, SIGNAL(textEdited(const QString&)), this, SLOT(refresh()));
|
|
connect(bitDepth, SIGNAL(activated(int)), this, SLOT(refresh()));
|
|
connect(widthSpinBox, SIGNAL(valueChanged(int)), this, SLOT(refresh()));
|
|
|
|
connect(prevAddressButton, SIGNAL(clicked(bool)), this, SLOT(onPrevAddressButtonClicked()));
|
|
connect(nextAddressButton, SIGNAL(clicked(bool)), this, SLOT(onNextAddressButtonClicked()));
|
|
|
|
connect(overrideBackgroundColor, SIGNAL(clicked(bool)), this, SLOT(refresh()));
|
|
connect(customBgColorCombo, SIGNAL(activated(int)), this, SLOT(refresh()));
|
|
|
|
connect(useCgram, SIGNAL(clicked()), this, SLOT(onUseCgramPressed()));
|
|
connect(cgramWidget, SIGNAL(selectedChanged()), this, SLOT(refresh()));
|
|
|
|
connect(imageGridWidget, SIGNAL(selectedChanged()), this, SLOT(updateTileInfo()));
|
|
|
|
connect(vramBaseButtonGroup, SIGNAL(buttonClicked(int)), this, SLOT(onVramBaseButtonClicked(int)));
|
|
}
|
|
|
|
void TileViewer::autoUpdate() {
|
|
if(isVisible() && autoUpdateBox->isChecked()) refresh();
|
|
}
|
|
|
|
void TileViewer::show() {
|
|
Window::show();
|
|
refresh();
|
|
}
|
|
|
|
void TileViewer::refresh() {
|
|
if(inUpdateFormCall || inExportClickedCall) return;
|
|
|
|
updateRendererSettings();
|
|
|
|
if(SNES::cartridge.loaded()) {
|
|
cgramWidget->refresh();
|
|
|
|
renderer.draw();
|
|
imageGridWidget->setImage(renderer.image);
|
|
}
|
|
|
|
updateForm();
|
|
updateTileInfo();
|
|
}
|
|
|
|
void TileViewer::onZoomChanged(int index) {
|
|
unsigned z = zoomCombo->itemData(index).toUInt();
|
|
imageGridWidget->setZoom(z);
|
|
}
|
|
|
|
void TileViewer::onExportClicked() {
|
|
if(renderer.image.isNull()) return;
|
|
|
|
inExportClickedCall = true;
|
|
|
|
QString selectedFile = QFileDialog::getSaveFileName(
|
|
this, "Export Tiles", config().path.current.exportVRAM, "PNG Image (*.png)");
|
|
|
|
if(!selectedFile.isEmpty()) {
|
|
QImageWriter writer(selectedFile, "PNG");
|
|
if(!writer.write(renderer.image)) {
|
|
QMessageBox::critical(this, "Export Tiles", "Unable to export tiles:\n\n" + writer.errorString());
|
|
}
|
|
config().path.current.exportVRAM = selectedFile;
|
|
}
|
|
|
|
inExportClickedCall = false;
|
|
}
|
|
|
|
void TileViewer::onUseCgramPressed() {
|
|
if(useCgram->isChecked()) {
|
|
if(!cgramWidget->hasSelected()) cgramWidget->setSelected(0);
|
|
} else {
|
|
cgramWidget->selectNone();
|
|
}
|
|
}
|
|
|
|
void TileViewer::onVramBaseButtonClicked(int index) {
|
|
if(!SNES::cartridge.loaded()) return;
|
|
|
|
unsigned addr = getVramBaseAddress(index);
|
|
|
|
source->setCurrentIndex(source->findData(TileRenderer::VRAM));
|
|
address->clear();
|
|
|
|
TileRenderer::BitDepth bd = TileRenderer::BitDepth::NONE;
|
|
if(index < 4) {
|
|
unsigned screenMode = SNES::ppu.bg_mode() & 7;
|
|
if(screenMode < 7) bd = TileRenderer::bitDepthForLayer(screenMode, index);
|
|
}
|
|
if(index >= 4) bd = TileRenderer::BitDepth::BPP4;
|
|
|
|
if(bd != TileRenderer::BitDepth::NONE) {
|
|
bitDepth->setCurrentIndex(bitDepth->findData(bd));
|
|
}
|
|
|
|
refresh();
|
|
unsigned tileId = addr / renderer.bytesInbetweenTiles();
|
|
|
|
QPoint cell(tileId % renderer.width, tileId / renderer.width);
|
|
imageGridWidget->setSelected(cell);
|
|
imageGridWidget->scrollToCell(cell);
|
|
}
|
|
|
|
void TileViewer::onPrevAddressButtonClicked() {
|
|
stepAdddressField(false);
|
|
}
|
|
|
|
void TileViewer::onNextAddressButtonClicked() {
|
|
stepAdddressField(true);
|
|
}
|
|
|
|
void TileViewer::stepAdddressField(bool forward) {
|
|
unsigned step = renderer.bytesInbetweenTiles();
|
|
if(renderer.source != TileRenderer::VRAM) step *= renderer.nTiles();
|
|
|
|
if(forward) {
|
|
unsigned max = renderer.maxAddress();
|
|
unsigned a = renderer.address + step;
|
|
if(a >= max) a = max - step;
|
|
if(step > max) a = 0;
|
|
renderer.address = a;
|
|
} else {
|
|
if(renderer.address >= step) {
|
|
renderer.address -= step;
|
|
} else {
|
|
renderer.address = 0;
|
|
}
|
|
}
|
|
renderer.address &= renderer.addressMask();
|
|
|
|
if(renderer.source == TileRenderer::VRAM) {
|
|
if(SNES::memory::vram.size() > 1<<16)
|
|
address->setText(hex<5>(renderer.address + SNES::ppu.vram_start_addr()));
|
|
else
|
|
address->setText(hex<4>(renderer.address));
|
|
} else {
|
|
address->setText(hex<6>(renderer.address));
|
|
}
|
|
|
|
refresh();
|
|
}
|
|
|
|
void TileViewer::updateRendererSettings() {
|
|
typedef TileRenderer::Source Source;
|
|
typedef TileRenderer::BitDepth Depth;
|
|
|
|
int si = source->currentIndex();
|
|
renderer.source = si >= 0 ? Source(source->itemData(si).toInt()) : Source::VRAM;
|
|
|
|
renderer.address = hex(address->text().toUtf8().data()) & renderer.addressMask();
|
|
|
|
int bd = bitDepth->currentIndex();
|
|
renderer.bitDepth = bd >= 0 ? Depth(bitDepth->itemData(bd).toInt()) : Depth::NONE;
|
|
|
|
int ci = customBgColorCombo->currentIndex();
|
|
renderer.overrideBackgroundColor = overrideBackgroundColor->isChecked();
|
|
renderer.customBackgroundColor = customBgColorCombo->itemData(ci).toUInt();
|
|
|
|
renderer.width = widthSpinBox->value();
|
|
|
|
if(cgramWidget->hasSelected()) {
|
|
renderer.paletteOffset = cgramWidget->selectedColor();
|
|
useCgram->setChecked(true);
|
|
}
|
|
|
|
renderer.useCgramPalette = useCgram->isChecked();
|
|
}
|
|
|
|
void TileViewer::updateForm() {
|
|
inUpdateFormCall = true;
|
|
|
|
exportButton->setEnabled(!renderer.image.isNull());
|
|
|
|
source->setCurrentIndex(source->findData(renderer.source));
|
|
|
|
bitDepth->setCurrentIndex(bitDepth->findData(renderer.bitDepth));
|
|
|
|
cgramWidget->setPaletteSize(renderer.colorsPerTile());
|
|
|
|
customBgColorCombo->setEnabled(overrideBackgroundColor->isChecked());
|
|
|
|
for(unsigned i = 0; i < N_VRAM_BASE_ITEMS; i++) {
|
|
unsigned a = getVramBaseAddress(i);
|
|
if(SNES::memory::vram.size() > 1<<16)
|
|
vramBaseAddress[i]->setText(string("0x", hex<5>(a + SNES::ppu.vram_start_addr())));
|
|
else
|
|
vramBaseAddress[i]->setText(string("0x", hex<4>(a)));
|
|
}
|
|
|
|
inUpdateFormCall = false;
|
|
}
|
|
|
|
void TileViewer::updateTileInfo() {
|
|
if(!SNES::cartridge.loaded()) { tileInfo->clear(); return; }
|
|
if(!imageGridWidget->selectionValid()) { tileInfo->clear(); return; }
|
|
|
|
unsigned tileId = imageGridWidget->selected().y() * renderer.width + imageGridWidget->selected().x();
|
|
|
|
string text;
|
|
if(tileId < renderer.nTiles()) {
|
|
unsigned tileAddr = renderer.address + (tileId * renderer.bytesInbetweenTiles());
|
|
if(renderer.isMode7()) tileAddr++;
|
|
|
|
if(renderer.source == TileRenderer::VRAM) {
|
|
if (SNES::memory::vram.size() > 1<<16) {
|
|
text = string("Selected Tile Address: 0x", hex<5>(tileAddr + SNES::ppu.vram_start_addr()));
|
|
} else {
|
|
text = string("Selected Tile Address: 0x", hex<4>(tileAddr & 0xffff));
|
|
}
|
|
} else {
|
|
text = string("Selected Tile Address: 0x", hex<6>(tileAddr & 0xffffff));
|
|
}
|
|
} else {
|
|
imageGridWidget->selectNone();
|
|
}
|
|
|
|
tileInfo->setText(text);
|
|
}
|
|
|
|
unsigned TileViewer::getVramBaseAddress(unsigned index) {
|
|
if(index < 4) return SNES::ppu.bg_tile_addr(index);
|
|
if(index >= 4 && index < 6) return SNES::ppu.oam_tile_addr(index - 4);
|
|
return 0;
|
|
}
|