]> Dogcows Code - chaz/openbox/blob - scripts/cycle.py
add a dispatched event for interactively moving a window.
[chaz/openbox] / 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"
This page took 0.0568959999999999 seconds and 4 git commands to generate.