Skip to content

Commit fc3f521

Browse files
authored
Merge pull request #7 from SublimeText/feature/delay-on-tap
Delay popup for quick tab switching
2 parents 31463c1 + cfd78d5 commit fc3f521

9 files changed

Lines changed: 95 additions & 15 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
- Tab captions come from `plugin/captions.py`; widgets, panels, and transient/non-tab views are excluded from MRU.
1717
- Ctrl-release detection lives in `plugin/ctrl_release/`.
1818
- Uner Linux, Ctrl-release polling uses the X11 key state API, so Sublime must run under X11 or XWayland; a native Wayland session has no pollable global key state and Sublime exposes no key-release API.
19-
- `tab_stack_open` opens the quick panel, starts a Ctrl-release poller, and commits when Ctrl is released.
19+
- `tab_stack_open` arms a pending session, starts a Ctrl-release poller, and opens the quick panel after a short delay unless Ctrl is released first.
2020
- Wrap-around cycling is implemented by intentionally closing and reopening the quick panel at the opposite boundary.
2121
That handoff keeps the session active and reuses the existing Ctrl-release poller.
2222
- `tab_stack.session_active` is the quick-panel session context key.
23+
- `tab_stack.session_pending` is the arming context before the panel appears.
2324
- Boundary-only contexts are `tab_stack.quick_panel_at_top` and `tab_stack.quick_panel_at_bottom`.
2425
- When `tab_stack.session_active` is `false`, `ctrl+tab` starts `tab_stack_open`.
2526
`ctrl+shift+tab` is bound as a no-op to prevent accidental presses.

‎Default.sublime-keymap‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@
33
{
44
"keys": ["ctrl+tab"],
55
"command": "tab_stack_open",
6-
"args": {"forward": true},
6+
"args": {"forward": true, "selected_index": 1},
77
"context": [
8+
{"key": "tab_stack.session_active", "operator": "equal", "operand": false},
9+
{"key": "tab_stack.session_pending", "operator": "equal", "operand": false},
810
// This context cannot be evaluated on non-view sheets.
911
// Window settings cannot be queried via `settings.*` either.
1012
// https://github.com/sublimehq/sublime_text/issues/5377
1113
// {"key": "tab_stack.ctrl_release_available", "operator": "equal", "operand": true}
1214
]
1315
},
16+
{
17+
"keys": ["ctrl+tab"],
18+
"command": "tab_stack_open",
19+
"args": {"forward": true, "selected_index": 2},
20+
"context": [
21+
{"key": "tab_stack.session_pending", "operator": "equal", "operand": true}
22+
]
23+
},
1424
// We just override this binding to prevent accidental presses of it.
1525
{
1626
"keys": ["ctrl+shift+tab"],

‎plugin/commands.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from .ctrl_release import is_available
55
from .history import current_group_selection_state, sync_selection_history
66
from .mru import collect_entries
7-
from .session import cancel_session, reopen_panel_at_index, show_panel
7+
from .session import cancel_session, reopen_panel_at_index, schedule_panel, show_panel
88
from .state import get_state
99

1010

1111
class TabStackOpenCommand(sublime_plugin.WindowCommand):
12-
def run(self, *, forward=True) -> None:
12+
def run(self, *, forward=True, selected_index: int = 1) -> None:
1313
window = self.window
1414
if window is None:
1515
return
@@ -21,18 +21,28 @@ def run(self, *, forward=True) -> None:
2121
return
2222

2323
state = get_state(window)
24+
25+
if state.session_pending:
26+
state.session_pending = False
27+
state.session_pending_token += 1
28+
state.session_active = True
29+
state.session_selected_index = selected_index
30+
show_panel(window, state)
31+
return
32+
2433
active_group = window.active_group()
2534
state.session_origin_selection = current_group_selection_state(window, active_group)
2635
history = sync_selection_history(window, prune_removed_sheets=True)
2736
entries = collect_entries(window, history)
2837
if not entries:
2938
return
3039

31-
state.session_active = True
40+
state.session_pending = True
41+
state.session_pending_token += 1
3242
state.session_group = active_group
3343
state.session_entries = entries
34-
state.session_selected_index = 1 if len(entries) > 1 else 0
35-
show_panel(window, state)
44+
state.session_selected_index = selected_index
45+
schedule_panel(window, state, token=state.session_pending_token)
3646

3747

3848
class TabStackCancelCommand(sublime_plugin.WindowCommand):

‎plugin/ctrl_release/linux.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def _ctrl_down(self) -> bool:
7373
state.keycode_right,
7474
)
7575

76+
def is_ctrl_down(self) -> bool:
77+
return self._ctrl_down()
78+
7679
def _open_x11_state(self) -> Optional[_X11State]:
7780
display_name = os.environ.get("DISPLAY")
7881
if not display_name:

‎plugin/ctrl_release/macos.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def _ctrl_down(self) -> bool:
5959
)
6060
)
6161

62+
def is_ctrl_down(self) -> bool:
63+
return self._ctrl_down()
64+
6265
def _open_core_graphics(self) -> Optional[Any]:
6366
return _get_core_graphics()
6467

‎plugin/ctrl_release/windows.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def _fire_release(self) -> None:
4444
def _ctrl_down(self) -> bool:
4545
return bool(self._user32.GetAsyncKeyState(_VK_CONTROL) & 0x8000)
4646

