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
27 class DualActionButton(gtk
.Button
):
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:
43 DURATION = 1.0 # in seconds
44 b = DualActionButton(gtk.Label('Default Action'), action_a,
45 gtk.Label('Longpress Action'), action_b,
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
)
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
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
:
98 self
.__current
_state
= state
99 child
= self
.get_child()
100 if child
is not None:
102 if state
== self
.DEFAULT
:
103 self
.add(self
.__default
_widget
)
104 elif state
== self
.LONGPRESS
:
105 self
.add(self
.__longpress
_widget
)
107 raise ValueError('State has to be either DEFAULT or LONGPRESS.')
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
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
()
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
):
145 self
.__force
_redraw
()
146 if self
.__pressed
_state
:
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')
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
)
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
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
)
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
):
241 self
.__start
_scrolling
()
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. """
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:
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
306 self
.__start
_scrolling
()
308 if self
.__scrolling
_possible
:
311 self
.__x
_offset
= (win_x
- lbl_x
) * self
.__x
_alignment
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
:
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:
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
349 if finished_callback
is not None:
352 # return False because at this point we've completed one scroll
353 # period; this kills off any timers running this function
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
)
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
) )
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
:
397 def __stop_scrolling(self
):
398 """ Make the text stop scrolling """
399 self
.__scrolling
= False
400 self
.__scrolling
_timer
= None
403 if __name__
== '__main__':
405 w
.set_geometry_hints(w
, 100, 20)
406 hb
= gtk
.HBox(homogeneous
=True, spacing
=1)
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'
415 btn
= gtk
.Button('start/stop')
417 btn
.connect('clicked', lambda w
,e
: setattr(e
,'scrolling', not e
.scrolling
), l
)
419 w
.connect('destroy', gtk
.main_quit
)