└── addons └── previous-tab ├── LICENSE ├── plugin.cfg └── plugin.gd /addons/previous-tab/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maxim Kovkel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/previous-tab/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="previous-tab" 4 | description="Switch to the previously active tab (last tab) by pressing Ctrl+Tab (macos: Option+Tab), including last doc, script and scene void: 14 | if OS.has_feature("macos"): 15 | _base_sh_key = KEY_ALT 16 | else: 17 | _reset_default_tabs_shortcuts() 18 | _base_sh_key = KEY_CTRL 19 | 20 | scene_changed.connect(_on_scene_changed) 21 | 22 | var script_editor = get_editor_interface().get_script_editor() 23 | _scripts_tab_container = _first_or_null(script_editor.find_children( 24 | "*", "TabContainer", true, false 25 | ) 26 | ) 27 | _scripts_item_list = _first_or_null(script_editor.find_children( 28 | "*", "ItemList", true, false 29 | )) 30 | _scenes_tab_bar = _get_scenes_tab_bar() 31 | 32 | if _scripts_tab_container: 33 | _scripts_tab_container.tab_changed.connect(_on_script_tab_changed) 34 | 35 | _switcher = Switcher.new() 36 | _switcher.editor_interface = get_editor_interface() 37 | _switcher.scripts_tab_container = _scripts_tab_container 38 | _switcher.base_sh_key = _base_sh_key 39 | get_editor_interface().get_base_control().add_child(_switcher) 40 | 41 | 42 | func _exit_tree() -> void: 43 | scene_changed.disconnect(_on_scene_changed) 44 | if _scripts_tab_container: 45 | _scripts_tab_container.tab_changed.disconnect(_on_script_tab_changed) 46 | 47 | 48 | func _input(event: InputEvent) -> void: 49 | if event is InputEventKey: 50 | var base_sh_key_pressed = Input.is_key_pressed(_base_sh_key) 51 | if base_sh_key_pressed and event.keycode in [KEY_TAB, KEY_BACKTAB]: 52 | if not _switcher.visible: 53 | _switcher.raise() 54 | 55 | 56 | func _on_scene_changed(node: Node): 57 | var path 58 | if node: 59 | path = node.scene_file_path 60 | if path: 61 | _add_to_history(HistoryItemScene.new( 62 | path, 63 | _scenes_tab_bar.get_tab_icon(_scenes_tab_bar.current_tab), 64 | get_editor_interface() 65 | )) 66 | 67 | 68 | func _get_scenes_tab_bar(): 69 | var _main_screen 70 | for c in get_editor_interface().get_base_control().find_children("*", "VBoxContainer", true, false): 71 | if c.name == "MainScreen": 72 | _main_screen = c 73 | break 74 | if _main_screen: 75 | var central_screen_box = _main_screen.get_parent().get_parent() 76 | return _first_or_null(central_screen_box.find_children("*", "TabBar", true, false)) 77 | return null 78 | 79 | 80 | func _on_script_tab_changed(idx): 81 | _add_to_history(HistoryItemScript.new( 82 | weakref(_scripts_tab_container.get_tab_control(idx)), 83 | _scripts_tab_container, 84 | _scripts_item_list 85 | )) 86 | 87 | 88 | func _reset_default_tabs_shortcuts(): 89 | var default_sh = get_editor_interface().get_editor_settings().get("shortcuts") as Array 90 | var check_sh = func(sh_name): 91 | if len(default_sh.filter(func(x): return x.name == sh_name)) == 0: 92 | get_editor_interface().get_editor_settings().set( 93 | "shortcuts", 94 | [{ "name": sh_name, "shortcuts": []}] 95 | ) 96 | check_sh.call("editor/next_tab") 97 | check_sh.call("editor/prev_tab") 98 | 99 | 100 | func _add_to_history(el: HistoryItem): 101 | _switcher.add_to_history(el) 102 | 103 | 104 | func _first_or_null(arr): 105 | if len(arr) == 0: 106 | return null 107 | return arr[0] 108 | 109 | 110 | class Switcher extends AcceptDialog: 111 | var editor_interface: EditorInterface 112 | var scripts_tab_container: TabContainer 113 | var base_sh_key 114 | 115 | var _history_tree: Tree 116 | var _root: TreeItem 117 | var _check_boxes: HBoxContainer 118 | 119 | var _history: Array[HistoryItem] = [] 120 | var _filter_types = [] 121 | 122 | func _init() -> void: 123 | title = "Switcher" 124 | 125 | var vb = VBoxContainer.new() 126 | 127 | _history_tree = Tree.new() 128 | _history_tree.hide_root = true 129 | _history_tree.hide_folding = true 130 | _history_tree.item_activated.connect(_handle_confirmed) 131 | _history_tree.size_flags_vertical = Control.SIZE_EXPAND_FILL 132 | _history_tree.focus_mode = Control.FOCUS_NONE 133 | _root = _history_tree.create_item() 134 | vb.add_child(_history_tree) 135 | 136 | _check_boxes = HBoxContainer.new() 137 | _check_boxes.alignment = BoxContainer.ALIGNMENT_END 138 | _add_filter_checkbox("script", true, _add_filter("script")) 139 | _add_filter_checkbox("scene", true, _add_filter("scene")) 140 | _add_filter_checkbox("doc", true, _add_filter("doc")) 141 | vb.add_child(_check_boxes) 142 | 143 | add_child(vb) 144 | 145 | get_ok_button().hide() 146 | 147 | func _ready() -> void: 148 | set_process_input(false) 149 | 150 | func _input(event: InputEvent) -> void: 151 | if event is InputEventKey and event.keycode == base_sh_key: 152 | if not event.pressed: 153 | _handle_confirmed() 154 | return 155 | 156 | var k = event as InputEventKey 157 | if k and k.pressed: 158 | if k.keycode in [KEY_PAGEUP, KEY_UP]: 159 | _select_prev() 160 | if k.keycode in [KEY_PAGEDOWN, KEY_DOWN, KEY_BACKTAB]: 161 | _select_next() 162 | if k.keycode == KEY_TAB: 163 | if k.shift_pressed: 164 | _select_prev() 165 | else: 166 | _select_next() 167 | 168 | func add_to_history(el: HistoryItem): 169 | el.add_to(_history) 170 | if len(_history) > 20: 171 | _history.resize(20) 172 | 173 | func raise(): 174 | popup_centered_ratio(0.3) 175 | set_process_input.bind(true).call_deferred() 176 | _update_tree() 177 | 178 | func _select_next(): 179 | var selected = _history_tree.get_selected() 180 | if not selected or _root.get_child_count() == 0: 181 | return 182 | var idx = selected.get_index() 183 | idx = wrapi(idx + 1, 0, _root.get_child_count()) 184 | _root.get_child(idx).select(0) 185 | _history_tree.ensure_cursor_is_visible() 186 | 187 | func _select_prev(): 188 | var selected = _history_tree.get_selected() 189 | if not selected or _root.get_child_count() == 0: 190 | return 191 | var idx = selected.get_index() 192 | idx = wrapi(idx - 1, 0, _root.get_child_count()) 193 | _root.get_child(idx).select(0) 194 | _history_tree.ensure_cursor_is_visible() 195 | 196 | func _handle_confirmed(): 197 | var selected = _history_tree.get_selected() 198 | if selected and selected.has_meta("ref"): 199 | selected.get_meta("ref").open() 200 | hide() 201 | 202 | func _update_tree(): 203 | _clear_tree_item_children(_root) 204 | 205 | var first_history_item: HistoryItem 206 | var item_to_select: TreeItem 207 | for el in _history.duplicate(): 208 | if el.is_valid() and el.has_filter(_filter_types): 209 | var item = _history_tree.create_item(_root) 210 | el.fill(item) 211 | item.set_meta("ref", el) 212 | 213 | if not first_history_item: 214 | first_history_item = el 215 | else: 216 | if not item_to_select and first_history_item.has_same_type_as(el): 217 | item_to_select = item 218 | if not el.is_valid(): 219 | _history.erase(el) 220 | 221 | if item_to_select: 222 | item_to_select.select(0) 223 | elif _root.get_child_count() > 1: 224 | _root.get_child(1).select(0) 225 | elif _root.get_child_count() > 0: 226 | _root.get_child(0).select(0) 227 | _history_tree.ensure_cursor_is_visible() 228 | 229 | func _clear_tree_item_children(item): 230 | if not item: 231 | return 232 | for child in item.get_children(): 233 | item.remove_child(child) 234 | child.free() 235 | 236 | func _add_filter(filter_name): 237 | return func(toggled): 238 | var found_filter_idx = _filter_types.find(filter_name) 239 | if found_filter_idx != -1: 240 | _filter_types.remove_at(found_filter_idx) 241 | if toggled: 242 | _filter_types.append(filter_name) 243 | _update_tree() 244 | 245 | func _add_filter_checkbox(cname, button_pressed, on_toggled): 246 | var check_box = CheckBox.new() 247 | check_box.text = cname 248 | check_box.toggled.connect(on_toggled) 249 | check_box.button_pressed = button_pressed 250 | _check_boxes.add_child(check_box) 251 | 252 | 253 | class HistoryItem: 254 | func add_to(history: Array[HistoryItem]): 255 | var copy = history.duplicate() 256 | for el in copy: 257 | if el.equals(self): 258 | history.erase(el) 259 | history.push_front(self) 260 | 261 | func equals(another) -> bool: 262 | return false 263 | 264 | func fill(item: TreeItem): 265 | pass 266 | 267 | func is_valid() -> bool: 268 | return false 269 | 270 | func open(): 271 | pass 272 | 273 | func has_filter(types) -> bool: 274 | return true 275 | 276 | func has_same_type_as(another) -> bool: 277 | return false 278 | 279 | 280 | class HistoryItemScene extends HistoryItem: 281 | var _editor_interface 282 | var _scene_path: String 283 | var _icon 284 | 285 | func _init(scene_path, icon, editor_interface) -> void: 286 | _scene_path = scene_path 287 | _icon = icon 288 | _editor_interface = editor_interface 289 | 290 | func equals(another) -> bool: 291 | if not another is HistoryItemScene: 292 | return false 293 | return self._scene_path == another._scene_path 294 | 295 | func fill(item: TreeItem): 296 | item.set_text(0, _scene_path.get_file().get_basename()) 297 | item.set_icon(0, _icon) 298 | 299 | func is_valid() -> bool: 300 | return self._scene_path in _editor_interface.get_open_scenes() 301 | 302 | func open(): 303 | if is_valid(): 304 | _editor_interface.open_scene_from_path(self._scene_path) 305 | 306 | func has_filter(types) -> bool: 307 | return "scene" in types 308 | 309 | func has_same_type_as(another) -> bool: 310 | return another is HistoryItemScene 311 | 312 | 313 | class HistoryItemScript extends HistoryItem: 314 | var _scripts_tab_container: TabContainer 315 | var _scripts_item_list: ItemList 316 | var _control: WeakRef 317 | 318 | func _init(control, scripts_tab_container, scripts_item_list) -> void: 319 | _control = control 320 | _scripts_tab_container = scripts_tab_container 321 | _scripts_item_list = scripts_item_list 322 | 323 | func equals(another) -> bool: 324 | if not another is HistoryItemScript: 325 | return false 326 | return self._control.get_ref() == another._control.get_ref() 327 | 328 | func fill(item: TreeItem): 329 | var control: Control = _control.get_ref() 330 | if control: 331 | var tab_idx = _scripts_tab_container.get_tab_idx_from_control(control) 332 | var list_item_idx = _find_item_list_idx_by_tab_idx(tab_idx) 333 | if list_item_idx != -1: 334 | item.set_text(0, _scripts_item_list.get_item_text(list_item_idx)) 335 | item.set_icon(0, _scripts_item_list.get_item_icon(list_item_idx)) 336 | 337 | func is_valid() -> bool: 338 | return self._control.get_ref() != null 339 | 340 | func open(): 341 | var control: Control = _control.get_ref() 342 | if control: 343 | var tab_idx = _scripts_tab_container.get_tab_idx_from_control(control) 344 | var item_idx = _find_item_list_idx_by_tab_idx(tab_idx) 345 | if item_idx != -1: 346 | if not _scripts_item_list.is_selected(item_idx): 347 | _scripts_item_list.select(item_idx) 348 | _scripts_item_list.item_selected.emit(item_idx) 349 | 350 | func has_filter(types) -> bool: 351 | var control: Control = _control.get_ref() 352 | if not control: 353 | return false 354 | if "EditorHelp" in str(control): 355 | return "doc" in types 356 | else: 357 | return "script" in types 358 | 359 | func _find_item_list_idx_by_tab_idx(tab_idx) -> int: 360 | for i in _scripts_item_list.item_count: 361 | var metadata = _scripts_item_list.get_item_metadata(i) 362 | if metadata == tab_idx: 363 | return i 364 | return -1 365 | 366 | func has_same_type_as(another) -> bool: 367 | return another is HistoryItemScript 368 | --------------------------------------------------------------------------------