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/>.
25 class DualActionButton(gtk
.Button
):
27 This button allows the user to carry out two different actions,
28 depending on how long the button is pressed. In order for this
29 to work, you need to specify two widgets that will be displayed
30 inside this DualActionButton and two callbacks (actions) that
31 will be called from the UI thread when the button is released
32 and a valid action is detected.
34 You normally create such a button like this:
41 DURATION = 1.0 # in seconds
42 b = DualActionButton(gtk.Label('Default Action'), action_a,
43 gtk.Label('Longpress Action'), action_b,
47 (DEFAULT
, LONGPRESS
) = range(2)
49 def __init__(self
, default_widget
, default_action
,
50 longpress_widget
=None, longpress_action
=None,
51 duration
=0.5, longpress_enabled
=True):
52 gtk
.Button
.__init
__(self
)
55 if longpress_widget
is not None:
56 longpress_widget
.show()
58 if longpress_widget
is None or longpress_action
is None:
59 longpress_enabled
= False
61 self
.__default
_widget
= default_widget
62 self
.__longpress
_widget
= longpress_widget
63 self
.__default
_action
= default_action
64 self
.__longpress
_action
= longpress_action
65 self
.__duration
= duration
66 self
.__current
_state
= -1
67 self
.__timeout
_id
= None
68 self
.__pressed
_state
= False
70 self
.__longpress
_enabled
= longpress_enabled
72 self
.connect('pressed', self
.__pressed
)
73 self
.connect('released', self
.__released
)
74 self
.connect('enter', self
.__enter
)
75 self
.connect('leave', self
.__leave
)
76 self
.connect_after('expose-event', self
.__expose
_event
)
78 self
.set_state(self
.DEFAULT
)
80 def set_longpress_enabled(self
, longpress_enabled
):
81 self
.__longpress
_enabled
= longpress_enabled
83 def get_longpress_enabled(self
):
84 return self
.__longpress
_enabled
86 def set_duration(self
, duration
):
87 self
.__duration
= duration
89 def get_duration(self
):
90 return self
.__duration
92 def set_state(self
, state
):
93 if state
!= self
.__current
_state
:
94 if not self
.__longpress
_enabled
and state
== self
.LONGPRESS
:
96 self
.__current
_state
= state
97 child
= self
.get_child()
100 if state
== self
.DEFAULT
:
101 self
.add(self
.__default
_widget
)
102 elif state
== self
.LONGPRESS
:
103 self
.add(self
.__longpress
_widget
)
105 raise ValueError('State has to be either DEFAULT or LONGPRESS.')
110 return self
.__current
_state
112 def add_timeout(self
):
113 self
.remove_timeout()
114 self
.__timeout
_id
= gobject
.timeout_add(int(1000*self
.__duration
),
115 self
.set_state
, self
.LONGPRESS
)
117 def remove_timeout(self
):
118 if self
.__timeout
_id
is not None:
119 gobject
.source_remove(self
.__timeout
_id
)
120 self
.__timeout
_id
= None
122 def __pressed(self
, widget
):
123 self
.__pressed
_state
= True
125 self
.__force
_redraw
()
127 def __released(self
, widget
):
128 self
.__pressed
_state
= False
129 self
.remove_timeout()
130 state
= self
.get_state()
131 self
.__force
_redraw
()
134 if state
== self
.DEFAULT
:
135 self
.__default
_action
()
136 elif state
== self
.LONGPRESS
:
137 self
.__longpress
_action
()
139 self
.set_state(self
.DEFAULT
)
141 def __enter(self
, widget
):
143 self
.__force
_redraw
()
144 if self
.__pressed
_state
:
147 def __leave(self
, widget
):
148 self
.__inside
= False
149 self
.__force
_redraw
()
150 if self
.__pressed
_state
:
151 self
.set_state(self
.DEFAULT
)
152 self
.remove_timeout()
154 def __force_redraw(self
):
155 self
.window
.invalidate_rect(self
.__get
_draw
_rect
(True), False)
157 def __get_draw_rect(self
, for_invalidation
=False):
158 rect
= self
.get_allocation()
159 width
, height
, BORDER
= 10, 5, 6
160 brx
, bry
= (rect
.x
+rect
.width
-BORDER
-width
,
161 rect
.y
+rect
.height
-BORDER
-height
)
163 displacement_x
= self
.style_get_property('child_displacement-x')
164 displacement_y
= self
.style_get_property('child_displacement-y')
167 # For redraw (rect invalidate), update both the "normal"
168 # and the "pressed" area by simply adding the displacement
169 # to the size (to remove the "other" drawing, too
170 return gtk
.gdk
.Rectangle(brx
, bry
, width
+displacement_x
,
171 height
+displacement_y
)
173 if self
.__pressed
_state
and self
.__inside
:
174 brx
+= displacement_x
175 bry
+= displacement_y
176 return gtk
.gdk
.Rectangle(brx
, bry
, width
, height
)
178 def __expose_event(self
, widget
, event
):
179 style
= self
.get_style()
180 rect
= self
.__get
_draw
_rect
()
181 if self
.__longpress
_enabled
:
182 style
.paint_handle(self
.window
, gtk
.STATE_NORMAL
, gtk
.SHADOW_NONE
,
183 rect
, self
, 'Detail', rect
.x
, rect
.y
, rect
.width
,
184 rect
.height
, gtk
.ORIENTATION_HORIZONTAL
)
187 class ScrollingLabel(gtk
.DrawingArea
):
188 """ A simple scrolling label widget - if the text doesn't fit in the
189 container, it will scroll back and forth at a pre-determined interval.
192 LEFT
, NO_CHANGE
, RIGHT
= [ -1, 0, 1 ]
194 def __init__( self
, pango_markup
, update_interval
=100, pixel_jump
=1,
195 delay_btwn_scrolls
=0, delay_halfway
=0 ):
196 """ Creates a new ScrollingLabel widget.
198 pixel_jump: the amount of pixels the text moves in one update
199 pango_markup: the markup that is displayed
200 update_interval: the amount of time (in milliseconds) between
202 delay_btwn_scrolls: The amount of time (in milliseconds) to
203 wait after a scroll has completed and the next one begins
204 delay_halfway: The amount of time (in milliseconds) to wait
205 when the text reaches the far right.
207 Scrolling is controlled by the 'scrolling' property, it must be
208 set to True for the text to start moving. Updating of the pango
209 markup is supported by setting the 'markup' property.
211 Note: the properties can be read from to get their current status
214 gtk
.DrawingArea
.__init
__(self
)
216 self
.__x
_direction
= self
.LEFT
217 self
.__x
_alignment
= 0
218 self
.__y
_alignment
= 0.5
219 self
.__scrolling
_timer
_id
= None
220 self
.__scrolling
_possible
= False
221 self
.__scrolling
= False
223 # user-defined parameters (can be changed on-the-fly)
224 self
.update_interval
= update_interval
225 self
.delay_btwn_scrolls
= delay_btwn_scrolls
226 self
.delay_halfway
= delay_halfway
227 self
.pixel_jump
= pixel_jump
229 self
.__graphics
_context
= None
230 self
.__pango
_layout
= self
.create_pango_layout('')
231 self
.markup
= pango_markup
233 self
.connect('expose-event', self
.__on
_expose
_event
)
234 self
.connect('size-allocate', lambda w
,a
: self
.__reset
_widget
() )
235 self
.connect('map', lambda w
: self
.__reset
_widget
() )
237 def __set_scrolling(self
, value
):
239 self
.__start
_scrolling
()
241 self
.__stop
_scrolling
()
243 def set_markup(self
, markup
):
244 """ Set the pango markup to be displayed by the widget """
245 self
.__pango
_layout
.set_markup(markup
)
246 self
.__reset
_widget
()
248 def get_markup( self
):
249 """ Returns the current markup contained in the widget """
250 return self
.__pango
_layout
.get_text()
252 def set_alignment(self
, x
, y
):
253 """ Set the text's alignment on the x axis when it's not possible to
254 scroll. The y axis setting does nothing atm. """
257 assert isinstance( i
, (float, int) ) and 0 <= i
<= 1
259 self
.__x
_alignment
= x
260 self
.__y
_alignment
= y
262 def get_alignment( self
):
263 """ Returns the current alignment settings (x, y). """
264 return self
.__x
_alignment
, self
.__y
_alignment
266 scrolling
= property( lambda s
: s
.__scrolling
, __set_scrolling
)
267 markup
= property( get_markup
, set_markup
)
268 alignment
= property( get_alignment
, lambda s
,i
: s
.set_alignment(*i
) )
270 def __on_expose_event( self
, widget
, event
):
271 """ Draws the text on the widget. This should be called indirectly by
272 using self.queue_draw() """
274 if self
.__graphics
_context
is None:
275 self
.__graphics
_context
= self
.window
.new_gc()
277 self
.window
.draw_layout( self
.__graphics
_context
,
278 int(self
.__x
_offset
), 0, self
.__pango
_layout
)
280 def __reset_widget( self
):
281 """ Reset the offset and find out whether or not it's even possible
282 to scroll the current text. Useful for when the window gets resized
283 or when new markup is set. """
285 # if there's no window, we can ignore this reset request because
286 # when the window is created this will be called again.
287 if self
.window
is None:
290 win_x
, win_y
= self
.window
.get_size()
291 lbl_x
, lbl_y
= self
.__pango
_layout
.get_pixel_size()
293 # If we don't request the proper height, the label might get squashed
294 # down to 0px. (this could still happen on the horizontal axis but we
295 # aren't affected by this problem in Panucci *yet*)
296 self
.set_size_request( -1, lbl_y
)
298 self
.__scrolling
_possible
= lbl_x
> win_x
300 # Remove any lingering scrolling timers
301 self
.__scrolling
_timer
= None
304 self
.__start
_scrolling
()
306 if self
.__scrolling
_possible
:
309 self
.__x
_offset
= (win_x
- lbl_x
) * self
.__x
_alignment
313 def __scroll_once(self
, halfway_callback
=None, finished_callback
=None):
314 """ Moves the text by 'self.pixel_jump' every time this is called.
315 Returns False when the text has completed one scrolling period and
316 'finished_callback' is run.
317 Returns False halfway through (when the text is all the way to the
318 right) if 'halfway_callback' is set after running the callback.
321 # prevent an accidental scroll (there's a race-condition somewhere
322 # but I'm too lazy to find out where).
323 if not self
.__scrolling
_possible
:
327 win_x
, win_y
= self
.window
.get_size()
328 lbl_x
, lbl_y
= self
.__pango
_layout
.get_pixel_size()
330 self
.__x
_offset
+= self
.pixel_jump
* self
.__x
_direction
332 if self
.__x
_direction
== self
.LEFT
and self
.__x
_offset
< win_x
- lbl_x
:
333 self
.__x
_direction
= self
.RIGHT
334 # set the offset to the maximum left bound otherwise some
335 # characters might get chopped off if using large pixel_jump's
336 self
.__x
_offset
= win_x
- lbl_x
338 if halfway_callback
is not None:
342 elif self
.__x
_direction
== self
.RIGHT
and self
.__x
_offset
> 0:
343 # end of scroll period; reset direction
344 self
.__x
_direction
= self
.LEFT
345 # don't allow the offset to be greater than 0
347 if finished_callback
is not None:
350 # return False because at this point we've completed one scroll
351 # period; this kills off any timers running this function
357 def __scroll_wait_callback(self
, delay
):
358 # Waits 'delay', then calls the scroll function
359 self
.__scrolling
_timer
= gobject
.timeout_add( delay
, self
.__scroll
)
362 """ When called, scrolls the text back and forth indefinitely while
363 waiting self.delay_btwn_scrolls between each scroll period. """
365 if self
.__scrolling
_possible
:
366 self
.__scrolling
_timer
= gobject
.timeout_add(
367 self
.update_interval
, self
.__scroll
_once
,
368 lambda: self
.__scroll
_wait
_callback
(self
.delay_halfway
),
369 lambda: self
.__scroll
_wait
_callback
(self
.delay_btwn_scrolls
) )
371 self
.__scrolling
_timer
= None
373 def __scrolling_timer_get(self
):
374 return self
.__scrolling
_timer
_id
376 def __scrolling_timer_set(self
, val
):
377 """ When changing the scrolling timer id, make sure that only one
378 timer is running at a time. This removes the previous timer
379 before adding a new one. """
381 if self
.__scrolling
_timer
_id
is not None:
382 gobject
.source_remove( self
.__scrolling
_timer
_id
)
383 self
.__scrolling
_timer
_id
= None
385 self
.__scrolling
_timer
_id
= val
387 __scrolling_timer
= property( __scrolling_timer_get
, __scrolling_timer_set
)
389 def __start_scrolling(self
):
390 """ Make the text start scrolling """
391 self
.__scrolling
= True
392 if self
.__scrolling
_timer
is None and self
.__scrolling
_possible
:
395 def __stop_scrolling(self
):
396 """ Make the text stop scrolling """
397 self
.__scrolling
= False
398 self
.__scrolling
_timer
= None
401 if __name__
== '__main__':
403 w
.set_geometry_hints(w
, 100, 20)
404 hb
= gtk
.HBox(homogeneous
=True, spacing
=1)
407 # scroll 7 pixels per 0.2 seconds, wait halfway for 0.5 seconds and finally
408 # wait 2 seconds after a complete scroll. wash, rinse, repeat.
409 l
= ScrollingLabel('N/A', 200, 7, 2000, 500)
410 l
.markup
= 'some random text 1234'
413 btn
= gtk
.Button('start/stop')
415 btn
.connect('clicked', lambda w
,e
: setattr(e
,'scrolling', not e
.scrolling
), l
)
417 w
.connect('destroy', gtk
.main_quit
)