X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/66dae888d1b46d754469d990a2bbc957163f0bad..5589b01ba74638fcf94ee21f5581b050ee233156:/wxPython/wx/lib/buttonpanel.py?ds=inline diff --git a/wxPython/wx/lib/buttonpanel.py b/wxPython/wx/lib/buttonpanel.py index d99ebee363..7df01da87d 100644 --- a/wxPython/wx/lib/buttonpanel.py +++ b/wxPython/wx/lib/buttonpanel.py @@ -11,7 +11,7 @@ # Python Code By: # # Andrea Gavana, @ 02 Oct 2006 -# Latest Revision: 02 Oct 2006, 17.00 GMT +# Latest Revision: 17 Oct 2006, 17.00 GMT # # # For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please @@ -45,6 +45,33 @@ classic look). Usage ----- +ButtonPanel supports 4 alignments: left, right, top, bottom, which have a +different meaning and behavior wrt wx.Toolbar. The easiest thing is to try +the demo to understand, but I'll try to explain how it works. + +CASE 1: ButtonPanel has a main caption text + +Left alignment means ButtonPanel is horizontal, with the text aligned to the +left. When you shrink the demo frame, if there is not enough room for all +the controls to be shown, the controls closest to the text are hidden; + +Right alignment means ButtonPanel is horizontal, with the text aligned to the +right. Item layout as above; + +Top alignment means ButtonPanel is vertical, with the text aligned to the top. +Item layout as above; + +Bottom alignment means ButtonPanel is vertical, with the text aligned to the +bottom. Item layout as above. + + +CASE 2: ButtonPanel has *no* main caption text +In this case, left and right alignment are the same (as top and bottom are the same), +but the layout strategy changes: now if there is not enough room for all the controls +to be shown, the last added items are hidden ("last" means on the far right for +horizontal ButtonPanels and far bottom for vertical ButtonPanels). + + The following example shows a simple implementation that uses ButtonPanel inside a very simple frame:: @@ -81,8 +108,6 @@ inside a very simple frame:: titleBar.AddButton(btn4) self.Bind(wx.EVT_BUTTON, self.OnButton, btn4) - titleBar.SetColor(BP_TEXT_COLOR, wx.WHITE) - titleBar.SetColor(BP_CAPTION_BORDER_COLOR, wx.WHITE) vSizer.Add(titleBar, 0, wx.EXPAND) vSizer.Add((20, 20)) vSizer.Add(self.logtext, 1, wx.EXPAND|wx.ALL, 5) @@ -104,38 +129,92 @@ License And Version: ButtonPanel Is Freeware And Distributed Under The wxPython License. -Latest Revision: Andrea Gavana @ 02 Oct 2006, 17.00 GMT -Version 0.1. +Latest Revision: Andrea Gavana @ 12 Oct 2006, 17.00 GMT +Version 0.3. """ import wx -# Some constants -BP_CAPTION_COLOR = 0, -BP_CAPTION_GRADIENT_COLOR = 1 -BP_CAPTION_BORDER_COLOR = 2 -BP_TEXT_COLOR = 3 - -# Buttons states -BP_BTN_NONE = 100 -BP_BTN_PRESSED = 101 -BP_BTN_HOVER = 102 +# Some constants to tune the BPArt class +BP_BACKGROUND_COLOR = 0 +""" Background brush colour when no gradient shading exists. """ +BP_GRADIENT_COLOR_FROM = 1 +""" Starting gradient colour, used only when BP_USE_GRADIENT style is applied. """ +BP_GRADIENT_COLOR_TO = 2 +""" Ending gradient colour, used only when BP_USE_GRADIENT style is applied. """ +BP_BORDER_COLOR = 3 +""" Pen colour to paint the border of ButtonPanel. """ +BP_TEXT_COLOR = 4 +""" Main ButtonPanel caption colour. """ +BP_BUTTONTEXT_COLOR = 5 +""" Text colour for buttons with text. """ +BP_BUTTONTEXT_INACTIVE_COLOR = 6 +""" Text colour for inactive buttons with text. """ +BP_SELECTION_BRUSH_COLOR = 7 +""" Brush colour to be used when hovering or selecting a button. """ +BP_SELECTION_PEN_COLOR = 8 +""" Pen colour to be used when hovering or selecting a button. """ +BP_SEPARATOR_COLOR = 9 +""" Pen colour used to paint the separators. """ +BP_TEXT_FONT = 10 +""" Font of the ButtonPanel main caption. """ +BP_BUTTONTEXT_FONT = 11 +""" Text font for the buttons with text. """ + +BP_BUTTONTEXT_ALIGN_BOTTOM = 12 +""" Flag that indicates the image and text in buttons is stacked. """ +BP_BUTTONTEXT_ALIGN_RIGHT = 13 +""" Flag that indicates the text is shown alongside the image in buttons with text. """ + +BP_SEPARATOR_SIZE = 14 +""" +Separator size. NB: This is not the line width, but the sum of the space before +and after the separator line plus the width of the line. +""" +BP_MARGINS_SIZE = 15 +""" +Size of the left/right margins in ButtonPanel (top/bottom for vertically +aligned ButtonPanels). +""" +BP_BORDER_SIZE = 16 +""" Size of the border. """ +BP_PADDING_SIZE = 17 +""" Inter-tool separator size. """ + +# Caption Gradient Type +BP_GRADIENT_NONE = 0 +""" No gradient shading should be used to paint the background. """ +BP_GRADIENT_VERTICAL = 1 +""" Vertical gradient shading should be used to paint the background. """ +BP_GRADIENT_HORIZONTAL = 2 +""" Horizontal gradient shading should be used to paint the background. """ # Flags for HitTest() method BP_HT_BUTTON = 200 BP_HT_NONE = 201 # Alignment of buttons in the panel -BP_ALIGN_RIGHT = 1 -BP_ALIGN_LEFT = 2 +BP_ALIGN_RIGHT = 1 +BP_ALIGN_LEFT = 2 +BP_ALIGN_TOP = 4 +BP_ALIGN_BOTTOM = 8 + +# ButtonPanel styles +BP_DEFAULT_STYLE = 1 +BP_USE_GRADIENT = 2 + +# Delay used to cancel the longHelp in the statusbar field +_DELAY = 3000 -# ButtonPanel default style -BP_DEFAULT_STYLE = 2 + +# Check for the new method in 2.7 (not present in 2.6.3.3) +if wx.VERSION_STRING < "2.7": + wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point) -def BrightenColor(color, factor): +def BrightenColour(color, factor): """ Bright the input colour by a factor.""" val = color.Red()*factor @@ -159,449 +238,1751 @@ def BrightenColor(color, factor): return wx.Color(red, green, blue) -# -- ButtonInfo class implementation ---------------------------------------- -# This class holds information about every button that is added to -# ButtonPanel. It is an auxiliary class that you should use -# every time you add a button. +def GrayOut(anImage): + """ + Convert the given image (in place) to a grayed-out version, + appropriate for a 'Disabled' appearance. + """ + + factor = 0.7 # 0 < f < 1. Higher Is Grayer -class ButtonInfo: + anImage = anImage.ConvertToImage() + if anImage.HasAlpha(): + anImage.ConvertAlphaToMask(1) + + if anImage.HasMask(): + maskColor = (anImage.GetMaskRed(), anImage.GetMaskGreen(), anImage.GetMaskBlue()) + else: + maskColor = None + + data = map(ord, list(anImage.GetData())) + + for i in range(0, len(data), 3): - def __init__(self, id=wx.ID_ANY, bmp=wx.NullBitmap, status=-1): - """ - Default class constructor. + pixel = (data[i], data[i+1], data[i+2]) + pixel = MakeGray(pixel, factor, maskColor) - Parameters: - - id: the button id; - - bmp: the associated bitmap; - - status: button status (pressed, hovered, None). - """ - if id == wx.ID_ANY: - id = wx.NewId() - self._id = id - self._bmp = bmp - self._status = status - self._rect = wx.Rect() + for x in range(3): + data[i+x] = pixel[x] + + anImage.SetData(''.join(map(chr, data))) + + anImage = anImage.ConvertToBitmap() + + return anImage + + +def MakeGray((r,g,b), factor, maskColor): + """ + Make a pixel grayed-out. If the pixel matches the maskColor, it won't be + changed. + """ + + if (r,g,b) != maskColor: + return map(lambda x: int((230 - x) * factor) + x, (r,g,b)) + else: + return (r,g,b) + + +# ---------------------------------------------------------------------------- # +# Class BPArt +# Handles all the drawings for buttons, separators and text and allows the +# programmer to set colours, sizes and gradient shadings for ButtonPanel +# ---------------------------------------------------------------------------- # + +class BPArt: + """ + BPArt is an art provider class which does all of the drawing for ButtonPanel. + This allows the library caller to customize the BPArt or to completely replace + all drawing with custom BPArts. + """ + + def __init__(self, parentStyle): + """ Default class constructor. """ + + base_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE) + + self._background_brush = wx.Brush(base_color, wx.SOLID) + self._gradient_color_to = wx.WHITE + self._gradient_color_from = wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION) + + if parentStyle & BP_USE_GRADIENT: + self._border_pen = wx.Pen(wx.WHITE, 3) + self._caption_text_color = wx.WHITE + self._buttontext_color = wx.Colour(70, 143, 255) + self._separator_pen = wx.Pen(BrightenColour(self._gradient_color_from, 1.4)) + self._gradient_type = BP_GRADIENT_VERTICAL + else: + self._border_pen = wx.Pen(BrightenColour(base_color, 0.9), 3) + self._caption_text_color = wx.BLACK + self._buttontext_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNTEXT) + self._separator_pen = wx.Pen(BrightenColour(base_color, 0.9)) + self._gradient_type = BP_GRADIENT_NONE + + self._buttontext_inactive_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT) + self._selection_brush = wx.Brush(wx.Color(225, 225, 255)) + self._selection_pen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION)) + + sysfont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + self._caption_font = wx.Font(sysfont.GetPointSize(), wx.DEFAULT, wx.NORMAL, wx.BOLD, + False, sysfont.GetFaceName()) + self._buttontext_font = wx.Font(sysfont.GetPointSize(), wx.DEFAULT, wx.NORMAL, wx.NORMAL, + False, sysfont.GetFaceName()) + + self._separator_size = 7 + self._margins_size = wx.Size(6, 6) + self._caption_border_size = 3 + self._padding_size = wx.Size(6, 6) + + + def GetMetric(self, id): + """ Returns sizes of customizable options. """ + + if id == BP_SEPARATOR_SIZE: + return self._separator_size + elif id == BP_MARGINS_SIZE: + return self._margins_size + elif id == BP_BORDER_SIZE: + return self._caption_border_size + elif id == BP_PADDING_SIZE: + return self._padding_size + else: + raise "\nERROR: Invalid Metric Ordinal. " + + + def SetMetric(self, id, new_val): + """ Sets sizes for customizable options. """ + + if id == BP_SEPARATOR_SIZE: + self._separator_size = new_val + elif id == BP_MARGINS_SIZE: + self._margins_size = new_val + elif id == BP_BORDER_SIZE: + self._caption_border_size = new_val + self._border_pen.SetWidth(new_val) + elif id == BP_PADDING_SIZE: + self._padding_size = new_val + else: + raise "\nERROR: Invalid Metric Ordinal. " + + + def GetColor(self, id): + """ Returns colours of customizable options. """ + + if id == BP_BACKGROUND_COLOR: + return self._background_brush.GetColour() + elif id == BP_GRADIENT_COLOR_FROM: + return self._gradient_color_from + elif id == BP_GRADIENT_COLOR_TO: + return self._gradient_color_to + elif id == BP_BORDER_COLOR: + return self._border_pen.GetColour() + elif id == BP_TEXT_COLOR: + return self._caption_text_color + elif id == BP_BUTTONTEXT_COLOR: + return self._buttontext_color + elif id == BP_BUTTONTEXT_INACTIVE_COLOR: + return self._buttontext_inactive_color + elif id == BP_SELECTION_BRUSH_COLOR: + return self._selection_brush.GetColour() + elif id == BP_SELECTION_PEN_COLOR: + return self._selection_pen.GetColour() + elif id == BP_SEPARATOR_COLOR: + return self._separator_pen.GetColour() + else: + raise "\nERROR: Invalid Colour Ordinal. " + + + def SetColor(self, id, colour): + """ Sets colours for customizable options. """ + + if id == BP_BACKGROUND_COLOR: + self._background_brush.SetColour(colour) + elif id == BP_GRADIENT_COLOR_FROM: + self._gradient_color_from = colour + elif id == BP_GRADIENT_COLOR_TO: + self._gradient_color_to = colour + elif id == BP_BORDER_COLOR: + self._border_pen.SetColour(colour) + elif id == BP_TEXT_COLOR: + self._caption_text_color = colour + elif id == BP_BUTTONTEXT_COLOR: + self._buttontext_color = colour + elif id == BP_BUTTONTEXT_INACTIVE_COLOR: + self._buttontext_inactive_color = colour + elif id == BP_SELECTION_BRUSH_COLOR: + self._selection_brush.SetColour(colour) + elif id == BP_SELECTION_PEN_COLOR: + self._selection_pen.SetColour(colour) + elif id == BP_SEPARATOR_COLOR: + self._separator_pen.SetColour(colour) + else: + raise "\nERROR: Invalid Colour Ordinal. " - def GetBitmap(self): - """ Returns the associated bitmap. """ + GetColour = GetColor + SetColour = SetColor - return self._bmp + def SetFont(self, id, font): + """ Sets font for customizable options. """ - def GetRect(self): - """ Returns the button rect. """ + if id == BP_TEXT_FONT: + self._caption_font = font + elif id == BP_BUTTONTEXT_FONT: + self._buttontext_font = font - return self._rect + def GetFont(self, id): + """ Returns font of customizable options. """ - def GetStatus(self): - """ Returns the button status. """ + if id == BP_TEXT_FONT: + return self._caption_font + elif id == BP_BUTTONTEXT_FONT: + return self._buttontext_font + + return wx.NoneFont - return self._status + def SetGradientType(self, gradient): + """ Sets the gradient type for BPArt drawings. """ - def GetId(self): - """ Returns the button id. """ + self._gradient_type = gradient - return self._id + def GetGradientType(self): + """ Returns the gradient type for BPArt drawings. """ - def SetRect(self, rect): - """ Sets the button rect. """ + return self._gradient_type - self._rect = rect + + def DrawSeparator(self, dc, rect, isVertical): + """ Draws a separator in ButtonPanel. """ + + dc.SetPen(self._separator_pen) + + if isVertical: + ystart = yend = rect.y + rect.height/2 + xstart = int(rect.x + 1.5*self._caption_border_size) + xend = int(rect.x + rect.width - 1.5*self._caption_border_size) + dc.DrawLine(xstart, ystart, xend, yend) + else: + xstart = xend = rect.x + rect.width/2 + ystart = int(rect.y + 1.5*self._caption_border_size) + yend = int(rect.y + rect.height - 1.5*self._caption_border_size) + dc.DrawLine(xstart, ystart, xend, yend) + + + def DrawCaption(self, dc, rect, captionText): + """ Draws the main caption text in ButtonPanel. """ + + textColour = self._caption_text_color + textFont = self._caption_font + padding = self._padding_size + + dc.SetTextForeground(textColour) + dc.SetFont(textFont) + dc.DrawText(captionText, rect.x + padding.x, rect.y+padding.y) + - def SetBitmap(self, bmp): - """ Sets the associated bitmap. """ + def DrawButton(self, dc, rect, parentSize, buttonBitmap, isVertical, + buttonStatus, isToggled, textAlignment, text=""): + """ Draws a button in ButtonPanel, together with its text (if any). """ + + bmpxsize, bmpysize = buttonBitmap.GetWidth(), buttonBitmap.GetHeight() + dx = dy = focus = 0 + + borderw = self._caption_border_size + padding = self._padding_size - self._bmp = bmp + buttonFont = self._buttontext_font + dc.SetFont(buttonFont) + if isVertical: + + rect = wx.Rect(borderw, rect.y, rect.width-2*borderw, rect.height) + + if text != "": - def SetStatus(self, status): - """ Sets the button status. """ + textW, textH = dc.GetTextExtent(text) + + if textAlignment == BP_BUTTONTEXT_ALIGN_RIGHT: + fullExtent = bmpxsize + padding.x/2 + textW + bmpypos = rect.y + (rect.height - bmpysize)/2 + bmpxpos = rect.x + (rect.width - fullExtent)/2 + textxpos = bmpxpos + padding.x/2 + bmpxsize + textypos = bmpypos + (bmpysize - textH)/2 + else: + bmpxpos = rect.x + (rect.width - bmpxsize)/2 + bmpypos = rect.y + padding.y + textxpos = rect.x + (rect.width - textW)/2 + textypos = bmpypos + bmpysize + padding.y/2 + else: + bmpxpos = rect.x + (rect.width - bmpxsize)/2 + bmpypos = rect.y + (rect.height - bmpysize)/2 + + + else: - self._status = status + rect = wx.Rect(rect.x, borderw, rect.width, rect.height-2*borderw) + if text != "": - def SetId(self, id): - """ Sets the button id. """ + textW, textH = dc.GetTextExtent(text) + + if textAlignment == BP_BUTTONTEXT_ALIGN_RIGHT: + fullExtent = bmpxsize + padding.x/2 + textW + bmpypos = rect.y + (rect.height - bmpysize)/2 + bmpxpos = rect.x + (rect.width - fullExtent)/2 + textxpos = bmpxpos + padding.x/2 + bmpxsize + textypos = bmpypos + (bmpysize - textH)/2 + else: + fullExtent = bmpysize + padding.y/2 + textH + bmpxpos = rect.x + (rect.width - bmpxsize)/2 + bmpypos = rect.y + (rect.height - fullExtent)/2 + textxpos = rect.x + (rect.width - textW)/2 + textypos = bmpypos + bmpysize + padding.y/2 + else: + bmpxpos = rect.x + (rect.width - bmpxsize)/2 + bmpypos = rect.y + (rect.height - bmpysize)/2 + + # Draw a button + # [ Padding | Text | .. Buttons .. | Padding ] - self._id = id + if buttonStatus in ["Pressed", "Toggled", "Hover"]: + dc.SetBrush(self._selection_brush) + dc.SetPen(self._selection_pen) + dc.DrawRoundedRectangleRect(rect, 4) - Bitmap = property(GetBitmap, SetBitmap) - Id = property(GetId, SetId) - Rect = property(GetRect, SetRect) - Status = property(GetStatus, SetStatus) - + if buttonStatus == "Pressed" or isToggled: + dx = dy = 1 + + dc.DrawBitmap(buttonBitmap, bmpxpos+dx, bmpypos+dy, True) - + if text != "": + isEnabled = buttonStatus != "Disabled" + self.DrawLabel(dc, text, isEnabled, textxpos+dx, textypos+dy) -# -- ButtonPanel class implementation ---------------------------------- -# This is the main class. + + def DrawLabel(self, dc, text, isEnabled, xpos, ypos): + """ Draws the label for a button. """ -BASE = wx.PyPanel + if not isEnabled: + dc.SetTextForeground(self._buttontext_inactive_color) + else: + dc.SetTextForeground(self._buttontext_color) + + dc.DrawText(text, xpos, ypos) -class ButtonPanel(BASE): - def __init__(self, parent, id=wx.ID_ANY, text="", style=BP_DEFAULT_STYLE, - alignment=BP_ALIGN_RIGHT, name="buttonPanel"): - """ - Default class constructor. + def DrawButtonPanel(self, dc, rect, style): + """ Paint the ButtonPanel's background. """ - - parent: parent window - - id: window ID - - text: text to draw - - style: window style - - alignment: alignment of buttons (left or right) - - name: window class name - """ + if style & BP_USE_GRADIENT: + # Draw gradient color in the backgroud of the panel + self.FillGradientColor(dc, rect) + + # Draw a rectangle around the panel + backBrush = (style & BP_USE_GRADIENT and [wx.TRANSPARENT_BRUSH] or \ + [self._background_brush])[0] + + dc.SetBrush(backBrush) + dc.SetPen(self._border_pen) + dc.DrawRectangleRect(rect) - BASE.__init__(self, parent, id, wx.DefaultPosition, wx.DefaultSize, - wx.NO_BORDER, name=name) - self._vButtons = [] - self._text = text - self._nStyle = style - self._alignment = alignment - self._nPadding = 6 - self._nBmpSize = 16 - self._colorFrom = wx.SystemSettings.GetColour(wx.SYS_COLOUR_ACTIVECAPTION) - self._colorTo = wx.WHITE - self._colorBorder = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE) - self._colorText = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) - self._firsttime = True - self._borderPenWidth = 3 + def FillGradientColor(self, dc, rect): + """ Gradient fill from colour 1 to colour 2 with top to bottom or left to right. """ + + if rect.height < 1 or rect.width < 1: + return + + isVertical = self._gradient_type == BP_GRADIENT_VERTICAL + size = (isVertical and [rect.height] or [rect.width])[0] + start = (isVertical and [rect.y] or [rect.x])[0] + + # calculate gradient coefficients + + col2 = self._gradient_color_from + col1 = self._gradient_color_to + + rf, gf, bf = 0, 0, 0 + rstep = float((col2.Red() - col1.Red()))/float(size) + gstep = float((col2.Green() - col1.Green()))/float(size) + bstep = float((col2.Blue() - col1.Blue()))/float(size) + + for coord in xrange(start, start + size): + + currCol = wx.Colour(col1.Red() + rf, col1.Green() + gf, col1.Blue() + bf) + dc.SetBrush(wx.Brush(currCol, wx.SOLID)) + dc.SetPen(wx.Pen(currCol)) + if isVertical: + dc.DrawLine(rect.x, coord, rect.x + rect.width, coord) + else: + dc.DrawLine(coord, rect.y, coord, rect.y + rect.height) + + rf += rstep + gf += gstep + bf += bstep + + +class StatusBarTimer(wx.Timer): + """Timer used for deleting StatusBar long help after _DELAY seconds.""" + + def __init__(self, owner): + """ + Default class constructor. + For internal use: do not call it in your code! + """ - self.Bind(wx.EVT_PAINT, self.OnPaint) - self.Bind(wx.EVT_SIZE, self.OnSize) - self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) - self.Bind(wx.EVT_MOTION, self.OnMouseMove) - self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) - self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) - self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) - self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnterWindow) + wx.Timer.__init__(self) + self._owner = owner + + + def Notify(self): + """The timer has expired.""" + + self._owner.OnStatusBarTimer() + +class Control(wx.EvtHandler): - def AddButton(self, btnInfo): + def __init__(self, parent, size=wx.Size(-1, -1)): """ - Adds a button to ButtonPanel. Remember to pass a ButtonInfo instance to - this method. See the demo for details. + Default class constructor. + + Base class for all pseudo controls + parent = parent object + size = (width, height) """ - self._vButtons.append(btnInfo) - self.Refresh() - + wx.EvtHandler.__init__(self) - def RemoveAllButtons(self): - """ Remove all the buttons from ButtonPanel. """ + self._parent = parent + self._id = wx.NewId() + self._size = size + self._isshown = True + self._focus = False - self._vButtons = [] - self.Refresh() + + def Show(self, show=True): + """ Shows or hide the control. """ + + self._isshown = show + + + def Hide(self): + """ Hides the control. """ + + self.Show(False) + + + def IsShown(self): + """ Returns whether the control is shown or not. """ + + return self._isshown - def GetAlignment(self): - """ Returns the button alignment (left, right). """ + def GetId(self): + """ Returns the control id. """ - return self._alignment + return self._id - def SetAlignment(self, alignment): - """ Sets the button alignment (left, right). """ + def GetBestSize(self): + """ Returns the control best size. """ - self._alignment = alignment + return self._size - def DoGetBestSize(self): - w = h = 0 - if self._text: - dc = wx.ClientDC(self) - boldFont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - boldFont.SetWeight(wx.FONTWEIGHT_BOLD) - dc.SetFont(boldFont) - tw, th = dc.GetTextExtent(self._text) - h = max(h, th) - w += tw + self._nPadding - - if self._vButtons: - bh = self._vButtons[0].GetBitmap().GetHeight() - bw = self._vButtons[0].GetBitmap().GetWidth() - - bh += 2*self._nPadding + 2*self._borderPenWidth - h = max(h, bh) + def Disable(self): + """ Disables the control. """ - bw = (len(self._vButtons)+1) * (bw + 2*self._nPadding) - w += bw - - return (w, h) + self.Enable(False) + def Enable(self, value=True): + """ Enables or disables the control. """ - def OnPaint(self, event): - """ Handles the wx.EVT_PAINT event for ButtonPanel. """ - - dc = wx.BufferedPaintDC(self) - rect = self.GetClientRect() + self.disabled = not value - ##print rect, self.GetRect(), self.GetBestSize(), self.GetMinSize() - - # Draw gradient color in the background of the panel - self.FillGradientColor(dc, rect) - backBrush = wx.TRANSPARENT_BRUSH - borderPen = wx.Pen(self._colorBorder) - size = self.GetSize() - borderPen.SetWidth(self._borderPenWidth) + def SetFocus(self, focus=True): + """ Sets or kills the focus on the control. """ - # Draw a rectangle around the panel - dc.SetBrush(backBrush) - dc.SetPen(borderPen) - dc.DrawRectangleRect(rect) + self._focus = focus - # Draw the text - textWidth, textHeight = 0, 0 - if self._text != "": - - dc.SetTextForeground(self._colorText) - borderPen.SetWidth(2) - boldFont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - boldFont.SetWeight(wx.FONTWEIGHT_BOLD) - dc.SetFont(boldFont) - - textWidth, textHeight = dc.GetTextExtent(self._text) + def HasFocus(self): + """ Returns whether the control has the focus or not. """ - if self._alignment == BP_ALIGN_RIGHT: - textX = self._nPadding - else: - textX = rect.width - textWidth - self._nPadding + return self._focus + - textY = (rect.GetHeight() - textHeight)/2 - dc.DrawText(self._text, textX, textY) - - if self._vButtons: - - height = self._vButtons[0].GetBitmap().GetHeight() - self._nBmpSize = self._vButtons[0].GetBitmap().GetWidth() - height += 2*self._nPadding + 2*self._borderPenWidth + def OnMouseEvent(self, x, y, event): + pass - if self._firsttime: # this probably isn't needed anymore now that DoGetBestSize is implemented... - self.GetContainingSizer().Layout() - self._firsttime = False - - # Draw all buttons - # [ Padding | Text | .. Buttons .. | Padding ] - - totalWidth = rect.width - self._nPadding*2 - textWidth + def Draw(self, rect): + pass - # The button is drawn inside a circle with padding of self._nPadding around it - # so the width of each image = imageWidth + 2 * self._nPadding - nImageWidth = self._nBmpSize + 2*self._nPadding - if self._alignment == BP_ALIGN_RIGHT: - leftEndX = self._nPadding + textWidth - posx = rect.width - nImageWidth - self._nPadding - - for ii in xrange(len(self._vButtons)): - - # Make sure we can keep drawing - if posx < leftEndX: - break - - # Draw a rectangle around the buttons - if self._vButtons[ii].GetStatus() == BP_BTN_HOVER: - - dc.SetBrush(wx.Brush(wx.Color(225, 225, 255))) - dc.SetPen(wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_ACTIVECAPTION))) - dc.DrawRectangle(posx, self._borderPenWidth, nImageWidth, nImageWidth) - dc.DrawBitmap(self._vButtons[ii].GetBitmap(), posx + self._nPadding, self._nPadding + self._borderPenWidth, True) - - elif self._vButtons[ii].GetStatus() == BP_BTN_PRESSED: - - dc.SetBrush(wx.Brush(wx.Color(225, 225, 255))) - dc.SetPen(wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_ACTIVECAPTION))) - dc.DrawRectangle(posx, self._borderPenWidth, nImageWidth, nImageWidth) - dc.DrawBitmap(self._vButtons[ii].GetBitmap(), posx + self._nPadding, self._nPadding + self._borderPenWidth + 1, True) - - else: - - dc.DrawBitmap(self._vButtons[ii].GetBitmap(), posx + self._nPadding, self._nPadding + self._borderPenWidth, True) - - self._vButtons[ii].SetRect(wx.Rect(posx, self._borderPenWidth, nImageWidth, nImageWidth)) - posx -= nImageWidth +class Sizer(object): + """ + Sizer + This is a mix-in class to add pseudo support to a wx sizer. Just create + a new class that derives from this class and the wx sizer and intercepts + any methods that add to the wx sizer. + """ + def __init__(self): + self.children = [] # list of child Pseudo Controls + + # Sizer doesn't use the x1,y1,x2,y2 so allow it to + # be called with or without the coordinates + def Draw(self, dc, x1=0, y1=0, x2=0, y2=0): + for item in self.children: + # use sizer coordinates rather than + # what is passed in + c = item.GetUserData() + c.Draw(dc, item.GetRect()) + + def GetBestSize(self): + # this should be handled by the wx.Sizer based class + return self.GetMinSize() + + +# Pseudo BoxSizer +class BoxSizer(Sizer, wx.BoxSizer): + def __init__(self, orient=wx.HORIZONTAL): + wx.BoxSizer.__init__(self, orient) + Sizer.__init__(self) + + #------------------------------------------- + # sizer overrides (only called from Python) + #------------------------------------------- + # no support for user data if it's a pseudocontrol + # since that is already used + def Add(self, item, proportion=0, flag=0, border=0, userData=None): + # check to see if it's a pseudo object or sizer + if isinstance(item, Sizer): + szitem = wx.BoxSizer.Add(self, item, proportion, flag, border, item) + self.children.append(szitem) + elif isinstance(item, Control): # Control should be what ever class your controls come from + sz = item.GetBestSize() + # add a spacer to track this object + szitem = wx.BoxSizer.Add(self, sz, proportion, flag, border, item) + self.children.append(szitem) + else: + wx.BoxSizer.Add(self, item, proportion, flag, border, userData) + + def Prepend(self, item, proportion=0, flag=0, border=0, userData=None): + # check to see if it's a pseudo object or sizer + if isinstance(item, Sizer): + szitem = wx.BoxSizer.Prepend(self, item, proportion, flag, border, item) + self.children.append(szitem) + elif isinstance(item, Control): # Control should be what ever class your controls come from + sz = item.GetBestSize() + # add a spacer to track this object + szitem = wx.BoxSizer.Prepend(self, sz, proportion, flag, border, item) + self.children.insert(0,szitem) + else: + wx.BoxSizer.Prepend(self, item, proportion, flag, border, userData) + + def Insert(self, before, item, proportion=0, flag=0, border=0, userData=None, realIndex=None): + # check to see if it's a pseudo object or sizer + if isinstance(item, Sizer): + szitem = wx.BoxSizer.Insert(self, before, item, proportion, flag, border, item) + self.children.append(szitem) + elif isinstance(item, Control): # Control should be what ever class your controls come from + sz = item.GetBestSize() + # add a spacer to track this object + szitem = wx.BoxSizer.Insert(self, before, sz, proportion, flag, border, item) + if realIndex is not None: + self.children.insert(realIndex,szitem) else: - - rightStartX = textX - self._nPadding - nImageWidth - posx = self._nPadding + self.children.insert(before,szitem) - for ii in xrange(len(self._vButtons)): - - # Make sure we can keep drawing - if posx > rightStartX: - break - - # Draw a rectangle around the buttons - if self._vButtons[ii].GetStatus() == BP_BTN_HOVER: - - dc.SetBrush(wx.Brush(wx.Color(225, 225, 255))) - dc.SetPen(wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_ACTIVECAPTION))) - dc.DrawRectangle(posx, self._borderPenWidth, nImageWidth, nImageWidth) - dc.DrawBitmap(self._vButtons[ii].GetBitmap(), posx + self._nPadding, self._nPadding + self._borderPenWidth, True) - - elif self._vButtons[ii].GetStatus() == BP_BTN_PRESSED: - - dc.SetBrush(wx.Brush(wx.Color(225, 225, 255))) - dc.SetPen(wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_ACTIVECAPTION))) - dc.DrawRectangle(posx, self._borderPenWidth, nImageWidth, nImageWidth) - dc.DrawBitmap(self._vButtons[ii].GetBitmap(), posx + self._nPadding, self._nPadding + self._borderPenWidth + 1, True) - - else: - - dc.DrawBitmap(self._vButtons[ii].GetBitmap(), posx + self._nPadding, self._nPadding + self._borderPenWidth, True) - - self._vButtons[ii].SetRect(wx.Rect(posx, self._borderPenWidth, nImageWidth, nImageWidth)) - posx += nImageWidth - - # Update all other buttons that they are not drawn (by setting the rect to 0) - for cur in xrange(ii+1, len(self._vButtons)): - self._vButtons[cur].SetRect(wx.Rect(0, 0, 0, 0)) + else: + wx.BoxSizer.Insert(self, before, item, proportion, flag, border, userData) - def OnEraseBackground(self, event): - """ Handles the wx.EVT_ERASE_BACKGROUND event for ButtonPanel (does nothing). """ + def Remove(self, indx, pop=-1): + + if pop >= 0: + self.children.pop(pop) + + wx.BoxSizer.Remove(self, indx) - pass - - - def OnSize(self, event): - """ Handles the wx.EVT_SIZE event for ButtonPanel. """ - - self.Refresh() - event.Skip() - - def SetColor(self, switch, color): - """ - Sets the color depending on switch: - - BP_CAPTION_COLOR: caption color; - - BP_CAPTION_GRADIENT_COLOR: gradient color; - - BP_CAPTION_BORDER_COLOR; border color; - - BP_TEXT_COLOR: text color. - """ - - if switch == BP_CAPTION_COLOR: - self._colorFrom = color - elif switch == BP_CAPTION_GRADIENT_COLOR: - self._colorTo = color - elif switch == BP_CAPTION_BORDER_COLOR: - self._colorBorder = color - elif switch == BP_TEXT_COLOR: - self._colorText = color - - - def FillGradientColor(self, dc, rect): - """ Gradient fill from colour 1 to colour 2 with top to bottom. """ + def Layout(self): - if rect.height < 1 or rect.width < 1: - return + for ii, child in enumerate(self.GetChildren()): + item = child.GetUserData() + if item and child.IsShown(): + self.SetItemMinSize(ii, *item.GetBestSize()) - size = rect.height + wx.BoxSizer.Layout(self) - # calculate gradient coefficients - style = self.GetParent().GetWindowStyleFlag() - col2 = self._colorFrom - col1 = self._colorTo + + def Show(self, item, show=True): - rf, gf, bf = 0, 0, 0 - rstep = float((col2.Red() - col1.Red()))/float(size) - gstep = float((col2.Green() - col1.Green()))/float(size) - bstep = float((col2.Blue() - col1.Blue()))/float(size) + child = self.GetChildren()[item] + if child and child.GetUserData(): + child.GetUserData().Show(show) - for y in xrange(rect.y, rect.y + size): - - currCol = wx.Colour(col1.Red() + rf, col1.Green() + gf, col1.Blue() + bf) - dc.SetBrush(wx.Brush(currCol, wx.SOLID)) - dc.SetPen(wx.Pen(currCol)) - dc.DrawLine(rect.x, y, rect.x + rect.width, y) - rf += rstep - gf += gstep - bf += bstep + wx.BoxSizer.Show(self, item, show) + - - def OnMouseMove(self, event): - """ Handles the wx.EVT_MOTION event for ButtonPanel. """ - - # Check to see if we are hovering a button - for ii in xrange(len(self._vButtons)): - if self._vButtons[ii].GetRect().Inside(event.GetPosition()): - self._vButtons[ii].SetStatus(BP_BTN_HOVER) - else: - self._vButtons[ii].SetStatus(BP_BTN_NONE) - - self.Refresh() - event.Skip() - +# ---------------------------------------------------------------------------- # +# Class Separator +# This class holds all the information to size and draw a separator inside +# ButtonPanel +# ---------------------------------------------------------------------------- # - def OnLeftDown(self, event): - """ Handles the wx.EVT_LEFT_DOWN event for ButtonPanel. """ - - tabId, hit = self.HitTest(event.GetPosition()) +class Separator(Control): - if hit == BP_HT_BUTTON: - - self._vButtons[tabId].SetStatus(BP_BTN_PRESSED) - self.Refresh() + def __init__(self, parent): + """ Default class constructor. """ + self._isshown = True + self._parent = parent + Control.__init__(self, parent) - def OnLeftUp(self, event): - """ Handles the wx.EVT_LEFT_UP event for ButtonPanel. """ + + def GetBestSize(self): + """ Returns the separator best size. """ + + # 10 is completely arbitrary, but it works anyhow + if self._parent.IsVertical(): + return wx.Size(10, self._parent._art.GetMetric(BP_SEPARATOR_SIZE)) + else: + return wx.Size(self._parent._art.GetMetric(BP_SEPARATOR_SIZE), 10) - tabId, hit = self.HitTest(event.GetPosition()) + + def Draw(self, dc, rect): + """ Draws the separator. Actually the drawing is done in BPArt. """ + + if not self.IsShown(): + return + + isVertical = self._parent.IsVertical() + self._parent._art.DrawSeparator(dc, rect, isVertical) - if hit == BP_HT_BUTTON: - if self._vButtons[tabId].GetStatus() == BP_BTN_PRESSED: - # Fire a button click event - btnEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self._vButtons[tabId].GetId()) - self.GetEventHandler().ProcessEvent(btnEvent) - - # Update the button status to be hovered - self._vButtons[tabId].SetStatus(BP_BTN_HOVER) - self.Refresh() + +# ---------------------------------------------------------------------------- # +# Class ButtonPanelText +# This class is used to hold data about the main caption in ButtonPanel +# ---------------------------------------------------------------------------- # + +class ButtonPanelText(Control): + + def __init__(self, parent, text=""): + """ Default class constructor. """ + + self._text = text + self._isshown = True + self._parent = parent + + Control.__init__(self, parent) + + + def GetText(self): + """ Returns the caption text. """ + + return self._text + + + def SetText(self, text=""): + """ Sets the caption text. """ + + self._text = text + + + def CreateDC(self): + """ Convenience function to create a DC. """ + + dc = wx.ClientDC(self._parent) + textFont = self._parent._art.GetFont(BP_TEXT_FONT) + dc.SetFont(textFont) + + return dc + + + def GetBestSize(self): + """ Returns the best size for the main caption in ButtonPanel. """ + + if self._text == "": + return wx.Size(0, 0) + + dc = self.CreateDC() + rect = self._parent.GetClientRect() + + tw, th = dc.GetTextExtent(self._text) + padding = self._parent._art.GetMetric(BP_PADDING_SIZE) + self._size = wx.Size(tw+2*padding.x, th+2*padding.y) + + return self._size + + + def Draw(self, dc, rect): + """ Draws the main caption. Actually the drawing is done in BPArt. """ + + if not self.IsShown(): + return + + captionText = self.GetText() + self._parent._art.DrawCaption(dc, rect, captionText) + + +# -- ButtonInfo class implementation ---------------------------------------- +# This class holds information about every button that is added to +# ButtonPanel. It is an auxiliary class that you should use +# every time you add a button. + +class ButtonInfo(Control): + + def __init__(self, parent, id=wx.ID_ANY, bmp=wx.NullBitmap, + status="Normal", text="", kind=wx.ITEM_NORMAL, + shortHelp="", longHelp=""): + """ + Default class constructor. + + Parameters: + - parent: the parent window (ButtonPanel); + - id: the button id; + - bmp: the associated bitmap; + - status: button status (pressed, hovered, normal). + - text: text to be displayed either below of to the right of the button + - kind: button kind, may be wx.ITEM_NORMAL for standard buttons or + wx.ITEM_CHECK for toggle buttons; + - shortHelp: a short help to be shown in the button tooltip; + - longHelp: this string is shown in the statusbar (if any) of the parent + frame when the mouse pointer is inside the button. + """ + + if id == wx.ID_ANY: + id = wx.NewId() + + self._status = status + self._rect = wx.Rect() + self._text = text + self._kind = kind + self._toggle = False + self._textAlignment = BP_BUTTONTEXT_ALIGN_BOTTOM + self._shortHelp = shortHelp + self._longHelp = longHelp + + disabledbmp = GrayOut(bmp) + + self._bitmaps = {"Normal": bmp, "Toggled": None, "Disabled": disabledbmp, + "Hover": None, "Pressed": None} + + Control.__init__(self, parent) + + + def GetBestSize(self): + """ Returns the best size for the button. """ + + xsize = self.GetBitmap().GetWidth() + ysize = self.GetBitmap().GetHeight() + + if self.HasText(): + # We have text in the button + dc = wx.ClientDC(self._parent) + normalFont = self._parent._art.GetFont(BP_BUTTONTEXT_FONT) + dc.SetFont(normalFont) + tw, th = dc.GetTextExtent(self.GetText()) + + if self.GetTextAlignment() == BP_BUTTONTEXT_ALIGN_BOTTOM: + xsize = max(xsize, tw) + ysize = ysize + th + else: + xsize = xsize + tw + ysize = max(ysize, th) + + border = self._parent._art.GetMetric(BP_BORDER_SIZE) + padding = self._parent._art.GetMetric(BP_PADDING_SIZE) + + if self._parent.IsVertical(): + xsize = xsize + 2*border + else: + ysize = ysize + 2*border + + self._size = wx.Size(xsize+2*padding.x, ysize+2*padding.y) + + return self._size + + + def Draw(self, dc, rect): + """ Draws the button on ButtonPanel. Actually the drawing is done in BPArt. """ + + if not self.IsShown(): + return + + buttonBitmap = self.GetBitmap() + isVertical = self._parent.IsVertical() + text = self.GetText() + parentSize = self._parent.GetSize()[not isVertical] + buttonStatus = self.GetStatus() + isToggled = self.GetToggled() + textAlignment = self.GetTextAlignment() + + self._parent._art.DrawButton(dc, rect, parentSize, buttonBitmap, isVertical, + buttonStatus, isToggled, textAlignment, text) + + self.SetRect(rect) + + + def CheckRefresh(self, status): + """ Checks whether a ButtonPanel repaint is needed or not. Convenience function. """ + + if status == self._status: + self._parent.RefreshRect(self.GetRect()) + + + def SetBitmap(self, bmp, status="Normal"): + """ Sets the associated bitmap. """ + + self._bitmaps[status] = bmp + self.CheckRefresh(status) + + + def GetBitmap(self, status=None): + """ Returns the associated bitmap. """ + + if status is None: + status = self._status + + if not self.IsEnabled(): + status = "Disabled" + + if self._bitmaps[status] is None: + return self._bitmaps["Normal"] + + return self._bitmaps[status] + + + def GetRect(self): + """ Returns the button rect. """ + + return self._rect + + + def GetStatus(self): + """ Returns the button status. """ + + return self._status + + + def GetId(self): + """ Returns the button id. """ + + return self._id + + + def SetRect(self, rect): + """ Sets the button rect. """ + + self._rect = rect + + + def SetStatus(self, status): + """ Sets the button status. """ + + if status == self._status: + return + + if self.GetToggled() and status == "Normal": + status = "Toggled" + + self._status = status + self._parent.RefreshRect(self.GetRect()) + + + def GetTextAlignment(self): + """ Returns the text alignment in the button (bottom or right). """ + + return self._textAlignment + + + def SetTextAlignment(self, alignment): + """ Sets the text alignment in the button (bottom or right). """ + + if alignment == self._textAlignment: + return + + self._textAlignment = alignment + + + def GetToggled(self): + """ Returns whether a wx.ITEM_CHECK button is toggled or not. """ + + if self._kind == wx.ITEM_NORMAL: + return False + + return self._toggle + + + def SetToggled(self, toggle=True): + """ Sets a wx.ITEM_CHECK button toggled/not toggled. """ + + if self._kind == wx.ITEM_NORMAL: + return + + self._toggle = toggle + + + def SetId(self, id): + """ Sets the button id. """ + + self._id = id + + + def AddStatus(self, name="Custom", bmp=wx.NullBitmap): + """ + Add a programmer-defined status in addition to the 5 default status: + - Normal; + - Disabled; + - Hover; + - Pressed; + - Toggled. + """ + + self._bitmaps.update({name: bmp}) + + + def Enable(self, enable=True): + + if enable: + self._status = "Normal" + else: + self._status = "Disabled" + + + def IsEnabled(self): + + return self._status != "Disabled" + + + def SetText(self, text=""): + """ Sets the text of the button. """ + + self._text = text + + + def GetText(self): + """ Returns the text associated to the button. """ + + return self._text + + + def HasText(self): + """ Returns whether the button has text or not. """ + + return self._text != "" + + + def SetKind(self, kind=wx.ITEM_NORMAL): + """ Sets the button type (standard or toggle). """ + + self._kind = kind + + + def GetKind(self): + """ Returns the button type (standard or toggle). """ + + return self._kind + + + def SetShortHelp(self, help=""): + """ Sets the help string to be shown in a tootip. """ + + self._shortHelp = help + + + def GetShortHelp(self): + """ Returns the help string shown in a tootip. """ + + return self._shortHelp + + + def SetLongHelp(self, help=""): + """ Sets the help string to be shown in the statusbar. """ + + self._longHelp = help + + + def GetLongHelp(self): + """ Returns the help string shown in the statusbar. """ + + return self._longHelp + + + Bitmap = property(GetBitmap, SetBitmap) + Id = property(GetId, SetId) + Rect = property(GetRect, SetRect) + Status = property(GetStatus, SetStatus) + + +# -- ButtonPanel class implementation ---------------------------------- +# This is the main class. + +class ButtonPanel(wx.PyPanel): + + def __init__(self, parent, id=wx.ID_ANY, text="", style=BP_DEFAULT_STYLE, + alignment=BP_ALIGN_LEFT, name="buttonPanel"): + """ + Default class constructor. + + - parent: parent window + - id: window ID + - text: text to draw + - style: window style + - alignment: alignment of buttons (left or right) + - name: window class name + """ + + wx.PyPanel.__init__(self, parent, id, wx.DefaultPosition, wx.DefaultSize, + wx.NO_BORDER, name=name) + + self._vButtons = [] + self._vSeparators = [] + + self._nStyle = style + self._alignment = alignment + self._statusTimer = None + self._useHelp = True + self._freezeCount = 0 + self._currentButton = -1 + self._haveTip = False + + self._art = BPArt(style) + + self._controlCreated = False + + direction = (self.IsVertical() and [wx.VERTICAL] or [wx.HORIZONTAL])[0] + self._mainsizer = BoxSizer(direction) + self.SetSizer(self._mainsizer) + + margins = self._art.GetMetric(BP_MARGINS_SIZE) + + # First spacer to create some room before the first text/button/control + self._mainsizer.Add((margins.x, margins.y), 0) + + # Last spacer to create some room before the last text/button/control + self._mainsizer.Add((margins.x, margins.y), 0) + + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_MOTION, self.OnMouseMove) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) + self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnterWindow) + + self.SetBarText(text) + self.LayoutItems() + + + def SetBarText(self, text): + """ Sets the main caption text (leave text="" for no text). """ + + self.Freeze() + + text = text.strip() + + if self._controlCreated: + self.RemoveText() + + self._text = ButtonPanelText(self, text) + lenChildren = len(self._mainsizer.GetChildren()) + + if text == "": + # Even if we have no text, we insert it an empty spacer anyway + # it is easier to handle if you have to recreate the sizer after. + if self.IsStandard(): + self._mainsizer.Insert(1, self._text, 0, wx.ALIGN_CENTER, + userData=self._text, realIndex=0) + else: + self._mainsizer.Insert(lenChildren-1, self._text, 0, wx.ALIGN_CENTER, + userData=self._text, realIndex=lenChildren) + + return + + # We have text, so insert the text and an expandable spacer + # alongside it. "Standard" ButtonPanel are left or top aligned. + if self.IsStandard(): + self._mainsizer.Insert(1, self._text, 0, wx.ALIGN_CENTER, + userData=self._text, realIndex=0) + self._mainsizer.Insert(2, (0, 0), 1, wx.EXPAND) + + else: + self._mainsizer.Insert(lenChildren-1, self._text, 0, wx.ALIGN_CENTER, + userData=self._text, realIndex=lenChildren) + self._mainsizer.Insert(lenChildren-1, (0, 0), 1, wx.EXPAND) + + + def RemoveText(self): + """ Removes the main caption text. """ + + lenChildren = len(self._mainsizer.GetChildren()) + lenCustom = len(self._vButtons) + len(self._vSeparators) + 1 + + if self.IsStandard(): + # Detach the text + self._mainsizer.Remove(1, 0) + if self.HasBarText(): + # Detach the expandable spacer + self._mainsizer.Remove(1, -1) + else: + # Detach the text + self._mainsizer.Remove(lenChildren-2, lenCustom-1) + if self.HasBarText(): + # Detach the expandable spacer + self._mainsizer.Remove(lenChildren-3, -1) + + + def GetBarText(self): + """ Returns the main caption text. """ + + return self._text.GetText() + + + def HasBarText(self): + """ Returns whether ButtonPanel has a main caption text or not. """ + + return hasattr(self, "_text") and self._text.GetText() != "" + + + def AddButton(self, btnInfo): + """ + Adds a button to ButtonPanel. Remember to pass a ButtonInfo instance to + this method. See the demo for details. + """ + + lenChildren = len(self._mainsizer.GetChildren()) + self._mainsizer.Insert(lenChildren-1, btnInfo, 0, wx.ALIGN_CENTER|wx.EXPAND, userData=btnInfo) + + self._vButtons.append(btnInfo) + + + def AddSpacer(self, size=(0, 0), proportion=1, flag=wx.EXPAND): + """ Adds a spacer (stretchable or fixed-size) to ButtonPanel. """ + + lenChildren = len(self._mainsizer.GetChildren()) + self._mainsizer.Insert(lenChildren-1, size, proportion, flag) + + + def AddControl(self, control, proportion=0, flag=wx.ALIGN_CENTER|wx.ALL, border=None): + """ Adds a wxPython control to ButtonPanel. """ + + lenChildren = len(self._mainsizer.GetChildren()) + + if border is None: + border = self._art.GetMetric(BP_PADDING_SIZE) + border = max(border.x, border.y) + + self._mainsizer.Insert(lenChildren-1, control, proportion, flag, border) + + + def AddSeparator(self): + """ Adds a separator line to ButtonPanel. """ + + lenChildren = len(self._mainsizer.GetChildren()) + separator = Separator(self) + + self._mainsizer.Insert(lenChildren-1, separator, 0, wx.EXPAND) + self._vSeparators.append(separator) + + + def RemoveAllButtons(self): + """ Remove all the buttons from ButtonPanel. """ + + self._vButtons = [] + + + def RemoveAllSeparators(self): + """ Remove all the separators from ButtonPanel. """ + + self._vSeparators = [] + + + def GetAlignment(self): + """ Returns the button alignment (left, right, top, bottom). """ + + return self._alignment + + + def SetAlignment(self, alignment): + """ Sets the button alignment (left, right, top, bottom). """ + + if alignment == self._alignment: + return + + self.Freeze() + + text = self.GetBarText() + + # Remove the text in any case + self.RemoveText() + + # Remove the first and last spacers + self._mainsizer.Remove(0, -1) + self._mainsizer.Remove(len(self._mainsizer.GetChildren())-1, -1) + + self._alignment = alignment + + # Recreate the sizer accordingly to the new alignment + self.ReCreateSizer(text) + + + def IsVertical(self): + """ Returns whether ButtonPanel is vertically aligned or not. """ + + return self._alignment not in [BP_ALIGN_RIGHT, BP_ALIGN_LEFT] + + + def IsStandard(self): + """ Returns whether ButtonPanel is aligned "Standard" (left/top) or not. """ + + return self._alignment in [BP_ALIGN_LEFT, BP_ALIGN_TOP] + + + def DoLayout(self): + """ + Do the Layout for ButtonPanel. + NB: Call this method every time you make a modification to the layout + or to the customizable sizes of the pseudo controls. + """ + + margins = self._art.GetMetric(BP_MARGINS_SIZE) + lenChildren = len(self._mainsizer.GetChildren()) + + self._mainsizer.SetItemMinSize(0, (margins.x, margins.y)) + self._mainsizer.SetItemMinSize(lenChildren-1, (margins.x, margins.y)) + + self._controlCreated = True + self.LayoutItems() + + # *VERY* WEIRD: the sizer seems not to respond to any layout until I + # change the ButtonPanel size and restore it back + size = self.GetSize() + self.SetSize((size.x+1, size.y+1)) + self.SetSize((size.x, size.y)) + + if self.IsFrozen(): + self.Thaw() + + + def ReCreateSizer(self, text): + """ Recreates the ButtonPanel sizer accordingly to the alignment specified. """ + + children = self._mainsizer.GetChildren() + self.RemoveAllButtons() + self.RemoveAllSeparators() + + # Create a new sizer depending on the alignment chosen + direction = (self.IsVertical() and [wx.VERTICAL] or [wx.HORIZONTAL])[0] + self._mainsizer = BoxSizer(direction) + + margins = self._art.GetMetric(BP_MARGINS_SIZE) + # First spacer to create some room before the first text/button/control + self._mainsizer.Add((margins.x, margins.y), 0) + + # Last spacer to create some room before the last text/button/control + self._mainsizer.Add((margins.x, margins.y), 0) + + # This is needed otherwise SetBarText goes mad + self._controlCreated = False + + for child in children: + userData = child.GetUserData() + if userData: + if isinstance(userData, ButtonInfo): + # It is a ButtonInfo, can't be anything else + self.AddButton(child.GetUserData()) + elif isinstance(userData, Separator): + self.AddSeparator() + + else: + if child.IsSpacer(): + # This is a spacer, expandable or not + self.AddSpacer(child.GetSize(), child.GetProportion(), + child.GetFlag()) + else: + # This is a wxPython control + self.AddControl(child.GetWindow(), child.GetProportion(), + child.GetFlag(), child.GetBorder()) + + self.SetSizer(self._mainsizer) + + # Now add the text. It doesn't matter if there is no text + self.SetBarText(text) + + self.DoLayout() + + self.Thaw() + + + def DoGetBestSize(self): + """ Returns the best size of ButtonPanel. """ + + w = h = btnWidth = btnHeight = 0 + isVertical = self.IsVertical() + + padding = self._art.GetMetric(BP_PADDING_SIZE) + border = self._art.GetMetric(BP_BORDER_SIZE) + margins = self._art.GetMetric(BP_MARGINS_SIZE) + separator_size = self._art.GetMetric(BP_SEPARATOR_SIZE) + + # Add the space required for the main caption + if self.HasBarText(): + w, h = self._text.GetBestSize() + if isVertical: + h += padding.y + else: + w += padding.x + else: + w = h = border + + # Add the button's sizes + for btn in self._vButtons: + + bw, bh = btn.GetBestSize() + btnWidth = max(btnWidth, bw) + btnHeight = max(btnHeight, bh) + + if isVertical: + w = max(w, btnWidth) + h += bh + else: + h = max(h, btnHeight) + w += bw + + # Add the control's sizes + for control in self.GetControls(): + cw, ch = control.GetSize() + if isVertical: + h += ch + w = max(w, cw) + else: + w += cw + h = max(h, ch) + + # Add the separator's sizes and the 2 SizerItems at the beginning + # and at the end + if self.IsVertical(): + h += 2*margins.y + len(self._vSeparators)*separator_size + else: + w += 2*margins.x + len(self._vSeparators)*separator_size + + return wx.Size(w, h) + + + def OnPaint(self, event): + """ Handles the wx.EVT_PAINT event for ButtonPanel. """ + + dc = wx.BufferedPaintDC(self) + rect = self.GetClientRect() + + self._art.DrawButtonPanel(dc, rect, self._nStyle) + self._mainsizer.Draw(dc) + + + def OnEraseBackground(self, event): + """ Handles the wx.EVT_ERASE_BACKGROUND event for ButtonPanel (does nothing). """ + + pass + + + def OnSize(self, event): + """ Handles the wx.EVT_SIZE event for ButtonPanel. """ + + # NOTE: It seems like LayoutItems number of calls can be optimized in some way. + # Currently every DoLayout (or every parent Layout()) calls about 3 times + # the LayoutItems method. Any idea on how to improve it? + self.LayoutItems() + self.Refresh() + + event.Skip() + + + def LayoutItems(self): + """ + Layout the items using a different algorithm depending on the existance + of the main caption. + """ + + nonspacers, allchildren = self.GetNonFlexibleChildren() + + if self.HasBarText(): + self.FlexibleLayout(nonspacers, allchildren) + else: + self.SizeLayout(nonspacers, allchildren) + + self._mainsizer.Layout() + + + def SizeLayout(self, nonspacers, children): + """ Layout the items when no main caption exists. """ + + size = self.GetSize() + isVertical = self.IsVertical() + + corner = 0 + indx1 = len(nonspacers) + + for item in nonspacers: + corner += self.GetItemSize(item, isVertical) + if corner > size[isVertical]: + indx1 = nonspacers.index(item) + break + + # Leave out the last spacer, it has to be there always + for ii in xrange(len(nonspacers)-1): + indx = children.index(nonspacers[ii]) + self._mainsizer.Show(indx, ii < indx1) + + + def GetItemSize(self, item, isVertical): + """ Returns the size of an item in the main ButtonPanel sizer. """ + + if item.GetUserData(): + return item.GetUserData().GetBestSize()[isVertical] + else: + return item.GetSize()[isVertical] + + + def FlexibleLayout(self, nonspacers, allchildren): + """ Layout the items when the main caption exists. """ + + if len(nonspacers) < 2: + return + + isVertical = self.IsVertical() + isStandard = self.IsStandard() + + size = self.GetSize()[isVertical] + padding = self._art.GetMetric(BP_PADDING_SIZE) + + fixed = (isStandard and [nonspacers[1]] or [nonspacers[-2]])[0] + + if isStandard: + nonspacers.reverse() + leftendx = fixed.GetSize()[isVertical] + padding.x + else: + rightstartx = size - fixed.GetSize()[isVertical] + size = 0 + + count = lennonspacers = len(nonspacers) + + for item in nonspacers: + if isStandard: + size -= self.GetItemSize(item, isVertical) + if size < leftendx: + break + else: + size += self.GetItemSize(item, isVertical) + if size > rightstartx: + break + + count = count - 1 + + nonspacers.reverse() + + for jj in xrange(2, lennonspacers): + indx = allchildren.index(nonspacers[jj]) + self._mainsizer.Show(indx, jj >= count) + + + def GetNonFlexibleChildren(self): + """ + Returns all the ButtonPanel main sizer's children that are not + flexible spacers. + """ + + children1 = [] + children2 = self._mainsizer.GetChildren() + + for child in children2: + if child.IsSpacer(): + if child.GetUserData() or child.GetProportion() == 0: + children1.append(child) + else: + children1.append(child) + + return children1, children2 + + + def GetControls(self): + """ Returns the wxPython controls that belongs to ButtonPanel. """ + + children2 = self._mainsizer.GetChildren() + children1 = [child for child in children2 if not child.IsSpacer()] + + return children1 + + + def SetStyle(self, style): + """ Sets ButtonPanel style. """ + + if style == self._nStyle: + return + + self._nStyle = style + self.Refresh() + + + def GetStyle(self): + """ Returns the ButtonPanel style. """ + + return self._nStyle + + + def OnMouseMove(self, event): + """ Handles the wx.EVT_MOTION event for ButtonPanel. """ + + # Check to see if we are hovering a button + tabId, flags = self.HitTest(event.GetPosition()) + + if flags != BP_HT_BUTTON: + self.RemoveHelp() + self.RepaintOldSelection() + self._currentButton = -1 + return + + btn = self._vButtons[tabId] + + if not btn.IsEnabled(): + self.RemoveHelp() + self.RepaintOldSelection() + return + + if tabId != self._currentButton: + self.RepaintOldSelection() + + if btn.GetRect().Contains(event.GetPosition()): + btn.SetStatus("Hover") + else: + btn.SetStatus("Normal") + + if tabId != self._currentButton: + self.RemoveHelp() + self.DoGiveHelp(btn) + + self._currentButton = tabId + event.Skip() + + + def OnLeftDown(self, event): + """ Handles the wx.EVT_LEFT_DOWN event for ButtonPanel. """ + + tabId, hit = self.HitTest(event.GetPosition()) + + if hit == BP_HT_BUTTON: + btn = self._vButtons[tabId] + if btn.IsEnabled(): + btn.SetStatus("Pressed") + self._currentButton = tabId + + + def OnLeftUp(self, event): + """ Handles the wx.EVT_LEFT_UP event for ButtonPanel. """ + + tabId, flags = self.HitTest(event.GetPosition()) + + if flags != BP_HT_BUTTON: + return + + hit = self._vButtons[tabId] + + if hit.GetStatus() == "Disabled": + return + + for btn in self._vButtons: + if btn != hit: + btn.SetFocus(False) + + if hit.GetStatus() == "Pressed": + # Fire a button click event + btnEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, hit.GetId()) + self.GetEventHandler().ProcessEvent(btnEvent) + + hit.SetToggled(not hit.GetToggled()) + + # Update the button status to be hovered + hit.SetStatus("Hover") + hit.SetFocus() + self._currentButton = tabId + def OnMouseLeave(self, event): """ Handles the wx.EVT_LEAVE_WINDOW event for ButtonPanel. """ - # Reset all buttons statuses - for ii in xrange(len(self._vButtons)): - self._vButtons[ii].SetStatus(BP_BTN_NONE) - - self.Refresh() + # Reset all buttons statuses + for btn in self._vButtons: + if not btn.IsEnabled(): + continue + btn.SetStatus("Normal") + + self.RemoveHelp() + event.Skip() def OnMouseEnterWindow(self, event): """ Handles the wx.EVT_ENTER_WINDOW event for ButtonPanel. """ - + + tabId, flags = self.HitTest(event.GetPosition()) + + if flags == BP_HT_BUTTON: + + hit = self._vButtons[tabId] + + if hit.GetStatus() == "Disabled": + event.Skip() + return + + self.DoGiveHelp(hit) + self._currentButton = tabId + event.Skip() + def DoGiveHelp(self, hit): + """ Gives tooltips and help in StatusBar. """ + + if not self.GetUseHelp(): + return + + shortHelp = hit.GetShortHelp() + if shortHelp: + self.SetToolTipString(shortHelp) + self._haveTip = True + + longHelp = hit.GetLongHelp() + if not longHelp: + return + + topLevel = wx.GetTopLevelParent(self) + + if isinstance(topLevel, wx.Frame) and topLevel.GetStatusBar(): + statusBar = topLevel.GetStatusBar() + + if self._statusTimer and self._statusTimer.IsRunning(): + self._statusTimer.Stop() + statusBar.PopStatusText(0) + + statusBar.PushStatusText(longHelp, 0) + self._statusTimer = StatusBarTimer(self) + self._statusTimer.Start(_DELAY, wx.TIMER_ONE_SHOT) + + + def RemoveHelp(self): + """ Removes the tooltips and statusbar help (if any) for a button. """ + + if not self.GetUseHelp(): + return + + if self._haveTip: + self.SetToolTipString("") + self._haveTip = False + + if self._statusTimer and self._statusTimer.IsRunning(): + topLevel = wx.GetTopLevelParent(self) + statusBar = topLevel.GetStatusBar() + self._statusTimer.Stop() + statusBar.PopStatusText(0) + self._statusTimer = None + + + def RepaintOldSelection(self): + """ Repaints the old selected/hovered button. """ + + current = self._currentButton + + if current == -1: + return + + btn = self._vButtons[current] + if not btn.IsEnabled(): + return + + btn.SetStatus("Normal") + + + def OnStatusBarTimer(self): + """ Handles the timer expiring to delete the longHelp in the StatusBar. """ + + topLevel = wx.GetTopLevelParent(self) + statusBar = topLevel.GetStatusBar() + statusBar.PopStatusText(0) + + + def SetUseHelp(self, useHelp=True): + """ Sets whether or not shortHelp and longHelp should be displayed. """ + + self._useHelp = useHelp + + + def GetUseHelp(self): + """ Returns whether or not shortHelp and longHelp should be displayed. """ + + return self._useHelp + + def HitTest(self, pt): """ HitTest method for ButtonPanel. Returns the button (if any) and a flag (if any). """ - btnIdx = -1 - for ii in xrange(len(self._vButtons)): - if self._vButtons[ii].GetRect().Inside(pt): + if not self._vButtons[ii].IsEnabled(): + continue + if self._vButtons[ii].GetRect().Contains(pt): return ii, BP_HT_BUTTON return -1, BP_HT_NONE + def GetBPArt(self): + """ Returns the associated BPArt art provider. """ + return self._art + + + def SetBPArt(self, art): + """ Sets a new BPArt to ButtonPanel. Useful only if another BPArt class is used. """ + + self._art = art + self.Refresh() + + if wx.VERSION < (2,7,1,1): + def Freeze(self): + """Freeze ButtonPanel.""" + + self._freezeCount = self._freezeCount + 1 + wx.PyPanel.Freeze(self) + + + def Thaw(self): + """Thaw ButtonPanel.""" + + if self._freezeCount == 0: + raise "\nERROR: Thawing Unfrozen ButtonPanel?" + + self._freezeCount = self._freezeCount - 1 + wx.PyPanel.Thaw(self) + + + def IsFrozen(self): + """ Returns whether a call to Freeze() has been done. """ + + return self._freezeCount != 0 + +