view mamba/habitats/editor.py @ 387:f86188a0f259

Tweak load list length
author Neil Muller <drnlmuller@gmail.com>
date Sat, 17 Sep 2011 01:44:53 +0200
parents 21caf90f604e
children 171c3d817c48
line wrap: on
line source

"""Habitat for editing levels."""

import pygame.display
from pygame.locals import SWSURFACE, KEYDOWN, K_1, K_2, MOUSEBUTTONDOWN
import sys
import traceback

from mamba.engine import Habitat, NewHabitatEvent
from mamba.widgets.level import EditLevelWidget
from mamba.widgets.text import TextWidget, TextButton
from mamba.widgets.imagebutton import ImageButtonWidget
from mamba.widgets.messagebox import MessageBox
from mamba.widgets.entrybox import EntryBox
from mamba.widgets.listbox import ListBox
from mamba.widgets.toollist import ToolListWidget
from mamba.widgets.editsprite import EditSpriteBox
from mamba.level import Level, Tileset, TILE_MAP, THING_MAP, InvalidMapError
from mamba.data import (check_level_exists, get_level_list, get_tileset_list,
        get_track_list)
from mamba.constants import (SCREEN, EDIT_SCREEN, NAME, ESCAPE_KEYS,
        RESERVED_NAMES)

MAX_TOOLS = 6
MODE_HEIGHT = 500


