diff --git a/README.md b/README.md index 1b51a55..3c1f05a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -Autocompletion, templates and addon development tools for Blenders text editor. - -Manual: http://code-autocomplete-manual.readthedocs.org/en/latest/ +Autocompletion, templates and addon development tools for the text editor. + +Manual: http://code-autocomplete-manual.readthedocs.org/en/latest/ diff --git a/__init__.py b/__init__.py index c545809..da1208a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,93 +1,149 @@ -''' -Copyright (C) 2016 Jacques Lucke -mail@jlucke.com - -Created by Jacques Lucke - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - - - -bl_info = { - "name": "Code Autocomplete", - "description": "Autocompletion, templates and addon development tools for the text editor.", - "author": "Jacques Lucke", - "version": (2, 0, 0), - "blender": (2, 7, 4), - "location": "Text Editor", - "category": "Development" - } - - - -# load and reload submodules -################################## - -# append jedi package path to make 'import jedi' available -import os, sys -sys.path.append(os.path.join(__path__[0], "jedi")) - -import importlib -from . import developer_utils -importlib.reload(developer_utils) -modules = developer_utils.setup_addon_modules(__path__, __name__, "bpy" in locals()) - - - -# register -################################## - -import bpy - -addon_keymaps = [] -def register_keymaps(): - global addon_keymaps - wm = bpy.context.window_manager - km = wm.keyconfigs.addon.keymaps.new(name = "Text", space_type = "TEXT_EDITOR") - kmi = km.keymap_items.new("code_autocomplete.select_whole_string", type = "Y", value = "PRESS", ctrl = True) - kmi = km.keymap_items.new("wm.call_menu", type = "SPACE", value = "PRESS", ctrl = True) - kmi.properties.name = "code_autocomplete_insert_template_menu" - kmi = km.keymap_items.new("wm.call_menu", type = "TAB", value = "PRESS", ctrl = True) - kmi.properties.name = "code_autocomplete_select_text_block" - addon_keymaps.append(km) - -def unregister_keymaps(): - wm = bpy.context.window_manager - for km in addon_keymaps: - for kmi in km.keymap_items: - km.keymap_items.remove(kmi) - wm.keyconfigs.addon.keymaps.remove(km) - addon_keymaps.clear() - -from . addon_development import AddonDevelopmentSceneProperties -from . quick_operators import register_menus, unregister_menus -from . code_templates.base import draw_template_menu - -def register(): - bpy.utils.register_module(__name__) - register_keymaps() - register_menus() - bpy.types.TEXT_MT_templates.append(draw_template_menu) - bpy.types.Scene.addon_development = bpy.props.PointerProperty(name = "Addon Development", type = AddonDevelopmentSceneProperties) - - print("Registered Code Autocomplete with {} modules.".format(len(modules))) - -def unregister(): - bpy.utils.unregister_module(__name__) - unregister_keymaps() - unregister_menus() - bpy.types.TEXT_MT_templates.remove(draw_template_menu) - - print("Unregistered Code Autocomplete") +''' +Copyright (C) 2016 Jacques Lucke +mail@jlucke.com + +Created by Jacques Lucke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + + + +bl_info = { + "name": "Code Autocomplete", + "description": "Autocompletion, templates and addon development tools for the text editor.", + "author": "Jacques Lucke", + "version": (2, 0, 0), + "blender": (2, 80, 0), + "location": "Text Editor", + "category": "Development" + } + + + +# load and reload submodules +################################## + +# append jedi package path to make 'import jedi' available +import os, sys +sys.path.append(os.path.join(__path__[0], "jedi")) + +import importlib +from . import developer_utils +importlib.reload(developer_utils) +modules = developer_utils.setup_addon_modules(__path__, __name__, "bpy" in locals()) + + + +# register +################################## + +import bpy + +addon_keymaps = [] +def register_keymaps(): + global addon_keymaps + wm = bpy.context.window_manager + km = wm.keyconfigs.addon.keymaps.new(name = "Text", space_type = "TEXT_EDITOR") + kmi = km.keymap_items.new("code_autocomplete.select_whole_string", type = "Y", value = "PRESS", ctrl = True) + kmi = km.keymap_items.new("wm.call_menu", type = "SPACE", value = "PRESS", ctrl = True) + kmi.properties.name = "code_autocomplete_insert_template_menu" + kmi = km.keymap_items.new("wm.call_menu", type = "TAB", value = "PRESS", ctrl = True) + kmi.properties.name = "code_autocomplete_select_text_block" + addon_keymaps.append(km) + +def unregister_keymaps(): + wm = bpy.context.window_manager + for km in addon_keymaps: + for kmi in km.keymap_items: + km.keymap_items.remove(kmi) + wm.keyconfigs.addon.keymaps.remove(km) + addon_keymaps.clear() + +from . addon_development import AddonDevelopmentSceneProperties +from . quick_operators import register_menus, unregister_menus +from . code_templates.base import draw_template_menu + + +classes = ( + # code_autocomplete_insert_template_menu, + # code_autocomplete.build_script, + # code_autocomplete.correct_whitespaces, + # code_autocomplete.convert_addon_indentation, + # SolveWhitespaceInconsistency, + # SelectWholeString, + # ConvertFileIndentation, + SelectTextBlockMenu, + OpenTextBlock, + CompletionProviders , + ContextBoxProperties, + DescriptionBoxProperties, + AddonPreferences, + TextBlock, + Provider, + Completion, + JediCompletion, + JediCompletionProvider, + OperatorCompletion, + ParameterCompletion, + OperatorCompletionProvider, + WordCompletion, + WordCompletionProvider, + BlockEvent, + AutocompleteHandler, + ContextUI, + ActiveTextArea, + AddonDeveloperPanel, + AddonFilesPanel, + SetDirectoryVisibility, + RunAddon, + RestartBlender, + NewFile, + NewDirectory, + FileMenuOpener, + OpenFile, + OpenExternalFileBrowser, + RenameFile, + DeleteFile, + SaveFiles, + ExportAddon, + ConvertAddonIndentation, + FindExistingAddon, + MakeAddonNameValid, + CreateNewAddon, + AddonDevelopmentSceneProperties, + TextBox, + ListItem, + ListBox, + Rectangle, +) + +def register(): + for i in classes: + bpy.utils.register_class(i) + register_keymaps() + register_menus() + bpy.types.TEXT_MT_templates.append(draw_template_menu) + bpy.types.Scene.addon_development: bpy.props.PointerProperty(name = "Addon Development", type = AddonDevelopmentSceneProperties) + + print("Registered Code Autocomplete with {} modules.".format(len(modules))) + +def unregister(): + for i in classes: + bpy.utils.unregister_class(i) + unregister_keymaps() + unregister_menus() + bpy.types.TEXT_MT_templates.remove(draw_template_menu) + + print("Unregistered Code Autocomplete") diff --git a/__pycache__/__init__.cpython-37.pyc b/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..34f6fe2 Binary files /dev/null and b/__pycache__/__init__.cpython-37.pyc differ diff --git a/__pycache__/developer_utils.cpython-37.pyc b/__pycache__/developer_utils.cpython-37.pyc new file mode 100644 index 0000000..a4128e2 Binary files /dev/null and b/__pycache__/developer_utils.cpython-37.pyc differ diff --git a/__pycache__/quick_operators.cpython-37.pyc b/__pycache__/quick_operators.cpython-37.pyc new file mode 100644 index 0000000..e6028bd Binary files /dev/null and b/__pycache__/quick_operators.cpython-37.pyc differ diff --git a/__pycache__/settings.cpython-37.pyc b/__pycache__/settings.cpython-37.pyc new file mode 100644 index 0000000..b260b9d Binary files /dev/null and b/__pycache__/settings.cpython-37.pyc differ diff --git a/__pycache__/text_block.cpython-37.pyc b/__pycache__/text_block.cpython-37.pyc new file mode 100644 index 0000000..c188a58 Binary files /dev/null and b/__pycache__/text_block.cpython-37.pyc differ diff --git a/addon_development/__init__.py b/addon_development/__init__.py index 458e33a..f2efe96 100644 --- a/addon_development/__init__.py +++ b/addon_development/__init__.py @@ -1,5 +1,5 @@ -import bpy -from bpy.props import * - -class AddonDevelopmentSceneProperties(bpy.types.PropertyGroup): - addon_name = StringProperty(name = "Addon Name", default = "", description = "Name of the currently selected addon") +import bpy +from bpy.props import * + +class AddonDevelopmentSceneProperties(bpy.types.PropertyGroup): + addon_name: StringProperty(name = "Addon Name", default = "", description = "Name of the currently selected addon") diff --git a/addon_development/__pycache__/__init__.cpython-37.pyc b/addon_development/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..b705e8b Binary files /dev/null and b/addon_development/__pycache__/__init__.cpython-37.pyc differ diff --git a/addon_development/__pycache__/addon_selection.cpython-37.pyc b/addon_development/__pycache__/addon_selection.cpython-37.pyc new file mode 100644 index 0000000..98f9502 Binary files /dev/null and b/addon_development/__pycache__/addon_selection.cpython-37.pyc differ diff --git a/addon_development/__pycache__/convert_indentation.cpython-37.pyc b/addon_development/__pycache__/convert_indentation.cpython-37.pyc new file mode 100644 index 0000000..5f2dbe3 Binary files /dev/null and b/addon_development/__pycache__/convert_indentation.cpython-37.pyc differ diff --git a/addon_development/__pycache__/export_addon.cpython-37.pyc b/addon_development/__pycache__/export_addon.cpython-37.pyc new file mode 100644 index 0000000..2ef93e0 Binary files /dev/null and b/addon_development/__pycache__/export_addon.cpython-37.pyc differ diff --git a/addon_development/__pycache__/file_operators.cpython-37.pyc b/addon_development/__pycache__/file_operators.cpython-37.pyc new file mode 100644 index 0000000..814a26b Binary files /dev/null and b/addon_development/__pycache__/file_operators.cpython-37.pyc differ diff --git a/addon_development/__pycache__/panels.cpython-37.pyc b/addon_development/__pycache__/panels.cpython-37.pyc new file mode 100644 index 0000000..ac012ec Binary files /dev/null and b/addon_development/__pycache__/panels.cpython-37.pyc differ diff --git a/addon_development/__pycache__/restart_blender.cpython-37.pyc b/addon_development/__pycache__/restart_blender.cpython-37.pyc new file mode 100644 index 0000000..1fa357f Binary files /dev/null and b/addon_development/__pycache__/restart_blender.cpython-37.pyc differ diff --git a/addon_development/__pycache__/run_addon.cpython-37.pyc b/addon_development/__pycache__/run_addon.cpython-37.pyc new file mode 100644 index 0000000..0422c15 Binary files /dev/null and b/addon_development/__pycache__/run_addon.cpython-37.pyc differ diff --git a/addon_development/__pycache__/utils.cpython-37.pyc b/addon_development/__pycache__/utils.cpython-37.pyc new file mode 100644 index 0000000..d8992ad Binary files /dev/null and b/addon_development/__pycache__/utils.cpython-37.pyc differ diff --git a/addon_development/addon_selection.py b/addon_development/addon_selection.py index b10e167..be99eaa 100644 --- a/addon_development/addon_selection.py +++ b/addon_development/addon_selection.py @@ -1,108 +1,108 @@ -import os -import bpy -from bpy.props import * -from datetime import datetime -from . utils import (get_directory_names, - current_addon_exists, - is_addon_name_valid, - get_current_addon_path, - get_addon_name, - correct_file_name, - get_settings, - addons_path, - new_addon_file) - -class FindExistingAddon(bpy.types.Operator): - bl_idname = "code_autocomplete.find_existing_addon" - bl_label = "Find Existing Addon" - bl_description = "Pick an existing addon" - bl_options = {"REGISTER"} - bl_property = "item" - - def get_items(self, context): - items = [] - directories = get_directory_names(addons_path) - for addon in directories: - items.append((addon, addon, "")) - return items - - item = bpy.props.EnumProperty(items = get_items) - - def invoke(self, context, event): - context.window_manager.invoke_search_popup(self) - return {"CANCELLED"} - - def execute(self, context): - get_settings().addon_name = self.item - path = get_current_addon_path() - bpy.ops.code_autocomplete.set_directory_visibility(directory = path, visibility = True) - context.area.tag_redraw() - return {"FINISHED"} - - -class MakeAddonNameValid(bpy.types.Operator): - bl_idname = "code_autocomplete.make_addon_name_valid" - bl_label = "Make Name Valid" - bl_description = "Make the addon name a valid module name" - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return not current_addon_exists() and not is_addon_name_valid() - - def execute(self, context): - name = get_addon_name() - get_settings().addon_name = correct_file_name(name, is_directory = True) - return {"FINISHED"} - - -new_addon_type_items = [ - ("BASIC", "Basic", ""), - ("MULTIFILE", "Multi-File (recommended)", "") ] - -class CreateNewAddon(bpy.types.Operator): - bl_idname = "code_autocomplete.new_addon" - bl_label = "New Addon" - bl_description = "Create a folder in the addon directory and setup a basic code base" - bl_options = {"REGISTER"} - - new_addon_type = EnumProperty(default = "BASIC", items = new_addon_type_items) - - @classmethod - def poll(cls, context): - return not current_addon_exists() and is_addon_name_valid() - - def execute(self, context): - self.create_addon_directory() - self.generate_from_template() - addon_path = get_current_addon_path() - bpy.ops.code_autocomplete.open_file(path = addon_path + "__init__.py") - bpy.ops.code_autocomplete.set_directory_visibility(directory = addon_path, visibility = True) - context.area.tag_redraw() - return {"FINISHED"} - - def create_addon_directory(self): - os.makedirs(get_current_addon_path()) - - def generate_from_template(self): - t = self.new_addon_type - if t == "BASIC": - code = self.read_template_file("basic.txt") - code = code.replace("BLENDER_VERSION", str(bpy.app.version)) - new_addon_file("__init__.py", code) - - if t == "MULTIFILE": - code = self.read_template_file("multifile.txt") - code = code.replace("CURRENT_YEAR", str(datetime.now().year)) - code = code.replace("BLENDER_VERSION", str(bpy.app.version)) - new_addon_file("__init__.py", code) - - code = self.read_template_file("developer_utils.txt") - new_addon_file("developer_utils.py", code) - - def read_template_file(self, path): - path = os.path.join(os.path.dirname(__file__), "addon_templates", path) - file = open(path) - text = file.read() - file.close() - return text +import os +import bpy +from bpy.props import * +from datetime import datetime +from . utils import (get_directory_names, + current_addon_exists, + is_addon_name_valid, + get_current_addon_path, + get_addon_name, + correct_file_name, + get_settings, + addons_path, + new_addon_file) + +class FindExistingAddon(bpy.types.Operator): + bl_idname = "code_autocomplete.find_existing_addon" + bl_label = "Find Existing Addon" + bl_description = "Pick an existing addon" + bl_options = {"REGISTER"} + bl_property = "item" + + def get_items(self, context): + items = [] + directories = get_directory_names(addons_path) + for addon in directories: + items.append((addon, addon, "")) + return items + + item: bpy.props.EnumProperty(items = get_items) + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"CANCELLED"} + + def execute(self, context): + get_settings().addon_name = self.item + path = get_current_addon_path() + bpy.ops.code_autocomplete.set_directory_visibility(directory = path, visibility = True) + context.area.tag_redraw() + return {"FINISHED"} + + +class MakeAddonNameValid(bpy.types.Operator): + bl_idname = "code_autocomplete.make_addon_name_valid" + bl_label = "Make Name Valid" + bl_description = "Make the addon name a valid module name" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return not current_addon_exists() and not is_addon_name_valid() + + def execute(self, context): + name = get_addon_name() + get_settings().addon_name = correct_file_name(name, is_directory = True) + return {"FINISHED"} + + +new_addon_type_items = [ + ("BASIC", "Basic", ""), + ("MULTIFILE", "Multi-File (recommended)", "") ] + +class CreateNewAddon(bpy.types.Operator): + bl_idname = "code_autocomplete.new_addon" + bl_label = "New Addon" + bl_description = "Create a folder in the addon directory and setup a basic code base" + bl_options = {"REGISTER"} + + new_addon_type: EnumProperty(default = "BASIC", items = new_addon_type_items) + + @classmethod + def poll(cls, context): + return not current_addon_exists() and is_addon_name_valid() + + def execute(self, context): + self.create_addon_directory() + self.generate_from_template() + addon_path = get_current_addon_path() + bpy.ops.code_autocomplete.open_file(path = addon_path + "__init__.py") + bpy.ops.code_autocomplete.set_directory_visibility(directory = addon_path, visibility = True) + context.area.tag_redraw() + return {"FINISHED"} + + def create_addon_directory(self): + os.makedirs(get_current_addon_path()) + + def generate_from_template(self): + t = self.new_addon_type + if t == "BASIC": + code = self.read_template_file("basic.txt") + code = code.replace("BLENDER_VERSION", str(bpy.app.version)) + new_addon_file("__init__.py", code) + + if t == "MULTIFILE": + code = self.read_template_file("multifile.txt") + code = code.replace("CURRENT_YEAR", str(datetime.now().year)) + code = code.replace("BLENDER_VERSION", str(bpy.app.version)) + new_addon_file("__init__.py", code) + + code = self.read_template_file("developer_utils.txt") + new_addon_file("developer_utils.py", code) + + def read_template_file(self, path): + path = os.path.join(os.path.dirname(__file__), "addon_templates", path) + file = open(path) + text = file.read() + file.close() + return text diff --git a/addon_development/addon_templates/basic.txt b/addon_development/addon_templates/basic.txt index ccbbcfb..96a9928 100644 --- a/addon_development/addon_templates/basic.txt +++ b/addon_development/addon_templates/basic.txt @@ -1,22 +1,22 @@ -import bpy - -bl_info = { - "name": "My Addon Name", - "description": "Single Line Explanation", - "author": "Your Name", - "version": (0, 0, 1), - "blender": BLENDER_VERSION, - "location": "View3D", - "warning": "This is an unstable version", - "wiki_url": "", - "category": "Object" } - - - - - -def register(): - bpy.utils.register_module(__name__) - -def unregister(): - bpy.utils.unregister_module(__name__) +import bpy + +bl_info = { + "name": "My Addon Name", + "description": "Single Line Explanation", + "author": "Your Name", + "version": (0, 0, 1), + "blender": BLENDER_VERSION, + "location": "View3D", + "warning": "This is an unstable version", + "wiki_url": "", + "category": "Object" } + + + + + +def register(): + bpy.utils.register_module(__name__) + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/addon_development/addon_templates/developer_utils.txt b/addon_development/addon_templates/developer_utils.txt index e031810..dcbe163 100644 --- a/addon_development/addon_templates/developer_utils.txt +++ b/addon_development/addon_templates/developer_utils.txt @@ -1,42 +1,42 @@ -import os -import sys -import pkgutil -import importlib - -def setup_addon_modules(path, package_name, reload): - """ - Imports and reloads all modules in this addon. - - path -- __path__ from __init__.py - package_name -- __name__ from __init__.py - - Individual modules can define a __reload_order_index__ property which - will be used to reload the modules in a specific order. The default is 0. - """ - def get_submodule_names(path = path[0], root = ""): - module_names = [] - for importer, module_name, is_package in pkgutil.iter_modules([path]): - if is_package: - sub_path = os.path.join(path, module_name) - sub_root = root + module_name + "." - module_names.extend(get_submodule_names(sub_path, sub_root)) - else: - module_names.append(root + module_name) - return module_names - - def import_submodules(names): - modules = [] - for name in names: - modules.append(importlib.import_module("." + name, package_name)) - return modules - - def reload_modules(modules): - modules.sort(key = lambda module: getattr(module, "__reload_order_index__", 0)) - for module in modules: - importlib.reload(module) - - names = get_submodule_names() - modules = import_submodules(names) - if reload: - reload_modules(modules) - return modules +import os +import sys +import pkgutil +import importlib + +def setup_addon_modules(path, package_name, reload): + """ + Imports and reloads all modules in this addon. + + path -- __path__ from __init__.py + package_name -- __name__ from __init__.py + + Individual modules can define a __reload_order_index__ property which + will be used to reload the modules in a specific order. The default is 0. + """ + def get_submodule_names(path = path[0], root = ""): + module_names = [] + for importer, module_name, is_package in pkgutil.iter_modules([path]): + if is_package: + sub_path = os.path.join(path, module_name) + sub_root = root + module_name + "." + module_names.extend(get_submodule_names(sub_path, sub_root)) + else: + module_names.append(root + module_name) + return module_names + + def import_submodules(names): + modules = [] + for name in names: + modules.append(importlib.import_module("." + name, package_name)) + return modules + + def reload_modules(modules): + modules.sort(key = lambda module: getattr(module, "__reload_order_index__", 0)) + for module in modules: + importlib.reload(module) + + names = get_submodule_names() + modules = import_submodules(names) + if reload: + reload_modules(modules) + return modules diff --git a/addon_development/addon_templates/multifile.txt b/addon_development/addon_templates/multifile.txt index 8f2ea32..0f56e1e 100644 --- a/addon_development/addon_templates/multifile.txt +++ b/addon_development/addon_templates/multifile.txt @@ -1,61 +1,61 @@ -''' -Copyright (C) CURRENT_YEAR YOUR NAME -YOUR@MAIL.com - -Created by YOUR NAME - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -bl_info = { - "name": "Your Addon Name", - "description": "", - "author": "Your Name", - "version": (0, 0, 1), - "blender": BLENDER_VERSION, - "location": "View3D", - "warning": "This addon is still in development.", - "wiki_url": "", - "category": "Object" } - - -import bpy - - -# load and reload submodules -################################## - -import importlib -from . import developer_utils -importlib.reload(developer_utils) -modules = developer_utils.setup_addon_modules(__path__, __name__, "bpy" in locals()) - - - -# register -################################## - -import traceback - -def register(): - try: bpy.utils.register_module(__name__) - except: traceback.print_exc() - - print("Registered {} with {} modules".format(bl_info["name"], len(modules))) - -def unregister(): - try: bpy.utils.unregister_module(__name__) - except: traceback.print_exc() - - print("Unregistered {}".format(bl_info["name"])) +''' +Copyright (C) CURRENT_YEAR YOUR NAME +YOUR@MAIL.com + +Created by YOUR NAME + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +bl_info = { + "name": "Your Addon Name", + "description": "", + "author": "Your Name", + "version": (0, 0, 1), + "blender": BLENDER_VERSION, + "location": "View3D", + "warning": "This addon is still in development.", + "wiki_url": "", + "category": "Object" } + + +import bpy + + +# load and reload submodules +################################## + +import importlib +from . import developer_utils +importlib.reload(developer_utils) +modules = developer_utils.setup_addon_modules(__path__, __name__, "bpy" in locals()) + + + +# register +################################## + +import traceback + +def register(): + try: bpy.utils.register_module(__name__) + except: traceback.print_exc() + + print("Registered {} with {} modules".format(bl_info["name"], len(modules))) + +def unregister(): + try: bpy.utils.unregister_module(__name__) + except: traceback.print_exc() + + print("Unregistered {}".format(bl_info["name"])) diff --git a/addon_development/convert_indentation.py b/addon_development/convert_indentation.py index 9ffbbb8..04e62a4 100644 --- a/addon_development/convert_indentation.py +++ b/addon_development/convert_indentation.py @@ -1,34 +1,34 @@ -import bpy -import os -from bpy.props import * -from . utils import current_addon_exists, get_current_addon_path - -class ConvertAddonIndentation(bpy.types.Operator): - bl_idname = "code_autocomplete.convert_addon_indentation" - bl_label = "Convert Addon Indentation" - bl_description = "" - bl_options = {"REGISTER"} - - old_indentation = StringProperty(default = "\t") - new_indentation = StringProperty(default = " ") - - @classmethod - def poll(cls, context): - return current_addon_exists() - - def execute(self, context): - paths = self.get_addon_files() - for path in paths: - bpy.ops.code_autocomplete.convert_file_indentation( - path = path, - old_indentation = self.old_indentation, - new_indentation = self.new_indentation) - return {"FINISHED"} - - def get_addon_files(self): - paths = [] - for root, dirs, files in os.walk(get_current_addon_path()): - for file in files: - if file.endswith(".py"): - paths.append(os.path.join(root, file)) - return paths +import bpy +import os +from bpy.props import * +from . utils import current_addon_exists, get_current_addon_path + +class ConvertAddonIndentation(bpy.types.Operator): + bl_idname = "code_autocomplete.convert_addon_indentation" + bl_label = "Convert Addon Indentation" + bl_description = "" + bl_options = {"REGISTER"} + + old_indentation: StringProperty(default = "\t") + new_indentation: StringProperty(default = " ") + + @classmethod + def poll(cls, context): + return current_addon_exists() + + def execute(self, context): + paths = self.get_addon_files() + for path in paths: + bpy.ops.code_autocomplete.convert_file_indentation( + path = path, + old_indentation = self.old_indentation, + new_indentation = self.new_indentation) + return {"FINISHED"} + + def get_addon_files(self): + paths = [] + for root, dirs, files in os.walk(get_current_addon_path()): + for file in files: + if file.endswith(".py"): + paths.append(os.path.join(root, file)) + return paths diff --git a/addon_development/export_addon.py b/addon_development/export_addon.py index e037aad..8ed7e4c 100644 --- a/addon_development/export_addon.py +++ b/addon_development/export_addon.py @@ -1,44 +1,44 @@ -import bpy -import os -import zipfile -from bpy.props import * -from . utils import current_addon_exists, get_addon_name, get_current_addon_path - -class ExportAddon(bpy.types.Operator): - bl_idname = "code_autocomplete.export_addon" - bl_label = "Export Addon" - bl_description = "Save a .zip file of the addon" - bl_options = {"REGISTER"} - - filepath = StringProperty(subtype = "FILE_PATH") - - @classmethod - def poll(cls, context): - return current_addon_exists() - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - subdirectory_name = get_addon_name() + os.sep - source_path = get_current_addon_path() - output_path = self.filepath - if not output_path.lower().endswith(".zip"): - output_path += ".zip" - zip_directory(source_path, output_path, additional_path = subdirectory_name) - return {"FINISHED"} - - -def zip_directory(source_path, output_path, additional_path = ""): - try: - parent_folder = os.path.dirname(source_path) - content = os.walk(source_path) - zip_file = zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) - for root, folders, files in content: - for data in folders + files: - absolute_path = os.path.join(root, data) - relative_path = additional_path + absolute_path[len(parent_folder+os.sep):] - zip_file.write(absolute_path, relative_path) - zip_file.close() - except: print("Could not zip the directory") +import bpy +import os +import zipfile +from bpy.props import * +from . utils import current_addon_exists, get_addon_name, get_current_addon_path + +class ExportAddon(bpy.types.Operator): + bl_idname = "code_autocomplete.export_addon" + bl_label = "Export Addon" + bl_description = "Save a .zip file of the addon" + bl_options = {"REGISTER"} + + filepath: StringProperty(subtype = "FILE_PATH") + + @classmethod + def poll(cls, context): + return current_addon_exists() + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def execute(self, context): + subdirectory_name = get_addon_name() + os.sep + source_path = get_current_addon_path() + output_path = self.filepath + if not output_path.lower().endswith(".zip"): + output_path += ".zip" + zip_directory(source_path, output_path, additional_path = subdirectory_name) + return {"FINISHED"} + + +def zip_directory(source_path, output_path, additional_path = ""): + try: + parent_folder = os.path.dirname(source_path) + content = os.walk(source_path) + zip_file = zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) + for root, folders, files in content: + for data in folders + files: + absolute_path = os.path.join(root, data) + relative_path = additional_path + absolute_path[len(parent_folder+os.sep):] + zip_file.write(absolute_path, relative_path) + zip_file.close() + except: print("Could not zip the directory") diff --git a/addon_development/file_operators.py b/addon_development/file_operators.py index 5806cb6..a857aec 100644 --- a/addon_development/file_operators.py +++ b/addon_development/file_operators.py @@ -1,207 +1,207 @@ -import bpy -import os -from bpy.props import * -from . utils import (get_directory_names, - current_addon_exists, - is_addon_name_valid, - get_current_addon_path, - get_addon_name, - correct_file_name, - get_settings, - addons_path, - new_file, - new_directory) - -class NewFile(bpy.types.Operator): - bl_idname = "code_autocomplete.new_file" - bl_label = "New File" - bl_description = "Create a new file in this directory" - bl_options = {"REGISTER"} - - directory = StringProperty(name = "Directory", default = "") - name = StringProperty(name = "File Name", default = "") - content = StringProperty(name = "Content", default = "") - - @classmethod - def poll(cls, context): - return current_addon_exists() - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self, width = 400) - - def draw(self, context): - layout = self.layout - layout.prop(self, "name") - - def execute(self, context): - if self.name != "": - path = self.directory + self.name - new_file(self.directory + self.name, self.content) - bpy.ops.code_autocomplete.open_file(path = path) - context.area.tag_redraw() - return {"FINISHED"} - - -class NewDirectory(bpy.types.Operator): - bl_idname = "code_autocomplete.new_directory" - bl_label = "New Directory" - bl_description = "Create a new subdirectory" - bl_options = {"REGISTER"} - - directory = StringProperty(name = "Directory", default = "") - name = StringProperty(name = "Directory Name", default = "") - - @classmethod - def poll(cls, context): - return current_addon_exists() - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self, width = 400) - - def draw(self, context): - layout = self.layout - layout.prop(self, "name") - - def execute(self, context): - if self.name != "": - new_directory(self.directory + self.name) - new_file(os.path.join(self.directory + self.name, "__init__.py")) - context.area.tag_redraw() - return {"FINISHED"} - - -class FileMenuOpener(bpy.types.Operator): - bl_idname = "code_autocomplete.open_file_menu" - bl_label = "Open File Menu" - - path = StringProperty(name = "Path", default = "") - - def invoke(self, context, event): - context.window_manager.popup_menu(self.drawMenu, title = "{} - File Menu".format(os.path.basename(self.path))) - return {"FINISHED"} - - def drawMenu(fileProps, self, context): - layout = self.layout - layout.operator_context = "INVOKE_DEFAULT" - - props = layout.operator("code_autocomplete.rename_file", text = "Rename") - props.path = fileProps.path - - props = layout.operator("code_autocomplete.open_file", text = "Open in Text Editor") - props.path = fileProps.path - - props = layout.operator("code_autocomplete.open_external_file_browser", text = "Open External") - props.directory = os.path.dirname(fileProps.path) - - layout.separator() - - props = layout.operator("code_autocomplete.delete_file", text = "Delete", icon = "ERROR") - props.path = fileProps.path - - -class OpenFile(bpy.types.Operator): - bl_idname = "code_autocomplete.open_file" - bl_label = "Open File" - bl_description = "Load the file into the text editor" - bl_options = {"REGISTER"} - - path = StringProperty(name = "Path", default = "") - - def execute(self, context): - text = None - for text_block in bpy.data.texts: - if text_block.filepath == self.path: - text = text_block - break - if not text: - text = bpy.data.texts.load(self.path, internal = False) - - context.space_data.text = text - return {"FINISHED"} - - -class OpenExternalFileBrowser(bpy.types.Operator): - bl_idname = "code_autocomplete.open_external_file_browser" - bl_label = "Open External File Browser" - bl_description = "" - bl_options = {"REGISTER"} - - directory = StringProperty(name = "Directory", default = "") - - def execute(self, context): - bpy.ops.wm.path_open(filepath = self.directory) - return {"FINISHED"} - - -class RenameFile(bpy.types.Operator): - bl_idname = "code_autocomplete.rename_file" - bl_label = "Open External File Browser" - bl_description = "" - bl_options = {"REGISTER"} - - path = StringProperty(name = "Directory", default = "") - new_name = StringProperty(name = "Directory", description = "New file name", default = "") - - def invoke(self, context, event): - self.new_name = os.path.basename(self.path) - return context.window_manager.invoke_props_dialog(self, width = 400) - - def draw(self, context): - layout = self.layout - layout.prop(self, "new_name") - - def execute(self, context): - new_path = os.path.join(os.path.dirname(self.path), self.new_name) - os.rename(self.path, new_path) - self.correct_text_block_paths(self.path, new_path) - context.area.tag_redraw() - return {"FINISHED"} - - def correct_text_block_paths(self, old_path, new_path): - for text in bpy.data.texts: - if text.filepath == old_path: - text.filepath = new_path - - -class DeleteFile(bpy.types.Operator): - bl_idname = "code_autocomplete.delete_file" - bl_label = "Delete File" - bl_description = "Delete file on the hard drive" - bl_options = {"REGISTER"} - - path = StringProperty(name = "Directory", default = "") - - def invoke(self, context, event): - return context.window_manager.invoke_confirm(self, event) - - def execute(self, context): - os.remove(self.path) - context.area.tag_redraw() - return {"FINISHED"} - - -class SaveFiles(bpy.types.Operator): - bl_idname = "code_autocomplete.save_files" - bl_label = "Save All Files" - bl_description = "Save all files which correspond to a file on the hard drive" - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - for text in bpy.data.texts: - save_text_block(text) - try: bpy.ops.text.resolve_conflict(resolution = "IGNORE") - except: pass - return {"FINISHED"} - - -def save_text_block(text_block): - if not text_block: return - if not os.path.exists(text_block.filepath): return - - file = open(text_block.filepath, mode = "w") - file.write(text_block.as_string()) - file.close() +import bpy +import os +from bpy.props import * +from . utils import (get_directory_names, + current_addon_exists, + is_addon_name_valid, + get_current_addon_path, + get_addon_name, + correct_file_name, + get_settings, + addons_path, + new_file, + new_directory) + +class NewFile(bpy.types.Operator): + bl_idname = "code_autocomplete.new_file" + bl_label = "New File" + bl_description = "Create a new file in this directory" + bl_options = {"REGISTER"} + + directory: StringProperty(name = "Directory", default = "") + name: StringProperty(name = "File Name", default = "") + content: StringProperty(name = "Content", default = "") + + @classmethod + def poll(cls, context): + return current_addon_exists() + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width = 400) + + def draw(self, context): + layout = self.layout + layout.prop(self, "name") + + def execute(self, context): + if self.name != "": + path = self.directory + self.name + new_file(self.directory + self.name, self.content) + bpy.ops.code_autocomplete.open_file(path = path) + context.area.tag_redraw() + return {"FINISHED"} + + +class NewDirectory(bpy.types.Operator): + bl_idname = "code_autocomplete.new_directory" + bl_label = "New Directory" + bl_description = "Create a new subdirectory" + bl_options = {"REGISTER"} + + directory: StringProperty(name = "Directory", default = "") + name: StringProperty(name = "Directory Name", default = "") + + @classmethod + def poll(cls, context): + return current_addon_exists() + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width = 400) + + def draw(self, context): + layout = self.layout + layout.prop(self, "name") + + def execute(self, context): + if self.name != "": + new_directory(self.directory + self.name) + new_file(os.path.join(self.directory + self.name, "__init__.py")) + context.area.tag_redraw() + return {"FINISHED"} + + +class FileMenuOpener(bpy.types.Operator): + bl_idname = "code_autocomplete.open_file_menu" + bl_label = "Open File Menu" + + path: StringProperty(name = "Path", default = "") + + def invoke(self, context, event): + context.window_manager.popup_menu(self.drawMenu, title = "{} - File Menu".format(os.path.basename(self.path))) + return {"FINISHED"} + + def drawMenu(fileProps, self, context): + layout = self.layout + layout.operator_context = "INVOKE_DEFAULT" + + props = layout.operator("code_autocomplete.rename_file", text = "Rename") + props.path = fileProps.path + + props = layout.operator("code_autocomplete.open_file", text = "Open in Text Editor") + props.path = fileProps.path + + props = layout.operator("code_autocomplete.open_external_file_browser", text = "Open External") + props.directory = os.path.dirname(fileProps.path) + + layout.separator() + + props = layout.operator("code_autocomplete.delete_file", text = "Delete", icon = "ERROR") + props.path = fileProps.path + + +class OpenFile(bpy.types.Operator): + bl_idname = "code_autocomplete.open_file" + bl_label = "Open File" + bl_description = "Load the file into the text editor" + bl_options = {"REGISTER"} + + path: StringProperty(name = "Path", default = "") + + def execute(self, context): + text = None + for text_block in bpy.data.texts: + if text_block.filepath == self.path: + text = text_block + break + if not text: + text = bpy.data.texts.load(self.path, internal = False) + + context.space_data.text = text + return {"FINISHED"} + + +class OpenExternalFileBrowser(bpy.types.Operator): + bl_idname = "code_autocomplete.open_external_file_browser" + bl_label = "Open External File Browser" + bl_description = "" + bl_options = {"REGISTER"} + + directory: StringProperty(name = "Directory", default = "") + + def execute(self, context): + bpy.ops.wm.path_open(filepath = self.directory) + return {"FINISHED"} + + +class RenameFile(bpy.types.Operator): + bl_idname = "code_autocomplete.rename_file" + bl_label = "Open External File Browser" + bl_description = "" + bl_options = {"REGISTER"} + + path: StringProperty(name = "Directory", default = "") + new_name: StringProperty(name = "Directory", description = "New file name", default = "") + + def invoke(self, context, event): + self.new_name = os.path.basename(self.path) + return context.window_manager.invoke_props_dialog(self, width = 400) + + def draw(self, context): + layout = self.layout + layout.prop(self, "new_name") + + def execute(self, context): + new_path = os.path.join(os.path.dirname(self.path), self.new_name) + os.rename(self.path, new_path) + self.correct_text_block_paths(self.path, new_path) + context.area.tag_redraw() + return {"FINISHED"} + + def correct_text_block_paths(self, old_path, new_path): + for text in bpy.data.texts: + if text.filepath == old_path: + text.filepath = new_path + + +class DeleteFile(bpy.types.Operator): + bl_idname = "code_autocomplete.delete_file" + bl_label = "Delete File" + bl_description = "Delete file on the hard drive" + bl_options = {"REGISTER"} + + path: StringProperty(name = "Directory", default = "") + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + os.remove(self.path) + context.area.tag_redraw() + return {"FINISHED"} + + +class SaveFiles(bpy.types.Operator): + bl_idname = "code_autocomplete.save_files" + bl_label = "Save All Files" + bl_description = "Save all files which correspond to a file on the hard drive" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + for text in bpy.data.texts: + save_text_block(text) + try: bpy.ops.text.resolve_conflict(resolution = "IGNORE") + except: pass + return {"FINISHED"} + + +def save_text_block(text_block): + if not text_block: return + if not os.path.exists(text_block.filepath): return + + file = open(text_block.filepath, mode = "w") + file.write(text_block.as_string()) + file.close() diff --git a/addon_development/panels.py b/addon_development/panels.py index 4d69bc6..da93bd3 100644 --- a/addon_development/panels.py +++ b/addon_development/panels.py @@ -1,121 +1,121 @@ -import bpy -import os -from collections import defaultdict -from bpy.props import * -from . utils import (get_settings, - get_current_addon_path, - current_addon_exists, - is_addon_name_valid, - get_directory_names, - get_file_names, - get_current_filepath, - get_addon_name) - -class AddonDeveloperPanel(bpy.types.Panel): - bl_idname = "addon_developer_panel" - bl_label = "Addon Development" - bl_space_type = "TEXT_EDITOR" - bl_region_type = "UI" - - def draw(self, context): - layout = self.layout - setting = get_settings() - row = layout.row(align = True) - row.prop(setting, "addon_name", text = "Name") - row.operator("code_autocomplete.find_existing_addon", icon = "EYEDROPPER", text = "") - - if not current_addon_exists(): - if not is_addon_name_valid(): - if get_addon_name() == "": - layout.label("Insert the name of your addon", icon = "INFO") - else: - layout.operator("code_autocomplete.make_addon_name_valid", icon = "ERROR", text = "Correct Addon Name") - else: - row = layout.row() - row.scale_y = 1.2 - row.operator_menu_enum("code_autocomplete.new_addon", "new_addon_type", icon = "NEW", text = "New Addon") - else: - row = layout.row() - row.scale_y = 1.5 - row.operator("code_autocomplete.run_addon", icon = "OUTLINER_DATA_POSE", text = "Run Addon") - layout.operator("code_autocomplete.export_addon", icon = "EXPORT", text = "Export as Zip") - layout.operator("code_autocomplete.restart_blender", icon = "BLENDER") - - -directory_visibility = defaultdict(bool) - -class AddonFilesPanel(bpy.types.Panel): - bl_idname = "addon_files_panel" - bl_label = "Addon Files" - bl_space_type = "TEXT_EDITOR" - bl_region_type = "UI" - - @classmethod - def poll(cls, context): - return current_addon_exists() - - def draw(self, context): - layout = self.layout - addon_path = get_current_addon_path() - - layout.operator("code_autocomplete.save_files", icon = "SAVE_COPY") - self.draw_directory(layout, addon_path) - - def draw_directory(self, layout, directory): - is_visible = self.is_directory_visible(directory) - box = layout.box() - - row = box.row(align = True) - - icon = "DOWNARROW_HLT" if is_visible else "RIGHTARROW" - props = row.operator("code_autocomplete.set_directory_visibility", text = os.path.split(directory[:-1])[-1], icon = icon, emboss = False) - props.directory = directory - props.visibility = not is_visible - - subrow = row.row(align = True) - subrow.active = False - props = subrow.operator("code_autocomplete.open_external_file_browser", text = "", icon = "FILESEL", emboss = False) - props.directory = directory - - if is_visible: - col = box.column(align = True) - directory_names = get_directory_names(directory) - for directory_name in directory_names: - row = col.row() - self.draw_directory(row, directory + directory_name + os.sep) - - file_names = get_file_names(directory) - col = box.column(align = True) - for file_name in file_names: - row = col.row() - row.alignment = "LEFT" - full_path = directory + file_name - props = row.operator("code_autocomplete.open_file_menu", icon = "COLLAPSEMENU", text = "", emboss = True) - props.path = full_path - if full_path == get_current_filepath(): - row.label("", icon = "RIGHTARROW_THIN") - operator = row.operator("code_autocomplete.open_file", text = file_name, emboss = False) - operator.path = full_path - - row = box.row(align = True) - operator = row.operator("code_autocomplete.new_file", icon = "PLUS", text = "File") - operator.directory = directory - operator = row.operator("code_autocomplete.new_directory", icon = "PLUS", text = "Directory") - operator.directory = directory - - def is_directory_visible(self, directory): - return directory_visibility[directory] - -class SetDirectoryVisibility(bpy.types.Operator): - bl_idname = "code_autocomplete.set_directory_visibility" - bl_label = "Toogle Directory Visibility" - bl_description = "" - bl_options = {"REGISTER"} - - directory = StringProperty(name = "Directory", default = "") - visibility = BoolProperty(name = "Visibility", default = True) - - def execute(self, context): - global directory_visibility - directory_visibility[self.directory] = self.visibility - return {"FINISHED"} +import bpy +import os +from collections import defaultdict +from bpy.props import * +from . utils import (get_settings, + get_current_addon_path, + current_addon_exists, + is_addon_name_valid, + get_directory_names, + get_file_names, + get_current_filepath, + get_addon_name) + +class AddonDeveloperPanel(bpy.types.Panel): + bl_idname = "addon_developer_panel" + bl_label = "Addon Development" + bl_space_type = "TEXT_EDITOR" + bl_region_type = "UI" + + def draw(self, context): + layout = self.layout + setting = get_settings() + row = layout.row(align = True) + row.prop(setting, "addon_name", text = "Name") + row.operator("code_autocomplete.find_existing_addon", icon = "EYEDROPPER", text = "") + + if not current_addon_exists(): + if not is_addon_name_valid(): + if get_addon_name() == "": + layout.label("Insert the name of your addon", icon = "INFO") + else: + layout.operator("code_autocomplete.make_addon_name_valid", icon = "ERROR", text = "Correct Addon Name") + else: + row = layout.row() + row.scale_y = 1.2 + row.operator_menu_enum("code_autocomplete.new_addon", "new_addon_type", icon = "NEW", text = "New Addon") + else: + row = layout.row() + row.scale_y = 1.5 + row.operator("code_autocomplete.run_addon", icon = "OUTLINER_DATA_POSE", text = "Run Addon") + layout.operator("code_autocomplete.export_addon", icon = "EXPORT", text = "Export as Zip") + layout.operator("code_autocomplete.restart_blender", icon = "BLENDER") + + +directory_visibility = defaultdict(bool) + +class AddonFilesPanel(bpy.types.Panel): + bl_idname = "addon_files_panel" + bl_label = "Addon Files" + bl_space_type = "TEXT_EDITOR" + bl_region_type = "UI" + + @classmethod + def poll(cls, context): + return current_addon_exists() + + def draw(self, context): + layout = self.layout + addon_path = get_current_addon_path() + + layout.operator("code_autocomplete.save_files", icon = "SAVE_COPY") + self.draw_directory(layout, addon_path) + + def draw_directory(self, layout, directory): + is_visible = self.is_directory_visible(directory) + box = layout.box() + + row = box.row(align = True) + + icon = "DOWNARROW_HLT" if is_visible else "RIGHTARROW" + props = row.operator("code_autocomplete.set_directory_visibility", text = os.path.split(directory[:-1])[-1], icon = icon, emboss = False) + props.directory = directory + props.visibility = not is_visible + + subrow = row.row(align = True) + subrow.active = False + props = subrow.operator("code_autocomplete.open_external_file_browser", text = "", icon = "FILESEL", emboss = False) + props.directory = directory + + if is_visible: + col = box.column(align = True) + directory_names = get_directory_names(directory) + for directory_name in directory_names: + row = col.row() + self.draw_directory(row, directory + directory_name + os.sep) + + file_names = get_file_names(directory) + col = box.column(align = True) + for file_name in file_names: + row = col.row() + row.alignment = "LEFT" + full_path = directory + file_name + props = row.operator("code_autocomplete.open_file_menu", icon = "COLLAPSEMENU", text = "", emboss = True) + props.path = full_path + if full_path == get_current_filepath(): + row.label("", icon = "RIGHTARROW_THIN") + operator = row.operator("code_autocomplete.open_file", text = file_name, emboss = False) + operator.path = full_path + + row = box.row(align = True) + operator = row.operator("code_autocomplete.new_file", icon = "PLUS", text = "File") + operator.directory = directory + operator = row.operator("code_autocomplete.new_directory", icon = "PLUS", text = "Directory") + operator.directory = directory + + def is_directory_visible(self, directory): + return directory_visibility[directory] + +class SetDirectoryVisibility(bpy.types.Operator): + bl_idname = "code_autocomplete.set_directory_visibility" + bl_label = "Toogle Directory Visibility" + bl_description = "" + bl_options = {"REGISTER"} + + directory: StringProperty(name = "Directory", default = "") + visibility: BoolProperty(name = "Visibility", default = True) + + def execute(self, context): + global directory_visibility + directory_visibility[self.directory] = self.visibility + return {"FINISHED"} diff --git a/addon_development/restart_blender.py b/addon_development/restart_blender.py index 7b76b74..1a0b6c7 100644 --- a/addon_development/restart_blender.py +++ b/addon_development/restart_blender.py @@ -1,92 +1,92 @@ -import os -import bpy -import sys -from bpy.app.handlers import persistent -from . utils import get_addon_name, get_settings -from . panels import directory_visibility - -class RestartBlender(bpy.types.Operator): - bl_idname = "code_autocomplete.restart_blender" - bl_label = "Restart Blender" - bl_description = "Close and open a new Blender instance to test the Addon on the startup file. (Currently only supported for windows)" - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return sys.platform == "win32" - - def invoke(self, context, event): - return context.window_manager.invoke_confirm(self, event) - - def execute(self, context): - bpy.ops.code_autocomplete.save_files() - save_status() - start_another_blender_instance() - bpy.ops.wm.quit_blender() - return {"FINISHED"} - - -# Save current settings to reload them in the new instance -########################################################## - -restart_data_path = os.path.join(os.path.dirname(__file__), "restart_data.txt") - -id_addon_name = "ADDON_NAME: " -id_current_path = "CURRENT_PATH: " -id_visiblie_path = "VISIBLE_PATH: " - -def save_status(): - file = open(restart_data_path, "w") - file.write(id_addon_name + get_addon_name() + "\n") - text_block = bpy.context.space_data.text - if text_block: - file.write(id_current_path + text_block.filepath + "\n") - for path, is_open in directory_visibility.items(): - if is_open: - file.write(id_visiblie_path + path + "\n") - - file.close() - -@persistent -def open_status(scene): - if os.path.exists(restart_data_path): - file = open(restart_data_path) - lines = file.readlines() - file.close() - os.remove(restart_data_path) - parse_startup_file_lines(lines) - -bpy.app.handlers.load_post.append(open_status) - -def parse_startup_file_lines(lines): - for line in lines: - if line.startswith(id_addon_name): - get_settings().addon_name = line[len(id_addon_name):].strip() - if line.startswith(id_current_path): - path = line[len(id_current_path):].strip() - if os.path.exists(path): - text_block = bpy.data.texts.load(path, internal = False) - for screen in bpy.data.screens: - for area in screen.areas: - for space in area.spaces: - if space.type == "TEXT_EDITOR": - space.text = text_block - if line.startswith(id_visiblie_path): - path = line[len(id_visiblie_path):].strip() - bpy.ops.code_autocomplete.set_directory_visibility(directory = path, visibility = True) - - - -# Restart Blender -########################## - -def start_another_blender_instance(): - open_file(bpy.app.binary_path) - -# only works for windows currently -def open_file(path): - if sys.platform == "win32": - os.startfile(path) - else: - opener = "open" if sys.platform == "darwin" else "xdg-open" - subprocess.call([opener, path]) +import os +import bpy +import sys +from bpy.app.handlers import persistent +from . utils import get_addon_name, get_settings +from . panels import directory_visibility + +class RestartBlender(bpy.types.Operator): + bl_idname = "code_autocomplete.restart_blender" + bl_label = "Restart Blender" + bl_description = "Close and open a new Blender instance to test the Addon on the startup file. (Currently only supported for windows)" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return sys.platform == "win32" + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + bpy.ops.code_autocomplete.save_files() + save_status() + start_another_blender_instance() + bpy.ops.wm.quit_blender() + return {"FINISHED"} + + +# Save current settings to reload them in the new instance +########################################################## + +restart_data_path = os.path.join(os.path.dirname(__file__), "restart_data.txt") + +id_addon_name = "ADDON_NAME: " +id_current_path = "CURRENT_PATH: " +id_visiblie_path = "VISIBLE_PATH: " + +def save_status(): + file = open(restart_data_path, "w") + file.write(id_addon_name + get_addon_name() + "\n") + text_block = bpy.context.space_data.text + if text_block: + file.write(id_current_path + text_block.filepath + "\n") + for path, is_open in directory_visibility.items(): + if is_open: + file.write(id_visiblie_path + path + "\n") + + file.close() + +@persistent +def open_status(scene): + if os.path.exists(restart_data_path): + file = open(restart_data_path) + lines = file.readlines() + file.close() + os.remove(restart_data_path) + parse_startup_file_lines(lines) + +bpy.app.handlers.load_post.append(open_status) + +def parse_startup_file_lines(lines): + for line in lines: + if line.startswith(id_addon_name): + get_settings().addon_name = line[len(id_addon_name):].strip() + if line.startswith(id_current_path): + path = line[len(id_current_path):].strip() + if os.path.exists(path): + text_block = bpy.data.texts.load(path, internal = False) + for screen in bpy.data.screens: + for area in screen.areas: + for space in area.spaces: + if space.type == "TEXT_EDITOR": + space.text = text_block + if line.startswith(id_visiblie_path): + path = line[len(id_visiblie_path):].strip() + bpy.ops.code_autocomplete.set_directory_visibility(directory = path, visibility = True) + + + +# Restart Blender +########################## + +def start_another_blender_instance(): + open_file(bpy.app.binary_path) + +# only works for windows currently +def open_file(path): + if sys.platform == "win32": + os.startfile(path) + else: + opener = "open" if sys.platform == "darwin" else "xdg-open" + subprocess.call([opener, path]) diff --git a/addon_development/run_addon.py b/addon_development/run_addon.py index ce581a2..d199cfd 100644 --- a/addon_development/run_addon.py +++ b/addon_development/run_addon.py @@ -1,26 +1,26 @@ -import bpy -import addon_utils -import importlib -import sys -from . utils import current_addon_exists, get_addon_name - -class RunAddon(bpy.types.Operator): - bl_idname = "code_autocomplete.run_addon" - bl_label = "Run Addon" - bl_description = "Unregister, reload and register it again." - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return current_addon_exists() - - def execute(self, context): - bpy.ops.code_autocomplete.save_files() - - addon_name = get_addon_name() - module = sys.modules.get(addon_name) - if module: - addon_utils.disable(addon_name) - importlib.reload(module) - addon_utils.enable(addon_name) - return {"FINISHED"} +import bpy +import addon_utils +import importlib +import sys +from . utils import current_addon_exists, get_addon_name + +class RunAddon(bpy.types.Operator): + bl_idname = "code_autocomplete.run_addon" + bl_label = "Run Addon" + bl_description = "Unregister, reload and register it again." + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return current_addon_exists() + + def execute(self, context): + bpy.ops.code_autocomplete.save_files() + + addon_name = get_addon_name() + module = sys.modules.get(addon_name) + if module: + addon_utils.disable(addon_name) + importlib.reload(module) + addon_utils.enable(addon_name) + return {"FINISHED"} diff --git a/addon_development/utils.py b/addon_development/utils.py index 53f308b..6b139e7 100644 --- a/addon_development/utils.py +++ b/addon_development/utils.py @@ -1,67 +1,67 @@ -import bpy -import os - -addons_path = bpy.utils.user_resource("SCRIPTS", "addons") - -def get_current_filepath(): - try: return bpy.context.space_data.text.filepath - except: return "" - -def current_addon_exists(): - return os.path.exists(get_current_addon_path()) and get_settings().addon_name != "" - -def get_current_addon_path(): - return os.path.join(addons_path, get_addon_name()) + os.sep - -def is_addon_name_valid(): - name = get_addon_name() - return name == correct_file_name(name, is_directory = True) and name != "" - -def get_addon_name(): - return get_settings().addon_name - -def get_settings(): - return bpy.context.scene.addon_development - - -def new_addon_file(path, default = ""): - new_file(get_current_addon_path() + path, default) - -def new_file(path, default = ""): - dirname = os.path.dirname(path) - new_directory(dirname) - if not os.path.exists(path): - file = open(path, "a") - file.write(default) - file.close() - -def new_directory(path): - if not os.path.exists(path): - os.makedirs(path) - - -def correct_file_name(name, is_directory = False): - new_name = "" - for char in name: - if char.isupper(): - new_name += char.lower() - elif char.islower() or char == "_": - new_name += char - elif char == " ": - new_name += "_" - elif char.isdigit() and len(new_name) > 0: - new_name += char - elif not is_directory and char == "." and new_name.count(".") == 0: - new_name += char - return new_name - - - -def get_directory_names(directory): - return [name for path, name in get_directory_content(directory) if not os.path.isfile(path)] -def get_file_names(directory): - return [name for path, name in get_directory_content(directory) if os.path.isfile(path)] - -ignore_names = ["__pycache__", ".git"] -def get_directory_content(directory): - return [(os.path.join(directory, name), name) for name in os.listdir(directory) if name not in ignore_names] +import bpy +import os + +addons_path = bpy.utils.user_resource("SCRIPTS", "addons") + +def get_current_filepath(): + try: return bpy.context.space_data.text.filepath + except: return "" + +def current_addon_exists(): + return os.path.exists(get_current_addon_path()) and get_settings().addon_name != "" + +def get_current_addon_path(): + return os.path.join(addons_path, get_addon_name()) + os.sep + +def is_addon_name_valid(): + name = get_addon_name() + return name == correct_file_name(name, is_directory = True) and name != "" + +def get_addon_name(): + return get_settings().addon_name + +def get_settings(): + return bpy.context.scene.addon_development + + +def new_addon_file(path, default = ""): + new_file(get_current_addon_path() + path, default) + +def new_file(path, default = ""): + dirname = os.path.dirname(path) + new_directory(dirname) + if not os.path.exists(path): + file = open(path, "a") + file.write(default) + file.close() + +def new_directory(path): + if not os.path.exists(path): + os.makedirs(path) + + +def correct_file_name(name, is_directory = False): + new_name = "" + for char in name: + if char.isupper(): + new_name += char.lower() + elif char.islower() or char == "_": + new_name += char + elif char == " ": + new_name += "_" + elif char.isdigit() and len(new_name) > 0: + new_name += char + elif not is_directory and char == "." and new_name.count(".") == 0: + new_name += char + return new_name + + + +def get_directory_names(directory): + return [name for path, name in get_directory_content(directory) if not os.path.isfile(path)] +def get_file_names(directory): + return [name for path, name in get_directory_content(directory) if os.path.isfile(path)] + +ignore_names = ["__pycache__", ".git"] +def get_directory_content(directory): + return [(os.path.join(directory, name), name) for name in os.listdir(directory) if name not in ignore_names] diff --git a/autocompletion/__pycache__/__init__.cpython-37.pyc b/autocompletion/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..32167ab Binary files /dev/null and b/autocompletion/__pycache__/__init__.cpython-37.pyc differ diff --git a/autocompletion/__pycache__/active_text_area.cpython-37.pyc b/autocompletion/__pycache__/active_text_area.cpython-37.pyc new file mode 100644 index 0000000..d62d4be Binary files /dev/null and b/autocompletion/__pycache__/active_text_area.cpython-37.pyc differ diff --git a/autocompletion/__pycache__/autocomplete_handler.cpython-37.pyc b/autocompletion/__pycache__/autocomplete_handler.cpython-37.pyc new file mode 100644 index 0000000..74df131 Binary files /dev/null and b/autocompletion/__pycache__/autocomplete_handler.cpython-37.pyc differ diff --git a/autocompletion/__pycache__/event_utils.cpython-37.pyc b/autocompletion/__pycache__/event_utils.cpython-37.pyc new file mode 100644 index 0000000..a0480f6 Binary files /dev/null and b/autocompletion/__pycache__/event_utils.cpython-37.pyc differ diff --git a/autocompletion/__pycache__/exception.cpython-37.pyc b/autocompletion/__pycache__/exception.cpython-37.pyc new file mode 100644 index 0000000..72eabd8 Binary files /dev/null and b/autocompletion/__pycache__/exception.cpython-37.pyc differ diff --git a/autocompletion/__pycache__/modal_operator.cpython-37.pyc b/autocompletion/__pycache__/modal_operator.cpython-37.pyc new file mode 100644 index 0000000..335e50a Binary files /dev/null and b/autocompletion/__pycache__/modal_operator.cpython-37.pyc differ diff --git a/autocompletion/active_text_area.py b/autocompletion/active_text_area.py index 8bc3c79..360900f 100644 --- a/autocompletion/active_text_area.py +++ b/autocompletion/active_text_area.py @@ -1,50 +1,50 @@ -import bpy -from . event_utils import is_event, get_area_under_event - -class ActiveTextArea: - def __init__(self): - self.x, self.y, self.width, self.height = 0, 0, 0, 0 - - def set_area(self, area): - if not area: return - self.x = area.x - self.y = area.y - self.width = area.width - self.height = area.height - - def get_text(self): - area = self.get() - if area: - space = area.spaces[0] - return getattr(space, "text", None) - - def get(self): - area = self.get_nearest_text_area() - if getattr(area, "type", "") == "TEXT_EDITOR": return area - - def update(self, event): - if is_event(event, "LEFTMOUSE", "PRESS"): - area = get_area_under_event(event) - self.settings_from_area(area) - else: - nearest_area = self.get_nearest_text_area() - self.settings_from_area(nearest_area) - - def settings_from_area(self, area): - if not area: return - self.x = area.x - self.y = area.y - self.width = area.width - self.height = area.height - - def get_nearest_text_area(self): - differences = [(area, self.get_area_difference(area)) for area in bpy.context.screen.areas if area.type == "TEXT_EDITOR"] + [(bpy.context.area, 10000)] - return min(differences, key = lambda x: x[1])[0] - - def get_area_difference(self, area): - difference = 0 - difference += abs(area.x - self.x) - difference += abs(area.y - self.y) - difference += abs(area.width - self.width) - difference += abs(area.height - self.height) - return difference +import bpy +from . event_utils import is_event, get_area_under_event + +class ActiveTextArea: + def __init__(self): + self.x, self.y, self.width, self.height = 0, 0, 0, 0 + + def set_area(self, area): + if not area: return + self.x = area.x + self.y = area.y + self.width = area.width + self.height = area.height + + def get_text(self): + area = self.get() + if area: + space = area.spaces[0] + return getattr(space, "text", None) + + def get(self): + area = self.get_nearest_text_area() + if getattr(area, "type", "") == "TEXT_EDITOR": return area + + def update(self, event): + if is_event(event, "LEFTMOUSE", "PRESS"): + area = get_area_under_event(event) + self.settings_from_area(area) + else: + nearest_area = self.get_nearest_text_area() + self.settings_from_area(nearest_area) + + def settings_from_area(self, area): + if not area: return + self.x = area.x + self.y = area.y + self.width = area.width + self.height = area.height + + def get_nearest_text_area(self): + differences = [(area, self.get_area_difference(area)) for area in bpy.context.screen.areas if area.type == "TEXT_EDITOR"] + [(bpy.context.area, 10000)] + return min(differences, key = lambda x: x[1])[0] + + def get_area_difference(self, area): + difference = 0 + difference += abs(area.x - self.x) + difference += abs(area.y - self.y) + difference += abs(area.width - self.width) + difference += abs(area.height - self.height) + return difference diff --git a/autocompletion/autocomplete_handler.py b/autocompletion/autocomplete_handler.py index 7f67e29..ed4ef11 100644 --- a/autocompletion/autocomplete_handler.py +++ b/autocompletion/autocomplete_handler.py @@ -1,242 +1,242 @@ -import bpy -import re -from mathutils import Vector -from . exception import BlockEvent -from . suggestions import complete -from .. settings import get_preferences -from .. graphics.text_box import TextBox -from .. graphics.utils import getDpiFactor -from .. graphics.list_box import ListItem, ListBox -from . event_utils import (is_event, is_event_in_list, - is_mouse_click, get_mouse_region_position, - is_event_over_area) - -move_index_commands = { - "DOWN_ARROW" : 1, - "UP_ARROW" : -1, - "PAGE_DOWN" : 4, - "PAGE_UP" : -4, - "END" : 10000, - "HOME" : -10000 } - -def is_event_changing_the_text(event): - if len(event.unicode) > 0: return True - if is_event_in_list(event, ["BACK_SPACE", "RET", "DEL"], "PRESS"): return True - return False - -class AutocompleteHandler: - def __init__(self): - self.context_ui = ContextUI() - self.completions = [] - self.draw_max = 8 - self.top_index = 0 - self.active_index = 0 - self.reload_completions = False - self.hide() - - def update(self, event, text_block, area): - if not is_event_over_area(event, area): return - self.update_settings() - self.check_event_for_insertion(event, text_block) - self.update_visibility(event, text_block) - if self.is_hidden: return - - if is_event_changing_the_text(event): - self.reload_completions = True - - if self.completions_amount > 0: - self.move_active_index(event) - - def update_settings(self): - self.draw_max = get_preferences().context_box.lines - - def check_event_for_insertion(self, event, text_block): - def insert_with_keyboard(): - if is_event_in_list(event, ("TAB", "RET")): - self.insert_completion(text_block, self.completions[self.active_index]) - raise BlockEvent() - - def insert_with_mouse(): - if not is_event(event, "LEFTMOUSE"): return - item = self.context_ui.get_item_under_event(event) - if item: - self.insert_completion(text_block, item.data) - raise BlockEvent() - - if self.completions_amount == 0: return - if self.is_hidden: return - insert_with_keyboard() - insert_with_mouse() - - def insert_completion(self, text_block, completion): - completion.insert(text_block) - if completion.finished_statement: self.hide() - self.active_index = 0 - - def update_visibility(self, event, text_block): - if is_mouse_click(event): - return self.hide() - - if is_event(event, "ESC", shift = True): - return self.show() - - # open after removing after . or ' or " - if is_event_in_list(event, ["BACK_SPACE", "DEL"], "PRESS"): - line = text_block.text_before_cursor - if len(line) > 0: - if line[-1] in "\"\'.": - return self.show() - - if is_event_in_list(event, ["BACK_SPACE", "DEL", "ESC", "RET", "LEFT_ARROW", "RIGHT_ARROW"], "PRESS"): - return self.hide() - - char = event.unicode.lower() - if len(char) > 0 and not event.alt: - if char in "abcdefghijklmnopqrstuvwxyz0123456789_({[\\/=@.": - return self.show() - if char in ":": - return self.hide() - - # open with string declaration start and close with its end - line = text_block.text_before_cursor - if char in "\"": - if line.count("\"") % 2 == 0: return self.show() - else: return self.hide() - if char in '\'': - if line.count('\'') % 2 == 0: return self.show() - else: return self.hide() - - # open with space in import statement and after comma - if is_event(event, "SPACE"): - line = text_block.text_before_cursor - if re.search("(import|from)\s*\.?\s*$", line) or re.search("(,|=)\s*$", line): - return self.show() - else: - return self.hide() - - def show(self): - if self.is_hidden: - self.is_hidden = False - self.reload_completions = True - - def hide(self): - self.is_hidden = True - - def move_active_index(self, event): - def move_with_keyboard(): - for key, amount in move_index_commands.items(): - if is_event(event, key): - self.change_active_index(amount) - raise BlockEvent() - def move_with_mouse(): - if not self.context_ui.event_over_context_box(event): return - if is_event(event, "WHEELUPMOUSE"): - self.change_active_index(-1) - raise BlockEvent() - if is_event(event, "WHEELDOWNMOUSE"): - self.change_active_index(1) - raise BlockEvent() - move_with_keyboard() - move_with_mouse() - - def change_active_index(self, amount): - self.active_index += amount - self.correct_selection_indices() - - def update_completions(self, text_block): - self.completions = complete(text_block) - self.correct_selection_indices() - - def correct_selection_indices(self): - index = self.active_index - if index < 0: - index = 0 - if index >= self.completions_amount: - index = self.completions_amount - 1 - if index < self.top_index: - self.top_index = index - if index > self.bottom_index: - self.top_index = index - self.draw_max + 1 - if self.completions_amount < self.draw_max: - self.top_index = 0 - self.active_index = index - - - def draw(self, text_block): - if self.is_hidden: return - - if self.reload_completions: - self.update_completions(text_block) - self.reload_completions = False - - items = self.get_display_items() - self.context_ui.update_settings() - self.context_ui.insert_items(items) - self.context_ui.draw(text_block) - - def get_display_items(self): - items = [] - for i, c in enumerate(self.completions): - if not self.top_index <= i < self.top_index + self.draw_max: continue - item = ListItem(c.name) - item.active = self.active_index == i - item.data = c - item.offset = 10 * getDpiFactor() if c.type.endswith("PARAMETER") else 0 - items.append(item) - return items - - @property - def completions_amount(self): - return len(self.completions) - - @property - def bottom_index(self): - return self.top_index + self.draw_max - 1 - - -class ContextUI: - def __init__(self): - self.context_box = ListBox() - self.description_box = TextBox() - - def update_settings(self): - settings = get_preferences() - - s = settings.context_box - self.context_box.font_size = s.font_size - self.context_box.line_height = s.line_height * getDpiFactor() - self.context_box.width = s.width * getDpiFactor() - self.context_box.padding = s.padding * getDpiFactor() - - s = settings.description_box - self.description_box.font_size = s.font_size - self.description_box.line_height = s.line_height * getDpiFactor() - self.description_box.padding = s.padding * getDpiFactor() - - def insert_items(self, items): - self.context_box.items = items - active_item = self.get_active_item() - if active_item: self.description_box.text = active_item.data.description - else: self.description_box.text = "" - - def get_active_item(self): - for item in self.context_box.items: - if item.active: return item - - def draw(self, text_block): - cursor = text_block.current_cursor_region_location - self.context_box.position = cursor.copy() - self.description_box.position = cursor + Vector((self.context_box.width + 10, 0)) - - if len(self.context_box.items) > 0: - self.context_box.draw() - if len(self.description_box.text) > 0: - self.description_box.draw() - - def event_over_context_box(self, event): - point = get_mouse_region_position(event) - return self.context_box.contains(point) - - def get_item_under_event(self, event): - point = get_mouse_region_position(event) - return self.context_box.get_item_under_point(point) +import bpy +import re +from mathutils import Vector +from . exception import BlockEvent +from . suggestions import complete +from .. settings import get_preferences +from .. graphics.text_box import TextBox +from .. graphics.utils import getDpiFactor +from .. graphics.list_box import ListItem, ListBox +from . event_utils import (is_event, is_event_in_list, + is_mouse_click, get_mouse_region_position, + is_event_over_area) + +move_index_commands = { + "DOWN_ARROW" : 1, + "UP_ARROW" : -1, + "PAGE_DOWN" : 4, + "PAGE_UP" : -4, + "END" : 10000, + "HOME" : -10000 } + +def is_event_changing_the_text(event): + if len(event.unicode) > 0: return True + if is_event_in_list(event, ["BACK_SPACE", "RET", "DEL"], "PRESS"): return True + return False + +class AutocompleteHandler: + def __init__(self): + self.context_ui = ContextUI() + self.completions = [] + self.draw_max = 8 + self.top_index = 0 + self.active_index = 0 + self.reload_completions = False + self.hide() + + def update(self, event, text_block, area): + if not is_event_over_area(event, area): return + self.update_settings() + self.check_event_for_insertion(event, text_block) + self.update_visibility(event, text_block) + if self.is_hidden: return + + if is_event_changing_the_text(event): + self.reload_completions = True + + if self.completions_amount > 0: + self.move_active_index(event) + + def update_settings(self): + self.draw_max = get_preferences().context_box.lines + + def check_event_for_insertion(self, event, text_block): + def insert_with_keyboard(): + if is_event_in_list(event, ("TAB", "RET")): + self.insert_completion(text_block, self.completions[self.active_index]) + raise BlockEvent() + + def insert_with_mouse(): + if not is_event(event, "LEFTMOUSE"): return + item = self.context_ui.get_item_under_event(event) + if item: + self.insert_completion(text_block, item.data) + raise BlockEvent() + + if self.completions_amount == 0: return + if self.is_hidden: return + insert_with_keyboard() + insert_with_mouse() + + def insert_completion(self, text_block, completion): + completion.insert(text_block) + if completion.finished_statement: self.hide() + self.active_index = 0 + + def update_visibility(self, event, text_block): + if is_mouse_click(event): + return self.hide() + + if is_event(event, "ESC", shift = True): + return self.show() + + # open after removing after . or ' or " + if is_event_in_list(event, ["BACK_SPACE", "DEL"], "PRESS"): + line = text_block.text_before_cursor + if len(line) > 0: + if line[-1] in "\"\'.": + return self.show() + + if is_event_in_list(event, ["BACK_SPACE", "DEL", "ESC", "RET", "LEFT_ARROW", "RIGHT_ARROW"], "PRESS"): + return self.hide() + + char = event.unicode.lower() + if len(char) > 0 and not event.alt: + if char in "abcdefghijklmnopqrstuvwxyz0123456789_({[\\/=@.": + return self.show() + if char in ":": + return self.hide() + + # open with string declaration start and close with its end + line = text_block.text_before_cursor + if char in "\"": + if line.count("\"") % 2 == 0: return self.show() + else: return self.hide() + if char in '\'': + if line.count('\'') % 2 == 0: return self.show() + else: return self.hide() + + # open with space in import statement and after comma + if is_event(event, "SPACE"): + line = text_block.text_before_cursor + if re.search("(import|from)\s*\.?\s*$", line) or re.search("(,|=)\s*$", line): + return self.show() + else: + return self.hide() + + def show(self): + if self.is_hidden: + self.is_hidden = False + self.reload_completions = True + + def hide(self): + self.is_hidden = True + + def move_active_index(self, event): + def move_with_keyboard(): + for key, amount in move_index_commands.items(): + if is_event(event, key): + self.change_active_index(amount) + raise BlockEvent() + def move_with_mouse(): + if not self.context_ui.event_over_context_box(event): return + if is_event(event, "WHEELUPMOUSE"): + self.change_active_index(-1) + raise BlockEvent() + if is_event(event, "WHEELDOWNMOUSE"): + self.change_active_index(1) + raise BlockEvent() + move_with_keyboard() + move_with_mouse() + + def change_active_index(self, amount): + self.active_index += amount + self.correct_selection_indices() + + def update_completions(self, text_block): + self.completions = complete(text_block) + self.correct_selection_indices() + + def correct_selection_indices(self): + index = self.active_index + if index < 0: + index = 0 + if index >= self.completions_amount: + index = self.completions_amount - 1 + if index < self.top_index: + self.top_index = index + if index > self.bottom_index: + self.top_index = index - self.draw_max + 1 + if self.completions_amount < self.draw_max: + self.top_index = 0 + self.active_index = index + + + def draw(self, text_block): + if self.is_hidden: return + + if self.reload_completions: + self.update_completions(text_block) + self.reload_completions = False + + items = self.get_display_items() + self.context_ui.update_settings() + self.context_ui.insert_items(items) + self.context_ui.draw(text_block) + + def get_display_items(self): + items = [] + for i, c in enumerate(self.completions): + if not self.top_index <= i < self.top_index + self.draw_max: continue + item = ListItem(c.name) + item.active = self.active_index == i + item.data = c + item.offset = 10 * getDpiFactor() if c.type.endswith("PARAMETER") else 0 + items.append(item) + return items + + @property + def completions_amount(self): + return len(self.completions) + + @property + def bottom_index(self): + return self.top_index + self.draw_max - 1 + + +class ContextUI: + def __init__(self): + self.context_box = ListBox() + self.description_box = TextBox() + + def update_settings(self): + settings = get_preferences() + + s = settings.context_box + self.context_box.font_size = s.font_size + self.context_box.line_height = s.line_height * getDpiFactor() + self.context_box.width = s.width * getDpiFactor() + self.context_box.padding = s.padding * getDpiFactor() + + s = settings.description_box + self.description_box.font_size = s.font_size + self.description_box.line_height = s.line_height * getDpiFactor() + self.description_box.padding = s.padding * getDpiFactor() + + def insert_items(self, items): + self.context_box.items = items + active_item = self.get_active_item() + if active_item: self.description_box.text = active_item.data.description + else: self.description_box.text = "" + + def get_active_item(self): + for item in self.context_box.items: + if item.active: return item + + def draw(self, text_block): + cursor = text_block.current_cursor_region_location + self.context_box.position = cursor.copy() + self.description_box.position = cursor + Vector((self.context_box.width + 10, 0)) + + if len(self.context_box.items) > 0: + self.context_box.draw() + if len(self.description_box.text) > 0: + self.description_box.draw() + + def event_over_context_box(self, event): + point = get_mouse_region_position(event) + return self.context_box.contains(point) + + def get_item_under_event(self, event): + point = get_mouse_region_position(event) + return self.context_box.get_item_under_point(point) diff --git a/autocompletion/event_utils.py b/autocompletion/event_utils.py index f4a03fe..dd69bdb 100644 --- a/autocompletion/event_utils.py +++ b/autocompletion/event_utils.py @@ -1,40 +1,40 @@ -import bpy -from mathutils import Vector - -def get_mouse_region_position(event): - return Vector((event.mouse_region_x, event.mouse_region_y)) - -def is_event(event, type, value = "PRESS", shift = False, ctrl = False, alt = False): - if event.type in ("LEFT_SHIFT", "RIGHT_SHIFT"): shift = True - if event.type in ("LEFT_CTRL", "RIGHT_CTRL"): ctrl = True - if event.type in ("LEFT_ALT", "RIGHT_ALT"): alt = True - if shift == "ANY": shift = event.shift - if ctrl == "ANY": ctrl = event.ctrl - if alt == "ANY": alt = event.alt - - return event.type == type and \ - event.value == value and \ - event.shift == shift and \ - event.ctrl == ctrl and \ - event.alt == alt - -def is_event_in_list(event, types, value = "PRESS", shift = False, ctrl = False, alt = False): - if not event.type in types: return - return is_event(event, event.type, value, shift, ctrl, alt) - -def is_mouse_click(event): - return is_event_in_list(event, ("LEFTMOUSE", "RIGHTMOUSE")) - -def get_area_under_event(event): - for area in bpy.context.screen.areas: - if is_event_over_area(event, area): return area - return None - -def is_event_over_area(event, area): - for region in area.regions: - if is_event_over_region(event, region): return True - return False - -def is_event_over_region(event, region): - return region.x <= event.mouse_x <= region.x + region.width and \ - region.y <= event.mouse_y <= region.y + region.height +import bpy +from mathutils import Vector + +def get_mouse_region_position(event): + return Vector((event.mouse_region_x, event.mouse_region_y)) + +def is_event(event, type, value = "PRESS", shift = False, ctrl = False, alt = False): + if event.type in ("LEFT_SHIFT", "RIGHT_SHIFT"): shift = True + if event.type in ("LEFT_CTRL", "RIGHT_CTRL"): ctrl = True + if event.type in ("LEFT_ALT", "RIGHT_ALT"): alt = True + if shift == "ANY": shift = event.shift + if ctrl == "ANY": ctrl = event.ctrl + if alt == "ANY": alt = event.alt + + return event.type == type and \ + event.value == value and \ + event.shift == shift and \ + event.ctrl == ctrl and \ + event.alt == alt + +def is_event_in_list(event, types, value = "PRESS", shift = False, ctrl = False, alt = False): + if not event.type in types: return + return is_event(event, event.type, value, shift, ctrl, alt) + +def is_mouse_click(event): + return is_event_in_list(event, ("LEFTMOUSE", "RIGHTMOUSE")) + +def get_area_under_event(event): + for area in bpy.context.screen.areas: + if is_event_over_area(event, area): return area + return None + +def is_event_over_area(event, area): + for region in area.regions: + if is_event_over_region(event, region): return True + return False + +def is_event_over_region(event, region): + return region.x <= event.mouse_x <= region.x + region.width and \ + region.y <= event.mouse_y <= region.y + region.height diff --git a/autocompletion/exception.py b/autocompletion/exception.py index 1acf90e..61e23ff 100644 --- a/autocompletion/exception.py +++ b/autocompletion/exception.py @@ -1,5 +1,5 @@ -# Reload this module in the beginning -__reload_order_index__ = -100 - -class BlockEvent(Exception): - pass +# Reload this module in the beginning +__reload_order_index__ = -100 + +class BlockEvent(Exception): + pass diff --git a/autocompletion/modal_operator.py b/autocompletion/modal_operator.py index 8156b92..4f77a7d 100644 --- a/autocompletion/modal_operator.py +++ b/autocompletion/modal_operator.py @@ -1,127 +1,127 @@ -import bpy -from . exception import BlockEvent -from . event_utils import is_event -from .. text_block import TextBlock -from .. settings import get_preferences -from . active_text_area import ActiveTextArea -from . autocomplete_handler import AutocompleteHandler -from . suggestions.jedi_completion import jedi_module_found -from . suggestions.generate_fake_bpy import fake_bpy_module_exists - -is_running = False -active_text_area = ActiveTextArea() - -class Autocomplete(bpy.types.Panel): - bl_idname = "autocomplete" - bl_label = "Autocomplete" - bl_space_type = "TEXT_EDITOR" - bl_region_type = "UI" - - def draw(self, context): - layout = self.layout - - - row = layout.row(align = True) - if not is_running: - row.operator("code_autocomplete.start_modal_operator", text = "Start") - else: - row.operator("code_autocomplete.stop_modal_operator", text = "Stop") - - if fake_bpy_module_exists(): - row.operator("code_autocomplete.regenerate_fake_bpy", text = "", icon = "RECOVER_AUTO") - else: - layout.operator("code_autocomplete.regenerate_fake_bpy", "Build BPY Module", icon = "ERROR") - - if jedi_module_found(): - providers = get_preferences().completion_providers - layout.prop(providers, "use_jedi_completion") - else: - layout.label("Jedi library not found", icon = "ERROR") - - -class StartModalOperator(bpy.types.Operator): - bl_idname = "code_autocomplete.start_modal_operator" - bl_label = "Start Modal Operator" - bl_description = "Activate the autocomplete feature in the text editor" - bl_options = {"REGISTER"} - - def execute(self, context): - bpy.ops.code_autocomplete.modal_text_operator("INVOKE_DEFAULT") - active_text_area.set_area(context.area) - global is_running - is_running = True - return {"FINISHED"} - - -class StopModalOperator(bpy.types.Operator): - bl_idname = "code_autocomplete.stop_modal_operator" - bl_label = "Stop Modal Operator" - bl_description = "Stop the autocompletion in the text editor" - bl_options = {"REGISTER"} - - def execute(self, context): - global is_running - is_running = False - return {"FINISHED"} - - -class ModalTextOperator(bpy.types.Operator): - bl_idname = "code_autocomplete.modal_text_operator" - bl_label = "Modal Text Operator" - bl_description = "" - bl_options = {"REGISTER"} - - def invoke(self, context, event): - args = (self, context) - self._handle = bpy.types.SpaceTextEditor.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") - context.window_manager.modal_handler_add(self) - self.handlers = [AutocompleteHandler()] - return {"RUNNING_MODAL"} - - def modal(self, context, event): - self.redraw_text_editors() - active_text_area.update(event) - - if not is_running or event.type == "F8": - return self.finish() - - return self.update_handlers(event) - - def redraw_text_editors(self): - for area in bpy.context.screen.areas: - if area.type == "TEXT_EDITOR": - area.tag_redraw() - - def update_handlers(self, event): - text_block = self.get_text_block() - area = active_text_area.get() - if not text_block: return {"PASS_THROUGH"} - - try: - for handler in self.handlers: - handler.update(event, text_block, area) - return {"PASS_THROUGH"} - except BlockEvent: - if get_preferences().debug: print("Event blocked: {} - {}".format(event.type, event.value)) - return {"RUNNING_MODAL"} - - def finish(self): - bpy.types.SpaceTextEditor.draw_handler_remove(self._handle, "WINDOW") - if get_preferences().debug: print("Finished modal text operator") - return {"FINISHED"} - - def draw_callback_px(tmp, self, context): - if context.area == active_text_area.get(): - text_block = self.get_text_block() - if not text_block: return - - for handler in self.handlers: - handler.draw(text_block) - - def get_text_block(self): - text = active_text_area.get_text() - area = active_text_area.get() - if text: - text_block = TextBlock(text) - text_block.set_context(area = area, space = area.spaces[0]) - return text_block +import bpy +from . exception import BlockEvent +from . event_utils import is_event +from .. text_block import TextBlock +from .. settings import get_preferences +from . active_text_area import ActiveTextArea +from . autocomplete_handler import AutocompleteHandler +from . suggestions.jedi_completion import jedi_module_found +from . suggestions.generate_fake_bpy import fake_bpy_module_exists + +is_running = False +active_text_area = ActiveTextArea() + +class Autocomplete(bpy.types.Panel): + bl_idname = "autocomplete" + bl_label = "Autocomplete" + bl_space_type = "TEXT_EDITOR" + bl_region_type = "UI" + + def draw(self, context): + layout = self.layout + + + row = layout.row(align = True) + if not is_running: + row.operator("code_autocomplete.start_modal_operator", text = "Start") + else: + row.operator("code_autocomplete.stop_modal_operator", text = "Stop") + + if fake_bpy_module_exists(): + row.operator("code_autocomplete.regenerate_fake_bpy", text = "", icon = "RECOVER_AUTO") + else: + layout.operator("code_autocomplete.regenerate_fake_bpy", "Build BPY Module", icon = "ERROR") + + if jedi_module_found(): + providers = get_preferences().completion_providers + layout.prop(providers, "use_jedi_completion") + else: + layout.label(text="Jedi library not found", icon = "ERROR") + + +class StartModalOperator(bpy.types.Operator): + bl_idname = "code_autocomplete.start_modal_operator" + bl_label = "Start Modal Operator" + bl_description = "Activate the autocomplete feature in the text editor" + bl_options = {"REGISTER"} + + def execute(self, context): + bpy.ops.code_autocomplete.modal_text_operator("INVOKE_DEFAULT") + active_text_area.set_area(context.area) + global is_running + is_running = True + return {"FINISHED"} + + +class StopModalOperator(bpy.types.Operator): + bl_idname = "code_autocomplete.stop_modal_operator" + bl_label = "Stop Modal Operator" + bl_description = "Stop the autocompletion in the text editor" + bl_options = {"REGISTER"} + + def execute(self, context): + global is_running + is_running = False + return {"FINISHED"} + + +class ModalTextOperator(bpy.types.Operator): + bl_idname = "code_autocomplete.modal_text_operator" + bl_label = "Modal Text Operator" + bl_description = "" + bl_options = {"REGISTER"} + + def invoke(self, context, event): + args = (self, context) + self._handle = bpy.types.SpaceTextEditor.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") + context.window_manager.modal_handler_add(self) + self.handlers = [AutocompleteHandler()] + return {"RUNNING_MODAL"} + + def modal(self, context, event): + self.redraw_text_editors() + active_text_area.update(event) + + if not is_running or event.type == "F8": + return self.finish() + + return self.update_handlers(event) + + def redraw_text_editors(self): + for area in bpy.context.screen.areas: + if area.type == "TEXT_EDITOR": + area.tag_redraw() + + def update_handlers(self, event): + text_block = self.get_text_block() + area = active_text_area.get() + if not text_block: return {"PASS_THROUGH"} + + try: + for handler in self.handlers: + handler.update(event, text_block, area) + return {"PASS_THROUGH"} + except BlockEvent: + if get_preferences().debug: print("Event blocked: {} - {}".format(event.type, event.value)) + return {"RUNNING_MODAL"} + + def finish(self): + bpy.types.SpaceTextEditor.draw_handler_remove(self._handle, "WINDOW") + if get_preferences().debug: print("Finished modal text operator") + return {"FINISHED"} + + def draw_callback_px(tmp, self, context): + if context.area == active_text_area.get(): + text_block = self.get_text_block() + if not text_block: return + + for handler in self.handlers: + handler.draw(text_block) + + def get_text_block(self): + text = active_text_area.get_text() + area = active_text_area.get() + if text: + text_block = TextBlock(text) + text_block.set_context(area = area, space = area.spaces[0]) + return text_block diff --git a/autocompletion/suggestions/__init__.py b/autocompletion/suggestions/__init__.py index bf9e9b7..f8ee377 100644 --- a/autocompletion/suggestions/__init__.py +++ b/autocompletion/suggestions/__init__.py @@ -1,27 +1,27 @@ -from . jedi_completion import JediCompletionProvider -from . word_completion import WordCompletionProvider -from . operator_completion import OperatorCompletionProvider -from . static_pattern_completion import StaticPatternProvider -from ... settings import get_preferences - -jedi_provider = JediCompletionProvider() -word_provider = WordCompletionProvider() -operator_provider = OperatorCompletionProvider() -static_pattern_provider = StaticPatternProvider() - -def complete(text_block): - setting = get_preferences().completion_providers - - completions = [] - completions.extend(static_pattern_provider.complete(text_block)) - if setting.use_operator_completion: completions.extend(operator_provider.complete(text_block)) - if setting.use_jedi_completion: completions.extend(jedi_provider.complete(text_block)) - if setting.use_word_completion: completions.extend(word_provider.complete(text_block)) - - list1, list2, list3 = [], [], [] - for c in completions: - if c.type == "OPERATOR_PARAMETER": list1.append(c) - elif c.type == "PARAMETER": list2.append(c) - else: list3.append(c) - - return list1 + list2 + list3 +from . jedi_completion import JediCompletionProvider +from . word_completion import WordCompletionProvider +from . operator_completion import OperatorCompletionProvider +from . static_pattern_completion import StaticPatternProvider +from ... settings import get_preferences + +jedi_provider = JediCompletionProvider() +word_provider = WordCompletionProvider() +operator_provider = OperatorCompletionProvider() +static_pattern_provider = StaticPatternProvider() + +def complete(text_block): + setting = get_preferences().completion_providers + + completions = [] + completions.extend(static_pattern_provider.complete(text_block)) + if setting.use_operator_completion: completions.extend(operator_provider.complete(text_block)) + if setting.use_jedi_completion: completions.extend(jedi_provider.complete(text_block)) + if setting.use_word_completion: completions.extend(word_provider.complete(text_block)) + + list1, list2, list3 = [], [], [] + for c in completions: + if c.type == "OPERATOR_PARAMETER": list1.append(c) + elif c.type == "PARAMETER": list2.append(c) + else: list3.append(c) + + return list1 + list2 + list3 diff --git a/autocompletion/suggestions/__pycache__/__init__.cpython-37.pyc b/autocompletion/suggestions/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..ce4da78 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/__init__.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/generate_fake_bpy.cpython-37.pyc b/autocompletion/suggestions/__pycache__/generate_fake_bpy.cpython-37.pyc new file mode 100644 index 0000000..0077b8a Binary files /dev/null and b/autocompletion/suggestions/__pycache__/generate_fake_bpy.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/interface.cpython-37.pyc b/autocompletion/suggestions/__pycache__/interface.cpython-37.pyc new file mode 100644 index 0000000..fb5d5d8 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/interface.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/jedi_completion.cpython-37.pyc b/autocompletion/suggestions/__pycache__/jedi_completion.cpython-37.pyc new file mode 100644 index 0000000..af80e06 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/jedi_completion.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/operator_completion.cpython-37.pyc b/autocompletion/suggestions/__pycache__/operator_completion.cpython-37.pyc new file mode 100644 index 0000000..e70b487 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/operator_completion.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/rna_utils.cpython-37.pyc b/autocompletion/suggestions/__pycache__/rna_utils.cpython-37.pyc new file mode 100644 index 0000000..314f176 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/rna_utils.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/static_pattern_completion.cpython-37.pyc b/autocompletion/suggestions/__pycache__/static_pattern_completion.cpython-37.pyc new file mode 100644 index 0000000..4dcdff3 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/static_pattern_completion.cpython-37.pyc differ diff --git a/autocompletion/suggestions/__pycache__/word_completion.cpython-37.pyc b/autocompletion/suggestions/__pycache__/word_completion.cpython-37.pyc new file mode 100644 index 0000000..8941d35 Binary files /dev/null and b/autocompletion/suggestions/__pycache__/word_completion.cpython-37.pyc differ diff --git a/autocompletion/suggestions/generate_fake_bpy.py b/autocompletion/suggestions/generate_fake_bpy.py index 336bd8d..a72f120 100644 --- a/autocompletion/suggestions/generate_fake_bpy.py +++ b/autocompletion/suggestions/generate_fake_bpy.py @@ -1,374 +1,374 @@ -import bpy -import os -import sys -import shutil -import inspect -import textwrap -from ... settings import get_preferences -from . rna_utils import get_readable_property_type - -fake_package_name = "_bpy_fake" -top_directory = os.path.join(os.path.dirname(__file__), "dynamic") -directory = os.path.join(top_directory, fake_package_name) -private_path = os.path.join(directory, "__private__") - -sys.path.append(top_directory) - -docstring_width = 70 -use_quote_marks = False - - -class GenerateFakeBPY(bpy.types.Operator): - bl_idname = "code_autocomplete.regenerate_fake_bpy" - bl_label = "Generate Fake BPY" - bl_description = "Regenerate the fake bpy module that the jedi autocompletion needs" - bl_options = {"REGISTER"} - - def execute(self, context): - regenerate_fake_bpy() - return {"FINISHED"} - -def fake_bpy_module_exists(): - return os.path.exists(private_path) - -def regenerate_fake_bpy(): - remove_old_fake() - generate_fake_bpy() - -def remove_old_fake(): - if os.path.exists(directory): - shutil.rmtree(directory, ignore_errors = True) - -def generate_fake_bpy(): - try: os.makedirs(directory) - except: pass - create_init() - create_private_subdirectory() - -def create_init(): - path = os.path.join(directory, "__init__.py") - file = open(path, "w+", encoding='utf-8') - file.write(init_content) - file.close() - -init_content = ''' -from . __private__.context import Context as context -from . __private__.blenddata import BlendData as data -''' - -def create_private_subdirectory(): - os.makedirs(private_path) - open(os.path.join(private_path, "__init__.py"), "a").close() - write_code_file("__init__", "") - write_code_file("bpy_struct", bpy_struct_content) - generate_code_files() - - -collection_types = {} -def generate_code_files(create_all = False): - types_to_generate = {"Context", "Panel"} - generated_types = set() - - while len(types_to_generate) > 0: - name = types_to_generate.pop() - generated_types.add(name) - type = getattr(bpy.types, name) - code, dependencies = get_code_and_dependencies(name, type) - write_code_file(name, code) - types_to_generate.update([d for d in dependencies if d not in generated_types]) - - if len(types_to_generate) == 0 and create_all: - bpy_types = [(name, type) for name, type in inspect.getmembers(bpy.types) if "." not in name] - for name, type in bpy_types: - if name not in generated_types: - types_to_generate.add(name) - - -def get_code_and_dependencies(name, type): - dependencies = get_dependencies(name, type) - - lines = [] - lines.extend(get_import_code_lines(dependencies)) - lines.append("class {}({}):".format(name, "" if name == "Context" else "bpy_struct")) - lines.extend(get_property_code_lines(type)) - lines.extend(get_function_code_lines(name, type)) - - return "\n".join(lines), dependencies - -def get_import_code_lines(dependencies): - return ["from . {} import {}".format(d.lower(), d) for d in dependencies] + ["from . bpy_struct import bpy_struct", "import mathutils", ""] - -def get_property_code_lines(type): - lines = [] - for property in get_type_properties(type): - lines.extend(get_property_definition_code_lines(property)) - return lines - -def get_property_definition_code_lines(property): - lines = [] - lines.append(" @property") - lines.append(" def {}(self):".format(property.identifier)) - lines.extend(get_property_docstring_lines(property, docstring_width)) - lines.append(" return {}".format(get_property_declaration(property))) - return lines - -def get_function_code_lines(name, type): - lines = [] - for function in type.bl_rna.functions: - lines.append(" def {}({}):".format(function.identifier, get_function_parameter_list(function))) - lines.extend(get_function_docstring_lines(function, docstring_width)) - lines.append(" return {}".format(get_function_return_list(function))) - - global collection_types - if name in collection_types: - subtype = collection_types[name] - lines.append(" def get(key): return {}()".format(subtype)) - lines.append(" def __getitem__(key): return {}()".format(subtype)) - lines.append(" def __iter__(key): yield {}()".format(subtype)) - - return lines - -def get_property_docstring_lines(property, width = 70, indent = 8): - lines = get_property_description_lines(property, width) - lines.extend(get_enum_item_lines(property, width)) - return make_docstring_from_lines(lines, indent) - -def get_function_docstring_lines(function, width = 70, indent = 8): - lines = get_function_description_lines(function, width) - parameter_lines = get_parameter_lines(function, width) - lines.extend(parameter_lines) - return make_docstring_from_lines(lines, indent) - -def get_parameter_lines(function, width): - lines = [] - params = [p for p in function.parameters if not p.is_output] - if len(params) > 0: - lines.append("") - lines.append("Parameter:") - lines.extend(get_parameter_list_lines(params, width)) - returns = [p for p in function.parameters if p.is_output] - if len(returns) > 0: - lines.append("") - lines.append("Returns:") - lines.extend(get_parameter_list_lines(returns, width)) - return lines - -def get_parameter_list_lines(params, width): - lines = [] - for param in params: - lines.append("{}:".format(param.identifier)) - description_lines = get_property_description_lines(param, width) - amount = len(description_lines) - if amount == 0: lines[-1] += " " - elif amount == 1: lines[-1] += " " + description_lines[0] - else: - indent_lines(description_lines, 2) - lines.extend(description_lines) - indent_lines(lines, 2) - return lines - -def get_property_description_lines(property, width): - type = "({})".format(get_readable_property_type(property)) - if property.description in (None, ""): return [type] - return textwrap.wrap(type + " " + property.description, width) - -def get_function_description_lines(function, width): - if function.description in (None, ""): return [] - return textwrap.wrap(function.description, width) - -def get_enum_item_lines(property, width): - if getattr(property, "enum_items", None) is None: return [] - items = property.enum_items - if len(items) == 0: return [] - quote_mark = "'" if use_quote_marks else "" - item_string = "["+ ", ".join(quote_mark + item.identifier + quote_mark for item in items) +"]" - return [""] + textwrap.wrap(item_string, width) - -def make_docstring_from_lines(lines, indent = 8): - if len(lines) == 0: return [] - lines[0] = "'''" + lines[0] - lines[-1] += "'''" - indent_lines(lines, indent) - return lines - -def indent_lines(lines, indent = 4): - spaces = " " * indent - for i in range(len(lines)): - lines[i] = spaces + lines[i] - -def get_dependencies(name, type): - def find_property_dependency(property): - if property.type == "POINTER": - dependencies.add(property.fixed_type.identifier) - if property.type == "COLLECTION": - if property.srna is None: dependencies.add(property.fixed_type.identifier) - else: dependencies.add(property.srna.identifier) - - dependencies = set() - if name in collection_types: - dependencies.add(collection_types[name]) - for property in get_type_properties(type): - find_property_dependency(property) - for function in type.bl_rna.functions: - for parameter in function.parameters: - find_property_dependency(parameter) - return dependencies - -def get_type_properties(type): - fakes = [] - if type.bl_rna.identifier == "Context": - fakes = fake_context_properties - return list(type.bl_rna.properties) + fakes - -def get_function_parameter_list(function): - parameters = ["self"] + [parameter.identifier for parameter in function.parameters if not parameter.is_output] - return ", ".join(parameters) - -def get_function_return_list(function): - returns = [parameter for parameter in function.parameters if parameter.is_output] - return ", ".join([get_property_declaration(parameter) for parameter in returns]) - -def get_property_declaration(property): - global collection_types - if property.type == "BOOLEAN": return "bool()" - if property.type == "INT": return "int()" - if property.type in ("STRING", "ENUM"): return "str()" - if property.type == "COLLECTION": - if property.srna is None: return "({}(),)".format(property.fixed_type.identifier) - else: - collection_types[property.srna.identifier] = property.fixed_type.identifier - return property.srna.identifier + "()" - if property.type == "FLOAT": - if property.array_length <= 1: return "float()" - if property.array_length in (2, 3): return "mathutils.Vector()" - if property.array_length == 16: return "mathutils.Matrix()" - if property.type == "POINTER": - return property.fixed_type.identifier + "()" - return "''" - -def write_code_file(name, code): - path = os.path.join(private_path, name.lower() + ".py") - file = open(path, "w+", encoding='utf-8') - file.write(code) - file.close() - - - - -bpy_struct_content = ''' -from . fcurve import FCurve - -class bpy_struct: - id_data = ID() - def as_pointer(): - return int() - def driver_add(path, index): - return FCurve() - def driver_remove(path, index): - return bool() - def keyframe_delete(data_path, index, frame, group): - return bool() - def keyframe_insert(data_path, index, frame, group): - return bool() - def path_from_id(property): - return str() - def path_resolve(path, coerce): - return - def property_unsert(property): - return -''' - -class FakeProp: - def __init__(self, identifier): - self.identifier = identifier - def __getattr__(self, name): - if name == "type": return "" - if name == "array_length": return 0 - if name == "srna": return None - if name == "fixed_type": return None - -class FakePointer(FakeProp): - def __init__(self, name, fixed_identifier): - self.identifier = name - self.type = "POINTER" - self.fixed_type = FakeProp(fixed_identifier) - -class FakeSequence(FakeProp): - def __init__(self, name, fixed_identifier): - self.identifier = name - self.type = "COLLECTION" - self.fixed_type = FakeProp(fixed_identifier) - -FP = FakePointer -FS = FakeSequence - -fake_context_properties = [ - FP("active_bone", "EditBone"), - FP("active_pose_bone", "PoseBone"), - FP("active_base", "ObjectBase"), - FP("active_object", "Object"), - FP("object", "Object"), - FP("edit_object", "Object"), - FP("sculpt_object", "Object"), - FP("vertex_paint_object", "Object"), - FP("weight_paint_object", "Object"), - FP("image_paint_object", "Object"), - FP("particle_edit_object", "Object"), - FP("gpencil_data", "GreasePencil"), - FP("gpencil_data_owner", "ID"), - FP("active_operator", "Operator"), - FP("texture_slot", "MaterialTextureSlot"), - FP("world", "World"), - FP("mesh", "Mesh"), - FP("armature", "Armature"), - FP("lattice", "Lattice"), - FP("curve", "Curve"), - FP("meta_ball", "MetaBall"), - FP("lamp", "Lamp"), - FP("speaker", "Speaker"), - FP("camera", "Camera"), - FP("material", "Material"), - FP("material_slot", "MaterialSlot"), - FP("texture", "Texture"), - FP("texture_user", "ID"), - FP("texture_user_property", "Property"), - FP("bone", "Bone"), - FP("particle_system", "ParticleSystem"), - FP("particle_system_editable", "ParticleSystem"), - FP("particle_settings", "ParticleSettings"), - FP("cloth", "ClothModifier"), - FP("soft_body", "SoftBodyModifier"), - FP("fluid", "FluidSimulationModifier"), - FP("smoke", "SmokeModifier"), - FP("collision", "CollisionModifier"), - FP("brush", "Brush"), - FP("dynamic_paint", "DynamicPaintModifier"), - FP("line_style", "FreestyleLineStyle"), - FP("edit_image", "Image"), - FP("edit_mask", "Mask"), - FP("active_node", "Node"), - FP("edit_text", "Text"), - FP("edit_movieclip", "MovieClip"), - FS("visible_objects", "Object"), - FS("visible_bases", "ObjectBase"), - FS("selectable_objects", "Object"), - FS("selectable_bases", "ObjectBase"), - FS("selected_objects", "Object"), - FS("selected_bases", "ObjectBase"), - FS("selected_editable_objects", "Object"), - FS("selected_editable_bases", "ObjectBase"), - FS("visible_bones", "EditBone"), - FS("editable_bones", "EditBone"), - FS("selected_bones", "EditBone"), - FS("selected_editable_bones", "EditBone"), - FS("visible_pose_bones", "PoseBone"), - FS("selected_pose_bones", "PoseBone"), - FS("sequences", "Sequence"), - FS("selected_sequences", "Sequence"), - FS("selected_editable_sequences", "Sequence"), - FS("visible_gpencil_layers", "GPencilLayer"), - FS("editable_gpencil_layers", "GPencilLayer"), - FS("editable_gpencil_strokes", "GPencilStroke"), - FS("active_gpencil_layer", "GPencilLayer"), - FS("active_gpencil_frame", "GPencilLayer"), - FS("selected_nodes", "Node") ] +import bpy +import os +import sys +import shutil +import inspect +import textwrap +from ... settings import get_preferences +from . rna_utils import get_readable_property_type + +fake_package_name = "_bpy_fake" +top_directory = os.path.join(os.path.dirname(__file__), "dynamic") +directory = os.path.join(top_directory, fake_package_name) +private_path = os.path.join(directory, "__private__") + +sys.path.append(top_directory) + +docstring_width = 70 +use_quote_marks = False + + +class GenerateFakeBPY(bpy.types.Operator): + bl_idname = "code_autocomplete.regenerate_fake_bpy" + bl_label = "Generate Fake BPY" + bl_description = "Regenerate the fake bpy module that the jedi autocompletion needs" + bl_options = {"REGISTER"} + + def execute(self, context): + regenerate_fake_bpy() + return {"FINISHED"} + +def fake_bpy_module_exists(): + return os.path.exists(private_path) + +def regenerate_fake_bpy(): + remove_old_fake() + generate_fake_bpy() + +def remove_old_fake(): + if os.path.exists(directory): + shutil.rmtree(directory, ignore_errors = True) + +def generate_fake_bpy(): + try: os.makedirs(directory) + except: pass + create_init() + create_private_subdirectory() + +def create_init(): + path = os.path.join(directory, "__init__.py") + file = open(path, "w+") + file.write(init_content) + file.close() + +init_content = ''' +from . __private__.context import Context as context +from . __private__.blenddata import BlendData as data +''' + +def create_private_subdirectory(): + os.makedirs(private_path) + open(os.path.join(private_path, "__init__.py"), "a").close() + write_code_file("__init__", "") + write_code_file("bpy_struct", bpy_struct_content) + generate_code_files() + + +collection_types = {} +def generate_code_files(create_all = False): + types_to_generate = {"Context", "Panel"} + generated_types = set() + + while len(types_to_generate) > 0: + name = types_to_generate.pop() + generated_types.add(name) + type = getattr(bpy.types, name) + code, dependencies = get_code_and_dependencies(name, type) + write_code_file(name, code) + types_to_generate.update([d for d in dependencies if d not in generated_types]) + + if len(types_to_generate) == 0 and create_all: + bpy_types = [(name, type) for name, type in inspect.getmembers(bpy.types) if "." not in name] + for name, type in bpy_types: + if name not in generated_types: + types_to_generate.add(name) + + +def get_code_and_dependencies(name, type): + dependencies = get_dependencies(name, type) + + lines = [] + lines.extend(get_import_code_lines(dependencies)) + lines.append("class {}({}):".format(name, "" if name == "Context" else "bpy_struct")) + lines.extend(get_property_code_lines(type)) + lines.extend(get_function_code_lines(name, type)) + + return "\n".join(lines), dependencies + +def get_import_code_lines(dependencies): + return ["from . {} import {}".format(d.lower(), d) for d in dependencies] + ["from . bpy_struct import bpy_struct", "import mathutils", ""] + +def get_property_code_lines(type): + lines = [] + for property in get_type_properties(type): + lines.extend(get_property_definition_code_lines(property)) + return lines + +def get_property_definition_code_lines(property): + lines = [] + lines.append(" @property") + lines.append(" def {}(self):".format(property.identifier)) + lines.extend(get_property_docstring_lines(property, docstring_width)) + lines.append(" return {}".format(get_property_declaration(property))) + return lines + +def get_function_code_lines(name, type): + lines = [] + for function in type.bl_rna.functions: + lines.append(" def {}({}):".format(function.identifier, get_function_parameter_list(function))) + lines.extend(get_function_docstring_lines(function, docstring_width)) + lines.append(" return {}".format(get_function_return_list(function))) + + global collection_types + if name in collection_types: + subtype = collection_types[name] + lines.append(" def get(key): return {}()".format(subtype)) + lines.append(" def __getitem__(key): return {}()".format(subtype)) + lines.append(" def __iter__(key): yield {}()".format(subtype)) + + return lines + +def get_property_docstring_lines(property, width = 70, indent = 8): + lines = get_property_description_lines(property, width) + lines.extend(get_enum_item_lines(property, width)) + return make_docstring_from_lines(lines, indent) + +def get_function_docstring_lines(function, width = 70, indent = 8): + lines = get_function_description_lines(function, width) + parameter_lines = get_parameter_lines(function, width) + lines.extend(parameter_lines) + return make_docstring_from_lines(lines, indent) + +def get_parameter_lines(function, width): + lines = [] + params = [p for p in function.parameters if not p.is_output] + if len(params) > 0: + lines.append("") + lines.append("Parameter:") + lines.extend(get_parameter_list_lines(params, width)) + returns = [p for p in function.parameters if p.is_output] + if len(returns) > 0: + lines.append("") + lines.append("Returns:") + lines.extend(get_parameter_list_lines(returns, width)) + return lines + +def get_parameter_list_lines(params, width): + lines = [] + for param in params: + lines.append("{}:".format(param.identifier)) + description_lines = get_property_description_lines(param, width) + amount = len(description_lines) + if amount == 0: lines[-1] += " " + elif amount == 1: lines[-1] += " " + description_lines[0] + else: + indent_lines(description_lines, 2) + lines.extend(description_lines) + indent_lines(lines, 2) + return lines + +def get_property_description_lines(property, width): + type = "({})".format(get_readable_property_type(property)) + if property.description in (None, ""): return [type] + return textwrap.wrap(type + " " + property.description, width) + +def get_function_description_lines(function, width): + if function.description in (None, ""): return [] + return textwrap.wrap(function.description, width) + +def get_enum_item_lines(property, width): + if getattr(property, "enum_items", None) is None: return [] + items = property.enum_items + if len(items) == 0: return [] + quote_mark = "'" if use_quote_marks else "" + item_string = "["+ ", ".join(quote_mark + item.identifier + quote_mark for item in items) +"]" + return [""] + textwrap.wrap(item_string, width) + +def make_docstring_from_lines(lines, indent = 8): + if len(lines) == 0: return [] + lines[0] = "'''" + lines[0] + lines[-1] += "'''" + indent_lines(lines, indent) + return lines + +def indent_lines(lines, indent = 4): + spaces = " " * indent + for i in range(len(lines)): + lines[i] = spaces + lines[i] + +def get_dependencies(name, type): + def find_property_dependency(property): + if property.type == "POINTER": + dependencies.add(property.fixed_type.identifier) + if property.type == "COLLECTION": + if property.srna is None: dependencies.add(property.fixed_type.identifier) + else: dependencies.add(property.srna.identifier) + + dependencies = set() + if name in collection_types: + dependencies.add(collection_types[name]) + for property in get_type_properties(type): + find_property_dependency(property) + for function in type.bl_rna.functions: + for parameter in function.parameters: + find_property_dependency(parameter) + return dependencies + +def get_type_properties(type): + fakes = [] + if type.bl_rna.identifier == "Context": + fakes = fake_context_properties + return list(type.bl_rna.properties) + fakes + +def get_function_parameter_list(function): + parameters = ["self"] + [parameter.identifier for parameter in function.parameters if not parameter.is_output] + return ", ".join(parameters) + +def get_function_return_list(function): + returns = [parameter for parameter in function.parameters if parameter.is_output] + return ", ".join([get_property_declaration(parameter) for parameter in returns]) + +def get_property_declaration(property): + global collection_types + if property.type == "BOOLEAN": return "bool()" + if property.type == "INT": return "int()" + if property.type in ("STRING", "ENUM"): return "str()" + if property.type == "COLLECTION": + if property.srna is None: return "({}(),)".format(property.fixed_type.identifier) + else: + collection_types[property.srna.identifier] = property.fixed_type.identifier + return property.srna.identifier + "()" + if property.type == "FLOAT": + if property.array_length <= 1: return "float()" + if property.array_length in (2, 3): return "mathutils.Vector()" + if property.array_length == 16: return "mathutils.Matrix()" + if property.type == "POINTER": + return property.fixed_type.identifier + "()" + return "''" + +def write_code_file(name, code): + path = os.path.join(private_path, name.lower() + ".py") + file = open(path, "w+") + file.write(code) + file.close() + + + + +bpy_struct_content = ''' +from . fcurve import FCurve + +class bpy_struct: + id_data = ID() + def as_pointer(): + return int() + def driver_add(path, index): + return FCurve() + def driver_remove(path, index): + return bool() + def keyframe_delete(data_path, index, frame, group): + return bool() + def keyframe_insert(data_path, index, frame, group): + return bool() + def path_from_id(property): + return str() + def path_resolve(path, coerce): + return + def property_unsert(property): + return +''' + +class FakeProp: + def __init__(self, identifier): + self.identifier = identifier + def __getattr__(self, name): + if name == "type": return "" + if name == "array_length": return 0 + if name == "srna": return None + if name == "fixed_type": return None + +class FakePointer(FakeProp): + def __init__(self, name, fixed_identifier): + self.identifier = name + self.type = "POINTER" + self.fixed_type = FakeProp(fixed_identifier) + +class FakeSequence(FakeProp): + def __init__(self, name, fixed_identifier): + self.identifier = name + self.type = "COLLECTION" + self.fixed_type = FakeProp(fixed_identifier) + +FP = FakePointer +FS = FakeSequence + +fake_context_properties = [ + FP("active_bone", "EditBone"), + FP("active_pose_bone", "PoseBone"), + FP("active_base", "ObjectBase"), + FP("active_object", "Object"), + FP("object", "Object"), + FP("edit_object", "Object"), + FP("sculpt_object", "Object"), + FP("vertex_paint_object", "Object"), + FP("weight_paint_object", "Object"), + FP("image_paint_object", "Object"), + FP("particle_edit_object", "Object"), + FP("gpencil_data", "GreasePencil"), + FP("gpencil_data_owner", "ID"), + FP("active_operator", "Operator"), + FP("texture_slot", "MaterialTextureSlot"), + FP("world", "World"), + FP("mesh", "Mesh"), + FP("armature", "Armature"), + FP("lattice", "Lattice"), + FP("curve", "Curve"), + FP("meta_ball", "MetaBall"), + FP("light", "Light"), + FP("speaker", "Speaker"), + FP("camera", "Camera"), + FP("material", "Material"), + FP("material_slot", "MaterialSlot"), + FP("texture", "Texture"), + FP("texture_user", "ID"), + FP("texture_user_property", "Property"), + FP("bone", "Bone"), + FP("particle_system", "ParticleSystem"), + FP("particle_system_editable", "ParticleSystem"), + FP("particle_settings", "ParticleSettings"), + FP("cloth", "ClothModifier"), + FP("soft_body", "SoftBodyModifier"), + FP("fluid", "FluidSimulationModifier"), + FP("smoke", "SmokeModifier"), + FP("collision", "CollisionModifier"), + FP("brush", "Brush"), + FP("dynamic_paint", "DynamicPaintModifier"), + FP("line_style", "FreestyleLineStyle"), + FP("edit_image", "Image"), + FP("edit_mask", "Mask"), + FP("active_node", "Node"), + FP("edit_text", "Text"), + FP("edit_movieclip", "MovieClip"), + FS("visible_objects", "Object"), + FS("visible_bases", "ObjectBase"), + FS("selectable_objects", "Object"), + FS("selectable_bases", "ObjectBase"), + FS("selected_objects", "Object"), + FS("selected_bases", "ObjectBase"), + FS("selected_editable_objects", "Object"), + FS("selected_editable_bases", "ObjectBase"), + FS("visible_bones", "EditBone"), + FS("editable_bones", "EditBone"), + FS("selected_bones", "EditBone"), + FS("selected_editable_bones", "EditBone"), + FS("visible_pose_bones", "PoseBone"), + FS("selected_pose_bones", "PoseBone"), + FS("sequences", "Sequence"), + FS("selected_sequences", "Sequence"), + FS("selected_editable_sequences", "Sequence"), + FS("visible_gpencil_layers", "GPencilLayer"), + FS("editable_gpencil_layers", "GPencilLayer"), + FS("editable_gpencil_strokes", "GPencilStroke"), + FS("active_gpencil_layer", "GPencilLayer"), + FS("active_gpencil_frame", "GPencilLayer"), + FS("selected_nodes", "Node") ] diff --git a/autocompletion/suggestions/interface.py b/autocompletion/suggestions/interface.py index b959115..a9e2d62 100644 --- a/autocompletion/suggestions/interface.py +++ b/autocompletion/suggestions/interface.py @@ -1,17 +1,17 @@ -class Provider: - def complete(self, text_block): - return [] - -class Completion: - def __getattr__(self, name): - if name == "name": - return "" - if name == "description": - return "" - if name == "type": - return "UNKNOWN" - if name == "finished_statement": - return True - - def insert(self, text_block): - pass +class Provider: + def complete(self, text_block): + return [] + +class Completion: + def __getattr__(self, name): + if name == "name": + return "" + if name == "description": + return "" + if name == "type": + return "UNKNOWN" + if name == "finished_statement": + return True + + def insert(self, text_block): + pass diff --git a/autocompletion/suggestions/jedi_completion.py b/autocompletion/suggestions/jedi_completion.py index 95341e2..f7df1d8 100644 --- a/autocompletion/suggestions/jedi_completion.py +++ b/autocompletion/suggestions/jedi_completion.py @@ -1,71 +1,71 @@ -import re -from . interface import Provider, Completion -from . generate_fake_bpy import fake_package_name - -try: import jedi -except: print("jedi library not found") - -def jedi_module_found(): - return "jedi" in globals() - -class JediCompletion(Completion): - def __init__(self, suggestion): - self.name = suggestion.name - self.description = suggestion.docstring() - if suggestion.type == "function": self.type = "FUNCTION" - if suggestion.type == "class": self.type = "CLASS" - if suggestion.type == "param": - self.type = "PARAMETER" - - def insert(self, text_block): - text_block.replace_current_word(self.name) - - -class JediCompletionProvider(Provider): - def complete(self, text_block): - source, line_index, character_index, filepath = get_completion_source(text_block) - - # jedi raises an error when trying to complete parts of the bpy module - # or when the jedi module is not found - try: - script = jedi.Script(source, line_index, character_index, filepath) - completions = script.completions() - ignored_words = (fake_package_name, "_bpy_path") - return [JediCompletion(c) for c in completions if c.name not in ignored_words] - except: - return [] - -def get_completion_source(text_block): - new_lines = [] - corrected_line_number = 0 - for line_number, line in enumerate(text_block.iter_lines()): - new_lines.extend(list(iter_corrected_lines_from_line(line))) - if line_number == text_block.current_line_index: - corrected_line_number = len(new_lines) - text = "\n".join(new_lines) - - filepath = text_block.filepath - character_index = len(text_block.text_before_cursor) - return text, corrected_line_number, character_index, filepath - -def iter_corrected_lines_from_line(line): - if "bpy" in line: - line = line.replace("import bpy", "import {} as bpy".format(fake_package_name)) - line = line.replace("from bpy", "from {}".format(fake_package_name)) - yield line - - if "def draw(self, context):" in line: - indentation = line.index("d") + 4 - yield " " * indentation + "context = bpy.__private__.context.Context()" - yield " " * indentation + "self.layout = bpy.__private__.uilayout.UILayout()" - - if "def execute(self, context):" in line or \ - "def poll(cls, context):" in line: - indentation = line.index("d") + 4 - yield " " * indentation + "context = bpy.__private__.context.Context()" - - if "def invoke(self, context, event):" in line or \ - "def modal(self, context, event):" in line: - indentation = line.index("d") + 4 - yield " " * indentation + "context = bpy.__private__.context.Context()" - yield " " * indentation + "event = bpy.__private__.event.Event()" +import re +from . interface import Provider, Completion +from . generate_fake_bpy import fake_package_name + +try: import jedi +except: print("jedi library not found") + +def jedi_module_found(): + return "jedi" in globals() + +class JediCompletion(Completion): + def __init__(self, suggestion): + self.name = suggestion.name + self.description = suggestion.docstring() + if suggestion.type == "function": self.type = "FUNCTION" + if suggestion.type == "class": self.type = "CLASS" + if suggestion.type == "param": + self.type = "PARAMETER" + + def insert(self, text_block): + text_block.replace_current_word(self.name) + + +class JediCompletionProvider(Provider): + def complete(self, text_block): + source, line_index, character_index, filepath = get_completion_source(text_block) + + # jedi raises an error when trying to complete parts of the bpy module + # or when the jedi module is not found + try: + script = jedi.Script(source, line_index, character_index, filepath) + completions = script.completions() + ignored_words = (fake_package_name, "_bpy_path") + return [JediCompletion(c) for c in completions if c.name not in ignored_words] + except: + return [] + +def get_completion_source(text_block): + new_lines = [] + corrected_line_number = 0 + for line_number, line in enumerate(text_block.iter_lines()): + new_lines.extend(list(iter_corrected_lines_from_line(line))) + if line_number == text_block.current_line_index: + corrected_line_number = len(new_lines) + text = "\n".join(new_lines) + + filepath = text_block.filepath + character_index = len(text_block.text_before_cursor) + return text, corrected_line_number, character_index, filepath + +def iter_corrected_lines_from_line(line): + if "bpy" in line: + line = line.replace("import bpy", "import {} as bpy".format(fake_package_name)) + line = line.replace("from bpy", "from {}".format(fake_package_name)) + yield line + + if "def draw(self, context):" in line: + indentation = line.index("d") + 4 + yield " " * indentation + "context = bpy.__private__.context.Context()" + yield " " * indentation + "self.layout = bpy.__private__.uilayout.UILayout()" + + if "def execute(self, context):" in line or \ + "def poll(cls, context):" in line: + indentation = line.index("d") + 4 + yield " " * indentation + "context = bpy.__private__.context.Context()" + + if "def invoke(self, context, event):" in line or \ + "def modal(self, context, event):" in line: + indentation = line.index("d") + 4 + yield " " * indentation + "context = bpy.__private__.context.Context()" + yield " " * indentation + "event = bpy.__private__.event.Event()" diff --git a/autocompletion/suggestions/operator_completion.py b/autocompletion/suggestions/operator_completion.py index 84bb645..0264324 100644 --- a/autocompletion/suggestions/operator_completion.py +++ b/autocompletion/suggestions/operator_completion.py @@ -1,125 +1,125 @@ -import re -import bpy -import textwrap -from . interface import Provider, Completion -from . rna_utils import (get_enum_items, - get_property_default, - get_enum_items_string, - get_operator_parameters, - make_operator_description, - get_readable_property_type) - - -class WordCompletion(Completion): - def __init__(self, word): - self.name = word - - def insert(self, text_block): - text_block.replace_current_word(self.name) - -class OperatorCompletion(Completion): - def __init__(self, category, operator_name): - self.operator = getattr(category, operator_name) - self.name = operator_name - - def insert(self, text_block): - text_block.replace_current_word(self.name) - - @property - def description(self): - return make_operator_description(self.operator) - -class ParameterCompletion(Completion): - def __init__(self, parameter): - self.name = parameter.identifier + " = " - self.type = "OPERATOR_PARAMETER" - self.description = "{} ({}) = {}\n\n{}\n{}".format( - parameter.name, - get_readable_property_type(parameter), - get_property_default(parameter), - parameter.description, - get_enum_items_string(parameter, 70)) - - def insert(self, text_block): - text_block.replace_current_word(self.name) - - -class OperatorCompletionProvider(Provider): - def complete(self, text_block): - current_word = text_block.current_word - parents = text_block.parents_of_current_word - operator = get_current_operator(text_block) - - if parents[:1] == ["bpy"]: - if len(parents) == 1 and "ops".startswith(current_word): - return [WordCompletion("ops")] - - operator = get_current_operator(text_block) - if operator is not None: - return list(iter_operator_inner_completions(operator, text_block)) - - completions = [] - completions.extend(iter_operator_completion_after_pattern(text_block, "bpy\.ops\.")) - completions.extend(iter_operator_completion_after_pattern(text_block, "\.operator\((\"|\')")) - completions.extend(iter_operator_completion_after_pattern(text_block, "keymap_items\.new\((\"|\')")) - return completions - -# pattern#text#.#move# -def iter_operator_completion_after_pattern(text_block, pattern = ""): - operator_start = text_block.get_current_text_after_pattern(pattern) - if operator_start is None: return - if "." not in operator_start: - yield from get_category_completions(operator_start) - else: - category, operator_name_start = operator_start.split(".", maxsplit = 1)[:2] - yield from iter_operator_completions(operator_name_start, category_name = category) - -# bpy.ops.#text# -def get_category_completions(current_word): - return [WordCompletion(category) for category in dir(bpy.ops) if category.startswith(current_word)] - -# bpy.ops.text.#move# -def iter_operator_completions(current_word, category_name): - category = getattr(bpy.ops, category_name, None) - if category is None: return - for operator_name in dir(category): - if current_word not in operator_name: continue - yield OperatorCompletion(category, operator_name) - -def get_current_operator(text_block): - function_path = text_block.get_current_function_path() - if function_path is None: return None - - parts = function_path.split(".") - if len(parts) != 4: return None - if not function_path.startswith("bpy.ops"): return None - category_name, operator_name = parts[2:] - category = getattr(bpy.ops, category_name, None) - if category is None: return None - operator = getattr(category, operator_name, None) - return operator - -# bpy.ops.text.move(#type# = "#NEXT_CHARACTER#") -def iter_operator_inner_completions(operator, text_block): - yield from iter_parameter_completions(operator, text_block) - yield from iter_enum_parameter_completions(operator, text_block) - -# bpy.ops.text.move(#type# = "NEXT_CHARACTER") -def iter_parameter_completions(operator, text_block): - word_start = text_block.get_current_text_after_pattern("[\(\,]\s*") - if word_start is None: return - for parameter in get_operator_parameters(operator): - if word_start in parameter.identifier: - yield ParameterCompletion(parameter) - -# bpy.ops.text.move(type = "#NEXT_CHARACTER#") -def iter_enum_parameter_completions(operator, text_block): - for parameter in get_operator_parameters(operator): - pattern = parameter.identifier + "\s*=\s*(\"|\')" - word_start = text_block.get_current_text_after_pattern(pattern) - if word_start is None: continue - word_start = word_start.upper() - for enum_item in get_enum_items(parameter): - if word_start in enum_item.upper(): - completion = WordCompletion(enum_item) - yield completion +import re +import bpy +import textwrap +from . interface import Provider, Completion +from . rna_utils import (get_enum_items, + get_property_default, + get_enum_items_string, + get_operator_parameters, + make_operator_description, + get_readable_property_type) + + +class WordCompletion(Completion): + def __init__(self, word): + self.name = word + + def insert(self, text_block): + text_block.replace_current_word(self.name) + +class OperatorCompletion(Completion): + def __init__(self, category, operator_name): + self.operator = getattr(category, operator_name) + self.name = operator_name + + def insert(self, text_block): + text_block.replace_current_word(self.name) + + @property + def description(self): + return make_operator_description(self.operator) + +class ParameterCompletion(Completion): + def __init__(self, parameter): + self.name = parameter.identifier + " = " + self.type = "OPERATOR_PARAMETER" + self.description = "{} ({}) = {}\n\n{}\n{}".format( + parameter.name, + get_readable_property_type(parameter), + get_property_default(parameter), + parameter.description, + get_enum_items_string(parameter, 70)) + + def insert(self, text_block): + text_block.replace_current_word(self.name) + + +class OperatorCompletionProvider(Provider): + def complete(self, text_block): + current_word = text_block.current_word + parents = text_block.parents_of_current_word + operator = get_current_operator(text_block) + + if parents[:1] == ["bpy"]: + if len(parents) == 1 and "ops".startswith(current_word): + return [WordCompletion("ops")] + + operator = get_current_operator(text_block) + if operator is not None: + return list(iter_operator_inner_completions(operator, text_block)) + + completions = [] + completions.extend(iter_operator_completion_after_pattern(text_block, "bpy\.ops\.")) + completions.extend(iter_operator_completion_after_pattern(text_block, "\.operator\((\"|\')")) + completions.extend(iter_operator_completion_after_pattern(text_block, "keymap_items\.new\((\"|\')")) + return completions + +# pattern#text#.#move# +def iter_operator_completion_after_pattern(text_block, pattern = ""): + operator_start = text_block.get_current_text_after_pattern(pattern) + if operator_start is None: return + if "." not in operator_start: + yield from get_category_completions(operator_start) + else: + category, operator_name_start = operator_start.split(".", maxsplit = 1)[:2] + yield from iter_operator_completions(operator_name_start, category_name = category) + +# bpy.ops.#text# +def get_category_completions(current_word): + return [WordCompletion(category) for category in dir(bpy.ops) if category.startswith(current_word)] + +# bpy.ops.text.#move# +def iter_operator_completions(current_word, category_name): + category = getattr(bpy.ops, category_name, None) + if category is None: return + for operator_name in dir(category): + if current_word not in operator_name: continue + yield OperatorCompletion(category, operator_name) + +def get_current_operator(text_block): + function_path = text_block.get_current_function_path() + if function_path is None: return None + + parts = function_path.split(".") + if len(parts) != 4: return None + if not function_path.startswith("bpy.ops"): return None + category_name, operator_name = parts[2:] + category = getattr(bpy.ops, category_name, None) + if category is None: return None + operator = getattr(category, operator_name, None) + return operator + +# bpy.ops.text.move(#type# = "#NEXT_CHARACTER#") +def iter_operator_inner_completions(operator, text_block): + yield from iter_parameter_completions(operator, text_block) + yield from iter_enum_parameter_completions(operator, text_block) + +# bpy.ops.text.move(#type# = "NEXT_CHARACTER") +def iter_parameter_completions(operator, text_block): + word_start = text_block.get_current_text_after_pattern("[\(\,]\s*") + if word_start is None: return + for parameter in get_operator_parameters(operator): + if word_start in parameter.identifier: + yield ParameterCompletion(parameter) + +# bpy.ops.text.move(type = "#NEXT_CHARACTER#") +def iter_enum_parameter_completions(operator, text_block): + for parameter in get_operator_parameters(operator): + pattern = parameter.identifier + "\s*=\s*(\"|\')" + word_start = text_block.get_current_text_after_pattern(pattern) + if word_start is None: continue + word_start = word_start.upper() + for enum_item in get_enum_items(parameter): + if word_start in enum_item.upper(): + completion = WordCompletion(enum_item) + yield completion diff --git a/autocompletion/suggestions/rna_utils.py b/autocompletion/suggestions/rna_utils.py index 5939064..504fd41 100644 --- a/autocompletion/suggestions/rna_utils.py +++ b/autocompletion/suggestions/rna_utils.py @@ -1,70 +1,70 @@ -import textwrap - -def join_lines(function): - def wrapper(*args, **kwargs): - return "\n".join(list(function(*args, **kwargs))) - return wrapper - -def indent(lines, indentation = 4): - prefix = " " * indentation - if isinstance(lines, str): lines = lines.split("\n") - return [prefix + line for line in lines] - -@join_lines -def make_operator_description(operator, width = 70): - rna = operator.get_rna().bl_rna - yield rna.name - yield "" - yield from textwrap.wrap(rna.description, width) - yield "" - - parameters = get_operator_parameters(operator) - if len(parameters) == 0: return - - yield "Parameters:" - for parameter in parameters: - yield from indent(make_property_description(parameter, width - 3), indentation = 3) - -@join_lines -def make_property_description(property, width = 70): - identifier = "{}: ({})".format(property.identifier, get_readable_property_type(property)) - description = property.description - - if len(identifier + description) + 2 > width: - yield identifier - yield from indent(textwrap.wrap(description, width - 3), indentation = 3) - else: - yield "{} {}".format(identifier, description) - -def get_operator_parameters(operator): - rna = operator.get_rna().bl_rna - return [prop for prop in rna.properties if prop.identifier != "rna_type"] - -def get_enum_items_string(property, width = 70): - items = get_enum_items(property) - if len(items) == 0: return "" - lines = textwrap.wrap(str(items), width) - return "\n".join(lines) - -def get_enum_items(property): - return [item.identifier for item in getattr(property, "enum_items", [])] - -def get_property_default(property): - if len(getattr(property, "default_array", [])) > 0: - return repr(property.default_array[:]) - return repr(getattr(property, "default", None)) - -def get_readable_property_type(property): - suffix = "[{}]".format(property.array_length) if getattr(property, "array_length", 1) > 1 else "" - if property.type == "BOOLEAN": return "Boolean" + suffix - if property.type == "INT": return "Integer" + suffix - if property.type == "STRING": return "String" + suffix - if property.type == "COLLECTION": return "Sequence of " + property.fixed_type.identifier - if property.type == "FLOAT": - if property.array_length <= 1: return "Float" - if property.array_length == 2: return "Vector 2D" - if property.array_length == 3: return "Vector 3D" - if property.array_length == 16: return "Matrix" - return "Float[{}]".format(property.array_length) - if property.type == "POINTER": return property.fixed_type.identifier - if property.type == "ENUM": return "Enum" +import textwrap + +def join_lines(function): + def wrapper(*args, **kwargs): + return "\n".join(list(function(*args, **kwargs))) + return wrapper + +def indent(lines, indentation = 4): + prefix = " " * indentation + if isinstance(lines, str): lines = lines.split("\n") + return [prefix + line for line in lines] + +@join_lines +def make_operator_description(operator, width = 70): + rna = operator.get_rna_type().bl_rna + yield rna.name + yield "" + yield from textwrap.wrap(rna.description, width) + yield "" + + parameters = get_operator_parameters(operator) + if len(parameters) == 0: return + + yield "Parameters:" + for parameter in parameters: + yield from indent(make_property_description(parameter, width - 3), indentation = 3) + +@join_lines +def make_property_description(property, width = 70): + identifier = "{}: ({})".format(property.identifier, get_readable_property_type(property)) + description = property.description + + if len(identifier + description) + 2 > width: + yield identifier + yield from indent(textwrap.wrap(description, width - 3), indentation = 3) + else: + yield "{} {}".format(identifier, description) + +def get_operator_parameters(operator): + rna = operator.get_rna_type().bl_rna + return [prop for prop in rna.properties if prop.identifier != "rna_type"] + +def get_enum_items_string(property, width = 70): + items = get_enum_items(property) + if len(items) == 0: return "" + lines = textwrap.wrap(str(items), width) + return "\n".join(lines) + +def get_enum_items(property): + return [item.identifier for item in getattr(property, "enum_items", [])] + +def get_property_default(property): + if len(getattr(property, "default_array", [])) > 0: + return repr(property.default_array[:]) + return repr(property.default) + +def get_readable_property_type(property): + suffix = "[{}]".format(property.array_length) if getattr(property, "array_length", 1) > 1 else "" + if property.type == "BOOLEAN": return "Boolean" + suffix + if property.type == "INT": return "Integer" + suffix + if property.type == "STRING": return "String" + suffix + if property.type == "COLLECTION": return "Sequence of " + property.fixed_type.identifier + if property.type == "FLOAT": + if property.array_length <= 1: return "Float" + if property.array_length == 2: return "Vector 2D" + if property.array_length == 3: return "Vector 3D" + if property.array_length == 16: return "Matrix" + return "Float[{}]".format(property.array_length) + if property.type == "POINTER": return property.fixed_type.identifier + if property.type == "ENUM": return "Enum" diff --git a/autocompletion/suggestions/static_pattern_completion.py b/autocompletion/suggestions/static_pattern_completion.py index 98f4aab..d9611ec 100644 --- a/autocompletion/suggestions/static_pattern_completion.py +++ b/autocompletion/suggestions/static_pattern_completion.py @@ -1,54 +1,54 @@ -import re -import bpy -from . interface import Provider, Completion - - -class WordCompletion(Completion): - def __init__(self, word): - self.name = word - - def insert(self, text_block): - text_block.replace_current_word(self.name) - -class StaticPatternProvider(Provider): - def complete(self, text_block): - return list(iter_static_completions(text_block)) - -def iter_static_completions(text_block): - for pattern, words in suggestions.items(): - word_start = text_block.get_current_text_after_pattern(pattern) - if word_start is None: continue - word_start = word_start.upper() - - secondaryCompletions = [] - for word in words: - if word.upper().startswith(word_start): - yield WordCompletion(word) - elif word_start in word.upper(): - secondaryCompletions.append(WordCompletion(word)) - yield from secondaryCompletions - -space_properties = bpy.types.Space.bl_rna.properties -space_types = sorted([item.identifier for item in space_properties["type"].enum_items]) - -region_types = ["WINDOW", "HEADER", "CHANNELS", "TEMPORARY", "UI", "TOOLS", "TOOL_PROPS", "PREVIEW"] - -handlers = [name for name in dir(bpy.app.handlers) if not name.startswith("_")] - -event_properties = bpy.types.Event.bl_rna.properties -event_types = sorted([item.identifier for item in event_properties["type"].enum_items]) -event_values = sorted([item.identifier for item in event_properties["value"].enum_items]) - -suggestions = { - "bl_space_type\s*=\s*(\"|\')" : space_types, - "bl_region_type\s*=\s*(\"|\')" : region_types, - "bl_options\s*=.*(,|{)\s*(\"|\')" : ["REGISTER", "UNDO", "BLOCKING", "GRAB_POINTER", "PRESET", "INTERNAL", "DEFAULT_CLOSED", "HIDE_HEADER"], - "bl_category\s*=\s*(\"|\')" : ["Tools", "Create", "Relations", "Animation", "Physics", "Grease Pencil"], - "return\s*\{\s*(\"|\')" : ["RUNNING_MODAL", "CANCELLED", "FINISHED", "PASS_THROUGH"], - "bpy\." : ["context", "data", "ops", "types", "utils", "path", "app", "props"], - "bpy\.app\." : ["handlers", "translations"], - "bpy\.app\.handlers\." : handlers, - "bpy\.props\." : [type_name for type_name in dir(bpy.props) if type_name[0] != "_"], - "keymap_items\.new\(.*, type = (\"|\')" : event_types, - "keymap_items\.new\(.*, value = (\"|\')" : event_values -} +import re +import bpy +from . interface import Provider, Completion + + +class WordCompletion(Completion): + def __init__(self, word): + self.name = word + + def insert(self, text_block): + text_block.replace_current_word(self.name) + +class StaticPatternProvider(Provider): + def complete(self, text_block): + return list(iter_static_completions(text_block)) + +def iter_static_completions(text_block): + for pattern, words in suggestions.items(): + word_start = text_block.get_current_text_after_pattern(pattern) + if word_start is None: continue + word_start = word_start.upper() + + secondaryCompletions = [] + for word in words: + if word.upper().startswith(word_start): + yield WordCompletion(word) + elif word_start in word.upper(): + secondaryCompletions.append(WordCompletion(word)) + yield from secondaryCompletions + +space_properties = bpy.types.Space.bl_rna.properties +space_types = sorted([item.identifier for item in space_properties["type"].enum_items]) + +region_types = ["WINDOW", "HEADER", "CHANNELS", "TEMPORARY", "UI", "UI", "TOOL_PROPS", "PREVIEW"] + +handlers = [name for name in dir(bpy.app.handlers) if not name.startswith("_")] + +event_properties = bpy.types.Event.bl_rna.properties +event_types = sorted([item.identifier for item in event_properties["type"].enum_items]) +event_values = sorted([item.identifier for item in event_properties["value"].enum_items]) + +suggestions = { + "bl_space_type\s*=\s*(\"|\')" : space_types, + "bl_region_type\s*=\s*(\"|\')" : region_types, + "bl_options\s*=.*(,|{)\s*(\"|\')" : ["REGISTER", "UNDO", "BLOCKING", "GRAB_POINTER", "PRESET", "INTERNAL", "DEFAULT_CLOSED", "HIDE_HEADER"], + "bl_category\s*=\s*(\"|\')" : ["Tools", "Create", "Relations", "Animation", "Physics", "Grease Pencil"], + "return\s*\{\s*(\"|\')" : ["RUNNING_MODAL", "CANCELLED", "FINISHED", "PASS_THROUGH"], + "bpy\." : ["context", "data", "ops", "types", "utils", "path", "app", "props"], + "bpy\.app\." : ["handlers", "translations"], + "bpy\.app\.handlers\." : handlers, + "bpy\.props\." : [type_name for type_name in dir(bpy.props) if type_name[0] != "_"], + "keymap_items\.new\(.*, type = (\"|\')" : event_types, + "keymap_items\.new\(.*, value = (\"|\')" : event_values +} diff --git a/autocompletion/suggestions/word_completion.py b/autocompletion/suggestions/word_completion.py index 33f6e9d..e9442cf 100644 --- a/autocompletion/suggestions/word_completion.py +++ b/autocompletion/suggestions/word_completion.py @@ -1,30 +1,30 @@ -import re -from . interface import Provider, Completion - - -class WordCompletion(Completion): - def __init__(self, word): - self.name = word - - def insert(self, text_block): - text_block.replace_current_word(self.name) - - -class WordCompletionProvider(Provider): - def complete(self, text_block): - words = text_block.get_existing_words() - current_word = text_block.current_word - if current_word in words: words.remove(current_word) - words = sort_words(words, current_word) - return [WordCompletion(word) for word in words] - -def sort_words(words, current_word): - current_word = current_word.lower() - best_matches, other_matches = [], [] - words.sort(key = str.lower) - for word in words: - if word.lower().startswith(current_word): - best_matches.append(word) - elif current_word in word.lower(): - other_matches.append(word) - return best_matches + other_matches +import re +from . interface import Provider, Completion + + +class WordCompletion(Completion): + def __init__(self, word): + self.name = word + + def insert(self, text_block): + text_block.replace_current_word(self.name) + + +class WordCompletionProvider(Provider): + def complete(self, text_block): + words = text_block.get_existing_words() + current_word = text_block.current_word + if current_word in words: words.remove(current_word) + words = sort_words(words, current_word) + return [WordCompletion(word) for word in words] + +def sort_words(words, current_word): + current_word = current_word.lower() + best_matches, other_matches = [], [] + words.sort(key = str.lower) + for word in words: + if word.lower().startswith(current_word): + best_matches.append(word) + elif current_word in word.lower(): + other_matches.append(word) + return best_matches + other_matches diff --git a/code_templates/__pycache__/__init__.cpython-37.pyc b/code_templates/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..3d7a056 Binary files /dev/null and b/code_templates/__pycache__/__init__.cpython-37.pyc differ diff --git a/code_templates/__pycache__/base.cpython-37.pyc b/code_templates/__pycache__/base.cpython-37.pyc new file mode 100644 index 0000000..808cbe9 Binary files /dev/null and b/code_templates/__pycache__/base.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_addon_info.cpython-37.pyc b/code_templates/__pycache__/insert_addon_info.cpython-37.pyc new file mode 100644 index 0000000..3dbcb80 Binary files /dev/null and b/code_templates/__pycache__/insert_addon_info.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_keymap.cpython-37.pyc b/code_templates/__pycache__/insert_keymap.cpython-37.pyc new file mode 100644 index 0000000..2a4e7aa Binary files /dev/null and b/code_templates/__pycache__/insert_keymap.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_keymap_item.cpython-37.pyc b/code_templates/__pycache__/insert_keymap_item.cpython-37.pyc new file mode 100644 index 0000000..54ae07c Binary files /dev/null and b/code_templates/__pycache__/insert_keymap_item.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_license.cpython-37.pyc b/code_templates/__pycache__/insert_license.cpython-37.pyc new file mode 100644 index 0000000..f9124e0 Binary files /dev/null and b/code_templates/__pycache__/insert_license.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_menu.cpython-37.pyc b/code_templates/__pycache__/insert_menu.cpython-37.pyc new file mode 100644 index 0000000..3f88886 Binary files /dev/null and b/code_templates/__pycache__/insert_menu.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_operator.cpython-37.pyc b/code_templates/__pycache__/insert_operator.cpython-37.pyc new file mode 100644 index 0000000..760b4d6 Binary files /dev/null and b/code_templates/__pycache__/insert_operator.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_panel.cpython-37.pyc b/code_templates/__pycache__/insert_panel.cpython-37.pyc new file mode 100644 index 0000000..538beb6 Binary files /dev/null and b/code_templates/__pycache__/insert_panel.cpython-37.pyc differ diff --git a/code_templates/__pycache__/insert_register.cpython-37.pyc b/code_templates/__pycache__/insert_register.cpython-37.pyc new file mode 100644 index 0000000..84014c5 Binary files /dev/null and b/code_templates/__pycache__/insert_register.cpython-37.pyc differ diff --git a/code_templates/base.py b/code_templates/base.py index 560b0ec..9e7897c 100644 --- a/code_templates/base.py +++ b/code_templates/base.py @@ -1,64 +1,64 @@ -import bpy -from bpy.props import * -from .. text_block import TextBlock -from .. graphics.utils import getDpiFactor - -class InsertTemplateMenu(bpy.types.Menu): - bl_idname = "code_autocomplete_insert_template_menu" - bl_label = "Insert Template" - - def draw(self, context): - layout = self.layout - layout.operator_context = "INVOKE_DEFAULT" - layout.operator("code_autocomplete.insert_panel", text = "Panel") - layout.operator_menu_enum("code_autocomplete.insert_menu", "menu_type", text = "Menu") - layout.operator_menu_enum("code_autocomplete.insert_operator", "operator_type", text = "Operator") - layout.separator() - layout.operator("code_autocomplete.insert_addon_info", "Addon Info") - layout.operator("code_autocomplete.insert_register", "Register") - layout.operator("code_autocomplete.insert_license", "License") - layout.menu("code_autocomplete_insert_keymap_menu", "Keymap") - -def draw_template_menu(self, context): - self.layout.menu("code_autocomplete_insert_template_menu", text = "Code Autocomplete") - -class InsertKeymapMenu(bpy.types.Menu): - bl_idname = "code_autocomplete_insert_keymap_menu" - bl_label = "Insert Template" - - def draw(self, context): - layout = self.layout - layout.operator_context = "INVOKE_DEFAULT" - layout.operator("code_autocomplete.insert_keymap", text = "Keymap") - layout.operator("code_autocomplete.insert_keymap_item", text = "Keymap Item") - -class InsertTemplateBase: - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return TextBlock.get_active() - -class InsertClassTemplateBase(InsertTemplateBase): - class_name = StringProperty(name = "Class Name", default = "") - - def invoke(self, context, event): - dpiFactor = getDpiFactor() - return context.window_manager.invoke_props_dialog(self, 300 * dpiFactor, 200 * dpiFactor) - - def draw(self, context): - layout = self.layout - layout.prop(self, "class_name", text = "Name") - - -def insert_template(code, changes = {}): - text_block = TextBlock.get_active() - - if text_block.current_line.strip() != "": - text_block.insert("\n") - text_block.current_character_index = 0 - - for old, new in changes.items(): - code = code.replace(old, new) - if text_block: - text_block.insert(code) +import bpy +from bpy.props import * +from .. text_block import TextBlock +from .. graphics.utils import getDpiFactor + +class InsertTemplateMenu(bpy.types.Menu): + bl_idname = "code_autocomplete_insert_template_menu" + bl_label = "Insert Template" + + def draw(self, context): + layout = self.layout + layout.operator_context = "INVOKE_DEFAULT" + layout.operator("code_autocomplete.insert_panel", text = "Panel") + layout.operator_menu_enum("code_autocomplete.insert_menu", "menu_type", text = "Menu") + layout.operator_menu_enum("code_autocomplete.insert_operator", "operator_type", text = "Operator") + layout.separator() + layout.operator("code_autocomplete.insert_addon_info", "Addon Info") + layout.operator("code_autocomplete.insert_register", "Register") + layout.operator("code_autocomplete.insert_license", "License") + layout.menu("code_autocomplete_insert_keymap_menu", "Keymap") + +def draw_template_menu(self, context): + self.layout.menu("code_autocomplete_insert_template_menu", text = "Code Autocomplete") + +class InsertKeymapMenu(bpy.types.Menu): + bl_idname = "code_autocomplete_insert_keymap_menu" + bl_label = "Insert Template" + + def draw(self, context): + layout = self.layout + layout.operator_context = "INVOKE_DEFAULT" + layout.operator("code_autocomplete.insert_keymap", text = "Keymap") + layout.operator("code_autocomplete.insert_keymap_item", text = "Keymap Item") + +class InsertTemplateBase: + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return TextBlock.get_active() + +class InsertClassTemplateBase(InsertTemplateBase): + class_name: StringProperty(name = "Class Name", default = "") + + def invoke(self, context, event): + dpiFactor = getDpiFactor() + return context.window_manager.invoke_props_dialog(self, 300 * dpiFactor, 200 * dpiFactor) + + def draw(self, context): + layout = self.layout + layout.prop(self, "class_name", text = "Name") + + +def insert_template(code, changes = {}): + text_block = TextBlock.get_active() + + if text_block.current_line.strip() != "": + text_block.insert("\n") + text_block.current_character_index = 0 + + for old, new in changes.items(): + code = code.replace(old, new) + if text_block: + text_block.insert(code) diff --git a/code_templates/insert_addon_info.py b/code_templates/insert_addon_info.py index b1e1169..a2a8d42 100644 --- a/code_templates/insert_addon_info.py +++ b/code_templates/insert_addon_info.py @@ -1,24 +1,24 @@ -import bpy -from . base import InsertTemplateBase, insert_template - -class InsertAddonInfo(bpy.types.Operator, InsertTemplateBase): - bl_idname = "code_autocomplete.insert_addon_info" - bl_label = "Insert Addon Info" - bl_description = "" - - def execute(self, context): - changes = { "BLENDER_VERSION" : str(bpy.app.version) } - insert_template(addon_info_template, changes) - return {"FINISHED"} - -addon_info_template = '''bl_info = { - "name": "My Addon Name", - "description": "", - "author": "Your Name", - "version": (0, 0, 1), - "blender": BLENDER_VERSION, - "location": "View3D", - "warning": "This addon is still in development.", - "wiki_url": "", - "category": "Object" } - ''' +import bpy +from . base import InsertTemplateBase, insert_template + +class InsertAddonInfo(bpy.types.Operator, InsertTemplateBase): + bl_idname = "code_autocomplete.insert_addon_info" + bl_label = "Insert Addon Info" + bl_description = "" + + def execute(self, context): + changes = { "BLENDER_VERSION" : str(bpy.app.version) } + insert_template(addon_info_template, changes) + return {"FINISHED"} + +addon_info_template = '''bl_info = { + "name": "My Addon Name", + "description": "", + "author": "Your Name", + "version": (0, 0, 1), + "blender": BLENDER_VERSION, + "location": "View3D", + "warning": "This addon is still in development.", + "wiki_url": "", + "category": "Object" } + ''' diff --git a/code_templates/insert_keymap.py b/code_templates/insert_keymap.py index 1b90f4f..638dc2b 100644 --- a/code_templates/insert_keymap.py +++ b/code_templates/insert_keymap.py @@ -1,59 +1,59 @@ -import bpy -from bpy.props import * -from .. text_block import TextBlock -from .. graphics.utils import getDpiFactor -from . base import InsertTemplateBase, insert_template - -class InsertKeymap(bpy.types.Operator, InsertTemplateBase): - bl_idname = "code_autocomplete.insert_keymap" - bl_label = "Insert Keymap" - bl_description = "" - - insert_callers = BoolProperty(name = "Extend Register Functions", default = True, - description = "Insert code in register and unregister functions if they exist") - - def invoke(self, context, event): - dpiFactor = getDpiFactor() - return context.window_manager.invoke_props_dialog(self, 200 * dpiFactor, 200 * dpiFactor) - - def draw(self, context): - layout = self.layout - layout.prop(self, "insert_callers") - - def execute(self, context): - insert_template(keymap_template) - if self.insert_callers: - self.insert_function_calls() - return {"FINISHED"} - - def insert_function_calls(self): - text_block = TextBlock.get_active() - - for i, line in enumerate(text_block.lines): - if line.startswith("def register():"): - text_block.current_line_index = i - text_block.move_cursor_to_line_end() - text_block.insert("\n register_keymaps()") - - for i, line in enumerate(text_block.lines): - if line.startswith("def unregister():"): - text_block.current_line_index = i - text_block.move_cursor_to_line_end() - text_block.insert("\n unregister_keymaps()") - - -keymap_template = '''addon_keymaps = [] -def register_keymaps(): - addon = bpy.context.window_manager.keyconfigs.addon - km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") - # insert keymap items here - addon_keymaps.append(km) - -def unregister_keymaps(): - wm = bpy.context.window_manager - for km in addon_keymaps: - for kmi in km.keymap_items: - km.keymap_items.remove(kmi) - wm.keyconfigs.addon.keymaps.remove(km) - addon_keymaps.clear() -''' +import bpy +from bpy.props import * +from .. text_block import TextBlock +from .. graphics.utils import getDpiFactor +from . base import InsertTemplateBase, insert_template + +class InsertKeymap(bpy.types.Operator, InsertTemplateBase): + bl_idname = "code_autocomplete.insert_keymap" + bl_label = "Insert Keymap" + bl_description = "" + + insert_callers: BoolProperty(name = "Extend Register Functions", default = True, + description = "Insert code in register and unregister functions if they exist") + + def invoke(self, context, event): + dpiFactor = getDpiFactor() + return context.window_manager.invoke_props_dialog(self, 200 * dpiFactor, 200 * dpiFactor) + + def draw(self, context): + layout = self.layout + layout.prop(self, "insert_callers") + + def execute(self, context): + insert_template(keymap_template) + if self.insert_callers: + self.insert_function_calls() + return {"FINISHED"} + + def insert_function_calls(self): + text_block = TextBlock.get_active() + + for i, line in enumerate(text_block.lines): + if line.startswith("def register():"): + text_block.current_line_index = i + text_block.move_cursor_to_line_end() + text_block.insert("\n register_keymaps()") + + for i, line in enumerate(text_block.lines): + if line.startswith("def unregister():"): + text_block.current_line_index = i + text_block.move_cursor_to_line_end() + text_block.insert("\n unregister_keymaps()") + + +keymap_template = '''addon_keymaps = [] +def register_keymaps(): + addon = bpy.context.window_manager.keyconfigs.addon + km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") + # insert keymap items here + addon_keymaps.append(km) + +def unregister_keymaps(): + wm = bpy.context.window_manager + for km in addon_keymaps: + for kmi in km.keymap_items: + km.keymap_items.remove(kmi) + wm.keyconfigs.addon.keymaps.remove(km) + addon_keymaps.clear() +''' diff --git a/code_templates/insert_keymap_item.py b/code_templates/insert_keymap_item.py index e1975b4..57c80b0 100644 --- a/code_templates/insert_keymap_item.py +++ b/code_templates/insert_keymap_item.py @@ -1,48 +1,48 @@ -import bpy -from .. text_block import TextBlock -from . base import InsertTemplateBase -from .. graphics.utils import getDpiFactor - -class InsertKeymapItem(bpy.types.Operator, InsertTemplateBase): - bl_idname = "code_autocomplete.insert_keymap_item" - bl_label = "Insert Keymap Item" - bl_description = "" - - def invoke(self, context, event): - wm = context.window_manager - self.temp_keymap = wm.keyconfigs.addon.keymaps.new("3D View", space_type = "VIEW_3D") - self.temp_keymap_item = self.temp_keymap.keymap_items.new(self.bl_idname, type = "P", value = "PRESS") - - dpiFactor = getDpiFactor() - return context.window_manager.invoke_props_dialog(self, 300 * dpiFactor, 200 * dpiFactor) - - def draw(self, context): - layout = self.layout - col = layout.column() - col.prop(self.temp_keymap_item, "type") - col.prop(self.temp_keymap_item, "value") - row = col.row() - row.prop(self.temp_keymap_item, "shift") - row.prop(self.temp_keymap_item, "ctrl") - row.prop(self.temp_keymap_item, "alt") - - def check(self, context): - return True - - def execute(self, context): - kmi = self.temp_keymap_item - - line = "kmi = km.keymap_items.new(\"transform.translate\", type = \"{}\", value = \"{}\"".format(kmi.type, kmi.value) - if kmi.shift: line += ", shift = True" - if kmi.ctrl: line += ", ctrl = True" - if kmi.alt: line += ", alt = True" - line += ")" - - text_block = TextBlock.get_active() - text_block.insert(line) - text_block.select_text_in_current_line("transform.translate") - - wm = context.window_manager - self.temp_keymap.keymap_items.remove(self.temp_keymap_item) - wm.keyconfigs.addon.keymaps.remove(self.temp_keymap) - return {"FINISHED"} +import bpy +from .. text_block import TextBlock +from . base import InsertTemplateBase +from .. graphics.utils import getDpiFactor + +class InsertKeymapItem(bpy.types.Operator, InsertTemplateBase): + bl_idname = "code_autocomplete.insert_keymap_item" + bl_label = "Insert Keymap Item" + bl_description = "" + + def invoke(self, context, event): + wm = context.window_manager + self.temp_keymap = wm.keyconfigs.addon.keymaps.new("3D View", space_type = "VIEW_3D") + self.temp_keymap_item = self.temp_keymap.keymap_items.new(self.bl_idname, type = "P", value = "PRESS") + + dpiFactor = getDpiFactor() + return context.window_manager.invoke_props_dialog(self, 300 * dpiFactor, 200 * dpiFactor) + + def draw(self, context): + layout = self.layout + col = layout.column() + col.prop(self.temp_keymap_item, "type") + col.prop(self.temp_keymap_item, "value") + row = col.row() + row.prop(self.temp_keymap_item, "shift") + row.prop(self.temp_keymap_item, "ctrl") + row.prop(self.temp_keymap_item, "alt") + + def check(self, context): + return True + + def execute(self, context): + kmi = self.temp_keymap_item + + line = "kmi = km.keymap_items.new(\"transform.translate\", type = \"{}\", value = \"{}\"".format(kmi.type, kmi.value) + if kmi.shift: line += ", shift = True" + if kmi.ctrl: line += ", ctrl = True" + if kmi.alt: line += ", alt = True" + line += ")" + + text_block = TextBlock.get_active() + text_block.insert(line) + text_block.select_text_in_current_line("transform.translate") + + wm = context.window_manager + self.temp_keymap.keymap_items.remove(self.temp_keymap_item) + wm.keyconfigs.addon.keymaps.remove(self.temp_keymap) + return {"FINISHED"} diff --git a/code_templates/insert_license.py b/code_templates/insert_license.py index 18ef889..cc1f256 100644 --- a/code_templates/insert_license.py +++ b/code_templates/insert_license.py @@ -1,51 +1,51 @@ -import bpy -from bpy.props import * -from datetime import datetime -from .. graphics.utils import getDpiFactor -from . base import InsertTemplateBase, insert_template - -class InsertLicense(bpy.types.Operator, InsertTemplateBase): - bl_idname = "code_autocomplete.insert_license" - bl_label = "Insert License" - bl_description = "" - - author_name = StringProperty(name = "Name", default = bpy.context.user_preferences.system.author) - author_mail = StringProperty(name = "eMail", default = "") - - def invoke(self, context, event): - dpiFactor = getDpiFactor() - return context.window_manager.invoke_props_dialog(self, 300 * dpiFactor, 200 * dpiFactor) - - def draw(self, context): - layout = self.layout - layout.prop(self, "author_name", text = "Name") - layout.prop(self, "author_mail", text = "E-Mail") - - def execute(self, context): - changes = { - "YOUR_NAME" : self.author_name, - "YOUR_MAIL" : self.author_mail, - "CURRENT_YEAR" : str(datetime.now().year) } - insert_template(license_template, changes) - return {"FINISHED"} - -license_template = """''' -Copyright (C) CURRENT_YEAR YOUR_NAME -YOUR_MAIL - -Created by YOUR_NAME - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -""" +import bpy +from bpy.props import * +from datetime import datetime +from .. graphics.utils import getDpiFactor +from . base import InsertTemplateBase, insert_template + +class InsertLicense(bpy.types.Operator, InsertTemplateBase): + bl_idname = "code_autocomplete.insert_license" + bl_label = "Insert License" + bl_description = "" + + #author_name: StringProperty(name = "Name", default = bpy.context.preferences.filepaths.author) + author_mail: StringProperty(name = "eMail", default = "") + + def invoke(self, context, event): + dpiFactor = getDpiFactor() + return context.window_manager.invoke_props_dialog(self, 300 * dpiFactor, 200 * dpiFactor) + + def draw(self, context): + layout = self.layout + #layout.prop(self, "author_name", text = "Name") + layout.prop(self, "author_mail", text = "E-Mail") + + def execute(self, context): + changes = { + #"YOUR_NAME" : self.author_name, + "YOUR_MAIL" : self.author_mail, + "CURRENT_YEAR" : str(datetime.now().year) } + insert_template(license_template, changes) + return {"FINISHED"} + +license_template = """''' +Copyright (C) CURRENT_YEAR YOUR_NAME +YOUR_MAIL + +Created by YOUR_NAME + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +""" diff --git a/code_templates/insert_menu.py b/code_templates/insert_menu.py index fa695e4..1200da2 100644 --- a/code_templates/insert_menu.py +++ b/code_templates/insert_menu.py @@ -1,43 +1,43 @@ -import bpy -from bpy.props import * -from . base import InsertClassTemplateBase, insert_template -from .. utils.variable_name_conversion import (get_valid_variable_name, - get_lower_case_with_underscores, - get_separated_capitalized_words) - -menu_type_items = [ - ("NORMAL", "Normal", ""), - ("PIE", "Pie", "") ] - -class InsertMenu(bpy.types.Operator, InsertClassTemplateBase): - bl_idname = "code_autocomplete.insert_menu" - bl_label = "Insert Menu" - bl_description = "" - - menu_type = EnumProperty(items = menu_type_items, default = "NORMAL") - - def execute(self, context): - if self.menu_type == "NORMAL": code = menu_template - if self.menu_type == "PIE": code = pie_menu_template - changes = { - "CLASS_NAME" : get_valid_variable_name(self.class_name), - "ID_NAME" : "view3d." + get_lower_case_with_underscores(self.class_name), - "LABEL" : get_separated_capitalized_words(self.class_name) } - insert_template(code, changes) - return {"FINISHED"} - -menu_template = '''class CLASS_NAME(bpy.types.Menu): - bl_idname = "ID_NAME" - bl_label = "LABEL" - - def draw(self, context): - layout = self.layout - ''' - -pie_menu_template = '''class CLASS_NAME(bpy.types.Menu): - bl_idname = "ID_NAME" - bl_label = "LABEL" - - def draw(self, context): - pie = self.layout.menu_pie() - ''' +import bpy +from bpy.props import * +from . base import InsertClassTemplateBase, insert_template +from .. utils.variable_name_conversion import (get_valid_variable_name, + get_lower_case_with_underscores, + get_separated_capitalized_words) + +menu_type_items = [ + ("NORMAL", "Normal", ""), + ("PIE", "Pie", "") ] + +class InsertMenu(bpy.types.Operator, InsertClassTemplateBase): + bl_idname = "code_autocomplete.insert_menu" + bl_label = "Insert Menu" + bl_description = "" + + menu_type: EnumProperty(items = menu_type_items, default = "NORMAL") + + def execute(self, context): + if self.menu_type == "NORMAL": code = menu_template + if self.menu_type == "PIE": code = pie_menu_template + changes = { + "CLASS_NAME" : get_valid_variable_name(self.class_name), + "ID_NAME" : "view3d." + get_lower_case_with_underscores(self.class_name), + "LABEL" : get_separated_capitalized_words(self.class_name) } + insert_template(code, changes) + return {"FINISHED"} + +menu_template = '''class CLASS_NAME(bpy.types.Menu): + bl_idname = "ID_NAME" + bl_label = "LABEL" + + def draw(self, context): + layout = self.layout + ''' + +pie_menu_template = '''class CLASS_NAME(bpy.types.Menu): + bl_idname = "ID_NAME" + bl_label = "LABEL" + + def draw(self, context): + pie = self.layout.menu_pie() + ''' diff --git a/code_templates/insert_operator.py b/code_templates/insert_operator.py index d81bc36..4d6a29b 100644 --- a/code_templates/insert_operator.py +++ b/code_templates/insert_operator.py @@ -1,103 +1,103 @@ -import bpy -from bpy.props import * -from . base import InsertClassTemplateBase, insert_template -from .. utils.variable_name_conversion import (get_valid_variable_name, - get_lower_case_with_underscores, - get_separated_capitalized_words) - -operator_type_items = [ - ("NORMAL", "Normal", ""), - ("MODAL", "Modal", ""), - ("MODAL_DRAW", "Modal Draw", "") ] - -class InsertOperator(bpy.types.Operator, InsertClassTemplateBase): - bl_idname = "code_autocomplete.insert_operator" - bl_label = "Insert Operator" - bl_description = "" - - operator_type = EnumProperty(items = operator_type_items, default = "NORMAL") - - def execute(self, context): - if self.operator_type == "NORMAL": code = operator_template - if self.operator_type == "MODAL": code = modal_operator_template - if self.operator_type == "MODAL_DRAW": code = modal_operator_draw_template - changes = { - "CLASS_NAME" : get_valid_variable_name(self.class_name), - "ID_NAME" : "my_operator." + get_lower_case_with_underscores(self.class_name), - "LABEL" : get_separated_capitalized_words(self.class_name) } - insert_template(code, changes) - return {"FINISHED"} - -operator_template = '''class CLASS_NAME(bpy.types.Operator): - bl_idname = "ID_NAME" - bl_label = "LABEL" - bl_description = "" - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - return {"FINISHED"} - ''' - -modal_operator_template = '''class CLASS_NAME(bpy.types.Operator): - bl_idname = "ID_NAME" - bl_label = "LABEL" - bl_description = "" - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return True - - def invoke(self, context, event): - context.window_manager.modal_handler_add(self) - return {"RUNNING_MODAL"} - - def modal(self, context, event): - - if event.type == "LEFTMOUSE": - return {"FINISHED"} - - if event.type in {"RIGHTMOUSE", "ESC"}: - return {"CANCELLED"} - - return {"RUNNING_MODAL"} - ''' - -modal_operator_draw_template = '''class CLASS_NAME(bpy.types.Operator): - bl_idname = "ID_NAME" - bl_label = "LABEL" - bl_description = "" - bl_options = {"REGISTER"} - - @classmethod - def poll(cls, context): - return True - - def invoke(self, context, event): - args = () - self.draw_handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") - context.window_manager.modal_handler_add(self) - return {"RUNNING_MODAL"} - - def modal(self, context, event): - context.area.tag_redraw() - - if event.type == "LEFTMOUSE": - return self.finish() - - if event.type in {"RIGHTMOUSE", "ESC"}: - return self.finish() - - return {"RUNNING_MODAL"} - - def finish(self): - bpy.types.SpaceView3D.draw_handler_remove(self.draw_handler, "WINDOW") - return {"FINISHED"} - - def draw_callback_px(self): - pass - ''' +import bpy +from bpy.props import * +from . base import InsertClassTemplateBase, insert_template +from .. utils.variable_name_conversion import (get_valid_variable_name, + get_lower_case_with_underscores, + get_separated_capitalized_words) + +operator_type_items = [ + ("NORMAL", "Normal", ""), + ("MODAL", "Modal", ""), + ("MODAL_DRAW", "Modal Draw", "") ] + +class InsertOperator(bpy.types.Operator, InsertClassTemplateBase): + bl_idname = "code_autocomplete.insert_operator" + bl_label = "Insert Operator" + bl_description = "" + + operator_type: EnumProperty(items = operator_type_items, default = "NORMAL") + + def execute(self, context): + if self.operator_type == "NORMAL": code = operator_template + if self.operator_type == "MODAL": code = modal_operator_template + if self.operator_type == "MODAL_DRAW": code = modal_operator_draw_template + changes = { + "CLASS_NAME" : get_valid_variable_name(self.class_name), + "ID_NAME" : "my_operator." + get_lower_case_with_underscores(self.class_name), + "LABEL" : get_separated_capitalized_words(self.class_name) } + insert_template(code, changes) + return {"FINISHED"} + +operator_template = '''class CLASS_NAME(bpy.types.Operator): + bl_idname = "ID_NAME" + bl_label = "LABEL" + bl_description = "" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + return {"FINISHED"} + ''' + +modal_operator_template = '''class CLASS_NAME(bpy.types.Operator): + bl_idname = "ID_NAME" + bl_label = "LABEL" + bl_description = "" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + return {"RUNNING_MODAL"} + + def modal(self, context, event): + + if event.type == "LEFTMOUSE": + return {"FINISHED"} + + if event.type in {"RIGHTMOUSE", "ESC"}: + return {"CANCELLED"} + + return {"RUNNING_MODAL"} + ''' + +modal_operator_draw_template = '''class CLASS_NAME(bpy.types.Operator): + bl_idname = "ID_NAME" + bl_label = "LABEL" + bl_description = "" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") + context.window_manager.modal_handler_add(self) + return {"RUNNING_MODAL"} + + def modal(self, context, event): + context.area.tag_redraw() + + if event.type == "LEFTMOUSE": + return self.finish() + + if event.type in {"RIGHTMOUSE", "ESC"}: + return self.finish() + + return {"RUNNING_MODAL"} + + def finish(self): + bpy.types.SpaceView3D.draw_handler_remove(self._handle, "WINDOW") + return {"FINISHED"} + + def draw_callback_px(tmp, self, context): + pass + ''' diff --git a/code_templates/insert_panel.py b/code_templates/insert_panel.py index 96a085f..9580c57 100644 --- a/code_templates/insert_panel.py +++ b/code_templates/insert_panel.py @@ -1,29 +1,29 @@ -import bpy -from . base import InsertClassTemplateBase, insert_template -from .. utils.variable_name_conversion import (get_valid_variable_name, - get_lower_case_with_underscores, - get_separated_capitalized_words) - -class InsertPanel(bpy.types.Operator, InsertClassTemplateBase): - bl_idname = "code_autocomplete.insert_panel" - bl_label = "Insert Panel" - bl_description = "" - - def execute(self, context): - changes = { - "CLASS_NAME" : get_valid_variable_name(self.class_name), - "ID_NAME" : get_lower_case_with_underscores(self.class_name), - "LABEL" : get_separated_capitalized_words(self.class_name) } - insert_template(panel_template, changes) - return {"FINISHED"} - -panel_template = '''class CLASS_NAME(bpy.types.Panel): - bl_idname = "ID_NAME" - bl_label = "LABEL" - bl_space_type = "VIEW_3D" - bl_region_type = "TOOLS" - bl_category = "category" - - def draw(self, context): - layout = self.layout - ''' +import bpy +from . base import InsertClassTemplateBase, insert_template +from .. utils.variable_name_conversion import (get_valid_variable_name, + get_lower_case_with_underscores, + get_separated_capitalized_words) + +class InsertPanel(bpy.types.Operator, InsertClassTemplateBase): + bl_idname = "code_autocomplete.insert_panel" + bl_label = "Insert Panel" + bl_description = "" + + def execute(self, context): + changes = { + "CLASS_NAME" : get_valid_variable_name(self.class_name), + "ID_NAME" : get_lower_case_with_underscores(self.class_name), + "LABEL" : get_separated_capitalized_words(self.class_name) } + insert_template(panel_template, changes) + return {"FINISHED"} + +panel_template = '''class CLASS_NAME(bpy.types.Panel): + bl_idname = "ID_NAME" + bl_label = "LABEL" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "category" + + def draw(self, context): + layout = self.layout + ''' diff --git a/code_templates/insert_register.py b/code_templates/insert_register.py index 8920d85..60773cc 100644 --- a/code_templates/insert_register.py +++ b/code_templates/insert_register.py @@ -1,21 +1,21 @@ -import bpy -from . base import InsertTemplateBase, insert_template - -class InsertRegister(bpy.types.Operator, InsertTemplateBase): - bl_idname = "code_autocomplete.insert_register" - bl_label = "Insert Keymap" - bl_description = "" - - def execute(self, context): - insert_template(register_template) - return {"FINISHED"} - -register_template = '''def register(): - bpy.utils.register_module(__name__) - -def unregister(): - bpy.utils.unregister_module(__name__) - -if __name__ == "__main__": - register() -''' +import bpy +from . base import InsertTemplateBase, insert_template + +class InsertRegister(bpy.types.Operator, InsertTemplateBase): + bl_idname = "code_autocomplete.insert_register" + bl_label = "Insert Keymap" + bl_description = "" + + def execute(self, context): + insert_template(register_template) + return {"FINISHED"} + +register_template = '''def register(): + bpy.utils.register_module(__name__) + +def unregister(): + bpy.utils.unregister_module(__name__) + +if __name__ == "__main__": + register() +''' diff --git a/developer_utils.py b/developer_utils.py index e031810..dcbe163 100644 --- a/developer_utils.py +++ b/developer_utils.py @@ -1,42 +1,42 @@ -import os -import sys -import pkgutil -import importlib - -def setup_addon_modules(path, package_name, reload): - """ - Imports and reloads all modules in this addon. - - path -- __path__ from __init__.py - package_name -- __name__ from __init__.py - - Individual modules can define a __reload_order_index__ property which - will be used to reload the modules in a specific order. The default is 0. - """ - def get_submodule_names(path = path[0], root = ""): - module_names = [] - for importer, module_name, is_package in pkgutil.iter_modules([path]): - if is_package: - sub_path = os.path.join(path, module_name) - sub_root = root + module_name + "." - module_names.extend(get_submodule_names(sub_path, sub_root)) - else: - module_names.append(root + module_name) - return module_names - - def import_submodules(names): - modules = [] - for name in names: - modules.append(importlib.import_module("." + name, package_name)) - return modules - - def reload_modules(modules): - modules.sort(key = lambda module: getattr(module, "__reload_order_index__", 0)) - for module in modules: - importlib.reload(module) - - names = get_submodule_names() - modules = import_submodules(names) - if reload: - reload_modules(modules) - return modules +import os +import sys +import pkgutil +import importlib + +def setup_addon_modules(path, package_name, reload): + """ + Imports and reloads all modules in this addon. + + path -- __path__ from __init__.py + package_name -- __name__ from __init__.py + + Individual modules can define a __reload_order_index__ property which + will be used to reload the modules in a specific order. The default is 0. + """ + def get_submodule_names(path = path[0], root = ""): + module_names = [] + for importer, module_name, is_package in pkgutil.iter_modules([path]): + if is_package: + sub_path = os.path.join(path, module_name) + sub_root = root + module_name + "." + module_names.extend(get_submodule_names(sub_path, sub_root)) + else: + module_names.append(root + module_name) + return module_names + + def import_submodules(names): + modules = [] + for name in names: + modules.append(importlib.import_module("." + name, package_name)) + return modules + + def reload_modules(modules): + modules.sort(key = lambda module: getattr(module, "__reload_order_index__", 0)) + for module in modules: + importlib.reload(module) + + names = get_submodule_names() + modules = import_submodules(names) + if reload: + reload_modules(modules) + return modules diff --git a/graphics/__pycache__/__init__.cpython-37.pyc b/graphics/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..fe9104f Binary files /dev/null and b/graphics/__pycache__/__init__.cpython-37.pyc differ diff --git a/graphics/__pycache__/list_box.cpython-37.pyc b/graphics/__pycache__/list_box.cpython-37.pyc new file mode 100644 index 0000000..44707ea Binary files /dev/null and b/graphics/__pycache__/list_box.cpython-37.pyc differ diff --git a/graphics/__pycache__/rectangle.cpython-37.pyc b/graphics/__pycache__/rectangle.cpython-37.pyc new file mode 100644 index 0000000..0d586a8 Binary files /dev/null and b/graphics/__pycache__/rectangle.cpython-37.pyc differ diff --git a/graphics/__pycache__/text_box.cpython-37.pyc b/graphics/__pycache__/text_box.cpython-37.pyc new file mode 100644 index 0000000..a246928 Binary files /dev/null and b/graphics/__pycache__/text_box.cpython-37.pyc differ diff --git a/graphics/__pycache__/utils.cpython-37.pyc b/graphics/__pycache__/utils.cpython-37.pyc new file mode 100644 index 0000000..f1ed59a Binary files /dev/null and b/graphics/__pycache__/utils.cpython-37.pyc differ diff --git a/graphics/list_box.py b/graphics/list_box.py index 8e9a49c..fe76f2a 100644 --- a/graphics/list_box.py +++ b/graphics/list_box.py @@ -1,101 +1,101 @@ -import blf -from bgl import * -from . utils import getDpi -from mathutils import Vector -from . rectangle import Rectangle - -class ListItem: - def __init__(self, text): - self.text = text - self.active = False - self.alignment = "LEFT" - self.data = None - self.offset = 0 - - -class ListBox: - def __init__(self): - self.items = [] - self.position = Vector((0, 0)) - self.width = 200 - self.line_height = 23 - self.font_size = 8 - self.padding = 5 - self.font = 1 - self.background_color = (1.0, 1.0, 1.0, 1.0) - self.background_border_color = (0.9, 0.76, 0.4, 1.0) - self.text_color = (0.1, 0.1, 0.1, 1.0) - self.active_item_color = (0.95, 0.95, 1.0, 1.0) - self.active_item_border_color = (1.0, 0.8, 0.5, 1.0) - self.calc_height() - - def contains(self, point): - background = self.get_background_rectangle() - return background.contains(point) - - def get_item_under_point(self, point): - for i, item in enumerate(self.items): - rec = self.get_item_rectangle(i) - if rec.contains(point): return item - - def draw(self): - self.calc_height() - self.draw_background() - self.draw_items() - - def draw_background(self): - background = self.get_background_rectangle() - background.draw() - - def get_background_rectangle(self): - self.calc_height() - - background = Rectangle( - x1 = self.position.x, - y1 = self.position.y, - x2 = self.position.x + self.width, - y2 = self.position.y - self.height ) - background.border_thickness = -1 - background.color = self.background_color - background.border_color = self.background_border_color - return background - - def calc_height(self): - self.height = len(self.items) * self.line_height + self.padding - - def draw_items(self): - for index in range(len(self.items)): - self.draw_item(index) - - def draw_item(self, index): - item = self.items[index] - item_rec = self.get_item_rectangle(index) - if item.active: - item_rec.draw() - self.draw_item_in_rectangle(item, item_rec) - - def get_item_rectangle(self, index): - item_rec = Rectangle() - item_rec.x1 = self.position.x - item_rec.y1 = self.position.y - index * self.line_height - self.padding / 2 - item_rec.x2 = self.position.x + self.width - item_rec.y2 = item_rec.y1 - self.line_height - item_rec.color = self.active_item_color - item_rec.border_color = self.active_item_border_color - item_rec.border_thickness = -1 - return item_rec - - def draw_item_in_rectangle(self, item, rectangle): - glColor4f(*self.text_color) - blf.size(self.font, self.font_size, int(getDpi())) - - if item.alignment == "LEFT": - x = rectangle.left + self.padding - if item.alignment == "CENTER": - x = rectangle.center.x - blf.dimensions(self.font, item.text)[0] / 2 - x += item.offset - - offset = blf.dimensions(self.font, "i")[1] / 2 - - blf.position(self.font, x, rectangle.center.y - offset, 0) - blf.draw(self.font, item.text) +import blf +from bgl import * +from . utils import getDpi +from mathutils import Vector +from . rectangle import Rectangle + +class ListItem: + def __init__(self, text): + self.text = text + self.active = False + self.alignment = "LEFT" + self.data = None + self.offset = 0 + + +class ListBox: + def __init__(self): + self.items = [] + self.position = Vector((0, 0)) + self.width = 200 + self.line_height = 23 + self.font_size = 8 + self.padding = 5 + self.font = 1 + self.background_color = (1.0, 1.0, 1.0, 1.0) + self.background_border_color = (0.9, 0.76, 0.4, 1.0) + self.text_color = (0.1, 0.1, 0.1, 1.0) + self.active_item_color = (0.95, 0.95, 1.0, 1.0) + self.active_item_border_color = (1.0, 0.8, 0.5, 1.0) + self.calc_height() + + def contains(self, point): + background = self.get_background_rectangle() + return background.contains(point) + + def get_item_under_point(self, point): + for i, item in enumerate(self.items): + rec = self.get_item_rectangle(i) + if rec.contains(point): return item + + def draw(self): + self.calc_height() + self.draw_background() + self.draw_items() + + def draw_background(self): + background = self.get_background_rectangle() + background.draw() + + def get_background_rectangle(self): + self.calc_height() + + background = Rectangle( + x1 = self.position.x, + y1 = self.position.y, + x2 = self.position.x + self.width, + y2 = self.position.y - self.height ) + background.border_thickness = -1 + background.color = self.background_color + background.border_color = self.background_border_color + return background + + def calc_height(self): + self.height = len(self.items) * self.line_height + self.padding + + def draw_items(self): + for index in range(len(self.items)): + self.draw_item(index) + + def draw_item(self, index): + item = self.items[index] + item_rec = self.get_item_rectangle(index) + if item.active: + item_rec.draw() + self.draw_item_in_rectangle(item, item_rec) + + def get_item_rectangle(self, index): + item_rec = Rectangle() + item_rec.x1 = self.position.x + item_rec.y1 = self.position.y - index * self.line_height - self.padding / 2 + item_rec.x2 = self.position.x + self.width + item_rec.y2 = item_rec.y1 - self.line_height + item_rec.color = self.active_item_color + item_rec.border_color = self.active_item_border_color + item_rec.border_thickness = -1 + return item_rec + + def draw_item_in_rectangle(self, item, rectangle): + glColor4f(*self.text_color) + blf.size(self.font, self.font_size, int(getDpi())) + + if item.alignment == "LEFT": + x = rectangle.left + self.padding + if item.alignment == "CENTER": + x = rectangle.center.x - blf.dimensions(self.font, item.text)[0] / 2 + x += item.offset + + offset = blf.dimensions(self.font, "i")[1] / 2 + + blf.position(self.font, x, rectangle.center.y - offset, 0) + blf.draw(self.font, item.text) diff --git a/graphics/rectangle.py b/graphics/rectangle.py index b405fcf..89686fc 100644 --- a/graphics/rectangle.py +++ b/graphics/rectangle.py @@ -1,86 +1,86 @@ -from bgl import * -from mathutils import Vector - -class Rectangle: - def __init__(self, x1 = 0, y1 = 0, x2 = 0, y2 = 0): - self.x1 = float(x1) - self.y1 = float(y1) - self.x2 = float(x2) - self.y2 = float(y2) - self.color = (0.8, 0.8, 0.8, 1.0) - self.border_color = (0.1, 0.1, 0.1, 1.0) - self.border_thickness = 0 - - @property - def width(self): - return abs(self.x1 - self.x2) - - @property - def height(self): - return abs(self.y1 - self.y2) - - @property - def left(self): - return min(self.x1, self.x2) - - @property - def right(self): - return max(self.x1, self.x2) - - @property - def top(self): - return max(self.y1, self.y2) - - @property - def bottom(self): - return min(self.y1, self.y2) - - @property - def center(self): - return Vector((self.center_x, self.center_y)) - - @property - def center_x(self): - return (self.x1 + self.x2) / 2 - - @property - def center_y(self): - return (self.y1 + self.y2) / 2 - - def contains(self, point): - return self.left <= point[0] <= self.right and self.bottom <= point[1] <= self.top - - def draw(self): - glColor4f(*self.color) - glEnable(GL_BLEND) - glBegin(GL_POLYGON) - glVertex2f(self.x1, self.y1) - glVertex2f(self.x2, self.y1) - glVertex2f(self.x2, self.y2) - glVertex2f(self.x1, self.y2) - glEnd() - - if self.border_thickness != 0: - self.drawBorder() - - def drawBorder(self): - thickness = self.border_thickness - thickness = min(abs(self.x1 - self.x2) / 2, abs(self.y1 - self.y2) / 2, thickness) - left, right = sorted([self.x1, self.x2]) - bottom, top = sorted([self.y1, self.y2]) - - if thickness > 0: - topBorder = Rectangle(left, top, right, top - thickness) - bottomBorder = Rectangle(left, bottom + thickness, right, bottom) - else: - topBorder = Rectangle(left + thickness, top, right - thickness, top - thickness) - bottomBorder = Rectangle(left + thickness, bottom + thickness, right - thickness, bottom) - leftBorder = Rectangle(left, top, left + thickness, bottom) - rightBorder = Rectangle(right - thickness, top, right, bottom) - - for border in (topBorder, bottomBorder, leftBorder, rightBorder): - border.color = self.border_color - border.draw() - - def __repr__(self): - return "({}, {}) - ({}, {})".format(self.x1, self.y1, self.x2, self.y2) +from bgl import * +from mathutils import Vector + +class Rectangle: + def __init__(self, x1 = 0, y1 = 0, x2 = 0, y2 = 0): + self.x1 = float(x1) + self.y1 = float(y1) + self.x2 = float(x2) + self.y2 = float(y2) + self.color = (0.8, 0.8, 0.8, 1.0) + self.border_color = (0.1, 0.1, 0.1, 1.0) + self.border_thickness = 0 + + @property + def width(self): + return abs(self.x1 - self.x2) + + @property + def height(self): + return abs(self.y1 - self.y2) + + @property + def left(self): + return min(self.x1, self.x2) + + @property + def right(self): + return max(self.x1, self.x2) + + @property + def top(self): + return max(self.y1, self.y2) + + @property + def bottom(self): + return min(self.y1, self.y2) + + @property + def center(self): + return Vector((self.center_x, self.center_y)) + + @property + def center_x(self): + return (self.x1 + self.x2) / 2 + + @property + def center_y(self): + return (self.y1 + self.y2) / 2 + + def contains(self, point): + return self.left <= point[0] <= self.right and self.bottom <= point[1] <= self.top + + def draw(self): + glColor4f(*self.color) + glEnable(GL_BLEND) + glBegin(GL_POLYGON) + glVertex2f(self.x1, self.y1) + glVertex2f(self.x2, self.y1) + glVertex2f(self.x2, self.y2) + glVertex2f(self.x1, self.y2) + glEnd() + + if self.border_thickness != 0: + self.drawBorder() + + def drawBorder(self): + thickness = self.border_thickness + thickness = min(abs(self.x1 - self.x2) / 2, abs(self.y1 - self.y2) / 2, thickness) + left, right = sorted([self.x1, self.x2]) + bottom, top = sorted([self.y1, self.y2]) + + if thickness > 0: + topBorder = Rectangle(left, top, right, top - thickness) + bottomBorder = Rectangle(left, bottom + thickness, right, bottom) + else: + topBorder = Rectangle(left + thickness, top, right - thickness, top - thickness) + bottomBorder = Rectangle(left + thickness, bottom + thickness, right - thickness, bottom) + leftBorder = Rectangle(left, top, left + thickness, bottom) + rightBorder = Rectangle(right - thickness, top, right, bottom) + + for border in (topBorder, bottomBorder, leftBorder, rightBorder): + border.color = self.border_color + border.draw() + + def __repr__(self): + return "({}, {}) - ({}, {})".format(self.x1, self.y1, self.x2, self.y2) diff --git a/graphics/text_box.py b/graphics/text_box.py index 21846bb..0af31ed 100644 --- a/graphics/text_box.py +++ b/graphics/text_box.py @@ -1,64 +1,64 @@ -import blf -import textwrap -from bgl import * -from . utils import getDpi -from mathutils import Vector -from . rectangle import Rectangle - -class TextBox: - def __init__(self): - self.text = "" - self.position = Vector((0, 0)) - self.width = 400 - self.background_color = (1.0, 1.0, 1.0, 1.0) - self.background_border_color = (0.9, 0.76, 0.4, 1.0) - self.text_color = (0.1, 0.1, 0.1, 1.0) - self.font_size = 8 - self.font = 1 - self.line_height = 23 - self.padding = 5 - - def draw(self): - blf.size(self.font, self.font_size, int(getDpi())) - - self.calc_lines() - background = self.get_background_rectangle() - background.draw() - - glColor4f(*self.text_color) - pos = self.position.copy() - pos.x += self.padding - pos.y -= self.padding - self.line_height / 2 - pos.y -= blf.dimensions(self.font, "i")[1] / 2 - for i, line in enumerate(self.lines): - pos.y -= self.line_height - blf.position(self.font, pos.x, pos.y, 0) - blf.draw(self.font, line) - - def calc_lines(self): - lines = self.text.split("\n") - while len(lines) > 0: - if lines[-1] != "": break - del lines[-1] - - self.lines = lines - - def get_background_rectangle(self): - self.calc_height() - self.calc_width() - background = Rectangle( - x1 = self.position.x, - y1 = self.position.y, - x2 = self.position.x + self.width, - y2 = self.position.y - self.height ) - background.border_thickness = -1 - background.color = self.background_color - background.border_color = self.background_border_color - return background - - def calc_height(self): - self.height = 2 * self.padding + self.line_height * len(self.lines) - - def calc_width(self): - widths = [blf.dimensions(self.font, line)[0] for line in self.lines] - self.width = max(widths) + 2 * self.padding +import blf +import textwrap +from bgl import * +from . utils import getDpi +from mathutils import Vector +from . rectangle import Rectangle + +class TextBox: + def __init__(self): + self.text = "" + self.position = Vector((0, 0)) + self.width = 400 + self.background_color = (1.0, 1.0, 1.0, 1.0) + self.background_border_color = (0.9, 0.76, 0.4, 1.0) + self.text_color = (0.1, 0.1, 0.1, 1.0) + self.font_size = 8 + self.font = 1 + self.line_height = 23 + self.padding = 5 + + def draw(self): + blf.size(self.font, self.font_size, int(getDpi())) + + self.calc_lines() + background = self.get_background_rectangle() + background.draw() + + glColor4f(*self.text_color) + pos = self.position.copy() + pos.x += self.padding + pos.y -= self.padding - self.line_height / 2 + pos.y -= blf.dimensions(self.font, "i")[1] / 2 + for i, line in enumerate(self.lines): + pos.y -= self.line_height + blf.position(self.font, pos.x, pos.y, 0) + blf.draw(self.font, line) + + def calc_lines(self): + lines = self.text.split("\n") + while len(lines) > 0: + if lines[-1] != "": break + del lines[-1] + + self.lines = lines + + def get_background_rectangle(self): + self.calc_height() + self.calc_width() + background = Rectangle( + x1 = self.position.x, + y1 = self.position.y, + x2 = self.position.x + self.width, + y2 = self.position.y - self.height ) + background.border_thickness = -1 + background.color = self.background_color + background.border_color = self.background_border_color + return background + + def calc_height(self): + self.height = 2 * self.padding + self.line_height * len(self.lines) + + def calc_width(self): + widths = [blf.dimensions(self.font, line)[0] for line in self.lines] + self.width = max(widths) + 2 * self.padding diff --git a/graphics/utils.py b/graphics/utils.py index 41993a4..766c8f8 100644 --- a/graphics/utils.py +++ b/graphics/utils.py @@ -1,9 +1,9 @@ -import bpy - -def getDpiFactor(): - return getDpi() / 72 - -def getDpi(): - systemPreferences = bpy.context.user_preferences.system - retinaFactor = getattr(systemPreferences, "pixel_size", 1) - return systemPreferences.dpi * retinaFactor +import bpy + +def getDpiFactor(): + return getDpi() / 72 + +def getDpi(): + systemPreferences = bpy.context.preferences.system + retinaFactor = getattr(systemPreferences, "pixel_size", 1) + return systemPreferences.dpi * retinaFactor diff --git a/quick_operators.py b/quick_operators.py index 5b60067..1132dce 100644 --- a/quick_operators.py +++ b/quick_operators.py @@ -1,150 +1,151 @@ -import bpy -import os -import re -from bpy.props import * -from . text_block import TextBlock - -class SolveWhitespaceInconsistency(bpy.types.Operator): - bl_idname = "code_autocomplete.correct_whitespaces" - bl_label = "Correct Whitespaces" - bl_description = "Convert whitespaces to spaces or tabs depending on what is set for this text block" - - def execute(self, context): - if context.edit_text.use_tabs_as_spaces: - bpy.ops.text.convert_whitespace(type = "SPACES") - else: - bpy.ops.text.convert_whitespace(type = "TABS") - return { "FINISHED" } - -class SelectWholeString(bpy.types.Operator): - bl_idname = "code_autocomplete.select_whole_string" - bl_label = "Select Whole String" - bl_description = "" - bl_options = {"REGISTER"} - - def execute(self, context): - text_block = TextBlock.get_active() - if not text_block: return {"CANCELLED"} - - line_text = text_block.current_line - character_index = text_block.current_character_index - - string_letter = text_block.get_string_definition_type(line_text, character_index) - if string_letter is None: return {"CANCELLED"} - start, end = text_block.get_range_surrounded_by_letter(line_text, string_letter, character_index) - if start != end: - text_block.set_selection_in_line(start, end) - - return {"FINISHED"} - - -class ConvertFileIndentation(bpy.types.Operator): - bl_idname = "code_autocomplete.convert_file_indentation" - bl_label = "Convert File Indentation" - bl_description = "" - bl_options = {"REGISTER"} - - path = StringProperty() - old_indentation = StringProperty(default = "\t") - new_indentation = StringProperty(default = " ") - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - if not os.path.exists(self.path): return {"CANCELLED"} - - old = self.old_indentation - new = self.new_indentation - - file = open(self.path, "r") - lines = file.readlines() - file.close() - - new_lines = [] - for line in lines: - new_line = "" - while line.startswith(old): - new_line += new - line = line[len(old):] - new_line += line.rstrip() - new_lines.append(new_line) - - file = open(self.path, "w") - file.write("\n".join(new_lines)) - file.close() - return {"FINISHED"} - - -class SelectTextBlockMenu(bpy.types.Menu): - bl_idname = "code_autocomplete_select_text_block" - bl_label = "Select Text Block" - - def draw(self, context): - layout = self.layout - - if len(bpy.data.texts) == 0: - layout.label("There are no texts in this file", icon = "INFO") - else: - for text in bpy.data.texts: - operator = layout.operator("code_autocomplete.open_text_block", text = text.name) - operator.name = text.name - -class OpenTextBlock(bpy.types.Operator): - bl_idname = "code_autocomplete.open_text_block" - bl_label = "Open Text Block" - bl_description = "" - bl_options = {"REGISTER"} - - name = StringProperty() - - @classmethod - def poll(cls, context): - return hasattr(context.space_data, "text") - - def execute(self, context): - context.space_data.text = bpy.data.texts[self.name] - return {"FINISHED"} - - -def right_click_menu_extension(self, context): - layout = self.layout - layout.separator() - layout.operator("text.comment") - layout.operator("text.uncomment") - - text_block = TextBlock.get_active() - if text_block: - line = text_block.current_line - match = re.match("def (\w+)\(\):", line) - if match: - function_name = match.group(1) - operator = layout.operator("code_autocomplete.execute_function") - operator.filepath = text_block.filepath - operator.function_name = function_name - - -def format_menu_extension(self, context): - text_block = TextBlock.get_active() - if text_block: - layout = self.layout - layout.operator("code_autocomplete.build_script") - layout.operator("code_autocomplete.correct_whitespaces") - props = layout.operator("code_autocomplete.convert_addon_indentation") - if text_block.use_tabs_as_spaces: - props.old_indentation = "\t" - props.new_indentation = " " - else: - props.old_indentation = " " - props.new_indentation = "\t" - - - -def register_menus(): - bpy.types.TEXT_MT_toolbox.append(right_click_menu_extension) - bpy.types.TEXT_MT_format.append(format_menu_extension) - -def unregister_menus(): - bpy.types.TEXT_MT_toolbox.remove(right_click_menu_extension) - bpy.types.TEXT_MT_format.remove(format_menu_extension) +import bpy +import os +import re +from bpy.props import * +from . text_block import TextBlock + +class SolveWhitespaceInconsistency(bpy.types.Operator): + bl_idname = "code_autocomplete.correct_whitespaces" + bl_label = "Correct Whitespaces" + bl_description = "Convert whitespaces to spaces or tabs depending on what is set for this text block" + + def execute(self, context): + if context.edit_text.use_tabs_as_spaces: + bpy.ops.text.convert_whitespace(type = "SPACES") + else: + bpy.ops.text.convert_whitespace(type = "TABS") + return { "FINISHED" } + +class SelectWholeString(bpy.types.Operator): + bl_idname = "code_autocomplete.select_whole_string" + bl_label = "Select Whole String" + bl_description = "" + bl_options = {"REGISTER"} + + def execute(self, context): + text_block = TextBlock.get_active() + if not text_block: return {"CANCELLED"} + + line_text = text_block.current_line + character_index = text_block.current_character_index + + string_letter = text_block.get_string_definition_type(line_text, character_index) + if string_letter is None: return {"CANCELLED"} + start, end = text_block.get_range_surrounded_by_letter(line_text, string_letter, character_index) + if start != end: + text_block.set_selection_in_line(start, end) + + return {"FINISHED"} + + +class ConvertFileIndentation(bpy.types.Operator): + bl_idname = "code_autocomplete.convert_file_indentation" + bl_label = "Convert File Indentation" + bl_description = "" + bl_options = {"REGISTER"} + + path: StringProperty() + old_indentation: StringProperty(default = "\t") + new_indentation: StringProperty(default = " ") + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + if not os.path.exists(self.path): return {"CANCELLED"} + + old = self.old_indentation + new = self.new_indentation + + file = open(self.path, "r") + lines = file.readlines() + file.close() + + new_lines = [] + for line in lines: + new_line = "" + while line.startswith(old): + new_line += new + line = line[len(old):] + new_line += line.rstrip() + new_lines.append(new_line) + + file = open(self.path, "w") + file.write("\n".join(new_lines)) + file.close() + return {"FINISHED"} + + +class SelectTextBlockMenu(bpy.types.Menu): + bl_idname = "code_autocomplete_select_text_block" + bl_label = "Select Text Block" + + def draw(self, context): + layout = self.layout + + if len(bpy.data.texts) == 0: + layout.label(text="There are no texts in this file", icon = "INFO") + else: + for text in bpy.data.texts: + operator = layout.operator("code_autocomplete.open_text_block", text = text.name) + operator.name = text.name + +class OpenTextBlock(bpy.types.Operator): + bl_idname = "code_autocomplete.open_text_block" + bl_label = "Open Text Block" + bl_description = "" + bl_options = {"REGISTER"} + + name: StringProperty() + + @classmethod + def poll(cls, context): + return hasattr(context.space_data, "text") + + def execute(self, context): + context.space_data.text = bpy.data.texts[self.name] + return {"FINISHED"} + + +def right_click_menu_extension(self, context): + layout = self.layout + layout.separator() + layout.operator("text.comment") + layout.operator("text.uncomment") + + text_block = TextBlock.get_active() + if text_block: + line = text_block.current_line + match = re.match("def (\w+)\(\):", line) + if match: + function_name = match.group(1) + operator = layout.operator("code_autocomplete.execute_function") + operator.filepath = text_block.filepath + operator.function_name = function_name + + +def format_menu_extension(self, context): + text_block = TextBlock.get_active() + if text_block: + layout = self.layout + layout.operator("code_autocomplete.build_script") + layout.operator("code_autocomplete.correct_whitespaces") + props = layout.operator("code_autocomplete.convert_addon_indentation") + if text_block.use_tabs_as_spaces: + props.old_indentation = "\t" + props.new_indentation = " " + else: + props.old_indentation = " " + props.new_indentation = "\t" + + + +def register_menus(): + bpy.types.TEXT_MT_context_menu.append(right_click_menu_extension) + bpy.types.TEXT_MT_format.append(format_menu_extension) + +def unregister_menus(): + bpy.types.TEXT_MT_context_menu.remove(right_click_menu_extension) + bpy.types.TEXT_MT_format.remove(format_menu_extension) + diff --git a/settings.py b/settings.py index 5ed5189..503e46d 100644 --- a/settings.py +++ b/settings.py @@ -1,72 +1,72 @@ -import bpy -import os -from bpy.props import * - -addon_name = os.path.basename(os.path.dirname(__file__)) - - -def prop_changed(self, context): - for area in bpy.context.screen.areas: - if area.type == "TEXT_EDITOR": - area.tag_redraw() - -class CompletionProviders (bpy.types.PropertyGroup): - use_jedi_completion = BoolProperty(default = True, name = "Use Jedi Completion", - update = prop_changed, description = "Use the Jedi autocompletion library for python") - use_word_completion = BoolProperty(default = True, name = "Use Word Completion", - update = prop_changed, description = "The context box will also contain words that you already used in the file") - use_operator_completion = BoolProperty(default = True, name = "Use Operator Completion", - update = prop_changed, description = "Activate the autocompletion for calling operators (bpy.ops)") - -class ContextBoxProperties(bpy.types.PropertyGroup): - font_size = IntProperty(default = 12, name = "Font Size", min = 10, update = prop_changed) - line_height = IntProperty(default = 21, name = "Line Height", min = 5, update = prop_changed) - width = IntProperty(default = 200, name = "Width", min = 10, update = prop_changed) - padding = IntProperty(default = 4, name = "Padding", min = 0, update = prop_changed) - lines = IntProperty(default = 8, name = "Lines", min = 1, update = prop_changed) - -class DescriptionBoxProperties(bpy.types.PropertyGroup): - font_size = IntProperty(default = 12, name = "Font Size", min = 10, update = prop_changed) - line_height = IntProperty(default = 21, name = "Line Height", min = 5, update = prop_changed) - padding = IntProperty(default = 4, name = "Padding", min = 0, update = prop_changed) - - -class AddonPreferences(bpy.types.AddonPreferences): - bl_idname = addon_name - - completion_providers = PointerProperty(type = CompletionProviders) - context_box = PointerProperty(type = ContextBoxProperties) - description_box = PointerProperty(type = DescriptionBoxProperties) - - debug = BoolProperty(default = False, name = "Debug", - update = prop_changed, description = "Turn on to get some debug information from this addon") - - def draw(self, context): - layout = self.layout - - col = layout.column() - col.label("Completion Providers:") - col.prop(self.completion_providers, "use_jedi_completion", "Jedi") - col.prop(self.completion_providers, "use_word_completion", "Existing Words") - col.prop(self.completion_providers, "use_operator_completion", "Operators") - - row = layout.row() - col = row.column(align = True) - col.label("Context Box") - col.prop(self.context_box, "font_size") - col.prop(self.context_box, "line_height") - col.prop(self.context_box, "width") - col.prop(self.context_box, "padding") - col.prop(self.context_box, "lines") - - col = row.column(align = True) - col.label("Description Box") - col.prop(self.description_box, "font_size") - col.prop(self.description_box, "line_height") - col.prop(self.description_box, "padding") - - layout.prop(self, "debug") - -def get_preferences(): - addon = bpy.context.user_preferences.addons.get(addon_name) - return getattr(addon, "preferences", None) +import bpy +import os +from bpy.props import * + +addon_name = os.path.basename(os.path.dirname(__file__)) + + +def prop_changed(self, context): + for area in bpy.context.screen.areas: + if area.type == "TEXT_EDITOR": + area.tag_redraw() + +class CompletionProviders (bpy.types.PropertyGroup): + use_jedi_completion: BoolProperty(default = True, name = "Use Jedi Completion", + update = prop_changed, description = "Use the Jedi autocompletion library for python") + use_word_completion: BoolProperty(default = True, name = "Use Word Completion", + update = prop_changed, description = "The context box will also contain words that you already used in the file") + use_operator_completion: BoolProperty(default = True, name = "Use Operator Completion", + update = prop_changed, description = "Activate the autocompletion for calling operators (bpy.ops)") + +class ContextBoxProperties(bpy.types.PropertyGroup): + font_size: IntProperty(default = 12, name = "Font Size", min = 10, update = prop_changed) + line_height: IntProperty(default = 21, name = "Line Height", min = 5, update = prop_changed) + width: IntProperty(default = 200, name = "Width", min = 10, update = prop_changed) + padding: IntProperty(default = 4, name = "Padding", min = 0, update = prop_changed) + lines: IntProperty(default = 8, name = "Lines", min = 1, update = prop_changed) + +class DescriptionBoxProperties(bpy.types.PropertyGroup): + font_size: IntProperty(default = 12, name = "Font Size", min = 10, update = prop_changed) + line_height: IntProperty(default = 21, name = "Line Height", min = 5, update = prop_changed) + padding: IntProperty(default = 4, name = "Padding", min = 0, update = prop_changed) + + +class AddonPreferences(bpy.types.AddonPreferences): + bl_idname = addon_name + + completion_providers: PointerProperty(type = CompletionProviders) + context_box: PointerProperty(type = ContextBoxProperties) + description_box: PointerProperty(type = DescriptionBoxProperties) + + debug: BoolProperty(default = False, name = "Debug", + update = prop_changed, description = "Turn on to get some debug information from this addon") + + def draw(self, context): + layout = self.layout + + col = layout.column() + col.label(text="Completion Providers:") + col.prop(self.completion_providers, "use_jedi_completion", "Jedi") + col.prop(self.completion_providers, "use_word_completion", "Existing Words") + col.prop(self.completion_providers, "use_operator_completion", "Operators") + + row = layout.row() + col = row.column(align = True) + col.label(text="Context Box") + col.prop(self.context_box, "font_size") + col.prop(self.context_box, "line_height") + col.prop(self.context_box, "width") + col.prop(self.context_box, "padding") + col.prop(self.context_box, "lines") + + col = row.column(align = True) + col.label(text="Description Box") + col.prop(self.description_box, "font_size") + col.prop(self.description_box, "line_height") + col.prop(self.description_box, "padding") + + layout.prop(self, "debug") + +def get_preferences(): + addon = bpy.context.preferences.addons.get(addon_name) + return getattr(addon, "preferences", None) diff --git a/text_block.py b/text_block.py index 638e017..40fac64 100644 --- a/text_block.py +++ b/text_block.py @@ -1,342 +1,342 @@ -import bpy, re -from mathutils import Vector - -class TextBlock: - def __init__(self, text_block): - if text_block is None: raise AttributeError() - self.text_block = text_block - self.set_context() - - def set_context(self, window = None, area = None, region = None, space = None): - context = bpy.context - self.window = window if window else context.window - self.area = area if window else context.area - self.region = region if window else context.region - self.space = space if window else context.space_data - - @classmethod - def get_active(cls): - text = getattr(bpy.context.space_data, "text", None) - if text: return TextBlock(text) - return None - - @property - def filepath(self): - return self.text_block.filepath - - @property - def current_cursor_region_location(self): - space = bpy.context.space_data - line = self.current_line_index - character = self.current_character_index - location = space.region_location_from_cursor(line, character) - return Vector(location) - - @property - def use_tabs_as_spaces(self): - return self.text_block.use_tabs_as_spaces - - @property - def current_line(self): - return self.text_block.current_line.body - @current_line.setter - def current_line(self, text): - self.text_block.current_line.body = text - - @property - def cursor_position(self): - return self.current_line_index, self.current_character_index - @cursor_position.setter - def cursor_position(self, position): - self.current_line_index = position[0] - self.current_character_index = position[1] - - @property - def current_character_index(self): - return self.get_character_index() - @current_character_index.setter - def current_character_index(self, index): - self.set_cursor_position_horizontal(index) - - @property - def current_line_index(self): - return self.get_line_index() - @current_line_index.setter - def current_line_index(self, index): - self.set_cursor_position_vertical(index) - - @property - def line_amount(self): - return len(self.text_block.lines) - - @property - def text_before_cursor(self): - return self.current_line[:self.current_character_index] - - @property - def current_word(self): - return self.get_last_word(self.text_before_cursor) - - - @property - def lines(self): - return self.get_all_lines() - @lines.setter - def lines(self, lines): - cursor_position = self.cursor_position - text = "\n".join(lines) - self.text_block.from_string(text) - self.cursor_position = cursor_position - - def get_all_lines(self): - return list(self.iter_lines()) - - def iter_lines(self): - for line in self.text_block.lines: - yield line.body - - def set_line_text(self, line_index, text): - self.text_block.lines[line_index].body = text - - # 'bpy.context.sce' -> 'sce' - def get_last_word(self, text): - match = re.search("(?!\w*\W).*", text) - if match: return match.group() - return "" - - @property - def parents_of_current_word(self): - return self.get_parent_words(self.text_before_cursor) - - # 'bpy.context.sce' -> ['bpy', 'context'] - def get_parent_words(self, text): - parents = [] - text = self.text_before_cursor - while True: - parent = self.get_parent_word(text) - if parent is None: break - text = text[:-len(self.get_last_word(text))-1] - parents.append(parent) - parents.reverse() - return parents - - @property - def parent_of_current_word(self): - return self.get_parent_word(self.text_before_cursor) - - # 'bpy.context.sce' -> 'context' - def get_parent_word(self, text): - match = re.search("(\w+)\.(?!.*\W)", text) - if match: - return match.group(1) - return None - - @property - def text(self): - return self.text_block.as_string() - - def get_existing_words(self): - existing_words = [] - existing_parts = set(re.sub("[^\w]", " ", self.text).split()) - for part in existing_parts: - if not part.isdigit(): existing_words.append(part) - return existing_words - - def insert(self, text): - bpy.ops.text.insert(self.override, text = text) - - def get_current_text_after_pattern(self, pattern): - return self.get_text_after_pattern(pattern, self.text_before_cursor) - - def get_text_after_pattern(self, pattern, text): - match = self.get_last_match(pattern, text) - if match: - return text[match.end():] - - def get_last_match(self, pattern, text): - match = None - for match in re.finditer(pattern, text): pass - return match - - def search_pattern_in_current_line(self, pattern): - return re.search(pattern, self.current_line) - - def replace_current_word(self, new_word): - self.delete_current_word() - self.insert(new_word) - - def delete_current_word(self): - match = re.search("\w*$", self.text_before_cursor) - if match: - length = match.end() - match.start() - for i in range(length): - self.remove_character_before_cursor() - - # "this.is.a.test(type = 'myt" -> "this.is.a.test" - def get_current_function_path(self): - text = self.text_before_cursor - open_bracket_index = self.get_current_open_bracket_index(text) - if open_bracket_index == -1: return None - text_before = text[:open_bracket_index-1] - match = re.search(r"(\w[\w\.]+\w)$", text_before) - if match: - return match.group(1) - return None - - # "test = this.is.anoth" -> "this.is" | "test = this.is.anoth." -> "this.is.anoth" - def get_current_parent_path(self): - path = self.get_current_path() - if self.text_before_cursor.endswith("."): return path - match = re.search(r"([\w\.]+)\.(?!.*\.)", path) - if match: - return match.group(1) - return "" - - # "test = this.is.anoth" -> "this.is.anoth" - def get_current_path(self): - text_before = self.text_before_cursor - match = re.search("(\w[\w\.]+\w)\.?$", text_before) - if match: - return match.group(1) - return "" - - # " event.type = 't" -> "event.type" - def get_current_line_assign_variable_path(self): - text_before = self.text_before_cursor - match = re.fullmatch("\s*([\w\.]+)\s*=.*", text_before) - if match: - return match.group(1) - return None - - # "if event.type == "A" and event.value != 'RE" -> "event.value" - def get_current_compare_variable_path(self): - text_before = self.text_before_cursor - match = self.get_last_match("([\w\.]+)\s*(==|<|>|!=|(not )?in \[)", text_before) - if match: - return match.group(1) - return None - - def get_current_open_bracket_index(self, text): - close_bracket_counter = 0 - current_open_bracket_index = -1 - - for i, c in enumerate(reversed(text)): - if c == ")": close_bracket_counter += 1 - elif c == "(": - if close_bracket_counter > 0: close_bracket_counter -= 1 - else: - current_open_bracket_index = len(text) - i - break - return current_open_bracket_index - - def select_match_in_current_line(self, match): - if match: - self.set_selection_in_line(match.start() + 1, match.end() + 1) - - def delete_selection(self): - self.insert(" ") - self.remove_character_before_cursor() - - def get_string_definition_type(self, text, current_index): - string_letter = None - for i in range(current_index): - letter = text[i] - if letter == '"': - if string_letter == '"': - string_letter = None - elif string_letter is None: - string_letter = letter - if letter == "'": - if string_letter == "'": - string_letter = None - elif string_letter is None: - string_letter = letter - return string_letter - - def get_range_surrounded_by_letter(self, text, letter, current_index): - text_before = text[:current_index] - text_after = text[current_index:] - - start_index = text_before.rfind(letter) + 1 - end_index = text_after.find(letter) + len(text_before) - - if 0 < start_index < end_index: - return start_index+1, end_index+1 - return current_index, current_index - - def select_text_in_current_line(self, text): - line = self.current_line - start = line.find(text) - if start != -1: - end = start + len(text) - self.set_selection_in_line(start + 1, end + 1) - - def set_selection_in_line(self, start, end): - line = self.current_line_index - if start > end: start, end = end, start - self.set_selection(line, start, line, end) - - def set_selection(self, start_line, start_character, end_line, end_character): - self.set_cursor_position(start_line, start_character, select = False) - self.set_cursor_position(end_line, end_character, select = True) - - def set_cursor_position(self, line_index, character_index, select = False): - self.set_cursor_position_vertical(line_index, select) - self.set_cursor_position_horizontal(character_index, select) - - def set_cursor_position_horizontal(self, target_index, select = False): - self.move_cursor_to_line_end(select) - self.move_cursor_left_to_target_index(target_index, select) - - def move_cursor_left_to_target_index(self, target_index, select): - target_index = max(1, target_index) - while self.get_character_index(select) >= target_index: - self.move_cursor_left(select) - - def set_cursor_position_vertical(self, target_line, select = False): - move_function = self.move_cursor_up - if target_line > self.get_line_index(select): - move_function = self.move_cursor_down - move_amount = abs(self.current_line_index - target_line) - for i in range(move_amount): - move_function(select) - - def get_character_index(self, select = False): - if select: return self.text_block.select_end_character - return self.text_block.current_character - def get_line_index(self, select = False): - return self.text_block.current_line_index - - def move_cursor_to_line_begin(self, select = False): - self.move_cursor("LINE_BEGIN", select) - def move_cursor_to_line_end(self, select = False): - self.move_cursor("LINE_END", select) - - # note: this may change the character index more than one (if there is TAB) - def move_cursor_right(self, select = False): - self.move_cursor("NEXT_CHARACTER", select) - def move_cursor_left(self, select = False): - self.move_cursor("PREVIOUS_CHARACTER", select) - - def move_cursor_up(self, select = False): - self.move_cursor("PREVIOUS_LINE", select) - def move_cursor_down(self, select = False): - self.move_cursor("NEXT_LINE", select) - - def move_cursor(self, type, select = False): - if select: bpy.ops.text.move_select(self.override, type = type) - else: bpy.ops.text.move(self.override, type = type) - - def remove_character_before_cursor(self): - bpy.ops.text.delete(self.override, type = "PREVIOUS_CHARACTER") - - def line_break(self): - bpy.ops.text.line_break(self.override) - - @property - def override(self): - return {"edit_text" : self.text_block, - "area" : self.area, - "space_data" : self.space, - "region" : self.region, - "window" : self.window} +import bpy, re +from mathutils import Vector + +class TextBlock: + def __init__(self, text_block): + if text_block is None: raise AttributeError() + self.text_block = text_block + self.set_context() + + def set_context(self, window = None, area = None, region = None, space = None): + context = bpy.context + self.window = window if window else context.window + self.area = area if window else context.area + self.region = region if window else context.region + self.space = space if window else context.space_data + + @classmethod + def get_active(cls): + text = getattr(bpy.context.space_data, "text", None) + if text: return TextBlock(text) + return None + + @property + def filepath(self): + return self.text_block.filepath + + @property + def current_cursor_region_location(self): + space = bpy.context.space_data + line = self.current_line_index + character = self.current_character_index + location = space.region_location_from_cursor(line, character) + return Vector(location) + + @property + def use_tabs_as_spaces(self): + return self.text_block.use_tabs_as_spaces + + @property + def current_line(self): + return self.text_block.current_line.body + @current_line.setter + def current_line(self, text): + self.text_block.current_line.body = text + + @property + def cursor_position(self): + return self.current_line_index, self.current_character_index + @cursor_position.setter + def cursor_position(self, position): + self.current_line_index = position[0] + self.current_character_index = position[1] + + @property + def current_character_index(self): + return self.get_character_index() + @current_character_index.setter + def current_character_index(self, index): + self.set_cursor_position_horizontal(index) + + @property + def current_line_index(self): + return self.get_line_index() + @current_line_index.setter + def current_line_index(self, index): + self.set_cursor_position_vertical(index) + + @property + def line_amount(self): + return len(self.text_block.lines) + + @property + def text_before_cursor(self): + return self.current_line[:self.current_character_index] + + @property + def current_word(self): + return self.get_last_word(self.text_before_cursor) + + + @property + def lines(self): + return self.get_all_lines() + @lines.setter + def lines(self, lines): + cursor_position = self.cursor_position + text = "\n".join(lines) + self.text_block.from_string(text) + self.cursor_position = cursor_position + + def get_all_lines(self): + return list(self.iter_lines()) + + def iter_lines(self): + for line in self.text_block.lines: + yield line.body + + def set_line_text(self, line_index, text): + self.text_block.lines[line_index].body = text + + # 'bpy.context.sce' -> 'sce' + def get_last_word(self, text): + match = re.search("(?!\w*\W).*", text) + if match: return match.group() + return "" + + @property + def parents_of_current_word(self): + return self.get_parent_words(self.text_before_cursor) + + # 'bpy.context.sce' -> ['bpy', 'context'] + def get_parent_words(self, text): + parents = [] + text = self.text_before_cursor + while True: + parent = self.get_parent_word(text) + if parent is None: break + text = text[:-len(self.get_last_word(text))-1] + parents.append(parent) + parents.reverse() + return parents + + @property + def parent_of_current_word(self): + return self.get_parent_word(self.text_before_cursor) + + # 'bpy.context.sce' -> 'context' + def get_parent_word(self, text): + match = re.search("(\w+)\.(?!.*\W)", text) + if match: + return match.group(1) + return None + + @property + def text(self): + return self.text_block.as_string() + + def get_existing_words(self): + existing_words = [] + existing_parts = set(re.sub("[^\w]", " ", self.text).split()) + for part in existing_parts: + if not part.isdigit(): existing_words.append(part) + return existing_words + + def insert(self, text): + bpy.ops.text.insert(self.override, text = text) + + def get_current_text_after_pattern(self, pattern): + return self.get_text_after_pattern(pattern, self.text_before_cursor) + + def get_text_after_pattern(self, pattern, text): + match = self.get_last_match(pattern, text) + if match: + return text[match.end():] + + def get_last_match(self, pattern, text): + match = None + for match in re.finditer(pattern, text): pass + return match + + def search_pattern_in_current_line(self, pattern): + return re.search(pattern, self.current_line) + + def replace_current_word(self, new_word): + self.delete_current_word() + self.insert(new_word) + + def delete_current_word(self): + match = re.search("\w*$", self.text_before_cursor) + if match: + length = match.end() - match.start() + for i in range(length): + self.remove_character_before_cursor() + + # "this.is.a.test(type = 'myt" -> "this.is.a.test" + def get_current_function_path(self): + text = self.text_before_cursor + open_bracket_index = self.get_current_open_bracket_index(text) + if open_bracket_index == -1: return None + text_before = text[:open_bracket_index-1] + match = re.search(r"(\w[\w\.]+\w)$", text_before) + if match: + return match.group(1) + return None + + # "test = this.is.anoth" -> "this.is" | "test = this.is.anoth." -> "this.is.anoth" + def get_current_parent_path(self): + path = self.get_current_path() + if self.text_before_cursor.endswith("."): return path + match = re.search(r"([\w\.]+)\.(?!.*\.)", path) + if match: + return match.group(1) + return "" + + # "test = this.is.anoth" -> "this.is.anoth" + def get_current_path(self): + text_before = self.text_before_cursor + match = re.search("(\w[\w\.]+\w)\.?$", text_before) + if match: + return match.group(1) + return "" + + # " event.type = 't" -> "event.type" + def get_current_line_assign_variable_path(self): + text_before = self.text_before_cursor + match = re.fullmatch("\s*([\w\.]+)\s*=.*", text_before) + if match: + return match.group(1) + return None + + # "if event.type == "A" and event.value != 'RE" -> "event.value" + def get_current_compare_variable_path(self): + text_before = self.text_before_cursor + match = self.get_last_match("([\w\.]+)\s*(==|<|>|!=|(not )?in \[)", text_before) + if match: + return match.group(1) + return None + + def get_current_open_bracket_index(self, text): + close_bracket_counter = 0 + current_open_bracket_index = -1 + + for i, c in enumerate(reversed(text)): + if c == ")": close_bracket_counter += 1 + elif c == "(": + if close_bracket_counter > 0: close_bracket_counter -= 1 + else: + current_open_bracket_index = len(text) - i + break + return current_open_bracket_index + + def select_match_in_current_line(self, match): + if match: + self.set_selection_in_line(match.start() + 1, match.end() + 1) + + def delete_selection(self): + self.insert(" ") + self.remove_character_before_cursor() + + def get_string_definition_type(self, text, current_index): + string_letter = None + for i in range(current_index): + letter = text[i] + if letter == '"': + if string_letter == '"': + string_letter = None + elif string_letter is None: + string_letter = letter + if letter == "'": + if string_letter == "'": + string_letter = None + elif string_letter is None: + string_letter = letter + return string_letter + + def get_range_surrounded_by_letter(self, text, letter, current_index): + text_before = text[:current_index] + text_after = text[current_index:] + + start_index = text_before.rfind(letter) + 1 + end_index = text_after.find(letter) + len(text_before) + + if 0 < start_index < end_index: + return start_index+1, end_index+1 + return current_index, current_index + + def select_text_in_current_line(self, text): + line = self.current_line + start = line.find(text) + if start != -1: + end = start + len(text) + self.set_selection_in_line(start + 1, end + 1) + + def set_selection_in_line(self, start, end): + line = self.current_line_index + if start > end: start, end = end, start + self.set_selection(line, start, line, end) + + def set_selection(self, start_line, start_character, end_line, end_character): + self.set_cursor_position(start_line, start_character, select = False) + self.set_cursor_position(end_line, end_character, select = True) + + def set_cursor_position(self, line_index, character_index, select = False): + self.set_cursor_position_vertical(line_index, select) + self.set_cursor_position_horizontal(character_index, select) + + def set_cursor_position_horizontal(self, target_index, select = False): + self.move_cursor_to_line_end(select) + self.move_cursor_left_to_target_index(target_index, select) + + def move_cursor_left_to_target_index(self, target_index, select): + target_index = max(1, target_index) + while self.get_character_index(select) >= target_index: + self.move_cursor_left(select) + + def set_cursor_position_vertical(self, target_line, select = False): + move_function = self.move_cursor_up + if target_line > self.get_line_index(select): + move_function = self.move_cursor_down + move_amount = abs(self.current_line_index - target_line) + for i in range(move_amount): + move_function(select) + + def get_character_index(self, select = False): + if select: return self.text_block.select_end_character + return self.text_block.current_character + def get_line_index(self, select = False): + return self.text_block.current_line_index + + def move_cursor_to_line_begin(self, select = False): + self.move_cursor("LINE_BEGIN", select) + def move_cursor_to_line_end(self, select = False): + self.move_cursor("LINE_END", select) + + # note: this may change the character index more than one (if there is TAB) + def move_cursor_right(self, select = False): + self.move_cursor("NEXT_CHARACTER", select) + def move_cursor_left(self, select = False): + self.move_cursor("PREVIOUS_CHARACTER", select) + + def move_cursor_up(self, select = False): + self.move_cursor("PREVIOUS_LINE", select) + def move_cursor_down(self, select = False): + self.move_cursor("NEXT_LINE", select) + + def move_cursor(self, type, select = False): + if select: bpy.ops.text.move_select(self.override, type = type) + else: bpy.ops.text.move(self.override, type = type) + + def remove_character_before_cursor(self): + bpy.ops.text.delete(self.override, type = "PREVIOUS_CHARACTER") + + def line_break(self): + bpy.ops.text.line_break(self.override) + + @property + def override(self): + return {"edit_text" : self.text_block, + "area" : self.area, + "space_data" : self.space, + "region" : self.region, + "window" : self.window} diff --git a/utils/__pycache__/__init__.cpython-37.pyc b/utils/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..53353cb Binary files /dev/null and b/utils/__pycache__/__init__.cpython-37.pyc differ diff --git a/utils/__pycache__/variable_name_conversion.cpython-37.pyc b/utils/__pycache__/variable_name_conversion.cpython-37.pyc new file mode 100644 index 0000000..757e821 Binary files /dev/null and b/utils/__pycache__/variable_name_conversion.cpython-37.pyc differ diff --git a/utils/variable_name_conversion.py b/utils/variable_name_conversion.py index 6446051..0d118c1 100644 --- a/utils/variable_name_conversion.py +++ b/utils/variable_name_conversion.py @@ -1,36 +1,36 @@ -import re - -def get_valid_variable_name(name): - return re.sub("\W+", "", name) - -def get_lower_case_with_underscores(name): - words = get_words(name) - words = [word.lower() for word in words] - output = "_".join(words) - return output - -def get_separated_capitalized_words(name): - words = get_words(name) - words = [word.capitalize() for word in words] - output = " ".join(words) - return output - -def get_words(name): - words = [] - current_word = "" - for char in name: - if char.islower(): - current_word += char - if char.isupper(): - if current_word.isupper() or len(current_word) == 0: - current_word += char - else: - words.append(current_word) - current_word = char - if char == "_": - words.append(current_word) - current_word = "" - - words.append(current_word) - words = [word for word in words if len(word) > 0] - return words +import re + +def get_valid_variable_name(name): + return re.sub("\W+", "", name) + +def get_lower_case_with_underscores(name): + words = get_words(name) + words = [word.lower() for word in words] + output = "_".join(words) + return output + +def get_separated_capitalized_words(name): + words = get_words(name) + words = [word.capitalize() for word in words] + output = " ".join(words) + return output + +def get_words(name): + words = [] + current_word = "" + for char in name: + if char.islower(): + current_word += char + if char.isupper(): + if current_word.isupper() or len(current_word) == 0: + current_word += char + else: + words.append(current_word) + current_word = char + if char == "_": + words.append(current_word) + current_word = "" + + words.append(current_word) + words = [word for word in words if len(word) > 0] + return words