47+
def is_ctrl_down(self) -> bool:
48+
return self._ctrl_down()
49+
4750
def _open_user32(self) -> Optional[Any]:
4851
return _get_user32()
4952

‎plugin/listener.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ def _ensure_selection_poller(window, state) -> None:
2020

2121
def _session_boundary_value(window, key: str) -> bool:
2222
state = get_state(window)
23+
if key == "tab_stack.session_pending":
24+
return state.session_pending
2325
if not state.session_active:
2426
return False
2527
if key == "tab_stack.session_active":
26-
return True
28+
return state.session_active
2729

2830
entries = state.session_entries
2931
if not entries or len(entries) < 2:
@@ -47,7 +49,7 @@ def on_activated(self, view) -> None:
4749
state.overlay_depth += 1
4850
return
4951

50-
if state.session_active or state.overlay_active:
52+
if state.session_active or state.session_pending or state.overlay_active:
5153
return
5254

5355
_ensure_selection_poller(window, state)
@@ -69,6 +71,7 @@ def on_query_context(self, view, key, operator, operand, match_all):
6971
value = is_available()
7072
elif key in {
7173
"tab_stack.session_active",
74+
"tab_stack.session_pending",
7275
"tab_stack.quick_panel_at_top",
7376
"tab_stack.quick_panel_at_bottom",
7477
}:

‎plugin/session.py‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55
from .sheets import apply_group_selection
66
from .state import TabStackWindowState
77

8+
SHOW_PANEL_DELAY_MS = 150
9+
10+
11+
def _ensure_ctrl_release_poller(window, state) -> None:
12+
if state.ctrl_release_poller:
13+
return
14+
15+
def on_release() -> None:
16+
if state.session_active:
17+
_commit_session(window, state)
18+
elif state.session_pending:
19+
_commit_pending_session(window, state)
20+
21+
state.ctrl_release_poller = CtrlReleasePoller(on_release, 25)
22+
state.ctrl_release_poller.start()
23+
824

925
def show_panel(window, state) -> None:
1026
if not state.session_active or not state.session_entries:
@@ -33,12 +49,23 @@ def on_highlight(index: int) -> None:
3349
placeholder="Release ctrl to close; Hit ctrl+escape to abort",
3450
)
3551

36-
if not state.ctrl_release_poller:
37-
state.ctrl_release_poller = CtrlReleasePoller(
38-
lambda: _commit_session(window, state),
39-
25,
40-
)
41-
state.ctrl_release_poller.start()
52+
_ensure_ctrl_release_poller(window, state)
53+
54+
55+
def schedule_panel(window, state, *, token: int) -> None:
56+
_ensure_ctrl_release_poller(window, state)
57+
58+
def open_if_still_pending() -> None:
59+
if not state.session_pending or state.session_pending_token != token:
60+
return
61+
if state.ctrl_release_poller is not None and not state.ctrl_release_poller.is_ctrl_down():
62+
state.clear_session()
63+
return
64+
state.session_active = True
65+
state.session_pending = False
66+
show_panel(window, state)
67+
68+
sublime.set_timeout(open_if_still_pending, SHOW_PANEL_DELAY_MS)
4269

4370

4471
def preview_entry(window, state: TabStackWindowState) -> None:
@@ -50,6 +77,18 @@ def preview_entry(window, state: TabStackWindowState) -> None:
5077
apply_group_selection(window, entry.selection, state.session_group)
5178

5279

80+
def _commit_pending_session(window, state) -> None:
81+
entries = state.session_entries
82+
if not entries:
83+
state.clear_session()
84+
return
85+
86+
index = clamp_index(state.session_selected_index, len(entries))
87+
selection = entries[index].selection
88+
apply_group_selection(window, selection, state.session_group)
89+
state.clear_session()
90+
91+
5392
def _commit_session(window, state) -> None:
5493
if not state.session_active:
5594
return
@@ -71,6 +110,9 @@ def _commit_session(window, state) -> None:
71110

72111
def cancel_session(window, state) -> None:
73112
if not state.session_active:
113+
if not state.session_pending:
114+
return
115+
state.clear_session()
74116
return
75117

76118
origin = state.session_origin_selection

‎plugin/state.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class SheetSelectionHistory(TypedDict):
2222
@dataclass
2323
class TabStackWindowState:
2424
session_active: bool = False
25+
session_pending: bool = False
26+
session_pending_token: int = 0
27+
"""Monotonically incremented token to track and implicitly cancel set_timeout callbacks."""
2528
session_origin_selection: Optional[GroupSelectionState] = None
2629
session_selected_index: int = 0
2730
session_group: int = 0
@@ -43,11 +46,13 @@ def overlay_active(self) -> bool:
4346

4447
def clear_session(self) -> None:
4548
self.session_active = False
49+
self.session_pending = False
4650
self.session_origin_selection = None
4751
self.session_selected_index = 0
4852
self.session_group = 0
4953
self.session_entries = None
5054
self.session_panel_reopening = False
55+
self.session_pending_token += 1
5156
if self.ctrl_release_poller is not None:
5257
self.ctrl_release_poller.stop()
5358
self.ctrl_release_poller = None

0 commit comments

Comments
 (0)