Package dtk :: Package ui :: Module tooltip

Source Code for Module dtk.ui.tooltip

  1  #! /usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3   
  4  # Copyright (C) 2011 ~ 2012 Deepin, Inc. 
  5  #               2011 ~ 2012 Xia Bin 
  6  # 
  7  # Author:     Xia Bin <xiabin@gmail.com> 
  8  # Maintainer: Xia Bin <xiabin@gmail.com> 
  9  # 
 10  # This program is free software: you can redistribute it and/or modify 
 11  # it under the terms of the GNU General Public License as published by 
 12  # the Free Software Foundation, either version 3 of the License, or 
 13  # any later version. 
 14  # 
 15  # This program is distributed in the hope that it will be useful, 
 16  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 17  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 18  # GNU General Public License for more details. 
 19  # 
 20  # You should have received a copy of the GNU General Public License 
 21  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 22   
 23  from animation import Animation, LinerInterpolator 
 24  from gtk import gdk 
 25  from label import Label 
 26  from theme import ui_theme 
 27  from utils import propagate_expose, color_hex_to_cairo, cairo_disable_antialias 
 28  import cairo 
 29  import gobject 
 30  import gtk 
 31   
 32   
 33  __all__ = ["text", "custom", "show_delay", "hide_delay", "hide_duration", 
 34          "background", "padding", "has_shadow", "disable", "always_update", 
 35          "disable_all"] 
