Remove trailing whitespace errors
[panucci.git] / src / panucci / widgets.py
blobc7aff8b21163b0e54ce94d5341e70eb1643269ee
2 # -*- coding: utf-8 -*-
4 # Additional GTK Widgets for use in Panucci
5 # Copyright (c) 2009 Thomas Perl <thpinfo.com>
7 # Panucci is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # Panucci is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
21 from __future__ import absolute_import
23 import gtk
24 import gobject
25 import pango
27 class DualActionButton(gtk.Button):
28 """
29 This button allows the user to carry out two different actions,
30 depending on how long the button is pressed. In order for this
31 to work, you need to specify two widgets that will be displayed
32 inside this DualActionButton and two callbacks (actions) that
33 will be called from the UI thread when the button is released
34 and a valid action is detected.
36 You normally create such a button like this:
38 def action_a():
39 ...
40 def action_b():
41 ...
43 DURATION = 1.0 # in seconds
44 b = DualActionButton(gtk.Label('Default Action'), action_a,
45 gtk.Label('Longpress Action'), action_b,
46 duration=DURATION)
47 window.add(b)
48 """
49 (DEFAULT, LONGPRESS) = range(2)
51 def __init__(self, default_widget, default_action,
52 longpress_widget=None, longpress_action=None,
53 duration=0.5, longpress_enabled=True):
54 gtk.Button.__init__(self)
56 default_widget.show()
57 if longpress_widget is not None:
58 longpress_widget.show()
60 if longpress_widget is None or longpress_action is None:
61 longpress_enabled = False
63 self.__default_widget = default_widget
64 self.__longpress_widget = longpress_widget
65 self.__default_action = default_action
66 self.__longpress_action = longpress_action
67 self.__duration = duration
68 self.__current_state = -1
69 self.__timeout_id = None
70 self.__pressed_state = False
71 self.__inside = False
72 self.__longpress_enabled = longpress_enabled
74 self.connect('pressed', self.__pressed)
75 self.connect('released', self.__released)
76 self.connect('enter', self.__enter)
77 self.connect('leave', self.__leave)
78 self.connect_after('expose-event', self.__expose_event)
80 self.set_state(self.DEFAULT)
82 def set_longpress_enabled(self, longpress_enabled):
83 self.__longpress_enabled = longpress_enabled
85 def get_longpress_enabled(self):
86 return self.__longpress_enabled
88 def set_duration(self, duration):
89 self.__duration = duration
91 def get_duration(self):
92 return self.__duration
94 def set_state(self, state):
95 if state != self.__current_state:
96 if not self.__longpress_enabled and state == self.LONGPRESS:
97 return False
98 self.__current_state = state
99 child = self.get_child()
100 if child is not None:
101 self.remove(child)
102 if state == self.DEFAULT:
103 self.add(self.__default_widget)
104 elif state == self.LONGPRESS:
105 self.add(self.__longpress_widget)
106 else:
107 raise ValueError('State has to be either DEFAULT or LONGPRESS.')
109 return False
111 def get_state(self):
112 return self.__current_state
114 def add_timeout(self):
115 self.remove_timeout()
116 self.__timeout_id = gobject.timeout_add(int(1000*self.__duration),
117 self.set_state, self.LONGPRESS)
119 def remove_timeout(self):
120 if self.__timeout_id is not None:
121 gobject.source_remove(self.__timeout_id)
122 self.__timeout_id = None
124 def __pressed(self, widget):
125 self.__pressed_state = True
126 self.add_timeout()
127 self.__force_redraw()
129 def __released(self, widget):
130 self.__pressed_state = False
131 self.remove_timeout()
132 state = self.get_state()
133 self.__force_redraw()
135 if self.__inside:
136 if state == self.DEFAULT:
137 self.__default_action()
138 elif state == self.LONGPRESS:
139 self.__longpress_action()
141 self.set_state(self.DEFAULT)
143 def __enter(self, widget):
144 self.__inside = True
145 self.__force_redraw()
146 if self.__pressed_state:
147 self.add_timeout()
149 def __leave(self, widget):
150 self.__inside = False
151 self.__force_redraw()
152 if self.__pressed_state:
153 self.set_state(self.DEFAULT)
154 self.remove_timeout()
156 def __force_redraw(self):
157 self.window.invalidate_rect(self.__get_draw_rect(True), False)
159 def __get_draw_rect(self, for_invalidation=False):
160 rect = self.get_allocation()
161 width, height, BORDER = 10, 5, 6
162 brx, bry = (rect.x+rect.width-BORDER-width,
163 rect.y+rect.height-BORDER-height)
165 displacement_x = self.style_get_property('child_displacement-x')
166 displacement_y = self.style_get_property('child_displacement-y')
168 if for_invalidation:
169 # For redraw (rect invalidate), update both the "normal"
170 # and the "pressed" area by simply adding the displacement
171 # to the size (to remove the "other" drawing, too
172 return gtk.gdk.Rectangle(brx, bry, width+displacement_x,
173 height+displacement_y)
174 else:
175 if self.__pressed_state and self.__inside:
176 brx += displacement_x
177 bry += displacement_y
178 return gtk.gdk.Rectangle(brx, bry, width, height)
180 def __expose_event(self, widget, event):
181 style = self.get_style()
182 rect = self.__get_draw_rect()
183 if self.__longpress_enabled:
184 style.paint_handle(self.window, gtk.STATE_NORMAL, gtk.SHADOW_NONE,
185 rect, self, 'Detail', rect.x, rect.y, rect.width,
186 rect.height, gtk.ORIENTATION_HORIZONTAL)
189 class ScrollingLabel(gtk.DrawingArea):
190 """ A simple scrolling label widget - if the text doesn't fit in the
191 container, it will scroll back and forth at a pre-determined interval.
194 LEFT, NO_CHANGE, RIGHT = [ -1, 0, 1 ]
196 def __init__( self, pango_markup, update_interval=100, pixel_jump=1,
197 delay_btwn_scrolls=0, delay_halfway=0 ):
198 """ Creates a new ScrollingLabel widget.
200 pixel_jump: the amount of pixels the text moves in one update
201 pango_markup: the markup that is displayed
202 update_interval: the amount of time (in milliseconds) between
203 scrolling updates
204 delay_btwn_scrolls: The amount of time (in milliseconds) to
205 wait after a scroll has completed and the next one begins
206 delay_halfway: The amount of time (in milliseconds) to wait
207 when the text reaches the far right.
209 Scrolling is controlled by the 'scrolling' property, it must be
210 set to True for the text to start moving. Updating of the pango
211 markup is supported by setting the 'markup' property.
213 Note: the properties can be read from to get their current status
216 gtk.DrawingArea.__init__(self)
217 self.__x_offset = 0
218 self.__x_direction = self.LEFT
219 self.__x_alignment = 0
220 self.__y_alignment = 0.5
221 self.__scrolling_timer_id = None
222 self.__scrolling_possible = False
223 self.__scrolling = False
225 # user-defined parameters (can be changed on-the-fly)
226 self.update_interval = update_interval
227 self.delay_btwn_scrolls = delay_btwn_scrolls
228 self.delay_halfway = delay_halfway
229 self.pixel_jump = pixel_jump
231 self.__graphics_context = None
232 self.__pango_layout = self.create_pango_layout('')
233 self.markup = pango_markup
235 self.connect('expose-event', self.__on_expose_event)
236 self.connect('size-allocate', lambda w,a: self.__reset_widget() )
237 self.connect('map', lambda w: self.__reset_widget() )
239 def __set_scrolling(self, value):
240 if value:
241 self.__start_scrolling()
242 else:
243 self.__stop_scrolling()
245 def set_markup(self, markup):
246 """ Set the pango markup to be displayed by the widget """
247 self.__pango_layout.set_markup(markup)
248 self.__reset_widget()
250 def get_markup( self ):
251 """ Returns the current markup contained in the widget """
252 return self.__pango_layout.get_text()
254 def set_alignment(self, x, y):
255 """ Set the text's alignment on the x axis when it's not possible to
256 scroll. The y axis setting does nothing atm. """
258 for i in x,y:
259 assert isinstance( i, (float, int) ) and 0 <= i <= 1
261 self.__x_alignment = x
262 self.__y_alignment = y
264 def get_alignment( self ):
265 """ Returns the current alignment settings (x, y). """
266 return self.__x_alignment, self.__y_alignment
268 scrolling = property( lambda s: s.__scrolling, __set_scrolling )
269 markup = property( get_markup, set_markup )
270 alignment = property( get_alignment, lambda s,i: s.set_alignment(*i) )
272 def __on_expose_event( self, widget, event ):
273 """ Draws the text on the widget. This should be called indirectly by
274 using self.queue_draw() """
276 if self.__graphics_context is None:
277 self.__graphics_context = self.window.new_gc()
279 self.window.draw_layout( self.__graphics_context,
280 int(self.__x_offset), 0, self.__pango_layout )
282 def __reset_widget( self ):
283 """ Reset the offset and find out whether or not it's even possible
284 to scroll the current text. Useful for when the window gets resized
285 or when new markup is set. """
287 # if there's no window, we can ignore this reset request because
288 # when the window is created this will be called again.
289 if self.window is None:
290 return
292 win_x, win_y = self.window.get_size()
293 lbl_x, lbl_y = self.__pango_layout.get_pixel_size()
295 # If we don't request the proper height, the label might get squashed
296 # down to 0px. (this could still happen on the horizontal axis but we
297 # aren't affected by this problem in Panucci *yet*)
298 self.set_size_request( -1, lbl_y )
300 self.__scrolling_possible = lbl_x > win_x
302 # Remove any lingering scrolling timers
303 self.__scrolling_timer = None
305 if self.__scrolling:
306 self.__start_scrolling()
308 if self.__scrolling_possible:
309 self.__x_offset = 0
310 else:
311 self.__x_offset = (win_x - lbl_x) * self.__x_alignment
313 self.queue_draw()
315 def __scroll_once(self, halfway_callback=None, finished_callback=None):
316 """ Moves the text by 'self.pixel_jump' every time this is called.
317 Returns False when the text has completed one scrolling period and
318 'finished_callback' is run.
319 Returns False halfway through (when the text is all the way to the
320 right) if 'halfway_callback' is set after running the callback.
323 # prevent an accidental scroll (there's a race-condition somewhere
324 # but I'm too lazy to find out where).
325 if not self.__scrolling_possible:
326 return False
328 rtn = True
329 win_x, win_y = self.window.get_size()
330 lbl_x, lbl_y = self.__pango_layout.get_pixel_size()
332 self.__x_offset += self.pixel_jump * self.__x_direction
334 if self.__x_direction == self.LEFT and self.__x_offset < win_x - lbl_x:
335 self.__x_direction = self.RIGHT
336 # set the offset to the maximum left bound otherwise some
337 # characters might get chopped off if using large pixel_jump's
338 self.__x_offset = win_x - lbl_x
340 if halfway_callback is not None:
341 halfway_callback()
342 rtn = False
344 elif self.__x_direction == self.RIGHT and self.__x_offset > 0:
345 # end of scroll period; reset direction
346 self.__x_direction = self.LEFT
347 # don't allow the offset to be greater than 0
348 self.__x_offset = 0
349 if finished_callback is not None:
350 finished_callback()
352 # return False because at this point we've completed one scroll
353 # period; this kills off any timers running this function
354 rtn = False
356 self.queue_draw()
357 return rtn
359 def __scroll_wait_callback(self, delay):
360 # Waits 'delay', then calls the scroll function
361 self.__scrolling_timer = gobject.timeout_add( delay, self.__scroll )
363 def __scroll(self):
364 """ When called, scrolls the text back and forth indefinitely while
365 waiting self.delay_btwn_scrolls between each scroll period. """
367 if self.__scrolling_possible:
368 self.__scrolling_timer = gobject.timeout_add(
369 self.update_interval, self.__scroll_once,
370 lambda: self.__scroll_wait_callback(self.delay_halfway),
371 lambda: self.__scroll_wait_callback(self.delay_btwn_scrolls) )
372 else:
373 self.__scrolling_timer = None
375 def __scrolling_timer_get(self):
376 return self.__scrolling_timer_id
378 def __scrolling_timer_set(self, val):
379 """ When changing the scrolling timer id, make sure that only one
380 timer is running at a time. This removes the previous timer
381 before adding a new one. """
383 if self.__scrolling_timer_id is not None:
384 gobject.source_remove( self.__scrolling_timer_id )
385 self.__scrolling_timer_id = None
387 self.__scrolling_timer_id = val
389 __scrolling_timer = property( __scrolling_timer_get, __scrolling_timer_set )
391 def __start_scrolling(self):
392 """ Make the text start scrolling """
393 self.__scrolling = True
394 if self.__scrolling_timer is None and self.__scrolling_possible:
395 self.__scroll()
397 def __stop_scrolling(self):
398 """ Make the text stop scrolling """
399 self.__scrolling = False
400 self.__scrolling_timer = None
403 if __name__ == '__main__':
404 w = gtk.Window()
405 w.set_geometry_hints(w, 100, 20)
406 hb = gtk.HBox(homogeneous=True, spacing=1)
407 w.add(hb)
409 # scroll 7 pixels per 0.2 seconds, wait halfway for 0.5 seconds and finally
410 # wait 2 seconds after a complete scroll. wash, rinse, repeat.
411 l = ScrollingLabel('N/A', 200, 7, 2000, 500)
412 l.markup = 'some random text 1234'
413 hb.pack_end(l)
415 btn = gtk.Button('start/stop')
416 hb.pack_start(btn)
417 btn.connect('clicked', lambda w,e: setattr(e,'scrolling', not e.scrolling), l)
419 w.connect('destroy', gtk.main_quit)
420 w.show_all()
421 gtk.main()