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 = ["last change: {}".format(mod_time)] line = "{} x {}, ".format(self.art_width, self.art_height) line += "{} frame".format(self.art_frames) # pluralize properly line += "s" if self.art_frames > 1 else "" line += ", {} layer".format(self.art_layers) line += "s" if self.art_layers > 1 else "" lines += [line] lines += ["char: {}, pal: {}".format(self.art_charset, 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 ["Unique colors: {}".format(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("Characters: {}".format(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()