from collections import OrderedDict
import json
import math
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QColor
from colorpicker import upsample
from PyQt5.QtWidgets import QUndoStack
from history import (
cmdAddColPal,
cmdAddPixRow,
cmdRemColPal,
cmdRemPixRow,
cmdSetCol,
cmdSetColPalName,
cmdSetPixBatch,
)
from source import Source
[docs]class ColorPalettes(QObject):
"""Data container for sprite/tile color palettes
:param data: List containing JSON color palette data
:type data: list(dict)
:param source: Subject source of palette, either sprite or tile
:type source: Source
:param parent: Parent widget, defaults to None
:type parent: QWidget, optional
"""
color_changed = pyqtSignal(Source, str)
name_changed = pyqtSignal(Source, str, str)
palette_added = pyqtSignal(Source, str, int)
palette_removed = pyqtSignal(Source, str)
def __init__(self, data, source, parent=None):
super().__init__(parent)
self.palettes = OrderedDict()
self.source = source
for spr_pal in data:
palette = spr_pal["contents"]
palette[:] = [
QColor(*upsample(color >> 5, (color >> 2) & 7, color & 3))
for color in palette
]
if self.source is Source.SPRITE:
palette[0] = QColor(0, 0, 0, 0)
self.palettes[spr_pal["name"]] = palette
[docs] def keys(self):
"""Retrieve color palette dict keys
:return: Color palette dict keys
:rtype: odict_keys
"""
return self.palettes.keys()
[docs] def values(self):
"""Retrieve color palette dict values
:return: Color palette dict values
:rtype: odict_values
"""
return self.palettes.values()
[docs] def items(self):
"""Retrieve color palette dict items
:return: Color palette dict items
:rtype: odict_items
"""
return self.palettes.items()
[docs] def setName(self, cur_name, new_name):
"""Sets the names of a color palette
:param cur_name: Name of target color palette
:type cur_name: str
:param new_name: New name for color palette
:type new_name: str
"""
replacement = {cur_name: new_name}
temp_palette_items = self.palettes.copy()
for name, _ in self.palettes.items():
palette = temp_palette_items.pop(name)
temp_palette_items[replacement.get(name, name)] = palette
self.palettes = temp_palette_items
self.name_changed.emit(self.source, cur_name, new_name)
[docs] def addPalette(self, name, contents, index=None):
"""Add a new color palette
:param name: New color palette name
:type name: str
:param contents: New color palette contents
:type contents: list(QColor)
:param index: Index in palettes to insert new palette, defaults
to None
:type index: int, optional
"""
if self.source is Source.SPRITE:
contents[0] = QColor(0, 0, 0, 0)
new_palettes = OrderedDict()
cur_palette = 0
if index is None or index == len(self.palettes):
index = len(self.palettes)
self.palettes[name] = contents
else:
for key, value in self.palettes.items():
if cur_palette == index:
new_palettes[name] = contents
new_palettes[key] = value
cur_palette += 1
self.palettes = new_palettes
self.palette_added.emit(self.source, name, index)
[docs] def remPalette(self, name):
"""Removes color palette
:param name: Name of color palette to be removed
:type name: str
"""
if name in self.palettes:
del self.palettes[name]
self.palette_removed.emit(self.source, name)
else:
self.palette_removed.emit(self.source, None)
def __getitem__(self, index):
"""Retrieves a color palette or color at a given index
:param index: Index of color palette or color
:type index: int OR tuple(str, int)
:return: Color palette OR color
:rtype: list(QColor) OR QColor
"""
if not isinstance(index, tuple):
return self.palettes[index]
name, index = index
return self.palettes[name][index]
def __setitem__(self, index, value):
"""Set color in palette
:param index: Index of color to be set
:type index: tuple(str, int)
:param value: New color value
:type value: QColor
"""
name, index = index
self.palettes[name][index] = value
self.color_changed.emit(self.source, name)
[docs]class PixelPalettes(QObject):
"""Data container for sprite/tile pixel palettes
:param data: List containing JSON pixel palette data
:type data: list(dict)
:param source: Subject source of palette, either sprite or tile
:type source: Source
:param parent: Parent widget, defaults to None
:type parent: QWidget, optional
"""
batch_updated = pyqtSignal(Source, set)
row_count_updated = pyqtSignal(Source, int)
def __init__(self, data, source, parent=None):
super().__init__(parent)
self.update_manifest = set()
self.palettes = []
self.source = source
for element in data:
self.palettes.append(element["contents"])
[docs] def batchUpdate(self):
"""Updates the pixel palette date based on self.update_manifest
"""
self.batch_updated.emit(self.source, self.update_manifest)
self.update_manifest.clear()
[docs] def getPalettes(self):
"""Returns the pixel palettes
:return: Pixel palettes
:rtype: list(list)
"""
return self.palettes
[docs] def addRow(self, row=None):
"""Adds a row of sprites/tiles to palette
:param row: Row data to be added, defaults to None
:type row: list(list), optional
"""
self.palettes.extend(
[[[0] * 8 for i in range(8)] for i in range(16)]
if row is None
else row
)
self.row_count_updated.emit(
self.source, math.floor(self.palettes.__len__() / 16)
)
[docs] def remRow(self):
"""Removes a row of sprites/tiles from the palette
:return: The removed row
:rtype: list(list)
"""
old_row = self.palettes[-16:]
del self.palettes[-16:]
self.row_count_updated.emit(
self.source, math.floor(self.palettes.__len__() / 16)
)
return old_row
def __getitem__(self, index):
"""Retrieves a pixel palette at an index
:param index: Index of pixel palette
:type index: int
:return: Pixel palette at index
:rtype: list(list)
"""
return self.palettes[index]
def __setitem__(self, index, value):
"""Sets pixel at index to a value
:param index: Sprite/tile index/row/column
:type index: tuple(int, int, int)
:param value: Value to set pixel to
:type value: int
"""
index, row, col = index
self.palettes[index][row][col] = value
self.update_manifest.add(index)
[docs]class GameData(QObject):
"""Represents centralized data object for JIDE, containing all sprite/tile/
tile map data and CRUD functions
:param data: Dict containing project JSON data
:type data: dict
:param parent: Parent widget, defaults to None
:type parent: QWidget, optional
"""
col_pal_updated = pyqtSignal(Source, str)
col_pal_renamed = pyqtSignal(Source, str, str)
col_pal_added = pyqtSignal(Source, str, int)
col_pal_removed = pyqtSignal(Source, str)
pix_batch_updated = pyqtSignal(Source, set)
row_count_updated = pyqtSignal(Source, int)
def __init__(self, data, parent=None):
super().__init__(parent)
self.undo_stack = QUndoStack(self)
self.game_name = data["gameName"]
self.sprite_pixel_palettes = PixelPalettes(
data["sprites"], Source.SPRITE
)
self.tile_pixel_palettes = PixelPalettes(data["tiles"], Source.TILE)
self.sprite_color_palettes = ColorPalettes(
data["spriteColorPalettes"], Source.SPRITE
)
self.tile_color_palettes = ColorPalettes(
data["tileColorPalettes"], Source.TILE
)
self.sprite_pixel_palettes.batch_updated.connect(
self.pix_batch_updated
)
self.tile_pixel_palettes.batch_updated.connect(self.pix_batch_updated)
self.sprite_color_palettes.color_changed.connect(self.col_pal_updated)
self.tile_color_palettes.color_changed.connect(self.col_pal_updated)
self.sprite_pixel_palettes.row_count_updated.connect(
self.row_count_updated
)
self.tile_pixel_palettes.row_count_updated.connect(
self.row_count_updated
)
self.sprite_color_palettes.name_changed.connect(self.col_pal_renamed)
self.tile_color_palettes.name_changed.connect(self.col_pal_renamed)
self.sprite_color_palettes.palette_added.connect(self.col_pal_added)
self.tile_color_palettes.palette_added.connect(self.col_pal_added)
self.sprite_color_palettes.palette_removed.connect(
self.col_pal_removed
)
self.tile_color_palettes.palette_removed.connect(self.col_pal_removed)
def getGameName(self):
return self.game_name
[docs] def getPixelPalettes(self, source):
"""Retrieve sprite/tile pixel palettes
:param source: Subject source of palettes, either sprite or tile
:type source: Source
:return: Pixel palettes
:rtype: list
"""
return (
self.sprite_pixel_palettes.getPalettes()
if source is Source.SPRITE
else self.tile_pixel_palettes.getPalettes()
)
[docs] def getElement(self, index, source):
"""Retrieve a sprite/tile pixel value
:param index: Index/row/col of pixel
:type index: tuple(int, int, int)
:param source: Subject source of pixel, either sprite or tile
:type source: Source
:return: Pixel value at index
:rtype: int
"""
return (
self.sprite_pixel_palettes[index]
if source is Source.SPRITE
else self.tile_pixel_palettes[index]
)
[docs] def getColPals(self, source):
"""Retrieve sprite/tile color palettes
:param source: Subject source of palettes, either sprite or tile
:type source: Source
:return: Color palettes of target source
:rtype: list
"""
return (
self.sprite_color_palettes.values()
if source is Source.SPRITE
else self.tile_color_palettes.values()
)
[docs] def getColPalNames(self, source):
"""Retrieve sprite/tile color palette names
:param source: Subject source of palette names, either sprite or tile
:type source: Source
:return: Names of target source color palettes
:rtype: list
"""
return (
self.sprite_color_palettes.keys()
if source is Source.SPRITE
else self.tile_color_palettes.keys()
)
[docs] def getColPal(self, name, source):
"""Retrieve a specific sprite/tile color palette
:param name: Name of color palette to retrieve
:type name: str
:param source: Subject source of palette, either sprite or tile
:type source: Source
:return: Color palette specified by name
:rtype: list
"""
return (
self.sprite_color_palettes[name]
if source is Source.SPRITE
else self.tile_color_palettes[name]
)
[docs] def setColor(self, name, index, color, source, orig=None):
"""Sets a color in a specific color palette
:param name: Name of color palette containing target color
:type name: str
:param index: Index of color in color palette
:type index: int
:param color: New color to be set to
:type color: QColor
:param source: Subject source of target palette, either sprite or tile
:type source: Source
:param orig: Original color of the target color, defaults to None
:type orig: QColor, optional
"""
target = (
self.sprite_color_palettes
if source is Source.SPRITE
else self.tile_color_palettes
)
command = cmdSetCol(
target, name, index, color, orig, "Set palette color"
)
self.undo_stack.push(command)
[docs] def previewColor(self, name, index, color, source):
"""Previews a color without committing it to the data; used by
colorpalette.ColorPreview
:param name: Name of color palette containing target color
:type name: str
:param index: Index of color in color palette
:type index: int
:param color: Color to preview
:type color: QColor
:param source: Subject source of target palette, either sprite or tile
:type source: Source
"""
if source is Source.SPRITE:
self.sprite_color_palettes[name, index] = color
else:
self.tile_color_palettes[name, index] = color
[docs] def setColPalName(self, cur_name, new_name, source):
"""Set name of a color palette
:param cur_name: Current name of target color palette
:type cur_name: str
:param new_name: New name for target palette
:type new_name: str
:param source: Subject source of target palette, either sprite or
tile
:type source: Source
"""
if source is Source.SPRITE:
if new_name not in self.sprite_color_palettes.keys():
command = cmdSetColPalName(
self.sprite_color_palettes,
cur_name,
new_name,
"Set palette name",
)
self.undo_stack.push(command)
else:
self.col_pal_renamed.emit(source, None, None)
else:
if new_name not in self.tile_color_palettes.keys():
command = cmdSetColPalName(
self.tile_color_palettes,
cur_name,
new_name,
"Set palette name",
)
self.undo_stack.push(command)
else:
self.col_pal_renamed.emit(source, None, None)
[docs] def addColPal(self, name, source):
"""Add a new color palette
:param name: Name for the new color palette
:type name: str
:param source: Subject source of new palette, either sprite or tile
:type source: Source
"""
if source is Source.SPRITE:
if name not in self.sprite_color_palettes.keys():
command = cmdAddColPal(
self.sprite_color_palettes,
name,
[QColor(0, 0, 0, 255)] * 16,
"Add color palette",
)
self.undo_stack.push(command)
else:
self.col_pal_added.emit(source, None, None)
else:
if name not in self.tile_color_palettes.keys():
command = cmdAddColPal(
self.tile_color_palettes,
name,
[QColor(0, 0, 0, 255)] * 16,
"Add color palette",
)
self.undo_stack.push(command)
else:
self.col_pal_added.emit(source, None, None)
[docs] def remColPal(self, name, source):
"""Remove a color palette
:param name: Name for the target color palette
:type name: str
:param source: Subject source of target palette, either sprite or tile
:type source: Source
"""
if source is Source.SPRITE:
if name in self.sprite_color_palettes.keys():
command = cmdRemColPal(
self.sprite_color_palettes, name, "Add color palette"
)
self.undo_stack.push(command)
else:
self.col_pal_removed.emit(source, None)
else:
if name in self.tile_color_palettes.keys():
command = cmdRemColPal(
self.tile_color_palettes, name, "Add color palette"
)
self.undo_stack.push(command)
else:
self.col_pal_removed.emit(source, None)
[docs] def setPixBatch(self, batch, source):
"""Sets pixels to new values based on the contents of a set of pixel deltas
:param batch: Set of pixel deltas represented by pixel indexes and
new values
:type batch: defaultdict
:param source: Subject source of pixels, either sprite or tile
:type source: [type]
"""
target = (
self.sprite_pixel_palettes
if source is Source.SPRITE
else self.tile_pixel_palettes
)
command = cmdSetPixBatch(target, batch, "Draw pixels")
self.undo_stack.push(command)
[docs] def addPixRow(self, source):
"""Add a new row of sprites or tiles
:param source: Subject source to add the row to, either sprite or tile
:type source: Source
"""
target = (
self.sprite_pixel_palettes
if source is Source.SPRITE
else self.tile_pixel_palettes
)
target_name = "sprite" if source is Source.SPRITE else "tile"
command = cmdAddPixRow(target, "Add {} row".format(target_name))
self.undo_stack.push(command)
[docs] def remPixRow(self, source):
"""Remove a row of sprites or tiles
:param source: Subject source to remove the row from, either sprite
or tile
:type source: Source
"""
target = (
self.sprite_pixel_palettes
if source is Source.SPRITE
else self.tile_pixel_palettes
)
target_name = "sprite" if source is Source.SPRITE else "tile"
command = cmdRemPixRow(target, "Add {} row".format(target_name))
self.undo_stack.push(command)
[docs] def setUndoStack(self, undo_stack):
"""Sets the undo stack for the GameData object
:param undo_stack: The undo stack to set
:type undo_stack: QUndoStack
"""
self.undo_stack = undo_stack
[docs] @classmethod
def fromFilename(cls, file_name, parent=None):
"""Class method to instantiate the GameData object from a JSON file
containing project data
:param file_name: Path to project data JSON file
:type file_name: str
:param parent: Parent widget for GameData object, defaults to None
:type parent: QWidget, optional
:return: Instance of GameData instantiated with data from
file
:rtype: GameData
"""
with open(file_name, "r") as data_file:
return cls(json.load(data_file), parent)