Skip to content

Commit 8eb33a6

Browse files
committed
Implemented Window Switcher Management
1 parent aca73d9 commit 8eb33a6

File tree

2 files changed

+193
-6
lines changed

2 files changed

+193
-6
lines changed

castervoice/rules/core/navigation_rules/window_mgmt_rule.py

+49-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,58 @@
1-
from dragonfly import MappingRule, Function, Repeat, ShortIntegerRef
1+
from dragonfly import MappingRule, Function, Repeat, DictListRef, Repetition, get_engine, ShortIntegerRef
22

33
from castervoice.lib import utilities
44
from castervoice.lib import virtual_desktops
55
from castervoice.lib.actions import Key
66
from castervoice.lib.ctrl.mgr.rule_details import RuleDetails
77
from castervoice.lib.merge.state.short import R
88

9+
try: # Try first loading from caster user directory
10+
from navigation_rules.window_mgmt_rule_support import refresh_open_windows_dictlist, debug_window_switching, switch_window, open_windows_dictlist, timerinstance
11+
except ImportError:
12+
from castervoice.rules.core.navigation_rules.window_mgmt_rule_support import refresh_open_windows_dictlist, debug_window_switching, switch_window, open_windows_dictlist, timerinstance
13+
14+
15+
"""
16+
Window Switch Manager to swap windows by saying words in their title.
17+
18+
Uses a timer to periodically load the list of open windows into a DictList,
19+
so they can be referenced by the "switch window" command.
20+
21+
Commands:
22+
23+
"window switch <windows>" -> switch to the window with the given word in its
24+
title. If multiple windows have that word in
25+
their title, then you can say more words in the
26+
window's title to disambiguate which one you
27+
mean. If you don't, the caster messaging window will be
28+
foregrounded instead with info on which windows
29+
are ambiguously being matched by your keywords.
30+
"window switch refresh" -> manually reload the list of windows. Useful while
31+
developing if you don't want to use the timer. Command disabled
32+
"window switch show" -> output information about which keywords can
33+
be used on their own to switch windows and which
34+
require multiple words.
35+
36+
"""
37+
938

