import copy
import json
from pathlib import Path
from PyQt5.QtCore import (
QSettings,
QCoreApplication,
pyqtSlot,
QRect
)
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QFileDialog,
QMessageBox,
QActionGroup,
QUndoStack
)
from ui.main_window_ui import Ui_main_window
from preferences_dialog import PreferencesDialog
from pixel_data import (
PixelData,
parse_pixel_data,
history_add_pixel_palette_row,
history_remove_pixel_palette_row
)
from color_data import (
ColorData,
parse_color_data,
history_set_color,
history_rename_color_palette,
history_add_color_palette,
history_remove_color_palette
)
from pixel_palette import PixelPalette
from color_palette import ColorPalette
from editor_scene import EditorScene
[docs]class Jide(QMainWindow, Ui_main_window):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.setup_window()
self.init_models()
self.init_ui()
self.setup_editor()
self.prefs = QSettings()
QCoreApplication.setOrganizationName("Connor Spangler")
QCoreApplication.setOrganizationDomain("https://github.com/cspang1")
QCoreApplication.setApplicationName("JIDE")
QApplication.processEvents()
self.load_project("./data/demo.jrf")
def setup_window(self):
self.tile_color_palette_dock.hide()
self.tile_pixel_palette_dock.hide()
self.map_color_palette_dock.hide()
self.map_pixel_palette_dock.hide()
self.tool_actions = QActionGroup(self)
self.tool_actions.addAction(self.action_select_tool)
self.tool_actions.addAction(self.action_pen_tool)
self.tool_actions.addAction(self.action_fill_tool)
self.tool_actions.addAction(self.action_line_tool)
self.tool_actions.addAction(self.action_rectangle_tool)
self.tool_actions.addAction(self.action_ellipse_tool)
self.undo_stack = QUndoStack(self)
action_undo = self.undo_stack.createUndoAction(self, "&Undo")
action_undo.setShortcut(QKeySequence.Undo)
action_redo = self.undo_stack.createRedoAction(self, "&Redo")
action_redo.setShortcut(QKeySequence.Redo)
self.menu_edit.addActions([action_undo, action_redo])
self.action_new.triggered.connect(self.new_project)
self.action_save.triggered.connect(self.save_project)
self.action_copy.triggered.connect(lambda temp: print("this will copy the current selection"))
self.action_paste.triggered.connect(lambda temp: print("this will paste the current selection"))
self.action_open.triggered.connect(self.select_file)
self.action_close.triggered.connect(self.close_project)
self.action_exit.triggered.connect(self.quit_application)
self.action_preferences.triggered.connect(self.open_preferences)
self.action_gen_dat_files.triggered.connect(lambda temp: print("this will generate .DAT files"))
self.action_load_jcap_system.triggered.connect(lambda temp: print("this will load the JCAP system"))
self.editor_tabs.currentChanged.connect(self.select_tab)
def init_models(self):
self.sprite_color_data = ColorData()
self.tile_color_data = ColorData()
self.sprite_pixel_data = PixelData()
self.tile_pixel_data = PixelData()
self.sprite_color_data.error_thrown.connect(self.show_error_dialog)
self.tile_color_data.error_thrown.connect(self.show_error_dialog)
self.sprite_pixel_data.error_thrown.connect(self.show_error_dialog)
self.tile_pixel_data.error_thrown.connect(self.show_error_dialog)
self.sprite_color_palette.color_set.connect(
lambda new_color, change_index:
history_set_color(
self.undo_stack,
self.sprite_color_data,
self.sprite_color_palette.color_palette_name_combo.currentText(),
new_color,
change_index,
)
)
self.tile_color_palette.color_set.connect(
lambda new_color, change_index:
history_set_color(
self.undo_stack,
self.tile_color_data,
self.tile_color_palette.color_palette_name_combo.currentText(),
new_color,
change_index,
)
)
self.sprite_color_palette.color_palette_renamed.connect(
lambda old_palette_name, new_palette_name:
history_rename_color_palette(
self.undo_stack,
self.sprite_color_data,
old_palette_name,
new_palette_name,
)
)
self.tile_color_palette.color_palette_renamed.connect(
lambda old_palette_name, new_palette_name:
history_rename_color_palette(
self.undo_stack,
self.tile_color_data,
old_palette_name,
new_palette_name,
)
)
self.sprite_color_palette.color_palette_added.connect(
lambda palette_name:
history_add_color_palette(
self.undo_stack,
self.sprite_color_data,
palette_name
)
)
self.tile_color_palette.color_palette_added.connect(
lambda palette_name:
history_add_color_palette(
self.undo_stack,
self.tile_color_data,
palette_name
)
)
self.sprite_color_palette.color_palette_removed.connect(
lambda palette_name:
history_remove_color_palette(
self.undo_stack,
self.sprite_color_data,
palette_name
)
)
self.tile_color_palette.color_palette_removed.connect(
lambda palette_name:
history_remove_color_palette(
self.undo_stack,
self.tile_color_data,
palette_name
)
)
self.sprite_pixel_palette.add_palette_row.connect(
lambda:
history_add_pixel_palette_row(
self.undo_stack,
self.sprite_pixel_data,
)
)
self.tile_pixel_palette.add_palette_row.connect(
lambda:
history_add_pixel_palette_row(
self.undo_stack,
self.tile_pixel_data
)
)
self.sprite_pixel_palette.remove_palette_row.connect(
lambda:
history_remove_pixel_palette_row(
self.undo_stack,
self.sprite_pixel_data,
)
)
self.tile_pixel_palette.remove_palette_row.connect(
lambda:
history_remove_pixel_palette_row(
self.undo_stack,
self.tile_pixel_data
)
)
def init_ui(self):
self.sprite_color_palette.set_transparency(True)
self.tile_color_palette.set_transparency(False)
self.sprite_color_palette.color_palette_changed.connect(
lambda palette_name: self.sprite_color_palette.change_palette(
self.sprite_color_data.get_color_palette(palette_name)
)
)
self.tile_color_palette.color_palette_changed.connect(
lambda palette_name: self.tile_color_palette.change_palette(
self.tile_color_data.get_color_palette(palette_name)
)
)
self.sprite_color_palette.color_palette_changed.connect(
lambda palette_name: self.sprite_pixel_palette.set_color_table(
self.sprite_color_data.get_color_palette(palette_name)
)
)
self.tile_color_palette.color_palette_changed.connect(
lambda palette_name: self.tile_pixel_palette.set_color_table(
self.tile_color_data.get_color_palette(palette_name)
)
)
self.sprite_color_palette.color_previewed.connect(self.sprite_pixel_palette.set_color)
self.tile_color_palette.color_previewed.connect(self.tile_pixel_palette.set_color)
self.sprite_color_data.color_palette_added.connect(self.sprite_color_palette.add_color_palette)
self.sprite_color_data.color_palette_removed.connect(self.sprite_color_palette.remove_color_palette)
self.tile_color_data.color_palette_added.connect(self.tile_color_palette.add_color_palette)
self.tile_color_data.color_palette_removed.connect(self.tile_color_palette.remove_color_palette)
self.sprite_color_data.color_palette_renamed.connect(self.sprite_color_palette.rename_color_palette)
self.tile_color_data.color_palette_renamed.connect(self.tile_color_palette.rename_color_palette)
self.sprite_color_data.color_updated.connect(self.sprite_color_palette.update_color)
self.tile_color_data.color_updated.connect(self.tile_color_palette.update_color)
self.sprite_color_data.color_updated.connect(
lambda _, color, index: self.sprite_pixel_palette.set_color(color, index)
)
self.tile_color_data.color_updated.connect(
lambda _, color, index: self.tile_pixel_palette.set_color(color, index)
)
self.sprite_pixel_data.data_updated.connect(
lambda: self.sprite_pixel_palette.set_pixel_palette(self.sprite_pixel_data.get_image())
)
self.tile_pixel_data.data_updated.connect(
lambda: self.tile_pixel_palette.set_pixel_palette(self.tile_pixel_data.get_image())
)
def setup_editor(self):
self.sprite_scene = EditorScene()
self.tile_scene = EditorScene()
self.sprite_editor_view.setScene(self.sprite_scene)
self.tile_editor_view.setScene(self.tile_scene)
self.sprite_pixel_data.data_updated.connect(
lambda: self.sprite_scene.set_scene_image(self.sprite_pixel_data.get_image())
)
self.tile_pixel_data.data_updated.connect(
lambda: self.tile_scene.set_scene_image(self.tile_pixel_data.get_image())
)
self.sprite_color_palette.color_previewed.connect(self.sprite_scene.set_color)
self.tile_color_palette.color_previewed.connect(self.tile_scene.set_color)
self.sprite_pixel_palette.elements_selected.connect(self.sprite_scene.select_cells)
self.tile_pixel_palette.elements_selected.connect(self.tile_scene.select_cells)
self.sprite_color_data.color_updated.connect(
lambda _, color, index: self.sprite_scene.set_color(color, index)
)
self.tile_color_data.color_updated.connect(
lambda _, color, index: self.tile_scene.set_color(color, index)
)
self.sprite_color_palette.color_palette_changed.connect(
lambda palette_name: self.sprite_scene.set_color_table(
self.sprite_color_data.get_color_palette(palette_name)
)
)
self.tile_color_palette.color_palette_changed.connect(
lambda palette_name: self.tile_scene.set_color_table(
self.tile_color_data.get_color_palette(palette_name)
)
)
def load_project(self, file_name):
if not file_name:
return
self.project_file = file_name
project_data = None
try:
with open(self.project_file, "r") as project_file:
project_data = json.load(project_file)
except OSError:
self.show_error_dialog("Unable to open project file")
return
except KeyError:
self.show_error_dialog("Unable to load project due to malformed data")
return
self.populate_models(project_data)
self.enable_ui()
self.editor_tabs.setCurrentIndex(0)
def populate_models(self, project_data):
for palette in parse_color_data(project_data["sprite_color_palettes"]):
self.sprite_color_data.add_color_palette(*palette)
for palette in parse_color_data(project_data["tile_color_palettes"]):
self.tile_color_data.add_color_palette(*palette)
sprite_data = parse_pixel_data(project_data["sprites"])
tile_data = parse_pixel_data(project_data["tiles"])
self.tile_map_data = project_data["tile_maps"]
self.sprite_pixel_data.set_image(*sprite_data[:3])
self.sprite_pixel_data.set_names(sprite_data[-1])
self.tile_pixel_data.set_image(*tile_data[:3])
self.tile_pixel_data.set_names(tile_data[-1])
self.sprite_pixel_palette.set_pixel_palette(self.sprite_pixel_data.get_image())
self.tile_pixel_palette.set_pixel_palette(self.tile_pixel_data.get_image())
self.sprite_pixel_palette.set_selection(QRect(0, 0, 1, 1))
self.tile_pixel_palette.set_selection(QRect(0, 0, 1, 1))
self.sprite_pixel_data.set_color_table(
[color.rgba() for color in self.sprite_color_data.get_color_palette(
self.sprite_color_palette.get_current_palette_name()
)]
)
self.tile_pixel_data.set_color_table(
[color.rgba() for color in self.tile_color_data.get_color_palette(
self.tile_color_palette.get_current_palette_name()
)]
)
self.sprite_color_palette.color_palette_engaged.connect(
lambda: self.editor_tabs.setCurrentIndex(0)
)
self.tile_color_palette.color_palette_engaged.connect(
lambda: self.editor_tabs.setCurrentIndex(1)
)
self.sprite_pixel_palette.pixel_palette_engaged.connect(
lambda: self.editor_tabs.setCurrentIndex(0)
)
self.tile_pixel_palette.pixel_palette_engaged.connect(
lambda: self.editor_tabs.setCurrentIndex(1)
)
def enable_ui(self):
self.tool_bar.setEnabled(True)
self.editor_tabs.setEnabled(True)
self.action_save.setEnabled(True)
self.action_close.setEnabled(True)
self.action_gen_dat_files.setEnabled(True)
self.action_load_jcap_system.setEnabled(True)
for palette in self.findChildren(ColorPalette):
palette.setEnabled(True)
for palette in self.findChildren(PixelPalette):
palette.setEnabled(True)
def select_tab(self, index):
if index == 0:
self.sprite_color_palette_dock.show()
self.sprite_pixel_palette_dock.show()
self.tile_color_palette_dock.hide()
self.tile_pixel_palette_dock.hide()
self.map_color_palette_dock.hide()
self.map_pixel_palette_dock.hide()
elif index == 1:
self.sprite_color_palette_dock.hide()
self.sprite_pixel_palette_dock.hide()
self.tile_color_palette_dock.show()
self.tile_pixel_palette_dock.show()
self.map_color_palette_dock.hide()
self.map_pixel_palette_dock.hide()
else:
self.sprite_color_palette_dock.hide()
self.sprite_pixel_palette_dock.hide()
self.tile_color_palette_dock.hide()
self.tile_pixel_palette_dock.hide()
self.map_color_palette_dock.show()
self.map_pixel_palette_dock.show()
def select_file(self):
self.check_unsaved_changes()
file_name, _ = QFileDialog.getOpenFileName(
self,
"Open file",
str(Path(__file__)),
"JCAP Resource File (*.jrf)",
)
self.load_project(file_name)
def open_preferences(self):
prefs = QSettings()
cpu_port = None
gpu_port = None
jcap_path = None
prefs.beginGroup("ports")
if prefs.contains("cpu_port"):
cpu_port = prefs.value("cpu_port")
if prefs.contains("gpu_port"):
gpu_port = prefs.value("gpu_port")
prefs.endGroup()
prefs.beginGroup("paths")
if prefs.contains("jcap_path"):
jcap_path = prefs.value("jcap_path")
prefs.endGroup()
prefs_dialog = PreferencesDialog(cpu_port, gpu_port, jcap_path)
valid_prefs = False
while not valid_prefs:
if prefs_dialog.exec():
cpu_port = prefs_dialog.get_cpu_port()
gpu_port = prefs_dialog.get_gpu_port()
jcap_path = prefs_dialog.get_jcap_path()
if cpu_port and gpu_port and cpu_port == gpu_port:
self.show_error_dialog("CPU and GPU COM ports must be different")
else:
valid_prefs = True
prefs.beginGroup("ports")
prefs.setValue("cpu_port", cpu_port)
prefs.setValue("gpu_port", gpu_port)
prefs.endGroup()
prefs.beginGroup("paths")
prefs.setValue("jcap_path", jcap_path)
prefs.endGroup()
@pyqtSlot(str)
def show_error_dialog(self, message):
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Critical)
msg_box.setText(message)
msg_box.setWindowTitle("Error")
msg_box.exec_()
@pyqtSlot()
def new_project(self):
self.close_project()
sprites = []
tiles = []
sprite_color_palettes = []
tile_color_palettes = []
tile_maps = []
blank_element = [[0 for _ in range(8)] for _ in range(8)]
sprite_names = ["sprite_" + str(i) for i in range(16)]
tile_names = ["tile_" + str(i) for i in range(16)]
blank_color_palette = [227 for _ in range(16)]
blank_tile_map = [[0, 0] for _ in range(30 * 40)]
sprites = [{"name": name, "contents": copy.deepcopy(blank_element)} for name in sprite_names]
tiles = [{"name": name, "contents": copy.deepcopy(blank_element)} for name in tile_names]
sprite_color_palettes = [{
"name": "sprite_color_palette_0",
"contents": blank_color_palette.copy()
}]
tile_color_palettes = [{
"name": "tile_color_palette_0",
"contents": blank_color_palette.copy()
}]
tile_maps = [{
"name": "tile_map_0",
"contents": copy.deepcopy(blank_tile_map)
}]
project_data = {
"sprites": sprites,
"tiles": tiles,
"sprite_color_palettes": sprite_color_palettes,
"tile_color_palettes": tile_color_palettes,
"tile_maps": tile_maps
}
self.populate_models(project_data)
self.enable_ui()
self.editor_tabs.setFocus()
@pyqtSlot()
def save_project(self):
sprites = self.sprite_pixel_data.to_json()
tiles = self.tile_pixel_data.to_json()
sprite_color_palettes = self.sprite_color_data.to_json()
tile_color_palettes = self.tile_color_data.to_json()
tile_maps = self.tile_map_data
project_data = {
"sprites": sprites,
"tiles": tiles,
"sprite_color_palettes": sprite_color_palettes,
"tile_color_palettes": tile_color_palettes,
"tile_maps": tile_maps
}
if not self.project_file:
options = QFileDialog.Options()
options |= QFileDialog.ReadOnly
options |= QFileDialog.HideNameFilterDetails
file_dialog = QFileDialog(self)
file_dialog.setOptions(options)
file_dialog.setNameFilter("JCAP Resource File (*.jrf)")
self.project_file, _ = file_dialog.getSaveFileName(self, 'Save File', '', 'JCAP Resource File (*.jrf)', options=options)
if not self.project_file:
return
try:
with open(self.project_file, "w") as project_file:
json.dump(project_data, project_file)
self.undo_stack.setClean()
except (IOError, PermissionError, OSError) as e:
self.show_error_dialog(f"Error while saving the project file: {e}")
@pyqtSlot()
def close_project(self):
self.check_unsaved_changes()
self.tool_bar.setEnabled(False)
self.action_save.setEnabled(False)
self.action_close.setEnabled(False)
self.action_gen_dat_files.setEnabled(False)
self.action_load_jcap_system.setEnabled(False)
self.editor_tabs.setCurrentIndex(0)
self.editor_tabs.setEnabled(False)
self.sprite_color_palette = ColorPalette()
self.sprite_color_palette_dock.setWidget(self.sprite_color_palette)
self.tile_color_palette = ColorPalette()
self.tile_color_palette_dock.setWidget(self.tile_color_palette)
self.sprite_pixel_palette = PixelPalette()
self.sprite_pixel_palette_dock.setWidget(self.sprite_pixel_palette)
self.tile_pixel_palette = PixelPalette()
self.tile_pixel_palette_dock.setWidget(self.tile_pixel_palette)
for palette in self.findChildren(ColorPalette):
palette.setEnabled(False)
for palette in self.findChildren(PixelPalette):
palette.setEnabled(False)
self.undo_stack.clear()
self.project_file = None
self.init_models()
self.init_ui()
self.setup_editor()
@pyqtSlot()
def quit_application(self):
self.check_unsaved_changes()
self.close()
def check_unsaved_changes(self):
if self.undo_stack.isClean():
return
save_prompt = QMessageBox()
save_prompt.setIcon(QMessageBox.Question)
save_prompt.setText("You have unsaved changes. Would you like to save them before closing the current project?")
save_prompt.setWindowTitle("Save Changes?")
save_prompt.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
# Show the QMessageBox and wait for the user's response
response = save_prompt.exec()
# Check the user's response
if response == QMessageBox.Yes:
self.save_project()