Mercurial > hg > tilerswift
comparison 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 (2019-09-16) |
parents | |
children | 37e3f2781fdf |
comparison
equal
deleted
inserted
replaced
103:2d1ffeafb003 | 104:dd2a309eefa9 |
---|---|
1 from PyQt5 import QtCore as QC, QtGui as QG, QtWidgets as QW | |
2 | |
3 from colours import NES_PALETTE, TILE_PALETTES, is_dark, widget_icon_path | |
4 | |
5 from nes import NES_ROM, Tile | |
6 | |
7 BUTTON_SIZE = 32 | |
8 | |
9 | |
10 class GridHolder(object): | |
11 def __init__(self, things, numrows, numcols, fillvalue=None): | |
12 self.numrows = numrows | |
13 self.numcols = numcols | |
14 self.fillvalue = fillvalue | |
15 if things is None: | |
16 self.things = [] | |
17 for i in range(self.numrows): | |
18 self.things.extend([fillvalue]*self.numcols) | |
19 else: | |
20 self.things = things | |
21 | |
22 def __getitem__(self, idx): | |
23 i, j = idx | |
24 try: | |
25 return self.things[i*self.numcols + j] | |
26 except IndexError: | |
27 return self.fillvalue | |
28 | |
29 def __setitem__(self, idx, value): | |
30 i, j = idx | |
31 self.things[i*self.numcols + j] = value | |
32 | |
33 def __repr__(self): | |
34 return f"GridHolder(numrows={self.numrows}, numcols={self.numcols}, fillvalue={self.fillvalue})" | |
35 | |
36 def __iter__(self): | |
37 for thing in self.things: | |
38 yield thing | |
39 | |
40 def __len__(self): | |
41 return len(self.things) | |
42 | |
43 def resize(self, numrows, numcols): | |
44 self.numrows = numrows | |
45 self.numcols = numcols | |
46 | |
47 | |
48 class TileGrid(QW.QWidget): | |
49 | |
50 DEFAULT_SCALE_FACTOR = 5 | |
51 | |
52 def __init__(self, numrows, numcols, scalefactor, spacing): | |
53 super().__init__() | |
54 | |
55 self.numcols = numcols | |
56 self.numrows = numrows | |
57 self.scalefactor = scalefactor | |
58 self.spacing = spacing | |
59 | |
60 self.setStyleSheet(self.CSS) | |
61 | |
62 def paintEvent(self, event): | |
63 painter = QG.QPainter(self) | |
64 | |
65 # I don't really know what this block of code means, but it's | |
66 # what I have to do in order to get this widget to obey CSS | |
67 # styles. | |
68 opt = QW.QStyleOption() | |
69 opt.initFrom(self) | |
70 style = self.style() | |
71 style.drawPrimitive(QW.QStyle.PE_Widget, opt, painter, self) | |
72 | |
73 # Let's be a little economical and only repaint the visible region | |
74 x_start, y_start, x_end, y_end = event.rect().getCoords() | |
75 i_start, j_start = self.coordsToTileIndices(x_start, y_start) | |
76 i_end, j_end = self.coordsToTileIndices(x_end, y_end) | |
77 | |
78 for i in range(i_start, i_end+1): | |
79 for j in range(j_start, j_end+1): | |
80 tile = self.tiles[i, j] | |
81 if tile: | |
82 rect = self.getRectangle(i, j) | |
83 flipx, flipy = self.flips[i, j] | |
84 painter.drawPixmap( | |
85 rect, | |
86 tile.pixmap.transformed(QG.QTransform().scale(flipx, flipy)) | |
87 ) | |
88 | |
89 def getRectangle(self, i, j): | |
90 return QC.QRect( | |
91 self.spacing + j*self.tilesize, | |
92 self.spacing + i*self.tilesize, | |
93 self.tilesize - self.spacing, | |
94 self.tilesize - self.spacing | |
95 ) | |
96 | |
97 def coordsToTileIndices(self, x, y): | |
98 j, i = ( | |
99 (x - self.spacing)//self.tilesize, | |
100 (y - self.spacing)//self.tilesize, | |
101 ) | |
102 return (i, j) | |
103 | |
104 def resize(self): | |
105 self.tilesize = self.spacing + self.scalefactor*8 | |
106 self.setFixedSize(QC.QSize( | |
107 self.tilesize*self.numcols + self.spacing, | |
108 self.tilesize*self.numrows + self.spacing, | |
109 )) | |
110 self.tiles.resize(self.numrows, self.numcols) | |
111 self.update() | |
112 | |
113 def zoomOut(self): | |
114 self.scalefactor -= 1 | |
115 if self.scalefactor < 1: | |
116 self.scalefactor = 1 | |
117 return | |
118 | |
119 self.resize() | |
120 | |
121 def zoomIn(self): | |
122 self.scalefactor += 1 | |
123 if self.scalefactor > 10: | |
124 self.scalefactor = 10 | |
125 return | |
126 | |
127 self.resize() | |
128 | |
129 def zoomOriginal(self): | |
130 self.scalefactor = self.DEFAULT_SCALE_FACTOR | |
131 | |
132 self.resize() | |
133 | |
134 def computeNumRows(self): | |
135 pass | |
136 | |
137 def increaseCols(self): | |
138 maxcols = len(self.tiles) | |
139 self.numcols += 1 | |
140 if self.numcols > maxcols: | |
141 self.numcols = maxcols | |
142 self.computeNumRows() | |
143 self.resize() | |
144 | |
145 def decreaseCols(self): | |
146 self.numcols -= 1 | |
147 if self.numcols < 1: | |
148 self.numcols = 1 | |
149 self.computeNumRows() | |
150 self.resize() | |
151 | |
152 | |
153 class ROMCanvas(TileGrid): | |
154 CSS = """ | |
155 background-image: url(img/checkerboard.png); | |
156 background-repeat: repeat-xy; | |
157 """ | |
158 | |
159 def __init__(self, rom, tile_picker): | |
160 self.rom = rom | |
161 self.tile_picker = tile_picker | |
162 tiles = rom.tiles | |
163 | |
164 super().__init__( | |
165 numcols=16, | |
166 numrows=len(tiles)//16, | |
167 spacing=2, | |
168 scalefactor=5 | |
169 ) | |
170 | |
171 self.tiles = GridHolder(tiles, self.numrows, self.numcols) | |
172 self.flips = GridHolder(None, self.numrows, self.numcols, fillvalue=(1, 1)) | |
173 | |
174 self.picked_tile = None | |
175 | |
176 self.resize() | |
177 | |
178 def computeNumRows(self): | |
179 self.numrows = len(self.tiles)//self.numcols | |
180 | |
181 def mousePressEvent(self, event): | |
182 i, j = self.coordsToTileIndices(event.x(), event.y()) | |
183 tile = self.tiles[i, j] | |
184 | |
185 self.tile_picker.picked_tile = tile | |
186 | |
187 | |
188 class TilePicker(TileGrid): | |
189 CSS = """ | |
190 background-image: url(img/grey-checkerboard.png); | |
191 background-repeat: repeat-xy; | |
192 """ | |
193 | |
194 def __init__(self, rom_canvas=None): | |
195 super().__init__( | |
196 numcols=50, | |
197 numrows=30, | |
198 spacing=0, | |
199 scalefactor=5, | |
200 ) | |
201 | |
202 self.tiles = GridHolder(None, self.numrows, self.numcols) | |
203 self.flips = GridHolder(None, self.numcols, self.numcols, fillvalue=(1, 1)) | |
204 self.picked_tile = None | |
205 | |
206 self.resize() | |
207 | |
208 def mousePressEvent(self, event): | |
209 if not self.picked_tile: | |
210 return | |
211 | |
212 i, j = self.coordsToTileIndices(event.x(), event.y()) | |
213 | |
214 if event.button() == QC.Qt.LeftButton: | |
215 self.tiles[i, j] = self.picked_tile | |
216 self.flips[i, j] = (1, 1) | |
217 elif event.button() == QC.Qt.RightButton: | |
218 self.tiles[i, j] = None | |
219 elif event.button() == QC.Qt.MidButton: | |
220 flipx, flipy = self.flips[i, j] | |
221 if flipx == 1: | |
222 if flipy == 1: | |
223 self.flips[i, j] = (1, -1) | |
224 else: | |
225 self.flips[i, j] = (-1, 1) | |
226 else: | |
227 if flipy == 1: | |
228 self.flips[i, j] = (-1, -1) | |
229 else: | |
230 self.flips[i, j] = (1, 1) | |
231 | |
232 self.update() | |
233 | |
234 def mouseMoveEvent(self, event): | |
235 i, j = self.coordsToTileIndices(event.x(), event.y()) | |
236 | |
237 | |
238 class ColourPicker(QW.QDialog): | |
239 | |
240 def __init__(self): | |
241 super().__init__() | |
242 self.setWindowTitle("Pick a colour") | |
243 | |
244 layout = QW.QGridLayout() | |
245 layout.setSpacing(0) | |
246 | |
247 for i in range(4): | |
248 for j in range(16): | |
249 colour_idx = i*16 + j | |
250 button = ColourButton(colour_idx) | |
251 button.pressed.connect(lambda c=colour_idx: self.colour_picked(c)) | |
252 layout.addWidget(button, i, j) | |
253 colours = QW.QWidget() | |
254 layout.setSizeConstraint(QW.QLayout.SetFixedSize) | |
255 colours.setLayout(layout) | |
256 | |
257 vlayout = QW.QVBoxLayout() | |
258 vlayout.setSpacing(0) | |
259 vlayout.setSizeConstraint(QW.QLayout.SetFixedSize) | |
260 vlayout.addWidget(colours) | |
261 | |
262 transparent_button = QW.QPushButton("Transparent") | |
263 | |
264 transparent_button.pressed.connect(lambda: self.colour_picked(None)) | |
265 vlayout.addWidget(transparent_button) | |
266 self.setLayout(vlayout) | |
267 | |
268 def colour_picked(self, colour_idx): | |
269 self.picked_idx = colour_idx | |
270 self.accept() | |
271 | |
272 | |
273 class ColourButton(QW.QPushButton): | |
274 def __init__(self, colour_idx): | |
275 super().__init__() | |
276 self.setFixedSize(QC.QSize(BUTTON_SIZE, BUTTON_SIZE)) | |
277 self.colour_idx = colour_idx | |
278 self.set_colour(colour_idx) | |
279 self.setToolTip("Change colour") | |
280 | |
281 def set_colour(self, colour_idx): | |
282 self.colour_idx = colour_idx | |
283 | |
284 if colour_idx is None: | |
285 # Enable transparency | |
286 self.setText("") | |
287 self.setStyleSheet("") | |
288 return | |
289 | |
290 self.setText(f"{colour_idx:0{2}X}") | |
291 | |
292 bgcolour = NES_PALETTE[colour_idx] | |
293 qt_colour = QG.QColor(bgcolour) | |
294 | |
295 textcolour = 'white' if is_dark(qt_colour) else 'black' | |
296 bordercolour = "#444444" if qt_colour.rgb() == 0 else qt_colour.darker().name() | |
297 self.setStyleSheet( | |
298 f''' | |
299 QPushButton {{ | |
300 color: {textcolour}; | |
301 background: {bgcolour}; | |
302 border: 2px outset {bordercolour}; | |
303 border-radius: 4px; | |
304 }} | |
305 QPushButton:pressed {{ | |
306 border-style: inset; | |
307 }}''' | |
308 ) | |
309 | |
310 | |
311 class ScrollAreaWithVerticalBar(QW.QScrollArea): | |
312 def sizeHint(self): | |
313 hint = super().sizeHint() | |
314 bar_width = self.verticalScrollBar().sizeHint().width() | |
315 return QC.QSize(hint.width() + bar_width, hint.height()) | |
316 | |
317 | |
318 class PaletteButton(ColourButton): | |
319 | |
320 def __init__(self, button_idx, colour_idx): | |
321 super().__init__(colour_idx) | |
322 self.button_idx = button_idx | |
323 | |
324 | |
325 class TilePaletteButtons(QW.QWidget): | |
326 def __init__(self, palette, callback=None): | |
327 super().__init__() | |
328 | |
329 self.layout = QW.QHBoxLayout() | |
330 self.layout.setSpacing(0) | |
331 margins = self.layout.contentsMargins() | |
332 margins.setLeft(0) | |
333 self.layout.setContentsMargins(margins) | |
334 self.layout.setSizeConstraint(QW.QLayout.SetFixedSize) | |
335 | |
336 for button_idx, color_idx in enumerate(palette): | |
337 button = PaletteButton(button_idx, color_idx) | |
338 if callback: | |
339 button.pressed.connect(lambda idx=button_idx: callback(idx)) | |
340 self.layout.addWidget(button) | |
341 self.setLayout(self.layout) | |
342 | |
343 | |
344 class ROMDockable(QW.QDockWidget): | |
345 | |
346 def __init__(self, filename, filetype, tile_picker): | |
347 super().__init__(filename.split("/")[-1]) | |
348 | |
349 rom = NES_ROM(filename) | |
350 | |
351 self.rom_canvas = ROMCanvas(rom, tile_picker) | |
352 self.colour_picker = ColourPicker() | |
353 | |
354 scroll = ScrollAreaWithVerticalBar(self) | |
355 scroll.setWidget(self.rom_canvas) | |
356 | |
357 self.toolbar = QW.QHBoxLayout() | |
358 | |
359 palette_layout = QW.QHBoxLayout() | |
360 palette_layout.setSpacing(0) | |
361 palette_layout.setSizeConstraint(QW.QLayout.SetFixedSize) | |
362 | |
363 self.rom_palette_buttons = TilePaletteButtons( | |
364 Tile.default_palette, | |
365 callback=self.pick_palette_colour | |
366 ) | |
367 palette_layout.addWidget(self.rom_palette_buttons) | |
368 | |
369 self.rom_palette = QW.QWidget() | |
370 self.rom_palette.setLayout(palette_layout) | |
371 | |
372 self.toolbar.addWidget(self.rom_palette) | |
373 | |
374 for icon, tooltip, function in [ | |
375 ("zoom-in", "Zoom in", "zoomIn"), | |
376 ("zoom-out", "Zoom out", "zoomOut"), | |
377 ("zoom-original", "Restore zoom", "zoomOriginal"), | |
378 ("increaseCols", "Increase number of columns", "increaseCols"), | |
379 ("decreaseCols", "Decrease number of columns", "decreaseCols"), | |
380 ]: | |
381 button = QW.QPushButton("") | |
382 iconpath = widget_icon_path(button) | |
383 button.setIcon(QG.QIcon(f"img/{iconpath}/{icon}.svg")) | |
384 button.setFixedSize(QC.QSize(32, 32)) | |
385 button.setIconSize(QC.QSize(24, 24)) | |
386 button.setToolTip(tooltip) | |
387 button.pressed.connect(getattr(self.rom_canvas, function)) | |
388 self.toolbar.addWidget(button) | |
389 | |
390 self.toolbar.addStretch() | |
391 | |
392 layout = QW.QVBoxLayout() | |
393 layout.addWidget(scroll) | |
394 toolbar_widget = QW.QWidget() | |
395 toolbar_widget.setLayout(self.toolbar) | |
396 layout.addWidget(toolbar_widget) | |
397 rom_canvas_widget = QW.QWidget() | |
398 rom_canvas_widget.setLayout(layout) | |
399 | |
400 self.setWidget(rom_canvas_widget) | |
401 | |
402 iconpath = widget_icon_path(self) | |
403 self.setStyleSheet( | |
404 f""" | |
405 QDockWidget | |
406 {{ | |
407 titlebar-close-icon: url(img/{iconpath}/widget-close.svg); | |
408 titlebar-normal-icon: url(img/{iconpath}/widget-undock.svg); | |
409 }} | |
410 """ | |
411 ) | |
412 | |
413 def pick_palette_colour(self, palette_idx): | |
414 if self.colour_picker.exec_(): | |
415 new_colour_idx = self.colour_picker.picked_idx | |
416 self.rom_palette_buttons.layout.itemAt(palette_idx).widget().set_colour(new_colour_idx) | |
417 for tile in self.rom_canvas.tiles: | |
418 palette = tile.palette | |
419 palette[palette_idx] = new_colour_idx | |
420 tile.set_palette(palette) | |
421 self.rom_canvas.update() | |
422 self.rom_canvas.tile_picker.update() | |
423 | |
424 def change_palette(self, name): | |
425 self.rom_canvas.palette = TILE_PALETTES[name] | |
426 for tile in self.rom_canvas.tiles: | |
427 tile.set_palette(self.rom_canvas.palette.copy()) | |
428 | |
429 for button_idx, colour_idx in enumerate(self.rom_canvas.palette): | |
430 self.rom_palette_buttons.layout.itemAt(button_idx).widget().set_colour(colour_idx) | |
431 self.rom_canvas.update() | |
432 self.rom_canvas.tile_picker.update() |