class EditorHabitat(Habitat):
    def __init__(self, level):
        super(EditorHabitat, self).__init__(EDIT_SCREEN)
        self.level = level
        self.container.paused = False
        self.edit_widget = EditLevelWidget(self.level)
        self.container.add(self.edit_widget)
        self.container.add_callback(KEYDOWN, self.keydown_event)
        self.container.add_callback(MOUSEBUTTONDOWN, self.mouse_event)
        self.mode = 'Tiles'
        self.sprite_mode = 'Add'

    def on_enter(self):
        # We need to juggle the display to the correct size
        # This is a horrible hack
        pygame.display.quit()
        pygame.display.init()
        pygame.display.set_mode(EDIT_SCREEN, SWSURFACE)
        pygame.display.set_caption('%s Level editor' % NAME)
        super(EditorHabitat, self).on_enter()
        self.setup_toolbar()

    def on_exit(self):
        # We need to juggle the display to the correct size
        # This is a horrible hack
        super(EditorHabitat, self).on_exit()
        pygame.display.quit()
        pygame.display.init()
        pygame.display.set_mode(SCREEN, SWSURFACE)
        pygame.display.set_caption(NAME)

    def keydown_event(self, ev, widget):
        if ev.key in ESCAPE_KEYS:
            from mamba.habitats.mainmenu import MainMenu
            NewHabitatEvent.post(MainMenu())
            return True
        elif ev.key == K_1:
            # Activate floor button
            self.floor_button.forced_click()
            return True

    def setup_toolbar(self):
        """Draw the editor toolbar"""
        button_height = 5
        button_left = 820
        button_padding = 2

        filename = TextButton(
                (button_left, button_height),
                'File: %s' % self.level.level_name, color='white')
        filename.add_callback('clicked', self.do_edit,
                'Specify filename', self.level.level_name, self.check_file)
        self.container.add(filename)
        button_height += filename.rect.height + button_padding
        levelname = TextButton((button_left, button_height),
                'Level: %s' % self.level.name, color='white')
        levelname.add_callback('clicked', self.do_edit,
                'Edit Level Title', self.level.name, self.update_name)
        self.container.add(levelname)
        button_height += levelname.rect.height + button_padding

        tilesetname = TextButton((button_left, button_height),
                'Tileset: %s' % self.level.tileset.name, color='white')
        tilesetname.add_callback('clicked', self.list_tilesets)
        self.container.add(tilesetname)
        button_height += tilesetname.surface.get_height() + button_padding
        trackname = TextButton((button_left, button_height),
                'Music: %s' % self.level.background_track, color='white')
        trackname.add_callback('clicked', self.list_tracks)
        self.container.add(trackname)
        button_height += trackname.surface.get_height() + button_padding
        # TODO: Add Image widget for the current tool
        if self.mode != 'Sprites':
            self.current_tool = TextWidget((button_left, button_height),
                    'Tool: Floor', color='white')
        else:
            self.current_tool = TextWidget((button_left, button_height),
                    '%s Sprite' % self.sprite_mode, color='white')
        self.container.add(self.current_tool)
        button_height += self.current_tool.surface.get_height()
        button_height += button_padding
        if self.mode != 'Sprites':
            self.floor_button = ImageButtonWidget(
                    (button_left, button_height), self.level.tileset.floor,
                    'Floor', color='white')
            self.container.add(self.floor_button)
            self.floor_button.add_callback('clicked', self.change_tool,
                    '.', 'Floor')
            self.edit_widget.set_tool('.')
            button_height += (self.floor_button.surface.get_height()
                    + button_padding)
        if self.mode == 'Tiles':
            tile_map = TILE_MAP
        elif self.mode == 'Things':
            tile_map = THING_MAP
        else:
            tile_map = []
        tool_list = []
        for tile_char in sorted(tile_map):
            try:
                tile = self.level.tileset[tile_char]
            except pygame.error:
                # Ignore stuff we can't load for now
                continue
            if tile is None:
                continue
            if tile.name:
                text = tile.name
            else:
                text = 'Tile'
            tile_button = ImageButtonWidget((0, 0), tile.image, text,
                    color='white')
            tile_button.add_callback('clicked', self.change_tool,
                    tile_char, text)
            tool_list.append(tile_button)
        if self.mode == "Sprites":
            for name in ['Add', 'Edit', 'Delete']:
                tile_button = TextButton((0, 0), '%s Sprite' % name)
                tile_button.add_callback('clicked', self.sprite_tool,
                        name)
                tool_list.append(tile_button)
        self.tool_widget = ToolListWidget((button_left, button_height),
                tool_list, MAX_TOOLS, start_key=K_2)
        self.container.add(self.tool_widget)
        button_height = self.container.rect.top + MODE_HEIGHT
        if self.mode == 'Tiles':
            mode_button1 = TextButton((button_left, button_height),
                'Things')
            mode_button1.add_callback('clicked', self.change_toolbar, 'Things')
            mode_button2 = TextButton((button_left + 100, button_height),
                'Sprites')
            mode_button2.add_callback('clicked', self.change_toolbar,
                    'Sprites')
        elif self.mode == 'Things':
            mode_button1 = TextButton((button_left, button_height),
                'Tiles')
            mode_button1.add_callback('clicked', self.change_toolbar, 'Tiles')
            mode_button2 = TextButton((button_left + 100, button_height),
                'Sprites')
            mode_button2.add_callback('clicked', self.change_toolbar,
                    'Sprites')
        elif self.mode == 'Sprites':
            mode_button1 = TextButton((button_left, button_height),
                'Tiles')
            mode_button1.add_callback('clicked', self.change_toolbar, 'Tiles')
            mode_button2 = TextButton((button_left + 100, button_height),
                'Things')
            mode_button2.add_callback('clicked', self.change_toolbar,
                    'Things')
        self.container.add(mode_button1)
        self.container.add(mode_button2)
        button_height += mode_button1.rect.height + button_padding
        button_height += 2

        new = TextButton((button_left, button_height), "New")
        new.add_callback('clicked', self.new)
        self.container.add(new)
        load = TextButton((button_left + 60, button_height), "Load")
        load.add_callback('clicked', self.load)
        self.container.add(load)

        save = TextButton((button_left + 120, button_height), "Save")
        save.add_callback('clicked', self.save)
        self.container.add(save)

    def change_tool(self, ev, widget, new_tool, text):
        self.edit_widget.set_tool(new_tool)
        self.current_tool.text = 'Tool: %s' % text
        self.current_tool.prepare()
        return True

    def save(self, ev, widget):
        message = None
        if not self.level.level_name:
            message = MessageBox((300, 300), 'Please enter a name')
        elif self.level.level_name in RESERVED_NAMES:
            message = MessageBox((300, 300), 'Reserved level name')
        try:
            self.level.validate_level()
        except InvalidMapError, error:
            message = MessageBox((300, 300), "Map isn't valid\n%s" % error)
        if message:
            self.container.paused = True
            self.container.add(message)
            message.grab_focus()
            return
        self.level.save_level()
        return True

    def new(self, ev, widget):
        self.load_level(ev, widget, 'blank')
        return True

    def load(self, ev, widget):
        self.container.paused = True
        levels = get_level_list()
        load_list = []
        for level_name in levels:
            if level_name in RESERVED_NAMES:
                continue
            load_button = TextButton((0, 0), level_name)
            load_button.add_callback('clicked', self.load_level, level_name)
            load_list.append(load_button)
        load_dialog = ListBox((200, 200), 'Select Level', load_list, 6)
        self.container.add(load_dialog)
        load_dialog.grab_focus()
        return True

    def load_level(self, ev, widget, level_name):
        try:
            new_level = Level(level_name, 'official')
        except (IOError, InvalidMapError, pygame.error), error:
            message = MessageBox((300, 300),
                    'Loading Level Failed: %s' % error, color='red')
            self.container.paused = True
            self.container.add(message)
            message.grab_focus()
            return False
        self.container.remove(self.edit_widget)
        self.level = new_level
        if level_name in RESERVED_NAMES:
            self.level.level_name = ''  # Special case for new level
        self.container.paused = False
        self.edit_widget = EditLevelWidget(self.level)
        self.container.add(self.edit_widget)
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def change_toolbar(self, ev, widget, new_mode):
        self.mode = new_mode
        self.edit_widget.tile_mode = (self.mode != 'Sprites')
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def clear_toolbar(self):
        """Remove every non-edit widget from the container"""
        for widget in self.container.children[:]:
            if widget is not self.edit_widget:
                self.container.remove(widget)

    def do_edit(self, ev, widget, message, init_value, callback):
        self.container.paused = True
        editbox = EntryBox((200, 200), message, init_value, callback)
        self.container.add(editbox)
        editbox.grab_focus()
        return True

    def update_name(self, new_name):
        self.container.paused = False
        self.level.name = new_name
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def list_tilesets(self, ev, widget):
        self.container.paused = True
        tilesets = get_tileset_list()
        load_list = []
        for name in tilesets:
            if name == 'common':
                continue
            load_button = TextButton((0, 0), name)
            load_button.add_callback('clicked', self.change_tileset, name)
            load_list.append(load_button)
        load_dialog = ListBox((200, 200), 'Select Level', load_list)
        self.container.add(load_dialog)
        load_dialog.grab_focus()
        return True

    def list_tracks(self, ev, widget):
        self.container.paused = True
        tracks = get_track_list()
        load_list = []
        for name in tracks:
            load_button = TextButton((0, 0), name)
            load_button.add_callback('clicked', self.change_track, name)
            load_list.append(load_button)
        load_dialog = ListBox((200, 200), 'Select Level', load_list, 6)
        self.container.add(load_dialog)
        load_dialog.grab_focus()
        return True

    def change_tileset(self, ev, widget, new_name):
        self.level.update_tiles_ascii()
        old_tileset = self.level.tileset
        try:
            self.level.tileset = Tileset(new_name)
            self.level.restart()
        except pygame.error, error:
            self.level.tileset = old_tileset
            message = MessageBox((300, 300),
                    'Unable to change tileset:: %s' % error,
                    self.refresh_display, color='red')
            self.container.paused = True
            self.container.add(message)
            message.grab_focus()
            return True
        self.container.paused = False
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def change_track(self, ev, widget, new_name):
        self.container.paused = False
        self.level.background_track = new_name
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def refresh_display(self):
        self.level.restart()
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def check_file(self, new_name):
        message = None
        if new_name == self.level.level_name:
            return True  # No-op change
        if not new_name:
            message = MessageBox((300, 300), 'Please enter a name')
        if new_name in RESERVED_NAMES:
            # This case is caught by the existance check, but the
            # importance of the reserved names means we use a different
            # message
            message = MessageBox((300, 300), 'Reserved level name')
        elif check_level_exists(new_name):
            message = MessageBox((300, 300), 'Name already in use')
        if message:
            self.container.paused = True
            self.container.add(message)
            message.grab_focus()
            return False
        self.container.paused = False
        self.level.level_name = new_name
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def sprite_tool(self, ev, widget, sprite_mode):
        """Handle sprite stuff"""
        self.sprite_mode = sprite_mode
        self.level.update_tiles_ascii()  # commit any changes
        self.clear_toolbar()
        self.setup_toolbar()
        return True

    def mouse_event(self, ev, widget):
        """Handle mouse clicks when we are in sprite mode"""
        if self.mode != 'Sprites':
            return False
        if self.container.paused:
            return False
        tile_pos = self.edit_widget.convert_pos(ev.pos)
        sprite = self.level.get_sprite_at(tile_pos)
        if self.sprite_mode == 'Delete' and sprite:
            self.level.remove_sprite(sprite)
            self.level.restart()
        elif self.sprite_mode == 'Edit' and sprite:
            self.edit_sprite(tile_pos, sprite)
        elif self.sprite_mode == 'Add' and sprite is None:
            self.edit_sprite(tile_pos, sprite)
        return True

    def edit_sprite(self, tile_pos, sprite):
        self.container.paused = True
        sprite_editor = EditSpriteBox((200, 100), tile_pos, sprite,
                post_callback=self.commit_line)
        self.container.add(sprite_editor)
        sprite_editor.grab_focus()

    def commit_line(self, sprite):
        try:
            self.level.validate_sprite(sprite)
        except:
            # We don't know what errors thwe constructor may show, so
            # we catch everything
            # We use sys.exc_info to get slight neater info
            exc_type, info, _ = sys.exc_info()
            info = traceback.format_exception_only(exc_type, info)[0]
            message = MessageBox((300, 300),
                    'Validation failed:\n%s' % info)
            self.container.paused = True
            self.container.add(message)
            message.grab_focus()
            return False
        # Validation successful, so add to level
        if self.sprite_mode == 'Add':
            self.level.add_sprite(sprite)
        elif self.sprite_mode == 'Edit':
            self.level.replace_sprite(sprite)
        self.level.restart()
        return True