diff --git a/.gitignore b/.gitignore index a1dd7a12..05216257 100644 --- a/.gitignore +++ b/.gitignore @@ -114,5 +114,5 @@ venv.bak/ /world_temp /resource_packs /plugins -/config -/cache +*.config +**/cache diff --git a/amulet_map_editor/api/wx/ui/base_define.py b/amulet_map_editor/api/wx/ui/base_define.py deleted file mode 100644 index 19c62d8c..00000000 --- a/amulet_map_editor/api/wx/ui/base_define.py +++ /dev/null @@ -1,107 +0,0 @@ -import wx -import wx.lib.scrolledpanel -from typing import Tuple, Type - -import PyMCTranslate -from amulet.api.data_types import VersionNumberTuple, PlatformType - -from amulet_map_editor.api.wx.ui.base_select import EVT_ITEM_CHANGE, BaseSelect -from amulet_map_editor.api.wx.ui.version_select import ( - VersionSelect, - EVT_VERSION_CHANGE, -) - - -class BaseDefine(wx.Panel): - def __init__( - self, - parent, - translation_manager: PyMCTranslate.TranslationManager, - select_type: Type[BaseSelect], - orientation=wx.VERTICAL, - platform: str = None, - version_number: Tuple[int, int, int] = None, - namespace: str = None, - default_name: str = None, - show_pick: bool = False, - **kwargs, - ): - super().__init__(parent) - - self._translation_manager = translation_manager - self._orientation = orientation - self._sizer = wx.BoxSizer(orientation) - left_sizer = wx.BoxSizer(wx.VERTICAL) - if orientation == wx.HORIZONTAL: - self._sizer.Add(left_sizer, 1, wx.EXPAND) - else: - self._sizer.Add(left_sizer, 2, wx.EXPAND) - - self._version_picker = VersionSelect( - self, - translation_manager, - platform, - version_number, - **kwargs, - ) - left_sizer.Add(self._version_picker, 0, wx.EXPAND) - self._version_picker.Bind(EVT_VERSION_CHANGE, self._on_version_change) - - self._picker = select_type( - self, - translation_manager, - self._version_picker.platform, - self._version_picker.version_number, - self._version_picker.force_blockstate, - namespace, - default_name, - show_pick, - ) - left_sizer.Add(self._picker, 1, wx.EXPAND | wx.TOP, 5) - self._picker.Bind(EVT_ITEM_CHANGE, self._on_picker_change) - - self.SetSizerAndFit(self._sizer) - self.Layout() - - def _on_picker_change(self, evt): - raise NotImplementedError("This method should be overridden in child classes.") - - def _on_version_change(self, evt): - self._picker.version = ( - evt.platform, - evt.version_number, - evt.force_blockstate, - ) - evt.Skip() - - @property - def platform(self) -> PlatformType: - return self._version_picker.platform - - @platform.setter - def platform(self, platform: PlatformType): - self._version_picker.platform = platform - - @property - def version_number(self) -> VersionNumberTuple: - return self._version_picker.version_number - - @version_number.setter - def version_number(self, version_number: VersionNumberTuple): - self._version_picker.version_number = version_number - - @property - def version(self) -> Tuple[PlatformType, VersionNumberTuple, bool]: - return self._version_picker.version - - @version.setter - def version(self, version: Tuple[PlatformType, VersionNumberTuple, bool]): - self._version_picker.version = version - - @property - def namespace(self) -> str: - return self._picker.namespace - - @namespace.setter - def namespace(self, namespace: str): - self._picker.namespace = namespace diff --git a/amulet_map_editor/api/wx/ui/base_select.py b/amulet_map_editor/api/wx/ui/base_select.py deleted file mode 100644 index 43214c78..00000000 --- a/amulet_map_editor/api/wx/ui/base_select.py +++ /dev/null @@ -1,214 +0,0 @@ -import wx -from wx.lib import newevent -from typing import Tuple, List, Optional - -import PyMCTranslate - -from amulet_map_editor.api.image import COLOUR_PICKER - -( - ItemNamespaceChangeEvent, - EVT_ITEM_NAMESPACE_CHANGE, -) = newevent.NewCommandEvent() # the namespace entry changed -( - ItemNameChangeEvent, - EVT_ITEM_NAME_CHANGE, -) = newevent.NewCommandEvent() # the name entry changed -( - ItemChangeEvent, - EVT_ITEM_CHANGE, -) = ( - newevent.NewCommandEvent() -) # the name or namespace changed. Generated after EVT_ITEM_NAME_CHANGE -( - PickEvent, - EVT_PICK, -) = newevent.NewCommandEvent() # The pick button was pressed - - -class BaseSelect(wx.Panel): - TypeName = "?" - - def __init__( - self, - parent: wx.Window, - translation_manager: PyMCTranslate.TranslationManager, - platform: str, - version_number: Tuple[int, int, int], - force_blockstate: bool = None, - namespace: str = None, - default_name: str = None, - show_pick: bool = False, - ): - super().__init__(parent, style=wx.BORDER_SIMPLE) - self._sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self._sizer) - - self._translation_manager = translation_manager - - self._platform: Optional[str] = None - self._version_number: Optional[Tuple[int, int, int]] = None - self._force_blockstate: Optional[bool] = force_blockstate - - sizer = wx.BoxSizer(wx.HORIZONTAL) - self._sizer.Add(sizer, 0, wx.EXPAND | wx.ALL, 5) - text = wx.StaticText(self, label="Namespace:", style=wx.ALIGN_CENTER) - sizer.Add(text, 1, wx.ALIGN_CENTER_VERTICAL) - self._namespace_combo = wx.ComboBox(self) - sizer.Add(self._namespace_combo, 2) - self._set_version((platform, version_number, force_blockstate or False)) - self._populate_namespace() - self.set_namespace(namespace) - - self._namespace_combo.Bind( - wx.EVT_TEXT, lambda evt: self._post_namespace_change() - ) - self._do_text_event = ( - True # some widgets create events. This is used to suppress them - ) - - self.Bind(EVT_ITEM_NAMESPACE_CHANGE, self._on_namespace_change) - sizer = wx.BoxSizer(wx.VERTICAL) - self._sizer.Add(sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 5) - header_sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(header_sizer, 0, wx.EXPAND | wx.BOTTOM, 5) - header_sizer.Add( - wx.StaticText( - self, label=f"{self.TypeName.capitalize()} name:", style=wx.ALIGN_CENTER - ), - 1, - wx.ALIGN_CENTER_VERTICAL, - ) - search_sizer = wx.BoxSizer(wx.HORIZONTAL) - header_sizer.Add(search_sizer, 2, wx.EXPAND) - self._search = wx.SearchCtrl(self) - search_sizer.Add(self._search, 1, wx.ALIGN_CENTER_VERTICAL) - self._search.Bind(wx.EVT_TEXT, self._on_search_change) - if show_pick: - pick_button = wx.BitmapButton(self, bitmap=COLOUR_PICKER.bitmap(22, 22)) - search_sizer.Add(pick_button, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) - pick_button.Bind( - wx.EVT_BUTTON, - lambda evt: wx.PostEvent(self, PickEvent(self.GetId(), widget=self)), - ) - self._list_box = wx.ListBox(self, style=wx.LB_SINGLE) - sizer.Add(self._list_box, 1, wx.EXPAND) - - self._names: List[str] = [] - self._populate_item_name() - self.set_name(default_name) - self._list_box.Bind(wx.EVT_LISTBOX, lambda evt: self._post_item_change()) - - def _post_namespace_change(self): - if self._do_text_event: - wx.PostEvent( - self, ItemNamespaceChangeEvent(self.GetId(), namespace=self.namespace) - ) - self._do_text_event = True - - def _post_item_change(self): - wx.PostEvent(self, ItemNameChangeEvent(self.GetId(), name=self.name)), - wx.PostEvent( - self, - ItemChangeEvent(self.GetId(), namespace=self.namespace, name=self.name), - ) - - @property - def version(self) -> Tuple[str, Tuple[int, int, int], bool]: - return self._platform, self._version_number, self._force_blockstate - - @version.setter - def version(self, version: Tuple[str, Tuple[int, int, int], bool]): - self._set_version(version) - self._populate_namespace() - self.namespace = None - - def _set_version(self, version: Tuple[str, Tuple[int, int, int], bool]): - assert ( - version[0] in self._translation_manager.platforms() - and version[1] in self._translation_manager.version_numbers(version[0]) - and isinstance(version[2], bool) - ), f"{version} is not a valid version" - self._platform, self._version_number, self._force_blockstate = version - - @property - def namespace(self) -> str: - return self._namespace_combo.GetValue() - - @namespace.setter - def namespace(self, namespace: str): - self.set_namespace(namespace) - wx.PostEvent( - self, ItemNamespaceChangeEvent(self.GetId(), namespace=self.namespace) - ) - - def set_namespace(self, namespace: str): - namespace = namespace or "minecraft" - if isinstance(namespace, str): - if namespace in self._namespace_combo.GetItems(): - self._namespace_combo.SetSelection( - self._namespace_combo.GetItems().index(namespace) - ) - else: - self._namespace_combo.ChangeValue(namespace) - - @property - def name(self) -> str: - name: str = self._list_box.GetString(self._list_box.GetSelection()) - if self._list_box.GetSelection() == 0 and name.startswith('"'): - name = name[1:-1] - return name - - @name.setter - def name(self, name: str): - if self.set_name(name): - self._post_item_change() - - def set_name(self, name: str) -> bool: - name = name or "" - self._search.ChangeValue(name) - return self._update_item_name(name) - - def _populate_namespace(self): - raise NotImplementedError("This method should be overridden in child classes.") - - def _populate_item_name(self): - raise NotImplementedError("This method should be overridden in child classes.") - - def _on_namespace_change(self, evt): - self._populate_item_name() - self.name = None - evt.Skip() - - def _on_search_change(self, evt): - search_str = evt.GetString() - if self._update_item_name(search_str): - self._post_item_change() - - def _update_item_name(self, search_str: str) -> bool: - names = [bn for bn in self._names if search_str in bn] - if search_str not in names: - names.insert(0, f'"{search_str}"') - - index = 0 - selection = self._list_box.GetSelection() - if selection != wx.NOT_FOUND: - current_string = self._list_box.GetString(selection) - if current_string in names: - index = names.index(current_string) - - self._list_box.SetItems(names) - if index: - # if the previously selected string is in the list select that - self._list_box.SetSelection(index) - return False - elif search_str in names: - # if the searched text perfectly matches select that - self._list_box.SetSelection(names.index(search_str)) - return True - elif len(self._list_box.GetItems()) >= 2: - self._list_box.SetSelection(1) - return True - else: - self._list_box.SetSelection(0) - return True diff --git a/amulet_map_editor/api/wx/ui/biome_select/__init__.py b/amulet_map_editor/api/wx/ui/biome_select/__init__.py deleted file mode 100644 index 99571245..00000000 --- a/amulet_map_editor/api/wx/ui/biome_select/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .biome_define import BiomeDefine diff --git a/amulet_map_editor/api/wx/ui/biome_select/biome_define.py b/amulet_map_editor/api/wx/ui/biome_select/biome_define.py deleted file mode 100644 index 42ee554e..00000000 --- a/amulet_map_editor/api/wx/ui/biome_select/biome_define.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Tuple - -import PyMCTranslate -import wx - -from amulet_map_editor.api.wx.ui.base_define import BaseDefine -from amulet_map_editor.api.wx.ui.biome_select.biome_select import BiomeSelect - - -class BiomeDefine(BaseDefine): - def __init__( - self, - parent, - translation_manager: PyMCTranslate.TranslationManager, - orientation=wx.VERTICAL, - platform: str = None, - version_number: Tuple[int, int, int] = None, - namespace: str = None, - biome_name: str = None, - show_pick_biome: bool = False, - **kwargs, - ): - super().__init__( - parent, - translation_manager, - BiomeSelect, - orientation, - platform, - version_number, - namespace, - default_name=biome_name, - show_pick=show_pick_biome, - show_force_blockstate=False, - **kwargs, - ) - - def _on_picker_change(self, evt): - evt.Skip() - - @property - def biome_name(self) -> str: - return self._picker.name - - @biome_name.setter - def biome_name(self, biome_name: str): - self._picker.name = biome_name - - @property - def biome(self) -> str: - return f"{self.namespace}:{self.biome_name}" - - @biome.setter - def biome(self, biome: str): - namespace, biome_name = biome.split(":") - self._picker.set_namespace(namespace) - self._picker.set_name(biome_name) - - @property - def universal_biome(self) -> str: - return self._translation_manager.get_version( - self.platform, self.version_number - ).biome.to_universal(self.biome) - - @universal_biome.setter - def universal_biome(self, universal_biome: str): - self.biome = self._translation_manager.get_version( - self.platform, self.version_number - ).biome.from_universal(universal_biome) diff --git a/amulet_map_editor/api/wx/ui/biome_select/biome_select.py b/amulet_map_editor/api/wx/ui/biome_select/biome_select.py deleted file mode 100644 index def90b2f..00000000 --- a/amulet_map_editor/api/wx/ui/biome_select/biome_select.py +++ /dev/null @@ -1,28 +0,0 @@ -from amulet_map_editor.api.wx.ui.base_select import BaseSelect - - -class BiomeSelect(BaseSelect): - TypeName = "Biome" - - def _populate_namespace(self): - version = self._translation_manager.get_version( - self._platform, self._version_number - ) - namespaces = list( - set( - [biome_id[: biome_id.find(":")] for biome_id in version.biome.biome_ids] - ) - ) - self._do_text_event = False - self._namespace_combo.Set(namespaces) - - def _populate_item_name(self): - version = self._translation_manager.get_version( - self._platform, self._version_number - ) - self._names = [ - biome_id[len(self.namespace) + 1 :] - for biome_id in version.biome.biome_ids - if biome_id.startswith(self.namespace) - ] - self._list_box.SetItems(self._names) diff --git a/amulet_map_editor/api/wx/ui/block_select/__init__.py b/amulet_map_editor/api/wx/ui/block_select/__init__.py deleted file mode 100644 index d33b294f..00000000 --- a/amulet_map_editor/api/wx/ui/block_select/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .block_select import BlockSelect -from .properties import PropertySelect, EVT_PROPERTIES_CHANGE -from .block_define import BlockDefine -from .multi_block_define import MultiBlockDefine diff --git a/amulet_map_editor/api/wx/ui/block_select/block_define.py b/amulet_map_editor/api/wx/ui/block_select/block_define.py deleted file mode 100644 index 837ae78e..00000000 --- a/amulet_map_editor/api/wx/ui/block_select/block_define.py +++ /dev/null @@ -1,176 +0,0 @@ -import wx -import wx.lib.scrolledpanel -from typing import Tuple, Optional, Dict - -import PyMCTranslate -from amulet.api.block import PropertyType, Block -from amulet.api.block_entity import BlockEntity - -from amulet_map_editor.api.wx.ui.base_define import BaseDefine -from amulet_map_editor.api.wx.ui.block_select import BlockSelect - -from amulet_map_editor.api.wx.ui.block_select.properties import ( - PropertySelect, - WildcardSNBTType, - EVT_PROPERTIES_CHANGE, -) - - -class BlockDefine(BaseDefine): - def __init__( - self, - parent, - translation_manager: PyMCTranslate.TranslationManager, - orientation=wx.VERTICAL, - platform: str = None, - version_number: Tuple[int, int, int] = None, - force_blockstate: bool = None, - namespace: str = None, - block_name: str = None, - properties: PropertyType = None, - wildcard_properties=False, - show_pick_block: bool = False, - **kwargs, - ): - super().__init__( - parent, - translation_manager, - BlockSelect, - orientation, - platform, - version_number, - namespace, - default_name=block_name, - show_pick=show_pick_block, - force_blockstate=force_blockstate, - **kwargs, - ) - - right_sizer = wx.BoxSizer(wx.VERTICAL) - if orientation == wx.HORIZONTAL: - self._sizer.Add(right_sizer, 1, wx.EXPAND | wx.LEFT, 5) - else: - self._sizer.Add(right_sizer, 1, wx.EXPAND | wx.TOP, 5) - - self._property_picker = PropertySelect( - self, - translation_manager, - self._version_picker.platform, - self._version_picker.version_number, - self._version_picker.force_blockstate, - self._picker.namespace, - self._picker.name, - properties, - wildcard_properties, - ) - right_sizer.Add(self._property_picker, 1, wx.EXPAND) - self._property_picker.Bind(EVT_PROPERTIES_CHANGE, self._on_property_change) - - self.SetSizerAndFit(self._sizer) - self.Layout() - - def _on_picker_change(self, evt): - self._update_properties() - evt.Skip() - - def _on_property_change(self, evt): - self.Layout() - evt.Skip() - - def _update_properties(self): - self._property_picker.version_block = ( - self._version_picker.platform, - self._version_picker.version_number, - self._version_picker.force_blockstate, - self._picker.namespace, - self._picker.name, - ) - - @property - def force_blockstate(self) -> bool: - return self._version_picker.force_blockstate - - @force_blockstate.setter - def force_blockstate(self, force_blockstate: bool): - self._version_picker.force_blockstate = force_blockstate - - @property - def block_name(self) -> str: - return self._picker.name - - @block_name.setter - def block_name(self, block_name: str): - self._picker.name = block_name - - @property - def str_properties(self) -> Dict[str, "WildcardSNBTType"]: - return self._property_picker.str_properties - - @str_properties.setter - def str_properties(self, str_properties: Dict[str, "WildcardSNBTType"]): - self._property_picker.str_properties = str_properties - - @property - def properties(self) -> PropertyType: - return self._property_picker.properties - - @properties.setter - def properties(self, properties: PropertyType): - self._property_picker.properties = properties - - @property - def block(self) -> Block: - return Block(self.namespace, self.block_name, self.properties) - - @block.setter - def block(self, block: Block): - self._picker.set_namespace(block.namespace) - self._picker.set_name(block.base_name) - self._update_properties() - self.properties = block.properties - - @property - def block_entity(self) -> Optional[BlockEntity]: - return None # TODO - - @block_entity.setter - def block_entity(self, block_entity: Optional[BlockEntity]): - if block_entity is not None: - pass # TODO - - @property - def universal_block(self) -> Tuple[Block, Optional[BlockEntity]]: - return self._translation_manager.get_version( - self.platform, self.version_number - ).block.to_universal(self.block, self.block_entity, self.force_blockstate)[:2] - - @universal_block.setter - def universal_block(self, universal_block: Tuple[Block, Optional[BlockEntity]]): - block, block_entity = universal_block - v_block, v_block_entity = self._translation_manager.get_version( - self.platform, self.version_number - ).block.from_universal(block, block_entity, self.force_blockstate)[:2] - if isinstance(v_block, Block): - self.block = v_block - self.block_entity = v_block_entity - - -if __name__ == "__main__": - - def main(): - app = wx.App() - translation_manager = PyMCTranslate.new_translation_manager() - dialog = wx.Dialog(None, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) - sizer = wx.BoxSizer() - dialog.SetSizer(sizer) - sizer.Add( - BlockDefine(dialog, translation_manager, wx.HORIZONTAL), - 1, - wx.ALL | wx.EXPAND, - 5, - ) - dialog.Show() - dialog.Fit() - app.MainLoop() - - main() diff --git a/amulet_map_editor/api/wx/ui/block_select/block_select.py b/amulet_map_editor/api/wx/ui/block_select/block_select.py deleted file mode 100644 index 8abf6f9d..00000000 --- a/amulet_map_editor/api/wx/ui/block_select/block_select.py +++ /dev/null @@ -1,63 +0,0 @@ -import wx -from typing import Tuple - -import PyMCTranslate - -from amulet_map_editor.api.wx.ui.base_select import BaseSelect - - -class BlockSelect(BaseSelect): - TypeName = "Block" - - def __init__( - self, - parent: wx.Window, - translation_manager: PyMCTranslate.TranslationManager, - platform: str, - version_number: Tuple[int, int, int], - force_blockstate: bool, - namespace: str = None, - block_name: str = None, - show_pick_block: bool = False, - ): - super().__init__( - parent, - translation_manager, - platform, - version_number, - force_blockstate, - namespace, - block_name, - show_pick_block, - ) - - def _populate_namespace(self): - version = self._translation_manager.get_version( - self._platform, self._version_number - ) - namespaces = version.block.namespaces(self._force_blockstate) - self._do_text_event = False - self._namespace_combo.Set(namespaces) - - def _populate_item_name(self): - version = self._translation_manager.get_version( - self._platform, self._version_number - ) - self._names = version.block.base_names(self.namespace, self._force_blockstate) - self._list_box.SetItems(self._names) - - -if __name__ == "__main__": - - def main(): - app = wx.App() - translation_manager = PyMCTranslate.new_translation_manager() - dialog = wx.Dialog(None) - sizer = wx.BoxSizer() - dialog.SetSizer(sizer) - sizer.Add(BlockSelect(dialog, translation_manager, "java", (1, 16, 0), False)) - dialog.Show() - dialog.Fit() - app.MainLoop() - - main() diff --git a/amulet_map_editor/api/wx/ui/block_select/properties.py b/amulet_map_editor/api/wx/ui/block_select/properties.py deleted file mode 100644 index 89d92026..00000000 --- a/amulet_map_editor/api/wx/ui/block_select/properties.py +++ /dev/null @@ -1,385 +0,0 @@ -import wx -from wx.lib import newevent -from typing import Tuple, Dict, Optional, List, Union -import weakref - -import PyMCTranslate -import amulet_nbt -from amulet_nbt import SNBTType -from amulet.api.block import PropertyDataTypes, PropertyType - -WildcardSNBTType = Union[SNBTType, str] - -from amulet_map_editor.api.image import ADD_ICON, SUBTRACT_ICON - -( - PropertiesChangeEvent, - EVT_PROPERTIES_CHANGE, -) = newevent.NewCommandEvent() # the properties changed - - -class PropertySelect(wx.Panel): - def __init__( - self, - parent: wx.Window, - translation_manager: PyMCTranslate.TranslationManager, - platform: str, - version_number: Tuple[int, int, int], - force_blockstate: bool, - namespace: str, - block_name: str, - properties: Dict[str, SNBTType] = None, - wildcard_mode=False, - ): - super().__init__(parent, style=wx.BORDER_SIMPLE) - self._parent = weakref.ref(parent) - sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(sizer) - - self._translation_manager = translation_manager - - self._platform: Optional[str] = None - self._version_number: Optional[Tuple[int, int, int]] = None - self._force_blockstate: Optional[bool] = None - self._namespace: Optional[str] = None - self._block_name: Optional[str] = None - - self._manual_enabled = False - self._simple = SimplePropertySelect(self, translation_manager, wildcard_mode) - sizer.Add(self._simple, 1, wx.EXPAND) - self._manual = ManualPropertySelect(self, translation_manager) - sizer.Add(self._manual, 1, wx.EXPAND) - - self._wildcard_mode = wildcard_mode - - self._set_version_block( - (platform, version_number, force_blockstate, namespace, block_name) - ) - self.set_properties(properties) - - @property - def parent(self) -> wx.Window: - return self._parent() - - @property - def wildcard_mode(self) -> bool: - return self._wildcard_mode - - @property - def version_block(self) -> Tuple[str, Tuple[int, int, int], bool, str, str]: - return ( - self._platform, - self._version_number, - self._force_blockstate, - self._namespace, - self._block_name, - ) - - @version_block.setter - def version_block( - self, version_block: Tuple[str, Tuple[int, int, int], bool, str, str] - ): - self._set_version_block(version_block) - self.str_properties = None - - def _set_version_block( - self, version_block: Tuple[str, Tuple[int, int, int], bool, str, str] - ): - version = version_block[:3] - assert ( - version[0] in self._translation_manager.platforms() - and version[1] in self._translation_manager.version_numbers(version[0]) - and isinstance(version[2], bool) - ), f"{version} is not a valid version" - self._platform, self._version_number, self._force_blockstate = version - block = version_block[3:5] - assert isinstance(block[0], str) and isinstance( - block[1], str - ), "The block namespace and block name must be strings" - self._namespace, self._block_name = block - self._set_ui() - - @property - def str_properties(self) -> Dict[str, WildcardSNBTType]: - if self._manual_enabled: - return self._manual.properties - else: - return self._simple.properties - - @str_properties.setter - def str_properties(self, properties: Dict[str, WildcardSNBTType]): - self.set_properties(properties) - wx.PostEvent( - self, PropertiesChangeEvent(self.GetId(), properties=self.str_properties) - ) - - @property - def properties(self) -> PropertyType: - if self.wildcard_mode: - raise Exception( - "Accessing the properties attribute is invalid in wildcard mode" - ) - return { - key: amulet_nbt.from_snbt(val) for key, val in self.str_properties.items() - } - - @properties.setter - def properties(self, properties: PropertyType): - self.str_properties = { - key: val.to_snbt() for key, val in (properties or {}).items() - } - - def set_properties(self, properties: Dict[str, SNBTType]): - properties = properties or {} - self.Freeze() - if self._manual_enabled: - self._manual.properties = properties - else: - self._simple.properties = properties - self.TopLevelParent.Layout() - self.Thaw() - - def _set_ui(self): - self.Freeze() - translator = self._translation_manager.get_version( - self._platform, self._version_number - ).block - - self._manual_enabled = self._block_name not in translator.base_names( - self._namespace, self._force_blockstate - ) - if self._manual_enabled: - self._simple.Hide() - self._manual.Show() - else: - self._simple.Show() - self._simple.set_specification( - translator.get_specification( - self._namespace, self._block_name, self._force_blockstate - ) - ) - self._manual.Hide() - self.Thaw() - - -class SimplePropertySelect(wx.Panel): - def __init__( - self, - parent: wx.Window, - translation_manager: PyMCTranslate.TranslationManager, - wildcard_mode, - ): - super().__init__(parent) - sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(sizer) - self._translation_manager = translation_manager - - header_sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(header_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) - label = wx.StaticText(self, label="Property Name", style=wx.ALIGN_CENTER) - header_sizer.Add(label, 1) - label = wx.StaticText( - self, label="Property Value (SNBT)", style=wx.ALIGN_CENTER - ) - header_sizer.Add(label, 1, wx.LEFT, 5) - self._property_sizer = wx.GridSizer(2, 5, 5) - sizer.Add(self._property_sizer, 0, wx.ALL | wx.EXPAND, 5) - - self._properties: Dict[str, wx.Choice] = {} - self._specification: dict = {} - self._wildcard_mode = wildcard_mode - - def set_specification(self, specification: dict): - self._specification = specification - - @property - def properties(self) -> Dict[str, SNBTType]: - return { - name: choice.GetString(choice.GetSelection()) - for name, choice in self._properties.items() - } - - @properties.setter - def properties(self, properties: Dict[str, SNBTType]): - self.Freeze() - self._properties.clear() - self._property_sizer.Clear(True) - spec_properties: Dict[str, List[str]] = self._specification.get( - "properties", {} - ) - spec_defaults = self._specification.get("defaults", {}) - - for name, choices in spec_properties.items(): - label = wx.StaticText(self, label=name) - self._property_sizer.Add(label, 0, wx.ALIGN_CENTER) - if self._wildcard_mode: - choices = ["*"] + choices - choice = wx.Choice(self, choices=choices) - self._property_sizer.Add(choice, 0, wx.EXPAND) - choice.Bind( - wx.EVT_CHOICE, - lambda evt: wx.PostEvent( - self, - PropertiesChangeEvent(self.GetId(), properties=self.properties), - ), - ) - val = spec_defaults[name] - if name in properties and val != "*": - try: - snbt = amulet_nbt.from_snbt(properties[name]).to_snbt() - except: - pass - else: - if snbt in choices: - val = snbt - if val in choices: - choice.SetSelection(choices.index(val)) - self._properties[name] = choice - self.Thaw() - self.Fit() - self.Layout() - - -class ManualPropertySelect(wx.Panel): - def __init__( - self, parent: wx.Window, translation_manager: PyMCTranslate.TranslationManager - ): - super().__init__(parent) - sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(sizer) - self._translation_manager = translation_manager - - header_sizer = wx.BoxSizer(wx.HORIZONTAL) - add_button = wx.BitmapButton( - self, bitmap=ADD_ICON.bitmap(30, 30), size=(30, 30) - ) - header_sizer.Add(add_button) - sizer.Add(header_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) - label = wx.StaticText(self, label="Property Name", style=wx.ALIGN_CENTER) - header_sizer.Add(label, 1, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) - label = wx.StaticText( - self, label="Property Value (SNBT)", style=wx.ALIGN_CENTER - ) - header_sizer.Add(label, 1, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) - header_sizer.AddStretchSpacer(1) - - self._property_sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add( - self._property_sizer, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 5 - ) - - add_button.Bind(wx.EVT_BUTTON, lambda evt: self._add_property()) - - self._property_index = 0 - self._properties: Dict[int, Tuple[wx.TextCtrl, wx.TextCtrl]] = {} - - def _post_property_change(self): - wx.PostEvent( - self, PropertiesChangeEvent(self.GetId(), properties=self.properties) - ) - - def _add_property(self, name: str = "", value: SNBTType = ""): - self.Freeze() - sizer = wx.BoxSizer(wx.HORIZONTAL) - self._property_index += 1 - subtract_button = wx.BitmapButton( - self, bitmap=SUBTRACT_ICON.bitmap(30, 30), size=(30, 30) - ) - sizer.Add(subtract_button, 0, wx.ALIGN_CENTER_VERTICAL) - index = self._property_index - subtract_button.Bind( - wx.EVT_BUTTON, lambda evt: self._on_remove_property(sizer, index) - ) - name_entry = wx.TextCtrl(self, value=name, style=wx.TE_CENTER) - sizer.Add(name_entry, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) - name_entry.Bind(wx.EVT_TEXT, lambda evt: self._post_property_change()) - value_entry = wx.TextCtrl(self, value=value, style=wx.TE_CENTER) - sizer.Add(value_entry, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) - snbt_text = wx.StaticText(self, style=wx.ALIGN_CENTER) - sizer.Add(snbt_text, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) - self._change_value("", snbt_text) - value_entry.Bind(wx.EVT_TEXT, lambda evt: self._on_value_change(evt, snbt_text)) - - self._property_sizer.Add(sizer, 1, wx.TOP | wx.EXPAND, 5) - self._properties[self._property_index] = (name_entry, value_entry) - self.Fit() - self.TopLevelParent.Layout() - self.Thaw() - - def _on_value_change(self, evt, snbt_text: wx.StaticText): - self._change_value(evt.GetString(), snbt_text) - self._post_property_change() - evt.Skip() - - def _change_value(self, snbt: SNBTType, snbt_text: wx.StaticText): - try: - nbt = amulet_nbt.from_snbt(snbt) - except: - snbt_text.SetLabel("Invalid SNBT") - snbt_text.SetBackgroundColour((255, 200, 200)) - else: - if isinstance(nbt, PropertyDataTypes): - snbt_text.SetLabel(nbt.to_snbt()) - snbt_text.SetBackgroundColour(wx.NullColour) - else: - snbt_text.SetLabel(f"{nbt.__class__.__name__} not valid") - snbt_text.SetBackgroundColour((255, 200, 200)) - self.Layout() - - def _on_remove_property(self, sizer: wx.Sizer, key: int): - self.Freeze() - self._property_sizer.Detach(sizer) - sizer.Clear(True) - del self._properties[key] - self.TopLevelParent.Layout() - self.Thaw() - self._post_property_change() - - @property - def properties(self) -> Dict[str, SNBTType]: - properties = {} - for name, value in self._properties.values(): - try: - nbt = amulet_nbt.from_snbt(value.GetValue()) - except: - continue - if name.GetValue() and isinstance(nbt, PropertyDataTypes): - properties[name.GetValue()] = nbt.to_snbt() - return properties - - @properties.setter - def properties(self, properties: Dict[str, SNBTType]): - self._property_sizer.Clear(True) - self._properties.clear() - self._property_index = 0 - for name, value in properties.items(): - self._add_property(name, value) - - -if __name__ == "__main__": - - def main(): - translation_manager = PyMCTranslate.new_translation_manager() - app = wx.App() - dialog = wx.Dialog(None) - sizer = wx.BoxSizer() - dialog.SetSizer(sizer) - sizer.Add( - PropertySelect( - dialog, - translation_manager, - "java", - (1, 16, 0), - False, - "minecraft", - "oak_fence", - ), - 1, - wx.ALL, - 5, - ) - dialog.Show() - dialog.Fit() - app.MainLoop() - - main() diff --git a/amulet_map_editor/api/wx/ui/demo.py b/amulet_map_editor/api/wx/ui/demo.py new file mode 100644 index 00000000..6e009993 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/demo.py @@ -0,0 +1,26 @@ +from amulet_map_editor.api.wx.ui.mc.biome.demo import demo as biome_demo +from amulet_map_editor.api.wx.ui.mc.block.demo import demo as block_demo +from amulet_map_editor.api.wx.ui.nbt_editor import demo as nbt_editor_demo +from amulet_map_editor.api.wx.ui.select_world import demo as select_world_demo +from amulet_map_editor.api.wx.ui.mc.version.demo import demo as version_select_demo + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + biome_demo() + block_demo() + nbt_editor_demo() + select_world_demo() + version_select_demo() + + +if __name__ == "__main__": + + import wx + + app = wx.App() + demo() + app.MainLoop() diff --git a/amulet_map_editor/api/wx/ui/events.py b/amulet_map_editor/api/wx/ui/events.py new file mode 100644 index 00000000..26300969 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/events.py @@ -0,0 +1,5 @@ +from wx.lib import newevent + +# Sizers only propagate size changes downwards +# This event can be used to propagate those changes upwards so that parent elements know of the size change +ChildSizeEvent, EVT_CHILD_SIZE = newevent.NewCommandEvent() diff --git a/amulet_map_editor/api/wx/ui/mc/__init__.py b/amulet_map_editor/api/wx/ui/mc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/api/wx/ui/mc/base/__init__.py b/amulet_map_editor/api/wx/ui/mc/base/__init__.py new file mode 100644 index 00000000..31653a0a --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/base/__init__.py @@ -0,0 +1,2 @@ +from .base_identifier_select import * +from .base_define import * diff --git a/amulet_map_editor/api/wx/ui/mc/base/base_define.py b/amulet_map_editor/api/wx/ui/mc/base/base_define.py new file mode 100644 index 00000000..d8105fee --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/base/base_define.py @@ -0,0 +1,59 @@ +import wx +import wx.lib.scrolledpanel + +import PyMCTranslate +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import ( + BaseIdentifierSelect, +) +from amulet_map_editor.api.wx.ui.mc.version import ( + VersionSelect, + EVT_VERSION_CHANGE, +) +from amulet_map_editor.api.wx.ui.mc.state import BaseResourceIDState, StateHolder + + +class BaseDefine(wx.Panel, StateHolder): + _version_picker: VersionSelect + _identifier_select: BaseIdentifierSelect + state: BaseResourceIDState + + def __init__( + self, + parent, + state: BaseResourceIDState, + *, + orientation=wx.VERTICAL, + ): + assert isinstance(state, BaseResourceIDState) + StateHolder.__init__(self, state) + wx.Panel.__init__(self, parent) + + self._orientation = orientation + self._sizer = wx.BoxSizer(orientation) + self.SetSizer(self._sizer) + + self._top_sizer = wx.BoxSizer(wx.VERTICAL) + if orientation == wx.HORIZONTAL: + self._sizer.Add(self._top_sizer, 1, wx.EXPAND) + else: + self._sizer.Add(self._top_sizer, 0, wx.EXPAND) + + self._version_picker = VersionSelect(self, state) + self._version_picker.Bind(EVT_VERSION_CHANGE, self._post_change) + self._top_sizer.Add(self._version_picker, 0, wx.EXPAND) + self._child_state_holders.append(self._version_picker) + self.Layout() + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + orientation=wx.VERTICAL, + **kwargs, + ): + raise NotImplementedError + + def _post_change(self, evt): + raise NotImplementedError diff --git a/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/__init__.py b/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/__init__.py new file mode 100644 index 00000000..d3771987 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/__init__.py @@ -0,0 +1,6 @@ +from .events import ( + PickEvent, + EVT_PICK, + BaseIDChangeEvent, +) +from .base_identifier_select import BaseIdentifierSelect diff --git a/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/base_identifier_select.py b/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/base_identifier_select.py new file mode 100644 index 00000000..44289ec2 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/base_identifier_select.py @@ -0,0 +1,216 @@ +import wx +import PyMCTranslate + +from amulet_map_editor import lang +from amulet_map_editor.api.image import COLOUR_PICKER +from amulet_map_editor.api.wx.ui.mc.state import BaseResourceIDState, StateHolder, State +from .events import ( + PickEvent, +) + + +class BaseIdentifierSelect(wx.Panel, StateHolder): + """ + BaseIdentifierSelect is a base class for a UI containing + a namespace choice + a base name search + a list of base names + """ + + state: BaseResourceIDState + + def __init__( + self, + parent: wx.Window, + state: BaseResourceIDState, + *, + show_pick: bool = False, + ): + assert isinstance(state, BaseResourceIDState) + StateHolder.__init__(self, state) + wx.Panel.__init__(self, parent, style=wx.BORDER_SIMPLE) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + self._sizer.Add(sizer, 0, wx.EXPAND | wx.ALL, 5) + text = wx.StaticText( + self, label=lang.get("widget.mc.namespace"), style=wx.ALIGN_CENTER + ) + sizer.Add(text, 1, wx.ALIGN_CENTER_VERTICAL) + self._namespace_combo = wx.ComboBox(self) + sizer.Add(self._namespace_combo, 2) + self._update_namespace() + + # This was previously done with EVT_TEXT but that is also triggered by the Set method. + # This is a workaround so that it is only triggered by user input. + self._namespace_combo.Bind(wx.EVT_CHAR, self._on_namespace_char) + self._namespace_combo.Bind(wx.EVT_COMBOBOX, self._on_namespace_change) + + sizer = wx.BoxSizer(wx.VERTICAL) + self._sizer.Add(sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 5) + header_sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(header_sizer, 0, wx.EXPAND | wx.BOTTOM, 5) + header_sizer.Add( + wx.StaticText( + self, + label=self.base_name_label, + style=wx.ALIGN_CENTER, + ), + 1, + wx.ALIGN_CENTER_VERTICAL, + ) + search_sizer = wx.BoxSizer(wx.HORIZONTAL) + header_sizer.Add(search_sizer, 2, wx.EXPAND) + self._search = wx.SearchCtrl(self) + search_sizer.Add(self._search, 1, wx.ALIGN_CENTER_VERTICAL) + self._search.Bind(wx.EVT_TEXT, self._on_search_change) + if show_pick: + pick_button = wx.BitmapButton(self, bitmap=COLOUR_PICKER.bitmap(22, 22)) + search_sizer.Add(pick_button, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) + pick_button.Bind( + wx.EVT_BUTTON, + lambda evt: wx.PostEvent(self, PickEvent(self.GetId(), widget=self)), + ) + self._base_name_list_box = wx.ListBox(self, style=wx.LB_SINGLE) + sizer.Add(self._base_name_list_box, 1, wx.EXPAND) + self._update_base_name() + self._base_name_list_box.Bind(wx.EVT_LISTBOX, self._on_base_name_change) + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + show_pick: bool = False, + **kwargs, + ): + raise NotImplementedError + + @property + def base_name_label(self) -> str: + raise NotImplementedError + + def _post_event( + self, + namespace: str, + base_name: str, + ): + raise NotImplementedError + + def _on_state_change(self): + if self.state.is_changed(State.Namespace) or self.state.is_changed( + State.ForceBlockstate + ): + self._update_namespace() + if self.state.is_changed(State.Namespace) or self.state.is_changed( + State.BaseName + ): + self._update_base_name() + + def _update_namespace(self): + if self._namespace_combo.GetItems() != self.state.valid_namespaces: + self._namespace_combo.Set(self.state.valid_namespaces) + namespace = self.state.namespace + if namespace != self._namespace_combo.GetValue(): + index = self._namespace_combo.FindString(namespace) + if index == wx.NOT_FOUND: + self._namespace_combo.ChangeValue(namespace) + else: + self._namespace_combo.SetSelection(index) + + def _update_base_name(self): + base_name = self.state.base_name + if base_name in self.state.valid_base_names: + # The base name is known + if self.state.is_changed(State.ForceBlockstate): + self._update_from_search() + location = self._base_name_list_box.FindString(base_name) + if location == wx.NOT_FOUND: + self._search.ChangeValue("") + self._update_from_search() + location = self._base_name_list_box.FindString(base_name) + self._base_name_list_box.SetSelection(location) + else: + # The base name is not known + self._search.ChangeValue(base_name) + self._update_from_search() + + def _update_from_search(self) -> bool: + """ + Update the base names based on the value in the search field. + + :return: True if the text in the field changed + """ + search_str = self._search.GetValue() + base_names = sorted( + [bn for bn in self.state.valid_base_names if search_str in bn] + ) + exact = search_str in base_names + + if (search_str and not exact) or (not search_str and not base_names): + # We have a search which is not a match or we don't have a search string or options + base_names.insert(0, f'"{search_str}"') + + # find the previously selected string + previous_string = None + selection = self._base_name_list_box.GetSelection() + if selection != wx.NOT_FOUND: + previous_string = self._base_name_list_box.GetString(selection) + + self.Freeze() + self._base_name_list_box.SetItems(base_names) + if exact: + # if the searched text perfectly matches select that + self._base_name_list_box.SetSelection(base_names.index(search_str)) + elif previous_string in base_names: + # if the previously selected string is in the list select that + index = base_names.index(previous_string) + self._base_name_list_box.SetSelection(index) + elif len(self._base_name_list_box.GetItems()) >= 2: + self._base_name_list_box.SetSelection(1) + else: + self._base_name_list_box.SetSelection(0) + self.Thaw() + + return previous_string != self._base_name_list_box.GetString( + self._base_name_list_box.GetSelection() + ) + + def _handle_namespace_change(self): + namespace = self._namespace_combo.GetValue() + if namespace != self.state.namespace: + with self.state as state: + state.namespace = namespace + self._post_event(self.state.namespace, self.state.base_name) + + def _on_namespace_change(self, evt): + self._handle_namespace_change() + if isinstance(evt, wx.KeyEvent): + evt.Skip() + + def _on_namespace_char(self, evt): + wx.CallAfter(self._handle_namespace_change) + evt.Skip() + + def _on_search_change(self, evt): + if self._update_from_search(): + self._on_change() + + def _on_base_name_change(self, evt): + self._on_change() + + def _on_change(self): + base_name = self._base_name_list_box.GetString( + self._base_name_list_box.GetSelection() + ) + if ( + self._base_name_list_box.GetSelection() == 0 + and base_name not in self.state.valid_base_names + ): + base_name = base_name[1:-1] + if base_name != self.state.namespace: + with self.state as state: + state.base_name = base_name + self._post_event(self.state.namespace, self.state.base_name) diff --git a/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/events.py b/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/events.py new file mode 100644 index 00000000..f111452e --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/base/base_identifier_select/events.py @@ -0,0 +1,30 @@ +import wx +from wx.lib import newevent + +( + PickEvent, + EVT_PICK, +) = newevent.NewCommandEvent() # The pick button was pressed + + +class BaseIDChangeEvent(wx.PyEvent): + """ + Run when the identifier changes. + """ + + def __init__( + self, + namespace: str, + base_name: str, + ): + wx.PyEvent.__init__(self) + self._namespace = namespace + self._base_name = base_name + + @property + def namespace(self) -> str: + return self._namespace + + @property + def base_name(self) -> str: + return self._base_name diff --git a/amulet_map_editor/api/wx/ui/mc/biome/__init__.py b/amulet_map_editor/api/wx/ui/mc/biome/__init__.py new file mode 100644 index 00000000..04dd142b --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/biome/__init__.py @@ -0,0 +1,2 @@ +from .identifier_select import BiomeIdentifierSelect +from .biome_define import BiomeDefine diff --git a/amulet_map_editor/api/wx/ui/mc/biome/biome_define.py b/amulet_map_editor/api/wx/ui/mc/biome/biome_define.py new file mode 100644 index 00000000..2cf215b3 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/biome/biome_define.py @@ -0,0 +1,193 @@ +import PyMCTranslate +import wx + +from amulet.api.data_types import VersionNumberTuple, PlatformType +from amulet_map_editor.api.wx.ui.mc.base.base_define import BaseDefine +from amulet_map_editor.api.wx.ui.mc.biome.identifier_select.biome_identifier_select import ( + BiomeIdentifierSelect, +) +from amulet_map_editor.api.wx.ui.mc.biome.identifier_select.events import ( + EVT_BIOME_ID_CHANGE, +) +from amulet_map_editor.api.wx.ui.mc.state import BiomeResourceIDState + +_BiomeChangeEventType = wx.NewEventType() +EVT_BIOME_CHANGE = wx.PyEventBinder(_BiomeChangeEventType) + + +class BiomeChangeEvent(wx.PyEvent): + def __init__( + self, + platform: PlatformType, + version_number: VersionNumberTuple, + force_blockstate: bool, + namespace: str, + base_name: str, + ): + wx.PyEvent.__init__(self, eventType=_BiomeChangeEventType) + self._platform = platform + self._version_number = version_number + self._force_blockstate = force_blockstate + self._namespace = namespace + self._base_name = base_name + + @property + def platform(self) -> PlatformType: + return self._platform + + @property + def version_number(self) -> VersionNumberTuple: + return self._version_number + + @property + def force_blockstate(self) -> bool: + return self._force_blockstate + + @property + def namespace(self) -> str: + return self._namespace + + @property + def base_name(self) -> str: + return self._base_name + + +class BiomeDefine(BaseDefine): + """ + A UI that merges a version select widget with a biome select widget. + """ + + state: BiomeResourceIDState + _identifier_select: BiomeIdentifierSelect + + def __init__( + self, + parent, + state: BiomeResourceIDState, + *, + show_pick_biome: bool = False, + orientation=wx.VERTICAL, + ): + assert isinstance(state, BiomeResourceIDState) + super().__init__( + parent, + state, + orientation=orientation, + ) + self._identifier_select = BiomeIdentifierSelect( + self, + state, + show_pick=show_pick_biome, + ) + self._identifier_select.Bind(EVT_BIOME_ID_CHANGE, self._post_change) + self._top_sizer.Add(self._identifier_select, 1, wx.EXPAND | wx.TOP, 5) + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + show_pick_biome: bool = False, + orientation=wx.VERTICAL, + **kwargs, + ): + return cls( + parent, + BiomeResourceIDState(translation_manager, **kwargs), + show_pick_biome=show_pick_biome, + orientation=orientation, + ) + + def _post_change(self, evt): + wx.PostEvent( + self, + BiomeChangeEvent( + self.state.platform, + self.state.version_number, + self.state.force_blockstate, + self.state.namespace, + self.state.base_name, + ), + ) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="BiomeDefine", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + obj = BiomeDefine.from_data(dialog, translation_manager, orientation=wx.HORIZONTAL) + + def on_biome_change(evt: BiomeChangeEvent): + print( + evt.platform, + evt.version_number, + evt.force_blockstate, + evt.namespace, + evt.base_name, + ) + + obj.Bind(EVT_BIOME_CHANGE, on_biome_change) + + sizer.Add( + obj, + 1, + wx.ALL | wx.EXPAND, + 5, + ) + dialog.Show() + dialog.Fit() + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + + def set_data( + platform: PlatformType, + version: VersionNumberTuple, + force_blockstate: bool, + namespace: str, + base_name: str, + ): + with obj.state as state: + state.platform = platform + state.version_number = version + state.force_blockstate = force_blockstate + state.namespace = namespace + state.base_name = base_name + + interval = 1_000 + wx.CallLater( + interval * 1, set_data, "java", (1, 17, 0), False, "minecraft", "end_highlands" + ) + wx.CallLater( + interval * 2, + set_data, + "bedrock", + (1, 17, 0), + False, + "minecraft", + "birch_forest_hills_mutated", + ) + wx.CallLater( + interval * 3, set_data, "java", (1, 17, 0), False, "minecraft", "random" + ) + wx.CallLater( + interval * 4, set_data, "bedrock", (1, 17, 0), False, "minecraft", "random" + ) + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/biome/demo.py b/amulet_map_editor/api/wx/ui/mc/biome/demo.py new file mode 100644 index 00000000..ce3ee07c --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/biome/demo.py @@ -0,0 +1,26 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.biome.identifier_select.biome_identifier_select import ( + demo as biome_select_demo, +) +from amulet_map_editor.api.wx.ui.mc.biome.biome_define import ( + demo as biome_define_demo, +) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + biome_select_demo() + biome_define_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/__init__.py b/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/__init__.py new file mode 100644 index 00000000..2501953c --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/__init__.py @@ -0,0 +1,2 @@ +from .biome_identifier_select import BiomeIdentifierSelect +from .events import BiomeIDChangeEvent, EVT_BIOME_ID_CHANGE diff --git a/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/biome_identifier_select.py b/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/biome_identifier_select.py new file mode 100644 index 00000000..64360189 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/biome_identifier_select.py @@ -0,0 +1,98 @@ +import wx + +import PyMCTranslate +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.mc.state import BiomeResourceIDState +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import ( + BaseIdentifierSelect, +) +from amulet_map_editor.api.wx.ui.mc.biome.identifier_select.events import ( + BiomeIDChangeEvent, + EVT_BIOME_ID_CHANGE, +) + + +class BiomeIdentifierSelect(BaseIdentifierSelect): + """ + A UI consisting of a namespace choice, biome name search box and list of biome names. + """ + + @property + def base_name_label(self) -> str: + return lang.get("widget.mc.biome.base_name") + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + show_pick: bool = False, + **kwargs + ): + return cls( + parent, + BiomeResourceIDState(translation_manager, **kwargs), + show_pick=show_pick, + ) + + def _post_event( + self, + namespace: str, + base_name: str, + ): + wx.PostEvent( + self, + BiomeIDChangeEvent( + namespace, + base_name, + ), + ) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + import PyMCTranslate + + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="BiomeIdentifierSelect", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + widget = BiomeIdentifierSelect.from_data( + dialog, + translation_manager, + platform="java", + version_number=(1, 16, 0), + force_blockstate=False, + ) + + def on_change(evt: BiomeIDChangeEvent): + print(evt.namespace, evt.base_name) + + widget.Bind(EVT_BIOME_ID_CHANGE, on_change) + sizer.Add( + widget, + 1, + wx.ALL | wx.EXPAND, + 5, + ) + dialog.Show() + dialog.Fit() + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/events.py b/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/events.py new file mode 100644 index 00000000..6d3decdd --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/biome/identifier_select/events.py @@ -0,0 +1,19 @@ +import wx +from ...base.base_identifier_select import BaseIDChangeEvent + +_BiomeIDChangeEventType = wx.NewEventType() +EVT_BIOME_ID_CHANGE = wx.PyEventBinder(_BiomeIDChangeEventType) + + +class BiomeIDChangeEvent(BaseIDChangeEvent): + """ + Run when the biome resource identifier changes. + """ + + def __init__( + self, + namespace: str, + base_name: str, + ): + super().__init__(namespace, base_name) + self.SetEventType(_BiomeIDChangeEventType) diff --git a/amulet_map_editor/api/wx/ui/mc/block/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/__init__.py new file mode 100644 index 00000000..106599cb --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/__init__.py @@ -0,0 +1,4 @@ +from .properties import * +from .identifier_select import * +from .define import * +from .multi_block_define import MultiBlockDefine diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/define/__init__.py new file mode 100644 index 00000000..cfe9599c --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/__init__.py @@ -0,0 +1,8 @@ +from .widget import * +from .button import * +from .events import ( + BlockChangeEvent, + EVT_BLOCK_CHANGE, + WildcardBlockChangeEvent, + EVT_WILDCARD_BLOCK_CHANGE, +) diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/button/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/define/button/__init__.py new file mode 100644 index 00000000..61e32fc4 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/button/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseBlockDefineButton +from .normal import BlockDefineButton +from .wildcard import WildcardBlockDefineButton diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/button/base.py b/amulet_map_editor/api/wx/ui/mc/block/define/button/base.py new file mode 100644 index 00000000..9fd2a770 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/button/base.py @@ -0,0 +1,74 @@ +import wx +from typing import Optional + +import PyMCTranslate +from amulet_map_editor.api.wx.ui.events import ChildSizeEvent +from amulet_map_editor.api.wx.ui.mc.block.define import BaseBlockDefine +from amulet_map_editor.api.wx.ui.mc.state import StateHolder, BlockState +from amulet_map_editor.api.wx.ui.simple import SimpleDialog + + +class BaseBlockDefineButton(wx.Button, StateHolder): + _block_widget: Optional[BaseBlockDefine] + + def __init__( + self, + parent: wx.Window, + state: BlockState, + *, + show_pick_block: bool = False, + max_char_length: int = 99999, + ): + assert isinstance(state, BlockState) + StateHolder.__init__(self, state) + wx.Button.__init__(self, parent, style=wx.BU_LEFT) + self._block_widget: Optional[BaseBlockDefine] = None + self.Bind(wx.EVT_BUTTON, self._on_press) + self._show_pick_block = show_pick_block + self._max_char_length = max(3, max_char_length) + self.update_button() + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + show_pick_block: bool = False, + max_char_length: int = 99999, + **kwargs, + ): + return cls( + parent, + BlockState(translation_manager, **kwargs), + show_pick_block=show_pick_block, + max_char_length=max_char_length, + ) + + def SetLabel(self, label: str): + if len(label) > self._max_char_length: + label = f"{label[:self._max_char_length]}..." + super().SetLabel(label) + + def _on_press(self, evt): + dialog = SimpleDialog(self, "Pick a Block") + self._block_widget = self._create_block_define(dialog) + self._child_state_holders.append(self._block_widget) + dialog.sizer.Add(self._block_widget, 1, wx.EXPAND) + dialog.Fit() + if dialog.ShowModal() == wx.ID_OK: + self.state = self._block_widget.state + self._block_widget.state = None + self._child_state_holders.remove(self._block_widget) + self._block_widget = None + dialog.Destroy() + + def _create_block_define(self, dialog: wx.Dialog) -> BaseBlockDefine: + raise NotImplementedError + + def _on_state_change(self): + self.update_button() + wx.PostEvent(self, ChildSizeEvent(0)) + + def update_button(self): + raise NotImplementedError diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/button/demo.py b/amulet_map_editor/api/wx/ui/mc/block/define/button/demo.py new file mode 100644 index 00000000..416cc806 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/button/demo.py @@ -0,0 +1,26 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.block.define.button.normal import ( + demo as normal_button_demo, +) +from amulet_map_editor.api.wx.ui.mc.block.define.button.wildcard import ( + demo as wildcard_button_demo, +) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + normal_button_demo() + wildcard_button_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/button/normal.py b/amulet_map_editor/api/wx/ui/mc/block/define/button/normal.py new file mode 100644 index 00000000..4bc96a8a --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/button/normal.py @@ -0,0 +1,65 @@ +import wx + +import PyMCTranslate + +from amulet_map_editor.api.wx.ui.mc.block.define.widget import BlockDefine +from amulet_map_editor.api.wx.ui.mc.block.define.button.base import ( + BaseBlockDefineButton, +) +from amulet_map_editor.api.wx.ui.mc.state import BlockState + + +class BlockDefineButton(BaseBlockDefineButton): + state: BlockState + + def _create_block_define(self, dialog: wx.Dialog) -> BlockDefine: + return BlockDefine( + dialog, + self.state.copy(), + orientation=wx.HORIZONTAL, + ) + + def update_button(self): + """Update the text on the button from the internal state.""" + blockstate = f"{self.state.namespace}:{self.state.base_name}" + if self.state.properties: + props = ",".join( + f"{key}={val}" for key, val in self.state.properties.items() + ) + blockstate = f"{blockstate}[{props}]" + self.SetLabel(f" {blockstate}") + self.SetToolTip(blockstate) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="BlockDefineButton", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + sizer.Add( + BlockDefineButton.from_data(dialog, translation_manager), + 0, + wx.ALL, + 5, + ) + dialog.Show() + dialog.Fit() + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/button/wildcard.py b/amulet_map_editor/api/wx/ui/mc/block/define/button/wildcard.py new file mode 100644 index 00000000..a2bbe829 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/button/wildcard.py @@ -0,0 +1,72 @@ +import wx + +import PyMCTranslate + +from amulet_map_editor.api.wx.ui.mc.block.define.widget import WildcardBlockDefine +from amulet_map_editor.api.wx.ui.mc.block.define.button.base import ( + BaseBlockDefineButton, +) +from amulet_map_editor.api.wx.ui.mc.state import BlockState + + +class WildcardBlockDefineButton(BaseBlockDefineButton): + state: BlockState + + def _create_block_define(self, dialog: wx.Dialog) -> WildcardBlockDefine: + return WildcardBlockDefine( + dialog, + self.state.copy(), + orientation=wx.HORIZONTAL, + ) + + def update_button(self): + """Update the text on the button from the internal state.""" + if self.state.properties_multiple: + properties = [ + f"{key}:({'|'.join([v.to_snbt() for v in val])})" + for key, val in self.state.properties_multiple.items() + ] + self.SetLabel( + f" {self.state.namespace}:{self.state.base_name}[{','.join(properties)}]" + ) + properties_str = ",\n".join(properties) + self.SetToolTip( + f"{self.state.namespace}:{self.state.base_name}[\n{properties_str}\n]" + ) + else: + self.SetLabel(f" {self.state.namespace}:{self.state.base_name}") + self.SetToolTip(f"{self.state.namespace}:{self.state.base_name}") + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="WildcardBlockDefineButton", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + sizer.Add( + WildcardBlockDefineButton.from_data(dialog, translation_manager), + 0, + wx.ALL, + 5, + ) + dialog.Show() + dialog.Fit() + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/demo.py b/amulet_map_editor/api/wx/ui/mc/block/define/demo.py new file mode 100644 index 00000000..517b1a52 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/demo.py @@ -0,0 +1,26 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.block.define.button.demo import ( + demo as block_button_demo, +) +from amulet_map_editor.api.wx.ui.mc.block.define.widget.demo import ( + demo as block_widget_demo, +) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + block_button_demo() + block_widget_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/events.py b/amulet_map_editor/api/wx/ui/mc/block/define/events.py new file mode 100644 index 00000000..70082392 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/events.py @@ -0,0 +1,107 @@ +import wx +from amulet.api.block import PropertyType, PropertyTypeMultiple +from amulet.api.data_types import VersionNumberTuple, PlatformType + + +class BaseBlockChangeEvent: + def __init__( + self, + platform: PlatformType, + version_number: VersionNumberTuple, + force_blockstate: bool, + namespace: str, + base_name: str, + ): + self._platform = platform + self._version_number = version_number + self._force_blockstate = force_blockstate + self._namespace = namespace + self._base_name = base_name + + @property + def platform(self) -> PlatformType: + return self._platform + + @property + def version_number(self) -> VersionNumberTuple: + return self._version_number + + @property + def force_blockstate(self) -> bool: + return self._force_blockstate + + @property + def namespace(self) -> str: + return self._namespace + + @property + def base_name(self) -> str: + return self._base_name + + +_BlockChangeEventType = wx.NewEventType() +EVT_BLOCK_CHANGE = wx.PyEventBinder(_BlockChangeEventType) + + +class BlockChangeEvent(wx.PyEvent, BaseBlockChangeEvent): + """ + Run when the block define UI changes. + """ + + def __init__( + self, + platform: PlatformType, + version_number: VersionNumberTuple, + force_blockstate: bool, + namespace: str, + base_name: str, + properties: PropertyType, + ): + wx.PyEvent.__init__(self, eventType=_BlockChangeEventType) + BaseBlockChangeEvent.__init__( + self, + platform, + version_number, + force_blockstate, + namespace, + base_name, + ) + self._properties = properties + + @property + def properties(self) -> PropertyType: + return self._properties + + +_WildcardBlockChangeEventType = wx.NewEventType() +EVT_WILDCARD_BLOCK_CHANGE = wx.PyEventBinder(_WildcardBlockChangeEventType) + + +class WildcardBlockChangeEvent(wx.PyEvent, BaseBlockChangeEvent): + """ + Run when the wildcard block define UI changes. + """ + + def __init__( + self, + platform: PlatformType, + version_number: VersionNumberTuple, + force_blockstate: bool, + namespace: str, + base_name: str, + selected_properties: PropertyTypeMultiple, + ): + wx.PyEvent.__init__(self, eventType=_WildcardBlockChangeEventType) + BaseBlockChangeEvent.__init__( + self, + platform, + version_number, + force_blockstate, + namespace, + base_name, + ) + self._selected_properties = selected_properties + + @property + def selected_properties(self) -> PropertyTypeMultiple: + return self._selected_properties diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/widget/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/define/widget/__init__.py new file mode 100644 index 00000000..a54df884 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/widget/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseBlockDefine +from .normal import BlockDefine +from .wildcard import WildcardBlockDefine diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/widget/base.py b/amulet_map_editor/api/wx/ui/mc/block/define/widget/base.py new file mode 100644 index 00000000..d051c373 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/widget/base.py @@ -0,0 +1,62 @@ +import PyMCTranslate +import wx + +from amulet_map_editor.api.wx.ui.mc.base.base_define import BaseDefine +from amulet_map_editor.api.wx.ui.mc.block import BlockIdentifierSelect + +from amulet_map_editor.api.wx.ui.mc.block import BasePropertySelect, EVT_BLOCK_ID_CHANGE +from amulet_map_editor.api.wx.ui.mc.state import BlockState + + +class BaseBlockDefine(BaseDefine): + """ + A UI that merges a version select widget with a block select widget and a property select. + """ + + state: BlockState + _identifier_select: BlockIdentifierSelect + _property_picker: BasePropertySelect + + def __init__( + self, + parent, + state: BlockState, + *, + orientation=wx.VERTICAL, + show_pick_block: bool = False, + ): + assert isinstance(state, BlockState) + BaseDefine.__init__( + self, + parent, + state, + orientation=orientation, + ) + self._identifier_select = BlockIdentifierSelect( + self, + state, + show_pick=show_pick_block, + ) + self._identifier_select.Bind(EVT_BLOCK_ID_CHANGE, self._post_change) + self._top_sizer.Add(self._identifier_select, 1, wx.EXPAND | wx.TOP, 5) + self._child_state_holders.append(self._identifier_select) + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + orientation=wx.VERTICAL, + show_pick_block: bool = False, + **kwargs, + ): + return cls( + parent, + BlockState( + translation_manager, + **kwargs, + ), + orientation=orientation, + show_pick_block=show_pick_block, + ) diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/widget/demo.py b/amulet_map_editor/api/wx/ui/mc/block/define/widget/demo.py new file mode 100644 index 00000000..92190cf0 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/widget/demo.py @@ -0,0 +1,26 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.block.define.widget.normal import ( + demo as block_widget_demo, +) +from amulet_map_editor.api.wx.ui.mc.block.define.widget.wildcard import ( + demo as wildcard_widget_demo, +) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + block_widget_demo() + wildcard_widget_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/widget/normal.py b/amulet_map_editor/api/wx/ui/mc/block/define/widget/normal.py new file mode 100644 index 00000000..d3f4c1aa --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/widget/normal.py @@ -0,0 +1,154 @@ +import wx +import wx.lib.scrolledpanel + +import PyMCTranslate +import amulet_nbt +from amulet_map_editor.api.wx.ui.events import EVT_CHILD_SIZE + +from amulet_map_editor.api.wx.ui.mc.block import ( + SinglePropertySelect, + EVT_SINGLE_PROPERTIES_CHANGE, +) +from amulet_map_editor.api.wx.ui.mc.block.define.widget.base import BaseBlockDefine +from amulet_map_editor.api.wx.ui.mc.block.define.events import ( + BlockChangeEvent, + EVT_BLOCK_CHANGE, +) +from amulet_map_editor.api.wx.ui.mc.state import BlockState + + +class BlockDefine(BaseBlockDefine): + """ + A UI that merges a version select widget with a block select widget and a property select. + """ + + state: BlockState + _property_picker: SinglePropertySelect + + def __init__( + self, + parent, + state: BlockState, + *, + show_pick_block: bool = False, + orientation=wx.VERTICAL, + ): + assert isinstance(state, BlockState) + super().__init__( + parent, + state, + orientation=orientation, + show_pick_block=show_pick_block, + ) + + right_sizer = wx.BoxSizer(wx.VERTICAL) + border = wx.LEFT if orientation == wx.HORIZONTAL else wx.TOP + self._sizer.Add(right_sizer, 1, wx.EXPAND | border, 5) + self._property_picker = self._create_property_picker() + self._property_picker.Bind(EVT_SINGLE_PROPERTIES_CHANGE, self._post_change) + right_sizer.Add(self._property_picker, 1, wx.EXPAND) + self._child_state_holders.append(self._property_picker) + self.Layout() + + def _create_property_picker(self) -> SinglePropertySelect: + return SinglePropertySelect(self, self.state) + + def _post_change(self, evt): + wx.PostEvent( + self, + BlockChangeEvent( + self.state.platform, + self.state.version_number, + self.state.force_blockstate, + self.state.namespace, + self.state.base_name, + self.state.properties, + ), + ) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + + def create_dialog(block): + dialog = wx.Dialog( + None, + title=f"BlockDefine with block {block['namespace']}:{block['base_name']}", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + obj = BlockDefine.from_data( + dialog, + translation_manager, + platform="java", + version_number=(1, 16, 0), + force_blockstate=False, + **block, + orientation=wx.HORIZONTAL, + ) + + sizer.Add( + obj, + 1, + wx.ALL, + 5, + ) + + def on_change(evt: BlockChangeEvent): + print( + evt.platform, + evt.version_number, + evt.force_blockstate, + evt.namespace, + evt.base_name, + evt.properties, + ) + + def on_close(evt): + dialog.Destroy() + + def on_child_size(evt): + dialog.Layout() + evt.Skip() + + obj.Bind(EVT_BLOCK_CHANGE, on_change) + dialog.Bind(wx.EVT_CLOSE, on_close) + dialog.Bind(EVT_CHILD_SIZE, on_child_size) + dialog.Show() + dialog.Fit() + + for block_ in ( + { + "namespace": "minecraft", + "base_name": "oak_fence", + "properties": { + "east": amulet_nbt.TAG_String("false"), + "north": amulet_nbt.TAG_String("true"), + "south": amulet_nbt.TAG_String("false"), + "west": amulet_nbt.TAG_String("false"), + }, + }, + { + "namespace": "modded", + "base_name": "block", + "properties": { + "test": amulet_nbt.TAG_String("hello"), + }, + }, + ): + create_dialog(block_) + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/define/widget/wildcard.py b/amulet_map_editor/api/wx/ui/mc/block/define/widget/wildcard.py new file mode 100644 index 00000000..95fda80d --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/define/widget/wildcard.py @@ -0,0 +1,162 @@ +import wx +import wx.lib.scrolledpanel + +import PyMCTranslate +import amulet_nbt +from amulet_map_editor.api.wx.ui.events import EVT_CHILD_SIZE + +from amulet_map_editor.api.wx.ui.mc.block import ( + MultiplePropertySelect, + EVT_MULTIPLE_PROPERTIES_CHANGE, +) +from amulet_map_editor.api.wx.ui.mc.block.define.widget.base import BaseBlockDefine +from amulet_map_editor.api.wx.ui.mc.block.define.events import ( + WildcardBlockChangeEvent, + EVT_WILDCARD_BLOCK_CHANGE, +) +from amulet_map_editor.api.wx.ui.mc.state import BlockState + + +class WildcardBlockDefine(BaseBlockDefine): + """ + A UI that merges a version select widget with a block select widget and a multi property select. + """ + + state: BlockState + _property_picker: MultiplePropertySelect + + def __init__( + self, + parent, + state: BlockState, + *, + show_pick_block: bool = False, + orientation=wx.VERTICAL, + ): + assert isinstance(state, BlockState) + BaseBlockDefine.__init__( + self, + parent, + state, + orientation=orientation, + show_pick_block=show_pick_block, + ) + + right_sizer = wx.BoxSizer(wx.VERTICAL) + border = wx.LEFT if orientation == wx.HORIZONTAL else wx.TOP + self._sizer.Add(right_sizer, 1, wx.EXPAND | border, 5) + self._property_picker = MultiplePropertySelect(self, state) + right_sizer.Add(self._property_picker, 1, wx.EXPAND) + self._property_picker.Bind(EVT_MULTIPLE_PROPERTIES_CHANGE, self._post_change) + self._child_state_holders.append(self._property_picker) + + self.Layout() + + def _post_change(self, evt): + wx.PostEvent( + self, + WildcardBlockChangeEvent( + self.state.platform, + self.state.version_number, + self.state.force_blockstate, + self.state.namespace, + self.state.base_name, + self.state.properties_multiple, + ), + ) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + + def create_dialog(block): + dialog = wx.Dialog( + None, + title=f"WildcardBlockDefine with block {block['namespace']}:{block['base_name']}", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + obj = WildcardBlockDefine.from_data( + dialog, + translation_manager, + platform="java", + version_number=(1, 16, 0), + force_blockstate=False, + **block, + orientation=wx.HORIZONTAL, + ) + + sizer.Add( + obj, + 1, + wx.ALL, + 5, + ) + + def on_change(evt: WildcardBlockChangeEvent): + print( + evt.platform, + evt.version_number, + evt.force_blockstate, + evt.namespace, + evt.base_name, + evt.selected_properties, + ) + + def on_close(evt): + dialog.Destroy() + + def on_child_size(evt): + dialog.Layout() + evt.Skip() + + obj.Bind(EVT_WILDCARD_BLOCK_CHANGE, on_change) + dialog.Bind(wx.EVT_CLOSE, on_close) + dialog.Bind(EVT_CHILD_SIZE, on_child_size) + dialog.Show() + dialog.Fit() + + for block_ in ( + { + "namespace": "minecraft", + "base_name": "oak_fence", + "properties_multiple": { + "east": ( + amulet_nbt.TAG_String("true"), + amulet_nbt.TAG_String("false"), + ), + "north": ( + amulet_nbt.TAG_String("true"), + amulet_nbt.TAG_String("false"), + ), + "south": (amulet_nbt.TAG_String("false"),), + "west": (amulet_nbt.TAG_String("true"),), + }, + }, + { + "namespace": "modded", + "base_name": "block", + "properties_multiple": { + "test": ( + amulet_nbt.TAG_String("hello"), + amulet_nbt.TAG_String("hello2"), + ), + }, + }, + ): + create_dialog(block_) + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/demo.py b/amulet_map_editor/api/wx/ui/mc/block/demo.py new file mode 100644 index 00000000..db9fa574 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/demo.py @@ -0,0 +1,31 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.block.identifier_select.block_identifier_select import ( + demo as block_select_demo, +) +from amulet_map_editor.api.wx.ui.mc.block.define.demo import ( + demo as block_define_demo, +) + +# from amulet_map_editor.api.wx.ui.mc.block.multi_block_define import ( +# demo as multi_block_define_demo, +# ) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + block_select_demo() + block_define_demo() + # multi_block_define_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/identifier_select/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/identifier_select/__init__.py new file mode 100644 index 00000000..9885e07e --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/identifier_select/__init__.py @@ -0,0 +1,2 @@ +from .block_identifier_select import BlockIdentifierSelect +from .events import BlockIDChangeEvent, EVT_BLOCK_ID_CHANGE diff --git a/amulet_map_editor/api/wx/ui/mc/block/identifier_select/block_identifier_select.py b/amulet_map_editor/api/wx/ui/mc/block/identifier_select/block_identifier_select.py new file mode 100644 index 00000000..f6a091fb --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/identifier_select/block_identifier_select.py @@ -0,0 +1,98 @@ +import wx + +import PyMCTranslate +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import ( + BaseIdentifierSelect, +) +from amulet_map_editor.api.wx.ui.mc.state import BlockResourceIDState +from amulet_map_editor.api.wx.ui.mc.block.identifier_select.events import ( + BlockIDChangeEvent, + EVT_BLOCK_ID_CHANGE, +) + + +class BlockIdentifierSelect(BaseIdentifierSelect): + """ + A UI consisting of a namespace choice, block name search box and list of block names. + """ + + @property + def base_name_label(self) -> str: + return lang.get("widget.mc.block.base_name") + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + show_pick: bool = False, + **kwargs + ): + return cls( + parent, + BlockResourceIDState(translation_manager, **kwargs), + show_pick=show_pick, + ) + + def _post_event( + self, + namespace: str, + base_name: str, + ): + wx.PostEvent( + self, + BlockIDChangeEvent( + namespace, + base_name, + ), + ) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + import PyMCTranslate + + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="BlockIdentifierSelect", + style=wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT | wx.RESIZE_BORDER, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + widget = BlockIdentifierSelect.from_data( + dialog, + translation_manager, + platform="java", + version_number=(1, 16, 0), + force_blockstate=False, + ) + + def on_change(evt: BlockIDChangeEvent): + print(evt.namespace, evt.base_name) + + widget.Bind(EVT_BLOCK_ID_CHANGE, on_change) + sizer.Add( + widget, + 1, + wx.ALL | wx.EXPAND, + 5, + ) + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + dialog.Show() + dialog.Fit() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/identifier_select/events.py b/amulet_map_editor/api/wx/ui/mc/block/identifier_select/events.py new file mode 100644 index 00000000..d885d06c --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/identifier_select/events.py @@ -0,0 +1,19 @@ +import wx +from ...base.base_identifier_select import BaseIDChangeEvent + +_BlockIDChangeEventType = wx.NewEventType() +EVT_BLOCK_ID_CHANGE = wx.PyEventBinder(_BlockIDChangeEventType) + + +class BlockIDChangeEvent(BaseIDChangeEvent): + """ + Run when the block resource identifier changes. + """ + + def __init__( + self, + namespace: str, + base_name: str, + ): + super().__init__(namespace, base_name) + self.SetEventType(_BlockIDChangeEventType) diff --git a/amulet_map_editor/api/wx/ui/block_select/multi_block_define.py b/amulet_map_editor/api/wx/ui/mc/block/multi_block_define.py similarity index 87% rename from amulet_map_editor/api/wx/ui/block_select/multi_block_define.py rename to amulet_map_editor/api/wx/ui/mc/block/multi_block_define.py index 542daee5..76bd99d6 100644 --- a/amulet_map_editor/api/wx/ui/block_select/multi_block_define.py +++ b/amulet_map_editor/api/wx/ui/mc/block/multi_block_define.py @@ -12,7 +12,8 @@ MAXIMIZE, MINIMIZE, ) -from amulet_map_editor.api.wx.ui.block_select import BlockDefine, EVT_PROPERTIES_CHANGE +from amulet_map_editor.api.wx.ui.mc.block.define import BlockDefine +from amulet_map_editor.api.wx.ui.mc.block.properties import EVT_SINGLE_PROPERTIES_CHANGE class MultiBlockDefine(wx.lib.scrolledpanel.ScrolledPanel): @@ -150,7 +151,7 @@ def __init__(self, parent: MultiBlockDefine, translation_manager, collapsed=Fals self.expand_button.Bind( wx.EVT_BUTTON, lambda evt: self._toggle_block_expand(parent) ) - self.block_define.Bind(EVT_PROPERTIES_CHANGE, self._on_properties_change) + self.block_define.Bind(EVT_SINGLE_PROPERTIES_CHANGE, self._on_properties_change) @property def collapsed(self) -> bool: @@ -180,25 +181,35 @@ def _on_properties_change(self, evt): def _gen_block_string(self): base = f"{self.block_define.namespace}:{self.block_define.block_name}" properties = ",".join( - ( - f"{key}={value}" - for key, value in self.block_define.str_properties.items() - ) + (f"{key}={value}" for key, value in self.block_define.properties.items()) ) return f"{base}[{properties}]" if properties else base +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="MultiBlockDefine", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + sizer.Add(MultiBlockDefine(dialog, translation_manager), 1, wx.EXPAND) + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + dialog.Show() + dialog.Fit() + + if __name__ == "__main__": def main(): app = wx.App() - translation_manager = PyMCTranslate.new_translation_manager() - dialog = wx.Dialog(None, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) - sizer = wx.BoxSizer() - dialog.SetSizer(sizer) - sizer.Add(MultiBlockDefine(dialog, translation_manager), 1, wx.EXPAND) - dialog.Show() - dialog.Fit() + demo() app.MainLoop() main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/properties/__init__.py new file mode 100644 index 00000000..43514eb8 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/__init__.py @@ -0,0 +1,11 @@ +from .base import BasePropertySelect +from .single import ( + SinglePropertySelect, + SinglePropertiesChangeEvent, + EVT_SINGLE_PROPERTIES_CHANGE, +) +from .multiple import ( + MultiplePropertySelect, + MultiplePropertiesChangeEvent, + EVT_MULTIPLE_PROPERTIES_CHANGE, +) diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/base.py b/amulet_map_editor/api/wx/ui/mc/block/properties/base.py new file mode 100644 index 00000000..319732de --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/base.py @@ -0,0 +1,29 @@ +import wx + +import PyMCTranslate +from amulet_map_editor.api.wx.ui.mc.state import StateHolder, BlockState + + +class BasePropertySelect(wx.Panel, StateHolder): + def __init__( + self, + parent: wx.Window, + state: BlockState, + ): + assert isinstance(state, BlockState) + StateHolder.__init__(self, state) + wx.Panel.__init__(self, parent) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + **kwargs + ): + return cls( + parent, + BlockState(translation_manager, **kwargs), + ) diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/demo.py b/amulet_map_editor/api/wx/ui/mc/block/properties/demo.py new file mode 100644 index 00000000..330645a5 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/demo.py @@ -0,0 +1,26 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.block.properties.single import ( + demo as single_properties_demo, +) +from amulet_map_editor.api.wx.ui.mc.block.properties.multiple import ( + demo as multiple_properties_demo, +) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + single_properties_demo() + multiple_properties_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/__init__.py new file mode 100644 index 00000000..063bf618 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/__init__.py @@ -0,0 +1,4 @@ +"""A UI from which a user can chose one or more values for each property.""" + +from .events import MultiplePropertiesChangeEvent, EVT_MULTIPLE_PROPERTIES_CHANGE +from .main import MultiplePropertySelect, demo diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/base.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/base.py new file mode 100644 index 00000000..a0447601 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/base.py @@ -0,0 +1,54 @@ +import wx + +from amulet.api.block import PropertyTypeMultiple +from amulet_map_editor.api.wx.ui.mc.state import StateHolder, BlockState, State +from .events import MultiplePropertiesChangeEvent + + +class BaseMultipleProperty(wx.Panel, StateHolder): + """ + A UI from which a user can choose zero or more values for each property. + + This is base class for both flavours of multiple property selection UIs. + Subclasses must implement the logic. + """ + + state: BlockState + + def __init__(self, parent: wx.Window, state: BlockState): + StateHolder.__init__(self, state) + wx.Panel.__init__(self, parent) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + def _rebuild_properties(self): + raise NotImplementedError + + def _tear_down_properties(self): + raise NotImplementedError + + def _update_properties(self): + raise NotImplementedError + + def _get_ui_properties_multiple(self) -> PropertyTypeMultiple: + raise NotImplementedError + + def _if_do_state_change(self) -> bool: + raise NotImplementedError + + def _on_state_change(self): + if self._if_do_state_change(): + if self.state.is_changed(State.BaseName): + self._rebuild_properties() + elif self.state.is_changed(State.PropertiesMultiple): + if self.state.properties_multiple != self._get_ui_properties_multiple(): + self._update_properties() + + def _on_property_change(self): + properties_multiple = self._get_ui_properties_multiple() + if properties_multiple != self.state.properties_multiple: + with self.state as state: + state.properties_multiple = properties_multiple + wx.PostEvent( + self, MultiplePropertiesChangeEvent(self.state.properties_multiple) + ) diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/events.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/events.py new file mode 100644 index 00000000..93dce040 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/events.py @@ -0,0 +1,19 @@ +import wx +from amulet.api.block import PropertyTypeMultiple + +_MultiplePropertiesChangeEventType = wx.NewEventType() +EVT_MULTIPLE_PROPERTIES_CHANGE = wx.PyEventBinder(_MultiplePropertiesChangeEventType) + + +class MultiplePropertiesChangeEvent(wx.PyCommandEvent): + """ + Run when the properties UI changes. + """ + + def __init__(self, selected_properties: PropertyTypeMultiple): + wx.PyCommandEvent.__init__(self, eventType=_MultiplePropertiesChangeEventType) + self._selected_properties = selected_properties + + @property + def selected_properties(self) -> PropertyTypeMultiple: + return self._selected_properties diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/main.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/main.py new file mode 100644 index 00000000..66ebf1a5 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/main.py @@ -0,0 +1,136 @@ +import wx + +import PyMCTranslate +import amulet_nbt +from amulet_map_editor.api.wx.ui.mc.state import State, BlockState +from amulet_map_editor.api.wx.ui.events import EVT_CHILD_SIZE + +from ..base import BasePropertySelect +from .vanilla import VanillaMultipleProperty +from .modded import ModdedMultipleProperty +from .events import MultiplePropertiesChangeEvent, EVT_MULTIPLE_PROPERTIES_CHANGE + + +class MultiplePropertySelect(BasePropertySelect): + """ + This is a UI which lets the user pick zero or more values for each property. + If the block is known it will be populated from the specification. + If it is not known the user can populate it themselves. + """ + + state: BlockState + + _vanilla: VanillaMultipleProperty + _modded: ModdedMultipleProperty + + def __init__( + self, + parent: wx.Window, + state: BlockState, + ): + BasePropertySelect.__init__( + self, + parent, + state, + ) + + self._vanilla = self._create_automatic() + self._sizer.Add(self._vanilla, 1, wx.EXPAND) + self._child_state_holders.append(self._vanilla) + self._modded = self._create_manual() + self._sizer.Add(self._modded, 1, wx.EXPAND) + self._child_state_holders.append(self._modded) + self._do_show() + + def _create_automatic(self) -> VanillaMultipleProperty: + return VanillaMultipleProperty(self, self.state) + + def _create_manual(self) -> ModdedMultipleProperty: + return ModdedMultipleProperty(self, self.state) + + def _do_show(self): + vanilla = self.state.is_supported + self._vanilla.Show(vanilla) + self._modded.Show(not vanilla) + + def _on_state_change(self): + if self.state.is_changed(State.BaseName): + self._do_show() + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + + def create_dialog(block): + dialog = wx.Dialog( + None, + title=f"MultiplePropertySelect with block {block['namespace']}:{block['base_name']}", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + obj = MultiplePropertySelect.from_data( + dialog, + translation_manager, + platform="java", + version_number=(1, 16, 0), + force_blockstate=False, + **block, + ) + + sizer.Add( + obj, + 1, + wx.ALL, + 5, + ) + + def on_change(evt: MultiplePropertiesChangeEvent): + print(evt.selected_properties) + + def on_close(evt): + dialog.Destroy() + + def on_child_size(evt): + dialog.Layout() + evt.Skip() + + obj.Bind(EVT_MULTIPLE_PROPERTIES_CHANGE, on_change) + dialog.Bind(wx.EVT_CLOSE, on_close) + dialog.Bind(EVT_CHILD_SIZE, on_child_size) + dialog.Show() + dialog.Fit() + + for block_ in ( + { + "namespace": "minecraft", + "base_name": "oak_fence", + "properties_multiple": { + "east": ( + amulet_nbt.TAG_String("true"), + amulet_nbt.TAG_String("false"), + ), + "north": ( + amulet_nbt.TAG_String("true"), + amulet_nbt.TAG_String("false"), + ), + "south": (amulet_nbt.TAG_String("false"),), + "west": (amulet_nbt.TAG_String("true"),), + }, + }, + { + "namespace": "modded", + "base_name": "block", + "properties_multiple": { + "test": ( + amulet_nbt.TAG_String("hello"), + amulet_nbt.TAG_String("hello2"), + ), + }, + }, + ): + create_dialog(block_) diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/modded.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/modded.py new file mode 100644 index 00000000..bc929153 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/modded.py @@ -0,0 +1,39 @@ +import wx +from collections import namedtuple + +from amulet_map_editor.api.wx.ui.mc.state import BlockState +from ..single.modded import BaseModdedSingleProperty +from .events import MultiplePropertiesChangeEvent + +PropertyStorage = namedtuple( + "PropertyStorage", ("sizer", "key_entry", "value_entry", "snbt_text") +) + + +class BaseModdedMultipleProperty(BaseModdedSingleProperty): + """ + A UI from which a user can choose zero or more values for each property. + + This is used when the block is not know so the user can define the properties themselves. + """ + + # TODO: This currently only allows on value to be defined. + state: BlockState + + def _on_property_change(self): + properties = self._get_ui_properties() + if properties != self.state.properties: + with self.state as state: + state.properties = properties + state.properties_multiple = { + key: (val,) for key, val in properties.items() + } + wx.PostEvent( + self, MultiplePropertiesChangeEvent(self.state.properties_multiple) + ) + + +class ModdedMultipleProperty(BaseModdedMultipleProperty): + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + self._rebuild_properties() diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/__init__.py new file mode 100644 index 00000000..80a87a6b --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/__init__.py @@ -0,0 +1 @@ +from .vanilla import VanillaMultipleProperty diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/popup.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/popup.py new file mode 100644 index 00000000..4d4248f0 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/popup.py @@ -0,0 +1,90 @@ +import wx +from typing import Tuple, Iterable, Optional, Sequence + + +class PropertyValueCheckList(wx.Panel): + def __init__( + self, parent: wx.Window, values: Iterable[str], selected: Iterable[bool] + ): + super().__init__(parent) + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + self._toggle_checkbox = wx.CheckBox(self, style=wx.CHK_3STATE) + self._toggle_checkbox.Set3StateValue(wx.CHK_CHECKED) + sizer.Add(self._toggle_checkbox, 0, wx.ALL, 3) + self._check_list_box = wx.CheckListBox(self, choices=values) + self._check_list_box.SetCheckedStrings( + [value for value, select in zip(values, selected) if select] + ) + sizer.Add(self._check_list_box, 0) + + self._toggle_checkbox.Bind(wx.EVT_CHECKBOX, self._on_toggle) + self._check_list_box.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) + + @property + def check_list_box(self) -> wx.CheckListBox: + return self._check_list_box + + def _on_toggle(self, evt): + if self._toggle_checkbox.GetValue(): + self._check_list_box.SetCheckedItems( + list(range(self._check_list_box.GetCount())) + ) + else: + self._check_list_box.SetCheckedItems([]) + + def update_check(self): + items = self._check_list_box.GetCheckedItems() + if len(items) == self._check_list_box.GetCount(): + self._toggle_checkbox.Set3StateValue(wx.CHK_CHECKED) + elif len(items) == 0: + self._toggle_checkbox.Set3StateValue(wx.CHK_UNCHECKED) + else: + self._toggle_checkbox.Set3StateValue(wx.CHK_UNDETERMINED) + + def _on_left_down(self, evt): + # not sure why I need to do this but it works + item = self._check_list_box.HitTest(evt.GetPosition()) + if item >= 0: + self._check_list_box.Check( + item, check=not self._check_list_box.IsChecked(item) + ) + self.update_check() + + +class PropertyValueComboPopup(wx.ComboPopup): + def __init__(self, values: Iterable[str], selected: Iterable[bool]): + super().__init__() + self._check_list: Optional[PropertyValueCheckList] = None + self._state = values, selected + + @property + def _check_list_box(self) -> wx.CheckListBox: + return self._check_list.check_list_box + + def GetCheckedStrings(self) -> Tuple[str]: + return self._check_list_box.GetCheckedStrings() + + def SetCheckedStrings(self, strings: Sequence[str]): + self._check_list_box.SetCheckedStrings(strings) + self._check_list.update_check() + + def GetItems(self) -> Tuple[str]: + return self._check_list_box.GetItems() + + def SetItems(self, strings: Sequence[str]): + self._check_list_box.SetItems(strings) + + def Create(self, parent): + self._check_list = PropertyValueCheckList(parent, *self._state) + return True + + def GetControl(self): + return self._check_list + + def GetStringValue(self): + return "|".join(self.GetCheckedStrings()) + + def GetAdjustedSize(self, minWidth, prefHeight, maxHeight): + self.GetControl().Fit() + return self.GetControl().GetSize() diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/vanilla.py b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/vanilla.py new file mode 100644 index 00000000..056e87be --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/multiple/vanilla/vanilla.py @@ -0,0 +1,101 @@ +import wx +from typing import Dict, Tuple + +import amulet_nbt +from amulet.api.block import PropertyTypeMultiple, PropertyValueType +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.mc.state import BlockState +from ..base import BaseMultipleProperty +from .popup import PropertyValueComboPopup + + +class BaseVanillaMultipleProperty(BaseMultipleProperty): + """ + A UI from which a user can choose zero or more values for each property. + + The UI is automatically populated from the given specification. + """ + + state: BlockState + + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + + header_sizer = wx.BoxSizer(wx.HORIZONTAL) + self._sizer.Add(header_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) + label = wx.StaticText( + self, label=lang.get("widget.mc.block.property.name"), style=wx.ALIGN_CENTER + ) + header_sizer.Add(label, 1) + label = wx.StaticText( + self, + label=lang.get("widget.mc.block.property.value"), + style=wx.ALIGN_CENTER, + ) + header_sizer.Add(label, 1, wx.LEFT, 5) + self._property_sizer = wx.GridSizer(2, 5, 5) + self._sizer.Add(self._property_sizer, 0, wx.ALL | wx.EXPAND, 5) + + self._states: PropertyTypeMultiple = {} + self._properties: Dict[str, Tuple[wx.ComboCtrl, PropertyValueComboPopup]] = {} + + def _rebuild_properties(self): + self._tear_down_properties() + valid_properties = self.state.valid_properties + current_properties = self.state.properties_multiple + for name in valid_properties: + self._create_property( + name, valid_properties[name], current_properties[name] + ) + self.Fit() + self.Layout() + + def _tear_down_properties(self): + self._properties.clear() + self._property_sizer.Clear(True) + + def _create_property( + self, + name: str, + choices: Tuple[PropertyValueType, ...], + selected: Tuple[PropertyValueType, ...] = None, + ): + label = wx.StaticText(self, label=name) + self._property_sizer.Add(label, 0, wx.ALIGN_CENTER) + + choice = wx.ComboCtrl(self, style=wx.CB_READONLY) + if selected is None: + selected = [bool] * len(choices) + else: + selected = [c in choices for c in selected] + popup = PropertyValueComboPopup([c.to_snbt() for c in choices], selected) + choice.SetPopupControl(popup) + choice.SetValue(popup.GetStringValue()) + self._property_sizer.Add(choice, 0, wx.EXPAND) + + def on_closeup(evt): + choice.SetValue(popup.GetStringValue()) + self._on_property_change() + + choice.Bind(wx.EVT_COMBOBOX_CLOSEUP, on_closeup) + self._properties[name] = (choice, popup) + + def _update_properties(self): + for name, choices in self.state.properties_multiple.items(): + property_ui = self._properties[name][1] + property_ui.SetCheckedStrings([c.to_snbt() for c in choices]) + + def _get_ui_properties_multiple(self) -> PropertyTypeMultiple: + return { + prop: tuple(amulet_nbt.from_snbt(v) for v in popup.GetCheckedStrings()) + for prop, (_, popup) in self._properties.items() + } + + def _if_do_state_change(self) -> bool: + return self.state.is_supported + + +class VanillaMultipleProperty(BaseVanillaMultipleProperty): + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + self._rebuild_properties() diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/single/__init__.py b/amulet_map_editor/api/wx/ui/mc/block/properties/single/__init__.py new file mode 100644 index 00000000..2928d4b9 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/single/__init__.py @@ -0,0 +1,4 @@ +"""A UI from which a user can chose one value for each property.""" + +from .events import SinglePropertiesChangeEvent, EVT_SINGLE_PROPERTIES_CHANGE +from .main import SinglePropertySelect, demo diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/single/base.py b/amulet_map_editor/api/wx/ui/mc/block/properties/single/base.py new file mode 100644 index 00000000..81b1d9c1 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/single/base.py @@ -0,0 +1,52 @@ +import wx + +from amulet.api.block import PropertyType +from amulet_map_editor.api.wx.ui.mc.state import StateHolder, BlockState, State +from .events import SinglePropertiesChangeEvent + + +class BaseSingleProperty(wx.Panel, StateHolder): + """ + A UI from which a user can choose one value for each property. + + This is base class for both flavours of single property selection UIs. + Subclasses must implement the logic. + """ + + state: BlockState + + def __init__(self, parent: wx.Window, state: BlockState): + StateHolder.__init__(self, state) + wx.Panel.__init__(self, parent) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + def _rebuild_properties(self): + raise NotImplementedError + + def _tear_down_properties(self): + raise NotImplementedError + + def _update_properties(self): + raise NotImplementedError + + def _get_ui_properties(self) -> PropertyType: + raise NotImplementedError + + def _if_do_state_change(self) -> bool: + raise NotImplementedError + + def _on_state_change(self): + if self._if_do_state_change(): + if self.state.is_changed(State.BaseName): + self._rebuild_properties() + elif self.state.is_changed(State.Properties): + if self.state.properties != self._get_ui_properties(): + self._update_properties() + + def _on_property_change(self): + properties = self._get_ui_properties() + if properties != self.state.properties: + with self.state as state: + state.properties = properties + wx.PostEvent(self, SinglePropertiesChangeEvent(self.state.properties)) diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/single/events.py b/amulet_map_editor/api/wx/ui/mc/block/properties/single/events.py new file mode 100644 index 00000000..9760efc8 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/single/events.py @@ -0,0 +1,19 @@ +import wx +from amulet.api.block import PropertyType + +_SinglePropertiesChangeEventType = wx.NewEventType() +EVT_SINGLE_PROPERTIES_CHANGE = wx.PyEventBinder(_SinglePropertiesChangeEventType) + + +class SinglePropertiesChangeEvent(wx.PyCommandEvent): + """ + Run when the properties UI changes. + """ + + def __init__(self, properties: PropertyType): + wx.PyCommandEvent.__init__(self, eventType=_SinglePropertiesChangeEventType) + self._properties = properties + + @property + def properties(self) -> PropertyType: + return self._properties diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/single/main.py b/amulet_map_editor/api/wx/ui/mc/block/properties/single/main.py new file mode 100644 index 00000000..758e1fcd --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/single/main.py @@ -0,0 +1,118 @@ +import wx + +import PyMCTranslate +import amulet_nbt +from amulet_map_editor.api.wx.ui.mc.state import State, BlockState +from amulet_map_editor.api.wx.ui.events import EVT_CHILD_SIZE + +from ..base import BasePropertySelect +from .vanilla import VanillaSingleProperty +from .modded import ModdedSingleProperty +from .events import SinglePropertiesChangeEvent, EVT_SINGLE_PROPERTIES_CHANGE + + +class SinglePropertySelect(BasePropertySelect): + """ + This is a UI which lets the user pick one value for each property for a given block. + If the block is known it will be populated from the specification. + If it is not known the user can populate it themselves. + """ + + state: BlockState + + _vanilla: VanillaSingleProperty + _modded: ModdedSingleProperty + + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + + self._vanilla = self._create_automatic() + self._sizer.Add(self._vanilla, 0, wx.EXPAND) + self._child_state_holders.append(self._vanilla) + self._modded = self._create_manual() + self._sizer.Add(self._modded, 0, wx.EXPAND) + self._child_state_holders.append(self._modded) + self._do_show() + + def _create_automatic(self) -> VanillaSingleProperty: + return VanillaSingleProperty(self, self.state) + + def _create_manual(self) -> ModdedSingleProperty: + return ModdedSingleProperty(self, self.state) + + def _do_show(self): + vanilla = self.state.is_supported + self._vanilla.Show(vanilla) + self._modded.Show(not vanilla) + + def _on_state_change(self): + if self.state.is_changed(State.BaseName): + self._do_show() + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + + def create_dialog(block): + dialog = wx.Dialog( + None, + title=f"SinglePropertySelect with block {block['namespace']}:{block['base_name']}", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + obj = SinglePropertySelect.from_data( + dialog, + translation_manager, + platform="java", + version_number=(1, 16, 0), + force_blockstate=False, + **block, + ) + sizer.Add( + obj, + 1, + wx.ALL, + 5, + ) + + def on_change(evt: SinglePropertiesChangeEvent): + print(evt.properties) + + def on_close(evt): + dialog.Destroy() + + def on_child_size(evt): + dialog.Layout() + evt.Skip() + + obj.Bind(EVT_SINGLE_PROPERTIES_CHANGE, on_change) + dialog.Bind(wx.EVT_CLOSE, on_close) + dialog.Bind(EVT_CHILD_SIZE, on_child_size) + dialog.Show() + dialog.Fit() + + for block_ in ( + { + "namespace": "minecraft", + "base_name": "oak_fence", + "properties": { + "east": amulet_nbt.TAG_String("false"), + "north": amulet_nbt.TAG_String("true"), + "south": amulet_nbt.TAG_String("false"), + "west": amulet_nbt.TAG_String("false"), + }, + }, + { + "namespace": "modded", + "base_name": "block", + "properties": { + "test": amulet_nbt.TAG_String("hello"), + }, + }, + ): + create_dialog(block_) diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/single/modded.py b/amulet_map_editor/api/wx/ui/mc/block/properties/single/modded.py new file mode 100644 index 00000000..7e3a0b7f --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/single/modded.py @@ -0,0 +1,175 @@ +import wx +from typing import Dict +from collections import namedtuple + +import amulet_nbt +from amulet_nbt import SNBTType +from amulet.api.block import PropertyDataTypes, PropertyType +from amulet_map_editor import lang +from amulet_map_editor.api.image import ADD_ICON, SUBTRACT_ICON +from amulet_map_editor.api.wx.ui.mc.state import BlockState +from amulet_map_editor.api.wx.ui.events import ChildSizeEvent +from .base import BaseSingleProperty + +PropertyStorage = namedtuple( + "PropertyStorage", ("sizer", "key_entry", "value_entry", "snbt_text") +) + + +class BaseModdedSingleProperty(BaseSingleProperty): + """ + A UI from which a user can choose one value for each property. + + This is used when the block is not know so the user can define the properties themselves. + """ + + state: BlockState + _properties: Dict[int, PropertyStorage] + + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + header_sizer = wx.BoxSizer(wx.HORIZONTAL) + add_button = wx.BitmapButton( + self, bitmap=ADD_ICON.bitmap(30, 30), size=(30, 30) + ) + header_sizer.Add(add_button) + self._sizer.Add(header_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) + label = wx.StaticText( + self, label=lang.get("widget.mc.block.property.name"), style=wx.ALIGN_CENTER + ) + header_sizer.Add(label, 1, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) + label = wx.StaticText( + self, + label=lang.get("widget.mc.block.property.value"), + style=wx.ALIGN_CENTER, + ) + header_sizer.Add(label, 1, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) + header_sizer.AddStretchSpacer(1) + + self._property_sizer = wx.BoxSizer(wx.VERTICAL) + self._sizer.Add( + self._property_sizer, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 5 + ) + + add_button.Bind(wx.EVT_BUTTON, self._on_add_property) + + self._property_id = 0 + self._properties = {} + + def _resize(self): + wx.PostEvent(self, ChildSizeEvent(0)) + + def _rebuild_properties(self): + self.Freeze() + self._tear_down_properties() + for name, prop in self.state.properties.items(): + self._create_property(name, prop.to_snbt()) + self.Fit() + self.Thaw() + + def _tear_down_properties(self): + self._property_sizer.Clear(True) + self._properties.clear() + self._property_id = 0 + + def _create_property(self, name: str = "", value: SNBTType = ""): + """ + Add a property to the UI with the given data. + + :param name: The name of the property. + :param value: The SNBT text of the value for that property. + :return: + """ + sizer = wx.BoxSizer(wx.HORIZONTAL) + self._property_id += 1 + subtract_button = wx.BitmapButton( + self, bitmap=SUBTRACT_ICON.bitmap(30, 30), size=(30, 30) + ) + sizer.Add(subtract_button, 0, wx.ALIGN_CENTER_VERTICAL) + property_id = self._property_id + subtract_button.Bind( + wx.EVT_BUTTON, lambda evt: self._on_remove_property(property_id) + ) + name_entry = wx.TextCtrl(self, value=name, style=wx.TE_CENTER) + sizer.Add(name_entry, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + name_entry.Bind(wx.EVT_TEXT, self._on_key_change) + value_entry = wx.TextCtrl(self, value=value, style=wx.TE_CENTER) + sizer.Add(value_entry, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + snbt_text = wx.StaticText(self, style=wx.ALIGN_CENTER) + sizer.Add(snbt_text, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + self._change_snbt_text(value, snbt_text) + value_entry.Bind( + wx.EVT_TEXT, lambda evt: self._on_value_change(evt, property_id) + ) + + self._property_sizer.Add(sizer, 1, wx.TOP | wx.EXPAND, 5) + self._properties[self._property_id] = PropertyStorage( + sizer, name_entry, value_entry, snbt_text + ) + self.Layout() + self._resize() + + def _update_properties(self): + if self.state.properties != self._get_ui_properties(): + self._rebuild_properties() + + def _get_ui_properties(self) -> PropertyType: + properties = {} + for prop in self._properties.values(): + try: + nbt = amulet_nbt.from_snbt(prop.value_entry.GetValue()) + except: + continue + name: str = prop.key_entry.GetValue() + if name and isinstance(nbt, PropertyDataTypes): + properties[name] = nbt + return properties + + def _if_do_state_change(self) -> bool: + return not self.state.is_supported + + def _on_key_change(self, evt): + wx.CallAfter(self._on_property_change) + evt.Skip() + + def _on_value_change(self, evt, property_id: int): + self._change_snbt_text(evt.GetString(), self._properties[property_id].snbt_text) + wx.CallAfter(self._on_property_change) + evt.Skip() + + def _change_snbt_text(self, snbt: SNBTType, snbt_text: wx.StaticText): + try: + nbt = amulet_nbt.from_snbt(snbt) + except: + snbt_text.SetLabel(lang.get("widget.mc.block.property.invalid_snbt")) + snbt_text.SetBackgroundColour((255, 200, 200)) + else: + if isinstance(nbt, PropertyDataTypes): + snbt_text.SetLabel(nbt.to_snbt()) + snbt_text.SetBackgroundColour(wx.NullColour) + else: + snbt_text.SetLabel( + lang.get("widget.mc.block.property.invalid_value_fstring").format( + val=nbt.__class__.__name__ + ) + ) + snbt_text.SetBackgroundColour((255, 200, 200)) + self.Layout() + + def _on_add_property(self, evt): + self._create_property() + self._on_property_change() + + def _on_remove_property(self, property_id: int): + property_state = self._properties.pop(property_id) + self._property_sizer.Detach(property_state.sizer) + property_state.sizer.Clear(True) + self.Layout() + self._resize() + self._on_property_change() + + +class ModdedSingleProperty(BaseModdedSingleProperty): + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + self._rebuild_properties() diff --git a/amulet_map_editor/api/wx/ui/mc/block/properties/single/vanilla.py b/amulet_map_editor/api/wx/ui/mc/block/properties/single/vanilla.py new file mode 100644 index 00000000..9bd1f54a --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/block/properties/single/vanilla.py @@ -0,0 +1,98 @@ +import wx +from typing import Dict, Tuple + +from amulet.api.block import PropertyType, PropertyValueType +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.mc.state import BlockState +from amulet_map_editor.api.wx.ui.simple import ChoiceRaw +from .base import BaseSingleProperty + + +class BaseVanillaSingleProperty(BaseSingleProperty): + """ + A UI from which a user can choose one value for each property. + + The UI is automatically populated from the given specification. + """ + + state: BlockState + + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + + self._property_sizer = wx.FlexGridSizer(2, 5, 5) + label = wx.StaticText( + self, label=lang.get("widget.mc.block.property.name"), style=wx.ALIGN_CENTER + ) + self._property_sizer.Add(label, 1, wx.ALIGN_CENTER) + label = wx.StaticText( + self, + label=lang.get("widget.mc.block.property.value"), + style=wx.ALIGN_CENTER, + ) + self._property_sizer.Add(label, 1, wx.ALIGN_CENTER) + + self._sizer.Add(self._property_sizer, 1, wx.ALL | wx.EXPAND, 5) + + self._properties: Dict[str, ChoiceRaw] = {} + + def _rebuild_properties(self): + self._tear_down_properties() + valid_properties = self.state.valid_properties + current_properties = self.state.properties + for name in valid_properties: + self._create_property( + name, valid_properties[name], current_properties[name] + ) + self.Fit() + self.Layout() + + def _tear_down_properties(self): + self._properties.clear() + child: wx.SizerItem + for i, child in enumerate(self._property_sizer.GetChildren()): + if i >= self._property_sizer.GetCols(): + if child.IsWindow(): + child.GetWindow().Destroy() + elif child.IsSizer(): + child.GetSizer().Clear(True) + self._property_sizer.Remove(self._property_sizer.GetCols()) + elif child.IsSpacer(): + self._property_sizer.Remove(self._property_sizer.GetCols()) + else: + raise Exception + + def _create_property( + self, + name: str, + choices: Tuple[PropertyValueType, ...], + default: PropertyValueType = None, + ): + label = wx.StaticText(self, label=name) + self._property_sizer.Add(label, 0, wx.ALIGN_CENTER) + choice = ChoiceRaw(self, choices=choices, default=default) + self._property_sizer.Add(choice, 0, wx.EXPAND) + choice.Bind( + wx.EVT_CHOICE, + lambda evt: self._on_property_change(), + ) + self._properties[name] = choice + + def _update_properties(self): + for name, nbt in self.state.properties.items(): + property_ui = self._properties[name] + property_ui.SetObject(nbt) + + def _get_ui_properties(self) -> PropertyType: + return { + name: choice.GetCurrentObject() for name, choice in self._properties.items() + } + + def _if_do_state_change(self) -> bool: + return self.state.is_supported + + +class VanillaSingleProperty(BaseVanillaSingleProperty): + def __init__(self, parent: wx.Window, state: BlockState): + super().__init__(parent, state) + self._rebuild_properties() diff --git a/amulet_map_editor/api/wx/ui/mc/demo.py b/amulet_map_editor/api/wx/ui/mc/demo.py new file mode 100644 index 00000000..21552ff1 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/demo.py @@ -0,0 +1,22 @@ +from amulet_map_editor.api.wx.ui.mc.version.demo import demo as version_select_demo +from amulet_map_editor.api.wx.ui.mc.biome.demo import demo as biome_demo +from amulet_map_editor.api.wx.ui.mc.block.demo import demo as block_demo + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + version_select_demo() + biome_demo() + block_demo() + + +if __name__ == "__main__": + + import wx + + app = wx.App() + demo() + app.MainLoop() diff --git a/amulet_map_editor/api/wx/ui/mc/state.py b/amulet_map_editor/api/wx/ui/mc/state.py new file mode 100644 index 00000000..fc005715 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/state.py @@ -0,0 +1,658 @@ +from abc import ABC, abstractmethod +from enum import Enum +from typing import Callable, List, Dict, Any, Union, Tuple, Optional +import copy + +from PyMCTranslate import TranslationManager, Version +from PyMCTranslate.py3.api.version.translators.block import BlockSpecification +from amulet.api.data_types import PlatformType, VersionNumberTuple, VersionNumberAny +from amulet.api.block import ( + PropertyType, + PropertyTypeMultiple, + Block, + PropertyDataTypes, +) +from amulet_map_editor import log + + +OnChangeType = Callable[[int], None] + + +class StrEnum: + def __str__(self): + return self.value + + def __eq__(self, other): + if isinstance(other, str): + return self.value == other + super().__eq__(other) + + def __hash__(self): + return hash(self.value) + + +class State(StrEnum, Enum): + Platform = "platform" + VersionNumber = "version_number" + ForceBlockstate = "force_blockstate" + Namespace = "namespace" + BaseName = "base_name" + Properties = "properties" + PropertiesMultiple = "properties_multiple" + ValidProperties = "valid_properties" + + +class BaseState(ABC): + _translation_manager: TranslationManager + _edit: bool + _state: Dict[StrEnum, Any] + _changed_state: Dict[StrEnum, Any] + _on_change: List[OnChangeType] + + def __init__(self, translation_manager: TranslationManager, **kwargs): + self._translation_manager = translation_manager + self._edit = False # Is the instance being edited + self._state = {} # The actual state + self._changed_state = {} # Temporary storage that new states are written to + self._on_change = [] # Functions to call to notify of any changes. + + def __enter__(self): + assert ( + not self._edit + ), "State is already being set. Release the state before editing again." + self._edit = True + self._changed_state = {} + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._edit = False + self._fix_new_state() + self._state.update(self._changed_state) + for on_change in self._on_change: + try: + on_change() + except: + log.warning(f"Error calling {on_change}", exc_info=True) + + def is_changed(self, state: Union[StrEnum, str]): + """Check if the state has changed.""" + return state in self._changed_state + + def _get_state(self, state: StrEnum) -> Any: + if state in self._changed_state: + return self._changed_state[state] + else: + return self._state[state] + + def _set_state(self, state: StrEnum, value: Any): + if not self._edit: + raise Exception("The state has not been opened for editing.") + self._changed_state[state] = value + + @abstractmethod + def _fix_new_state(self): + """ + The new state may have only been partially set. + Update the new state so that when merged with the old state is fully valid. + Called when released in __exit__ + """ + raise NotImplementedError + + def bind_on_change(self, on_change: OnChangeType): + self._on_change.append(on_change) + + def unbind_on_change(self, on_change: OnChangeType): + while on_change in self._on_change: + self._on_change.remove(on_change) + + @property + def translation_manager(self) -> TranslationManager: + return self._translation_manager + + def copy(self): + new_state = self.__class__(self.translation_manager) + new_state._state = copy.deepcopy(self._state) + new_state._changed_state = copy.deepcopy(self._state) + return new_state + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, memodict=None): + return self.copy() + + +class StateHolder: + # The state stored for this instance. + _state: BaseState + # A list of child states. When the state changes these will be updated + _child_state_holders: List["StateHolder"] + + def __init__(self, state: BaseState): + assert isinstance(state, BaseState) + self._state = state + self._child_state_holders = [] + self._state.bind_on_change(self._on_state_change) + + @property + def state(self): + return self._state + + @state.setter + def state(self, state: BaseState): + if self._state is not None: + self._state.unbind_on_change(self._on_state_change) + self._state = state + if state is not None: + self._state.bind_on_change(self._on_state_change) + self._on_state_change() + for child in self._child_state_holders: + child.state = state + + def _on_state_change(self): + pass + + +class PlatformState(BaseState): + def __init__( + self, + translation_manager: TranslationManager, + *, + platform: str = None, + allow_universal: bool = False, + allow_vanilla: bool = True, + platform_key: Callable[[str], bool] = None, + **kwargs, + ): + super().__init__(translation_manager, **kwargs) + self._allow_universal = allow_universal + self._allow_vanilla = allow_vanilla + self._platform_key = platform_key + self._state[State.Platform] = self._sanitise_platform(platform) + + def _fix_new_state(self): + if self.is_changed(State.Platform): + self._changed_state[State.Platform] = self._sanitise_platform( + self._changed_state[State.Platform] + ) + + def _sanitise_platform(self, platform: str = None) -> PlatformType: + if platform is not None and platform in self.valid_platforms: + return platform + else: + return self.valid_platforms[0] + + @property + def valid_platforms(self) -> List[PlatformType]: + return [ + p + for p in self._translation_manager.platforms() + if ( + self._allow_universal + if p == "universal" + else (self._platform_key is None or self._platform_key(p)) + ) + ] + + @property + def platform(self) -> PlatformType: + return self._get_state(State.Platform) + + @platform.setter + def platform(self, platform: PlatformType): + self._set_state(State.Platform, platform) + + def copy(self): + new_state = super().copy() + new_state._allow_universal = self._allow_universal + new_state._allow_vanilla = self._allow_vanilla + new_state._platform_key = self._platform_key + return new_state + + +class VersionState(PlatformState): + def __init__( + self, + translation_manager: TranslationManager, + *, + version_number: VersionNumberAny = None, + allow_numerical: bool = True, + allow_blockstate: bool = True, + force_blockstate: bool = None, + **kwargs, + ): + super().__init__(translation_manager, **kwargs) + self._allow_numerical = allow_numerical + self._allow_blockstate = allow_blockstate + self._state[State.VersionNumber] = self._sanitise_version(version_number) + self._state[State.ForceBlockstate] = self._sanitise_force_blockstate( + force_blockstate + ) + + def _fix_new_state(self): + super()._fix_new_state() + if self.is_changed(State.Platform) or self.is_changed(State.VersionNumber): + self._changed_state[State.VersionNumber] = self._sanitise_version( + self.version_number + ) + if self.is_changed(State.VersionNumber) or self.is_changed( + State.ForceBlockstate + ): + self._changed_state[ + State.ForceBlockstate + ] = self._sanitise_force_blockstate(self.force_blockstate) + self._fix_version_change() + + def _fix_version_change(self): + pass + + def _get_version(self) -> Version: + return self._translation_manager.get_version(self.platform, self.version_number) + + def _sanitise_version( + self, version_number: VersionNumberAny = None + ) -> VersionNumberTuple: + if version_number is not None: + if version_number in self.valid_version_numbers: + return version_number + else: + try: + return self._translation_manager.get_version( + self.platform, version_number + ).version_number + except KeyError: + pass + return self.valid_version_numbers[-1] + + @property + def valid_version_numbers(self) -> List[VersionNumberTuple]: + return [ + v + for v in self._translation_manager.version_numbers(self.platform) + if (v >= (1, 13, 0) and self._allow_blockstate) + or (v < (1, 13, 0) and self._allow_numerical) + ] + + @property + def version_number(self) -> VersionNumberTuple: + return self._get_state(State.VersionNumber) + + @version_number.setter + def version_number(self, version_number: VersionNumberAny): + self._set_state(State.VersionNumber, version_number) + + def _sanitise_force_blockstate(self, force_blockstate: bool = None) -> bool: + if self.has_abstract_format: + return bool(force_blockstate) + else: + return False + + @property + def has_abstract_format(self) -> bool: + return self._get_version().has_abstract_format + + @property + def force_blockstate(self) -> bool: + return self._get_state(State.ForceBlockstate) + + @force_blockstate.setter + def force_blockstate(self, force_blockstate: bool): + self._set_state(State.ForceBlockstate, force_blockstate) + + def copy(self): + new_state = super().copy() + new_state._allow_numerical = self._allow_numerical + new_state._allow_blockstate = self._allow_blockstate + return new_state + + +class BaseNamespaceState(VersionState): + def __init__( + self, + translation_manager: TranslationManager, + *, + namespace: str = None, + **kwargs, + ): + super().__init__(translation_manager, **kwargs) + self._state[State.Namespace] = self._sanitise_namespace(namespace) + + def _fix_new_state(self): + super()._fix_new_state() + if self.is_changed(State.ForceBlockstate) or self.is_changed(State.Namespace): + self._changed_state[State.Namespace] = self._sanitise_namespace( + self.namespace + ) + + def _sanitise_namespace(self, namespace: str = None) -> str: + if isinstance(namespace, str) and namespace: + return namespace + else: + return self.valid_namespaces[0] + + @property + @abstractmethod + def valid_namespaces(self) -> List[str]: + raise NotImplementedError + + @property + def namespace(self) -> str: + return self._get_state(State.Namespace) + + @namespace.setter + def namespace(self, namespace: str): + self._set_state(State.Namespace, namespace) + + +class BaseResourceIDState(BaseNamespaceState): + def __init__( + self, + translation_manager: TranslationManager, + *, + base_name: str = None, + **kwargs, + ): + super().__init__(translation_manager, **kwargs) + self._state[State.BaseName] = self._sanitise_base_name(base_name) + + def _fix_new_state(self): + super()._fix_new_state() + if self.is_changed(State.Namespace) or self.is_changed(State.BaseName): + self._changed_state[State.BaseName] = self._sanitise_base_name( + self.base_name + ) + + def _sanitise_base_name(self, base_name: str = None) -> str: + if isinstance(base_name, str) and base_name: + return base_name + else: + valid = self.valid_base_names + if valid: + return valid[0] + else: + return "" + + @property + @abstractmethod + def valid_base_names(self) -> List[str]: + raise NotImplementedError + + @property + def base_name(self) -> str: + return self._get_state(State.BaseName) + + @base_name.setter + def base_name(self, base_name: str): + self._set_state(State.BaseName, base_name) + + @property + def is_supported(self): + return self.base_name in self.valid_base_names + + +class BiomeNamespaceState(BaseNamespaceState): + @property + def valid_namespaces(self) -> List[str]: + # TODO: make the biome translator similar to the block translator + return list( + set(biome.split(":", 1)[0] for biome in self._get_version().biome.biome_ids) + ) + + +class BiomeResourceIDState(BiomeNamespaceState, BaseResourceIDState): + @property + def valid_base_names(self) -> List[str]: + biomes = [] + for biome in self._get_version().biome.biome_ids: + namespace, base_name = biome.split(":", 1) + if namespace == self.namespace: + biomes.append(base_name) + return biomes + + def _fix_version_change(self): + if not self.is_changed(State.Namespace) or self.is_changed(State.BaseName): + universal_biome = self._translation_manager.get_version( + self._state[State.Platform], self._state[State.VersionNumber] + ).biome.to_universal(f"{self.namespace}:{self.base_name}") + version_biome = self._translation_manager.get_version( + self.platform, self.version_number + ).biome.from_universal(universal_biome) + namespace, base_name = version_biome.split(":") + self._changed_state[State.Namespace] = self._sanitise_namespace(namespace) + self._changed_state[State.BaseName] = self._sanitise_base_name(base_name) + + +class BlockNamespaceState(BaseNamespaceState): + @property + def valid_namespaces(self) -> List[str]: + return self._get_version().block.namespaces(self.force_blockstate) + + +class BlockResourceIDState(BlockNamespaceState, BaseResourceIDState): + @property + def valid_base_names(self) -> List[str]: + return self._get_version().block.base_names( + self.namespace, self.force_blockstate + ) + + +class BlockState(BlockResourceIDState): + """ + A class to store the state of a block id and properties. + Supports a single value per property and a sequence of values. + """ + + def __init__( + self, + translation_manager: TranslationManager, + *, + properties: PropertyType = None, + properties_multiple: PropertyTypeMultiple = None, + valid_properties: PropertyTypeMultiple = None, + **kwargs, + ): + super().__init__(translation_manager, **kwargs) + ( + self._state[State.Properties], + self._state[State.PropertiesMultiple], + self._state[State.ValidProperties], + ) = self._sync_properties( + self._sanitise_properties(properties), + self._sanitise_properties_multiple(properties_multiple), + self._sanitise_properties_multiple(valid_properties), + ) + + def _fix_version_change(self): + universal_block, _, _ = self._translation_manager.get_version( + self._state[State.Platform], self._state[State.VersionNumber] + ).block.to_universal( + Block(self.namespace, self.base_name, self.properties), + force_blockstate=self.force_blockstate, + ) + version_block, _, _ = self._translation_manager.get_version( + self.platform, self.version_number + ).block.from_universal(universal_block) + if isinstance(version_block, Block): + version_block: Block + if self.is_changed(State.Namespace) or self.is_changed(State.BaseName): + if ( + version_block.namespace == self.namespace + and version_block.base_name == self.base_name + ): + if not self.is_changed(State.Properties): + self._changed_state[ + State.Properties + ] = self._sanitise_properties(version_block.properties) + else: + self._changed_state[State.Namespace] = self._sanitise_namespace( + version_block.namespace + ) + self._changed_state[State.BaseName] = self._sanitise_base_name( + version_block.base_name + ) + if not self.is_changed(State.Properties): + self._changed_state[State.Properties] = self._sanitise_properties( + version_block.properties + ) + + def _fix_new_state(self): + super()._fix_new_state() + validate = self.is_changed(State.BaseName) + if self.is_changed(State.Properties): + self._changed_state[State.Properties] = self._sanitise_properties( + self.properties + ) + validate = True + if self.is_changed(State.PropertiesMultiple): + self._changed_state[ + State.PropertiesMultiple + ] = self._sanitise_properties_multiple(self.properties_multiple) + validate = True + if self.is_changed(State.ValidProperties) and self.is_supported: + self._changed_state[ + State.ValidProperties + ] = self._sanitise_properties_multiple(self.valid_properties) + + if validate: + ( + self._changed_state[State.Properties], + self._changed_state[State.PropertiesMultiple], + self._changed_state[State.ValidProperties], + ) = self._sync_properties() + + def _get_block_spec(self): + if self.is_supported: + return self._get_version().block.get_specification( + self.namespace, self.base_name, self.force_blockstate + ) + else: + return BlockSpecification({}) + + @property + def default_properties(self) -> PropertyType: + """The default properties for this block.""" + if self.is_supported: + return self._get_block_spec().default_properties + else: + return {} + + @property + def valid_properties(self) -> PropertyTypeMultiple: + """The properties that are valid for this block.""" + if self.is_supported: + return self._get_block_spec().valid_properties + else: + return self._get_state(State.ValidProperties) + + @valid_properties.setter + def valid_properties(self, valid_properties: PropertyTypeMultiple): + self._set_state(State.ValidProperties, valid_properties) + + @staticmethod + def _sanitise_properties(properties: PropertyType = None) -> PropertyType: + if isinstance(properties, dict): + return { + key: val + for key, val in properties.items() + if isinstance(val, PropertyDataTypes) + } + else: + return {} + + def _sync_properties( + self, + properties: PropertyType = None, + properties_multiple: PropertyTypeMultiple = None, + valid_properties: PropertyTypeMultiple = None, + ) -> Tuple[PropertyType, PropertyTypeMultiple, Optional[PropertyTypeMultiple]]: + """Make sure that all the properties states are in sync.""" + if properties is None: + properties = self.properties + if properties_multiple is None: + properties_multiple = self.properties_multiple + if valid_properties is None or self.is_supported: + valid_properties = self.valid_properties + if not self.is_supported: + # Nothing is known. Populate/extend valid from the filled out. + valid_properties = { + prop: tuple( + set( + valid_properties.get(prop, ()) + + properties_multiple.get(prop, ()) + + ((properties[prop],) if prop in properties else ()) + ) + ) + for prop in set( + list(valid_properties) + + list(properties) + + list(properties_multiple) + ) + } + # Make sure there are valid properties. + valid_properties = { + key: val for key, val in valid_properties.items() if val + } + default_properties = {key: val[0] for key, val in valid_properties.items()} + else: + default_properties = self.default_properties + + # Make sure all the properties are defined and are a subset of valid properties + properties = { + name: properties[name] + if name in properties + and isinstance(properties[name], PropertyDataTypes) + and properties[name] in valid_properties[name] + else default_properties[name] + for name in valid_properties + } + + # Make sure all the multiple properties are defined and are a subset of valid properties + properties_multiple = { + name: tuple( + val + for val in properties_multiple[name] + if isinstance(val, PropertyDataTypes) and val in valid_properties[name] + ) + if name in properties_multiple + and isinstance(properties_multiple[name], (list, tuple)) + else valid_properties[name] + for name in valid_properties + } + if self.is_supported: + return properties, properties_multiple, {} + else: + return properties, properties_multiple, valid_properties + + @property + def properties(self) -> PropertyType: + """The value for each property that is currently active for this block.""" + return self._get_state(State.Properties) + + @properties.setter + def properties(self, properties: PropertyType): + self._set_state(State.Properties, properties) + + @staticmethod + def _sanitise_properties_multiple( + properties: PropertyTypeMultiple = None, + ) -> PropertyTypeMultiple: + if isinstance(properties, dict): + return { + name: tuple( + val + for val in properties[name] + if isinstance(val, PropertyDataTypes) + ) + for name in properties + } + else: + return {} + + @property + def properties_multiple(self) -> PropertyTypeMultiple: + """The values for each property that are currently active for this block.""" + return self._get_state(State.PropertiesMultiple) + + @properties_multiple.setter + def properties_multiple(self, properties_multiple: PropertyTypeMultiple): + self._set_state(State.PropertiesMultiple, properties_multiple) diff --git a/amulet_map_editor/api/wx/ui/mc/version/__init__.py b/amulet_map_editor/api/wx/ui/mc/version/__init__.py new file mode 100644 index 00000000..18eccbe6 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/version/__init__.py @@ -0,0 +1,8 @@ +from .events import ( + PlatformChangeEvent, + EVT_PLATFORM_CHANGE, + VersionChangeEvent, + EVT_VERSION_CHANGE, +) +from .platform_select import PlatformSelect +from .version_select import VersionSelect diff --git a/amulet_map_editor/api/wx/ui/mc/version/demo.py b/amulet_map_editor/api/wx/ui/mc/version/demo.py new file mode 100644 index 00000000..375003b4 --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/version/demo.py @@ -0,0 +1,26 @@ +import wx +from amulet_map_editor.api.wx.ui.mc.version.platform_select import ( + demo as platform_demo, +) +from amulet_map_editor.api.wx.ui.mc.version.version_select import ( + demo as version_demo, +) + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + platform_demo() + version_demo() + + +if __name__ == "__main__": + + def main(): + app = wx.App() + demo() + app.MainLoop() + + main() diff --git a/amulet_map_editor/api/wx/ui/mc/version/events.py b/amulet_map_editor/api/wx/ui/mc/version/events.py new file mode 100644 index 00000000..8099e78d --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/version/events.py @@ -0,0 +1,59 @@ +import wx +from amulet.api.data_types import VersionNumberTuple + +_PlatformEventType = wx.NewEventType() +EVT_PLATFORM_CHANGE = wx.PyEventBinder(_PlatformEventType) + + +class PlatformChangeEvent(wx.PyEvent): + """ + Event run when the platform input is changed. + Is run when the user or code changes the platform. + """ + + def __init__(self, platform: str): + wx.PyEvent.__init__(self, eventType=_PlatformEventType) + self._platform = platform + + @property + def platform(self) -> str: + """The platform that the selection was changed to.""" + return self._platform + + +_VersionChangeEventType = wx.NewEventType() +EVT_VERSION_CHANGE = wx.PyEventBinder(_VersionChangeEventType) + + +class VersionChangeEvent(wx.PyEvent): + """ + Event is run at the same time as :class:`FormatChangeEvent` but holds all the information about the version. + """ + + def __init__( + self, + platform: str, + version_number: VersionNumberTuple, + force_blockstate: bool, + ): + wx.PyEvent.__init__(self, eventType=_VersionChangeEventType) + self._platform = platform + self._version_number = version_number + self._force_blockstate = force_blockstate + + @property + def platform(self) -> str: + """The platform that the selection was changed to.""" + return self._platform + + @property + def version_number(self) -> VersionNumberTuple: + """The version_number that the selection was changed to.""" + return self._version_number + + @property + def force_blockstate(self) -> bool: + """ + True if the format is force blockstate, False otherwise. + """ + return self._force_blockstate diff --git a/amulet_map_editor/api/wx/ui/mc/version/platform_select.py b/amulet_map_editor/api/wx/ui/mc/version/platform_select.py new file mode 100644 index 00000000..e5ba2faa --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/version/platform_select.py @@ -0,0 +1,139 @@ +from amulet_map_editor.api.wx.ui.simple import SimpleChoice +import wx +import PyMCTranslate +from typing import Type, Any + +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.mc.state import PlatformState, StateHolder, State +from .events import PlatformChangeEvent, EVT_PLATFORM_CHANGE + + +class PlatformSelect(wx.Panel, StateHolder): + """ + A UI element that allows you to pick between the platforms in the translator. + """ + + state: PlatformState + + def __init__( + self, + parent: wx.Window, + state: PlatformState, + **kwargs, + ): + """ + Construct a :class:`PlatformSelect` UI. + + :param parent: The parent window. + :param state: optional PlatformSelect instance holding the state of the platform. + """ + # init the state + StateHolder.__init__(self, state) + + # init the panel + wx.Panel.__init__(self, parent, style=wx.BORDER_SIMPLE) + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + self._sizer = wx.FlexGridSizer(2, 5, 5) + sizer.Add(self._sizer, 1, wx.ALL | wx.EXPAND, 5) + + self._sizer.AddGrowableCol(0) + self._sizer.AddGrowableCol(1) + + self._platform_choice: SimpleChoice = self._add_ui_element( + lang.get("widget.mc.platform"), SimpleChoice + ) + self._update_platform() + self._platform_choice.Bind( + wx.EVT_CHOICE, + self._on_platform_change, + ) + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + **kwargs, + ): + """ + :param parent: The parent window. + :param translation_manager: The translation manager to populate from. + """ + return cls( + parent, + PlatformState( + translation_manager, + **kwargs, + ), + ) + + def _add_ui_element( + self, label: str, obj: Type[wx.Control], shown=True, **kwargs + ) -> Any: + text = wx.StaticText(self, label=label, style=wx.ALIGN_CENTER) + self._sizer.Add(text, 0, wx.ALIGN_CENTER) + wx_obj = obj(self, **kwargs) + self._sizer.Add(wx_obj, 0, wx.EXPAND) + if not shown: + text.Hide() + wx_obj.Hide() + return wx_obj + + def _update_platform(self): + """Push the internal platform state to the UI.""" + self._platform_choice.SetItems(self.state.valid_platforms) + self._platform_choice.SetSelection( + self._platform_choice.GetItems().index(self.state.platform) + ) + + def _on_state_change(self): + if self.state.is_changed(State.Platform): + self._update_platform() + + def _post_change(self): + wx.PostEvent(self, PlatformChangeEvent(self.state.platform)) + + def _on_platform_change(self, evt): + """The event run when the platform choice is changed by a user.""" + platform = self._platform_choice.GetCurrentString() + if platform != self.state.platform: + with self.state as state: + state.platform = platform + self._post_change() + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + dialog = wx.Dialog( + None, + title="PlatformSelect", + style=wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT, + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + obj = PlatformSelect.from_data(dialog, translation_manager, platform="java") + sizer.Add( + obj, + 1, + wx.ALL | wx.EXPAND, + 5, + ) + + def set_data(platform: str): + with obj.state as state: + state.platform = platform + + wx.CallLater(1000, set_data, "bedrock") + + def on_change(evt: PlatformChangeEvent): + print(evt.platform) + + obj.Bind(EVT_PLATFORM_CHANGE, on_change) + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + dialog.Show() + dialog.Fit() diff --git a/amulet_map_editor/api/wx/ui/mc/version/version_select.py b/amulet_map_editor/api/wx/ui/mc/version/version_select.py new file mode 100644 index 00000000..5200f71c --- /dev/null +++ b/amulet_map_editor/api/wx/ui/mc/version/version_select.py @@ -0,0 +1,201 @@ +from amulet_map_editor.api.wx.ui.simple import SimpleChoice, ChoiceRaw +import wx +import PyMCTranslate +from typing import Optional + +from amulet.api.data_types import VersionNumberTuple, PlatformType +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.mc.state import VersionState, State +from .platform_select import PlatformSelect +from .events import VersionChangeEvent, EVT_VERSION_CHANGE + + +class VersionSelect(PlatformSelect): + """ + A UI element that allows you to pick between the platforms and versions in the translator. + """ + + state: VersionState + + def __init__( + self, + parent: wx.Window, + state: VersionState, + *, + show_force_blockstate: bool = True, + **kwargs, + ): + """ + Construct a :class:`VersionSelect` UI. + + :param parent: The parent window. + :param state: optional VersionSelect instance holding the state of the platform and version. + """ + super().__init__(parent, state, **kwargs) + + self._version_choice: Optional[ChoiceRaw] = self._add_ui_element( + lang.get("widget.mc.version"), ChoiceRaw, reverse=True, sort=True + ) + self._update_version_number() + self._version_choice.Bind( + wx.EVT_CHOICE, + self._on_version_number_change, + ) + + self._blockstate_choice: Optional[SimpleChoice] = self._add_ui_element( + lang.get("widget.mc.block_format"), + SimpleChoice, + shown=show_force_blockstate, + ) + self._blockstate_choice.SetItems( + [ + lang.get("widget.mc.block_format.native"), + lang.get("widget.mc.block_format.blockstate"), + ] + ) + self._update_force_blockstate() + self._blockstate_choice.Bind(wx.EVT_CHOICE, self._on_blockstate_change) + + @classmethod + def from_data( + cls, + parent: wx.Window, + translation_manager: PyMCTranslate.TranslationManager, + *, + show_force_blockstate: bool = True, + **kwargs, + ): + """ + :param parent: The parent window. + :param translation_manager: The translation manager to populate from. + :param show_force_blockstate: Should the format selection be shown to the user. + """ + return cls( + parent, + VersionState(translation_manager, **kwargs), + show_force_blockstate=show_force_blockstate, + ) + + def _update_version_number(self): + """Push the internal version number state to the UI.""" + self._version_choice.SetItems(self.state.valid_version_numbers) + self._version_choice.SetSelection( + self._version_choice.values.index(self.state.version_number) + ) + + def _update_force_blockstate(self): + """Push the internal block format state to the UI.""" + self._blockstate_choice.Enable(self.state.has_abstract_format) + self._blockstate_choice.SetSelection(int(self.state.force_blockstate)) + + def _on_state_change(self): + super()._on_state_change() + if self.state.is_changed(State.VersionNumber): + self._update_version_number() + if self.state.is_changed(State.ForceBlockstate): + self._update_force_blockstate() + + def _post_change(self): + wx.PostEvent( + self, + VersionChangeEvent( + self.state.platform, + self.state.version_number, + self.state.force_blockstate, + ), + ) + + def _on_version_number_change(self, evt): + version = self._version_choice.GetCurrentObject() + if version != self.state.version_number: + with self.state as state: + state.version_number = version + self._post_change() + + def _on_blockstate_change(self, evt): + force_blockstate = bool(self._version_choice.GetCurrentSelection()) + if force_blockstate != self.state.force_blockstate: + with self.state as state: + state.force_blockstate = force_blockstate + self._post_change() + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + translation_manager = PyMCTranslate.new_translation_manager() + for cls, title in ( + ( + lambda *args: VersionSelect.from_data(*args, show_force_blockstate=False), + "VersionSelect without format choice", + ), + (VersionSelect.from_data, "VersionSelect with format choice"), + ): + dialog = wx.Dialog( + None, title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT + ) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + select: VersionSelect = cls(dialog, translation_manager) + sizer.Add(select, 0, wx.ALL, 5) + dialog.Show() + dialog.Fit() + + def on_change(evt: VersionChangeEvent): + print( + evt.platform, + evt.version_number, + evt.force_blockstate, + ) + + select.Bind(EVT_VERSION_CHANGE, on_change) + + def get_on_close(dialog_): + def on_close(evt): + dialog_.Destroy() + + return on_close + + dialog.Bind(wx.EVT_CLOSE, get_on_close(dialog)) + + def set_version( + obj: VersionSelect, + platform: PlatformType, + version: VersionNumberTuple, + force_blockstate: bool, + ): + with obj.state as state: + state.platform = platform + state.version_number = version + state.force_blockstate = force_blockstate + + interval = 1_000 + + wx.CallLater(interval * 1, set_version, select, "java", (1, 15, 0), False) + wx.CallLater(interval * 2, set_version, select, "java", (1, 17, 0), False) + wx.CallLater(interval * 3, set_version, select, "bedrock", (1, 15, 0), False) + wx.CallLater(interval * 4, set_version, select, "bedrock", (1, 17, 0), False) + + def set_version2( + obj: VersionSelect, + platform: PlatformType, + version: VersionNumberTuple, + force_blockstate: bool, + ): + with obj.state as state: + state.force_blockstate = force_blockstate + state.version_number = version + state.platform = platform + + wx.CallLater(interval * 5, set_version2, select, "java", (1, 15, 0), False) + wx.CallLater(interval * 6, set_version2, select, "java", (1, 17, 0), False) + wx.CallLater(interval * 7, set_version2, select, "bedrock", (1, 15, 0), False) + wx.CallLater(interval * 8, set_version2, select, "bedrock", (1, 17, 0), False) + + def set_platform(obj: VersionSelect, platform: PlatformType): + obj.platform = platform + + wx.CallLater(interval * 9, set_platform, select, "java") + wx.CallLater(interval * 10, set_platform, select, "bedrock") diff --git a/amulet_map_editor/api/wx/ui/nbt_editor.py b/amulet_map_editor/api/wx/ui/nbt_editor.py index 814b346d..8ce9b9ad 100644 --- a/amulet_map_editor/api/wx/ui/nbt_editor.py +++ b/amulet_map_editor/api/wx/ui/nbt_editor.py @@ -13,8 +13,6 @@ nbt_resources = image.nbt -NBT_FILE = b"\x0A\x00\x0B\x68\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x08\x00\x04\x6E\x61\x6D\x65\x00\x09\x42\x61\x6E\x61\x6E\x72\x61\x6D\x61\x00" - class NBTRadioButton(simple.SimplePanel): def __init__(self, parent, nbt_tag_class, icon): @@ -381,12 +379,23 @@ def save(self, evt): self.Close() -if __name__ == "__main__": - import wx.lib.inspection - - app = wx.App() - wx.lib.inspection.InspectionTool().Show() +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ frame = wx.Frame(None) - NBTEditor(frame, nbt.load(NBT_FILE), callback=lambda nbt_data: print(nbt_data)) + NBTEditor( + frame, + nbt.load( + b"\x0A\x00\x0B\x68\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x08\x00\x04\x6E\x61\x6D\x65\x00\x09\x42\x61\x6E\x61\x6E\x72\x61\x6D\x61\x00" + ), + callback=lambda nbt_data: print(nbt_data), + ) frame.Show() + + +if __name__ == "__main__": + app = wx.App() + demo() app.MainLoop() diff --git a/amulet_map_editor/api/wx/ui/select_world.py b/amulet_map_editor/api/wx/ui/select_world.py index 1b5564d3..e85d1e72 100644 --- a/amulet_map_editor/api/wx/ui/select_world.py +++ b/amulet_map_editor/api/wx/ui/select_world.py @@ -2,7 +2,7 @@ import wx import glob from sys import platform -from typing import List, Dict, Tuple, Callable, TYPE_CHECKING +from typing import List, Dict, Tuple, Callable, TYPE_CHECKING, Optional import traceback from amulet import load_format @@ -57,7 +57,7 @@ def get_world_image(image_path: str) -> Tuple[wx.Bitmap, int]: or world_images[image_path][0] != os.stat(image_path)[8] ): img = wx.Image(image_path, wx.BITMAP_TYPE_ANY) - width = min((img.GetWidth() / img.GetHeight()) * 128, 300) + width = int(min((img.GetWidth() / img.GetHeight()) * 128, 300)) world_images[image_path] = ( os.stat(image_path)[8], @@ -327,17 +327,34 @@ def _update_recent(self, path): self._open_world_callback(path) +WORLD_SELECT_DIALOG_STYLE = ( + wx.DEFAULT_DIALOG_STYLE + | wx.MAXIMIZE_BOX + | wx.TAB_TRAVERSAL + | wx.CLIP_CHILDREN + | wx.RESIZE_BORDER +) + + @preserve_ui_preferences class WorldSelectDialog(wx.Dialog): - def __init__(self, parent: wx.Window, open_world_callback: Callable[[str], None]): + def __init__( + self, + parent: Optional[wx.Window], + open_world_callback: Callable[[str], None], + **kwargs, + ): + if isinstance(parent, wx.Window): + size = wx.Size(*[int(s * 0.95) for s in parent.GetSize()]) + else: + size = wx.DefaultSize super().__init__( parent, title=lang.get("select_world.title"), pos=wx.Point(50, 50), - size=wx.Size(*[int(s * 0.95) for s in parent.GetSize()]), - style=wx.CAPTION | wx.CLOSE_BOX | wx.MAXIMIZE_BOX - # | wx.MAXIMIZE - | wx.SYSTEM_MENU | wx.TAB_TRAVERSAL | wx.CLIP_CHILDREN | wx.RESIZE_BORDER, + size=size, + style=kwargs.pop("style", WORLD_SELECT_DIALOG_STYLE), + **kwargs, ) self.Bind(wx.EVT_CLOSE, self._hide_event) @@ -349,7 +366,8 @@ def _run_callback(self, path): self._open_world_callback(path) def _hide_event(self, evt): - self._close() + if self.IsModal(): + self.EndModal(0) evt.Skip() def _close(self): @@ -357,3 +375,27 @@ def _close(self): self.EndModal(0) else: self.Close() + + +def demo(): + """ + Show a demo version of the UI. + An app instance must be created first. + """ + + def open_world_callback(path): + print(f"Open world {path}") + + dialog = WorldSelectDialog( + None, + open_world_callback, + style=WORLD_SELECT_DIALOG_STYLE | wx.DIALOG_NO_PARENT, + ) + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + dialog.Show() + + +if __name__ == "__main__": + app = wx.App() + demo() + app.MainLoop() diff --git a/amulet_map_editor/api/wx/ui/simple.py b/amulet_map_editor/api/wx/ui/simple.py index 9dec9a28..29726139 100644 --- a/amulet_map_editor/api/wx/ui/simple.py +++ b/amulet_map_editor/api/wx/ui/simple.py @@ -5,8 +5,6 @@ from wx.lib.scrolledpanel import ScrolledPanel from typing import Iterable, Union, Any, List, Optional, Sequence, Dict, Tuple -from amulet_map_editor import log - class SimpleSizer: def __init__(self, sizer_dir=wx.VERTICAL): @@ -40,6 +38,17 @@ def __init__(self, parent: wx.Window, sizer_dir=wx.VERTICAL, **kwargs): self.SetupScrolling() self.SetAutoLayout(1) + def DoGetBestSize(self): + sizer = self.GetSizer() + if sizer is None: + return -1, -1 + else: + sx, sy = sizer.CalcMin() + return ( + sx + wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X), + sy + wx.SystemSettings.GetMetric(wx.SYS_HSCROLL_Y), + ) + class SimpleChoice(wx.Choice): """A wrapper for wx.Choice that sets up the UI for you.""" @@ -62,17 +71,35 @@ def GetCurrentString(self) -> str: StringableType = Any +ChoicesType = Iterable[Union[StringableType, Tuple[Any, str]]] -class SimpleChoiceAny(wx.Choice): - """An extension for wx.Choice that enables showing and returning objects that are not strings.""" +class ChoiceRaw(wx.Choice): + """ + An extension for wx.Choice that enables handling for more than just strings. + The normal wx.Choice class only allows the storage of string objects. + This became an issue when more complex data needed to be displayed in a choice. + This class can be created from an iterable of any object and the result of str(obj) will be displayed to the user. + It can also be given a iterable of tuples containing the object and a string to show to the user. + """ - def __init__(self, parent: wx.Window, sort=True, reverse=False): + def __init__( + self, + parent: wx.Window, + *, + choices: ChoicesType = (), + default: Any = None, + sort=False, + reverse=False + ): super().__init__(parent) self._values: List[Any] = [] # the data hidden behind the string self._keys: List[str] = [] # the strings shown to the user self._sorted = sort self._reverse = reverse + if choices: + self.SetItems(choices) + self.SetObject(default) @property def keys(self) -> Tuple[str, ...]: @@ -89,49 +116,70 @@ def items(self) -> Tuple[Tuple[str, Any], ...]: """Get the string value and the data hidden behind the value""" return tuple(zip(self._keys, self._values)) + def _set_items(self, items: Iterable[Tuple[Any, str]], default: Any = None): + if items: + if self._sorted: + try: + items = sorted(items, key=lambda x: x[0]) + except TypeError: + items = sorted(items, key=lambda x: x[1]) + if self._reverse: + items = reversed(items) + self._values, self._keys = zip(*items) + super().SetItems(self._keys) + if default in self._values: + self.SetSelection(self._values.index(default)) + else: + self.SetSelection(0) + def SetItems( self, - items: Union[Iterable[StringableType], Dict[StringableType, Any]], + items: Union[ + Iterable[Tuple[Any, str]], + Iterable[StringableType], + Dict[StringableType, Any], + ], default: StringableType = None, ): - """Set items. Does not have to be strings. - If items is a dictionary the string of the values are show to the user and the key is returned from GetAny + """ + Set items. Does not have to be strings. + Can be an iterable of any object and the result of str(obj) will be displayed to the user. + If the object is a tuple + It can also be given a iterable of tuples containing the object and a string to show to the user. + + + If items is a dictionary the string of the values are show to the user and the key is returned from GetCurrentObject If it is just an iterable the string of the values are shown and the raw equivalent input is returned.""" - if not items: - return if isinstance(items, dict): - items: List[Tuple[str, Any]] = [ - (str(value), key) for key, value in items.items() - ] - if self._sorted: - items = sorted(items, key=lambda x: x[0], reverse=self._reverse) - self._keys = [key.strip() for key, _ in items] - self._values = [value for _, value in items] - else: - if self._sorted: - self._values = list(sorted(items)) - if self._reverse: - self._values.reverse() - else: - self._values = list(items) - self._keys = [str(v).strip() for v in self._values] - super().SetItems(self._keys) - if default is not None and default in self._values: - self.SetSelection(self._values.index(default)) + items = tuple(items.items()) else: + items_ = [] + for item in items: + if ( + isinstance(item, (tuple, list)) + and len(item) == 2 + and isinstance(item[1], str) + ): + item = (item[0], str(item[1])) + else: + item = (item, str(item)) + items_.append(item) + items = items_ + self._set_items(items, default) + + def SetObject(self, obj: Any): + """Set the selected item from the data hidden behind the text.""" + if obj in self._values: + self.SetSelection(self._values.index(obj)) + elif self.GetCurrentSelection() == wx.NOT_FOUND and self._values: self.SetSelection(0) - def SetValue(self, value: Any): - if value in self._keys: - self.SetSelection(self._keys.index(value)) - - def GetAny(self) -> Optional[Any]: - """Return the value currently selected in the form before it was converted to a string""" - log.warning( - "SimpleChoiceAny.GetAny is being depreciated and will be removed in the future. Please use SimpleChoiceAny.GetCurrentObject instead", - exc_info=True, - ) - return self.GetCurrentObject() + def SetValue(self, key: Any): + """Set the selected item based on the text in the choice.""" + if key in self._keys: + self.SetSelection(self._keys.index(key)) + elif self.GetCurrentSelection() == wx.NOT_FOUND and self._values: + self.SetSelection(0) def GetCurrentObject(self) -> Optional[Any]: """Return the value currently selected in the form before it was converted to a string""" @@ -143,6 +191,9 @@ def GetCurrentString(self) -> str: return self.GetString(self.GetSelection()) +SimpleChoiceAny = ChoiceRaw + + class SimpleDialog(wx.Dialog): """A dialog with ok and cancel buttons set up.""" diff --git a/amulet_map_editor/api/wx/ui/version_select.py b/amulet_map_editor/api/wx/ui/version_select.py deleted file mode 100644 index b975e934..00000000 --- a/amulet_map_editor/api/wx/ui/version_select.py +++ /dev/null @@ -1,254 +0,0 @@ -from amulet_map_editor.api.wx.ui.simple import SimpleChoice, SimpleChoiceAny -import wx -from wx.lib import newevent -import PyMCTranslate -from typing import Tuple, Optional, Type, Any - -from amulet.api.data_types import VersionNumberTuple, PlatformType - -( - PlatformChangeEvent, - EVT_PLATFORM_CHANGE, -) = newevent.NewCommandEvent() # the platform entry changed -( - VersionNumberChangeEvent, - EVT_VERSION_NUMBER_CHANGE, -) = newevent.NewCommandEvent() # the version number entry changed -( - FormatChangeEvent, - EVT_FORMAT_CHANGE, -) = ( - newevent.NewCommandEvent() -) # the format entry changed (is fired even if the entry isn't visible) -( - VersionChangeEvent, - EVT_VERSION_CHANGE, -) = ( - newevent.NewCommandEvent() -) # one of the above changed. Fired after EVT_FORMAT_CHANGE - - -class PlatformSelect(wx.Panel): - def __init__( - self, - parent: wx.Window, - translation_manager: PyMCTranslate.TranslationManager, - platform: PlatformType = None, - allow_universal: bool = True, - allow_vanilla: bool = True, - allowed_platforms: Tuple[PlatformType, ...] = None, - **kwargs - ): - super().__init__(parent, style=wx.BORDER_SIMPLE) - self._sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self._sizer) - - self._translation_manager = translation_manager - self._allow_universal = allow_universal - self._allow_vanilla = allow_vanilla - self._allowed_platforms = allowed_platforms - self._platform_choice: SimpleChoice = self._add_ui_element( - "Platform:", SimpleChoice - ) - self._populate_platform() - self._set_platform(platform) - self._platform_choice.Bind( - wx.EVT_CHOICE, - lambda evt: wx.PostEvent( - self, PlatformChangeEvent(self.GetId(), platform=self.platform) - ), - ) - - def _add_ui_element( - self, label: str, obj: Type[wx.Control], shown=True, **kwargs - ) -> Any: - sizer = wx.BoxSizer(wx.HORIZONTAL) - self._sizer.Add(sizer, 0, wx.EXPAND | wx.ALL, 5) - text = wx.StaticText(self, label=label, style=wx.ALIGN_CENTER) - sizer.Add(text, 1) - wx_obj = obj(self, **kwargs) - sizer.Add(wx_obj, 2) - if not shown: - text.Hide() - wx_obj.Hide() - return wx_obj - - @property - def platform(self) -> PlatformType: - return self._platform_choice.GetCurrentString() - - @platform.setter - def platform(self, platform: PlatformType): - self._set_platform(platform) - wx.PostEvent(self, PlatformChangeEvent(self.GetId(), platform=self.platform)) - - def _set_platform(self, platform: PlatformType): - if platform and platform in self._platform_choice.GetItems(): - self._platform_choice.SetSelection( - self._platform_choice.GetItems().index(platform) - ) - else: - self._platform_choice.SetSelection(0) - - def _populate_platform(self): - platforms = self._translation_manager.platforms() - if self._allowed_platforms is not None: - platforms = [p for p in platforms if p in self._allowed_platforms] - if not self._allow_universal: - platforms = [p for p in platforms if p != "universal"] - if not self._allow_vanilla: - platforms = [p for p in platforms if p == "universal"] - self._platform_choice.SetItems(platforms) - - -class VersionSelect(PlatformSelect): - def __init__( - self, - parent: wx.Window, - translation_manager: PyMCTranslate.TranslationManager, - platform: PlatformType = None, - version_number: VersionNumberTuple = None, - force_blockstate: bool = None, - show_force_blockstate: bool = True, - allow_numerical: bool = True, - allow_blockstate: bool = True, - **kwargs - ): - super().__init__(parent, translation_manager, platform, **kwargs) - self._allow_numerical = allow_numerical - self._allow_blockstate = allow_blockstate - - self.Bind(EVT_PLATFORM_CHANGE, self._on_platform_change) - - self._version_choice: Optional[SimpleChoiceAny] = self._add_ui_element( - "Version:", SimpleChoiceAny, reverse=True - ) - self._populate_version() - self._set_version_number(version_number) - self._version_choice.Bind( - wx.EVT_CHOICE, - lambda evt: wx.PostEvent( - self, - VersionNumberChangeEvent( - self.GetId(), version_number=self.version_number - ), - ), - ) - - self.Bind(EVT_VERSION_NUMBER_CHANGE, self._on_version_number_change) - self._blockstate_choice: Optional[SimpleChoice] = self._add_ui_element( - "Format:", SimpleChoice, shown=show_force_blockstate - ) - self._blockstate_choice.SetItems(["native", "blockstate"]) - self._blockstate_choice.SetSelection(0) - self._populate_blockstate() - self._set_force_blockstate(force_blockstate) - self._blockstate_choice.Bind( - wx.EVT_CHOICE, lambda evt: self._post_version_change() - ) - - def _post_version_change(self): - wx.PostEvent( - self, - FormatChangeEvent(self.GetId(), force_blockstate=self.force_blockstate), - ), - wx.PostEvent( - self, - VersionChangeEvent( - self.GetId(), - platform=self.platform, - version_number=self.version_number, - force_blockstate=self.force_blockstate, - ), - ) - - @property - def version_number(self) -> VersionNumberTuple: - return self._version_choice.GetCurrentObject() - - @version_number.setter - def version_number(self, version_number: VersionNumberTuple): - self._set_version_number(version_number) - wx.PostEvent( - self, - VersionNumberChangeEvent(self.GetId(), version_number=self.version_number), - ) - - def _set_version_number(self, version_number: VersionNumberTuple): - if version_number and version_number in self._version_choice.values: - self._version_choice.SetSelection( - self._version_choice.values.index(version_number) - ) - else: - self._version_choice.SetSelection(0) - - @property - def force_blockstate(self) -> bool: - return self._blockstate_choice.GetCurrentString() == "blockstate" - - @force_blockstate.setter - def force_blockstate(self, force_blockstate: bool): - self._set_force_blockstate(force_blockstate) - self._post_version_change() - - def _set_force_blockstate(self, force_blockstate: bool): - if force_blockstate is not None: - self._blockstate_choice.SetSelection(int(force_blockstate)) - - @property - def version(self) -> Tuple[PlatformType, VersionNumberTuple, bool]: - return self.platform, self.version_number, self.force_blockstate - - @version.setter - def version(self, version: Tuple[PlatformType, VersionNumberTuple, bool]): - platform, version_number, force_blockstate = version - self._set_platform(platform) - self._set_version_number(version_number) - self.force_blockstate = force_blockstate - - def _populate_version(self): - versions = self._translation_manager.version_numbers(self.platform) - if not self._allow_blockstate: - versions = [v for v in versions if v < (1, 13, 0)] - if not self._allow_numerical: - versions = [v for v in versions if v >= (1, 13, 0)] - self._version_choice.SetItems(versions) - - def _populate_blockstate(self): - if self._translation_manager.get_version( - self.platform, self.version_number - ).has_abstract_format: - self._blockstate_choice.Enable() - else: - self._blockstate_choice.Disable() - - def _on_platform_change(self, evt): - self._populate_version() - self.version_number = None - evt.Skip() - - def _on_version_number_change(self, evt): - self._populate_blockstate() - self.force_blockstate = None - evt.Skip() - - -if __name__ == "__main__": - - def main(): - translation_manager = PyMCTranslate.new_translation_manager() - app = wx.App() - for cls in ( - PlatformSelect, - VersionSelect, - lambda *args: VersionSelect(*args, show_force_blockstate=False), - ): - dialog = wx.Dialog(None) - sizer = wx.BoxSizer() - dialog.SetSizer(sizer) - sizer.Add(cls(dialog, translation_manager)) - dialog.Show() - dialog.Fit() - app.MainLoop() - - main() diff --git a/amulet_map_editor/lang/en.lang b/amulet_map_editor/lang/en.lang index 44989d2c..cb550cf9 100644 --- a/amulet_map_editor/lang/en.lang +++ b/amulet_map_editor/lang/en.lang @@ -69,6 +69,22 @@ select_world.recent_worlds=Recently Opened Worlds select_world.no_loader_found=Could not find a loader for this world. select_world.loading_world_failed=Error loading world. + +# Minecraft object widget labels +widget.mc.platform=Platform: +widget.mc.version=Version: +widget.mc.block_format=Format: +widget.mc.block_format.native=native +widget.mc.block_format.blockstate=blockstate +widget.mc.namespace=Namespace: +widget.mc.block.base_name=Block Name: +widget.mc.biome.base_name=Biome Name: +widget.mc.block.property.name=Property Name +widget.mc.block.property.value=Property Value (SNBT) +widget.mc.block.property.invalid_snbt=Invalid SNBT +widget.mc.block.property.invalid_value_fstring={val} not valid + + # About ## The default program when a world is opened program_about.tab_name=About @@ -150,6 +166,15 @@ program_3d_edit.select_tool.button_point2_tooltip=Press and hold this button and program_3d_edit.select_tool.button_selection_box=Move Box program_3d_edit.select_tool.button_selection_box_tooltip=Press and hold this button and use the movement controls to move the active box. +## Fill/Replace tool +program_3d_edit.fill_tool.replace=Replace +program_3d_edit.fill_tool.replace_mode.single=Single +program_3d_edit.fill_tool.replace_mode.sequence=Sequence +program_3d_edit.fill_tool.replace_mode.map=Map +program_3d_edit.fill_tool.swap=▲ Swap ▼ +program_3d_edit.fill_tool.find=Find +program_3d_edit.fill_tool.fill=Fill + ## Paste tool program_3d_edit.paste_tool.location_label=Location program_3d_edit.paste_tool.location_x_label=x diff --git a/amulet_map_editor/programs/edit/api/behaviour/block_selection_behaviour.py b/amulet_map_editor/programs/edit/api/behaviour/block_selection_behaviour.py index 14696d69..7d9d03dd 100644 --- a/amulet_map_editor/programs/edit/api/behaviour/block_selection_behaviour.py +++ b/amulet_map_editor/programs/edit/api/behaviour/block_selection_behaviour.py @@ -299,7 +299,7 @@ def _get_active_points(self) -> Tuple[NPArray2x3, NPArray2x3]: @property def active_block_positions( self, - ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int]]: + ) -> Tuple[BlockCoordinates, BlockCoordinates]: """Get the active box positions. The coordinates for the maximum point of the box will be one less because this is the block position.""" if self._active_selection is None: @@ -310,7 +310,7 @@ def active_block_positions( @active_block_positions.setter def active_block_positions( - self, positions: Tuple[Tuple[int, int, int], Tuple[int, int, int]] + self, positions: Tuple[BlockCoordinates, BlockCoordinates] ): """Set the active box positions. This should only be used when not editing. diff --git a/amulet_map_editor/programs/edit/api/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/api/canvas/base_edit_canvas.py index 0b56a97e..712885f9 100644 --- a/amulet_map_editor/programs/edit/api/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/api/canvas/base_edit_canvas.py @@ -30,6 +30,7 @@ from amulet.api.data_types import OperationYieldType, Dimension from amulet_map_editor import experimental_bedrock_resources +from amulet_map_editor.api.wx.ui.events import EVT_CHILD_SIZE from amulet_map_editor.api.opengl.canvas import EventCanvas from amulet_map_editor.api.opengl.resource_pack.resource_pack import OpenGLResourcePack from amulet_map_editor.programs.edit.api.selection import ( @@ -223,6 +224,7 @@ def bind_events(self): self.buttons.bind_events() self.mouse.bind_events() self.renderer.bind_events() + self.Bind(EVT_CHILD_SIZE, self._do_layout) def enable(self): """Enable the canvas and start it working.""" @@ -284,6 +286,10 @@ def selection(self) -> SelectionManager: """A simple class for storing the selection state.""" return self._selection.value + def _do_layout(self, evt): + self.Layout() + evt.Skip() + def _on_size(self, evt): wx.CallAfter(self._set_size) evt.Skip() diff --git a/amulet_map_editor/programs/edit/api/selection.py b/amulet_map_editor/programs/edit/api/selection.py index c87dbcc4..ea8a53ec 100644 --- a/amulet_map_editor/programs/edit/api/selection.py +++ b/amulet_map_editor/programs/edit/api/selection.py @@ -1,6 +1,7 @@ from typing import Tuple, Optional, Any, TYPE_CHECKING import wx import weakref +from amulet.api.data_types import BlockCoordinates from amulet.api.selection import SelectionGroup, SelectionBox from amulet.api.history.history_manager import ObjectHistoryManager from amulet.api.history import Changeable @@ -10,7 +11,7 @@ if TYPE_CHECKING: from amulet_map_editor.programs.edit.api.canvas import EditCanvas -BoxType = Tuple[Tuple[int, int, int], Tuple[int, int, int]] # min and max positions +BoxType = Tuple[BlockCoordinates, BlockCoordinates] # min and max positions _SelectionChangeEventType = wx.NewEventType() @@ -60,7 +61,7 @@ def _on_destroy(self, evt): @property def selection_corners( self, - ) -> Tuple[Tuple[Tuple[int, int, int], Tuple[int, int, int]], ...]: + ) -> Tuple[BoxType, ...]: """Get the minimum and maximum points of each selection :return: The minimum and maximum points of each selection """ @@ -69,9 +70,7 @@ def selection_corners( @selection_corners.setter def selection_corners( self, - selection_corners: Tuple[ - Tuple[Tuple[int, int, int], Tuple[int, int, int]], ... - ], + selection_corners: Tuple[BoxType, ...], ): """Set the minimum and maximum points of each selection Will create events that allow the program to update. @@ -84,9 +83,7 @@ def selection_corners( def set_selection_corners( self, - selection_corners: Tuple[ - Tuple[Tuple[int, int, int], Tuple[int, int, int]], ... - ], + selection_corners: Tuple[BoxType, ...], ): """Set the minimum and maximum points of each selection Note this method will not trigger the history logic. diff --git a/amulet_map_editor/programs/edit/api/ui/file.py b/amulet_map_editor/programs/edit/api/ui/file.py index 3e76cef0..7e6c9bca 100644 --- a/amulet_map_editor/programs/edit/api/ui/file.py +++ b/amulet_map_editor/programs/edit/api/ui/file.py @@ -4,7 +4,7 @@ from amulet_map_editor.programs.edit.api.edit_canvas_container import ( EditCanvasContainer, ) -from amulet_map_editor.api.wx.ui.simple import SimpleChoiceAny +from amulet_map_editor.api.wx.ui.simple import ChoiceRaw from amulet_map_editor.programs.edit.api.events import ( EVT_CAMERA_MOVED, EVT_UNDO, @@ -56,7 +56,7 @@ def __init__(self, canvas: "EditCanvas"): self.Add(self._location_button, 0, wx.TOP | wx.BOTTOM | wx.RIGHT | wx.CENTER, 5) - self._dim_options = SimpleChoiceAny(canvas) + self._dim_options = ChoiceRaw(canvas) self._dim_options.SetToolTip(lang.get("program_3d_edit.file_ui.dim_tooltip")) self._dim_options.SetItems(level.level_wrapper.dimensions) self._dim_options.SetValue(level.level_wrapper.dimensions[0]) diff --git a/amulet_map_editor/programs/edit/api/ui/nudge_button.py b/amulet_map_editor/programs/edit/api/ui/nudge_button.py index 6ac469dd..31094f8b 100644 --- a/amulet_map_editor/programs/edit/api/ui/nudge_button.py +++ b/amulet_map_editor/programs/edit/api/ui/nudge_button.py @@ -5,6 +5,7 @@ import numpy import math +from amulet.api.data_types import BlockCoordinates from amulet_map_editor.api.opengl.camera import Camera from amulet_map_editor.api.opengl.matrix import rotation_matrix_xy from amulet_map_editor.programs.edit.api.key_config import ( @@ -102,7 +103,7 @@ def _on_held(self, evt: InputHeldEvent): if self._timeout: self._timeout -= 1 - def _rotate(self, offset: Tuple[int, int, int]) -> Tuple[int, int, int]: + def _rotate(self, offset: BlockCoordinates) -> BlockCoordinates: x, y, z = offset ry = self.camera.rotation[0] x, y, z, _ = ( @@ -117,5 +118,5 @@ def _rotate(self, offset: Tuple[int, int, int]) -> Tuple[int, int, int]: ) return x, y, z - def _move(self, offset: Tuple[int, int, int]): + def _move(self, offset: BlockCoordinates): pass diff --git a/amulet_map_editor/programs/edit/api/ui/select_location/select_location.py b/amulet_map_editor/programs/edit/api/ui/select_location/select_location.py index 4a4fa877..94189c90 100644 --- a/amulet_map_editor/programs/edit/api/ui/select_location/select_location.py +++ b/amulet_map_editor/programs/edit/api/ui/select_location/select_location.py @@ -62,7 +62,7 @@ def location(self) -> BlockCoordinates: return self._x.GetValue(), self._y.GetValue(), self._z.GetValue() @location.setter - def location(self, location: Tuple[int, int, int]): + def location(self, location: BlockCoordinates): x, y, z = location self._x.SetValue(x) self._y.SetValue(y) diff --git a/amulet_map_editor/programs/edit/api/ui/tool/base_operation_choice.py b/amulet_map_editor/programs/edit/api/ui/tool/base_operation_choice.py index 9dc0f35f..04c83e78 100644 --- a/amulet_map_editor/programs/edit/api/ui/tool/base_operation_choice.py +++ b/amulet_map_editor/programs/edit/api/ui/tool/base_operation_choice.py @@ -4,7 +4,7 @@ from amulet_map_editor import log from amulet_map_editor.api.image import REFRESH_ICON -from amulet_map_editor.api.wx.ui.simple import SimpleChoiceAny +from amulet_map_editor.api.wx.ui.simple import ChoiceRaw from amulet_map_editor.api.wx.ui.traceback_dialog import TracebackDialog from amulet_map_editor.programs.edit.api.operations import OperationUIType @@ -25,10 +25,16 @@ def __init__(self, canvas: "EditCanvas"): self._active_operation: Optional[OperationUIType] = None self._last_active_operation_id: Optional[str] = None + self._operation_choice: Optional[ChoiceRaw] = None + self._reload_operation: Optional[wx.BitmapButton] = None + self._operations: Optional[UIOperationManager] = None + self._operation_sizer: Optional[wx.BoxSizer] = None + def setup(self): + super().setup() horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL) - self._operation_choice = SimpleChoiceAny(self.canvas) + self._operation_choice = ChoiceRaw(self.canvas) self._reload_operation = wx.BitmapButton( self.canvas, bitmap=REFRESH_ICON.bitmap(16, 16) ) diff --git a/amulet_map_editor/programs/edit/api/ui/tool/base_tool_ui.py b/amulet_map_editor/programs/edit/api/ui/tool/base_tool_ui.py index 469c4610..aa079020 100644 --- a/amulet_map_editor/programs/edit/api/ui/tool/base_tool_ui.py +++ b/amulet_map_editor/programs/edit/api/ui/tool/base_tool_ui.py @@ -1,5 +1,5 @@ import wx -from typing import Union +from typing import Union, TYPE_CHECKING from amulet_map_editor.programs.edit.api.edit_canvas_container import ( EditCanvasContainer, @@ -8,10 +8,29 @@ BaseToolUIType = Union[wx.Window, wx.Sizer, "BaseToolUI"] +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.api.canvas import EditCanvas + class BaseToolUI(EditCanvasContainer, CanvasToggleElement): """The abstract base class for all tools that are to be loaded into the canvas.""" + def __init__(self, canvas: "EditCanvas"): + super().__init__(canvas) + self._is_setup = False + + @property + def is_setup(self) -> bool: + return self._is_setup + + def setup(self): + """ + Behaviour run before showing the tool for the first time. + This is useful so tools only need creating when they are first used. + Reduces the delay when first loading. + """ + self._is_setup = True + @property def name(self) -> str: """The name of the tool.""" diff --git a/amulet_map_editor/programs/edit/api/ui/tool/default_base_tool_ui.py b/amulet_map_editor/programs/edit/api/ui/tool/default_base_tool_ui.py index 1f07c08d..c7f4f46b 100644 --- a/amulet_map_editor/programs/edit/api/ui/tool/default_base_tool_ui.py +++ b/amulet_map_editor/programs/edit/api/ui/tool/default_base_tool_ui.py @@ -1,5 +1,5 @@ import wx -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from OpenGL.GL import ( glClear, GL_DEPTH_BUFFER_BIT, @@ -26,6 +26,10 @@ class DefaultBaseToolUI(BaseToolUI): def __init__(self, canvas: "EditCanvas"): super().__init__(canvas) + self._camera_behaviour: Optional[CameraBehaviour] + + def setup(self): + super().setup() self._camera_behaviour = CameraBehaviour(self.canvas) @property diff --git a/amulet_map_editor/programs/edit/api/ui/tool_manager.py b/amulet_map_editor/programs/edit/api/ui/tool_manager.py index 5a3c6555..a6da68bf 100644 --- a/amulet_map_editor/programs/edit/api/ui/tool_manager.py +++ b/amulet_map_editor/programs/edit/api/ui/tool_manager.py @@ -16,6 +16,7 @@ ExportTool, OperationTool, SelectTool, + FillReplaceTool, ChunkTool, PasteTool, ) @@ -45,6 +46,7 @@ def __init__(self, canvas: "EditCanvas"): self.Add(tool_select_sizer, 0, wx.EXPAND, 0) self.register_tool(SelectTool) + self.register_tool(FillReplaceTool) self.register_tool(PasteTool) self.register_tool(OperationTool) self.register_tool(ImportTool) @@ -110,6 +112,8 @@ def _enable_tool(self, tool: str): self._active_tool.Show() elif isinstance(self._active_tool, wx.Sizer): self._active_tool.ShowItems(show=True) + if not self._active_tool.is_setup: + self._active_tool.setup() self._active_tool.enable() self.canvas.reset_bound_events() self.canvas.Layout() diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/construction.py index da6415ff..38ddb4f7 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/construction.py @@ -7,7 +7,7 @@ from amulet.api.data_types import Dimension, OperationReturnType from amulet.level.formats.construction import ConstructionFormatWrapper -from amulet_map_editor.api.wx.ui.version_select import VersionSelect +from amulet_map_editor.api.wx.ui.mc.version import VersionSelect from amulet_map_editor.programs.edit.api.operations import ( SimpleOperationPanel, OperationError, @@ -37,10 +37,10 @@ def __init__( style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT, ) self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) - self._version_define = VersionSelect( + self._version_define = VersionSelect.from_data( self, world.translation_manager, - options.get("platform", None) or world.level_wrapper.platform, + platform=options.get("platform", None) or world.level_wrapper.platform, allow_universal=False, ) self._sizer.Add(self._version_define, 0, wx.CENTRE, 5) diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/mcstructure.py index e0491f20..96ad62ae 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/mcstructure.py @@ -7,7 +7,7 @@ from amulet.api.data_types import Dimension, OperationReturnType from amulet.level.formats.mcstructure import MCStructureFormatWrapper -from amulet_map_editor.api.wx.ui.version_select import VersionSelect +from amulet_map_editor.api.wx.ui.mc.version import VersionSelect from amulet_map_editor.programs.edit.api.operations import ( SimpleOperationPanel, OperationError, @@ -37,10 +37,10 @@ def __init__( style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT, ) self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) - self._version_define = VersionSelect( + self._version_define = VersionSelect.from_data( self, world.translation_manager, - options.get("platform", None) or world.level_wrapper.platform, + platform=options.get("platform", None) or world.level_wrapper.platform, allowed_platforms=("bedrock",), allow_numerical=False, ) diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/schematic.py index 323f73d9..3b51e832 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/schematic.py @@ -7,7 +7,7 @@ from amulet.api.data_types import Dimension, OperationReturnType from amulet.level.formats.schematic import SchematicFormatWrapper -from amulet_map_editor.api.wx.ui.version_select import PlatformSelect +from amulet_map_editor.api.wx.ui.mc.version import PlatformSelect from amulet_map_editor.programs.edit.api.operations import ( SimpleOperationPanel, OperationError, @@ -48,10 +48,10 @@ def __init__( style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE, ) self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) - self._platform_define = PlatformSelect( + self._platform_define = PlatformSelect.from_data( self, world.translation_manager, - options.get("platform", None) or world.level_wrapper.platform, + platform=options.get("platform", None) or world.level_wrapper.platform, allow_universal=False, ) self._sizer.Add(self._platform_define, 0, wx.CENTRE, 5) diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/sponge_schematic.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/sponge_schematic.py index a4192b6f..c71341d5 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/sponge_schematic.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/export_operations/sponge_schematic.py @@ -7,7 +7,7 @@ from amulet.api.data_types import Dimension, OperationReturnType from amulet.level.formats.sponge_schem import SpongeSchemFormatWrapper -from amulet_map_editor.api.wx.ui.version_select import VersionSelect +from amulet_map_editor.api.wx.ui.mc.version import VersionSelect from amulet_map_editor.programs.edit.api.operations import ( SimpleOperationPanel, OperationError, @@ -37,10 +37,10 @@ def __init__( style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT, ) self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) - self._version_define = VersionSelect( + self._version_define = VersionSelect.from_data( self, world.translation_manager, - options.get("platform", None) or world.level_wrapper.platform, + platform=options.get("platform", None) or world.level_wrapper.platform, allowed_platforms=("java",), allow_numerical=False, ) diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/fill.py index 433e4d18..04f3a93a 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/fill.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/fill.py @@ -3,8 +3,8 @@ from amulet.operations.fill import fill -from amulet_map_editor.api.wx.ui.base_select import EVT_PICK -from amulet_map_editor.api.wx.ui.block_select import BlockDefine +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import EVT_PICK +from amulet_map_editor.api.wx.ui.mc.block import BlockDefine from amulet_map_editor.programs.edit.api.operations import DefaultOperationUI if TYPE_CHECKING: @@ -28,10 +28,10 @@ def __init__( options = self._load_options({}) - self._block_define = BlockDefine( + self._block_define = BlockDefine.from_data( self, world.translation_manager, - wx.VERTICAL, + orientation=wx.VERTICAL, *(options.get("fill_block_options", []) or [world.level_wrapper.platform]), show_pick_block=True ) diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/replace.py index 5d97b2b1..7ead4924 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/replace.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/replace.py @@ -4,8 +4,8 @@ from amulet.api.block import Block -from amulet_map_editor.api.wx.ui.base_select import EVT_PICK -from amulet_map_editor.api.wx.ui.block_select import BlockDefine +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import EVT_PICK +from amulet_map_editor.api.wx.ui.mc.block import BlockDefine from amulet_map_editor.api.wx.ui.simple import SimpleScrollablePanel from amulet_map_editor.programs.edit.api.operations import DefaultOperationUI @@ -27,10 +27,10 @@ def __init__( self.Freeze() options = self._load_options({}) - self._original_block = BlockDefine( + self._original_block = BlockDefine.from_data( self, world.level_wrapper.translation_manager, - wx.VERTICAL, + orientation=wx.VERTICAL, *( options.get("original_block_options", []) or [world.level_wrapper.platform] @@ -40,10 +40,10 @@ def __init__( ) self._sizer.Add(self._original_block, 1, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) self._original_block.Bind(EVT_PICK, lambda evt: self._on_pick_block_button(1)) - self._replacement_block = BlockDefine( + self._replacement_block = BlockDefine.from_data( self, world.level_wrapper.translation_manager, - wx.VERTICAL, + orientation=wx.VERTICAL, *( options.get("replacement_block_options", []) or [world.level_wrapper.platform] @@ -178,17 +178,6 @@ def _replace(self): count += 1 yield count / iter_count - def DoGetBestClientSize(self): - sizer = self.GetSizer() - if sizer is None: - return -1, -1 - else: - sx, sy = self.GetSizer().CalcMin() - return ( - sx + wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X), - sy + wx.SystemSettings.GetMetric(wx.SYS_HSCROLL_Y), - ) - export = { "name": "Replace", # the name of the plugin diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/set_biome.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/set_biome.py index 6c087b97..0f62e1ea 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/set_biome.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/set_biome.py @@ -5,10 +5,10 @@ from amulet.utils import block_coords_to_chunk_coords from amulet.api.chunk.biomes import BiomesShape -from amulet_map_editor.api.wx.ui.base_select import EVT_PICK -from amulet_map_editor.api.wx.ui.biome_select import BiomeDefine +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import EVT_PICK +from amulet_map_editor.api.wx.ui.mc.biome import BiomeDefine from amulet_map_editor.programs.edit.api.operations import SimpleOperationPanel -from amulet_map_editor.api.wx.ui.simple import SimpleChoiceAny +from amulet_map_editor.api.wx.ui.simple import ChoiceRaw if TYPE_CHECKING: from amulet.api.level import BaseLevel @@ -46,7 +46,7 @@ def __init__( self.Freeze() options = self._load_options({}) - self._mode = SimpleChoiceAny(self, sort=False) + self._mode = ChoiceRaw(self) self._mode.SetItems({mode: lang[mode] for mode in MODES.keys()}) self._sizer.Add(self._mode, 0, Border, 5) self._mode.Bind(wx.EVT_CHOICE, self._on_mode_change) diff --git a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/waterlog.py index 24b22ccb..248f5fe6 100644 --- a/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/waterlog.py +++ b/amulet_map_editor/programs/edit/plugins/operations/stock_plugins/operations/waterlog.py @@ -2,9 +2,9 @@ from typing import TYPE_CHECKING, Tuple import wx -from amulet_map_editor.api.wx.ui.base_select import EVT_PICK +from amulet_map_editor.api.wx.ui.mc.base.base_identifier_select import EVT_PICK from amulet_map_editor.api.wx.ui.simple import SimpleDialog -from amulet_map_editor.api.wx.ui.block_select import BlockDefine +from amulet_map_editor.api.wx.ui.mc.block import BlockDefine from amulet_map_editor.programs.edit.api.operations import DefaultOperationUI from amulet_map_editor.api import image @@ -80,10 +80,10 @@ def on_button(evt): ) self._mode_description.Fit() - self._block_define = BlockDefine( + self._block_define = BlockDefine.from_data( self, world.level_wrapper.translation_manager, - wx.VERTICAL, + orientation=wx.VERTICAL, *(options.get("fill_block_options", []) or [world.level_wrapper.platform]), show_pick_block=True ) diff --git a/amulet_map_editor/programs/edit/plugins/tools/__init__.py b/amulet_map_editor/programs/edit/plugins/tools/__init__.py index 104ae379..b492160e 100644 --- a/amulet_map_editor/programs/edit/plugins/tools/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/tools/__init__.py @@ -1,4 +1,5 @@ from .select import SelectTool +from .fill_replace import FillReplaceTool from .operation import OperationTool from .export_tool import ExportTool from .import_tool import ImportTool diff --git a/amulet_map_editor/programs/edit/plugins/tools/chunk.py b/amulet_map_editor/programs/edit/plugins/tools/chunk.py index f3907994..46b1bfa7 100644 --- a/amulet_map_editor/programs/edit/plugins/tools/chunk.py +++ b/amulet_map_editor/programs/edit/plugins/tools/chunk.py @@ -27,10 +27,17 @@ class ChunkTool(wx.BoxSizer, DefaultBaseToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.HORIZONTAL) DefaultBaseToolUI.__init__(self, canvas) + self._selection: Optional[ChunkSelectionBehaviour] = None + self._button_panel: Optional[wx.Panel] = None + self._min_y: Optional[wx.SpinCtrl] = None + self._max_y: Optional[wx.SpinCtrl] = None + self._dimensions: Dict[Dimension, Tuple[int, int]] = {} + def setup(self): + super().setup() self._selection = ChunkSelectionBehaviour(self.canvas) - self._button_panel = wx.Panel(canvas) + self._button_panel = wx.Panel(self.canvas) button_sizer = wx.BoxSizer(wx.VERTICAL) self._button_panel.SetSizer(button_sizer) @@ -63,7 +70,6 @@ def __init__(self, canvas: "EditCanvas"): self._max_y.SetToolTip(lang.get("program_3d_edit.chunk_tool.max_y_tooltip")) y_sizer.Add(self._max_y, flag=wx.ALIGN_CENTER) self._max_y.Bind(wx.EVT_SPINCTRL, self._on_update_clipping) - self._dimensions: Dict[Dimension, Tuple[int, int]] = {} delete_button = wx.Button( self._button_panel, diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/__init__.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/__init__.py new file mode 100644 index 00000000..62263bb7 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/__init__.py @@ -0,0 +1 @@ +from .fill_replace_tool import FillReplaceTool diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/__main__.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/__main__.py new file mode 100644 index 00000000..1f0b19cb --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/__main__.py @@ -0,0 +1,73 @@ +import wx +import PyMCTranslate +from amulet_map_editor.programs.edit.plugins.tools.fill_replace.fill_replace_widget import ( + FillReplaceWidget, +) +from amulet_map_editor.programs.edit.plugins.tools.fill_replace.block_container import ( + SrcBlockState, +) +from amulet_map_editor.api.wx.ui.events import EVT_CHILD_SIZE + +if __name__ == "__main__": + + class FillReplaceTool(wx.BoxSizer): + def __init__(self, canvas): + wx.BoxSizer.__init__(self, wx.VERTICAL) + self._panel = wx.Panel(canvas) + self.Add(self._panel) + panel_sizer = wx.BoxSizer(wx.VERTICAL) + self._panel.SetSizer(panel_sizer) + translation_manager = PyMCTranslate.new_translation_manager() + find_state = SrcBlockState( + translation_manager, + platform="java", + version_number=(1, 16, 0), + namespace="minecraft", + base_name="air", + ) + fill_state = SrcBlockState( + translation_manager, + platform="java", + version_number=(1, 16, 0), + namespace="minecraft", + base_name="stone", + ) + for state_ in (find_state, fill_state): + with state_ as state: + state.platform = "bedrock" + state.version_number = None + + self._operations = FillReplaceWidget(self._panel, find_state, fill_state) + panel_sizer.Add(self._operations, 1, wx.LEFT | wx.TOP, 5) + self._button = wx.Button(self._panel, label="Run Operation") + panel_sizer.Add( + self._button, + 0, + wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM | wx.TOP | wx.FIXED_MINSIZE, + 5, + ) + + def main(): + app = wx.App() + dialog = wx.Dialog(None, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) + sizer = wx.BoxSizer() + dialog.SetSizer(sizer) + cls = FillReplaceTool(dialog) + sizer.Add( + cls, + 1, + wx.ALL, + 5, + ) + dialog.Show() + dialog.Fit() + + def do_layout(evt): + dialog.Layout() + + dialog.Bind(EVT_CHILD_SIZE, do_layout) + dialog.Bind(wx.EVT_CLOSE, lambda evt: dialog.Destroy()) + wx.lib.inspection.InspectionTool().Show() + app.MainLoop() + + main() diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/__init__.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/__init__.py new file mode 100644 index 00000000..c545b58b --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/__init__.py @@ -0,0 +1,4 @@ +from .base import BaseBlockContainer +from .find import FindBlockContainer +from .fill import FillBlockContainer +from .block_entry import SrcBlockState diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/base.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/base.py new file mode 100644 index 00000000..ea0870f7 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/base.py @@ -0,0 +1,110 @@ +from typing import List, Dict, Any +import wx + +from amulet_map_editor.api.wx.ui.events import ChildSizeEvent +from .block_entry import BaseBlockEntry, EVT_BLOCK_CLOSE +from .block_entry.custom_fill_button import SrcBlockState +from amulet_map_editor.api.wx.ui.mc.state import StateHolder + + +class BaseBlockContainer(wx.Panel, StateHolder): + """This is a UI element that contains one or more block buttons.""" + + _blocks: List[BaseBlockEntry] + state: SrcBlockState + + def __init__(self, parent: wx.Window, default_state: SrcBlockState): + StateHolder.__init__(self, default_state) + wx.Panel.__init__(self, parent) + self._expert = False + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + + top_sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(top_sizer, 0, wx.EXPAND, 5) + + self._add_button = wx.Button(self, label="➕") + self._add_button.Bind(wx.EVT_BUTTON, lambda evt: self._add_block()) + self._add_button.SetMinSize((28, 28)) + self._add_button.Show(self._expert) + top_sizer.Add(self._add_button) + + label = wx.StaticText(self, label=self.name, style=wx.ALIGN_CENTER_HORIZONTAL) + top_sizer.Add(label, 1, wx.ALIGN_CENTER_VERTICAL) + + self._block_sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._block_sizer, 0, wx.EXPAND, 0) + + self._blocks = [] + self._add_block() + + def _post_change_size(self): + """Call this when the size changes to notify parent elements.""" + wx.PostEvent(self, ChildSizeEvent(0)) + + @property + def name(self) -> str: + raise NotImplementedError + + def set_expert(self, expert: bool): + self._expert = expert + self._add_button.Show(expert) + for block in self._blocks: + block.show_close(expert) + if not expert: + while len(self._blocks) > 1: + self._do_destroy_block_entry(self._blocks[-1]) + + def _create_block(self) -> BaseBlockEntry: + raise NotImplementedError + + def _add_block(self): + self._do_add_block() + self._post_change_size() + + def _do_add_block(self): + block = self._create_block() + block.Bind(EVT_BLOCK_CLOSE, self._on_block_entry_close) + self._block_sizer.Add(block, 0, wx.EXPAND | wx.TOP, 5) + self._blocks.append(block) + if len(self._blocks) == 1: + # if there is only one block it cannot be removed. + self._blocks[-1].enable_close(False) + else: + if len(self._blocks) == 2: + # if this is the second block that was added enable the first + self._blocks[0].enable_close() + # enable the block that was just added + self._blocks[-1].enable_close() + block.show_close(self._expert) + + def _on_block_entry_close(self, evt): + window = evt.GetEventObject() + if isinstance(window, BaseBlockEntry): + self._destroy_block_entry(window) + + def _destroy_block_entry(self, window: BaseBlockEntry): + self._do_destroy_block_entry(window) + self._post_change_size() + + def _do_destroy_block_entry(self, window: BaseBlockEntry): + if window in self._blocks: + self._blocks.remove(window) + window.Destroy() + if len(self._blocks) == 1: + block = self._blocks[-1] + block.enable_close(False) + + @property + def states(self) -> List[SrcBlockState]: + return [block.state for block in self._blocks] + + @states.setter + def states(self, states: List[SrcBlockState]): + while len(states) > len(self._blocks): + self._do_add_block() + while len(states) < len(self._blocks): + self._do_destroy_block_entry(self._blocks[-1]) + for state, block in zip(states, self._blocks): + block.state = state + self._post_change_size() diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/__init__.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/__init__.py new file mode 100644 index 00000000..f3f241e1 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/__init__.py @@ -0,0 +1,5 @@ +from .events import BlockCloseEvent, EVT_BLOCK_CLOSE +from .base import BaseBlockEntry +from .find import FindBlockEntry +from .fill import FillBlockEntry +from .custom_fill_button import SrcBlockState diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/base.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/base.py new file mode 100644 index 00000000..034ed81e --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/base.py @@ -0,0 +1,51 @@ +import wx +from typing import Optional + +from amulet_map_editor.api.wx.ui.mc.block import BaseBlockDefineButton +from .events import BlockCloseEvent +from .custom_fill_button import SrcBlockState + + +class BaseBlockEntry(wx.Panel): + """A UI element that holds a block button, weight entry and close button""" + + def __init__( + self, + parent: wx.Window, + ): + super().__init__(parent) + self._sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(self._sizer) + self._block_button: Optional[BaseBlockDefineButton] = None + self._close_button: Optional[wx.Button] = None + + def _init_close_button(self): + self._close_button = wx.Button(self, label="❌") + self._close_button.Bind(wx.EVT_BUTTON, self._on_close) + self._close_button.SetMinSize((28, 28)) + self._sizer.Add(self._close_button) + + def _on_close(self, evt): + evt2 = BlockCloseEvent() + evt2.SetEventObject(self) + wx.PostEvent(self, evt2) + + @property + def block_button(self) -> BaseBlockDefineButton: + raise NotImplementedError + + def show_close(self, show: bool = True): + """Show or hide the close button.""" + self._close_button.Show(show) + self.Layout() + + def enable_close(self, enable: bool = True): + self._close_button.Enable(enable) + + @property + def state(self) -> SrcBlockState: + return self.block_button.state + + @state.setter + def state(self, state: SrcBlockState): + self.block_button.state = state diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/custom_fill_button.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/custom_fill_button.py new file mode 100644 index 00000000..c2d5d17c --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/custom_fill_button.py @@ -0,0 +1,200 @@ +import wx +from typing import Dict, Any, Tuple, Optional +from enum import Enum + +from PyMCTranslate import TranslationManager +from amulet.api.block import PropertyValueType +from amulet_map_editor.api.wx.ui.mc.state import BlockState, StrEnum, State +from amulet_map_editor.api.wx.ui.mc.block import ( + BlockDefineButton, + BlockDefine, + SinglePropertySelect, +) +from amulet_map_editor.api.wx.ui.mc.block.properties.single.vanilla import ( + BaseVanillaSingleProperty, +) + + +class StateExtra(StrEnum, Enum): + FromSrc = "from_src" + + +FromSrcType = Dict[str, bool] + + +class SrcBlockState(BlockState): + def __init__( + self, + translation_manager: TranslationManager, + *, + from_source: FromSrcType = None, + **kwargs, + ): + super().__init__(translation_manager, **kwargs) + self._state[StateExtra.FromSrc] = self._sanitise_from_source(from_source) + + def _fix_new_state(self): + super()._fix_new_state() + self._state[StateExtra.FromSrc] = self._sanitise_from_source(self.from_source) + + def _sanitise_from_source(self, from_source: FromSrcType = None) -> FromSrcType: + if not isinstance(from_source, dict): + from_source = {} + return { + key: bool(from_source.get(key, True)) + for key in self.valid_properties.keys() + } + + @property + def from_source(self) -> FromSrcType: + """Should the value for this property be driven by the source block.""" + return self._get_state(StateExtra.FromSrc) + + @from_source.setter + def from_source(self, from_source: FromSrcType): + self._set_state(StateExtra.FromSrc, from_source) + + +class CustomVanillaSingleProperty(BaseVanillaSingleProperty): + """A modification of the normal vanilla property that adds a check box for each property.""" + + state: SrcBlockState + + def __init__( + self, + parent: wx.Window, + state: SrcBlockState, + *, + show_from_source: bool = False, + ): + assert isinstance(state, SrcBlockState) + super().__init__(parent, state) + self._show_from_source = show_from_source + label = wx.StaticText(self, label="src", style=wx.ALIGN_CENTER) + self._property_sizer.SetCols(3) + self._property_sizer.Add(label, 1, wx.ALIGN_CENTER) + label.Show(show_from_source) + self._property_sizer.AddGrowableCol(0) + self._property_sizer.AddGrowableCol(1) + self._check_boxes: Dict[str, wx.CheckBox] = {} + self._rebuild_properties() + + def _tear_down_properties(self): + self._check_boxes.clear() + super()._tear_down_properties() + + def _create_property( + self, + name: str, + choices: Tuple[PropertyValueType, ...], + default: PropertyValueType = None, + ): + super()._create_property(name, choices, default) + check_box = wx.CheckBox(self) + self._property_sizer.Add(check_box, 1, wx.ALIGN_CENTER) + self._check_boxes[name] = check_box + check_box.Bind(wx.EVT_CHECKBOX, lambda evt: self._on_src_change()) + check_box.Show(self._show_from_source) + + def show_from_source(self, show_from_source: bool): + self._show_from_source = show_from_source + for i in range( + 0, self._property_sizer.GetItemCount(), self._property_sizer.GetCols() + ): + self._property_sizer.Show(i + 2, show=show_from_source) + self.Layout() + + def _get_ui_src(self) -> FromSrcType: + return { + key: check_box.GetValue() for key, check_box in self._check_boxes.items() + } + + def _on_src_change(self): + from_source = self._get_ui_src() + if from_source != self.state.from_source: + with self.state as state: + state.from_source = from_source + # wx.PostEvent(self, SinglePropertiesChangeEvent(self.state.from_source)) + + def _on_state_change(self): + super()._on_state_change() + if self.state.is_changed(State.Properties) or self.state.is_changed( + StateExtra.FromSrc + ): + if self.state.from_source != self._get_ui_src(): + for key, val in self.state.from_source.items(): + self._check_boxes[key].SetValue(val) + + +class CustomSinglePropertySelect(SinglePropertySelect): + _vanilla: CustomVanillaSingleProperty + state: SrcBlockState + + def __init__( + self, + parent: wx.Window, + state: SrcBlockState, + *, + show_from_source: bool = False, + ): + self._show_from_source = show_from_source + super().__init__( + parent, + state, + ) + + def _create_automatic(self) -> CustomVanillaSingleProperty: + return CustomVanillaSingleProperty( + self, self.state, show_from_source=self._show_from_source + ) + + def show_from_source(self, show_from_source: bool): + self._vanilla.show_from_source(show_from_source) + + +class CustomBlockDefine(BlockDefine): + _property_picker: CustomSinglePropertySelect + state: SrcBlockState + + def __init__(self, *args, show_from_source: bool = False, **kwargs): + self._show_from_source = show_from_source + super().__init__(*args, **kwargs) + + def _create_property_picker(self) -> SinglePropertySelect: + return CustomSinglePropertySelect( + self, + self.state, + show_from_source=self._show_from_source, + ) + + def show_from_source(self, show_from_source: bool): + self._show_from_source = show_from_source + self._property_picker.show_from_source(show_from_source) + + +class CustomBlockDefineButton(BlockDefineButton): + _block_widget: Optional[CustomBlockDefine] + + def __init__( + self, + parent: wx.Window, + state: BlockState, + *, + show_from_source: bool = False, + **kwargs, + ): + self._show_from_source = show_from_source + super().__init__(parent, state, **kwargs) + + def _create_block_define(self, dialog: wx.Dialog) -> BlockDefine: + return CustomBlockDefine( + dialog, + self.state.copy(), + orientation=wx.HORIZONTAL, + show_from_source=self._show_from_source, + ) + + def show_from_source(self, show_from_source: bool): + self._show_from_source = show_from_source + if self._block_widget is not None: + self._block_widget.show_from_source(show_from_source) diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/events.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/events.py new file mode 100644 index 00000000..7e889230 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/events.py @@ -0,0 +1,3 @@ +from wx.lib import newevent + +BlockCloseEvent, EVT_BLOCK_CLOSE = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/fill.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/fill.py new file mode 100644 index 00000000..c52007cb --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/fill.py @@ -0,0 +1,46 @@ +import wx + +from .base import BaseBlockEntry +from .custom_fill_button import CustomBlockDefineButton, SrcBlockState + + +class FillBlockEntry(BaseBlockEntry): + """A UI element that holds a block button, weight entry and close button""" + + def __init__( + self, + parent: wx.Window, + state: SrcBlockState, + ): + super().__init__(parent) + self._init_close_button() + self._block_button = CustomBlockDefineButton( + self, + state, + max_char_length=40, + ) + self._sizer.Add(self._block_button, 1) + self._weight = wx.SpinCtrl(self, initial=1, min=0, max=100) + self._sizer.Add(self._weight) + self.show_weight(False) + + @property + def block_button(self) -> CustomBlockDefineButton: + return self._block_button + + @property + def weight(self) -> float: + """The weighting value for this entry.""" + return self._weight.GetValue() + + @weight.setter + def weight(self, weight: float): + self._weight.SetValue(weight) + + def show_weight(self, show: bool = True): + """Show or hide the weight entry.""" + self._weight.Show(show) + self.Layout() + + def show_from_source(self, from_source: bool): + self._block_button.show_from_source(from_source) diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/find.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/find.py new file mode 100644 index 00000000..64d5e53c --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/block_entry/find.py @@ -0,0 +1,27 @@ +import wx + +from amulet_map_editor.api.wx.ui.mc.block import WildcardBlockDefineButton +from .base import BaseBlockEntry +from .custom_fill_button import SrcBlockState + + +class FindBlockEntry(BaseBlockEntry): + """A UI element that holds a wildcard block button and close button""" + + def __init__( + self, + parent: wx.Window, + state: SrcBlockState, + ): + super().__init__(parent) + self._init_close_button() + self._block_button = WildcardBlockDefineButton( + self, + state, + max_char_length=40, + ) + self._sizer.Add(self._block_button, 1) + + @property + def block_button(self) -> WildcardBlockDefineButton: + return self._block_button diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/fill.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/fill.py new file mode 100644 index 00000000..bf70aadf --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/fill.py @@ -0,0 +1,40 @@ +from typing import Tuple, List +from amulet_map_editor import lang +from .base import BaseBlockContainer +from .block_entry import FillBlockEntry +from .block_entry.custom_fill_button import SrcBlockState + + +class FillBlockContainer(BaseBlockContainer): + _blocks: List[FillBlockEntry] + state: SrcBlockState + + @property + def name(self) -> str: + return lang.get("program_3d_edit.fill_tool.fill") + + def _do_add_block(self): + super()._do_add_block() + if len(self._blocks) == 2: + for block in self._blocks: + block.show_weight() + elif len(self._blocks) >= 2: + self._blocks[-1].show_weight() + + def _create_block(self) -> FillBlockEntry: + return FillBlockEntry(self, self.state.copy()) + + def _do_destroy_block_entry(self, window: FillBlockEntry): + super()._do_destroy_block_entry(window) + if len(self._blocks) == 1: + block = self._blocks[-1] + block.show_weight(False) + + def show_from_source(self, from_source: bool): + for block in self._blocks: + block.show_from_source(from_source) + + @property + def weights(self) -> Tuple[float, ...]: + """The weighting values for the blocks contained in this widget. May be unused.""" + return tuple(entry.weight for entry in self._blocks) diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/find.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/find.py new file mode 100644 index 00000000..3aa2f7ec --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/block_container/find.py @@ -0,0 +1,15 @@ +from typing import List +from amulet_map_editor import lang +from .base import BaseBlockContainer +from .block_entry import FindBlockEntry + + +class FindBlockContainer(BaseBlockContainer): + _blocks: List[FindBlockEntry] + + @property + def name(self) -> str: + return lang.get("program_3d_edit.fill_tool.find") + + def _create_block(self) -> FindBlockEntry: + return FindBlockEntry(self, self.state) diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/fill_replace_tool.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/fill_replace_tool.py new file mode 100644 index 00000000..839358b9 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/fill_replace_tool.py @@ -0,0 +1,104 @@ +from typing import TYPE_CHECKING, Optional +import wx +from OpenGL.GL import ( + glClear, + GL_DEPTH_BUFFER_BIT, +) + +from amulet_map_editor.api.opengl.camera import Projection +from amulet_map_editor.programs.edit.api.behaviour.block_selection_behaviour import ( + BlockSelectionBehaviour, +) +from amulet_map_editor.programs.edit.api.ui.tool import DefaultBaseToolUI +from .fill_replace_widget import FillReplaceWidget +from .block_container.block_entry.custom_fill_button import SrcBlockState + +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.api.canvas import EditCanvas + +""" +FillReplaceTool(wx.BoxSizer) A sizer containing the FillReplaceWidget. + FillReplaceWidget(wx.Panel) A panel containing mode buttons and one or more fill/replace operations + OperationContainer(wx.lib.scrolledpanel.ScrolledPanel) A ScrolledPanel containing one or more fill/replace operations. + ReplaceOperationWidget + FindBlockContainer + FindBlockEntry + ... + FillBlockContainer + FillBlockEntry + ... + ... +""" + + +class FillReplaceTool(wx.BoxSizer, DefaultBaseToolUI): + """A sizer containing the FillReplaceWidget.""" + + def __init__(self, canvas: "EditCanvas"): + wx.BoxSizer.__init__(self, wx.VERTICAL) + DefaultBaseToolUI.__init__(self, canvas) + self._panel: Optional[wx.Panel] = None + self._operations: Optional[FillReplaceWidget] = None + self._button: Optional[wx.Button] = None + self._selection: Optional[BlockSelectionBehaviour] = None + + def setup(self): + super().setup() + self._panel = wx.Panel(self.canvas) + self.Add(self._panel) + panel_sizer = wx.BoxSizer(wx.VERTICAL) + self._panel.SetSizer(panel_sizer) + + find_state = SrcBlockState( + self.canvas.world.translation_manager, + platform="java", + version_number=(1, 16, 0), + namespace="minecraft", + base_name="air", + ) + fill_state = SrcBlockState( + self.canvas.world.translation_manager, + platform="java", + version_number=(1, 16, 0), + namespace="minecraft", + base_name="stone", + ) + for state_ in (find_state, fill_state): + with state_ as state: + state.platform = self.canvas.world.level_wrapper.platform + state.version_number = self.canvas.world.level_wrapper.version + + self._operations = FillReplaceWidget(self._panel, find_state, fill_state) + panel_sizer.Add(self._operations, 1, wx.LEFT | wx.TOP, 5) + + self._button = wx.Button(self._panel, label="Run Operation") + panel_sizer.Add( + self._button, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM | wx.TOP, 5 + ) + self._button.Bind(wx.EVT_BUTTON, self._operation) + + self._selection = BlockSelectionBehaviour(self.canvas) + + @property + def name(self) -> str: + return "Fill/Replace" + + def bind_events(self): + super().bind_events() + self._selection.bind_events() + + def enable(self): + super().enable() + self._selection.enable() + + def _on_draw(self, evt): + self.canvas.renderer.start_draw() + if self.canvas.camera.projection_mode == Projection.PERSPECTIVE: + self.canvas.renderer.draw_sky_box() + glClear(GL_DEPTH_BUFFER_BIT) + self.canvas.renderer.draw_level() + self._selection.draw() + self.canvas.renderer.end_draw() + + def _operation(self, evt): + pass diff --git a/amulet_map_editor/programs/edit/plugins/tools/fill_replace/fill_replace_widget.py b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/fill_replace_widget.py new file mode 100644 index 00000000..561aedb8 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/tools/fill_replace/fill_replace_widget.py @@ -0,0 +1,205 @@ +from enum import Enum +from typing import Tuple, List +import wx +from wx.lib.scrolledpanel import ScrolledPanel + +from amulet_map_editor import lang +from amulet_map_editor.api.wx.ui.simple import ChoiceRaw +from amulet_map_editor.api.wx.ui.events import ChildSizeEvent +from .block_container import FillBlockContainer, FindBlockContainer +from .block_container.block_entry.custom_fill_button import SrcBlockState + + +class ReplaceMode(Enum): + Single = 0 + Sequence = 1 + Map = 2 + + +class ReplaceOperationWidget(wx.Panel): + """A widget containing a single Fill and optional Find widget.""" + + def __init__( + self, + parent: wx.Window, + default_find_state: SrcBlockState, + default_fill_state: SrcBlockState, + ): + super().__init__(parent, style=wx.BORDER_SIMPLE) + + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + + self._find = FindBlockContainer(self, default_find_state) + self._find.Hide() + sizer.Add(self._find, 0, wx.EXPAND | wx.ALL, 5) + + self._swap_button = wx.Button( + self, wx.ID_ANY, lang.get("program_3d_edit.fill_tool.swap") + ) + self._swap_button.Bind(wx.EVT_BUTTON, self._swap_blocks) + self._swap_button.Hide() + sizer.Add(self._swap_button, 0, wx.EXPAND | wx.ALL, 5) + + self._fill = FillBlockContainer(self, default_fill_state) + sizer.Add(self._fill, 0, wx.EXPAND | wx.ALL, 5) + + def _post_change_size(self): + """Call this to resize and notify parent elements.""" + wx.PostEvent(self, ChildSizeEvent(0)) + + def _swap_blocks(self, evt): + self._find.states, self._fill.states = self._fill.states, self._find.states + self._post_change_size() + + def set_replace(self, replace: bool): + self._find.Show(replace) + self._swap_button.Show(replace) + + def set_expert(self, expert: bool): + self._find.set_expert(expert) + self._fill.set_expert(expert) + + def show_from_source(self, from_source: bool): + self._fill.show_from_source(from_source) + + @property + def operation(self) -> Tuple[List[SrcBlockState], List[SrcBlockState]]: + return self._find.states, self._fill.states + + +class OperationContainer(ScrolledPanel): + """A ScrolledPanel containing one or more fill/replace operations.""" + + def __init__(self, parent: wx.Window): + super().__init__(parent) + self._operations: List[ReplaceOperationWidget] = [] + self._operation_sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._operation_sizer) + self.SetupScrolling(scroll_x=False) + self.FitInside() + + def DoGetBestSize(self): + sizer = self.GetSizer() + if sizer is None: + return -1, -1 + else: + sx, sy = sizer.CalcMin() + return ( + sx + wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X), + sy, + ) + + def __iter__(self): + yield from self._operations + + def add_operation(self, replace_operation: ReplaceOperationWidget): + self._operation_sizer.Add(replace_operation, 0, wx.TOP, 5) + self._operations.append(replace_operation) + + +class FillReplaceWidget(wx.Panel): + """A panel containing mode buttons and one or more fill/replace operations""" + + def __init__( + self, + parent: wx.Window, + default_find_state: SrcBlockState, + default_fill_state: SrcBlockState, + ): + super().__init__(parent) + self._default_find_state = default_find_state + self._default_fill_state = default_fill_state + + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + + top_sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(top_sizer, 0, wx.EXPAND) + + self._replace = wx.CheckBox( + self, wx.ID_ANY, lang.get("program_3d_edit.fill_tool.replace") + ) + self._replace.Bind(wx.EVT_CHECKBOX, self._on_check_change) + top_sizer.Add(self._replace, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + + self._expert = wx.CheckBox(self, wx.ID_ANY, "Expert") + self._expert.Bind(wx.EVT_CHECKBOX, self._on_check_change) + top_sizer.Add(self._expert, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + + self._pull_source = wx.CheckBox(self, wx.ID_ANY, "Pull From Source") + self._pull_source.Hide() + self._pull_source.Bind(wx.EVT_CHECKBOX, self._on_check_change) + top_sizer.Add(self._pull_source, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + + self._multiple = wx.CheckBox(self, wx.ID_ANY, "Multiple") + self._multiple.Hide() + self._multiple.Bind(wx.EVT_CHECKBOX, self._on_check_change) + top_sizer.Add(self._multiple, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5) + + self._replace_mode = ChoiceRaw(self) + self._replace_mode.Hide() + self._replace_mode.SetItems( + { + ReplaceMode.Single: lang.get( + "program_3d_edit.fill_tool.replace_mode.single" + ), + ReplaceMode.Sequence: lang.get( + "program_3d_edit.fill_tool.replace_mode.sequence" + ), + ReplaceMode.Map: lang.get("program_3d_edit.fill_tool.replace_mode.map"), + } + ) + top_sizer.Add(self._replace_mode, 0, wx.LEFT, 5) + + self._operation_panel = OperationContainer(self) + sizer.Add(self._operation_panel, 0) + self._add_operation() + + def _post_change_size(self): + """Call this to resize and notify parent elements.""" + wx.PostEvent(self, ChildSizeEvent(0)) + + def _add_operation(self): + replace_operation = ReplaceOperationWidget( + self._operation_panel, self._default_find_state, self._default_fill_state + ) + replace_operation.set_expert(self.is_expert) + replace_operation.set_replace(self.is_replace) + self._operation_panel.add_operation(replace_operation) + self._operation_panel.FitInside() + + def _update_buttons(self): + self._pull_source.Show(self.is_expert) + # TODO: when multiple support is added uncomment this + # self._multiple.Show(self.is_expert) + # self._replace_mode.Show(self.is_expert and self._multiple.GetValue()) + for operation in self._operation_panel: + operation.set_replace(self.is_replace) + operation.set_expert(self.is_expert) + operation.show_from_source(self.from_source) + self._post_change_size() + + def _on_check_change(self, evt): + self._update_buttons() + + @property + def is_replace(self) -> bool: + """Is the replace check box ticked.""" + return self._replace.GetValue() + + @property + def is_expert(self) -> bool: + return self._expert.GetValue() + + @property + def from_source(self) -> bool: + return self._pull_source.GetValue() + + @property + def replace_mode(self) -> ReplaceMode: + return self._replace_mode.GetCurrentObject() + + @property + def operations(self) -> List[Tuple[List[SrcBlockState], List[SrcBlockState]]]: + return [op.operation for op in self._operation_panel] diff --git a/amulet_map_editor/programs/edit/plugins/tools/import_tool.py b/amulet_map_editor/programs/edit/plugins/tools/import_tool.py index 32eb1d9f..d58d0a95 100644 --- a/amulet_map_editor/programs/edit/plugins/tools/import_tool.py +++ b/amulet_map_editor/programs/edit/plugins/tools/import_tool.py @@ -1,5 +1,5 @@ import wx -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import traceback import amulet @@ -19,10 +19,14 @@ class ImportTool(wx.BoxSizer, DefaultBaseToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.VERTICAL) DefaultBaseToolUI.__init__(self, canvas) + self._selection: Optional[StaticSelectionBehaviour] = None + self._open_file_button: Optional[wx.Button] = None + def setup(self): + super().setup() self._selection = StaticSelectionBehaviour(self.canvas) - self._open_file_button = wx.Button(canvas, label="Import File") + self._open_file_button = wx.Button(self.canvas, label="Import File") self._open_file_button.Bind(wx.EVT_BUTTON, self._on_open_file) self.AddStretchSpacer() self.Add(self._open_file_button, flag=wx.ALL, border=10) diff --git a/amulet_map_editor/programs/edit/plugins/tools/paste.py b/amulet_map_editor/programs/edit/plugins/tools/paste.py index 53161159..99242743 100644 --- a/amulet_map_editor/programs/edit/plugins/tools/paste.py +++ b/amulet_map_editor/programs/edit/plugins/tools/paste.py @@ -1,5 +1,5 @@ import wx -from typing import TYPE_CHECKING, Tuple, Union, Type +from typing import TYPE_CHECKING, Tuple, Union, Type, Optional from OpenGL.GL import ( glClear, GL_DEPTH_BUFFER_BIT, @@ -8,7 +8,7 @@ import numpy import weakref -from amulet.api.data_types import PointCoordinates +from amulet.api.data_types import PointCoordinates, BlockCoordinates from amulet.operations.paste import paste_iter from amulet.utils.matrix import ( rotation_matrix_xyz, @@ -101,11 +101,11 @@ class TupleIntInput(TupleInput): WindowCls = wx.SpinCtrl @property - def value(self) -> Tuple[int, int, int]: + def value(self) -> BlockCoordinates: return self.x.GetValue(), self.y.GetValue(), self.z.GetValue() @value.setter - def value(self, value: Tuple[int, int, int]): + def value(self, value: BlockCoordinates): self.x.SetValue(value[0]) self.y.SetValue(value[1]) self.z.SetValue(value[2]) @@ -233,7 +233,7 @@ def __init__( super().__init__(parent, camera, keybinds, label, tooltip) self._paste_tool = weakref.ref(paste_tool) - def _move(self, offset: Tuple[int, int, int]): + def _move(self, offset: BlockCoordinates): ox, oy, oz = offset x, y, z = self._paste_tool().location self._paste_tool().location = x + ox, y + oy, z + oz @@ -243,13 +243,31 @@ class PasteTool(wx.BoxSizer, DefaultBaseToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.HORIZONTAL) DefaultBaseToolUI.__init__(self, canvas) - - self._selection = StaticSelectionBehaviour(self.canvas) - self._cursor = PointerBehaviour(self.canvas) + self._selection: Optional[StaticSelectionBehaviour] = None + self._cursor: Optional[PointerBehaviour] = None self._moving = False self._is_enabled = False + self._paste_panel: Optional[wx.Panel] = None + self._paste_sizer: Optional[wx.BoxSizer] = None + self._location: Optional[TupleIntInput] = None + self._move_button: Optional[MoveButton] = None + self._free_rotation: Optional[wx.CheckBox] = None + self._rotation: Optional[RotationTupleInput] = None + self._rotate_left_button: Optional[wx.BitmapButton] = None + self._rotate_right_button: Optional[wx.BitmapButton] = None + self._scale: Optional[TupleFloatInput] = None + self._mirror_horizontal_button: Optional[wx.BitmapButton] = None + self._mirror_vertical_button: Optional[wx.BitmapButton] = None + self._copy_air: Optional[wx.CheckBox] = None + self._copy_water: Optional[wx.CheckBox] = None + self._copy_lava: Optional[wx.CheckBox] = None + + def setup(self): + super().setup() + self._selection = StaticSelectionBehaviour(self.canvas) + self._cursor = PointerBehaviour(self.canvas) - self._paste_panel = wx.Panel(canvas) + self._paste_panel = wx.Panel(self.canvas) self._paste_sizer = wx.BoxSizer(wx.VERTICAL) self._paste_panel.SetSizer(self._paste_sizer) self.Add(self._paste_panel, 0, wx.ALIGN_CENTER_VERTICAL) diff --git a/amulet_map_editor/programs/edit/plugins/tools/select.py b/amulet_map_editor/programs/edit/plugins/tools/select.py index f4228b2c..395f7f2a 100644 --- a/amulet_map_editor/programs/edit/plugins/tools/select.py +++ b/amulet_map_editor/programs/edit/plugins/tools/select.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Type, Any, Callable, Tuple +from typing import TYPE_CHECKING, Type, Any, Callable, Optional import wx from OpenGL.GL import ( glClear, @@ -46,21 +46,21 @@ def __init__( class Point1MoveButton(BaseSelectionMoveButton): - def _move(self, offset: Tuple[int, int, int]): + def _move(self, offset: BlockCoordinates): ox, oy, oz = offset (x, y, z), point2 = self._selection.active_block_positions self._selection.active_block_positions = (x + ox, y + oy, z + oz), point2 class Point2MoveButton(BaseSelectionMoveButton): - def _move(self, offset: Tuple[int, int, int]): + def _move(self, offset: BlockCoordinates): ox, oy, oz = offset point1, (x, y, z) = self._selection.active_block_positions self._selection.active_block_positions = point1, (x + ox, y + oy, z + oz) class SelectionMoveButton(BaseSelectionMoveButton): - def _move(self, offset: Tuple[int, int, int]): + def _move(self, offset: BlockCoordinates): ox, oy, oz = offset (x1, y1, z1), (x2, y2, z2) = self._selection.active_block_positions self._selection.active_block_positions = (x1 + ox, y1 + oy, z1 + oz), ( @@ -74,11 +74,28 @@ class SelectTool(wx.BoxSizer, DefaultBaseToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.HORIZONTAL) DefaultBaseToolUI.__init__(self, canvas) - + self._selection: Optional[BlockSelectionBehaviour] = None + self._inspect_block: Optional[InspectBlockBehaviour] = None + self._button_panel: Optional[wx.Panel] = None + self._x1: Optional[wx.SpinCtrl] = None + self._y1: Optional[wx.SpinCtrl] = None + self._z1: Optional[wx.SpinCtrl] = None + self._x2: Optional[wx.SpinCtrl] = None + self._y2: Optional[wx.SpinCtrl] = None + self._z2: Optional[wx.SpinCtrl] = None + self._box_size_selector_fstring: Optional[str] = None + self._box_size_selector_text: Optional[wx.StaticText] = None + self._box_volume_text: Optional[wx.StaticText] = None + self._point1_move: Optional[Point1MoveButton] = None + self._point2_move: Optional[Point2MoveButton] = None + self._selection_move: Optional[SelectionMoveButton] = None + + def setup(self): + super().setup() self._selection = BlockSelectionBehaviour(self.canvas) self._inspect_block = InspectBlockBehaviour(self.canvas, self._selection) - self._button_panel = wx.Panel(canvas) + self._button_panel = wx.Panel(self.canvas) button_sizer = wx.BoxSizer(wx.VERTICAL) self._button_panel.SetSizer(button_sizer)