Skip to content

Commit 0fcf91e

Browse files
committed
Delay showing the popup for quick tap mode
If only one ctrl+tab is intended and ctrl is quickly released, showing the popup for that brief period of time can be annoying. Instead, do what FireFox does and delay opening of the preview panel for a bit. If the key was released before the timeout expires, do not show the panel at all. Additionally, when ctrl+tab is pressed a second time we will force-open the panel because at this point the user clearly wants to do more than just quickly go to the previous tab. The only downside of this method is that now there is some "flicker" when the popup opens after the tab has already been focused.
1 parent 31463c1 commit 0fcf91e

9 files changed

Lines changed: 91 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: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
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 (
8+
cancel_session,
9+
preview_entry,
10+
reopen_panel_at_index,
11+
schedule_panel,
12+
show_panel,
13+
)
814
from .state import get_state
915

1016

1117
class TabStackOpenCommand(sublime_plugin.WindowCommand):
12-
def run(self, *, forward=True) -> None:
18+
def run(self, *, forward=True, selected_index: int = 1) -> None:
1319
window = self.window
1420
if window is None:
1521
return
@@ -21,18 +27,30 @@ def run(self, *, forward=True) -> None:
2127
return
2228

2329
state = get_state(window)
30+
31+
if state.session_pending:
32+
state.session_pending = False
33+
state.session_pending_token += 1
34+
state.session_active = True
35+
state.session_selected_index = selected_index
36+
preview_entry(window, state)
37+
show_panel(window, state)
38+
return
39+
2440
active_group = window.active_group()
2541
state.session_origin_selection = current_group_selection_state(window, active_group)
2642
history = sync_selection_history(window, prune_removed_sheets=True)
2743
entries = collect_entries(window, history)
2844
if not entries:
2945
return
3046

31-
state.session_active = True
47+
state.session_pending = True
48+
state.session_pending_token += 1
3249
state.session_group = active_group
3350
state.session_entries = entries
34-
state.session_selected_index = 1 if len(entries) > 1 else 0
35-
show_panel(window, state)
51+
state.session_selected_index = selected_index
52+
preview_entry(window, state)
53+
schedule_panel(window, state, token=state.session_pending_token)
3654

3755

3856
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: 36 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+
state.clear_session()
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:
@@ -71,6 +98,9 @@ def _commit_session(window, state) -> None:
7198

7299
def cancel_session(window, state) -> None:
73100
if not state.session_active:
101+
if not state.session_pending:
102+
return
103+
state.clear_session()
74104
return
75105

76106
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)