playscii/ui_edit_panel.py

546 lines
19 KiB
Python

import os
from game_world import STATE_FILE_EXTENSION, TOP_GAME_DIR
from ui_button import UIButton
from ui_chooser_dialog import ScrollArrowButton
from ui_colors import UIColors
from ui_element import UIElement
from ui_list_operations import (
LO_LOAD_STATE,
LO_NONE,
LO_OPEN_GAME_DIR,
LO_SELECT_OBJECTS,
LO_SET_OBJECT_ROOMS,
LO_SET_ROOM,
LO_SET_ROOM_CAMERA,
LO_SET_ROOM_EDGE_OBJ,
LO_SET_ROOM_EDGE_WARP,
LO_SET_ROOM_OBJECTS,
LO_SET_SPAWN_CLASS,
)
class GamePanel(UIElement):
"base class of game edit UI panels"
tile_y = 5
game_mode_visible = True
fg_color = UIColors.black
bg_color = UIColors.lightgrey
titlebar_fg = UIColors.white
titlebar_bg = UIColors.darkgrey
text_left = True
support_keyboard_navigation = True
support_scrolling = True
keyboard_nav_offset = -2
def __init__(self, ui):
self.ui = ui
self.world = self.ui.app.gw
UIElement.__init__(self, ui)
self.buttons = []
self.create_buttons()
self.keyboard_nav_index = 0
def create_buttons(self):
pass
# label and main item draw functions - overridden in subclasses
def get_label(self):
pass
def refresh_items(self):
pass
# reset all buttons to default state
def clear_buttons(self, button_list=None):
buttons = button_list or self.buttons
for button in buttons:
self.reset_button(button)
def reset_button(self, button):
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
button.can_hover = True
def highlight_button(self, button):
button.normal_fg_color = UIButton.clicked_fg_color
button.normal_bg_color = UIButton.clicked_bg_color
button.hovered_fg_color = UIButton.clicked_fg_color
button.hovered_bg_color = UIButton.clicked_bg_color
button.can_hover = True
def draw_titlebar(self):
# only shade titlebar if panel has keyboard focus
fg = (
self.titlebar_fg
if self is self.ui.keyboard_focus_element
else self.fg_color
)
bg = (
self.titlebar_bg
if self is self.ui.keyboard_focus_element
else self.bg_color
)
self.art.clear_line(0, 0, 0, fg, bg)
label = self.get_label()
if len(label) > self.tile_width:
label = label[: self.tile_width]
if self.text_left:
self.art.write_string(0, 0, 0, 0, label)
else:
self.art.write_string(0, 0, -1, 0, label, None, None, True)
def reset_art(self):
self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
self.draw_titlebar()
self.refresh_items()
UIElement.reset_art(self)
def clicked(self, mouse_button):
# always handle input, even if we didn't hit a button
UIElement.clicked(self, mouse_button)
return True
def hovered(self):
# mouse hover on focus
if (
self.ui.app.mouse_dx
or self.ui.app.mouse_dy
and self is not self.ui.keyboard_focus_element
):
self.ui.keyboard_focus_element = self
if self.ui.active_dialog:
self.ui.active_dialog.reset_art()
class ListButton(UIButton):
width = 28
clear_before_caption_draw = True
class ListScrollArrowButton(ScrollArrowButton):
x = ListButton.width
normal_bg_color = UIButton.normal_bg_color
class ListScrollUpArrowButton(ListScrollArrowButton):
y = 1
class ListScrollDownArrowButton(ListScrollArrowButton):
up = False
class EditListPanel(GamePanel):
tile_width = ListButton.width + 1
tile_y = 5
scrollbar_shade_char = 54
# height will change based on how many items in list
tile_height = 30
snap_left = True
spawn_msg = "Click anywhere in the world view to spawn a %s"
# transient state
titlebar = "List titlebar"
items = []
# text helping user know how to bail
cancel_tip = "ESC cancels"
list_operation_labels = {
LO_NONE: "Stuff:",
LO_SELECT_OBJECTS: "Select objects:",
LO_SET_SPAWN_CLASS: "Class to spawn:",
LO_LOAD_STATE: "State to load:",
LO_SET_ROOM: "Change room:",
LO_SET_ROOM_OBJECTS: "Set objects for %s:",
LO_SET_OBJECT_ROOMS: "Set rooms for %s:",
LO_OPEN_GAME_DIR: "Open game:",
LO_SET_ROOM_EDGE_WARP: "Set edge warp room/object:",
LO_SET_ROOM_EDGE_OBJ: "Set edge bounds object:",
LO_SET_ROOM_CAMERA: "Set room camera marker:",
}
list_operations_allow_kb_focus = [
LO_SET_ROOM_EDGE_WARP,
LO_SET_ROOM_EDGE_OBJ,
LO_SET_ROOM_CAMERA,
]
class ListItem:
def __init__(self, name, obj):
self.name, self.obj = name, obj
def __str__(self):
return self.name
def __init__(self, ui):
# topmost index of items to show in view
self.list_scroll_index = 0
# list operation, ie what does clicking in list do
self.list_operation = LO_NONE
# save & restore a scroll index for each flavor of list
self.scroll_indices = {}
for list_op in self.list_operation_labels:
self.scroll_indices[list_op] = 0
# map list operations to list builder functions
self.list_functions = {
LO_NONE: self.list_none,
LO_SELECT_OBJECTS: self.list_objects,
LO_SET_SPAWN_CLASS: self.list_classes,
LO_LOAD_STATE: self.list_states,
LO_SET_ROOM: self.list_rooms,
LO_SET_ROOM_OBJECTS: self.list_objects,
LO_SET_OBJECT_ROOMS: self.list_rooms,
LO_OPEN_GAME_DIR: self.list_games,
LO_SET_ROOM_EDGE_WARP: self.list_rooms_and_objects,
LO_SET_ROOM_EDGE_OBJ: self.list_objects,
LO_SET_ROOM_CAMERA: self.list_objects,
}
# map list operations to "item clicked" functions
self.click_functions = {
LO_SELECT_OBJECTS: self.select_object,
LO_SET_SPAWN_CLASS: self.set_spawn_class,
LO_LOAD_STATE: self.load_state,
LO_SET_ROOM: self.set_room,
LO_SET_ROOM_OBJECTS: self.set_room_object,
LO_SET_OBJECT_ROOMS: self.set_object_room,
LO_OPEN_GAME_DIR: self.open_game_dir,
LO_SET_ROOM_EDGE_WARP: self.set_room_edge_warp,
LO_SET_ROOM_EDGE_OBJ: self.set_room_bounds_obj,
LO_SET_ROOM_CAMERA: self.set_room_camera,
}
# separate lists for item buttons vs other controls
self.list_buttons = []
# set when game resets
self.should_reset_list = False
GamePanel.__init__(self, ui)
def create_buttons(self):
def list_callback(item=None):
if not item:
return
self.clicked_item(item)
for y in range(self.tile_height - 1):
button = ListButton(self)
button.y = y + 1
button.callback = list_callback
# button.cb_art set by refresh_items()
self.list_buttons.append(button)
self.buttons = self.list_buttons[:]
self.up_button = ListScrollUpArrowButton(self)
self.up_button.callback = self.scroll_list_up
self.buttons.append(self.up_button)
self.down_button = ListScrollDownArrowButton(self)
self.down_button.callback = self.scroll_list_down
# TODO: adjust height according to screen tile height
self.down_button.y = self.tile_height - 1
self.buttons.append(self.down_button)
def reset_art(self):
GamePanel.reset_art(self)
x = self.tile_width - 1
for y in range(1, self.tile_height):
self.art.set_tile_at(
0, 0, x, y, self.scrollbar_shade_char, UIColors.medgrey
)
def cancel(self):
self.set_list_operation(LO_NONE)
self.world.classname_to_spawn = None
def scroll_list_up(self):
if self.list_scroll_index > 0:
self.list_scroll_index -= 1
def scroll_list_down(self):
max_scroll = len(self.items) - self.tile_height
# max_scroll = len(self.element.items) - self.element.items_in_view
if self.list_scroll_index <= max_scroll:
self.list_scroll_index += 1
def clicked_item(self, item):
# do thing appropriate to current list operation
self.click_functions[self.list_operation](item)
def wheel_moved(self, wheel_y):
if wheel_y > 0:
self.scroll_list_up()
return True
if wheel_y < 0:
self.scroll_list_down()
return True
def set_list_operation(self, new_op):
"changes list type and sets new items"
if new_op == LO_LOAD_STATE and not self.world.game_dir:
return
if new_op == LO_NONE:
self.list_operation = new_op
self.ui.keyboard_focus_element = None
self.ui.refocus_keyboard()
return
# list is doing something, set us as keyboard focus
# (but not if a dialog just came up)
if not self.ui.active_dialog:
self.ui.keyboard_focus_element = self
self.items = []
self.clear_buttons(self.list_buttons)
# save old list type's scroll index so we can restore it later
self.scroll_indices[self.list_operation] = self.list_scroll_index
self.list_operation = new_op
self.items = self.list_functions[self.list_operation]()
# restore saved scroll index for new list type
self.list_scroll_index = self.scroll_indices[self.list_operation]
# keep in bounds if list size changed since last view
self.list_scroll_index = min(self.list_scroll_index, len(self.items))
def get_label(self):
label = "{} ({})".format(
self.list_operation_labels[self.list_operation],
self.cancel_tip,
)
# some labels contain variables
if "%s" in label:
if self.list_operation == LO_SET_ROOM_OBJECTS:
if self.world.current_room:
label %= self.world.current_room.name
elif self.list_operation == LO_SET_OBJECT_ROOMS:
if len(self.world.selected_objects) == 1:
label %= self.world.selected_objects[0].name
return label
def should_highlight(self, item):
if self.list_operation == LO_SELECT_OBJECTS:
return item.obj in self.world.selected_objects
elif self.list_operation == LO_SET_SPAWN_CLASS:
return item.name == self.world.classname_to_spawn
elif self.list_operation == LO_LOAD_STATE:
last_gs = os.path.basename(self.world.last_state_loaded)
last_gs = os.path.splitext(last_gs)[0]
return item.name == last_gs
elif self.list_operation == LO_SET_ROOM:
return self.world.current_room and item.name == self.world.current_room.name
elif self.list_operation == LO_SET_ROOM_OBJECTS:
return (
self.world.current_room and item.name in self.world.current_room.objects
)
elif self.list_operation == LO_SET_OBJECT_ROOMS:
return (
len(self.world.selected_objects) == 1
and item.name in self.world.selected_objects[0].rooms
)
return False
def game_reset(self):
self.should_reset_list = True
def items_changed(self):
"called by anything that changes the items list, eg object add/delete"
self.items = self.list_functions[self.list_operation]()
# change selected item index if it's OOB
if self.keyboard_nav_index >= len(self.items):
self.keyboard_nav_index = len(self.items) - 1
def refresh_items(self):
for i, b in enumerate(self.list_buttons):
if i >= len(self.items):
b.caption = ""
b.cb_arg = None
self.reset_button(b)
b.can_hover = False
else:
index = self.list_scroll_index + i
item = self.items[index]
b.cb_arg = item
b.caption = item.name[: self.tile_width - 1]
b.can_hover = True
# change button appearance if this item should remain
# highlighted/selected
if self.should_highlight(item):
self.highlight_button(b)
else:
self.reset_button(b)
self.draw_buttons()
def post_keyboard_navigate(self):
# check for scrolling
if len(self.items) <= len(self.list_buttons):
return
# wrap if at end of list
if self.keyboard_nav_index + self.list_scroll_index >= len(self.items):
self.keyboard_nav_index = 0
self.list_scroll_index = 0
# scroll down
elif self.keyboard_nav_index >= len(self.list_buttons):
self.scroll_list_down()
self.keyboard_nav_index -= 1
# wrap if at top of list
elif self.list_scroll_index == 0 and self.keyboard_nav_index < 0:
self.list_scroll_index = len(self.items) - len(self.list_buttons)
self.keyboard_nav_index = len(self.list_buttons) - 1
# scroll up
elif self.keyboard_nav_index < 0:
self.scroll_list_up()
self.keyboard_nav_index += 1
def update(self):
if self.should_reset_list:
self.set_list_operation(self.list_operation)
self.should_reset_list = False
# redraw contents every update
self.draw_titlebar()
self.refresh_items()
GamePanel.update(self)
self.renderable.alpha = 1 if self is self.ui.keyboard_focus_element else 0.5
def is_visible(self):
return GamePanel.is_visible(self) and self.list_operation != LO_NONE
#
# list functions
#
def list_classes(self):
items = []
base_class = self.world.modules["game_object"].GameObject
# get list of available classes from GameWorld
for classname, classdef in self.world._get_all_loaded_classes().items():
# ignore non-GO classes, eg GameRoom, GameHUD
if not issubclass(classdef, base_class):
continue
if classdef.exclude_from_class_list:
continue
item = self.ListItem(classname, classdef)
items.append(item)
# sort classes alphabetically
items.sort(key=lambda i: i.name)
return items
def list_objects(self):
items = []
# include just-spawned objects too
all_objects = self.world.objects.copy()
all_objects.update(self.world.new_objects)
for obj in all_objects.values():
if obj.exclude_from_object_list:
continue
if (
self.world.list_only_current_room_objects
and self.world.current_room.name not in obj.rooms
):
continue
li = self.ListItem(obj.name, obj)
items.append(li)
# sort object names alphabetically
items.sort(key=lambda i: i.name)
return items
def list_states(self):
items = []
# list state files in current game dir
for filename in os.listdir(self.world.game_dir):
if filename.endswith("." + STATE_FILE_EXTENSION):
li = self.ListItem(filename[:-3], None)
items.append(li)
items.sort(key=lambda i: i.name)
return items
def list_rooms(self):
items = []
for room in self.world.rooms.values():
li = self.ListItem(room.name, room)
items.append(li)
items.sort(key=lambda i: i.name)
return items
def list_games(self):
def get_dirs(dirname):
dirs = []
for filename in os.listdir(dirname):
if os.path.isdir(dirname + filename):
dirs.append(filename)
return dirs
# get list of both app dir games and user dir games
docs_game_dir = self.ui.app.documents_dir + TOP_GAME_DIR
items = []
game_dirs = get_dirs(TOP_GAME_DIR) + get_dirs(docs_game_dir)
game_dirs.sort()
for game in game_dirs:
li = self.ListItem(game, None)
items.append(li)
return items
def list_rooms_and_objects(self):
items = self.list_rooms()
# prefix room names with "ROOM:"
for item in items:
item.name = "ROOM: {}".format(item.name)
items += self.list_objects()
return items
def list_none(self):
return []
#
# "clicked list item" functions
#
def select_object(self, item):
# add to/remove from/overwrite selected list based on mod keys
if self.ui.app.il.ctrl_pressed:
self.world.deselect_object(item.obj)
elif self.ui.app.il.shift_pressed:
self.world.select_object(item.obj, force=True)
else:
self.world.deselect_all()
self.world.select_object(item.obj, force=True)
def set_spawn_class(self, item):
# set this class to be the one spawned when GameWorld is clicked
self.world.classname_to_spawn = item.name
self.ui.message_line.post_line(
self.spawn_msg % self.world.classname_to_spawn, 5
)
def load_state(self, item):
self.world.load_game_state(item.name)
def set_room(self, item):
self.world.change_room(item.name)
def set_room_object(self, item):
# add/remove object from current room
if item.name in self.world.current_room.objects:
self.world.current_room.remove_object_by_name(item.name)
else:
self.world.current_room.add_object_by_name(item.name)
def set_object_room(self, item):
# UI can only show a single object's rooms, do nothing if many selected
if len(self.world.selected_objects) != 1:
return
# add if not in room, remove if in room
obj = self.world.selected_objects[0]
room = self.world.rooms[item.name]
if room.name in obj.rooms:
room.remove_object(obj)
else:
room.add_object(obj)
def open_game_dir(self, item):
self.world.set_game_dir(item.name, True)
def set_room_edge_warp(self, item):
dialog = self.ui.active_dialog
dialog.field_texts[dialog.active_field] = item.obj.name
self.ui.keyboard_focus_element = dialog
def set_room_bounds_obj(self, item):
dialog = self.ui.active_dialog
dialog.field_texts[dialog.active_field] = item.obj.name
self.ui.keyboard_focus_element = dialog
def set_room_camera(self, item):
dialog = self.ui.active_dialog
dialog.field_texts[dialog.active_field] = item.obj.name
self.ui.keyboard_focus_element = dialog