diff --git a/node_efficiency_tools.py b/node_efficiency_tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..901c25a15746628571e0382e172f4d44b1bd8a1e
--- /dev/null
+++ b/node_efficiency_tools.py
@@ -0,0 +1,1511 @@
+# ##### 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 #####
+
+bl_info = {
+    'name': "Nodes Efficiency Tools",
+    'author': "Bartek Skorupa",
+    'version': (2, 20),
+    'blender': (2, 6, 6),
+    'location': "Node Editor Properties Panel (Ctrl-SPACE)",
+    'description': "Nodes Efficiency Tools",
+    'warning': "",
+    'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Nodes/Nodes_Efficiency_Tools",
+    'tracker_url': "http://projects.blender.org/tracker/?func=detail&atid=468&aid=33543&group_id=153",
+    'category': "Node",
+    }
+
+import bpy
+from bpy.types import Operator, Panel, Menu
+from bpy.props import FloatProperty, EnumProperty, BoolProperty
+
+#################
+# rl_outputs:
+# list of outputs of Input Render Layer
+# with attributes determinig if pass is used,
+# and MultiLayer EXR outputs names and corresponding render engines
+#
+# rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
+rl_outputs = (
+    ('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
+    ('use_pass_color', 'Color', 'Color', True, False),
+    ('use_pass_combined', 'Image', 'Combined', True, True),
+    ('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
+    ('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
+    ('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
+    ('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
+    ('use_pass_emit', 'Emit', 'Emit', True, False),
+    ('use_pass_environment', 'Environment', 'Env', True, False),
+    ('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
+    ('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
+    ('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
+    ('use_pass_indirect', 'Indirect', 'Indirect', True, False),
+    ('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
+    ('use_pass_mist', 'Mist', 'Mist', True, False),
+    ('use_pass_normal', 'Normal', 'Normal', True, True),
+    ('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
+    ('use_pass_reflection', 'Reflect', 'Reflect', True, False),
+    ('use_pass_refraction', 'Refract', 'Refract', True, False),
+    ('use_pass_shadow', 'Shadow', 'Shadow', True, True),
+    ('use_pass_specular', 'Specular', 'Spec', True, False),
+    ('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
+    ('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
+    ('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
+    ('use_pass_uv', 'UV', 'UV', True, True),
+    ('use_pass_vector', 'Speed', 'Vector', True, True),
+    ('use_pass_z', 'Z', 'Depth', True, True),
+    )
+# list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
+blend_types = [
+    ('MIX', 'Mix', 'Mix Mode'),
+    ('ADD', 'Add', 'Add Mode'),
+    ('MULTIPLY', 'Multiply', 'Multiply Mode'),
+    ('SUBTRACT', 'Subtract', 'Subtract Mode'),
+    ('SCREEN', 'Screen', 'Screen Mode'),
+    ('DIVIDE', 'Divide', 'Divide Mode'),
+    ('DIFFERENCE', 'Difference', 'Difference Mode'),
+    ('DARKEN', 'Darken', 'Darken Mode'),
+    ('LIGHTEN', 'Lighten', 'Lighten Mode'),
+    ('OVERLAY', 'Overlay', 'Overlay Mode'),
+    ('DODGE', 'Dodge', 'Dodge Mode'),
+    ('BURN', 'Burn', 'Burn Mode'),
+    ('HUE', 'Hue', 'Hue Mode'),
+    ('SATURATION', 'Saturation', 'Saturation Mode'),
+    ('VALUE', 'Value', 'Value Mode'),
+    ('COLOR', 'Color', 'Color Mode'),
+    ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
+    ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
+    ]
+# list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
+operations = [
+    ('ADD', 'Add', 'Add Mode'),
+    ('MULTIPLY', 'Multiply', 'Multiply Mode'),
+    ('SUBTRACT', 'Subtract', 'Subtract Mode'),
+    ('DIVIDE', 'Divide', 'Divide Mode'),
+    ('SINE', 'Sine', 'Sine Mode'),
+    ('COSINE', 'Cosine', 'Cosine Mode'),
+    ('TANGENT', 'Tangent', 'Tangent Mode'),
+    ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
+    ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
+    ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
+    ('POWER', 'Power', 'Power Mode'),
+    ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
+    ('MINIMUM', 'Minimum', 'Minimum Mode'),
+    ('MAXIMUM', 'Maximum', 'Maximum Mode'),
+    ('ROUND', 'Round', 'Round Mode'),
+    ('LESS_THAN', 'Less Than', 'Less Thann Mode'),
+    ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
+    ]
+# in BatchChangeNodes additional types/operations in a form that can be used as 'items' for EnumProperty.
+navs = [
+    ('CURRENT', 'Current', 'Leave at current state'),
+    ('NEXT', 'Next', 'Next blend type/operation'),
+    ('PREV', 'Prev', 'Previous blend type/operation'),
+    ]
+# list of mixing shaders
+merge_shaders = ('MIX', 'ADD')
+# list of regular shaders. Entry: (identified, type, name for humans). Will be used in SwapShaders and menus.
+# Keeping mixed case to avoid having to translate entries when adding new nodes in SwapNodes.
+regular_shaders = (
+    ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
+    ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
+    ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
+    ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
+    ('ShaderNodeEmission', 'EMISSION', 'Emission'),
+    ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
+    ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
+    ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
+    ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
+    ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
+    ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
+    ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
+    )
+
+
+def get_nodes_links(context):
+    space = context.space_data
+    tree = space.node_tree
+    nodes = tree.nodes
+    links = tree.links
+    active = nodes.active
+    context_active = context.active_node
+    # check if we are working on regular node tree or node group is currently edited.
+    # if group is edited - active node of space_tree is the group
+    # if context.active_node != space active node - it means that the group is being edited.
+    # in such case we set "nodes" to be nodes of this group, "links" to be links of this group
+    # if context.active_node == space.active_node it means that we are not currently editing group
+    is_main_tree = True
+    if active:
+        is_main_tree = context_active == active
+    if not is_main_tree:  # if group is currently edited
+        tree = active.node_tree
+        nodes = tree.nodes
+        links = tree.links
+
+    return nodes, links
+
+
+class NodeToolBase:
+    @classmethod
+    def poll(cls, context):
+        space = context.space_data
+        return space.type == 'NODE_EDITOR' and space.node_tree is not None
+
+
+class MergeNodes(Operator, NodeToolBase):
+    bl_idname = "node.merge_nodes"
+    bl_label = "Merge Nodes"
+    bl_description = "Merge Selected Nodes"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    mode = EnumProperty(
+            name="mode",
+            description="All possible blend types and math operations",
+            items=blend_types + [op for op in operations if op not in blend_types],
+            )
+    merge_type = EnumProperty(
+            name="merge type",
+            description="Type of Merge to be used",
+            items=(
+                ('AUTO', 'Auto', 'Automatic Output Type Detection'),
+                ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
+                ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
+                ('MATH', 'Math Node', 'Merge using Math Nodes'),
+                ),
+            )
+
+    def execute(self, context):
+        tree_type = context.space_data.node_tree.type
+        if tree_type == 'COMPOSITING':
+            node_type = 'CompositorNode'
+        elif tree_type == 'SHADER':
+            node_type = 'ShaderNode'
+        nodes, links = get_nodes_links(context)
+        mode = self.mode
+        merge_type = self.merge_type
+        selected_mix = []  # entry = [index, loc]
+        selected_shader = []  # entry = [index, loc]
+        selected_math = []  # entry = [index, loc]
+
+        for i, node in enumerate(nodes):
+            if node.select and node.outputs:
+                if merge_type == 'AUTO':
+                    for (type, types_list, dst) in (
+                            ('SHADER', merge_shaders, selected_shader),
+                            ('RGBA', [t[0] for t in blend_types], selected_mix),
+                            ('VALUE', [t[0] for t in operations], selected_math),
+                            ):
+                        output_type = node.outputs[0].type
+                        valid_mode = mode in types_list
+                        # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
+                        # Cheat that output type is 'RGBA',
+                        # and that 'MIX' exists in math operations list.
+                        # This way when selected_mix list is analyzed:
+                        # Node data will be appended even though it doesn't meet requirements.
+                        if output_type != 'SHADER' and mode == 'MIX':
+                            output_type = 'RGBA'
+                            valid_mode = True
+                        if output_type == type and valid_mode:
+                            dst.append([i, node.location.x, node.location.y])
+                else:
+                    for (type, types_list, dst) in (
+                            ('SHADER', merge_shaders, selected_shader),
+                            ('MIX', [t[0] for t in blend_types], selected_mix),
+                            ('MATH', [t[0] for t in operations], selected_math),
+                            ):
+                        if merge_type == type and mode in types_list:
+                            dst.append([i, node.location.x, node.location.y])
+        # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
+        # use only 'Mix' nodes for merging.
+        # For that we add selected_math list to selected_mix list and clear selected_math.
+        if selected_mix and selected_math and merge_type == 'AUTO':
+            selected_mix += selected_math
+            selected_math = []
+
+        for nodes_list in [selected_mix, selected_shader, selected_math]:
+            if nodes_list:
+                count_before = len(nodes)
+                # sort list by loc_x - reversed
+                nodes_list.sort(key=lambda k: k[1], reverse=True)
+                # get maximum loc_x
+                loc_x = nodes_list[0][1] + 350.0
+                nodes_list.sort(key=lambda k: k[2], reverse=True)
+                loc_y = nodes_list[len(nodes_list) - 1][2]
+                offset_y = 40.0
+                if nodes_list == selected_shader:
+                    offset_y = 150.0
+                the_range = len(nodes_list) - 1
+                do_hide = True
+                if len(nodes_list) == 1:
+                    the_range = 1
+                    do_hide = False
+                for i in range(the_range):
+                    if nodes_list == selected_mix:
+                        add_type = node_type + 'MixRGB'
+                        add = nodes.new(add_type)
+                        add.blend_type = mode
+                        add.show_preview = False
+                        add.hide = do_hide
+                        first = 1
+                        second = 2
+                        add.width_hidden = 100.0
+                    elif nodes_list == selected_math:
+                        add_type = node_type + 'Math'
+                        add = nodes.new(add_type)
+                        add.operation = mode
+                        add.hide = do_hide
+                        first = 0
+                        second = 1
+                        add.width_hidden = 100.0
+                    elif nodes_list == selected_shader:
+                        if mode == 'MIX':
+                            add_type = node_type + 'MixShader'
+                            add = nodes.new(add_type)
+                            first = 1
+                            second = 2
+                            add.width_hidden = 100.0
+                        elif mode == 'ADD':
+                            add_type = node_type + 'AddShader'
+                            add = nodes.new(add_type)
+                            first = 0
+                            second = 1
+                            add.width_hidden = 100.0
+                    add.location = loc_x, loc_y
+                    loc_y += offset_y
+                    add.select = True
+                count_adds = i + 1
+                count_after = len(nodes)
+                index = count_after - 1
+                # add link from "first" selected and "first" add node
+                links.new(nodes[nodes_list[0][0]].outputs[0], nodes[count_after - 1].inputs[first])
+                # add links between added ADD nodes and between selected and ADD nodes
+                for i in range(count_adds):
+                    if i < count_adds - 1:
+                        links.new(nodes[index - 1].inputs[first], nodes[index].outputs[0])
+                    if len(nodes_list) > 1:
+                        links.new(nodes[index].inputs[second], nodes[nodes_list[i + 1][0]].outputs[0])
+                    index -= 1
+                # set "last" of added nodes as active
+                nodes.active = nodes[count_before]
+                for i, x, y in nodes_list:
+                    nodes[i].select = False
+
+        return {'FINISHED'}
+
+
+class BatchChangeNodes(Operator, NodeToolBase):
+    bl_idname = "node.batch_change"
+    bl_label = "Batch Change"
+    bl_description = "Batch Change Blend Type and Math Operation"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    blend_type = EnumProperty(
+            name="Blend Type",
+            items=blend_types + navs,
+            )
+    operation = EnumProperty(
+            name="Operation",
+            items=operations + navs,
+            )
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        blend_type = self.blend_type
+        operation = self.operation
+        for node in context.selected_nodes:
+            if node.type == 'MIX_RGB':
+                if not blend_type in [nav[0] for nav in navs]:
+                    node.blend_type = blend_type
+                else:
+                    if blend_type == 'NEXT':
+                        index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
+                        #index = blend_types.index(node.blend_type)
+                        if index == len(blend_types) - 1:
+                            node.blend_type = blend_types[0][0]
+                        else:
+                            node.blend_type = blend_types[index + 1][0]
+
+                    if blend_type == 'PREV':
+                        index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
+                        if index == 0:
+                            node.blend_type = blend_types[len(blend_types) - 1][0]
+                        else:
+                            node.blend_type = blend_types[index - 1][0]
+
+            if node.type == 'MATH':
+                if not operation in [nav[0] for nav in navs]:
+                    node.operation = operation
+                else:
+                    if operation == 'NEXT':
+                        index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
+                        #index = operations.index(node.operation)
+                        if index == len(operations) - 1:
+                            node.operation = operations[0][0]
+                        else:
+                            node.operation = operations[index + 1][0]
+
+                    if operation == 'PREV':
+                        index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
+                        #index = operations.index(node.operation)
+                        if index == 0:
+                            node.operation = operations[len(operations) - 1][0]
+                        else:
+                            node.operation = operations[index - 1][0]
+
+        return {'FINISHED'}
+
+
+class ChangeMixFactor(Operator, NodeToolBase):
+    bl_idname = "node.factor"
+    bl_label = "Change Factor"
+    bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    # option: Change factor.
+    # If option is 1.0 or 0.0 - set to 1.0 or 0.0
+    # Else - change factor by option value.
+    option = FloatProperty()
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        option = self.option
+        selected = []  # entry = index
+        for si, node in enumerate(nodes):
+            if node.select:
+                if node.type in {'MIX_RGB', 'MIX_SHADER'}:
+                    selected.append(si)
+
+        for si in selected:
+            fac = nodes[si].inputs[0]
+            nodes[si].hide = False
+            if option in {0.0, 1.0}:
+                fac.default_value = option
+            else:
+                fac.default_value += option
+
+        return {'FINISHED'}
+
+
+class NodesCopySettings(Operator):
+    bl_idname = "node.copy_settings"
+    bl_label = "Copy Settings"
+    bl_description = "Copy Settings of Active Node to Selected Nodes"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    @classmethod
+    def poll(cls, context):
+        space = context.space_data
+        valid = False
+        if (space.type == 'NODE_EDITOR' and
+                space.node_tree is not None and
+                context.active_node is not None and
+                context.active_node.type is not 'FRAME'
+                ):
+            valid = True
+        return valid
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        selected = [n for n in nodes if n.select]
+        reselect = []  # duplicated nodes will be selected after execution
+        active = nodes.active
+        if active.select:
+            reselect.append(active)
+
+        for node in selected:
+            if node.type == active.type and node != active:
+                # duplicate active, relink links as in 'node', append copy to 'reselect', delete node
+                bpy.ops.node.select_all(action='DESELECT')
+                nodes.active = active
+                active.select = True
+                bpy.ops.node.duplicate()
+                copied = nodes.active
+                # Copied active should however inherit some properties from 'node'
+                attributes = (
+                    'hide', 'show_preview', 'mute', 'label',
+                    'use_custom_color', 'color', 'width', 'width_hidden',
+                    )
+                for attr in attributes:
+                    setattr(copied, attr, getattr(node, attr))
+                # Handle scenario when 'node' is in frame. 'copied' is in same frame then.
+                if copied.parent:
+                    bpy.ops.node.parent_clear()
+                locx = node.location.x
+                locy = node.location.y
+                # get absolute node location
+                parent = node.parent
+                while parent:
+                    locx += parent.location.x
+                    locy += parent.location.y
+                    parent = parent.parent
+                copied.location = [locx, locy]
+                # reconnect links from node to copied
+                for i, input in enumerate(node.inputs):
+                    if input.links:
+                        link = input.links[0]
+                        links.new(link.from_socket, copied.inputs[i])
+                for out, output in enumerate(node.outputs):
+                    if output.links:
+                        out_links = output.links
+                        for link in out_links:
+                            links.new(copied.outputs[out], link.to_socket)
+                bpy.ops.node.select_all(action='DESELECT')
+                node.select = True
+                bpy.ops.node.delete()
+                reselect.append(copied)
+            else:  # If selected wasn't copied, need to reselect it afterwards.
+                reselect.append(node)
+        # clean up
+        bpy.ops.node.select_all(action='DESELECT')
+        for node in reselect:
+            node.select = True
+        nodes.active = active
+
+        return {'FINISHED'}
+
+
+class NodesCopyLabel(Operator, NodeToolBase):
+    bl_idname = "node.copy_label"
+    bl_label = "Copy Label"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    option = EnumProperty(
+            name="option",
+            description="Source of name of label",
+            items=(
+                ('FROM_ACTIVE', 'from active', 'from active node',),
+                ('FROM_NODE', 'from node', 'from node linked to selected node'),
+                ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
+                )
+            )
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        option = self.option
+        active = nodes.active
+        if option == 'FROM_ACTIVE':
+            if active:
+                src_label = active.label
+                for node in [n for n in nodes if n.select and nodes.active != n]:
+                    node.label = src_label
+        elif option == 'FROM_NODE':
+            selected = [n for n in nodes if n.select]
+            for node in selected:
+                for input in node.inputs:
+                    if input.links:
+                        src = input.links[0].from_node
+                        node.label = src.label
+                        break
+        elif option == 'FROM_SOCKET':
+            selected = [n for n in nodes if n.select]
+            for node in selected:
+                for input in node.inputs:
+                    if input.links:
+                        src = input.links[0].from_socket
+                        node.label = src.name
+                        break
+
+        return {'FINISHED'}
+
+
+class NodesClearLabel(Operator, NodeToolBase):
+    bl_idname = "node.clear_label"
+    bl_label = "Clear Label"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    option = BoolProperty()
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        for node in [n for n in nodes if n.select]:
+            node.label = ''
+
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        if self.option:
+            return self.execute(context)
+        else:
+            return context.window_manager.invoke_confirm(self, event)
+
+
+class NodesAddTextureSetup(Operator):
+    bl_idname = "node.add_texture"
+    bl_label = "Texture Setup"
+    bl_description = "Add Texture Node Setup to Selected Shaders"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    @classmethod
+    def poll(cls, context):
+        space = context.space_data
+        valid = False
+        if space.type == 'NODE_EDITOR':
+            if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None:
+                valid = True
+        return valid
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        active = nodes.active
+        valid = False
+        if active:
+            if active.select:
+                if active.type in {
+                        'BSDF_ANISOTROPIC',
+                        'BSDF_DIFFUSE',
+                        'BSDF_GLOSSY',
+                        'BSDF_GLASS',
+                        'BSDF_REFRACTION',
+                        'BSDF_TRANSLUCENT',
+                        'BSDF_TRANSPARENT',
+                        'BSDF_VELVET',
+                        'EMISSION',
+                        'AMBIENT_OCCLUSION',
+                        }:
+                    if not active.inputs[0].is_linked:
+                        valid = True
+        if valid:
+            locx = active.location.x
+            locy = active.location.y
+            tex = nodes.new('ShaderNodeTexImage')
+            tex.location = [locx - 200.0, locy + 28.0]
+            map = nodes.new('ShaderNodeMapping')
+            map.location = [locx - 490.0, locy + 80.0]
+            coord = nodes.new('ShaderNodeTexCoord')
+            coord.location = [locx - 700, locy + 40.0]
+            active.select = False
+            nodes.active = tex
+
+            links.new(tex.outputs[0], active.inputs[0])
+            links.new(map.outputs[0], tex.inputs[0])
+            links.new(coord.outputs[2], map.inputs[0])
+
+        return {'FINISHED'}
+
+
+class NodesAddReroutes(Operator, NodeToolBase):
+    bl_idname = "node.add_reroutes"
+    bl_label = "Add Reroutes"
+    bl_description = "Add Reroutes to Outputs"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    option = EnumProperty(
+            name="option",
+            items=[
+                ('ALL', 'to all', 'Add to all outputs'),
+                ('LOOSE', 'to loose', 'Add only to loose outputs'),
+                ('LINKED', 'to linked', 'Add only to linked outputs'),
+                ]
+            )
+
+    def execute(self, context):
+        tree_type = context.space_data.node_tree.type
+        option = self.option
+        nodes, links = get_nodes_links(context)
+        # output valid when option is 'all' or when 'loose' output has no links
+        valid = False
+        post_select = []  # nodes to be selected after execution
+        # create reroutes and recreate links
+        for node in [n for n in nodes if n.select]:
+            if node.outputs:
+                x = node.location.x
+                y = node.location.y
+                width = node.width
+                # unhide 'REROUTE' nodes to avoid issues with location.y
+                if node.type == 'REROUTE':
+                    node.hide = False
+                # When node is hidden - width_hidden not usable.
+                # Hack needed to calculate real width
+                if node.hide:
+                    bpy.ops.node.select_all(action='DESELECT')
+                    helper = nodes.new('NodeReroute')
+                    helper.select = True
+                    node.select = True
+                    # resize node and helper to zero. Then check locations to calculate width
+                    bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
+                    width = 2.0 * (helper.location.x - node.location.x)
+                    # restore node location
+                    node.location = x, y
+                    # delete helper
+                    node.select = False
+                    # only helper is selected now
+                    bpy.ops.node.delete()
+                x = node.location.x + width + 20.0
+                if node.type != 'REROUTE':
+                    y -= 35.0
+                y_offset = -22.0
+                loc = x, y
+            reroutes_count = 0  # will be used when aligning reroutes added to hidden nodes
+            for out_i, output in enumerate(node.outputs):
+                pass_used = False  # initial value to be analyzed if 'R_LAYERS'
+                # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
+                if node.type != 'R_LAYERS':
+                    pass_used = True
+                else:  # if 'R_LAYERS' check if output represent used render pass
+                    node_scene = node.scene
+                    node_layer = node.layer
+                    # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
+                    if output.name == 'Alpha':
+                        pass_used = True
+                    else:
+                        # check entries in global 'rl_outputs' variable
+                        for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
+                            if output.name == out_name:
+                                pass_used = getattr(node_scene.render.layers[node_layer], render_pass)
+                                break
+                if pass_used:
+                    valid = ((option == 'ALL') or
+                             (option == 'LOOSE' and not output.links) or
+                             (option == 'LINKED' and output.links))
+                    # Add reroutes only if valid, but offset location in all cases.
+                    if valid:
+                        n = nodes.new('NodeReroute')
+                        nodes.active = n
+                        for link in output.links:
+                            links.new(n.outputs[0], link.to_socket)
+                        links.new(output, n.inputs[0])
+                        n.location = loc
+                        post_select.append(n)
+                    reroutes_count += 1
+                    y += y_offset
+                    loc = x, y
+            # disselect the node so that after execution of script only newly created nodes are selected
+            node.select = False
+            # nicer reroutes distribution along y when node.hide
+            if node.hide:
+                y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
+                for reroute in [r for r in nodes if r.select]:
+                    reroute.location.y -= y_translate
+            for node in post_select:
+                node.select = True
+
+        return {'FINISHED'}
+
+
+class NodesSwap(Operator, NodeToolBase):
+    bl_idname = "node.swap_nodes"
+    bl_label = "Swap Nodes"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    option = EnumProperty(
+            items=[
+                ('CompositorNodeSwitch', 'Switch', 'Switch'),
+                ('NodeReroute', 'Reroute', 'Reroute'),
+                ('NodeMixRGB', 'Mix Node', 'Mix Node'),
+                ('NodeMath', 'Math Node', 'Math Node'),
+                ('CompositorNodeAlphaOver', 'Alpha Over', 'Alpha Over'),
+                ('ShaderNodeBsdfTransparent', 'Transparent BSDF', 'Transparent BSDF'),
+                ('ShaderNodeBsdfGlossy', 'Glossy BSDF', 'Glossy BSDF'),
+                ('ShaderNodeBsdfGlass', 'Glass BSDF', 'Glass BSDF'),
+                ('ShaderNodeBsdfDiffuse', 'Diffuse BSDF', 'Diffuse BSDF'),
+                ('ShaderNodeEmission', 'Emission', 'Emission'),
+                ('ShaderNodeBsdfVelvet', 'Velvet BSDF', 'Velvet BSDF'),
+                ('ShaderNodeBsdfTranslucent', 'Translucent BSDF', 'Translucent BSDF'),
+                ('ShaderNodeAmbientOcclusion', 'Ambient Occlusion', 'Ambient Occlusion'),
+                ('ShaderNodeBackground', 'Background', 'Background'),
+                ('ShaderNodeBsdfRefraction', 'Refraction BSDF', 'Refraction BSDF'),
+                ('ShaderNodeBsdfAnisotropic', 'Anisotropic BSDF', 'Anisotropic BSDF'),
+                ('ShaderNodeHoldout', 'Holdout', 'Holdout'),
+                ]
+            )
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        tree_type = context.space_data.tree_type
+        if tree_type == 'CompositorNodeTree':
+            prefix = 'Compositor'
+        elif tree_type == 'ShaderNodeTree':
+            prefix = 'Shader'
+        option = self.option
+        selected = [n for n in nodes if n.select]
+        reselect = []
+        mode = None  # will be used to set proper operation or blend type in new Math or Mix nodes.
+        # regular_shaders - global list. Entry: (identifier, type, name for humans)
+        # example: ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF')
+        swap_shaders = option in (s[0] for s in regular_shaders)
+        if swap_shaders:
+            # replace_types - list of node types that can be replaced using selected option
+            replace_types = [type[1] for type in regular_shaders]
+            new_type = option
+        elif option == 'CompositorNodeSwitch':
+            replace_types = ('REROUTE', 'MIX_RGB', 'MATH', 'ALPHAOVER')
+            new_type = option
+        elif option == 'NodeReroute':
+            replace_types = ('SWITCH')
+            new_type = option
+        elif option == 'NodeMixRGB':
+            replace_types = ('REROUTE', 'SWITCH', 'MATH', 'ALPHAOVER')
+            new_type = prefix + option
+        elif option == 'NodeMath':
+            replace_types = ('REROUTE', 'SWITCH', 'MIX_RGB', 'ALPHAOVER')
+            new_type = prefix + option
+        elif option == 'CompositorNodeAlphaOver':
+            replace_types = ('REROUTE', 'SWITCH', 'MATH', 'MIX_RGB')
+            new_type = option
+        for node in selected:
+            if node.type in replace_types:
+                hide = node.hide
+                if node.type == 'REROUTE':
+                    hide = True
+                new_node = nodes.new(new_type)
+                # if swap Mix to Math of vice-verca - try to set blend type or operation accordingly
+                if new_node.type == 'MIX_RGB':
+                    if node.type == 'MATH':
+                        if node.operation in [entry[0] for entry in blend_types]:
+                            new_node.blend_type = node.operation
+                elif new_node.type == 'MATH':
+                    if node.type == 'MIX_RGB':
+                        if node.blend_type in [entry[0] for entry in operations]:
+                            new_node.operation = node.blend_type
+                old_inputs_count = len(node.inputs)
+                new_inputs_count = len(new_node.inputs)
+                if swap_shaders:
+                    replace = []
+                    for old_i, old_input in enumerate(node.inputs):
+                        for new_i, new_input in enumerate(new_node.inputs):
+                            if old_input.name == new_input.name:
+                                replace.append((old_i, new_i))
+                                break
+                elif new_inputs_count == 1:
+                    replace = ((0, 0), )  # old input 0 (first of the entry) will be replaced by new input 0.
+                elif new_inputs_count == 2:
+                    if old_inputs_count == 1:
+                        replace = ((0, 0), )
+                    elif old_inputs_count == 2:
+                        replace = ((0, 0), (1, 1))
+                    elif old_inputs_count == 3:
+                        replace = ((1, 0), (2, 1))
+                elif new_inputs_count == 3:
+                    if old_inputs_count == 1:
+                        replace = ((0, 1), )
+                    elif old_inputs_count == 2:
+                        replace = ((0, 1), (1, 2))
+                    elif old_inputs_count == 3:
+                        replace = ((0, 0), (1, 1), (2, 2))
+                if replace:
+                    for old_i, new_i in replace:
+                        if node.inputs[old_i].links:
+                            in_link = node.inputs[old_i].links[0]
+                            links.new(in_link.from_socket, new_node.inputs[new_i])
+                for out_link in node.outputs[0].links:
+                    links.new(new_node.outputs[0], out_link.to_socket)
+                new_node.location = node.location
+                new_node.label = node.label
+                new_node.hide = hide
+                new_node.mute = node.mute
+                new_node.show_preview = node.show_preview
+                new_node.width_hidden = node.width_hidden
+                nodes.active = new_node
+                reselect.append(new_node)
+                bpy.ops.node.select_all(action="DESELECT")
+                node.select = True
+                bpy.ops.node.delete()
+            else:
+                reselect.append(node)
+        for node in reselect:
+            node.select = True
+
+        return {'FINISHED'}
+
+
+class NodesLinkActiveToSelected(Operator):
+    bl_idname = "node.link_active_to_selected"
+    bl_label = "Link Active Node to Selected"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    replace = BoolProperty()
+    use_node_name = BoolProperty()
+    use_outputs_names = BoolProperty()
+
+    @classmethod
+    def poll(cls, context):
+        space = context.space_data
+        valid = False
+        if space.type == 'NODE_EDITOR':
+            if space.node_tree is not None and context.active_node is not None:
+                if context.active_node.select:
+                    valid = True
+        return valid
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        replace = self.replace
+        use_node_name = self.use_node_name
+        use_outputs_names = self.use_outputs_names
+        active = nodes.active
+        selected = [node for node in nodes if node.select and node != active]
+        outputs = []  # Only usable outputs of active nodes will be stored here.
+        for out in active.outputs:
+            if active.type != 'R_LAYERS':
+                outputs.append(out)
+            else:
+                # 'R_LAYERS' node type needs special handling.
+                # outputs of 'R_LAYERS' are callable even if not seen in UI.
+                # Only outputs that represent used passes should be taken into account
+                # Check if pass represented by output is used.
+                # global 'rl_outputs' list will be used for that
+                for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
+                    pass_used = False  # initial value. Will be set to True if pass is used
+                    if out.name == 'Alpha':
+                        # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
+                        pass_used = True
+                    elif out.name == out_name:
+                        # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
+                        pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
+                        break
+                if pass_used:
+                    outputs.append(out)
+        doit = True  # Will be changed to False when links successfully added to previous output.
+        for out in outputs:
+            if doit:
+                for node in selected:
+                    dst_name = node.name  # Will be compared with src_name if needed.
+                    # When node has label - use it as dst_name
+                    if node.label:
+                        dst_name = node.label
+                    valid = True  # Initial value. Will be changed to False if names don't match.
+                    src_name = dst_name  # If names not used - this asignment will keep valid = True.
+                    if use_node_name:
+                        # Set src_name to source node name or label
+                        src_name = active.name
+                        if active.label:
+                            src_name = active.label
+                    elif use_outputs_names:
+                        src_name = (out.name, )
+                        for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
+                            if out.name in {out_name, exr_name}:
+                                src_name = (out_name, exr_name)
+                    if dst_name not in src_name:
+                        valid = False
+                    if valid:
+                        for input in node.inputs:
+                            if input.type == out.type or node.type == 'REROUTE':
+                                if replace or not input.is_linked:
+                                    links.new(out, input)
+                                    if not use_node_name and not use_outputs_names:
+                                        doit = False
+                                    break
+
+        return {'FINISHED'}
+
+
+class AlignNodes(Operator, NodeToolBase):
+    bl_idname = "node.align_nodes"
+    bl_label = "Align nodes"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    # option: 'Vertically', 'Horizontally'
+    option = EnumProperty(
+            name="option",
+            description="Direction",
+            items=(
+                ('AXIS_X', "Align Vertically", 'Align Vertically'),
+                ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'),
+                )
+            )
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        selected = []  # entry = [index, loc.x, loc.y, width, height]
+        frames_reselect = []  # entry = frame node. will be used to reselect all selected frames
+        active = nodes.active
+        for i, node in enumerate(nodes):
+            if node.select:
+                if node.type == 'FRAME':
+                    node.select = False
+                    frames_reselect.append(i)
+                else:
+                    locx = node.location.x
+                    locy = node.location.y
+                    parent = node.parent
+                    while parent is not None:
+                        locx += parent.location.x
+                        locy += parent.location.y
+                        parent = parent.parent
+                    selected.append([i, locx, locy])
+        count = len(selected)
+        # add reroute node then scale all to 0.0 and calculate widths and heights of nodes
+        if count > 1:  # aligning makes sense only if at least 2 nodes are selected
+            helper = nodes.new('NodeReroute')
+            helper.select = True
+            bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
+            # store helper's location for further calculations
+            zero_x = helper.location.x
+            zero_y = helper.location.y
+            nodes.remove(helper)
+            # helper is deleted but its location is stored
+            # helper's width and height are 0.0.
+            # Check loc of other nodes in relation to helper to calculate their dimensions
+            # and append them to entries of "selected"
+            total_w = 0.0  # total width of all nodes. Will be calculated later.
+            total_h = 0.0  # total height of all nodes. Will be calculated later
+            for j, [i, x, y] in enumerate(selected):
+                locx = nodes[i].location.x
+                locy = nodes[i].location.y
+                # take node's parent (frame) into account. Get absolute location
+                parent = nodes[i].parent
+                while parent is not None:
+                        locx += parent.location.x
+                        locy += parent.location.y
+                        parent = parent.parent
+                width = abs((zero_x - locx) * 2.0)
+                height = abs((zero_y - locy) * 2.0)
+                selected[j].append(width)  # complete selected's entry for nodes[i]
+                selected[j].append(height)  # complete selected's entry for nodes[i]
+                total_w += width  # add nodes[i] width to total width of all nodes
+                total_h += height  # add nodes[i] height to total height of all nodes
+            selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2]))
+            selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1]))
+            min_x = selected_sorted_x[0][1]  # min loc.x
+            min_x_loc_y = selected_sorted_x[0][2]  # loc y of node with min loc x
+            min_x_w = selected_sorted_x[0][3]  # width of node with max loc x
+            max_x = selected_sorted_x[count - 1][1]  # max loc.x
+            max_x_loc_y = selected_sorted_x[count - 1][2]  # loc y of node with max loc.x
+            max_x_w = selected_sorted_x[count - 1][3]  # width of node with max loc.x
+            min_y = selected_sorted_y[0][2]  # min loc.y
+            min_y_loc_x = selected_sorted_y[0][1]  # loc.x of node with min loc.y
+            min_y_h = selected_sorted_y[0][4]  # height of node with min loc.y
+            min_y_w = selected_sorted_y[0][3]  # width of node with min loc.y
+            max_y = selected_sorted_y[count - 1][2]  # max loc.y
+            max_y_loc_x = selected_sorted_y[count - 1][1]  # loc x of node with max loc.y
+            max_y_w = selected_sorted_y[count - 1][3]  # width of node with max loc.y
+            max_y_h = selected_sorted_y[count - 1][4]  # height of node with max loc.y
+
+            if self.option == 'AXIS_X':
+                loc_x = min_x
+                #loc_y = (max_x_loc_y + min_x_loc_y) / 2.0
+                loc_y = (max_y - max_y_h / 2.0 + min_y - min_y_h / 2.0) / 2.0
+                offset_x = (max_x - min_x - total_w + max_x_w) / (count - 1)
+                for i, x, y, w, h in selected_sorted_x:
+                    nodes[i].location.x = loc_x
+                    nodes[i].location.y = loc_y + h / 2.0
+                    parent = nodes[i].parent
+                    while parent is not None:
+                        nodes[i].location.x -= parent.location.x
+                        nodes[i].location.y -= parent.location.y
+                        parent = parent.parent
+                    loc_x += offset_x + w
+            else:  # if self.option == 'AXIS_Y'
+                #loc_x = (max_y_loc_x + max_y_w / 2.0 + min_y_loc_x + min_y_w / 2.0) / 2.0
+                loc_x = (max_x + max_x_w / 2.0 + min_x + min_x_w / 2.0) / 2.0
+                loc_y = min_y
+                offset_y = (max_y - min_y + total_h - min_y_h) / (count - 1)
+                for i, x, y, w, h in selected_sorted_y:
+                    nodes[i].location.x = loc_x - w / 2.0
+                    nodes[i].location.y = loc_y
+                    parent = nodes[i].parent
+                    while parent is not None:
+                        nodes[i].location.x -= parent.location.x
+                        nodes[i].location.y -= parent.location.y
+                        parent = parent.parent
+                    loc_y += offset_y - h
+
+            # reselect selected frames
+            for i in frames_reselect:
+                nodes[i].select = True
+            # restore active node
+            nodes.active = active
+
+        return {'FINISHED'}
+
+
+class SelectParentChildren(Operator, NodeToolBase):
+    bl_idname = "node.select_parent_child"
+    bl_label = "Select Parent or Children"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    option = EnumProperty(
+            name="option",
+            items=(
+                ('PARENT', 'Select Parent', 'Select Parent Frame'),
+                ('CHILD', 'Select Children', 'Select members of selected frame'),
+                )
+            )
+
+    def execute(self, context):
+        nodes, links = get_nodes_links(context)
+        option = self.option
+        selected = [node for node in nodes if node.select]
+        if option == 'PARENT':
+            for sel in selected:
+                parent = sel.parent
+                if parent:
+                    parent.select = True
+        else:  # option == 'CHILD'
+            for sel in selected:
+                children = [node for node in nodes if node.parent == sel]
+                for kid in children:
+                    kid.select = True
+
+        return {'FINISHED'}
+
+
+#############################################################
+#  P A N E L S
+#############################################################
+
+class EfficiencyToolsPanel(Panel, NodeToolBase):
+    bl_idname = "NODE_PT_efficiency_tools"
+    bl_space_type = 'NODE_EDITOR'
+    bl_region_type = 'UI'
+    bl_label = "Efficiency Tools (Ctrl-SPACE)"
+
+    def draw(self, context):
+        type = context.space_data.tree_type
+        layout = self.layout
+
+        box = layout.box()
+        box.menu(MergeNodesMenu.bl_idname)
+        if type == 'ShaderNodeTree':
+            box.operator(NodesAddTextureSetup.bl_idname, text="Add Image Texture (Ctrl T)")
+        box.menu(BatchChangeNodesMenu.bl_idname, text="Batch Change...")
+        box.menu(NodeAlignMenu.bl_idname, text="Align Nodes (Shift =)")
+        box.menu(CopyToSelectedMenu.bl_idname, text="Copy to Selected (Shift-C)")
+        box.operator(NodesClearLabel.bl_idname).option = True
+        box.menu(AddReroutesMenu.bl_idname, text="Add Reroutes")
+        box.menu(NodesSwapMenu.bl_idname, text="Swap Nodes")
+        box.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected")
+
+
+#############################################################
+#  M E N U S
+#############################################################
+
+class EfficiencyToolsMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_node_tools_menu"
+    bl_label = "Efficiency Tools"
+
+    def draw(self, context):
+        type = context.space_data.tree_type
+        layout = self.layout
+        layout.menu(MergeNodesMenu.bl_idname, text="Merge Selected Nodes")
+        if type == 'ShaderNodeTree':
+            layout.operator(NodesAddTextureSetup.bl_idname, text="Add Image Texture with coordinates")
+        layout.menu(BatchChangeNodesMenu.bl_idname, text="Batch Change")
+        layout.menu(NodeAlignMenu.bl_idname, text="Align Nodes")
+        layout.menu(CopyToSelectedMenu.bl_idname, text="Copy to Selected")
+        layout.operator(NodesClearLabel.bl_idname).option = True
+        layout.menu(AddReroutesMenu.bl_idname, text="Add Reroutes")
+        layout.menu(NodesSwapMenu.bl_idname, text="Swap Nodes")
+        layout.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected")
+
+
+class MergeNodesMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_merge_nodes_menu"
+    bl_label = "Merge Selected Nodes"
+
+    def draw(self, context):
+        type = context.space_data.tree_type
+        layout = self.layout
+        if type == 'ShaderNodeTree':
+            layout.menu(MergeShadersMenu.bl_idname, text="Use Shaders")
+        layout.menu(MergeMixMenu.bl_idname, text="Use Mix Nodes")
+        layout.menu(MergeMathMenu.bl_idname, text="Use Math Nodes")
+
+
+class MergeShadersMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_merge_shaders_menu"
+    bl_label = "Merge Selected Nodes using Shaders"
+
+    def draw(self, context):
+        layout = self.layout
+        for type in merge_shaders:
+            props = layout.operator(MergeNodes.bl_idname, text=type)
+            props.mode = type
+            props.merge_type = 'SHADER'
+
+
+class MergeMixMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_merge_mix_menu"
+    bl_label = "Merge Selected Nodes using Mix"
+
+    def draw(self, context):
+        layout = self.layout
+        for type, name, description in blend_types:
+            props = layout.operator(MergeNodes.bl_idname, text=name)
+            props.mode = type
+            props.merge_type = 'MIX'
+
+
+class MergeMathMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_merge_math_menu"
+    bl_label = "Merge Selected Nodes using Math"
+
+    def draw(self, context):
+        layout = self.layout
+        for type, name, description in operations:
+            props = layout.operator(MergeNodes.bl_idname, text=name)
+            props.mode = type
+            props.merge_type = 'MATH'
+
+
+class BatchChangeNodesMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_batch_change_nodes_menu"
+    bl_label = "Batch Change Selected Nodes"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.menu(BatchChangeBlendTypeMenu.bl_idname)
+        layout.menu(BatchChangeOperationMenu.bl_idname)
+
+
+class BatchChangeBlendTypeMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_batch_change_blend_type_menu"
+    bl_label = "Batch Change Blend Type"
+
+    def draw(self, context):
+        layout = self.layout
+        for type, name, description in blend_types:
+            props = layout.operator(BatchChangeNodes.bl_idname, text=name)
+            props.blend_type = type
+            props.operation = 'CURRENT'
+
+
+class BatchChangeOperationMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_batch_change_operation_menu"
+    bl_label = "Batch Change Math Operation"
+
+    def draw(self, context):
+        layout = self.layout
+        for type, name, description in operations:
+            props = layout.operator(BatchChangeNodes.bl_idname, text=name)
+            props.blend_type = 'CURRENT'
+            props.operation = type
+
+
+class CopyToSelectedMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_copy_node_properties_menu"
+    bl_label = "Copy to Selected"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.operator(NodesCopySettings.bl_idname, text="Settings from Active")
+        layout.menu(CopyLabelMenu.bl_idname)
+
+
+class CopyLabelMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_copy_label_menu"
+    bl_label = "Copy Label"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.operator(NodesCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
+        layout.operator(NodesCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
+        layout.operator(NodesCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
+
+
+class AddReroutesMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_add_reroutes_menu"
+    bl_label = "Add Reroutes"
+    bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.operator(NodesAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
+        layout.operator(NodesAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
+        layout.operator(NodesAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
+
+
+class NodesSwapMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_swap_menu"
+    bl_label = "Swap Nodes"
+
+    def draw(self, context):
+        type = context.space_data.tree_type
+        layout = self.layout
+        if type == 'ShaderNodeTree':
+            layout.menu(ShadersSwapMenu.bl_idname, text="Swap Shaders")
+        layout.operator(NodesSwap.bl_idname, text="Change to Mix Nodes").option = 'NodeMixRGB'
+        layout.operator(NodesSwap.bl_idname, text="Change to Math Nodes").option = 'NodeMath'
+        if type == 'CompositorNodeTree':
+            layout.operator(NodesSwap.bl_idname, text="Change to Alpha Over").option = 'CompositorNodeAlphaOver'
+        if type == 'CompositorNodeTree':
+            layout.operator(NodesSwap.bl_idname, text="Change to Switches").option = 'CompositorNodeSwitch'
+            layout.operator(NodesSwap.bl_idname, text="Change to Reroutes").option = 'NodeReroute'
+
+
+class ShadersSwapMenu(Menu):
+    bl_idname = "NODE_MT_shaders_swap_menu"
+    bl_label = "Swap Shaders"
+
+    @classmethod
+    def poll(cls, context):
+        space = context.space_data
+        valid = False
+        if space.type == 'NODE_EDITOR':
+            if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None:
+                valid = True
+        return valid
+
+    def draw(self, context):
+        layout = self.layout
+        for opt, type, txt in regular_shaders:
+            layout.operator(NodesSwap.bl_idname, text=txt).option = opt
+
+
+class LinkActiveToSelectedMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_link_active_to_selected_menu"
+    bl_label = "Link Active to Selected"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.menu(LinkStandardMenu.bl_idname)
+        layout.menu(LinkUseNamesMenu.bl_idname, text="Use names/labels")
+
+
+class LinkStandardMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_link_standard_menu"
+    bl_label = "To All Selected"
+
+    def draw(self, context):
+        layout = self.layout
+        props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links (Shift-F)")
+        props.replace = False
+        props.use_node_name = False
+        props.use_outputs_names = False
+        props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links (Ctrl-Shift-F)")
+        props.replace = True
+        props.use_node_name = False
+        props.use_outputs_names = False
+
+
+class LinkUseNamesMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_link_use_names_menu"
+    bl_label = "Link Active to Selected"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.menu(LinkUseNodeNameMenu.bl_idname, text="Use Node Name/Label")
+        layout.menu(LinkUseOutputsNamesMenu.bl_idname, text="Use Outputs Names")
+
+
+class LinkUseNodeNameMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_link_use_node_name_menu"
+    bl_label = "Use Node Name/Label"
+
+    def draw(self, context):
+        layout = self.layout
+        props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links")
+        props.replace = True
+        props.use_node_name = True
+        props.use_outputs_names = False
+        props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links")
+        props.replace = False
+        props.use_node_name = True
+        props.use_outputs_names = False
+
+
+class LinkUseOutputsNamesMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_link_use_outputs_names_menu"
+    bl_label = "Use Outputs Names"
+
+    def draw(self, context):
+        layout = self.layout
+        props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links")
+        props.replace = True
+        props.use_node_name = False
+        props.use_outputs_names = True
+        props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links")
+        props.replace = False
+        props.use_node_name = False
+        props.use_outputs_names = True
+
+
+class NodeAlignMenu(Menu, NodeToolBase):
+    bl_idname = "NODE_MT_node_align_menu"
+    bl_label = "Align Nodes"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.operator(AlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X'
+        layout.operator(AlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y'
+
+
+#############################################################
+#  MENU ITEMS
+#############################################################
+
+def select_parent_children_buttons(self, context):
+    layout = self.layout
+    layout.operator(SelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
+    layout.operator(SelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
+
+#############################################################
+#  REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
+#############################################################
+
+addon_keymaps = []
+# kmi_defs entry: (identifier, key, CTRL, SHIFT, ALT, props)
+# props entry: (property name, property value)
+kmi_defs = (
+    # MERGE NODES
+    # MergeNodes with Ctrl (AUTO).
+    (MergeNodes.bl_idname, 'NUMPAD_0', True, False, False,
+        (('mode', 'MIX'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'ZERO', True, False, False,
+        (('mode', 'MIX'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, False,
+        (('mode', 'ADD'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'EQUAL', True, False, False,
+        (('mode', 'ADD'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, False,
+        (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'EIGHT', True, False, False,
+        (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, False,
+        (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'MINUS', True, False, False,
+        (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, False,
+        (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'SLASH', True, False, False,
+        (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),)),
+    (MergeNodes.bl_idname, 'COMMA', True, False, False,
+        (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'PERIOD', True, False, False,
+        (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),)),
+    # MergeNodes with Ctrl Alt (MIX)
+    (MergeNodes.bl_idname, 'NUMPAD_0', True, False, True,
+        (('mode', 'MIX'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'ZERO', True, False, True,
+        (('mode', 'MIX'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, True,
+        (('mode', 'ADD'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'EQUAL', True, False, True,
+        (('mode', 'ADD'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, True,
+        (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'EIGHT', True, False, True,
+        (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, True,
+        (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'MINUS', True, False, True,
+        (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, True,
+        (('mode', 'DIVIDE'), ('merge_type', 'MIX'),)),
+    (MergeNodes.bl_idname, 'SLASH', True, False, True,
+        (('mode', 'DIVIDE'), ('merge_type', 'MIX'),)),
+    # MergeNodes with Ctrl Shift (MATH)
+    (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, True, False,
+        (('mode', 'ADD'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'EQUAL', True, True, False,
+        (('mode', 'ADD'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, True, False,
+        (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'EIGHT', True, True, False,
+        (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, True, False,
+        (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'MINUS', True, True, False,
+        (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, True, False,
+        (('mode', 'DIVIDE'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'SLASH', True, True, False,
+        (('mode', 'DIVIDE'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'COMMA', True, True, False,
+        (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),)),
+    (MergeNodes.bl_idname, 'PERIOD', True, True, False,
+        (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),)),
+    # BATCH CHANGE NODES
+    # BatchChangeNodes with Alt
+    (BatchChangeNodes.bl_idname, 'NUMPAD_0', False, False, True,
+        (('blend_type', 'MIX'), ('operation', 'CURRENT'),)),
+    (BatchChangeNodes.bl_idname, 'ZERO', False, False, True,
+        (('blend_type', 'MIX'), ('operation', 'CURRENT'),)),
+    (BatchChangeNodes.bl_idname, 'NUMPAD_PLUS', False, False, True,
+        (('blend_type', 'ADD'), ('operation', 'ADD'),)),
+    (BatchChangeNodes.bl_idname, 'EQUAL', False, False, True,
+        (('blend_type', 'ADD'), ('operation', 'ADD'),)),
+    (BatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', False, False, True,
+        (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),)),
+    (BatchChangeNodes.bl_idname, 'EIGHT', False, False, True,
+        (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),)),
+    (BatchChangeNodes.bl_idname, 'NUMPAD_MINUS', False, False, True,
+        (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),)),
+    (BatchChangeNodes.bl_idname, 'MINUS', False, False, True,
+        (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),)),
+    (BatchChangeNodes.bl_idname, 'NUMPAD_SLASH', False, False, True,
+        (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),)),
+    (BatchChangeNodes.bl_idname, 'SLASH', False, False, True,
+        (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),)),
+    (BatchChangeNodes.bl_idname, 'COMMA', False, False, True,
+        (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),)),
+    (BatchChangeNodes.bl_idname, 'PERIOD', False, False, True,
+        (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),)),
+    (BatchChangeNodes.bl_idname, 'DOWN_ARROW', False, False, True,
+        (('blend_type', 'NEXT'), ('operation', 'NEXT'),)),
+    (BatchChangeNodes.bl_idname, 'UP_ARROW', False, False, True,
+        (('blend_type', 'PREV'), ('operation', 'PREV'),)),
+    # LINK ACTIVE TO SELECTED
+    # Don't use names, replace links (Ctrl Shift F)
+    (NodesLinkActiveToSelected.bl_idname, 'F', True, True, False,
+        (('replace', True), ('use_node_name', False), ('use_outputs_names', False),)),
+    # Don't use names, don't replace links (Shift F)
+    (NodesLinkActiveToSelected.bl_idname, 'F', False, True, False,
+        (('replace', False), ('use_node_name', False), ('use_outputs_names', False),)),
+    # CHANGE MIX FACTOR
+    (ChangeMixFactor.bl_idname, 'LEFT_ARROW', False, False, True, (('option', -0.1),)),
+    (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, False, True, (('option', 0.1),)),
+    (ChangeMixFactor.bl_idname, 'LEFT_ARROW', False, True, True, (('option', -0.01),)),
+    (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, True, True, (('option', 0.01),)),
+    (ChangeMixFactor.bl_idname, 'LEFT_ARROW', True, True, True, (('option', 0.0),)),
+    (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', True, True, True, (('option', 1.0),)),
+    (ChangeMixFactor.bl_idname, 'NUMPAD_0', True, True, True, (('option', 0.0),)),
+    (ChangeMixFactor.bl_idname, 'ZERO', True, True, True, (('option', 0.0),)),
+    (ChangeMixFactor.bl_idname, 'NUMPAD_1', True, True, True, (('option', 1.0),)),
+    (ChangeMixFactor.bl_idname, 'ONE', True, True, True, (('option', 1.0),)),
+    # CLEAR LABEL (Alt L)
+    (NodesClearLabel.bl_idname, 'L', False, False, True, (('option', False),)),
+    # SELECT PARENT/CHILDREN
+    # Select Children
+    (SelectParentChildren.bl_idname, 'RIGHT_BRACKET', False, False, False, (('option', 'CHILD'),)),
+    # Select Parent
+    (SelectParentChildren.bl_idname, 'LEFT_BRACKET', False, False, False, (('option', 'PARENT'),)),
+    (NodesAddTextureSetup.bl_idname, 'T', True, False, False, None),
+    # MENUS
+    ('wm.call_menu', 'SPACE', True, False, False, (('name', EfficiencyToolsMenu.bl_idname),)),
+    ('wm.call_menu', 'SLASH', False, False, False, (('name', AddReroutesMenu.bl_idname),)),
+    ('wm.call_menu', 'NUMPAD_SLASH', False, False, False, (('name', AddReroutesMenu.bl_idname),)),
+    ('wm.call_menu', 'EQUAL', False, True, False, (('name', NodeAlignMenu.bl_idname),)),
+    ('wm.call_menu', 'F', False, False, True, (('name', LinkUseNamesMenu.bl_idname),)),
+    ('wm.call_menu', 'C', False, True, False, (('name', CopyToSelectedMenu.bl_idname),)),
+    ('wm.call_menu', 'S', False, True, False, (('name', NodesSwapMenu.bl_idname),)),
+    )
+
+
+def register():
+    bpy.utils.register_module(__name__)
+    km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
+    for (identifier, key, CTRL, SHIFT, ALT, props) in kmi_defs:
+        kmi = km.keymap_items.new(identifier, key, 'PRESS', ctrl=CTRL, shift=SHIFT, alt=ALT)
+        if props:
+            for prop, value in props:
+                setattr(kmi.properties, prop, value)
+        addon_keymaps.append((km, kmi))
+    # menu items
+    bpy.types.NODE_MT_select.append(select_parent_children_buttons)
+
+
+def unregister():
+    bpy.utils.unregister_module(__name__)
+    bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
+    for km, kmi in addon_keymaps:
+        km.keymap_items.remove(kmi)
+    addon_keymaps.clear()
+
+if __name__ == "__main__":
+    register()