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
.screeninfo
.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
)
148 self
.screeninfo
= otk
.display
.screenInfo(data
.screen
)
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."""
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).
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
)
172 self
.state
= data
.state
175 self
.setDataInfo(data
)
178 self
.items
= [] # so it doesnt try start partway through the list
181 self
.chooseStartPos()
182 self
.initpos
= self
.menupos
186 if not self
.items
: return # don't bother doing anything
188 self
.menuwidgets
[self
.menupos
].setHighlighted(0)
190 if initial
and not self
.START_WITH_NEXT
:
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
204 def grabfunc(self
, data
):
205 """A callback method that grabs away all keystrokes so that navigating
206 the cycling menu is possible."""
209 # have all the modifiers this started with been released?
210 if not self
.state
& data
.state
:
212 elif data
.action
== ob
.KeyAction
.Press
:
213 # has Escape been pressed?
214 if data
.key
== "Escape":
218 self
.menupos
= self
.initpos
219 # has Enter been pressed?
220 elif data
.key
== "Return":
224 # activate, and deiconify/unshade/raise
225 self
.activateTarget(notreverting
)
231 def next(self
, data
):
232 """Focus the next window."""
235 def previous(self
, data
):
236 """Focus the previous window."""
239 #---------------------- Window Cycling --------------------
241 class _CycleWindows(_Cycle
):
243 This is a basic cycling class for Windows.
245 An example of inheriting from and modifying this class is
246 _ClassCycleWindows, which allows users to cycle around windows of a certain
247 application name/class only.
249 This class has an underscored name because I use the singleton pattern
250 (so CycleWindows is an actual instance of this class). This doesn't have
251 to be followed, but if it isn't followed then the user will have to create
252 their own instances of your class and use that (not always a bad thing).
254 An example of using the CycleWindows singleton:
256 from cycle import CycleWindows
257 CycleWindows.INCLUDE_ICONS = 0 # I don't like cycling to icons
258 ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindows.next)
259 ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindows.previous)
262 """If this is non-zero then windows from all desktops will be included in
263 the stacking list."""
264 INCLUDE_ALL_DESKTOPS
= 0
266 """If this is non-zero then windows which are iconified on the current
267 desktop will be included in the stacking list."""
270 """If this is non-zero then windows which are iconified from all desktops
271 will be included in the stacking list."""
272 INCLUDE_ICONS_ALL_DESKTOPS
= 1
274 """If this is non-zero then windows which are on all-desktops at once will
276 INCLUDE_OMNIPRESENT
= 1
278 """A better default for window cycling than generic cycling."""
279 ACTIVATE_WHILE_CYCLING
= 1
281 """When cycling focus, raise the window chosen as well as focusing it."""
285 _Cycle
.__init
__(self
)
288 if self
.cycling
: self
.populateLists()
289 def closewindow(data
):
290 if self
.cycling
: self
.populateLists()
292 ob
.ebind(ob
.EventAction
.NewWindow
, newwindow
)
293 ob
.ebind(ob
.EventAction
.CloseWindow
, closewindow
)
295 def shouldAdd(self
, client
):
296 """Determines if a client should be added to the cycling list."""
297 curdesk
= self
.screen
.desktop()
298 desk
= client
.desktop()
300 if not client
.normal(): return 0
301 if not (client
.canFocus() or client
.focusNotify()): return 0
302 if focus
.AVOID_SKIP_TASKBAR
and client
.skipTaskbar(): return 0
305 if self
.INCLUDE_ICONS
:
306 if self
.INCLUDE_ICONS_ALL_DESKTOPS
: return 1
307 if desk
== curdesk
: return 1
309 if self
.INCLUDE_OMNIPRESENT
and desk
== 0xffffffff: return 1
310 if self
.INCLUDE_ALL_DESKTOPS
: return 1
311 if desk
== curdesk
: return 1
315 def populateItems(self
):
316 # get the list of clients, keeping iconic windows at the bottom
318 for c
in focus
._clients
:
319 if self
.shouldAdd(c
):
320 if c
.iconic(): iconic_clients
.append(c
)
321 else: self
.items
.append(c
)
322 self
.items
.extend(iconic_clients
)
324 def menuLabel(self
, client
):
325 if client
.iconic(): t
= '[' + client
.iconTitle() + ']'
326 else: t
= client
.title()
328 if self
.INCLUDE_ALL_DESKTOPS
:
330 if d
== 0xffffffff: d
= self
.screen
.desktop()
331 t
= self
.screen
.desktopName(d
) + " - " + t
335 def itemEqual(self
, client1
, client2
):
336 return client1
.window() == client2
.window()
338 def activateTarget(self
, final
):
339 """Activates (focuses and, if the user requested it, raises a window).
340 If final is true, then this is the very last window we're activating
341 and the user has finished cycling."""
343 client
= self
.items
[self
.menupos
]
344 except IndexError: return # empty list
346 # move the to client's desktop if required
347 if not (client
.iconic() or client
.desktop() == 0xffffffff or \
348 client
.desktop() == self
.screen
.desktop()):
349 root
= self
.screeninfo
.rootWindow()
350 ob
.send_client_msg(root
, otk
.atoms
.net_current_desktop
,
351 root
, client
.desktop())
353 # send a net_active_window message for the target
354 if final
or not client
.iconic():
355 if final
: r
= self
.RAISE_WINDOW
357 ob
.send_client_msg(self
.screeninfo
.rootWindow(),
358 otk
.atoms
.openbox_active_window
,
359 client
.window(), final
, r
)
364 CycleWindows
= _CycleWindows()
366 #---------------------- Window Cycling --------------------
368 class _CycleWindowsLinear(_CycleWindows
):
370 This class is an example of how to inherit from and make use of the
371 _CycleWindows class. This class also uses the singleton pattern.
373 An example of using the CycleWindowsLinear singleton:
375 from cycle import CycleWindowsLinear
376 CycleWindows.ALL_DESKTOPS = 1 # I want all my windows in the list
377 ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindowsLinear.next)
378 ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindowsLinear.previous)
381 """When cycling focus, raise the window chosen as well as focusing it."""
384 """If this is true, a popup window will be displayed with the options
389 _CycleWindows
.__init
__(self
)
391 def shouldAdd(self
, client
):
392 """Determines if a client should be added to the cycling list."""
393 curdesk
= self
.screen
.desktop()
394 desk
= client
.desktop()
396 if not client
.normal(): return 0
397 if not (client
.canFocus() or client
.focusNotify()): return 0
398 if focus
.AVOID_SKIP_TASKBAR
and client
.skipTaskbar(): return 0
400 if client
.iconic(): return 0
401 if self
.INCLUDE_OMNIPRESENT
and desk
== 0xffffffff: return 1
402 if self
.INCLUDE_ALL_DESKTOPS
: return 1
403 if desk
== curdesk
: return 1
407 def populateItems(self
):
408 # get the list of clients, keeping iconic windows at the bottom
410 for c
in self
.screen
.clients
:
411 if self
.shouldAdd(c
):
414 def chooseStartPos(self
):
416 t
= focus
._clients
[0]
417 for i
,c
in zip(range(len(self
.items
)), self
.items
):
418 if self
.itemEqual(c
, t
):
422 def menuLabel(self
, client
):
425 if self
.INCLUDE_ALL_DESKTOPS
:
427 if d
== 0xffffffff: d
= self
.screen
.desktop()
428 t
= self
.screen
.desktopName(d
) + " - " + t
433 CycleWindowsLinear
= _CycleWindowsLinear()
435 #----------------------- Desktop Cycling ------------------
436 class _CycleDesktops(_Cycle
):
440 from cycle import CycleDesktops
441 ob.kbind(["W-d"], ob.KeyContext.All, CycleDesktops.next)
442 ob.kbind(["W-S-d"], ob.KeyContext.All, CycleDesktops.previous)
445 def __init__(self
, name
, index
):
448 def __eq__(self
, other
):
449 return other
.index
== self
.index
452 _Cycle
.__init
__(self
)
454 def populateItems(self
):
455 for i
in range(self
.screen
.numDesktops()):
457 _CycleDesktops
.Desktop(self
.screen
.desktopName(i
), i
))
459 def menuLabel(self
, desktop
):
462 def chooseStartPos(self
):
463 self
.menupos
= self
.screen
.desktop()
465 def activateTarget(self
, final
):
466 # TODO: refactor this bit
468 desktop
= self
.items
[self
.menupos
]
469 except IndexError: return
471 root
= self
.screeninfo
.rootWindow()
472 ob
.send_client_msg(root
, otk
.atoms
.net_current_desktop
,
475 CycleDesktops
= _CycleDesktops()
477 print "Loaded cycle.py"