Mercurial > hg > tilerswift
view widgets.py @ 104:dd2a309eefa9
refactor: move widgets into their own module
This allows multiple ROM canvases to be opened at once, and each one
has its own palette, which can change the palette in the tile picker.
author | Jordi Gutiérrez Hermoso <jordigh@octave.org> |
---|---|
date | Sun, 15 Sep 2019 22:03:03 -0400 |
parents | |
children | 37e3f2781fdf |
line wrap: on
line source
from PyQt5 import QtCore as QC, QtGui as QG, QtWidgets as QW from colours import NES_PALETTE, TILE_PALETTES, is_dark, widget_icon_path from nes import NES_ROM, Tile BUTTON_SIZE = 32 class GridHolder(object): def __init__(self, things, numrows, numcols, fillvalue=None): self.numrows = numrows self.numcols = numcols self.fillvalue = fillvalue if things is None: self.things = [] for i in range(self.numrows): self.things.extend([fillvalue]*self.numcols) else: self.things = things def __getitem__(self, idx): i, j = idx try: return self.things[i*self.numcols + j] except IndexError: return self.fillvalue def __setitem__(self, idx, value): i, j = idx self.things[i*self.numcols + j] = value def __repr__(self): return f"GridHolder(numrows={self.numrows}, numcols={self.numcols}, fillvalue={self.fillvalue})" def __iter__(self): for thing in self.things: yield thing def __len__(self): return len(self.things) def resize(self, numrows, numcols): self.numrows = numrows self.numcols = numcols class TileGrid(QW.QWidget): DEFAULT_SCALE_FACTOR = 5 def __init__(self, numrows, numcols, scalefactor, spacing): super().__init__() self.numcols = numcols self.numrows = numrows self.scalefactor = scalefactor self.spacing = spacing self.setStyleSheet(self.CSS) def paintEvent(self, event): painter = QG.QPainter(self) # I don't really know what this block of code means, but it's # what I have to do in order to get this widget to obey CSS # styles. opt = QW.QStyleOption() opt.initFrom(self) style = self.style() style.drawPrimitive(QW.QStyle.PE_Widget, opt, painter, self) # Let's be a little economical and only repaint the visible region x_start, y_start, x_end, y_end = event.rect().getCoords() i_start, j_start = self.coordsToTileIndices(x_start, y_start) i_end, j_end = self.coordsToTileIndices(x_end, y_end) for i in range(i_start, i_end+1): for j in range(j_start, j_end+1): tile = self.tiles[i, j] if tile: rect = self.getRectangle(i, j) flipx, flipy = self.flips[i, j] painter.drawPixmap( rect, tile.pixmap.transformed(QG.QTransform().scale(flipx, flipy)) ) def getRectangle(self, i, j): return QC.QRect( self.spacing + j*self.tilesize, self.spacing + i*self.tilesize, self.tilesize - self.spacing, self.tilesize - self.spacing ) def coordsToTileIndices(self, x, y): j, i = ( (x - self.spacing)//self.tilesize, (y - self.spacing)//self.tilesize, ) return (i, j) def resize(self): self.tilesize = self.spacing + self.scalefactor*8 self.setFixedSize(QC.QSize( self.tilesize*self.numcols + self.spacing, self.tilesize*self.numrows + self.spacing, )) self.tiles.resize(self.numrows, self.numcols) self.update() def zoomOut(self): self.scalefactor -= 1 if self.scalefactor < 1: self.scalefactor = 1 return self.resize() def zoomIn(self): self.scalefactor += 1 if self.scalefactor > 10: self.scalefactor = 10 return self.resize() def zoomOriginal(self): self.scalefactor = self.DEFAULT_SCALE_FACTOR self.resize() def computeNumRows(self): pass def increaseCols(self): maxcols = len(self.tiles) self.numcols += 1 if self.numcols > maxcols: self.numcols = maxcols self.computeNumRows() self.resize() def decreaseCols(self): self.numcols -= 1 if self.numcols < 1: self.numcols = 1 self.computeNumRows() self.resize() class ROMCanvas(TileGrid): CSS = """ background-image: url(img/checkerboard.png); background-repeat: repeat-xy; """ def __init__(self, rom, tile_picker): self.rom = rom self.tile_picker = tile_picker tiles = rom.tiles super().__init__( numcols=16, numrows=len(tiles)//16, spacing=2, scalefactor=5 ) self.tiles = GridHolder(tiles, self.numrows, self.numcols) self.flips = GridHolder(None, self.numrows, self.numcols, fillvalue=(1, 1)) self.picked_tile = None self.resize() def computeNumRows(self): self.numrows = len(self.tiles)//self.numcols def mousePressEvent(self, event): i, j = self.coordsToTileIndices(event.x(), event.y()) tile = self.tiles[i, j] self.tile_picker.picked_tile = tile class TilePicker(TileGrid): CSS = """ background-image: url(img/grey-checkerboard.png); background-repeat: repeat-xy; """ def __init__(self, rom_canvas=None): super().__init__( numcols=50, numrows=30, spacing=0, scalefactor=5, ) self.tiles = GridHolder(None, self.numrows, self.numcols) self.flips = GridHolder(None, self.numcols, self.numcols, fillvalue=(1, 1)) self.picked_tile = None self.resize() def mousePressEvent(self, event): if not self.picked_tile: return i, j = self.coordsToTileIndices(event.x(), event.y()) if event.button() == QC.Qt.LeftButton: self.tiles[i, j] = self.picked_tile self.flips[i, j] = (1, 1) elif event.button() == QC.Qt.RightButton: self.tiles[i, j] = None elif event.button() == QC.Qt.MidButton: flipx, flipy = self.flips[i, j] if flipx == 1: if flipy == 1: self.flips[i, j] = (1, -1) else: self.flips[i, j] = (-1, 1) else: if flipy == 1: self.flips[i, j] = (-1, -1) else: self.flips[i, j] = (1, 1) self.update() def mouseMoveEvent(self, event): i, j = self.coordsToTileIndices(event.x(), event.y()) class ColourPicker(QW.QDialog): def __init__(self): super().__init__() self.setWindowTitle("Pick a colour") layout = QW.QGridLayout() layout.setSpacing(0) for i in range(4): for j in range(16): colour_idx = i*16 + j button = ColourButton(colour_idx) button.pressed.connect(lambda c=colour_idx: self.colour_picked(c)) layout.addWidget(button, i, j) colours = QW.QWidget() layout.setSizeConstraint(QW.QLayout.SetFixedSize) colours.setLayout(layout) vlayout = QW.QVBoxLayout() vlayout.setSpacing(0) vlayout.setSizeConstraint(QW.QLayout.SetFixedSize) vlayout.addWidget(colours) transparent_button = QW.QPushButton("Transparent") transparent_button.pressed.connect(lambda: self.colour_picked(None)) vlayout.addWidget(transparent_button) self.setLayout(vlayout) def colour_picked(self, colour_idx): self.picked_idx = colour_idx self.accept() class ColourButton(QW.QPushButton): def __init__(self, colour_idx): super().__init__() self.setFixedSize(QC.QSize(BUTTON_SIZE, BUTTON_SIZE)) self.colour_idx = colour_idx self.set_colour(colour_idx) self.setToolTip("Change colour") def set_colour(self, colour_idx): self.colour_idx = colour_idx if colour_idx is None: # Enable transparency self.setText("") self.setStyleSheet("") return self.setText(f"{colour_idx:0{2}X}") bgcolour = NES_PALETTE[colour_idx] qt_colour = QG.QColor(bgcolour) textcolour = 'white' if is_dark(qt_colour) else 'black' bordercolour = "#444444" if qt_colour.rgb() == 0 else qt_colour.darker().name() self.setStyleSheet( f''' QPushButton {{ color: {textcolour}; background: {bgcolour}; border: 2px outset {bordercolour}; border-radius: 4px; }} QPushButton:pressed {{ border-style: inset; }}''' ) class ScrollAreaWithVerticalBar(QW.QScrollArea): def sizeHint(self): hint = super().sizeHint() bar_width = self.verticalScrollBar().sizeHint().width() return QC.QSize(hint.width() + bar_width, hint.height()) class PaletteButton(ColourButton): def __init__(self, button_idx, colour_idx): super().__init__(colour_idx) self.button_idx = button_idx class TilePaletteButtons(QW.QWidget): def __init__(self, palette, callback=None): super().__init__() self.layout = QW.QHBoxLayout() self.layout.setSpacing(0) margins = self.layout.contentsMargins() margins.setLeft(0) self.layout.setContentsMargins(margins) self.layout.setSizeConstraint(QW.QLayout.SetFixedSize) for button_idx, color_idx in enumerate(palette): button = PaletteButton(button_idx, color_idx) if callback: button.pressed.connect(lambda idx=button_idx: callback(idx)) self.layout.addWidget(button) self.setLayout(self.layout) class ROMDockable(QW.QDockWidget): def __init__(self, filename, filetype, tile_picker): super().__init__(filename.split("/")[-1]) rom = NES_ROM(filename) self.rom_canvas = ROMCanvas(rom, tile_picker) self.colour_picker = ColourPicker() scroll = ScrollAreaWithVerticalBar(self) scroll.setWidget(self.rom_canvas) self.toolbar = QW.QHBoxLayout() palette_layout = QW.QHBoxLayout() palette_layout.setSpacing(0) palette_layout.setSizeConstraint(QW.QLayout.SetFixedSize) self.rom_palette_buttons = TilePaletteButtons( Tile.default_palette, callback=self.pick_palette_colour ) palette_layout.addWidget(self.rom_palette_buttons) self.rom_palette = QW.QWidget() self.rom_palette.setLayout(palette_layout) self.toolbar.addWidget(self.rom_palette) for icon, tooltip, function in [ ("zoom-in", "Zoom in", "zoomIn"), ("zoom-out", "Zoom out", "zoomOut"), ("zoom-original", "Restore zoom", "zoomOriginal"), ("increaseCols", "Increase number of columns", "increaseCols"), ("decreaseCols", "Decrease number of columns", "decreaseCols"), ]: button = QW.QPushButton("") iconpath = widget_icon_path(button) button.setIcon(QG.QIcon(f"img/{iconpath}/{icon}.svg")) button.setFixedSize(QC.QSize(32, 32)) button.setIconSize(QC.QSize(24, 24)) button.setToolTip(tooltip) button.pressed.connect(getattr(self.rom_canvas, function)) self.toolbar.addWidget(button) self.toolbar.addStretch() layout = QW.QVBoxLayout() layout.addWidget(scroll) toolbar_widget = QW.QWidget() toolbar_widget.setLayout(self.toolbar) layout.addWidget(toolbar_widget) rom_canvas_widget = QW.QWidget() rom_canvas_widget.setLayout(layout) self.setWidget(rom_canvas_widget) iconpath = widget_icon_path(self) self.setStyleSheet( f""" QDockWidget {{ titlebar-close-icon: url(img/{iconpath}/widget-close.svg); titlebar-normal-icon: url(img/{iconpath}/widget-undock.svg); }} """ ) def pick_palette_colour(self, palette_idx): if self.colour_picker.exec_(): new_colour_idx = self.colour_picker.picked_idx self.rom_palette_buttons.layout.itemAt(palette_idx).widget().set_colour(new_colour_idx) for tile in self.rom_canvas.tiles: palette = tile.palette palette[palette_idx] = new_colour_idx tile.set_palette(palette) self.rom_canvas.update() self.rom_canvas.tile_picker.update() def change_palette(self, name): self.rom_canvas.palette = TILE_PALETTES[name] for tile in self.rom_canvas.tiles: tile.set_palette(self.rom_canvas.palette.copy()) for button_idx, colour_idx in enumerate(self.rom_canvas.palette): self.rom_palette_buttons.layout.itemAt(button_idx).widget().set_colour(colour_idx) self.rom_canvas.update() self.rom_canvas.tile_picker.update()