├── .gitignore ├── README.md └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.py[cod] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TapTapSwap 2 | Blender addon - Keymap shortcuts to swap some areas of blender 3 | 4 | **[Download latest](https://raw.githubusercontent.com/Pullusb/TapTapSwap/master/TapTapSwap.py)** 5 | 6 | Old blender 2.7 compatible version can be found [here](https://github.com/Pullusb/SB_blender_addons_old_2_7) 7 | 8 | ## [Demo Youtube](https://www.youtube.com/watch?v=43v5kxFkcZk) 9 | 10 | --- 11 | 12 | ### Description 13 | 14 | You'll stop having both outliner and the properties panels on screen when they are one "Tab" away from each other ! 15 | 16 | Keymap **Tab** key to super quickly swap between **outliner and properties**. 17 | Exactly like *Shift+F7* for Property editor and *Shift+F9* for outliner but waaay faster ;) 18 | 19 | You can add the keymap manually the exact same way the addon does [as shown in this french forum](http://blenderlounge.fr/forum/viewtopic.php?f=5&t=1446&start=45) (which was the direct source for creating this addon) 20 | 21 | *But also:* 22 | Keymap **Z** to swap between **dopesheet and graph editor**. (Hjalti Hjalmarsson idea) 23 | 24 | Keymap **shift+Z** to swap between **timeline and dopesheet** (my idea, hurray !) 25 | 26 | Keymap **Ctrl+Tab** to swap between **outliner modes**, add Shift to reverse 27 | 28 | *bonus :* 29 | Keymap **ctrl+shift+alt+X** to iterate in properties panels tabs (the shortcut is way to complex but it's hard to get free left-hand-accessible shortcuts these days ^^) 30 | 31 | ### note 32 | [robert-trirop](https://github.com/robert-trirop) made an addon "[editor switcher](https://github.com/robert-trirop/editor_switcher)" that call a pie menu for switching area (with all area choices), if you want a quick way to change/relocate area, it may be better for you. 33 | 34 | --- 35 | ### Changelog 36 | 37 | #### 1.7.0 (2022/06/22) 38 | 39 | - changed from single file mode to folder addon mode. 40 | 41 | #### 1.6.0 (20/12/2020) 42 | 43 | - Fix an uncorrect keymap unregister that has been there forever 44 | - add doc url 45 | - code cleanup 46 | 47 | #### 2.8 update (19/02/2019) 48 | Make it work with blender 2.8 49 | 50 | #### 1.4 Update (20/03/2017) 51 | new shortcut : 52 | **shift+Z swap timeline/dopesheet** 53 | 54 | #### 1.2 Update (11/03/2017) 55 | new shortcut : 56 | **ctrl+shift+alt+X iterate swap of properties panels tabs** according to active object type and data availability of this object. 57 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "TapTapSwap", 3 | "description": "Add some useful swapping shortcut", 4 | "author": "Samuel Bernou, Tonton, based on Cédric Lepiller/Hjalti Hjalmarsson ideas", 5 | "version": (1, 7, 6), 6 | "blender": (3, 0, 0), 7 | "location": "Hit TAB swap outliner/property editor, \ 8 | Ctrl+TAB swap outliner mode, add Shift to reverse, \ 9 | Z swap dopesheet/graph editor, shift+Z in timeline, \ 10 | Ctrl+Shift+Alt+X swap active object's properties tabs from anywhere", 11 | "warning": "", 12 | "doc_url": "https://github.com/Pullusb/TapTapSwap", 13 | "category": "User Interface"} 14 | 15 | import bpy 16 | 17 | def set_panel(panel): 18 | '''take a panel name and apply it to properties zone''' 19 | if bpy.context.area.type == 'PROPERTIES': 20 | bpy.context.area.spaces.active.context = panel 21 | return 22 | 23 | for area in bpy.context.screen.areas: 24 | if area.type == 'PROPERTIES': 25 | for space in area.spaces: 26 | if space.type == 'PROPERTIES': 27 | space.context = panel 28 | return 29 | 30 | def get_panel(): 31 | '''return active panel name of the properties zone''' 32 | if bpy.context.area.type == 'PROPERTIES': 33 | return bpy.context.area.spaces.active.context 34 | 35 | for area in bpy.context.screen.areas: 36 | if area.type == 'PROPERTIES': 37 | for space in area.spaces: 38 | if space.type == 'PROPERTIES': 39 | return space.context 40 | 41 | def bone_has_physics(ob): 42 | if ob.rigid_body or ob.rigid_body_constraint: 43 | return 1 44 | else: 45 | return 0 46 | 47 | def has_physics(ob): 48 | if ob.rigid_body or ob.rigid_body_constraint: 49 | return 1 50 | 51 | if ob.type == 'MESH' and ob.collision and ob.collision.use: 52 | return 1 53 | 54 | if hasattr(ob, 'field') and ob.field and ob.field.type != 'NONE': 55 | return 1 56 | 57 | if has_mod(ob): 58 | for m in ob.modifiers: 59 | if m.type in ['CLOTH', 'SOFT_BODY', 'FLUID_SIMULATION', 'DYNAMIC_PAINT', 'SMOKE']: 60 | return 1 61 | return 0 62 | 63 | def has_mod(ob): 64 | if ob.type == 'GPENCIL': 65 | return len(ob.grease_pencil_modifiers) 66 | else: 67 | return len(ob.modifiers) 68 | 69 | def has_gp_fx(ob): 70 | return len(ob.shader_effects) 71 | 72 | def has_const(ob): 73 | return len(ob.constraints) 74 | 75 | def has_mat(ob): 76 | return len(ob.material_slots) 77 | 78 | def has_particles(ob): 79 | return len(ob.particle_systems) 80 | 81 | def swap_properties_panel(): 82 | obj = bpy.context.object 83 | if not obj: 84 | return (1, 'must select an active object') 85 | 86 | pan = get_panel() 87 | if not pan: 88 | return (1, '"properties" region must be visible on screen') 89 | 90 | objects_dic = { 91 | 'MESH': ['DATA','MATERIAL','OBJECT','MODIFIER','PARTICLES','PHYSICS','CONSTRAINT',], 92 | 'CURVE' : ['DATA','MATERIAL','OBJECT','MODIFIER','PHYSICS','CONSTRAINT',], 93 | 'EMPTY': ['DATA','PHYSICS', 'OBJECT','CONSTRAINT',], 94 | 'ARMATURE': ['DATA', 'BONE','BONE_CONSTRAINT', 'PHYSICS', 'OBJECT','CONSTRAINT',], 95 | 'CAMERA' : ['DATA','OBJECT','CONSTRAINT',], 96 | 'LATTICE' : ['DATA','OBJECT','MODIFIER','CONSTRAINT',], 97 | 'LIGHT' : ['DATA','PHYSICS','OBJECT','CONSTRAINT',], 98 | 'FONT' : ['DATA','MATERIAL','PHYSICS','OBJECT','CONSTRAINT', 'MODIFIER',], 99 | 'GPENCIL' : ['DATA','MATERIAL','OBJECT','MODIFIER', 'SHADERFX','PHYSICS', 'CONSTRAINT',] 100 | } 101 | 102 | props = objects_dic[obj.type] 103 | 104 | # Actualize props list to keep only available 105 | for p in list(props): 106 | if p == 'PARTICLES' and not has_particles(obj): 107 | props.remove(p) 108 | elif p == 'MODIFIER' and not has_mod(obj): 109 | props.remove(p) 110 | elif p == 'SHADERFX' and not has_gp_fx(obj): 111 | props.remove(p) 112 | elif p == 'MATERIAL' and not has_mat(obj): 113 | props.remove(p) 114 | elif p == 'CONSTRAINT' and not has_const(obj): 115 | props.remove(p) 116 | elif p == 'PHYSICS' and not has_physics(obj): 117 | props.remove(p) 118 | 119 | if pan in props: 120 | nextpan = props[(props.index(pan) + 1) % len(props)] 121 | else: 122 | nextpan = props[0] 123 | 124 | set_panel(nextpan) 125 | 126 | class UI_OT_swap_panel_prop(bpy.types.Operator): 127 | bl_idname = "taptap.swap_panel_prop" 128 | bl_label = "Swap Panel Properties" 129 | bl_description = "Swap panel on properties" 130 | bl_options = {"REGISTER", "INTERNAL"} 131 | 132 | def execute(self, context): 133 | swap_properties_panel() 134 | return {"FINISHED"} 135 | 136 | class UI_OT_swap_timeline_dopesheet_mode(bpy.types.Operator): 137 | bl_idname = "taptap.swap_timeline_dopesheet_mode" 138 | bl_label = "Swap Timeline and Dopesheet" 139 | bl_description = "Swap Timeline and Dopesheet editor" 140 | bl_options = {"REGISTER", "INTERNAL"} 141 | 142 | def execute(self, context): 143 | space = context.area.spaces.active # context.area.spaces[0] 144 | space.mode = 'DOPESHEET' if space.mode == 'TIMELINE' else 'TIMELINE' 145 | return {"FINISHED"} 146 | 147 | def set_outliner_mode(mode): 148 | '''take a panel name and apply it to properties zone''' 149 | area = bpy.context.area 150 | if area.type == 'OUTLINER': 151 | area.spaces[0].display_mode = mode 152 | return 1 153 | return 0 154 | 155 | def get_outliner_mode(): 156 | '''return active panel name of the properties zone''' 157 | area = bpy.context.area 158 | if area.type == 'OUTLINER': 159 | return area.spaces[0].display_mode 160 | return 0 161 | 162 | def swap_outliner_mode(revert=False): 163 | modes = [ 164 | "SCENES", 165 | "VIEW_LAYER", 166 | "SEQUENCE", 167 | "LIBRARIES", 168 | "DATA_API", 169 | "LIBRARY_OVERRIDES", 170 | "ORPHAN_DATA", 171 | ] 172 | 173 | mode = get_outliner_mode() 174 | if not mode: 175 | return (1, 'No active outliner') 176 | 177 | idx = modes.index(mode) 178 | offset = -1 if revert else 1 179 | nextmode = modes[(idx + offset) % len(modes)] 180 | set_outliner_mode(nextmode) 181 | 182 | class UI_OT_swap_outliner_mode(bpy.types.Operator): 183 | bl_idname = "taptap.swap_outliner_mode" 184 | bl_label = "Swap Outliner Mode" 185 | bl_description = "Swap outliner mode" 186 | bl_options = {"REGISTER", "INTERNAL"} 187 | 188 | revert : bpy.props.BoolProperty(name="Revert") 189 | 190 | def execute(self, context): 191 | swap_outliner_mode(revert=self.revert) 192 | return {"FINISHED"} 193 | 194 | ###--KEYMAPS 195 | 196 | addon_keymaps = [] 197 | def register_keymaps(): 198 | addon = bpy.context.window_manager.keyconfigs.addon 199 | 200 | ###### Object Property swap 201 | 202 | ## All editor: 203 | km = addon.keymaps.new(name = "Window", space_type='EMPTY', region_type='WINDOW') 204 | ## ctrl+shift+X taken by carver ! so add alt 205 | kmi = km.keymap_items.new("taptap.swap_panel_prop", type = "X", value = "PRESS", shift = True, ctrl = True, alt = True) 206 | addon_keymaps.append((km, kmi)) 207 | 208 | ###### Outliner / Properties Swap 209 | 210 | ## From Properties to Outliner - Tab 211 | km = addon.keymaps.new(name = "Property Editor",space_type='PROPERTIES', region_type='WINDOW') 212 | kmi = km.keymap_items.new("wm.context_set_enum", type = "TAB", value = "PRESS") 213 | kmi.properties.data_path = 'area.type' 214 | kmi.properties.value = 'OUTLINER' 215 | addon_keymaps.append((km, kmi)) 216 | 217 | ## From Outliner to Properties - Tab 218 | km = addon.keymaps.new(name = "Outliner",space_type='OUTLINER', region_type='WINDOW') 219 | kmi = km.keymap_items.new("wm.context_set_enum", type = "TAB", value = "PRESS") 220 | kmi.properties.data_path = 'area.type' 221 | kmi.properties.value = 'PROPERTIES' 222 | addon_keymaps.append((km, kmi)) 223 | 224 | ## Outliner mode swap 225 | kmi = km.keymap_items.new("taptap.swap_outliner_mode", type = "TAB", value = "PRESS", ctrl = True) 226 | kmi.properties.revert=False 227 | addon_keymaps.append((km, kmi)) 228 | 229 | kmi = km.keymap_items.new("taptap.swap_outliner_mode", type = "TAB", value = "PRESS", ctrl = True, shift=True) 230 | kmi.properties.revert=True 231 | addon_keymaps.append((km, kmi)) 232 | 233 | ###### Dopesheet / Timeline / GraphEditor Swap - Z 234 | 235 | km = addon.keymaps.new(name = "Dopesheet", space_type='DOPESHEET_EDITOR', region_type='WINDOW') 236 | 237 | ## Dopesheet -> Graph - Z 238 | kmi = km.keymap_items.new("wm.context_set_enum", type = "Z", value = "PRESS") 239 | kmi.properties.data_path = 'area.type' 240 | kmi.properties.value = 'GRAPH_EDITOR' 241 | addon_keymaps.append((km, kmi)) 242 | 243 | ## Dopesheet <-> Timeline Swap - shift Z 244 | kmi = km.keymap_items.new("taptap.swap_timeline_dopesheet_mode", type = "Z", value = "PRESS", shift = True) 245 | addon_keymaps.append((km, kmi)) 246 | 247 | ## Graph -> dopesheet - Z 248 | km = addon.keymaps.new(name = "Graph Editor",space_type='GRAPH_EDITOR', region_type='WINDOW') 249 | kmi = km.keymap_items.new("wm.context_set_enum", type = "Z", value = "PRESS") 250 | kmi.properties.data_path = 'area.type' 251 | kmi.properties.value = 'DOPESHEET_EDITOR' 252 | addon_keymaps.append((km, kmi)) 253 | 254 | def unregister_keymaps(): 255 | for km, kmi in addon_keymaps: 256 | if kmi in km.keymap_items[:]: 257 | # check first to skip, kmi registered in 'Window' (avoir error: Cannot be removed from window) 258 | km.keymap_items.remove(kmi) 259 | addon_keymaps.clear() 260 | 261 | classes = ( 262 | UI_OT_swap_panel_prop, 263 | UI_OT_swap_timeline_dopesheet_mode, 264 | UI_OT_swap_outliner_mode, 265 | ) 266 | 267 | ###--REGISTER 268 | 269 | def register(): 270 | if bpy.app.background: 271 | return 272 | 273 | for cls in classes: 274 | bpy.utils.register_class(cls) 275 | 276 | register_keymaps() 277 | 278 | def unregister(): 279 | if bpy.app.background: 280 | return 281 | 282 | unregister_keymaps() 283 | 284 | for cls in reversed(classes): 285 | bpy.utils.unregister_class(cls) 286 | 287 | if __name__ == "__main__": 288 | register() 289 | --------------------------------------------------------------------------------