diff --git a/README.md b/README.md index 0d90bcc..4366fe0 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,8 @@ Group them into categories, add descriptions or web links. The data is stored inside a json file. This file is automatically added to the export, you do not need to add it yourself. If you provide license files instead of a text, they are also exported. +If you add paths to license data, it will be automatically adjusted if you rename a file or folder inside the editor. + You can change the project license file either with a button at the upper right, in the license menu. Or inside the project settings under the menu `Plugins` -> `Licenses`. ### Compatibility @@ -245,6 +247,10 @@ License class. ### Changelog +#### 1.7.0 + +- Add renaming of paths when a file or folder gets renamed inside the editor. + #### 1.6.1 - Workaround show engine components, calling static function is bugged diff --git a/addons/licenses/component.gd b/addons/licenses/component.gd index 02032ca..fd1543e 100644 --- a/addons/licenses/component.gd +++ b/addons/licenses/component.gd @@ -59,6 +59,16 @@ class License: self.web = data.get("web", "") return self + func duplicate(): + var dup = new() + dup.name = self.name + dup.identifier = self.identifier + dup.text = self.text + dup.file = self.file + dup.web = self.web + + return dup + ## Identifier var id: String ## Use to structure the licenses to top categories. E.g. Textures, Fonts, ... @@ -145,3 +155,18 @@ func deserialize(data: Dictionary): for license: Dictionary in data.get("licenses", []): self.licenses.append(License.new().deserialize(license)) return self + +func duplicate(): + var dup = new() + dup.id = self.id + dup.category = self.category + dup.name = self.name + dup.version = self.version + dup.copyright = self.copyright.duplicate() + dup.contact = self.contact + dup.web = self.web + dup.paths = self.paths.duplicate() + for license in self.licenses: + dup.licenses.append(license.duplicate()) + + return dup diff --git a/addons/licenses/internal/components_container.gd b/addons/licenses/internal/components_container.gd new file mode 100644 index 0000000..6a0af4f --- /dev/null +++ b/addons/licenses/internal/components_container.gd @@ -0,0 +1,39 @@ +extends Node + +const Component := preload("../component.gd") +const Licenses := preload("../licenses.gd") + +signal components_changed() + +var _components: Array[Component] = [] + +func emit_changed() -> void: + self.components_changed.emit() + +func add_component(component: Component) -> void: + self._components.append(component) + self._components.sort_custom(Licenses.compare_components_ascending) + +func set_components(components_: Array[Component]) -> void: + self._components = components_ + +func components() -> Array[Component]: + return self._components + +func get_at(idx: int) -> Component: + return self._components[idx] + +func remove_at(idx: int) -> void: + self._components.remove_at(idx) + +func remove_component(component: Component) -> void: + var idx: int = self._components.find(component) + if idx == -1: + return + self.remove_at(idx) + +func sort_custom(fn: Callable) -> void: + self._components.sort_custom(fn) + +func count() -> int: + return len(self._components) diff --git a/addons/licenses/internal/components_tree.gd b/addons/licenses/internal/components_tree.gd index f9f08e4..5c899ee 100644 --- a/addons/licenses/internal/components_tree.gd +++ b/addons/licenses/internal/components_tree.gd @@ -2,24 +2,18 @@ extends Tree const Component := preload("../component.gd") -const ComponentDetailTree := preload("component_detail_tree.gd") -const BaseHandler := preload("handler/base.gd") -const ObjectHandler := preload("handler/object.gd") -const ArrayHandler := preload("handler/array.gd") -const StringHandler := preload("handler/string.gd") -const StringFileHandler := preload("handler/string_file.gd") -const StringMultiLineHandler := preload("handler/string_multiline.gd") +const ComponentsContainer := preload("components_container.gd") const Licenses := preload("../licenses.gd") const _BTN_ID_REMOVE: int = 2 -signal components_changed() - -@export_node_path("Tree") var _component_detail_path; @onready var _component_detail: ComponentDetailTree = self.get_node(_component_detail_path) +signal component_selected(comp: Component) +signal component_remove(comp: Component) +signal component_add(comp: Component) var show_readonly_components: bool = false: set = set_show_readonly_components -var _components: Array[Component] = [] +var _components: ComponentsContainer ## cached value var _readonly_components: Array[Component] = [] @@ -29,32 +23,14 @@ func set_show_readonly_components(show: bool) -> void: self._readonly_components = Licenses.get_required_engine_components() else: self._readonly_components = [] - -func _ready() -> void: - self._component_detail.component_edited.connect(self._on_component_edited) - self._component_detail.handlers = [ObjectHandler, StringFileHandler, StringMultiLineHandler, StringHandler, ArrayHandler] - -func add_component(component: Component) -> void: - self._components.append(component) - self._components.sort_custom(Licenses.compare_components_ascending) - self.reload() - -# will not emit components_changed signal -func set_components(components_: Array[Component]) -> void: - self._components = components_ self.reload() -func get_components() -> Array[Component]: - return self._components - -# also reloads the view -# do not use internally -func select_component(component: Component) -> void: - self._component_detail.set_component(component) - self.reload() +func _ready() -> void: + self.allow_rmb_select = true -func get_selected_component() -> Component: - return self._component_detail.get_component() +# only used once +func set_components(comp: ComponentsContainer) -> void: + self._components = comp func _create_category_item(category_cache: Dictionary, category: String, root: TreeItem) -> TreeItem: if category in category_cache: @@ -82,11 +58,7 @@ func _add_tree_item(component: Component, idx: int, parent: TreeItem) -> TreeIte item.set_tooltip_text(0, tooltip) return item -func _add_component(component: Component, category_cache: Dictionary, root: TreeItem, idx: int) -> TreeItem: - var parent: TreeItem = self._create_category_item(category_cache, component.category, root) - return self._add_tree_item(component, idx, parent) - -func reload() -> void: +func reload(scroll_to: Component = null) -> void: self.clear() if self.get_root() != null: push_error("could not clear") @@ -99,23 +71,25 @@ func reload() -> void: # count current added custom components var idx: int = 0 - while idx < len(self._components) or readonly_idx < len(self._readonly_components): + while idx < self._components.count() or readonly_idx < len(self._readonly_components): var component: Component = null var cur_idx: int = 0 var cmp: bool = false - if idx < len(self._components) and readonly_idx < len(self._readonly_components): - cmp = Licenses.compare_components_ascending(self._components[idx], self._readonly_components[readonly_idx]) + # compare readonly items with editable, to determine which one to show first + if idx < self._components.count() and readonly_idx < len(self._readonly_components): + cmp = Licenses.compare_components_ascending(self._components.get_at(idx), self._readonly_components[readonly_idx]) if readonly_idx >= len(self._readonly_components) or cmp: - component = self._components[idx] + component = self._components.get_at(idx) cur_idx = idx idx = idx + 1 - elif idx >= len(self._components) or not cmp: + elif idx >= self._components.count() or not cmp: component = self._readonly_components[readonly_idx] cur_idx = readonly_idx readonly_idx = readonly_idx + 1 - var item: TreeItem = self._add_component(component, category_cache, root, cur_idx) - if component == self._component_detail.get_component(): + var parent: TreeItem = self._create_category_item(category_cache, component.category, root) + var item: TreeItem = self._add_tree_item(component, cur_idx, parent) + if scroll_to != null && component == scroll_to: self.scroll_to_item(item) item.select(0) @@ -127,7 +101,7 @@ func _get_drag_data(at_position: Vector2) -> Variant: var item: TreeItem = self.get_item_at_position(at_position) if item == null: return null - if not item.has_meta("idx") or item.get_meta("readonly"): + if not item.has_meta("idx") or (item.get_meta("readonly") as bool): return null self.set_drop_mode_flags(Tree.DROP_MODE_INBETWEEN) @@ -145,61 +119,41 @@ func _drop_data(at_position: Vector2, data: Variant) -> void: var category: String = "" # below or above item with category if to_item.has_meta("idx"): - category = self._components[to_item.get_meta("idx")].category + category = self._components.get_at(to_item.get_meta("idx") as int).category # below category node elif shift == 1: category = to_item.get_text(0) - var cur_component: Component = self._components[data.get_meta("idx")] + var cur_component: Component = self._components.get_at(data.get_meta("idx") as int) if cur_component.category == category: return cur_component.category = category self._components.sort_custom(Licenses.compare_components_ascending) - self.components_changed.emit() - self.reload() - self._component_detail.reload() + self._components.emit_changed() func _on_item_selected() -> void: var item: TreeItem = self.get_selected() if not item.has_meta("readonly"): return var component: Component - if item.get_meta("readonly"): + if item.get_meta("readonly") as bool: component = self._readonly_components[item.get_meta("idx")] else: - component = self._components[item.get_meta("idx")] - self._component_detail.set_component(component) + component = self._components.get_at(item.get_meta("idx") as int) + self.component_selected.emit(component) func _on_button_clicked(item: TreeItem, column: int, id: int, mouse_button_idx: int) -> void: match id: _BTN_ID_REMOVE: - var comp: Component = self._components[item.get_meta("idx")] - self._components.remove_at(item.get_meta("idx")) - self._components.sort_custom(Licenses.compare_components_ascending) - self.components_changed.emit() - # refresh detail view if the current component was removed - if comp == self._component_detail.get_component(): - self._component_detail.set_component(null) - self.reload() - -# callback from commponent detail tree -func _on_component_edited(component: Component) -> void: - self._components.sort_custom(Licenses.compare_components_ascending) - self.components_changed.emit() - self.reload() + self.component_remove.emit(self._components.get_at(item.get_meta("idx") as int)) func _on_item_edited() -> void: var category_item: TreeItem = self.get_edited() var old_category: String = category_item.get_meta("category") var new_category: String = category_item.get_text(0) category_item.set_meta("category", new_category) - for component: Component in self._components: + for component: Component in self._components.components(): if component.category == old_category: component.category = new_category self._components.sort_custom(Licenses.compare_components_ascending) - self.components_changed.emit() - # we cannot reload the tree while it is processing any kind of input/signals - # https://github.com/godotengine/godot/issues/50084 - call_deferred("reload") - # reload detail view as the category can be changed - self._component_detail.reload() + self._components.emit_changed() diff --git a/addons/licenses/internal/file_system_watcher.gd b/addons/licenses/internal/file_system_watcher.gd new file mode 100644 index 0000000..2421615 --- /dev/null +++ b/addons/licenses/internal/file_system_watcher.gd @@ -0,0 +1,36 @@ +extends RefCounted + +const ComponentsContainer := preload("components_container.gd") + +var _components: ComponentsContainer + +func _init(components: ComponentsContainer): + self._components = components + var fs_dock: FileSystemDock = EditorInterface.get_file_system_dock() + fs_dock.files_moved.connect(self._on_file_moved) + fs_dock.folder_moved.connect(self._on_folder_moved) + +func _on_file_moved(old_file: String, new_file: String) -> void: + var changed: bool = false + for comp in self._components.components(): + for idx in range(comp.paths.size()): + var path: String = comp.paths[idx] + if path == old_file: + changed = true + comp.paths[idx] = new_file + if changed: + self._components.emit_changed() + +func _on_folder_moved(old_folder: String, new_folder: String) -> void: + var old_folder_no_slash: String = old_folder.rstrip("/") + var changed: bool = false + for comp in self._components.components(): + for idx in range(comp.paths.size()): + var path: String = comp.paths[idx] + if path == old_folder_no_slash: + changed = true + comp.paths[idx] = new_folder.rstrip("/") + if path.begins_with(old_folder): + comp.paths[idx] = new_folder.rstrip("/") + "/" + comp.paths[idx].trim_prefix(old_folder) + if changed: + self._components.emit_changed() diff --git a/addons/licenses/internal/licenses.gd b/addons/licenses/internal/licenses.gd index fcd1331..86e451a 100644 --- a/addons/licenses/internal/licenses.gd +++ b/addons/licenses/internal/licenses.gd @@ -2,13 +2,34 @@ extends MarginContainer const Licenses := preload("../licenses.gd") +const Component := preload("../component.gd") const ComponentsTree := preload("components_tree.gd") +const ComponentDetailTree := preload("component_detail_tree.gd") +const Toolbar := preload("toolbar.gd") +const ComponentsContainer := preload("components_container.gd") +const FileSystemWatcher := preload("file_system_watcher.gd") +# handler +const BaseHandler := preload("handler/base.gd") +const ObjectHandler := preload("handler/object.gd") +const ArrayHandler := preload("handler/array.gd") +const StringHandler := preload("handler/string.gd") +const StringFileHandler := preload("handler/string_file.gd") +const StringMultiLineHandler := preload("handler/string_multiline.gd") -@export_node_path("Tree") var _components_tree_path; @onready var _components_tree: ComponentsTree = self.get_node(self._components_tree_path) +@export_node_path("Tree") var _components_tree_path +@onready var _components_tree: ComponentsTree = self.get_node(self._components_tree_path) +@export_node_path("Tree") var _component_detail_tree_path +@onready var _component_detail_tree: ComponentDetailTree = self.get_node(self._component_detail_tree_path) +@export_node_path("HBoxContainer") var _toolbar_path +@onready var _toolbar: Toolbar = self.get_node(self._toolbar_path) +var _item_menu: PopupMenu @export var _license_file_edit: LineEdit = null @export var _license_file_load_button: Button = null @export var _set_license_filepath_button: Button = null +var _components: ComponentsContainer = ComponentsContainer.new() +var _file_watcher: FileSystemWatcher + func _ready() -> void: self._license_file_load_button.icon = self.get_theme_icon("Load", "EditorIcons") self._license_file_load_button.pressed.connect(self._on_data_file_load_button_clicked) @@ -16,7 +37,26 @@ func _ready() -> void: self._set_license_filepath_button.pressed.connect(self._on_set_license_filepath_clicked) self._license_file_edit.text_submitted.connect(self._on_data_file_edit_changed) self._license_file_edit.text = Licenses.get_license_data_filepath() + + self._create_item_menu() + self._components_tree.gui_input.connect(self._on_gui_input) + self.add_child(self._components) + + self._components_tree.set_components(self._components) + self._components_tree.component_selected.connect(self._on_component_tree_selected) + self._components_tree.component_remove.connect(self._on_component_tree_remove) + self._components_tree.component_add.connect(self._on_component_tree_add) + + self._component_detail_tree.component_edited.connect(self._on_component_detail_edited) + self._component_detail_tree.handlers = [ObjectHandler, StringFileHandler, StringMultiLineHandler, StringHandler, ArrayHandler] + + self._toolbar.add_component.connect(self._on_toolbar_add_component) + self._toolbar.show_engine_components.connect(self._on_toolbar_show_engine_components) + self.reload() + self._components.components_changed.connect(self._on_components_changed) + if Engine.is_editor_hint(): + self._file_watcher = FileSystemWatcher.new(self._components) func reload() -> void: self._update_set_license_filepath_button() @@ -27,8 +67,19 @@ func reload() -> void: else: self._license_file_edit.right_icon = self.get_theme_icon("NodeWarning", "EditorIcons") self._license_file_edit.tooltip_text = res.err_msg - res.components.sort_custom(Licenses.compare_components_ascending) - self._components_tree.set_components(res.components) + + self._components.set_components(res.components) + self._components.sort_custom(Licenses.compare_components_ascending) + self._components.emit_changed() + +func _create_item_menu() -> void: + self._item_menu = PopupMenu.new() + self._item_menu.name = "item_menu" + self._item_menu.id_pressed.connect(self._on_item_menu_pressed) + self._item_menu.add_icon_item(self.get_theme_icon("Duplicate", "EditorIcons"), "Duplicate") + self._item_menu.add_icon_item(self.get_theme_icon("Remove", "EditorIcons"), "Delete") + self._item_menu.size = self._item_menu.get_contents_minimum_size() + self.add_child(self._item_menu) func _update_set_license_filepath_button() -> void: if Licenses.get_license_data_filepath() == self._license_file_edit.text: @@ -60,5 +111,58 @@ func _on_set_license_filepath_clicked() -> void: Licenses.set_license_data_filepath(self._license_file_edit.text) self._update_set_license_filepath_button() +func _on_component_tree_selected(comp: Component) -> void: + self._component_detail_tree.set_component(comp) + +func _on_component_tree_add(comp: Component) -> void: + self._components.add_component(comp) + self._component_detail_tree.set_component(comp) + self._components.emit_changed() + +func _on_component_tree_remove(comp: Component) -> void: + self._components.remove_component(comp) + self._components.sort_custom(Licenses.compare_components_ascending) + # refresh detail view if the current component was removed + if comp == self._component_detail_tree.get_component(): + self._component_detail_tree.set_component(null) + self._components.emit_changed() + +func _on_toolbar_add_component(comp: Component) -> void: + self._on_component_tree_add(comp) + +func _on_toolbar_show_engine_components(show: bool) -> void: + self._components_tree.show_readonly_components = show + +# callback from component detail tree +func _on_component_detail_edited(component: Component) -> void: + self._components.sort_custom(Licenses.compare_components_ascending) + # we cannot reload the tree while it is processing any kind of input/signals + # https://github.com/godotengine/godot/issues/50084 + call_deferred("_emit_changed") + func _on_components_changed() -> void: - Licenses.save(self._components_tree.get_components(), self._license_file_edit.text) + Licenses.save(self._components.components(), self._license_file_edit.text) + self._component_detail_tree.reload() + self._components_tree.reload(self._component_detail_tree.get_component()) + +func _on_gui_input(event: InputEvent) -> void: + if !(event is InputEventMouseButton) || !event.pressed || event.button_index != MOUSE_BUTTON_RIGHT: + return + var item: TreeItem = self._components_tree.get_item_at_position(event.position) + if item == null || !item.has_meta("idx") || !item.has_meta("readonly") || (item.get_meta("readonly") as bool): + return + + self._item_menu.popup_on_parent(Rect2(self.get_local_mouse_position(), self._item_menu.size)) + +func _on_item_menu_pressed(btn_idx: int) -> void: + var idx: int = self._components_tree.get_selected().get_meta("idx") as int + match btn_idx: + 0: + var new_comp: Component = self._components.get_at(idx).duplicate() + new_comp.name = new_comp.name + " Copy" + self._on_component_tree_add(new_comp) + 1: + self._on_component_tree_remove(self._components.get_at(idx)) + +func _emit_changed(): + self._components.emit_changed() diff --git a/addons/licenses/internal/licenses.tscn b/addons/licenses/internal/licenses.tscn index b625bbf..5f1401d 100644 --- a/addons/licenses/internal/licenses.tscn +++ b/addons/licenses/internal/licenses.tscn @@ -21,6 +21,8 @@ theme_override_constants/margin_right = 4 theme_override_constants/margin_bottom = 4 script = ExtResource("3") _components_tree_path = NodePath("VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components") +_component_detail_tree_path = NodePath("VBoxContainer/HSplitContainer/component_detail") +_toolbar_path = NodePath("VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/toolbar") _license_file_edit = NodePath("VBoxContainer/license_file_config/edit") _license_file_load_button = NodePath("VBoxContainer/license_file_config/file_button") _set_license_filepath_button = NodePath("VBoxContainer/license_file_config/set_license_filepath_button") @@ -63,7 +65,6 @@ layout_mode = 2 script = ExtResource("2_a8iu8") _menu_button = NodePath("menu") _add_button = NodePath("add") -_components_tree_path = NodePath("../components") [node name="menu" type="Button" parent="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/toolbar"] layout_mode = 2 @@ -88,7 +89,6 @@ theme_override_styles/panel = SubResource("StyleBoxEmpty_5cx7n") hide_root = true scroll_horizontal_enabled = false script = ExtResource("2") -_component_detail_path = NodePath("../../../component_detail") [node name="component_detail" type="Tree" parent="VBoxContainer/HSplitContainer"] layout_mode = 2 @@ -99,7 +99,6 @@ hide_root = true script = ExtResource("1") [connection signal="button_clicked" from="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" to="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" method="_on_button_clicked"] -[connection signal="components_changed" from="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" to="." method="_on_components_changed"] [connection signal="item_edited" from="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" to="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" method="_on_item_edited"] [connection signal="item_selected" from="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" to="VBoxContainer/HSplitContainer/PanelContainer/VBoxContainer/components" method="_on_item_selected"] [connection signal="button_clicked" from="VBoxContainer/HSplitContainer/component_detail" to="VBoxContainer/HSplitContainer/component_detail" method="_on_button_clicked"] diff --git a/addons/licenses/internal/toolbar.gd b/addons/licenses/internal/toolbar.gd index 89a2f37..955e023 100644 --- a/addons/licenses/internal/toolbar.gd +++ b/addons/licenses/internal/toolbar.gd @@ -5,9 +5,11 @@ const Licenses := preload("../licenses.gd") const Component := preload("../component.gd") const ComponentTree := preload("components_tree.gd") +signal add_component(comp: Component) +signal show_engine_components(show: bool) + @export var _menu_button: Button = null @export var _add_button: Button = null -@export_node_path("Tree") var _components_tree_path; @onready var _components_tree: ComponentTree = self.get_node(_components_tree_path) var _menu: PopupMenu var _add_menu: PopupMenu @@ -107,14 +109,13 @@ func _on_add_pressed() -> void: func _on_menu_pressed() -> void: self._menu.popup_on_parent(Rect2(self._menu_button.global_position + Vector2(0.0, self._menu_button.size.y), self._menu.get_contents_minimum_size())) -# add liense entry based on engine entry +# add license entry based on engine entry func _on_engine_add_id_pressed(id: int) -> void: var component: Component = Licenses.get_engine_component(self._add_engine_menu.get_item_metadata(id)) if component == null: return component.readonly = false - self._components_tree.add_component(component) - self._components_tree.select_component(component) + self.add_component.emit(component) # add component entry based on plugin cfg func _on_plugin_add_id_pressed(id: int) -> void: @@ -125,20 +126,16 @@ func _on_plugin_add_id_pressed(id: int) -> void: component.copyright.append(cfg["plugin"].get("author", "")) component.version = cfg["plugin"].get("version", "") component.paths.append(self._add_plugin_menu.get_item_metadata(id).get_base_dir()) - self._components_tree.add_component(component) - self._components_tree.select_component(component) + self.add_component.emit(component) func _on_menu_id_pressed(id: int) -> void: match id: 0: self._menu.toggle_item_checked(0) - self._components_tree.show_readonly_components = self._menu.is_item_checked(0) - self._components_tree.reload() + self.show_engine_components.emit(self._menu.is_item_checked(0)) func _on_add_id_pressed(id: int) -> void: match id: 0: var component: Component = Component.new() - self._components_tree.add_component(component) - self._components_tree.select_component(component) - self._components_tree.select_component(component) + self.add_component.emit(component) diff --git a/addons/licenses/plugin.cfg b/addons/licenses/plugin.cfg index 43146ea..0555b12 100644 --- a/addons/licenses/plugin.cfg +++ b/addons/licenses/plugin.cfg @@ -3,7 +3,7 @@ name="License Manager" description="Manage license and copyright for third party graphics, software or libraries." author="Iceflower S" -version="1.6.1" +version="1.7.0" script="plugin.gd" license="MIT" repository="https://github.com/kenyoni-software/godot-addons"