10
$\begingroup$

Blender has a built in template for adding a custom menu to the main header (INFO_HT_header). This can be found by going into the text editor under templates->Python->UI Menu.

Can I relocate this custom menu to be inserted between "Window" and "Help" sub menus? If so, how?

$\endgroup$
3

5 Answers 5

13
$\begingroup$

There is probably a more elegant way

Actually there is not. We can only append and prepend. If every stock entry was a draw callback in draw._draw_funcs, it would be possible to insert inbetween. Until then, there are only two hacks, which I wouldn't recommend for everyday use (rather stick to iKlsR's solution):

  1. Wrapping all UILayout calls - wiki link
  2. Injecting modified layout code the hard way - included in addon code

Note that 1. works for a single addon only at a time, and 2. only works as long as there's just one addon replacing the code object of a certain bpy type.


Content from Blender wiki, by ideasman42:

This page shows how to intercept every draw call in blenders python ui which can be used for some UI tricks.

First heres an example of how to do just that.

Tested to work with Blender 2.70

Intercept UI draw/poll

This is a simple example of how to intercept poll and draw functions, without making any behavioral changes.

classes = ["Panel", "Menu", "Header"]


def draw_override(func_orig, self_real, context):
    print("override draw:", self_real)

    ret = func_orig(self_real, context)
    return ret


def poll_override(func_orig, cls, context):
    print("override poll:", func_orig.__self__)

    ret = func_orig(context)

    return ret

import bpy


for cls_name in classes:
    cls = getattr(bpy.types, cls_name)

    for subcls in cls.__subclasses__():
        if "draw" in subcls.__dict__:  # dont want to get parents draw()

            def replace_draw():
                # function also serves to hold draw_orig in a local namespace
                draw_orig = subcls.draw
                def draw(self, context):
                    return draw_override(draw_orig, self, context)
                subcls.draw = draw

            replace_draw()

        if "poll" in subcls.__dict__:  # dont want to get parents poll()
            def replace_poll():
                # function also serves to hold poll_orig in a local namespace
                poll_orig = subcls.poll
                def poll(cls, context):
                    return poll_override(poll_orig, cls, context)
                subcls.poll = classmethod(poll)

            replace_poll()

Override layout & functions

This is the extended from the above code but extended to subclass the layout type used within draw().

In this example operators and properties are filtered out based on their names, but all sorts of things are possible with this - modifying args to functions, changing text etc.

classes = ["Panel", "Menu", "Header"]


import bpy

UILayout = bpy.types.UILayout

op_blacklist = [
    "render.render",
    "object.modifier_add",
    "object.forcefield_toggle",
    ]

prop_blacklist = [
    "Object.location",
    "Object.scale",
    "Object.rotation_euler",
    "RenderSettings.display_mode",
    ]


def filter_operator(op_id):
    if op_id in op_blacklist:
        return False
    return True


def filter_prop(data, prop):
    prop_id = "%s.%s" % (data.__class__.__name__, prop)
    if prop_id in prop_blacklist:
        return False
    return True


class OperatorProperties_FAKE:
    pass


class UILayout_FAKE(bpy.types.UILayout):
    __slots__ = ()

    def __getattribute__(self, attr):
        # ensure we always pass down UILayout_FAKE instances
        if attr in ("row", "split", "column", "box", "column_flow"):
            real_func = UILayout.__getattribute__(self, attr)

            def dummy_func(*args, **kw):
                print("    wrapped", attr)
                ret = real_func(*args, **kw)
                return UILayout_FAKE(ret)
            return dummy_func

        elif attr in ("operator", "operator_menu_enum", "operator_enum"):
            real_func = UILayout.__getattribute__(self, attr)

            def dummy_func(*args, **kw):
                print("    wrapped", attr)
                if filter_operator(args[0]):
                    ret = real_func(*args, **kw)
                else:
                    # UILayout.__getattribute__(self, "label")()
                    # may need to be set
                    ret = OperatorProperties_FAKE()
                return ret
            return dummy_func
        elif attr in ("prop", "prop_enum"):
            real_func = UILayout.__getattribute__(self, attr)

            def dummy_func(*args, **kw):
                print("    wrapped", attr)
                if filter_prop(args[0], args[1]):
                    ret = real_func(*args, **kw)
                else:
                    ret = None
                return ret
            return dummy_func
        else:
            return UILayout.__getattribute__(self, attr)

        print(self, attr)

    def operator(*args, **kw):
        print("OP")
        return super().operator(*args, **kw)


