...
[iramuteq] / aui / aui_utilities.py
1 """
2 This module contains some common functions used by wxPython-AUI to
3 manipulate colours, bitmaps, text, gradient shadings and custom
4 dragging images for AuiNotebook tabs.
5 """
6
7 __author__ = "Andrea Gavana <andrea.gavana@gmail.com>"
8 __date__ = "31 March 2009"
9
10
11 import wx
12
13 from aui_constants import *
14
15
16 if wx.Platform == "__WXMAC__":
17     import Carbon.Appearance
18     
19     
20 def BlendColour(fg, bg, alpha):
21     """
22     Blends the two colour component `fg` and `bg` into one colour component, adding
23     an optional alpha channel.
24
25     :param `fg`: the first colour component;
26     :param `bg`: the second colour component;
27     :param `alpha`: an optional transparency value.
28     """
29     
30     result = bg + (alpha*(fg - bg))
31     
32     if result < 0.0:
33         result = 0.0
34     if result > 255:
35         result = 255
36         
37     return result
38
39
40 def StepColour(c, ialpha):
41     """
42     Darken/lighten the input colour `c`.
43
44     :param `c`: a colour to darken/lighten;
45     :param `ialpha`: a transparency value.
46     """
47     
48     if ialpha == 100:
49         return c
50         
51     r, g, b = c.Red(), c.Green(), c.Blue()
52
53     # ialpha is 0..200 where 0 is completely black
54     # and 200 is completely white and 100 is the same
55     # convert that to normal alpha 0.0 - 1.0
56     ialpha = min(ialpha, 200)
57     ialpha = max(ialpha, 0)
58     alpha = (ialpha - 100.0)/100.0
59
60     if ialpha > 100:
61     
62         # blend with white
63         bg = 255
64         alpha = 1.0 - alpha  # 0 = transparent fg 1 = opaque fg
65     
66     else:
67     
68         # blend with black
69         bg = 0
70         alpha = 1.0 + alpha  # 0 = transparent fg 1 = opaque fg
71     
72     r = BlendColour(r, bg, alpha)
73     g = BlendColour(g, bg, alpha)
74     b = BlendColour(b, bg, alpha)
75
76     return wx.Colour(r, g, b)
77
78
79 def LightContrastColour(c):
80     """
81     Creates a new, lighter colour based on the input colour `c`.
82
83     :param `c`: the input colour to analyze.
84     """
85
86     amount = 120
87
88     # if the colour is especially dark, then
89     # make the contrast even lighter
90     if c.Red() < 128 and c.Green() < 128 and c.Blue() < 128:
91         amount = 160
92
93     return StepColour(c, amount)
94
95
96 def ChopText(dc, text, max_size):
97     """
98     Chops the input `text` if its size does not fit in `max_size`, by cutting the
99     text and adding ellipsis at the end.
100
101     :param `dc`: a `wx.DC` device context;
102     :param `text`: the text to chop;
103     :param `max_size`: the maximum size in which the text should fit.
104     """
105     
106     # first check if the text fits with no problems
107     x, y, dummy = dc.GetMultiLineTextExtent(text)
108     
109     if x <= max_size:
110         return text
111
112     textLen = len(text)
113     last_good_length = 0
114     
115     for i in xrange(textLen, -1, -1):
116         s = text[0:i]
117         s += "..."
118
119         x, y = dc.GetTextExtent(s)
120         last_good_length = i
121         
122         if x < max_size:
123             break
124
125     ret = text[0:last_good_length] + "..."    
126     return ret
127
128
129 def BitmapFromBits(bits, w, h, colour):
130     """
131     BitmapFromBits() is a utility function that creates a
132     masked bitmap from raw bits (XBM format).
133
134     :param `bits`: a string containing the raw bits of the bitmap;
135     :param `w`: the bitmap width;
136     :param `h`: the bitmap height;
137     :param `colour`: the colour which will replace all white pixels in the
138      raw bitmap.
139     """
140
141     img = wx.BitmapFromBits(bits, w, h).ConvertToImage()
142     img.Replace(0, 0, 0, 123, 123, 123)
143     img.Replace(255, 255, 255, colour.Red(), colour.Green(), colour.Blue())
144     img.SetMaskColour(123, 123, 123)
145     return wx.BitmapFromImage(img)
146
147
148 def IndentPressedBitmap(rect, button_state):
149     """
150     Indents the input rectangle `rect` based on the value of `button_state`.
151
152     :param `rect`: an instance of wx.Rect;
153     :param `button_state`: an L{AuiNotebook} button state.
154     """
155
156     if button_state == AUI_BUTTON_STATE_PRESSED:
157         rect.x += 1
158         rect.y += 1
159
160     return rect
161
162
163 def GetBaseColour():
164     """
165     Returns the face shading colour on push buttons/backgrounds, mimicking as closely
166     as possible the platform UI colours.
167     """
168
169     if wx.Platform == "__WXMAC__":
170
171         if hasattr(wx, 'MacThemeColour'):
172             base_colour = wx.MacThemeColour(Carbon.Appearance.kThemeBrushToolbarBackground)
173         else:
174             brush = wx.Brush(wx.BLACK)
175             brush.MacSetTheme(Carbon.Appearance.kThemeBrushToolbarBackground)
176             base_colour = brush.GetColour()
177
178     else:
179         
180         base_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)
181
182     # the base_colour is too pale to use as our base colour,
183     # so darken it a bit
184     if ((255-base_colour.Red()) +
185         (255-base_colour.Green()) +
186         (255-base_colour.Blue()) < 60):
187     
188         base_colour = StepColour(base_colour, 92)
189     
190     return base_colour
191
192
193 def MakeDisabledBitmap(bitmap):
194     """
195     Convert the given image (in place) to a grayed-out version,
196     appropriate for a 'disabled' appearance.
197
198     :param `bitmap`: the bitmap to gray-out.
199     """
200
201     anImage = bitmap.ConvertToImage()    
202     factor = 0.7        # 0 < f < 1.  Higher Is Grayer
203     
204     if anImage.HasMask():
205         maskColour = (anImage.GetMaskRed(), anImage.GetMaskGreen(), anImage.GetMaskBlue())
206     else:
207         maskColour = None
208         
209     data = map(ord, list(anImage.GetData()))
210
211     for i in range(0, len(data), 3):
212         
213         pixel = (data[i], data[i+1], data[i+2])
214         pixel = MakeGray(pixel, factor, maskColour)
215
216         for x in range(3):
217             data[i+x] = pixel[x]
218
219     anImage.SetData(''.join(map(chr, data)))
220     
221     return anImage.ConvertToBitmap()
222
223
224 def MakeGray(rgbTuple, factor, maskColour):
225     """
226     Make a pixel grayed-out. If the pixel matches the `maskColour`, it won't be
227     changed.
228
229     :param `rgbTuple`: a tuple representing a pixel colour;
230     :param `factor`: a graying-out factor;
231     :param `maskColour`: a colour mask.
232     """
233
234     if rgbTuple != maskColour:
235         r, g, b = rgbTuple
236         return map(lambda x: int((230 - x) * factor) + x, (r, g, b))
237     else:
238         return rgbTuple
239
240
241 def Clip(a, b, c):
242     """
243     Clips the value in `a` based on the extremes `b` and `c`.
244
245     :param `a`: the value to analyze;
246     :param `b`: a minimum value;
247     :param `c`: a maximum value.
248     """
249
250     return ((a < b and [b]) or [(a > c and [c] or [a])[0]])[0]
251
252
253 def LightColour(colour, percent):
254     """
255     Brighten input `colour` by `percent`.
256
257     :param `colour`: the colour to be brightened;
258     :param `percent`: brightening percentage.
259     """
260     
261     end_colour = wx.WHITE
262     
263     rd = end_colour.Red() - colour.Red()
264     gd = end_colour.Green() - colour.Green()
265     bd = end_colour.Blue() - colour.Blue()
266
267     high = 100
268
269     # We take the percent way of the colour from colour -. white
270     i = percent
271     r = colour.Red() + ((i*rd*100)/high)/100
272     g = colour.Green() + ((i*gd*100)/high)/100
273     b = colour.Blue() + ((i*bd*100)/high)/100
274     return wx.Colour(r, g, b)
275
276
277 def PaneCreateStippleBitmap():
278     """
279     Creates a stipple bitmap to be used in a `wx.Brush`.
280     This is used to draw sash resize hints.
281     """
282
283     data = [0, 0, 0, 192, 192, 192, 192, 192, 192, 0, 0, 0]
284     img = wx.EmptyImage(2, 2)
285     counter = 0
286     
287     for ii in xrange(2):
288         for jj in xrange(2):
289             img.SetRGB(ii, jj, data[counter], data[counter+1], data[counter+2])
290             counter = counter + 3
291     
292     return img.ConvertToBitmap()
293
294
295 def DrawMACCloseButton(colour, backColour=None):
296     """
297     Draws the wxMAC tab close button using `wx.GraphicsContext`.
298
299     :param `colour`: the colour to use to draw the circle;
300     :param `backColour`: the optional background colour for the circle.
301     """
302
303     bmp = wx.EmptyBitmapRGBA(16, 16)
304     dc = wx.MemoryDC()
305     dc.SelectObject(bmp)
306
307     gc = wx.GraphicsContext.Create(dc)    
308     gc.SetBrush(wx.Brush(colour))
309     path = gc.CreatePath()
310     path.AddCircle(6.5, 7, 6.5)
311     path.CloseSubpath()
312     gc.FillPath(path)
313     
314     path = gc.CreatePath()
315     if backColour is not None:
316         pen = wx.Pen(backColour, 2)
317     else:
318         pen = wx.Pen("white", 2)
319         
320     pen.SetCap(wx.CAP_BUTT)
321     pen.SetJoin(wx.JOIN_BEVEL)
322     gc.SetPen(pen)
323     path.MoveToPoint(3.5, 4)
324     path.AddLineToPoint(9.5, 10)
325     path.MoveToPoint(3.5, 10)
326     path.AddLineToPoint(9.5, 4)
327     path.CloseSubpath()
328     gc.DrawPath(path)
329
330     dc.SelectObject(wx.NullBitmap)
331     return bmp
332
333
334 def DarkenBitmap(bmp, caption_colour, new_colour):
335     """
336     Darkens the input bitmap on wxMAC using the input colour.
337     
338     :param `bmp`: the bitmap to be manipulated;
339     :param `caption_colour`: the colour of the pane caption;
340     :param `new_colour`: the colour used to darken the bitmap.
341     """
342
343     image = bmp.ConvertToImage()
344     red = caption_colour.Red()/float(new_colour.Red())
345     green = caption_colour.Green()/float(new_colour.Green())
346     blue = caption_colour.Blue()/float(new_colour.Blue())
347     image = image.AdjustChannels(red, green, blue)
348     return image.ConvertToBitmap()
349
350     
351 def DrawGradientRectangle(dc, rect, start_colour, end_colour, direction, offset=0, length=0):
352     """
353     Draws a gradient-shaded rectangle.
354
355     :param `dc`: a `wx.DC` device context;
356     :param `rect`: the rectangle in which to draw the gradient;
357     :param `start_colour`: the first colour of the gradient;
358     :param `end_colour`: the second colour of the gradient;
359     :param `direction`: the gradient direction (horizontal or vertical).
360     """
361     
362     if direction == AUI_GRADIENT_VERTICAL:
363         dc.GradientFillLinear(rect, start_colour, end_colour, wx.SOUTH)
364     else:
365         dc.GradientFillLinear(rect, start_colour, end_colour, wx.EAST)
366         
367
368 def FindFocusDescendant(ancestor):
369     """
370     Find a window with the focus, that is also a descendant of the given window.
371     This is used to determine the window to initially send commands to.
372
373     :param `ancestor`: the window to check for ancestry.    
374     """
375
376     # Process events starting with the window with the focus, if any.
377     focusWin = wx.Window.FindFocus()
378     win = focusWin
379
380     # Check if this is a descendant of this frame.
381     # If not, win will be set to NULL.
382     while win:
383         if win == ancestor:
384             break
385         else:
386             win = win.GetParent()
387
388     if win is None:
389         focusWin = None
390
391     return focusWin
392
393
394 def GetLabelSize(dc, label, vertical):
395     """
396     Returns the L{AuiToolBar} item label size.
397
398     :param `label`: the toolbar tool label;
399     :param `vertical`: whether the toolbar tool orientation is vertical or not.
400     """
401
402     text_width = text_height = 0
403
404     # get the text height
405     dummy, text_height = dc.GetTextExtent("ABCDHgj")
406     # get the text width
407     if label.strip():
408         text_width, dummy = dc.GetTextExtent(label)
409
410     if vertical:
411         tmp = text_height
412         text_height = text_width
413         text_width = tmp
414
415     return wx.Size(text_width, text_height)
416
417
418 #---------------------------------------------------------------------------
419 # TabDragImage implementation
420 # This class handles the creation of a custom image when dragging
421 # AuiNotebook tabs
422 #---------------------------------------------------------------------------
423
424 class TabDragImage(wx.DragImage):
425     """
426     This class handles the creation of a custom image in case of drag and
427     drop of a notebook tab.
428     """
429
430     def __init__(self, notebook, page, button_state, tabArt):
431         """
432         Default class constructor.
433         
434         For internal use: do not call it in your code!
435
436         :param `notebook`: an instance of L{AuiNotebook};
437         :param `page`: the dragged L{AuiNotebook} page;
438         :param `button_state`: the state of the close button on the tab;
439         :param `tabArt`: an instance of L{AuiDefaultTabArt} or one of its derivations.
440         """
441
442         self._backgroundColour = wx.NamedColour("pink")        
443         self._bitmap = self.CreateBitmap(notebook, page, button_state, tabArt)
444         wx.DragImage.__init__(self, self._bitmap)
445
446
447     def CreateBitmap(self, notebook, page, button_state, tabArt):
448         """
449         Actually creates the drag and drop bitmap.
450
451         :param `notebook`: an instance of L{AuiNotebook};
452         :param `page`: the dragged L{AuiNotebook} page;
453         :param `button_state`: the state of the close button on the tab;
454         :param `tabArt`: an instance of L{AuiDefaultTabArt} or one of its derivations.
455         """
456
457         control = page.control
458         memory = wx.MemoryDC(wx.EmptyBitmap(1, 1))
459
460         tab_size, x_extent = tabArt.GetTabSize(memory, notebook, page.caption, page.bitmap, page.active,
461                                                button_state, control)
462             
463         tab_width, tab_height = tab_size
464         rect = wx.Rect(0, 0, tab_width, tab_height)
465
466         bitmap = wx.EmptyBitmap(tab_width+1, tab_height+1)
467         memory.SelectObject(bitmap)
468
469         if wx.Platform == "__WXMAC__":
470             memory.SetBackground(wx.TRANSPARENT_BRUSH)
471         else:
472             memory.SetBackground(wx.Brush(self._backgroundColour))
473             
474         memory.SetBackgroundMode(wx.TRANSPARENT)
475         memory.Clear()
476
477         paint_control = wx.Platform != "__WXMAC__"
478         tabArt.DrawTab(memory, notebook, page, rect, button_state, paint_control=paint_control)
479         
480         memory.SetBrush(wx.TRANSPARENT_BRUSH)
481         memory.SetPen(wx.BLACK_PEN)
482         memory.DrawRoundedRectangle(0, 0, tab_width+1, tab_height+1, 2)
483
484         memory.SelectObject(wx.NullBitmap)
485         
486         # Gtk and Windows unfortunatly don't do so well with transparent
487         # drawing so this hack corrects the image to have a transparent
488         # background.
489         if wx.Platform != '__WXMAC__':
490             timg = bitmap.ConvertToImage()
491             if not timg.HasAlpha():
492                 timg.InitAlpha()
493             for y in xrange(timg.GetHeight()):
494                 for x in xrange(timg.GetWidth()):
495                     pix = wx.Colour(timg.GetRed(x, y),
496                                     timg.GetGreen(x, y),
497                                     timg.GetBlue(x, y))
498                     if pix == self._backgroundColour:
499                         timg.SetAlpha(x, y, 0)
500             bitmap = timg.ConvertToBitmap()
501         return bitmap        
502
503
504 def GetDockingImage(direction, useAero, center):
505     """
506     Returns the correct name of the docking bitmap depending on the input parameters.
507
508     :param `useAero`: whether L{AuiManager} is using Aero-style or Whidbey-style docking
509      images or not;
510     :param `center`: whether we are looking for the center diamond-shaped bitmap or not. 
511     """
512
513     suffix = (center and [""] or ["_single"])[0]
514     prefix = ""
515     if useAero == 2:
516         # Whidbey docking guides
517         prefix = "whidbey_"
518     elif useAero == 1:
519         # Aero docking style
520         prefix = "aero_"
521         
522     if direction == wx.TOP:
523         bmp_unfocus = eval("%sup%s"%(prefix, suffix)).GetBitmap()
524         bmp_focus = eval("%sup_focus%s"%(prefix, suffix)).GetBitmap()
525     elif direction == wx.BOTTOM:
526         bmp_unfocus = eval("%sdown%s"%(prefix, suffix)).GetBitmap()
527         bmp_focus = eval("%sdown_focus%s"%(prefix, suffix)).GetBitmap()
528     elif direction == wx.LEFT:
529         bmp_unfocus = eval("%sleft%s"%(prefix, suffix)).GetBitmap()
530         bmp_focus = eval("%sleft_focus%s"%(prefix, suffix)).GetBitmap()
531     elif direction == wx.RIGHT:
532         bmp_unfocus = eval("%sright%s"%(prefix, suffix)).GetBitmap()
533         bmp_focus = eval("%sright_focus%s"%(prefix, suffix)).GetBitmap()
534     else:
535         bmp_unfocus = eval("%stab%s"%(prefix, suffix)).GetBitmap()
536         bmp_focus = eval("%stab_focus%s"%(prefix, suffix)).GetBitmap()
537
538     return bmp_unfocus, bmp_focus
539
540
541 def TakeScreenShot(rect):
542     """
543     Takes a screenshot of the screen at given position and size (rect).
544
545     :param `rect`: the screen rectangle for which we want to take a screenshot.
546     """
547
548     # Create a DC for the whole screen area
549     dcScreen = wx.ScreenDC()
550
551     # Create a Bitmap that will later on hold the screenshot image
552     # Note that the Bitmap must have a size big enough to hold the screenshot
553     # -1 means using the current default colour depth
554     bmp = wx.EmptyBitmap(rect.width, rect.height)
555
556     # Create a memory DC that will be used for actually taking the screenshot
557     memDC = wx.MemoryDC()
558
559     # Tell the memory DC to use our Bitmap
560     # all drawing action on the memory DC will go to the Bitmap now
561     memDC.SelectObject(bmp)
562
563     # Blit (in this case copy) the actual screen on the memory DC
564     # and thus the Bitmap
565     memDC.Blit( 0,            # Copy to this X coordinate
566                 0,            # Copy to this Y coordinate
567                 rect.width,   # Copy this width
568                 rect.height,  # Copy this height
569                 dcScreen,     # From where do we copy?
570                 rect.x,       # What's the X offset in the original DC?
571                 rect.y        # What's the Y offset in the original DC?
572                 )
573
574     # Select the Bitmap out of the memory DC by selecting a new
575     # uninitialized Bitmap
576     memDC.SelectObject(wx.NullBitmap)
577
578     return bmp
579
580
581 def RescaleScreenShot(bmp, thumbnail_size=200):
582     """
583     Rescales a bitmap to be 300 pixels wide (or tall) at maximum.
584
585     :param `bmp`: the bitmap to rescale;
586     :param `thumbnail_size`: the maximum size of every page thumbnail.
587     """
588
589     bmpW, bmpH = bmp.GetWidth(), bmp.GetHeight()
590     img = bmp.ConvertToImage()
591
592     newW, newH = bmpW, bmpH
593     
594     if bmpW > bmpH:
595         if bmpW > thumbnail_size:
596             ratio = bmpW/float(thumbnail_size)
597             newW, newH = int(bmpW/ratio), int(bmpH/ratio)
598             img.Rescale(newW, newH, wx.IMAGE_QUALITY_HIGH)
599     else:
600         if bmpH > thumbnail_size:
601             ratio = bmpH/float(thumbnail_size)
602             newW, newH = int(bmpW/ratio), int(bmpH/ratio)
603             img.Rescale(newW, newH, wx.IMAGE_QUALITY_HIGH)
604
605     newBmp = img.ConvertToBitmap()
606     otherBmp = wx.EmptyBitmap(newW+5, newH+5)    
607
608     memDC = wx.MemoryDC()
609     memDC.SelectObject(otherBmp)
610     memDC.SetBackground(wx.WHITE_BRUSH)
611     memDC.Clear()
612     
613     memDC.SetPen(wx.TRANSPARENT_PEN)
614
615     pos = 0
616     for i in xrange(5, 0, -1):
617         brush = wx.Brush(wx.Colour(50*i, 50*i, 50*i))
618         memDC.SetBrush(brush)
619         memDC.DrawRoundedRectangle(0, 0, newW+5-pos, newH+5-pos, 2)
620         pos += 1
621
622     memDC.DrawBitmap(newBmp, 0, 0, True)
623      
624     # Select the Bitmap out of the memory DC by selecting a new
625     # uninitialized Bitmap
626     memDC.SelectObject(wx.NullBitmap)
627
628     return otherBmp
629
630
631 def GetSlidingPoints(rect, size, direction):
632     """
633     Returns the point at which the sliding in and out of a minimized pane begins.
634
635     :param `rect`: the L{AuiToolBar} tool screen rectangle;
636     :param `size`: the pane window size;
637     :param `direction`: the pane docking direction.
638     """
639
640     if direction == AUI_DOCK_LEFT:
641         startX, startY = rect.x + rect.width + 2, rect.y
642     elif direction == AUI_DOCK_TOP:
643         startX, startY = rect.x, rect.y + rect.height + 2
644     elif direction == AUI_DOCK_RIGHT:
645         startX, startY = rect.x - size.x - 2, rect.y
646     elif direction == AUI_DOCK_BOTTOM:
647         startX, startY = rect.x, rect.y - size.y - 2
648     else:
649         raise Exception("How did we get here?")
650
651     caption_height = wx.SystemSettings.GetMetric(wx.SYS_CAPTION_Y)
652     frame_border_x = wx.SystemSettings.GetMetric(wx.SYS_FRAMESIZE_X)
653     frame_border_y = wx.SystemSettings.GetMetric(wx.SYS_FRAMESIZE_Y)
654     
655     stopX = size.x + caption_height + frame_border_x
656     stopY = size.x + frame_border_y
657     
658     return startX, startY, stopX, stopY
659
660
661 def CopyAttributes(newArt, oldArt):
662     """
663     Copies pens, brushes, colours and fonts from the old tab art to the new one.
664
665     :param `newArt`: the new instance of L{AuiDefaultTabArt};
666     :param `oldArt`: the old instance of L{AuiDefaultTabArt}.
667     """    
668     
669     attrs = dir(oldArt)
670
671     for attr in attrs:
672         if attr.startswith("_") and (attr.endswith("_colour") or attr.endswith("_font") or \
673                                      attr.endswith("_font") or attr.endswith("_brush") or \
674                                      attr.endswith("Pen") or attr.endswith("_pen")):
675             setattr(newArt, attr, getattr(oldArt, attr))
676
677     return newArt            
678