Exemple #1
0
class EditCanvas(BaseEditCanvas):
    def __init__(self, parent: wx.Window, world: "BaseLevel",
                 close_callback: Callable):
        super().__init__(parent, world)
        self._close_callback = close_callback
        self._file_panel: Optional[FilePanel] = None
        self._tool_sizer: Optional[ToolManagerSizer] = None
        self.buttons.register_actions(self.key_binds)

        # Tracks if an operation has been started and not finished.
        self._operation_running = False
        # This lock stops two threads from editing the world simultaneously
        # call run_operation to acquire it.
        self._edit_lock = RLock()

    def _setup(self) -> Generator[OperationYieldType, None, None]:
        yield from super()._setup()
        canvas_sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(canvas_sizer)

        self._file_panel = FilePanel(self)
        canvas_sizer.Add(self._file_panel, 0, wx.EXPAND, 0)

        self._tool_sizer = ToolManagerSizer(self)
        canvas_sizer.Add(self._tool_sizer, 1, wx.EXPAND, 0)

    def _finalise(self):
        super()._finalise()
        self._tool_sizer.enable()

    def bind_events(self):
        """Set up all events required to run.
        Note this will also bind subclass events."""
        self._tool_sizer.bind_events()
        # binding the tool events first will run them last so they can't accidentally block UI events.
        super().bind_events()
        self._file_panel.bind_events()
        self.Bind(EVT_EDIT_CLOSE, self._on_close)

    def enable(self):
        super().enable()
        self._tool_sizer.enable()

    def disable(self):
        super().disable()
        self._tool_sizer.disable()

    def _on_close(self, _):
        self._close_callback()

    @property
    def tools(self):
        return self._tool_sizer.tools

    @property
    def key_binds(self) -> KeybindGroup:
        config_ = CONFIG.get(EDIT_CONFIG_ID, {})
        user_keybinds = config_.get("user_keybinds", {})
        group = config_.get("keybind_group", DefaultKeybindGroupId)
        if group in user_keybinds:
            return user_keybinds[group]
        elif group in PresetKeybinds:
            return PresetKeybinds[group]
        else:
            return DefaultKeys

    def _deselect(self):
        # TODO: Re-implement this
        self._tool_sizer.enable_default_tool()

    def run_operation(
        self,
        operation: Callable[[], OperationReturnType],
        title="Amulet",
        msg="Running Operation",
        throw_exceptions=False,
    ) -> Any:
        with self._edit_lock:
            if self._operation_running:
                raise Exception(
                    "run_operation cannot be called from within itself. "
                    "This function has already been called by parent code so you do not need to run it again"
                )
            self._operation_running = True

            def operation_wrapper():
                yield 0, "Disabling Threads"
                self.renderer.disable_threads()
                yield 0, msg
                op = operation()
                if isinstance(op, GeneratorType):
                    yield from op
                yield 0, "Creating Undo Point"
                yield from self.create_undo_point_iter()
                return op

            err = None
            out = None
            try:
                out = show_loading_dialog(
                    operation_wrapper,
                    title,
                    msg,
                    self,
                )
            except OperationError as e:
                msg = f"Error running operation: {e}"
                log.info(msg)
                self.world.restore_last_undo_point()
                wx.MessageDialog(self, msg, style=wx.OK).ShowModal()
                err = e
            except OperationSuccessful as e:
                msg = str(e)
                log.info(msg)
                self.world.restore_last_undo_point()
                wx.MessageDialog(self, msg, style=wx.OK).ShowModal()
                err = e
            except OperationSilentAbort as e:
                self.world.restore_last_undo_point()
                err = e
            except Exception as e:
                log.error(traceback.format_exc())
                dialog = TracebackDialog(
                    self,
                    "Exception while running operation",
                    str(e),
                    traceback.format_exc(),
                )
                dialog.ShowModal()
                dialog.Destroy()
                err = e
                self.world.restore_last_undo_point()

            self.renderer.enable_threads()
            self.renderer.render_world.rebuild_changed()
            self._operation_running = False
            if err is not None and throw_exceptions:
                raise err
            return out

    def create_undo_point(self, world=True, non_world=True):
        self.world.create_undo_point(world, non_world)
        wx.PostEvent(self, CreateUndoEvent())

    def create_undo_point_iter(self,
                               world=True,
                               non_world=True) -> Generator[float, None, bool]:
        result = yield from self.world.create_undo_point_iter(world, non_world)
        wx.PostEvent(self, CreateUndoEvent())
        return result

    def undo(self):
        self.world.undo()
        self.renderer.render_world.rebuild_changed()
        wx.PostEvent(self, UndoEvent())

    def redo(self):
        self.world.redo()
        self.renderer.render_world.rebuild_changed()
        wx.PostEvent(self, RedoEvent())

    def cut(self):
        self.run_operation(lambda: cut(self.world, self.dimension, self.
                                       selection.selection_group))

    def copy(self):
        self.run_operation(lambda: copy(self.world, self.dimension, self.
                                        selection.selection_group))

    def paste(self, structure: BaseLevel, dimension: Dimension):
        assert isinstance(
            structure,
            BaseLevel), "Structure given is not a subclass of BaseLevel."
        assert (dimension in structure.dimensions
                ), "The requested dimension does not exist for this object."
        wx.PostEvent(self, ToolChangeEvent(tool="Paste"))
        wx.PostEvent(self, PasteEvent(structure=structure,
                                      dimension=dimension))

    def paste_from_cache(self):
        if structure_cache:
            self.paste(*structure_cache.get_structure())
        else:
            wx.MessageBox(
                "A structure needs to be copied before one can be pasted.")

    def delete(self):
        self.run_operation(lambda: delete(self.world, self.dimension, self.
                                          selection.selection_group))

    def goto(self):
        location = show_goto(self, *self.camera.location)
        if location:
            self.camera.location = location

    def select_all(self):
        all_chunk_coords = tuple(self.world.all_chunk_coords(self.dimension))
        if all_chunk_coords:
            min_x, min_z = max_x, max_z = all_chunk_coords[0]
            for x, z in all_chunk_coords:
                if x < min_x:
                    min_x = x
                elif x > max_x:
                    max_x = x
                if z < min_z:
                    min_z = z
                elif z > max_z:
                    max_z = z

            self.selection.selection_corners = [(
                (
                    min_x * self.world.sub_chunk_size,
                    self.world.bounds(self.dimension).min[1],
                    min_z * self.world.sub_chunk_size,
                ),
                (
                    (max_x + 1) * self.world.sub_chunk_size,
                    self.world.bounds(self.dimension).max[1],
                    (max_z + 1) * self.world.sub_chunk_size,
                ),
            )]

        else:
            self.selection.selection_corners = []

    def save(self):
        self.renderer.disable_threads()

        def save():
            yield 0, "Running Pre-Save Operations."
            pre_save_op = self.world.pre_save_operation()
            try:
                while True:
                    yield next(pre_save_op)
            except StopIteration as e:
                if e.value:
                    yield from self.create_undo_point_iter()
                else:
                    self.world.restore_last_undo_point()

            yield 0, "Saving Chunks."
            for chunk_index, chunk_count in self.world.save_iter():
                yield chunk_index / chunk_count

        show_loading_dialog(save, "Saving world.", "Please wait.", self)
        wx.PostEvent(self, SaveEvent())
        self.renderer.enable_threads()