36 37 -class ChildLocation:
38 - def __init__(self):
39 self.x = 0 40 self.y = 0 41 self.child = None 42 self.container = None
43
44 -def window_to_alloc(widget, x, y):
45 if widget.get_has_window() and widget.parent: 46 (wx, wy) = widget.window.get_position() 47 x += wx - widget.allocation.x 48 y += wy - widget.allocation.y 49 else: 50 x -= widget.allocation.x 51 y -= widget.allocation.y 52 return (x, y)
53
54 -def child_location_foreach(widget, cl): #cl = child_location
55 if not widget.is_drawable(): 56 return 57 if not cl.child : 58 (x, y) = cl.container.translate_coordinates(widget, int(cl.x), int(cl.y)) 59 if x >= 0 and x < widget.allocation.width and \ 60 y >=0 and y < widget.allocation.height: 61 if isinstance(widget, gtk.Container): 62 tmp = ChildLocation() 63 (tmp.x, tmp.y, tmp.container) = (x, y, widget) 64 widget.forall(child_location_foreach, tmp) 65 if tmp.child: 66 cl.child = tmp.child 67 else: 68 cl.child = widget 69 else: 70 cl.child = widget 71
72 -def coords_to_parent(window, x, y):
73 if window.get_window_type() == gdk.WINDOW_OFFSCREEN: 74 (px, py) = (-1, -1) 75 window.emit("to-embedder", window, x, y, px, py) 76 return (px, py) 77 else: 78 p = window.get_position() 79 return (x + p[0], y + p[1])
80
81 -def find_at_coords(gdkwindow, window_x, window_y):
82 cl = ChildLocation() 83 84 widget = gdkwindow.get_user_data() 85 if widget == None: 86 return (None, cl.x, cl.y) 87 88 cl.x = window_x 89 cl.y = window_y 90 91 while gdkwindow and gdkwindow != widget.window: 92 (cl.x, cl.y) = coords_to_parent(gdkwindow, cl.x, cl.y) 93 gdkwindow = gdkwindow.get_effective_parent() 94 95 if not gdkwindow: 96 return (None, cl.x, cl.y) 97 98 (cl.x, cl.y) = window_to_alloc(widget, cl.x, cl.y) 99 100 #find child 101 if isinstance(widget, gtk.Container): 102 cl.container = widget 103 cl.child = None 104 tmp_widget = widget 105 106 widget.forall(child_location_foreach, cl) 107 108 if cl.child and WidgetInfo.get_info(cl.child): 109 widget = cl.child 110 elif cl.container and WidgetInfo.get_info(cl.container): 111 widget = cl.container 112 113 (cl.x, cl.y) = tmp_widget.translate_coordinates(widget, int(cl.x), int(cl.y)) 114 115 116 if WidgetInfo.get_info(widget): 117 return (widget, cl.x, cl.y) 118 119 120 p = widget.get_parent() 121 while p: 122 if WidgetInfo.get_info(p): 123 return (p, cl.x, cl.y) 124 else: 125 p = p.get_parent() 126 127 return (None, cl.x, cl.y)
128
129 130 -def update_tooltip(display):
131 ''' 132 this function will be invoked every gdk event has received. 133 so reduce the time as possible as we can. 134 ''' 135 if TooltipInfo.enable_count == 0: 136 return 137 try : 138 (window, x, y) = display.get_window_at_pointer() 139 except: 140 return True 141 142 (widget, tx, ty) = find_at_coords(window, x, y) 143 if widget == None: 144 pass 145 # print "nop" 146 if not widget \ 147 or tx < 0 or tx >= widget.allocation.width \ 148 or ty < 0 or ty >= widget.allocation.height: 149 hide_tooltip() 150 return True 151 152 if TooltipInfo.widget != widget: 153 TooltipInfo.prewidget = widget 154 TooltipInfo.winfo = WidgetInfo.get_info(widget) 155 TooltipInfo.show_delay = TooltipInfo.winfo.show_delay 156 157 TooltipInfo.tmpwidget = widget 158 (rx, ry) = window.get_origin() 159 160 if TooltipInfo.pos_info != (int(rx+x), int(ry+y)) and TooltipInfo.show_id != 0: 161 hide_tooltip() 162 163 if TooltipInfo.show_id == 0: 164 if TooltipInfo.in_quickshow: 165 show_delay = 300 166 else: 167 show_delay = TooltipInfo.winfo.show_delay 168 TooltipInfo.pos_info = (int(rx+x), int(ry+y)) 169 TooltipInfo.show_id = gobject.timeout_add(show_delay, lambda : show_tooltip(*TooltipInfo.pos_info))
170
171 172 -class TooltipInfo:
173 widget = None 174 tmpwidget = None 175 prewidget = None 176 pos_info = None 177 window = None 178 alignment = None 179 winfo = None 180 offset_x = 5 181 offset_y = 5 182 on_showing = False 183 need_update = True 184 #displays = [] 185 stamp = 0 186 enable_count = 0 187 show_id = 0 188 189 in_quickshow = False 190 quickshow_id = 0 191 quickshow_delay = 2500
192
193 -def generate_tooltip_content():
194 """ generate child widget and update the TooltipInfo""" 195 if TooltipInfo.widget == TooltipInfo.prewidget and TooltipInfo.alignment.child and not TooltipInfo.need_update: 196 return 197 198 TooltipInfo.widget = TooltipInfo.tmpwidget 199 200 TooltipInfo.winfo = WidgetInfo.get_info(TooltipInfo.widget) 201 winfo = TooltipInfo.winfo 202 203 pre_child = TooltipInfo.alignment.child 204 205 if pre_child and winfo == WidgetInfo.get_info(pre_child) and not TooltipInfo.need_update: 206 return 207 208 if winfo.custom: 209 child = winfo.custom(*winfo.custom_args, **winfo.custom_kargs) 210 elif winfo.text: 211 child = Label(winfo.text, *winfo.text_args, **winfo.text_kargs) 212 else: 213 raise Warning, "tooltip enable's widget must has text or custom property" 214 215 if pre_child: 216 TooltipInfo.alignment.remove(pre_child) 217 pre_child.destroy() 218 219 TooltipInfo.alignment.set_padding(winfo.padding_t, winfo.padding_l, winfo.padding_b, winfo.padding_r) 220 TooltipInfo.alignment.add(child) 221 TooltipInfo.alignment.show_all() 222 223 allocation = gtk.gdk.Rectangle(0, 0, *TooltipInfo.alignment.child.size_request()) 224 allocation.width += winfo.padding_l + winfo.padding_r 225 allocation.height += winfo.padding_t + winfo.padding_b 226 TooltipInfo.window.size_allocate(allocation) 227 TooltipInfo.window.modify_bg(gtk.STATE_NORMAL, winfo.background) 228 if winfo.always_update: 229 TooltipInfo.need_update = True 230 else: 231 TooltipInfo.need_update = False
232
233 234 -def enable_quickshow():
235 def disable_q(): 236 TooltipInfo.in_quickshow = False 237 if TooltipInfo.quickshow_id != 0: 238 gobject.source_remove(TooltipInfo.quickshow_id)
239 TooltipInfo.in_quickshow = True 240 if TooltipInfo.quickshow_id == 0: 241 TooltipInfo.quickshow_id = gobject.timeout_add(TooltipInfo.quickshow_delay, disable_q) 242 else: 243 gobject.source_remove(TooltipInfo.quickshow_id) 244 TooltipInfo.quickshow_id = gobject.timeout_add(TooltipInfo.quickshow_delay, disable_q) 245
246 247 -def hide_tooltip():
248 TooltipInfo.window.hide() 249 TooltipInfo.on_showing = False 250 if TooltipInfo.show_id != 0: 251 gobject.source_remove(TooltipInfo.show_id) 252 TooltipInfo.show_id = 0 253 if TooltipInfo.window.get_realized(): 254 TooltipInfo.window.animation.stop() 255 return False
256
257 -def show_tooltip(x, y):
258 if TooltipInfo.enable_count == 0 or not TooltipInfo.winfo.enable: 259 return 260 generate_tooltip_content() 261 enable_quickshow() 262 263 #What will happen if the content widget is very big? 264 #---------------------------------------------- 265 (p_w, p_h) = (10, 10) #TODO: pointer size ? 266 (w, h) = TooltipInfo.window.get_root_window().get_size() 267 (t_w, t_h) = TooltipInfo.window.size_request() 268 269 if x + p_w + t_w > w: 270 POS_H = 0 #left 271 else: 272 POS_H = 1 #right 273 274 if y + p_h + t_h > h: 275 POS_V = 2 #top 276 else: 277 POS_V = 4 #bttom 278 279 p = POS_H + POS_V 280 ###################################### 281 # LEFT(0) RIGHT(1) # 282 #------------------------------------# 283 #TOP(2) 2 3 # 284 #------------------------------------# 285 #BOTTOM(4) 4 5 # 286 ###################################### 287 if p == 2: 288 TooltipInfo.window.move(x - t_w, y - t_h) 289 elif p == 3: 290 TooltipInfo.window.move(x, y - t_h) 291 elif p == 4: 292 TooltipInfo.window.move(x - t_w, y) 293 elif p == 5: 294 TooltipInfo.window.move(x + p_w, y + p_h) 295 else: 296 assert False, "This shouldn't appaer!!!!!!" 297 #------------------------------------------ 298 299 TooltipInfo.window.show() 300 TooltipInfo.on_showing = True
301
302 303 304 305 -def __init_window():
306 def on_realize(win): 307 win.swindow = gtk.gdk.Window(win.get_parent_window(), 308 width=0, height=0, 309 window_type=gtk.gdk.WINDOW_TEMP, 310 wclass=gtk.gdk.INPUT_OUTPUT, 311 event_mask=(win.get_events() | gdk.EXPOSURE_MASK), 312 visual=win.get_visual(), 313 colormap=win.get_colormap(), 314 ) 315 win.swindow.set_user_data(win) 316 317 #TODO: set duration dynamicly 318 win.animation = Animation([win.window, win.swindow], gdk.Window.set_opacity, 1000, [0, 1], 319 lambda *args: 1 - LinerInterpolator(*args))
320 321 def on_map(win): 322 winfo = TooltipInfo.winfo 323 win.animation.init(1) 324 win.animation.start_after(winfo.hide_delay) 325 geo = win.window.get_geometry() 326 win.swindow.move_resize(geo[0]+TooltipInfo.offset_x, geo[1]+TooltipInfo.offset_y, 327 win.allocation.width, win.allocation.height) 328 329 win.swindow.show() 330 331 def on_expose_event(win, e): 332 cr = win.swindow.cairo_create() 333 cr.set_source_rgba(1, 1, 1, 0) 334 cr.set_operator(cairo.OPERATOR_SOURCE) 335 cr.paint() 336 winfo = TooltipInfo.winfo 337 if winfo.has_shadow: 338 339 (x, y, width, height) = (0, 0, win.allocation.width, win.allocation.height) 340 (o_x, o_y) = (5, 5) 341 342 343 #right-bottom corner 344 radial = cairo.RadialGradient(width - o_x, height-o_y, 1, width -o_x, height-o_y, o_x) 345 radial.add_color_stop_rgba(0.0, 0,0,0, 0.3) 346 radial.add_color_stop_rgba(0.6, 0,0,0, 0.1) 347 radial.add_color_stop_rgba(1, 0,0,0, 0) 348 cr.set_source(radial) 349 cr.rectangle(width-o_x, height-o_y, o_x, o_y) 350 cr.fill() 351 352 #left-bottom corner 353 radial = cairo.RadialGradient(o_x, height-o_y, 1, o_x, height-o_y, o_x) 354 radial.add_color_stop_rgba(0.0, 0,0,0, 0.3) 355 radial.add_color_stop_rgba(0.6, 0,0,0, 0.1) 356 radial.add_color_stop_rgba(1, 0,0,0, 0) 357 cr.set_source(radial) 358 cr.rectangle(0, height-o_y, o_x, o_y) 359 cr.fill() 360 361 #left-top corner 362 radial = cairo.RadialGradient(width-o_x, o_y, 1, width-o_x, o_y, o_x) 363 radial.add_color_stop_rgba(0.0, 0,0,0, 0.3) 364 radial.add_color_stop_rgba(0.6, 0,0,0, 0.1) 365 radial.add_color_stop_rgba(1, 0,0,0, 0) 366 cr.set_source(radial) 367 cr.rectangle(width-o_x, 0, o_x, o_y) 368 cr.fill() 369 370 371 vradial = cairo.LinearGradient(0, height-o_y, 0, height) 372 vradial.add_color_stop_rgba(0.0, 0,0,0, .5) 373 vradial.add_color_stop_rgba(0.4, 0,0,0, 0.25) 374 vradial.add_color_stop_rgba(1, 0,0,0, 0.0) 375 cr.set_source(vradial) 376 cr.rectangle(o_x, height-o_x, width-2*o_x, height) 377 cr.fill() 378 379 hradial = cairo.LinearGradient(width-o_x, 0, width, 0) 380 hradial.add_color_stop_rgba(0.0, 0,0,0, .5) 381 hradial.add_color_stop_rgba(0.4, 0,0,0, 0.25) 382 hradial.add_color_stop_rgba(1, 0,0,0, 0.0) 383 cr.set_source(hradial) 384 cr.rectangle(width-o_x, o_y, width, height-2*o_y) 385 cr.fill() 386 387 gtk.Alignment.do_expose_event(TooltipInfo.alignment, e) 388 propagate_expose(win, e) 389 return True 390 391 def on_unmap(win): 392 win.swindow.hide() 393 394 def on_expose_alignment(widget, event): 395 '''Expose tooltip label.''' 396 rect = widget.allocation 397 cr = widget.window.cairo_create() 398 399 with cairo_disable_antialias(cr): 400 cr.set_line_width(1) 401 cr.set_source_rgba(*color_hex_to_cairo(ui_theme.get_color("tooltip_frame").get_color())) 402 cr.rectangle(rect.x + 1, rect.y + 1, rect.width - 1, rect.height - 1) 403 cr.stroke() 404 return True 405 406 TooltipInfo.window = gtk.Window(gtk.WINDOW_POPUP) 407 TooltipInfo.window.set_colormap(gtk.gdk.Screen().get_rgba_colormap()) 408 TooltipInfo.alignment = gtk.Alignment() 409 TooltipInfo.window.add(TooltipInfo.alignment) 410 TooltipInfo.window.connect('realize', on_realize) 411 TooltipInfo.window.connect('map', on_map) 412 TooltipInfo.window.connect('unmap', on_unmap) 413 TooltipInfo.window.connect('expose-event', on_expose_event) 414 TooltipInfo.alignment.connect('expose-event', on_expose_alignment) 415 __init_window() 416 417 418 #TODO:detect display? 419 #FIXME: 420 display = None
421 -def init_widget(widget):
422 TooltipInfo.enable_count += 1 423 w_info = WidgetInfo() 424 WidgetInfo.set_info(widget, w_info) 425 if widget.get_has_window(): 426 widget.add_events(gdk.POINTER_MOTION_MASK|gdk.POINTER_MOTION_HINT_MASK) 427 else: 428 widget.connect('realize', 429 lambda w: w.window.set_events(w.window.get_events() | gdk.POINTER_MOTION_HINT_MASK | gdk.POINTER_MOTION_MASK)) 430 if not display: 431 init_tooltip(widget) 432 return w_info
433 -def init_tooltip(win):
434 global display 435 if not display: 436 display = win.get_display() 437 #gobject.timeout_add(100, lambda : update_tooltip(display)) 438 #win.connect('focus-out-event', lambda w, e: hide_tooltip(True)) 439 win.connect('leave-notify-event', lambda w, e: hide_tooltip())
440
441 442 443 # 444 #the Interface of dtk Tooltip, the core is the WidgetInfo's attribute 445 # 446 447 -class WidgetInfo(object):
448 __DATA_NAME = "_deepin_tooltip_info" 449 450 @staticmethod
451 - def get_info(widget):
452 return widget.get_data(WidgetInfo.__DATA_NAME)
453 @staticmethod
454 - def set_info(widget, info):
455 return widget.set_data(WidgetInfo.__DATA_NAME, info)
456
457 - def __init__(self):
458 object.__setattr__(self, "show_delay", 1000) 459 object.__setattr__(self, "hide_delay", 3000) 460 object.__setattr__(self, "hide_duration", 1000) 461 object.__setattr__(self, "text", None) 462 object.__setattr__(self, "text_args", None) 463 object.__setattr__(self, "text_kargs", None) 464 object.__setattr__(self, "custom", None) 465 object.__setattr__(self, "custom_args", None) 466 object.__setattr__(self, "custom_kargs", None) 467 object.__setattr__(self, "background", gtk.gdk.Color(ui_theme.get_color("tooltip_background").get_color())) 468 object.__setattr__(self, "padding_t", 5) 469 object.__setattr__(self, "padding_b", 5) 470 object.__setattr__(self, "padding_l", 5) 471 object.__setattr__(self, "padding_r", 5) 472 object.__setattr__(self, "has_shadow", True) 473 object.__setattr__(self, "enable", False) #don't modify the "enable" init value 474 object.__setattr__(self, "always_update", False)
475
476 - def __setattr__(self, key, value):
477 if hasattr(self, key): 478 object.__setattr__(self, key, value) 479 else: 480 raise Warning, "Tooltip didn't support the \"%s\" property" % key 481 TooltipInfo.need_update = True 482 if key == "text" or key == "custom": 483 self.enable = True
484 485 all_method = {}
486 -def chainmethod(func):
487 all_method[func.__name__] = func 488 def wrap(*args, **kargs): 489 return func(*args, **kargs)
490 wrap.__dict__ = all_method 491 return wrap 492
493 # 494 #you can write yourself wrap function use "set_value" or direct modify the WidgetInfo's attribute 495 # 496 @chainmethod 497 -def set_value(widgets, kv):
498 if not isinstance(widgets, list): 499 widgets = [widgets] 500 for w in widgets: 501 w_info = WidgetInfo.get_info(w) 502 if not w_info: 503 w_info = init_widget(w) 504 for k in kv: 505 setattr(w_info, k, kv[k]) 506 return set_value
507
508 #------------------the default wrap function --------------------------------------- 509 @chainmethod 510 -def text(widget, content, *args, **kargs):
511 ''' 512 set the tooltip's text content. 513 the "content", "*args" and "**kargs" are pass to the dtk.ui.Label, 514 so you can change the text's color and some other property. 515 516 @param widget: the widget of you want to change. 517 @param content: the text which you want show. 518 @param args: pass to the dtk.ui.Label 519 @param kargs: pass to the dtk.ui.Label 520 ''' 521 set_value(widget, { 522 "text": content, 523 "text_args":args, 524 "text_kargs":kargs 525 }) 526 return text
527
528 @chainmethod 529 -def custom(widget, cb, *args, **kargs):
530 ''' 531 Set the custom tooltip content. 532 533 @param widget: the widget of you want to change. 534 @param cb: the function used to generate the content widget. this function should return an gtk.Widget. Be careful: if this function generate it's content affected by other runtime factor, you alsow should use "always_update" 535 to disable the internal cache mechanism 536 @param args: pass to the cb 537 @param kargs: pass to the cb 538 ''' 539 set_value(widget, { 540 "custom" : cb, 541 "custom_args" : args, 542 "custom_kargs" : kargs 543 }) 544 return custom
545 @chainmethod
546 -def show_delay(widget, delay):
547 ''' 548 set the time of the tooltip's begin show after pointer stay on the widget. 549 550 @param widget: the widget of you want to change. 551 @param delay: the time of start begin show. 552 ''' 553 delay = max(250, delay) 554 set_value(widget, {"show_delay": delay}) 555 return show_delay
556
557 @chainmethod 558 -def hide_delay(widget, delay):
559 ''' 560 set the time of the tooltip's start to hide. 561 562 @param widget: the widget of you want to change. 563 @param delay: the time of start begin hide. 564 ''' 565 set_value(widget, {"hide_delay": delay}) 566 return hide_delay
567
568 @chainmethod 569 -def hide_duration(widget, delay):
570 ''' 571 set the duration of the tooltip's hide effect duration. 572 573 @param widget: the widget of you want to change. 574 @param delay: the time of the effect duration. 575 ''' 576 set_value(widget, {"hide_duration": delay}) 577 return hide_duration
578
579 @chainmethod 580 -def background(widget, color):
581 ''' 582 set the background of the tooltip's content. 583 584 @param widget: the widget of you want to change. 585 @param color: the gdk.Color of background. 586 ''' 587 set_value(widget, {"background": color}) 588 return background
589
590 @chainmethod 591 -def padding(widget, t, l, b, r):
592 ''' 593 set the padding of the tooltip's content. 594 595 @param widget: the widget of you want to change. 596 597 @param t: the top space 598 @param l: the left space 599 @param b: the bottom space 600 @param r: the right space 601 ''' 602 kv = {} 603 if t >= 0: 604 kv["padding_t"] = int(t) 605 if b >= 0: 606 kv["padding_b"] = int(b) 607 if l >= 0: 608 kv["padding_l"] = int(l) 609 if r >= 0: 610 kv["padding_r"] = int(r) 611 612 set_value(widget, kv) 613 return padding
614
615 616 @chainmethod 617 -def has_shadow(widget, need):
618 ''' 619 whether this widget's tooltip need shadow. 620 621 @param widget: the widget of you want disable tooltip. 622 @param need : wheter need shadow . 623 ''' 624 set_value(widget, {"has_shadow": need}) 625 return has_shadow
626
627 628 @chainmethod 629 -def disable(widget, is_disable):
630 ''' 631 disable this widget's tooltip 632 633 @param widget: the widget of you want disable tooltip. 634 @param is_disable: wheter disable tooltip. 635 ''' 636 winfo = WidgetInfo.get_info(widget) 637 if is_disable: 638 if winfo.enable : 639 winfo.enable = False 640 TooltipInfo.enable_count -= 1 641 else: 642 if not winfo.enable : 643 winfo.enable = True 644 TooltipInfo.enable_count += 1 645 return disable
646
647 @chainmethod 648 -def always_update(widget, need):
649 ''' 650 Always create the new tooltip's content, used to show the 651 652 curstom tooltip content generate by function and the function's 653 654 return widget is different every time be invoked. 655 656 @param widget: Gtk.Widget instance. 657 @param need: whether alwasy update. 658 ''' 659 set_value(widget, {"always_update" : need}) 660 return always_update
661
662 #------------------------this is global effect function--------------------- 663 -def disable_all(is_disable):
664 ''' 665 ''' 666 count = TooltipInfo.enable_count 667 if is_disable: 668 if count > 0: 669 TooltipInfo.enable_count = -count 670 else: 671 if count < 0: 672 TooltipInfo.enable_count = -count
673
674 675 -def tooltip_handler(event):
676 gtk.main_do_event(event) 677 if event.type == gdk.MOTION_NOTIFY: 678 # print "leave", time.time() 679 update_tooltip(display) 680 elif event.type == gdk.LEAVE_NOTIFY: 681 # print "leave", time.time() 682 hide_tooltip()
683 gdk.event_handler_set(tooltip_handler) 684