]> icculus.org git repositories - dana/openbox.git/blob - scripts/cycle.py
80 cols
[dana/openbox.git] / 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         self.cycle(data, 1)
234         
235     def previous(self, data):
236         """Focus the previous window."""
237         self.cycle(data, 0)
238
239 #---------------------- Window Cycling --------------------
240 import focus
241 class _CycleWindows(_Cycle):
242     """
243     This is a basic cycling class for Windows.
244
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.
248
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).
253
254     An example of using the CycleWindows singleton:
255
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)
260     """
261
262     """If this is non-zero then windows from all desktops will be included in
263        the stacking list."""
264     INCLUDE_ALL_DESKTOPS = 0
265
266     """If this is non-zero then windows which are iconified on the current 
267        desktop will be included in the stacking list."""
268     INCLUDE_ICONS = 1
269
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
273
274     """If this is non-zero then windows which are on all-desktops at once will
275        be included."""
276     INCLUDE_OMNIPRESENT = 1
277
278     """A better default for window cycling than generic cycling."""
279     ACTIVATE_WHILE_CYCLING = 1
280
281     """When cycling focus, raise the window chosen as well as focusing it."""
282     RAISE_WINDOW = 1
283
284     def __init__(self):
285         _Cycle.__init__(self)
286
287         def newwindow(data):
288             if self.cycling: self.populateLists()
289         def closewindow(data):
290             if self.cycling: self.populateLists()
291
292         ob.ebind(ob.EventAction.NewWindow, newwindow)
293         ob.ebind(ob.EventAction.CloseWindow, closewindow)
294
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()
299
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
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.desktopName(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             root = self.screeninfo.rootWindow()
350             ob.send_client_msg(root, otk.atoms.net_current_desktop,
351                                root, client.desktop())
352         
353         # send a net_active_window message for the target
354         if final or not client.iconic():
355             if final: r = self.RAISE_WINDOW
356             else: r = 0
357             ob.send_client_msg(self.screeninfo.rootWindow(),
358                                otk.atoms.openbox_active_window,
359                                client.window(), final, r)
360             if not final:
361                 focus._skip += 1
362
363 # The singleton.
364 CycleWindows = _CycleWindows()
365
366 #---------------------- Window Cycling --------------------
367 import focus
368 class _CycleWindowsLinear(_CycleWindows):
369     """
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.
372
373     An example of using the CycleWindowsLinear singleton:
374
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)
379     """
380
381     """When cycling focus, raise the window chosen as well as focusing it."""
382     RAISE_WINDOW = 0
383
384     """If this is true, a popup window will be displayed with the options
385        while cycling."""
386     SHOW_POPUP = 0
387
388     def __init__(self):
389         _CycleWindows.__init__(self)
390
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()
395
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
399
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
404
405         return 0
406
407     def populateItems(self):
408         # get the list of clients, keeping iconic windows at the bottom
409         iconic_clients = []
410         for c in self.screen.clients:
411             if self.shouldAdd(c):
412                 self.items.append(c)
413
414     def chooseStartPos(self):
415         if focus._clients:
416             t = focus._clients[0]
417             for i,c in zip(range(len(self.items)), self.items):
418                 if self.itemEqual(c, t):
419                     self.menupos = i
420                     break
421         
422     def menuLabel(self, client):
423         t = client.title()
424
425         if self.INCLUDE_ALL_DESKTOPS:
426             d = client.desktop()
427             if d == 0xffffffff: d = self.screen.desktop()
428             t = self.screen.desktopName(d) + " - " + t
429
430         return t
431     
432 # The singleton.
433 CycleWindowsLinear = _CycleWindowsLinear()
434
435 #----------------------- Desktop Cycling ------------------
436 class _CycleDesktops(_Cycle):
437     """
438     Example of usage:
439
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)
443     """
444     class Desktop:
445         def __init__(self, name, index):
446             self.name = name
447             self.index = index
448         def __eq__(self, other):
449             return other.index == self.index
450
451     def __init__(self):
452         _Cycle.__init__(self)
453
454     def populateItems(self):
455         for i in range(self.screen.numDesktops()):
456             self.items.append(
457                 _CycleDesktops.Desktop(self.screen.desktopName(i), i))
458
459     def menuLabel(self, desktop):
460         return desktop.name
461
462     def chooseStartPos(self):
463         self.menupos = self.screen.desktop()
464
465     def activateTarget(self, final):
466         # TODO: refactor this bit
467         try:
468             desktop = self.items[self.menupos]
469         except IndexError: return
470
471         root = self.screeninfo.rootWindow()
472         ob.send_client_msg(root, otk.atoms.net_current_desktop,
473                            root, desktop.index)
474
475 CycleDesktops = _CycleDesktops()
476
477 print "Loaded cycle.py"