class NewGameDirDialog(UIDialog): title = 'New game' field0_label = 'Name of new game folder:' field1_label = 'Name of new game:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False), Field(label=field1_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Create' game_mode_visible = True # TODO: only allow names that don't already exist def get_initial_field_text(self, field_number): # provide a reasonable non-blank name if field_number == 0: return 'newgame' elif field_number == 1: return type(self.ui.app.gw).game_title def confirm_pressed(self): if self.ui.app.gw.create_new_game(self.field_texts[0], self.field_texts[1]): self.ui.app.enter_game_mode() self.dismiss()
class AddRoomDialog(UIDialog): title = 'Add new room' field0_label = 'Name for new room:' field1_label = 'Class of new room:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False), Field(label=field1_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Add' game_mode_visible = True invalid_room_name_error = 'Invalid room name.' def get_initial_field_text(self, field_number): # provide a reasonable non-blank name if field_number == 0: return 'Room ' + str(len(self.ui.app.gw.rooms) + 1) elif field_number == 1: return 'GameRoom' def is_input_valid(self): return self.field_texts[0] != '', self.invalid_room_name_error def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return self.ui.app.gw.add_room(self.field_texts[0], self.field_texts[1]) self.dismiss()
class SaveAsDialog(BaseFileDialog): title = 'Save art' field0_label = 'New filename for art:' field2_label = 'Save folder:' field3_label = ' %s' tile_width = 60 field0_width = 56 y_spacing = 0 fields = [ Field(label=field0_label, type=str, width=field0_width, oneline=False), Field(label='', type=None, width=0, oneline=True), Field(label=field2_label, type=None, width=0, oneline=True), Field(label=field3_label, type=None, width=0, oneline=True), Field(label='', type=None, width=0, oneline=True) ] confirm_caption = 'Save' always_redraw_labels = True def get_initial_field_text(self, field_number): if field_number == 0: # special case: if opening playscii/art/new, change # it to documents dir to avoid writing to application dir # (still possible if you open other files) if os.path.dirname(self.ui.active_art.filename) == ART_DIR[:-1]: self.ui.active_art.filename = self.ui.app.documents_dir + self.ui.active_art.filename # TODO: handle other files from app dir as well? not as important filename = os.path.basename(self.ui.active_art.filename) filename = os.path.splitext(filename)[0] return filename return '' def get_file_extension(self): """ Return file extension this dialog saves as; other dialogs are based on this class so we don't want to hardcore .psci """ return ART_FILE_EXTENSION def get_dir(self): return os.path.dirname(self.ui.active_art.filename) def get_field_label(self, field_index): label = self.fields[field_index].label # show dir art will be saved into if field_index == 3: label %= self.get_dir() return label def is_input_valid(self): return self.is_filename_valid(0) def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return SaveCommand.execute(self.ui.console, [self.field_texts[0]]) self.dismiss()
class PNGExportOptionsDialog(ExportOptionsDialog): title = 'PNG image export options' field0_label = 'Scale factor (%s pixels)' field1_label = 'CRT filter' fields = [ Field(label=field0_label, type=int, width=6, oneline=False), Field(label=field1_label, type=bool, width=0, oneline=True) ] # redraw dynamic labels always_redraw_labels = True invalid_scale_error = 'Scale must be greater than 0' def get_initial_field_text(self, field_number): if field_number == 0: return str(DEFAULT_SCALE) elif field_number == 1: return [' ', UIDialog.true_field_text][DEFAULT_CRT] def get_field_label(self, field_index): label = self.fields[field_index].label if field_index == 0: valid, _ = self.is_input_valid() if not valid: label %= '???' else: # calculate exported image size art = self.ui.active_art scale = int(self.field_texts[0]) width = art.charset.char_width * art.width * scale height = art.charset.char_height * art.height * scale label %= '%s x %s' % (width, height) return label def is_input_valid(self): # scale factor: >0 int try: int(self.field_texts[0]) except: return False, self.invalid_scale_error if int(self.field_texts[0]) <= 0: return False, self.invalid_scale_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return self.dismiss() # compile options for exporter options = { 'scale': int(self.field_texts[0]), 'crt': bool(self.field_texts[1].strip()) } ExportOptionsDialog.do_export(self.ui.app, self.filename, options)
class AddFrameDialog(UIDialog): title = 'Add new frame' field0_label = 'Index to add frame before:' field1_label = 'Hold time (in seconds) for new frame:' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=int, width=field_width, oneline=True), Field(label=field1_label, type=float, width=field_width, oneline=False) ] confirm_caption = 'Add' invalid_index_error = 'Invalid index. (1-%s allowed)' invalid_delay_error = 'Invalid hold time.' def get_initial_field_text(self, field_number): if field_number == 0: return str(self.ui.active_art.frames + 1) elif field_number == 1: return str(DEFAULT_FRAME_DELAY) def is_valid_frame_index(self, index): try: index = int(index) except: return False if index < 1 or index > self.ui.active_art.frames + 1: return False return True def is_valid_frame_delay(self, delay): try: delay = float(delay) except: return False return delay > 0 def is_input_valid(self): if not self.is_valid_frame_index(self.field_texts[0]): return False, self.invalid_index_error % str( self.ui.active_art.frames + 1) if not self.is_valid_frame_delay(self.field_texts[1]): return False, self.invalid_delay_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return index = int(self.field_texts[0]) delay = float(self.field_texts[1]) self.ui.active_art.insert_frame_before_index(index - 1, delay) self.dismiss()
class AddLayerDialog(UIDialog): title = 'Add new layer' field0_label = 'Name for new layer:' field1_label = 'Z-depth for new layer:' field0_width = UIDialog.default_field_width field1_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=str, width=field0_width, oneline=False), Field(label=field1_label, type=float, width=field1_width, oneline=True) ] confirm_caption = 'Add' name_exists_error = 'Layer by that name already exists.' invalid_z_error = 'Invalid number.' def get_initial_field_text(self, field_number): if field_number == 0: return 'Layer %s' % str(self.ui.active_art.layers + 1) elif field_number == 1: return str( self.ui.active_art.layers_z[self.ui.active_art.active_layer] + DEFAULT_LAYER_Z_OFFSET) def is_valid_layer_name(self, name, exclude_active_layer=False): for i, layer_name in enumerate(self.ui.active_art.layer_names): if exclude_active_layer and i == self.ui.active_layer: continue if layer_name == name: return False return True def is_input_valid(self): valid_name = self.is_valid_layer_name(self.field_texts[0]) if not valid_name: return False, self.name_exists_error try: z = float(self.field_texts[1]) except: return False, self.invalid_z_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return name = self.field_texts[0] z = float(self.field_texts[1]) self.ui.active_art.add_layer(z, name) self.dismiss()
class EDSCIIImportOptionsDialog(ImportOptionsDialog): title = 'Import EDSCII (legacy format) art' field0_label = 'Width override (leave 0 to guess):' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=int, width=field_width, oneline=False) ] invalid_width_error = 'Invalid width override.' def get_initial_field_text(self, field_number): if field_number == 0: return '0' return '' def is_input_valid(self): # valid widths: any >=0 int try: int(self.field_texts[0]) except: return False, self.invalid_width_error if int(self.field_texts[0]) < 0: return False, self.invalid_width_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return width = int(self.field_texts[0]) width = width if width > 0 else None options = {'width_override':width} self.dismiss() # self.filename is set in our importer's file_chooser_dialog_class ImportOptionsDialog.do_import(self.ui.app, self.filename, options)
class EditObjectPropertyDialog(UIDialog): "dialog invoked by panel property click, modified at runtime as needed" base_title = 'Set %s' field0_base_label = 'New %s for %s:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_base_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Set' center_in_window = False game_mode_visible = True def is_input_valid(self): try: self.fields[0].type(self.field_texts[0]) except: return False, '' return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return # set property for selected object(s) new_value = self.fields[0].type(self.field_texts[0]) for obj in self.ui.app.gw.selected_objects: obj.set_object_property(self.item.prop_name, new_value) self.dismiss() # restore keyboard focus to edit panel self.ui.keyboard_focus_element = self.ui.edit_object_panel
class FrameDelayDialog(AddFrameDialog): field0_label = 'New hold time (in seconds) for frame:' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=float, width=field_width, oneline=False) ] confirm_caption = 'Set' def get_initial_field_text(self, field_number): if field_number == 0: return str(self.ui.active_art.frame_delays[ self.ui.active_art.active_frame]) def is_input_valid(self): if not self.is_valid_frame_delay(self.field_texts[0]): return False, self.invalid_delay_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return delay = float(self.field_texts[0]) self.ui.active_art.frame_delays[ self.ui.active_art.active_frame] = delay self.dismiss()
class SetCameraZoomDialog(UIDialog): title = 'Set camera zoom' field0_label = 'New camera zoom %:' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=float, width=field_width, oneline=True) ] confirm_caption = 'Set' invalid_zoom_error = 'Zoom % must be a number greater than zero.' all_modes_visible = True game_mode_visible = True def get_initial_field_text(self, field_number): if field_number == 0: return '%.1f' % self.ui.app.camera.get_current_zoom_pct() return '' def is_input_valid(self): try: zoom = float(self.field_texts[0]) except: return False, self.invalid_zoom_error if zoom <= 0: return False, self.invalid_zoom_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return new_zoom_pct = float(self.field_texts[0]) camera = self.ui.app.camera camera.z = camera.get_base_zoom() / (new_zoom_pct / 100) self.dismiss()
class SetLayerZDialog(UIDialog): title = 'Set layer Z-depth' field0_label = 'Z-depth for layer:' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=float, width=field_width, oneline=False) ] confirm_caption = 'Set' invalid_z_error = 'Invalid number.' def get_initial_field_text(self, field_number): # populate with existing z if field_number == 0: return str( self.ui.active_art.layers_z[self.ui.active_art.active_layer]) def is_input_valid(self): try: z = float(self.field_texts[0]) except: return False, self.invalid_z_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return new_z = float(self.field_texts[0]) self.ui.active_art.layers_z[self.ui.active_art.active_layer] = new_z self.ui.active_art.set_unsaved_changes(True) self.ui.app.grid.reset() self.dismiss()
class PaletteFromFileDialog(UIDialog): title = 'Create palette from file' field0_label = 'Filename to create palette from:' field1_label = 'Filename for new palette:' field2_label = 'Colors in new palette:' field0_width = field1_width = UIDialog.default_field_width field2_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=str, width=field0_width, oneline=False), Field(label=field1_label, type=str, width=field1_width, oneline=False), Field(label=field2_label, type=int, width=field2_width, oneline=True) ] confirm_caption = 'Create' invalid_color_error = 'Palettes must be between 2 and 256 colors.' bad_output_filename_error = 'Enter a filename for the new palette.' def get_initial_field_text(self, field_number): # NOTE: PaletteFromImageChooserDialog.confirm_pressed which invokes us # sets fields 0 and 1 if field_number == 2: return str(256) return '' def valid_colors(self, colors): try: c = int(colors) except: return False return 2 <= c <= 256 def is_input_valid(self): valid_colors = self.valid_colors(self.field_texts[2]) if not valid_colors: return False, self.invalid_color_error if not self.field_texts[1].strip(): return False, self.bad_output_filename_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return src_filename = self.field_texts[0] palette_filename = self.field_texts[1] colors = int(self.field_texts[2]) new_pal = PaletteFromFile(self.ui.app, src_filename, palette_filename, colors) self.dismiss()
class SaveGameStateDialog(UIDialog): title = 'Save game state' field_label = 'New filename for game state:' field_width = UIDialog.default_field_width fields = [ Field(label=field_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Save' game_mode_visible = True def confirm_pressed(self): SaveGameStateCommand.execute(self.ui.console, [self.field_texts[0]]) self.dismiss()
class FrameDelayAllDialog(FrameDelayDialog): field0_label = 'New hold time (in seconds) for all frames:' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=float, width=field_width, oneline=False) ] def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return delay = float(self.field_texts[0]) for i in range(self.ui.active_art.frames): self.ui.active_art.frame_delays[i] = delay self.dismiss()
class SetRoomEdgeWarpsDialog(UIDialog): title = 'Set room edge warps' tile_width = 48 fields = 4 field0_label = 'Name of room/object to warp at LEFT edge:' field1_label = 'Name of room/object to warp at RIGHT edge:' field2_label = 'Name of room/object to warp at TOP edge:' field3_label = 'Name of room/object to warp at BOTTOM edge:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False), Field(label=field1_label, type=str, width=field_width, oneline=False), Field(label=field2_label, type=str, width=field_width, oneline=False), Field(label=field3_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Set' game_mode_visible = True def get_initial_field_text(self, field_number): room = self.ui.app.gw.current_room names = {0: room.left_edge_warp_dest_name, 1: room.right_edge_warp_dest_name, 2: room.top_edge_warp_dest_name, 3: room.bottom_edge_warp_dest_name} return names[field_number] def dismiss(self): self.ui.edit_list_panel.set_list_operation(LO_NONE) UIDialog.dismiss(self) def confirm_pressed(self): room = self.ui.app.gw.current_room room.left_edge_warp_dest_name = self.field_texts[0] room.right_edge_warp_dest_name = self.field_texts[1] room.top_edge_warp_dest_name = self.field_texts[2] room.bottom_edge_warp_dest_name = self.field_texts[3] room.reset_edge_warps() self.dismiss()
class SetLayerNameDialog(AddLayerDialog): title = 'Set layer name' field0_label = 'New name for this layer:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Rename' def confirm_pressed(self): new_name = self.field_texts[0] self.ui.active_art.layer_names[ self.ui.active_art.active_layer] = new_name self.ui.active_art.set_unsaved_changes(True) self.dismiss()
class LoadGameStateDialog(UIDialog): title = 'Open game state' field_label = 'Game state file to open:' field_width = UIDialog.default_field_width fields = [ Field(label=field_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Open' game_mode_visible = True # TODO: only allow valid game state file in current game directory def confirm_pressed(self): LoadGameStateCommand.execute(self.ui.console, [self.field_texts[0]]) self.dismiss()
class SetRoomCamDialog(UIDialog): title = 'Set room camera marker' tile_width = 48 field0_label = 'Name of location marker object for this room:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Set' game_mode_visible = True def dismiss(self): self.ui.edit_list_panel.set_list_operation(LO_NONE) UIDialog.dismiss(self) def confirm_pressed(self): self.ui.app.gw.current_room.set_camera_marker_name(self.field_texts[0]) self.dismiss()
def __init__(self, ui, options): self.ui = ui # semikludge: track whether user has selected anything in a new dir, # so double click behavior is consistent even on initial selections self.first_selection_made = False if self.ui.width_tiles - 20 > self.big_width: self.tile_width = self.big_width self.fields[0] = Field(label='', type=str, width=self.tile_width - 4, oneline=True) if self.ui.height_tiles - 30 > self.big_height: self.tile_height = self.big_height self.items_in_view = self.tile_height - self.item_start_y - 3 self.field_texts = [''] # set active field earlier than UIDialog.init so set_initial_dir # can change its text self.active_field = 0 if self.directory_aware: self.set_initial_dir() self.selected_item_index = 0 # scroll index - how far into items list current screen view begins self.scroll_index = 0 self.items = self.get_items() self.set_selected_item_index(self.get_initial_selection(), True, False) self.load_selected_item() # start scroll index higher if initial selection would be offscreen if self.selected_item_index >= self.items_in_view: self.scroll_index = self.selected_item_index - self.items_in_view + 1 # for convenience, create another list where 1st item button starts at 0 self.item_buttons = [] self.up_arrow_button, self.down_arrow_button = None, None # marker for preview drawing self.description_end_y = 0 # UIDialog init runs: reset_art, draw_buttons etc UIDialog.__init__(self, ui, options) # UIDialog/UIElement initializes self.buttons, create item buttons after self.init_buttons() self.reset_art(False) # preview SpriteRenderable (loaded on item change?) self.preview_renderable = UISpriteRenderable(ui.app) # don't blend preview images, eg charsets self.preview_renderable.blend = False # offset into items list view provided by buttons starts from self.position_preview()
class FrameIndexDialog(AddFrameDialog): field0_label = 'Move this frame before index:' field_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=int, width=field_width, oneline=False) ] confirm_caption = 'Set' def is_input_valid(self): if not self.is_valid_frame_index(self.field_texts[0]): return False, self.invalid_index_error % str( self.ui.active_art.frames + 1) return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return # set new frame index (effectively moving it in the sequence) dest_index = int(self.field_texts[0]) self.ui.active_art.move_frame_to_index(self.ui.active_art.active_frame, dest_index) self.dismiss()
class SetRoomBoundsObjDialog(UIDialog): title = 'Set room edge object' field0_label = 'Name of object to use for room bounds:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Set' game_mode_visible = True def get_initial_field_text(self, field_number): if field_number == 0: return self.ui.app.gw.current_room.warp_edge_bounds_obj_name def dismiss(self): self.ui.edit_list_panel.set_list_operation(LO_NONE) UIDialog.dismiss(self) def confirm_pressed(self): room = self.ui.app.gw.current_room room.warp_edge_bounds_obj_name = self.field_texts[0] room.reset_edge_warps() self.dismiss()
def clicked_item(self, item): # if property is a bool just toggle/set it, no need for a dialog if item.prop_type is bool: for obj in self.world.selected_objects: # if multiple object values vary, set it True if item.prop_value == PropertyItem.multi_value_text: obj.set_object_property(item.prop_name, True) else: val = getattr(obj, item.prop_name) obj.set_object_property(item.prop_name, not val) return # set dialog values appropriate to property being edited EditObjectPropertyDialog.title = EditObjectPropertyDialog.base_title % item.prop_name # can't set named tuple values directly, build a new one and set it old_field = EditObjectPropertyDialog.fields[0] new_label = EditObjectPropertyDialog.field0_base_label % ( item.prop_type.__name__, item.prop_name) new_type = item.prop_type # if None, assume string if item.prop_type is type(None): new_type = str new_field = Field(label=new_label, type=new_type, width=old_field.width, oneline=old_field.oneline) EditObjectPropertyDialog.fields[0] = new_field tile_x = int(self.ui.width_tiles * self.ui.scale) - self.tile_width tile_x -= EditObjectPropertyDialog.tile_width # give dialog a handle to item EditObjectPropertyDialog.item = item self.ui.open_dialog(EditObjectPropertyDialog) self.ui.active_dialog.field_texts[0] = str(item.prop_value) self.ui.active_dialog.tile_x, self.ui.active_dialog.tile_y = tile_x, self.tile_y self.ui.active_dialog.reset_loc()
class RenameRoomDialog(UIDialog): title = 'Rename room' field0_label = 'New name for current room:' field_width = UIDialog.default_field_width fields = [ Field(label=field0_label, type=str, width=field_width, oneline=False) ] confirm_caption = 'Rename' game_mode_visible = True invalid_room_name_error = 'Invalid room name.' def get_initial_field_text(self, field_number): if field_number == 0: return self.ui.app.gw.current_room.name def is_input_valid(self): return self.field_texts[0] != '', self.invalid_room_name_error def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return world = self.ui.app.gw world.rename_room(world.current_room, self.field_texts[0]) self.dismiss()
class ResizeArtDialog(UIDialog): title = 'Resize art' field_width = UIDialog.default_short_field_width field0_label = 'New Width:' field1_label = 'New Height:' field2_label = 'Crop Start X:' field3_label = 'Crop Start Y:' field4_label = 'Fill new tiles with BG color' fields = [ Field(label=field0_label, type=int, width=field_width, oneline=True), Field(label=field1_label, type=int, width=field_width, oneline=True), Field(label=field2_label, type=int, width=field_width, oneline=True), Field(label=field3_label, type=int, width=field_width, oneline=True), Field(label=field4_label, type=bool, width=0, oneline=True) ] confirm_caption = 'Resize' invalid_width_error = 'Invalid width.' invalid_height_error = 'Invalid height.' invalid_start_error = 'Invalid crop origin.' def get_initial_field_text(self, field_number): if field_number == 0: return str(self.ui.active_art.width) elif field_number == 1: return str(self.ui.active_art.height) elif field_number == 4: return UIDialog.true_field_text else: return '0' def is_input_valid(self): "file can't already exist, dimensions must be >0 and <= max" if not self.is_valid_dimension(self.field_texts[0], self.ui.app.max_art_width): return False, self.invalid_width_error if not self.is_valid_dimension(self.field_texts[1], self.ui.app.max_art_height): return False, self.invalid_height_error try: int(self.field_texts[2]) except: return False, self.invalid_start_error if not 0 <= int(self.field_texts[2]) < self.ui.active_art.width: return False, self.invalid_start_error try: int(self.field_texts[3]) except: return False, self.invalid_start_error if not 0 <= int(self.field_texts[3]) < self.ui.active_art.height: return False, self.invalid_start_error return True, None def is_valid_dimension(self, dimension, max_dimension): try: dimension = int(dimension) except: return False return 0 < dimension <= max_dimension def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return w, h = int(self.field_texts[0]), int(self.field_texts[1]) start_x, start_y = int(self.field_texts[2]), int(self.field_texts[3]) bg_fill = bool(self.field_texts[4].strip()) self.ui.resize_art(self.ui.active_art, w, h, start_x, start_y, bg_fill) self.dismiss()
class NewArtDialog(BaseFileDialog): title = 'New art' field0_label = 'Filename of new art:' field2_label = 'Width:' field4_label = 'Height:' field6_label = 'Save folder:' field7_label = ' %s' tile_width = 60 field0_width = 56 y_spacing = 0 field1_width = field2_width = UIDialog.default_short_field_width fields = [ Field(label=field0_label, type=str, width=field0_width, oneline=False), Field(label='', type=None, width=0, oneline=True), Field(label=field2_label, type=int, width=field1_width, oneline=True), Field(label='', type=None, width=0, oneline=True), Field(label=field4_label, type=int, width=field2_width, oneline=True), Field(label='', type=None, width=0, oneline=True), Field(label=field6_label, type=None, width=0, oneline=True), Field(label=field7_label, type=None, width=0, oneline=True), Field(label='', type=None, width=0, oneline=True) ] confirm_caption = 'Create' invalid_width_error = 'Invalid width.' invalid_height_error = 'Invalid height.' def get_initial_field_text(self, field_number): if field_number == 0: return 'new%s' % len(self.ui.app.art_loaded_for_edit) elif field_number == 2: return str(DEFAULT_WIDTH) elif field_number == 4: return str(DEFAULT_HEIGHT) return '' def get_field_label(self, field_index): label = self.fields[field_index].label # show dir art will be saved into if field_index == 7: label %= self.get_dir() return label def get_file_extension(self): return ART_FILE_EXTENSION def get_dir(self): return self.ui.app.documents_dir + ART_DIR def is_input_valid(self): "warn if file already exists, dimensions must be >0 and <= max" if not self.is_valid_dimension(self.field_texts[2], self.ui.app.max_art_width): return False, self.invalid_width_error if not self.is_valid_dimension(self.field_texts[4], self.ui.app.max_art_height): return False, self.invalid_height_error return self.is_filename_valid(0) def is_valid_dimension(self, dimension, max_dimension): try: dimension = int(dimension) except: return False return 0 < dimension <= max_dimension def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return name = self.field_texts[0] w, h = int(self.field_texts[2]), int(self.field_texts[4]) self.ui.app.new_art_for_edit(name, w, h) self.ui.app.log('Created %s.psci with size %s x %s' % (name, w, h)) self.dismiss()
class ChooserDialog(UIDialog): title = 'Chooser' confirm_caption = 'Set' cancel_caption = 'Close' message = '' # if True, chooser shows files; show filename on first line of description show_filenames = False directory_aware = False tile_width, tile_height = 60, 20 # use these if screen is big enough big_width, big_height = 80, 30 fields = [ Field(label='', type=str, width=tile_width - 4, oneline=True) ] item_start_x, item_start_y = 2, 4 no_preview_label = 'No preview available!' show_preview_image = True item_button_class = ChooserItemButton chooser_item_class = ChooserItem scrollbar_shade_char = 54 flip_preview_y = True def __init__(self, ui, options): self.ui = ui # semikludge: track whether user has selected anything in a new dir, # so double click behavior is consistent even on initial selections self.first_selection_made = False if self.ui.width_tiles - 20 > self.big_width: self.tile_width = self.big_width self.fields[0] = Field(label='', type=str, width=self.tile_width - 4, oneline=True) if self.ui.height_tiles - 30 > self.big_height: self.tile_height = self.big_height self.items_in_view = self.tile_height - self.item_start_y - 3 self.field_texts = [''] # set active field earlier than UIDialog.init so set_initial_dir # can change its text self.active_field = 0 if self.directory_aware: self.set_initial_dir() self.selected_item_index = 0 # scroll index - how far into items list current screen view begins self.scroll_index = 0 self.items = self.get_items() self.set_selected_item_index(self.get_initial_selection(), True, False) self.load_selected_item() # start scroll index higher if initial selection would be offscreen if self.selected_item_index >= self.items_in_view: self.scroll_index = self.selected_item_index - self.items_in_view + 1 # for convenience, create another list where 1st item button starts at 0 self.item_buttons = [] self.up_arrow_button, self.down_arrow_button = None, None # marker for preview drawing self.description_end_y = 0 # UIDialog init runs: reset_art, draw_buttons etc UIDialog.__init__(self, ui, options) # UIDialog/UIElement initializes self.buttons, create item buttons after self.init_buttons() self.reset_art(False) # preview SpriteRenderable (loaded on item change?) self.preview_renderable = UISpriteRenderable(ui.app) # don't blend preview images, eg charsets self.preview_renderable.blend = False # offset into items list view provided by buttons starts from self.position_preview() def init_buttons(self): for i in range(self.items_in_view): button = self.item_button_class(self) button.x = self.item_start_x button.y = i + self.item_start_y self.buttons.append(button) self.item_buttons.append(button) # create scrollbar buttons self.item_button_width = self.item_buttons[0].width self.up_arrow_button = ScrollArrowButton(self) self.up_arrow_button.x = self.item_start_x + self.item_button_width self.up_arrow_button.y = self.item_start_y self.down_arrow_button = ScrollArrowButton(self) self.down_arrow_button.x = self.item_start_x + self.item_button_width self.down_arrow_button.y = self.item_start_y + self.items_in_view - 1 self.down_arrow_button.up = False self.buttons += [self.up_arrow_button, self.down_arrow_button] def set_initial_dir(self): # for directory-aware dialogs, subclasses specify here where to start self.current_dir = '.' def change_current_dir(self, new_dir): # check permissions: # os.access(new_dir, os.R_OK) seems to always return True, # so try/catch listdir instead try: l = os.listdir(new_dir) except PermissionError as e: line = 'No permission to access %s!' % os.path.abspath(new_dir) self.ui.message_line.post_line(line, error=True) return False self.current_dir = new_dir if not self.current_dir.endswith('/'): self.current_dir += '/' # redo items and redraw self.selected_item_index = 0 self.scroll_index = 0 self.field_texts[self.active_field] = self.current_dir self.items = self.get_items() self.reset_art(False) return True def set_selected_item_index(self, new_index, set_field_text=True, update_view=True): """ set the view's selected item to specified index perform usually-necessary refresh functions for convenience """ move_dir = new_index - self.selected_item_index self.selected_item_index = new_index can_scroll = len(self.items) > self.items_in_view should_scroll = self.selected_item_index >= self.scroll_index + self.items_in_view or self.selected_item_index < self.scroll_index if not can_scroll: self.scroll_index = 0 elif should_scroll: # keep selection in bounds self.selected_item_index = min(self.selected_item_index, len(self.items)-1) # scrolling up if move_dir <= 0: self.scroll_index = self.selected_item_index # scrolling down elif move_dir > 0 and self.selected_item_index - self.scroll_index == self.items_in_view: self.scroll_index = self.selected_item_index - self.items_in_view + 1 # keep scroll in bounds self.scroll_index = min(self.scroll_index, self.get_max_scroll()) # don't select/load null items if set_field_text: item = self.get_selected_item() if item: self.field_texts[self.active_field] = item.name if update_view: self.load_selected_item() self.reset_art(False) self.position_preview() def get_max_scroll(self): return len(self.items) - self.items_in_view def get_selected_item(self): # return None if out of bounds return self.items[self.selected_item_index] if self.selected_item_index < len(self.items) else None def load_selected_item(self): item = self.get_selected_item() item.load(self.ui.app) def get_initial_selection(self): # subclasses return index of initial selection return 0 def set_preview(self): item = self.get_selected_item() if self.show_preview_image: self.preview_renderable.texture = item.get_preview_texture(self.ui.app) def get_items(self): # subclasses generate lists of items here return [] def position_preview(self, reset=True): if reset: self.set_preview() if not self.preview_renderable.texture: return qw, qh = self.art.quad_width, self.art.quad_height # determine x position, then width as (dialog width - x) x = (self.item_button_width + self.item_start_x + 3) * qw self.preview_renderable.x = self.x + x self.preview_renderable.scale_x = (self.tile_width - 2) * qw - x # determine height based on width, then y position img_inv_aspect = self.preview_renderable.texture.height / self.preview_renderable.texture.width screen_aspect = self.ui.app.window_width / self.ui.app.window_height self.preview_renderable.scale_y = self.preview_renderable.scale_x * img_inv_aspect * screen_aspect y = (self.description_end_y + 1) * qh # if preview height is above max allotted size, set height to fill size # and scale down width max_y = (self.tile_height - 3) * qh if self.preview_renderable.scale_y > max_y - y: self.preview_renderable.scale_y = max_y - y self.preview_renderable.scale_x = self.preview_renderable.scale_y * (1 / img_inv_aspect) * (1 / screen_aspect) # flip in Y for some (palettes) but not for others (charsets) if self.flip_preview_y: self.preview_renderable.scale_y = -self.preview_renderable.scale_y else: y += self.preview_renderable.scale_y self.preview_renderable.y = self.y - y def get_height(self, msg_lines): return self.tile_height def reset_buttons(self): # (re)generate buttons from contents of self.items for i,button in enumerate(self.item_buttons): # ??? each button's callback loads charset/palette/whatev if i >= len(self.items): button.never_draw = True # clear item, might be left over from a previous dir view! button.item = None continue item = self.items[self.scroll_index + i] button.caption = item.label button.item = item button.never_draw = False # highlight selected item if i == self.selected_item_index - self.scroll_index: button.normal_fg_color = UIButton.clicked_fg_color button.normal_bg_color = UIButton.clicked_bg_color # still allow hover and click button.hovered_fg_color = UIButton.clicked_fg_color button.hovered_bg_color = UIButton.clicked_bg_color else: button.normal_fg_color = UIButton.normal_fg_color button.normal_bg_color = UIButton.normal_bg_color button.hovered_fg_color = UIButton.hovered_fg_color button.hovered_bg_color = UIButton.hovered_bg_color # init_buttons has not yet run on first reset_art if not self.up_arrow_button: return # dim scroll buttons if we don't have enough items to scroll state, hover = 'normal', True if len(self.items) <= self.items_in_view: state = 'dimmed' hover = False for button in [self.up_arrow_button, self.down_arrow_button]: button.set_state(state) button.can_hover = hover def get_description_filename(self, item): "returns a description-appropriate filename for given item" # truncate from start to fit in description area if needed max_width = self.tile_width max_width -= self.item_start_x + self.item_button_width + 5 if len(item.name) > max_width - 1: return '…' + item.name[-max_width:] return item.name def get_selected_description_lines(self): item = self.get_selected_item() lines = [] if self.show_filenames: lines += [self.get_description_filename(item)] lines += item.get_description_lines() or [] return lines def draw_selected_description(self): x = self.tile_width - 2 y = self.item_start_y lines = self.get_selected_description_lines() for line in lines: # trim line if it's too long max_width = self.tile_width - self.item_button_width - 7 line = line[:max_width] self.art.write_string(0, 0, x, y, line, None, None, right_justify=True) y += 1 self.description_end_y = y def reset_art(self, resize=True): self.reset_buttons() # UIDialog does: clear window, draw titlebar and confirm/cancel buttons # doesn't: draw message or fields UIDialog.reset_art(self, resize, clear_buttons=False) # init_buttons hasn't run yet on first call to reset_art if not self.up_arrow_button: return self.draw_selected_description() # draw scrollbar shading # dim if no scrolling fg = self.up_arrow_button.normal_fg_color if len(self.items) <= self.items_in_view: fg = self.up_arrow_button.dimmed_fg_color for y in range(self.up_arrow_button.y + 1, self.down_arrow_button.y): self.art.set_tile_at(0, 0, self.up_arrow_button.x, y, self.scrollbar_shade_char, fg) def update_drag(self, mouse_dx, mouse_dy): UIDialog.update_drag(self, mouse_dx, mouse_dy) # update thumbnail renderable's position too self.position_preview(False) def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed): keystr = sdl2.SDL_GetKeyName(key).decode() # up/down keys navigate list new_index = self.selected_item_index navigated = False if keystr == 'Return': # if handle_enter returns True, bail before rest of input handling - # make sure any changes to handle_enter are safe for this! if self.handle_enter(shift_pressed, alt_pressed, ctrl_pressed): return elif keystr == 'Up': navigated = True if self.selected_item_index > 0: new_index -= 1 elif keystr == 'Down': navigated = True if self.selected_item_index < len(self.items) - 1: new_index += 1 elif keystr == 'PageUp': navigated = True page_size = int(self.items_in_view / 2) new_index -= page_size new_index = max(0, new_index) # scroll follows selection jumps self.scroll_index -= page_size self.scroll_index = max(0, self.scroll_index) elif keystr == 'PageDown': navigated = True page_size = int(self.items_in_view / 2) new_index += page_size new_index = min(new_index, len(self.items) - 1) self.scroll_index += page_size self.scroll_index = min(self.scroll_index, self.get_max_scroll()) # home/end: beginning/end of list, respectively elif keystr == 'Home': navigated = True new_index = 0 self.scroll_index = 0 elif keystr == 'End': navigated = True new_index = len(self.items) - 1 self.scroll_index = len(self.items) - self.items_in_view self.set_selected_item_index(new_index, set_field_text=navigated) # handle alphanumeric input etc UIDialog.handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed) # if we didn't navigate, seek based on new alphanumeric input if not navigated: self.text_input_seek() def text_input_seek(self): field_text = self.field_texts[self.active_field] if field_text.strip() == '': return # seek should be case-insensitive field_text = field_text.lower() # field text may be a full path; only care about the base field_text = os.path.basename(field_text) for i,item in enumerate(self.items): # match to base item name within dir # (if it's a dir, snip last / for match) item_base = item.name.lower() if item_base.endswith('/'): item_base = item_base[:-1] item_base = os.path.basename(item_base) item_base = os.path.splitext(item_base)[0] if item_base.startswith(field_text): self.set_selected_item_index(i, set_field_text=False) break def handle_enter(self, shift_pressed, alt_pressed, ctrl_pressed): "handle Enter key, return False if rest of handle_input should continue" # if selected item is already in text field, pick it field_text = self.field_texts[self.active_field] selected_item = self.get_selected_item() if field_text.strip() == '': self.field_texts[self.active_field] = field_text = selected_item.name return True if field_text == selected_item.name: # this (and similar following cases) should count as having # made a selection self.first_selection_made = True selected_item.picked(self) return True # else navigate to directory or file if it's real if not os.path.exists(field_text): self.field_texts[self.active_field] = selected_item.name return True # special case for parent dir .. if self.directory_aware and field_text == self.current_dir and selected_item.name == '..': self.first_selection_made = True return self.change_current_dir('..') if self.directory_aware and os.path.isdir(field_text): self.first_selection_made = True return self.change_current_dir(field_text) if os.path.isfile(field_text): file_dir_name = os.path.dirname(field_text) # if a file, change to its dir and select it if self.directory_aware and file_dir_name != self.current_dir: if self.change_current_dir(file_dir_name): for i,item in enumerate(self.items): if item.name == field_text: self.set_selected_item_index(i) item.picked(self) return True return False def render(self): UIDialog.render(self) if self.show_preview_image and self.preview_renderable.texture: self.preview_renderable.render()
class ConvertImageOptionsDialog(ImportOptionsDialog): title = 'Convert bitmap image options' field0_label = 'Color palette:' field1_label = 'Current palette (%s)' field2_label = 'From source image; # of colors:' field3_label = ' ' field5_label = 'Converted art size:' field6_label = 'Best fit to current size (%s)' field7_label = '%% of source image: (%s)' field8_label = ' ' field10_label = 'Smooth (bicubic) scale source image' radio_groups = [(1, 2), (6, 7)] field_width = UIDialog.default_short_field_width # to get the layout we want, we must specify 0 padding lines and # add some blank ones :/ y_spacing = 0 fields = [ Field(label=field0_label, type=None, width=0, oneline=True), Field(label=field1_label, type=bool, width=0, oneline=True), Field(label=field2_label, type=bool, width=0, oneline=True), Field(label=field3_label, type=int, width=field_width, oneline=True), Field(label='', type=None, width=0, oneline=True), Field(label=field5_label, type=None, width=0, oneline=True), Field(label=field6_label, type=bool, width=0, oneline=True), Field(label=field7_label, type=bool, width=0, oneline=True), Field(label=field8_label, type=float, width=field_width, oneline=True), Field(label='', type=None, width=0, oneline=True), Field(label=field10_label, type=bool, width=0, oneline=True), Field(label='', type=None, width=0, oneline=True) ] invalid_color_error = 'Palettes must be between 2 and 256 colors.' invalid_scale_error = 'Scale must be greater than 0.0' # redraw dynamic labels always_redraw_labels = True def get_initial_field_text(self, field_number): if field_number == 1: return UIDialog.true_field_text elif field_number == 3: # # of colors from source image return '64' elif field_number == 6: return UIDialog.true_field_text elif field_number == 8: # % of source image size return '50.0' elif field_number == 10: return ' ' return '' def get_field_label(self, field_index): label = self.fields[field_index].label # custom label replacements to show palette, possible convert sizes if field_index == 1: label %= self.ui.active_art.palette.name if self.ui.active_art else DEFAULT_PALETTE elif field_index == 6: # can't assume any art is open, use defaults if needed w = self.ui.active_art.width if self.ui.active_art else DEFAULT_WIDTH h = self.ui.active_art.height if self.ui.active_art else DEFAULT_HEIGHT label %= '%s x %s' % (w, h) elif field_index == 7: # scale # might not be valid valid, _ = self.is_input_valid() if not valid: return label % '???' label %= '%s x %s' % self.get_tile_scale() return label def get_tile_scale(self): "returns scale in tiles of image dimensions" # filename won't be set just after dialog is created if not hasattr(self, 'filename'): return 0, 0 scale = float(self.field_texts[8]) / 100 # can't assume any art is open, use defaults if needed if self.ui.active_art: cw = self.ui.active_art.charset.char_width ch = self.ui.active_art.charset.char_height else: charset = self.ui.app.load_charset(DEFAULT_CHARSET) cw, ch = charset.char_width, charset.char_height width = self.image_width / cw height = self.image_height / ch width *= scale height *= scale return int(width), int(height) def is_input_valid(self): # colors: int between 2 and 256 try: int(self.field_texts[3]) except: return False, self.invalid_color_error colors = int(self.field_texts[3]) if colors < 2 or colors > 256: return False, self.invalid_color_error # % scale: >0 float try: float(self.field_texts[8]) except: return False, self.invalid_scale_error if float(self.field_texts[8]) <= 0: return False, self.invalid_scale_error return True, None def confirm_pressed(self): valid, reason = self.is_input_valid() if not valid: return self.dismiss() # compile options for importer options = {} # create new palette from image? if self.field_texts[1].strip(): options[ 'palette'] = self.ui.active_art.palette.name if self.ui.active_art else DEFAULT_PALETTE else: # create new palette palette_filename = os.path.basename(self.filename) colors = int(self.field_texts[3]) new_pal = PaletteFromFile(self.ui.app, self.filename, palette_filename, colors) # palette now loaded and saved to disk options['palette'] = new_pal.name # rescale art? if self.field_texts[6].strip(): options[ 'art_width'] = self.ui.active_art.width if self.ui.active_art else DEFAULT_WIDTH options[ 'art_height'] = self.ui.active_art.height if self.ui.active_art else DEFAULT_HEIGHT else: # art dimensions = scale% of image dimensions, in tiles options['art_width'], options['art_height'] = self.get_tile_scale() options['bicubic_scale'] = bool(self.field_texts[10].strip()) ImportOptionsDialog.do_import(self.ui.app, self.filename, options)