]> icculus.org git repositories - dana/openbox.git/blob - scripts/cycle.py
restart and catch errors appropriately
[dana/openbox.git] / scripts / cycle.py
1 import ob, otk, config
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.screen.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
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."""
153         pass
154
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).
159            """
160
161         initial = 0
162
163         if not self.cycling:
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)
169
170             self.cycling = 1
171             self.state = data.state
172             self.menupos = 0
173
174             self.setDataInfo(data)
175
176             self.createPopup()
177             self.items = [] # so it doesnt try start partway through the list
178             self.populateLists()
179
180             self.chooseStartPos()
181             self.initpos = self.menupos
182
183             initial = 1
184         
185         if not self.items: return # don't bother doing anything
186         
187         self.menuwidgets[self.menupos].setHighlighted(0)
188
189         if initial and not self.START_WITH_NEXT:
190             pass
191         else:
192             if forward:
193                 self.menupos += 1
194             else:
195                 self.menupos -= 1
196         # wrap around
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
202
203     def grabfunc(self, data):
204         """A callback method that grabs away all keystrokes so that navigating 
205            the cycling menu is possible."""
206         done = 0
207         notreverting = 1
208         # have all the modifiers this started with been released?
209         if not self.state & data.state:
210             done = 1
211         elif data.action == ob.KeyAction.Press:
212             # has Escape been pressed?
213             if data.key == "Escape":
214                 done = 1
215                 notreverting = 0
216                 # revert
217                 self.menupos = self.initpos
218             # has Enter been pressed?
219             elif data.key == "Return":
220                 done = 1
221
222         if done:
223             # activate, and deiconify/unshade/raise
224             self.activateTarget(notreverting)
225             self.destroyPopup()
226             self.cycling = 0
227             ob.kungrab()
228             ob.mungrab()
229
230     def next(self, data):
231         """Focus the next window."""
232         self.cycle(data, 1)
233         
234     def previous(self, data):
235         """Focus the previous window."""
236         self.cycle(data, 0)
237
238 #---------------------- Window Cycling --------------------
239 import focus
240 class _CycleWindows(_Cycle):
241     """
242     This is a basic cycling class for Windows.
243
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.
247
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).
252
253     An example of using the CycleWindows singleton:
254
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)
259     """
260
261     """If this is non-zero then windows from all desktops will be included in
262        the stacking list."""
263     INCLUDE_ALL_DESKTOPS = 0
264
265     """If this is non-zero then windows which are iconified on the current 
266        desktop will be included in the stacking list."""
267     INCLUDE_ICONS = 1
268
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
272
273     """If this is non-zero then windows which are on all-desktops at once will
274        be included."""
275     INCLUDE_OMNIPRESENT = 1
276
277     """A better default for window cycling than generic cycling."""
278     ACTIVATE_WHILE_CYCLING = 1
279
280     """When cycling focus, raise the window chosen as well as focusing it."""
281     RAISE_WINDOW = 1
282
283     def __init__(self):
284         _Cycle.__init__(self)
285
286         def newwindow(data):
287             if self.cycling: self.populateLists()
288         def closewindow(data):
289             if self.cycling: self.populateLists()
290
291         ob.ebind(ob.EventAction.NewWindow, newwindow)
292         ob.ebind(ob.EventAction.CloseWindow, closewindow)
293
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()
298
299         if not client.normal(): return 0
300         if not (client.canFocus() or client.focusNotify()): return 0
301         if config.get('focus', 'avoid_skip_taskbar') and client.skipTaskbar():
302                       return 0
303
304         if client.iconic():
305             if self.INCLUDE_ICONS:
306                 if self.INCLUDE_ICONS_ALL_DESKTOPS: return 1
307                 if desk == curdesk: return 1
308             return 0
309         if self.INCLUDE_OMNIPRESENT and desk == 0xffffffff: return 1
310         if self.INCLUDE_ALL_DESKTOPS: return 1
311         if desk == curdesk: return 1
312
313         return 0
314
315     def populateItems(self):
316         # get the list of clients, keeping iconic windows at the bottom
317         iconic_clients = []
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)
323
324     def menuLabel(self, client):
325         if client.iconic(): t = '[' + client.iconTitle() + ']'
326         else: t = client.title()
327
328         if self.INCLUDE_ALL_DESKTOPS:
329             d = client.desktop()
330             if d == 0xffffffff: d = self.screen.desktop()
331             t = self.screen.desktopNames()[d] + " - " + t
332
333         return t
334     
335     def itemEqual(self, client1, client2):
336         return client1.window() == client2.window()
337
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."""
342         try:
343             client = self.items[self.menupos]
344         except IndexError: return # empty list
345
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             self.screen.changeDesktop(client.desktop())
350         
351         # send a net_active_window message for the target
352         if final or not client.iconic():
353             if final: r = self.RAISE_WINDOW
354             else: r = 0
355             client.focus(final, r)
356             if not final:
357                 focus._skip += 1
358
359 # The singleton.
360 CycleWindows = _CycleWindows()
361
362 #---------------------- Window Cycling --------------------
363 import focus
364 class _CycleWindowsLinear(_CycleWindows):
365     """
366     This class is an example of how to inherit from and make use of the
367     _CycleWindows class.  This class also uses the singleton pattern.
368
369     An example of using the CycleWindowsLinear singleton:
370
371         from cycle import CycleWindowsLinear
372         CycleWindows.ALL_DESKTOPS = 1  # I want all my windows in the list
373         ob.kbind(["A-Tab"], ob.KeyContext.All, CycleWindowsLinear.next)
374         ob.kbind(["A-S-Tab"], ob.KeyContext.All, CycleWindowsLinear.previous)
375     """
376
377     """When cycling focus, raise the window chosen as well as focusing it."""
378     RAISE_WINDOW = 0
379
380     """If this is true, a popup window will be displayed with the options
381        while cycling."""
382     SHOW_POPUP = 0
383
384     def __init__(self):
385         _CycleWindows.__init__(self)
386
387     def shouldAdd(self, client):
388         """Determines if a client should be added to the cycling list."""
389         curdesk = self.screen.desktop()
390         desk = client.desktop()
391
392         if not client.normal(): return 0
393         if not (client.canFocus() or client.focusNotify()): return 0
394         if config.get('focus', 'avoid_skip_taskbar') and client.skipTaskbar():
395             return 0
396
397         if client.iconic(): return 0
398         if self.INCLUDE_OMNIPRESENT and desk == 0xffffffff: return 1
399         if self.INCLUDE_ALL_DESKTOPS: return 1
400         if desk == curdesk: return 1
401
402         return 0
403
404     def populateItems(self):
405         # get the list of clients, keeping iconic windows at the bottom
406         iconic_clients = []
407         for c in self.screen.clients:
408             if self.shouldAdd(c):
409                 self.items.append(c)
410
411     def chooseStartPos(self):
412         if focus._clients:
413             t = focus._clients[0]
414             for i,c in zip(range(len(self.items)), self.items):
415                 if self.itemEqual(c, t):
416                     self.menupos = i
417                     break
418         
419     def menuLabel(self, client):
420         t = client.title()
421
422         if self.INCLUDE_ALL_DESKTOPS:
423             d = client.desktop()
424             if d == 0xffffffff: d = self.screen.desktop()
425             t = self.screen.desktopNames()[d] + " - " + t
426
427         return t
428     
429 # The singleton.
430 CycleWindowsLinear = _CycleWindowsLinear()
431
432 #----------------------- Desktop Cycling ------------------
433 class _CycleDesktops(_Cycle):
434     """
435     Example of usage:
436
437        from cycle import CycleDesktops
438        ob.kbind(["W-d"], ob.KeyContext.All, CycleDesktops.next)
439        ob.kbind(["W-S-d"], ob.KeyContext.All, CycleDesktops.previous)
440     """
441     class Desktop:
442         def __init__(self, name, index):
443             self.name = name
444             self.index = index
445         def __eq__(self, other):
446             return other.index == self.index
447
448     def __init__(self):
449         _Cycle.__init__(self)
450
451     def populateItems(self):
452         names = self.screen.desktopNames()
453         num = self.screen.numDesktops()
454         for n, i in zip(names[:num], range(num)):
455             self.items.append(_CycleDesktops.Desktop(n, i))
456
457     def menuLabel(self, desktop):
458         return desktop.name
459
460     def chooseStartPos(self):
461         self.menupos = self.screen.desktop()
462
463     def activateTarget(self, final):
464         # TODO: refactor this bit
465         try:
466             desktop = self.items[self.menupos]
467         except IndexError: return
468
469         self.screen.changeDesktop(desktop.index)
470
471 CycleDesktops = _CycleDesktops()
472
473 print "Loaded cycle.py"