Package qubx :: Module toolspace
[hide private]
[frames] | no frames]

Source Code for Module qubx.toolspace

   1  """ 
   2  Widget with switchable mouse controllers (Tools) and transparent overlays and layers. 
   3   
   4  Colors are given as a pair: COLORREF=(name_for_prefs, (r,g,b,a)).  (r,g,b,a) is the initial default. 
   5  Each time it draws, a ToolSpace looks up its colors (and font) in self.appearance (type L{TS_Appearance}): 
   6   
   7      >>> r,g,b,a = self.appearance.color(COLORREF) 
   8   
   9  You can change the color: 
  10   
  11      >>> self.appearance.colors[COLORREF].set( (r,g,b,a) ) 
  12   
  13  Or swap in a different TS_Appearance, e.g. for black and white printing: 
  14   
  15      >>> set_aside, self.appearance = self.appearance, black_and_white_appearance 
  16   
  17  Appearance prefs are saved by L{qubx.util_panels.SettingsFace}. 
  18   
  19  Layers and sublayers are laid out statically, in multiples of appearance.emsize. 
  20  Negative coordinates or dimensions indicate distance from the right or bottom edge. 
  21  When font size or widget dimensions change, it updates the layout. 
  22   
  23  Copyright 2008-2014 Research Foundation State University of New York  
  24  This file is part of QUB Express.                                           
  25   
  26  QUB Express is free software; you can redistribute it and/or modify           
  27  it under the terms of the GNU General Public License as published by  
  28  the Free Software Foundation, either version 3 of the License, or     
  29  (at your option) any later version.                                   
  30   
  31  QUB Express is distributed in the hope that it will be useful,                
  32  but WITHOUT ANY WARRANTY; without even the implied warranty of        
  33  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
  34  GNU General Public License for more details.                          
  35   
  36  You should have received a copy of the GNU General Public License,    
  37  named LICENSE.txt, in the QUB Express program directory.  If not, see         
  38  <http://www.gnu.org/licenses/>.                                       
  39   
  40  """ 
  41   
  42  import cairo 
  43  import collections 
  44  import ctypes 
  45  import gtk 
  46  import gobject 
  47  import traceback 
  48  import weakref 
  49   
  50  from gtk import gdk 
  51  from gtk import keysyms 
  52  gtkHas2Click = False 
  53  DBL_CLICK_MS = 250 
  54   
  55  from itertools import izip, count 
  56  from math import * 
  57   
  58  import qubx.pyenv 
  59  from qubx.util_types import * 
  60  from qubx.accept import acceptIntGreaterThanOrEqualTo 
  61  from qubx.GTK import build_menuitem 
  62   
  63  ColorInfo = collections.defaultdict(lambda: Anon()) # { name : Anon(label='whatever') } # use labels if exist 
  64   
  65  LAYER_BG = ('toolspace.layer.bg', (.12,.12,.12,.85)) 
  66  ColorInfo[LAYER_BG[0]].label = 'Default layer background' 
  67  LAYER_FG = ('toolspace.layer.fg', (.1,1,.1,.9)) 
  68  ColorInfo[LAYER_FG[0]].label = 'Default layer foreground' 
  69  LAYER_BORDER = ('toolspace.layer.border', (.5, .5, .5, .5)) 
  70  ColorInfo[LAYER_BORDER[0]].label = 'Default layer border' 
  71  COLOR_CLEAR = ('toolspace.clear', (0,0,0,0)) 
  72  ColorInfo[COLOR_CLEAR[0]].label = 'Constant: transparent' 
  73  COLOR_BLACK = ('toolspace.black', (0,0,0,1)) 
  74  ColorInfo[COLOR_BLACK[0]].label = 'Constant: black' 
  75  COLOR_WHITE = ('toolspace.white', (1,1,1,1)) 
  76  ColorInfo[COLOR_WHITE[0]].label = 'Constant: white' 
  77  COLOR_LABEL = ('toolspace.label', LAYER_FG[1]) 
  78  ColorInfo[COLOR_LABEL[0]].label = 'Default layer text' 
  79  COLOR_HOVER = ('toolspace.label.hover', COLOR_LABEL[1]) 
  80  ColorInfo[COLOR_HOVER[0]].label = 'Default layer text mouseover' 
  81  COLOR_POPUP = ('toolspace.popup', (0, 0, 1, 1)) 
  82  ColorInfo[COLOR_POPUP[0]].label = 'Default popup menu' 
  83  COLOR_DROPDOWN = ('toolspace.dropdown', LAYER_FG[1]) 
  84  ColorInfo[COLOR_DROPDOWN[0]].label = 'Default dropdown menu text' 
  85  COLOR_RANGE = ('toolspace.range', (.1, .5, .7, .5)) 
  86  ColorInfo[COLOR_RANGE[0]].label = 'Data scroll thumb' 
  87  COLOR_RANGE_HOVER = ('toolspace.range.hover', (.2, .7, 1, .6)) 
  88  ColorInfo[COLOR_RANGE_HOVER[0]].label = 'Data scroll thumb mouseover' 
  89  COLOR_RANGE_EXPAND = ('toolspace.range.expand', (1, .2, .8, .9)) 
  90  ColorInfo[COLOR_RANGE_EXPAND[0]].label = 'Data scroll thumb mouseover edge' 
  91  COLOR_TOOLTIP_BG = ('toolspace.tooltip.bg', (1,1,.3,.8)) 
  92  ColorInfo[COLOR_TOOLTIP_BG[0]].label = 'Tooltip background' 
  93  COLOR_TOOLTIP_FG = ('toolspace.tooltip.fg', (0,0,0,.85)) 
  94  ColorInfo[COLOR_TOOLTIP_FG[0]].label = 'Tooltip text' 
  95  COLOR_CHECK = ('toolspace.check', (1,.5,0,.8)) 
  96  ColorInfo[COLOR_CHECK[0]].label = 'Default checkbox' 
  97  COLOR_PALETTE_TEXT = ('toolspace.palette.text', (1,1,1,1)) 
  98  ColorInfo[COLOR_PALETTE_TEXT[0]].label = 'Color-chooser marker' 
  99  COLOR_PALETTE_MORE = ('toolspace.palette.more', (.1, .1, .1, 1)) 
 100  ColorInfo[COLOR_PALETTE_MORE[0]].label = 'Color-chooser menu' 
 101  COLOR_RADIO_FILL = ('toolspace.radio.fill', (1, .5, 0, .8)) 
 102  ColorInfo[COLOR_RADIO_FILL[0]].label = 'Radio button fill' 
 103  COLOR_FOCUSED = ('toolspace.focused', (.75, .45, 1, .5)) 
 104  ColorInfo[COLOR_FOCUSED[0]].label = 'Panel border focused' 
 105  COLOR_MAG_RIM = ('toolspace.mag.rim', (1, 1, 1, .8)) 
 106  ColorInfo[COLOR_MAG_RIM[0]].label = 'Zoom icon rim' 
 107  COLOR_MAG_LENS = ('toolspace.mag.lens', (1, 1, 1, .35)) 
 108  ColorInfo[COLOR_MAG_LENS[0]].label = 'Zoom icon lens' 
 109  PALETTE_H_EMS = 2.3 
 110   
 111  TOOLBAR_PAD = 0.5 
 112   