def draw_override(func_orig, self_real, context):
    if 1:
        class Wrapper(self_real.__class__):
            def __getattribute__(self, attr):
                if attr == "layout":
                    ret = self_real.layout
                    return UILayout_FAKE(ret)
                else:
                    return super().__getattr__(self, attr)

            @property
            def layout(self):
                ret = self_real.layout
                print("wrapped")
                return ret
        print(1)
        self_wrap = Wrapper(self_real)
        ret = func_orig(self_wrap, context)
    else:
        # simple, no wrapping
        ret = func_orig(self_wrap, context)

    return ret


def poll_override(func_orig, context):

    ret = func_orig(context)

    return ret


for cls_name in classes:
    cls = getattr(bpy.types, cls_name)

    for subcls in cls.__subclasses__():
        if "draw" in subcls.__dict__:  # dont want to get parents draw()

            def replace_draw():
                # function also serves to hold draw_old in a local namespace
                draw_orig = subcls.draw

                def draw(self, context):
                    return draw_override(draw_orig, self, context)
                subcls.draw = draw

            replace_draw()

        if "poll" in subcls.__dict__:  # dont want to get parents poll()
            def replace_poll():
                # function also serves to hold draw_old in a local namespace
                poll_orig = subcls.poll

                def poll(context):
                    return poll_override(poll_orig, context)
                subcls.poll = classmethod(poll)

            replace_poll()

Relevant code from my experimental addon to inject an own menu after the Templates menu below.

It essentially determines the path of the file which defines the original layout getattr(bpy.types, "TEXT_HT_header").draw.__code__.co_filename, looks for a hard-coded piece of code row.menu("TEXT_MT_templates") starting from …draw.__code__.co_firstlineno - 1, gets hold of the draw function code and inserts its own piece of code which adds the custom menu (it has to adjust the indention to be valid).

Then it executes the new code (string) in isolation exec("".join(lines), {}, l) with locals backed by an initially empty object l = {}. The compiled draw function can then be accessed by l['draw'] and the original draw function can be substituted: getattr(bpy.types, "TEXT_HT_header").draw = l['draw'].

A reference to the original draw function should be stored so that it can be restored if necessary.

class DrawFuncStore:
    bpy_type = "TEXT_HT_header"
    bpy_type_class = getattr(bpy.types, bpy_type)
    draw = None

def insert_menu():
    insert_after = 'row.menu("TEXT_MT_templates")'
    insert_code  = '        row.menu("TEXT_MT_snippets")\n' \
                   '        from bpy.app.translations import pgettext_iface as iface_\n' # meh...
    DrawFuncStore.draw = DrawFuncStore.bpy_type_class.draw
    #module = bpy_type_class.__module__

    filepath = DrawFuncStore.bpy_type_class.draw.__code__.co_filename
    if filepath == "<string>":
        return
    try:
        file = open(filepath, "r")
        lines = file.readlines()
    except:
        append_menu()
        return

    line_start = DrawFuncStore.bpy_type_class.draw.__code__.co_firstlineno - 1

    for i in range(line_start, len(lines)):
        line = lines[i]
        if not line[0].isspace() and line.lstrip()[0] not in ("#", "\n", "\r"):
            break

    line_end = i

    # Unindent draw func by one level, since it won't sit inside a class
    lines = [l[4:] for l in lines[line_start:line_end]]

    for i, line in enumerate(lines, 1):
        if insert_after in line:
            #print("FOUND INSERT LINE")
            lines.insert(i, insert_code)
            break
    else:
        append_menu()
        return

    # Debug output
    #f = open("D:\\s.txt", "w").writelines(lines)

    l = {}
    exec("".join(lines), {}, l)
    #print(l)

    #bpy_type_class.draw.__code__ = code_object # Doesn't work, since a single func is not a module

    DrawFuncStore.bpy_type_class.draw = l['draw'] # exec defined our custom draw() func!

def append_menu():
    bpy.types.TEXT_MT_templates.prepend(draw_item)

