]> Dogcows Code - chaz/openbox/blob - scripts/cycle.py
defualt START_WITH_NEXT to true for desktops too
[chaz/openbox] / scripts / cycle.py
1 import ob, otk
2 class _Cycle:
3 """
4 This is a basic cycling class for anything, from xOr's stackedcycle.py,
5 that pops up a cycling menu when there's more than one thing to be cycled
6 to.
7 An example of inheriting from and modifying this class is _CycleWindows,
8 which allows users to cycle around windows.
9
10 This class could conceivably be used to cycle through anything -- desktops,
11 windows of a specific class, XMMS playlists, etc.
12 """
13
14 """This specifies a rough limit of characters for the cycling list titles.
15 Titles which are larger will be chopped with an elipsis in their
16 center."""
17 TITLE_SIZE_LIMIT = 80
18
19 """If this is non-zero then windows will be activated as they are
20 highlighted in the cycling list (except iconified windows)."""
21 ACTIVATE_WHILE_CYCLING = 0
22
23 """If this is true, we start cycling with the next (or previous) thing
24 selected."""
25 START_WITH_NEXT = 1
26
27 """If this is true, a popup window will be displayed with the options
28 while cycling."""
29 SHOW_POPUP = 1
30
31 def __init__(self):
32 """Initialize an instance of this class. Subclasses should
33 do any necessary event binding in their constructor as well.
34 """
35 self.cycling = 0 # internal var used for going through the menu
36 self.items = [] # items to cycle through
37
38 self.widget = None # the otk menu widget
39 self.menuwidgets = [] # labels in the otk menu widget TODO: RENAME
40
41 def createPopup(self):
42 """Creates the cycling popup menu.
43 """
44 self.widget = otk.Widget(self.screen.number(), ob.openbox,
45 otk.Widget.Vertical, 0, 1)
46
47 def destroyPopup(self):
48 """Destroys (or rather, cleans up after) the cycling popup menu.
49 """
50 self.menuwidgets = []
51 self.widget = 0
52
53 def populateItems(self):
54 """Populate self.items with the appropriate items that can currently
55 be cycled through. self.items may be cleared out before this
56 method is called.
57 """
58 pass
59
60 def menuLabel(self, item):
61 """Return a string indicating the menu label for the given item.
62 Don't worry about title truncation.
63 """
64 pass
65
66 def itemEqual(self, item1, item2):
67 """Compare two items, return 1 if they're "equal" for purposes of
68 cycling, and 0 otherwise.
69 """
70 # suggestion: define __eq__ on item classes so that this works
71 # in the general case. :)
72 return item1 == item2
73
74 def populateLists(self):
75 """Populates self.items and self.menuwidgets, and then shows and
76 positions the cycling popup. You probably shouldn't mess with
77 this function; instead, see populateItems and menuLabel.
78 """
79 self.widget.hide()
80
81 try:
82 current = self.items[self.menupos]
83 except IndexError:
84 current = None
85 oldpos = self.menupos
86 self.menupos = -1
87
88 self.items = []
89 self.populateItems()
90
91 # make the widgets
92 i = 0
93 self.menuwidgets = []
94 for i in range(len(self.items)):
95 c = self.items[i]
96
97 w = otk.Label(self.widget)
98 # current item might have shifted after a populateItems()
99 # call, so we need to do this test.
100 if current and self.itemEqual(c, current):
101 self.menupos = i
102 w.setHilighted(1)
103 self.menuwidgets.append(w)
104
105 t = self.menuLabel(c)
106 # TODO: maybe subclasses will want to truncate in different ways?
107 if len(t) > self.TITLE_SIZE_LIMIT: # limit the length of titles
108 t = t[:self.TITLE_SIZE_LIMIT / 2 - 2] + "..." + \
109 t[0 - self.TITLE_SIZE_LIMIT / 2 - 2:]
110 w.setText(t)
111
112 # The item we were on might be gone entirely
113 if self.menupos < 0:
114 # try stay at the same spot in the menu
115 if oldpos >= len(self.items):
116 self.menupos = len(self.items) - 1
117 else:
118 self.menupos = oldpos
119
120 # find the size for the popup
121 width = 0
122 height = 0
123 for w in self.menuwidgets:
124 size = w.minSize()
125 if size.width() > width: width = size.width()
126 height += size.height()
127
128 # show or hide the list and its child widgets
129 if len(self.items) > 1:
130 size = self.screeninfo.size()
131 self.widget.moveresize(otk.Rect((size.width() - width) / 2,
132 (size.height() - height) / 2,
133 width, height))
134 if self.SHOW_POPUP: self.widget.show(1)
135
136 def activateTarget(self, final):
137 """Activates (focuses and, if the user requested it, raises a window).
138 If final is true, then this is the very last window we're activating
139 and the user has finished cycling.
140 """
141 pass
142
143 def setDataInfo(self, data):
144 """Retrieve and/or calculate information when we start cycling,
145 preferably caching it. Data is what's given to callback functions.
146 """
147 self.screen = ob.openbox.screen(data.screen)
148 self.screeninfo = otk.display.screenInfo(data.screen)
149
150 def chooseStartPos(self):
151 """Set self.menupos to a number between 0 and len(self.items) - 1.
152 By default the initial menupos is 0, but this can be used to change
153 it to some other position."""
154 pass
155
156 def cycle(self, data, forward):
157 """Does the actual job of cycling through windows. data is a callback
158 parameter, while forward is a boolean indicating whether the
159 cycling goes forwards (true) or backwards (false).
160 """
161
162 initial = 0
163
164 if not self.cycling:
165 ob.kgrab(data.screen, self.grabfunc)
166 # the pointer grab causes pointer events during the keyboard grab
167 # to go away, which means we don't get enter notifies when the
168 # popup disappears, screwing up the focus
169 ob.mgrab(data.screen)
170
171 self.cycling = 1
172 self.state = data.state
173 self.menupos = 0
174
175 self.setDataInfo(data)
176
177 self.createPopup()
178 self.items = [] # so it doesnt try start partway through the list
179 self.populateLists()
180
181 self.chooseStartPos()
182 self.initpos = self.menupos
183
184 initial = 1
185
186 if not self.items: return # don't bother doing anything
187
188 self.menuwidgets[self.menupos].setHighlighted(0)
189
190 if initial and not self.START_WITH_NEXT:
191 pass
192 else:
193 if forward:
194 self.menupos += 1
195 else:
196 self.menupos -= 1
197 # wrap around
198 if self.menupos < 0: self.menupos = len(self.items) - 1
199 elif self.menupos >= len(self.items): self.menupos = 0
200 self.menuwidgets[self.menupos].setHighlighted(1)
201 if self.ACTIVATE_WHILE_CYCLING:
202 self.activateTarget(0) # activate, but dont deiconify/unshade/raise
203
204 def grabfunc(self, data):
205 """A callback method that grabs away all keystrokes so that navigating
206 the cycling menu is possible."""
207 done = 0
208 notreverting = 1
209 # have all the modifiers this started with been released?
210 if not self.state & data.state:
211 done = 1
212 elif data.action == ob.KeyAction.Press:
213 # has Escape been pressed?
214 if data.key == "Escape":
215 done = 1
216 notreverting = 0
217 # revert
218 self.menupos = self.initpos
219 # has Enter been pressed?
220 elif data.key == "Return":
221 done = 1
222
223 if done:
224 # activate, and deiconify/unshade/raise
225 self.activateTarget(notreverting)
226 self.destroyPopup()
227 self.cycling = 0
228 ob.kungrab()
229 ob.mungrab()
230
231 def next(self, data):
232 """Focus the next window."""
233 if not data.state:
234 raise RuntimeError("next must be bound to a key" +
235 "combination with at least one modifier")
236 self.cycle(data, 1)
237
238 def previous(self, data):
239 """Focus the previous window."""
240 if not data.state:
241 raise RuntimeError("previous must be bound to a key" +
242 "combination with at least one modifier")
243 self.cycle(data, 0)
244
245 #---------------------- Window Cycling --------------------
246 import focus
247 class _CycleWindows(_Cycle):
248 """
249 This is a basic cycling class for Windows.
250
251 An example of inheriting from and modifying this class is _ClassCycleWindows,
252 which allows users to cycle around windows of a certain application
253 name/class only.
254
255 This class has an underscored name because I use the singleton pattern
256 (so CycleWindows is an actual instance of this class). This doesn't have
257 to be followed, but if it isn't followed then the user will have to create
258 their own instances of your class and use that (not always a bad thing).
259
260 An example of using the CycleWindows singleton:
261
262 from cycle import CycleWindows
263 CycleWindows.INCLUDE_ICONS = 0 # I don't like cycling to icons
264 ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindows.next)
265 ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindows.previous)
266 """
267
268 """If this is non-zero then windows from all desktops will be included in
269 the stacking list."""
270 INCLUDE_ALL_DESKTOPS = 0
271
272 """If this is non-zero then windows which are iconified on the current
273 desktop will be included in the stacking list."""
274 INCLUDE_ICONS = 1
275
276 """If this is non-zero then windows which are iconified from all desktops
277 will be included in the stacking list."""
278 INCLUDE_ICONS_ALL_DESKTOPS = 1
279
280 """If this is non-zero then windows which are on all-desktops at once will
281 be included."""
282 INCLUDE_OMNIPRESENT = 1
283
284 """A better default for window cycling than generic cycling."""
285 ACTIVATE_WHILE_CYCLING = 1
286
287 """When cycling focus, raise the window chosen as well as focusing it."""
288 RAISE_WINDOW = 1
289
290 def __init__(self):
291 _Cycle.__init__(self)
292
293 def newwindow(data):
294 if self.cycling: self.populateLists()
295 def closewindow(data):
296 if self.cycling: self.populateLists()
297
298 ob.ebind(ob.EventAction.NewWindow, newwindow)
299 ob.ebind(ob.EventAction.CloseWindow, closewindow)
300
301 def shouldAdd(self, client):
302 """Determines if a client should be added to the cycling list."""
303 curdesk = self.screen.desktop()
304 desk = client.desktop()
305
306 if not client.normal(): return 0
307 if not (client.canFocus() or client.focusNotify()): return 0
308 if focus.AVOID_SKIP_TASKBAR and client.skipTaskbar(): return 0
309
310 if client.iconic():
311 if self.INCLUDE_ICONS:
312 if self.INCLUDE_ICONS_ALL_DESKTOPS: return 1
313 if desk == curdesk: return 1
314 return 0
315 if self.INCLUDE_OMNIPRESENT and desk == 0xffffffff: return 1
316 if self.INCLUDE_ALL_DESKTOPS: return 1
317 if desk == curdesk: return 1
318
319 return 0
320
321 def populateItems(self):
322 # get the list of clients, keeping iconic windows at the bottom
323 iconic_clients = []
324 for c in focus._clients:
325 if self.shouldAdd(c):
326 if c.iconic(): iconic_clients.append(c)
327 else: self.items.append(c)
328 self.items.extend(iconic_clients)
329
330 def menuLabel(self, client):
331 if client.iconic(): t = '[' + client.iconTitle() + ']'
332 else: t = client.title()
333
334 if self.INCLUDE_ALL_DESKTOPS:
335 d = client.desktop()
336 if d == 0xffffffff: d = self.screen.desktop()
337 t = self.screen.desktopName(d) + " - " + t
338
339 return t
340
341 def itemEqual(self, client1, client2):
342 return client1.window() == client2.window()
343
344 def activateTarget(self, final):
345 """Activates (focuses and, if the user requested it, raises a window).
346 If final is true, then this is the very last window we're activating
347 and the user has finished cycling."""
348 try:
349 client = self.items[self.menupos]
350 except IndexError: return # empty list
351
352 # move the to client's desktop if required
353 if not (client.iconic() or client.desktop() == 0xffffffff or \
354 client.desktop() == self.screen.desktop()):
355 root = self.screeninfo.rootWindow()
356 ob.send_client_msg(root, otk.atoms.net_current_desktop,
357 root, client.desktop())
358
359 # send a net_active_window message for the target
360 if final or not client.iconic():
361 if final: r = self.RAISE_WINDOW
362 else: r = 0
363 ob.send_client_msg(self.screeninfo.rootWindow(),
364 otk.atoms.openbox_active_window,
365 client.window(), final, r)
366 if not final:
367 focus._skip += 1
368
369 # The singleton.
370 CycleWindows = _CycleWindows()
371
372 #---------------------- Window Cycling --------------------
373 import focus
374 class _CycleWindowsLinear(_CycleWindows):
375 """
376 This class is an example of how to inherit from and make use of the
377 _CycleWindows class. This class also uses the singleton pattern.
378
379 An example of using the CycleWindowsLinear singleton:
380
381 from cycle import CycleWindowsLinear
382 CycleWindows.ALL_DESKTOPS = 1 # I want all my windows in the list
383 ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindowsLinear.next)
384 ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindowsLinear.previous)
385 """
386
387 """When cycling focus, raise the window chosen as well as focusing it."""
388 RAISE_WINDOW = 0
389
390 """If this is true, a popup window will be displayed with the options
391 while cycling."""
392 SHOW_POPUP = 0
393
394 def __init__(self):
395 _CycleWindows.__init__(self)
396
397 def shouldAdd(self, client):
398 """Determines if a client should be added to the cycling list."""
399 curdesk = self.screen.desktop()
400 desk = client.desktop()
401
402 if not client.normal(): return 0
403 if not (client.canFocus() or client.focusNotify()): return 0
404 if focus.AVOID_SKIP_TASKBAR and client.skipTaskbar(): return 0
405
406 if client.iconic(): return 0
407 if self.INCLUDE_OMNIPRESENT and desk == 0xffffffff: return 1
408 if self.INCLUDE_ALL_DESKTOPS: return 1
409 if desk == curdesk: return 1
410
411 return 0
412
413 def populateItems(self):
414 # get the list of clients, keeping iconic windows at the bottom
415 iconic_clients = []
416 for i in range(self.screen.clientCount()):
417 c = self.screen.client(i)
418 if self.shouldAdd(c):
419 self.items.append(c)
420
421 def chooseStartPos(self):
422 if focus._clients:
423 t = focus._clients[0]
424 for i,c in zip(range(len(self.items)), self.items):
425 if self.itemEqual(c, t):
426 self.menupos = i
427 break
428
429 def menuLabel(self, client):
430 t = client.title()
431
432 if self.INCLUDE_ALL_DESKTOPS:
433 d = client.desktop()
434 if d == 0xffffffff: d = self.screen.desktop()
435 t = self.screen.desktopName(d) + " - " + t
436
437 return t
438
439 # The singleton.
440 CycleWindowsLinear = _CycleWindowsLinear()
441
442 #----------------------- Desktop Cycling ------------------
443 class _CycleDesktops(_Cycle):
444 """
445 Example of usage:
446
447 from cycle import CycleDesktops
448 ob.kbind(["W-d"], ob.KeyContext.All, CycleDesktops.next)
449 ob.kbind(["W-S-d"], ob.KeyContext.All, CycleDesktops.previous)
450 """
451 class Desktop:
452 def __init__(self, name, index):
453 self.name = name
454 self.index = index
455 def __eq__(self, other):
456 return other.index == self.index
457
458 def __init__(self):
459 _Cycle.__init__(self)
460
461 def populateItems(self):
462 for i in range(self.screen.numDesktops()):
463 self.items.append(
464 _CycleDesktops.Desktop(self.screen.desktopName(i), i))
465
466 def menuLabel(self, desktop):
467 return desktop.name
468
469 def chooseStartPos(self):
470 self.menupos = self.screen.desktop()
471
472 def activateTarget(self, final):
473 # TODO: refactor this bit
474 try:
475 desktop = self.items[self.menupos]
476 except IndexError: return
477
478 root = self.screeninfo.rootWindow()
479 ob.send_client_msg(root, otk.atoms.net_current_desktop,
480 root, desktop.index)
481
482 CycleDesktops = _CycleDesktops()
483
484 print "Loaded cycle.py"
This page took 0.067261 seconds and 5 git commands to generate.