From 4950fcb70b4225a3f4dfcbd48a206c7819f7d64f Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 13:14:54 +0100 Subject: [PATCH 01/84] Moved operations back into the edit program --- amulet_map_editor/programs/edit/edit.py | 4 ++-- amulet_map_editor/{ => programs/edit}/plugins/__init__.py | 0 .../programs/edit/plugins/export_operations/__init__.py | 0 .../edit}/plugins/export_operations/construction.py | 0 .../edit}/plugins/export_operations/mcstructure.py | 0 .../edit}/plugins/export_operations/schematic.py | 0 .../programs/edit/plugins/import_operations/__init__.py | 0 .../edit}/plugins/import_operations/construction.py | 0 .../edit}/plugins/import_operations/mcstructure.py | 0 .../edit}/plugins/import_operations/schematic.py | 0 .../programs/edit/plugins/internal_operations/__init__.py | 0 .../{ => programs/edit}/plugins/internal_operations/copy.py | 0 .../{ => programs/edit}/plugins/internal_operations/cut.py | 0 .../{ => programs/edit}/plugins/internal_operations/delete.py | 0 .../{ => programs/edit}/plugins/internal_operations/paste.py | 0 .../programs/edit/plugins/operations/__init__.py | 0 .../{ => programs/edit}/plugins/operations/clone.py | 0 .../{ => programs/edit}/plugins/operations/delete_chunk.py | 0 .../{ => programs/edit}/plugins/operations/fill.py | 0 .../{ => programs/edit}/plugins/operations/replace.py | 0 .../{ => programs/edit}/plugins/operations/waterlog.py | 0 .../programs/edit/ui/tool_options/operation/export_tool.py | 2 +- .../programs/edit/ui/tool_options/operation/import_tool.py | 2 +- .../edit/ui/tool_options/operation/select_operation.py | 2 +- 24 files changed, 5 insertions(+), 5 deletions(-) rename amulet_map_editor/{ => programs/edit}/plugins/__init__.py (100%) create mode 100644 amulet_map_editor/programs/edit/plugins/export_operations/__init__.py rename amulet_map_editor/{ => programs/edit}/plugins/export_operations/construction.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/export_operations/mcstructure.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/export_operations/schematic.py (100%) create mode 100644 amulet_map_editor/programs/edit/plugins/import_operations/__init__.py rename amulet_map_editor/{ => programs/edit}/plugins/import_operations/construction.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/import_operations/mcstructure.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/import_operations/schematic.py (100%) create mode 100644 amulet_map_editor/programs/edit/plugins/internal_operations/__init__.py rename amulet_map_editor/{ => programs/edit}/plugins/internal_operations/copy.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/internal_operations/cut.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/internal_operations/delete.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/internal_operations/paste.py (100%) create mode 100644 amulet_map_editor/programs/edit/plugins/operations/__init__.py rename amulet_map_editor/{ => programs/edit}/plugins/operations/clone.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/operations/delete_chunk.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/operations/fill.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/operations/replace.py (100%) rename amulet_map_editor/{ => programs/edit}/plugins/operations/waterlog.py (100%) diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 162b8290..eee9aaac 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -6,13 +6,13 @@ import traceback import os -from amulet.api.selection import SelectionGroup, SelectionBox +from amulet.api.selection import SelectionGroup from amulet.api.structure import Structure from amulet.api.data_types import OperationType, OperationReturnType from amulet_map_editor import log, CONFIG from amulet_map_editor.programs import BaseWorldProgram, MenuData -from amulet_map_editor import plugins +from amulet_map_editor.programs.edit import plugins from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog diff --git a/amulet_map_editor/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py similarity index 100% rename from amulet_map_editor/plugins/__init__.py rename to amulet_map_editor/programs/edit/plugins/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/export_operations/__init__.py b/amulet_map_editor/programs/edit/plugins/export_operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/export_operations/construction.py similarity index 100% rename from amulet_map_editor/plugins/export_operations/construction.py rename to amulet_map_editor/programs/edit/plugins/export_operations/construction.py diff --git a/amulet_map_editor/plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/export_operations/mcstructure.py similarity index 100% rename from amulet_map_editor/plugins/export_operations/mcstructure.py rename to amulet_map_editor/programs/edit/plugins/export_operations/mcstructure.py diff --git a/amulet_map_editor/plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/export_operations/schematic.py similarity index 100% rename from amulet_map_editor/plugins/export_operations/schematic.py rename to amulet_map_editor/programs/edit/plugins/export_operations/schematic.py diff --git a/amulet_map_editor/programs/edit/plugins/import_operations/__init__.py b/amulet_map_editor/programs/edit/plugins/import_operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/plugins/import_operations/construction.py b/amulet_map_editor/programs/edit/plugins/import_operations/construction.py similarity index 100% rename from amulet_map_editor/plugins/import_operations/construction.py rename to amulet_map_editor/programs/edit/plugins/import_operations/construction.py diff --git a/amulet_map_editor/plugins/import_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/import_operations/mcstructure.py similarity index 100% rename from amulet_map_editor/plugins/import_operations/mcstructure.py rename to amulet_map_editor/programs/edit/plugins/import_operations/mcstructure.py diff --git a/amulet_map_editor/plugins/import_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/import_operations/schematic.py similarity index 100% rename from amulet_map_editor/plugins/import_operations/schematic.py rename to amulet_map_editor/programs/edit/plugins/import_operations/schematic.py diff --git a/amulet_map_editor/programs/edit/plugins/internal_operations/__init__.py b/amulet_map_editor/programs/edit/plugins/internal_operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/plugins/internal_operations/copy.py b/amulet_map_editor/programs/edit/plugins/internal_operations/copy.py similarity index 100% rename from amulet_map_editor/plugins/internal_operations/copy.py rename to amulet_map_editor/programs/edit/plugins/internal_operations/copy.py diff --git a/amulet_map_editor/plugins/internal_operations/cut.py b/amulet_map_editor/programs/edit/plugins/internal_operations/cut.py similarity index 100% rename from amulet_map_editor/plugins/internal_operations/cut.py rename to amulet_map_editor/programs/edit/plugins/internal_operations/cut.py diff --git a/amulet_map_editor/plugins/internal_operations/delete.py b/amulet_map_editor/programs/edit/plugins/internal_operations/delete.py similarity index 100% rename from amulet_map_editor/plugins/internal_operations/delete.py rename to amulet_map_editor/programs/edit/plugins/internal_operations/delete.py diff --git a/amulet_map_editor/plugins/internal_operations/paste.py b/amulet_map_editor/programs/edit/plugins/internal_operations/paste.py similarity index 100% rename from amulet_map_editor/plugins/internal_operations/paste.py rename to amulet_map_editor/programs/edit/plugins/internal_operations/paste.py diff --git a/amulet_map_editor/programs/edit/plugins/operations/__init__.py b/amulet_map_editor/programs/edit/plugins/operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/plugins/operations/clone.py b/amulet_map_editor/programs/edit/plugins/operations/clone.py similarity index 100% rename from amulet_map_editor/plugins/operations/clone.py rename to amulet_map_editor/programs/edit/plugins/operations/clone.py diff --git a/amulet_map_editor/plugins/operations/delete_chunk.py b/amulet_map_editor/programs/edit/plugins/operations/delete_chunk.py similarity index 100% rename from amulet_map_editor/plugins/operations/delete_chunk.py rename to amulet_map_editor/programs/edit/plugins/operations/delete_chunk.py diff --git a/amulet_map_editor/plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/operations/fill.py similarity index 100% rename from amulet_map_editor/plugins/operations/fill.py rename to amulet_map_editor/programs/edit/plugins/operations/fill.py diff --git a/amulet_map_editor/plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/operations/replace.py similarity index 100% rename from amulet_map_editor/plugins/operations/replace.py rename to amulet_map_editor/programs/edit/plugins/operations/replace.py diff --git a/amulet_map_editor/plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/operations/waterlog.py similarity index 100% rename from amulet_map_editor/plugins/operations/waterlog.py rename to amulet_map_editor/programs/edit/plugins/operations/waterlog.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/export_tool.py b/amulet_map_editor/programs/edit/ui/tool_options/operation/export_tool.py index 00ef3e5b..f99f3347 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/operation/export_tool.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/operation/export_tool.py @@ -1,5 +1,5 @@ from .select_operation import BaseSelectOperationUI -from amulet_map_editor import plugins +from amulet_map_editor.programs.edit import plugins class SelectExportOperationUI(BaseSelectOperationUI): diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/import_tool.py b/amulet_map_editor/programs/edit/ui/tool_options/operation/import_tool.py index b3de550f..22ce00f5 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/operation/import_tool.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/operation/import_tool.py @@ -1,5 +1,5 @@ from .select_operation import BaseSelectOperationUI -from amulet_map_editor import plugins +from amulet_map_editor.programs.edit import plugins class SelectImportOperationUI(BaseSelectOperationUI): diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py b/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py index 462512d3..92597a15 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py @@ -4,7 +4,7 @@ from amulet_map_editor import log from amulet_map_editor.amulet_wx.simple import SimplePanel, SimpleChoiceAny -from amulet_map_editor import plugins +from amulet_map_editor.programs.edit import plugins if TYPE_CHECKING: from amulet.api.world import World From 18219ab511ada03e2b8b32ee42b21c790d33dcb5 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 15:20:47 +0100 Subject: [PATCH 02/84] Added an example plugin for the fixed function operation pipeline --- .../examples/1_fixed_function_pipeline.py | 25 +++++++++++++++++++ .../edit/plugins/examples/__init__.py | 0 2 files changed, 25 insertions(+) create mode 100644 amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py create mode 100644 amulet_map_editor/programs/edit/plugins/examples/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py new file mode 100644 index 00000000..a19f46eb --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py @@ -0,0 +1,25 @@ +# Fixed function example plugin +# The fixed function operation pipeline works in much the same way as MCEdit Unified filters with some modifications +# You define a function and it will appear in the UI for you to run + + +from amulet.api.selection import SelectionGroup +from amulet.api.world import World +from amulet.api.data_types import Dimension + + +# for those that are new to python 3 the thing after the colon is the object type that the variable should be +def operation(world: World, dimension: Dimension, selection: SelectionGroup, options: dict): + # world is the object that contains all the data related to the world + # dimension in a string used to identify the currently loaded dimension. It can be used to access the right dimension from the world + # selection is an object describing the selections made by the user. It is possible there may not be any boxes selected + # options will be explored in further examples + pass + + +export = { # This is what the program will actually look for. It describes how the operation will work + "v": 2, # the version number of the plugin system + "name": "Plugin Name", # the name of the plugin + "mode": "fixed", # specify that the operation is using the fixed function pipeline + "operation": operation, # the actual function to call when running the plugin +} diff --git a/amulet_map_editor/programs/edit/plugins/examples/__init__.py b/amulet_map_editor/programs/edit/plugins/examples/__init__.py new file mode 100644 index 00000000..e69de29b From dc9df888536993e670b8df3b4100576cd93152c1 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 15:49:14 +0100 Subject: [PATCH 03/84] Added two more examples on the fixed function pipeline --- .../examples/2_fixed_function_pipeline.py | 28 +++++++++ .../examples/3_fixed_function_pipeline.py | 59 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py create mode 100644 amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py diff --git a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py new file mode 100644 index 00000000..a761da22 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py @@ -0,0 +1,28 @@ +# Fixed function example plugin 2 +# see example 1 before looking at this example +# Notes about the operation + + +from amulet.api.selection import SelectionGroup +from amulet.api.world import World +from amulet.api.data_types import Dimension + + +# Notes about the operation +# The operation is allowed to yield floats in the range 0 to 1. +# This is used to update the loading bar in the UI. Without this the UI may appear to be not responding +# The operation is allowed to return a value however nothing will be done with it + +def operation(world: World, dimension: Dimension, selection: SelectionGroup, options: dict): + for i in range(10): + # do some logic + yield (i+1)/10 + return 'hello' # This will not actually do anything but is allowed + + +export = { # This is what the program will actually look for. It describes how the operation will work + "v": 2, # the version number of the plugin system + "name": "Plugin Name", # the name of the plugin + "mode": "fixed", # specify that the operation is using the fixed function pipeline + "operation": operation, # the actual function to call when running the plugin +} diff --git a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py new file mode 100644 index 00000000..c68287a3 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py @@ -0,0 +1,59 @@ +# Fixed function example plugin 3 +# see example 2 before looking at this example +# This example shows how to use options with the fixed function pipeline + + +from amulet.api.selection import SelectionGroup +from amulet.api.world import World +from amulet.api.data_types import Dimension + + +operation_options = { # options is a dictionary where the key is the description shown to the user and the value describes how to build the UI + "Text Label": ["label"], # This will just be a label + + # bool examples https://wxpython.org/Phoenix/docs/html/wx.CheckBox.html + "Bool input default": ["bool"], # This will be a check box that takes the default value (unchecked) + "Bool input False": ["bool", False], # This will be a check box that starts unchecked + "Bool input True": ["bool", True], # This will be a check box that starts checked + + # int examples https://wxpython.org/Phoenix/docs/html/wx.SpinCtrl.html + "Int input default": ["int"], # This will be an integer input with starting value 0 + "Int input 10": ["int", 10], # This will be an integer input with starting value 10 (or whatever you put in the second slot) + "Int input 10 bounded": ["int", 10, 0, 20], # same as above but must be between the third and fourth values + + # float examples https://wxpython.org/Phoenix/docs/html/wx.SpinCtrlDouble.html + "Float input default": ["float"], # This will be a float input with starting value 0.0 + "Float input 10": ["float", 10], # This will be a float input with starting value 10.0 (or whatever you put in the second slot) + "Float input 10 bounded": ["float", 10, 0, 20], # same as above but must be between the third and fourth values + + # string input examples https://wxpython.org/Phoenix/docs/html/wx.TextCtrl.html + "String input empty": ["str"], # This will be a text input with an empty starting value + "String input empty2": ["str", ""], # Same as the above + "String input hello": ["str", "hello"], # Text entry with starting value "hello" + + # string choice examples https://wxpython.org/Phoenix/docs/html/wx.Choice.html + "Text choice": ["str_choice", "choice 1", "choice 2", "choice 3"], + + # OS examples + "File picker": ["file"], # UI to pick a file + "Folder picker": ["directory"] # UI to pick a directory +} + + +def operation(world: World, dimension: Dimension, selection: SelectionGroup, options: dict): + # When the user presses the run button this function will be run as normal but + # since the "options" key was defined in export this function will get another + # input in the form of a dictionary where the keys are the same as you defined + # them in the options dictionary above and the values are what the user picked + # in the UI (bool, int, float, str) + # If "options" is not defined in export this will just be an empty dictionary + pass + + +export = { + "v": 2, + "name": "Plugin Name", + "mode": "fixed", + "operation": operation, + "options": operation_options # The options you defined above should be added here to show in the UI +} From d7606d8fdab3f61aa71f006325adfb28300ba148 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 16:04:24 +0100 Subject: [PATCH 04/84] Added example for yielding string from operation --- .../edit/plugins/examples/2_fixed_function_pipeline.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py index a761da22..5deef2b4 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py @@ -11,12 +11,17 @@ # Notes about the operation # The operation is allowed to yield floats in the range 0 to 1. # This is used to update the loading bar in the UI. Without this the UI may appear to be not responding +# It can optionally also yield a float and a string. The float is the same as the above and the string is used to display in the loading bar # The operation is allowed to return a value however nothing will be done with it def operation(world: World, dimension: Dimension, selection: SelectionGroup, options: dict): for i in range(10): # do some logic yield (i+1)/10 + + for i in range(10): + # do some logic + yield (i+1)/10, f"Step {i} of 10" return 'hello' # This will not actually do anything but is allowed From 61d028fb176e650f554e1c02ab1d33cb677f862f Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 17:14:19 +0100 Subject: [PATCH 05/84] Cleaned up operation examples --- .../edit/plugins/examples/1_fixed_function_pipeline.py | 3 +-- .../edit/plugins/examples/2_fixed_function_pipeline.py | 9 ++++----- .../edit/plugins/examples/3_fixed_function_pipeline.py | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py index a19f46eb..000ad9e5 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py @@ -18,8 +18,7 @@ def operation(world: World, dimension: Dimension, selection: SelectionGroup, opt export = { # This is what the program will actually look for. It describes how the operation will work - "v": 2, # the version number of the plugin system - "name": "Plugin Name", # the name of the plugin + "name": "Fixed Function Pipeline Example 1", # the name of the plugin "mode": "fixed", # specify that the operation is using the fixed function pipeline "operation": operation, # the actual function to call when running the plugin } diff --git a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py index 5deef2b4..f35e8254 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py @@ -25,9 +25,8 @@ def operation(world: World, dimension: Dimension, selection: SelectionGroup, opt return 'hello' # This will not actually do anything but is allowed -export = { # This is what the program will actually look for. It describes how the operation will work - "v": 2, # the version number of the plugin system - "name": "Plugin Name", # the name of the plugin - "mode": "fixed", # specify that the operation is using the fixed function pipeline - "operation": operation, # the actual function to call when running the plugin +export = { + "name": "Fixed Function Pipeline Example 2", + "mode": "fixed", + "operation": operation, } diff --git a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py index c68287a3..0adc0f28 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py @@ -51,8 +51,7 @@ def operation(world: World, dimension: Dimension, selection: SelectionGroup, opt export = { - "v": 2, - "name": "Plugin Name", + "name": "Fixed Function Pipeline Example 3", "mode": "fixed", "operation": operation, "options": operation_options # The options you defined above should be added here to show in the UI From a9c394e5c4a05c164f3174893303d6ff0112c853 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 17:16:48 +0100 Subject: [PATCH 06/84] Exposed the canvas classes in the canvas init --- amulet_map_editor/programs/edit/canvas/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/amulet_map_editor/programs/edit/canvas/__init__.py b/amulet_map_editor/programs/edit/canvas/__init__.py index e69de29b..3b5fbf1e 100644 --- a/amulet_map_editor/programs/edit/canvas/__init__.py +++ b/amulet_map_editor/programs/edit/canvas/__init__.py @@ -0,0 +1,2 @@ +from .canvas import EditCanvas +from .controllable_canvas import ControllableEditCanvas \ No newline at end of file From 956ee82dee844d29e8a51612e036d3c04a4881fb Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 17:22:17 +0100 Subject: [PATCH 07/84] Made mode in export optional --- .../programs/edit/plugins/examples/1_fixed_function_pipeline.py | 1 - .../programs/edit/plugins/examples/2_fixed_function_pipeline.py | 1 - .../programs/edit/plugins/examples/3_fixed_function_pipeline.py | 1 - 3 files changed, 3 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py index 000ad9e5..d6785573 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/1_fixed_function_pipeline.py @@ -19,6 +19,5 @@ def operation(world: World, dimension: Dimension, selection: SelectionGroup, opt export = { # This is what the program will actually look for. It describes how the operation will work "name": "Fixed Function Pipeline Example 1", # the name of the plugin - "mode": "fixed", # specify that the operation is using the fixed function pipeline "operation": operation, # the actual function to call when running the plugin } diff --git a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py index f35e8254..faf275ee 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/2_fixed_function_pipeline.py @@ -27,6 +27,5 @@ def operation(world: World, dimension: Dimension, selection: SelectionGroup, opt export = { "name": "Fixed Function Pipeline Example 2", - "mode": "fixed", "operation": operation, } diff --git a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py index 0adc0f28..738ee9b0 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py @@ -52,7 +52,6 @@ def operation(world: World, dimension: Dimension, selection: SelectionGroup, opt export = { "name": "Fixed Function Pipeline Example 3", - "mode": "fixed", "operation": operation, "options": operation_options # The options you defined above should be added here to show in the UI } From edc4cee7cecbf224439e563d34b309537fc30104 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 25 May 2020 18:08:56 +0100 Subject: [PATCH 08/84] Updated operation loading code to load the new format Logic is now implemented in a class to standardise the api The new code is a lot more compact because it is being offloaded to the operation itself --- amulet_map_editor/programs/edit/edit.py | 8 +- .../programs/edit/plugins/__init__.py | 273 ++++++------------ .../operation/select_operation.py | 4 +- 3 files changed, 102 insertions(+), 183 deletions(-) diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index eee9aaac..4529eb33 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -308,7 +308,7 @@ def _run_operation(self, operation_path=None) -> Any: elif inp == "options": operation_inputs.append( - plugins.options.get(operation_path, {}) + plugins.plugin_options.get(operation_path, {}) ) self._operation_options.Disable() @@ -361,7 +361,7 @@ def _run_operation(self, operation_path=None) -> Any: operation["operation"], operation_input_definitions, structure, - plugins.options.get(operation_path, {}), + plugins.plugin_options.get(operation_path, {}), ) else: # trigger UI to show select box multiple UI @@ -394,10 +394,10 @@ def _run_main_operation( operation_inputs.append(structure) elif inp == "options": if options: - plugins.options[operation_path] = options + plugins.plugin_options[operation_path] = options operation_inputs.append(options) else: - operation_inputs.append(plugins.options.get(operation_path, {})) + operation_inputs.append(plugins.plugin_options.get(operation_path, {})) self._canvas.disable_threads() try: diff --git a/amulet_map_editor/programs/edit/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py index ac0aaf3a..d2c3c4ec 100644 --- a/amulet_map_editor/programs/edit/plugins/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/__init__.py @@ -1,62 +1,85 @@ -""" ->>> # Example plugin ->>> from typing import List, Any, Callable, Dict, Optional ->>> from amulet.api.selection import SelectionGroup ->>> from amulet.api.structure import Structure ->>> from amulet.api.world import World ->>> WX_OBJ = Any ->>> Dimension = Any ->>> Options = Dict ->>> Destination = {"x": int, "y": int, "z": int} ->>> ->>> def show_ui(parent, world: World, options: dict) -> dict: # see below. Only needed if using wxoptions ->>> # create a UI and show it (probably best using wx.Dialog) ->>> # build the UI using the options that were returned last time (or an empty dictionary if it is the first) ->>> # and return the options how you wish ->>> ->>> export = { ->>> # required ->>> "v": 1, # a version 1 plugin ->>> "name": "Plugin Name", # the name of the plugin ->>> "features": List[str], # a list of features that enable functionality in the UI ->>> # the valid options are: (any invalid options will cause the operation to fail to load) ->>> "src_selection" # The user is required to select an area before running the plugin. They will be prompted if they do not. ->>> "dst_location_absolute" # This enables a UI to select a destination location. It also enables an optional callable and inputs at structure_callable. ->>> # If the callable is defined it will be run. If not the selection is extracted and used. ->>> # After the function has run the user will be shown a UI to pick a destination location for the Structure that was returned. ->>> # TODO: "options" # a simple system to create a UI to be shown to the user. Enables "options" key ->>> "wxoptions" # enables "wxoptions" key storing a callable allowing direct use of wx ->>> ->>> "options": dict, # requires "options" in features. A simple system of defining options from which a simple UI can be created ->>> "wxoptions": Callable[[WX_OBJ, World, Options], Options], # a more complex system allowing users to work directly with wx ->>> ->>> # if one of the dst_location options is enabled the following is valid ->>> "structure_callable_inputs": List[str], # see inputs below ->>> "structure_callable": Callable[[World, Dimension, ...], Structure] # World and Dimension are always the first two inputs. Above inputs follow. ->>> # this function is run before selecting the destination locations and returns a Structure for use there ->>> ->>> "inputs": List[str], # see inputs below ->>> "operation": Callable[[World, Dimension, ...], Optional[Any]], # the actual function to call when running the plugin ->>> # World and Dimension are always the first two inputs. Above inputs follow. ->>> } ->>> ->>> # Input format ->>> # a list of inputs to give to the plugin. World class and dimension are first and these follow ->>> # possible inputs (max one from each group) ->>> {"src_selection": SelectionGroup} # the user created selection ->>> { # requires a dst_location feature to be enabled. Only valid in main inputs ->>> "structure": Structure # an extracted Structure as returned by structure_callable or the area of the World selected by src_box ->>> } ->>> { ->>> "options": dict, # "options" or "wxoptions" feature must be enabled ->>> } -""" -import functools import os import glob import importlib.util from amulet import log -from typing import Dict, List +from typing import Dict, List, TYPE_CHECKING, Callable, Any +import wx + +if TYPE_CHECKING: + from ..canvas import ControllableEditCanvas + from amulet.api.world import World + + +PathType = str +OperationStorageType = Dict[PathType, "OperationLoader"] + + +class OperationUI: + """The base class that all operations must inherit from.""" + pass + + +class FixedFunctionUI(OperationUI): + def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", operation: Callable, options: Dict[str, Any]): + super().__init__() + # TODO + + +class OperationLoadException(Exception): + pass + + +class OperationLoader: + """A class to handle loading and reloading operations from their python modules/packages""" + def __init__( + self, + export_dict: dict, + path: str + ): + self._path = path + self._name = "" + self._ui = None + self._load(export_dict) + + def _load(self, export_dict: dict): + if not isinstance(export_dict, dict): + raise OperationLoadException("Export must be a dictionary.") + + if "name" in export_dict: + self._name = export_dict["name"] + if not isinstance(self.name, str): + raise OperationLoadException('"name" in export must exist and be a string.') + else: + raise OperationLoadException('"name" is not defined in export.') + + mode = export_dict.get("mode", "fixed") + if not isinstance(mode, str): + raise OperationLoadException('"name" in export is not a string.') + + if mode == "fixed": + operation = export_dict.get("operation", None) + if not callable(operation): + raise OperationLoadException('"operation" in export must be callable.') + if operation.__code__.co_argcount != 4: + raise OperationLoadException('"operation" function in export must have 4 inputs.') + options = export_dict.get("options", {}) + if not isinstance(options, dict): + raise OperationLoadException('"operation" in export must be a dictionary if defined.') + self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, operation, options) + elif mode == "dynamic": + operation = export_dict.get("operation", None) + if not issubclass(operation, OperationUI): + raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') + self._ui = operation + else: + raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') + + @property + def name(self) -> str: + return self._name + + def setup_ui(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World"): + self._ui(parent, canvas, world) def _load_module_file(module_path: str): @@ -80,120 +103,16 @@ def _load_module_directory(module_path): return mod -def _error(name, msg): - log.error(f"Error loading plugin {name}. {msg}") - - -def parse_export(plugin: dict, operations_dict: Dict[str, dict], path: str): - error = functools.partial(_error, path) - - if not isinstance(plugin, dict): - error("Export must be a dictionary.") - return - if not isinstance(plugin.get("name", None), str): - error('"name" in export must exist and be a string.') - return - structure_callable_options = ["options"] - input_options = ["options"] - dst_ui_enabled = False - features = plugin.get("features", []) - feature_index = 0 - stop = False - - while feature_index < len(features): - feature = features[feature_index] - feature_index += 1 - if feature == "src_selection": - structure_callable_options.append("src_selection") - input_options.append("src_selection") - elif feature == "wxoptions": - if "wxoptions" not in plugin: - error( - '"wxoptions" key must be defined if wxoptions feature is enabled.' - ) - stop = True - break - elif not callable(plugin["wxoptions"]): - error('"wxoptions" must be callable.') - stop = True - break - elif "options" in features: - error( - "Only one of options and wxoptions features may be enabled at once." - ) - stop = True - break - elif feature == "options": - if "options" not in plugin: - error('"options" key must be defined if options feature is enabled.') - stop = True - break - elif "wxoptions" in features: - error( - "Only one of options and wxoptions features may be enabled at once." - ) - stop = True - break - elif feature == "dst_location_absolute": - if dst_ui_enabled: - error("Only one dst_location feature can be enabled at once") - dst_ui_enabled = True - input_options.append("dst_location") - input_options.append("structure") - if stop: - return - - if dst_ui_enabled: - if "structure_callable" in plugin: - if not callable(plugin["structure_callable"]): - error('"structure_callable" must be a callable if defined.') - return - structure_callable_enabled = True - else: - if "src_selection" not in features: - features.append("src_selection") - structure_callable_enabled = False - else: - structure_callable_enabled = False - - plugin_operations = [ - ( - "operation", - plugin.get("operation", None), - plugin.get("inputs", []), - input_options, - ) - ] - if structure_callable_enabled: - plugin_operations.append( - ( - "structure_callable", - plugin.get("structure_callable", None), - plugin.get("structure_callable_inputs", []), - structure_callable_options, - ) - ) - for name, operation, inputs, options in plugin_operations: - if not callable(operation): - error(f'"{name}" in export must exist and be a function.') - stop = True - return - if not isinstance(inputs, list): - error(f'"{name}" inputs in export must be a list.') - stop = True - return - for inp in inputs: - if inp not in options: - error(f'"{inp}" is not a supported input for "{name}"') - stop = True - return - if stop: - return - - operations_dict[path] = plugin - - -def _load_operations(operations_: Dict[str, dict], path: str): +def parse_export(plugin: dict, operations_dict: Dict[str, OperationLoader], path: str): + try: + operations_dict[path] = OperationLoader(plugin, path) + except OperationLoadException as e: + log.error(f"Error loading plugin {path}. {e}") + except Exception as e: + log.error(f"Exception loading plugin {path}. {e}") + + +def _load_operations(operations_: OperationStorageType, path: str): """load all operations from a specified directory""" if os.path.isdir(path): for fpath in glob.iglob(os.path.join(path, "*.py")): @@ -224,18 +143,18 @@ def _load_operations(operations_: Dict[str, dict], path: str): def _load_operations_group(dir_paths: List[str]): """Load operations from a list of directory paths""" - operations_: Dict[str, dict] = {} + operations_: OperationStorageType = {} for dir_path in dir_paths: _load_operations(operations_, dir_path) return operations_ -all_operations: Dict[str, dict] = {} -internal_operations: Dict[str, dict] = {} -operations: Dict[str, dict] = {} -export_operations: Dict[str, dict] = {} -import_operations: Dict[str, dict] = {} -_meta: Dict[str, Dict[str, dict]] = { +all_operations: OperationStorageType = {} +internal_operations: OperationStorageType = {} +operations: OperationStorageType = {} +export_operations: OperationStorageType = {} +import_operations: OperationStorageType = {} +_meta: Dict[str, OperationStorageType] = { 'internal_operations': internal_operations, 'operations': operations, 'export_operations': export_operations, @@ -247,7 +166,7 @@ def _load_operations_group(dir_paths: List[str]): 'import_operations' } -options: Dict[str, dict] = {} +plugin_options: Dict[str, dict] = {} def merge_operations(): diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py b/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py index 92597a15..365ad47e 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py @@ -62,9 +62,9 @@ def _change_options(self, evt): if "options" in operation.get("features", []): pass # TODO: implement this elif "wxoptions" in operation.get("features", []): - options = operation["wxoptions"](self, self._world(), plugins.options.get(operation_path, {})) + options = operation["wxoptions"](self, self._world(), plugins.plugin_options.get(operation_path, {})) if isinstance(options, dict): - plugins.options[operation_path] = options + plugins.plugin_options[operation_path] = options else: log.error(f"Plugin {operation['name']} at {operation_path} did not return options in a valid format") evt.Skip() From 32fbd3a89e091c53ddc455b9a422d4942f46cf1a Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 11:23:45 +0100 Subject: [PATCH 09/84] Reorganised the operation logic --- .../programs/edit/plugins/__init__.py | 202 +----------------- .../plugins/{examples => api}/__init__.py | 0 .../programs/edit/plugins/api/data_types.py | 7 + .../edit/plugins/api/fixed_pipeline.py | 26 +++ .../programs/edit/plugins/api/loader.py | 188 ++++++++++++++++ .../programs/edit/plugins/api/operation_ui.py | 3 + .../__init__.py | 0 .../export_operations}/__init__.py | 0 .../export_operations/construction.py | 0 .../export_operations/mcstructure.py | 0 .../export_operations/schematic.py | 0 .../import_operations}/__init__.py | 0 .../import_operations/construction.py | 0 .../import_operations/mcstructure.py | 0 .../import_operations/schematic.py | 0 .../internal_operations}/__init__.py | 0 .../internal_operations/copy.py | 0 .../internal_operations/cut.py | 0 .../internal_operations/delete.py | 0 .../internal_operations/paste.py | 0 .../stock_plugins/operations/__init__.py | 0 .../{ => stock_plugins}/operations/clone.py | 0 .../operations/delete_chunk.py | 0 .../{ => stock_plugins}/operations/fill.py | 0 .../{ => stock_plugins}/operations/replace.py | 0 .../operations/waterlog.py | 0 26 files changed, 228 insertions(+), 198 deletions(-) rename amulet_map_editor/programs/edit/plugins/{examples => api}/__init__.py (100%) create mode 100644 amulet_map_editor/programs/edit/plugins/api/data_types.py create mode 100644 amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py create mode 100644 amulet_map_editor/programs/edit/plugins/api/loader.py create mode 100644 amulet_map_editor/programs/edit/plugins/api/operation_ui.py rename amulet_map_editor/programs/edit/plugins/{export_operations => stock_plugins}/__init__.py (100%) rename amulet_map_editor/programs/edit/plugins/{import_operations => stock_plugins/export_operations}/__init__.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/export_operations/construction.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/export_operations/mcstructure.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/export_operations/schematic.py (100%) rename amulet_map_editor/programs/edit/plugins/{internal_operations => stock_plugins/import_operations}/__init__.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/import_operations/construction.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/import_operations/mcstructure.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/import_operations/schematic.py (100%) rename amulet_map_editor/programs/edit/plugins/{operations => stock_plugins/internal_operations}/__init__.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/internal_operations/copy.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/internal_operations/cut.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/internal_operations/delete.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/internal_operations/paste.py (100%) create mode 100644 amulet_map_editor/programs/edit/plugins/stock_plugins/operations/__init__.py rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/operations/clone.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/operations/delete_chunk.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/operations/fill.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/operations/replace.py (100%) rename amulet_map_editor/programs/edit/plugins/{ => stock_plugins}/operations/waterlog.py (100%) diff --git a/amulet_map_editor/programs/edit/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py index d2c3c4ec..93c00d5e 100644 --- a/amulet_map_editor/programs/edit/plugins/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/__init__.py @@ -1,198 +1,4 @@ -import os -import glob -import importlib.util -from amulet import log -from typing import Dict, List, TYPE_CHECKING, Callable, Any -import wx - -if TYPE_CHECKING: - from ..canvas import ControllableEditCanvas - from amulet.api.world import World - - -PathType = str -OperationStorageType = Dict[PathType, "OperationLoader"] - - -class OperationUI: - """The base class that all operations must inherit from.""" - pass - - -class FixedFunctionUI(OperationUI): - def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", operation: Callable, options: Dict[str, Any]): - super().__init__() - # TODO - - -class OperationLoadException(Exception): - pass - - -class OperationLoader: - """A class to handle loading and reloading operations from their python modules/packages""" - def __init__( - self, - export_dict: dict, - path: str - ): - self._path = path - self._name = "" - self._ui = None - self._load(export_dict) - - def _load(self, export_dict: dict): - if not isinstance(export_dict, dict): - raise OperationLoadException("Export must be a dictionary.") - - if "name" in export_dict: - self._name = export_dict["name"] - if not isinstance(self.name, str): - raise OperationLoadException('"name" in export must exist and be a string.') - else: - raise OperationLoadException('"name" is not defined in export.') - - mode = export_dict.get("mode", "fixed") - if not isinstance(mode, str): - raise OperationLoadException('"name" in export is not a string.') - - if mode == "fixed": - operation = export_dict.get("operation", None) - if not callable(operation): - raise OperationLoadException('"operation" in export must be callable.') - if operation.__code__.co_argcount != 4: - raise OperationLoadException('"operation" function in export must have 4 inputs.') - options = export_dict.get("options", {}) - if not isinstance(options, dict): - raise OperationLoadException('"operation" in export must be a dictionary if defined.') - self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, operation, options) - elif mode == "dynamic": - operation = export_dict.get("operation", None) - if not issubclass(operation, OperationUI): - raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') - self._ui = operation - else: - raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') - - @property - def name(self) -> str: - return self._name - - def setup_ui(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World"): - self._ui(parent, canvas, world) - - -def _load_module_file(module_path: str): - spec = importlib.util.spec_from_file_location( - os.path.basename(module_path), module_path - ) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -def _load_module_directory(module_path): - import sys - - spec = importlib.util.spec_from_file_location( - os.path.basename(os.path.dirname(module_path)), module_path - ) - mod = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = mod - spec.loader.exec_module(mod) - return mod - - -def parse_export(plugin: dict, operations_dict: Dict[str, OperationLoader], path: str): - try: - operations_dict[path] = OperationLoader(plugin, path) - except OperationLoadException as e: - log.error(f"Error loading plugin {path}. {e}") - except Exception as e: - log.error(f"Exception loading plugin {path}. {e}") - - -def _load_operations(operations_: OperationStorageType, path: str): - """load all operations from a specified directory""" - if os.path.isdir(path): - for fpath in glob.iglob(os.path.join(path, "*.py")): - if fpath.endswith("__init__.py"): - continue - - mod = _load_module_file(fpath) - if hasattr(mod, "export"): - plugin = getattr(mod, "export") - parse_export(plugin, operations_, fpath) - elif hasattr(mod, "exports"): - for plugin in getattr(mod, "exports"): - parse_export(plugin, operations_, fpath) - - for dpath in glob.iglob(os.path.join(path, "**", "__init__.py")): - mod = _load_module_directory(dpath) - if hasattr(mod, "export"): - plugin = getattr(mod, "export") - parse_export( - plugin, operations_, os.path.basename(os.path.dirname(dpath)) - ) - elif hasattr(mod, "exports"): - for plugin in getattr(mod, "exports"): - parse_export( - plugin, operations_, os.path.basename(os.path.dirname(dpath)) - ) - - -def _load_operations_group(dir_paths: List[str]): - """Load operations from a list of directory paths""" - operations_: OperationStorageType = {} - for dir_path in dir_paths: - _load_operations(operations_, dir_path) - return operations_ - - -all_operations: OperationStorageType = {} -internal_operations: OperationStorageType = {} -operations: OperationStorageType = {} -export_operations: OperationStorageType = {} -import_operations: OperationStorageType = {} -_meta: Dict[str, OperationStorageType] = { - 'internal_operations': internal_operations, - 'operations': operations, - 'export_operations': export_operations, - 'import_operations': import_operations -} -_public = { - 'operations', - 'export_operations', - 'import_operations' -} - -plugin_options: Dict[str, dict] = {} - - -def merge_operations(): - """Merge all loaded operations into all_operations""" - all_operations.clear() - for ops in _meta.values(): - all_operations.update(ops) - - -def _reload_operation_name(dir_name): - """reload all operations in a directory name.""" - if dir_name in _public: - os.makedirs(os.path.join("plugins", dir_name), exist_ok=True) - _meta[dir_name].clear() - name_operations = _load_operations_group( - [os.path.join(os.path.dirname(__file__), dir_name)] + - [os.path.join("plugins", dir_name)] * (dir_name in _public) - ) - _meta[dir_name].update(name_operations) - - -def reload_operations(): - """Reload all operations""" - for dir_name in _meta.keys(): - _reload_operation_name(dir_name) - merge_operations() - - -reload_operations() +from .api.operation_ui import OperationUI +from .api.data_types import PathType, OperationStorageType +from .api.fixed_pipeline import FixedFunctionUI, OperationError, OperationSuccessful +from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, plugin_options, reload_operations \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/plugins/examples/__init__.py b/amulet_map_editor/programs/edit/plugins/api/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/examples/__init__.py rename to amulet_map_editor/programs/edit/plugins/api/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/api/data_types.py b/amulet_map_editor/programs/edit/plugins/api/data_types.py new file mode 100644 index 00000000..5650487e --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/api/data_types.py @@ -0,0 +1,7 @@ +from typing import Dict, TYPE_CHECKING + +if TYPE_CHECKING: + from .loader import OperationLoader + +PathType = str +OperationStorageType = Dict[PathType, "OperationLoader"] \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py new file mode 100644 index 00000000..e248f679 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -0,0 +1,26 @@ +import wx +from typing import Callable, Dict, Any, TYPE_CHECKING + +from .operation_ui import OperationUI + +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.canvas.controllable_canvas import ControllableEditCanvas + from amulet.api.world import World + + +class OperationError(Exception): + """Error to raise if something went wrong when running the operation. + Eg. if the operation requires something it is not given""" + pass + + +class OperationSuccessful(Exception): + """raise this if you want to exit the operation without creating an undo point. + Any changes to the world since the last undo point will be reverted.""" + pass + + +class FixedFunctionUI(OperationUI): + def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", operation: Callable, options: Dict[str, Any]): + super().__init__() + # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py new file mode 100644 index 00000000..d4169cfc --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -0,0 +1,188 @@ +import os +import glob +import importlib.util +from amulet import log +from typing import Dict, List, TYPE_CHECKING +import wx + +from .fixed_pipeline import FixedFunctionUI +from .operation_ui import OperationUI +from .data_types import OperationStorageType + +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.canvas import ControllableEditCanvas + from amulet.api.world import World + + +class OperationLoadException(Exception): + """Exception for internal use""" + pass + + +class OperationLoader: + """A class to handle loading and reloading operations from their python modules/packages""" + def __init__( + self, + export_dict: dict, + path: str + ): + self._path = path + self._name = "" + self._ui = None + self._load(export_dict) + + def _load(self, export_dict: dict): + if not isinstance(export_dict, dict): + raise OperationLoadException("Export must be a dictionary.") + + if "name" in export_dict: + self._name = export_dict["name"] + if not isinstance(self.name, str): + raise OperationLoadException('"name" in export must exist and be a string.') + else: + raise OperationLoadException('"name" is not defined in export.') + + mode = export_dict.get("mode", "fixed") + if not isinstance(mode, str): + raise OperationLoadException('"name" in export is not a string.') + + if mode == "fixed": + operation = export_dict.get("operation", None) + if not callable(operation): + raise OperationLoadException('"operation" in export must be callable.') + if operation.__code__.co_argcount != 4: + raise OperationLoadException('"operation" function in export must have 4 inputs.') + options = export_dict.get("options", {}) + if not isinstance(options, dict): + raise OperationLoadException('"operation" in export must be a dictionary if defined.') + self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, operation, options) + elif mode == "dynamic": + operation = export_dict.get("operation", None) + if not issubclass(operation, OperationUI): + raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') + self._ui = operation + else: + raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') + + @property + def name(self) -> str: + return self._name + + def setup_ui(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World"): + self._ui(parent, canvas, world) + + +def _load_module_file(module_path: str): + spec = importlib.util.spec_from_file_location( + os.path.basename(module_path), module_path + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def _load_module_directory(module_path): + import sys + + spec = importlib.util.spec_from_file_location( + os.path.basename(os.path.dirname(module_path)), module_path + ) + mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + return mod + + +def parse_export(plugin: dict, operations_dict: Dict[str, OperationLoader], path: str): + try: + operations_dict[path] = OperationLoader(plugin, path) + except OperationLoadException as e: + log.error(f"Error loading plugin {path}. {e}") + except Exception as e: + log.error(f"Exception loading plugin {path}. {e}") + + +def _load_operations(operations_: OperationStorageType, path: str): + """load all operations from a specified directory""" + if os.path.isdir(path): + for fpath in glob.iglob(os.path.join(path, "*.py")): + if fpath.endswith("__init__.py"): + continue + + mod = _load_module_file(fpath) + if hasattr(mod, "export"): + plugin = getattr(mod, "export") + parse_export(plugin, operations_, fpath) + elif hasattr(mod, "exports"): + for plugin in getattr(mod, "exports"): + parse_export(plugin, operations_, fpath) + + for dpath in glob.iglob(os.path.join(path, "**", "__init__.py")): + mod = _load_module_directory(dpath) + if hasattr(mod, "export"): + plugin = getattr(mod, "export") + parse_export( + plugin, operations_, os.path.basename(os.path.dirname(dpath)) + ) + elif hasattr(mod, "exports"): + for plugin in getattr(mod, "exports"): + parse_export( + plugin, operations_, os.path.basename(os.path.dirname(dpath)) + ) + + +def _load_operations_group(dir_paths: List[str]): + """Load operations from a list of directory paths""" + operations_: OperationStorageType = {} + for dir_path in dir_paths: + _load_operations(operations_, dir_path) + return operations_ + + +all_operations: OperationStorageType = {} +internal_operations: OperationStorageType = {} +operations: OperationStorageType = {} +export_operations: OperationStorageType = {} +import_operations: OperationStorageType = {} +_meta: Dict[str, OperationStorageType] = { + 'internal_operations': internal_operations, + 'operations': operations, + 'export_operations': export_operations, + 'import_operations': import_operations +} +_public = { + 'operations', + 'export_operations', + 'import_operations' +} + +plugin_options: Dict[str, dict] = {} + + +def merge_operations(): + """Merge all loaded operations into all_operations""" + all_operations.clear() + for ops in _meta.values(): + all_operations.update(ops) + + +def _reload_operation_name(dir_name): + """reload all operations in a directory name.""" + if dir_name in _public: + os.makedirs(os.path.join("plugins", dir_name), exist_ok=True) + _meta[dir_name].clear() + name_operations = _load_operations_group( + [os.path.join(os.path.dirname(__file__), dir_name)] + + [os.path.join("plugins", dir_name)] * (dir_name in _public) + ) + _meta[dir_name].update(name_operations) + + +def reload_operations(): + """Reload all operations""" + for dir_name in _meta.keys(): + _reload_operation_name(dir_name) + merge_operations() + + +reload_operations() diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py new file mode 100644 index 00000000..9165f7d0 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -0,0 +1,3 @@ +class OperationUI: + """The base class that all operations must inherit from.""" + pass diff --git a/amulet_map_editor/programs/edit/plugins/export_operations/__init__.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/export_operations/__init__.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/import_operations/__init__.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/import_operations/__init__.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/export_operations/construction.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py diff --git a/amulet_map_editor/programs/edit/plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/export_operations/mcstructure.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py diff --git a/amulet_map_editor/programs/edit/plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/export_operations/schematic.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py diff --git a/amulet_map_editor/programs/edit/plugins/internal_operations/__init__.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/internal_operations/__init__.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/import_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/import_operations/construction.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py diff --git a/amulet_map_editor/programs/edit/plugins/import_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/import_operations/mcstructure.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py diff --git a/amulet_map_editor/programs/edit/plugins/import_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/import_operations/schematic.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py diff --git a/amulet_map_editor/programs/edit/plugins/operations/__init__.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/operations/__init__.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/__init__.py diff --git a/amulet_map_editor/programs/edit/plugins/internal_operations/copy.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/internal_operations/copy.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py diff --git a/amulet_map_editor/programs/edit/plugins/internal_operations/cut.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/internal_operations/cut.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py diff --git a/amulet_map_editor/programs/edit/plugins/internal_operations/delete.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/delete.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/internal_operations/delete.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/delete.py diff --git a/amulet_map_editor/programs/edit/plugins/internal_operations/paste.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/paste.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/internal_operations/paste.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/paste.py diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/__init__.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/programs/edit/plugins/operations/clone.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/operations/clone.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py diff --git a/amulet_map_editor/programs/edit/plugins/operations/delete_chunk.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/delete_chunk.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/operations/delete_chunk.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/operations/delete_chunk.py diff --git a/amulet_map_editor/programs/edit/plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/operations/fill.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py diff --git a/amulet_map_editor/programs/edit/plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/operations/replace.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py diff --git a/amulet_map_editor/programs/edit/plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py similarity index 100% rename from amulet_map_editor/programs/edit/plugins/operations/waterlog.py rename to amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py From 7411b95dca594e8006b5fd0ebf2d1216590dee70 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 11:57:53 +0100 Subject: [PATCH 10/84] Added a mechanism for loading and saving options --- .../programs/edit/plugins/__init__.py | 2 +- .../edit/plugins/api/fixed_pipeline.py | 4 ++-- .../programs/edit/plugins/api/operation_ui.py | 22 ++++++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py index 93c00d5e..b67e75b6 100644 --- a/amulet_map_editor/programs/edit/plugins/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/__init__.py @@ -1,4 +1,4 @@ -from .api.operation_ui import OperationUI from .api.data_types import PathType, OperationStorageType +from .api.operation_ui import OperationUI from .api.fixed_pipeline import FixedFunctionUI, OperationError, OperationSuccessful from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, plugin_options, reload_operations \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index e248f679..a2a18a92 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -21,6 +21,6 @@ class OperationSuccessful(Exception): class FixedFunctionUI(OperationUI): - def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", operation: Callable, options: Dict[str, Any]): - super().__init__() + def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): + super().__init__(options_path) # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index 9165f7d0..dac6b2cc 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -1,3 +1,23 @@ +import pickle +from typing import Any +import os + + class OperationUI: """The base class that all operations must inherit from.""" - pass + def __init__(self, options_path: str): + self._options_path = options_path + + def _load_options(self, default=None) -> Any: + """Load previously saved options from disk or return the default options.""" + try: + with open(self._options_path, "rb") as f: + return pickle.load(f) + except: + return default + + def _save_options(self, options: Any): + """Save the given options to disk so that they persist in the next session.""" + os.makedirs(os.path.basename(self._options_path), exist_ok=True) + with open(self._options_path, "wb") as f: + return pickle.dump(options, f) From 87b2db629fe20a1dd87ccd8c517f065ddfca5d87 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 12:04:06 +0100 Subject: [PATCH 11/84] Added parent, canvas and world to the base operation ui class This will make it more obvious what the API of the operation class should be and will handle weak referencing those objects to stop memory leaks --- .../edit/plugins/api/fixed_pipeline.py | 2 +- .../programs/edit/plugins/api/operation_ui.py | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index a2a18a92..87ed5ec1 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -22,5 +22,5 @@ class OperationSuccessful(Exception): class FixedFunctionUI(OperationUI): def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): - super().__init__(options_path) + super().__init__(parent, canvas, world, options_path) # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index dac6b2cc..44350494 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -1,13 +1,34 @@ import pickle -from typing import Any +from typing import Any, TYPE_CHECKING import os +import wx +import weakref + +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.canvas.controllable_canvas import ControllableEditCanvas + from amulet.api.world import World class OperationUI: """The base class that all operations must inherit from.""" - def __init__(self, options_path: str): + def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str): + self._parent = weakref.ref(parent) + self._canvas = weakref.ref(canvas) + self._world = weakref.ref(world) self._options_path = options_path + @property + def parent(self) -> wx.Window: + return self._parent() + + @property + def canvas(self) -> "ControllableEditCanvas": + return self._canvas() + + @property + def world(self) -> "World": + return self._world() + def _load_options(self, default=None) -> Any: """Load previously saved options from disk or return the default options.""" try: From d348cf5784672758d4e409779013a89d53350876 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 12:29:19 +0100 Subject: [PATCH 12/84] Added in the options path to operation creation --- .../programs/edit/plugins/__init__.py | 2 +- .../programs/edit/plugins/api/data_types.py | 2 +- .../programs/edit/plugins/api/loader.py | 27 +++++++++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py index b67e75b6..c2fb9930 100644 --- a/amulet_map_editor/programs/edit/plugins/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/__init__.py @@ -1,4 +1,4 @@ from .api.data_types import PathType, OperationStorageType from .api.operation_ui import OperationUI from .api.fixed_pipeline import FixedFunctionUI, OperationError, OperationSuccessful -from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, plugin_options, reload_operations \ No newline at end of file +from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, plugin_options, reload_operations diff --git a/amulet_map_editor/programs/edit/plugins/api/data_types.py b/amulet_map_editor/programs/edit/plugins/api/data_types.py index 5650487e..b7269f7e 100644 --- a/amulet_map_editor/programs/edit/plugins/api/data_types.py +++ b/amulet_map_editor/programs/edit/plugins/api/data_types.py @@ -4,4 +4,4 @@ from .loader import OperationLoader PathType = str -OperationStorageType = Dict[PathType, "OperationLoader"] \ No newline at end of file +OperationStorageType = Dict[PathType, "OperationLoader"] diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index d4169cfc..ac06db93 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -2,8 +2,10 @@ import glob import importlib.util from amulet import log -from typing import Dict, List, TYPE_CHECKING +from typing import Dict, List, TYPE_CHECKING, Callable, Optional, Type import wx +import struct +import hashlib from .fixed_pipeline import FixedFunctionUI from .operation_ui import OperationUI @@ -28,7 +30,7 @@ def __init__( ): self._path = path self._name = "" - self._ui = None + self._ui: Optional[Callable[[wx.Window, "ControllableEditCanvas", "World"], OperationUI]] = None self._load(export_dict) def _load(self, export_dict: dict): @@ -42,6 +44,21 @@ def _load(self, export_dict: dict): else: raise OperationLoadException('"name" is not defined in export.') + options_path = os.path.abspath( + os.path.join( + "config", + "edit_plugins", + f"""{self._name}_{ + struct.unpack( + "H", + hashlib.sha1( + self._path.encode('utf-8') + ).digest()[:2] + )[0] + }""" # generate a file name that identifiable to the operation but "unique" to the path + ) + ) + mode = export_dict.get("mode", "fixed") if not isinstance(mode, str): raise OperationLoadException('"name" in export is not a string.') @@ -55,12 +72,12 @@ def _load(self, export_dict: dict): options = export_dict.get("options", {}) if not isinstance(options, dict): raise OperationLoadException('"operation" in export must be a dictionary if defined.') - self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, operation, options) + self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, options_path, operation, options) elif mode == "dynamic": - operation = export_dict.get("operation", None) + operation: Type[OperationUI] = export_dict.get("operation", None) if not issubclass(operation, OperationUI): raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') - self._ui = operation + self._ui = lambda parent, canvas, world: operation(parent, canvas, world, options_path) else: raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') From b91bc9b6b5acc6dc9141300305549ff6eafd0569 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 14:18:59 +0100 Subject: [PATCH 13/84] Added a fixed reference for the plugins directories --- amulet_map_editor/programs/edit/plugins/api/loader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index ac06db93..e7016634 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -16,6 +16,10 @@ from amulet.api.world import World +STOCK_PLUGINS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "stock_plugins")) +CUSTOM_PLUGINS_DIR = os.path.abspath("plugins") + + class OperationLoadException(Exception): """Exception for internal use""" pass @@ -189,8 +193,8 @@ def _reload_operation_name(dir_name): os.makedirs(os.path.join("plugins", dir_name), exist_ok=True) _meta[dir_name].clear() name_operations = _load_operations_group( - [os.path.join(os.path.dirname(__file__), dir_name)] + - [os.path.join("plugins", dir_name)] * (dir_name in _public) + [os.path.join(STOCK_PLUGINS_DIR, dir_name)] + + [os.path.join(CUSTOM_PLUGINS_DIR, dir_name)] * (dir_name in _public) ) _meta[dir_name].update(name_operations) From f037ead0c76c1a13ed1bf1b105a58660fd3323a5 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 14:19:18 +0100 Subject: [PATCH 14/84] Added a more compact run operation method --- amulet_map_editor/programs/edit/edit.py | 153 +++--------------------- 1 file changed, 19 insertions(+), 134 deletions(-) diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 4529eb33..38037f55 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -290,151 +290,36 @@ def _run_operation_event(self, evt): self._run_operation() evt.Skip() - def _run_operation(self, operation_path=None) -> Any: - if operation_path is None: - operation_path = self._operation_options.operation - operation = plugins.all_operations[operation_path] - features = operation.get("features", []) - operation_input_definitions = operation.get("inputs", []) - if any(feature in features for feature in ("dst_location_absolute",)): - if "structure_callable" in operation: - operation_inputs = [] - for inp in operation.get("structure_callable_inputs", []): - if inp == "src_selection": - selection = self._get_box() - if selection is None: - return - operation_inputs.append(selection) - - elif inp == "options": - operation_inputs.append( - plugins.plugin_options.get(operation_path, {}) - ) - - self._operation_options.Disable() - - self._canvas.disable_threads() - try: - structure = show_loading_dialog( - lambda: operation["structure_callable"]( - self._world, self._canvas.dimension, *operation_inputs - ), - f'Running structure operation for {operation["name"]}.', - "Please wait for the operation to finish.", - self, - ) - except Exception as e: - log.error(f"Error running structure operation: {e}\n{traceback.format_exc()}") - wx.MessageBox(f"Error running structure operation: {e}") - self._world.restore_last_undo_point() - self._canvas.enable_threads() - self._operation_options.Enable() - return - self._canvas.enable_threads() - - self._operation_options.Enable() - if not isinstance(structure, Structure): - log.error("Object returned from structure_callable was not a Structure. Aborting.") - wx.MessageBox( - "Object returned from structure_callable was not a Structure. Aborting." - ) - return - else: - selection = self._get_box() - if selection is None: - return - self._operation_options.Disable() - structure = show_loading_dialog( - lambda: Structure.from_world( - self._world, selection, self._canvas.dimension - ), - f'Running structure operation for {operation["name"]}.', - "Copying structure from world.", - self, - ) - self._operation_options.Enable() - - if "dst_location_absolute" in features: - # trigger UI to show select box UI - self._operation_options.enable_select_destination_ui( - operation_path, - operation["operation"], - operation_input_definitions, - structure, - plugins.plugin_options.get(operation_path, {}), - ) - else: - # trigger UI to show select box multiple UI - raise NotImplementedError - - else: - self._operation_options.Disable() - out = self._run_main_operation( - operation_path, operation["operation"], operation_input_definitions - ) - self._operation_options.Enable() - return out - - def _run_main_operation( - self, - operation_path: str, - operation: OperationType, - operation_input_definitions: List[str], - options=None, - structure=None, - ) -> Any: - operation_inputs = [] - for inp in operation_input_definitions: - if inp == "src_selection": - selection = self._get_box() - if selection is None: - return - operation_inputs.append(selection) - elif inp == "structure": - operation_inputs.append(structure) - elif inp == "options": - if options: - plugins.plugin_options[operation_path] = options - operation_inputs.append(options) - else: - operation_inputs.append(plugins.plugin_options.get(operation_path, {})) - + def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: self._canvas.disable_threads() try: out = show_loading_dialog( - lambda: operation( - self._world, self._canvas.dimension, *operation_inputs - ), - f"Running Operation ?.", - "Please wait for the operation to finish.", + operation, + title, + msg, self, ) self._world.create_undo_point() self._file_panel.update_buttons() except Exception as e: - operation_info = plugins.all_operations[operation_path] - log.error( - f'Error occurred while running operation: {operation_info["name"]} v{operation_info["v"]}' - ) - log.error(f"{e}\n{traceback.format_exc()}") - wx.MessageBox(f"Error running operation: {e}") - self._world.restore_last_undo_point() - out = None + self._canvas.enable_threads() + raise e self._canvas.enable_threads() return out - def _cut(self) -> bool: - return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'cut.py')) - - def _copy(self) -> bool: - return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'copy.py')) - - def _paste(self) -> bool: - self.show_operation_options(None) - return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'paste.py')) - - def _delete(self) -> bool: - return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'delete.py')) + # TODO + # def _cut(self) -> bool: + # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'cut.py')) + # + # def _copy(self) -> bool: + # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'copy.py')) + # + # def _paste(self) -> bool: + # self.show_operation_options(None) + # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'paste.py')) + # + # def _delete(self) -> bool: + # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'delete.py')) def show_select_options(self, _): self._operation_options.Hide() From feb3a8f39dca9aded66f25cc8552e144ec8670d6 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 14:21:46 +0100 Subject: [PATCH 15/84] Renamed the two edit canvas classes --- amulet_map_editor/programs/edit/canvas/__init__.py | 4 ++-- .../programs/edit/canvas/{canvas.py => base_edit_canvas.py} | 2 +- .../edit/canvas/{controllable_canvas.py => edit_canvas.py} | 4 ++-- amulet_map_editor/programs/edit/edit.py | 6 +++--- .../programs/edit/plugins/api/fixed_pipeline.py | 4 ++-- amulet_map_editor/programs/edit/plugins/api/loader.py | 6 +++--- amulet_map_editor/programs/edit/plugins/api/operation_ui.py | 6 +++--- amulet_map_editor/programs/edit/ui/file.py | 4 ++-- amulet_map_editor/programs/edit/ui/tool_options/select.py | 4 ++-- 9 files changed, 20 insertions(+), 20 deletions(-) rename amulet_map_editor/programs/edit/canvas/{canvas.py => base_edit_canvas.py} (99%) rename amulet_map_editor/programs/edit/canvas/{controllable_canvas.py => edit_canvas.py} (99%) diff --git a/amulet_map_editor/programs/edit/canvas/__init__.py b/amulet_map_editor/programs/edit/canvas/__init__.py index 3b5fbf1e..90eaf733 100644 --- a/amulet_map_editor/programs/edit/canvas/__init__.py +++ b/amulet_map_editor/programs/edit/canvas/__init__.py @@ -1,2 +1,2 @@ -from .canvas import EditCanvas -from .controllable_canvas import ControllableEditCanvas \ No newline at end of file +from .base_edit_canvas import BaseEditCanvas +from .edit_canvas import EditCanvas \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/canvas/canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py similarity index 99% rename from amulet_map_editor/programs/edit/canvas/canvas.py rename to amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 71d222e9..386c78f4 100644 --- a/amulet_map_editor/programs/edit/canvas/canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -27,7 +27,7 @@ MODE_STRUCTURE = 2 # MODE_DISABLED and draw structure if exists -class EditCanvas(BaseCanvas): +class BaseEditCanvas(BaseCanvas): def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) self._last_mouse_x = 0 diff --git a/amulet_map_editor/programs/edit/canvas/controllable_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py similarity index 99% rename from amulet_map_editor/programs/edit/canvas/controllable_canvas.py rename to amulet_map_editor/programs/edit/canvas/edit_canvas.py index 0b625105..933bb0a7 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -2,7 +2,7 @@ import wx import numpy -from .canvas import EditCanvas +from .base_edit_canvas import BaseEditCanvas from amulet_map_editor.opengl.mesh.world_renderer.world import sin, cos from ..events import ( CameraMoveEvent, @@ -32,7 +32,7 @@ } -class ControllableEditCanvas(EditCanvas): +class EditCanvas(BaseEditCanvas): def __init__(self, world_panel: 'EditExtension', world: 'World'): super().__init__(world_panel, world) self._persistent_actions = set() diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 38037f55..7de68e16 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -16,7 +16,7 @@ from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog -from .canvas.controllable_canvas import ControllableEditCanvas +from .canvas.edit_canvas import EditCanvas from .ui.file import FilePanel from .ui.tool_options.operation import OperationUI from .ui.tool_options.select import SelectOptions @@ -79,7 +79,7 @@ def __init__(self, parent, world: "World"): self._sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._sizer) self._world = world - self._canvas: Optional[ControllableEditCanvas] = None + self._canvas: Optional[EditCanvas] = None self._temp = wx.StaticText(self, label="Please wait while the renderer loads") self._temp.SetFont(wx.Font(40, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)) self._sizer.Add(self._temp) @@ -95,7 +95,7 @@ def enable(self): if self._canvas is None: self.Update() - self._canvas = ControllableEditCanvas(self, self._world) + self._canvas = EditCanvas(self, self._world) config = CONFIG.get(EDIT_CONFIG_ID, {}) user_keybinds = config.get("user_keybinds", {}) diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 87ed5ec1..6a347c23 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -4,7 +4,7 @@ from .operation_ui import OperationUI if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World @@ -21,6 +21,6 @@ class OperationSuccessful(Exception): class FixedFunctionUI(OperationUI): - def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): + def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): super().__init__(parent, canvas, world, options_path) # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index e7016634..0ed378f1 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -12,7 +12,7 @@ from .data_types import OperationStorageType if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas import EditCanvas from amulet.api.world import World @@ -34,7 +34,7 @@ def __init__( ): self._path = path self._name = "" - self._ui: Optional[Callable[[wx.Window, "ControllableEditCanvas", "World"], OperationUI]] = None + self._ui: Optional[Callable[[wx.Window, "EditCanvas", "World"], OperationUI]] = None self._load(export_dict) def _load(self, export_dict: dict): @@ -89,7 +89,7 @@ def _load(self, export_dict: dict): def name(self) -> str: return self._name - def setup_ui(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World"): + def setup_ui(self, parent: wx.Window, canvas: "EditCanvas", world: "World"): self._ui(parent, canvas, world) diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index 44350494..b93542ad 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -5,13 +5,13 @@ import weakref if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World class OperationUI: """The base class that all operations must inherit from.""" - def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str): + def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str): self._parent = weakref.ref(parent) self._canvas = weakref.ref(canvas) self._world = weakref.ref(world) @@ -22,7 +22,7 @@ def parent(self) -> wx.Window: return self._parent() @property - def canvas(self) -> "ControllableEditCanvas": + def canvas(self) -> "EditCanvas": return self._canvas() @property diff --git a/amulet_map_editor/programs/edit/ui/file.py b/amulet_map_editor/programs/edit/ui/file.py index 04ae560e..47a9b700 100644 --- a/amulet_map_editor/programs/edit/ui/file.py +++ b/amulet_map_editor/programs/edit/ui/file.py @@ -6,12 +6,12 @@ from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World class FilePanel(wx.Panel): - def __init__(self, canvas: 'ControllableEditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): + def __init__(self, canvas: 'EditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): wx.Panel.__init__(self, canvas) self._canvas = weakref.ref(canvas) self._world = weakref.ref(world) diff --git a/amulet_map_editor/programs/edit/ui/tool_options/select.py b/amulet_map_editor/programs/edit/ui/tool_options/select.py index 765bf1a3..1479664b 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/select.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/select.py @@ -9,11 +9,11 @@ ) if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas class SelectOptions(wx.Panel): - def __init__(self, canvas: 'ControllableEditCanvas'): + def __init__(self, canvas: 'EditCanvas'): wx.Panel.__init__(self, canvas) self._canvas = weakref.ref(canvas) self._sizer = wx.BoxSizer(wx.VERTICAL) From 22f6e3c70bbe141ca6b3b64bd5ba3ba440cc9532 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 26 May 2020 14:27:57 +0100 Subject: [PATCH 16/84] Moved events into the canvas package --- .../programs/edit/canvas/base_edit_canvas.py | 2 +- .../programs/edit/canvas/edit_canvas.py | 5 +---- .../programs/edit/{ => canvas}/events.py | 3 +++ amulet_map_editor/programs/edit/edit.py | 12 ++++-------- amulet_map_editor/programs/edit/ui/tool.py | 2 +- .../programs/edit/ui/tool_options/select.py | 6 ------ 6 files changed, 10 insertions(+), 20 deletions(-) rename amulet_map_editor/programs/edit/{ => canvas}/events.py (91%) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 386c78f4..1c0e473d 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -17,7 +17,7 @@ from amulet_map_editor.opengl import textureatlas from amulet_map_editor.opengl.canvas.base import BaseCanvas from amulet_map_editor import log -from ..events import CameraMoveEvent +from amulet_map_editor.programs.edit.canvas.events import CameraMoveEvent if TYPE_CHECKING: from amulet.api.world import World diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 933bb0a7..68f3a4f8 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -4,11 +4,8 @@ from .base_edit_canvas import BaseEditCanvas from amulet_map_editor.opengl.mesh.world_renderer.world import sin, cos -from ..events import ( +from amulet_map_editor.programs.edit.canvas.events import ( CameraMoveEvent, - BoxGreenCornerChangeEvent, - BoxBlueCornerChangeEvent, - BoxCoordsEnableEvent, ) from amulet_map_editor.amulet_wx.key_config import serialise_key_event, KeybindGroup diff --git a/amulet_map_editor/programs/edit/events.py b/amulet_map_editor/programs/edit/canvas/events.py similarity index 91% rename from amulet_map_editor/programs/edit/events.py rename to amulet_map_editor/programs/edit/canvas/events.py index 40b9ea3f..a544b937 100644 --- a/amulet_map_editor/programs/edit/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -1,7 +1,10 @@ from wx.lib import newevent + CameraMoveEvent, EVT_CAMERA_MOVE = newevent.NewEvent() +ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() + SelectToolEnabledEvent, EVT_SELECT_TOOL_ENABLED = newevent.NewEvent() OperationToolEnabledEvent, EVT_OPERATION_TOOL_ENABLED = newevent.NewEvent() ImportToolEnabledEvent, EVT_IMPORT_TOOL_ENABLED = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 7de68e16..a11f6929 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -1,18 +1,14 @@ import wx -from typing import TYPE_CHECKING, Optional, List, Callable, Any +from typing import TYPE_CHECKING, Optional, Callable, Any from types import GeneratorType import webbrowser import time -import traceback -import os from amulet.api.selection import SelectionGroup -from amulet.api.structure import Structure -from amulet.api.data_types import OperationType, OperationReturnType +from amulet.api.data_types import OperationReturnType -from amulet_map_editor import log, CONFIG +from amulet_map_editor import CONFIG from amulet_map_editor.programs import BaseWorldProgram, MenuData -from amulet_map_editor.programs.edit import plugins from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog @@ -23,7 +19,7 @@ from .ui.tool import ToolSelect from .key_config import DefaultKeys, DefaultKeybindGroupId, PresetKeybinds, KeybindKeys -from .events import ( +from amulet_map_editor.programs.edit.canvas.events import ( EVT_CAMERA_MOVE, EVT_SELECT_TOOL_ENABLED, EVT_OPERATION_TOOL_ENABLED, diff --git a/amulet_map_editor/programs/edit/ui/tool.py b/amulet_map_editor/programs/edit/ui/tool.py index 938f9fc1..7aa4fd83 100644 --- a/amulet_map_editor/programs/edit/ui/tool.py +++ b/amulet_map_editor/programs/edit/ui/tool.py @@ -1,6 +1,6 @@ import wx -from amulet_map_editor.programs.edit.events import ( +from amulet_map_editor.programs.edit.canvas.events import ( SelectToolEnabledEvent, OperationToolEnabledEvent, ImportToolEnabledEvent, diff --git a/amulet_map_editor/programs/edit/ui/tool_options/select.py b/amulet_map_editor/programs/edit/ui/tool_options/select.py index 1479664b..ba2cf2f4 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/select.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/select.py @@ -2,12 +2,6 @@ import wx import weakref -from amulet_map_editor.programs.edit.events import ( - EVT_BOX_GREEN_CORNER_CHANGE, - EVT_BOX_BLUE_CORNER_CHANGE, - EVT_BOX_COORDS_ENABLE -) - if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas From 7a8227a247ae2719d8a2a91503f2222b0eae26f4 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 27 May 2020 10:04:16 +0100 Subject: [PATCH 17/84] Renamed EditCanvas back to ControllableEditCanvas EditCanvas will be implemented again and will include the UI elements contained within --- amulet_map_editor/programs/edit/canvas/__init__.py | 2 +- .../canvas/{edit_canvas.py => controllable_edit_canvas.py} | 2 +- amulet_map_editor/programs/edit/edit.py | 6 +++--- .../programs/edit/plugins/api/fixed_pipeline.py | 4 ++-- amulet_map_editor/programs/edit/plugins/api/loader.py | 6 +++--- amulet_map_editor/programs/edit/plugins/api/operation_ui.py | 6 +++--- amulet_map_editor/programs/edit/ui/file.py | 4 ++-- amulet_map_editor/programs/edit/ui/tool_options/select.py | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) rename amulet_map_editor/programs/edit/canvas/{edit_canvas.py => controllable_edit_canvas.py} (99%) diff --git a/amulet_map_editor/programs/edit/canvas/__init__.py b/amulet_map_editor/programs/edit/canvas/__init__.py index 90eaf733..b5a1504d 100644 --- a/amulet_map_editor/programs/edit/canvas/__init__.py +++ b/amulet_map_editor/programs/edit/canvas/__init__.py @@ -1,2 +1,2 @@ from .base_edit_canvas import BaseEditCanvas -from .edit_canvas import EditCanvas \ No newline at end of file +from .controllable_edit_canvas import ControllableEditCanvas \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py similarity index 99% rename from amulet_map_editor/programs/edit/canvas/edit_canvas.py rename to amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index 68f3a4f8..ce59579d 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -29,7 +29,7 @@ } -class EditCanvas(BaseEditCanvas): +class ControllableEditCanvas(BaseEditCanvas): def __init__(self, world_panel: 'EditExtension', world: 'World'): super().__init__(world_panel, world) self._persistent_actions = set() diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index a11f6929..0725af6a 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -12,7 +12,7 @@ from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog -from .canvas.edit_canvas import EditCanvas +from .canvas.controllable_edit_canvas import ControllableEditCanvas from .ui.file import FilePanel from .ui.tool_options.operation import OperationUI from .ui.tool_options.select import SelectOptions @@ -75,7 +75,7 @@ def __init__(self, parent, world: "World"): self._sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._sizer) self._world = world - self._canvas: Optional[EditCanvas] = None + self._canvas: Optional[ControllableEditCanvas] = None self._temp = wx.StaticText(self, label="Please wait while the renderer loads") self._temp.SetFont(wx.Font(40, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)) self._sizer.Add(self._temp) @@ -91,7 +91,7 @@ def enable(self): if self._canvas is None: self.Update() - self._canvas = EditCanvas(self, self._world) + self._canvas = ControllableEditCanvas(self, self._world) config = CONFIG.get(EDIT_CONFIG_ID, {}) user_keybinds = config.get("user_keybinds", {}) diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 6a347c23..6d1daa7d 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -4,7 +4,7 @@ from .operation_ui import OperationUI if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet.api.world import World @@ -21,6 +21,6 @@ class OperationSuccessful(Exception): class FixedFunctionUI(OperationUI): - def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): + def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): super().__init__(parent, canvas, world, options_path) # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index 0ed378f1..e7016634 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -12,7 +12,7 @@ from .data_types import OperationStorageType if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas import EditCanvas + from amulet_map_editor.programs.edit.canvas import ControllableEditCanvas from amulet.api.world import World @@ -34,7 +34,7 @@ def __init__( ): self._path = path self._name = "" - self._ui: Optional[Callable[[wx.Window, "EditCanvas", "World"], OperationUI]] = None + self._ui: Optional[Callable[[wx.Window, "ControllableEditCanvas", "World"], OperationUI]] = None self._load(export_dict) def _load(self, export_dict: dict): @@ -89,7 +89,7 @@ def _load(self, export_dict: dict): def name(self) -> str: return self._name - def setup_ui(self, parent: wx.Window, canvas: "EditCanvas", world: "World"): + def setup_ui(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World"): self._ui(parent, canvas, world) diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index b93542ad..eacf8f2d 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -5,13 +5,13 @@ import weakref if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet.api.world import World class OperationUI: """The base class that all operations must inherit from.""" - def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str): + def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str): self._parent = weakref.ref(parent) self._canvas = weakref.ref(canvas) self._world = weakref.ref(world) @@ -22,7 +22,7 @@ def parent(self) -> wx.Window: return self._parent() @property - def canvas(self) -> "EditCanvas": + def canvas(self) -> "ControllableEditCanvas": return self._canvas() @property diff --git a/amulet_map_editor/programs/edit/ui/file.py b/amulet_map_editor/programs/edit/ui/file.py index 47a9b700..51d2b56f 100644 --- a/amulet_map_editor/programs/edit/ui/file.py +++ b/amulet_map_editor/programs/edit/ui/file.py @@ -6,12 +6,12 @@ from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet.api.world import World class FilePanel(wx.Panel): - def __init__(self, canvas: 'EditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): + def __init__(self, canvas: 'ControllableEditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): wx.Panel.__init__(self, canvas) self._canvas = weakref.ref(canvas) self._world = weakref.ref(world) diff --git a/amulet_map_editor/programs/edit/ui/tool_options/select.py b/amulet_map_editor/programs/edit/ui/tool_options/select.py index ba2cf2f4..b2d19e51 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/select.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/select.py @@ -3,11 +3,11 @@ import weakref if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas class SelectOptions(wx.Panel): - def __init__(self, canvas: 'EditCanvas'): + def __init__(self, canvas: 'ControllableEditCanvas'): wx.Panel.__init__(self, canvas) self._canvas = weakref.ref(canvas) self._sizer = wx.BoxSizer(wx.VERTICAL) From 96d09d450c5e38ac01a634d7f2ddf384b290ba4e Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 27 May 2020 10:06:59 +0100 Subject: [PATCH 18/84] Added EditCanvas class which will add the embedded UIs --- amulet_map_editor/programs/edit/canvas/__init__.py | 2 +- amulet_map_editor/programs/edit/canvas/edit_canvas.py | 5 +++++ amulet_map_editor/programs/edit/edit.py | 6 +++--- .../programs/edit/plugins/api/fixed_pipeline.py | 4 ++-- amulet_map_editor/programs/edit/plugins/api/loader.py | 6 +++--- amulet_map_editor/programs/edit/plugins/api/operation_ui.py | 6 +++--- amulet_map_editor/programs/edit/ui/file.py | 4 ++-- amulet_map_editor/programs/edit/ui/tool_options/select.py | 4 ++-- 8 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 amulet_map_editor/programs/edit/canvas/edit_canvas.py diff --git a/amulet_map_editor/programs/edit/canvas/__init__.py b/amulet_map_editor/programs/edit/canvas/__init__.py index b5a1504d..90eaf733 100644 --- a/amulet_map_editor/programs/edit/canvas/__init__.py +++ b/amulet_map_editor/programs/edit/canvas/__init__.py @@ -1,2 +1,2 @@ from .base_edit_canvas import BaseEditCanvas -from .controllable_edit_canvas import ControllableEditCanvas \ No newline at end of file +from .edit_canvas import EditCanvas \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py new file mode 100644 index 00000000..c4033717 --- /dev/null +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -0,0 +1,5 @@ +from .controllable_edit_canvas import ControllableEditCanvas + + +class EditCanvas(ControllableEditCanvas): + pass diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 0725af6a..a11f6929 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -12,7 +12,7 @@ from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog -from .canvas.controllable_edit_canvas import ControllableEditCanvas +from .canvas.edit_canvas import EditCanvas from .ui.file import FilePanel from .ui.tool_options.operation import OperationUI from .ui.tool_options.select import SelectOptions @@ -75,7 +75,7 @@ def __init__(self, parent, world: "World"): self._sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._sizer) self._world = world - self._canvas: Optional[ControllableEditCanvas] = None + self._canvas: Optional[EditCanvas] = None self._temp = wx.StaticText(self, label="Please wait while the renderer loads") self._temp.SetFont(wx.Font(40, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)) self._sizer.Add(self._temp) @@ -91,7 +91,7 @@ def enable(self): if self._canvas is None: self.Update() - self._canvas = ControllableEditCanvas(self, self._world) + self._canvas = EditCanvas(self, self._world) config = CONFIG.get(EDIT_CONFIG_ID, {}) user_keybinds = config.get("user_keybinds", {}) diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 6d1daa7d..6a347c23 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -4,7 +4,7 @@ from .operation_ui import OperationUI if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World @@ -21,6 +21,6 @@ class OperationSuccessful(Exception): class FixedFunctionUI(OperationUI): - def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): + def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): super().__init__(parent, canvas, world, options_path) # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index e7016634..0ed378f1 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -12,7 +12,7 @@ from .data_types import OperationStorageType if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas import EditCanvas from amulet.api.world import World @@ -34,7 +34,7 @@ def __init__( ): self._path = path self._name = "" - self._ui: Optional[Callable[[wx.Window, "ControllableEditCanvas", "World"], OperationUI]] = None + self._ui: Optional[Callable[[wx.Window, "EditCanvas", "World"], OperationUI]] = None self._load(export_dict) def _load(self, export_dict: dict): @@ -89,7 +89,7 @@ def _load(self, export_dict: dict): def name(self) -> str: return self._name - def setup_ui(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World"): + def setup_ui(self, parent: wx.Window, canvas: "EditCanvas", world: "World"): self._ui(parent, canvas, world) diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index eacf8f2d..b93542ad 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -5,13 +5,13 @@ import weakref if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World class OperationUI: """The base class that all operations must inherit from.""" - def __init__(self, parent: wx.Window, canvas: "ControllableEditCanvas", world: "World", options_path: str): + def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str): self._parent = weakref.ref(parent) self._canvas = weakref.ref(canvas) self._world = weakref.ref(world) @@ -22,7 +22,7 @@ def parent(self) -> wx.Window: return self._parent() @property - def canvas(self) -> "ControllableEditCanvas": + def canvas(self) -> "EditCanvas": return self._canvas() @property diff --git a/amulet_map_editor/programs/edit/ui/file.py b/amulet_map_editor/programs/edit/ui/file.py index 51d2b56f..47a9b700 100644 --- a/amulet_map_editor/programs/edit/ui/file.py +++ b/amulet_map_editor/programs/edit/ui/file.py @@ -6,12 +6,12 @@ from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World class FilePanel(wx.Panel): - def __init__(self, canvas: 'ControllableEditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): + def __init__(self, canvas: 'EditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): wx.Panel.__init__(self, canvas) self._canvas = weakref.ref(canvas) self._world = weakref.ref(world) diff --git a/amulet_map_editor/programs/edit/ui/tool_options/select.py b/amulet_map_editor/programs/edit/ui/tool_options/select.py index b2d19e51..ba2cf2f4 100644 --- a/amulet_map_editor/programs/edit/ui/tool_options/select.py +++ b/amulet_map_editor/programs/edit/ui/tool_options/select.py @@ -3,11 +3,11 @@ import weakref if TYPE_CHECKING: - from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas class SelectOptions(wx.Panel): - def __init__(self, canvas: 'ControllableEditCanvas'): + def __init__(self, canvas: 'EditCanvas'): wx.Panel.__init__(self, canvas) self._canvas = weakref.ref(canvas) self._sizer = wx.BoxSizer(wx.VERTICAL) From 5c7bd8b1f0fb9d63bf287fe54b2a15bfddf1d38d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 27 May 2020 10:51:25 +0100 Subject: [PATCH 19/84] Cleaned up the edit canvas classes --- amulet_map_editor/amulet_wx/key_config.py | 9 +- .../programs/edit/canvas/base_edit_canvas.py | 3 +- .../edit/canvas/controllable_edit_canvas.py | 90 +++++++++---------- .../programs/edit/canvas/edit_canvas.py | 1 + amulet_map_editor/programs/edit/key_config.py | 4 +- 5 files changed, 53 insertions(+), 54 deletions(-) diff --git a/amulet_map_editor/amulet_wx/key_config.py b/amulet_map_editor/amulet_wx/key_config.py index 5d8b6164..90fff80e 100644 --- a/amulet_map_editor/amulet_wx/key_config.py +++ b/amulet_map_editor/amulet_wx/key_config.py @@ -7,10 +7,11 @@ ModifierKeyType = str KeyType = Union[int, str] KeybindGroupIdType = str -KeybindIdType = str +KeyActionType = str ModifierType = Tuple[ModifierKeyType, ...] SerialisedKeyType = Tuple[ModifierType, KeyType] -KeybindGroup = Dict[KeybindIdType, SerialisedKeyType] +KeybindGroup = Dict[KeyActionType, SerialisedKeyType] +ActionLookupType = Dict[SerialisedKeyType, KeyActionType] KeybindContainer = Dict[KeybindGroupIdType, KeybindGroup] MouseLeft = "MOUSE_LEFT" @@ -287,7 +288,7 @@ def __init__( self, parent: wx.Window, selected_group: KeybindGroupIdType, - entries: Sequence[KeybindIdType], + entries: Sequence[KeyActionType], fixed_keybinds: KeybindContainer, user_keybinds: KeybindContainer ): @@ -311,7 +312,7 @@ def __init__( self, parent: wx.Window, selected_group: KeybindGroupIdType, - entries: Sequence[KeybindIdType], + entries: Sequence[KeyActionType], fixed_keybinds: KeybindContainer, user_keybinds: KeybindContainer ): diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 1c0e473d..d4d3cd5b 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -28,6 +28,8 @@ class BaseEditCanvas(BaseCanvas): + """Adds base logic for drawing everything related to the edit program to the BaseCanvas. + All the user interaction code is implemented in ControllableEditCanvas to make them easier to read.""" def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) self._last_mouse_x = 0 @@ -35,7 +37,6 @@ def __init__(self, parent: wx.Window, world: 'World'): self._mouse_delta_x = 0 self._mouse_delta_y = 0 self._mouse_lock = False - self._mouse_moved = False # load the resource packs os.makedirs('resource_packs', exist_ok=True) diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index ce59579d..deef1721 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Set import wx import numpy @@ -7,37 +7,25 @@ from amulet_map_editor.programs.edit.canvas.events import ( CameraMoveEvent, ) -from amulet_map_editor.amulet_wx.key_config import serialise_key_event, KeybindGroup +from amulet_map_editor.amulet_wx.key_config import serialise_key_event, KeybindGroup, ActionLookupType if TYPE_CHECKING: from amulet.api.world import World from amulet_map_editor.programs.edit.edit import EditExtension -key_map = { - 'up': wx.WXK_SPACE, - 'down': wx.WXK_SHIFT, - 'forwards': 87, - 'backwards': 83, - 'left': 65, - 'right': 68, - - 'look_left': 74, - 'look_right': 76, - 'look_up': 73, - 'look_down': 75, -} - - class ControllableEditCanvas(BaseEditCanvas): + """Adds the user interaction logic to BaseEditCanvas""" def __init__(self, world_panel: 'EditExtension', world: 'World'): super().__init__(world_panel, world) - self._persistent_actions = set() - self._key_binds = {} + self._mouse_moved = False # has the mouse position changed since the last frame + self._persistent_actions: Set[str] = set() # wx only fires events for when a key is initially pressed or released. This stores actions for keys that are held down. + self._key_binds: ActionLookupType = {} # a store for which keys run which actions self.Bind(wx.EVT_KILL_FOCUS, self._on_loss_focus) self.Bind(wx.EVT_MOTION, self._on_mouse_motion) + # key press actions self.Bind(wx.EVT_LEFT_DOWN, self._press) self.Bind(wx.EVT_LEFT_UP, self._release) self.Bind(wx.EVT_MIDDLE_DOWN, self._press) @@ -48,6 +36,7 @@ def __init__(self, world_panel: 'EditExtension', world: 'World'): self.Bind(wx.EVT_KEY_UP, self._release) self.Bind(wx.EVT_MOUSEWHEEL, self._release) + # timer to deal with persistent actions self._input_timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self._process_persistent_inputs, self._input_timer) @@ -63,12 +52,15 @@ def set_key_binds(self, key_binds: KeybindGroup): self._key_binds = dict(zip(key_binds.values(), key_binds.keys())) def _press(self, evt): + """Event to handle a number of different key presses""" self._process_action(evt, True) def _release(self, evt): + """Event to handle a number of different key releases""" self._process_action(evt, False) def _process_action(self, evt, press: bool): + """Logic to handle a key being pressed or released.""" key = serialise_key_event(evt) if key is None: return @@ -103,7 +95,8 @@ def _process_action(self, evt, press: bool): self._selection_group.delete_active() else: # run once on button press and frequently until released if action == "box click": - self._box_click() + if self.select_mode == 0: + self.box_select("add box modifier" in self._persistent_actions) elif action == "toggle mouse mode": self._toggle_mouse_lock() elif action == "speed+": @@ -119,17 +112,23 @@ def _process_action(self, evt, press: bool): if a in self._persistent_actions: self._persistent_actions.remove(a) - def _toggle_mouse_lock(self): - self.SetFocus() - if self._mouse_lock: - self._release_mouse() - else: - self.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) - self._mouse_delta_x = self._mouse_delta_y = self._last_mouse_x = self._last_mouse_y = 0 - self._mouse_lock = True - self._change_box_location() + def box_select(self, add_modifier: bool = False): + position = self._selection_group.box_click(add_modifier) + if position is not None: + self.select_distance2 = int(numpy.linalg.norm(position - self.camera_location)) + + # if self._selection_box.select_state <= 1: + # self._selection_box.select_state += 1 + # elif self._selection_box.select_state == 2: + # self._selection_box.point1, self._selection_box.point2 = self._selection_box2.point1, self._selection_box2.point2 + # self._selection_box.select_state = 1 + # x, y, z = self._selection_box.point1 + # wx.PostEvent(self, BoxGreenCornerChangeEvent(x=x, y=y, z=z)) + # wx.PostEvent(self, BoxBlueCornerChangeEvent(x=x, y=y, z=z)) + # wx.PostEvent(self, BoxCoordsEnableEvent(enabled=self._selection_box.select_state == 2)) def _process_persistent_inputs(self, evt): + """Handle actions run by keys that are being held down.""" forward, up, right, pitch, yaw = 0, 0, 0, 0, 0 if 'up' in self._persistent_actions: up += 1 @@ -156,6 +155,7 @@ def _process_persistent_inputs(self, evt): evt.Skip() def move_camera_relative(self, forward, up, right, pitch, yaw): + """Move the camera relative to its current location.""" if (forward, up, right, pitch, yaw) == (0, 0, 0, 0, 0): if not self._mouse_lock and self._mouse_moved: self._mouse_moved = False @@ -174,30 +174,24 @@ def move_camera_relative(self, forward, up, right, pitch, yaw): self._change_box_location() wx.PostEvent(self, CameraMoveEvent(x=self._camera[0], y=self._camera[1], z=self._camera[2], rx=self._camera[3], ry=self._camera[4])) - def box_select(self, add_modifier: bool = False): - position = self._selection_group.box_click(add_modifier) - if position is not None: - self.select_distance2 = int(numpy.linalg.norm(position - self.camera_location)) - - # if self._selection_box.select_state <= 1: - # self._selection_box.select_state += 1 - # elif self._selection_box.select_state == 2: - # self._selection_box.point1, self._selection_box.point2 = self._selection_box2.point1, self._selection_box2.point2 - # self._selection_box.select_state = 1 - # x, y, z = self._selection_box.point1 - # wx.PostEvent(self, BoxGreenCornerChangeEvent(x=x, y=y, z=z)) - # wx.PostEvent(self, BoxBlueCornerChangeEvent(x=x, y=y, z=z)) - # wx.PostEvent(self, BoxCoordsEnableEvent(enabled=self._selection_box.select_state == 2)) - - def _box_click(self): - if self.select_mode == 0: - self.box_select("add box modifier" in self._persistent_actions) + def _toggle_mouse_lock(self): + """Toggle mouse selection mode.""" + self.SetFocus() + if self._mouse_lock: + self._release_mouse() + else: + self.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) + self._mouse_delta_x = self._mouse_delta_y = self._last_mouse_x = self._last_mouse_y = 0 + self._mouse_lock = True + self._change_box_location() def _release_mouse(self): + """Release the mouse""" self.SetCursor(wx.NullCursor) self._mouse_lock = False def _on_mouse_motion(self, evt): + """Event fired when the mouse is moved.""" self.SetFocus() if self._mouse_lock: if self._last_mouse_x == 0: @@ -232,9 +226,11 @@ def _on_mouse_motion(self, evt): self._mouse_moved = True def _on_loss_focus(self, evt): + """Event fired when the user tabs out of the window.""" self._escape() evt.Skip() def _escape(self): + """Release the mouse and remove all key presses to the camera doesn't fly off into the distance.""" self._persistent_actions.clear() self._release_mouse() diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index c4033717..6acbcd2b 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -2,4 +2,5 @@ class EditCanvas(ControllableEditCanvas): + """Adds embedded UI elements to the canvas.""" pass diff --git a/amulet_map_editor/programs/edit/key_config.py b/amulet_map_editor/programs/edit/key_config.py index 2f262bdd..d5285c4b 100644 --- a/amulet_map_editor/programs/edit/key_config.py +++ b/amulet_map_editor/programs/edit/key_config.py @@ -3,7 +3,7 @@ KeybindContainer, KeybindGroup, KeybindGroupIdType, - KeybindIdType, + KeyActionType, Space, Shift, MouseLeft, @@ -15,7 +15,7 @@ ) -KeybindKeys: List[KeybindIdType] = [ +KeybindKeys: List[KeyActionType] = [ "up", "down", "forwards", From 85bae2b0bf3f3f6e3e7fac9f02e042111607e082 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 27 May 2020 11:50:52 +0100 Subject: [PATCH 20/84] Cleaned up camera variables --- amulet_map_editor/opengl/data_types.py | 4 +++ .../opengl/mesh/world_renderer/world.py | 35 ++++++++++++------- .../programs/edit/canvas/base_edit_canvas.py | 33 +++++++++++------ .../edit/canvas/controllable_edit_canvas.py | 26 +++++++------- .../programs/edit/canvas/events.py | 1 + 5 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 amulet_map_editor/opengl/data_types.py diff --git a/amulet_map_editor/opengl/data_types.py b/amulet_map_editor/opengl/data_types.py new file mode 100644 index 00000000..48aded92 --- /dev/null +++ b/amulet_map_editor/opengl/data_types.py @@ -0,0 +1,4 @@ +from typing import Tuple, Union + +CameraLocationType = Tuple[Union[int, float], Union[int, float], Union[int, float]] +CameraRotationType = Tuple[Union[int, float], Union[int, float]] diff --git a/amulet_map_editor/opengl/mesh/world_renderer/world.py b/amulet_map_editor/opengl/mesh/world_renderer/world.py index e18c251f..8672b3e7 100644 --- a/amulet_map_editor/opengl/mesh/world_renderer/world.py +++ b/amulet_map_editor/opengl/mesh/world_renderer/world.py @@ -12,13 +12,13 @@ from amulet_map_editor import log from .chunk import RenderChunk from .region import ChunkManager +from amulet_map_editor.opengl.data_types import CameraLocationType, CameraRotationType from amulet_map_editor.opengl.resource_pack import ResourcePackManager from amulet_map_editor.opengl.mesh.base.tri_mesh import Drawable if TYPE_CHECKING: from amulet.api.world import World - def sin(theta: Union[int, float]) -> float: return math.sin(math.radians(theta)) @@ -137,7 +137,8 @@ def __init__( ): super().__init__(context_identifier, resource_pack, texture, texture_bounds, translator) self._world = world - self._camera = [0, 150, 0, 90, 0] + self._camera_location: CameraLocationType = (0, 150, 0) + self._camera_rotation: CameraRotationType = (90, 0) self._dimension = "overworld" self._render_distance = 10 self._garbage_distance = 20 @@ -178,12 +179,20 @@ def close(self): self.disable() @property - def camera(self): - return self._camera + def camera_location(self) -> CameraLocationType: + return self._camera_location + + @camera_location.setter + def camera_location(self, value: CameraLocationType): + self._camera_location = value + + @property + def camera_rotation(self) -> CameraRotationType: + return self._camera_rotation - @camera.setter - def camera(self, value: List[Union[int, float]]): - self._camera = value + @camera_rotation.setter + def camera_rotation(self, value: CameraRotationType): + self._camera_rotation = value @property def dimension(self) -> str: @@ -218,7 +227,7 @@ def garbage_distance(self, val: int): def chunk_coords(self) -> Generator[Tuple[int, int], None, None]: """Get all of the chunks to draw/load""" - cx, cz = int(self._camera[0]) >> 4, int(self._camera[2]) >> 4 + cx, cz = int(self.camera_location[0]) >> 4, int(self.camera_location[2]) >> 4 sign = 1 length = 1 @@ -233,7 +242,7 @@ def chunk_coords(self) -> Generator[Tuple[int, int], None, None]: length += 1 def draw(self, transformation_matrix: numpy.ndarray): - self._chunk_manager.draw(transformation_matrix, self._camera[:3]) + self._chunk_manager.draw(transformation_matrix, self.camera_location) def run_garbage_collector(self, remove_all=False): if remove_all: @@ -242,10 +251,10 @@ def run_garbage_collector(self, remove_all=False): else: safe_area = ( self._dimension, - self._camera[0]//16 - self.garbage_distance, - self._camera[2]//16 - self.garbage_distance, - self._camera[0]//16 + self.garbage_distance, - self._camera[2]//16 + self.garbage_distance + self.camera_location[0]//16 - self.garbage_distance, + self.camera_location[2]//16 - self.garbage_distance, + self.camera_location[0]//16 + self.garbage_distance, + self.camera_location[2]//16 + self.garbage_distance ) self._chunk_manager.unload(safe_area[1:]) self._world.unload(safe_area) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index d4d3cd5b..c470eed1 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -11,13 +11,14 @@ from amulet.api.data_types import PointCoordinatesNDArray from amulet.api.selection import SelectionGroup +from amulet_map_editor.opengl.data_types import CameraLocationType, CameraRotationType from amulet_map_editor.opengl.mesh.world_renderer.world import RenderWorld, cos, tan, atan from amulet_map_editor.opengl.mesh.selection import RenderSelection, RenderSelectionGroup from amulet_map_editor.opengl.mesh.structure import RenderStructure from amulet_map_editor.opengl import textureatlas from amulet_map_editor.opengl.canvas.base import BaseCanvas from amulet_map_editor import log -from amulet_map_editor.programs.edit.canvas.events import CameraMoveEvent +from amulet_map_editor.programs.edit.canvas.events import CameraMoveEvent, CameraRotateEvent if TYPE_CHECKING: from amulet.api.world import World @@ -32,8 +33,6 @@ class BaseEditCanvas(BaseCanvas): All the user interaction code is implemented in ControllableEditCanvas to make them easier to read.""" def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) - self._last_mouse_x = 0 - self._last_mouse_y = 0 self._mouse_delta_x = 0 self._mouse_delta_y = 0 self._mouse_lock = False @@ -65,7 +64,8 @@ def __init__(self, parent: wx.Window, world: 'World'): self._resource_pack_translator ) - self._camera: List[float] = [0.0, 100.0, 0.0, 45.0, 45.0] + self._camera_location: CameraLocationType = (0.0, 100.0, 0.0) + self._camera_rotation: CameraRotationType = (45.0, 45.0) self._camera_move_speed = 2 self._camera_rotate_speed = 2 self._select_distance = 10 @@ -190,15 +190,28 @@ def dimension(self, dimension: int): self._render_world.dimension = dimension @property - def camera_location(self) -> Tuple[float, float, float]: - return tuple(self._camera[:3]) + def camera_location(self) -> CameraLocationType: + return self._camera_location @camera_location.setter - def camera_location(self, location: Tuple[Union[int, float], Union[int, float], Union[int, float]]): - self._camera[:3] = location + def camera_location(self, location: CameraLocationType): + assert len(location) == 3 and all(isinstance(v, (int, float)) for v in location), "format for camera_location is invalid" + self._camera_location = location self._transformation_matrix = None self._change_box_location() - wx.PostEvent(self, CameraMoveEvent(x=self._camera[0], y=self._camera[1], z=self._camera[2], rx=self._camera[3], ry=self._camera[4])) + wx.PostEvent(self, CameraMoveEvent(location=self.camera_location)) + + @property + def camera_rotation(self) -> CameraRotationType: + return self._camera_rotation + + @camera_rotation.setter + def camera_rotation(self, rotation: CameraRotationType): + assert len(rotation) == 2 and all(isinstance(v, (int, float)) for v in rotation), "format for camera_rotation is invalid" + self._camera_rotation = rotation + self._transformation_matrix = None + self._change_box_location() + wx.PostEvent(self, CameraRotateEvent(rotation=self.camera_rotation)) @property def camera_move_speed(self) -> float: @@ -302,7 +315,7 @@ def _look_vector(self) -> numpy.ndarray: screen_dx = atan(self.aspect_ratio * tan(self.fov / 2) * self._mouse_delta_x / screen_x) screen_dy = atan(cos(screen_dx) * tan(self.fov / 2) * self._mouse_delta_y / screen_y) look_vector = numpy.matmul(self.rotation_matrix(screen_dy, screen_dx), look_vector) - look_vector = numpy.matmul(self.rotation_matrix(*self._camera[3:5]), look_vector)[:3] + look_vector = numpy.matmul(self.rotation_matrix(*self.camera_rotation), look_vector)[:3] look_vector[abs(look_vector) < 0.000001] = 0.000001 return look_vector diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index deef1721..aa6201f5 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -18,6 +18,8 @@ class ControllableEditCanvas(BaseEditCanvas): """Adds the user interaction logic to BaseEditCanvas""" def __init__(self, world_panel: 'EditExtension', world: 'World'): super().__init__(world_panel, world) + self._last_mouse_x = 0 + self._last_mouse_y = 0 self._mouse_moved = False # has the mouse position changed since the last frame self._persistent_actions: Set[str] = set() # wx only fires events for when a key is initially pressed or released. This stores actions for keys that are held down. self._key_binds: ActionLookupType = {} # a store for which keys run which actions @@ -161,18 +163,18 @@ def move_camera_relative(self, forward, up, right, pitch, yaw): self._mouse_moved = False self._change_box_location() return - self._camera[0] += self._camera_move_speed * (cos(self._camera[4]) * right + sin(self._camera[4]) * forward) - self._camera[1] += self._camera_move_speed * up - self._camera[2] += self._camera_move_speed * (sin(self._camera[4]) * right - cos(self._camera[4]) * forward) - - self._camera[3] += self._camera_rotate_speed * pitch - if not -90 <= self._camera[3] <= 90: - self._camera[3] = max(min(self._camera[3], 90), -90) - self._camera[4] += self._camera_rotate_speed * yaw - self._transformation_matrix = None - self._render_world.camera = self._camera - self._change_box_location() - wx.PostEvent(self, CameraMoveEvent(x=self._camera[0], y=self._camera[1], z=self._camera[2], rx=self._camera[3], ry=self._camera[4])) + x, y, z = self.camera_location + rx, ry = self.camera_rotation + x += self._camera_move_speed * (cos(ry) * right + sin(ry) * forward) + y += self._camera_move_speed * up + z += self._camera_move_speed * (sin(ry) * right - cos(ry) * forward) + + rx += self._camera_rotate_speed * pitch + if not -90 <= rx <= 90: + rx = max(min(rx, 90), -90) + ry += self._camera_rotate_speed * yaw + self.camera_location = self._render_world.camera_location = (x, y, z) + self.camera_rotation = self._render_world.camera_rotation = (rx, ry) def _toggle_mouse_lock(self): """Toggle mouse selection mode.""" diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index a544b937..4bcf7ba2 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -2,6 +2,7 @@ CameraMoveEvent, EVT_CAMERA_MOVE = newevent.NewEvent() +CameraRotateEvent, EVT_CAMERA_ROTATE = newevent.NewEvent() ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() From c11c2d445474bfcde9f9c35b08ad072a6b7a2a81 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 27 May 2020 12:02:12 +0100 Subject: [PATCH 21/84] Added some documentation to the base edit canvas --- .../programs/edit/canvas/base_edit_canvas.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index c470eed1..3c992fe7 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -91,13 +91,17 @@ def __init__(self, parent: wx.Window, world: 'World'): @property def selection_group(self) -> SelectionGroup: + """Create a SelectionGroup class from the selected boxes.""" return self._selection_group.create_selection_group() @property def active_selection(self) -> Optional[RenderSelection]: + """Get the selection box that is currently active. + May be None if no selection box exists.""" return self._selection_group.active_selection def enable(self): + """Enable the canvas and start it working.""" self.SetCurrent(self._context) self._render_world.enable() self._draw_timer.Start(33) @@ -105,22 +109,28 @@ def enable(self): self._rebuild_timer.Start(1000) def disable(self): + """Disable the canvas and unload all geometry.""" self._draw_timer.Stop() self._gc_timer.Stop() self._rebuild_timer.Stop() self._render_world.disable() def disable_threads(self): + """Stop the generation of new chunk geometry. + Makes it safe to modify the world data.""" self._render_world.chunk_generator.stop() def enable_threads(self): + """Start the generation of new chunk geometry.""" self._render_world.chunk_generator.start() def close(self): + """Close and destroy the canvas and all contained data.""" self._render_world.close() super()._close() def is_closeable(self): + """Check that the canvas and contained data is safe to be closed.""" return self._render_world.is_closeable() def _load_resource_pack(self, *resource_packs: minecraft_model_reader.JavaRP): @@ -128,6 +138,7 @@ def _load_resource_pack(self, *resource_packs: minecraft_model_reader.JavaRP): self._create_atlas() def _create_atlas(self): + """Create and bind the atlas texture.""" texture_atlas, self._texture_bounds, width, height = textureatlas.create_atlas( self._resource_pack.textures ) @@ -137,7 +148,7 @@ def _create_atlas(self): log.info('Finished setting up texture atlas in OpenGL') @property - def structure(self) -> RenderStructure: + def structure(self) -> Optional[RenderStructure]: return self._structure @structure.setter From 8e135e0bf6b5dea9f3df947ac10bf9a19db49cec Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 27 May 2020 15:01:18 +0100 Subject: [PATCH 22/84] Rearranged a bunch of logic for the UI within the canvas This is still very broken --- .../programs/edit/canvas/base_edit_canvas.py | 8 +- .../edit/canvas/controllable_edit_canvas.py | 6 +- .../programs/edit/canvas/edit_canvas.py | 177 +++++++++++++- .../programs/edit/canvas/events.py | 5 + .../programs/edit/{ => canvas}/ui/__init__.py | 0 .../programs/edit/canvas/ui/base_ui.py | 14 ++ .../programs/edit/{ => canvas}/ui/file.py | 34 ++- .../programs/edit/{ => canvas}/ui/goto.py | 0 .../programs/edit/{ => canvas}/ui/tool.py | 0 .../{ => canvas}/ui/tool_options/__init__.py | 0 .../ui/tool_options/operation/__init__.py | 0 .../ui/tool_options/operation/export_tool.py | 0 .../ui/tool_options/operation/import_tool.py | 0 .../operation/select_destination.py | 0 .../operation/select_operation.py | 0 .../{ => canvas}/ui/tool_options/select.py | 0 amulet_map_editor/programs/edit/edit.py | 225 +++--------------- 17 files changed, 243 insertions(+), 226 deletions(-) rename amulet_map_editor/programs/edit/{ => canvas}/ui/__init__.py (100%) create mode 100644 amulet_map_editor/programs/edit/canvas/ui/base_ui.py rename amulet_map_editor/programs/edit/{ => canvas}/ui/file.py (67%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/goto.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/__init__.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/operation/__init__.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/operation/export_tool.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/operation/import_tool.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/operation/select_destination.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/operation/select_operation.py (100%) rename amulet_map_editor/programs/edit/{ => canvas}/ui/tool_options/select.py (100%) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 3c992fe7..5310b364 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -1,8 +1,9 @@ import wx from OpenGL.GL import * import os -from typing import TYPE_CHECKING, Optional, Any, Dict, Tuple, List, Generator, Union +from typing import TYPE_CHECKING, Optional, Any, Dict, Tuple, List, Generator import numpy +import weakref import minecraft_model_reader from amulet.api.chunk import Chunk @@ -33,6 +34,7 @@ class BaseEditCanvas(BaseCanvas): All the user interaction code is implemented in ControllableEditCanvas to make them easier to read.""" def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) + self._world = weakref.ref(world) self._mouse_delta_x = 0 self._mouse_delta_y = 0 self._mouse_lock = False @@ -89,6 +91,10 @@ def __init__(self, parent: wx.Window, world: 'World'): self._rebuild_timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self._rebuild, self._rebuild_timer) + @property + def world(self) -> "World": + return self._world() + @property def selection_group(self) -> SelectionGroup: """Create a SelectionGroup class from the selected boxes.""" diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index aa6201f5..64b164d1 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -4,19 +4,15 @@ from .base_edit_canvas import BaseEditCanvas from amulet_map_editor.opengl.mesh.world_renderer.world import sin, cos -from amulet_map_editor.programs.edit.canvas.events import ( - CameraMoveEvent, -) from amulet_map_editor.amulet_wx.key_config import serialise_key_event, KeybindGroup, ActionLookupType if TYPE_CHECKING: from amulet.api.world import World - from amulet_map_editor.programs.edit.edit import EditExtension class ControllableEditCanvas(BaseEditCanvas): """Adds the user interaction logic to BaseEditCanvas""" - def __init__(self, world_panel: 'EditExtension', world: 'World'): + def __init__(self, world_panel: wx.Window, world: 'World'): super().__init__(world_panel, world) self._last_mouse_x = 0 self._last_mouse_y = 0 diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 6acbcd2b..42aaa39b 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -1,6 +1,179 @@ -from .controllable_edit_canvas import ControllableEditCanvas +import wx +from typing import TYPE_CHECKING, Callable, Any +from types import GeneratorType +import time + +from amulet.api.data_types import OperationReturnType + +from amulet_map_editor import CONFIG +from amulet_map_editor.programs.edit.edit import EDIT_CONFIG_ID +from amulet_map_editor.programs.edit.key_config import DefaultKeys, DefaultKeybindGroupId, PresetKeybinds +from amulet_map_editor.programs.edit.canvas.ui.goto import show_goto + +from amulet_map_editor.programs.edit.canvas.events import ( + EVT_SELECT_TOOL_ENABLED, + EVT_OPERATION_TOOL_ENABLED, + EVT_EXPORT_TOOL_ENABLED, + EVT_IMPORT_TOOL_ENABLED, + EVT_CAMERA_MOVE, +) +from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas +from amulet_map_editor.programs.edit.canvas.ui.file import FilePanel +from amulet_map_editor.programs.edit.canvas.ui.tool_options.operation import OperationUI +from amulet_map_editor.programs.edit.canvas.ui.tool_options.select import SelectOptions +from amulet_map_editor.programs.edit.canvas.ui.tool import ToolSelect + +if TYPE_CHECKING: + from amulet.api.world import World + + +def show_loading_dialog( + run: Callable[[], OperationReturnType], title: str, message: str, parent: wx.Window +) -> Any: + dialog = wx.ProgressDialog( + title, + message, + parent=parent, + style=wx.PD_APP_MODAL + | wx.PD_ELAPSED_TIME + | wx.PD_REMAINING_TIME + | wx.PD_AUTO_HIDE, + ) + t = time.time() + try: + obj = run() + if isinstance(obj, GeneratorType): + try: + while True: + progress = next(obj) + if isinstance(progress, (list, tuple)): + if len(progress) >= 2: + message = progress[1] + if len(progress) >= 1: + progress = progress[0] + if isinstance(progress, (int, float)) and isinstance(message, str): + dialog.Update(min(99.99, max(0, progress)), message) + except StopIteration as e: + obj = e.value + except Exception as e: + dialog.Destroy() + raise e + time.sleep(max(0.2 - time.time() + t, 0)) + dialog.Destroy() + return obj class EditCanvas(ControllableEditCanvas): """Adds embedded UI elements to the canvas.""" - pass + def __init__(self, parent: wx.Window, world: World): + super().__init__(parent, world) + config = CONFIG.get(EDIT_CONFIG_ID, {}) + user_keybinds = config.get("user_keybinds", {}) + group = config.get("keybind_group", DefaultKeybindGroupId) + if group in user_keybinds: + keybinds = user_keybinds[group] + elif group in PresetKeybinds: + keybinds = PresetKeybinds[group] + else: + keybinds = DefaultKeys + self.set_key_binds( + keybinds + ) + + self._file_panel = FilePanel(self) + self._select_options = SelectOptions(self) + self._operation_options = OperationUI( + self, self._world, self._run_operation_event, self._run_main_operation + ) + self._tool_panel = ToolSelect(self) + + self.Bind(EVT_CAMERA_MOVE, self._file_panel.move_event) + self._tool_panel.Bind(EVT_SELECT_TOOL_ENABLED, self.show_select_options) + self._tool_panel.Bind( + EVT_OPERATION_TOOL_ENABLED, self.show_operation_options + ) + self._tool_panel.Bind(EVT_IMPORT_TOOL_ENABLED, self.show_import_options) + self._tool_panel.Bind(EVT_EXPORT_TOOL_ENABLED, self.show_export_options) + + self._file_panel.Hide() + self._select_options.Hide() + self._operation_options.Hide() + self._tool_panel.Hide() + + canvas_sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(canvas_sizer) + bottom_sizer0 = wx.BoxSizer(wx.HORIZONTAL) + middle_sizer0 = wx.BoxSizer(wx.VERTICAL) + top_sizer0 = wx.BoxSizer(wx.HORIZONTAL) + top_sizer0.AddStretchSpacer(1) + top_sizer0.Add(self._file_panel, 0, wx.EXPAND, 0) + canvas_sizer.Add(top_sizer0, 0, wx.EXPAND, 0) + middle_sizer0.AddStretchSpacer(1) + middle_sizer0.Add(self._select_options, 0, 0, 0) + middle_sizer0.Add(self._operation_options, 0, 0, 0) + middle_sizer0.AddStretchSpacer(1) + canvas_sizer.Add(middle_sizer0, 1, wx.EXPAND, 0) + bottom_sizer0.AddStretchSpacer(1) + bottom_sizer0.Add(self._tool_panel, 0, wx.EXPAND, 0) + bottom_sizer0.AddStretchSpacer(1) + canvas_sizer.Add(bottom_sizer0, 0, wx.EXPAND, 0) + + self._file_panel.Show() + self._select_options.Show() + self._tool_panel.Show() + + def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: + self._canvas.disable_threads() + try: + out = show_loading_dialog( + operation, + title, + msg, + self, + ) + self._world.create_undo_point() + self._file_panel.update_buttons() + except Exception as e: + self._canvas.enable_threads() + raise e + self._canvas.enable_threads() + return out + + def undo(self): + self.world.undo() + self._file_panel.update_buttons() + + def redo(self): + self.world.redo() + self._file_panel.update_buttons() + + def cut(self): + pass + + def copy(self): + pass + + def paste(self): + pass + + def delete(self): + pass + + def goto(self): + location = show_goto(self, *self.camera_location) + if location: + self.camera_location = location + + def save(self): + self.disable_threads() + + def save(): + for chunk_index, chunk_count in self._world.save_iter(): + yield chunk_index / chunk_count + + show_loading_dialog(lambda: save(), f"Saving world.", "Please wait.", self) + self._file_panel.update_buttons() + self.enable_threads() + + def close(self): + pass diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index 4bcf7ba2..8d2395df 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -6,11 +6,16 @@ ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() +# TODO: reimplement these using the above SelectToolEnabledEvent, EVT_SELECT_TOOL_ENABLED = newevent.NewEvent() OperationToolEnabledEvent, EVT_OPERATION_TOOL_ENABLED = newevent.NewEvent() ImportToolEnabledEvent, EVT_IMPORT_TOOL_ENABLED = newevent.NewEvent() ExportToolEnabledEvent, EVT_EXPORT_TOOL_ENABLED = newevent.NewEvent() +UndoEvent, EVT_UNDO = newevent.NewEvent() +RedoEvent, EVT_REDO = newevent.NewEvent() +SaveEvent, EVT_SAVE = newevent.NewEvent() + BoxGreenCornerChangeEvent, EVT_BOX_GREEN_CORNER_CHANGE = newevent.NewEvent() BoxBlueCornerChangeEvent, EVT_BOX_BLUE_CORNER_CHANGE = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/ui/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/__init__.py rename to amulet_map_editor/programs/edit/canvas/ui/__init__.py diff --git a/amulet_map_editor/programs/edit/canvas/ui/base_ui.py b/amulet_map_editor/programs/edit/canvas/ui/base_ui.py new file mode 100644 index 00000000..5836d9d7 --- /dev/null +++ b/amulet_map_editor/programs/edit/canvas/ui/base_ui.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING +import weakref + +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class BaseUI: + def __init__(self, canvas: "EditCanvas"): + self._canvas = weakref.ref(canvas) + + @property + def canvas(self) -> "EditCanvas": + return self._canvas() diff --git a/amulet_map_editor/programs/edit/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py similarity index 67% rename from amulet_map_editor/programs/edit/ui/file.py rename to amulet_map_editor/programs/edit/canvas/ui/file.py index 47a9b700..90a98461 100644 --- a/amulet_map_editor/programs/edit/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -2,7 +2,7 @@ import wx import weakref -from .goto import show_goto +from .base_ui import BaseUI from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny if TYPE_CHECKING: @@ -10,22 +10,21 @@ from amulet.api.world import World -class FilePanel(wx.Panel): - def __init__(self, canvas: 'EditCanvas', world: 'World', undo_evt, redo_evt, save_evt, close_evt): +class FilePanel(wx.Panel, BaseUI): + def __init__(self, canvas: 'EditCanvas'): wx.Panel.__init__(self, canvas) - self._canvas = weakref.ref(canvas) - self._world = weakref.ref(world) + BaseUI.__init__(self, canvas) top_sizer = wx.BoxSizer(wx.HORIZONTAL) self._location_button = wx.Button(self, label=', '.join([f'{s:.2f}' for s in self._canvas().camera_location])) - self._location_button.Bind(wx.EVT_BUTTON, lambda evt: self.show_goto()) + self._location_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.goto()) top_sizer.Add(self._location_button, 0, wx.ALL | wx.CENTER, 5) dim_label = wx.StaticText(self, label="Dimension:") self._dim_options = SimpleChoiceAny(self) - self._dim_options.SetItems(self._world().world_wrapper.dimensions) + self._dim_options.SetItems(self.canvas.world.world_wrapper.dimensions) self._dim_options.SetValue("overworld") self._dim_options.Bind(wx.EVT_CHOICE, self._on_dimension_change) @@ -38,10 +37,10 @@ def create_button(text, operation): top_sizer.Add(button, 0, wx.ALL, 5) return button - self._undo_button: Optional[wx.Button] = create_button('Undo', undo_evt) - self._redo_button: Optional[wx.Button] = create_button('Redo', redo_evt) - self._save_button: Optional[wx.Button] = create_button('Save', save_evt) - create_button('Close', close_evt) + self._undo_button: Optional[wx.Button] = create_button('Undo', lambda evt: self.canvas.undo()) + self._redo_button: Optional[wx.Button] = create_button('Redo', lambda evt: self.canvas.redo()) + self._save_button: Optional[wx.Button] = create_button('Save', lambda evt: self.canvas.save()) + create_button('Close', lambda evt: self.canvas.close()) self.update_buttons() self.SetSizer(top_sizer) @@ -49,9 +48,9 @@ def create_button(text, operation): self.Layout() def update_buttons(self): - self._undo_button.SetLabel(f"Undo | {self._world().chunk_history_manager.undo_count}") - self._redo_button.SetLabel(f"Redo | {self._world().chunk_history_manager.redo_count}") - self._save_button.SetLabel(f"Save | {self._world().chunk_history_manager.unsaved_changes}") + self._undo_button.SetLabel(f"Undo | {self.canvas.world.chunk_history_manager.undo_count}") + self._redo_button.SetLabel(f"Redo | {self.canvas.world.chunk_history_manager.redo_count}") + self._save_button.SetLabel(f"Save | {self.canvas.world.chunk_history_manager.unsaved_changes}") def _on_dimension_change(self, evt): self.change_dimension() @@ -60,14 +59,9 @@ def _on_dimension_change(self, evt): def change_dimension(self): dimension = self._dim_options.GetAny() if dimension is not None: - self._canvas().dimension = dimension + self.canvas.dimension = dimension def move_event(self, evt): self._location_button.SetLabel(f'{evt.x:.2f}, {evt.y:.2f}, {evt.z:.2f}') self.Layout() self.GetParent().Layout() - - def show_goto(self): - location = show_goto(self, *self._canvas().camera_location) - if location: - self._canvas().camera_location = location diff --git a/amulet_map_editor/programs/edit/ui/goto.py b/amulet_map_editor/programs/edit/canvas/ui/goto.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/goto.py rename to amulet_map_editor/programs/edit/canvas/ui/goto.py diff --git a/amulet_map_editor/programs/edit/ui/tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool.py rename to amulet_map_editor/programs/edit/canvas/ui/tool.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/__init__.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/__init__.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/operation/__init__.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/__init__.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/export_tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/export_tool.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/operation/export_tool.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/export_tool.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/import_tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/import_tool.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/operation/import_tool.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/import_tool.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/select_destination.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_destination.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/operation/select_destination.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_destination.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_operation.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/operation/select_operation.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_operation.py diff --git a/amulet_map_editor/programs/edit/ui/tool_options/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/select.py similarity index 100% rename from amulet_map_editor/programs/edit/ui/tool_options/select.py rename to amulet_map_editor/programs/edit/canvas/ui/tool_options/select.py diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index a11f6929..7cb0d903 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -1,11 +1,6 @@ import wx -from typing import TYPE_CHECKING, Optional, Callable, Any -from types import GeneratorType +from typing import TYPE_CHECKING, Optional import webbrowser -import time - -from amulet.api.selection import SelectionGroup -from amulet.api.data_types import OperationReturnType from amulet_map_editor import CONFIG from amulet_map_editor.programs import BaseWorldProgram, MenuData @@ -13,19 +8,7 @@ from .canvas.edit_canvas import EditCanvas -from .ui.file import FilePanel -from .ui.tool_options.operation import OperationUI -from .ui.tool_options.select import SelectOptions -from .ui.tool import ToolSelect -from .key_config import DefaultKeys, DefaultKeybindGroupId, PresetKeybinds, KeybindKeys - -from amulet_map_editor.programs.edit.canvas.events import ( - EVT_CAMERA_MOVE, - EVT_SELECT_TOOL_ENABLED, - EVT_OPERATION_TOOL_ENABLED, - EVT_IMPORT_TOOL_ENABLED, - EVT_EXPORT_TOOL_ENABLED, -) +from .key_config import DefaultKeybindGroupId, PresetKeybinds, KeybindKeys if TYPE_CHECKING: from amulet.api.world import World @@ -33,42 +16,6 @@ EDIT_CONFIG_ID = "amulet_edit" -def show_loading_dialog( - run: Callable[[], OperationReturnType], title: str, message: str, parent: wx.Window -) -> Any: - dialog = wx.ProgressDialog( - title, - message, - parent=parent, - style=wx.PD_APP_MODAL - | wx.PD_ELAPSED_TIME - | wx.PD_REMAINING_TIME - | wx.PD_AUTO_HIDE, - ) - t = time.time() - try: - obj = run() - if isinstance(obj, GeneratorType): - try: - while True: - progress = next(obj) - if isinstance(progress, (list, tuple)): - if len(progress) >= 2: - message = progress[1] - if len(progress) >= 1: - progress = progress[0] - if isinstance(progress, (int, float)) and isinstance(message, str): - dialog.Update(min(99.99, max(0, progress)), message) - except StopIteration as e: - obj = e.value - except Exception as e: - dialog.Destroy() - raise e - time.sleep(max(0.2 - time.time() + t, 0)) - dialog.Destroy() - return obj - - class EditExtension(wx.Panel, BaseWorldProgram): def __init__(self, parent, world: "World"): wx.Panel.__init__(self, parent) @@ -80,11 +27,6 @@ def __init__(self, parent, world: "World"): self._temp.SetFont(wx.Font(40, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)) self._sizer.Add(self._temp) - self._file_panel: Optional[FilePanel] = None - self._select_options: Optional[SelectOptions] = None - self._operation_options: Optional[OperationUI] = None - self._tool_panel: Optional[ToolSelect] = None - self.Bind(wx.EVT_SIZE, self._on_resize) def enable(self): @@ -92,78 +34,15 @@ def enable(self): self.Update() self._canvas = EditCanvas(self, self._world) - - config = CONFIG.get(EDIT_CONFIG_ID, {}) - user_keybinds = config.get("user_keybinds", {}) - group = config.get("keybind_group", DefaultKeybindGroupId) - if group in user_keybinds: - keybinds = user_keybinds[group] - elif group in PresetKeybinds: - keybinds = PresetKeybinds[group] - else: - keybinds = DefaultKeys - self._canvas.set_key_binds( - keybinds - ) - - self._file_panel = FilePanel( - self._canvas, - self._world, - self._undo, - self._redo, - self._save_world, - self._close_world, - ) - self._select_options = SelectOptions(self._canvas) - self._operation_options = OperationUI( - self._canvas, self._world, self._run_operation_event, self._run_main_operation - ) - self._tool_panel = ToolSelect(self._canvas) - - self._canvas.Bind(EVT_CAMERA_MOVE, self._file_panel.move_event) - self._tool_panel.Bind(EVT_SELECT_TOOL_ENABLED, self.show_select_options) - self._tool_panel.Bind( - EVT_OPERATION_TOOL_ENABLED, self.show_operation_options - ) - self._tool_panel.Bind(EVT_IMPORT_TOOL_ENABLED, self.show_import_options) - self._tool_panel.Bind(EVT_EXPORT_TOOL_ENABLED, self.show_export_options) - - self._file_panel.Hide() - self._select_options.Hide() - self._operation_options.Hide() - self._tool_panel.Hide() - self._sizer.Add(self._canvas, 1, wx.EXPAND) - canvas_sizer = wx.BoxSizer(wx.VERTICAL) - self._canvas.SetSizer(canvas_sizer) - bottom_sizer0 = wx.BoxSizer(wx.HORIZONTAL) - middle_sizer0 = wx.BoxSizer(wx.VERTICAL) - top_sizer0 = wx.BoxSizer(wx.HORIZONTAL) - top_sizer0.AddStretchSpacer(1) - top_sizer0.Add(self._file_panel, 0, wx.EXPAND, 0) - canvas_sizer.Add(top_sizer0, 0, wx.EXPAND, 0) - middle_sizer0.AddStretchSpacer(1) - middle_sizer0.Add(self._select_options, 0, 0, 0) - middle_sizer0.Add(self._operation_options, 0, 0, 0) - middle_sizer0.AddStretchSpacer(1) - canvas_sizer.Add(middle_sizer0, 1, wx.EXPAND, 0) - bottom_sizer0.AddStretchSpacer(1) - bottom_sizer0.Add(self._tool_panel, 0, wx.EXPAND, 0) - bottom_sizer0.AddStretchSpacer(1) - canvas_sizer.Add(bottom_sizer0, 0, wx.EXPAND, 0) - self._temp.Destroy() - self._file_panel.Show() - self._select_options.Show() - self._tool_panel.Show() self.Layout() self._canvas.Update() self._canvas.enable() self._canvas.set_size(self.GetSize()[0], self.GetSize()[1]) self._canvas.draw() - self._file_panel.change_dimension() def disable(self): if self._canvas is not None: @@ -191,36 +70,36 @@ def _close_world(self, _): ) response = msg.ShowModal() if response == wx.ID_YES: - self._save_world() + self._canvas.save() elif response == wx.ID_CANCEL: return self.GetGrandParent().GetParent().close_world(self._world.world_path) def menu(self, menu: MenuData) -> MenuData: menu.setdefault("&File", {}).setdefault("system", {}).setdefault( - "Save\tCtrl+s", lambda evt: self._save_world() + "Save\tCtrl+s", lambda evt: self._canvas.save() ) # menu.setdefault('&File', {}).setdefault('system', {}).setdefault('Save As', lambda evt: self.GetGrandParent().close_world(self.world.world_path)) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Undo\tCtrl+z", lambda evt: self._undo() + "Undo\tCtrl+z", lambda evt: self._canvas.undo() ) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Redo\tCtrl+y", lambda evt: self._redo() + "Redo\tCtrl+y", lambda evt: self._canvas.redo() ) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Cut\tCtrl+x", lambda evt: self._cut() + "Cut\tCtrl+x", lambda evt: self._canvas.cut() ) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Copy\tCtrl+c", lambda evt: self._copy() + "Copy\tCtrl+c", lambda evt: self._canvas.copy() ) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Paste\tCtrl+v", lambda evt: self._paste() + "Paste\tCtrl+v", lambda evt: self._canvas.paste() ) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Delete\tDelete", lambda evt: self._delete() + "Delete\tDelete", lambda evt: self._canvas.delete() ) menu.setdefault("&Edit", {}).setdefault("shortcut", {}).setdefault( - "Goto\tCtrl+g", lambda evt: self._file_panel.show_goto() + "Goto\tCtrl+g", lambda evt: self._canvas.goto() ) menu.setdefault("&Options", {}).setdefault("options", {}).setdefault( "Controls...", lambda evt: self._edit_controls() @@ -253,56 +132,6 @@ def _on_resize(self, event): self._canvas.set_size(self.GetSize()[0], self.GetSize()[1]) event.Skip() - def _undo(self, *_): - self._world.undo() - self._file_panel.update_buttons() - - def _redo(self, *_): - self._world.redo() - self._file_panel.update_buttons() - - def _save_world(self, *_): - self._canvas.disable_threads() - - def save(): - for chunk_index, chunk_count in self._world.save_iter(): - yield 100 * chunk_index / chunk_count - - show_loading_dialog(lambda: save(), f"Saving world.", "Please wait.", self) - self._file_panel.update_buttons() - self._canvas.enable_threads() - - def _get_box(self) -> Optional[SelectionGroup]: - group = self._canvas.selection_group - if group.selection_boxes: - return group - else: - wx.MessageBox( - "You must select an area of the world before running this operation" - ) - return None - - def _run_operation_event(self, evt): - self._run_operation() - evt.Skip() - - def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: - self._canvas.disable_threads() - try: - out = show_loading_dialog( - operation, - title, - msg, - self, - ) - self._world.create_undo_point() - self._file_panel.update_buttons() - except Exception as e: - self._canvas.enable_threads() - raise e - self._canvas.enable_threads() - return out - # TODO # def _cut(self) -> bool: # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'cut.py')) @@ -317,21 +146,21 @@ def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: # def _delete(self) -> bool: # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'delete.py')) - def show_select_options(self, _): - self._operation_options.Hide() - self._select_options.enable() - self.Layout() - - def _show_operation_options(self, enable: Callable): - self._select_options.Hide() - enable() - self.Layout() + # def show_select_options(self, _): + # self._operation_options.Hide() + # self._select_options.enable() + # self.Layout() - def show_operation_options(self, _): - self._show_operation_options(self._operation_options.enable_operation_ui) + # def _show_operation_options(self, enable: Callable): + # self._select_options.Hide() + # enable() + # self.Layout() - def show_import_options(self, _): - self._show_operation_options(self._operation_options.enable_import_ui) - - def show_export_options(self, _): - self._show_operation_options(self._operation_options.enable_export_ui) + # def show_operation_options(self, _): + # self._show_operation_options(self._operation_options.enable_operation_ui) + # + # def show_import_options(self, _): + # self._show_operation_options(self._operation_options.enable_import_ui) + # + # def show_export_options(self, _): + # self._show_operation_options(self._operation_options.enable_export_ui) From 1e501e10e77e3e116a55d04c66191d864659641b Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 28 May 2020 12:41:47 +0100 Subject: [PATCH 23/84] Redesigned the tool selection UI --- .../programs/edit/canvas/edit_canvas.py | 54 ++------- .../programs/edit/canvas/events.py | 6 - .../programs/edit/canvas/ui/__init__.py | 1 + .../programs/edit/canvas/ui/file.py | 2 - .../programs/edit/canvas/ui/tool.py | 44 ------- .../programs/edit/canvas/ui/tool/__init__.py | 1 + .../programs/edit/canvas/ui/tool/tool.py | 74 ++++++++++++ .../{tool_options => tool/tools}/__init__.py | 0 .../tools/base_operation.py} | 0 .../edit/canvas/ui/tool/tools/base_tool_ui.py | 13 +++ .../operation => tool/tools}/export_tool.py | 4 +- .../operation => tool/tools}/import_tool.py | 4 +- .../edit/canvas/ui/tool/tools/operation.py | 8 ++ .../tools}/operation/select_destination.py | 0 .../ui/{tool_options => tool/tools}/select.py | 9 +- .../ui/tool_options/operation/__init__.py | 108 ------------------ amulet_map_editor/programs/edit/edit.py | 3 +- .../programs/edit/plugins/api/loader.py | 2 + 18 files changed, 120 insertions(+), 213 deletions(-) delete mode 100644 amulet_map_editor/programs/edit/canvas/ui/tool.py create mode 100644 amulet_map_editor/programs/edit/canvas/ui/tool/__init__.py create mode 100644 amulet_map_editor/programs/edit/canvas/ui/tool/tool.py rename amulet_map_editor/programs/edit/canvas/ui/{tool_options => tool/tools}/__init__.py (100%) rename amulet_map_editor/programs/edit/canvas/ui/{tool_options/operation/select_operation.py => tool/tools/base_operation.py} (100%) create mode 100644 amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py rename amulet_map_editor/programs/edit/canvas/ui/{tool_options/operation => tool/tools}/export_tool.py (59%) rename amulet_map_editor/programs/edit/canvas/ui/{tool_options/operation => tool/tools}/import_tool.py (59%) create mode 100644 amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation.py rename amulet_map_editor/programs/edit/canvas/ui/{tool_options => tool/tools}/operation/select_destination.py (100%) rename amulet_map_editor/programs/edit/canvas/ui/{tool_options => tool/tools}/select.py (95%) delete mode 100644 amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/__init__.py diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 42aaa39b..3215bd6d 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -9,19 +9,13 @@ from amulet_map_editor.programs.edit.edit import EDIT_CONFIG_ID from amulet_map_editor.programs.edit.key_config import DefaultKeys, DefaultKeybindGroupId, PresetKeybinds from amulet_map_editor.programs.edit.canvas.ui.goto import show_goto +from amulet_map_editor.programs.edit.canvas.ui.tool import Tool from amulet_map_editor.programs.edit.canvas.events import ( - EVT_SELECT_TOOL_ENABLED, - EVT_OPERATION_TOOL_ENABLED, - EVT_EXPORT_TOOL_ENABLED, - EVT_IMPORT_TOOL_ENABLED, EVT_CAMERA_MOVE, ) from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet_map_editor.programs.edit.canvas.ui.file import FilePanel -from amulet_map_editor.programs.edit.canvas.ui.tool_options.operation import OperationUI -from amulet_map_editor.programs.edit.canvas.ui.tool_options.select import SelectOptions -from amulet_map_editor.programs.edit.canvas.ui.tool import ToolSelect if TYPE_CHECKING: from amulet.api.world import World @@ -65,7 +59,7 @@ def show_loading_dialog( class EditCanvas(ControllableEditCanvas): """Adds embedded UI elements to the canvas.""" - def __init__(self, parent: wx.Window, world: World): + def __init__(self, parent: wx.Window, world: "World"): super().__init__(parent, world) config = CONFIG.get(EDIT_CONFIG_ID, {}) user_keybinds = config.get("user_keybinds", {}) @@ -81,46 +75,18 @@ def __init__(self, parent: wx.Window, world: World): ) self._file_panel = FilePanel(self) - self._select_options = SelectOptions(self) - self._operation_options = OperationUI( - self, self._world, self._run_operation_event, self._run_main_operation - ) - self._tool_panel = ToolSelect(self) - self.Bind(EVT_CAMERA_MOVE, self._file_panel.move_event) - self._tool_panel.Bind(EVT_SELECT_TOOL_ENABLED, self.show_select_options) - self._tool_panel.Bind( - EVT_OPERATION_TOOL_ENABLED, self.show_operation_options - ) - self._tool_panel.Bind(EVT_IMPORT_TOOL_ENABLED, self.show_import_options) - self._tool_panel.Bind(EVT_EXPORT_TOOL_ENABLED, self.show_export_options) - - self._file_panel.Hide() - self._select_options.Hide() - self._operation_options.Hide() - self._tool_panel.Hide() canvas_sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(canvas_sizer) - bottom_sizer0 = wx.BoxSizer(wx.HORIZONTAL) - middle_sizer0 = wx.BoxSizer(wx.VERTICAL) - top_sizer0 = wx.BoxSizer(wx.HORIZONTAL) - top_sizer0.AddStretchSpacer(1) - top_sizer0.Add(self._file_panel, 0, wx.EXPAND, 0) - canvas_sizer.Add(top_sizer0, 0, wx.EXPAND, 0) - middle_sizer0.AddStretchSpacer(1) - middle_sizer0.Add(self._select_options, 0, 0, 0) - middle_sizer0.Add(self._operation_options, 0, 0, 0) - middle_sizer0.AddStretchSpacer(1) - canvas_sizer.Add(middle_sizer0, 1, wx.EXPAND, 0) - bottom_sizer0.AddStretchSpacer(1) - bottom_sizer0.Add(self._tool_panel, 0, wx.EXPAND, 0) - bottom_sizer0.AddStretchSpacer(1) - canvas_sizer.Add(bottom_sizer0, 0, wx.EXPAND, 0) - - self._file_panel.Show() - self._select_options.Show() - self._tool_panel.Show() + + file_sizer = wx.BoxSizer(wx.HORIZONTAL) + file_sizer.AddStretchSpacer(1) + file_sizer.Add(self._file_panel, 0, wx.EXPAND, 0) + canvas_sizer.Add(file_sizer, 0, wx.EXPAND, 0) + + tool_sizer = Tool(self) + canvas_sizer.Add(tool_sizer, 1, wx.EXPAND, 0) def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: self._canvas.disable_threads() diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index 8d2395df..b122cee0 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -6,12 +6,6 @@ ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() -# TODO: reimplement these using the above -SelectToolEnabledEvent, EVT_SELECT_TOOL_ENABLED = newevent.NewEvent() -OperationToolEnabledEvent, EVT_OPERATION_TOOL_ENABLED = newevent.NewEvent() -ImportToolEnabledEvent, EVT_IMPORT_TOOL_ENABLED = newevent.NewEvent() -ExportToolEnabledEvent, EVT_EXPORT_TOOL_ENABLED = newevent.NewEvent() - UndoEvent, EVT_UNDO = newevent.NewEvent() RedoEvent, EVT_REDO = newevent.NewEvent() SaveEvent, EVT_SAVE = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/canvas/ui/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/__init__.py index e69de29b..358c9b42 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/__init__.py +++ b/amulet_map_editor/programs/edit/canvas/ui/__init__.py @@ -0,0 +1 @@ +from .base_ui import BaseUI \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index 90a98461..126f223d 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -1,13 +1,11 @@ from typing import TYPE_CHECKING, Optional import wx -import weakref from .base_ui import BaseUI from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas - from amulet.api.world import World class FilePanel(wx.Panel, BaseUI): diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool.py deleted file mode 100644 index 7aa4fd83..00000000 --- a/amulet_map_editor/programs/edit/canvas/ui/tool.py +++ /dev/null @@ -1,44 +0,0 @@ -import wx - -from amulet_map_editor.programs.edit.canvas.events import ( - SelectToolEnabledEvent, - OperationToolEnabledEvent, - ImportToolEnabledEvent, - ExportToolEnabledEvent -) - - -class ToolSelect(wx.Panel): - def __init__(self, canvas): - wx.Panel.__init__(self, canvas) - - self.select_button = wx.Button(self, label="Select") - self.select_button.Bind(wx.EVT_BUTTON, self._select_evt) - self.operation_button = wx.Button(self, label="Operation") - self.operation_button.Bind(wx.EVT_BUTTON, self._operation_evt) - - self.import_button = wx.Button(self, label="Import") - self.import_button.Bind(wx.EVT_BUTTON, self._import_evt) - self.export_button = wx.Button(self, label="Export") - self.export_button.Bind(wx.EVT_BUTTON, self._export_evt) - - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(self.select_button) - sizer.Add(self.operation_button) - sizer.Add(self.import_button) - sizer.Add(self.export_button) - self.SetSizer(sizer) - sizer.Fit(self) - self.Layout() - - def _select_evt(self, evt): - wx.PostEvent(self, SelectToolEnabledEvent()) - - def _operation_evt(self, evt): - wx.PostEvent(self, OperationToolEnabledEvent()) - - def _import_evt(self, evt): - wx.PostEvent(self, ImportToolEnabledEvent()) - - def _export_evt(self, evt): - wx.PostEvent(self, ExportToolEnabledEvent()) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/tool/__init__.py new file mode 100644 index 00000000..f3f99636 --- /dev/null +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/__init__.py @@ -0,0 +1 @@ +from .tool import Tool \ No newline at end of file diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py new file mode 100644 index 00000000..29f4faa2 --- /dev/null +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py @@ -0,0 +1,74 @@ +import wx +from typing import TYPE_CHECKING, Type, Dict, Optional + +from amulet_map_editor.programs.edit.canvas.ui import BaseUI +from amulet_map_editor.programs.edit.canvas.ui.tool.tools.base_tool_ui import BaseToolUI, BaseToolUIType +from amulet_map_editor.programs.edit.canvas.events import ( + ToolChangeEvent, + EVT_TOOL_CHANGE +) + +from .tools.select import SelectOptions +from .tools.operation import SelectOperationUI +from .tools.import_tool import SelectImportOperationUI +from .tools.export_tool import SelectExportOperationUI + +if TYPE_CHECKING: + from ...edit_canvas import EditCanvas + + +class Tool(wx.BoxSizer, BaseUI): + def __init__(self, canvas: "EditCanvas"): + wx.BoxSizer.__init__(self, wx.VERTICAL) + BaseUI.__init__(self, canvas) + + self._tools: Dict[str, BaseToolUIType] = {} + self._active_tool: Optional[BaseToolUIType] = None + + self._tool_option_sizer = wx.BoxSizer(wx.VERTICAL) + self.Add(self._tool_option_sizer, 1, wx.EXPAND, 0) + + tool_select_sizer = wx.BoxSizer(wx.HORIZONTAL) + tool_select_sizer.AddStretchSpacer(1) + self._tool_select = ToolSelect(canvas) + tool_select_sizer.Add(self._tool_select, 0, wx.EXPAND, 0) + tool_select_sizer.AddStretchSpacer(1) + self.Add(tool_select_sizer, 0, wx.EXPAND, 0) + + self.canvas.Bind(EVT_TOOL_CHANGE, lambda evt: self._enable_tool(evt.tool)) + + self.register_tool("Select", SelectOptions) + self.register_tool("Operation", SelectOperationUI) + self.register_tool("Import", SelectImportOperationUI) + self.register_tool("Export", SelectExportOperationUI) + + def register_tool(self, name: str, tool_cls: Type[BaseToolUIType]): + assert issubclass(tool_cls, (wx.Window, wx.Sizer)) and issubclass(tool_cls, BaseToolUI) + self._tool_select.register_tool(name) + tool = tool_cls(self.canvas) + tool.Hide() + self._tools[name] = tool + self._tool_option_sizer.Add(tool, 1, wx.EXPAND, 0) + + def _enable_tool(self, tool: str): + if tool in self._tools: + if self._active_tool is not None: + self._active_tool.Hide() + self._active_tool = self._tools[tool] + self._active_tool.Show() + + +class ToolSelect(wx.Panel, BaseUI): + def __init__(self, canvas: "EditCanvas"): + wx.Panel.__init__(self, canvas) + BaseUI.__init__(self, canvas) + + self._sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(self._sizer) + + def register_tool(self, name: str): + button = wx.Button(self, label=name) + self._sizer.Add(button) + self._sizer.Fit(self) + self.Layout() + button.Bind(wx.EVT_BUTTON, lambda evt: wx.PostEvent(self.canvas, ToolChangeEvent(tool=name))) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/__init__.py similarity index 100% rename from amulet_map_editor/programs/edit/canvas/ui/tool_options/__init__.py rename to amulet_map_editor/programs/edit/canvas/ui/tool/tools/__init__.py diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py similarity index 100% rename from amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_operation.py rename to amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py new file mode 100644 index 00000000..452c64db --- /dev/null +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py @@ -0,0 +1,13 @@ +import wx +from typing import Union +from ...base_ui import BaseUI + +BaseToolUIType = Union[wx.Window, wx.Sizer, "BaseToolUI"] + + +class BaseToolUI(BaseUI): + def enable(self): + raise NotImplementedError + + def disable(self): + raise NotImplementedError diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/export_tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/export_tool.py similarity index 59% rename from amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/export_tool.py rename to amulet_map_editor/programs/edit/canvas/ui/tool/tools/export_tool.py index f99f3347..b94a128b 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/export_tool.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/export_tool.py @@ -1,8 +1,8 @@ -from .select_operation import BaseSelectOperationUI +from .base_operation import BaseSelectOperationUI from amulet_map_editor.programs.edit import plugins class SelectExportOperationUI(BaseSelectOperationUI): @property - def _operations(self): + def _operations(self) -> plugins.OperationStorageType: return plugins.export_operations diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/import_tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/import_tool.py similarity index 59% rename from amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/import_tool.py rename to amulet_map_editor/programs/edit/canvas/ui/tool/tools/import_tool.py index 22ce00f5..5f10062f 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/import_tool.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/import_tool.py @@ -1,8 +1,8 @@ -from .select_operation import BaseSelectOperationUI +from .base_operation import BaseSelectOperationUI from amulet_map_editor.programs.edit import plugins class SelectImportOperationUI(BaseSelectOperationUI): @property - def _operations(self): + def _operations(self) -> plugins.OperationStorageType: return plugins.import_operations diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation.py new file mode 100644 index 00000000..727f576b --- /dev/null +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation.py @@ -0,0 +1,8 @@ +from .base_operation import BaseSelectOperationUI +from amulet_map_editor.programs.edit import plugins + + +class SelectOperationUI(BaseSelectOperationUI): + @property + def _operations(self) -> plugins.OperationStorageType: + return plugins.operations diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_destination.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py similarity index 100% rename from amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/select_destination.py rename to amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py similarity index 95% rename from amulet_map_editor/programs/edit/canvas/ui/tool_options/select.py rename to amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index ba2cf2f4..a5d95251 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool_options/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -1,15 +1,16 @@ from typing import TYPE_CHECKING, Type, Any import wx -import weakref + +from .base_tool_ui import BaseToolUI if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -class SelectOptions(wx.Panel): +class SelectOptions(wx.Panel, BaseToolUI): def __init__(self, canvas: 'EditCanvas'): wx.Panel.__init__(self, canvas) - self._canvas = weakref.ref(canvas) + BaseToolUI.__init__(self, canvas) self._sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._sizer) @@ -47,7 +48,7 @@ def __init__(self, canvas: 'EditCanvas'): # self._canvas().Bind(EVT_BOX_COORDS_ENABLE, self._enable_scrolls) def enable(self): - self._canvas().select_mode = 0 + self.canvas.select_mode = 0 self.Show() def _add_row(self, label: str, wx_object: Type[wx.Object], **kwargs) -> Any: diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/__init__.py b/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/__init__.py deleted file mode 100644 index c0ef5aff..00000000 --- a/amulet_map_editor/programs/edit/canvas/ui/tool_options/operation/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -import wx -import weakref -from typing import Optional, Any, Callable, List - -from amulet.api.structure import Structure - -from .select_destination import SelectDestinationUI -from .select_operation import SelectOperationUI -from .import_tool import SelectImportOperationUI -from .export_tool import SelectExportOperationUI - - -class OperationUI(wx.Panel): - def __init__(self, canvas, world, run_operation, run_main_operation): - wx.Panel.__init__(self, canvas) - - self._world = weakref.ref(world) - self._canvas = weakref.ref(canvas) - self._run_main_operation = run_main_operation - - middle_sizer = wx.BoxSizer(wx.VERTICAL) - - # UI to select which operation to run - self._operation_ui: Optional[SelectOperationUI] = SelectOperationUI(self, world, run_operation) - middle_sizer.Add(self._operation_ui) - self._operation_ui.Layout() - self._operation_ui.Fit() - self._operation_ui.Hide() - - # UI to select which operation to run - self._import_ui: Optional[SelectImportOperationUI] = SelectImportOperationUI(self, world, run_operation) - middle_sizer.Add(self._import_ui) - self._import_ui.Layout() - self._import_ui.Fit() - self._import_ui.Hide() - - # UI to select which operation to run - self._export_ui: Optional[SelectExportOperationUI] = SelectExportOperationUI(self, world, run_operation) - middle_sizer.Add(self._export_ui) - self._export_ui.Layout() - self._export_ui.Fit() - self._export_ui.Hide() - - # UI to select where to put a structure - self._select_destination_ui: Optional[SelectDestinationUI] = SelectDestinationUI( - self, - self._destination_select_cancel, - self._destination_select_confirm, - canvas.structure_locations - ) - middle_sizer.Add(self._select_destination_ui) - self._select_destination_ui.Layout() - self._select_destination_ui.Fit() - self._select_destination_ui.Hide() - - self._shown_ui = self._operation_ui - - self.SetSizer(middle_sizer) - middle_sizer.Fit(self) - self.Layout() - - def _hide_all(self): - self._operation_ui.Hide() - self._select_destination_ui.Hide() - self._import_ui.Hide() - self._export_ui.Hide() - - def _enable_operation_ui(self, ui: wx.Window): - self._hide_all() - self._shown_ui = ui - self.Show() - ui.Show() - self._canvas().select_mode = 1 - self.Fit() - - def enable_operation_ui(self): - self._enable_operation_ui(self._operation_ui) - - def enable_import_ui(self): - self._enable_operation_ui(self._import_ui) - - def enable_export_ui(self): - self._enable_operation_ui(self._export_ui) - - def enable_select_destination_ui(self, operation_path: Any, operation: Callable, operation_input_definitions: List[str], structure: Structure, options: dict): - self._select_destination_ui.setup(operation_path, operation, operation_input_definitions, structure, options) - self._hide_all() - self.Show() - self._select_destination_ui.Show() - self.Fit() - self._canvas().structure = structure - self._canvas().select_mode = 2 - - def _destination_select_cancel(self): - self.enable_operation_ui() - - def _destination_select_confirm(self, *args, **kwargs): - self._select_destination_ui.Disable() - self._run_main_operation(*args, **kwargs) - self._select_destination_ui.Enable() - self.enable_operation_ui() - - def hide(self): - self.Hide() - - @property - def operation(self): - return self._shown_ui.operation diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 7cb0d903..ff086c86 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -6,6 +6,7 @@ from amulet_map_editor.programs import BaseWorldProgram, MenuData from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog +EDIT_CONFIG_ID = "amulet_edit" from .canvas.edit_canvas import EditCanvas from .key_config import DefaultKeybindGroupId, PresetKeybinds, KeybindKeys @@ -13,7 +14,7 @@ if TYPE_CHECKING: from amulet.api.world import World -EDIT_CONFIG_ID = "amulet_edit" + class EditExtension(wx.Panel, BaseWorldProgram): diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index 0ed378f1..31410b1d 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -81,6 +81,8 @@ def _load(self, export_dict: dict): operation: Type[OperationUI] = export_dict.get("operation", None) if not issubclass(operation, OperationUI): raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') + if not issubclass(operation, wx.Window): + raise OperationLoadException('"operation" must be a subclass of wx.Window.') self._ui = lambda parent, canvas, world: operation(parent, canvas, world, options_path) else: raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') From d993ad9b8fcace6c9eda595e2141c6b014ac3f03 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 28 May 2020 16:48:34 +0100 Subject: [PATCH 24/84] Reimplemented tool selection --- amulet_map_editor/opengl/canvas/base.py | 13 ++- .../programs/edit/canvas/ui/tool/tool.py | 17 +++- .../canvas/ui/tool/tools/base_operation.py | 87 +++++++------------ .../programs/edit/plugins/__init__.py | 4 +- .../edit/plugins/api/fixed_pipeline.py | 5 +- .../programs/edit/plugins/api/loader.py | 12 ++- .../programs/edit/plugins/api/operation_ui.py | 9 +- 7 files changed, 73 insertions(+), 74 deletions(-) diff --git a/amulet_map_editor/opengl/canvas/base.py b/amulet_map_editor/opengl/canvas/base.py index 5ab41eb0..cd3dc297 100644 --- a/amulet_map_editor/opengl/canvas/base.py +++ b/amulet_map_editor/opengl/canvas/base.py @@ -5,6 +5,7 @@ import numpy import math from typing import Optional +from amulet_map_editor.opengl.data_types import CameraLocationType, CameraRotationType class BaseCanvas(glcanvas.GLCanvas): @@ -38,6 +39,14 @@ def _setup_opengl(self): def _close(self): glDeleteTextures([self._gl_texture_atlas]) + @property + def camera_location(self) -> CameraLocationType: + raise NotImplementedError + + @property + def camera_rotation(self) -> CameraRotationType: + raise NotImplementedError + @property def fov(self) -> float: return self._projection[0] @@ -107,9 +116,9 @@ def transformation_matrix(self) -> numpy.ndarray: # camera translation if self._transformation_matrix is None: transformation_matrix = numpy.eye(4, dtype=numpy.float32) - transformation_matrix[3, :3] = numpy.array(self._camera[:3]) * -1 + transformation_matrix[3, :3] = numpy.array(self.camera_location) * -1 - transformation_matrix = numpy.matmul(transformation_matrix, self.rotation_matrix(*self._camera[3:5])) + transformation_matrix = numpy.matmul(transformation_matrix, self.rotation_matrix(*self.camera_rotation)) self._transformation_matrix = numpy.matmul(transformation_matrix, self.projection_matrix()) return self._transformation_matrix diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py index 29f4faa2..6329e6e1 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py @@ -38,6 +38,7 @@ def __init__(self, canvas: "EditCanvas"): self.canvas.Bind(EVT_TOOL_CHANGE, lambda evt: self._enable_tool(evt.tool)) self.register_tool("Select", SelectOptions) + self._enable_tool("Select") self.register_tool("Operation", SelectOperationUI) self.register_tool("Import", SelectImportOperationUI) self.register_tool("Export", SelectExportOperationUI) @@ -46,16 +47,26 @@ def register_tool(self, name: str, tool_cls: Type[BaseToolUIType]): assert issubclass(tool_cls, (wx.Window, wx.Sizer)) and issubclass(tool_cls, BaseToolUI) self._tool_select.register_tool(name) tool = tool_cls(self.canvas) - tool.Hide() + if isinstance(tool, wx.Window): + tool.Hide() + elif isinstance(tool, wx.Sizer): + tool.ShowItems(show=False) self._tools[name] = tool self._tool_option_sizer.Add(tool, 1, wx.EXPAND, 0) def _enable_tool(self, tool: str): if tool in self._tools: if self._active_tool is not None: - self._active_tool.Hide() + if isinstance(self._active_tool, wx.Window): + self._active_tool.Hide() + elif isinstance(self._active_tool, wx.Sizer): + self._active_tool.ShowItems(show=False) self._active_tool = self._tools[tool] - self._active_tool.Show() + if isinstance(self._active_tool, wx.Window): + self._active_tool.Show() + elif isinstance(self._active_tool, wx.Sizer): + self._active_tool.ShowItems(show=True) + self.canvas.Layout() class ToolSelect(wx.Panel, BaseUI): diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py index 365ad47e..4fc07e6a 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py @@ -1,76 +1,49 @@ import wx -import weakref -from typing import TYPE_CHECKING, Callable, Dict +from typing import TYPE_CHECKING, Optional -from amulet_map_editor import log -from amulet_map_editor.amulet_wx.simple import SimplePanel, SimpleChoiceAny -from amulet_map_editor.programs.edit import plugins +from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny +from amulet_map_editor.programs.edit.plugins import OperationUIType, OperationStorageType +from amulet_map_editor.programs.edit.canvas.ui.tool.tools.base_tool_ui import BaseToolUI if TYPE_CHECKING: - from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas import EditCanvas -class BaseSelectOperationUI(SimplePanel): - def __init__(self, parent, world: 'World', run_operation: Callable): - super().__init__(parent) - self._world = weakref.ref(world) - self._operation_choice = SimpleChoiceAny(self) - self._operation_choice.SetItems({key: value["name"] for key, value in self._operations.items()}) - self._operation_choice.Bind(wx.EVT_CHOICE, self._operation_selection_change) - self.add_object(self._operation_choice, 0, wx.ALL | wx.EXPAND) - self._options_button = wx.Button( - self, - label="Change Options" - ) - run_button = wx.Button( - self, - label="Run Operation" - ) - self._options_button.Bind(wx.EVT_BUTTON, self._change_options) - run_button.Bind(wx.EVT_BUTTON, run_operation) - self.add_object(self._options_button, 0, wx.ALL | wx.EXPAND) - self.add_object(run_button, 0, wx.ALL | wx.EXPAND) - self._operation_selection_change_() +class BaseSelectOperationUI(wx.BoxSizer, BaseToolUI): + def __init__(self, canvas: "EditCanvas"): + wx.BoxSizer.__init__(self, wx.VERTICAL) + BaseToolUI.__init__(self, canvas) + self._active_operation: Optional[OperationUIType] = None + + self._operation_choice = SimpleChoiceAny(self.canvas) + self._operation_choice.SetItems({key: value.name for key, value in self._operations.items()}) + self._operation_choice.Bind(wx.EVT_CHOICE, self._on_operation_change) + self.AddStretchSpacer(1) + self.Add(self._operation_choice) + self._operation_sizer = wx.BoxSizer(wx.VERTICAL) + self.Add(self._operation_sizer) + self.AddStretchSpacer(1) + + self._operation_change() @property - def _operations(self) -> Dict[str, dict]: + def _operations(self) -> OperationStorageType: raise NotImplementedError @property def operation(self) -> str: return self._operation_choice.GetAny() - def _operation_selection_change(self, evt): - self._operation_selection_change_() + def _on_operation_change(self, evt): + self._operation_change() evt.Skip() - def _operation_selection_change_(self): - operation_path = self._operation_choice.GetAny() - if operation_path: - operation = self._operations[operation_path] - if "options" in operation.get("features", []) or "wxoptions" in operation.get("features", []): - self._options_button.Enable() - else: - self._options_button.Disable() - else: - self._options_button.Disable() - - def _change_options(self, evt): + def _operation_change(self): operation_path = self._operation_choice.GetAny() if operation_path: operation = self._operations[operation_path] - if "options" in operation.get("features", []): - pass # TODO: implement this - elif "wxoptions" in operation.get("features", []): - options = operation["wxoptions"](self, self._world(), plugins.plugin_options.get(operation_path, {})) - if isinstance(options, dict): - plugins.plugin_options[operation_path] = options - else: - log.error(f"Plugin {operation['name']} at {operation_path} did not return options in a valid format") - evt.Skip() - - -class SelectOperationUI(BaseSelectOperationUI): - @property - def _operations(self) -> Dict[str, dict]: - return plugins.operations + if self._active_operation is not None: + self._active_operation.unload() + self._operation_sizer.GetItem(self._active_operation).DeleteWindows() + self._active_operation = operation(self.canvas, self.canvas, self.canvas.world) + self._operation_sizer.Add(self._active_operation, 1, wx.EXPAND) diff --git a/amulet_map_editor/programs/edit/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py index c2fb9930..232d66a4 100644 --- a/amulet_map_editor/programs/edit/plugins/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/__init__.py @@ -1,4 +1,4 @@ from .api.data_types import PathType, OperationStorageType -from .api.operation_ui import OperationUI +from .api.operation_ui import OperationUI, OperationUIType from .api.fixed_pipeline import FixedFunctionUI, OperationError, OperationSuccessful -from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, plugin_options, reload_operations +from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, reload_operations diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 6a347c23..2375ee20 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -20,7 +20,8 @@ class OperationSuccessful(Exception): pass -class FixedFunctionUI(OperationUI): +class FixedFunctionUI(wx.Panel, OperationUI): def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): - super().__init__(parent, canvas, world, options_path) + wx.Panel.__init__(self, parent) + OperationUI.__init__(self, parent, canvas, world, options_path) # TODO diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index 31410b1d..2b8adc5d 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -8,7 +8,7 @@ import hashlib from .fixed_pipeline import FixedFunctionUI -from .operation_ui import OperationUI +from .operation_ui import OperationUI, OperationUIType from .data_types import OperationStorageType if TYPE_CHECKING: @@ -81,8 +81,8 @@ def _load(self, export_dict: dict): operation: Type[OperationUI] = export_dict.get("operation", None) if not issubclass(operation, OperationUI): raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') - if not issubclass(operation, wx.Window): - raise OperationLoadException('"operation" must be a subclass of wx.Window.') + if not issubclass(operation, (wx.Window, wx.Sizer)): + raise OperationLoadException('"operation" must be a subclass of wx.Window or wx.Sizer.') self._ui = lambda parent, canvas, world: operation(parent, canvas, world, options_path) else: raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') @@ -91,8 +91,8 @@ def _load(self, export_dict: dict): def name(self) -> str: return self._name - def setup_ui(self, parent: wx.Window, canvas: "EditCanvas", world: "World"): - self._ui(parent, canvas, world) + def __call__(self, parent: wx.Window, canvas: "EditCanvas", world: "World") -> OperationUIType: + return self._ui(parent, canvas, world) def _load_module_file(module_path: str): @@ -179,8 +179,6 @@ def _load_operations_group(dir_paths: List[str]): 'import_operations' } -plugin_options: Dict[str, dict] = {} - def merge_operations(): """Merge all loaded operations into all_operations""" diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index b93542ad..9a1ba5a8 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -1,5 +1,5 @@ import pickle -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Union import os import wx import weakref @@ -8,6 +8,8 @@ from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World +OperationUIType = Union[wx.Window, wx.Sizer, "OperationUI"] + class OperationUI: """The base class that all operations must inherit from.""" @@ -29,6 +31,11 @@ def canvas(self) -> "EditCanvas": def world(self) -> "World": return self._world() + def unload(self): + """Unbind any events that have been set up and make safe to destroy the UI. + The UI will be destroyed from externally.""" + raise NotImplementedError + def _load_options(self, default=None) -> Any: """Load previously saved options from disk or return the default options.""" try: From 5b38baa5f848d06a9d00ef8dce9a3759460b38e2 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 28 May 2020 17:18:01 +0100 Subject: [PATCH 25/84] Cleaned up the file ui --- .../programs/edit/canvas/ui/file.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index 126f223d..98b77634 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -8,31 +8,27 @@ from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -class FilePanel(wx.Panel, BaseUI): +class FilePanel(wx.BoxSizer, BaseUI): def __init__(self, canvas: 'EditCanvas'): - wx.Panel.__init__(self, canvas) + wx.BoxSizer.__init__(self, wx.HORIZONTAL) BaseUI.__init__(self, canvas) - top_sizer = wx.BoxSizer(wx.HORIZONTAL) - - self._location_button = wx.Button(self, label=', '.join([f'{s:.2f}' for s in self._canvas().camera_location])) + self._location_button = wx.Button(canvas, label=', '.join([f'{s:.2f}' for s in self._canvas().camera_location])) self._location_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.goto()) - top_sizer.Add(self._location_button, 0, wx.ALL | wx.CENTER, 5) + self.Add(self._location_button, 0, wx.TOP | wx.BOTTOM | wx.RIGHT | wx.CENTER, 5) - dim_label = wx.StaticText(self, label="Dimension:") - self._dim_options = SimpleChoiceAny(self) + self._dim_options = SimpleChoiceAny(canvas) self._dim_options.SetItems(self.canvas.world.world_wrapper.dimensions) self._dim_options.SetValue("overworld") self._dim_options.Bind(wx.EVT_CHOICE, self._on_dimension_change) - top_sizer.Add(dim_label, 0, wx.ALL | wx.CENTER, 5) - top_sizer.Add(self._dim_options, 0, wx.ALL | wx.CENTER, 5) + self.Add(self._dim_options, 0, wx.TOP | wx.BOTTOM | wx.RIGHT | wx.CENTER, 5) def create_button(text, operation): - button = wx.Button(self, label=text) + button = wx.Button(canvas, label=text) button.Bind(wx.EVT_BUTTON, operation) - top_sizer.Add(button, 0, wx.ALL, 5) + self.Add(button, 0, wx.TOP | wx.BOTTOM | wx.RIGHT, 5) return button self._undo_button: Optional[wx.Button] = create_button('Undo', lambda evt: self.canvas.undo()) @@ -41,8 +37,7 @@ def create_button(text, operation): create_button('Close', lambda evt: self.canvas.close()) self.update_buttons() - self.SetSizer(top_sizer) - top_sizer.Fit(self) + # self.Fit(self) self.Layout() def update_buttons(self): @@ -60,6 +55,7 @@ def change_dimension(self): self.canvas.dimension = dimension def move_event(self, evt): - self._location_button.SetLabel(f'{evt.x:.2f}, {evt.y:.2f}, {evt.z:.2f}') + x, y, z = evt.location + self._location_button.SetLabel(f'{x:.2f}, {y:.2f}, {z:.2f}') self.Layout() - self.GetParent().Layout() + self.canvas.Layout() From 87e2ed460f7fef1c54038f5b557db351ead3d31f Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 09:34:00 +0100 Subject: [PATCH 26/84] Cleaned up updating the UI for undo/redo logic --- .../programs/edit/canvas/edit_canvas.py | 17 ++++++------ .../programs/edit/canvas/events.py | 1 + .../programs/edit/canvas/ui/file.py | 27 ++++++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 3215bd6d..e2d62ab6 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -12,7 +12,10 @@ from amulet_map_editor.programs.edit.canvas.ui.tool import Tool from amulet_map_editor.programs.edit.canvas.events import ( - EVT_CAMERA_MOVE, + UndoEvent, + RedoEvent, + CreateUndoEvent, + SaveEvent, ) from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet_map_editor.programs.edit.canvas.ui.file import FilePanel @@ -74,14 +77,12 @@ def __init__(self, parent: wx.Window, world: "World"): keybinds ) - self._file_panel = FilePanel(self) - self.Bind(EVT_CAMERA_MOVE, self._file_panel.move_event) - canvas_sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(canvas_sizer) file_sizer = wx.BoxSizer(wx.HORIZONTAL) file_sizer.AddStretchSpacer(1) + self._file_panel = FilePanel(self) file_sizer.Add(self._file_panel, 0, wx.EXPAND, 0) canvas_sizer.Add(file_sizer, 0, wx.EXPAND, 0) @@ -98,7 +99,7 @@ def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: self, ) self._world.create_undo_point() - self._file_panel.update_buttons() + wx.PostEvent(self, CreateUndoEvent()) except Exception as e: self._canvas.enable_threads() raise e @@ -107,11 +108,11 @@ def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: def undo(self): self.world.undo() - self._file_panel.update_buttons() + wx.PostEvent(self, UndoEvent()) def redo(self): self.world.redo() - self._file_panel.update_buttons() + wx.PostEvent(self, RedoEvent()) def cut(self): pass @@ -138,7 +139,7 @@ def save(): yield chunk_index / chunk_count show_loading_dialog(lambda: save(), f"Saving world.", "Please wait.", self) - self._file_panel.update_buttons() + wx.PostEvent(self, SaveEvent()) self.enable_threads() def close(self): diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index b122cee0..dc506ed9 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -8,6 +8,7 @@ UndoEvent, EVT_UNDO = newevent.NewEvent() RedoEvent, EVT_REDO = newevent.NewEvent() +CreateUndoEvent, EVT_CREATE_UNDO = newevent.NewEvent() SaveEvent, EVT_SAVE = newevent.NewEvent() BoxGreenCornerChangeEvent, EVT_BOX_GREEN_CORNER_CHANGE = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index 98b77634..7a7b7cad 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -3,6 +3,13 @@ from .base_ui import BaseUI from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny +from amulet_map_editor.programs.edit.canvas.events import ( + EVT_CAMERA_MOVE, + EVT_UNDO, + EVT_REDO, + EVT_CREATE_UNDO, + EVT_SAVE +) if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas @@ -15,6 +22,7 @@ def __init__(self, canvas: 'EditCanvas'): self._location_button = wx.Button(canvas, label=', '.join([f'{s:.2f}' for s in self._canvas().camera_location])) self._location_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.goto()) + self.canvas.Bind(EVT_CAMERA_MOVE, self._on_camera_move) self.Add(self._location_button, 0, wx.TOP | wx.BOTTOM | wx.RIGHT | wx.CENTER, 5) @@ -32,29 +40,34 @@ def create_button(text, operation): return button self._undo_button: Optional[wx.Button] = create_button('Undo', lambda evt: self.canvas.undo()) + self.canvas.Bind(EVT_UNDO, self._on_update_buttons) self._redo_button: Optional[wx.Button] = create_button('Redo', lambda evt: self.canvas.redo()) + self.canvas.Bind(EVT_REDO, self._on_update_buttons) self._save_button: Optional[wx.Button] = create_button('Save', lambda evt: self.canvas.save()) + self.canvas.Bind(EVT_SAVE, self._on_update_buttons) + self.canvas.Bind(EVT_CREATE_UNDO, self._on_update_buttons) create_button('Close', lambda evt: self.canvas.close()) - self.update_buttons() + self._update_buttons() # self.Fit(self) self.Layout() - def update_buttons(self): + def _on_update_buttons(self, evt): + self._update_buttons() + evt.Skip() + + def _update_buttons(self): self._undo_button.SetLabel(f"Undo | {self.canvas.world.chunk_history_manager.undo_count}") self._redo_button.SetLabel(f"Redo | {self.canvas.world.chunk_history_manager.redo_count}") self._save_button.SetLabel(f"Save | {self.canvas.world.chunk_history_manager.unsaved_changes}") def _on_dimension_change(self, evt): - self.change_dimension() - evt.Skip() - - def change_dimension(self): dimension = self._dim_options.GetAny() if dimension is not None: self.canvas.dimension = dimension + evt.Skip() - def move_event(self, evt): + def _on_camera_move(self, evt): x, y, z = evt.location self._location_button.SetLabel(f'{x:.2f}, {y:.2f}, {z:.2f}') self.Layout() From 631dec33566e008daf681c062aba1c404dcc5d04 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 09:41:32 +0100 Subject: [PATCH 27/84] Fixed some usage of canvas --- amulet_map_editor/programs/edit/canvas/edit_canvas.py | 6 +++--- amulet_map_editor/programs/edit/canvas/ui/file.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index e2d62ab6..f9611baa 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -90,7 +90,7 @@ def __init__(self, parent: wx.Window, world: "World"): canvas_sizer.Add(tool_sizer, 1, wx.EXPAND, 0) def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: - self._canvas.disable_threads() + self.disable_threads() try: out = show_loading_dialog( operation, @@ -101,9 +101,9 @@ def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: self._world.create_undo_point() wx.PostEvent(self, CreateUndoEvent()) except Exception as e: - self._canvas.enable_threads() + self.enable_threads() raise e - self._canvas.enable_threads() + self.enable_threads() return out def undo(self): diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index 7a7b7cad..578fb020 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -20,7 +20,7 @@ def __init__(self, canvas: 'EditCanvas'): wx.BoxSizer.__init__(self, wx.HORIZONTAL) BaseUI.__init__(self, canvas) - self._location_button = wx.Button(canvas, label=', '.join([f'{s:.2f}' for s in self._canvas().camera_location])) + self._location_button = wx.Button(canvas, label=', '.join([f'{s:.2f}' for s in self.canvas.camera_location])) self._location_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.goto()) self.canvas.Bind(EVT_CAMERA_MOVE, self._on_camera_move) From 1e2e83a08d11057ef78146009512596eb765ba2c Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 10:51:12 +0100 Subject: [PATCH 28/84] Stopped the selection tool filling the whole screen --- .../programs/edit/canvas/ui/tool/tools/select.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index a5d95251..be9ba120 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -7,12 +7,12 @@ from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -class SelectOptions(wx.Panel, BaseToolUI): +class SelectOptions(wx.BoxSizer, BaseToolUI): def __init__(self, canvas: 'EditCanvas'): - wx.Panel.__init__(self, canvas) + wx.BoxSizer.__init__(self, wx.VERTICAL) BaseToolUI.__init__(self, canvas) - self._sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self._sizer) + + self.Add(wx.Panel(canvas)) # self._x1: wx.SpinCtrl = self._add_row('x1', wx.SpinCtrl, min=-30000000, max=30000000) # self._y1: wx.SpinCtrl = self._add_row('y1', wx.SpinCtrl, min=-30000000, max=30000000) @@ -53,7 +53,7 @@ def enable(self): def _add_row(self, label: str, wx_object: Type[wx.Object], **kwargs) -> Any: sizer = wx.BoxSizer(wx.HORIZONTAL) - self._sizer.Add(sizer, 0, 0) + self.Add(sizer, 0, 0) name_text = wx.StaticText(self, label=label) sizer.Add(name_text, flag=wx.CENTER | wx.ALL | wx.EXPAND, border=5) obj = wx_object(self, **kwargs) From 5763137fefe4093e8ae5dd4122b0ded21f606631 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 10:52:01 +0100 Subject: [PATCH 29/84] Added updating of the UI if the dimension is manually changed --- .../opengl/mesh/world_renderer/world.py | 7 ++++--- .../programs/edit/canvas/base_edit_canvas.py | 13 +++++++++---- amulet_map_editor/programs/edit/canvas/events.py | 1 + amulet_map_editor/programs/edit/canvas/ui/file.py | 9 +++++++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/amulet_map_editor/opengl/mesh/world_renderer/world.py b/amulet_map_editor/opengl/mesh/world_renderer/world.py index 8672b3e7..b21e620e 100644 --- a/amulet_map_editor/opengl/mesh/world_renderer/world.py +++ b/amulet_map_editor/opengl/mesh/world_renderer/world.py @@ -8,6 +8,7 @@ import minecraft_model_reader import PyMCTranslate from amulet.api.block import BlockManager +from amulet.api.data_types import Dimension from amulet_map_editor import log from .chunk import RenderChunk @@ -139,7 +140,7 @@ def __init__( self._world = world self._camera_location: CameraLocationType = (0, 150, 0) self._camera_rotation: CameraRotationType = (90, 0) - self._dimension = "overworld" + self._dimension: Dimension = "overworld" self._render_distance = 10 self._garbage_distance = 20 self._chunk_manager = ChunkManager(self.context_identifier, self.texture) @@ -195,11 +196,11 @@ def camera_rotation(self, value: CameraRotationType): self._camera_rotation = value @property - def dimension(self) -> str: + def dimension(self) -> Dimension: return self._dimension @dimension.setter - def dimension(self, dimension: int): + def dimension(self, dimension: Dimension): self._chunk_generator.stop() self._dimension = dimension self.run_garbage_collector(True) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 5310b364..4e7f2117 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -9,7 +9,7 @@ from amulet.api.chunk import Chunk from amulet.api.structure import Structure from amulet.api.errors import ChunkLoadError -from amulet.api.data_types import PointCoordinatesNDArray +from amulet.api.data_types import PointCoordinatesNDArray, Dimension from amulet.api.selection import SelectionGroup from amulet_map_editor.opengl.data_types import CameraLocationType, CameraRotationType @@ -19,7 +19,11 @@ from amulet_map_editor.opengl import textureatlas from amulet_map_editor.opengl.canvas.base import BaseCanvas from amulet_map_editor import log -from amulet_map_editor.programs.edit.canvas.events import CameraMoveEvent, CameraRotateEvent +from amulet_map_editor.programs.edit.canvas.events import ( + CameraMoveEvent, + CameraRotateEvent, + DimensionChangeEvent, +) if TYPE_CHECKING: from amulet.api.world import World @@ -199,12 +203,13 @@ def select_mode(self, select_mode: int): self._select_mode = select_mode @property - def dimension(self) -> str: + def dimension(self) -> Dimension: return self._render_world.dimension @dimension.setter - def dimension(self, dimension: int): + def dimension(self, dimension: Dimension): self._render_world.dimension = dimension + wx.PostEvent(self, DimensionChangeEvent(dimension=dimension)) @property def camera_location(self) -> CameraLocationType: diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index dc506ed9..3d6e4c47 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -3,6 +3,7 @@ CameraMoveEvent, EVT_CAMERA_MOVE = newevent.NewEvent() CameraRotateEvent, EVT_CAMERA_ROTATE = newevent.NewEvent() +DimensionChangeEvent, EVT_DIMENSION_CHANGE = newevent.NewEvent() ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index 578fb020..a1db9f8c 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -62,11 +62,20 @@ def _update_buttons(self): self._save_button.SetLabel(f"Save | {self.canvas.world.chunk_history_manager.unsaved_changes}") def _on_dimension_change(self, evt): + """Run when the dimension selection is changed by the user.""" dimension = self._dim_options.GetAny() if dimension is not None: self.canvas.dimension = dimension evt.Skip() + def _change_dimension(self, evt): + """Run when the dimension attribute in the canvas is changed. + This is run when the user changes the attribute and when it is changed manually in code.""" + dimension = evt.dimension + index = self._dim_options.FindString(dimension) + if not (index == wx.NOT_FOUND or index == self._dim_options.GetSelection()): + self._dim_options.SetSelection(index) + def _on_camera_move(self, evt): x, y, z = evt.location self._location_button.SetLabel(f'{x:.2f}, {y:.2f}, {z:.2f}') From 83617a1d5aa51e9a6ae0fe5fbfb10746d30e2143 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 11:08:54 +0100 Subject: [PATCH 30/84] Fixed some camera issues The selection was being rebuild twice per frame. This also fixes the box not updating when selection mode changes --- .../programs/edit/canvas/base_edit_canvas.py | 23 ++++++++----------- .../edit/canvas/controllable_edit_canvas.py | 6 ++--- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 4e7f2117..490db95b 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -78,6 +78,7 @@ def __init__(self, parent: wx.Window, world: 'World'): self._select_distance2 = 10 self._select_mode = MODE_NORMAL + self._selection_moved = True # has the selection point moved and does the box need rebuilding self._selection_group = RenderSelectionGroup( self.context_identifier, self._texture_bounds, @@ -183,7 +184,7 @@ def select_distance(self) -> int: @select_distance.setter def select_distance(self, distance: int): self._select_distance = distance - self._change_box_location() + self._selection_moved = True @property def select_distance2(self) -> int: @@ -192,7 +193,7 @@ def select_distance2(self) -> int: @select_distance2.setter def select_distance2(self, distance: int): self._select_distance2 = distance - self._change_box_location() + self._selection_moved = True @property def select_mode(self) -> int: @@ -201,6 +202,7 @@ def select_mode(self) -> int: @select_mode.setter def select_mode(self, select_mode: int): self._select_mode = select_mode + self._selection_moved = True @property def dimension(self) -> Dimension: @@ -220,7 +222,7 @@ def camera_location(self, location: CameraLocationType): assert len(location) == 3 and all(isinstance(v, (int, float)) for v in location), "format for camera_location is invalid" self._camera_location = location self._transformation_matrix = None - self._change_box_location() + self._selection_moved = True wx.PostEvent(self, CameraMoveEvent(location=self.camera_location)) @property @@ -232,7 +234,7 @@ def camera_rotation(self, rotation: CameraRotationType): assert len(rotation) == 2 and all(isinstance(v, (int, float)) for v in rotation), "format for camera_rotation is invalid" self._camera_rotation = rotation self._transformation_matrix = None - self._change_box_location() + self._selection_moved = True wx.PostEvent(self, CameraRotateEvent(rotation=self.camera_rotation)) @property @@ -263,16 +265,6 @@ def _change_box_location(self): self._selection_group.update_position(position, box_index) - # if self._selection_box.select_state == 0: - # (x, y, z) = self._selection_box.point1 = self._selection_box.point2 = location - # wx.PostEvent(self, BoxGreenCornerChangeEvent(x=x, y=y, z=z)) - # wx.PostEvent(self, BoxBlueCornerChangeEvent(x=x, y=y, z=z)) - # elif self._selection_box.select_state == 1: - # (x, y, z) = self._selection_box.point2 = location - # wx.PostEvent(self, BoxBlueCornerChangeEvent(x=x, y=y, z=z)) - # elif self._selection_box.select_state == 2: - # self._selection_box2.point1 = self._selection_box2.point2 = location - def ray_collision(self): vector_start = self.camera_location direction_vector = self._look_vector() @@ -406,6 +398,9 @@ def draw(self): for location in self.structure_locations: transform[3, 0:3] = location self._structure.draw(numpy.matmul(transform, self.transformation_matrix), 0, 0) + if self._selection_moved: + self._selection_moved = False + self._change_box_location() self._selection_group.draw(self.transformation_matrix, tuple(self.camera_location), self._select_mode == MODE_NORMAL) self.SwapBuffers() diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index 64b164d1..18a8a0f8 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -154,10 +154,10 @@ def _process_persistent_inputs(self, evt): def move_camera_relative(self, forward, up, right, pitch, yaw): """Move the camera relative to its current location.""" - if (forward, up, right, pitch, yaw) == (0, 0, 0, 0, 0): + if not any((forward, up, right, pitch, yaw)): if not self._mouse_lock and self._mouse_moved: self._mouse_moved = False - self._change_box_location() + self._selection_moved = True return x, y, z = self.camera_location rx, ry = self.camera_rotation @@ -181,7 +181,7 @@ def _toggle_mouse_lock(self): self.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) self._mouse_delta_x = self._mouse_delta_y = self._last_mouse_x = self._last_mouse_y = 0 self._mouse_lock = True - self._change_box_location() + self._selection_moved = True def _release_mouse(self): """Release the mouse""" From 50a7bf421c7a2fe9446031c6d33317a70f97d99b Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 12:53:30 +0100 Subject: [PATCH 31/84] Redesigned the main menu The worlds and menu are now in a flatnotebook which allows closing tabs with a close button on the tab. The style is changed when the menu is open to stop it being closed. Tabs can be rearranged. Cleaned up tab closing logic --- amulet_map_editor/amulet_ui.py | 64 +++++++++++++++++--------- amulet_map_editor/programs/__init__.py | 7 ++- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index 24c0b1cf..818984d7 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -1,4 +1,5 @@ import wx +from wx.lib.agw import flatnotebook import os from typing import Dict import webbrowser @@ -7,9 +8,11 @@ from amulet_map_editor.amulet_wx.world_select import WorldSelectDialog from amulet_map_editor import lang, version, log, IMG_DIR from amulet_map_editor.programs import WorldManagerUI -from amulet_map_editor.amulet_wx import simple from amulet_map_editor.programs import BaseWorldUI +NOTEBOOK_MENU_STYLE = flatnotebook.FNB_NO_X_BUTTON | flatnotebook.FNB_HIDE_ON_SINGLE_TAB | flatnotebook.FNB_NAV_BUTTONS_WHEN_NEEDED +NOTEBOOK_STYLE = NOTEBOOK_MENU_STYLE | flatnotebook.FNB_X_ON_TAB + class AmuletMainWindow(wx.Frame): def __init__(self, parent): @@ -37,10 +40,13 @@ def __init__(self, parent): self._open_worlds: Dict[str, BaseWorldUI] = {} - self.world_tab_holder = wx.Notebook( - self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 + self.world_tab_holder = flatnotebook.FlatNotebook( + self, + agwStyle=NOTEBOOK_MENU_STYLE ) + self.world_tab_holder.Bind(flatnotebook.EVT_FLATNOTEBOOK_PAGE_CLOSING, self._on_page_close) + self._main_menu = AmuletMainMenu(self.world_tab_holder, self._open_world) self._last_page: BaseWorldUI = self._main_menu @@ -50,7 +56,7 @@ def __init__(self, parent): lang.get('main_menu') ) - self.Bind(wx.EVT_CLOSE, self._on_close) + self.Bind(wx.EVT_CLOSE, self._on_close_app) self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._page_change) self.Show() @@ -104,6 +110,10 @@ def _disable_enable(self): if self._last_page is not None: self._last_page.disable() self._last_page: BaseWorldUI = current + if self._last_page is self._main_menu: + self.world_tab_holder.SetAGWWindowStyleFlag(NOTEBOOK_MENU_STYLE) + else: + self.world_tab_holder.SetAGWWindowStyleFlag(NOTEBOOK_STYLE) if self._last_page is not None: self._last_page.enable() @@ -120,7 +130,7 @@ def _open_world(self, path: str): """Open a world panel add add it to the notebook""" if path in self._open_worlds: self.world_tab_holder.SetSelection( - self.world_tab_holder.FindPage(self._open_worlds[path]) + self.world_tab_holder.GetPageIndex(self._open_worlds[path]) ) self._disable_enable() else: @@ -137,17 +147,20 @@ def close_world(self, path: str): """Close a given world and remove it from the notebook""" if path in self._open_worlds: world = self._open_worlds[path] - world.disable() - world.close() self.world_tab_holder.DeletePage( - self.world_tab_holder.FindPage(world) + self.world_tab_holder.GetPageIndex(world) ) - del self._open_worlds[path] - self._last_page: BaseWorldUI = self.world_tab_holder.GetCurrentPage() - if self._last_page is not None: - self._last_page.enable() - def _on_close(self, evt): + def _on_page_close(self, evt: flatnotebook.EVT_FLATNOTEBOOK_PAGE_CLOSING): + page: WorldManagerUI = self.world_tab_holder.GetCurrentPage() + path = page.path + page.disable() + page.close() + self._last_page = None + del self._open_worlds[path] + + + def _on_close_app(self, evt): close = True for path, world in list(self._open_worlds.items()): if world.is_closeable(): @@ -162,14 +175,17 @@ def _on_close(self, evt): ) -class AmuletMainMenu(simple.SimplePanel, BaseWorldUI): - def __init__(self, parent, open_world): +class AmuletMainMenu(wx.Panel, BaseWorldUI): + def __init__(self, parent: wx.Window, open_world): super(AmuletMainMenu, self).__init__( parent ) + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + sizer.AddStretchSpacer(1) self._open_world_callback = open_world - sizer = wx.BoxSizer() - self.add_object(sizer, 0, wx.ALL | wx.CENTER) + name_sizer = wx.BoxSizer() + sizer.Add(name_sizer, 0, wx.CENTER) img = wx.Image( os.path.join(IMG_DIR, 'icon128.png'), wx.BITMAP_TYPE_ANY @@ -189,29 +205,31 @@ def __init__(self, parent, open_world): (0, 0), (64, 64) ) - sizer.Add(icon, flag=wx.CENTER) + name_sizer.Add(icon, flag=wx.CENTER) amulet_converter = wx.StaticText(self, label='Amulet') amulet_converter.SetFont(wx.Font(40, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)) - sizer.Add( + name_sizer.Add( amulet_converter, flag=wx.CENTER ) - sizer.Add(icon2, flag=wx.CENTER) + name_sizer.Add(icon2, flag=wx.CENTER) button_font = wx.Font(20, wx.DECORATIVE, wx.NORMAL, wx.NORMAL) self._open_world_button = wx.Button(self, label='Open World', size=(400, 70)) self._open_world_button.SetFont(button_font) self._open_world_button.Bind(wx.EVT_BUTTON, self._show_world_select) - self.add_object(self._open_world_button, 0, wx.ALL|wx.CENTER) + sizer.Add(self._open_world_button, 0, wx.ALL | wx.CENTER, 5) self._help_button = wx.Button(self, label='Help', size=(400, 70)) self._help_button.SetFont(button_font) self._help_button.Bind(wx.EVT_BUTTON, self._documentation) - self.add_object(self._help_button, 0, wx.ALL | wx.CENTER) + sizer.Add(self._help_button, 0, wx.ALL | wx.CENTER, 5) self._help_button = wx.Button(self, label='Amulet Discord', size=(400, 70)) self._help_button.SetFont(button_font) self._help_button.Bind(wx.EVT_BUTTON, self._discord) - self.add_object(self._help_button, 0, wx.ALL | wx.CENTER) + sizer.Add(self._help_button, 0, wx.ALL | wx.CENTER, 5) + + sizer.AddStretchSpacer(2) def _show_world_select(self, evt): select_world = WorldSelectDialog(self, self._open_world_callback) diff --git a/amulet_map_editor/programs/__init__.py b/amulet_map_editor/programs/__init__.py index 52916764..756d61a5 100644 --- a/amulet_map_editor/programs/__init__.py +++ b/amulet_map_editor/programs/__init__.py @@ -62,8 +62,9 @@ def menu(self, menu: MenuData) -> MenuData: class WorldManagerUI(wx.Notebook, BaseWorldUI): - def __init__(self, parent, path): + def __init__(self, parent: wx.Window, path: str): super().__init__(parent, style=wx.NB_LEFT) + self._path = path self._finished = False self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._page_change) try: @@ -77,6 +78,10 @@ def __init__(self, parent, path): self._load_extensions() self._finished = True + @property + def path(self) -> str: + return self._path + def menu(self, menu: MenuData) -> MenuData: menu.setdefault('&File', {}).setdefault('exit', {}).setdefault('Close World', lambda evt: self.GetGrandParent().close_world(self.world.world_path)) return self._extensions[self.GetSelection()].menu(menu) From 3987638148e5dccbb7340ecf5d4fc8897410d1fd Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 15:55:36 +0100 Subject: [PATCH 32/84] Reworked program closing logic --- amulet_map_editor/amulet_ui.py | 33 +++++----- amulet_map_editor/programs/__init__.py | 32 +++++----- amulet_map_editor/programs/convert/convert.py | 7 ++- .../programs/edit/canvas/edit_canvas.py | 3 +- .../programs/edit/canvas/events.py | 1 + amulet_map_editor/programs/edit/edit.py | 63 +++++++++++++------ 6 files changed, 88 insertions(+), 51 deletions(-) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index 818984d7..a8392b6d 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -1,7 +1,7 @@ import wx from wx.lib.agw import flatnotebook import os -from typing import Dict +from typing import Dict, Union import webbrowser from amulet.api.errors import LoaderNoneMatched @@ -10,9 +10,11 @@ from amulet_map_editor.programs import WorldManagerUI from amulet_map_editor.programs import BaseWorldUI -NOTEBOOK_MENU_STYLE = flatnotebook.FNB_NO_X_BUTTON | flatnotebook.FNB_HIDE_ON_SINGLE_TAB | flatnotebook.FNB_NAV_BUTTONS_WHEN_NEEDED +NOTEBOOK_MENU_STYLE = flatnotebook.FNB_NO_X_BUTTON | flatnotebook.FNB_HIDE_ON_SINGLE_TAB | flatnotebook.FNB_NAV_BUTTONS_WHEN_NEEDED NOTEBOOK_STYLE = NOTEBOOK_MENU_STYLE | flatnotebook.FNB_X_ON_TAB +CLOSEABLE_PAGE_TYPE = Union[WorldManagerUI] + class AmuletMainWindow(wx.Frame): def __init__(self, parent): @@ -38,7 +40,7 @@ def __init__(self, parent): icon.CopyFromBitmap(wx.Bitmap(os.path.join(os.path.dirname(__file__), 'img', 'icon64.png'), wx.BITMAP_TYPE_ANY)) self.SetIcon(icon) - self._open_worlds: Dict[str, BaseWorldUI] = {} + self._open_worlds: Dict[str, CLOSEABLE_PAGE_TYPE] = {} self.world_tab_holder = flatnotebook.FlatNotebook( self, @@ -118,7 +120,6 @@ def _disable_enable(self): self._last_page.enable() def _add_world_tab(self, obj, obj_name): - # TODO: find a way for the tab to be optionally closeable self.world_tab_holder.AddPage(obj, obj_name, True) self._disable_enable() @@ -135,7 +136,7 @@ def _open_world(self, path: str): self._disable_enable() else: try: - world = WorldManagerUI(self.world_tab_holder, path) + world = WorldManagerUI(self.world_tab_holder, path, lambda: self.close_world(path)) except LoaderNoneMatched as e: log.error(e) wx.MessageBox(str(e)) @@ -152,20 +153,24 @@ def close_world(self, path: str): ) def _on_page_close(self, evt: flatnotebook.EVT_FLATNOTEBOOK_PAGE_CLOSING): - page: WorldManagerUI = self.world_tab_holder.GetCurrentPage() - path = page.path - page.disable() - page.close() - self._last_page = None - del self._open_worlds[path] - + page: CLOSEABLE_PAGE_TYPE = self.world_tab_holder.GetCurrentPage() + if page.is_closeable(): + path = page.path + page.disable() + page.close() + self._last_page = None + del self._open_worlds[path] + else: + evt.Veto() def _on_close_app(self, evt): close = True - for path, world in list(self._open_worlds.items()): - if world.is_closeable(): + for path, page in list(self._open_worlds.items()): + page: CLOSEABLE_PAGE_TYPE + if page.is_closeable(): self.close_world(path) else: + log.info(f"{page.world_name} cannot be closed.") close = False if close: evt.Skip() diff --git a/amulet_map_editor/programs/__init__.py b/amulet_map_editor/programs/__init__.py index 756d61a5..1358d5ed 100644 --- a/amulet_map_editor/programs/__init__.py +++ b/amulet_map_editor/programs/__init__.py @@ -62,11 +62,10 @@ def menu(self, menu: MenuData) -> MenuData: class WorldManagerUI(wx.Notebook, BaseWorldUI): - def __init__(self, parent: wx.Window, path: str): + def __init__(self, parent: wx.Window, path: str, close_self_callback: Callable[[], None]): super().__init__(parent, style=wx.NB_LEFT) self._path = path - self._finished = False - self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._page_change) + self._close_self_callback = close_self_callback try: self.world = world_interface.load_world(path) except LoaderNoneMatched as e: @@ -76,14 +75,14 @@ def __init__(self, parent: wx.Window, path: str): self._extensions: List[BaseWorldProgram] = [] self._last_extension: int = -1 self._load_extensions() - self._finished = True + self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._page_change) @property def path(self) -> str: return self._path def menu(self, menu: MenuData) -> MenuData: - menu.setdefault('&File', {}).setdefault('exit', {}).setdefault('Close World', lambda evt: self.GetGrandParent().close_world(self.world.world_path)) + menu.setdefault('&File', {}).setdefault('exit', {}).setdefault('Close World', lambda evt: self._close_self_callback) return self._extensions[self.GetSelection()].menu(menu) def _load_extensions(self): @@ -92,7 +91,7 @@ def _load_extensions(self): select = True for extension_name, extension in _extensions: try: - ext = extension(self, self.world) + ext = extension(self, self.world, self._close_self_callback) self._extensions.append(ext) self.AddPage(ext, extension_name, select) select = False @@ -114,10 +113,9 @@ def close(self): def _page_change(self, evt): """Method to fire when the page is changed""" if self.GetSelection() != self._last_extension: - if self._finished: - self._extensions[self._last_extension].disable() - self._extensions[self.GetSelection()].enable() - self.GetGrandParent().create_menu() + self._extensions[self._last_extension].disable() + self._extensions[self.GetSelection()].enable() + self.GetGrandParent().create_menu() self._last_extension = self.GetSelection() def disable(self): @@ -138,11 +136,16 @@ def disable(self): """Run when the panel is hidden/disabled""" pass - def is_closeable(self): + def is_closeable(self) -> bool: + """ + Check if it is safe to close the UI. + If this is going to return False it should notify the user. + :return: True if the program can be closed, False otherwise + """ return True def close(self): - """Run when the world is closed""" + """Fully close the UI. Called when destroying the UI.""" pass def menu(self, menu: MenuData) -> MenuData: @@ -150,12 +153,13 @@ def menu(self, menu: MenuData) -> MenuData: class AboutExtension(SimplePanel, BaseWorldProgram): - def __init__(self, container, world: 'World'): + def __init__(self, container, world: 'World', close_self_callback: Callable[[], None]): SimplePanel.__init__( self, container ) self.world = world + self._close_self_callback = close_self_callback self._close_world_button = wx.Button(self, wx.ID_ANY, label='Close World') self._close_world_button.Bind(wx.EVT_BUTTON, self._close_world) @@ -180,7 +184,7 @@ def __init__(self, container, world: 'World'): ) def _close_world(self, evt): - self.GetGrandParent().GetParent().close_world(self.world.world_path) + self._close_self_callback() _fixed_extensions.append(("About", AboutExtension)) diff --git a/amulet_map_editor/programs/convert/convert.py b/amulet_map_editor/programs/convert/convert.py index e8746d60..b00924fa 100644 --- a/amulet_map_editor/programs/convert/convert.py +++ b/amulet_map_editor/programs/convert/convert.py @@ -1,7 +1,7 @@ import wx from concurrent.futures import ThreadPoolExecutor import webbrowser -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable from amulet import world_interface from amulet.api.world import World @@ -20,12 +20,13 @@ class ConvertExtension(SimplePanel, BaseWorldProgram): - def __init__(self, container, world: World): + def __init__(self, container, world: World, close_self_callback: Callable[[], None]): SimplePanel.__init__( self, container ) self.world = world + self._close_self_callback = close_self_callback self._close_world_button = wx.Button(self, wx.ID_ANY, label='Close World') self._close_world_button.Bind(wx.EVT_BUTTON, self._close_world) @@ -160,4 +161,4 @@ def is_closeable(self): return work_count == 0 def _close_world(self, evt): - self.GetGrandParent().GetParent().close_world(self.world.world_path) + self._close_self_callback() diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index f9611baa..d1f992b3 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -16,6 +16,7 @@ RedoEvent, CreateUndoEvent, SaveEvent, + EditCloseEvent, ) from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet_map_editor.programs.edit.canvas.ui.file import FilePanel @@ -143,4 +144,4 @@ def save(): self.enable_threads() def close(self): - pass + wx.PostEvent(self, EditCloseEvent()) diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index 3d6e4c47..e2a27094 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -11,6 +11,7 @@ RedoEvent, EVT_REDO = newevent.NewEvent() CreateUndoEvent, EVT_CREATE_UNDO = newevent.NewEvent() SaveEvent, EVT_SAVE = newevent.NewEvent() +EditCloseEvent, EVT_EDIT_CLOSE = newevent.NewEvent() BoxGreenCornerChangeEvent, EVT_BOX_GREEN_CORNER_CHANGE = newevent.NewEvent() BoxBlueCornerChangeEvent, EVT_BOX_BLUE_CORNER_CHANGE = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index ff086c86..9d41f89b 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -1,13 +1,13 @@ import wx -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Callable import webbrowser -from amulet_map_editor import CONFIG -from amulet_map_editor.programs import BaseWorldProgram, MenuData -from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog - EDIT_CONFIG_ID = "amulet_edit" +from amulet_map_editor import CONFIG, log +from amulet_map_editor.programs import BaseWorldProgram, MenuData +from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog +from amulet_map_editor.programs.edit.canvas.events import EVT_EDIT_CLOSE from .canvas.edit_canvas import EditCanvas from .key_config import DefaultKeybindGroupId, PresetKeybinds, KeybindKeys @@ -15,28 +15,26 @@ from amulet.api.world import World - - class EditExtension(wx.Panel, BaseWorldProgram): - def __init__(self, parent, world: "World"): + def __init__(self, parent, world: "World", close_self_callback: Callable[[], None]): wx.Panel.__init__(self, parent) self._sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._sizer) self._world = world self._canvas: Optional[EditCanvas] = None + self._close_self_callback = close_self_callback self._temp = wx.StaticText(self, label="Please wait while the renderer loads") self._temp.SetFont(wx.Font(40, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)) self._sizer.Add(self._temp) - self.Bind(wx.EVT_SIZE, self._on_resize) - def enable(self): if self._canvas is None: self.Update() self._canvas = EditCanvas(self, self._world) self._sizer.Add(self._canvas, 1, wx.EXPAND) - + self.Bind(wx.EVT_SIZE, self._on_resize) + self._canvas.Bind(EVT_EDIT_CLOSE, self._on_close) self._temp.Destroy() self.Layout() @@ -49,32 +47,59 @@ def disable(self): if self._canvas is not None: self._canvas.disable() + def _on_close(self, evt: EVT_EDIT_CLOSE): + if self.is_closeable(): + self._close_self_callback() + evt.Skip() + def close(self): + """Fully close the UI. Called when destroying the UI.""" self.disable() if self._canvas is not None: self._canvas.close() - def is_closeable(self): + def is_closeable(self) -> bool: + """ + Check if it is safe to close the UI. + :return: True if the program can be closed, False otherwise + """ if self._canvas is not None: - return self._canvas.is_closeable() and not bool( - self._world.chunk_history_manager.unsaved_changes - ) + if self._canvas.is_closeable(): + return self._check_close_world() + log.info(f"The canvas in edit for world {self._world.world_wrapper.world_name} was not closeable for some reason.") + return False return not bool(self._world.chunk_history_manager.unsaved_changes) - def _close_world(self, _): + def _check_close_world(self) -> bool: + """ + Check if it is safe to close the world and prompt the user if it is not. + :return: True if the world can be closed, False otherwise + """ unsaved_changes = self._world.chunk_history_manager.unsaved_changes if unsaved_changes: msg = wx.MessageDialog( self, - f"There {'is' if unsaved_changes == 1 else 'are'} {unsaved_changes} unsaved change{'s' if unsaved_changes >= 2 else ''}. Would you like to save?", + f"""There { + 'is' if unsaved_changes == 1 else 'are' + } {unsaved_changes} unsaved change{ + 's' if unsaved_changes >= 2 else '' + } in { + self._world.world_wrapper.world_name + }. Would you like to save?""", style=wx.YES_NO | wx.CANCEL | wx.CANCEL_DEFAULT, ) response = msg.ShowModal() if response == wx.ID_YES: self._canvas.save() + return True + elif response == wx.ID_NO: + return True elif response == wx.ID_CANCEL: - return - self.GetGrandParent().GetParent().close_world(self._world.world_path) + log.info(f"""Aborting closing world { + self._world.world_wrapper.world_name + } because the user pressed cancel.""") + return False + return True def menu(self, menu: MenuData) -> MenuData: menu.setdefault("&File", {}).setdefault("system", {}).setdefault( From f7c85fa1319e9c34582f74a69782c86c9d956b27 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 16:11:13 +0100 Subject: [PATCH 33/84] Cleaned up canvas loading --- amulet_map_editor/programs/edit/canvas/base_edit_canvas.py | 1 + amulet_map_editor/programs/edit/edit.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 490db95b..7a7e00cb 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -38,6 +38,7 @@ class BaseEditCanvas(BaseCanvas): All the user interaction code is implemented in ControllableEditCanvas to make them easier to read.""" def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) + self.Hide() self._world = weakref.ref(world) self._mouse_delta_x = 0 self._mouse_delta_y = 0 diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 9d41f89b..e50b1956 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -36,6 +36,8 @@ def enable(self): self.Bind(wx.EVT_SIZE, self._on_resize) self._canvas.Bind(EVT_EDIT_CLOSE, self._on_close) self._temp.Destroy() + self._canvas.Show() + self._canvas.draw() self.Layout() self._canvas.Update() From 9bf7c200b25e9b3b8cc72b724e14461f40d710dc Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 16:18:21 +0100 Subject: [PATCH 34/84] Fixed crash when closing from the edit program --- amulet_map_editor/programs/edit/edit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index e50b1956..d6722012 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -52,7 +52,6 @@ def disable(self): def _on_close(self, evt: EVT_EDIT_CLOSE): if self.is_closeable(): self._close_self_callback() - evt.Skip() def close(self): """Fully close the UI. Called when destroying the UI.""" From 801a4a408f90783cecf89b4b18ed86233fbbb460 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 29 May 2020 16:29:51 +0100 Subject: [PATCH 35/84] Matched the edit program background colour to the canvas This should mask the canvas appearing --- amulet_map_editor/opengl/canvas/base.py | 1 - amulet_map_editor/programs/edit/canvas/base_edit_canvas.py | 4 ++++ amulet_map_editor/programs/edit/edit.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/amulet_map_editor/opengl/canvas/base.py b/amulet_map_editor/opengl/canvas/base.py index cd3dc297..64037ce6 100644 --- a/amulet_map_editor/opengl/canvas/base.py +++ b/amulet_map_editor/opengl/canvas/base.py @@ -22,7 +22,6 @@ def __init__(self, parent: wx.Window): self._transformation_matrix: Optional[numpy.ndarray] = None def _setup_opengl(self): - glClearColor(0.5, 0.66, 1.0, 1.0) glEnable(GL_DEPTH_TEST) glEnable(GL_CULL_FACE) glDepthFunc(GL_LEQUAL) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 7a7e00cb..25b3e611 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -36,8 +36,12 @@ class BaseEditCanvas(BaseCanvas): """Adds base logic for drawing everything related to the edit program to the BaseCanvas. All the user interaction code is implemented in ControllableEditCanvas to make them easier to read.""" + + background_colour = (0.5, 0.66, 1.0) + def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) + glClearColor(*self.background_colour, 1.0) self.Hide() self._world = weakref.ref(world) self._mouse_delta_x = 0 diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index d6722012..851242d2 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -19,6 +19,7 @@ class EditExtension(wx.Panel, BaseWorldProgram): def __init__(self, parent, world: "World", close_self_callback: Callable[[], None]): wx.Panel.__init__(self, parent) self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetBackgroundColour(tuple(int(v*255) for v in EditCanvas.background_colour)) self.SetSizer(self._sizer) self._world = world self._canvas: Optional[EditCanvas] = None From 77462f62f0987fa0fcbf21bae7bcd5b788591ec3 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 11:44:54 +0100 Subject: [PATCH 36/84] Added a secret way to bring up the inspection UI --- amulet_map_editor/amulet_ui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index a8392b6d..6fc53932 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -1,4 +1,5 @@ import wx +import wx.lib.inspection from wx.lib.agw import flatnotebook import os from typing import Dict, Union @@ -210,6 +211,7 @@ def __init__(self, parent: wx.Window, open_world): (0, 0), (64, 64) ) + icon2.Bind(wx.EVT_LEFT_DOWN, lambda evt: wx.lib.inspection.InspectionTool().Show()) name_sizer.Add(icon, flag=wx.CENTER) amulet_converter = wx.StaticText(self, label='Amulet') From 96d2a4a5cfbbefade7da63c1dda6afd95288019d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 11:54:18 +0100 Subject: [PATCH 37/84] Fixed crash when closing from the main menu --- amulet_map_editor/amulet_ui.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index 6fc53932..5c86c736 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -155,14 +155,15 @@ def close_world(self, path: str): def _on_page_close(self, evt: flatnotebook.EVT_FLATNOTEBOOK_PAGE_CLOSING): page: CLOSEABLE_PAGE_TYPE = self.world_tab_holder.GetCurrentPage() - if page.is_closeable(): - path = page.path - page.disable() - page.close() - self._last_page = None - del self._open_worlds[path] - else: - evt.Veto() + if page is not self._main_menu: + if page.is_closeable(): + path = page.path + page.disable() + page.close() + self._last_page = None + del self._open_worlds[path] + else: + evt.Veto() def _on_close_app(self, evt): close = True From 0e6b76a15926f917b0c8cd7699c489a300db21c5 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 11:55:22 +0100 Subject: [PATCH 38/84] Added parsing fixed options and creating UI --- .../edit/plugins/api/fixed_pipeline.py | 179 +++++++++++++++++- .../programs/edit/plugins/api/loader.py | 8 +- 2 files changed, 180 insertions(+), 7 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 2375ee20..6cb5b163 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -1,6 +1,7 @@ import wx -from typing import Callable, Dict, Any, TYPE_CHECKING +from typing import Callable, Dict, Any, TYPE_CHECKING, Sequence +from amulet.api.data_types import OperationType from .operation_ui import OperationUI if TYPE_CHECKING: @@ -21,7 +22,179 @@ class OperationSuccessful(Exception): class FixedFunctionUI(wx.Panel, OperationUI): - def __init__(self, parent: wx.Window, canvas: "EditCanvas", world: "World", options_path: str, operation: Callable, options: Dict[str, Any]): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str, + operation: OperationType, + options: Dict[str, Any] + ): wx.Panel.__init__(self, parent) OperationUI.__init__(self, parent, canvas, world, options_path) - # TODO + self._operation = operation + + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + self._options_sizer = wx.BoxSizer(wx.VERTICAL) + self._sizer.Add(self._options_sizer) + self._run_button = wx.Button(self, label="Run Operation") + self._run_button.Bind(wx.EVT_BUTTON, self._run_operation) + self._sizer.Add(self._run_button, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self._options = {} + self._create_options(options) + + self.Fit() + self.Layout() + self.GetParent().Fit() + self.GetParent().Layout() + self.GetTopLevelParent().Fit() + + def unload(self): + pass + + def _create_options(self, options: Dict[str, Sequence]): + create_functions: Dict[str, Callable[[str, Sequence], None]] = { + "label": self._create_label, + "bool": self._create_bool, + "int": self._create_int, + "float": self._create_float, + "str": self._create_string, + "str_choice": self._create_str_choice, + "file": self._create_file_picker, + "directory": self._create_directory_picker + } + for option_name, option in options.items(): + if not (isinstance(option, (list, tuple)) and option): + continue + option_type, option = option[0], option[1:] + if option_type not in create_functions: + continue + create_functions[option_type](option_name, option) + + def _create_label(self, option_name: str, options: Sequence): + label = wx.StaticText(self, label=option_name) + self._options_sizer.Add(label, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + def _create_horizontal_options_sizer(self, label) -> wx.BoxSizer: + sizer = wx.BoxSizer(wx.HORIZONTAL) + label = wx.StaticText(self, label=label) + sizer.Add(label, 0, wx.RIGHT, 5) + self._options_sizer.Add(sizer, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + return sizer + + def _create_bool(self, option_name: str, options: Sequence): + if options and not isinstance(options[0], bool): + return + sizer = self._create_horizontal_options_sizer(option_name) + option = wx.CheckBox(self) + sizer.Add(option) + if options: + option.SetValue(options[0]) + self._options[option_name] = option + + def _create_int(self, option_name: str, options: Sequence): + if len(options) in {0, 1, 3} and all(isinstance(o, int) for o in options): + sizer = self._create_horizontal_options_sizer(option_name) + if len(options) == 0: + option = wx.SpinCtrl( + self, + min=-30_000_000, + max=30_000_000, + initial=0, + ) + elif len(options) == 1: + option = wx.SpinCtrl( + self, + min=-30_000_000, + max=30_000_000, + initial=options[0], + ) + elif len(options) == 3: + option = wx.SpinCtrl( + self, + min=min(options[1:3]), + max=max(options[1:3]), + initial=options[0], + ) + else: + return # should not get here + sizer.Add(option) + self._options[option_name] = option + + def _create_float(self, option_name: str, options: Sequence): + if len(options) in {0, 1, 3} and all(isinstance(o, (int, float)) for o in options): + sizer = self._create_horizontal_options_sizer(option_name) + if len(options) == 0: + option = wx.SpinCtrlDouble( + self, + min=-30_000_000, + max=30_000_000, + initial=0, + ) + elif len(options) == 1: + option = wx.SpinCtrlDouble( + self, + min=-30_000_000, + max=30_000_000, + initial=options[0], + ) + elif len(options) == 3: + option = wx.SpinCtrlDouble( + self, + min=min(options[1:3]), + max=max(options[1:3]), + initial=options[0], + ) + else: + return # should not get here + sizer.Add(option) + self._options[option_name] = option + + def _create_string(self, option_name: str, options: Sequence): + if options and not isinstance(options[0], str): + return + sizer = self._create_horizontal_options_sizer(option_name) + option = wx.TextCtrl(self) + sizer.Add(option) + if options: + option.SetValue(options[0]) + self._options[option_name] = option + + def _create_str_choice(self, option_name: str, options: Sequence): + if not (options and all(isinstance(o, str) for o in options)): + return + sizer = self._create_horizontal_options_sizer(option_name) + option = wx.Choice(self, choices=options) + sizer.Add(option) + self._options[option_name] = option + + def _create_file_picker(self, option_name: str, options: Sequence): + sizer = self._create_horizontal_options_sizer(option_name) + option = wx.FilePickerCtrl( + self, + style=wx.FLP_SAVE + ) + sizer.Add(option) + self._options[option_name] = option + + def _create_directory_picker(self, option_name: str, options: Sequence): + sizer = self._create_horizontal_options_sizer(option_name) + option = wx.DirPickerCtrl( + self, + style=0 + ) + sizer.Add(option) + self._options[option_name] = option + + def _get_values(self): + return {} # TODO + + def _run_operation(self, evt): + return + # self.canvas.run_operation( + # + # ) + # self._operation(self.world, self.canvas.dimension, self._get_values()) diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index 2b8adc5d..dd9df1f8 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -78,12 +78,12 @@ def _load(self, export_dict: dict): raise OperationLoadException('"operation" in export must be a dictionary if defined.') self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, options_path, operation, options) elif mode == "dynamic": - operation: Type[OperationUI] = export_dict.get("operation", None) - if not issubclass(operation, OperationUI): + operation_ui: Type[OperationUI] = export_dict.get("operation", None) + if not issubclass(operation_ui, OperationUI): raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') - if not issubclass(operation, (wx.Window, wx.Sizer)): + if not issubclass(operation_ui, (wx.Window, wx.Sizer)): raise OperationLoadException('"operation" must be a subclass of wx.Window or wx.Sizer.') - self._ui = lambda parent, canvas, world: operation(parent, canvas, world, options_path) + self._ui = lambda parent, canvas, world: operation_ui(parent, canvas, world, options_path) else: raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') From 738a47ff36577dd463f745055942d7d88fab5785 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 12:05:48 +0100 Subject: [PATCH 39/84] Fixed the world selection dialog not getting destroyed --- amulet_map_editor/amulet_ui.py | 2 ++ amulet_map_editor/programs/convert/convert.py | 1 + 2 files changed, 3 insertions(+) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index 5c86c736..64ac678c 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -127,6 +127,7 @@ def _add_world_tab(self, obj, obj_name): def _show_open_world(self): select_world = WorldSelectDialog(self, self._open_world) select_world.ShowModal() + select_world.Destroy() def _open_world(self, path: str): """Open a world panel add add it to the notebook""" @@ -242,6 +243,7 @@ def __init__(self, parent: wx.Window, open_world): def _show_world_select(self, evt): select_world = WorldSelectDialog(self, self._open_world_callback) select_world.ShowModal() + select_world.Destroy() def _documentation(self, evt): webbrowser.open('https://github.com/Amulet-Team/Amulet-Map-Editor/blob/master/amulet_map_editor/readme.md') diff --git a/amulet_map_editor/programs/convert/convert.py b/amulet_map_editor/programs/convert/convert.py index b00924fa..b7fe993c 100644 --- a/amulet_map_editor/programs/convert/convert.py +++ b/amulet_map_editor/programs/convert/convert.py @@ -95,6 +95,7 @@ def _help_controls(self): def _show_world_select(self, evt): select_world = WorldSelectDialog(self, self._output_world_callback) select_world.ShowModal() + select_world.Destroy() def _output_world_callback(self, path): if path == self.world.world_path: From e33c9f3b288e98d9d5cbe2543282098878a66995 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 12:40:08 +0100 Subject: [PATCH 40/84] Fixed some UI updating issues --- .../programs/edit/canvas/ui/tool/tools/base_operation.py | 3 +-- .../programs/edit/plugins/api/fixed_pipeline.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py index 4fc07e6a..879db99e 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py @@ -18,11 +18,9 @@ def __init__(self, canvas: "EditCanvas"): self._operation_choice = SimpleChoiceAny(self.canvas) self._operation_choice.SetItems({key: value.name for key, value in self._operations.items()}) self._operation_choice.Bind(wx.EVT_CHOICE, self._on_operation_change) - self.AddStretchSpacer(1) self.Add(self._operation_choice) self._operation_sizer = wx.BoxSizer(wx.VERTICAL) self.Add(self._operation_sizer) - self.AddStretchSpacer(1) self._operation_change() @@ -47,3 +45,4 @@ def _operation_change(self): self._operation_sizer.GetItem(self._active_operation).DeleteWindows() self._active_operation = operation(self.canvas, self.canvas, self.canvas.world) self._operation_sizer.Add(self._active_operation, 1, wx.EXPAND) + self.Layout() diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 6cb5b163..99918a03 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -35,6 +35,7 @@ def __init__( OperationUI.__init__(self, parent, canvas, world, options_path) self._operation = operation + self.Hide() self._sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._sizer) self._options_sizer = wx.BoxSizer(wx.VERTICAL) @@ -46,11 +47,8 @@ def __init__( self._options = {} self._create_options(options) - self.Fit() self.Layout() - self.GetParent().Fit() - self.GetParent().Layout() - self.GetTopLevelParent().Fit() + self.Show() def unload(self): pass From 1e9fe640c03b70fe7d8d411f7273b257004124fc Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 13:16:12 +0100 Subject: [PATCH 41/84] Fixed the int entry accepting non-numeric characters --- amulet_map_editor/amulet_wx/validators.py | 38 +++++++++++++++++++ .../edit/plugins/api/fixed_pipeline.py | 5 ++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 amulet_map_editor/amulet_wx/validators.py diff --git a/amulet_map_editor/amulet_wx/validators.py b/amulet_map_editor/amulet_wx/validators.py new file mode 100644 index 00000000..492a8845 --- /dev/null +++ b/amulet_map_editor/amulet_wx/validators.py @@ -0,0 +1,38 @@ +import wx + + +class BaseValidator(wx.Validator): + ''' Validates data as it is entered into the text controls. ''' + + def __init__(self): + super().__init__() + self.Bind(wx.EVT_CHAR, self.OnChar) + + def Clone(self): + return self.__class__() + + def Validate(self, win): + return True + + def TransferToWindow(self): + return True + + def TransferFromWindow(self): + return True + + def OnChar(self, event): + event.Skip() + + +class IntValidator(BaseValidator): + def OnChar(self, event): + keycode = int(event.GetKeyCode()) + if keycode > 256 or 48 <= keycode <= 57 or keycode == 45: + event.Skip() + + +class FloatValidator(BaseValidator): + def OnChar(self, event): + keycode = int(event.GetKeyCode()) + if keycode > 256 or 45 <= keycode <= 57: + event.Skip() diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 99918a03..4f9081b8 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -1,6 +1,8 @@ import wx from typing import Callable, Dict, Any, TYPE_CHECKING, Sequence +from amulet_map_editor.amulet_wx.validators import IntValidator, FloatValidator + from amulet.api.data_types import OperationType from .operation_ui import OperationUI @@ -119,6 +121,7 @@ def _create_int(self, option_name: str, options: Sequence): ) else: return # should not get here + option.SetValidator(IntValidator()) sizer.Add(option) self._options[option_name] = option @@ -147,7 +150,7 @@ def _create_float(self, option_name: str, options: Sequence): initial=options[0], ) else: - return # should not get here + return # should not get here sizer.Add(option) self._options[option_name] = option From cb38b74ff09a15600483e49b59b312477c1f8474 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 14:38:03 +0100 Subject: [PATCH 42/84] Set up operation running and added a file open type --- .../edit/plugins/api/fixed_pipeline.py | 67 ++++++++++++++----- .../examples/3_fixed_function_pipeline.py | 3 +- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 4f9081b8..20660925 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -1,14 +1,18 @@ import wx from typing import Callable, Dict, Any, TYPE_CHECKING, Sequence -from amulet_map_editor.amulet_wx.validators import IntValidator, FloatValidator +from amulet_map_editor.amulet_wx.validators import IntValidator -from amulet.api.data_types import OperationType +from amulet.api.data_types import OperationReturnType from .operation_ui import OperationUI if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World + from amulet.api.data_types import Dimension + from amulet.api.selection import SelectionGroup + +FixedOperationType = Callable[["World", "Dimension", "SelectionGroup", Dict[str, Any]], OperationReturnType] class OperationError(Exception): @@ -30,7 +34,7 @@ def __init__( canvas: "EditCanvas", world: "World", options_path: str, - operation: OperationType, + operation: FixedOperationType, options: Dict[str, Any] ): wx.Panel.__init__(self, parent) @@ -46,7 +50,7 @@ def __init__( self._run_button.Bind(wx.EVT_BUTTON, self._run_operation) self._sizer.Add(self._run_button, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) - self._options = {} + self._options: Dict[str, wx.Window] = {} self._create_options(options) self.Layout() @@ -63,7 +67,8 @@ def _create_options(self, options: Dict[str, Sequence]): "float": self._create_float, "str": self._create_string, "str_choice": self._create_str_choice, - "file": self._create_file_picker, + "file_open": self._create_file_open_picker, + "file_save": self._create_file_save_picker, "directory": self._create_directory_picker } for option_name, option in options.items(): @@ -81,7 +86,7 @@ def _create_label(self, option_name: str, options: Sequence): def _create_horizontal_options_sizer(self, label) -> wx.BoxSizer: sizer = wx.BoxSizer(wx.HORIZONTAL) label = wx.StaticText(self, label=label) - sizer.Add(label, 0, wx.RIGHT, 5) + sizer.Add(label, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) self._options_sizer.Add(sizer, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) return sizer @@ -169,14 +174,24 @@ def _create_str_choice(self, option_name: str, options: Sequence): return sizer = self._create_horizontal_options_sizer(option_name) option = wx.Choice(self, choices=options) + option.SetSelection(0) sizer.Add(option) self._options[option_name] = option - def _create_file_picker(self, option_name: str, options: Sequence): + def _create_file_save_picker(self, option_name: str, options: Sequence): sizer = self._create_horizontal_options_sizer(option_name) option = wx.FilePickerCtrl( self, - style=wx.FLP_SAVE + style=wx.FLP_SAVE | wx.FLP_USE_TEXTCTRL + ) + sizer.Add(option) + self._options[option_name] = option + + def _create_file_open_picker(self, option_name: str, options: Sequence): + sizer = self._create_horizontal_options_sizer(option_name) + option = wx.FilePickerCtrl( + self, + style=wx.FLP_OPEN | wx.FLP_USE_TEXTCTRL ) sizer.Add(option) self._options[option_name] = option @@ -185,17 +200,37 @@ def _create_directory_picker(self, option_name: str, options: Sequence): sizer = self._create_horizontal_options_sizer(option_name) option = wx.DirPickerCtrl( self, - style=0 + style=wx.DIRP_USE_TEXTCTRL ) sizer.Add(option) self._options[option_name] = option - def _get_values(self): - return {} # TODO + def _get_values(self) -> Dict[str, Any]: + options = {} + for key, window in self._options.items(): + if isinstance( + window, + ( + wx.CheckBox, + wx.SpinCtrl, + wx.SpinCtrlDouble, + wx.TextCtrl, + + ) + ): + options[key] = window.GetValue() + elif isinstance(window, wx.Choice): + options[key] = window.GetString(window.GetSelection()) + elif isinstance(window, (wx.FilePickerCtrl, wx.DirPickerCtrl)): + options[key] = window.GetPath() + return options def _run_operation(self, evt): - return - # self.canvas.run_operation( - # - # ) - # self._operation(self.world, self.canvas.dimension, self._get_values()) + self.canvas.run_operation( + lambda: self._operation( + self.world, + self.canvas.dimension, + self.canvas.selection_group, + self._get_values() + ) + ) diff --git a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py index 738ee9b0..c0cd5474 100644 --- a/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/examples/3_fixed_function_pipeline.py @@ -35,7 +35,8 @@ "Text choice": ["str_choice", "choice 1", "choice 2", "choice 3"], # OS examples - "File picker": ["file"], # UI to pick a file + "File Open picker": ["file_open"], # UI to pick an existing file + "File Save picker": ["file_save"], # UI to pick a file to save to "Folder picker": ["directory"] # UI to pick a directory } From e0a6f76730987d68e12f6a8b31b3258932f8fb97 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 14:52:05 +0100 Subject: [PATCH 43/84] Updated delete_chunk to work with the new operation system --- amulet_map_editor/programs/edit/canvas/edit_canvas.py | 4 ++-- .../plugins/stock_plugins/operations/delete_chunk.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index d1f992b3..48f20dee 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -99,7 +99,7 @@ def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: msg, self, ) - self._world.create_undo_point() + self.world.create_undo_point() wx.PostEvent(self, CreateUndoEvent()) except Exception as e: self.enable_threads() @@ -136,7 +136,7 @@ def save(self): self.disable_threads() def save(): - for chunk_index, chunk_count in self._world.save_iter(): + for chunk_index, chunk_count in self.world.save_iter(): yield chunk_index / chunk_count show_loading_dialog(lambda: save(), f"Saving world.", "Please wait.", self) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/delete_chunk.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/delete_chunk.py index d689c981..b339ae03 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/delete_chunk.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/delete_chunk.py @@ -1,9 +1,11 @@ from amulet.operations.delete_chunk import delete_chunk + +def delete_chunk_wrapper(world, dimension, selection, _): + return delete_chunk(world, dimension, selection) + + export = { - "v": 1, # a version 1 plugin "name": "Delete Chunks", # the name of the plugin - "features": ["src_selection"], - "inputs": ["src_selection"], # the inputs to give to the plugin - "operation": delete_chunk # the actual function to call when running the plugin + "operation": delete_chunk_wrapper # the actual function to call when running the plugin } From 3949207920f35120e2d64b4cdc2347b7d6829a34 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 15:45:52 +0100 Subject: [PATCH 44/84] Fixed an error when switching between tools Set up better loading and unloading --- .../programs/edit/canvas/ui/tool/tool.py | 2 ++ .../canvas/ui/tool/tools/base_operation.py | 21 +++++++++++++++---- .../edit/canvas/ui/tool/tools/select.py | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py index 6329e6e1..430a9f7f 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py @@ -57,6 +57,7 @@ def register_tool(self, name: str, tool_cls: Type[BaseToolUIType]): def _enable_tool(self, tool: str): if tool in self._tools: if self._active_tool is not None: + self._active_tool.disable() if isinstance(self._active_tool, wx.Window): self._active_tool.Hide() elif isinstance(self._active_tool, wx.Sizer): @@ -66,6 +67,7 @@ def _enable_tool(self, tool: str): self._active_tool.Show() elif isinstance(self._active_tool, wx.Sizer): self._active_tool.ShowItems(show=True) + self._active_tool.enable() self.canvas.Layout() diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py index 879db99e..1724b6a9 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py @@ -22,7 +22,7 @@ def __init__(self, canvas: "EditCanvas"): self._operation_sizer = wx.BoxSizer(wx.VERTICAL) self.Add(self._operation_sizer) - self._operation_change() + # self._operation_change() @property def _operations(self) -> OperationStorageType: @@ -36,13 +36,26 @@ def _on_operation_change(self, evt): self._operation_change() evt.Skip() + def _unload_active_operation(self): + if self._active_operation is not None: + self._active_operation.unload() + if isinstance(self._active_operation, wx.Window): + self._active_operation.Destroy() + elif isinstance(self._active_operation, wx.Sizer): + self._operation_sizer.GetItem(self._active_operation).DeleteWindows() + self._active_operation = None + def _operation_change(self): operation_path = self._operation_choice.GetAny() if operation_path: operation = self._operations[operation_path] - if self._active_operation is not None: - self._active_operation.unload() - self._operation_sizer.GetItem(self._active_operation).DeleteWindows() + self.disable() self._active_operation = operation(self.canvas, self.canvas, self.canvas.world) self._operation_sizer.Add(self._active_operation, 1, wx.EXPAND) self.Layout() + + def enable(self): + self._operation_change() + + def disable(self): + self._unload_active_operation() diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index be9ba120..51797e4b 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -49,7 +49,9 @@ def __init__(self, canvas: 'EditCanvas'): def enable(self): self.canvas.select_mode = 0 - self.Show() + + def disable(self): + pass def _add_row(self, label: str, wx_object: Type[wx.Object], **kwargs) -> Any: sizer = wx.BoxSizer(wx.HORIZONTAL) From 21744651a4390af53d33fab6799fa970adba73ff Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 16:34:37 +0100 Subject: [PATCH 45/84] Added better error handling --- .../programs/edit/canvas/edit_canvas.py | 27 ++++++++++++++++--- .../programs/edit/plugins/__init__.py | 3 ++- .../programs/edit/plugins/api/errors.py | 24 +++++++++++++++++ .../edit/plugins/api/fixed_pipeline.py | 12 --------- 4 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 amulet_map_editor/programs/edit/plugins/api/errors.py diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 48f20dee..a11f42a3 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -2,14 +2,16 @@ from typing import TYPE_CHECKING, Callable, Any from types import GeneratorType import time +import traceback from amulet.api.data_types import OperationReturnType -from amulet_map_editor import CONFIG +from amulet_map_editor import CONFIG, log from amulet_map_editor.programs.edit.edit import EDIT_CONFIG_ID from amulet_map_editor.programs.edit.key_config import DefaultKeys, DefaultKeybindGroupId, PresetKeybinds from amulet_map_editor.programs.edit.canvas.ui.goto import show_goto from amulet_map_editor.programs.edit.canvas.ui.tool import Tool +from amulet_map_editor.programs.edit.plugins import OperationError, OperationSuccessful, OperationSilentAbort from amulet_map_editor.programs.edit.canvas.events import ( UndoEvent, @@ -90,7 +92,7 @@ def __init__(self, parent: wx.Window, world: "World"): tool_sizer = Tool(self) canvas_sizer.Add(tool_sizer, 1, wx.EXPAND, 0) - def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: + def run_operation(self, operation: Callable[[], None], title="", msg="", create_backup=True) -> Any: self.disable_threads() try: out = show_loading_dialog( @@ -99,9 +101,26 @@ def run_operation(self, operation: Callable[[], None], title="", msg="") -> Any: msg, self, ) - self.world.create_undo_point() - wx.PostEvent(self, CreateUndoEvent()) + if create_backup: + self.world.create_undo_point() + wx.PostEvent(self, CreateUndoEvent()) + except OperationError as e: + msg = f"Error running operation: {e}" + log.info(msg) + wx.MessageDialog(self, msg, style=wx.OK).ShowModal() + self.enable_threads() + raise e + except OperationSuccessful as e: + msg = str(e) + log.info(msg) + wx.MessageDialog(self, msg, style=wx.OK).ShowModal() + self.enable_threads() + raise e + except OperationSilentAbort as e: + self.enable_threads() + raise e except Exception as e: + log.error(traceback.format_exc()) self.enable_threads() raise e self.enable_threads() diff --git a/amulet_map_editor/programs/edit/plugins/__init__.py b/amulet_map_editor/programs/edit/plugins/__init__.py index 232d66a4..061e5ebb 100644 --- a/amulet_map_editor/programs/edit/plugins/__init__.py +++ b/amulet_map_editor/programs/edit/plugins/__init__.py @@ -1,4 +1,5 @@ from .api.data_types import PathType, OperationStorageType from .api.operation_ui import OperationUI, OperationUIType -from .api.fixed_pipeline import FixedFunctionUI, OperationError, OperationSuccessful +from .api.fixed_pipeline import FixedFunctionUI +from .api.errors import OperationError, OperationSuccessful, OperationSilentAbort from .api.loader import all_operations, internal_operations, operations, export_operations, import_operations, reload_operations diff --git a/amulet_map_editor/programs/edit/plugins/api/errors.py b/amulet_map_editor/programs/edit/plugins/api/errors.py new file mode 100644 index 00000000..71cde404 --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/api/errors.py @@ -0,0 +1,24 @@ +class BaseOperationException(Exception): + pass + + +class OperationError(BaseOperationException): + """Error to raise if something went wrong when running the operation. + Will notify the user with a dialog box with the message f"Error running operation {msg}" + Changes will be rolled back to the last backup. + Eg. if the operation requires something it is not given""" + pass + + +class OperationSuccessful(BaseOperationException): + """raise this if you want to exit the operation without creating an undo point. + Will notify the user with a dialog box containing the message. + Changes will be rolled back to the last backup.""" + pass + + +class OperationSilentAbort(BaseOperationException): + """Error to raise if something went wrong when running the operation but you do not want the user to see. + This error will be handled silently. + Changes will be rolled back to the last backup.""" + pass diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 20660925..70f63819 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -15,18 +15,6 @@ FixedOperationType = Callable[["World", "Dimension", "SelectionGroup", Dict[str, Any]], OperationReturnType] -class OperationError(Exception): - """Error to raise if something went wrong when running the operation. - Eg. if the operation requires something it is not given""" - pass - - -class OperationSuccessful(Exception): - """raise this if you want to exit the operation without creating an undo point. - Any changes to the world since the last undo point will be reverted.""" - pass - - class FixedFunctionUI(wx.Panel, OperationUI): def __init__( self, From 8c5d830cf32fe1e0178976e67a177e775f1fc17b Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 17:08:39 +0100 Subject: [PATCH 46/84] Reworked loader logic to not require mode --- .../programs/edit/plugins/api/loader.py | 43 +++++++++---------- .../programs/edit/plugins/api/operation_ui.py | 2 +- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index dd9df1f8..a9ced139 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -63,29 +63,28 @@ def _load(self, export_dict: dict): ) ) - mode = export_dict.get("mode", "fixed") - if not isinstance(mode, str): - raise OperationLoadException('"name" in export is not a string.') - - if mode == "fixed": - operation = export_dict.get("operation", None) - if not callable(operation): - raise OperationLoadException('"operation" in export must be callable.') - if operation.__code__.co_argcount != 4: - raise OperationLoadException('"operation" function in export must have 4 inputs.') - options = export_dict.get("options", {}) - if not isinstance(options, dict): - raise OperationLoadException('"operation" in export must be a dictionary if defined.') - self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, options_path, operation, options) - elif mode == "dynamic": - operation_ui: Type[OperationUI] = export_dict.get("operation", None) - if not issubclass(operation_ui, OperationUI): - raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') - if not issubclass(operation_ui, (wx.Window, wx.Sizer)): - raise OperationLoadException('"operation" must be a subclass of wx.Window or wx.Sizer.') - self._ui = lambda parent, canvas, world: operation_ui(parent, canvas, world, options_path) + if "operation" in export_dict: + if issubclass(export_dict["operation"], (wx.Window, wx.Sizer)): + operation_ui: Type[OperationUI] = export_dict.get("operation", None) + if not issubclass(operation_ui, OperationUI): + raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') + self._ui = lambda parent, canvas, world: operation_ui(parent, canvas, world, options_path) + + elif callable(export_dict["operation"]): + operation = export_dict.get("operation", None) + if not callable(operation): + raise OperationLoadException('"operation" in export must be callable.') + if operation.__code__.co_argcount != 4: + raise OperationLoadException('"operation" function in export must have 4 inputs.') + options = export_dict.get("options", {}) + if not isinstance(options, dict): + raise OperationLoadException('"operation" in export must be a dictionary if defined.') + self._ui = lambda parent, canvas, world: FixedFunctionUI(parent, canvas, world, options_path, operation, options) + + else: + raise OperationLoadException('"operation" in export must be a callable, or a subclass of wx.Window or wx.Sizer.') else: - raise OperationLoadException('"mode" in export must be either "fixed" or "dynamic".') + raise OperationLoadException('"operation" is not present in export.') @property def name(self) -> str: diff --git a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py index 9a1ba5a8..93b70976 100644 --- a/amulet_map_editor/programs/edit/plugins/api/operation_ui.py +++ b/amulet_map_editor/programs/edit/plugins/api/operation_ui.py @@ -46,6 +46,6 @@ def _load_options(self, default=None) -> Any: def _save_options(self, options: Any): """Save the given options to disk so that they persist in the next session.""" - os.makedirs(os.path.basename(self._options_path), exist_ok=True) + os.makedirs(os.path.dirname(self._options_path), exist_ok=True) with open(self._options_path, "wb") as f: return pickle.dump(options, f) From e736c2b4502745df5344ab5fcb30560238b30954 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 17:10:14 +0100 Subject: [PATCH 47/84] Redesigned fill operation --- .../plugins/stock_plugins/operations/fill.py | 98 +++++++++++-------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py index fd6652d8..7ae9feb8 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py @@ -2,56 +2,70 @@ import wx from amulet.operations.fill import fill -from amulet.api.selection import SelectionGroup -from amulet.api.block import Block -from amulet.api.data_types import Dimension from amulet_map_editor.amulet_wx.block_select import BlockDefine -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.programs.edit.plugins import OperationUI if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -def fill_( - world: "World", - dimension: Dimension, - selection: SelectionGroup, - options: dict -): - if isinstance(options.get('fill_block'), Block): - yield from fill(world, dimension, selection, options) - else: - raise Exception('Please specify a block before running the fill operation') - - -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Fill') - block_define = BlockDefine( - dialog, - world.world_wrapper.translation_manager, - *(options.get("fill_block_options", []) or [world.world_wrapper.platform]) - ) - dialog.sizer.Add(block_define, 1) - dialog.Fit() - if dialog.ShowModal() == wx.ID_OK: - options = { - "fill_block": world.world_wrapper.translation_manager.get_version( - block_define.platform, - block_define.version - ).block.to_universal( - block_define.block, - force_blockstate=block_define.force_blockstate - )[0], - "fill_block_options": block_define.options - } - return options +class Fill(wx.Panel, OperationUI): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + wx.Panel.__init__(self, parent) + OperationUI.__init__(self, parent, canvas, world, options_path) + + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + options = self._load_options({}) + + self._block_define = BlockDefine( + self, + world.world_wrapper.translation_manager, + *(options.get("fill_block_options", []) or [world.world_wrapper.platform]) + ) + self._sizer.Add(self._block_define, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self._run_button = wx.Button(self, label="Run Operation") + self._run_button.Bind(wx.EVT_BUTTON, self._run_operation) + self._sizer.Add(self._run_button, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self.Layout() + + def _get_fill_block(self): + return self.world.world_wrapper.translation_manager.get_version( + self._block_define.platform, + self._block_define.version + ).block.to_universal( + self._block_define.block, + force_blockstate=self._block_define.force_blockstate + )[0] + + def unload(self): + self._save_options({ + "fill_block": self._get_fill_block(), + "fill_block_options": self._block_define.options + }) + + def _run_operation(self, _): + self.canvas.run_operation( + lambda: fill( + self.world, + self.canvas.dimension, + self.canvas.selection_group, + self._get_fill_block() + ) + ) export = { - "v": 1, # a version 1 plugin "name": "Fill", # the name of the plugin - "features": ["src_selection", "wxoptions"], - "inputs": ["src_selection", "options"], # the inputs to give to the plugin - "operation": fill_, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": Fill, # the actual function to call when running the plugin } From edd9956363a99c4a413fd197d6c3f25316ad8a1e Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 17:34:51 +0100 Subject: [PATCH 48/84] Fixed error loading some operations --- amulet_map_editor/programs/edit/plugins/api/loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index a9ced139..39f48320 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -6,6 +6,7 @@ import wx import struct import hashlib +import inspect from .fixed_pipeline import FixedFunctionUI from .operation_ui import OperationUI, OperationUIType @@ -64,7 +65,7 @@ def _load(self, export_dict: dict): ) if "operation" in export_dict: - if issubclass(export_dict["operation"], (wx.Window, wx.Sizer)): + if inspect.isclass(export_dict["operation"]) and issubclass(export_dict["operation"], (wx.Window, wx.Sizer)): operation_ui: Type[OperationUI] = export_dict.get("operation", None) if not issubclass(operation_ui, OperationUI): raise OperationLoadException('"operation" must be a subclass of edit.plugins.OperationUI.') From 275d7340c882ca6cd0f89d912cdafe9d8697a5b2 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 2 Jun 2020 17:35:05 +0100 Subject: [PATCH 49/84] Fixed block select layout issues --- amulet_map_editor/amulet_wx/block_select.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/amulet_map_editor/amulet_wx/block_select.py b/amulet_map_editor/amulet_wx/block_select.py index 41e87419..af871f4a 100644 --- a/amulet_map_editor/amulet_wx/block_select.py +++ b/amulet_map_editor/amulet_wx/block_select.py @@ -349,6 +349,7 @@ def _set_properties(self, properties: Dict[str, str] = None): if 'properties' in specification: for prop, options in specification['properties'].items(): self._add_property(prop, options, properties.get(prop, None)) - self.Fit() - self.Layout() - self.GetTopLevelParent().Fit() + parent = self.GetParent() # there may be a better way to do this + while parent is not None: + parent.Layout() + parent = parent.GetParent() From 943f538b6d6dc86117e2dfbaa4841281ec0342b3 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 11:41:36 +0100 Subject: [PATCH 50/84] Updated replace to use the new operation UI system Also fixed some UI updating --- amulet_map_editor/amulet_wx/block_select.py | 1 + amulet_map_editor/amulet_wx/simple.py | 2 +- .../programs/edit/canvas/edit_canvas.py | 2 +- .../stock_plugins/operations/replace.py | 196 ++++++++++-------- 4 files changed, 111 insertions(+), 90 deletions(-) diff --git a/amulet_map_editor/amulet_wx/block_select.py b/amulet_map_editor/amulet_wx/block_select.py index af871f4a..321006bc 100644 --- a/amulet_map_editor/amulet_wx/block_select.py +++ b/amulet_map_editor/amulet_wx/block_select.py @@ -349,6 +349,7 @@ def _set_properties(self, properties: Dict[str, str] = None): if 'properties' in specification: for prop, options in specification['properties'].items(): self._add_property(prop, options, properties.get(prop, None)) + self.SetMinSize(self.GetSizer().CalcMin()) parent = self.GetParent() # there may be a better way to do this while parent is not None: parent.Layout() diff --git a/amulet_map_editor/amulet_wx/simple.py b/amulet_map_editor/amulet_wx/simple.py index 7659d93d..c584e470 100644 --- a/amulet_map_editor/amulet_wx/simple.py +++ b/amulet_map_editor/amulet_wx/simple.py @@ -5,7 +5,7 @@ class SimpleSizer: def __init__(self, sizer_dir=wx.VERTICAL): - self.sizer = wx.BoxSizer(sizer_dir) + self._sizer = self.sizer = wx.BoxSizer(sizer_dir) def add_object(self, obj, space=1, options=wx.ALL): self.sizer.Add( diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index a11f42a3..4ccea61a 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -92,7 +92,7 @@ def __init__(self, parent: wx.Window, world: "World"): tool_sizer = Tool(self) canvas_sizer.Add(tool_sizer, 1, wx.EXPAND, 0) - def run_operation(self, operation: Callable[[], None], title="", msg="", create_backup=True) -> Any: + def run_operation(self, operation: Callable[[], OperationReturnType], title="", msg="", create_backup=True) -> Any: self.disable_threads() try: out = show_loading_dialog( diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py index ee921cf8..42a2a95a 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py @@ -1,101 +1,121 @@ -from typing import TYPE_CHECKING, Tuple, Dict +from typing import TYPE_CHECKING import wx import numpy from amulet.api.block import Block -from amulet.api.selection import SelectionGroup -from amulet.api.data_types import Dimension from amulet_map_editor.amulet_wx.block_select import BlockDefine -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.programs.edit.plugins import OperationUI +from amulet_map_editor.amulet_wx.simple import SimpleScrollablePanel if TYPE_CHECKING: from amulet.api.world import World - - -def replace( - world: "World", - dimension: Dimension, - selection: SelectionGroup, - options: dict -): - original_block_options: Tuple[str, Tuple[int, int, int], bool, str, str, Dict[str, str]] = options.get("original_block_options") - replacement_block: Block = options.get("replacement_block") - if original_block_options is None or not isinstance(replacement_block, Block): - # verify that the options are actually given - raise Exception('Please specify the blocks before running the replace operation') - - original_platform, original_version, original_blockstate, original_namespace, original_base_name, original_properties = original_block_options - replacement_block_id = world.palette.get_add_block(replacement_block) - - original_block_matches = [] - universal_block_count = 0 - - iter_count = len(list(world.get_chunk_slices(selection, dimension))) - count = 0 - - for chunk, slices, _ in world.get_chunk_slices(selection, dimension): - if universal_block_count < len(world.palette): - for universal_block_id in range(universal_block_count, len(world.palette)): - version_block = world.translation_manager.get_version( - original_platform, - original_version - ).block.from_universal( - world.palette[universal_block_id], - force_blockstate=original_blockstate - )[0] - if version_block.namespace == original_namespace and \ - version_block.base_name == original_base_name\ - and all(original_properties.get(prop) in ['*', val.to_snbt()] for prop, val in version_block.properties.items()): - original_block_matches.append(universal_block_id) - - universal_block_count = len(world.palette) - blocks = chunk.blocks[slices] - blocks[numpy.isin(blocks, original_block_matches)] = replacement_block_id - chunk.blocks[slices] = blocks - chunk.changed = True - - count += 1 - yield 100 * count / iter_count - - -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Replace', wx.HORIZONTAL) - - original_block = BlockDefine( - dialog, - world.world_wrapper.translation_manager, - *(options.get("original_block_options", []) or [world.world_wrapper.platform]), - wildcard=True - ) - replacement_block = BlockDefine( - dialog, - world.world_wrapper.translation_manager, - *(options.get("replacement_block_options", []) or [world.world_wrapper.platform]) - ) - dialog.sizer.Add(original_block, 0) - dialog.sizer.Add(replacement_block, 0) - dialog.Fit() - - if dialog.ShowModal() == wx.ID_OK: - options = { - "original_block_options": original_block.options, - "replacement_block": world.translation_manager.get_version( - replacement_block.platform, - replacement_block.version - ).block.to_universal( - replacement_block.block, - force_blockstate=replacement_block.force_blockstate - )[0], - "replacement_block_options": replacement_block.options, - } - return options + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class Replace(SimpleScrollablePanel, OperationUI): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleScrollablePanel.__init__(self, parent) + OperationUI.__init__(self, parent, canvas, world, options_path) + + options = self._load_options({}) + + self._original_block = BlockDefine( + self, + world.world_wrapper.translation_manager, + *(options.get("original_block_options", []) or [world.world_wrapper.platform]), + wildcard=True + ) + self._sizer.Add(self._original_block, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + self._replacement_block = BlockDefine( + self, + world.world_wrapper.translation_manager, + *(options.get("replacement_block_options", []) or [world.world_wrapper.platform]) + ) + self._sizer.Add(self._replacement_block, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self._run_button = wx.Button(self, label="Run Operation") + self._run_button.Bind(wx.EVT_BUTTON, self._run_operation) + self._sizer.Add(self._run_button, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self.Layout() + + def _get_replacement_block(self) -> Block: + return self.world.translation_manager.get_version( + self._replacement_block.platform, + self._replacement_block.version + ).block.to_universal( + self._replacement_block.block, + force_blockstate=self._replacement_block.force_blockstate + )[0] + + def unload(self): + self._save_options({ + "original_block_options": self._original_block.options, + "replacement_block": self._get_replacement_block(), + "replacement_block_options": self._replacement_block.options, + }) + + def _run_operation(self, _): + self.canvas.run_operation( + lambda: self._replace() + ) + + def _replace(self): + world = self.world + selection = self.canvas.selection_group + dimension = self.canvas.dimension + + original_platform, original_version, original_blockstate, original_namespace, original_base_name, original_properties = self._original_block.options + replacement_block = self._get_replacement_block() + + replacement_block_id = world.palette.get_add_block(replacement_block) + + original_block_matches = [] + universal_block_count = 0 + + iter_count = len(list(world.get_chunk_slices(selection, dimension))) + count = 0 + + for chunk, slices, _ in world.get_chunk_slices(selection, dimension): + if universal_block_count < len(world.palette): + for universal_block_id in range(universal_block_count, len(world.palette)): + version_block = world.translation_manager.get_version( + original_platform, + original_version + ).block.from_universal( + world.palette[universal_block_id], + force_blockstate=original_blockstate + )[0] + if version_block.namespace == original_namespace and \ + version_block.base_name == original_base_name \ + and all(original_properties.get(prop) in ['*', val.to_snbt()] for prop, val in version_block.properties.items()): + original_block_matches.append(universal_block_id) + + universal_block_count = len(world.palette) + blocks = chunk.blocks[slices] + blocks[numpy.isin(blocks, original_block_matches)] = replacement_block_id + chunk.blocks[slices] = blocks + chunk.changed = True + + count += 1 + yield 100 * 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 = { - "v": 1, # a version 1 plugin "name": "Replace", # the name of the plugin - "features": ["src_selection", "wxoptions"], - "inputs": ["src_selection", "options"], # the inputs to give to the plugin - "operation": replace, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": Replace, # the actual function to call when running the plugin } From 3bd6569c1f5685bd40ade882e74af657dbe0bd6c Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 12:58:43 +0100 Subject: [PATCH 51/84] Made block selection more pretty --- amulet_map_editor/amulet_wx/block_select.py | 56 ++++++++++--------- .../plugins/stock_plugins/operations/fill.py | 4 +- .../stock_plugins/operations/replace.py | 8 ++- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/amulet_map_editor/amulet_wx/block_select.py b/amulet_map_editor/amulet_wx/block_select.py index 321006bc..23578a63 100644 --- a/amulet_map_editor/amulet_wx/block_select.py +++ b/amulet_map_editor/amulet_wx/block_select.py @@ -1,4 +1,4 @@ -from amulet_map_editor.amulet_wx.simple import SimplePanel, SimpleText, SimpleChoice, SimpleChoiceAny +from amulet_map_editor.amulet_wx.simple import SimpleChoice, SimpleChoiceAny import wx import PyMCTranslate import amulet_nbt @@ -6,17 +6,19 @@ from amulet.api.block import Block -class BaseSelect(SimplePanel): - def __init__(self, parent): - super().__init__(parent) +class BaseSelect(wx.Panel): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) def _add_ui_element(self, label: str, obj: Type[wx.Control], **kwargs) -> Any: - panel = SimplePanel(self, wx.HORIZONTAL) - self.add_object(panel, 0) - text = SimpleText(panel, label) - panel.add_object(text, 0, wx.CENTER | wx.ALL) - wx_obj = obj(panel, **kwargs) - panel.add_object(wx_obj, 0, wx.CENTER | wx.ALL) + sizer = wx.BoxSizer(wx.HORIZONTAL) + self._sizer.Add(sizer, 0, wx.ALIGN_CENTER_HORIZONTAL) + text = wx.StaticText(self, label=label) + sizer.Add(text, 0, wx.CENTER | wx.LEFT | wx.RIGHT, 5) + wx_obj = obj(self, **kwargs) + sizer.Add(wx_obj, 0, wx.CENTER | wx.BOTTOM | wx.TOP | wx.RIGHT, 5) return wx_obj @@ -31,7 +33,7 @@ def __init__( allowed_platforms: Tuple[str, ...] = None, **kwargs ): - super().__init__(parent) + super().__init__(parent, **kwargs) self._translation_manager = translation_manager self._allow_universal = allow_universal self._allow_vanilla = allow_vanilla @@ -269,6 +271,7 @@ def __init__( base_name: str = None, properties: Dict[str, str] = None, wildcard: bool = False, + properties_style=0, **kwargs ): super().__init__( @@ -281,11 +284,13 @@ def __init__( base_name, **kwargs ) - self._properties: List[SimplePanel] = [] + self._properties: Dict[str, SimpleChoice] = {} self._wildcard = wildcard self._base_name_list.Bind(wx.EVT_CHOICE, self._on_base_name_change) - self._properties_panel: Optional[SimplePanel] = SimplePanel(self, wx.VERTICAL) - self.add_object(self._properties_panel, 0) + self._properties_panel: wx.Panel = wx.Panel(self, wx.VERTICAL, style=properties_style) + properties_sizer = wx.BoxSizer(wx.VERTICAL) + self._properties_panel.SetSizer(properties_sizer) + self._sizer.Add(self._properties_panel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) self._set_properties(properties) @property @@ -295,8 +300,8 @@ def options(self) -> Tuple[str, Tuple[int, int, int], bool, str, str, Dict[str, @property def properties(self) -> Dict[str, str]: return { - prop.GetChildren()[0].GetLabel(): prop.GetChildren()[1].GetString(prop.GetChildren()[1].GetSelection()) - for prop in self._properties + property_name: ui.GetString(ui.GetSelection()) + for property_name, ui in self._properties.items() } @property @@ -311,19 +316,20 @@ def block(self) -> Block: ) def _clear_properties(self): - for prop in self._properties: - prop.Destroy() + for child in self._properties_panel.GetChildren(): + child.Destroy() self._properties.clear() self._properties_panel.Layout() def _add_property(self, property_name: str, property_values: List[str], default: str = None): - prop_panel = SimplePanel(self._properties_panel, wx.HORIZONTAL) - self._properties.append(prop_panel) - self._properties_panel.add_object(prop_panel, 0) - name_text = SimpleText(prop_panel, property_name) - prop_panel.add_object(name_text, 0, wx.CENTER | wx.ALL) - name_list = SimpleChoice(prop_panel) - prop_panel.add_object(name_list, 0, wx.CENTER | wx.ALL) + sizer = wx.BoxSizer(wx.HORIZONTAL) + self._properties_panel.GetSizer().Add(sizer, 0, wx.ALL, 5) + name_text = wx.StaticText(self._properties_panel, label=property_name) + sizer.Add(name_text, 0, wx.CENTER | wx.RIGHT, 5) + name_list = SimpleChoice(self._properties_panel) + sizer.Add(name_list, 0, wx.CENTER, 5) + + self._properties[property_name] = name_list if self._wildcard: property_values.insert(0, "*") diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py index 7ae9feb8..a1572dca 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py @@ -29,7 +29,9 @@ def __init__( self._block_define = BlockDefine( self, world.world_wrapper.translation_manager, - *(options.get("fill_block_options", []) or [world.world_wrapper.platform]) + *(options.get("fill_block_options", []) or [world.world_wrapper.platform]), + style=wx.BORDER_SIMPLE, + properties_style=wx.BORDER_SIMPLE ) self._sizer.Add(self._block_define, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py index 42a2a95a..e35692f5 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py @@ -29,13 +29,17 @@ def __init__( self, world.world_wrapper.translation_manager, *(options.get("original_block_options", []) or [world.world_wrapper.platform]), - wildcard=True + wildcard=True, + style=wx.BORDER_SIMPLE, + properties_style=wx.BORDER_SIMPLE ) self._sizer.Add(self._original_block, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) self._replacement_block = BlockDefine( self, world.world_wrapper.translation_manager, - *(options.get("replacement_block_options", []) or [world.world_wrapper.platform]) + *(options.get("replacement_block_options", []) or [world.world_wrapper.platform]), + style=wx.BORDER_SIMPLE, + properties_style=wx.BORDER_SIMPLE ) self._sizer.Add(self._replacement_block, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) From 1210cf7251cc2b8fb3a69ee7ef6ada44442a3b5a Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 15:26:01 +0100 Subject: [PATCH 52/84] Added resetting canvas bind events With the option for third party creators to bind events we need a way to ensure that events do not get left over --- .../programs/edit/canvas/base_edit_canvas.py | 26 +++++++++++++++---- .../edit/canvas/controllable_edit_canvas.py | 13 ++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 25b3e611..2d133e7e 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -1,7 +1,7 @@ import wx from OpenGL.GL import * import os -from typing import TYPE_CHECKING, Optional, Any, Dict, Tuple, List, Generator +from typing import TYPE_CHECKING, Optional, Any, Dict, Tuple, List, Generator, Set import numpy import weakref @@ -43,6 +43,9 @@ def __init__(self, parent: wx.Window, world: 'World'): super().__init__(parent) glClearColor(*self.background_colour, 1.0) self.Hide() + + self._bound_events: Set[wx.PyEventBinder] = set() + self._world = weakref.ref(world) self._mouse_delta_x = 0 self._mouse_delta_y = 0 @@ -93,14 +96,27 @@ def __init__(self, parent: wx.Window, world: 'World'): self._structure_locations: List[numpy.ndarray] = [] self._draw_timer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self._on_draw, self._draw_timer) - self._gc_timer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self._gc, self._gc_timer) - self._rebuild_timer = wx.Timer(self) + self._bind_base_events() + + def reset_bound_events(self): + """Unbind all events and re-bind the default events. + We are allowing users to bind custom events so we should have a way to reset what is bound.""" + for event in self._bound_events: + self.Unbind(event) + self._bind_base_events() + + def _bind_base_events(self): + self.Bind(wx.EVT_TIMER, self._on_draw, self._draw_timer) + self.Bind(wx.EVT_TIMER, self._gc, self._gc_timer) self.Bind(wx.EVT_TIMER, self._rebuild, self._rebuild_timer) + def Bind(self, event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY): + """Bind an event to the canvas.""" + self._bound_events.add(event) + super().Bind(event, handler, source, id, id2) + @property def world(self) -> "World": return self._world() diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index 18a8a0f8..5f2f902b 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -20,6 +20,17 @@ def __init__(self, world_panel: wx.Window, world: 'World'): self._persistent_actions: Set[str] = set() # wx only fires events for when a key is initially pressed or released. This stores actions for keys that are held down. self._key_binds: ActionLookupType = {} # a store for which keys run which actions + # timer to deal with persistent actions + self._input_timer = wx.Timer(self) + self._bind_controllable_events() + + def reset_bound_events(self): + """Unbind all events and re-bind the default events. + We are allowing users to bind custom events so we should have a way to reset what is bound.""" + super().reset_bound_events() + self._bind_controllable_events() + + def _bind_controllable_events(self): self.Bind(wx.EVT_KILL_FOCUS, self._on_loss_focus) self.Bind(wx.EVT_MOTION, self._on_mouse_motion) @@ -34,8 +45,6 @@ def __init__(self, world_panel: wx.Window, world: 'World'): self.Bind(wx.EVT_KEY_UP, self._release) self.Bind(wx.EVT_MOUSEWHEEL, self._release) - # timer to deal with persistent actions - self._input_timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self._process_persistent_inputs, self._input_timer) def enable(self): From dec405cdda9adf0844895a60462aa750053f5ae4 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 15:26:35 +0100 Subject: [PATCH 53/84] Fixed select destination taking the lowest value if a letter is typed --- .../edit/canvas/ui/tool/tools/operation/select_destination.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py index 18575a8f..d261e6f9 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py @@ -4,6 +4,7 @@ from amulet.api.structure import Structure from amulet_map_editor.amulet_wx.simple import SimplePanel +from amulet_map_editor.amulet_wx.validators import IntValidator class SelectDestinationUI(SimplePanel): @@ -22,6 +23,8 @@ def __init__(self, parent, cancel_callback, confirm_callback, locations: List[nu self._x: wx.SpinCtrl = self._add_row('x', wx.SpinCtrl, min=-30000000, max=30000000) self._y: wx.SpinCtrl = self._add_row('y', wx.SpinCtrl, min=-30000000, max=30000000) self._z: wx.SpinCtrl = self._add_row('z', wx.SpinCtrl, min=-30000000, max=30000000) + for ui in (self._x, self._y, self._z): + ui.SetValidator(IntValidator()) self._copy_air: wx.CheckBox = self._add_row('Copy Air', wx.CheckBox) self._x.Bind(wx.EVT_SPINCTRL, self._on_location_change) self._y.Bind(wx.EVT_SPINCTRL, self._on_location_change) From e954387d80423914058e57e6a22627bb7897a214 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 15:28:30 +0100 Subject: [PATCH 54/84] Renamed block and world select --- amulet_map_editor/amulet_ui.py | 2 +- .../amulet_wx/{block_select.py => select_block.py} | 0 .../amulet_wx/{world_select.py => select_world.py} | 0 amulet_map_editor/programs/__init__.py | 2 +- amulet_map_editor/programs/convert/convert.py | 2 +- .../plugins/stock_plugins/export_operations/construction.py | 2 +- .../edit/plugins/stock_plugins/export_operations/mcstructure.py | 2 +- .../edit/plugins/stock_plugins/export_operations/schematic.py | 2 +- .../programs/edit/plugins/stock_plugins/operations/fill.py | 2 +- .../programs/edit/plugins/stock_plugins/operations/replace.py | 2 +- .../programs/edit/plugins/stock_plugins/operations/waterlog.py | 2 +- 11 files changed, 9 insertions(+), 9 deletions(-) rename amulet_map_editor/amulet_wx/{block_select.py => select_block.py} (100%) rename amulet_map_editor/amulet_wx/{world_select.py => select_world.py} (100%) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index 64ac678c..17343f50 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -6,7 +6,7 @@ import webbrowser from amulet.api.errors import LoaderNoneMatched -from amulet_map_editor.amulet_wx.world_select import WorldSelectDialog +from amulet_map_editor.amulet_wx.select_world import WorldSelectDialog from amulet_map_editor import lang, version, log, IMG_DIR from amulet_map_editor.programs import WorldManagerUI from amulet_map_editor.programs import BaseWorldUI diff --git a/amulet_map_editor/amulet_wx/block_select.py b/amulet_map_editor/amulet_wx/select_block.py similarity index 100% rename from amulet_map_editor/amulet_wx/block_select.py rename to amulet_map_editor/amulet_wx/select_block.py diff --git a/amulet_map_editor/amulet_wx/world_select.py b/amulet_map_editor/amulet_wx/select_world.py similarity index 100% rename from amulet_map_editor/amulet_wx/world_select.py rename to amulet_map_editor/amulet_wx/select_world.py diff --git a/amulet_map_editor/programs/__init__.py b/amulet_map_editor/programs/__init__.py index 1358d5ed..fc63bdf6 100644 --- a/amulet_map_editor/programs/__init__.py +++ b/amulet_map_editor/programs/__init__.py @@ -10,7 +10,7 @@ from amulet_map_editor import log from amulet_map_editor.amulet_wx.simple import SimplePanel -from amulet_map_editor.amulet_wx.world_select import WorldUI +from amulet_map_editor.amulet_wx.select_world import WorldUI if TYPE_CHECKING: from amulet.api.world import World diff --git a/amulet_map_editor/programs/convert/convert.py b/amulet_map_editor/programs/convert/convert.py index b7fe993c..d65d0e9a 100644 --- a/amulet_map_editor/programs/convert/convert.py +++ b/amulet_map_editor/programs/convert/convert.py @@ -8,7 +8,7 @@ from amulet_map_editor import lang, log from amulet_map_editor.amulet_wx.simple import SimplePanel -from amulet_map_editor.amulet_wx.world_select import WorldSelectDialog, WorldUI +from amulet_map_editor.amulet_wx.select_world import WorldSelectDialog, WorldUI from amulet_map_editor.programs import BaseWorldProgram, MenuData if TYPE_CHECKING: diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py index 80a69faa..eab0fc4a 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.block_select import VersionSelect +from amulet_map_editor.amulet_wx.select_block import VersionSelect from amulet_map_editor.amulet_wx.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py index 9b2bc671..784d4588 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.block_select import VersionSelect +from amulet_map_editor.amulet_wx.select_block import VersionSelect from amulet_map_editor.amulet_wx.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py index dab139a3..b50627a4 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.block_select import PlatformSelect +from amulet_map_editor.amulet_wx.select_block import PlatformSelect from amulet_map_editor.amulet_wx.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py index a1572dca..82bad248 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py @@ -2,7 +2,7 @@ import wx from amulet.operations.fill import fill -from amulet_map_editor.amulet_wx.block_select import BlockDefine +from amulet_map_editor.amulet_wx.select_block import BlockDefine from amulet_map_editor.programs.edit.plugins import OperationUI if TYPE_CHECKING: diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py index e35692f5..97392fd8 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py @@ -3,7 +3,7 @@ import numpy from amulet.api.block import Block -from amulet_map_editor.amulet_wx.block_select import BlockDefine +from amulet_map_editor.amulet_wx.select_block import BlockDefine from amulet_map_editor.programs.edit.plugins import OperationUI from amulet_map_editor.amulet_wx.simple import SimpleScrollablePanel diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py index de65878f..c82df054 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py @@ -5,7 +5,7 @@ from amulet.api.selection import SelectionGroup from amulet.api.block import Block from amulet.api.data_types import Dimension -from amulet_map_editor.amulet_wx.block_select import BlockDefine +from amulet_map_editor.amulet_wx.select_block import BlockDefine from amulet_map_editor.amulet_wx.simple import SimpleDialog if TYPE_CHECKING: From 1615fc2b0cd0b44b6d87c321f04ee9c9b97573b8 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 15:43:47 +0100 Subject: [PATCH 55/84] Reorganised amulet_wx --- amulet_map_editor/amulet_ui.py | 2 +- amulet_map_editor/amulet_wx/simple.py | 16 ++++------------ amulet_map_editor/amulet_wx/ui/__init__.py | 0 .../amulet_wx/{ => ui}/select_block.py | 0 .../ui/select_paste_location.py} | 2 +- .../amulet_wx/{ => ui}/select_world.py | 0 amulet_map_editor/amulet_wx/util/__init__.py | 0 amulet_map_editor/amulet_wx/{ => util}/icon.py | 0 .../amulet_wx/{ => util}/key_config.py | 2 +- .../amulet_wx/{ => util}/validators.py | 0 amulet_map_editor/programs/__init__.py | 2 +- amulet_map_editor/programs/convert/convert.py | 2 +- .../edit/canvas/controllable_edit_canvas.py | 2 +- amulet_map_editor/programs/edit/edit.py | 2 +- amulet_map_editor/programs/edit/key_config.py | 2 +- .../programs/edit/plugins/api/fixed_pipeline.py | 4 +--- .../export_operations/construction.py | 2 +- .../export_operations/mcstructure.py | 2 +- .../stock_plugins/export_operations/schematic.py | 2 +- .../plugins/stock_plugins/operations/fill.py | 2 +- .../plugins/stock_plugins/operations/replace.py | 2 +- .../plugins/stock_plugins/operations/waterlog.py | 2 +- 22 files changed, 19 insertions(+), 29 deletions(-) create mode 100644 amulet_map_editor/amulet_wx/ui/__init__.py rename amulet_map_editor/amulet_wx/{ => ui}/select_block.py (100%) rename amulet_map_editor/{programs/edit/canvas/ui/tool/tools/operation/select_destination.py => amulet_wx/ui/select_paste_location.py} (98%) rename amulet_map_editor/amulet_wx/{ => ui}/select_world.py (100%) create mode 100644 amulet_map_editor/amulet_wx/util/__init__.py rename amulet_map_editor/amulet_wx/{ => util}/icon.py (100%) rename amulet_map_editor/amulet_wx/{ => util}/key_config.py (99%) rename amulet_map_editor/amulet_wx/{ => util}/validators.py (100%) diff --git a/amulet_map_editor/amulet_ui.py b/amulet_map_editor/amulet_ui.py index 17343f50..f5bc9bc7 100644 --- a/amulet_map_editor/amulet_ui.py +++ b/amulet_map_editor/amulet_ui.py @@ -6,7 +6,7 @@ import webbrowser from amulet.api.errors import LoaderNoneMatched -from amulet_map_editor.amulet_wx.select_world import WorldSelectDialog +from amulet_map_editor.amulet_wx.ui.select_world import WorldSelectDialog from amulet_map_editor import lang, version, log, IMG_DIR from amulet_map_editor.programs import WorldManagerUI from amulet_map_editor.programs import BaseWorldUI diff --git a/amulet_map_editor/amulet_wx/simple.py b/amulet_map_editor/amulet_wx/simple.py index c584e470..785ed3d0 100644 --- a/amulet_map_editor/amulet_wx/simple.py +++ b/amulet_map_editor/amulet_wx/simple.py @@ -1,3 +1,6 @@ +"""A collection of classes for wx objects to abstract away +the repeated code and make working with wx a bit more simple.""" + import wx from wx.lib.scrolledpanel import ScrolledPanel from typing import Iterable, Union, Any, List, Optional, Sequence, Dict @@ -31,6 +34,7 @@ def __init__(self, parent: wx.Window, sizer_dir=wx.VERTICAL): class SimpleScrollablePanel(ScrolledPanel, SimpleSizer): + """A scrolled panel that automatically sets itself up.""" def __init__(self, parent: wx.Window, sizer_dir=wx.VERTICAL, **kwargs): ScrolledPanel.__init__( self, @@ -43,18 +47,6 @@ def __init__(self, parent: wx.Window, sizer_dir=wx.VERTICAL, **kwargs): self.SetAutoLayout(1) -class SimpleText(wx.StaticText): - def __init__(self, parent: wx.Window, text): - super().__init__( - parent, - wx.ID_ANY, - text, - wx.DefaultPosition, - wx.DefaultSize, - 0 - ) - - class SimpleChoice(wx.Choice): def __init__(self, parent: wx.Window, choices: Sequence[str] = (), default: Optional[str] = None): super().__init__( diff --git a/amulet_map_editor/amulet_wx/ui/__init__.py b/amulet_map_editor/amulet_wx/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/amulet_wx/select_block.py b/amulet_map_editor/amulet_wx/ui/select_block.py similarity index 100% rename from amulet_map_editor/amulet_wx/select_block.py rename to amulet_map_editor/amulet_wx/ui/select_block.py diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py b/amulet_map_editor/amulet_wx/ui/select_paste_location.py similarity index 98% rename from amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py rename to amulet_map_editor/amulet_wx/ui/select_paste_location.py index d261e6f9..c7d0ba1e 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/operation/select_destination.py +++ b/amulet_map_editor/amulet_wx/ui/select_paste_location.py @@ -4,7 +4,7 @@ from amulet.api.structure import Structure from amulet_map_editor.amulet_wx.simple import SimplePanel -from amulet_map_editor.amulet_wx.validators import IntValidator +from amulet_map_editor.amulet_wx.util.validators import IntValidator class SelectDestinationUI(SimplePanel): diff --git a/amulet_map_editor/amulet_wx/select_world.py b/amulet_map_editor/amulet_wx/ui/select_world.py similarity index 100% rename from amulet_map_editor/amulet_wx/select_world.py rename to amulet_map_editor/amulet_wx/ui/select_world.py diff --git a/amulet_map_editor/amulet_wx/util/__init__.py b/amulet_map_editor/amulet_wx/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/amulet_wx/icon.py b/amulet_map_editor/amulet_wx/util/icon.py similarity index 100% rename from amulet_map_editor/amulet_wx/icon.py rename to amulet_map_editor/amulet_wx/util/icon.py diff --git a/amulet_map_editor/amulet_wx/key_config.py b/amulet_map_editor/amulet_wx/util/key_config.py similarity index 99% rename from amulet_map_editor/amulet_wx/key_config.py rename to amulet_map_editor/amulet_wx/util/key_config.py index 90fff80e..0f73d1f7 100644 --- a/amulet_map_editor/amulet_wx/key_config.py +++ b/amulet_map_editor/amulet_wx/util/key_config.py @@ -2,7 +2,7 @@ from amulet_map_editor.amulet_wx.simple import SimpleDialog, SimpleScrollablePanel, SimpleChoice from typing import Dict, Tuple, Optional, Union, Sequence -from amulet_map_editor.amulet_wx.icon import ADD_ICON, SUBTRACT_ICON, EDIT_ICON +from amulet_map_editor.amulet_wx.util.icon import ADD_ICON, SUBTRACT_ICON, EDIT_ICON ModifierKeyType = str KeyType = Union[int, str] diff --git a/amulet_map_editor/amulet_wx/validators.py b/amulet_map_editor/amulet_wx/util/validators.py similarity index 100% rename from amulet_map_editor/amulet_wx/validators.py rename to amulet_map_editor/amulet_wx/util/validators.py diff --git a/amulet_map_editor/programs/__init__.py b/amulet_map_editor/programs/__init__.py index fc63bdf6..72c93d18 100644 --- a/amulet_map_editor/programs/__init__.py +++ b/amulet_map_editor/programs/__init__.py @@ -10,7 +10,7 @@ from amulet_map_editor import log from amulet_map_editor.amulet_wx.simple import SimplePanel -from amulet_map_editor.amulet_wx.select_world import WorldUI +from amulet_map_editor.amulet_wx.ui.select_world import WorldUI if TYPE_CHECKING: from amulet.api.world import World diff --git a/amulet_map_editor/programs/convert/convert.py b/amulet_map_editor/programs/convert/convert.py index d65d0e9a..44ce4c4b 100644 --- a/amulet_map_editor/programs/convert/convert.py +++ b/amulet_map_editor/programs/convert/convert.py @@ -8,7 +8,7 @@ from amulet_map_editor import lang, log from amulet_map_editor.amulet_wx.simple import SimplePanel -from amulet_map_editor.amulet_wx.select_world import WorldSelectDialog, WorldUI +from amulet_map_editor.amulet_wx.ui.select_world import WorldSelectDialog, WorldUI from amulet_map_editor.programs import BaseWorldProgram, MenuData if TYPE_CHECKING: diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index 5f2f902b..2f7d162d 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -4,7 +4,7 @@ from .base_edit_canvas import BaseEditCanvas from amulet_map_editor.opengl.mesh.world_renderer.world import sin, cos -from amulet_map_editor.amulet_wx.key_config import serialise_key_event, KeybindGroup, ActionLookupType +from amulet_map_editor.amulet_wx.util.key_config import serialise_key_event, KeybindGroup, ActionLookupType if TYPE_CHECKING: from amulet.api.world import World diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index 851242d2..c451a05e 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -6,7 +6,7 @@ from amulet_map_editor import CONFIG, log from amulet_map_editor.programs import BaseWorldProgram, MenuData -from amulet_map_editor.amulet_wx.key_config import KeyConfigDialog +from amulet_map_editor.amulet_wx.util.key_config import KeyConfigDialog from amulet_map_editor.programs.edit.canvas.events import EVT_EDIT_CLOSE from .canvas.edit_canvas import EditCanvas from .key_config import DefaultKeybindGroupId, PresetKeybinds, KeybindKeys diff --git a/amulet_map_editor/programs/edit/key_config.py b/amulet_map_editor/programs/edit/key_config.py index d5285c4b..fca9855a 100644 --- a/amulet_map_editor/programs/edit/key_config.py +++ b/amulet_map_editor/programs/edit/key_config.py @@ -1,5 +1,5 @@ from typing import List -from amulet_map_editor.amulet_wx.key_config import ( +from amulet_map_editor.amulet_wx.util.key_config import ( KeybindContainer, KeybindGroup, KeybindGroupIdType, diff --git a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py index 70f63819..a2156139 100644 --- a/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py +++ b/amulet_map_editor/programs/edit/plugins/api/fixed_pipeline.py @@ -1,7 +1,7 @@ import wx from typing import Callable, Dict, Any, TYPE_CHECKING, Sequence -from amulet_map_editor.amulet_wx.validators import IntValidator +from amulet_map_editor.amulet_wx.util.validators import IntValidator from amulet.api.data_types import OperationReturnType from .operation_ui import OperationUI @@ -9,8 +9,6 @@ if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas from amulet.api.world import World - from amulet.api.data_types import Dimension - from amulet.api.selection import SelectionGroup FixedOperationType = Callable[["World", "Dimension", "SelectionGroup", Dict[str, Any]], OperationReturnType] diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py index eab0fc4a..15a5ef75 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.select_block import VersionSelect +from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect from amulet_map_editor.amulet_wx.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py index 784d4588..0fd08bd7 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.select_block import VersionSelect +from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect from amulet_map_editor.amulet_wx.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py index b50627a4..65d71a8a 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.select_block import PlatformSelect +from amulet_map_editor.amulet_wx.ui.select_block import PlatformSelect from amulet_map_editor.amulet_wx.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py index 82bad248..834a9a98 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/fill.py @@ -2,7 +2,7 @@ import wx from amulet.operations.fill import fill -from amulet_map_editor.amulet_wx.select_block import BlockDefine +from amulet_map_editor.amulet_wx.ui.select_block import BlockDefine from amulet_map_editor.programs.edit.plugins import OperationUI if TYPE_CHECKING: diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py index 97392fd8..1e2b66bc 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py @@ -3,7 +3,7 @@ import numpy from amulet.api.block import Block -from amulet_map_editor.amulet_wx.select_block import BlockDefine +from amulet_map_editor.amulet_wx.ui.select_block import BlockDefine from amulet_map_editor.programs.edit.plugins import OperationUI from amulet_map_editor.amulet_wx.simple import SimpleScrollablePanel diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py index c82df054..b756ad59 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py @@ -5,7 +5,7 @@ from amulet.api.selection import SelectionGroup from amulet.api.block import Block from amulet.api.data_types import Dimension -from amulet_map_editor.amulet_wx.select_block import BlockDefine +from amulet_map_editor.amulet_wx.ui.select_block import BlockDefine from amulet_map_editor.amulet_wx.simple import SimpleDialog if TYPE_CHECKING: From 5083633efdffc56481183301bc936d43ae2c3ed5 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 15:49:25 +0100 Subject: [PATCH 56/84] Added some documentation to simple --- amulet_map_editor/amulet_wx/simple.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/amulet_map_editor/amulet_wx/simple.py b/amulet_map_editor/amulet_wx/simple.py index 785ed3d0..c32dccd5 100644 --- a/amulet_map_editor/amulet_wx/simple.py +++ b/amulet_map_editor/amulet_wx/simple.py @@ -48,6 +48,7 @@ def __init__(self, parent: wx.Window, sizer_dir=wx.VERTICAL, **kwargs): class SimpleChoice(wx.Choice): + """A wrapper for wx.Choice that sets up the UI for you.""" def __init__(self, parent: wx.Window, choices: Sequence[str] = (), default: Optional[str] = None): super().__init__( parent, @@ -67,6 +68,7 @@ def GetCurrentString(self) -> str: class SimpleChoiceAny(wx.Choice): + """An extension for wx.Choice that enables showing and returning objects that are not strings.""" def __init__(self, parent: wx.Window, sort=True, reverse=False): super().__init__( parent @@ -119,6 +121,7 @@ def GetAny(self) -> Optional[Any]: class SimpleDialog(wx.Dialog): + """A dialog with ok and cancel buttons set up.""" def __init__(self, parent: wx.Window, title, sizer_dir=wx.VERTICAL): wx.Dialog.__init__( self, From c7664803e441d48e7c35ce20775c6fe9c608edad Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 15:50:27 +0100 Subject: [PATCH 57/84] Moved simple into ui --- amulet_map_editor/amulet_wx/ui/select_block.py | 2 +- amulet_map_editor/amulet_wx/ui/select_paste_location.py | 2 +- amulet_map_editor/amulet_wx/ui/select_world.py | 2 +- amulet_map_editor/amulet_wx/{ => ui}/simple.py | 0 amulet_map_editor/amulet_wx/util/key_config.py | 2 +- amulet_map_editor/programs/__init__.py | 2 +- amulet_map_editor/programs/convert/convert.py | 2 +- amulet_map_editor/programs/edit/canvas/ui/file.py | 2 +- amulet_map_editor/programs/edit/canvas/ui/goto.py | 2 +- .../programs/edit/canvas/ui/tool/tools/base_operation.py | 2 +- .../plugins/stock_plugins/export_operations/construction.py | 2 +- .../edit/plugins/stock_plugins/export_operations/mcstructure.py | 2 +- .../edit/plugins/stock_plugins/export_operations/schematic.py | 2 +- .../plugins/stock_plugins/import_operations/construction.py | 2 +- .../edit/plugins/stock_plugins/import_operations/mcstructure.py | 2 +- .../edit/plugins/stock_plugins/import_operations/schematic.py | 2 +- .../programs/edit/plugins/stock_plugins/operations/replace.py | 2 +- .../programs/edit/plugins/stock_plugins/operations/waterlog.py | 2 +- 18 files changed, 17 insertions(+), 17 deletions(-) rename amulet_map_editor/amulet_wx/{ => ui}/simple.py (100%) diff --git a/amulet_map_editor/amulet_wx/ui/select_block.py b/amulet_map_editor/amulet_wx/ui/select_block.py index 23578a63..6d331319 100644 --- a/amulet_map_editor/amulet_wx/ui/select_block.py +++ b/amulet_map_editor/amulet_wx/ui/select_block.py @@ -1,4 +1,4 @@ -from amulet_map_editor.amulet_wx.simple import SimpleChoice, SimpleChoiceAny +from amulet_map_editor.amulet_wx.ui.simple import SimpleChoice, SimpleChoiceAny import wx import PyMCTranslate import amulet_nbt diff --git a/amulet_map_editor/amulet_wx/ui/select_paste_location.py b/amulet_map_editor/amulet_wx/ui/select_paste_location.py index c7d0ba1e..b86bc0b8 100644 --- a/amulet_map_editor/amulet_wx/ui/select_paste_location.py +++ b/amulet_map_editor/amulet_wx/ui/select_paste_location.py @@ -3,7 +3,7 @@ from typing import Optional, List, Callable, Type, Any from amulet.api.structure import Structure -from amulet_map_editor.amulet_wx.simple import SimplePanel +from amulet_map_editor.amulet_wx.ui.simple import SimplePanel from amulet_map_editor.amulet_wx.util.validators import IntValidator diff --git a/amulet_map_editor/amulet_wx/ui/select_world.py b/amulet_map_editor/amulet_wx/ui/select_world.py index d47b05b5..073cf4fc 100644 --- a/amulet_map_editor/amulet_wx/ui/select_world.py +++ b/amulet_map_editor/amulet_wx/ui/select_world.py @@ -5,7 +5,7 @@ from typing import List, Dict, Tuple, Callable, TYPE_CHECKING from amulet_map_editor import lang, CONFIG from amulet import world_interface -from amulet_map_editor.amulet_wx import simple +from amulet_map_editor.amulet_wx.ui import simple from amulet_map_editor import log if TYPE_CHECKING: diff --git a/amulet_map_editor/amulet_wx/simple.py b/amulet_map_editor/amulet_wx/ui/simple.py similarity index 100% rename from amulet_map_editor/amulet_wx/simple.py rename to amulet_map_editor/amulet_wx/ui/simple.py diff --git a/amulet_map_editor/amulet_wx/util/key_config.py b/amulet_map_editor/amulet_wx/util/key_config.py index 0f73d1f7..2fb5d53f 100644 --- a/amulet_map_editor/amulet_wx/util/key_config.py +++ b/amulet_map_editor/amulet_wx/util/key_config.py @@ -1,5 +1,5 @@ import wx -from amulet_map_editor.amulet_wx.simple import SimpleDialog, SimpleScrollablePanel, SimpleChoice +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog, SimpleScrollablePanel, SimpleChoice from typing import Dict, Tuple, Optional, Union, Sequence from amulet_map_editor.amulet_wx.util.icon import ADD_ICON, SUBTRACT_ICON, EDIT_ICON diff --git a/amulet_map_editor/programs/__init__.py b/amulet_map_editor/programs/__init__.py index 72c93d18..12dad1d3 100644 --- a/amulet_map_editor/programs/__init__.py +++ b/amulet_map_editor/programs/__init__.py @@ -9,7 +9,7 @@ from amulet import world_interface from amulet_map_editor import log -from amulet_map_editor.amulet_wx.simple import SimplePanel +from amulet_map_editor.amulet_wx.ui.simple import SimplePanel from amulet_map_editor.amulet_wx.ui.select_world import WorldUI if TYPE_CHECKING: diff --git a/amulet_map_editor/programs/convert/convert.py b/amulet_map_editor/programs/convert/convert.py index 44ce4c4b..334a58b4 100644 --- a/amulet_map_editor/programs/convert/convert.py +++ b/amulet_map_editor/programs/convert/convert.py @@ -7,7 +7,7 @@ from amulet.api.world import World from amulet_map_editor import lang, log -from amulet_map_editor.amulet_wx.simple import SimplePanel +from amulet_map_editor.amulet_wx.ui.simple import SimplePanel from amulet_map_editor.amulet_wx.ui.select_world import WorldSelectDialog, WorldUI from amulet_map_editor.programs import BaseWorldProgram, MenuData diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index a1db9f8c..e2c29586 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -2,7 +2,7 @@ import wx from .base_ui import BaseUI -from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny +from amulet_map_editor.amulet_wx.ui.simple import SimpleChoiceAny from amulet_map_editor.programs.edit.canvas.events import ( EVT_CAMERA_MOVE, EVT_UNDO, diff --git a/amulet_map_editor/programs/edit/canvas/ui/goto.py b/amulet_map_editor/programs/edit/canvas/ui/goto.py index 257bef0e..44812c79 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/goto.py +++ b/amulet_map_editor/programs/edit/canvas/ui/goto.py @@ -1,6 +1,6 @@ from typing import Optional, Tuple import wx -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog def show_goto(parent, x: float, y: float, z: float) -> Optional[Tuple[float, float, float]]: diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py index 1724b6a9..0fe89d6f 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py @@ -1,7 +1,7 @@ import wx from typing import TYPE_CHECKING, Optional -from amulet_map_editor.amulet_wx.simple import SimpleChoiceAny +from amulet_map_editor.amulet_wx.ui.simple import SimpleChoiceAny from amulet_map_editor.programs.edit.plugins import OperationUIType, OperationStorageType from amulet_map_editor.programs.edit.canvas.ui.tool.tools.base_tool_ui import BaseToolUI diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py index 15a5ef75..5ea771dc 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py @@ -2,7 +2,7 @@ import wx from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py index 0fd08bd7..e62392a1 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py @@ -2,7 +2,7 @@ import wx from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py index 65d71a8a..69b55932 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py @@ -2,7 +2,7 @@ import wx from amulet_map_editor.amulet_wx.ui.select_block import PlatformSelect -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py index 9e298206..063e6617 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py @@ -2,7 +2,7 @@ import wx import os -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.block import BlockManager from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py index f2f3945f..1a08a5bf 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py @@ -2,7 +2,7 @@ import wx import os -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.block import BlockManager from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py index 48e4e0b8..7516cc57 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py @@ -2,7 +2,7 @@ import wx import os -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.block import BlockManager from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py index 1e2b66bc..cdce0def 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/replace.py @@ -5,7 +5,7 @@ from amulet.api.block import Block from amulet_map_editor.amulet_wx.ui.select_block import BlockDefine from amulet_map_editor.programs.edit.plugins import OperationUI -from amulet_map_editor.amulet_wx.simple import SimpleScrollablePanel +from amulet_map_editor.amulet_wx.ui.simple import SimpleScrollablePanel if TYPE_CHECKING: from amulet.api.world import World diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py index b756ad59..19b1b0ab 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py @@ -6,7 +6,7 @@ from amulet.api.block import Block from amulet.api.data_types import Dimension from amulet_map_editor.amulet_wx.ui.select_block import BlockDefine -from amulet_map_editor.amulet_wx.simple import SimpleDialog +from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog if TYPE_CHECKING: from amulet.api.world import World From 573ff801d45f676389d97fbef7b5550e25b81e61 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 16:44:19 +0100 Subject: [PATCH 58/84] Minimised UI flickering --- amulet_map_editor/programs/edit/canvas/ui/file.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index e2c29586..dca47b99 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -78,6 +78,8 @@ def _change_dimension(self, evt): def _on_camera_move(self, evt): x, y, z = evt.location - self._location_button.SetLabel(f'{x:.2f}, {y:.2f}, {z:.2f}') - self.Layout() - self.canvas.Layout() + label = f'{x:.2f}, {y:.2f}, {z:.2f}' + old_label = self._location_button.GetLabel() + self._location_button.SetLabel(label) + if len(label) != len(old_label): + self.canvas.Layout() From 4d988e09c8bb414eeae37d8705bcda49ff846c26 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 16:45:40 +0100 Subject: [PATCH 59/84] Updated the waterlog operation --- .../stock_plugins/operations/waterlog.py | 104 ++++++++++-------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py index 19b1b0ab..3a24c1e8 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/waterlog.py @@ -1,26 +1,71 @@ +import numpy from typing import TYPE_CHECKING import wx -import numpy -from amulet.api.selection import SelectionGroup -from amulet.api.block import Block -from amulet.api.data_types import Dimension from amulet_map_editor.amulet_wx.ui.select_block import BlockDefine -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog +from amulet_map_editor.programs.edit.plugins import OperationUI if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class Waterlog(wx.Panel, OperationUI): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + wx.Panel.__init__(self, parent) + OperationUI.__init__(self, parent, canvas, world, options_path) + + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + options = self._load_options({}) + + self._block_define = BlockDefine( + self, + world.world_wrapper.translation_manager, + *(options.get("fill_block_options", []) or [world.world_wrapper.platform]), + style=wx.BORDER_SIMPLE, + properties_style=wx.BORDER_SIMPLE + ) + self._sizer.Add(self._block_define, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self._run_button = wx.Button(self, label="Run Operation") + self._run_button.Bind(wx.EVT_BUTTON, self._run_operation) + self._sizer.Add(self._run_button, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + + self.Layout() + + def _get_fill_block(self): + return self.world.world_wrapper.translation_manager.get_version( + self._block_define.platform, + self._block_define.version + ).block.to_universal( + self._block_define.block, + force_blockstate=self._block_define.force_blockstate + )[0] + + def unload(self): + self._save_options({ + "fill_block": self._get_fill_block(), + "fill_block_options": self._block_define.options + }) + def _run_operation(self, _): + self.canvas.run_operation( + lambda: self._waterlog() + ) -def waterlog( - world: "World", - dimension: Dimension, - selection: SelectionGroup, - options: dict -): - waterlog_block = options.get("fill_block", None) - waterlog_block: Block - if isinstance(waterlog_block, Block): + def _waterlog(self): + waterlog_block = self._get_fill_block() + world = self.world + selection = self.canvas.selection_group + dimension = self.canvas.dimension iter_count = len(list(world.get_chunk_slices(selection, dimension, True))) count = 0 for chunk, slices, _ in world.get_chunk_slices(selection, dimension, True): @@ -41,38 +86,9 @@ def waterlog( chunk.changed = True count += 1 yield 100 * count / iter_count - else: - raise Exception('Please specify a block before running the waterlog operation') - - -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Waterlog') - block_define = BlockDefine( - dialog, - world.world_wrapper.translation_manager, - *(options.get("fill_block_options", []) or [world.world_wrapper.platform]) - ) - dialog.sizer.Add(block_define) - dialog.Fit() - if dialog.ShowModal() == wx.ID_OK: - options = { - "fill_block": world.world_wrapper.translation_manager.get_version( - block_define.platform, - block_define.version - ).block.to_universal( - block_define.block, - force_blockstate=block_define.force_blockstate - )[0], - "fill_block_options": block_define.options - } - return options export = { - "v": 1, # a version 1 plugin "name": "Waterlog", # the name of the plugin - "features": ["src_selection", "wxoptions"], - "inputs": ["src_selection", "options"], # the inputs to give to the plugin - "operation": waterlog, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": Waterlog, # the actual function to call when running the plugin } From d034fda3457e264e675af76fd2eed450234a7ca4 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 18:58:32 +0100 Subject: [PATCH 60/84] Fixed some issues with the canvas run_operation method --- .../programs/edit/canvas/edit_canvas.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 4ccea61a..c796cad8 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -92,8 +92,16 @@ def __init__(self, parent: wx.Window, world: "World"): tool_sizer = Tool(self) canvas_sizer.Add(tool_sizer, 1, wx.EXPAND, 0) - def run_operation(self, operation: Callable[[], OperationReturnType], title="", msg="", create_backup=True) -> Any: + def run_operation( + self, + operation: Callable[[], OperationReturnType], + title="", + msg="", + throw_exceptions=False + ) -> Any: self.disable_threads() + err = None + out = None try: out = show_loading_dialog( operation, @@ -101,29 +109,32 @@ def run_operation(self, operation: Callable[[], OperationReturnType], title="", msg, self, ) - if create_backup: - self.world.create_undo_point() - wx.PostEvent(self, CreateUndoEvent()) + self.world.create_undo_point() + wx.PostEvent(self, CreateUndoEvent()) except OperationError as e: msg = f"Error running operation: {e}" log.info(msg) + self.world.restore_last_undo_point() wx.MessageDialog(self, msg, style=wx.OK).ShowModal() - self.enable_threads() - raise e + err = e except OperationSuccessful as e: msg = str(e) log.info(msg) + self.world.restore_last_undo_point() wx.MessageDialog(self, msg, style=wx.OK).ShowModal() - self.enable_threads() - raise e + err = e except OperationSilentAbort as e: - self.enable_threads() - raise e + self.world.restore_last_undo_point() + err = e except Exception as e: + self.world.restore_last_undo_point() log.error(traceback.format_exc()) - self.enable_threads() - raise e + wx.MessageDialog(self, f"Exception running operation: {e}\nSee the console for more details", style=wx.OK).ShowModal() + err = e + self.enable_threads() + if err is not None and throw_exceptions: + raise err return out def undo(self): From a9ec9db2b69559de11df4b102c17db02d375d809 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 3 Jun 2020 18:58:58 +0100 Subject: [PATCH 61/84] Reimplemented copy, cut and delete --- .../programs/edit/canvas/edit_canvas.py | 28 ++++++++++++++-- amulet_map_editor/programs/edit/edit.py | 33 ------------------- .../stock_plugins/internal_operations/copy.py | 14 ++------ .../stock_plugins/internal_operations/cut.py | 27 +++------------ .../internal_operations/delete.py | 25 ++++---------- 5 files changed, 40 insertions(+), 87 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index c796cad8..eaed24cf 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -12,6 +12,10 @@ from amulet_map_editor.programs.edit.canvas.ui.goto import show_goto from amulet_map_editor.programs.edit.canvas.ui.tool import Tool from amulet_map_editor.programs.edit.plugins import OperationError, OperationSuccessful, OperationSilentAbort +from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.cut import cut +from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.copy import copy +from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.delete import delete +from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.paste import paste from amulet_map_editor.programs.edit.canvas.events import ( UndoEvent, @@ -146,16 +150,34 @@ def redo(self): wx.PostEvent(self, RedoEvent()) def cut(self): - pass + self.run_operation( + lambda: cut( + self.world, + self.dimension, + self.selection_group + ) + ) def copy(self): - pass + self.run_operation( + lambda: copy( + self.world, + self.dimension, + self.selection_group + ) + ) def paste(self): pass def delete(self): - pass + self.run_operation( + lambda: delete( + self.world, + self.dimension, + self.selection_group + ) + ) def goto(self): location = show_goto(self, *self.camera_location) diff --git a/amulet_map_editor/programs/edit/edit.py b/amulet_map_editor/programs/edit/edit.py index c451a05e..e8d6b9b8 100644 --- a/amulet_map_editor/programs/edit/edit.py +++ b/amulet_map_editor/programs/edit/edit.py @@ -159,36 +159,3 @@ def _on_resize(self, event): if self._canvas is not None: self._canvas.set_size(self.GetSize()[0], self.GetSize()[1]) event.Skip() - - # TODO - # def _cut(self) -> bool: - # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'cut.py')) - # - # def _copy(self) -> bool: - # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'copy.py')) - # - # def _paste(self) -> bool: - # self.show_operation_options(None) - # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'paste.py')) - # - # def _delete(self) -> bool: - # return self._run_operation(os.path.join(os.path.dirname(plugins.__file__), 'internal_operations', 'delete.py')) - - # def show_select_options(self, _): - # self._operation_options.Hide() - # self._select_options.enable() - # self.Layout() - - # def _show_operation_options(self, enable: Callable): - # self._select_options.Hide() - # enable() - # self.Layout() - - # def show_operation_options(self, _): - # self._show_operation_options(self._operation_options.enable_operation_ui) - # - # def show_import_options(self, _): - # self._show_operation_options(self._operation_options.enable_import_ui) - # - # def show_export_options(self, _): - # self._show_operation_options(self._operation_options.enable_export_ui) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py index 88e762cf..12e5013f 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py @@ -3,12 +3,13 @@ from amulet.api.structure import structure_buffer, Structure from amulet.api.data_types import Dimension from amulet.api.selection import SelectionGroup +from amulet_map_editor.programs.edit.plugins.api.errors import OperationSilentAbort if TYPE_CHECKING: from amulet.api.world import World -def copy_selection( +def copy( world: "World", dimension: Dimension, selection: SelectionGroup @@ -17,13 +18,4 @@ def copy_selection( world, selection, dimension ) structure_buffer.append(structure) - return True - - -export = { - "v": 1, # a version 1 plugin - "name": "Copy", # the name of the plugin - "features": ["src_selection"], - "inputs": ["src_selection"], # the inputs to give to the plugin - "operation": copy_selection # the actual function to call when running the plugin -} + raise OperationSilentAbort diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py index dc0a9d19..0575280c 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py @@ -1,42 +1,25 @@ from typing import TYPE_CHECKING from amulet.api.structure import structure_buffer, Structure -from amulet.api.data_types import Dimension +from amulet.api.data_types import Dimension, OperationReturnType from amulet.api.selection import SelectionGroup -from amulet.operations.fill import fill -from amulet.api.block import Block +from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.delete import delete if TYPE_CHECKING: from amulet.api.world import World -def copy_selection( +def cut( world: "World", dimension: Dimension, selection: SelectionGroup -): +) -> OperationReturnType: structure = Structure.from_world( world, selection, dimension ) structure_buffer.append(structure) - yield from fill( + yield from delete( world, dimension, selection, - { - "fill_block": world.translation_manager.get_version( - 'java', (1, 15, 2) - ).block.to_universal( - Block("minecraft", "air") - )[0] - } ) - - -export = { - "v": 1, # a version 1 plugin - "name": "Copy", # the name of the plugin - "features": ["src_selection"], - "inputs": ["src_selection"], # the inputs to give to the plugin - "operation": copy_selection # the actual function to call when running the plugin -} diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/delete.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/delete.py index 0f7ee0e6..39e18388 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/delete.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/delete.py @@ -3,7 +3,7 @@ from amulet.operations.fill import fill from amulet.api.selection import SelectionGroup from amulet.api.block import Block -from amulet.api.data_types import Dimension +from amulet.api.data_types import Dimension, OperationReturnType if TYPE_CHECKING: from amulet.api.world import World @@ -13,25 +13,14 @@ def delete( world: "World", dimension: Dimension, selection: SelectionGroup -): +) -> OperationReturnType: yield from fill( world, dimension, selection, - { - "fill_block": world.translation_manager.get_version( - 'java', (1, 15, 2) - ).block.to_universal( - Block("minecraft", "air") - )[0] - } + world.translation_manager.get_version( + 'java', (1, 15, 2) + ).block.to_universal( + Block("minecraft", "air") + )[0] ) - - -export = { - "v": 1, # a version 1 plugin - "name": "Delete", # the name of the plugin - "features": ["src_selection"], - "inputs": ["src_selection"], # the inputs to give to the plugin - "operation": delete, # the actual function to call when running the plugin -} From 9c5261da60419544a41a12e1bc2343bcc7d3976a Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 12:12:35 +0100 Subject: [PATCH 62/84] Added an event for when the selected point moves --- amulet_map_editor/programs/edit/canvas/base_edit_canvas.py | 2 ++ amulet_map_editor/programs/edit/canvas/events.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index 2d133e7e..a9cbd7bc 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -23,6 +23,7 @@ CameraMoveEvent, CameraRotateEvent, DimensionChangeEvent, + SelectionPointChangeEvent, ) if TYPE_CHECKING: @@ -284,6 +285,7 @@ def _change_box_location(self): else: position, box_index = self._box_location_closest() + wx.PostEvent(self, SelectionPointChangeEvent(location=position)) self._selection_group.update_position(position, box_index) def ray_collision(self): diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index e2a27094..322e45b0 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -1,10 +1,10 @@ from wx.lib import newevent - CameraMoveEvent, EVT_CAMERA_MOVE = newevent.NewEvent() CameraRotateEvent, EVT_CAMERA_ROTATE = newevent.NewEvent() DimensionChangeEvent, EVT_DIMENSION_CHANGE = newevent.NewEvent() +# the active tool changed ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() UndoEvent, EVT_UNDO = newevent.NewEvent() @@ -13,6 +13,11 @@ SaveEvent, EVT_SAVE = newevent.NewEvent() EditCloseEvent, EVT_EDIT_CLOSE = newevent.NewEvent() +# the block highlighted by the cursor changed position. +SelectionPointChangeEvent, EVT_SELECTION_POINT_CHANGE = newevent.NewEvent() + +# events fired when the active selection box changes. TODO: reimplement these +BoxChangeEvent, EVT_BOX_CHANGE = newevent.NewEvent() BoxGreenCornerChangeEvent, EVT_BOX_GREEN_CORNER_CHANGE = newevent.NewEvent() BoxBlueCornerChangeEvent, EVT_BOX_BLUE_CORNER_CHANGE = newevent.NewEvent() From 7cce620eeb5832104c18c29af7a1c8c8a1e21031 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 16:55:39 +0100 Subject: [PATCH 63/84] Updated the structure cache references --- .../edit/plugins/stock_plugins/internal_operations/copy.py | 4 ++-- .../edit/plugins/stock_plugins/internal_operations/cut.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py index 12e5013f..cce89e27 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/copy.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from amulet.api.structure import structure_buffer, Structure +from amulet.api.structure import structure_cache, Structure from amulet.api.data_types import Dimension from amulet.api.selection import SelectionGroup from amulet_map_editor.programs.edit.plugins.api.errors import OperationSilentAbort @@ -17,5 +17,5 @@ def copy( structure = Structure.from_world( world, selection, dimension ) - structure_buffer.append(structure) + structure_cache.add_structure(structure) raise OperationSilentAbort diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py index 0575280c..f62603ba 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/cut.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from amulet.api.structure import structure_buffer, Structure +from amulet.api.structure import structure_cache, Structure from amulet.api.data_types import Dimension, OperationReturnType from amulet.api.selection import SelectionGroup from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.delete import delete @@ -17,7 +17,7 @@ def cut( structure = Structure.from_world( world, selection, dimension ) - structure_buffer.append(structure) + structure_cache.add_structure(structure) yield from delete( world, dimension, From 3c630d06494de9d1e73b214933bb0a1f167cc28d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 16:55:54 +0100 Subject: [PATCH 64/84] Fixed selection typing --- amulet_map_editor/opengl/mesh/selection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/amulet_map_editor/opengl/mesh/selection.py b/amulet_map_editor/opengl/mesh/selection.py index abd17292..ad8cff8f 100644 --- a/amulet_map_editor/opengl/mesh/selection.py +++ b/amulet_map_editor/opengl/mesh/selection.py @@ -1,7 +1,7 @@ import numpy from OpenGL.GL import * import itertools -from typing import Tuple, Dict, Any, Optional, List +from typing import Tuple, Dict, Any, Optional, List, Union from amulet_map_editor.opengl.mesh.base.tri_mesh import TriMesh, Drawable from amulet.api.selection import SelectionGroup, SelectionBox @@ -34,7 +34,7 @@ def __iter__(self): if box.is_static: yield box - def __contains__(self, position: BlockCoordinatesAny): + def __contains__(self, position: Union[BlockCoordinatesAny, PointCoordinatesAny]): return any(position in box for box in self._boxes) def __getitem__(self, index: int) -> "RenderSelection": @@ -90,7 +90,7 @@ def delete_active(self): else: self._active = None - def update_position(self, position: PointCoordinatesAny, box_index: Optional[int]): + def update_position(self, position: BlockCoordinatesAny, box_index: Optional[int]): self._last_position = position self._hover_box = box_index self._temp_box.point1 = self._temp_box.point2 = position @@ -188,7 +188,7 @@ def being_resized(self) -> bool: """ return self._being_resized - def __contains__(self, position: BlockCoordinatesAny) -> bool: + def __contains__(self, position: Union[BlockCoordinatesAny, PointCoordinatesAny]) -> bool: """ Is the block position inside the selection box cuboid. :param position: (x, y, z) From 3c2ca612162d45c2281aa4d3a3e96ab45d287af7 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 16:56:56 +0100 Subject: [PATCH 65/84] Added a reusable UI for paste operations --- .../amulet_wx/ui/select_paste_location.py | 80 ------------------- .../programs/edit/ui/__init__.py | 0 .../programs/edit/ui/select_location.py | 71 ++++++++++++++++ 3 files changed, 71 insertions(+), 80 deletions(-) delete mode 100644 amulet_map_editor/amulet_wx/ui/select_paste_location.py create mode 100644 amulet_map_editor/programs/edit/ui/__init__.py create mode 100644 amulet_map_editor/programs/edit/ui/select_location.py diff --git a/amulet_map_editor/amulet_wx/ui/select_paste_location.py b/amulet_map_editor/amulet_wx/ui/select_paste_location.py deleted file mode 100644 index b86bc0b8..00000000 --- a/amulet_map_editor/amulet_wx/ui/select_paste_location.py +++ /dev/null @@ -1,80 +0,0 @@ -import wx -import numpy -from typing import Optional, List, Callable, Type, Any - -from amulet.api.structure import Structure -from amulet_map_editor.amulet_wx.ui.simple import SimplePanel -from amulet_map_editor.amulet_wx.util.validators import IntValidator - - -class SelectDestinationUI(SimplePanel): - def __init__(self, parent, cancel_callback, confirm_callback, locations: List[numpy.ndarray]): - super().__init__(parent) - self._cancel_callback = cancel_callback - self._confirm_callback = confirm_callback - self._locations = locations - - self._operation_path: Optional[str] = None - self._operation: Optional[Callable] = None - self._operation_input_definitions: Optional[List[str]] = None - self._structure: Optional[Structure] = None - self._options: Optional[dict] = None - - self._x: wx.SpinCtrl = self._add_row('x', wx.SpinCtrl, min=-30000000, max=30000000) - self._y: wx.SpinCtrl = self._add_row('y', wx.SpinCtrl, min=-30000000, max=30000000) - self._z: wx.SpinCtrl = self._add_row('z', wx.SpinCtrl, min=-30000000, max=30000000) - for ui in (self._x, self._y, self._z): - ui.SetValidator(IntValidator()) - self._copy_air: wx.CheckBox = self._add_row('Copy Air', wx.CheckBox) - self._x.Bind(wx.EVT_SPINCTRL, self._on_location_change) - self._y.Bind(wx.EVT_SPINCTRL, self._on_location_change) - self._z.Bind(wx.EVT_SPINCTRL, self._on_location_change) - - sizer = wx.BoxSizer(wx.HORIZONTAL) - self.add_object(sizer, 0, 0) - self._cancel = wx.Button(self, label="Cancel") - sizer.Add(self._cancel, flag=wx.CENTER, border=5) - self._confirm = wx.Button(self, label="Confirm") - sizer.Add(self._confirm, flag=wx.CENTER, border=5) - - self._cancel.Bind(wx.EVT_BUTTON, self._on_cancel) - self._confirm.Bind(wx.EVT_BUTTON, self._on_confirm) - - def setup(self, operation_path: Any, operation: Callable, operation_input_definitions: List[str], structure: Structure, options: dict): - self._operation_path = operation_path - self._operation = operation - self._operation_input_definitions = operation_input_definitions - self._structure = structure - self._locations.clear() - self._locations.append(structure.selection.min) - self._x.SetValue(structure.selection.min[0]) - self._y.SetValue(structure.selection.min[1]) - self._z.SetValue(structure.selection.min[2]) - self._copy_air.SetValue(options.get('copy_air', False)) - self._options = options - - def _add_row(self, label: str, wx_object: Type[wx.Object], **kwargs) -> Any: - sizer = wx.BoxSizer(wx.HORIZONTAL) - self.add_object(sizer, 0, 0) - name_text = wx.StaticText(self, label=label) - sizer.Add(name_text, flag=wx.CENTER | wx.ALL | wx.EXPAND, border=5) - obj = wx_object(self, **kwargs) - sizer.Add(obj, flag=wx.CENTER | wx.ALL, border=5) - return obj - - def _on_location_change(self, evt): - self._locations[-1] = numpy.array([self._x.GetValue(), self._y.GetValue(), self._z.GetValue()]) - - def _on_cancel(self, evt): - self._cancel_callback() - - def _on_confirm(self, evt): - self._options['location'] = (self._x.GetValue(), self._y.GetValue(), self._z.GetValue()) - self._options['copy_air'] = self._copy_air.GetValue() - self._confirm_callback( - self._operation_path, - self._operation, - self._operation_input_definitions, - options=self._options, - structure=self._structure - ) diff --git a/amulet_map_editor/programs/edit/ui/__init__.py b/amulet_map_editor/programs/edit/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amulet_map_editor/programs/edit/ui/select_location.py b/amulet_map_editor/programs/edit/ui/select_location.py new file mode 100644 index 00000000..9ced6b5c --- /dev/null +++ b/amulet_map_editor/programs/edit/ui/select_location.py @@ -0,0 +1,71 @@ +import wx +import numpy +from typing import Optional, Callable, Type, Any, TYPE_CHECKING + +from amulet.api.structure import Structure +from amulet.api.data_types import BlockCoordinates +from amulet_map_editor.amulet_wx.ui.simple import SimplePanel +from amulet_map_editor.amulet_wx.util.validators import IntValidator +from amulet_map_editor.programs.edit.canvas.ui.base_ui import BaseUI + +if TYPE_CHECKING: + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class SelectLocationUI(SimplePanel, BaseUI): + """A UI element that can be dropped into the EditCanvas and let the user pick a location for a structure. + This UI does not allow for rotation. + Will send EVT_SELECT_CONFIRM when the user confirms the selection.""" + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + structure: Structure, + confirm_callback: Callable[[], None] + ): + SimplePanel.__init__(self, parent) + BaseUI.__init__(self, canvas) + + self._structure = structure + self.canvas.structure_locations.clear() + self.canvas.structure_locations.append(numpy.array([0, 0, 0])) + self.canvas.structure = structure + + def _add_row(label: str, wx_object: Type[wx.Object], **kwargs) -> Any: + sizer = wx.BoxSizer(wx.HORIZONTAL) + self.add_object(sizer, 0, 0) + name_text = wx.StaticText(self, label=label) + sizer.Add(name_text, flag=wx.CENTER | wx.ALL | wx.EXPAND, border=5) + obj = wx_object(self, **kwargs) + sizer.Add(obj, flag=wx.CENTER | wx.ALL, border=5) + return obj + + self._x: wx.SpinCtrl = _add_row('x', wx.SpinCtrl, min=-30000000, max=30000000) + self._y: wx.SpinCtrl = _add_row('y', wx.SpinCtrl, min=-30000000, max=30000000) + self._z: wx.SpinCtrl = _add_row('z', wx.SpinCtrl, min=-30000000, max=30000000) + for ui in (self._x, self._y, self._z): + ui.SetValidator(IntValidator()) + self._copy_air: wx.CheckBox = _add_row('Copy Air', wx.CheckBox) + self._x.Bind(wx.EVT_SPINCTRL, self._on_location_change) + self._y.Bind(wx.EVT_SPINCTRL, self._on_location_change) + self._z.Bind(wx.EVT_SPINCTRL, self._on_location_change) + + self._confirm = wx.Button(self, label="Confirm") + self.sizer.Add(self._confirm, flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, border=5) + + self._confirm.Bind(wx.EVT_BUTTON, lambda evt: confirm_callback()) + + @property + def location(self) -> BlockCoordinates: + return self._x.GetValue(), self._y.GetValue(), self._z.GetValue() + + @property + def copy_air(self) -> bool: + return self._copy_air.GetValue() + + @property + def structure(self) -> Structure: + return self._structure + + def _on_location_change(self, evt): + self.canvas.structure_locations[-1] = numpy.array(self.location) From b52513513235bf7bc4524a547c51954e98b2d099 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 16:58:21 +0100 Subject: [PATCH 66/84] Added missing bind event calls --- .../programs/edit/canvas/edit_canvas.py | 10 ++++++++-- amulet_map_editor/programs/edit/canvas/ui/file.py | 12 +++++++----- .../programs/edit/canvas/ui/tool/tool.py | 4 ++++ .../edit/canvas/ui/tool/tools/base_operation.py | 3 +++ .../edit/canvas/ui/tool/tools/base_tool_ui.py | 3 +++ 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index eaed24cf..d0955c0e 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -93,8 +93,14 @@ def __init__(self, parent: wx.Window, world: "World"): file_sizer.Add(self._file_panel, 0, wx.EXPAND, 0) canvas_sizer.Add(file_sizer, 0, wx.EXPAND, 0) - tool_sizer = Tool(self) - canvas_sizer.Add(tool_sizer, 1, wx.EXPAND, 0) + self._tool_sizer = Tool(self) + canvas_sizer.Add(self._tool_sizer, 1, wx.EXPAND, 0) + + self.bind_events() + + def bind_events(self): + self._file_panel.bind_events() + self._tool_sizer.bind_events() def run_operation( self, diff --git a/amulet_map_editor/programs/edit/canvas/ui/file.py b/amulet_map_editor/programs/edit/canvas/ui/file.py index dca47b99..70570c1e 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/file.py +++ b/amulet_map_editor/programs/edit/canvas/ui/file.py @@ -22,7 +22,6 @@ def __init__(self, canvas: 'EditCanvas'): self._location_button = wx.Button(canvas, label=', '.join([f'{s:.2f}' for s in self.canvas.camera_location])) self._location_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.goto()) - self.canvas.Bind(EVT_CAMERA_MOVE, self._on_camera_move) self.Add(self._location_button, 0, wx.TOP | wx.BOTTOM | wx.RIGHT | wx.CENTER, 5) @@ -40,18 +39,21 @@ def create_button(text, operation): return button self._undo_button: Optional[wx.Button] = create_button('Undo', lambda evt: self.canvas.undo()) - self.canvas.Bind(EVT_UNDO, self._on_update_buttons) self._redo_button: Optional[wx.Button] = create_button('Redo', lambda evt: self.canvas.redo()) - self.canvas.Bind(EVT_REDO, self._on_update_buttons) self._save_button: Optional[wx.Button] = create_button('Save', lambda evt: self.canvas.save()) - self.canvas.Bind(EVT_SAVE, self._on_update_buttons) - self.canvas.Bind(EVT_CREATE_UNDO, self._on_update_buttons) create_button('Close', lambda evt: self.canvas.close()) self._update_buttons() # self.Fit(self) self.Layout() + def bind_events(self): + self.canvas.Bind(EVT_CAMERA_MOVE, self._on_camera_move) + self.canvas.Bind(EVT_UNDO, self._on_update_buttons) + self.canvas.Bind(EVT_REDO, self._on_update_buttons) + self.canvas.Bind(EVT_SAVE, self._on_update_buttons) + self.canvas.Bind(EVT_CREATE_UNDO, self._on_update_buttons) + def _on_update_buttons(self, evt): self._update_buttons() evt.Skip() diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py index 430a9f7f..2be0379a 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tool.py @@ -43,6 +43,10 @@ def __init__(self, canvas: "EditCanvas"): self.register_tool("Import", SelectImportOperationUI) self.register_tool("Export", SelectExportOperationUI) + def bind_events(self): + for tool in self._tools.values(): + tool.bind_events() + def register_tool(self, name: str, tool_cls: Type[BaseToolUIType]): assert issubclass(tool_cls, (wx.Window, wx.Sizer)) and issubclass(tool_cls, BaseToolUI) self._tool_select.register_tool(name) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py index 0fe89d6f..40243be0 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py @@ -24,6 +24,9 @@ def __init__(self, canvas: "EditCanvas"): # self._operation_change() + def bind_events(self): + pass + @property def _operations(self) -> OperationStorageType: raise NotImplementedError diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py index 452c64db..70368a3a 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_tool_ui.py @@ -11,3 +11,6 @@ def enable(self): def disable(self): raise NotImplementedError + + def bind_events(self): + raise NotImplementedError From a8e58edbbe7b96172c6ad20132e74e0088e2fc8d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 16:59:51 +0100 Subject: [PATCH 67/84] Added a paste ui within the select tool This is what will be used with ctrl+v and can be used by others --- .../programs/edit/canvas/edit_canvas.py | 13 +++-- .../programs/edit/canvas/events.py | 1 + .../edit/canvas/ui/tool/tools/select.py | 50 +++++++++++++++++-- .../internal_operations/paste.py | 29 ----------- 4 files changed, 57 insertions(+), 36 deletions(-) delete mode 100644 amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/paste.py diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index d0955c0e..6c1a1e7c 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -5,6 +5,7 @@ import traceback from amulet.api.data_types import OperationReturnType +from amulet.api.structure import Structure, structure_cache from amulet_map_editor import CONFIG, log from amulet_map_editor.programs.edit.edit import EDIT_CONFIG_ID @@ -15,7 +16,6 @@ from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.cut import cut from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.copy import copy from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.delete import delete -from amulet_map_editor.programs.edit.plugins.stock_plugins.internal_operations.paste import paste from amulet_map_editor.programs.edit.canvas.events import ( UndoEvent, @@ -23,6 +23,7 @@ CreateUndoEvent, SaveEvent, EditCloseEvent, + PasteEvent, ) from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet_map_editor.programs.edit.canvas.ui.file import FilePanel @@ -173,8 +174,14 @@ def copy(self): ) ) - def paste(self): - pass + def paste(self, structure: Structure = None): + if not isinstance(structure, Structure): + if structure_cache: + structure = structure_cache.get_structure() + else: + wx.MessageBox("A structure needs to be copied before one can be pasted.") + return + wx.PostEvent(self, PasteEvent(structure=structure)) def delete(self): self.run_operation( diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index 322e45b0..e99eaa58 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -6,6 +6,7 @@ # the active tool changed ToolChangeEvent, EVT_TOOL_CHANGE = newevent.NewEvent() +PasteEvent, EVT_PASTE = newevent.NewEvent() UndoEvent, EVT_UNDO = newevent.NewEvent() RedoEvent, EVT_REDO = newevent.NewEvent() diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index 51797e4b..531e725f 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -1,7 +1,12 @@ -from typing import TYPE_CHECKING, Type, Any +from typing import TYPE_CHECKING, Type, Any, Optional import wx +import numpy -from .base_tool_ui import BaseToolUI +from amulet.operations.paste import paste_iter + +from amulet_map_editor.programs.edit.canvas.ui.tool.tools.base_tool_ui import BaseToolUI +from amulet_map_editor.programs.edit.ui.select_location import SelectLocationUI +from amulet_map_editor.programs.edit.canvas.events import EVT_PASTE if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas @@ -12,7 +17,14 @@ def __init__(self, canvas: 'EditCanvas'): wx.BoxSizer.__init__(self, wx.VERTICAL) BaseToolUI.__init__(self, canvas) - self.Add(wx.Panel(canvas)) + self._button_panel = wx.Panel(canvas) + button_sizer = wx.BoxSizer(wx.VERTICAL) + self._button_panel.SetSizer(button_sizer) + button = wx.Button(self._button_panel, label="hello") + button_sizer.Add(button) + self.Add(self._button_panel) + + self._paste_panel: Optional[SelectLocationUI] = None # self._x1: wx.SpinCtrl = self._add_row('x1', wx.SpinCtrl, min=-30000000, max=30000000) # self._y1: wx.SpinCtrl = self._add_row('y1', wx.SpinCtrl, min=-30000000, max=30000000) @@ -47,11 +59,41 @@ def __init__(self, canvas: 'EditCanvas'): # self._canvas().Bind(EVT_BOX_BLUE_CORNER_CHANGE, self._blue_corner_renderer_change) # self._canvas().Bind(EVT_BOX_COORDS_ENABLE, self._enable_scrolls) + def bind_events(self): + self.canvas.Bind(EVT_PASTE, self._paste) + + def _remove_paste(self): + if self._paste_panel is not None: + self._paste_panel.Destroy() + self._paste_panel = None + + def _paste(self, evt): + structure = evt.structure + self._button_panel.Hide() + self._remove_paste() + self._paste_panel = SelectLocationUI(self.canvas, self.canvas, structure, self._paste_confirm) + self.Add(self._paste_panel, 0, wx.ALIGN_CENTER_VERTICAL) + self.Layout() + + def _paste_confirm(self): + self.canvas.run_operation( + lambda: paste_iter( + self.canvas.world, + self.canvas.dimension, + self._paste_panel.structure, + [self._paste_panel.location], + self._paste_panel.copy_air + ) + ) + def enable(self): + self._remove_paste() + self._button_panel.Show() + self.Layout() self.canvas.select_mode = 0 def disable(self): - pass + self._remove_paste() def _add_row(self, label: str, wx_object: Type[wx.Object], **kwargs) -> Any: sizer = wx.BoxSizer(wx.HORIZONTAL) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/paste.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/paste.py deleted file mode 100644 index 4b3ebd1f..00000000 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/internal_operations/paste.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import TYPE_CHECKING -from amulet.operations.paste import paste - -from amulet.api.structure import structure_buffer, Structure -from amulet.api.data_types import Dimension - -if TYPE_CHECKING: - from amulet.api.world import World - - -def get_structure( - world: "World", - dimension: Dimension, -) -> Structure: - if structure_buffer: - return structure_buffer[-1] - else: - raise Exception('You need to copy a selection before pasting.') - - -export = { - "v": 1, # a version 1 plugin - "name": "Paste", # the name of the plugin - "features": ["dst_location_absolute"], - "structure_callable_inputs": [], - "structure_callable": get_structure, - "inputs": ["structure", "options"], # the inputs to give to the plugin - "operation": paste # the actual function to call when running the plugin -} From 796755da2defad753459c38054793cdff725fd5d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 17:02:08 +0100 Subject: [PATCH 68/84] Added an attribute to the canvas class to access the pointer location --- .../programs/edit/canvas/base_edit_canvas.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index a9cbd7bc..af2fe720 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -9,7 +9,7 @@ from amulet.api.chunk import Chunk from amulet.api.structure import Structure from amulet.api.errors import ChunkLoadError -from amulet.api.data_types import PointCoordinatesNDArray, Dimension +from amulet.api.data_types import PointCoordinatesNDArray, Dimension, BlockCoordinates from amulet.api.selection import SelectionGroup from amulet_map_editor.opengl.data_types import CameraLocationType, CameraRotationType @@ -88,6 +88,8 @@ def __init__(self, parent: wx.Window, world: 'World'): self._select_mode = MODE_NORMAL self._selection_moved = True # has the selection point moved and does the box need rebuilding + self._selection_location: BlockCoordinates = (0, 0, 0) + self._selection_group = RenderSelectionGroup( self.context_identifier, self._texture_bounds, @@ -226,6 +228,10 @@ def select_mode(self, select_mode: int): self._select_mode = select_mode self._selection_moved = True + @property + def selection_location(self) -> BlockCoordinates: + return self._selection_location + @property def dimension(self) -> Dimension: return self._render_world.dimension @@ -284,8 +290,8 @@ def _change_box_location(self): position, box_index = self._box_location_distance(self.select_distance) else: position, box_index = self._box_location_closest() - - wx.PostEvent(self, SelectionPointChangeEvent(location=position)) + self._selection_location = position.tolist() + wx.PostEvent(self, SelectionPointChangeEvent(location=position.tolist())) self._selection_group.update_position(position, box_index) def ray_collision(self): From ae5dcd3e30ae380f1820e57e452d33f956b1b678 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 5 Jun 2020 17:49:32 +0100 Subject: [PATCH 69/84] Disconnected drawing modes and box editing logic --- .../programs/edit/canvas/base_edit_canvas.py | 66 ++++++++++++------- .../edit/canvas/controllable_edit_canvas.py | 2 +- .../programs/edit/canvas/edit_canvas.py | 8 +-- .../canvas/ui/tool/tools/base_operation.py | 3 + .../edit/canvas/ui/tool/tools/select.py | 4 +- .../programs/edit/ui/select_location.py | 1 + 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index af2fe720..b1c709be 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -29,10 +29,6 @@ if TYPE_CHECKING: from amulet.api.world import World -MODE_NORMAL = 0 # normal selection -MODE_DISABLED = 1 # non-interactive selection boxes -MODE_STRUCTURE = 2 # MODE_DISABLED and draw structure if exists - class BaseEditCanvas(BaseCanvas): """Adds base logic for drawing everything related to the edit program to the BaseCanvas. @@ -85,18 +81,21 @@ def __init__(self, parent: wx.Window, world: 'World'): self._camera_rotate_speed = 2 self._select_distance = 10 self._select_distance2 = 10 - self._select_mode = MODE_NORMAL self._selection_moved = True # has the selection point moved and does the box need rebuilding self._selection_location: BlockCoordinates = (0, 0, 0) + self._draw_selection = True + self._selection_editable = True self._selection_group = RenderSelectionGroup( self.context_identifier, self._texture_bounds, self._gl_texture_atlas ) + + self._draw_structure = False self._structure: Optional[RenderStructure] = None - self._structure_locations: List[numpy.ndarray] = [] + self._structure_locations: List[numpy.ndarray] = [] # TODO rewrite this self._draw_timer = wx.Timer(self) self._gc_timer = wx.Timer(self) @@ -124,6 +123,10 @@ def Bind(self, event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY): def world(self) -> "World": return self._world() + @property + def selection_location(self) -> BlockCoordinates: + return self._selection_location + @property def selection_group(self) -> SelectionGroup: """Create a SelectionGroup class from the selected boxes.""" @@ -150,12 +153,12 @@ def disable(self): self._rebuild_timer.Stop() self._render_world.disable() - def disable_threads(self): + def _disable_threads(self): """Stop the generation of new chunk geometry. Makes it safe to modify the world data.""" self._render_world.chunk_generator.stop() - def enable_threads(self): + def _enable_threads(self): """Start the generation of new chunk geometry.""" self._render_world.chunk_generator.start() @@ -197,10 +200,39 @@ def structure(self, structure: Structure): self._resource_pack_translator ) + @property + def draw_structure(self) -> bool: + """Should the moveable structure be drawn""" + return self._draw_structure + + @draw_structure.setter + def draw_structure(self, draw_structure: bool): + self._draw_structure = bool(draw_structure) + @property def structure_locations(self) -> List[numpy.ndarray]: + """The locations where the structure should be displayed. DO NOT USE THIS YET. + todo: rewrite this to allow rotation.""" return self._structure_locations + @property + def draw_selection(self) -> bool: + """Should the selection box(es) be drawn""" + return self._draw_selection + + @draw_selection.setter + def draw_selection(self, draw_selection: bool): + self._draw_selection = bool(draw_selection) + + @property + def selection_editable(self) -> bool: + """Should the selection box(es) be editable""" + return self._selection_editable + + @selection_editable.setter + def selection_editable(self, selection_editable: bool): + self._selection_editable = bool(selection_editable) + @property def select_distance(self) -> int: return self._select_distance @@ -219,19 +251,6 @@ def select_distance2(self, distance: int): self._select_distance2 = distance self._selection_moved = True - @property - def select_mode(self) -> int: - return self._select_mode - - @select_mode.setter - def select_mode(self, select_mode: int): - self._select_mode = select_mode - self._selection_moved = True - - @property - def selection_location(self) -> BlockCoordinates: - return self._selection_location - @property def dimension(self) -> Dimension: return self._render_world.dimension @@ -422,7 +441,7 @@ def _on_draw(self, event): def draw(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self._render_world.draw(self.transformation_matrix) - if self._select_mode == MODE_STRUCTURE and self._structure is not None: + if self._draw_structure and self._structure is not None: transform = numpy.eye(4, dtype=numpy.float32) for location in self.structure_locations: transform[3, 0:3] = location @@ -430,7 +449,8 @@ def draw(self): if self._selection_moved: self._selection_moved = False self._change_box_location() - self._selection_group.draw(self.transformation_matrix, tuple(self.camera_location), self._select_mode == MODE_NORMAL) + if self._draw_selection: + self._selection_group.draw(self.transformation_matrix, tuple(self.camera_location), self._selection_editable) self.SwapBuffers() def _gc(self, event): diff --git a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py index 2f7d162d..a9beace2 100644 --- a/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/controllable_edit_canvas.py @@ -102,7 +102,7 @@ def _process_action(self, evt, press: bool): self._selection_group.delete_active() else: # run once on button press and frequently until released if action == "box click": - if self.select_mode == 0: + if self.selection_editable: self.box_select("add box modifier" in self._persistent_actions) elif action == "toggle mouse mode": self._toggle_mouse_lock() diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 6c1a1e7c..0d9c1b9d 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -110,7 +110,7 @@ def run_operation( msg="", throw_exceptions=False ) -> Any: - self.disable_threads() + self._disable_threads() err = None out = None try: @@ -143,7 +143,7 @@ def run_operation( wx.MessageDialog(self, f"Exception running operation: {e}\nSee the console for more details", style=wx.OK).ShowModal() err = e - self.enable_threads() + self._enable_threads() if err is not None and throw_exceptions: raise err return out @@ -198,7 +198,7 @@ def goto(self): self.camera_location = location def save(self): - self.disable_threads() + self._disable_threads() def save(): for chunk_index, chunk_count in self.world.save_iter(): @@ -206,7 +206,7 @@ def save(): show_loading_dialog(lambda: save(), f"Saving world.", "Please wait.", self) wx.PostEvent(self, SaveEvent()) - self.enable_threads() + self._enable_threads() def close(self): wx.PostEvent(self, EditCloseEvent()) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py index 40243be0..48ae1567 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/base_operation.py @@ -59,6 +59,9 @@ def _operation_change(self): def enable(self): self._operation_change() + self.canvas.draw_structure = False + self.canvas.draw_selection = True + self.canvas.selection_editable = False def disable(self): self._unload_active_operation() diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index 531e725f..7398a13b 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -90,7 +90,9 @@ def enable(self): self._remove_paste() self._button_panel.Show() self.Layout() - self.canvas.select_mode = 0 + self.canvas.draw_structure = False + self.canvas.draw_selection = True + self.canvas.selection_editable = True def disable(self): self._remove_paste() diff --git a/amulet_map_editor/programs/edit/ui/select_location.py b/amulet_map_editor/programs/edit/ui/select_location.py index 9ced6b5c..7d4d4e6d 100644 --- a/amulet_map_editor/programs/edit/ui/select_location.py +++ b/amulet_map_editor/programs/edit/ui/select_location.py @@ -30,6 +30,7 @@ def __init__( self.canvas.structure_locations.clear() self.canvas.structure_locations.append(numpy.array([0, 0, 0])) self.canvas.structure = structure + self.canvas.draw_structure = True def _add_row(label: str, wx_object: Type[wx.Object], **kwargs) -> Any: sizer = wx.BoxSizer(wx.HORIZONTAL) From 8f3a3e219d19015663cb0658219c7bf777d5aad3 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 12:53:04 +0100 Subject: [PATCH 70/84] Modified open world UI ratio --- amulet_map_editor/amulet_wx/ui/select_world.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amulet_map_editor/amulet_wx/ui/select_world.py b/amulet_map_editor/amulet_wx/ui/select_world.py index 073cf4fc..7ba2522b 100644 --- a/amulet_map_editor/amulet_wx/ui/select_world.py +++ b/amulet_map_editor/amulet_wx/ui/select_world.py @@ -281,7 +281,7 @@ def __init__(self, parent, open_world_callback): wx.HORIZONTAL ) self._open_world_callback = open_world_callback - self.add_object(WorldSelectUI(self, self._update_recent), 2, wx.ALL | wx.EXPAND) + self.add_object(WorldSelectUI(self, self._update_recent), 1, wx.ALL | wx.EXPAND) self._recent_worlds = RecentWorldUI(self, self._update_recent) self.add_object(self._recent_worlds, 1, wx.EXPAND) From 0337f84101df83fc40219c5d2bea4a3c5c05c9ad Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 13:05:19 +0100 Subject: [PATCH 71/84] Added buttons to the select tool --- .../programs/edit/canvas/ui/tool/tools/select.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index 7398a13b..b4420f2d 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -20,8 +20,18 @@ def __init__(self, canvas: 'EditCanvas'): self._button_panel = wx.Panel(canvas) button_sizer = wx.BoxSizer(wx.VERTICAL) self._button_panel.SetSizer(button_sizer) - button = wx.Button(self._button_panel, label="hello") - button_sizer.Add(button) + delete_button = wx.Button(self._button_panel, label="Delete") + button_sizer.Add(delete_button, 0, wx.ALL, 5) + delete_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.delete()) + copy_button = wx.Button(self._button_panel, label="Copy") + button_sizer.Add(copy_button, 0, wx.ALL, 5) + copy_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.copy()) + cut_button = wx.Button(self._button_panel, label="Cut") + button_sizer.Add(cut_button, 0, wx.ALL, 5) + cut_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.cut()) + paste_button = wx.Button(self._button_panel, label="Paste") + button_sizer.Add(paste_button, 0, wx.ALL, 5) + paste_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.paste()) self.Add(self._button_panel) self._paste_panel: Optional[SelectLocationUI] = None From 273ec6f758c0ce6e88d6d7628f2ca5b8cedf301a Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 13:15:51 +0100 Subject: [PATCH 72/84] Fixed backspace not working with int validator --- amulet_map_editor/amulet_wx/util/validators.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/amulet_map_editor/amulet_wx/util/validators.py b/amulet_map_editor/amulet_wx/util/validators.py index 492a8845..7e6980d4 100644 --- a/amulet_map_editor/amulet_wx/util/validators.py +++ b/amulet_map_editor/amulet_wx/util/validators.py @@ -1,8 +1,19 @@ import wx +SpecialChrs = { + wx.WXK_BACK, + wx.WXK_DELETE, + wx.WXK_SHIFT, + wx.WXK_END, + wx.WXK_HOME, + wx.WXK_LEFT, + wx.WXK_RIGHT, +} + + class BaseValidator(wx.Validator): - ''' Validates data as it is entered into the text controls. ''' + """Validates data as it is entered into the text controls.""" def __init__(self): super().__init__() @@ -27,12 +38,12 @@ def OnChar(self, event): class IntValidator(BaseValidator): def OnChar(self, event): keycode = int(event.GetKeyCode()) - if keycode > 256 or 48 <= keycode <= 57 or keycode == 45: + if keycode in SpecialChrs or 48 <= keycode <= 57 or keycode == 45: event.Skip() class FloatValidator(BaseValidator): def OnChar(self, event): keycode = int(event.GetKeyCode()) - if keycode > 256 or 45 <= keycode <= 57: + if keycode in SpecialChrs or 45 <= keycode <= 57: event.Skip() From e80a53d3deb9c8c8940eb27a61c6f7dfd354906f Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 14:36:37 +0100 Subject: [PATCH 73/84] Added a helper class to make more simple operations easier Operations that only require a static ui and a run button can use this class to implement and handle most of the base logic rather than recreating it each time --- .../plugins/api/simple_operation_panel.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py diff --git a/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py b/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py new file mode 100644 index 00000000..4a7aea8b --- /dev/null +++ b/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py @@ -0,0 +1,44 @@ +import wx +from typing import TYPE_CHECKING + +from amulet.api.selection import SelectionGroup +from amulet.api.data_types import Dimension, OperationReturnType + +from .operation_ui import OperationUI + +if TYPE_CHECKING: + from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class SimpleOperationPanel(wx.Panel, OperationUI): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + wx.Panel.__init__(self, parent) + OperationUI.__init__(self, parent, canvas, world, options_path) + + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + + def _add_run_button(self, label="Run Operation"): + self._run_button = wx.Button(self, label=label) + self._run_button.Bind(wx.EVT_BUTTON, self._run_operation) + self._sizer.Add(self._run_button, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 5) + self.Layout() + + def _run_operation(self, _): + self.canvas.run_operation( + lambda: self._operation( + self.world, + self.canvas.dimension, + self.canvas.selection_group + ) + ) + + def _operation(self, world: "World", dimension: Dimension, target_box: SelectionGroup) -> OperationReturnType: + raise NotImplementedError From da422982a51b6e74043a0171c765d6beea407f14 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 15:22:10 +0100 Subject: [PATCH 74/84] Made operation names save to be file names --- amulet_map_editor/programs/edit/plugins/api/loader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/loader.py b/amulet_map_editor/programs/edit/plugins/api/loader.py index 39f48320..f7603081 100644 --- a/amulet_map_editor/programs/edit/plugins/api/loader.py +++ b/amulet_map_editor/programs/edit/plugins/api/loader.py @@ -7,6 +7,7 @@ import struct import hashlib import inspect +import string from .fixed_pipeline import FixedFunctionUI from .operation_ui import OperationUI, OperationUIType @@ -20,6 +21,8 @@ STOCK_PLUGINS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "stock_plugins")) CUSTOM_PLUGINS_DIR = os.path.abspath("plugins") +ValidChrs = set("-_.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + class OperationLoadException(Exception): """Exception for internal use""" @@ -53,14 +56,14 @@ def _load(self, export_dict: dict): os.path.join( "config", "edit_plugins", - f"""{self._name}_{ + f"""{''.join(c for c in self._name if c in ValidChrs)}_{ struct.unpack( "H", hashlib.sha1( self._path.encode('utf-8') ).digest()[:2] )[0] - }""" # generate a file name that identifiable to the operation but "unique" to the path + }.config""" # generate a file name that identifiable to the operation but "unique" to the path ) ) From 6ce3b9cdb75fe1351091871eef4f547f4f6f3082 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 15:22:37 +0100 Subject: [PATCH 75/84] Updated the existing structure exporters --- .../export_operations/construction.py | 118 ++++++++------- .../export_operations/mcstructure.py | 120 ++++++++------- .../export_operations/schematic.py | 142 ++++++++++-------- 3 files changed, 204 insertions(+), 176 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py index 5ea771dc..c14a094c 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/construction.py @@ -1,75 +1,81 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError -from amulet.api.data_types import Dimension +from amulet.api.data_types import Dimension, OperationReturnType from amulet.structure_interface.construction import ConstructionFormatWrapper +from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationError + + if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + +class ExportConstruction(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) -def export_construction( - world: "World", - dimension: Dimension, - selection: SelectionGroup, - options: dict -): - path, platform, version = options.get('path', None), options.get('platform', None), options.get('version', None) - if isinstance(path, str) and path.endswith('.construction') and platform and version: - wrapper = ConstructionFormatWrapper(path, 'w') - wrapper.platform = platform - wrapper.version = version - wrapper.selection = selection - wrapper.translation_manager = world.translation_manager - wrapper.open() - for cx, cz in selection.chunk_locations(): - try: - chunk = world.get_chunk(cx, cz, dimension) - wrapper.commit_chunk(chunk, world.palette) - except ChunkLoadError: - continue + options = self._load_options({}) - wrapper.close() - else: - raise Exception('Please specify a save location and version in the options before running.') + self._file_picker = wx.FilePickerCtrl( + self, + path=options.get('path', ''), + wildcard="Construction file (*.construction)|*.construction", + 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, + world.translation_manager, + options.get("platform", None) or world.world_wrapper.platform, + allow_universal=False + ) + self._sizer.Add(self._version_define, 0, wx.CENTRE, 5) + self._add_run_button("Export") + self.Layout() + def unload(self): + self._save_options({ + "path": self._file_picker.GetPath(), + "platform": self._version_define.platform, + "version": self._version_define.version + }) -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Export Construction') - file_picker = wx.FilePickerCtrl( - dialog, - path=options.get('path', ''), - wildcard="Construction file (*.construction)|*.construction", - style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT - ) - dialog.sizer.Add(file_picker, 0, wx.ALL, 5) - version_define = VersionSelect( - dialog, - world.translation_manager, - options.get("platform", None) or world.world_wrapper.platform, - allow_universal=False - ) - dialog.sizer.Add(version_define, 0) - dialog.Fit() + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup) -> OperationReturnType: + path = self._file_picker.GetPath() + platform = self._version_define.platform + version = self._version_define.version + if isinstance(path, str) and path.endswith('.construction') and platform and version: + wrapper = ConstructionFormatWrapper(path, 'w') + wrapper.platform = platform + wrapper.version = version + wrapper.selection = selection + wrapper.translation_manager = world.translation_manager + wrapper.open() + for cx, cz in selection.chunk_locations(): + try: + chunk = world.get_chunk(cx, cz, dimension) + wrapper.commit_chunk(chunk, world.palette) + except ChunkLoadError: + continue - if dialog.ShowModal() == wx.ID_OK: - options = { - "path": file_picker.GetPath(), - "platform": version_define.platform, - "version": version_define.version - } - return options + wrapper.close() + else: + raise OperationError('Please specify a save location and version in the options before running.') export = { - "v": 1, # a version 1 plugin - "name": "Export Construction", # the name of the plugin - "features": ["src_selection", "wxoptions"], - "inputs": ["src_selection", "options"], # the inputs to give to the plugin - "operation": export_construction, # the actual function to call when running the plugin - "wxoptions": show_ui + "name": "\tExport Construction", # the name of the plugin + "operation": ExportConstruction, # the UI class to display } diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py index e62392a1..04448e59 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/mcstructure.py @@ -1,75 +1,85 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError -from amulet.api.data_types import Dimension +from amulet.api.data_types import Dimension, OperationReturnType from amulet.structure_interface.mcstructure import MCStructureFormatWrapper +from amulet_map_editor.amulet_wx.ui.select_block import VersionSelect +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationError + + if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class ExportMCStructure(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) + + options = self._load_options({}) + self._file_picker = wx.FilePickerCtrl( + self, + path=options.get('path', ''), + wildcard="mcstructure file (*.mcstructure)|*.mcstructure", + 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, + world.translation_manager, + options.get("platform", None) or world.world_wrapper.platform, + allowed_platforms=("bedrock", ), + allow_numerical=False + ) + self._sizer.Add(self._version_define, 0, wx.CENTRE, 5) + self._add_run_button("Export") + self.Layout() -def export_mcstructure( - world: "World", - dimension: Dimension, - selection: SelectionGroup, - options: dict -): - assert len(selection.selection_boxes) == 1, "The mcstructure format only supports a single selection box." - path, version = options.get('path', None), options.get('version', None) - if isinstance(path, str) and path.endswith('.mcstructure') and version: - wrapper = MCStructureFormatWrapper(path, 'w') - wrapper.selection = selection - wrapper.version = version - wrapper.translation_manager = world.translation_manager - wrapper.open() - for cx, cz in wrapper.selection.chunk_locations(): - try: - chunk = world.get_chunk(cx, cz, dimension) - wrapper.commit_chunk(chunk, world.palette) - except ChunkLoadError: - continue + def unload(self): + self._save_options({ + "path": self._file_picker.GetPath(), + "version": self._version_define.version + }) - wrapper.close() - else: - raise Exception('Please specify a save location and platform in the options before running.') + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup) -> OperationReturnType: + if len(selection.selection_boxes) == 0: + raise OperationError("No selection was given to export.") + elif len(selection.selection_boxes) != 1: + raise OperationError("The mcstructure format only supports a single selection box.") + path = self._file_picker.GetPath() + version = self._version_define.version -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Export Bedrock .mcstructure') - file_picker = wx.FilePickerCtrl( - dialog, - path=options.get('path', ''), - wildcard="mcstructure file (*.mcstructure)|*.mcstructure", - style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT - ) - dialog.sizer.Add(file_picker, 0, wx.ALL, 5) - version_define = VersionSelect( - dialog, - world.translation_manager, - options.get("platform", None) or world.world_wrapper.platform, - allowed_platforms=("bedrock", ), - allow_numerical=False - ) - dialog.sizer.Add(version_define, 0) - dialog.Fit() + if isinstance(path, str) and path.endswith('.mcstructure') and version: + wrapper = MCStructureFormatWrapper(path, 'w') + wrapper.selection = selection + wrapper.version = version + wrapper.translation_manager = world.translation_manager + wrapper.open() + for cx, cz in wrapper.selection.chunk_locations(): + try: + chunk = world.get_chunk(cx, cz, dimension) + wrapper.commit_chunk(chunk, world.palette) + except ChunkLoadError: + continue - if dialog.ShowModal() == wx.ID_OK: - options = { - "path": file_picker.GetPath(), - "version": version_define.version - } - return options + wrapper.close() + else: + raise OperationError('Please specify a save location and version in the options before running.') export = { - "v": 1, # a version 1 plugin "name": "Export Bedrock .mcstructure", # the name of the plugin - "features": ["src_selection", "wxoptions"], - "inputs": ["src_selection", "options"], # the inputs to give to the plugin - "operation": export_mcstructure, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": ExportMCStructure, # the UI class to display } diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py index 69b55932..a2c43431 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/export_operations/schematic.py @@ -1,63 +1,22 @@ from typing import TYPE_CHECKING import wx -from amulet_map_editor.amulet_wx.ui.select_block import PlatformSelect -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError -from amulet.api.data_types import Dimension +from amulet.api.data_types import Dimension, OperationReturnType from amulet.structure_interface.schematic import SchematicFormatWrapper +from amulet_map_editor.amulet_wx.ui.select_block import PlatformSelect +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationError + + if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -def export_schematic( - world: "World", - dimension: Dimension, - selection: SelectionGroup, - options: dict -): - assert len(selection.selection_boxes) == 1, "The schematic format only supports a single selection box." - path, platform = options.get('path', None), options.get('platform', None) - if isinstance(path, str) and path.endswith('.schematic') and platform: - wrapper = SchematicFormatWrapper(path, 'w') - wrapper.platform = platform - wrapper.selection = selection - wrapper.translation_manager = world.translation_manager - wrapper.open() - for cx, cz in wrapper.selection.chunk_locations(): - try: - chunk = world.get_chunk(cx, cz, dimension) - wrapper.commit_chunk(chunk, world.palette) - except ChunkLoadError: - continue - - wrapper.close() - else: - raise Exception('Please specify a save location and platform in the options before running.') - - -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Export Schematic (legacy)') - file_picker = wx.FilePickerCtrl( - dialog, - path=options.get('path', ''), - wildcard="Schematic file (*.schematic)|*.schematic", - style=wx.FLP_USE_TEXTCTRL | wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT - ) - dialog.sizer.Add(file_picker, 0, wx.ALL | wx.CENTER, 5) - platform_define = PlatformSelect( - dialog, - world.translation_manager, - options.get("platform", None) or world.world_wrapper.platform, - allow_universal=False - ) - dialog.sizer.Add(platform_define, 0, wx.CENTER, 5) - dialog.sizer.Add( - wx.StaticText( - dialog, - label="""The Schematic format is a +WarningMsg = """The Schematic format is a legacy format that can only save data in the numerical format. Anything that was @@ -66,25 +25,78 @@ def show_ui(parent, world: "World", options: dict) -> dict: saved in the schematic file. We suggest using the Construction -file format instead.""", - style=wx.ALIGN_CENTRE_HORIZONTAL - ), 0, wx.ALL | wx.CENTER, 5 - ) - dialog.Fit() +file format instead.""" + + +class ExportSchematic(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) + + options = self._load_options({}) + + self._file_picker = wx.FilePickerCtrl( + self, + path=options.get('path', ''), + wildcard="Schematic file (*.schematic)|*.schematic", + 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._platform_define = PlatformSelect( + self, + world.translation_manager, + options.get("platform", None) or world.world_wrapper.platform, + allow_universal=False + ) + self._sizer.Add(self._platform_define, 0, wx.CENTRE, 5) + self._sizer.Add( + wx.StaticText( + self, + label=WarningMsg, + style=wx.ALIGN_CENTRE_HORIZONTAL + ), 0, wx.ALL | wx.CENTER, 5 + ) + self._add_run_button("Export") + self.Layout() + + def unload(self): + self._save_options({ + "path": self._file_picker.GetPath(), + "platform": self._platform_define.platform, + }) + + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup) -> OperationReturnType: + if len(selection.selection_boxes) == 0: + raise OperationError("No selection was given to export.") + elif len(selection.selection_boxes) != 1: + raise OperationError("The schematic format only supports a single selection box.") + + path = self._file_picker.GetPath() + platform = self._platform_define.platform + if isinstance(path, str) and path.endswith('.schematic') and platform: + wrapper = SchematicFormatWrapper(path, 'w') + wrapper.platform = platform + wrapper.selection = selection + wrapper.translation_manager = world.translation_manager + wrapper.open() + for cx, cz in wrapper.selection.chunk_locations(): + try: + chunk = world.get_chunk(cx, cz, dimension) + wrapper.commit_chunk(chunk, world.palette) + except ChunkLoadError: + continue - if dialog.ShowModal() == wx.ID_OK: - options = { - "path": file_picker.GetPath(), - "platform": platform_define.platform - } - return options + wrapper.close() + else: + raise OperationError('Please specify a save location and platform in the options before running.') export = { - "v": 1, # a version 1 plugin "name": "Export Schematic (legacy)", # the name of the plugin - "features": ["src_selection", "wxoptions"], - "inputs": ["src_selection", "options"], # the inputs to give to the plugin - "operation": export_schematic, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": ExportSchematic, # the UI class to display } From cb74a310a4c6cb74a3df9a32b8839ebfd59b55cd Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 15:51:04 +0100 Subject: [PATCH 76/84] Updated the clone operation --- .../plugins/api/simple_operation_panel.py | 2 +- .../plugins/stock_plugins/operations/clone.py | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py b/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py index 4a7aea8b..036530f6 100644 --- a/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py +++ b/amulet_map_editor/programs/edit/plugins/api/simple_operation_panel.py @@ -40,5 +40,5 @@ def _run_operation(self, _): ) ) - def _operation(self, world: "World", dimension: Dimension, target_box: SelectionGroup) -> OperationReturnType: + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup) -> OperationReturnType: raise NotImplementedError diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py index 18dc74e8..bbe551c6 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py @@ -1,9 +1,40 @@ -from amulet.operations.paste import paste +from typing import TYPE_CHECKING +import wx + +from amulet.api.structure import Structure +from amulet.api.selection import SelectionGroup +from amulet.api.data_types import Dimension, OperationReturnType + +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel + +if TYPE_CHECKING: + from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + + +class Clone(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) + self._add_run_button() + self.Layout() + + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup) -> OperationReturnType: + structure = Structure.from_world( + world, selection, dimension + ) + self.canvas.paste(structure) + + def unload(self): + pass + export = { - "v": 1, # a version 1 plugin "name": "Clone", # the name of the plugin - "features": ["src_selection", "dst_location_absolute"], - "inputs": ["structure", "options"], # the inputs to give to the plugin - "operation": paste # the actual function to call when running the plugin + "operation": Clone # the actual function to call when running the plugin } From 879ddd1dabb71ea5783d12f88939e7fbe2c7d9e1 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 15:52:14 +0100 Subject: [PATCH 77/84] Enabled the select UI on paste --- amulet_map_editor/programs/edit/canvas/edit_canvas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/amulet_map_editor/programs/edit/canvas/edit_canvas.py b/amulet_map_editor/programs/edit/canvas/edit_canvas.py index 0d9c1b9d..999869af 100644 --- a/amulet_map_editor/programs/edit/canvas/edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/edit_canvas.py @@ -24,6 +24,7 @@ SaveEvent, EditCloseEvent, PasteEvent, + ToolChangeEvent, ) from amulet_map_editor.programs.edit.canvas.controllable_edit_canvas import ControllableEditCanvas from amulet_map_editor.programs.edit.canvas.ui.file import FilePanel @@ -181,6 +182,7 @@ def paste(self, structure: Structure = None): else: wx.MessageBox("A structure needs to be copied before one can be pasted.") return + wx.PostEvent(self, ToolChangeEvent(tool="Select")) wx.PostEvent(self, PasteEvent(structure=structure)) def delete(self): From 4542cbdded448d0dcf4b1ab680def4c2d88eeaf7 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 15:57:36 +0100 Subject: [PATCH 78/84] Stopped clone incrementing the undo counter --- .../programs/edit/plugins/stock_plugins/operations/clone.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py index bbe551c6..4cc72fd8 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/operations/clone.py @@ -6,6 +6,7 @@ from amulet.api.data_types import Dimension, OperationReturnType from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationSilentAbort if TYPE_CHECKING: from amulet.api.world import World @@ -29,6 +30,7 @@ def _operation(self, world: "World", dimension: Dimension, selection: SelectionG world, selection, dimension ) self.canvas.paste(structure) + raise OperationSilentAbort def unload(self): pass From 1ebaefaef4ecc26bc7794f9d60e8e6c8be44c359 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 16:03:39 +0100 Subject: [PATCH 79/84] Moved SelectLocationUI back into canvas.ui It didn't make much sense having two separate ui locations. Reusable UI elements will be imported into the edit init to expose them there --- amulet_map_editor/programs/edit/__init__.py | 2 ++ .../programs/edit/{ => canvas}/ui/select_location.py | 2 +- amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py | 3 +-- amulet_map_editor/programs/edit/ui/__init__.py | 0 4 files changed, 4 insertions(+), 3 deletions(-) rename amulet_map_editor/programs/edit/{ => canvas}/ui/select_location.py (97%) delete mode 100644 amulet_map_editor/programs/edit/ui/__init__.py diff --git a/amulet_map_editor/programs/edit/__init__.py b/amulet_map_editor/programs/edit/__init__.py index 8da4d8f8..c05f7f6f 100644 --- a/amulet_map_editor/programs/edit/__init__.py +++ b/amulet_map_editor/programs/edit/__init__.py @@ -1,5 +1,7 @@ from .edit import EditExtension +from .canvas.ui.select_location import SelectLocationUI + export = { "name": "3D Editor", "ui": EditExtension diff --git a/amulet_map_editor/programs/edit/ui/select_location.py b/amulet_map_editor/programs/edit/canvas/ui/select_location.py similarity index 97% rename from amulet_map_editor/programs/edit/ui/select_location.py rename to amulet_map_editor/programs/edit/canvas/ui/select_location.py index 7d4d4e6d..36beff18 100644 --- a/amulet_map_editor/programs/edit/ui/select_location.py +++ b/amulet_map_editor/programs/edit/canvas/ui/select_location.py @@ -34,7 +34,7 @@ def __init__( def _add_row(label: str, wx_object: Type[wx.Object], **kwargs) -> Any: sizer = wx.BoxSizer(wx.HORIZONTAL) - self.add_object(sizer, 0, 0) + self.add_object(sizer, 0, wx.ALIGN_CENTER_HORIZONTAL) name_text = wx.StaticText(self, label=label) sizer.Add(name_text, flag=wx.CENTER | wx.ALL | wx.EXPAND, border=5) obj = wx_object(self, **kwargs) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index b4420f2d..4a67de44 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -1,11 +1,10 @@ from typing import TYPE_CHECKING, Type, Any, Optional import wx -import numpy from amulet.operations.paste import paste_iter from amulet_map_editor.programs.edit.canvas.ui.tool.tools.base_tool_ui import BaseToolUI -from amulet_map_editor.programs.edit.ui.select_location import SelectLocationUI +from amulet_map_editor.programs.edit.canvas.ui.select_location import SelectLocationUI from amulet_map_editor.programs.edit.canvas.events import EVT_PASTE if TYPE_CHECKING: diff --git a/amulet_map_editor/programs/edit/ui/__init__.py b/amulet_map_editor/programs/edit/ui/__init__.py deleted file mode 100644 index e69de29b..00000000 From fd5b19ed1cb49ffcfbfefdc12181e6512e8d336b Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 16:25:11 +0100 Subject: [PATCH 80/84] Improved the selection box for use underground --- amulet_map_editor/programs/edit/canvas/base_edit_canvas.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py index b1c709be..e314d752 100644 --- a/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py +++ b/amulet_map_editor/programs/edit/canvas/base_edit_canvas.py @@ -332,6 +332,7 @@ def _box_location_closest(self) -> Tuple[PointCoordinatesNDArray, Optional[int]] cx: Optional[int] = None cz: Optional[int] = None chunk: Optional[Chunk] = None + in_air = False box_index, nearest_selection_box = self._selection_group.closest_intersection(self.camera_location, self._look_vector()) @@ -352,7 +353,11 @@ def _box_location_closest(self) -> Tuple[PointCoordinatesNDArray, Optional[int]] chunk = None if chunk is not None and self._render_world.world.palette[chunk.blocks[x % 16, y, z % 16]].namespaced_name != 'universal_minecraft:air': - return location, None + # the block is not air + if in_air: # if we have previously found an air block + return location, None + elif not in_air: + in_air = True return location, None def _box_location_distance(self, distance: int) -> Tuple[PointCoordinatesNDArray, Optional[int]]: From db214215a8e81226f86275cfb09fc8d568d51439 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 8 Jun 2020 18:50:41 +0100 Subject: [PATCH 81/84] Fixed the select UI not being in the centre of the screen --- .../programs/edit/canvas/ui/tool/tools/select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index 4a67de44..90a1d4df 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -13,7 +13,7 @@ class SelectOptions(wx.BoxSizer, BaseToolUI): def __init__(self, canvas: 'EditCanvas'): - wx.BoxSizer.__init__(self, wx.VERTICAL) + wx.BoxSizer.__init__(self, wx.HORIZONTAL) BaseToolUI.__init__(self, canvas) self._button_panel = wx.Panel(canvas) @@ -31,7 +31,7 @@ def __init__(self, canvas: 'EditCanvas'): paste_button = wx.Button(self._button_panel, label="Paste") button_sizer.Add(paste_button, 0, wx.ALL, 5) paste_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.paste()) - self.Add(self._button_panel) + self.Add(self._button_panel, 0, wx.ALIGN_CENTER_VERTICAL) self._paste_panel: Optional[SelectLocationUI] = None From 3a3ccfa227fa2fa29063ca37b56aaf7bdbac1053 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 9 Jun 2020 12:03:41 +0100 Subject: [PATCH 82/84] Readded the box resizing UI The events need implementing to make it work again --- .../programs/edit/canvas/events.py | 9 +- .../edit/canvas/ui/tool/tools/select.py | 141 ++++++++++-------- 2 files changed, 82 insertions(+), 68 deletions(-) diff --git a/amulet_map_editor/programs/edit/canvas/events.py b/amulet_map_editor/programs/edit/canvas/events.py index e99eaa58..d35fa5cd 100644 --- a/amulet_map_editor/programs/edit/canvas/events.py +++ b/amulet_map_editor/programs/edit/canvas/events.py @@ -18,8 +18,7 @@ SelectionPointChangeEvent, EVT_SELECTION_POINT_CHANGE = newevent.NewEvent() # events fired when the active selection box changes. TODO: reimplement these -BoxChangeEvent, EVT_BOX_CHANGE = newevent.NewEvent() -BoxGreenCornerChangeEvent, EVT_BOX_GREEN_CORNER_CHANGE = newevent.NewEvent() -BoxBlueCornerChangeEvent, EVT_BOX_BLUE_CORNER_CHANGE = newevent.NewEvent() - -BoxCoordsEnableEvent, EVT_BOX_COORDS_ENABLE = newevent.NewEvent() +BoxChangeEvent, EVT_BOX_CHANGE = newevent.NewEvent() # one or more of the box coordinates have changed +BoxPoint1ChangeEvent, EVT_BOX_POINT_1_CHANGE = newevent.NewEvent() # the first box corner has changed +BoxPoint2ChangeEvent, EVT_BOX_POINT_2_CHANGE = newevent.NewEvent() # the second box corner has changed +BoxEditToggleEvent, EVT_BOX_EDIT_TOGGLE = newevent.NewEvent() # the box has switched between edit and static mode diff --git a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py index 90a1d4df..2e6d689e 100644 --- a/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py +++ b/amulet_map_editor/programs/edit/canvas/ui/tool/tools/select.py @@ -3,9 +3,15 @@ from amulet.operations.paste import paste_iter +from amulet_map_editor.amulet_wx.util.validators import IntValidator from amulet_map_editor.programs.edit.canvas.ui.tool.tools.base_tool_ui import BaseToolUI from amulet_map_editor.programs.edit.canvas.ui.select_location import SelectLocationUI -from amulet_map_editor.programs.edit.canvas.events import EVT_PASTE +from amulet_map_editor.programs.edit.canvas.events import ( + EVT_PASTE, + EVT_BOX_POINT_1_CHANGE, + EVT_BOX_POINT_2_CHANGE, + EVT_BOX_EDIT_TOGGLE, +) if TYPE_CHECKING: from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas @@ -20,53 +26,59 @@ def __init__(self, canvas: 'EditCanvas'): button_sizer = wx.BoxSizer(wx.VERTICAL) self._button_panel.SetSizer(button_sizer) delete_button = wx.Button(self._button_panel, label="Delete") - button_sizer.Add(delete_button, 0, wx.ALL, 5) + button_sizer.Add(delete_button, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL | wx.EXPAND, 5) delete_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.delete()) copy_button = wx.Button(self._button_panel, label="Copy") - button_sizer.Add(copy_button, 0, wx.ALL, 5) + button_sizer.Add(copy_button, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL | wx.EXPAND, 5) copy_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.copy()) cut_button = wx.Button(self._button_panel, label="Cut") - button_sizer.Add(cut_button, 0, wx.ALL, 5) + button_sizer.Add(cut_button, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL | wx.EXPAND, 5) cut_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.cut()) paste_button = wx.Button(self._button_panel, label="Paste") - button_sizer.Add(paste_button, 0, wx.ALL, 5) + button_sizer.Add(paste_button, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL | wx.EXPAND, 5) paste_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.paste()) self.Add(self._button_panel, 0, wx.ALIGN_CENTER_VERTICAL) self._paste_panel: Optional[SelectLocationUI] = None - # self._x1: wx.SpinCtrl = self._add_row('x1', wx.SpinCtrl, min=-30000000, max=30000000) - # self._y1: wx.SpinCtrl = self._add_row('y1', wx.SpinCtrl, min=-30000000, max=30000000) - # self._z1: wx.SpinCtrl = self._add_row('z1', wx.SpinCtrl, min=-30000000, max=30000000) - # self._x1.Bind(wx.EVT_SPINCTRL, self._green_corner_input_change) - # self._y1.Bind(wx.EVT_SPINCTRL, self._green_corner_input_change) - # self._z1.Bind(wx.EVT_SPINCTRL, self._green_corner_input_change) - # - # self._x2: wx.SpinCtrl = self._add_row('x2', wx.SpinCtrl, min=-30000000, max=30000000) - # self._y2: wx.SpinCtrl = self._add_row('y2', wx.SpinCtrl, min=-30000000, max=30000000) - # self._z2: wx.SpinCtrl = self._add_row('z2', wx.SpinCtrl, min=-30000000, max=30000000) - # self._x2.Bind(wx.EVT_SPINCTRL, self._blue_corner_input_change) - # self._y2.Bind(wx.EVT_SPINCTRL, self._blue_corner_input_change) - # self._z2.Bind(wx.EVT_SPINCTRL, self._blue_corner_input_change) - # - # self._x1.Disable() - # self._y1.Disable() - # self._z1.Disable() - # self._x2.Disable() - # self._y2.Disable() - # self._z2.Disable() - # - # self._x1.SetBackgroundColour((160, 215, 145)) - # self._y1.SetBackgroundColour((160, 215, 145)) - # self._z1.SetBackgroundColour((160, 215, 145)) - # - # self._x2.SetBackgroundColour((150, 150, 215)) - # self._y2.SetBackgroundColour((150, 150, 215)) - # self._z2.SetBackgroundColour((150, 150, 215)) - # - # self._canvas().Bind(EVT_BOX_GREEN_CORNER_CHANGE, self._green_corner_renderer_change) - # self._canvas().Bind(EVT_BOX_BLUE_CORNER_CHANGE, self._blue_corner_renderer_change) - # self._canvas().Bind(EVT_BOX_COORDS_ENABLE, self._enable_scrolls) + self._x1: wx.SpinCtrl = self._add_row('x1', wx.SpinCtrl, min=-30000000, max=30000000) + self._y1: wx.SpinCtrl = self._add_row('y1', wx.SpinCtrl, min=-30000000, max=30000000) + self._z1: wx.SpinCtrl = self._add_row('z1', wx.SpinCtrl, min=-30000000, max=30000000) + self._x1.Bind(wx.EVT_SPINCTRL, self._green_corner_input_change) + self._y1.Bind(wx.EVT_SPINCTRL, self._green_corner_input_change) + self._z1.Bind(wx.EVT_SPINCTRL, self._green_corner_input_change) + self._x1.SetValidator(IntValidator()) + self._y1.SetValidator(IntValidator()) + self._z1.SetValidator(IntValidator()) + + self._x2: wx.SpinCtrl = self._add_row('x2', wx.SpinCtrl, min=-30000000, max=30000000) + self._y2: wx.SpinCtrl = self._add_row('y2', wx.SpinCtrl, min=-30000000, max=30000000) + self._z2: wx.SpinCtrl = self._add_row('z2', wx.SpinCtrl, min=-30000000, max=30000000) + self._x2.Bind(wx.EVT_SPINCTRL, self._blue_corner_input_change) + self._y2.Bind(wx.EVT_SPINCTRL, self._blue_corner_input_change) + self._z2.Bind(wx.EVT_SPINCTRL, self._blue_corner_input_change) + self._x2.SetValidator(IntValidator()) + self._y2.SetValidator(IntValidator()) + self._z2.SetValidator(IntValidator()) + + self._x1.Disable() + self._y1.Disable() + self._z1.Disable() + self._x2.Disable() + self._y2.Disable() + self._z2.Disable() + + self._x1.SetBackgroundColour((160, 215, 145)) + self._y1.SetBackgroundColour((160, 215, 145)) + self._z1.SetBackgroundColour((160, 215, 145)) + + self._x2.SetBackgroundColour((150, 150, 215)) + self._y2.SetBackgroundColour((150, 150, 215)) + self._z2.SetBackgroundColour((150, 150, 215)) + + self._canvas().Bind(EVT_BOX_POINT_1_CHANGE, self._green_corner_renderer_change) + self._canvas().Bind(EVT_BOX_POINT_2_CHANGE, self._blue_corner_renderer_change) + self._canvas().Bind(EVT_BOX_EDIT_TOGGLE, self._enable_scrolls) def bind_events(self): self.canvas.Bind(EVT_PASTE, self._paste) @@ -108,33 +120,36 @@ def disable(self): def _add_row(self, label: str, wx_object: Type[wx.Object], **kwargs) -> Any: sizer = wx.BoxSizer(wx.HORIZONTAL) - self.Add(sizer, 0, 0) - name_text = wx.StaticText(self, label=label) + self._button_panel.GetSizer().Add(sizer, 0, 0) + name_text = wx.StaticText(self._button_panel, label=label) sizer.Add(name_text, flag=wx.CENTER | wx.ALL | wx.EXPAND, border=5) - obj = wx_object(self, **kwargs) + obj = wx_object(self._button_panel, **kwargs) sizer.Add(obj, flag=wx.CENTER | wx.ALL, border=5) return obj - # def _green_corner_input_change(self, _): - # self._canvas().active_selection.point1 = [self._x1.GetValue(), self._y1.GetValue(), self._z1.GetValue()] - # - # def _blue_corner_input_change(self, _): - # self._canvas().active_selection.point2 = [self._x2.GetValue(), self._y2.GetValue(), self._z2.GetValue()] - # - # def _green_corner_renderer_change(self, evt): - # self._x1.SetValue(evt.x) - # self._y1.SetValue(evt.y) - # self._z1.SetValue(evt.z) - # - # def _blue_corner_renderer_change(self, evt): - # self._x2.SetValue(evt.x) - # self._y2.SetValue(evt.y) - # self._z2.SetValue(evt.z) - # - # def _enable_scrolls(self, evt): - # self._x1.Enable(evt.enabled) - # self._y1.Enable(evt.enabled) - # self._z1.Enable(evt.enabled) - # self._x2.Enable(evt.enabled) - # self._y2.Enable(evt.enabled) - # self._z2.Enable(evt.enabled) + def _green_corner_input_change(self, _): + self._canvas().active_selection.point1 = [self._x1.GetValue(), self._y1.GetValue(), self._z1.GetValue()] + + def _blue_corner_input_change(self, _): + self._canvas().active_selection.point2 = [self._x2.GetValue(), self._y2.GetValue(), self._z2.GetValue()] + + def _green_corner_renderer_change(self, evt): + x, y, z = evt.location + self._x1.SetValue(x) + self._y1.SetValue(y) + self._z1.SetValue(z) + + def _blue_corner_renderer_change(self, evt): + x, y, z = evt.location + self._x2.SetValue(x) + self._y2.SetValue(y) + self._z2.SetValue(z) + + def _enable_scrolls(self, evt): + enabled = not evt.edit + self._x1.Enable(enabled) + self._y1.Enable(enabled) + self._z1.Enable(enabled) + self._x2.Enable(enabled) + self._y2.Enable(enabled) + self._z2.Enable(enabled) From 19d3c59114d61a67126e2aedb4dfd00c0e8a040d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 9 Jun 2020 16:06:16 +0100 Subject: [PATCH 83/84] Reimplemented the import operations --- .../import_operations/construction.py | 113 +++++++++--------- .../import_operations/mcstructure.py | 112 +++++++++-------- .../import_operations/schematic.py | 111 +++++++++-------- 3 files changed, 176 insertions(+), 160 deletions(-) diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py index 063e6617..4851b967 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/construction.py @@ -1,74 +1,79 @@ -from typing import TYPE_CHECKING +import os import wx +from typing import TYPE_CHECKING + -import os -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog from amulet.api.block import BlockManager +from amulet.api.structure import Structure +from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension -from amulet.api.structure import Structure from amulet.structure_interface.construction import ConstructionFormatWrapper -from amulet.operations.paste import paste + +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationError if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Import Construction') - file_picker = wx.FilePickerCtrl( - dialog, - path=options.get('path', ''), - wildcard="Construction file (*.construction)|*.construction", - style=wx.FLP_USE_TEXTCTRL | wx.FLP_OPEN - ) - dialog.sizer.Add(file_picker, 0, wx.ALL, 5) - dialog.Fit() +class ImportConstruction(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) - if dialog.ShowModal() == wx.ID_OK: - options = { - "path": file_picker.GetPath() - } - return options + options = self._load_options({}) + self._file_picker = wx.FilePickerCtrl( + self, + path=options.get('path', ''), + wildcard="Construction file (*.construction)|*.construction", + style=wx.FLP_USE_TEXTCTRL | wx.FLP_OPEN + ) + self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) + self._add_run_button("Import") + self.Layout() -def import_construction( - world: "World", - dimension: Dimension, - options: dict -) -> Structure: - path = options.get('path', None) - if isinstance(path, str) and path.endswith('.construction') and os.path.isfile(path): - wrapper = ConstructionFormatWrapper(path, 'r') - wrapper.translation_manager = world.translation_manager - wrapper.open() - selection = wrapper.selection + def unload(self): + self._save_options({ + "path": self._file_picker.GetPath() + }) - global_palette = BlockManager() - chunks = {} - for (cx, cz) in wrapper.all_chunk_coords(): - try: - chunks[(cx, cz)] = wrapper.load_chunk(cx, cz, global_palette) - except ChunkLoadError: - pass + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup): + path = self._file_picker.GetPath() + if isinstance(path, str) and path.endswith('.construction') and os.path.isfile(path): + wrapper = ConstructionFormatWrapper(path, 'r') + wrapper.translation_manager = world.translation_manager + wrapper.open() + selection = wrapper.selection - wrapper.close() - return Structure( - chunks, - global_palette, - selection - ) - else: - raise Exception('Please specify a construction file in the options before running.') + global_palette = BlockManager() + chunks = {} + for (cx, cz) in wrapper.all_chunk_coords(): + try: + chunks[(cx, cz)] = wrapper.load_chunk(cx, cz, global_palette) + except ChunkLoadError: + pass + + wrapper.close() + self.canvas.paste( + Structure( + chunks, + global_palette, + selection + ) + ) + else: + raise OperationError('Please specify a construction file in the options before running.') export = { - "v": 1, # a version 1 plugin - "name": "Import Construction", # the name of the plugin - "features": ["src_selection", "wxoptions", "dst_location_absolute"], - "structure_callable_inputs": ["options"], # the inputs to give to the plugin - "structure_callable": import_construction, - "inputs": ["structure", "options"], # the inputs to give to the plugin - "operation": paste, # the actual function to call when running the plugin - "wxoptions": show_ui + "name": "\tImport Construction", # the name of the plugin + "operation": ImportConstruction, # the UI class to display } diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py index 1a08a5bf..789da54c 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/mcstructure.py @@ -1,74 +1,80 @@ -from typing import TYPE_CHECKING import wx - import os -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog +from typing import TYPE_CHECKING + + from amulet.api.block import BlockManager +from amulet.api.structure import Structure +from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension -from amulet.api.structure import Structure from amulet.structure_interface.mcstructure import MCStructureFormatWrapper -from amulet.operations.paste import paste + +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationError if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas + +class ImportMCStructure(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Import Bedrock .mcstructure') - file_picker = wx.FilePickerCtrl( - dialog, - path=options.get('path', ''), - wildcard="Bedrock mcstructure file (*.mcstructure)|*.mcstructure", - style=wx.FLP_USE_TEXTCTRL | wx.FLP_OPEN - ) - dialog.sizer.Add(file_picker, 0, wx.ALL, 5) - dialog.Fit() + options = self._load_options({}) - if dialog.ShowModal() == wx.ID_OK: - options = { - "path": file_picker.GetPath() - } - return options + self._file_picker = wx.FilePickerCtrl( + self, + path=options.get('path', ''), + wildcard="Bedrock mcstructure file (*.mcstructure)|*.mcstructure", + style=wx.FLP_USE_TEXTCTRL | wx.FLP_OPEN + ) + self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) + self._add_run_button("Import") + self.Layout() + def unload(self): + self._save_options({ + "path": self._file_picker.GetPath() + }) -def import_schematic( - world: "World", - dimension: Dimension, - options: dict -) -> Structure: - path = options.get('path', None) - if isinstance(path, str) and path.endswith('.mcstructure') and os.path.isfile(path): - wrapper = MCStructureFormatWrapper(path, 'r') - wrapper.translation_manager = world.translation_manager - wrapper.open() - selection = wrapper.selection + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup): + path = self._file_picker.GetPath() - global_palette = BlockManager() - chunks = {} - for (cx, cz) in wrapper.all_chunk_coords(): - try: - chunks[(cx, cz)] = wrapper.load_chunk(cx, cz, global_palette) - except ChunkLoadError: - pass + if isinstance(path, str) and path.endswith('.mcstructure') and os.path.isfile(path): + wrapper = MCStructureFormatWrapper(path, 'r') + wrapper.translation_manager = world.translation_manager + wrapper.open() + selection = wrapper.selection - wrapper.close() - return Structure( - chunks, - global_palette, - selection - ) - else: - raise Exception('Please specify a mcstructure file in the options before running.') + global_palette = BlockManager() + chunks = {} + for (cx, cz) in wrapper.all_chunk_coords(): + try: + chunks[(cx, cz)] = wrapper.load_chunk(cx, cz, global_palette) + except ChunkLoadError: + pass + + wrapper.close() + self.canvas.paste( + Structure( + chunks, + global_palette, + selection + ) + ) + else: + raise OperationError('Please specify a mcstructure file in the options before running.') export = { - "v": 1, # a version 1 plugin "name": "Import Bedrock .mcstructure", # the name of the plugin - "features": ["src_selection", "wxoptions", "dst_location_absolute"], - "structure_callable_inputs": ["options"], # the inputs to give to the plugin - "structure_callable": import_schematic, - "inputs": ["structure", "options"], # the inputs to give to the plugin - "operation": paste, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": ImportMCStructure, # the UI class to display } diff --git a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py index 7516cc57..b87e5c6b 100644 --- a/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py +++ b/amulet_map_editor/programs/edit/plugins/stock_plugins/import_operations/schematic.py @@ -1,74 +1,79 @@ -from typing import TYPE_CHECKING import wx - import os -from amulet_map_editor.amulet_wx.ui.simple import SimpleDialog +from typing import TYPE_CHECKING + + from amulet.api.block import BlockManager +from amulet.api.structure import Structure +from amulet.api.selection import SelectionGroup from amulet.api.errors import ChunkLoadError from amulet.api.data_types import Dimension -from amulet.api.structure import Structure from amulet.structure_interface.schematic import SchematicFormatWrapper -from amulet.operations.paste import paste + +from amulet_map_editor.programs.edit.plugins.api.simple_operation_panel import SimpleOperationPanel +from amulet_map_editor.programs.edit.plugins.api.errors import OperationError if TYPE_CHECKING: from amulet.api.world import World + from amulet_map_editor.programs.edit.canvas.edit_canvas import EditCanvas -def show_ui(parent, world: "World", options: dict) -> dict: - dialog = SimpleDialog(parent, 'Import Schematic (legacy)') - file_picker = wx.FilePickerCtrl( - dialog, - path=options.get('path', ''), - wildcard="Schematic file (*.schematic)|*.schematic", - style=wx.FLP_USE_TEXTCTRL | wx.FLP_OPEN - ) - dialog.sizer.Add(file_picker, 0, wx.ALL, 5) - dialog.Fit() +class ImportSchematic(SimpleOperationPanel): + def __init__( + self, + parent: wx.Window, + canvas: "EditCanvas", + world: "World", + options_path: str + ): + SimpleOperationPanel.__init__(self, parent, canvas, world, options_path) - if dialog.ShowModal() == wx.ID_OK: - options = { - "path": file_picker.GetPath() - } - return options + options = self._load_options({}) + self._file_picker = wx.FilePickerCtrl( + self, + path=options.get('path', ''), + wildcard="Schematic file (*.schematic)|*.schematic", + style=wx.FLP_USE_TEXTCTRL | wx.FLP_OPEN + ) + self._sizer.Add(self._file_picker, 0, wx.ALL | wx.CENTER, 5) + self._add_run_button("Import") + self.Layout() -def import_schematic( - world: "World", - dimension: Dimension, - options: dict -) -> Structure: - path = options.get('path', None) - if isinstance(path, str) and path.endswith('.schematic') and os.path.isfile(path): - wrapper = SchematicFormatWrapper(path, 'r') - wrapper.translation_manager = world.translation_manager - wrapper.open() - selection = wrapper.selection + def unload(self): + self._save_options({ + "path": self._file_picker.GetPath() + }) - global_palette = BlockManager() - chunks = {} - for (cx, cz) in wrapper.all_chunk_coords(): - try: - chunks[(cx, cz)] = wrapper.load_chunk(cx, cz, global_palette) - except ChunkLoadError: - pass + def _operation(self, world: "World", dimension: Dimension, selection: SelectionGroup): + path = self._file_picker.GetPath() + if isinstance(path, str) and path.endswith('.schematic') and os.path.isfile(path): + wrapper = SchematicFormatWrapper(path, 'r') + wrapper.translation_manager = world.translation_manager + wrapper.open() + selection = wrapper.selection - wrapper.close() - return Structure( - chunks, - global_palette, - selection - ) - else: - raise Exception('Please specify a schematic file in the options before running.') + global_palette = BlockManager() + chunks = {} + for (cx, cz) in wrapper.all_chunk_coords(): + try: + chunks[(cx, cz)] = wrapper.load_chunk(cx, cz, global_palette) + except ChunkLoadError: + pass + + wrapper.close() + self.canvas.paste( + Structure( + chunks, + global_palette, + selection + ) + ) + else: + raise OperationError('Please specify a schematic file in the options before running.') export = { - "v": 1, # a version 1 plugin "name": "Import Schematic (legacy)", # the name of the plugin - "features": ["src_selection", "wxoptions", "dst_location_absolute"], - "structure_callable_inputs": ["options"], # the inputs to give to the plugin - "structure_callable": import_schematic, - "inputs": ["structure", "options"], # the inputs to give to the plugin - "operation": paste, # the actual function to call when running the plugin - "wxoptions": show_ui + "operation": ImportSchematic, # the UI class to display } From 6b8e1a2a8a156fab3299e03d8a540a896d425adc Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 9 Jun 2020 16:07:57 +0100 Subject: [PATCH 84/84] Increased the version number --- amulet_map_editor/version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amulet_map_editor/version b/amulet_map_editor/version index 6620cc43..0310bb2e 100644 --- a/amulet_map_editor/version +++ b/amulet_map_editor/version @@ -1 +1 @@ -0.6.7.0 \ No newline at end of file +0.6.8.0 \ No newline at end of file