]> icculus.org git repositories - mikachu/openbox.git/blob - scripts/cycle.py
keep track of if the move is the final move or not, and pass it along
[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.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 focus.AVOID_SKIP_TASKBAR and client.skipTaskbar(): return 0
302
303         if client.iconic():
304             if self.INCLUDE_ICONS:
305                 if self.INCLUDE_ICONS_ALL_DESKTOPS: return 1
306                 if desk == curdesk: return 1
307             return 0
308         if self.INCLUDE_OMNIPRESENT and desk == 0xffffffff: return 1
309         if self.INCLUDE_ALL_DESKTOPS: return 1
310         if desk == curdesk: return 1
311
312         return 0
313
314     def populateItems(self):
315         # get the list of clients, keeping iconic windows at the bottom
316         iconic_clients = []
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)
322
323     def menuLabel(self, client):
324         if client.iconic(): t = '[' + client.iconTitle() + ']'
325         else: t = client.title()
326
327         if self.INCLUDE_ALL_DESKTOPS:
328             d = client.desktop()
329             if d == 0xffffffff: d = self.screen.desktop()
330             t = self.screen.desktopName(d) + " - " + t
331
332         return t
333     
334     def itemEqual(self, client1, client2):
335         return client1.window() == client2.window()
336
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."""
341         try:
342             client = self.items[self.menupos]
343         except IndexError: return # empty list
344
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())
349         
350         # send a net_active_window message for the target
351         if final or not client.iconic():
352             if final: r = self.RAISE_WINDOW
353             else: r = 0
354             client.focus(final, r)
355             if not final:
356                 focus._skip += 1
357
358 # The singleton.
359 CycleWindows = _CycleWindows()
360
361 #---------------------- Window Cycling --------------------
362 import focus
363 class _CycleWindowsLinear(_CycleWindows):
364     """
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.
367
368     An example of using the CycleWindowsLinear singleton:
369
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)
374     """
375
376     """When cycling focus, raise the window chosen as well as focusing it."""
377     RAISE_WINDOW = 0
378
379     """If this is true, a popup window will be displayed with the options
380        while cycling."""
381     SHOW_POPUP = 0
382
383     def __init__(self):
384         _CycleWindows.__init__(self)
385
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()
390
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
394
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
399
400         return 0
401
402     def populateItems(self):
403         # get the list of clients, keeping iconic windows at the bottom
404         iconic_clients = []
405         for c in self.screen.clients:
406             if self.shouldAdd(c):
407                 self.items.append(c)
408
409     def chooseStartPos(self):
410         if focus._clients:
411             t = focus._clients[0]
412             for i,c in zip(range(len(self.items)), self.items):
413                 if self.itemEqual(c, t):
414                     self.menupos = i
415                     break
416         
417     def menuLabel(self, client):
418         t = client.title()
419
420         if self.INCLUDE_ALL_DESKTOPS:
421             d = client.desktop()
422             if d == 0xffffffff: d = self.screen.desktop()
423             t = self.screen.desktopName(d) + " - " + t
424
425         return t
426     
427 # The singleton.
428 CycleWindowsLinear = _CycleWindowsLinear()
429
430 #----------------------- Desktop Cycling ------------------
431 class _CycleDesktops(_Cycle):
432     """
433     Example of usage:
434
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)
438     """
439     class Desktop:
440         def __init__(self, name, index):
441             self.name = name
442             self.index = index
443         def __eq__(self, other):
444             return other.index == self.index
445
446     def __init__(self):
447         _Cycle.__init__(self)
448
449     def populateItems(self):
450         for i in range(self.screen.numDesktops()):
451             self.items.append(
452                 _CycleDesktops.Desktop(self.screen.desktopName(i), i))
453
454     def menuLabel(self, desktop):
455         return desktop.name
456
457     def chooseStartPos(self):
458         self.menupos = self.screen.desktop()
459
460     def activateTarget(self, final):
461         # TODO: refactor this bit
462         try:
463             desktop = self.items[self.menupos]
464         except IndexError: return
465
466         self.screen.changeDesktop(desktop.index)
467
468 CycleDesktops = _CycleDesktops()
469
470 print "Loaded cycle.py"