From e1facda618ddd52e15641dddbe2e993f215d8599 Mon Sep 17 00:00:00 2001 From: Variable <77773850+Variable-ind@users.noreply.github.com> Date: Tue, 10 Jan 2023 22:26:13 +0500 Subject: [PATCH] Added Class System to ExtensionsAPI (#808) * Added Class System to Api * formatting * removed extends * update extension version * Added Some New Fail-Safes * fix typo * Formatting * Update ExtensionsAPI.gd * Typo * formatting * formatting --- project.godot | 2 +- src/Autoload/ExtensionsAPI.gd | 512 ++++++++++++++++------------ src/Preferences/HandleExtensions.gd | 4 + 3 files changed, 294 insertions(+), 224 deletions(-) diff --git a/project.godot b/project.godot index 455ae8aac..89155ad68 100644 --- a/project.godot +++ b/project.godot @@ -272,7 +272,7 @@ config/macos_native_icon="res://assets/graphics/icons/icon.icns" config/windows_native_icon="res://assets/graphics/icons/icon.ico" config/custom_user_dir_name.X11="pixelorama" config/Version="v0.11-dev" -config/ExtensionsAPI_Version=2 +config/ExtensionsAPI_Version=3 [audio] diff --git a/src/Autoload/ExtensionsAPI.gd b/src/Autoload/ExtensionsAPI.gd index 549524ed5..276764c79 100644 --- a/src/Autoload/ExtensionsAPI.gd +++ b/src/Autoload/ExtensionsAPI.gd @@ -1,247 +1,313 @@ # gdlint: ignore=max-public-methods extends Node -enum { FILE, EDIT, SELECT, IMAGE, VIEW, WINDOW, HELP } +# use these variables in your extension to access the api +var general = GeneralAPI.new() +var menu = MenuAPI.new() +var dialog = DialogAPI.new() +var panel = PanelAPI.new() +var theme = ThemeAPI.new() +var tools = ToolAPI.new() +var project = ProjectAPI.new() + +# This fail-safe below is designed to work ONLY if Pixelorama is launched in Godot Editor +var _action_history: Dictionary = {} -func show_error(text: String) -> void: - # useful for displaying messages like "Incompatible API" etc... - Global.error_dialog.set_text(text) - Global.error_dialog.popup_centered() - Global.dialog_open(true) +func check_sanity(extension_name: String): + if extension_name in _action_history.keys(): + var extension_history = _action_history[extension_name] + if extension_history != []: + var error_msg = str( + "Extension: ", + extension_name, + " contains actons: ", + extension_history, + " which are not removed properly" + ) + print(error_msg) +func clear_history(extension_name: String): + if extension_name in _action_history.keys(): + _action_history.erase(extension_name) + + +func add_action(action: String): + var extension_name = _get_caller_extension_name() + if extension_name != "Unknown": + if extension_name in _action_history.keys(): + var extension_history: Array = _action_history[extension_name] + extension_history.append(action) + else: # If the extension history does'nt exist yet then creat it + _action_history[extension_name] = [action] + + +func remove_action(action: String): + var extension_name = _get_caller_extension_name() + if extension_name != "Unknown": + if extension_name in _action_history.keys(): + _action_history[extension_name].erase(action) + + +func _get_caller_extension_name() -> String: + var stack = get_stack() + for trace in stack: + # Get extension name that called the action + var arr: Array = trace["source"].split("/") + var idx = arr.find("Extensions") + if idx != -1: + return _arr[idx + 1] + return "Unknown" + + +func _exit_tree(): + for keys in _action_history.keys(): + check_sanity(keys) + + +# The Api Methods Start Here func get_api_version() -> int: return ProjectSettings.get_setting("application/config/ExtensionsAPI_Version") -func get_pixelorama_version() -> String: - return ProjectSettings.get_setting("application/config/Version") +class GeneralAPI: + # Version And Config + func get_pixelorama_version() -> String: + return ProjectSettings.get_setting("application/config/Version") + + func get_config_file() -> ConfigFile: + return Global.config_cache + + # Nodes + func get_global() -> Global: + return Global + + func get_extensions_node() -> Node: + return Global.control.get_node("Extensions") + + func get_canvas() -> Canvas: + return Global.canvas -func dialog_open(open: bool) -> void: - Global.dialog_open(open) +class MenuAPI: + enum { FILE, EDIT, SELECT, IMAGE, VIEW, WINDOW, HELP } + + # Menu methods + func _get_popup_menu(menu_type: int) -> PopupMenu: + match menu_type: + FILE: + return Global.top_menu_container.file_menu_button.get_popup() + EDIT: + return Global.top_menu_container.edit_menu_button.get_popup() + SELECT: + return Global.top_menu_container.select_menu_button.get_popup() + IMAGE: + return Global.top_menu_container.image_menu_button.get_popup() + VIEW: + return Global.top_menu_container.view_menu_button.get_popup() + WINDOW: + return Global.top_menu_container.window_menu_button.get_popup() + HELP: + return Global.top_menu_container.help_menu_button.get_popup() + return null + + func add_menu_item(menu_type: int, item_name: String, item_metadata, item_id := -1) -> int: + # item_metadata is usually a popup node you want to appear when you click the item_name + # that popup should also have an (menu_item_clicked) function inside it's script + var popup_menu: PopupMenu = _get_popup_menu(menu_type) + if not popup_menu: + return -1 + popup_menu.add_item(item_name, item_id) + var idx := item_id + if item_id == -1: + idx = popup_menu.get_item_count() - 1 + popup_menu.set_item_metadata(idx, item_metadata) + ExtensionsApi.add_action("add_menu") + return idx + + func remove_menu_item(menu_type: int, item_idx: int) -> void: + var popup_menu: PopupMenu = _get_popup_menu(menu_type) + if not popup_menu: + return + popup_menu.remove_item(item_idx) + ExtensionsApi.remove_action("add_menu") -func get_current_project() -> Project: - return Global.current_project +class DialogAPI: + func show_error(text: String) -> void: + # useful for displaying messages like "Incompatible API" etc... + Global.error_dialog.set_text(text) + Global.error_dialog.popup_centered() + Global.dialog_open(true) + + func get_dialogs_parent_node() -> Node: + return Global.control.get_node("Dialogs") + + func dialog_open(open: bool) -> void: + Global.dialog_open(open) -func get_current_cel_info() -> Dictionary: - # As types of cel are added to Pixelorama, - # then the old extension would have no idea how to identify the types they use - # E.g the extension may try to use a GroupCel as a PixelCel (if it doesn't know the difference) - # So it's encouraged to use this function to access cels - var project = get_current_project() - var cel = project.get_current_cel() - # Add cel types as we have more and more cels - if cel is PixelCel: - return {"cel": cel, "type": "PixelCel"} - elif cel is GroupCel: - return {"cel": cel, "type": "GroupCel"} - else: - return {"cel": cel, "type": "BaseCel"} +class PanelAPI: + func set_tabs_visible(visible: bool) -> void: + var dockable := _get_dockable_container_ui() + dockable.set_tabs_visible(visible) + + func get_tabs_visible() -> bool: + var dockable := _get_dockable_container_ui() + return dockable.get_tabs_visible() + + func add_node_as_tab(node: Node, alongside_node: String) -> void: + var dockable := _get_dockable_container_ui() + dockable.add_child(node) + var tab = _find_tab_with_node(alongside_node, dockable) + if not tab: + push_error("Tab not found") + return + tab.insert_node(0, node) # Insert at the beginning + ExtensionsApi.add_action("add_tab") + # INSTRUCTION + # After this check if tabs are invisible, if they are, then make tabs visible + # and after doing yield(get_tree(), "idle_frame") twice make them invisible again + + func remove_node_from_tab(node: Node) -> void: + if node == null: + return + var dockable = Global.control.find_node("DockableContainer") + var tab = _find_tab_with_node(node.name, dockable) + if not tab: + push_error("Tab not found") + return + tab.remove_node(node) + node.get_parent().remove_child(node) + node.queue_free() + ExtensionsApi.remove_action("add_tab") + + # PRIVATE METHODS + func _get_dockable_container_ui() -> Node: + return Global.control.find_node("DockableContainer") + + func _find_tab_with_node(node_name: String, dockable_container): + var root = dockable_container.layout.root + var tabs = _get_tabs_in_root(root) + for tab in tabs: + var idx = tab.find_name(node_name) + if idx != -1: + return tab + return null + + func _get_tabs_in_root(parent_resource): + var parents := [] # Resources have no get_parent_resource() so this is an alternative + var scanned := [] # To keep track of already discovered layout_split resources + var child_number := 0 + parents.append(parent_resource) + var scan_target = parent_resource + var tabs := [] + # Get children in the parent, the initial parent is the node we entered as "parent" + while child_number < 2: + # If parent isn't a (layout_split) resource then there is no point + # in continuing (This is just a Sanity Check and should always pass) + if !scan_target.has_method("get_first"): + break + + var child_resource + if child_number == 0: + child_resource = scan_target.get_first() # First child + elif child_number == 1: + child_resource = scan_target.get_second() # Second child + + # If the child resource is a tab and it wasn't discovered before, add it to "paths" + if child_resource.has_method("get_current_tab"): + if !tabs.has(child_resource): + tabs.append(child_resource) + # If "child_resource" is another layout_split resource then we need to scan it too + elif child_resource.has_method("get_first") and !scanned.has(child_resource): + scanned.append(child_resource) + parents.append(child_resource) + scan_target = parents[-1] # Set this as the next scan target + # Reset child_number by setting it to -1, because later it will + # get added by "child_number += 1" to make it 0 + child_number = -1 + child_number += 1 + # If we have reached the bottom, then make the child's parent as + # the next parent and move on to the next child in the parent + if child_number == 2: + scan_target = parents.pop_back() + child_number = 0 + # If there is no parent left to get scanned + if scan_target == null: + return tabs -func get_global() -> Global: - return Global +class ThemeAPI: + func add_theme(theme: Theme) -> void: + var themes: BoxContainer = Global.preferences_dialog.find_node("Themes") + themes.themes.append(theme) + themes.add_theme(theme) + ExtensionsApi.add_action("add_theme") + + func find_theme_index(theme: Theme) -> int: + var themes: BoxContainer = Global.preferences_dialog.find_node("Themes") + return themes.themes.find(theme) + + func get_theme() -> Theme: + return Global.control.theme + + func set_theme(idx: int) -> bool: + var themes: BoxContainer = Global.preferences_dialog.find_node("Themes") + if idx >= 0 and idx < themes.themes.size(): + themes.buttons_container.get_child(idx).emit_signal("pressed") + return true + else: + return false + + func remove_theme(theme: Theme) -> void: + Global.preferences_dialog.themes.remove_theme(theme) + ExtensionsApi.remove_action("add_theme") -func get_extensions_node() -> Node: - return Global.control.get_node("Extensions") +class ToolAPI: + # Tool methods + func add_tool( + tool_name: String, + display_name: String, + shortcut: String, + scene: PackedScene, + extra_hint := "", + extra_shortucts := [] + ) -> void: + var tool_class := Tools.Tool.new( + tool_name, display_name, shortcut, scene, extra_hint, extra_shortucts + ) + Tools.tools[tool_name] = tool_class + Tools.add_tool_button(tool_class) + ExtensionsApi.add_action("add_tool") + + func remove_tool(tool_name: String) -> void: + # Re-assigning the tools in case the tool to be removed is also Active + Tools.assign_tool("Pencil", BUTTON_LEFT) + Tools.assign_tool("Eraser", BUTTON_RIGHT) + var tool_class: Tools.Tool = Tools.tools[tool_name] + if tool_class: + Tools.remove_tool(tool_class) + ExtensionsApi.remove_action("add_tool") -func get_dockable_container_ui() -> Node: - return Global.control.find_node("DockableContainer") +class ProjectAPI: + func get_current_project() -> Project: + return Global.current_project - -func get_config_file() -> ConfigFile: - return Global.config_cache - - -func get_canvas() -> Canvas: - return Global.canvas - - -func get_dialogs_parent_node() -> Node: - return Global.control.get_node("Dialogs") - - -# Dockable container methods -# Adds a node as a tab next to an already existing panel to the dockable container -func add_node_as_tab(node: Node, alongside_node: String) -> void: - var dockable := get_dockable_container_ui() - dockable.add_child(node) - var tab = _find_tab_with_node(alongside_node, dockable) - if not tab: - push_error("Tab not found") - return - - tab.insert_node(0, node) # Insert at the beginning - if !dockable.get_tabs_visible(): - dockable.set_tabs_visible(true) - # A hacky way to fix tabs that sometimes are not visible when an extension is loaded - yield(get_tree(), "idle_frame") - yield(get_tree(), "idle_frame") - dockable.set_tabs_visible(false) - - -# Removes a node from the dockable container -func remove_node_from_tab(node: Node) -> void: - var dockable = Global.control.find_node("DockableContainer") - var tab = _find_tab_with_node(node.name, dockable) - if not tab: - push_error("Tab not found") - return - tab.remove_node(node) - node.get_parent().remove_child(node) - node.queue_free() - - -func _find_tab_with_node(node_name: String, dockable_container): - var root = dockable_container.layout.root - var tabs = _get_tabs_in_root(root) - for tab in tabs: - var idx = tab.find_name(node_name) - if idx != -1: - return tab - return null - - -# Returns all existing tabs inside Split resource -func _get_tabs_in_root(parent_resource): - var parents := [] # Resources have no get_parent_resource() so this is an alternative - var scanned := [] # To keep track of already discovered layout_split resources - var child_number := 0 - parents.append(parent_resource) - var scan_target = parent_resource - - var tabs := [] - - # Get children in the parent, the initial parent is the node we entered as "parent" - while child_number < 2: - # If parent isn't a (layout_split) resource then there is no point - # in continuing (This is just a Sanity Check and should always pass) - if !scan_target.has_method("get_first"): - break - - var child_resource - if child_number == 0: - child_resource = scan_target.get_first() # First child - elif child_number == 1: - child_resource = scan_target.get_second() # Second child - - # If the child resource is a tab and it wasn't discovered before, add it to "paths" - if child_resource.has_method("get_current_tab"): - if !tabs.has(child_resource): - tabs.append(child_resource) - # If "child_resource" is another layout_split resource then we need to scan it too - elif child_resource.has_method("get_first") and !scanned.has(child_resource): - scanned.append(child_resource) - parents.append(child_resource) - scan_target = parents[-1] # Set this as the next scan target - # Reset child_number by setting it to -1, because later it will - # get added by "child_number += 1" to make it 0 - child_number = -1 - child_number += 1 - # If we have reached the bottom, then make the child's parent as - # the next parent and move on to the next child in the parent - if child_number == 2: - scan_target = parents.pop_back() - child_number = 0 - # If there is no parent left to get scanned - if scan_target == null: - return tabs - - -# Menu methods -func _get_popup_menu(menu_type: int) -> PopupMenu: - match menu_type: - FILE: - return Global.top_menu_container.file_menu_button.get_popup() - EDIT: - return Global.top_menu_container.edit_menu_button.get_popup() - SELECT: - return Global.top_menu_container.select_menu_button.get_popup() - IMAGE: - return Global.top_menu_container.image_menu_button.get_popup() - VIEW: - return Global.top_menu_container.view_menu_button.get_popup() - WINDOW: - return Global.top_menu_container.window_menu_button.get_popup() - HELP: - return Global.top_menu_container.help_menu_button.get_popup() - return null - - -func add_menu_item(menu_type: int, item_name: String, item_metadata, item_id := -1) -> int: - var popup_menu: PopupMenu = _get_popup_menu(menu_type) - if not popup_menu: - return -1 - popup_menu.add_item(item_name, item_id) - var idx := item_id - if item_id == -1: - idx = popup_menu.get_item_count() - 1 - popup_menu.set_item_metadata(idx, item_metadata) - return idx - - -func remove_menu_item(menu_type: int, item_idx: int) -> void: - var popup_menu: PopupMenu = _get_popup_menu(menu_type) - if not popup_menu: - return - popup_menu.remove_item(item_idx) - - -# Tool methods -func add_tool( - tool_name: String, - display_name: String, - shortcut: String, - scene: PackedScene, - extra_hint := "", - extra_shortucts := [] -) -> void: - var tool_class := Tools.Tool.new( - tool_name, display_name, shortcut, scene, extra_hint, extra_shortucts - ) - Tools.tools[tool_name] = tool_class - Tools.add_tool_button(tool_class) - - -func remove_tool(tool_name: String) -> void: - # Re-assigning the tools in case the tool to be removed is also Active - Tools.assign_tool("Pencil", BUTTON_LEFT) - Tools.assign_tool("Eraser", BUTTON_RIGHT) - var tool_class: Tools.Tool = Tools.tools[tool_name] - if tool_class: - Tools.remove_tool(tool_class) - - -# Theme methods -func add_theme(theme: Theme) -> void: - var themes: BoxContainer = Global.preferences_dialog.find_node("Themes") - themes.themes.append(theme) - themes.add_theme(theme) - - -func find_theme_index(theme: Theme) -> int: - var themes: BoxContainer = Global.preferences_dialog.find_node("Themes") - return themes.themes.find(theme) - - -func get_theme() -> Theme: - return Global.control.theme - - -func set_theme(idx: int) -> bool: - var themes: BoxContainer = Global.preferences_dialog.find_node("Themes") - if idx >= 0 and idx < themes.themes.size(): - themes.buttons_container.get_child(idx).emit_signal("pressed") - return true - else: - return false - - -func remove_theme(theme: Theme) -> void: - Global.preferences_dialog.themes.remove_theme(theme) + func get_current_cel_info() -> Dictionary: + # As types of cel are added to Pixelorama, + # then the old extension would have no idea how to identify the types they use + # E.g the extension may try to use a GroupCel as a PixelCel (if it doesn't know the difference) + # So it's encouraged to use this function to access cels + var project = get_current_project() + var cel = project.get_current_cel() + # Add cel types as we have more and more cels + if cel is PixelCel: + return {"cel": cel, "type": "PixelCel"} + elif cel is GroupCel: + return {"cel": cel, "type": "GroupCel"} + else: + return {"cel": cel, "type": "BaseCel"} diff --git a/src/Preferences/HandleExtensions.gd b/src/Preferences/HandleExtensions.gd index 981759944..fb787dbee 100644 --- a/src/Preferences/HandleExtensions.gd +++ b/src/Preferences/HandleExtensions.gd @@ -165,6 +165,8 @@ func _add_extension(file_name: String) -> void: Global.error_dialog.popup_centered() Global.dialog_open(true) print("Incompatible API") + # Don't put it in faulty, (it's merely incompatible) + remover_directory.remove(EXTENSIONS_PATH.plus_file("Faulty.txt")) return var extension := Extension.new() @@ -192,6 +194,7 @@ func _enable_extension(extension: Extension, save_to_config := true) -> void: var id: String = extension.file_name if extension.enabled: + ExtensionsApi.clear_history(extension.file_name) for node in extension.nodes: var scene_path: String = extension_path.plus_file(node) var extension_scene: PackedScene = load(scene_path) @@ -206,6 +209,7 @@ func _enable_extension(extension: Extension, save_to_config := true) -> void: if ext_node.is_in_group(id): # Node for extention found extension_parent.remove_child(ext_node) ext_node.queue_free() + ExtensionsApi.check_sanity(extension.file_name) if save_to_config: Global.config_cache.set_value("extensions", extension.file_name, extension.enabled)