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
7 An example of inheriting from and modifying this class is _CycleWindows,
8 which allows users to cycle around windows.
10 This class could conceivably be used to cycle through anything -- desktops,
11 windows of a specific class, XMMS playlists, etc.
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
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
23 """If this is true, we start cycling with the next (or previous) thing
27 """If this is true, a popup window will be displayed with the options
32 """Initialize an instance of this class. Subclasses should
33 do any necessary event binding in their constructor as well.
35 self
.cycling
= 0 # internal var used for going through the menu
36 self
.items
= [] # items to cycle through
38 self
.widget
= None # the otk menu widget
39 self
.menuwidgets
= [] # labels in the otk menu widget TODO: RENAME
41 def createPopup(self
):
42 """Creates the cycling popup menu.
44 self
.widget
= otk
.Widget(self
.screen
.number(), ob
.openbox
,
45 otk
.Widget
.Vertical
, 0, 1)
47 def destroyPopup(self
):
48 """Destroys (or rather, cleans up after) the cycling popup menu.
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
60 def menuLabel(self
, item
):
61 """Return a string indicating the menu label for the given item.
62 Don't worry about title truncation.
66 def itemEqual(self
, item1
, item2
):
67 """Compare two items, return 1 if they're "equal" for purposes of
68 cycling, and 0 otherwise.
70 # suggestion: define __eq__ on item classes so that this works
71 # in the general case. :)
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.
82 current
= self
.items
[self
.menupos
]
94 for i
in range(len(self
.items
)):
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
):
103 self
.menuwidgets
.append(w
)
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:]
112 # The item we were on might be gone entirely
114 # try stay at the same spot in the menu
115 if oldpos
>= len(self
.items
):
116 self
.menupos
= len(self
.items
) - 1
118 self
.menupos
= oldpos
120 # find the size for the popup
123 for w
in self
.menuwidgets
:
125 if size
.width() > width
: width
= size
.width()
126 height
+= size
.height()
128 # show or hide the list and its child widgets
129 if len(self
.items
) > 1:
130 size
= self
.screen
.size()
131 self
.widget
.moveresize(otk
.Rect((size
.width() - width
) / 2,
132 (size
.height() - height
) / 2,
134 if self
.SHOW_POPUP
: self
.widget
.show(1)
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.
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.
147 self
.screen
= ob
.openbox
.screen(data
.screen
)
149 def chooseStartPos(self
):
150 """Set self.menupos to a number between 0 and len(self.items) - 1.
151 By default the initial menupos is 0, but this can be used to change
152 it to some other position."""
155 def cycle(self
, data
, forward
):
156 """Does the actual job of cycling through windows. data is a callback
157 parameter, while forward is a boolean indicating whether the
158 cycling goes forwards (true) or backwards (false).
164 ob
.kgrab(data
.screen
, self
.grabfunc
)
165 # the pointer grab causes pointer events during the keyboard grab
166 # to go away, which means we don't get enter notifies when the
167 # popup disappears, screwing up the focus
168 ob
.mgrab(data
.screen
)
171 self
.state
= data
.state
174 self
.setDataInfo(data
)
177 self
.items
= [] # so it doesnt try start partway through the list
180 self
.chooseStartPos()
181 self
.initpos
= self
.menupos
185 if not self
.items
: return # don't bother doing anything
187 self
.menuwidgets
[self
.menupos
].setHighlighted(0)
189 if initial
and not self
.START_WITH_NEXT
:
197 if self
.menupos
< 0: self
.menupos
= len(self
.items
) - 1
198 elif self
.menupos
>= len(self
.items
): self
.menupos
= 0
199 self
.menuwidgets
[self
.menupos
].setHighlighted(1)
200 if self
.ACTIVATE_WHILE_CYCLING
:
201 self
.activateTarget(0) # activate, but dont deiconify/unshade/raise
203 def grabfunc(self
, data
):
204 """A callback method that grabs away all keystrokes so that navigating
205 the cycling menu is possible."""
208 # have all the modifiers this started with been released?
209 if not self
.state
& data
.state
:
211 elif data
.action
== ob
.KeyAction
.Press
:
212 # has Escape been pressed?
213 if data
.key
== "Escape":
217 self
.menupos
= self
.initpos
218 # has Enter been pressed?
219 elif data
.key
== "Return":
223 # activate, and deiconify/unshade/raise
224 self
.activateTarget(notreverting
)
230 def next(self
, data
):
231 """Focus the next window."""
234 def previous(self
, data
):
235 """Focus the previous window."""
238 #---------------------- Window Cycling --------------------
240 class _CycleWindows(_Cycle
):
242 This is a basic cycling class for Windows.
244 An example of inheriting from and modifying this class is
245 _ClassCycleWindows, which allows users to cycle around windows of a certain
246 application name/class only.
248 This class has an underscored name because I use the singleton pattern
249 (so CycleWindows is an actual instance of this class). This doesn't have
250 to be followed, but if it isn't followed then the user will have to create
251 their own instances of your class and use that (not always a bad thing).
253 An example of using the CycleWindows singleton:
255 from cycle import CycleWindows
256 CycleWindows.INCLUDE_ICONS = 0 # I don't like cycling to icons
257 ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindows.next)
258 ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindows.previous)
261 """If this is non-zero then windows from all desktops will be included in
262 the stacking list."""
263 INCLUDE_ALL_DESKTOPS
= 0
265 """If this is non-zero then windows which are iconified on the current
266 desktop will be included in the stacking list."""
269 """If this is non-zero then windows which are iconified from all desktops
270 will be included in the stacking list."""
271 INCLUDE_ICONS_ALL_DESKTOPS
= 1
273 """If this is non-zero then windows which are on all-desktops at once will
275 INCLUDE_OMNIPRESENT
= 1
277 """A better default for window cycling than generic cycling."""
278 ACTIVATE_WHILE_CYCLING
= 1
280 """When cycling focus, raise the window chosen as well as focusing it."""
284 _Cycle
.__init
__(self
)
287 if self
.cycling
: self
.populateLists()
288 def closewindow(data
):
289 if self
.cycling
: self
.populateLists()
291 ob
.ebind(ob
.EventAction
.NewWindow
, newwindow
)
292 ob
.ebind(ob
.EventAction
.CloseWindow
, closewindow
)
294 def shouldAdd(self
, client
):
295 """Determines if a client should be added to the cycling list."""
296 curdesk
= self
.screen
.desktop()
297 desk
= client
.desktop()
299 if not client
.normal(): return 0
300 if not (client
.canFocus() or client
.focusNotify()): return 0
301 if focus
.AVOID_SKIP_TASKBAR
and client
.skipTaskbar(): return 0
304 if self
.INCLUDE_ICONS
:
305 if self
.INCLUDE_ICONS_ALL_DESKTOPS
: return 1
306 if desk
== curdesk
: return 1
308 if self
.INCLUDE_OMNIPRESENT
and desk
== 0xffffffff: return 1
309 if self
.INCLUDE_ALL_DESKTOPS
: return 1
310 if desk
== curdesk
: return 1
314 def populateItems(self
):
315 # get the list of clients, keeping iconic windows at the bottom
317 for c
in focus
._clients
:
318 if self
.shouldAdd(c
):
319 if c
.iconic(): iconic_clients
.append(c
)
320 else: self
.items
.append(c
)
321 self
.items
.extend(iconic_clients
)
323 def menuLabel(self
, client
):
324 if client
.iconic(): t
= '[' + client
.iconTitle() + ']'
325 else: t
= client
.title()
327 if self
.INCLUDE_ALL_DESKTOPS
:
329 if d
== 0xffffffff: d
= self
.screen
.desktop()
330 t
= self
.screen
.desktopName(d
) + " - " + t
334 def itemEqual(self
, client1
, client2
):
335 return client1
.window() == client2
.window()
337 def activateTarget(self
, final
):
338 """Activates (focuses and, if the user requested it, raises a window).
339 If final is true, then this is the very last window we're activating
340 and the user has finished cycling."""
342 client
= self
.items
[self
.menupos
]
343 except IndexError: return # empty list
345 # move the to client's desktop if required
346 if not (client
.iconic() or client
.desktop() == 0xffffffff or \
347 client
.desktop() == self
.screen
.desktop()):
348 self
.screen
.changeDesktop(client
.desktop())
350 # send a net_active_window message for the target
351 if final
or not client
.iconic():
352 if final
: r
= self
.RAISE_WINDOW
354 client
.focus(final
, r
)
359 CycleWindows
= _CycleWindows()
361 #---------------------- Window Cycling --------------------
363 class _CycleWindowsLinear(_CycleWindows
):
365 This class is an example of how to inherit from and make use of the
366 _CycleWindows class. This class also uses the singleton pattern.
368 An example of using the CycleWindowsLinear singleton:
370 from cycle import CycleWindowsLinear
371 CycleWindows.ALL_DESKTOPS = 1 # I want all my windows in the list
372 ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindowsLinear.next)
373 ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindowsLinear.previous)
376 """When cycling focus, raise the window chosen as well as focusing it."""
379 """If this is true, a popup window will be displayed with the options
384 _CycleWindows
.__init
__(self
)
386 def shouldAdd(self
, client
):
387 """Determines if a client should be added to the cycling list."""
388 curdesk
= self
.screen
.desktop()
389 desk
= client
.desktop()
391 if not client
.normal(): return 0
392 if not (client
.canFocus() or client
.focusNotify()): return 0
393 if focus
.AVOID_SKIP_TASKBAR
and client
.skipTaskbar(): return 0
395 if client
.iconic(): return 0
396 if self
.INCLUDE_OMNIPRESENT
and desk
== 0xffffffff: return 1
397 if self
.INCLUDE_ALL_DESKTOPS
: return 1
398 if desk
== curdesk
: return 1
402 def populateItems(self
):
403 # get the list of clients, keeping iconic windows at the bottom
405 for c
in self
.screen
.clients
:
406 if self
.shouldAdd(c
):
409 def chooseStartPos(self
):
411 t
= focus
._clients
[0]
412 for i
,c
in zip(range(len(self
.items
)), self
.items
):
413 if self
.itemEqual(c
, t
):
417 def menuLabel(self
, client
):
420 if self
.INCLUDE_ALL_DESKTOPS
:
422 if d
== 0xffffffff: d
= self
.screen
.desktop()
423 t
= self
.screen
.desktopName(d
) + " - " + t
428 CycleWindowsLinear
= _CycleWindowsLinear()
430 #----------------------- Desktop Cycling ------------------
431 class _CycleDesktops(_Cycle
):
435 from cycle import CycleDesktops
436 ob.kbind(["W-d"], ob.KeyContext.All, CycleDesktops.next)
437 ob.kbind(["W-S-d"], ob.KeyContext.All, CycleDesktops.previous)
440 def __init__(self
, name
, index
):
443 def __eq__(self
, other
):
444 return other
.index
== self
.index
447 _Cycle
.__init
__(self
)
449 def populateItems(self
):
450 for i
in range(self
.screen
.numDesktops()):
452 _CycleDesktops
.Desktop(self
.screen
.desktopName(i
), i
))
454 def menuLabel(self
, desktop
):
457 def chooseStartPos(self
):
458 self
.menupos
= self
.screen
.desktop()
460 def activateTarget(self
, final
):
461 # TODO: refactor this bit
463 desktop
= self
.items
[self
.menupos
]
464 except IndexError: return
466 self
.screen
.changeDesktop(desktop
.index
)
468 CycleDesktops
= _CycleDesktops()
470 print "Loaded cycle.py"