diff options
Diffstat (limited to 'release/scripts/startup/bl_operators/wm.py')
-rw-r--r-- | release/scripts/startup/bl_operators/wm.py | 440 |
1 files changed, 317 insertions, 123 deletions
diff --git a/release/scripts/startup/bl_operators/wm.py b/release/scripts/startup/bl_operators/wm.py index 869070ed778..022ee1576d8 100644 --- a/release/scripts/startup/bl_operators/wm.py +++ b/release/scripts/startup/bl_operators/wm.py @@ -21,12 +21,12 @@ import bpy from bpy.types import Operator from bpy.props import ( - StringProperty, - BoolProperty, - IntProperty, - FloatProperty, - EnumProperty, - ) + BoolProperty, + EnumProperty, + FloatProperty, + IntProperty, + StringProperty, +) from bpy.app.translations import pgettext_tip as tip_ @@ -59,8 +59,8 @@ rna_relative_prop = BoolProperty( def context_path_validate(context, data_path): try: value = eval("context.%s" % data_path) if data_path else Ellipsis - except AttributeError as e: - if str(e).startswith("'NoneType'"): + except AttributeError as ex: + if str(ex).startswith("'NoneType'"): # One of the items in the rna path is None, just ignore this value = Ellipsis else: @@ -130,6 +130,20 @@ def execute_context_assign(self, context): return operator_path_undo_return(context, data_path) +def module_filesystem_remove(path_base, module_name): + import os + module_name = os.path.splitext(module_name)[0] + for f in os.listdir(path_base): + f_base = os.path.splitext(f)[0] + if f_base == module_name: + f_full = os.path.join(path_base, f) + + if os.path.isdir(f_full): + os.rmdir(f_full) + else: + os.remove(f_full) + + class BRUSH_OT_active_index_set(Operator): """Set active sculpt/paint brush from it's number""" bl_idname = "brush.active_index_set" @@ -145,11 +159,12 @@ class BRUSH_OT_active_index_set(Operator): description="Brush number", ) - _attr_dict = {"sculpt": "use_paint_sculpt", - "vertex_paint": "use_paint_vertex", - "weight_paint": "use_paint_weight", - "image_paint": "use_paint_image", - } + _attr_dict = { + "sculpt": "use_paint_sculpt", + "vertex_paint": "use_paint_vertex", + "weight_paint": "use_paint_weight", + "image_paint": "use_paint_image", + } def execute(self, context): attr = self._attr_dict.get(self.mode) @@ -831,7 +846,7 @@ class WM_OT_context_modal_mouse(Operator): class WM_OT_url_open(Operator): - "Open a website in the web-browser" + """Open a website in the web-browser""" bl_idname = "wm.url_open" bl_label = "" bl_options = {'INTERNAL'} @@ -848,7 +863,7 @@ class WM_OT_url_open(Operator): class WM_OT_path_open(Operator): - "Open a path in a file browser" + """Open a path in a file browser""" bl_idname = "wm.path_open" bl_label = "" bl_options = {'INTERNAL'} @@ -892,6 +907,15 @@ class WM_OT_path_open(Operator): def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""): + + def operator_exists_pair(a, b): + # Not fast, this is only for docs. + return b in dir(getattr(bpy.ops, a)) + + def operator_exists_single(a): + a, b = a.partition("_OT_")[::2] + return operator_exists_pair(a.lower(), b) + id_split = doc_id.split(".") url = rna = None @@ -905,48 +929,51 @@ def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""): class_name, class_prop = id_split # an operator (common case - just button referencing an op) - if hasattr(bpy.types, class_name.upper() + "_OT_" + class_prop): + if operator_exists_pair(class_name, class_prop): if do_url: - url = ("%s/bpy.ops.%s.html#bpy.ops.%s.%s" % (url_prefix, class_name, class_name, class_prop)) + url = ( + "%s/bpy.ops.%s.html#bpy.ops.%s.%s" % + (url_prefix, class_name, class_name, class_prop) + ) + else: + rna = "bpy.ops.%s.%s" % (class_name, class_prop) + elif operator_exists_single(class_name): + # note: ignore the prop name since we don't have a way to link into it + class_name, class_prop = class_name.split("_OT_", 1) + class_name = class_name.lower() + if do_url: + url = ( + "%s/bpy.ops.%s.html#bpy.ops.%s.%s" % + (url_prefix, class_name, class_name, class_prop) + ) else: rna = "bpy.ops.%s.%s" % (class_name, class_prop) else: + # an RNA setting, common case rna_class = getattr(bpy.types, class_name) - # an operator setting (selected from a running operator), rare case - # note: Py defined operators are subclass of Operator, - # C defined operators are subclass of OperatorProperties. - # we may need to check on this at some point. - if issubclass(rna_class, (bpy.types.Operator, bpy.types.OperatorProperties)): - # note: ignore the prop name since we don't have a way to link into it - class_name, class_prop = class_name.split("_OT_", 1) - class_name = class_name.lower() + # detect if this is a inherited member and use that name instead + rna_parent = rna_class.bl_rna + rna_prop = rna_parent.properties.get(class_prop) + if rna_prop: + rna_parent = rna_parent.base + while rna_parent and rna_prop == rna_parent.properties.get(class_prop): + class_name = rna_parent.identifier + rna_parent = rna_parent.base + if do_url: - url = ("%s/bpy.ops.%s.html#bpy.ops.%s.%s" % (url_prefix, class_name, class_name, class_prop)) + url = ( + "%s/bpy.types.%s.html#bpy.types.%s.%s" % + (url_prefix, class_name, class_name, class_prop) + ) else: - rna = "bpy.ops.%s.%s" % (class_name, class_prop) + rna = "bpy.types.%s.%s" % (class_name, class_prop) else: - # an RNA setting, common case - - # detect if this is a inherited member and use that name instead - rna_parent = rna_class.bl_rna - rna_prop = rna_parent.properties.get(class_prop) - if rna_prop: - rna_parent = rna_parent.base - while rna_parent and rna_prop == rna_parent.properties.get(class_prop): - class_name = rna_parent.identifier - rna_parent = rna_parent.base - - if do_url: - url = ("%s/bpy.types.%s.html#bpy.types.%s.%s" % (url_prefix, class_name, class_name, class_prop)) - else: - rna = ("bpy.types.%s.%s" % (class_name, class_prop)) + # We assume this is custom property, only try to generate generic url/rna_id... + if do_url: + url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,)) else: - # We assume this is custom property, only try to generate generic url/rna_id... - if do_url: - url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,)) - else: - rna = "bpy.types.bpy_struct" + rna = "bpy.types.bpy_struct" return url if do_url else rna @@ -962,9 +989,12 @@ class WM_OT_doc_view_manual(Operator): def _find_reference(rna_id, url_mapping, verbose=True): if verbose: print("online manual check for: '%s'... " % rna_id) - from fnmatch import fnmatch + from fnmatch import fnmatchcase + # XXX, for some reason all RNA ID's are stored lowercase + # Adding case into all ID's isn't worth the hassle so force lowercase. + rna_id = rna_id.lower() for pattern, url_suffix in url_mapping: - if fnmatch(rna_id, pattern): + if fnmatchcase(rna_id, pattern): if verbose: print(" match found: '%s' --> '%s'" % (pattern, url_suffix)) return url_suffix @@ -989,11 +1019,12 @@ class WM_OT_doc_view_manual(Operator): if url is None: self.report( - {'WARNING'}, - "No reference available %r, " - "Update info in 'rna_manual_reference.py' " - "or callback to bpy.utils.manual_map()" % - self.doc_id) + {'WARNING'}, + "No reference available %r, " + "Update info in 'rna_manual_reference.py' " + "or callback to bpy.utils.manual_map()" % + self.doc_id + ) return {'CANCELLED'} else: import webbrowser @@ -1083,14 +1114,14 @@ class WM_OT_properties_edit(Operator): "use_soft_limits": self.use_soft_limits, "soft_range": (self.soft_min, self.soft_max), "hard_range": (self.min, self.max), - } + } def execute(self, context): from rna_prop_ui import ( - rna_idprop_ui_prop_get, - rna_idprop_ui_prop_clear, - rna_idprop_ui_prop_update, - ) + rna_idprop_ui_prop_get, + rna_idprop_ui_prop_clear, + rna_idprop_ui_prop_update, + ) data_path = self.data_path value = self.value @@ -1204,8 +1235,9 @@ class WM_OT_properties_edit(Operator): self.soft_min = prop_ui.get("soft_min", self.min) self.soft_max = prop_ui.get("soft_max", self.max) self.use_soft_limits = ( - self.min != self.soft_min or - self.max != self.soft_max) + self.min != self.soft_min or + self.max != self.soft_max + ) # store for comparison self._cmp_props = self._cmp_props_get() @@ -1267,9 +1299,9 @@ class WM_OT_properties_add(Operator): def execute(self, context): from rna_prop_ui import ( - rna_idprop_ui_prop_get, - rna_idprop_ui_prop_update, - ) + rna_idprop_ui_prop_get, + rna_idprop_ui_prop_update, + ) data_path = self.data_path item = eval("context.%s" % data_path) @@ -1284,10 +1316,10 @@ class WM_OT_properties_add(Operator): return prop_new - prop = unique_name( - {*item.keys(), - *type(item).bl_rna.properties.keys(), - }) + prop = unique_name({ + *item.keys(), + *type(item).bl_rna.properties.keys(), + }) item[prop] = 1.0 rna_idprop_ui_prop_update(item, prop) @@ -1301,7 +1333,7 @@ class WM_OT_properties_add(Operator): class WM_OT_properties_context_change(Operator): - "Jump to a different tab inside the properties editor" + """Jump to a different tab inside the properties editor""" bl_idname = "wm.properties_context_change" bl_label = "" bl_options = {'INTERNAL'} @@ -1327,9 +1359,9 @@ class WM_OT_properties_remove(Operator): def execute(self, context): from rna_prop_ui import ( - rna_idprop_ui_prop_clear, - rna_idprop_ui_prop_update, - ) + rna_idprop_ui_prop_clear, + rna_idprop_ui_prop_update, + ) data_path = self.data_path item = eval("context.%s" % data_path) prop = self.property @@ -1367,7 +1399,10 @@ class WM_OT_appconfig_default(Operator): filepath = os.path.join(bpy.utils.preset_paths("interaction")[0], "blender.py") if os.path.exists(filepath): - bpy.ops.script.execute_preset(filepath=filepath, menu_idname="USERPREF_MT_interaction_presets") + bpy.ops.script.execute_preset( + filepath=filepath, + menu_idname="USERPREF_MT_interaction_presets", + ) return {'FINISHED'} @@ -1387,7 +1422,10 @@ class WM_OT_appconfig_activate(Operator): filepath = self.filepath.replace("keyconfig", "interaction") if os.path.exists(filepath): - bpy.ops.script.execute_preset(filepath=filepath, menu_idname="USERPREF_MT_interaction_presets") + bpy.ops.script.execute_preset( + filepath=filepath, + menu_idname="USERPREF_MT_interaction_presets", + ) return {'FINISHED'} @@ -1492,7 +1530,7 @@ class WM_OT_blenderplayer_start(Operator): "-g", "show_profile", "=", "%d" % gs.show_framerate_profile, "-g", "show_properties", "=", "%d" % gs.show_debug_properties, "-g", "ignore_deprecation_warnings", "=", "%d" % (not gs.use_deprecation_warnings), - ]) + ]) # finish the call with the path to the blend file args.append(filepath) @@ -1503,7 +1541,7 @@ class WM_OT_blenderplayer_start(Operator): class WM_OT_keyconfig_test(Operator): - "Test key-config for conflicts" + """Test key-config for conflicts""" bl_idname = "wm.keyconfig_test" bl_label = "Test Key Configuration for Conflicts" @@ -1520,7 +1558,7 @@ class WM_OT_keyconfig_test(Operator): class WM_OT_keyconfig_import(Operator): - "Import key configuration from a python script" + """Import key configuration from a python script""" bl_idname = "wm.keyconfig_import" bl_label = "Import Key Configuration..." @@ -1568,8 +1606,8 @@ class WM_OT_keyconfig_import(Operator): shutil.copy(self.filepath, path) else: shutil.move(self.filepath, path) - except Exception as e: - self.report({'ERROR'}, "Installing keymap failed: %s" % e) + except Exception as ex: + self.report({'ERROR'}, "Installing keymap failed: %s" % ex) return {'CANCELLED'} # sneaky way to check we're actually running the code. @@ -1587,7 +1625,7 @@ class WM_OT_keyconfig_import(Operator): class WM_OT_keyconfig_export(Operator): - "Export key configuration to a python script" + """Export key configuration to a python script""" bl_idname = "wm.keyconfig_export" bl_label = "Export Key Configuration..." @@ -1622,10 +1660,11 @@ class WM_OT_keyconfig_export(Operator): wm = context.window_manager - keyconfig_utils.keyconfig_export(wm, - wm.keyconfigs.active, - self.filepath, - ) + keyconfig_utils.keyconfig_export( + wm, + wm.keyconfigs.active, + self.filepath, + ) return {'FINISHED'} @@ -1636,7 +1675,7 @@ class WM_OT_keyconfig_export(Operator): class WM_OT_keymap_restore(Operator): - "Restore key map(s)" + """Restore key map(s)""" bl_idname = "wm.keymap_restore" bl_label = "Restore Key Map(s)" @@ -1659,7 +1698,7 @@ class WM_OT_keymap_restore(Operator): class WM_OT_keyitem_restore(Operator): - "Restore key map item" + """Restore key map item""" bl_idname = "wm.keyitem_restore" bl_label = "Restore Key Map Item" @@ -1684,7 +1723,7 @@ class WM_OT_keyitem_restore(Operator): class WM_OT_keyitem_add(Operator): - "Add key map item" + """Add key map item""" bl_idname = "wm.keyitem_add" bl_label = "Add Key Map Item" @@ -1706,7 +1745,7 @@ class WM_OT_keyitem_add(Operator): class WM_OT_keyitem_remove(Operator): - "Remove key map item" + """Remove key map item""" bl_idname = "wm.keyitem_remove" bl_label = "Remove Key Map Item" @@ -1727,7 +1766,7 @@ class WM_OT_keyitem_remove(Operator): class WM_OT_keyconfig_remove(Operator): - "Remove key config" + """Remove key config""" bl_idname = "wm.keyconfig_remove" bl_label = "Remove Key Config" @@ -1745,6 +1784,7 @@ class WM_OT_keyconfig_remove(Operator): class WM_OT_operator_cheat_sheet(Operator): + """List all the Operators in a text-block, useful for scripting""" bl_idname = "wm.operator_cheat_sheet" bl_label = "Operator Cheat Sheet" @@ -1773,7 +1813,7 @@ class WM_OT_operator_cheat_sheet(Operator): # Add-on Operators class WM_OT_addon_enable(Operator): - "Enable an add-on" + """Enable an add-on""" bl_idname = "wm.addon_enable" bl_label = "Enable Add-on" @@ -1801,12 +1841,14 @@ class WM_OT_addon_enable(Operator): info_ver = info.get("blender", (0, 0, 0)) if info_ver > bpy.app.version: - self.report({'WARNING'}, - ("This script was written Blender " - "version %d.%d.%d and might not " - "function (correctly), " - "though it is enabled" % - info_ver)) + self.report( + {'WARNING'}, + "This script was written Blender " + "version %d.%d.%d and might not " + "function (correctly), " + "though it is enabled" % + info_ver + ) return {'FINISHED'} else: @@ -1817,7 +1859,7 @@ class WM_OT_addon_enable(Operator): class WM_OT_addon_disable(Operator): - "Disable an add-on" + """Disable an add-on""" bl_idname = "wm.addon_disable" bl_label = "Disable Add-on" @@ -1846,7 +1888,7 @@ class WM_OT_addon_disable(Operator): class WM_OT_theme_install(Operator): - "Load and apply a Blender XML theme file" + """Load and apply a Blender XML theme file""" bl_idname = "wm.theme_install" bl_label = "Install Theme..." @@ -1890,7 +1932,10 @@ class WM_OT_theme_install(Operator): try: shutil.copyfile(xmlfile, path_dest) - bpy.ops.script.execute_preset(filepath=path_dest, menu_idname="USERPREF_MT_interface_theme_presets") + bpy.ops.script.execute_preset( + filepath=path_dest, + menu_idname="USERPREF_MT_interface_theme_presets", + ) except: traceback.print_exc() @@ -1905,7 +1950,7 @@ class WM_OT_theme_install(Operator): class WM_OT_addon_refresh(Operator): - "Scan add-on directories for new modules" + """Scan add-on directories for new modules""" bl_idname = "wm.addon_refresh" bl_label = "Refresh" @@ -1917,10 +1962,12 @@ class WM_OT_addon_refresh(Operator): return {'FINISHED'} +# Note: shares some logic with WM_OT_app_template_install +# but not enough to de-duplicate. Fixed here may apply to both. class WM_OT_addon_install(Operator): - "Install an add-on" + """Install an add-on""" bl_idname = "wm.addon_install" - bl_label = "Install from File..." + bl_label = "Install Add-on from File..." overwrite = BoolProperty( name="Overwrite", @@ -1951,20 +1998,6 @@ class WM_OT_addon_install(Operator): options={'HIDDEN'}, ) - @staticmethod - def _module_remove(path_addons, module): - import os - module = os.path.splitext(module)[0] - for f in os.listdir(path_addons): - f_base = os.path.splitext(f)[0] - if f_base == module: - f_full = os.path.join(path_addons, f) - - if os.path.isdir(f_full): - os.rmdir(f_full) - else: - os.remove(f_full) - def execute(self, context): import addon_utils import traceback @@ -2017,7 +2050,7 @@ class WM_OT_addon_install(Operator): if self.overwrite: for f in file_to_extract.namelist(): - WM_OT_addon_install._module_remove(path_addons, f) + module_filesystem_remove(path_addons, f) else: for f in file_to_extract.namelist(): path_dest = os.path.join(path_addons, os.path.basename(f)) @@ -2035,7 +2068,7 @@ class WM_OT_addon_install(Operator): path_dest = os.path.join(path_addons, os.path.basename(pyfile)) if self.overwrite: - WM_OT_addon_install._module_remove(path_addons, os.path.basename(pyfile)) + module_filesystem_remove(path_addons, os.path.basename(pyfile)) elif os.path.exists(path_dest): self.report({'WARNING'}, "File already installed to %r\n" % path_dest) return {'CANCELLED'} @@ -2070,7 +2103,10 @@ class WM_OT_addon_install(Operator): bpy.utils.refresh_script_paths() # print message - msg = tip_("Modules Installed from %r into %r (%s)") % (pyfile, path_addons, ", ".join(sorted(addons_new))) + msg = ( + tip_("Modules Installed (%s) from %r into %r") % + (", ".join(sorted(addons_new)), pyfile, path_addons) + ) print(msg) self.report({'INFO'}, msg) @@ -2083,7 +2119,7 @@ class WM_OT_addon_install(Operator): class WM_OT_addon_remove(Operator): - "Delete the add-on from the file system" + """Delete the add-on from the file system""" bl_idname = "wm.addon_remove" bl_label = "Remove Add-on" @@ -2142,7 +2178,7 @@ class WM_OT_addon_remove(Operator): class WM_OT_addon_expand(Operator): - "Display information and preferences for this add-on" + """Display information and preferences for this add-on""" bl_idname = "wm.addon_expand" bl_label = "" bl_options = {'INTERNAL'} @@ -2164,8 +2200,9 @@ class WM_OT_addon_expand(Operator): return {'FINISHED'} + class WM_OT_addon_userpref_show(Operator): - "Show add-on user preferences" + """Show add-on user preferences""" bl_idname = "wm.addon_userpref_show" bl_label = "" bl_options = {'INTERNAL'} @@ -2186,9 +2223,166 @@ class WM_OT_addon_userpref_show(Operator): info = addon_utils.module_bl_info(mod) info["show_expanded"] = True - bpy.context.user_preferences.active_section = 'ADDONS' + context.user_preferences.active_section = 'ADDONS' context.window_manager.addon_filter = 'All' context.window_manager.addon_search = info["name"] bpy.ops.screen.userpref_show('INVOKE_DEFAULT') return {'FINISHED'} + + +# Note: shares some logic with WM_OT_addon_install +# but not enough to de-duplicate. Fixes here may apply to both. +class WM_OT_app_template_install(Operator): + """Install an application-template""" + bl_idname = "wm.app_template_install" + bl_label = "Install Template from File..." + + overwrite = BoolProperty( + name="Overwrite", + description="Remove existing template with the same ID", + default=True, + ) + + filepath = StringProperty( + subtype='FILE_PATH', + ) + filter_folder = BoolProperty( + name="Filter folders", + default=True, + options={'HIDDEN'}, + ) + filter_glob = StringProperty( + default="*.zip", + options={'HIDDEN'}, + ) + + def execute(self, context): + import traceback + import zipfile + import shutil + import os + + filepath = self.filepath + + path_app_templates = bpy.utils.user_resource( + 'SCRIPTS', os.path.join("startup", "bl_app_templates_user"), + create=True, + ) + + if not path_app_templates: + self.report({'ERROR'}, "Failed to get add-ons path") + return {'CANCELLED'} + + if not os.path.isdir(path_app_templates): + try: + os.makedirs(path_app_templates, exist_ok=True) + except: + traceback.print_exc() + + app_templates_old = set(os.listdir(path_app_templates)) + + # check to see if the file is in compressed format (.zip) + if zipfile.is_zipfile(filepath): + try: + file_to_extract = zipfile.ZipFile(filepath, 'r') + except: + traceback.print_exc() + return {'CANCELLED'} + + if self.overwrite: + for f in file_to_extract.namelist(): + module_filesystem_remove(path_app_templates, f) + else: + for f in file_to_extract.namelist(): + path_dest = os.path.join(path_app_templates, os.path.basename(f)) + if os.path.exists(path_dest): + self.report({'WARNING'}, "File already installed to %r\n" % path_dest) + return {'CANCELLED'} + + try: # extract the file to "bl_app_templates_user" + file_to_extract.extractall(path_app_templates) + except: + traceback.print_exc() + return {'CANCELLED'} + + else: + # Only support installing zipfiles + self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath) + return {'CANCELLED'} + + app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old + + # in case a new module path was created to install this addon. + bpy.utils.refresh_script_paths() + + # print message + msg = ( + tip_("Template Installed (%s) from %r into %r") % + (", ".join(sorted(app_templates_new)), filepath, path_app_templates) + ) + print(msg) + self.report({'INFO'}, msg) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + wm.fileselect_add(self) + return {'RUNNING_MODAL'} + + +classes = ( + BRUSH_OT_active_index_set, + WM_OT_addon_disable, + WM_OT_addon_enable, + WM_OT_addon_expand, + WM_OT_addon_install, + WM_OT_addon_refresh, + WM_OT_addon_remove, + WM_OT_addon_userpref_show, + WM_OT_app_template_install, + WM_OT_appconfig_activate, + WM_OT_appconfig_default, + WM_OT_blenderplayer_start, + WM_OT_context_collection_boolean_set, + WM_OT_context_cycle_array, + WM_OT_context_cycle_enum, + WM_OT_context_cycle_int, + WM_OT_context_menu_enum, + WM_OT_context_modal_mouse, + WM_OT_context_pie_enum, + WM_OT_context_scale_float, + WM_OT_context_scale_int, + WM_OT_context_set_boolean, + WM_OT_context_set_enum, + WM_OT_context_set_float, + WM_OT_context_set_id, + WM_OT_context_set_int, + WM_OT_context_set_string, + WM_OT_context_set_value, + WM_OT_context_toggle, + WM_OT_context_toggle_enum, + WM_OT_copy_prev_settings, + WM_OT_doc_view, + WM_OT_doc_view_manual, + WM_OT_keyconfig_activate, + WM_OT_keyconfig_export, + WM_OT_keyconfig_import, + WM_OT_keyconfig_remove, + WM_OT_keyconfig_test, + WM_OT_keyitem_add, + WM_OT_keyitem_remove, + WM_OT_keyitem_restore, + WM_OT_keymap_restore, + WM_OT_operator_cheat_sheet, + WM_OT_operator_pie_enum, + WM_OT_path_open, + WM_OT_properties_add, + WM_OT_properties_context_change, + WM_OT_properties_edit, + WM_OT_properties_remove, + WM_OT_sysinfo, + WM_OT_theme_install, + WM_OT_url_open, +) |