]> icculus.org git repositories - mikachu/openbox.git/blob - scripts/cycle.py
defualt START_WITH_NEXT to true for desktops too
[mikachu/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         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"