1039
class WindowManagementRule(MappingRule):
1140
mapping = {
12-
'maximize win':
41+
'window maximize':
1342
R(Function(utilities.maximize_window)),
14-
'minimize win':
43+
'window minimize':
1544
R(Function(utilities.minimize_window)),
16-
17-
# Workspace management
45+
'window restore':
46+
R(Function(utilities.restore_window)),
47+
# Window Switcher Management
48+
"window switch <windows>":
49+
R(Function(switch_window), rdescript=""), # Block printing out rdescript
50+
# Manualy refreshes open windows if `timerinstance.set()` not used
51+
# "window switch refresh":
52+
# R(Function(lambda: refresh_open_windows_dictlist())),
53+
"window switch show":
54+
R(Function(debug_window_switching)),
55+
# Virtual Workspace Management
1856
"show work [spaces]":
1957
R(Key("w-tab")),
2058
"(create | new) work [space]":
@@ -27,7 +65,6 @@ class WindowManagementRule(MappingRule):
2765
R(Key("wc-right"))*Repeat(extra="n"),
2866
"(previous | prior) work [space] [<n>]":
2967
R(Key("wc-left"))*Repeat(extra="n"),
30-
3168
"go work [space] <n>":
3269
R(Function(virtual_desktops.go_to_desktop_number)),
3370
"send work [space] <n>":
@@ -38,9 +75,15 @@ class WindowManagementRule(MappingRule):
3875

3976
extras = [
4077
ShortIntegerRef("n", 1, 20, default=1),
78+
Repetition(name="windows", min=1, max=5,
79+
child=DictListRef("window_by_keyword", open_windows_dictlist))
4180
]
4281

4382

83+
# Window switch update sopen_windows_dictlist every 2 second
84+
timerinstance.set()
85+
86+
4487
def get_rule():
4588
details = RuleDetails(name="window management rule")
4689
return WindowManagementRule, details
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# All credit goes to caspark
2+
# This is adapted from caspark's grammar at https://gist.github.com/caspark/9c2c5e2853a14b6e28e9aa4f121164a6
3+
4+
from __future__ import print_function
5+
6+
import re
7+
import time
8+
import six
9+
10+
from dragonfly import Window, DictList, get_engine, get_current_engine
11+
from castervoice.lib import utilities
12+
from castervoice.lib.util import recognition_history
13+
14+
_history = recognition_history.get_and_register_history(1)
15+
16+
open_windows_dictlist = DictList("open_windows")
17+
18+
WORD_SPLITTER = re.compile('[^a-zA-Z0-9]+')
19+
20+
21+
def lower_if_not_abbreviation(s):
22+
if len(s) <= 4 and s.upper() == s:
23+
return s
24+
else:
25+
return s.lower()
26+
27+
28+
def find_window(window_matcher_func, timeout_ms=3000):
29+
"""
30+
Returns a Window matching the given matcher function, or raises an error otherwise
31+
"""
32+
steps = int(timeout_ms / 100)
33+
for i in range(steps):
34+
for win in Window.get_all_windows():
35+
if window_matcher_func(win):
36+
return win
37+
time.sleep(0.1)
38+
raise ValueError(
39+
"no matching window found within {} ms".format(timeout_ms))
40+
41+
42+
def refresh_open_windows_dictlist():
43+
"""
44+
Refreshes `open_windows_dictlist`
45+
"""
46+
window_options = {}
47+
for window in (x for x in Window.get_all_windows() if
48+
x.is_valid and
49+
x.is_enabled and
50+
x.is_visible and
51+
not x.executable.startswith("C:\\Windows") and
52+
x.classname != "DgnResultsBoxWindow"):
53+
for word in {lower_if_not_abbreviation(word)
54+
for word
55+
in WORD_SPLITTER.split(window.title)
56+
if len(word)}:
57+
if word in window_options:
58+
window_options[word] += [window]
59+
else:
60+
window_options[word] = [window]
61+
62+
window_options = {k: v for k,
63+
v in six.iteritems(window_options) if v is not None}
64+
open_windows_dictlist.set(window_options)
65+
66+
67+
def debug_window_switching():
68+
"""
69+
Prints out contents of `open_windows_dictlist`
70+
"""
71+
options = open_windows_dictlist.copy()
72+
print("*** Windows known:\n",
73+
"\n".join(sorted({w.title for list_of_windows in six.itervalues(options) for w in list_of_windows})))
74+
75+
print("*** Single word switching options:\n", "\n".join(
76+
"{}: '{}'".format(
77+
k.ljust(20), "', '".join(window.title for window in options[k])
78+
) for k in sorted(six.iterkeys(options)) if len(options[k]) == 1))
79+
print("*** Ambiguous switching options:\n", "\n".join(
80+
"{}: '{}'".format(
81+
k.ljust(20), "', '".join(window.title for window in options[k])
82+
) for k in sorted(six.iterkeys(options)) if len(options[k]) > 1))
83+
84+
85+
def switch_window(windows):
86+
"""
87+
Matches keywords to window titles stored in `open_windows_dictlist`
88+
"""
89+
matched_window_handles = {w.handle: w for w in windows[0]}
90+
for window_options in windows[1:]:
91+
matched_window_handles = {
92+
w.handle: w for w in window_options if w.handle in matched_window_handles}
93+
if six.PY2:
94+
matched_windows = matched_window_handles.values()
95+
else:
96+
matched_windows = list(matched_window_handles.values())
97+
if len(matched_windows) == 1:
98+
window = matched_windows[0]
99+
print("Window Management: Switching to", window.title)
100+
window.set_foreground()
101+
else:
102+
try:
103+
# Brings caster messaging window to the forefront
104+
messaging_title = utilities.get_caster_messaging_window()
105+
messaging_window = find_window(
106+
lambda w: messaging_title in w.title, timeout_ms=100)
107+
if messaging_window.is_minimized:
108+
messaging_window.restore()
109+
else:
110+
messaging_window.set_foreground()
111+
except ValueError:
112+
# window didn't exist, it'll be created when we write some output
113+
pass
114+
if len(matched_windows) >= 2: # Keywords match more than one window title
115+
print("Ambiguous window switch command:\n", "\n".join(
116+
"'{}' from {} (handle: {})".format(w.title, w.executable, w.handle) for w in matched_windows))
117+
else:
118+
# At this point the series of keywords do not match any single window title.
119+
# Uses recognition history to inform what keywords were said in <windows> repetition element
120+
spec_n_word = 2 # `window switch`
121+
# Edge case: if the spec `window switch <windows>` word length changes.
122+
# The `spec_n_word` integer equals `n` number of words in spec excluding <windows>
123+
words = list(map(str, _history[0]))
124+
del words[:spec_n_word]
125+
print("Window Management: No matching window title containing keywords: `{}`".format(
126+
' '.join(map(str, words))))
127+
128+
129+
class Timer:
130+
"""
131+
Dragonfly timer runs every 2 seconds updating open_windows_dictlist
132+
"""
133+
timer = None
134+
135+
def __init__(self):
136+
pass
137+
138+
def set(self):
139+
if self.timer is None:
140+
self.timer = get_engine().create_timer(refresh_open_windows_dictlist, 2)
141+
self.timer.start()
142+
143+
144+
timerinstance = Timer()

0 commit comments

Comments
 (0)