def remove_menu():
    if DrawFuncStore.draw is not None:
        DrawFuncStore.bpy_type_class.draw = DrawFuncStore.draw
        DrawFuncStore.draw = None

    else:
        bpy.types.TEXT_MT_templates.remove(draw_item)
$\endgroup$
6
  • $\begingroup$ Prepend is actually a nice next-best solution that I didn't know existed. At least it puts my menu in the ballpark instead of off the screen for people with lower resolutions. Append adds not just after the help menu, but waaaaay after all the version and scene info (vert/face count and such). I think I will use this method for now. $\endgroup$
    – PolyMesh
    Oct 20, 2013 at 4:37
  • 1
    $\begingroup$ I talked to Ton a while ago about this, and he kind of liked the idea to add several draw functions to _draw_funcs by default (editor picker, menus, everything else). That would allow for adding after menu, not inbetween existing elements however. One draw function per element seems a bit overkill, maybe there's a better solution... $\endgroup$
    – CodeManX
    Oct 20, 2013 at 19:45
  • $\begingroup$ Would it be possible to write what was in your dead link? Could you actually paste the relevant code here instead of giving a soon to be dead link to your addon. The relevant part is probably only a couple of lines right. $\endgroup$
    – Školstvo
    Dec 3, 2019 at 19:37
  • $\begingroup$ Yeah I don't understand where you are changing the order in your not dead link. There are only two references to "bpy.types.TEXT_MT_templates" and they both just prepend and remove. Not that you actually clarified what the link is supposed to contain. Or what I'm supposed to be looking for. $\endgroup$
    – Školstvo
    Dec 3, 2019 at 19:56
  • 1
    $\begingroup$ @Školstvo I added the Wiki content as well as the relevant code from the addon with some explanation. Note that my code was written for Blender v2.67 and the layout code has probably changed numerous times since then, so it may not work in newer versions out of the box. $\endgroup$
    – CodeManX
    Dec 3, 2019 at 23:42
5
$\begingroup$

There is probably a more elegant way but this works. If you right click one of these menu items and select Edit Source it should bring up space_info.py, the script that defines and draws these. You can then simply clone an existing class and add it to the menu via the INFO_HT_header() class in the desired order, (here I added it after Window before Help).

sub.menu("INFO_MT_window")
sub.menu("ExampleMenu") # this is the example menu
sub.menu("INFO_MT_help")

enter image description here

In the example above, I used the example class defined in Templates > Python > UI Menu Simple (lines 4 to 12) and just commented out bl_idname and renamed the class.

$\endgroup$
2
  • $\begingroup$ So you are saying there is no way to do this as an add-on. I have to actually edit the space_info.py INFO_HT_header to add my menu? Bummer. $\endgroup$
    – PolyMesh
    Oct 20, 2013 at 4:28
  • $\begingroup$ As I said, there is probably a more elegant way but CoDEmanX who is alot more familiar with the codebase says there isn't so since you are writing an addon, I'd say go with either of his suggestions. $\endgroup$
    – iKlsR
    Oct 20, 2013 at 4:41
4
$\begingroup$

The code to append/prepend a button to any of the headers in Blender can be found below:

class addon_function(bpy.types.Operator):
     """add documentation here"""
     .
     .
     .
     def execute(self, context):
          ...

def addon_button(self, context):
     self.layout.operator(
          pysoy_model.bl_idname,
          text=addon_function.__doc__,
          icon='MOD_MESHDEFORM')

def register():
     bpy.utils.register_class(addon_function)
     bpy.types.VIEW3D_HT_header.append(addon_button)

def unregister():
     bpy.utils.unregister_class(addon_function)
     bpy.types.VIEW3D_HT_header.remove(addon_button)
# This allows you to run the script directly from blenders text editor
# to test the addon without having to install it.
if __name__ == "__main__":
     register()

The main line is: bpy.types.VIEW3D_HT_header.append(addon_button), where you can add change the bpy.types.* to:

  1. Info Header: INFO_HT_HEADER
  2. 3D View Header: VIEW3D_HT_HEADER
  3. Timeline Header: TIME_HT_HEADER
  4. Outliner Header: OUTLINER_HT_HEADER
  5. Properties Header: PROPERTIES_HT_HEADER, etc.

For more Information on addon and Icon Button:

$\endgroup$
1
$\begingroup$

Not easily. You may want to try removing the Help menu and then re-appending it after you've appended your custom menu.