class EditCanvas(BaseEditCanvas):
    def __init__(self, parent: wx.Window, world: "BaseLevel",
                 close_callback: Callable):
        super().__init__(parent, world)
        self._close_callback = close_callback
        self._file_panel: Optional[FilePanel] = None
        self._tool_sizer: Optional[ToolManagerSizer] = None
        config_ = CONFIG.get(EDIT_CONFIG_ID, {})
        user_keybinds = config_.get("user_keybinds", {})
        group = config_.get("keybind_group", DefaultKeybindGroupId)
        if group in user_keybinds:
            keybinds = user_keybinds[group]
        elif group in PresetKeybinds:
            keybinds = PresetKeybinds[group]
        else:
            keybinds = DefaultKeys
        for action_id, (modifier_keys, trigger_key) in keybinds.items():
            self.buttons.register_action(action_id, trigger_key, modifier_keys)

    def _setup(self) -> Generator[OperationYieldType, None, None]:
        yield from super()._setup()
        canvas_sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(canvas_sizer)

        self._file_panel = FilePanel(self)
        canvas_sizer.Add(self._file_panel, 0, wx.EXPAND, 0)

        self._tool_sizer = ToolManagerSizer(self)
        canvas_sizer.Add(self._tool_sizer, 1, wx.EXPAND, 0)

    def _finalise(self):
        super()._finalise()
        self._tool_sizer.enable_default_tool()

    def bind_events(self):
        """Set up all events required to run.
        Note this will also bind subclass events."""
        self._tool_sizer.bind_events()
        # binding the tool events first will run them last so they can't accidentally block UI events.
        super().bind_events()
        self._file_panel.bind_events()
        self.Bind(EVT_EDIT_CLOSE, self._on_close)

    def _on_close(self, _):
        self._close_callback()

    @property
    def tools(self):
        return self._tool_sizer.tools

    def _deselect(self) -> bool:
        return self._tool_sizer.enable_default_tool()

    def run_operation(
        self,
        operation: Callable[[], OperationReturnType],
        title="Amulet",
        msg="Running Operation",
        throw_exceptions=False,
    ) -> Any:
        def operation_wrapper():
            yield 0, "Disabling Threads"
            self.renderer.disable_threads()
            yield 0, msg
            op = operation()
            if isinstance(op, GeneratorType):
                yield from op
            return op

        err = None
        out = None
        try:
            out = show_loading_dialog(
                operation_wrapper,
                title,
                msg,
                self,
            )
            self.create_undo_point()
        except OperationError as e:
            msg = f"Error running operation: {e}"
            log.info(msg)
            self.world.restore_last_undo_point()
            wx.MessageDialog(self, msg, style=wx.OK).ShowModal()
            err = e
        except OperationSuccessful as e:
            msg = str(e)
            log.info(msg)
            self.world.restore_last_undo_point()
            wx.MessageDialog(self, msg, style=wx.OK).ShowModal()
            err = e
        except OperationSilentAbort as e:
            self.world.restore_last_undo_point()
            err = e
        except Exception as e:
            self.world.restore_last_undo_point()
            log.error(traceback.format_exc())
            wx.MessageDialog(
                self,
                f"Exception running operation: {e}\nSee the console for more details",
                style=wx.OK,
            ).ShowModal()
            err = e

        self.renderer.enable_threads()
        self.renderer.render_world.rebuild_changed()
        if err is not None and throw_exceptions:
            raise err
        return out

    def create_undo_point(self, world=True, non_world=True):
        self.world.create_undo_point(world, non_world)
        wx.PostEvent(self, CreateUndoEvent())

    def undo(self):
        self.world.undo()
        self.renderer.render_world.rebuild_changed()
        wx.PostEvent(self, UndoEvent())

    def redo(self):
        self.world.redo()
        self.renderer.render_world.rebuild_changed()
        wx.PostEvent(self, RedoEvent())

    def cut(self):
        self.run_operation(lambda: cut(self.world, self.dimension, self.
                                       selection.selection_group))

    def copy(self):
        self.run_operation(lambda: copy(self.world, self.dimension, self.
                                        selection.selection_group))

    def paste(self, structure: BaseLevel, dimension: Dimension):
        assert isinstance(
            structure,
            BaseLevel), "Structure given is not a subclass of BaseLevel."
        assert (dimension in structure.dimensions
                ), "The requested dimension does not exist for this object."
        wx.PostEvent(self, ToolChangeEvent(tool="Paste"))
        wx.PostEvent(self, PasteEvent(structure=structure,
                                      dimension=dimension))

    def paste_from_cache(self):
        if structure_cache:
            self.paste(*structure_cache.get_structure())
        else:
            wx.MessageBox(
                "A structure needs to be copied before one can be pasted.")

    def delete(self):
        self.run_operation(lambda: delete(self.world, self.dimension, self.
                                          selection.selection_group))

    def goto(self):
        location = show_goto(self, *self.camera.location)
        if location:
            self.camera.location = location

    def select_all(self):
        all_chunk_coords = tuple(self.world.all_chunk_coords(self.dimension))
        if all_chunk_coords:
            min_x, min_z = max_x, max_z = all_chunk_coords[0]
            for x, z in all_chunk_coords:
                if x < min_x:
                    min_x = x
                elif x > max_x:
                    max_x = x
                if z < min_z:
                    min_z = z
                elif z > max_z:
                    max_z = z

            self.selection.selection_corners = [(
                (
                    min_x * self.world.sub_chunk_size,
                    self.world.selection_bounds.min[1],
                    min_z * self.world.sub_chunk_size,
                ),
                (
                    (max_x + 1) * self.world.sub_chunk_size,
                    self.world.selection_bounds.max[1],
                    (max_z + 1) * self.world.sub_chunk_size,
                ),
            )]

        else:
            self.selection.selection_corners = []

    def save(self):
        self.renderer.disable_threads()

        def save():
            for chunk_index, chunk_count in self.world.save_iter():
                yield chunk_index / chunk_count

        show_loading_dialog(lambda: save(), f"Saving world.", "Please wait.",
                            self)
        wx.PostEvent(self, SaveEvent())
        self.renderer.enable_threads()

    def close(self):
        wx.PostEvent(self, EditCloseEvent())