113 -class TS_Color(object):
114 """Represents one named color. 115 116 @ivar name: 117 @ivar value: (r,g,b,a) 118 @ivar OnSet: L{WeakEvent}(TS_Color) when color.set() was called. 119 """
120 - def __init__(self, name, default):
121 self.name = name 122 self.value = WITHALPHA(default or (1, 1, 1, 1)) 123 self.OnSet = WeakEvent() # (TS_Color)
124 - def set(self, value):
125 self.value = WITHALPHA(value) 126 self.OnSet(self)
127
128 -class TS_Appearance(object):
129 """Holds color and font preferences. 130 131 @ivar colors: dict: name -> TS_Color 132 @ivar color_by_name: dict: name -> TS_Color 133 @ivar colordef_by_name: dict: name -> (r,g,b,a); values from color_preset() which haven't been requested by any components yet. 134 @ivar font_size: size in points of the default font 135 @ivar font_bold: (bool) default font is bold? 136 @ivar line_width: multiplier, default=1.0 137 @ivar emsize: width in pixels of a capital 'M' 138 @ivar opengl: True if OpenGL is allowed 139 @ivar hide_hidden_signals: False to show controls for all signals 140 @ivar multi_line_data: True to add lines when lo-res data is expanded 141 @ivar auto_scale_data: True to auto-scale data signals 142 @ivar color_idealized: True to draw idealization with class colors 143 @ivar gauss_intensity: True to draw fit curve with intensity gradient (otherwise +/- 1 std) 144 @ivar OnSetFontSize: L{WeakEvent}(font_size) when set_font_size() is called 145 @ivar OnSetLineWidth: L{WeakEvent}(line_width) when set_line_width() is called 146 @ivar OnSetOpenGL: L{WeakEvent}(opengl) when set_opengl() is called 147 @ivar OnAddColor: L{WeakEvent}(name, (r,g,b,a)) when a color is looked up for the first time 148 @ivar OnSetColor: L{WeakEvent}(name, (r,g,b,a)) when a color is changed 149 @ivar OnSetHideHiddenSignals: L{WeakEvent}(bool) 150 @ivar OnSetMultiLineData: L{WeakEvent}(bool) 151 @ivar OnSetAutoScaleData: L{WeakEvent}(bool) 152 @ivar OnSetColorIdealized: L{WeakEvent}(bool) 153 @ivar OnSetGaussIntensity: L{WeakEvent}(bool) 154 """
155 - def __init__(self, font_size=11, font_bold=True, opengl=False, line_width=1.0):
156 self.__ref = Reffer() 157 self.OnSetFontSize = WeakEvent() # (font_size) 158 self.set_font_size(font_size, font_bold) 159 self.OnSetOpenGL = WeakEvent() 160 self.__opengl = opengl 161 self.__hide_hidden_signals = False 162 self.__multi_line_data = False 163 self.__auto_scale_data = False 164 self.__color_idealized = False 165 self.__gauss_intensity = True 166 self.OnSetLineWidth = WeakEvent() # (line_width) 167 self.__line_width = line_width 168 self.colors = {} # name : TS_Color 169 self.color_by_name = {} 170 self.colordef_by_name = {} 171 self.OnAddColor = WeakEvent() # (name, value) 172 self.OnSetColor = WeakEvent() # (name, value) 173 self.OnSetHideHiddenSignals = WeakEvent() # (bool) 174 self.OnSetMultiLineData = WeakEvent() # (bool) 175 self.OnSetAutoScaleData = WeakEvent() # (bool) 176 self.OnSetColorIdealized = WeakEvent() 177 self.OnSetGaussIntensity = WeakEvent()
178 - def color(self, nm_def):
179 """Returns (r,g,b,a) (in [0..1]) corresponding to COLORREF tuple nm_def, from user prefs.""" 180 try: 181 return self.colors[nm_def[0]].value 182 except KeyError: 183 color = TS_Color(*nm_def) 184 self.colors[nm_def[0]] = color 185 self.color_by_name[color.name] = color 186 if color.name in self.colordef_by_name: 187 color.set(self.colordef_by_name[color.name]) 188 del self.colordef_by_name[color.name] 189 self.OnAddColor(color) 190 color.OnSet += self.__ref(self.__onSetColor) 191 return color.value
192 - def __onSetColor(self, color):
193 self.OnSetColor(color.name, color.value)
194 - def color_preset(self, name, default):
195 """Initializes the named color with a new default, to override the one in its COLORREF tuple, as from saved preferences.""" 196 if name in self.color_by_name: 197 self.color_by_name[name].set(default) 198 else: 199 # not registered yet; don't know its full COLORREF: set aside until first use: 200 self.colordef_by_name[name] = default
201 - def setup_context(self, context):
202 """Sets the font in a cairo drawing context.""" 203 context.select_font_face('sans-serif', cairo.FONT_SLANT_NORMAL, 204 self.font_bold and cairo.FONT_WEIGHT_BOLD or cairo.FONT_WEIGHT_NORMAL) 205 context.set_font_size(self.font_size)
206 - def calc_font_spacing(self):
207 """Returns (emsize, fheight) -- width of a capital M, and overall font height (advisory).""" 208 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 128, 128) 209 context = cairo.Context(surface) 210 self.setup_context(context) 211 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 212 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents('M') 213 return width, fheight
214 - def set_font_size(self, pts, bold=None):
215 """Changes the default font size and weight, and signals OnSetFontSize.""" 216 self.__font_size = pts 217 if not (bold is None): 218 self.__font_bold = bold 219 self.emsize, fheight = self.calc_font_spacing() 220 self.emsize = int(round(self.emsize)) 221 self.OnSetFontSize(pts)
222 font_size = property(lambda self: self.__font_size, lambda self, x: self.set_font_size(x, self.__font_bold)) 223 font_bold = property(lambda self: self.__font_bold, lambda self, x: self.set_font_size(self.__font_size, x))
224 - def set_opengl(self, opengl):
225 global GLSpace 226 if opengl != self.opengl: 227 self.__opengl = opengl 228 GLSpace = (HAVE_OPENGL and opengl) and ToolSpace_GL or ToolSpace 229 self.OnSetOpenGL(opengl)
230 opengl = property(lambda self: self.__opengl, lambda self, x: self.set_opengl(x), doc="obsolete: poor cross-platform support")
231 - def set_line_width(self, x):
232 if self.__line_width != x: 233 self.__line_width = x 234 self.OnSetLineWidth(x)
235 line_width = property(lambda self: self.__line_width, lambda self, x: self.set_line_width(x), doc="used mainly for data drawing")
236 - def set_hide_hidden_signals(self, x):
237 if x != self.__hide_hidden_signals: 238 self.__hide_hidden_signals = x 239 self.OnSetHideHiddenSignals(x)
240 hide_hidden_signals = property(lambda self: self.__hide_hidden_signals, lambda self, x: self.set_hide_hidden_signals(x), doc="True to omit scope controls for hidden signals")
241 - def set_multi_line_data(self, x):
242 if x != self.__multi_line_data: 243 self.__multi_line_data = x 244 self.OnSetMultiLineData(x)
245 multi_line_data = property(lambda self: self.__multi_line_data, lambda self, x: self.set_multi_line_data(x), doc="True to allow multiple lines of data in the low-res panel if it is larger than hi-res")
246 - def set_auto_scale_data(self, x):
247 if x != self.__auto_scale_data: 248 self.__auto_scale_data = x 249 self.OnSetAutoScaleData(x)
250 auto_scale_data = property(lambda self: self.__auto_scale_data, lambda self, x: self.set_auto_scale_data(x), doc="True to automatically rescale data onscreen.")
251 - def set_color_idealized(self, x):
252 if x != self.__color_idealized: 253 self.__color_idealized = x 254 self.OnSetColorIdealized(x)
255 color_idealized = property(lambda self: self.__color_idealized, lambda self, x: self.set_color_idealized(x), doc="True to draw idealization with class colors")
256 - def set_gauss_intensity(self, x):
257 if x != self.__gauss_intensity: 258 self.__gauss_intensity = x 259 self.OnSetGaussIntensity(x)
260 gauss_intensity = property(lambda self: self.__gauss_intensity, lambda self, x: self.set_gauss_intensity(x), doc="True to draw fit curves with intensity gradient")
261 262 Appearance = TS_Appearance() # default global appearance object 263
264 -class TS_ColorsDialog(gtk.Dialog):
265 """Modeless window for changing the (r,g,b,a) values in appearance.colors."""
266 - def __init__(self, app_name, appearance):
267 gtk.Dialog.__init__(self, '%s - Colors'%app_name, None, 0) 268 action_area = self.get_action_area() 269 btn = gtk.Button('OK') 270 btn.connect('clicked', lambda btn: self.response(gtk.RESPONSE_ACCEPT) or self.destroy()) 271 btn.show() 272 action_area.pack_end(btn, False, True) 273 btn = gtk.Button('Apply') 274 btn.connect('clicked', lambda btn: self.on_apply()) 275 btn.show() 276 action_area.pack_end(btn, False, True) 277 btn = gtk.Button('Cancel') 278 btn.connect('clicked', lambda btn: self.on_cancel() or self.response(gtk.RESPONSE_REJECT) or self.destroy()) 279 btn.show() 280 action_area.pack_end(btn, False, True) 281 self.connect('destroy', lambda win: self.destroy()) 282 self.set_size_request(300, 200) 283 scr = gtk.ScrolledWindow() 284 scr.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 285 scr.show() 286 self.vbox.pack_start(scr, True, True) 287 v = gtk.VBox() 288 v.show() 289 scr.add_with_viewport(v) 290 self.appearance = appearance 291 self.names = [(ColorInfo[key].label or key, key) for key in appearance.colors] # sorted(appearance.color_by_name.keys()) 292 self.names.sort() 293 self.colors = [appearance.color_by_name[name] for lbl, name in self.names] 294 self.swatches = [] 295 for lbl_name, color in izip(self.names, self.colors): 296 caption, name = lbl_name 297 h = gtk.HBox() 298 h.show() 299 lbl = gtk.Label(caption) 300 lbl.show() 301 h.pack_start(lbl, False, True) 302 swatch = gtk.ColorButton(qubx.GTK.RGBAtoGDK(color.value)) 303 swatch.set_use_alpha(True) 304 swatch.set_alpha(int(round(65535.0*(color.value[3])))) 305 swatch.set_size_request(36, -1) 306 swatch.connect('color-set', self.__onClickSwatch, name) 307 swatch.show() 308 h.pack_end(swatch, False, True) 309 self.swatches.append(swatch) 310 v.pack_start(h, False, True)
311 - def __onClickSwatch(self, btn, name):
312 self.appearance.color_by_name[name].set(SETALPHA(qubx.GTK.GDKtoRGBA(btn.get_color()), btn.get_alpha()/65535.0))
313 - def show(self, on_cancel, on_apply):
314 """Shows the dialog and sets a restore action. The colors are changed as you pick them, and the old colors forgotten; you should save a copy. 315 316 @param on_cancel: When the user presses "Cancel", it calls on_cancel() so you can restore their old colors. 317 """ 318 gtk.Dialog.show(self) 319 self.on_cancel = on_cancel 320 self.on_apply = on_apply
321 322
323 -def EditOneColor(colorref, caption="Pick a color", appearance=None):
324 """Runs a modal dialog so the user can change one color in the prefs.""" 325 appr = appearance or Appearance 326 color = appr.color_by_name[colorref[0]] 327 dlg = gtk.ColorSelectionDialog(caption) 328 picker = dlg.get_color_selection() 329 picker.set_current_color(qubx.GTK.RGBAtoGDK(color.value)) 330 picker.set_has_opacity_control(True) 331 picker.set_current_alpha(int(round(65535.0*(color.value[3])))) 332 dlg.ok_button.connect('clicked', lambda it: dlg.response(gtk.RESPONSE_ACCEPT)) 333 response = dlg.run() 334 dlg.destroy() 335 if gtk.RESPONSE_ACCEPT == response: 336 color.set(SETALPHA(qubx.GTK.GDKtoRGBA(picker.get_current_color()), picker.get_current_alpha()/65535.0))
337 338
339 -class ToolSpace(gtk.DrawingArea):
340 """Base widget with these features: 341 - OnDraw and OnOverlay events; OnDraw is double-buffered. 342 - designate a L{Tool} to receive mouse events (and optionally draw overlays) 343 (anyone can connect to the mouse events, but please use Tool to do so) 344 - transparent L{Layer}s and L{SubLayer}s 345 - TS_Appearance for font and color coordination 346 347 @ivar appearance: L{TS_Appearance} 348 @ivar dim: (width, height) last time drawn 349 @ivar layerset: L{LayerSet} -- swappable collection of L{Layer}s 350 @ivar tool: L{Tool} -- object which receives mouse events 351 @ivar tooltip: L{Overlay_Tooltip} 352 @ivar can_focus: True if it wants keyboard events -- will steal focus when mouse passes over 353 354 @ivar OnDraw: L{WeakEvent}(context, w, h) if opengl_dimension == 0 355 @ivar OnDrawGL: L{WeakEvent}(gldrawable, glcontext, w, h) if opengl_dimension 356 @ivar OnOverlay: L{WeakEvent}(context, w, h) 357 @ivar OnPress: L{WeakEvent}(x, y, event) 358 @ivar OnRelease: L{WeakEvent}(x, y, event) 359 @ivar OnDblClick: L{WeakEvent}(x, y, event) 360 @ivar OnDrag: L{WeakEvent}(x, y, event) 361 @ivar OnRoll: L{WeakEvent}(x, y, event) 362 @ivar OnScroll: L{WeakEvent}(x, y, event, amount) 363 @ivar OnKeyPress: L{WeakEvent}(event) 364 @ivar OnKeyRelease: L{WeakEvent}(event) 365 @ivar OnNeedPopup: L{WeakEvent}(x, y, event) 366 @ivar OnChangeLayerset: L{WeakEvent}(ToolSpace, L{LayerSet}) 367 @ivar OnChangeTool: L{WeakEvent}(ToolSpace, L{Tool}) 368 369 """ 370 371 __explore_featured = ['appearance', 'dim', 'layerset', 'tool', 'tooltip', 'can_focus', 372 'OnDraw', 'OnOverlay', 'OnPress', 'OnRelease', 'OnDblClick', 'OnDrag', 'OnRoll', 373 'OnScroll', 'OnKeyPress', 'OnKeyRelease', 'OnNeedPopup', 'OnChangeLayerset', 'OnChangeTool', 374 'set_size_request', 'add_layer', 'remove_layer', 'invalidate_layer', 'redraw_canvas', 375 'find_layer', 'draw_all_overlays', 'draw_to_drawable', 'draw_to_context'] 376
377 - def __init__(self, appearance=None, opengl=0, can_focus=False):
378 gtk.DrawingArea.__init__(self) 379 self.can_focus = can_focus 380 self.__ref = Reffer() 381 self._invalid = True 382 self._layers = [] 383 self.__appearance = None 384 self.appearance = appearance 385 self.opengl = HAVE_OPENGL and opengl or 0 386 if self.opengl: 387 try: 388 try: 389 glconfig = gtk.gdkgl.Config(mode=gtk.gdkgl.MODE_RGBA | gtk.gdkgl.MODE_DOUBLE) 390 except gtk.gdkgl.NoMatches: 391 glconfig = gtk.gdkgl.Config(mode=gtk.gdkgl.MODE_RGBA) 392 self.set_gl_capability(glconfig) 393 except: 394 traceback.print_exc() 395 self.opengl = 0 396 self.set_size_request(50, 50) 397 self.tooltip = Overlay_Tooltip(self) 398 self._dim = (0, 0) 399 self._layer_scale = (1.0, 1.0) 400 self._fat = 1.0 # line width 401 self._unit_width = 0.0 # printing fix / ignored when zero 402 self._dragging = False 403 self._tool = None 404 self._layerset = None 405 self._mouse_layer = None 406 self._button_layer = None 407 self.__focused = False 408 self.OnDraw = WeakEvent() # (context, w, h) 409 self.OnDrawGL = WeakEvent() 410 self.OnOverlay = WeakEvent() # (context, w, h) 411 self.OnPress = WeakEvent() # (x, y, e) 412 self.OnRelease = WeakEvent() # (x, y, e) 413 self.OnDblClick = WeakEvent() # (x, y, e) 414 self.OnDrag = WeakEvent() # (x, y, e) 415 self.OnRoll = WeakEvent() # (x, y, e) 416 self.OnScroll = WeakEvent() # (x, y, e, amount +/-) 417 self.OnKeyPress = WeakEvent() 418 self.OnKeyRelease = WeakEvent() 419 self.OnNeedPopup = WeakEvent() # (x, y, e) 420 self.OnChangeTool = WeakEvent() 421 self.OnChangeLayerset = WeakEvent() 422 self.connect_after("realize", self._realize) 423 self.connect("size_allocate", self.resize_event) 424 self.connect("expose_event", self._expose) 425 self.add_events(gdk.BUTTON_PRESS_MASK | 426 gdk.BUTTON_RELEASE_MASK | 427 gdk.POINTER_MOTION_MASK | 428 gdk.SCROLL_MASK | 429 gdk.KEY_PRESS_MASK | 430 gdk.KEY_RELEASE_MASK | 431 gdk.ENTER_NOTIFY_MASK) 432 self.connect("button_press_event", self.button_press) 433 self.connect("button_release_event", self.button_release) 434 self.connect("motion_notify_event", self.motion_notify) 435 self.connect("scroll_event", self.scroll) 436 self.connect("key_press_event", self.key_press) 437 self.connect("key_release_event", self.key_release) 438 self.connect('focus_out_event', self.focus_out) 439 self.connect('focus_in_event', self.focus_in) 440 self.connect('enter_notify_event', self.enter_notify) 441 self.__lastPressTime = 0
442 dim = property(lambda self: self._dim)
443 - def set_tool(self, tool):
444 # always de/activate for easier updating of tool.layers 445 #if tool == self._tool: 446 # return 447 if self._tool: 448 self._tool.deactivate(self) 449 self._tool = tool 450 if self._tool: 451 self._tool.activate(self) 452 # self.grab_focus() 453 self.OnChangeTool(self, tool)
454 tool = property(lambda s: s._tool, lambda s,x: s.set_tool(x))
455 - def set_layerset(self, ls):
456 if ls == self._layerset: 457 return 458 if self._layerset: 459 self._layerset.deactivate(self) 460 self._layerset = ls 461 if self._layerset: 462 self._layerset.activate(self) 463 self.OnChangeLayerset(self, ls)
464 layerset = property(lambda self: self._layerset, lambda self, x: self.set_layerset(x))
465 - def set_appearance(self, appearance):
466 if self.__appearance: 467 self.__appearance.OnSetFontSize -= self.__ref(self.__onSetFontSize) 468 self.__appearance.OnSetColor -= self.__ref(self.__onSetColor) 469 self.__appearance.OnSetLineWidth -= self.__ref(self.__onSetLineWidth) 470 self.__appearance = appearance or Appearance 471 self.__appearance.OnSetFontSize += self.__ref(self.__onSetFontSize) 472 self.__appearance.OnSetColor += self.__ref(self.__onSetColor) 473 self.__appearance.OnSetLineWidth += self.__ref(self.__onSetLineWidth) 474 self.redraw_canvas(invalid_layers=True)
475 appearance = property(lambda s: s.__appearance, lambda s,x: s.set_appearance(x))
476 - def set_can_focus(self, can):
477 self.__can_focus = can 478 if hasattr(gtk.DrawingArea, 'set_can_focus'): # gtk >= 2.22 479 gtk.DrawingArea.set_can_focus(self, can)
480 can_focus = property(lambda s: s.__can_focus, lambda s,x: s.set_can_focus(x))
481 - def focus_in(self, w, e):
482 self.__focused = True 483 self.redraw_canvas(False)
484 - def focus_out(self, w, e):
485 self.__focused = False 486 self.redraw_canvas(False)
487 - def enter_notify(self, w, e):
488 if self.__can_focus and (e.mode == gdk.CROSSING_NORMAL): 489 self.grab_focus()
490 - def add_layer(self, layer):
491 """@param layer: L{Layer}""" 492 self._layers.insert(0, layer) 493 layer.OnInvalidate += self.__ref(self.invalidate_layer) 494 layer.space = self 495 layer.on_show() 496 self.redraw_canvas(False) 497 return layer
498 - def remove_layer(self, layer):
499 if layer == self._button_layer: 500 self._button_layer = None 501 layer.button_release(0, 0, None) #? 502 if layer == self._mouse_layer: 503 self._mouse_layer = None 504 layer.mouse_exit() 505 layer.OnInvalidate -= self.__ref(self.invalidate_layer) 506 layer.on_hide() 507 layer.space = None 508 self._layers.remove(layer) 509 self.redraw_canvas(False)
510 - def invalidate_layer(self, layer):
511 # when a layer sends OnInvalidate, redraw all overlays 512 self.redraw_canvas(False)
513 - def redraw_canvas(self, invalid=True, immediately=False, invalid_layers=False):
514 """Requests repaint, or does it immediately. 515 516 @param invalid: True to call OnDraw again; False to re-use the double-buffered image (and repaint overlays only) 517 @param immediately: True to repaint now; False to wait until 'expose' event 518 @param invalid_layers: True to force repaint of all layers (each layer has its own double-buffer) 519 """ 520 self._invalid = self._invalid or invalid 521 if invalid_layers: 522 for layer in self._layers: 523 layer.invalidate() 524 if self.window: 525 alloc = self.get_allocation() 526 rect = gdk.Rectangle(0, 0, alloc.width, alloc.height) 527 self.window.invalidate_rect(rect, True) 528 if immediately: 529 self.window.process_updates(True)
530 - def _realize(self, *args):
531 if self.opengl: 532 try: 533 gldrawable = self.get_gl_drawable() 534 glcontext = self.get_gl_context() 535 OpenGL.GL.glClearColor(0.0, 0.0, 0.0, 0.0) 536 if not gldrawable.gl_begin(glcontext): 537 return 538 self.texPrimary, self.texOverlay = OpenGL.GL.glGenTextures(2) 539 gldrawable.gl_end() 540 except: 541 traceback.print_exc() 542 self.opengl = False 543 self.resize_event() # windows seems to have it backwards?
544 - def resize_event(self, *args):
545 if not self.window: 546 return 547 w, h = self.allocation.width, self.allocation.height 548 if (w > 0) and (h > 0): 549 pow2w = 2**int(ceil(log(w)/log(2))) 550 pow2h = 2**int(ceil(log(h)/log(2))) 551 self.tex_use_w = w * 1.0 / pow2w 552 self.tex_use_h = h * 1.0 / pow2h 553 self.pow2w, self.pow2h = pow2w, pow2h 554 self.surfCairo = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) 555 self.pixOverlay = (ctypes.c_ubyte * pow2w * pow2h * 4)() 556 self.surfOverlay = cairo.ImageSurface.create_for_data(self.pixOverlay, cairo.FORMAT_ARGB32, pow2w, pow2h, 4*pow2w) 557 558 if self.opengl: 559 try: 560 gldrawable = self.get_gl_drawable() 561 glcontext = self.get_gl_context() 562 if not gldrawable.gl_begin(glcontext): 563 return 564 565 OpenGL.GL.glViewport(0, 0, w, h) 566 567 def init_tex(): 568 OpenGL.GL.glPixelStorei(OpenGL.GL.GL_UNPACK_ALIGNMENT, 1) 569 OpenGL.GL.glTexParameteri(OpenGL.GL.GL_TEXTURE_2D, OpenGL.GL.GL_TEXTURE_WRAP_S, OpenGL.GL.GL_CLAMP) 570 OpenGL.GL.glTexParameteri (OpenGL.GL.GL_TEXTURE_2D, OpenGL.GL.GL_TEXTURE_WRAP_T, OpenGL.GL.GL_CLAMP) 571 OpenGL.GL.glTexParameteri (OpenGL.GL.GL_TEXTURE_2D, OpenGL.GL.GL_TEXTURE_MAG_FILTER, OpenGL.GL.GL_NEAREST) 572 OpenGL.GL.glTexParameteri (OpenGL.GL.GL_TEXTURE_2D, OpenGL.GL.GL_TEXTURE_MIN_FILTER, OpenGL.GL.GL_NEAREST)
573 574 if (w > 0) and (h > 0): 575 OpenGL.GL.glBindTexture(OpenGL.GL.GL_TEXTURE_2D, self.texPrimary) 576 init_tex() 577 OpenGL.GL.glTexImage2D(OpenGL.GL.GL_TEXTURE_2D, 0, OpenGL.GL.GL_RGBA8, pow2w, pow2h, 0, OpenGL.GL.GL_BGRA, OpenGL.GL.GL_UNSIGNED_BYTE, None) 578 OpenGL.GL.glBindTexture(OpenGL.GL.GL_TEXTURE_2D, self.texOverlay) 579 init_tex() 580 OpenGL.GL.glTexImage2D(OpenGL.GL.GL_TEXTURE_2D, 0, OpenGL.GL.GL_RGBA8, pow2w, pow2h, 0, OpenGL.GL.GL_BGRA, OpenGL.GL.GL_UNSIGNED_BYTE, None) 581 582 gldrawable.gl_end() 583 except: 584 traceback.print_exc() 585 self.opengl = False 586 qubx.pyenv.env.gc_collect_on_idle() 587 self.redraw_canvas()
588 - def find_layer(self, x, y):
589 """Finds the L{Layer} at coords x,y; returns (Layer, x_in_layer, y_in_layer); Layer may be None.""" 590 lxpp, lypp = self._layer_scale 591 lx, ly = x*lxpp, y*lypp 592 for layer in reversed(self._layers): 593 if layer.pt_in_rect(lx, ly): 594 return layer, lx-layer.x, ly-layer.y 595 return None, x, y
596 - def __onSetFontSize(self, font_size):
597 self.redraw_canvas(invalid_layers=True)
598 - def __onSetColor(self, nm, val):
599 self.redraw_canvas(invalid_layers=True)
600 - def __onSetLineWidth(self, val):
601 self.redraw_canvas(invalid_layers=False)
602 - def button_press(self, widget, event):
603 global gtkHas2Click 604 self._button_layer, bx, by = self.find_layer(event.x, event.y) 605 if self._button_layer: 606 self._button_layer.button_press(bx, by, event) 607 elif event.button == 3: 608 pass 609 elif event.type == gdk._2BUTTON_PRESS: 610 gtkHas2Click = True 611 self.OnDblClick(event.x, event.y, event) 612 elif (not gtkHas2Click) and ((event.time - self.__lastPressTime) < DBL_CLICK_MS): 613 self.OnDblClick(event.x, event.y, event) 614 else: 615 self._dragging = (event.button==1) 616 self.OnPress(event.x, event.y, event) 617 if not gtkHas2Click: 618 self.__lastPressTime = event.time 619 return False
620 - def button_release(self, widget, event):
621 if self._button_layer: 622 lxpp, lypp = self._layer_scale 623 self._button_layer.button_release(lxpp*event.x-self._button_layer.x, lypp*event.y-self._button_layer.y, event) 624 self._button_layer = None 625 self.motion_notify(widget, event) # in case already outside bounds 626 elif (event.button == 3) or (event.state & gdk.CONTROL_MASK): 627 self.OnNeedPopup(event.x, event.y, event) 628 self._dragging = False 629 else: 630 self.release_button(event) 631 return False
632 - def release_button(self, event):
633 if self._dragging: 634 self._dragging = False 635 self.OnRelease(event.x, event.y, event)
636 - def motion_notify(self, widget, event):
637 lxpp, lypp = self._layer_scale 638 lx, ly = lxpp * event.x, lypp * event.y 639 if self._button_layer: 640 self._button_layer.motion_notify(lx-self._button_layer.x, ly-self._button_layer.y, event) 641 elif self._mouse_layer: 642 if self._mouse_layer.pt_in_rect(event.x, event.y): 643 self._mouse_layer.motion_notify(lx-self._mouse_layer.x, ly-self._mouse_layer.y, event) 644 else: 645 self._mouse_layer.mouse_exit() 646 self._mouse_layer = None 647 if self._tool and self._tool.cursor: 648 self.window.set_cursor(self._tool.cursor) 649 elif self._dragging: 650 self.OnDrag(event.x, event.y, event) 651 if not self._mouse_layer: 652 self._mouse_layer, mx, my = self.find_layer(event.x, event.y) 653 if self._mouse_layer: 654 self._mouse_layer.mouse_enter() 655 self._mouse_layer.motion_notify(mx, my, event) 656 if self._tool and self._tool.cursor: 657 self.window.set_cursor(None) 658 elif not self._dragging: 659 self.OnRoll(event.x, event.y, event)
660
661 - def scroll(self, widget, event):
662 offset = (event.direction == gdk.SCROLL_UP) and 1 or -1 663 if self._mouse_layer: 664 lxpp, lypp = self._layer_scale 665 self._mouse_layer.scroll(lxpp*event.x-self._mouse_layer.x, lypp*event.y-self._mouse_layer.y, event, offset) 666 else: 667 self.OnScroll(event.x, event.y, event, offset)
668 - def key_press(self, widget, event):
669 if qubx.pyenv.env.globals['global_key_press'](widget, event): 670 return True 671 self.OnKeyPress(event) 672 return True
673 - def key_release(self, widget, event):
674 self.OnKeyRelease(event) 675 return True
676 - def _expose(self, widget, event):
677 if not self.window: 678 return 679 w, h = self.window.get_size() 680 self._dim = (w, h) 681 if (w<1) or (h<1): return 682 if self.opengl and self.appearance.opengl: 683 gldrawable = self.get_gl_drawable() 684 glcontext = self.get_gl_context() 685 if not gldrawable.gl_begin(glcontext): 686 return 687 688 OpenGL.GL.glEnable(OpenGL.GL.GL_BLEND) 689 OpenGL.GL.glBlendFunc(OpenGL.GL.GL_SRC_ALPHA, OpenGL.GL.GL_ONE_MINUS_SRC_ALPHA) 690 OpenGL.GL.glMatrixMode(OpenGL.GL.GL_PROJECTION) 691 OpenGL.GL.glLoadIdentity() 692 OpenGL.GL.glOrtho(0, w, 0, h, -1, 1) 693 OpenGL.GL.glMatrixMode(OpenGL.GL.GL_MODELVIEW) 694 OpenGL.GL.glLoadIdentity() 695 OpenGL.GL.glClearColor(0.0, 0.0, 0.0, 1.0) 696 OpenGL.GL.glClear(OpenGL.GL.GL_COLOR_BUFFER_BIT | OpenGL.GL.GL_DEPTH_BUFFER_BIT) 697 698 OpenGL.GL.glTexEnvf( OpenGL.GL.GL_TEXTURE_ENV, OpenGL.GL.GL_TEXTURE_ENV_MODE, OpenGL.GL.GL_MODULATE ) 699 700 def draw_whole_rect(): 701 OpenGL.GL.glBegin(OpenGL.GL.GL_QUADS) 702 OpenGL.GL.glTexCoord2f(0, 0) 703 OpenGL.GL.glVertex3f(0, 0, 0) 704 OpenGL.GL.glTexCoord2f(self.tex_use_w, 0) 705 OpenGL.GL.glVertex3f(w, 0, 0) 706 OpenGL.GL.glTexCoord2f(self.tex_use_w, self.tex_use_h) 707 OpenGL.GL.glVertex3f(w, h, 0) 708 OpenGL.GL.glTexCoord2f(0, self.tex_use_h) 709 OpenGL.GL.glVertex3f(0, h, 0) 710 OpenGL.GL.glEnd()
711 712 if not self._invalid: 713 OpenGL.GL.glEnable(OpenGL.GL.GL_TEXTURE_2D) 714 OpenGL.GL.glBlendFunc(OpenGL.GL.GL_ONE, OpenGL.GL.GL_ZERO) 715 OpenGL.GL.glBindTexture(OpenGL.GL.GL_TEXTURE_2D, self.texPrimary) 716 OpenGL.GL.glColor4f(1,1,1,1) 717 draw_whole_rect() 718 OpenGL.GL.glDisable(OpenGL.GL.GL_TEXTURE_2D) 719 OpenGL.GL.glBlendFunc(OpenGL.GL.GL_SRC_ALPHA, OpenGL.GL.GL_ONE_MINUS_SRC_ALPHA) 720 721 # flip everything but texPrimary 722 OpenGL.GL.glScalef(1.0, -1.0, 1.0) # y increasing downwards 723 OpenGL.GL.glTranslatef(0.0, -h, 0.0) # y increasing downwards 724 OpenGL.GL.glTranslatef(0.375, 0.375, 0.0) # to pixel edges 725 726 if self._invalid: 727 self.OnDrawGL(gldrawable, glcontext, w, h) 728 #OpenGL.GL.glReadBuffer(OpenGL.GL.GL_BACK) 729 OpenGL.GL.glBindTexture(OpenGL.GL.GL_TEXTURE_2D, self.texPrimary) 730 OpenGL.GL.glCopyTexSubImage2D(OpenGL.GL.GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h) 731 #OpenGL.GL.glCopyTexImage2D(OpenGL.GL.GL_TEXTURE_2D, 0, OpenGL.GL.GL_BGRA, 0, 0, w, h, 0) 732 733 context = cairo.Context(self.surfOverlay) 734 context.set_operator(cairo.OPERATOR_CLEAR) 735 context.paint() 736 context.set_operator(cairo.OPERATOR_OVER) 737 self.draw_all_overlays(context, w, h) 738 739 OpenGL.GL.glBindTexture(OpenGL.GL.GL_TEXTURE_2D, self.texOverlay) 740 OpenGL.GL.glTexSubImage2D(OpenGL.GL.GL_TEXTURE_2D, 0, 0, 0, self.pow2w, self.pow2h, OpenGL.GL.GL_BGRA, OpenGL.GL.GL_UNSIGNED_BYTE, self.pixOverlay) 741 #OpenGL.GL.glTexImage2D(OpenGL.GL.GL_TEXTURE_2D, 0, OpenGL.GL.GL_RGBA, self.pow2w, self.pow2h, 0, OpenGL.GL.GL_BGRA, OpenGL.GL.GL_UNSIGNED_BYTE, self.pixOverlay) 742 OpenGL.GL.glEnable(OpenGL.GL.GL_TEXTURE_2D) 743 OpenGL.GL.glColor4f(1,1,1,1) 744 draw_whole_rect() 745 OpenGL.GL.glDisable(OpenGL.GL.GL_TEXTURE_2D) 746 747 if self.can_focus and self.__focused: 748 OpenGL.GL.glColor4f(*self.appearance.color(COLOR_FOCUSED)) 749 OpenGL.GL.glLineWidth(0.5*self.appearance.emsize) 750 OpenGL.GL.glBegin(OpenGL.GL.GL_LINE_STRIP) 751 OpenGL.GL.glVertex3f(0, 0, 0) 752 OpenGL.GL.glVertex3f(0, h, 0) 753 OpenGL.GL.glVertex3f(w, h, 0) 754 OpenGL.GL.glVertex3f(w, 0, 0) 755 OpenGL.GL.glVertex3f(0, 0, 0) 756 OpenGL.GL.glEnd() 757 758 if gldrawable.is_double_buffered(): 759 gldrawable.swap_buffers() 760 else: 761 OpenGL.GL.glFlush() 762 763 gldrawable.gl_end() 764 765 else: # cairo: 766 767 context = self.window.cairo_create() 768 if self._invalid: 769 subctx = cairo.Context(self.surfCairo) 770 self.appearance.setup_context(subctx) 771 subctx.set_operator(cairo.OPERATOR_CLEAR) 772 subctx.paint() 773 subctx.set_operator(cairo.OPERATOR_OVER) 774 #subctx.rectangle(0, 0, w, h) 775 #subctx.clip() 776 self.OnDraw(subctx, w, h) 777 778 subctx = cairo.Context(self.surfOverlay) 779 #subctx.rectangle(0, 0, w, h) 780 #subctx.clip() 781 subctx.set_source_rgba(0,0,0,1) 782 subctx.paint() 783 subctx.set_source_surface(self.surfCairo, 0, 0) 784 subctx.paint() 785 self.draw_all_overlays(subctx, w, h) 786 context.set_source_surface(self.surfOverlay, 0, 0) 787 context.paint() 788 if self.can_focus and self.__focused: 789 context.set_source_rgba(*self.appearance.color(COLOR_FOCUSED)) 790 context.set_line_width(0.5*self.appearance.emsize) 791 context.rectangle(0, 0, w, h) 792 context.stroke() 793 self._invalid = False
794 - def draw_all_overlays(self, context, w, h):
795 self.appearance.setup_context(context) 796 context.save() 797 self.OnOverlay(context, w, h) 798 context.restore() 799 # pass 1: find minimum dimensions, scale if too big 800 # (layer rq_* in em units) 801 max_pos_x = max_pos_y = 0 802 min_neg_x = min_neg_y = 0 803 for layer in self._layers: 804 if layer.rq_x >= 0: 805 max_pos_x = max(max_pos_x, layer.rq_x + layer.w_min) 806 else: 807 min_neg_x = min(min_neg_x, layer.rq_x) 808 if layer.rq_y >= 0: 809 max_pos_y = max(max_pos_y, layer.rq_y + layer.h_min) 810 else: 811 min_neg_y = min(min_neg_y, layer.rq_y) 812 # extra em separation between pos and neg layers if both sides populated 813 sep_x = (max_pos_x and min_neg_x) and 1 or 0 814 sep_y = (max_pos_y and min_neg_y) and 1 or 0 815 layer_dx = (max_pos_x - min_neg_x + sep_x) * self.appearance.emsize 816 layer_dy = (max_pos_y - min_neg_y + sep_y) * self.appearance.emsize 817 layers_w, layers_h = max(w, layer_dx), max(h, layer_dy) 818 (layx_per_pix, layy_per_pix) = (layers_w * 1.0 / w, layers_h * 1.0 / h) 819 if (layx_per_pix, layy_per_pix) != self._layer_scale: 820 self._layer_scale = (layx_per_pix, layy_per_pix) 821 for layer in self._layers: 822 layer.invalidate() 823 # pass 2: render 824 context.save() 825 context.scale(1.0/layx_per_pix, 1.0/layy_per_pix) 826 for layer in self._layers: 827 context.save() 828 layer.render(layers_w, layers_h, self.appearance) 829 context.translate(layer.x, layer.y) 830 context.set_source_surface(layer.surface) 831 context.rectangle(0, 0, layer.w, layer.h) 832 context.fill() 833 context.restore() 834 context.restore()
835 - def draw_to_drawable(self, drawable, px0=0, py0=0, width=0, height=0, overlay=True):
836 """Draws everything to drawable with origin px0,py0, resizing if you specify width and height.""" 837 dw, dh = drawable.get_size() 838 w = width or dw 839 h = height or dh 840 context = drawable.cairo_create() 841 self.appearance.setup_context(context) 842 if px0 or py0: 843 context.translate(px0, py0) 844 self.draw_to_context(context, w, h, overlay=overlay)
845 - def draw_to_context(self, context, width, height, fat=1.0, unit_width=0.0, overlay=True):
846 """Draws everything to a cairo context with width and height. 847 @param fat: line width scaler 848 @param unit_width: kluge for printing 849 @param overlay: include overlays and layers (False: just OnDraw) 850 """ 851 save_dim = self._dim 852 self._dim = width, height 853 f, u = self._fat, self._unit_width 854 self._fat, self._unit_width = fat, unit_width 855 w, h = width, height 856 self.appearance.setup_context(context) 857 context.save() 858 ##if self.opengl_dimension: TODO read and composit texPrimary 859 ##else: TODO composit surfCairo 860 context.set_source_rgb(1, 1, 1) 861 context.rectangle(0, 0, w, h) 862 context.fill() 863 self.OnDraw(context, w, h) 864 context.restore() 865 if overlay: 866 self.draw_all_overlays(context, w, h) 867 # restore non-printing line thickness 868 self._fat, self._unit_width = f, u 869 self._dim = save_dim
870 871 GLSpace = ToolSpace 872 HAVE_OPENGL = False 873 if False: 874 try: 875 import __main__ 876 import os 877 try: 878 glprefs_path = os.path.join(__main__.QUBX_HOME_PATH, 'Presets', 'Appearance', '__active.qpr') 879 try: 880 glprefs = qubx.tree.Open(glprefs_path, True).find('opengl') 881 wanted = glprefs.data and glprefs.data[0] 882 except: 883 wanted = True 884 if wanted: 885 import OpenGL 886 OpenGL.ERROR_CHECKING = False 887 import gtk.gtkgl 888 import gtk.gdkgl 889 import OpenGL.GL 890 import OpenGL.GLU 891 HAVE_OPENGL = True
892 - class ToolSpace_GL(ToolSpace, gtk.gtkgl.Widget):
893 pass
894 GLSpace = ToolSpace_GL 895 finally: 896 try: 897 del glprefs 898 except: 899 pass 900 except: 901 traceback.print_exc() 902 903 904
905 -class LayerSet(object):
906 - def __init__(self, layers=[]):
907 self.layers = layers[:] 908 self.__space = None
909 space = property(lambda self: self.__space and self.__space())
910 - def activate(self, space):
911 self.__space = weakref.ref(space) 912 for layer in self.layers: 913 space.add_layer(layer) 914 space.redraw_canvas(True)
915 - def deactivate(self, space):
916 for layer in self.layers: 917 try: 918 space.remove_layer(layer) 919 except: 920 traceback.print_exc() 921 printer(layer.__dict__) 922 space.redraw_canvas(True) 923 self.__space = None
924 - def add_layer(self, layer):
925 if not (layer in self.layers): 926 self.layers.append(layer) 927 space = self.space 928 if space: 929 space.add_layer(layer) 930 else: 931 traceback.print_stack() 932 print 'Warning: double-add of layer'
933 - def remove_layer(self, layer):
934 self.layers.remove(layer) 935 space = self.space 936 if space: 937 space.remove_layer(layer)
938
939 -def activate_cursor(space, cursor, counter=0):
940 if space.window: 941 space.window.set_cursor(cursor) 942 elif counter < 100: 943 gobject.idle_add(activate_cursor, space, cursor, counter+1)
944
945 -class Tool(object):
946 """Receives mouse events from a L{ToolSpace}. Can draw an overlay. 947 948 @ivar space: when active, the L{ToolSpace}; otherwise None 949 @ivar layers: list of L{Layer}s to add to space when active 950 @ivar cursor: gdk.Cursor or None 951 """
952 - def __init__(self):
953 self.ref = Reffer() 954 self.layers = [] 955 self.__space = None 956 self.cursor = None
957 space = property(lambda self: self.__space and self.__space())
958 - def activate(self, space):
959 self.__space = weakref.ref(space) 960 space.OnOverlay += self.ref(self.onOverlay), 'Tool.onOverlay' 961 space.OnPress += self.ref(self.onPress), 'Tool.onPress' 962 space.OnRelease += self.ref(self.onRelease), 'Tool.onRelease' 963 space.OnDblClick += self.ref(self.onDblClick), 'Tool.onDblClick' 964 space.OnDrag += self.ref(self.onDrag), 'Tool.onDrag' 965 space.OnRoll += self.ref(self.onRoll), 'Tool.onRoll' 966 space.OnScroll += self.ref(self.onScroll), 'Tool.onScroll' 967 space.OnKeyPress += self.ref(self.onKeyPress) 968 space.OnKeyRelease += self.ref(self.onKeyRelease) 969 space.OnNeedPopup += self.ref(self.onNeedPopup), 'Tool.onNeedPopup' 970 for layer in self.layers: 971 space.add_layer(layer) 972 if self.cursor: 973 activate_cursor(space, self.cursor) 974 self.onActivate()
975 - def deactivate(self, space):
976 self.onDeactivate() 977 space.OnOverlay -= self.ref(self.onOverlay) 978 space.OnPress -= self.ref(self.onPress) 979 space.OnRelease -= self.ref(self.onRelease) 980 space.OnDblClick -= self.ref(self.onDblClick) 981 space.OnDrag -= self.ref(self.onDrag) 982 space.OnRoll -= self.ref(self.onRoll) 983 space.OnScroll -= self.ref(self.onScroll) 984 space.OnKeyPress -= self.ref(self.onKeyPress) 985 space.OnKeyRelease -= self.ref(self.onKeyRelease) 986 space.OnNeedPopup -= self.ref(self.onNeedPopup) 987 for layer in self.layers: 988 space.remove_layer(layer) 989 if self.cursor and space.window: 990 space.window.set_cursor(None) 991 self.__space = None
992 - def onActivate(self):
993 self.space.redraw_canvas(False)
994 - def onDeactivate(self):
995 self.space.redraw_canvas(False)
996 - def onOverlay(self, context, w, h):
997 pass
998 - def onPress(self, x, y, e):
999 pass
1000 - def onRelease(self, x, y, e):
1001 pass
1002 - def onDblClick(self, x, y, e):
1003 pass
1004 - def onDrag(self, x, y, e):
1005 pass
1006 - def onRoll(self, x, y, e):
1007 pass
1008 - def onScroll(self, x, y, e, amount):
1009 pass
1010 - def onKeyPress(self, event):
1011 pass
1012 - def onKeyRelease(self, event):
1013 pass
1014 - def onNeedPopup(self, x, y, e):
1015 # by default treat it like an ordinary click 1016 self.onPress(x, y, e) 1017 self.onRelease(x, y, e)
1018 1019 1020
1021 -class Layer(object):
1022 """A floating, transparent, clickable area in a L{ToolSpace}. 1023 1024 @ivar appearance: L{TS_Appearance} of the L{ToolSpace} 1025 @ivar rq_x: requested x coord, in units of appearance.emsize 1026 @ivar rq_y: requested y coord, in units of appearance.emsize 1027 @ivar rq_w: requested width, in units of appearance.emsize; negative to specify distance from right edge 1028 @ivar rq_h: requested height, in units of appearance.emsize; negative to specify distance from bottom 1029 @ivar w_min: minimum requested width in ems, if rq_w is negative 1030 @ivar h_min: minimum requested height in ems, if rq_h is negative 1031 @ivar x: actual x coord, in pixels 1032 @ivar y: actualy coord, in pixels 1033 @ivar w: actual width, in pixels 1034 @ivar h: actual height, in pixels 1035 @ivar cBG: background COLORREF 1036 @ivar border: width of border in pixels 1037 @ivar cBorder: border COLORREF 1038 @ivar subs: list of L{SubLayer} 1039 @ivar surface: cairo image surface for double buffering 1040 @ivar invalid: True if surface needs to be repainted 1041 @ivar OnInvalidate: L{WeakEvent}(Layer) called when it needs repainting 1042 """
1043 - def __init__(self, x=0, y=0, w=10, h=10, cBG=LAYER_BG, border=0, cBorder=LAYER_BORDER, 1044 w_min=1, h_min=1):
1045 """ 1046 @param x: x >= 0: number of 'M' widths from the ToolSpace's left edge; x < 0: number of ... ToolSpace's right edge 1047 @param y: y >= 0: number of 'M' widths from the ToolSpace's top edge; y < 0: number of ... ToolSpace's bottom edge 1048 @param w: w >= 0: number of 'M' widths across; w < 0: number of 'M's between Layer's right and ToolSpace's right 1049 @param h: h >= 0: number of 'M' widths tall; h < 0: number of 'M's between Layer's bottom and ToolSpace's bottom 1050 @param cBG: COLORREF of background 1051 @param border: width in pixels of border 1052 @param cBorder: COLORREF of border 1053 @param w_min: minimum number of 'M' widths across, if w is negative 1054 @param h_min: minimum number of 'M' widths tall, if h is negative 1055 """ 1056 self.__ref = Reffer() 1057 self.appearance = Appearance 1058 self.rq_x = x 1059 self.rq_y = y 1060 self.rq_w = w 1061 self.rq_h = h 1062 self.rq_w_min = w_min if (w < 0) else w 1063 self.rq_h_min = h_min if (h < 0) else h 1064 self.__x, self.__y = x, y 1065 self.__w, self.__h = w, h 1066 self.cBG = cBG 1067 self.border = border 1068 self.cBorder = cBorder 1069 self.subs = [] 1070 self.sub = None 1071 self.__sub_hover = None 1072 self.surface = None 1073 self.invalid = True 1074 self.OnInvalidate = WeakEvent() # (Layer) 1075 self.__space = None
1076 - def set_space(self, space):
1077 if space is None: 1078 self.__space = None 1079 else: 1080 self.__space = weakref.ref(space)
1081 space = property(lambda self: self.__space and self.__space(), lambda self, x: self.set_space(x))
1082 - def set_x(self, x):
1083 if self.rq_x == x: return 1084 self.rq_x = x 1085 self.invalidate()
1086 - def set_y(self, y):
1087 if self.rq_y == y: return 1088 self.rq_y = y 1089 self.invalidate()
1090 - def set_w(self, w):
1091 if self.rq_w == w: return 1092 self.rq_w = w 1093 if w >= 0: 1094 self.rq_w_min = w 1095 self.invalidate()
1096 - def set_h(self, h):
1097 if self.rq_h == h: return 1098 self.rq_h = h 1099 if h >= 0: 1100 self.rq_h_min = h 1101 self.invalidate()
1102 - def set_w_min(self, w):
1103 if self.rq_w_min == w: return 1104 self.rq_w_min = w 1105 self.invalidate()
1106 - def set_h_min(self, h):
1107 if self.rq_h_min == h: return 1108 self.rq_h_min = h 1109 self.invalidate()
1110 x = property(lambda self: self.__x, lambda self, x: self.set_x(x)) 1111 y = property(lambda self: self.__y, lambda self, x: self.set_y(x)) 1112 w = property(lambda self: self.__w, lambda self, x: self.set_w(x)) 1113 h = property(lambda self: self.__h, lambda self, x: self.set_h(x)) 1114 w_min = property(lambda self: self.rq_w_min, lambda self, x: self.set_w_min(x)) 1115 h_min = property(lambda self: self.rq_h_min, lambda self, x: self.set_h_min(x))
1116 - def pt_in_rect(self, x, y):
1117 """Returns True if x,y is inside this layer.""" 1118 return (self.__x <= x < (self.__x + self.__w)) and (self.__y <= y < (self.__y + self.__h))
1119 - def invalidate(self, sub=None):
1120 """Requests repaint.""" 1121 self.invalid = True 1122 self.OnInvalidate(self)
1123 - def __reposition(self, space_w, space_h, appearance, resurface=True):
1124 em = appearance.emsize 1125 self.__x = int(round(em*((self.rq_x < 0) and (space_w*1.0/em + self.rq_x) or self.rq_x))) 1126 self.__y = int(round(em*((self.rq_y < 0) and (space_h*1.0/em + self.rq_y) or self.rq_y))) 1127 w = max(0, min(16384, int(round(em*((self.rq_w < 0) and (space_w*1.0/em - self.__x*1.0/em + self.rq_w) or self.rq_w))))) 1128 h = max(0, min(16384, int(round(em*((self.rq_h < 0) and (space_h*1.0/em - self.__y*1.0/em + self.rq_h) or self.rq_h))))) 1129 if (w != self.__w) or (h != self.__h) or not self.surface: 1130 self.__w = w 1131 self.__h = h 1132 if resurface: 1133 self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) 1134 self.invalid = True 1135 for sub in self.subs: 1136 if sub.rq_x < 0: 1137 sub.set_actual_x(w*1.0/em + sub.rq_x) 1138 if sub.rq_y < 0: 1139 sub.set_actual_y(h*1.0/em + sub.rq_y)
1140 - def render(self, space_w, space_h, appearance):
1141 """Repaints (and repositions) surface, if needed.""" 1142 self.__reposition(space_w, space_h, appearance) 1143 if (not self.invalid) and (self.appearance == appearance): 1144 return 1145 self.appearance = appearance 1146 self.invalid = False 1147 context = cairo.Context(self.surface) 1148 appearance.setup_context(context) 1149 context.set_operator(cairo.OPERATOR_SOURCE) 1150 context.set_source_rgba(0,0,0,0) 1151 context.paint() 1152 context.set_operator(cairo.OPERATOR_OVER) 1153 context.translate(-self.x, -self.y) 1154 self.render_to(context, space_w, space_h, appearance)
1155 - def render_to(self, context, space_w, space_h, appearance):
1156 """Paints directly into a cairo context, as for copy/print.""" 1157 saved = self.appearance, self.__w, self.__h, self.__x, self.__y 1158 self.appearance = appearance 1159 self.__reposition(space_w, space_h, appearance, resurface=False) 1160 w, h = self.__w, self.__h 1161 context.translate(self.x, self.y) 1162 1163 context.save() 1164 context.rectangle(0, 0, w, h) 1165 if self.border: 1166 context.set_line_width(self.border) 1167 context.set_source_rgba(* appearance.color(self.cBorder)) 1168 context.stroke_preserve() 1169 context.set_source_rgba(* self.appearance.color(self.cBG)) 1170 context.fill() 1171 context.restore() 1172 context.set_source_rgba(* self.appearance.color(LAYER_FG)) 1173 1174 context.save() 1175 self.draw(context) 1176 context.restore() 1177 1178 em = appearance.emsize 1179 for sub in reversed(self.subs): 1180 context.save() 1181 context.translate(em*sub.x, em*sub.y) 1182 sub.draw(context, w, h, appearance) 1183 context.restore() 1184 self.appearance, self.__w, self.__h, self.__x, self.__y = saved
1185 - def add_sublayer(self, sub):
1186 """Adds a L{SubLayer} (label or control).""" 1187 self.subs.insert(0, sub) 1188 sub.layer = self 1189 sub.OnInvalidate += self.__ref(self.invalidate) 1190 self.invalidate() 1191 self.__w = 0 # force reposition 1192 return sub
1193 - def remove_sublayer(self, sub):
1194 i = self.subs.index(sub) 1195 del self.subs[i] 1196 sub.layer = None 1197 sub.OnInvalidate -= self.__ref(self.invalidate) 1198 self.invalidate()
1199 - def clear_sublayers(self):
1200 """Removes all sublayers.""" 1201 for sub in self.subs: 1202 sub.layer = None 1203 sub.OnInvalidate -= self.__ref(self.invalidate) 1204 self.subs = [] 1205 self.invalidate()
1206 - def draw(self, context):
1207 """Override this method to draw on the layer itself.""" 1208 pass
1209 - def on_show(self):
1210 """Override this method to do something when the layer is added to a L{ToolSpace}.""" 1211 pass
1212 - def on_hide(self):
1213 """Override this method to do something when the layer is removed from a L{ToolSpace}.""" 1214 pass
1215 - def mouse_enter(self):
1216 """Override this method to do something when the mouse enters the layer.""" 1217 pass
1218 - def mouse_exit(self):
1219 """Override this method to do something when the mouse leaves the layer.""" 1220 self.sub_hover = None
1221 - def button_press(self, x, y, event):
1222 """Override this method to handle mouse-down. If not handled, return True.""" 1223 return self.sub_press(x, y, event)
1224 - def find_sub(self, x, y):
1225 """Returns the L{SubLayer} at local coordinates (x, y), or None.""" 1226 em = self.appearance.emsize 1227 for sub in self.subs: 1228 if sub.pt_in_rect(x, y, em): 1229 return sub 1230 return None
1231 - def sub_press(self, x, y, event):
1232 """If there is a L{SubLayer} at (x,y), passes it the event and returns False. (True if there's no sub)""" 1233 self.sub = self.find_sub(x, y) 1234 if self.sub: 1235 em = self.appearance.emsize 1236 self.sub.button_press(x-em*self.sub.x, y-em*self.sub.y, event) 1237 return False 1238 return True
1239 - def button_release(self, x, y, event):
1240 """Override this method to handle mouse-up. If not handled, return True""" 1241 return self.sub_release(x, y, event)
1242 - def sub_release(self, x, y, event):
1243 """If there is a L{SubLayer} at (x,y), passes it the event and returns False. (True if there's no sub)""" 1244 if self.sub: 1245 em = self.appearance.emsize 1246 self.sub.button_release(x-em*self.sub.x, y-em*self.sub.y, event) 1247 self.sub = None 1248 return False 1249 return True
1250 - def set_sub_hover(self, sub_hover, x=0, y=0, event=None):
1251 if self.__sub_hover == sub_hover: return 1252 if self.__sub_hover: 1253 self.__sub_hover.exit() 1254 self.__sub_hover = sub_hover 1255 if sub_hover: 1256 em = self.appearance.emsize 1257 sub_hover.enter(x and (x-em*sub_hover.x), y and (y-em*sub_hover.y), event) 1258 space = self.space 1259 if space: 1260 space.tooltip.sublayer = sub_hover
1261 sub_hover = property(lambda self: self.__sub_hover, lambda self, x: self.set_sub_hover(x))
1262 - def motion_notify(self, x, y, event):
1263 """Override this method to handle mouse motion.""" 1264 em = self.appearance.emsize 1265 if self.sub: 1266 self.sub.mouse_drag(x-em*self.sub.x, y-em*self.sub.y, event) 1267 return 1268 if self.sub_hover: 1269 if not self.sub_hover.pt_in_rect(x, y, em): 1270 self.sub_hover = None 1271 if not self.sub_hover: 1272 self.set_sub_hover(self.find_sub(x, y), x, y, event) 1273 if self.sub_hover: 1274 self.sub_hover.mouse_move(x-em*self.sub_hover.x, y-em*self.sub_hover.y, event)
1275 - def scroll(self, x, y, event, offset):
1276 """Override this event to handle the mouse scroll wheel.""" 1277 em = self.appearance.emsize 1278 for sub in self.subs: 1279 if sub.pt_in_rect(x, y, em): 1280 sub.scroll(x-em*sub.x, y-em*sub.y, event, offset) 1281 return
1282 1283
1284 -def ignore_event(*args):
1285 pass
1286
1287 -class SubLayer(object):
1288 """A specific display element or control on a L{Layer} in a L{ToolSpace}. 1289 1290 @ivar x: number of 'M' widths between Layer's left and SubLayer's left 1291 @ivar y: number of 'M' widths between Layer's top and SubLayer's top 1292 @ivar rq_w: requested width, in 'M' units 1293 @ivar rq_h: requested height, in 'M' units 1294 @ivar w: actual width, in pixels 1295 @ivar h: actual height, in pixels 1296 @ivar action: when clicked, calls action(x, y, event) 1297 @ivar scroll: when mouse-scroll-wheeled upon, calls scroll(x, y, event, offset) 1298 @ivar mouse_move: when the mouse moves over it, calls mouse_move(x, y, event) 1299 @ivar mouse_drag: when the mouse moves with the button down, calls mouse_drag(x, y, event) 1300 @ivar enter: when the mouse enters, calls mouse_enter(x, y, event) 1301 @ivar exit: when the mouse leaves, calls mouse_exit() 1302 @ivar border: width of border in pixels 1303 @ivar cBorder: border COLORREF 1304 @ivar cBG: background COLORREF, or None for transparent 1305 @ivar invalid: True if it needs to be repainted 1306 @ivar OnInvalidate: L{WeakEvent}(SubLayer) called when it needs repainting 1307 @ivar OnChangeTooltip: L{WeakEvent}(SubLayer) 1308 @ivar OnHideTooltip: L{WeakEvent}(SubLayer) subclasses can call to request tooltip hide 1309 """
1310 - def __init__(self, x=0, y=0, w=1, h=1, action=ignore_event, scroll=ignore_event, 1311 mouse_move=ignore_event, mouse_drag=ignore_event, 1312 enter=ignore_event, exit=ignore_event, 1313 border=0, cBorder=LAYER_BORDER, tooltip="", cBG=None, **kw):
1314 """ 1315 @param x: number of 'M' widths from the Layer's left edge 1316 @param y: number of 'M' widths from the Layer's top edge 1317 @param w: w >= 0: number of 'M' widths across; w < 0: number of 'M's between SubLayer's right and Layer's right 1318 @param h: h >= 0: number of 'M' widths tall; h < 0: number of 'M's between SubLayer's bottom and Layer's bottom 1319 @param action: when clicked, calls action(x, y, event) 1320 @param scroll: when mouse-scroll-wheeled upon, calls scroll(x, y, event, offset) 1321 @param mouse_move: when the mouse moves over it, calls mouse_move(x, y, event) 1322 @param mouse_drag: when the mouse moves with the button down, calls mouse_drag(x, y, event) 1323 @param enter: when the mouse enters, calls mouse_enter(x, y, event) 1324 @param exit: when the mouse leaves, calls mouse_exit() 1325 @param border: width in pixels of border 1326 @param cBorder: COLORREF of border 1327 @param cBG: COLORREF of background, or None for transparent 1328 @param tooltip: mouse-over info 1329 """ 1330 self.rq_x = x 1331 self.rq_y = y 1332 self.__x = x 1333 self.__y = y 1334 self.rq_w = w 1335 self.rq_h = h 1336 self.__w = w 1337 self.__h = h 1338 self.__tooltip = "" 1339 self.__action = WeakCall("%s.action"%self.__class__) 1340 self.__action.assign(action) 1341 self.__scroll = WeakCall("%s.scroll"%self.__class__) 1342 self.__scroll.assign(scroll) 1343 self.__mouse_move = WeakCall("%s.mouse_move"%self.__class__) 1344 self.__mouse_move.assign(mouse_move) 1345 self.__mouse_drag = WeakCall("%s.mouse_drag"%self.__class__) 1346 self.__mouse_drag.assign(mouse_drag) 1347 self.__enter = WeakCall("%s.enter"%self.__class__) 1348 self.__enter.assign(enter) 1349 self.__exit = WeakCall("%s.exit"%self.__class__) 1350 self.__exit.assign(exit) 1351 self.border = border 1352 self.cBorder = cBorder 1353 self.__cBG = cBG 1354 self.invalid = True 1355 self.OnInvalidate = WeakEvent() # (SubLayer) 1356 self.OnChangeTooltip = WeakEvent() 1357 self.OnHideTooltip = WeakEvent() 1358 self.__layer = None 1359 self.tooltip = tooltip 1360 for k in kw: 1361 self.__dict__[k] = kw[k]
1362 - def set_layer(self, layer):
1363 self.__layer = None if (layer is None) else weakref.ref(layer)
1364 layer = property(lambda self: self.__layer and self.__layer(), lambda self, x: self.set_layer(x)) 1365 action = property(lambda self: self.__action, lambda self, x: self.__action.assign(x)) 1366 scroll = property(lambda self: self.__scroll, lambda self, x: self.__scroll.assign(x)) 1367 mouse_move = property(lambda self: self.__mouse_move, lambda self, x: self.__mouse_move.assign(x)) 1368 mouse_drag = property(lambda self: self.__mouse_drag, lambda self, x: self.__mouse_drag.assign(x)) 1369 enter = property(lambda self: self.__enter, lambda self, x: self.__enter.assign(x)) 1370 exit = property(lambda self: self.__exit, lambda self, x: self.__exit.assign(x))
1371 - def set_x(self, x):
1372 if x != self.__x: 1373 self.__x = self.rq_x = x 1374 self.invalidate()
1375 - def set_y(self, y):
1376 if y != self.__y: 1377 self.__y = self.rq_y = y 1378 self.invalidate()
1379 - def set_w(self, w):
1380 if w != self.__w: 1381 self.rq_w = w 1382 self.invalidate()
1383 - def set_h(self, h):
1384 if h != self.__h: 1385 self.rq_h = h 1386 self.invalidate()
1387 - def set_tooltip(self, x):
1388 self.__tooltip = x 1389 self.OnChangeTooltip(self)
1390 - def set_cBG(self, x):
1391 self.__cBG = x 1392 self.invalidate()
1393 x = property(lambda self: self.__x, lambda self, x: self.set_x(x)) 1394 y = property(lambda self: self.__y, lambda self, x: self.set_y(x)) 1395 w = property(lambda self: self.__w, lambda self, x: self.set_w(x)) 1396 h = property(lambda self: self.__h, lambda self, x: self.set_h(x)) 1397 tooltip = property(lambda self: self.__tooltip, lambda self, x: self.set_tooltip(x)) 1398 cBG = property(lambda self: self.__cBG, lambda self, x: self.set_cBG(x))
1399 - def set_actual_x(self, x):
1400 self.__x = x
1401 - def set_actual_y(self, y):
1402 self.__y = y
1403 - def pt_in_rect(self, x, y, em):
1404 """Returns True if (x, y) Layer-relative coordinates are inside this SubLayer. 1405 @param em: width of 'M' in pixels 1406 """ 1407 return (self.__x*em <= x < (self.__x*em + self.__w)) and (self.__y*em <= y < (self.__y*em + self.__h))
1408 - def invalidate(self):
1409 """Marks this SubLayer as needing a repaint, and calls OnInvalidate.""" 1410 self.invalid = True 1411 self.OnInvalidate(self)
1412 - def draw(self, context, w, h, appearance):
1413 """Override this method to paint the SubLayer. Don't forget to call SubLayer.draw(context, w, h, appearance).""" 1414 self.invalid = False 1415 em = appearance.emsize 1416 self.__w = em*max(1, (self.rq_w < 0) and (w*1.0/em - self.__x + self.rq_w) or self.rq_w) 1417 self.__h = em*max(1, (self.rq_h < 0) and (h*1.0/em - self.__y + self.rq_h) or self.rq_h) 1418 if self.cBG: 1419 context.set_source_rgba(* appearance.color(self.cBG)) 1420 context.rectangle(0, 0, self.__w, self.__h) 1421 context.fill() 1422 if self.border: 1423 context.save() 1424 context.set_source_rgba(* appearance.color(self.cBorder)) 1425 context.set_line_width(self.border) 1426 context.rectangle(0, 0, self.__w, self.__h) 1427 context.stroke() 1428 context.restore()
1429 - def button_press(self, x, y, e):
1430 """Override this method to handle mouse-down (by default, click: action(x,y,e)).""" 1431 pass
1432 - def button_release(self, x, y, e):
1433 """Override this method to handle mouse-up (by default, click: action(x,y,e)).""" 1434 if (0 <= x < self.__w) and (0 <= y < self.__h): 1435 self.action(x, y, e)
1436 1437
1438 -class RotRect(object):
1439 """Represents a rectangle that has been rotated theta radians about (x0, y0). 1440 1441 @ivar l: left 1442 @ivar t: top 1443 @ivar r: right 1444 @ivar b: bottom 1445 @ivar x0: x-coord of the center of rotation 1446 @ivar y0: y-coord of the center of rotation 1447 @ivar theta: radians to rotate about x0, y0 1448 """
1449 - def __init__(self, l, t, r, b, x0=0, y0=0, theta=0, **kw):
1450 """ 1451 @param l: left 1452 @param t: top 1453 @param r: right 1454 @param b: bottom 1455 @param x0: x-coord of the center of rotation 1456 @param y0: y-coord of the center of rotation 1457 @param theta: radians to rotate about x0, y0 1458 @param ...: adds any additional keyword arguments as instance variables 1459 """ 1460 self.l = l 1461 self.t = t 1462 self.r = r 1463 self.b = b 1464 self.x0 = x0 1465 self.y0 = y0 1466 self.theta = theta 1467 for k,v in kw.iteritems(): 1468 self.__dict__[k] = v
1469 - def contains(self, x, y):
1470 """Returns True if (x,y) is within the rotated rectangle.""" 1471 if self.theta or self.x0 or self.y0: 1472 costh, sinth = cos(-self.theta), sin(-self.theta) 1473 x, y = costh*(x-self.x0) - sinth*(y-self.y0), sinth*(x-self.x0) + costh*(y-self.y0) 1474 return (self.l <= x <= self.r) and (self.t <= y <= self.b)
1475 1476
1477 -class OverlayRgnTool(Tool):
1478 """Base class for a Tool which defines RotRect regions of interest. 1479 @ivar start_x: mouse x coordinate where it was invoked 1480 @ivar start_y: mouse y coordinate where it was invoked 1481 @ivar prev_tool: Tool to reinstate when this one is done 1482 @ivar rgns: list of L{RotRect} 1483 @ivar rgn: RotRect currently under the mouse, or None; It redraws when rgn changes. 1484 """
1485 - def __init__(self, x, y, prev_tool):
1486 Tool.__init__(self) 1487 self.start_x, self.start_y = x, y 1488 self.x = self.y = -1 1489 self.prev_tool = prev_tool 1490 self.rgns = [] 1491 self.rgn = None
1492 - def onRoll(self, x, y, e):
1493 self.x, self.y = x, y 1494 rgn = None 1495 for i, r in enumerate(self.rgns): 1496 if r.contains(x, y): 1497 rgn = i 1498 break 1499 if self.rgn != rgn: 1500 self.rgn = rgn 1501 self.space.redraw_canvas(False)
1502 - def onKeyPress(self, e):
1503 if e.keyval in (keysyms.Up, keysyms.Left): 1504 self.prev_rgn() 1505 elif e.keyval in (keysyms.Down, keysyms.Right): 1506 self.next_rgn() 1507 elif e.keyval == keysyms.Tab: 1508 if (e.state & gdk.SHIFT_MASK) or (e.state & gdk.CONTROL_MASK): 1509 self.prev_rgn() 1510 else: 1511 self.next_rgn() 1512 elif e.keyval in (keysyms.space, keysyms.Return): 1513 if not (self.rgn is None): 1514 r = self.rgns[self.rgn] 1515 x, y = (r.l+r.r)/2, (r.t+r.b)/2 1516 self.onPress(x, y, e) 1517 self.onRelease(x, y, e) 1518 elif e.keyval == keysyms.Escape: 1519 self.space.tool = self.prev_tool
1520 - def prev_rgn(self):
1521 i = (len(self.rgns) if (self.rgn is None) else self.rgn) or len(self.rgns) 1522 i -= 1 1523 self.rgn = i 1524 self.space.redraw_canvas(False)
1525 - def next_rgn(self):
1526 i = -1 if (self.rgn is None) else self.rgn 1527 i += 1 1528 if i == len(self.rgns): 1529 i = 0 1530 self.rgn = i 1531 self.space.redraw_canvas(False)
1532 1533
1534 -class OverlayRgnCaptionedTool(OverlayRgnTool):
1535 """Base class for a Tool which presents a message and highlights regions of interest."""
1536 - def __init__(self, x, y, prev_tool, caption):
1537 OverlayRgnTool.__init__(self, x, y, prev_tool) 1538 self.caption = caption 1539 self.rgns = [RotRect(0, 0, 1, 1)] # we reserve rgns[0] for the cancel box
1540 - def onOverlay(self, ctx, w, h):
1541 ctx.save() 1542 mask = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) # transparent black 1543 context = cairo.Context(mask) 1544 context.set_operator(cairo.OPERATOR_SOURCE) 1545 context.set_source_rgba(0,0,0,.3) 1546 context.paint() 1547 1548 self.space.appearance.setup_context(context) 1549 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 1550 th = fascent + fdescent 1551 cap_width = context.text_extents(self.caption)[2] 1552 box_width = cap_width + 3*th 1553 x0, y0 = (w - box_width)/2, th 1554 context.save() 1555 context.translate(x0, y0) 1556 context.set_source_rgba(0,0,0,.75) 1557 context.rectangle(0, 0, box_width, 3*th) 1558 context.fill() 1559 context.set_source_rgba(1, 1, 1, 1) 1560 context.move_to(th, fascent+th) 1561 context.show_text(self.caption) 1562 context.set_source_rgba(.8,0,0,1) 1563 context.rectangle(cap_width+2*th, 0, th, th) 1564 context.fill() 1565 self.rgns[0] = RotRect(x0+cap_width+2*th, y0, x0+cap_width+3*th, y0+th) 1566 context.restore() 1567 1568 context.set_source_rgba(0,0,0,0) 1569 for r in self.rgns[1:]: 1570 context.save() 1571 context.translate(r.x0, r.y0) 1572 context.rotate(r.theta) 1573 context.rectangle(r.l, r.t, r.r-r.l, r.b-r.t) 1574 context.fill() 1575 context.restore() 1576 1577 ctx.set_source_surface(mask) 1578 ctx.paint() 1579 ctx.restore()
1580 1581 1582
1583 -class Spotlight(Tool):
1584 """Demo tool to shine a spotlight on a L{ToolSpace}."""
1585 - def __init__(self):
1586 Tool.__init__(self) 1587 self.x = -100 1588 self.y = -100
1589 - def onRoll(self, x, y, e):
1590 self.x, self.y = x, y 1591 self.space.redraw_canvas(False)
1592 - def onOverlay(self, context, w, h):
1593 context.save() 1594 mask = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) # transparent black 1595 maskex = cairo.Context(mask) 1596 maskex.set_operator(cairo.OPERATOR_SOURCE) 1597 maskex.set_source_rgba(0,0,0,.2) 1598 maskex.paint() 1599 maskex.set_source_rgba(0,0,0,0) 1600 maskex.arc(self.x+4, self.y+6, 15, 0, 2*pi) 1601 maskex.fill() 1602 context.set_source_surface(mask) 1603 context.paint() 1604 context.restore()
1605 1606 1607
1608 -class Layer_Toolbar(Layer):
1609 """Layer containing a row or column of buttons; automatically laid out; spotlighted on rollover with tooltips. 1610 1611 @ivar dim: 0: horizontal; 1: vertical 1612 @ivar toolsub: L{SubLayer} corresponding to currently picked tool 1613 """
1614 - def __init__(self, dim=0, *args, **kw):
1615 Layer.__init__(self, *args, **kw) 1616 self.__ref = Reffer() 1617 self.dim = dim 1618 self.toolsub = None
1619 - def add_tool(self, label, sublayer, tool=None, active=False):
1620 """Adds a L{SubLayer} (button) with its label (tooltip) and associated L{Tool}. 1621 If tool != None, but self.toolsub == None (no tool picked yet), simulates a click on sublayer. 1622 """ 1623 self.add_sublayer(sublayer) 1624 if tool: 1625 listener = sublayer.action._listener() 1626 if listener is None: listener = lambda x,y,e: None 1627 sub_action = weakref.ref(listener) 1628 sublayer.action = self.__ref(lambda x,y,e: self.pick_tool(sublayer, tool, sub_action(), x, y, e)) 1629 1630 sublayer.tooltip = label 1631 listener = sublayer.enter._listener() 1632 if listener is None: listener = lambda x,y,e: None 1633 sub_enter = weakref.ref(listener) 1634 sublayer.enter = self.__ref(lambda x,y,e: self.enter_tool(sublayer, tool, label, sub_enter(), x, y, e)) 1635 1636 listener = sublayer.exit._listener() 1637 if listener is None: 1638 listener = lambda: None 1639 sub_exit = weakref.ref(listener) 1640 sublayer.exit = self.__ref(lambda: self.exit_tool(sub_exit())) 1641 1642 if tool and (active or (not self.toolsub)): 1643 sublayer.action(0, 0, None)
1644 - def motion_notify(self, x, y, event):
1645 sub = self.sub_hover 1646 Layer.motion_notify(self, x, y, event) 1647 if self.sub_hover != sub: 1648 self.invalidate()
1649 - def pick_tool(self, toolsub, tool, sub_action, x, y, e):
1650 """button click handler""" 1651 self.toolsub = toolsub 1652 self.space.tool = tool 1653 self.invalidate() 1654 if sub_action: 1655 sub_action(x, y, e)
1656 - def draw(self, context):
1657 ts = self.toolsub 1658 em = self.appearance.emsize 1659 if ts: # blue spotlight on picked tool 1660 context.set_source_rgba(.4, .4, 1, .6) 1661 context.arc(ts.x*em+ts.w/2, ts.y*em+ts.h/2, max(ts.w, ts.h)/2+2, 0, 2*pi) 1662 context.fill() 1663 ts = self.sub_hover 1664 if ts: # white spotlight on mouse-over tool 1665 context.set_source_rgba(1,1,1,.2) 1666 context.arc(ts.x*em+ts.w/2, ts.y*em+ts.h/2, max(ts.w, ts.h)/2+2, 0, 2*pi) 1667 context.fill() 1668 self.layout()
1669 - def layout(self):
1670 em = self.appearance.emsize 1671 p = 0 1672 if self.dim == 0: # x 1673 for sublayer in reversed(self.subs): 1674 sublayer.x = p*1.0/em + .5 1675 p += sublayer.w + em 1676 sublayer.y = (self.h - sublayer.h)*1.0/(2*em) 1677 if self.rq_w >= 0: 1678 self.w = p*1.0/em + 1 1679 else: 1680 self.w_min = p*1.0/em + 1 1681 else: # y 1682 for sublayer in reversed(self.subs): 1683 sublayer.x = (self.w - sublayer.w)*1.0/(2*em) 1684 sublayer.y = p*1.0/em + .5 1685 p += sublayer.h + 1 1686 if self.rq_h >= 0: 1687 self.h = p*1.0/em + 1 1688 else: 1689 self.h_min = p*1.0/em + 1
1690 - def enter_tool(self, sub, tool, label, sub_enter, x, y, e):
1691 if sub_enter: 1692 sub_enter(x, y, e)
1693 - def exit_tool(self, sub_exit):
1694 if sub_exit: 1695 sub_exit()
1696 1697
1698 -class Overlay_Tooltip(object):
1699 """Draws a tool-tip on a L{ToolSpace}. Every ToolSpace has one. 1700 1701 @ivar space: L{ToolSpace} 1702 @ivar sublayer: L{SubLayer} under mouse 1703 @ivar cFG: COLORREF of the text 1704 @ivar cBG: COLORREF of the background 1705 """
1706 - def __init__(self, space, cFG=COLOR_TOOLTIP_FG, cBG=COLOR_TOOLTIP_BG):
1707 self.__space = None 1708 self.__cFG = cFG 1709 self.__cBG = cBG 1710 self.__reg = False 1711 self.__sub = None 1712 self.__text = "" 1713 self.__x = self.__y = -1 1714 self.__xdir = self.__ydir = -1 1715 self.__hidden = False 1716 self.on_overlay = self.__on_overlay # capture reference for WeakEvent 1717 self.on_change = self.__on_change 1718 self.on_hide = self.__on_hide 1719 self.space = space
1720 - def set_space(self, space):
1721 if space is None: 1722 self.__space = None 1723 else: 1724 self.__space = weakref.ref(space)
1725 space = property(lambda self: self.__space and self.__space(), lambda self, x: self.set_space(x))
1726 - def set_sublayer(self, sub):
1727 if sub == self.__sub: return 1728 old_sub = self.sublayer 1729 if old_sub: 1730 old_sub.OnChangeTooltip -= self.on_change 1731 old_sub.OnHideTooltip -= self.on_hide 1732 self.__sub = None if (sub is None) else weakref.ref(sub) 1733 if sub: 1734 sub.OnChangeTooltip += self.on_change 1735 sub.OnHideTooltip += self.on_hide 1736 self.__hidden = False 1737 self.on_change(sub)
1738 sublayer = property(lambda self: self.__sub and self.__sub(), lambda self, x: self.set_sublayer(x))
1739 - def __on_change(self, sub):
1740 if self.__hidden: return 1741 space = self.space 1742 em = space.appearance.emsize 1743 tip = sub and sub.tooltip or "" 1744 redraw = False 1745 tall = sub and (sub.layer.h > sub.layer.w) 1746 if (space.dim[0] < 15*space.appearance.emsize) or not tip: 1747 space.set_tooltip_text(tip) 1748 if not tip: 1749 self.__text = "" 1750 self.__x = self.__y = -1 1751 self.__xdir = self.__ydir = -1 1752 if self.__reg: 1753 redraw = True 1754 space.OnOverlay -= self.on_overlay 1755 self.__reg = False 1756 else: 1757 redraw = True 1758 lxpp, lypp = space._layer_scale 1759 self.__text = tip 1760 self.__x = sub.layer.rq_x / lxpp 1761 self.__xdir = 1 1762 if self.__x >= 0: 1763 if tall: 1764 self.__x += (sub.layer.w / em) / lxpp 1765 else: 1766 self.__x += sub.x / lxpp 1767 if (self.__x < 0) or (sub.rq_x < 0): 1768 self.__x = (sub.layer.x / em + sub.x) / lxpp 1769 self.__xdir = -1 1770 self.__y = sub.layer.rq_y / lypp 1771 if self.__y >= 0: 1772 if tall: 1773 self.__y += sub.y / lypp 1774 else: 1775 self.__y += (sub.layer.h / em) / lypp 1776 self.__ydir = 1 1777 else: 1778 self.__y = (sub.layer.y / em) / lypp 1779 self.__ydir = -1 1780 if not self.__reg: 1781 space.OnOverlay += self.on_overlay 1782 self.__reg = True 1783 if redraw: 1784 space.redraw_canvas(False)
1785 - def __on_hide(self, sub):
1786 self.__hidden = True 1787 self.space.redraw_canvas(False)
1788 - def __on_overlay(self, context, w, h):
1789 if self.__hidden: return 1790 space = self.space 1791 if w < 15*space.appearance.emsize: return 1792 context.save() 1793 em = space.appearance.emsize 1794 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 1795 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self.__text) 1796 widthem = width + 2*em 1797 x, y = em*self.__x, em*self.__y 1798 if self.__xdir > 0: 1799 x = max(0, min(w-widthem, em*self.__x + em)) 1800 elif self.__xdir < 0: 1801 x = max(0, min(w-widthem, em*self.__x - (em + widthem))) 1802 if self.__ydir > 0: 1803 y = max(0, min(h-height, em*self.__y + em)) 1804 elif self.__ydir < 0: 1805 y = max(0, min(h-height, em*self.__y - (2*em + height))) 1806 ymargin = em - (fascent + fdescent)/2 1807 context.set_source_rgba(* space.appearance.color(self.__cBG)) 1808 context.rectangle(x, y, width+2*em, 2*em) 1809 context.fill() 1810 context.set_source_rgba(* space.appearance.color(self.__cFG)) 1811 context.move_to(x+em-xbearing, y+ymargin+fascent) 1812 context.show_text(self.__text) 1813 context.restore()
1814 1815 1816 1817
1818 -class SubLayer_Label(SubLayer):
1819 """Displays a string of text. 1820 1821 @ivar label: a string 1822 @ivar color: COLORREF for text 1823 @ivar hover_color: COLORREF for mouse-over text 1824 """
1825 - def __init__(self, label, justify=0, vcenter=1, color=COLOR_LABEL, hover_color=COLOR_HOVER, rotate=0, *args, **kw):
1826 """See also: L{SubLayer.__init__} 1827 1828 @param label: a string 1829 @param justify: negative: left-justify; zero: center; positive: right-justify 1830 @param vcenter: True/nonzero to center vertically 1831 @param color: COLORREF for text 1832 @param hover_color: COLORREF for mouse-over text 1833 """ 1834 self.__ref = Reffer() 1835 kw['enter'] = self.__ref(self.__enter) 1836 kw['exit'] = self.__ref(self.__exit) 1837 SubLayer.__init__(self, *args, **kw) 1838 self._label = label 1839 self.justify = justify 1840 self.vcenter = vcenter 1841 self._color = color 1842 self.hover_color = hover_color 1843 self._rotate = rotate 1844 self.bx = self.by = 0 1845 self.entered = False
1846 - def set_label(self, label):
1847 self._label = label 1848 self.by = 0 1849 self.invalidate()
1850 label = property(lambda self: self._label, lambda self, x: self.set_label(x))
1851 - def set_color(self, color):
1852 self._color = color 1853 self.invalidate()
1854 color = property(lambda self: self._color, lambda self, x: self.set_color(x))
1855 - def set_if_color(self, context, appearance):
1856 if self.entered: 1857 context.set_source_rgba(* appearance.color(self.hover_color)) 1858 else: 1859 context.set_source_rgba(* appearance.color(self._color))
1860 - def set_rotate(self, x):
1861 self._rotate = x 1862 self.invalidate()
1863 rotate = property(lambda self: self._rotate, lambda self, x: self.set_rotate(x))
1864 - def draw(self, context, w, h, appearance):
1865 SubLayer.draw(self, context, w, h, appearance) 1866 context.save() 1867 self.set_if_color(context, appearance) 1868 if self.entered and not self._label: 1869 context.set_source_rgba(*SETALPHA(appearance.color(self.hover_color), .2)) 1870 context.rectangle(0, 0, self.w, self.h) 1871 context.fill() 1872 1873 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 1874 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self._label) 1875 vtotal = fascent + fdescent 1876 w, h = self.w, self.h 1877 if self._rotate: 1878 if pi/4 < abs(self._rotate) < 3*pi/4: 1879 w, h = h, w 1880 context.translate(self.w/2, self.h/2) 1881 context.rotate(-self._rotate) 1882 context.translate(-w/2, -h/2) 1883 if self.vcenter: 1884 self.by = (h - vtotal)/2 + fascent 1885 else: 1886 self.by = (h - vtotal) + fascent 1887 if self.justify < 0: 1888 self.bx = - xbearing 1889 elif self.justify > 0: 1890 self.bx = (w - width) - xbearing 1891 else: 1892 self.bx = (w - width)/2 - xbearing 1893 1894 context.move_to(self.bx, self.by) 1895 context.show_text(self.label) 1896 context.restore()
1897 - def __enter(self, x, y, e):
1898 self.entered = True 1899 self.OnInvalidate(self)
1900 - def __exit(self):
1901 self.entered = False 1902 self.OnInvalidate(self)
1903
1904 -class SubLayer_UpDown(SubLayer):
1905 """The little up and down buttons, such as next to a number."""
1906 - def __init__(self, up=lambda:None, down=lambda:None, color=LAYER_FG, *args, **kw):
1907 """See also: L{SubLayer.__init__} 1908 1909 @param up: when the up button is clicked, calls up() 1910 @param down: when the down button is clicked, calls down() 1911 """ 1912 self.__ref = Reffer() 1913 kw['action'] = self.__ref(self.__action) 1914 kw['mouse_move'] = self.__ref(self.__mouse_move) 1915 kw['exit'] = self.__ref(self.__exit) 1916 SubLayer.__init__(self, *args, **kw) 1917 self.__up = WeakCall("SubLayer_UpDown.up") 1918 self.__up.assign(up) 1919 self.__down = WeakCall("SubLayer_UpDown.down") 1920 self.__down.assign(down) 1921 self.mouse_x = self.mouse_y = -1 1922 self.color = color
1923 up = property(lambda self: self.__up, lambda self, x: self.__up.assign(x)) 1924 down = property(lambda self: self.__down, lambda self, x: self.__down.assign(x))
1925 - def __action(self, x, y, e):
1926 if y < self.h/2: 1927 self.up() 1928 else: 1929 self.down()
1930 - def __mouse_move(self, x, y, e):
1931 self.mouse_x, self.mouse_y = x, y 1932 self.OnInvalidate(self)
1933 - def __exit(self):
1934 self.mouse_x = self.mouse_y = -1 1935 self.OnInvalidate(self)
1936 - def draw(self, context, w, h, appearance):
1937 SubLayer.draw(self, context, w, h, appearance) 1938 context.set_source_rgba(* appearance.color(self.color)) 1939 context.set_line_width(appearance.emsize / 5.0) 1940 context.save() 1941 context.move_to(self.w/2, 2) 1942 context.line_to(1, self.h/2-2) 1943 context.line_to(self.w-1, self.h/2-2) 1944 if 0 <= self.mouse_y < self.h/2: 1945 context.fill_preserve() 1946 context.stroke() 1947 else: 1948 context.fill() 1949 context.restore() 1950 context.save() 1951 context.move_to(self.w/2, self.h-2) 1952 context.line_to(1, self.h/2+2) 1953 context.line_to(self.w-1, self.h/2+2) 1954 if self.h/2 <= self.mouse_y: 1955 context.fill_preserve() 1956 context.stroke() 1957 else: 1958 context.fill() 1959 context.restore()
1960 1961
1962 -class SubLayer_Check(SubLayer):
1963 """A box that is active or not; click to toggle."""
1964 - def __init__(self, color=COLOR_CHECK, caption=None, active=False, *args, **kw):
1965 """See also: L{SubLayer.__init__} 1966 1967 @param color: COLORREF for the box 1968 """ 1969 self.__ref = Reffer() 1970 kw['enter'] = self.__ref(self.__enter) 1971 kw['exit'] = self.__ref(self.__exit) 1972 kw['action'] = self.__ref(self.__action) 1973 SubLayer.__init__(self, *args, **kw) 1974 self.color = color 1975 self.caption = caption 1976 self.__active = active 1977 self.entered = False 1978 self.OnToggle = WeakEvent() # (sublayer, active)
1979 - def get_active(self):
1980 return self.__active
1981 - def set_active(self, x):
1982 if self.__active != x: 1983 self.__active = x 1984 self.invalidate()
1985 active = property(get_active, set_active)
1986 - def __action(self, x, y, e):
1987 self.__active = not self.__active 1988 self.OnToggle(self, self.__active) 1989 self.invalidate()
1990 - def draw(self, context, w, h, appearance):
1991 SubLayer.draw(self, context, w, h, appearance) 1992 r, g, b, a = appearance.color(self.color) 1993 if self.entered: 1994 h, s, v = RGBtoHSV(r, g, b) 1995 r, g, b = HSVtoRGB(h, .99, .99) 1996 context.set_source_rgba(r, g, b, a) 1997 context.set_line_width(appearance.emsize/5.0) 1998 side = 0.8 * min(self.w, self.h) 1999 y0 = (self.h - side) / 2 2000 if self.caption: 2001 x0 = .75*appearance.emsize 2002 else: 2003 x0 = (self.w - side) / 2 2004 context.rectangle(x0, y0, side, side) 2005 if self.__active: 2006 context.stroke_preserve() 2007 context.fill() 2008 else: 2009 context.stroke() 2010 if self.caption: 2011 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 2012 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self.caption) 2013 vtotal = fascent + fdescent 2014 self.by = (self.h - vtotal)/2 + fascent 2015 self.bx = side + 1.5*appearance.emsize - xbearing 2016 context.move_to(self.bx, self.by) 2017 context.show_text(self.caption)
2018 - def __enter(self, x, y, e):
2019 self.entered = True 2020 self.OnInvalidate(self)
2021 - def __exit(self):
2022 self.entered = False 2023 self.OnInvalidate(self)
2024 2025 2026
2027 -class SubLayer_Popup(SubLayer):
2028 """A circle with inscribed triangle, which shows a menu when clicked."""
2029 - def __init__(self, popup=None, color=COLOR_POPUP, tooltip=None, on_popup=None, caption=None, *args, **kw):
2030 """See also: L{SubLayer.__init__} 2031 2032 @param popup: a gtk.Menu 2033 @param color: COLORREF for the circle and triangle 2034 @param tooltip: a string to display on mouse-over 2035 @param on_popup: lambda: should_show to modify popup contents or cancel, before showing 2036 """ 2037 self.__ref = Reffer() 2038 kw['action'] = self.__ref(self.__action) 2039 kw['enter'] = self.__ref(self.__enter) 2040 kw['exit'] = self.__ref(self.__exit) 2041 SubLayer.__init__(self, *args, **kw) 2042 self.popup = popup 2043 self.color = color 2044 self.tooltip = tooltip 2045 self.__on_popup = WeakCall("SubLayer_Popup.on_popup") 2046 self.__on_popup.assign(on_popup) 2047 self.caption = caption 2048 self.entered = False
2049 on_popup = property(lambda self: self.__on_popup, lambda self, x: self.__on_popup.assign(x))
2050 - def __action(self, x, y, e):
2051 self.do_popup(e.button, e.time)
2052 - def do_popup(self, button=1, time=0):
2053 self.OnHideTooltip(self) 2054 if self.popup: 2055 if self.on_popup and not self.on_popup(): 2056 return 2057 self.popup.popup(None, None, None, 0, time)
2058 - def draw(self, context, w, h, appearance):
2059 SubLayer.draw(self, context, w, h, appearance) 2060 if self.entered: 2061 context.set_source_rgba(* SETALPHA(appearance.color(self.color), 1)) 2062 context.set_line_width(1.6) 2063 else: 2064 context.set_source_rgba(* SETALPHA(appearance.color(self.color), .7)) 2065 context.set_line_width(.6) 2066 context.move_to(self.w-1, self.h/2) 2067 context.arc(self.w - self.h/2, self.h/2, self.h/2-1, 0, 2*pi) 2068 context.stroke() 2069 context.save() 2070 context.translate(2+self.w-self.h, 2) 2071 qubx.GTK.draw_inscribed_triangle(context, self.h-4, self.h-4, NOALPHA(appearance.color(self.color)), 3*pi/2) 2072 context.restore() 2073 if not self.caption: 2074 return 2075 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 2076 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self.caption) 2077 vtotal = fascent + fdescent 2078 self.by = (self.h - vtotal)/2 + fascent 2079 self.bx = - xbearing 2080 context.set_source_rgba(*appearance.color(self.color)) 2081 context.move_to(self.bx+0.25*appearance.emsize, self.by) 2082 context.show_text(self.caption)
2083 - def __enter(self, x, y, e):
2084 self.entered = True 2085 em = self.layer.space.appearance.emsize 2086 self.OnInvalidate(self)
2087 - def __exit(self):
2088 self.entered = False 2089 self.OnInvalidate(self)
2090
2091 -class Button_Popup(ToolSpace):
2092 __explore_featured = ['scale', 'layer', 'sub', 'do_popup']
2093 - def __init__(self, popup=None, color=COLOR_POPUP, tooltip=None, on_popup=None, cBG=COLOR_BLACK, scale=1.0, *args, **kw):
2094 ToolSpace.__init__(self) 2095 self.scale = scale 2096 if tooltip: 2097 self.set_tooltip_text(tooltip) 2098 w = h = int(round(2.3*scale)) 2099 self.set_size_request(w, h) 2100 self.__ref = Reffer() 2101 Appearance.OnSetFontSize += self.__ref(self.__onSetFontSize) 2102 self.__onSetFontSize(Appearance.font_size) 2103 self.layer = Layer(x=0, y=0, w=w, h=h, cBG=cBG) 2104 self.add_layer(self.layer) 2105 self.sub = SubLayer_Popup(popup, color, "", on_popup, x=0, y=0, w=w, h=h) 2106 self.layer.add_sublayer(self.sub)
2107 - def do_popup(self, *args, **kw):
2108 self.sub.do_popup(*args, **kw)
2109 - def __onSetFontSize(self, font_size):
2110 w = h = int(round(2.3 * self.scale * Appearance.emsize)) 2111 self.set_size_request(w, h)
2112
2113 -class Button_Popup_PNG(ToolSpace):
2114 __explore_featured = ['scale', 'layer', 'sub', 'do_popup']
2115 - def __init__(self, popup=None, path="", tooltip=None, on_popup=None, scale=1.0, *args, **kw):
2116 ToolSpace.__init__(self) 2117 if tooltip: 2118 self.set_tooltip_text(tooltip) 2119 self.scale = scale 2120 w = h = int(round(2.3*scale)) 2121 self.set_size_request(w, h) 2122 self.__ref = Reffer() 2123 Appearance.OnSetFontSize += self.__ref(self.__onSetFontSize) 2124 self.__onSetFontSize(Appearance.font_size) 2125 self.layer = Layer(x=0, y=0, w=w, h=h, cBG=COLOR_CLEAR) 2126 self.add_layer(self.layer) 2127 self.sub = SubLayer_Popup_PNG(popup, path, "", on_popup, x=0, y=0, w=w, h=h) 2128 self.layer.add_sublayer(self.sub)
2129 - def do_popup(self, *args, **kw):
2130 self.sub.do_popup(*args, **kw)
2131 - def __onSetFontSize(self, font_size):
2132 w = h = int(round(2.3 * self.scale * Appearance.emsize)) 2133 self.set_size_request(w, h)
2134 2135
2136 -class SubLayer_MenuLines(SubLayer):
2137 """A collection of horizontal lines, which shows a menu when clicked."""
2138 - def __init__(self, popup=None, tooltip=None, on_popup=None, *args, **kw):
2139 """See also: L{SubLayer.__init__} 2140 2141 @param popup: a gtk.Menu 2142 @param tooltip: a string to display on mouse-over 2143 @param on_popup: lambda: should_show to modify popup contents or cancel, before showing 2144 """ 2145 self.__ref = Reffer() 2146 kw['action'] = self.__ref(self.__action) 2147 kw['enter'] = self.__ref(self.__enter) 2148 kw['exit'] = self.__ref(self.__exit) 2149 SubLayer.__init__(self, *args, **kw) 2150 self.popup = popup 2151 self.tooltip = tooltip 2152 self.__on_popup = WeakCall("SubLayer_MenuLines.on_popup") 2153 self.__on_popup.assign(on_popup) 2154 self.entered = False
2155 on_popup = property(lambda self: self.__on_popup, lambda self, x: self.__on_popup.assign(x))
2156 - def __action(self, x, y, e):
2157 self.do_popup(e.button, e.time)
2158 - def do_popup(self, button=1, time=0):
2159 self.OnHideTooltip(self) 2160 if self.popup: 2161 if self.on_popup and not self.on_popup(): 2162 return 2163 self.popup.popup(None, None, None, 0, time)
2164 - def draw(self, context, w, h, appearance):
2165 SubLayer.draw(self, context, w, h, appearance) 2166 context.set_source_rgba(.5, .5, .5, .75) 2167 context.rectangle(1, 1, self.w-2, self.h-2) 2168 context.fill() 2169 if self.entered: 2170 context.set_source_rgba(1, 1, 1, 1) 2171 else: 2172 context.set_source_rgba(0, 0, 0, .9) 2173 num_line = 3 2174 num_div = 2*num_line + 1 2175 div = (self.h-2) * 1.0 / num_div 2176 context.save() 2177 context.translate(1, 1) 2178 for i in xrange(num_line): 2179 cutout = .1 if i else 0.0 2180 context.rectangle((.1+cutout)*self.w, (2*i+1)*div, (.7-cutout)*self.w, 0.7*div) 2181 context.fill() 2182 context.restore()
2183 - def __enter(self, x, y, e):
2184 self.entered = True 2185 self.invalidate()
2186 - def __exit(self):
2187 self.entered = False 2188 self.invalidate()
2189
2190 -class Button_MenuLines(ToolSpace):
2191 __explore_featured = ['scale', 'layer', 'sub', 'do_popup']
2192 - def __init__(self, popup=None, tooltip=None, on_popup=None, scale=1.0, *args, **kw):
2193 ToolSpace.__init__(self) 2194 self.scale = scale 2195 if tooltip: 2196 self.set_tooltip_text(tooltip) 2197 w = h = int(round(2.3*scale)) 2198 self.set_size_request(w, h) 2199 self.__ref = Reffer() 2200 Appearance.OnSetFontSize += self.__ref(self.__onSetFontSize) 2201 self.__onSetFontSize(Appearance.font_size) 2202 self.layer = Layer(x=0, y=0, w=w, h=h, cBG=COLOR_CLEAR) 2203 self.add_layer(self.layer) 2204 self.sub = SubLayer_MenuLines(popup, "", on_popup, x=0, y=0, w=w, h=h) 2205 self.layer.add_sublayer(self.sub)
2206 - def do_popup(self, *args, **kw):
2207 self.sub.do_popup(*args, **kw)
2208 - def __onSetFontSize(self, font_size):
2209 w = h = int(round(2.3 * self.scale * Appearance.emsize)) 2210 self.set_size_request(w, h)
2211 2212
2213 -class SubLayer_DropDown(SubLayer):
2214 """A combo-box in the style of csDropDownList (enumerated choices only). Uses L{Tool_DropDown} to show the menu items. 2215 Displays the menu using a L{Tool}, therefore can't be used in L{Tool}.layers, only in L{ToolSpace} proper. 2216 2217 @ivar label: string that's showing when the menu is not open 2218 @ivar menu: you directly edit this list of menu items (label, action), where label is a string, and action() is called when clicked. 2219 @ivar OnDropDown: L{WeakEvent}(SubLayer_DropDown) before the menu is shown 2220 """
2221 - def __init__(self, label, justify=0, vcenter=1, color=COLOR_DROPDOWN, *args, **kw):
2222 """See also: L{SubLayer.__init__} 2223 2224 @param label: string that shows when the menu is not open 2225 @param justify: negative: left-justify; zero: center: positive; right-justify 2226 @param vcenter: True/nonzero to center text vertically 2227 @param color: COLORREF for the inscribed triangle 2228 """ 2229 self.__ref = Reffer() 2230 kw['action'] = self.__ref(self._action) 2231 SubLayer.__init__(self, *args, **kw) 2232 self._label = label 2233 self.justify = justify 2234 self.vcenter = vcenter 2235 self.menu = [] # (label, action()) 2236 self._color = color 2237 self.bx = self.by = 0 2238 self.OnDropDown = WeakEvent()
2239 - def set_label(self, label):
2240 self._label = label 2241 self.by = 0 2242 self.invalidate()
2243 label = property(lambda self: self._label, lambda self, x: self.set_label(x))
2244 - def set_color(self, color):
2245 self._color = color 2246 self.invalidate()
2247 color = property(lambda self: self._color, lambda self, x: self.set_color(x))
2248 - def draw(self, context, w, h, appearance):
2249 SubLayer.draw(self, context, w, h, appearance) 2250 lblw = self.w - appearance.emsize - self.h 2251 #if not self.by: ##?? 2252 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 2253 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self._label) 2254 vtotal = fascent + fdescent 2255 if self.vcenter: 2256 self.by = (self.h - vtotal)/2 + fascent 2257 else: 2258 self.by = (self.h - vtotal) + fascent 2259 if self.justify < 0: 2260 self.bx = - xbearing 2261 elif self.justify > 0: 2262 self.bx = (lblw - width) - xbearing 2263 else: 2264 self.bx = (lblw - appearance.emsize - self.h - width)/2 - xbearing 2265 2266 # 2267 if self.color: 2268 context.set_source_rgba(* appearance.color(self._color)) 2269 context.move_to(self.bx, self.by) 2270 context.show_text(self.label) 2271 context.translate(lblw+appearance.emsize+2, 2) 2272 qubx.GTK.draw_inscribed_triangle(context, self.h-4, self.h-4, NOALPHA(appearance.color(self.color)), 3*pi/2)
2273 - def _action(self, x, y, e):
2274 if not self.menu: return 2275 menu = gtk.Menu() 2276 for label, action in self.menu: 2277 build_menuitem(label, self.__ref(bind(action)), menu=menu) 2278 menu.popup(None, None, None, 0, e.time)
2279 #if isinstance(self.layer.space.tool, Tool_DropDown) and self.layer.space.tool.sublayer == self: 2280 # self.layer.space.tool = self.layer.space.tool.prev_tool 2281 #else: 2282 # self.OnDropDown(self) 2283 # self.layer.space.tool = Tool_DropDown(self, self.layer.space.tool) 2284
2285 -class Tool_DropDown(OverlayRgnTool):
2286 """Displays a drop-down menu (from L{SubLayer_DropDown}) and handles mouse clicks."""
2287 - def __init__(self, sublayer, prev_tool):
2288 """ 2289 @param sublayer: L{SubLayer_DropDown} 2290 @param prev_tool: L{Tool} to reinstate when the menu is done; prior value of sublayer.layer.space.tool 2291 """ 2292 self.sublayer = sublayer 2293 self.prev_tool = prev_tool 2294 OverlayRgnTool.__init__(self, 0, 0, prev_tool)
2295 - def onOverlay(self, context, w, h):
2296 context.save() 2297 mw = self.sublayer.w 2298 em = self.space.appearance.emsize 2299 lxpp, lypp = self.sublayer.layer.space._layer_scale 2300 mh = len(self.sublayer.menu) * 3*em + em 2301 x = em*(self.sublayer.x + self.sublayer.layer.rq_x) / lxpp 2302 if x < 0: 2303 x += self.sublayer.layer.space.dim[0] 2304 y = em*(self.sublayer.y + self.sublayer.layer.rq_y) / lypp 2305 if y >= 0: 2306 y += self.sublayer.layer.h/lypp + em 2307 else: 2308 y += self.sublayer.layer.space.dim[1] - em - mh 2309 rgns = [] 2310 codes = [] 2311 def store_rect(xA, yA, xW, yH, c): 2312 rgns.append(RotRect(xA, yA, xA+xW, yA+yH)) 2313 codes.append(c)
2314 context.set_source_rgba(* self.space.appearance.color(self.sublayer.layer.cBG)) 2315 context.rectangle(x, y, mw, mh) 2316 context.fill() 2317 context.set_source_rgba(* self.space.appearance.color(self.sublayer.color)) 2318 context.set_line_width(em/10.0) 2319 yy = y + em 2320 for i, item in enumerate(self.sublayer.menu): 2321 lbl, action = item 2322 if lbl == '-': 2323 context.move_to(x+em, yy+em) 2324 context.line_to(x+self.w-em, yy+em) 2325 context.stroke() 2326 else: 2327 context.move_to(x+em, yy+2*em) 2328 context.show_text(lbl) 2329 store_rect(x, yy, mw, 3*em, i) 2330 if i == self.rgn: 2331 context.move_to(x+em, yy+2*em+1.5) 2332 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(lbl) 2333 context.line_to(x+em+width, yy+2*em+1.5) 2334 context.stroke() 2335 yy += 3*em 2336 store_rect(x, y, mw, mh, -2) # harmless click in bg of windowlet 2337 store_rect(0, 0, w, h, -1) # out-of-bounds -> close 2338 self.rgns = rgns 2339 self.codes = codes 2340 context.restore()
2341 - def onRelease(self, x, y, e):
2342 c = None 2343 if not (self.rgn is None): 2344 c = self.codes[self.rgn] 2345 if c >= 0: 2346 lbl, action = self.sublayer.menu[c] 2347 action() 2348 if c != -2: 2349 self.space.tool = self.prev_tool
2350 2351 2352
2353 -class SubLayer_Range(SubLayer):
2354 """A slightly nonstandard scroll bar; grab the left or right edge to resize the thumb. 2355 2356 @ivar bounds: (lo, hi) smallest and largest possible value 2357 @ivar quantum: smallest delta value 2358 @ivar left: value at left edge of thumb 2359 @ivar right: value at right edge of thumb 2360 @ivar OnMoving: L{WeakEvent}(SubLayer_Range, left, right) called when the user is still dragging 2361 @ivar OnSet: L{WeakEvent}(SubLayer_Range, left, right, by_mouse) called when left and/or right have changed 2362 """
2363 - def __init__(self, lo, hi, quantum, *args, **kw):
2364 self.__ref = Reffer() 2365 kw['exit'] = self.__ref(self.__exit) 2366 kw['mouse_move'] = self.__ref(self.__mouse_move) 2367 kw['mouse_drag'] = self.__ref(self.__mouse_drag) 2368 SubLayer.__init__(self, *args, **kw) 2369 self._lo = lo 2370 self._hi = max(hi, lo+quantum) 2371 self._range = self._hi - self._lo 2372 self._quantum = quantum 2373 self._left = self._lo 2374 self._right = self._hi 2375 self.OnMoving = WeakEvent() # (SubLayer, left, right) 2376 self.OnSet = WeakEvent() # (SubLayer, left, right, by_mouse) 2377 self._inhandle = self._inleft = self._inright = False
2378 - def set_bounds(self, x, event_if_moved=True):
2379 lo, hi = x 2380 hi = max(hi, lo+self.quantum) 2381 if (hi != self._hi) or (lo != self._lo): 2382 self._lo, self._hi = lo, hi 2383 self._range = hi - lo 2384 l,r = self._left, self._right 2385 wid = min(r-l, hi-lo) 2386 if l < lo: 2387 l += (lo - l) 2388 r = l + wid 2389 elif r > hi: 2390 r -= (r - hi) 2391 l = r - wid 2392 if (r != self._right) or (l != self._left): 2393 self._left, self._right = l,r 2394 if event_if_moved: self.OnSet(self, l, r, False) 2395 self.invalidate()
2396 - def set_range(self, l, r, event_if_moved=True, by_mouse=False):
2397 l = max(self._lo, min(r-self.quantum, l)) 2398 r = min(self._hi, max(l+self.quantum, r)) 2399 if (l != self._left) or (r != self._right): 2400 self._left, self._right = l,r 2401 self.invalidate() 2402 if event_if_moved: 2403 self.doOnSet(by_mouse)
2404 - def set_left(self, x):
2405 l = max(self._lo, min(self._right-self.quantum, x)) 2406 if l != self._left: 2407 self._left = l 2408 self.invalidate() 2409 self.doOnSet()
2410 - def set_right(self, x):
2411 r = min(self._hi, max(self._left+self.quantum, x)) 2412 if r != self._right: 2413 self._right = r 2414 self.invalidate() 2415 self.doOnSet()
2416 - def doOnSet(self, by_mouse=False):
2417 if self._inhandle or self._inleft or self._inright: 2418 self.OnMoving(self, self._left, self._right) 2419 else: 2420 self.OnSet(self, self._left, self._right, by_mouse)
2421 - def set_quantum(self, x):
2422 self._quantum = x 2423 if (self._right - self._left) < x: 2424 self.set_range(self._left, self._left + x)
2425 bounds = property(lambda self: (self._lo, self._hi), lambda self, x: self.set_bounds(x)) 2426 left = property(lambda self: self._left, lambda self, x: self.set_left(x)) 2427 right = property(lambda self: self._right, lambda self, x: self.set_right(x)) 2428 quantum = property(lambda self: self._quantum, lambda self, x: self.set_quantum(x))
2429 - def x2p(self, x):
2430 return int(round( self.w * float(x - self._lo) / self._range ))
2431 - def p2x(self, p):
2432 return self._range * float(p) / self.w + self._lo
2433 - def __mouse_move(self, x, y, e):
2434 em = self.layer.appearance.emsize 2435 pl, pr = self.x2p(self._left), self.x2p(self._right) 2436 if (pr - pl) < (2*em): 2437 pl -= em 2438 pr += em 2439 inleft = (-em/2) <= (x - pl) <= (em/2) 2440 if inleft: 2441 inright = False 2442 else: 2443 inright = (-em/2) <= (x - pr) <= (em/2) 2444 if inleft or inright: 2445 inhandle = False 2446 else: 2447 inhandle = pl <= x <= pr 2448 if (inhandle != self._inhandle) or (inleft != self._inleft) or (inright != self._inright): 2449 self._inhandle, self._inleft, self._inright = inhandle, inleft, inright 2450 self.invalidate()
2451 - def __exit(self):
2452 self._inhandle = self._inleft = self._inright = False 2453 self.invalidate()
2454 - def button_press(self, x, y, e):
2455 self.bx = x 2456 if self._inleft or self._inhandle: 2457 self.vv = self._left 2458 elif self._inright: 2459 self.vv = self._right
2460 - def button_release(self, x, y, e):
2461 if self._inhandle or self._inleft or self._inright: 2462 self.OnSet(self, self._left, self._right, True) 2463 else: 2464 page = self._right - self._left 2465 if 0 <= x < self.x2p(self.left): 2466 page = min(page, self.left - self.bounds[0]) 2467 self.set_range(self.left - page, self.right - page, by_mouse=True) # self.p2x(x) 2468 elif self.x2p(self.right) < x <= self.w: 2469 page = min(page, self.bounds[1] - self.right) 2470 self.set_range(self.left + page, self.right + page, by_mouse=True) # self.p2x(x)
2471 - def __mouse_drag(self, x, y, e):
2472 dx = self._range * float(x - self.bx) / self.w 2473 if self._inleft: 2474 self.left = self.vv + dx 2475 elif self._inright: 2476 self.right = self.vv + dx 2477 elif self._inhandle: 2478 left = self.vv + dx 2479 range_x = (self._right - self._left) 2480 left = max(left, self._lo) 2481 left = min(left, self._hi - range_x) 2482 dx = left - self.vv 2483 self._right = left + range_x 2484 self.left = left
2485
2486 - def draw(self, context, w, h, appearance):
2487 SubLayer.draw(self, context, w, h, appearance) 2488 def color(hover, expander=False): 2489 if hover: 2490 context.set_source_rgba(* appearance.color(COLOR_RANGE_HOVER)) 2491 elif expander: 2492 context.set_source_rgba(* appearance.color(COLOR_RANGE_EXPAND)) 2493 else: 2494 context.set_source_rgba(* appearance.color(COLOR_RANGE))
2495 color(self._inhandle) 2496 context.set_line_width(self.h/8) 2497 context.move_to(0, self.h/2) 2498 context.line_to(self.w, self.h/2) 2499 context.stroke() 2500 context.set_line_width(4*(self.h/8)) 2501 context.move_to(self.x2p(self.left), self.h/2) 2502 context.line_to(self.x2p(self.right), self.h/2) 2503 context.stroke() 2504 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 2505 xbearing, ybearing, char_w, height, xadvance, yadvance = context.text_extents('[') 2506 baseline = (self.h - fascent - fdescent) / 2 + fascent 2507 color(False, self._inleft) 2508 context.move_to(self.x2p(self._left) - char_w/2, baseline) 2509 context.show_text('[') 2510 color(False, self._inright) 2511 context.move_to(self.x2p(self._right) - char_w/2, baseline) 2512 context.show_text(']')
2513
2514 -class SubLayer_Mag(SubLayer_Label):
2515 """Draws a magnifying glass under the label."""
2516 - def __init__(self, color_rim=COLOR_MAG_RIM, color_lens=COLOR_MAG_LENS, *args, **kw):
2517 kw['justify'] = 0 2518 kw['vcenter'] = 1 2519 if not ('appearance' in kw): 2520 em = Appearance.emsize 2521 else: 2522 em = kw['appearance'].emsize 2523 if not ('w' in kw): 2524 kw['w'] = 2.5 2525 if not ('h' in kw): 2526 kw['h'] = 3 2527 SubLayer_Label.__init__(self, *args, **kw) 2528 self.color_rim = color_rim 2529 self.color_lens = color_lens
2530 - def draw(self, context, w, h, appearance):
2531 SubLayer.draw(self, context, w, h, appearance) 2532 r = .75 * self.w/2 2533 cx, cy = self.w/2, r+1 2534 context.save() 2535 context.set_line_width(.5) 2536 context.set_source_rgba(* appearance.color(self.color_rim)) 2537 context.move_to(cx+r, cy) 2538 context.arc(cx, cy, r, 0, 2*pi) 2539 context.stroke_preserve() 2540 context.set_source_rgba(* appearance.color(self.color_lens)) 2541 context.fill() 2542 # draw line from c,c to w,w, except first r 2543 cy += r/2 2544 theta = atan2(self.h - r/2 - 1, self.w/2) 2545 dx = r*sin(theta) 2546 dy = r*cos(theta) 2547 context.set_source_rgba(* appearance.color(self.color_rim)) 2548 context.set_line_width(1.5) 2549 context.move_to(cx+dx, cy+dy) 2550 context.line_to(self.w, self.w) 2551 context.stroke() 2552 context.restore() 2553 2554 em = appearance.emsize 2555 height, self.rq_h = self.rq_h, int(round(self.h*0.68/em)) 2556 SubLayer_Label.draw(self, context, w, h, appearance) 2557 self.rq_h = height 2558 SubLayer.draw(self, context, w, h, appearance)
2559
2560 -class SubLayer_PNG(SubLayer):
2561 - def __init__(self, path, tooltip=None, **kw):
2562 self.path = path 2563 self.image_surface = None 2564 SubLayer.__init__(self, **kw) 2565 self.tooltip = tooltip
2566 - def draw(self, context, w, h, appearance):
2567 SubLayer.draw(self, context, w, h, appearance) 2568 if not self.image_surface: 2569 self.image_surface = cairo.ImageSurface.create_from_png(self.path) 2570 img_height = self.image_surface.get_height() 2571 img_width = self.image_surface.get_width() 2572 width_ratio = float(self.w) / float(img_width) 2573 height_ratio = float(self.h) / float(img_height) 2574 scale_xy = min(height_ratio, width_ratio) 2575 context.save() 2576 context.translate((self.w - scale_xy*img_width)/2, (self.h - scale_xy*img_height)/2) 2577 context.scale(scale_xy, scale_xy) 2578 context.set_source_surface(self.image_surface) 2579 context.rectangle(0, 0, self.w/scale_xy, self.h/scale_xy) 2580 context.fill() 2581 context.restore()
2582
2583 -class SubLayer_Popup_PNG(SubLayer_PNG):
2584 - def __init__(self, popup=None, path="", tooltip=None, on_popup=None, caption=None, *args, **kw):
2585 self.__ref = Reffer() 2586 kw['action'] = self.__ref(self.__action) 2587 kw['enter'] = self.__ref(self.__enter) 2588 kw['exit'] = self.__ref(self.__exit) 2589 SubLayer_PNG.__init__(self, path, *args, **kw) 2590 self.popup = popup 2591 self.tooltip = tooltip 2592 self.__on_popup = WeakCall("SubLayer_Popup_PNG.on_popup") 2593 self.__on_popup.assign(on_popup) 2594 self.caption = caption 2595 self.entered = False
2596 on_popup = property(lambda self: self.__on_popup, lambda self, x: self.__on_popup.assign(x))
2597 - def __action(self, x, y, e):
2598 self.do_popup(e.button, e.time)
2599 - def do_popup(self, button=1, time=0):
2600 self.OnHideTooltip(self) 2601 if self.popup: 2602 if self.on_popup and not self.on_popup(): 2603 return 2604 self.popup.popup(None, None, None, 0, time)
2605 - def __enter(self, x, y, e):
2606 self.entered = True 2607 em = self.layer.space.appearance.emsize 2608 self.OnInvalidate(self)
2609 - def __exit(self):
2610 self.entered = False 2611 self.OnInvalidate(self)
2612 2613
2614 -class SubLayer_Icon(SubLayer):
2615 """A little icon suitable for L{Layer_Toolbar}."""
2616 - def __init__(self, *args, **kw):
2617 if not ('appearance' in kw): 2618 em = Appearance.emsize 2619 else: 2620 em = kw['appearance'].emsize 2621 if not ('w' in kw): 2622 kw['w'] = 2.5 2623 if not ('h' in kw): 2624 kw['h'] = 2.5 2625 SubLayer.__init__(self, *args, **kw)
2626 2627
2628 -class SubLayer_Ruler(SubLayer_Icon):
2629 """A little yellow ruler icon."""
2630 - def draw(self, context, w, h, appearance):
2631 hh = self.h 2632 ww = min(self.w, int(round(.4*hh))) 2633 x0 = (self.w - ww) / 2.0 2634 SubLayer.draw(self, context, w, h, appearance) 2635 context.set_line_width(.5) 2636 context.rectangle(x0+1, 1, ww-2, hh-2) 2637 context.set_source_rgb(1,1,0) 2638 context.fill_preserve() 2639 context.set_source_rgb(0,0,0) 2640 context.stroke() 2641 context.set_line_width(.25) 2642 y = 3.5 2643 for i in count(1): 2644 if y >= hh: break 2645 x = (i%3) and (ww/3) or (2*ww/3) 2646 context.move_to(x0+1, y) 2647 context.line_to(x0+x, y) 2648 context.stroke() 2649 y += 2
2650 2651
2652 -class SubLayer_Eraser(SubLayer_Icon):
2653 """An eraser icon."""
2654 - def draw(self, context, w, h, appearance):
2655 SubLayer.draw(self, context, w, h, appearance) 2656 ww = self.w - 4 2657 hw = ww/2 2658 face_h = hw/1.816 2659 dx = hw - hw/1.234 2660 a = 2 2661 b = a + dx 2662 c = a + hw 2663 d = c + dx 2664 f = self.w - 2 2665 e = f - dx 2666 A = self.h - 3 2667 B = A - face_h 2668 D = 3 2669 C = D + face_h 2670 2671 context.set_source_rgb(.5,.3,.8) 2672 context.move_to(b,B) 2673 context.line_to(d,B) 2674 context.line_to(f,D) 2675 context.line_to(c,D) 2676 context.line_to(b,B) 2677 context.fill() 2678 context.move_to(d,B) 2679 context.line_to(f,D) 2680 context.line_to(e,C) 2681 context.line_to(c,A) 2682 context.line_to(d,B) 2683 context.fill() 2684 #context.set_source_rgb(.6,.33,.45) 2685 context.move_to(a,A) 2686 context.line_to(c,A) 2687 context.line_to(d,B) 2688 context.line_to(b,B) 2689 context.line_to(a,A) 2690 #context.fill_preserve() 2691 context.set_line_width(.5) 2692 context.set_source_rgb(.9,.4,.6) 2693 context.stroke() 2694 context.move_to(c,A) 2695 context.line_to(e,C) 2696 context.line_to(f,D) 2697 context.line_to(c,D) 2698 context.line_to(b,B) 2699 context.stroke() 2700 context.move_to(d,B) 2701 context.line_to(f,D) 2702 context.stroke()
2703 2704
2705 -class SubLayer_Stamp(SubLayer_Icon):
2706 """A rubber stamp icon."""
2707 - def draw(self, context, w, h, appearance):
2708 SubLayer.draw(self, context, w, h, appearance) 2709 draw_stamp(context, self.w, self.h, appearance)
2710
2711 -def draw_stamp(context, w, h, appearance):
2712 m = w * .18 2713 ww = w - 2*m 2714 hw = ww/2 2715 context.set_source_rgb(.6, .5, .5) 2716 context.set_line_width(appearance.emsize/4) 2717 context.rectangle(m, m+.6*ww, ww, .4*ww) 2718 context.stroke() 2719 context.move_to(m+.33*ww, m+.7*ww) 2720 context.line_to(m+.67*ww, m+.7*ww) 2721 context.line_to(m+.5*ww, m+.95*ww) 2722 context.fill() 2723 context.rectangle(m+.45*ww, m+.2*ww, .1*ww, .45*ww) 2724 context.fill() 2725 context.arc(m+.5*ww, m+.2*ww, .15*ww, 0, 2*pi) 2726 context.stroke()
2727 2728
2729 -class SubLayer_Arrow(SubLayer_Icon):
2730 """An arrow."""
2731 - def __init__(self, color=LAYER_FG, angle=pi, *args, **kw):
2732 SubLayer_Icon.__init__(self, *args, **kw) 2733 self.__color = color 2734 self.__angle = angle
2735 - def set_angle(self, angle):
2736 self.__angle = angle 2737 self.invalidate()
2738 angle = property(lambda self: self.__angle, lambda self, x: self.set_angle(x))
2739 - def set_color(self, color):
2740 self.__color = color 2741 self.invalidate()
2742 color = property(lambda self: self.__color, lambda self, x: self.set_color(x))
2743 - def draw(self, context, w, h, appearance):
2744 SubLayer_Icon.draw(self, context, w, h, appearance) 2745 context.set_source_rgba(*appearance.color(self.__color)) 2746 qubx.GTK.draw_inscribed_arrow(context, self.w, self.h, point_theta=self.__angle)
2747 2748
2749 -class SubLayer_Progress(SubLayer):
2750 """Progress bar, from 0 to 100. 2751 2752 @ivar progress: float from 0 to 100 2753 """
2754 - def __init__(self, color=LAYER_FG, *args, **kw):
2755 SubLayer.__init__(self, *args, **kw) 2756 self.__color = color 2757 self.__progress = 0.0
2758 - def set_progress(self, x):
2759 self.__progress = x 2760 self.invalidate()
2761 progress = property(lambda self: self.__progress, lambda self, x: self.set_progress(x))
2762 - def draw(self, context, w, h, appearance):
2763 SubLayer.draw(self, context, w, h, appearance) 2764 context.set_source_rgba( *appearance.color(self.__color) ) 2765 context.rectangle(1, 1, int(round((self.w-2)*self.__progress/100.0)), self.h-2) 2766 context.fill() 2767 context.set_line_width(.5) 2768 context.rectangle(1, 1, self.w-2, self.h-2) 2769 context.set_source_rgba(.5, .5, .5, .5) 2770 context.stroke()
2771 2772
2773 -class SubLayer_Radio(SubLayer_Label):
2774 """Label with radio button behavior. All events via fellow.on_radio. 2775 2776 @ivar active: bool 2777 """
2778 - def __init__(self, fellow=None, on_radio=lambda sub: None, fill_color=COLOR_RADIO_FILL, active=None, *args, **kw):
2779 self.__ref = Reffer() 2780 kw['action'] = self.__ref(self.__action) 2781 if not ('justify' in kw): 2782 kw['justify'] = -1 2783 if not ('vcenter' in kw): 2784 kw['vcenter'] = 1 2785 SubLayer_Label.__init__(self, *args, **kw) 2786 self.fellow = fellow 2787 self.depends = [] 2788 if self.fellow: 2789 self.fellow.depends.append(self) 2790 self.on_radio = on_radio 2791 self.__color = self.color 2792 self.fill_color = fill_color 2793 self.__active = (not self.fellow) if (active is None) else active 2794 self.color = self.__color
2795 - def set_active(self, x, sub=None, user=False):
2796 self.__active = x 2797 if self.fellow: 2798 if x: 2799 self.fellow.set_active(False, self, user) 2800 else: 2801 for d in self.depends: 2802 if d != sub: 2803 d.active = False 2804 if user: 2805 self.on_radio(sub or self) 2806 self.invalidate()
2807 active = property(lambda self: self.__active, lambda self, x: self.set_active(x))
2808 - def __action(self, x, y, e):
2809 self.set_active(True, user=True)
2810 - def draw(self, context, w, h, appearance):
2811 SubLayer.draw(self, context, w, h, appearance) # to set up w, h 2812 side = min(self.w, self.h) 2813 context.save() 2814 context.translate(1.5*side, 0) 2815 SubLayer_Label.draw(self, context, w, h, appearance) 2816 context.restore() 2817 r, g, b, a = appearance.color(self.fill_color) 2818 if self.entered: 2819 h, s, v = RGBtoHSV(r, g, b) 2820 r, g, b = HSVtoRGB(h, .99, .99) 2821 context.set_source_rgba(r, g, b, a) 2822 context.set_line_width(appearance.emsize/5.0) 2823 rad = side * 0.35 2824 context.move_to(side/2+rad, side/2) 2825 context.arc(side/2, side/2, rad, 0, 2*pi) 2826 if self.__active: 2827 context.stroke_preserve() 2828 context.fill() 2829 else: 2830 context.stroke()
2831 2832 2833 2834 # copied from qubx.modelGTK to avoid circular imports 2835 COLOR = collections.defaultdict(lambda:(.5, .5, .5)) 2836 for i,c in enumerate([(0,0,0), (1,0,0), (0,0,1), (0,1,0), (0,1,1), (1,1,0), (1,0,1)]): 2837 COLOR[i] = c 2838 offset = len(COLOR) 2839 for i in xrange(20): 2840 COLOR[offset+i] = HSVtoRGB((i*2.0/20)%1.0, (40.0-i)/40, (20.0+i)/40) 2841 COLOR_CLASS = lambda c: ('modelGTK.class[%i]' % c, COLOR[c]) 2842
2843 -class Palette(ToolSpace):
2844 __explore_featured = ['OnClickColor', 'color', 'do_click']
2845 - def __init__(self, vertical=True, columns=1):
2846 ToolSpace.__init__(self) 2847 self.__ref = Reffer() 2848 self.OnClickColor = WeakEvent() # (Palette, color) 2849 self.__ref = Reffer() 2850 self.OnDraw += self.__ref(self.__onDraw) 2851 self.__labels = [] 2852 x = y = 0 2853 w = h = PALETTE_H_EMS 2854 dx = dy = 0 2855 if vertical: 2856 self.set_size_request(15*columns,220) 2857 dy = h 2858 else: 2859 self.set_size_request(220, 15*columns) 2860 dx = w 2861 for c in xrange(columns): 2862 if vertical: y = 0 2863 else: x = 0 2864 for i in xrange(10): 2865 layer = Layer(x=x, y=y, w=w, h=h, cBG=COLOR_CLASS(c*10+i)) 2866 self.add_layer(layer) 2867 sub = SubLayer_Label(((c*10+i) == 1) and 'O' or '', 0, 1, w=w, h=h, border=3, 2868 color=COLOR_PALETTE_TEXT, hover_color=COLOR_PALETTE_TEXT, action=self.__ref(bind(self.do_click, c*10+i))) 2869 layer.add_sublayer(sub) 2870 self.__labels.append(sub) 2871 x += dx 2872 y += dy 2873 if vertical: x += dy 2874 else: y += dx 2875 if vertical: 2876 x = 0 2877 w = -.01 2878 else: 2879 y = 0 2880 h = -.01 2881 layer = Layer(x=x, y=y, w=w, h=h, cBG=COLOR_PALETTE_MORE) 2882 self.add_layer(layer) 2883 self.subMore = SubLayer_Label('...', 0, 1, w=w, h=h, border=3, color=COLOR_PALETTE_TEXT, 2884 hover_color=COLOR_PALETTE_TEXT, action=self.__ref(self.__onClickMenu)) 2885 layer.add_sublayer(self.subMore) 2886 self.__color = 1
2887 - def do_click(self, x):
2888 self.set_color(x) 2889 self.OnClickColor(self, x)
2890 - def __onClickMenu(self, x, y, e):
2891 menu = gtk.Menu() 2892 for i in xrange(16): 2893 build_menuitem(str(i), self.__ref(bind(self.do_click, i)), menu=menu, item_class=gtk.CheckMenuItem, active=(i == self.__color)) 2894 build_menuitem('Other...', self.__ref(self.__onClickOther), menu=menu, item_class=gtk.CheckMenuItem, active=(self.__color >= 16)) 2895 menu.popup(None, None, None, 0, e.time)
2896 - def __onClickOther(self, item):
2897 grp = qubx.pyenvGTK.prompt_entry('Pick color/group:', self.__color, acceptIntGreaterThanOrEqualTo(0)) 2898 if grp is None: 2899 return 2900 self.set_color(grp) 2901 self.OnClickColor(self, grp)
2902 - def set_color(self, x):
2903 if self.__color == x: 2904 return 2905 if 0 <= self.__color < len(self.__labels): 2906 self.__labels[self.__color].label = '' 2907 self.__color = x 2908 if 0 <= x < len(self.__labels): 2909 self.__labels[self.__color].label = 'O'
2910 color = property(lambda self: self.__color, lambda self, x: self.set_color(x))
2911 - def __onDraw(self, context, w, h):
2912 context.set_source_rgb(0,0,0) 2913 context.paint()
2914 2915 2916 2917 ZOOM_FACTOR = 1.02 2918 ZOOM_FACTOR_PER_PIXEL = .001 2919 ZOOM_DELAY_MS = 20 2920 COLOR_ZOOM = ('qubx.toolspace.zoom', (0, 0, 0, 1)) 2921 ColorInfo[COLOR_ZOOM[0]].label = 'Chart zoom button' 2922 COLOR_ZOOM_HOVER = ('qubx.toolspace.zoom.hover', (.5, 0, 0, 1)) 2923 ColorInfo[COLOR_ZOOM_HOVER[0]].label = 'Chart zoom button mouseover' 2924
2925 -class SubLayer_SmoothZoom(SubLayer_Label):
2926 - def __init__(self, symbol='+', zoom_out=False, **kw):
2927 self.__ref = Reffer() 2928 kw['mouse_drag'] = self.__ref(self.__mouse_drag) 2929 if not ('border' in kw): 2930 kw['border'] = 1 2931 if not ('color' in kw): 2932 kw['color'] = COLOR_ZOOM 2933 kw['hover_color'] = COLOR_ZOOM_HOVER 2934 if not ('cBorder' in kw): 2935 kw['cBorder'] = kw['color'] 2936 SubLayer_Label.__init__(self, symbol, 0, 1, **kw) 2937 self.__zoom_out = zoom_out 2938 self.OnZoom = WeakEvent() # (sub, factor) 2939 self.__timer = None
2940 - def button_press(self, x, y, e):
2941 self.remove_timer() 2942 self.__x0, self.__y0 = x, y 2943 self.__dx, self.__dy, self.__accel_pix = 0, 0, 0 2944 self.__x, self.__y, self.__e_state = x, y, e.state 2945 self.__do_zoom() 2946 self.__timer = gobject.timeout_add(ZOOM_DELAY_MS, self.__do_zoom)
2947 - def button_release(self, x, y, e):
2948 self.remove_timer()
2949 - def remove_timer(self):
2950 if self.__timer: 2951 gobject.source_remove(self.__timer) 2952 self.__timer = None
2953 - def __mouse_drag(self, x, y, e):
2954 self.__x, self.__y, self.__e_state = x, y, e.state 2955 dx = x - self.__x0 2956 dy = y - self.__y0 2957 if (not self.__dx) and (not self.__dy): 2958 self.__dx, self.__dy = dx, dy 2959 if dx or dy: 2960 self.__dm = sqrt(dx*dx + dy*dy) 2961 else: 2962 # the magnitude of (dx, dy) in the (self.__dx, self.__dy) direction = (A dot B) / mag(B) 2963 self.__accel_pix = (dx*self.__dx + dy*self.__dy) / self.__dm
2964 - def __do_zoom(self):
2965 factor = ZOOM_FACTOR + self.__accel_pix * ZOOM_FACTOR_PER_PIXEL 2966 if self.__zoom_out: 2967 factor = 1.0 / factor 2968 self.OnZoom(self, factor) 2969 return True # repeat
2970 2971
2972 -def draw_pan_icon(cr, w, h):
2973 w = int(w) 2974 h = int(h) 2975 margin = min(w, h)/6 2976 cr.save() 2977 cr.translate(margin, margin) 2978 w -= 2*margin 2979 h -= 2*margin 2980 cr.set_line_width(1.0) 2981 cr.set_source_rgba(1, 1, 1, 1) 2982 cr.rectangle(0, 0, w, h) 2983 cr.fill_preserve() 2984 cr.set_source_rgba(0,0,0,1) 2985 cr.stroke() 2986 2987 margin = min(w, h)/10.0 2988 cr.set_line_width(0.5) 2989 cr.move_to(w/2, h/2) 2990 cr.line_to(w/2, margin) 2991 cr.line_to(w/2+margin, 2*margin) 2992 cr.stroke() 2993 cr.move_to(w/2, margin) 2994 cr.line_to(w/2-margin, 2*margin) 2995 cr.stroke() 2996 cr.move_to(w/2, h/2) 2997 cr.line_to(w/2, h-margin) 2998 cr.line_to(w/2+margin, h-2*margin) 2999 cr.stroke() 3000 cr.move_to(w/2, h-margin) 3001 cr.line_to(w/2-margin, h-2*margin) 3002 cr.stroke() 3003 cr.move_to(w/2, h/2) 3004 cr.line_to(margin, h/2) 3005 cr.line_to(2*margin, h/2+margin) 3006 cr.stroke() 3007 cr.move_to(margin, h/2) 3008 cr.line_to(2*margin, h/2-margin) 3009 cr.stroke() 3010 cr.move_to(w/2, h/2) 3011 cr.line_to(w-margin, h/2) 3012 cr.line_to(w-2*margin, h/2+margin) 3013 cr.stroke() 3014 cr.move_to(w-margin, h/2) 3015 cr.line_to(w-2*margin, h/2-margin) 3016 cr.stroke() 3017 cr.restore()
3018
3019 -class SubLayer_PanIcon(SubLayer_Icon):
3020 - def draw(self, context, w, h, appearance):
3021 SubLayer_Icon.draw(self, context, w, h, appearance) 3022 draw_pan_icon(context, self.w, self.h)
3023 3024
3025 -def measure_string(cr, points, s):
3026 cr.set_font_size(points) 3027 xbearing, ybearing, width, height, xadvance, yadvance = cr.text_extents(s) 3028 fascent, fdescent, fheight, fxadvance, fyadvance = cr.font_extents() 3029 return Anon(xbearing=xbearing, ybearing=ybearing, width=width, height=height, xadvance=xadvance, yadvance=yadvance, 3030 font=Anon(ascent=fascent, descent=fdescent, height=fheight, xadvance=fxadvance, yadvance=fyadvance))
3031