$\endgroup$
1
  • $\begingroup$ Unfortunately I think I would have to also remove the 5 or so elements after Help for this method to work since Help is not the last element in the layout. $\endgroup$
    – PolyMesh
    Oct 20, 2013 at 4:40
-4
$\begingroup$
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>
import bpy
from bpy.types import Header, Menu


class INFO_HT_header(Header):
    bl_space_type = 'INFO'

    def draw(self, context):
        layout = self.layout

        window = context.window
        scene = context.scene
        rd = scene.render

        row = layout.row(align=True)
        row.template_header()

        INFO_MT_editor_menus.draw_collapsible(context, layout)

        if window.screen.show_fullscreen:
            layout.operator("screen.back_to_previous", icon='SCREEN_BACK', text="Back to Previous")
            layout.separator()
        else:
            layout.template_ID(context.window, "screen", new="screen.new", unlink="screen.delete")
            layout.template_ID(context.screen, "scene", new="scene.new", unlink="scene.delete")

        layout.separator()

        if rd.has_multiple_engines:
            layout.prop(rd, "engine", text="")

        layout.separator()

        layout.template_running_jobs()

        layout.template_reports_banner()

        row = layout.row(align=True)

        if bpy.app.autoexec_fail is True and bpy.app.autoexec_fail_quiet is False:
            row.label("Auto-run disabled", icon='ERROR')
            if bpy.data.is_saved:
                props = row.operator("wm.revert_mainfile", icon='SCREEN_BACK', text="Reload Trusted")
                props.use_scripts = True

            row.operator("script.autoexec_warn_clear", text="Ignore")

            # include last so text doesn't push buttons out of the header
            row.label(bpy.app.autoexec_fail_message)
            return

        row.operator("wm.splash", text="", icon='BLENDER', emboss=False)
        row.label(text=scene.statistics(), translate=False)


class INFO_MT_editor_menus(Menu):
    bl_idname = "INFO_MT_editor_menus"
    bl_label = ""

    def draw(self, context):
        self.draw_menus(self.layout, context)

    @staticmethod
    def draw_menus(layout, context):
        scene = context.scene
        rd = scene.render

        layout.menu("INFO_MT_file")

        if rd.use_game_engine:
            layout.menu("INFO_MT_game")
        else:
            layout.menu("INFO_MT_render")

        layout.menu("INFO_MT_window")
        layout.menu("INFO_MT_help")
#this is comment 1 adding our name to header
        layout.menu("INFO_MT_google")


class INFO_MT_file(Menu):
    bl_label = "File"

    def draw(self, context):
        layout = self.layout

        layout.operator_context = 'INVOKE_AREA'
        layout.operator("wm.read_homefile", text="New", icon='NEW')
        layout.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
        layout.menu("INFO_MT_file_open_recent", icon='OPEN_RECENT')
        layout.operator("wm.revert_mainfile", icon='FILE_REFRESH')
        layout.operator("wm.recover_last_session", icon='RECOVER_LAST')
        layout.operator("wm.recover_auto_save", text="Recover Auto Save...", icon='RECOVER_AUTO')

        layout.separator()

        layout.operator_context = 'EXEC_AREA' if context.blend_data.is_saved else 'INVOKE_AREA'
        layout.operator("wm.save_mainfile", text="Save", icon='FILE_TICK')

        layout.operator_context = 'INVOKE_AREA'
        layout.operator("wm.save_as_mainfile", text="Save As...", icon='SAVE_AS')
        layout.operator_context = 'INVOKE_AREA'
        layout.operator("wm.save_as_mainfile", text="Save Copy...", icon='SAVE_COPY').copy = True

        layout.separator()

        layout.operator("screen.userpref_show", text="User Preferences...", icon='PREFERENCES')

        layout.operator_context = 'INVOKE_AREA'
        layout.operator("wm.save_homefile", icon='SAVE_PREFS')
        layout.operator("wm.read_factory_settings", icon='LOAD_FACTORY')

        if any(bpy.utils.app_template_paths()):
            app_template = context.user_preferences.app_template
            if app_template:
                layout.operator(
                    "wm.read_factory_settings",
                    text="Load Factory Template Settings",
                    icon='LOAD_FACTORY',
                ).app_template = app_template
            del app_template

        layout.menu("USERPREF_MT_app_templates", icon='FILE_BLEND')

        layout.separator()

        layout.operator_context = 'INVOKE_AREA'
        layout.operator("wm.link", text="Link", icon='LINK_BLEND')
        layout.operator("wm.append", text="Append", icon='APPEND_BLEND')
        layout.menu("INFO_MT_file_previews")

        layout.separator()

        layout.menu("INFO_MT_file_import", icon='IMPORT')
        layout.menu("INFO_MT_file_export", icon='EXPORT')

        layout.separator()

        layout.menu("INFO_MT_file_external_data", icon='EXTERNAL_DATA')

        layout.separator()

        layout.operator_context = 'EXEC_AREA'
        if bpy.data.is_dirty and context.user_preferences.view.use_quit_dialog:
            layout.operator_context = 'INVOKE_SCREEN'  # quit dialog
        layout.operator("wm.quit_blender", text="Quit", icon='QUIT')


