playscii/ui_file_chooser_dialog.py

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()