470 lines
16 KiB
Python
470 lines
16 KiB
Python
import json
|
|
import os
|
|
import time
|
|
|
|
from PIL import Image
|
|
|
|
from art import (
|
|
ART_DIR,
|
|
ART_FILE_EXTENSION,
|
|
ART_SCRIPT_DIR,
|
|
SCRIPT_FILE_EXTENSION,
|
|
THUMBNAIL_CACHE_DIR,
|
|
)
|
|
from charset import CHARSET_DIR, CHARSET_FILE_EXTENSION
|
|
from image_export import write_thumbnail
|
|
from palette import PALETTE_DIR, PALETTE_EXTENSIONS
|
|
from texture import Texture
|
|
from ui_art_dialog import ImportOptionsDialog, PaletteFromFileDialog
|
|
from ui_chooser_dialog import ChooserDialog, ChooserItem
|
|
from ui_console import OpenCommand
|
|
|
|
|
|
class BaseFileChooserItem(ChooserItem):
|
|
hide_file_extension = False
|
|
|
|
def get_short_dir_name(self):
|
|
# name should end in / but don't assume
|
|
dir_name = self.name[:-1] if self.name.endswith("/") else self.name
|
|
return os.path.basename(dir_name) + "/"
|
|
|
|
def get_label(self):
|
|
if os.path.isdir(self.name):
|
|
return self.get_short_dir_name()
|
|
else:
|
|
label = os.path.basename(self.name)
|
|
if self.hide_file_extension:
|
|
return os.path.splitext(label)[0]
|
|
else:
|
|
return label
|
|
|
|
def get_description_lines(self):
|
|
if os.path.isdir(self.name):
|
|
if self.name == "..":
|
|
return ["[parent folder]"]
|
|
# TODO: # of items in dir?
|
|
return []
|
|
return None
|
|
|
|
def picked(self, element):
|
|
# if this is different from the last clicked item, pick it
|
|
if element.selected_item_index != self.index:
|
|
ChooserItem.picked(self, element)
|
|
element.first_selection_made = True
|
|
return
|
|
# if we haven't yet clicked something in this view, require another
|
|
# click before opening it (consistent double click behavior for
|
|
# initial selections)
|
|
if not element.first_selection_made:
|
|
element.first_selection_made = True
|
|
return
|
|
if self.name == ".." and self.name != "/":
|
|
new_dir = os.path.abspath(os.path.abspath(element.current_dir) + "/..")
|
|
element.change_current_dir(new_dir)
|
|
elif os.path.isdir(self.name):
|
|
new_dir = element.current_dir + self.get_short_dir_name()
|
|
element.change_current_dir(new_dir)
|
|
else:
|
|
element.confirm_pressed()
|
|
element.first_selection_made = False
|
|
|
|
|
|
class BaseFileChooserDialog(ChooserDialog):
|
|
"base class for choosers whose items correspond with files"
|
|
|
|
chooser_item_class = BaseFileChooserItem
|
|
show_filenames = True
|
|
file_extensions = []
|
|
|
|
def set_initial_dir(self):
|
|
self.current_dir = self.ui.app.documents_dir
|
|
self.field_texts[self.active_field] = self.current_dir
|
|
|
|
def get_filenames(self):
|
|
"subclasses override: get list of desired filenames"
|
|
return self.get_sorted_dir_list()
|
|
|
|
def get_sorted_dir_list(self):
|
|
"common code for getting sorted directory + file lists"
|
|
# list parent, then dirs, then filenames with extension(s)
|
|
parent = [] if self.current_dir == "/" else [".."]
|
|
if not os.path.exists(self.current_dir):
|
|
return parent
|
|
dirs, files = [], []
|
|
for filename in os.listdir(self.current_dir):
|
|
# skip unix-hidden files
|
|
if filename.startswith("."):
|
|
continue
|
|
full_filename = self.current_dir + filename
|
|
# if no extensions specified, take any file
|
|
if len(self.file_extensions) == 0:
|
|
self.file_extensions = [""]
|
|
for ext in self.file_extensions:
|
|
if os.path.isdir(full_filename):
|
|
dirs += [full_filename + "/"]
|
|
break
|
|
elif filename.lower().endswith(ext.lower()):
|
|
files += [full_filename]
|
|
break
|
|
dirs.sort(key=lambda x: x.lower())
|
|
files.sort(key=lambda x: x.lower())
|
|
return parent + dirs + files
|
|
|
|
def get_items(self):
|
|
"populate and return items from list of files, loading as needed"
|
|
items = []
|
|
# find all suitable files (images)
|
|
filenames = self.get_filenames()
|
|
# use manual counter, as we skip past some files that don't fit
|
|
i = 0
|
|
for filename in filenames:
|
|
item = self.chooser_item_class(i, filename)
|
|
if not item.valid:
|
|
continue
|
|
items.append(item)
|
|
i += 1
|
|
return items
|
|
|
|
|
|
#
|
|
# art chooser
|
|
#
|
|
|
|
|
|
class ArtChooserItem(BaseFileChooserItem):
|
|
# set in load()
|
|
art_width = None
|
|
hide_file_extension = True
|
|
|
|
def get_description_lines(self):
|
|
lines = BaseFileChooserItem.get_description_lines(self)
|
|
if lines is not None:
|
|
return lines
|
|
if not self.art_width:
|
|
return []
|
|
mod_time = time.gmtime(self.art_mod_time)
|
|
mod_time = time.strftime("%Y-%m-%d %H:%M:%S", mod_time)
|
|
lines = [f"last change: {mod_time}"]
|
|
line = f"{self.art_width} x {self.art_height}, "
|
|
line += f"{self.art_frames} frame"
|
|
# pluralize properly
|
|
line += "s" if self.art_frames > 1 else ""
|
|
line += f", {self.art_layers} layer"
|
|
line += "s" if self.art_layers > 1 else ""
|
|
lines += [line]
|
|
lines += [f"char: {self.art_charset}, pal: {self.art_palette}"]
|
|
return lines
|
|
|
|
def get_preview_texture(self, app):
|
|
if os.path.isdir(self.name):
|
|
return
|
|
thumbnail_filename = (
|
|
app.cache_dir + THUMBNAIL_CACHE_DIR + self.art_hash + ".png"
|
|
)
|
|
# create thumbnail if it doesn't exist
|
|
if not os.path.exists(thumbnail_filename):
|
|
write_thumbnail(app, self.name, thumbnail_filename)
|
|
# read thumbnail
|
|
img = Image.open(thumbnail_filename)
|
|
img = img.convert("RGBA")
|
|
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
|
return Texture(img.tobytes(), *img.size)
|
|
|
|
def load(self, app):
|
|
if os.path.isdir(self.name):
|
|
return
|
|
if not os.path.exists(self.name):
|
|
return
|
|
# get last modified time for description
|
|
self.art_mod_time = os.path.getmtime(self.name)
|
|
# get file's hash for unique thumbnail name
|
|
self.art_hash = app.get_file_hash(self.name)
|
|
# rather than load the entire art, just get some high level stats
|
|
d = json.load(open(self.name))
|
|
self.art_width, self.art_height = d["width"], d["height"]
|
|
self.art_frames = len(d["frames"])
|
|
self.art_layers = len(d["frames"][0]["layers"])
|
|
self.art_charset = d["charset"]
|
|
self.art_palette = d["palette"]
|
|
|
|
|
|
class ArtChooserDialog(BaseFileChooserDialog):
|
|
title = "Open art"
|
|
confirm_caption = "Open"
|
|
cancel_caption = "Cancel"
|
|
chooser_item_class = ArtChooserItem
|
|
flip_preview_y = False
|
|
directory_aware = True
|
|
file_extensions = [ART_FILE_EXTENSION]
|
|
|
|
def set_initial_dir(self):
|
|
# TODO: IF no art in Documents dir yet, start in app/art/ for examples?
|
|
# get last opened dir, else start in docs/game art dir
|
|
if self.ui.app.last_art_dir:
|
|
self.current_dir = self.ui.app.last_art_dir
|
|
else:
|
|
self.current_dir = (
|
|
self.ui.app.gw.game_dir
|
|
if self.ui.app.gw.game_dir
|
|
else self.ui.app.documents_dir
|
|
)
|
|
self.current_dir += ART_DIR
|
|
self.field_texts[self.active_field] = self.current_dir
|
|
|
|
def confirm_pressed(self):
|
|
if not os.path.exists(self.field_texts[0]):
|
|
return
|
|
self.ui.app.last_art_dir = self.current_dir
|
|
OpenCommand.execute(self.ui.console, [self.field_texts[0]])
|
|
self.dismiss()
|
|
|
|
|
|
#
|
|
# generic file chooser for importers
|
|
#
|
|
class GenericImportChooserDialog(BaseFileChooserDialog):
|
|
title = "Import %s"
|
|
confirm_caption = "Import"
|
|
cancel_caption = "Cancel"
|
|
# allowed extensions set by invoking
|
|
file_extensions = []
|
|
show_preview_image = False
|
|
directory_aware = True
|
|
|
|
def __init__(self, ui, options):
|
|
self.title %= ui.app.importer.format_name
|
|
self.file_extensions = ui.app.importer.allowed_file_extensions
|
|
BaseFileChooserDialog.__init__(self, ui, options)
|
|
|
|
def set_initial_dir(self):
|
|
if self.ui.app.last_import_dir:
|
|
self.current_dir = self.ui.app.last_import_dir
|
|
else:
|
|
self.current_dir = self.ui.app.documents_dir
|
|
self.field_texts[self.active_field] = self.current_dir
|
|
|
|
def confirm_pressed(self):
|
|
filename = self.field_texts[0]
|
|
if not os.path.exists(filename):
|
|
return
|
|
self.ui.app.last_import_dir = self.current_dir
|
|
self.dismiss()
|
|
# importer might offer a dialog for options
|
|
if self.ui.app.importer.options_dialog_class:
|
|
options = {"filename": filename}
|
|
self.ui.open_dialog(self.ui.app.importer.options_dialog_class, options)
|
|
else:
|
|
ImportOptionsDialog.do_import(self.ui.app, filename, {})
|
|
|
|
|
|
class ImageChooserItem(BaseFileChooserItem):
|
|
def get_preview_texture(self, app):
|
|
if os.path.isdir(self.name):
|
|
return
|
|
# may not be a valid image file
|
|
try:
|
|
img = Image.open(self.name)
|
|
except Exception:
|
|
return
|
|
try:
|
|
img = img.convert("RGBA")
|
|
except Exception:
|
|
# (probably) PIL bug: some images just crash! return None
|
|
return
|
|
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
|
return Texture(img.tobytes(), *img.size)
|
|
|
|
|
|
class ImageFileChooserDialog(BaseFileChooserDialog):
|
|
cancel_caption = "Cancel"
|
|
chooser_item_class = ImageChooserItem
|
|
flip_preview_y = False
|
|
directory_aware = True
|
|
file_extensions = ["png", "jpg", "jpeg", "bmp", "gif"]
|
|
|
|
|
|
class PaletteFromImageChooserDialog(ImageFileChooserDialog):
|
|
title = "Palette from image"
|
|
confirm_caption = "Choose"
|
|
|
|
def confirm_pressed(self):
|
|
if not os.path.exists(self.field_texts[0]):
|
|
return
|
|
# open new dialog, pipe our field 0 into its field 0
|
|
filename = self.field_texts[0]
|
|
self.dismiss()
|
|
self.ui.open_dialog(PaletteFromFileDialog)
|
|
self.ui.active_dialog.field_texts[0] = filename
|
|
# base new palette filename on source image
|
|
palette_filename = os.path.basename(filename)
|
|
palette_filename = os.path.splitext(palette_filename)[0]
|
|
self.ui.active_dialog.field_texts[1] = palette_filename
|
|
|
|
|
|
#
|
|
# palette chooser
|
|
#
|
|
|
|
|
|
class PaletteChooserItem(BaseFileChooserItem):
|
|
def get_label(self):
|
|
return os.path.splitext(self.name)[0]
|
|
|
|
def get_description_lines(self):
|
|
colors = len(self.palette.colors)
|
|
return [f"Unique colors: {str(colors - 1)}"]
|
|
|
|
def get_preview_texture(self, app):
|
|
return self.palette.src_texture
|
|
|
|
def load(self, app):
|
|
self.palette = app.load_palette(self.name)
|
|
|
|
|
|
class PaletteChooserDialog(BaseFileChooserDialog):
|
|
title = "Choose palette"
|
|
chooser_item_class = PaletteChooserItem
|
|
|
|
def get_initial_selection(self):
|
|
if not self.ui.active_art:
|
|
return 0
|
|
for item in self.items:
|
|
# depend on label being same as palette's internal name,
|
|
# eg filename minus extension
|
|
if item.label == self.ui.active_art.palette.name:
|
|
return item.index
|
|
# print("couldn't find initial selection for %s, returning 0" % self.__class__.__name__)
|
|
return 0
|
|
|
|
def get_filenames(self):
|
|
filenames = []
|
|
# search all files in dirs with appropriate extensions
|
|
for dirname in self.ui.app.get_dirnames(PALETTE_DIR, False):
|
|
for filename in os.listdir(dirname):
|
|
for ext in PALETTE_EXTENSIONS:
|
|
if filename.lower().endswith(ext.lower()):
|
|
filenames.append(filename)
|
|
filenames.sort(key=lambda x: x.lower())
|
|
return filenames
|
|
|
|
def confirm_pressed(self):
|
|
item = self.get_selected_item()
|
|
self.ui.active_art.set_palette(item.palette, log=True)
|
|
self.ui.popup.set_active_palette(item.palette)
|
|
|
|
|
|
#
|
|
# charset chooser
|
|
#
|
|
|
|
|
|
class CharsetChooserItem(BaseFileChooserItem):
|
|
def get_label(self):
|
|
return os.path.splitext(self.name)[0]
|
|
|
|
def get_description_lines(self):
|
|
# first comment in file = description
|
|
lines = []
|
|
for line in open(self.charset.filename, encoding="utf-8").readlines():
|
|
line = line.strip()
|
|
if line.startswith("//"):
|
|
lines.append(line[2:])
|
|
break
|
|
lines.append(f"Characters: {str(self.charset.last_index)}")
|
|
return lines
|
|
|
|
def get_preview_texture(self, app):
|
|
return self.charset.texture
|
|
|
|
def load(self, app):
|
|
self.charset = app.load_charset(self.name)
|
|
|
|
|
|
class CharSetChooserDialog(BaseFileChooserDialog):
|
|
title = "Choose character set"
|
|
flip_preview_y = False
|
|
chooser_item_class = CharsetChooserItem
|
|
|
|
def get_initial_selection(self):
|
|
if not self.ui.active_art:
|
|
return 0
|
|
for item in self.items:
|
|
if item.label == self.ui.active_art.charset.name:
|
|
return item.index
|
|
# print("couldn't find initial selection for %s, returning 0" % self.__class__.__name__)
|
|
return 0
|
|
|
|
def get_filenames(self):
|
|
filenames = []
|
|
# search all files in dirs with appropriate extensions
|
|
for dirname in self.ui.app.get_dirnames(CHARSET_DIR, False):
|
|
for filename in os.listdir(dirname):
|
|
if filename.lower().endswith(CHARSET_FILE_EXTENSION.lower()):
|
|
filenames.append(filename)
|
|
filenames.sort(key=lambda x: x.lower())
|
|
return filenames
|
|
|
|
def confirm_pressed(self):
|
|
item = self.get_selected_item()
|
|
self.ui.active_art.set_charset(item.charset, log=True)
|
|
self.ui.popup.set_active_charset(item.charset)
|
|
# change in charset aspect should be treated as a resize
|
|
# for purposes of grid, camera, cursor, overlay
|
|
self.ui.adjust_for_art_resize(self.ui.active_art)
|
|
|
|
|
|
class ArtScriptChooserItem(BaseFileChooserItem):
|
|
def get_label(self):
|
|
label = os.path.splitext(self.name)[0]
|
|
return os.path.basename(label)
|
|
|
|
def get_description_lines(self):
|
|
lines = []
|
|
# read every comment line until a non-comment line is encountered
|
|
for line in self.script.readlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
if not line.startswith("#"):
|
|
break
|
|
# snip #
|
|
line = line[line.index("#") + 1 :]
|
|
lines.append(line)
|
|
return lines
|
|
|
|
def load(self, app):
|
|
self.script = open(self.name)
|
|
|
|
|
|
class RunArtScriptDialog(BaseFileChooserDialog):
|
|
title = "Run Artscript"
|
|
tile_width, big_width = 70, 90
|
|
tile_height, big_height = 15, 25
|
|
chooser_item_class = ArtScriptChooserItem
|
|
show_preview_image = False
|
|
|
|
def get_filenames(self):
|
|
filenames = []
|
|
# search all files in dirs with appropriate extensions
|
|
for dirname in self.ui.app.get_dirnames(ART_SCRIPT_DIR, False):
|
|
for filename in os.listdir(dirname):
|
|
if filename.lower().endswith(SCRIPT_FILE_EXTENSION.lower()):
|
|
filenames.append(dirname + filename)
|
|
filenames.sort(key=lambda x: x.lower())
|
|
return filenames
|
|
|
|
def confirm_pressed(self):
|
|
item = self.get_selected_item()
|
|
self.ui.app.last_art_script = item.name
|
|
self.ui.active_art.run_script(item.name, log=False)
|
|
self.dismiss()
|
|
|
|
|
|
class OverlayImageFileChooserDialog(ImageFileChooserDialog):
|
|
title = "Choose overlay image"
|
|
confirm_caption = "Choose"
|
|
|
|
def confirm_pressed(self):
|
|
filename = self.field_texts[0]
|
|
self.ui.app.set_overlay_image(filename)
|
|
self.dismiss()
|