class INFO_MT_file_import(Menu):
    bl_idname = "INFO_MT_file_import"
    bl_label = "Import"

    def draw(self, context):
        if bpy.app.build_options.collada:
            self.layout.operator("wm.collada_import", text="Collada (Default) (.dae)")
        if bpy.app.build_options.alembic:
            self.layout.operator("wm.alembic_import", text="Alembic (.abc)")


class INFO_MT_file_export(Menu):
    bl_idname = "INFO_MT_file_export"
    bl_label = "Export"

    def draw(self, context):
        if bpy.app.build_options.collada:
            self.layout.operator("wm.collada_export", text="Collada (Default) (.dae)")
        if bpy.app.build_options.alembic:
            self.layout.operator("wm.alembic_export", text="Alembic (.abc)")


class INFO_MT_file_external_data(Menu):
    bl_label = "External Data"

    def draw(self, context):
        layout = self.layout

        icon = 'CHECKBOX_HLT' if bpy.data.use_autopack else 'CHECKBOX_DEHLT'
        layout.operator("file.autopack_toggle", icon=icon)

        layout.separator()

        pack_all = layout.row()
        pack_all.operator("file.pack_all")
        pack_all.active = not bpy.data.use_autopack

        unpack_all = layout.row()
        unpack_all.operator("file.unpack_all")
        unpack_all.active = not bpy.data.use_autopack

        layout.separator()

        layout.operator("file.make_paths_relative")
        layout.operator("file.make_paths_absolute")
        layout.operator("file.report_missing_files")
        layout.operator("file.find_missing_files")


class INFO_MT_file_previews(Menu):
    bl_label = "Data Previews"

    def draw(self, context):
        layout = self.layout

        layout.operator("wm.previews_ensure")
        layout.operator("wm.previews_batch_generate")

        layout.separator()

        layout.operator("wm.previews_clear")
        layout.operator("wm.previews_batch_clear")


class INFO_MT_game(Menu):
    bl_label = "Game"

    def draw(self, context):
        layout = self.layout

        gs = context.scene.game_settings

        layout.operator("view3d.game_start")

        layout.separator()

        layout.prop(gs, "show_debug_properties")
        layout.prop(gs, "show_framerate_profile")
        layout.prop(gs, "show_physics_visualization")
        layout.prop(gs, "use_deprecation_warnings")
        layout.prop(gs, "use_animation_record")
        layout.separator()
        layout.prop(gs, "use_auto_start")


class INFO_MT_render(Menu):
    bl_label = "Render"

    def draw(self, context):
        layout = self.layout

        layout.operator("render.render", text="Render Image", icon='RENDER_STILL').use_viewport = True
        props = layout.operator("render.render", text="Render Animation", icon='RENDER_ANIMATION')
        props.animation = True
        props.use_viewport = True

        layout.separator()

        layout.operator("render.opengl", text="OpenGL Render Image")
        layout.operator("render.opengl", text="OpenGL Render Animation").animation = True
        layout.menu("INFO_MT_opengl_render")

        layout.separator()

        layout.operator("render.view_show")
        layout.operator("render.play_rendered_anim", icon='PLAY')


class INFO_MT_opengl_render(Menu):
    bl_label = "OpenGL Render Options"

    def draw(self, context):
        layout = self.layout

        rd = context.scene.render
        layout.prop(rd, "use_antialiasing")
        layout.prop(rd, "use_full_sample")

        layout.prop_menu_enum(rd, "antialiasing_samples")
        layout.prop_menu_enum(rd, "alpha_mode")


class INFO_MT_window(Menu):
    bl_label = "Window"

    def draw(self, context):
        import sys

        layout = self.layout

        layout.operator("wm.window_duplicate")
        layout.operator("wm.window_fullscreen_toggle", icon='FULLSCREEN_ENTER')

        layout.separator()

        layout.operator("screen.screenshot")
        layout.operator("screen.screencast")

        if sys.platform[:3] == "win":
            layout.separator()
            layout.operator("wm.console_toggle", icon='CONSOLE')

        if context.scene.render.use_multiview:
            layout.separator()
            layout.operator("wm.set_stereo_3d", icon='CAMERA_STEREO')


class INFO_MT_help(Menu):
    bl_label = "Help"

    def draw(self, context):
        layout = self.layout

        layout.operator(
                "wm.url_open", text="Manual", icon='HELP',
                ).url = "https://docs.blender.org/manual/en/dev/"
        layout.operator(
                "wm.url_open", text="Release Log", icon='URL',
                ).url = "http://wiki.blender.org/index.php/Dev:Ref/Release_Notes/%d.%d" % bpy.app.version[:2]
        layout.separator()

        layout.operator(
                "wm.url_open", text="Blender Website", icon='URL',
                ).url = "https://www.blender.org"
        layout.operator(
                "wm.url_open", text="Blender Store", icon='URL',
                ).url = "https://store.blender.org"
        layout.operator(
                "wm.url_open", text="Developer Community", icon='URL',
                ).url = "https://www.blender.org/get-involved/"
        layout.operator(
                "wm.url_open", text="User Community", icon='URL',
                ).url = "https://www.blender.org/support/user-community"
        layout.separator()
        layout.operator(
                "wm.url_open", text="Report a Bug", icon='URL',
                ).url = "https://developer.blender.org/maniphest/task/edit/form/1"
        layout.separator()

        layout.operator(
                "wm.url_open", text="Python API Reference", icon='URL',
                ).url = bpy.types.WM_OT_doc_view._prefix

        layout.operator("wm.operator_cheat_sheet", icon='TEXT')
        layout.operator("wm.sysinfo", icon='TEXT')
        layout.separator()

        layout.operator("wm.splash", icon='BLENDER')
#this is comment 2 adding our class
class INFO_MT_google(Menu):
    bl_label = "googleweb"

    def draw(self, context):
        layout = self.layout

        layout.operator(
                "wm.url_open", text="google_on_web", icon='HELP',
                ).url = "https://www.google.com/"


classes = (
    INFO_HT_header,
    INFO_MT_editor_menus,
    INFO_MT_file,
#this is comment 3 calling our class
    INFO_MT_google,
    INFO_MT_file_import,
    INFO_MT_file_export,
    INFO_MT_file_external_data,
    INFO_MT_file_previews,
    INFO_MT_game,
    INFO_MT_render,
    INFO_MT_opengl_render,
    INFO_MT_window,
    INFO_MT_help,
)

if __name__ == "__main__":  # only for live edit.
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)
$\endgroup$
4
  • 6
    $\begingroup$ Please elaborate on what this code snippet is actually doing. Consider that the question is: how to insert items in between existing ones, preferably without overriding existend draw methods. $\endgroup$
    – brockmann
    Apr 23, 2018 at 9:13
  • 3
    $\begingroup$ Answers need to be more then a massive code block. Further more there is a lot of duplicated code in there. You could of at least trimmed it down to a readable example. $\endgroup$
    – David
    Apr 23, 2018 at 12:26
  • $\begingroup$ #this is comment 1 adding our name to header layout.menu("INFO_MT_google") #this is comment 2 adding our class class INFO_MT_google(Menu): bl_label = "googleweb" def draw(self, context): layout = self.layout layout.operator( "wm.url_open", text="google_on_web", icon='HELP', ).url = "google.com" #this is comment 3 calling our class INFO_MT_google, $\endgroup$
    – programmer
    Apr 23, 2018 at 17:31
  • $\begingroup$ space_info.py file paste this code to get added new menu to header $\endgroup$
    – programmer
    Apr 23, 2018 at 17:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .