Connection list, showing IM accounts in the buddylist.
import wx
from wx import AutoBufferedPaintDC, RectS, Point, Rect, Size, MemoryDC, CallLater
from gui import skin
from gui.skin.skinobjects import SkinColor,Margins,MarginSizer
from gui.textutil import default_font,CopyFont, Wrap,GetTextWidth
from gui.uberwidgets.uberwidget import UberWidget
from gui.uberwidgets.UberButton import UberButton
from gui.uberwidgets.umenu import UMenu
from cgui import SimplePanel
from common.Protocol import ProtocolStatus,OfflineReason
from common import profile, pref, actions
from gui.toolbox.scrolling import WheelScrollMixin
accounts = None
ShowNothingStates = set([ProtocolStatus.CONNECTING,
class ConnectionsPanel(SimplePanel, UberWidget):
        Panel that holds the list of accounts, used for frame and holding button
    def __init__(self, parent):
        SimplePanel.__init__(self, parent, wx.FULL_REPAINT_ON_RESIZE)
        content = self.content = wx.BoxSizer(wx.VERTICAL)
        self.SetSkinKey('AccountPanels', True)
        cl = self.cl = ConnectionList(self)
        content.Add(cl, 1, wx.EXPAND)
        statebutton = self.statebutton = UberButton(self, icon = self.iconshow, skin = self.buttonskin)
        self.button_has_been_pressed = False
        statebutton.Bind(wx.EVT_BUTTON, self.__OnButtonClick)
        if self.expandup:
            content.Add(statebutton, 0, wx.EXPAND)
            content.Insert(0, statebutton, 0, wx.EXPAND)
        self.Sizer = MarginSizer(self.framesize, content)
        Bind = self.Bind
        Bind(wx.EVT_SIZE,  self.OnSize)
        Bind(wx.EVT_PAINT, self.OnPaint)
        profile.prefs.add_observer(self.WhenOrderChanges,'buddylist.order') #@UndefinedVariable
    def WhenOrderChanges(self,*a):
        order = pref('buddylist.order', [])
            cli = order.index('clist')
            bli = order.index('blist')
        except ValueError: #not in list
            self.expandup = False
            self.expandup = bli<cli
        self.iconshow = self.iconup if self.expandup else self.icondown
        self.iconhide = self.icondown if self.expandup else self.iconup
        if hasattr(self,'statebutton'):
            statebutton = self.statebutton
            content = self.content
            if self.expandup:
                content.Add(statebutton, 0, wx.EXPAND)
                content.Insert(0, statebutton, 0, wx.EXPAND)
            self.statebutton.SetIcon(self.iconhide if self.cl.ShowAll else self.iconshow)
    def UpdateSkin(self):
            The usual.
        key = self.skinkey
        s= lambda k,d=None: skin.get('%s.%s'%(key,k),d)
        self.framebg        = s('Frame',      lambda: SkinColor(wx.BLACK))
        self.framesize      = s('FrameSize',  lambda: Margins([0,0,0,0]))
        self.iconup         = s('icons.show', None)
        self.icondown       = s('icons.hide', lambda: wx.BitmapFromImage(wx.ImageFromBitmap(self.iconup).Mirror(False)))
        self.buttonskin     = s('buttonskin', None)
        if self.Sizer:
    def __OnButtonClick(self, e):
        self.button_has_been_pressed = True
        return self.ToggleState(e)
    def ToggleState(self, event = None):
            Toggles the List between Show All mode and hide Online/Offline mode
        cl = self.cl
        cl.ShowAll = not cl.ShowAll
        if cl.ShowAll:
        self.statebutton.SetIcon(self.iconhide if cl.ShowAll else self.iconshow)
    def OnSize(self,event):
            Relayout on resize
    def OnPaint(self,event):
            Standard paints.
        dc   = AutoBufferedPaintDC(self)
        rect = RectS(self.Size)
panel_flags = wx.EXPAND|wx.BOTTOM|wx.LEFT|wx.RIGHT
class ConnectionList(WheelScrollMixin, wx.ScrolledWindow, UberWidget):
        This is a scrollable list of all accounts associated to the profile.
        Its size is the height of all shown panels up to a certain height defined
        in the prefs.  Has two modes. In ShowAll mode all accounts are shown
        regardless of state. When ShowAll is false only those accounts that have
        an error, or are currently in between offline and online states are shown.
        Once a state is online or offline it will disappear after a period of time.
        When the list of accounts changes, the list destroys all existing panels,
        and recreates them in the correct order.
    def __init__(self,parent):
        super(ConnectionList, self).__init__(parent)
        #set here so that when a profile change occurs the reference updates correctly
        global accounts
        accounts = profile.account_manager.accounts
        self.Sizer = wx.BoxSizer(wx.VERTICAL)
        self.ShowAll = False
        self.oldaccounts = []
        self.panels      = {}
        self.menu = UMenu(self)
        Bind = self.Bind
        Bind(wx.EVT_PAINT, self.OnPaint)
        Bind(wx.EVT_ERASE_BACKGROUND, lambda e: None)
        Bind(wx.EVT_SCROLLWIN_LINEDOWN, lambda e: self.Scroll(0,self.ViewStart[1]+SCROLLCONST))
        Bind(wx.EVT_SCROLLWIN_LINEUP, lambda e: self.Scroll(0,self.ViewStart[1]-SCROLLCONST))
        Bind(wx.EVT_SCROLLWIN, lambda e: (e.Skip(), wx.CallAfter(self.Refresh)))
        Bind(wx.EVT_KEY_DOWN, self.OnKey)
    def OnKey(self,event):
        key = event.KeyCode
        if key == wx.WXK_PAGEDOWN:
        elif key == wx.WXK_PAGEUP:
    def UpdateSkin(self):
        key = self.skinkey
        s = lambda k, d = None: skin.get('%s.%s' % (key,k), d)
        self.bg = s('backgrounds.behind', SkinColor(wx.WHITE))
        self.spacing = s('spacing', 0)
    def OnPaint(self, event):
        self.bg.Draw(AutoBufferedPaintDC(self), RectS(self.ClientSize))
    def _on_mousewheel(self, e):
            Handles mouse wheel scroll events to make the list scroll with mouse
            wheel use.
        super(ConnectionList, self)._on_mousewheel(e)
        if not self.bg.ytile:
    def get_wheel_lines(self, rotation, e):
        val = rotation / float(e.GetWheelDelta()) * e.LinesPerAction
        return val / 2.0 # 2-line panels
    def rotation_for_lines(self, lines, e):
        return float(e.GetWheelDelta()) / e.LinesPerAction * lines * 2.0
    def ScrollLines(self, lines):
        dist = 0
        a = b = None
            accts = list(accounts)
            if len(accts) > 1:
                a = self.panels[accts[1]].Position.y
                b = self.panels[accts[0]].Position.y
                dist = 0
        except Exception:
            dist = 0
            if a is not None and b is not None:
                dist = a - b
        if dist:
            dist = lines * dist - getattr(self, 'borrowed_dist', 0)
            self.borrowed_dist = int(round(dist)) - dist
            self.Scroll(0, self.ViewStart.y + int(round(dist)))
            super(ConnectionList, self).ScrollLines(lines)
    def InitShow(self,*a):
            Because accounts are loaded late, the decision to show most happen
            later.  Decides the initial show based off if the account
        #ShowAll is True if no accounts autoLogin
        def show():
            if not self.Parent.button_has_been_pressed: # don't toggle state if user has already clicked.
                self.ShowAll = not any(getattr(account, 'autologin', False) for account in profile.account_manager.accounts)
                self.Parent.statebutton.icon = self.Parent.iconhide if self.ShowAll else self.Parent.iconshow#Label = 'Hide' if self.ShowAll else 'Show'
        self.showtimer = CallLater(500, show)
    def SetShowAll(self, show):
        Set the ShowAll mode True or False
        True    - Show all accounts
        False   - Show only accounts in error or between states
        self.showall = show
    def GetShowAll(self):
        "Returns ShowAll mode True or False"
        return self.showall
    ShowAll = property(GetShowAll, SetShowAll)
    def UpdateChildrenShown(self):
        Loop through the children showing or hiding the ones affected by the
        a ShowAll switch.
        for child in self.Children:
            if hasattr(child, 'DetermineShow'):
    def OnSize(self, event = None):
        When the list has a change in size sets the virtual size and calls
        SelfSize to firgure out the appropriate MinSize
        if event is not None:
        # Make sure virtual Width is the same as the Client Width
        self.SetVirtualSizeHints(self.ClientSize.width, -1, self.ClientSize.width, -1)
        if self.Size.width == self.lastwidth:
    def WhenListChanges(self, *a):
            scrolly = self.GetScrollPos(wx.VERTICAL)
            accts = list(accounts)
            # Early exit hack here if the accounts list is not empty and equal
            # to our cached one in self.oldaccounts--the logic for preserving
            # the scroll is always a few pixels off, so don't do anything if we
            # don't have to.
            if accts and self.oldaccounts == accts:
            self.Sizer.Clear(deleteWindows = False)
            oldaccts, panels = self.oldaccounts, self.panels
            old, new = set(oldaccts), set(accts)
            for acct in old - new:
                panel = panels.pop(acct)
            for acct in new - old:
                panels[acct] = ConnectionPanel(self, acct)
            # Add a spacer to make sure the first visible item obeys the spacing between
            # itself and the edge of the list
            # create a new panel for each account and place it in the sizer
    #        try:
            for acct in accts:
                self.Sizer.Add(panels[acct], 0, panel_flags, self.spacing)
            self.oldaccounts = accts
            wx.CallAfter(self.Scroll, 0, scrolly)
    def SelfSize(self):
            Figures out the maximum height of the list by the smaller of shown elements
            or the pref connectionlist.max_height
        #h is the minimum of height of all items + spacing or the height specified in prefs
        h = min(sum(child.MinSize.height+self.spacing for child in self.Children if child.Shown) + self.spacing, pref('connectionlist.max_height', 200))
        #Hide if no items shown
        self.Show(h > self.spacing)
        self.MinSize = wx.Size(-1, h)
        if self.Shown:
        self.lastwidth = self.Size.width
    def EnsureVirtualHeight(self):
            This is called to make sure that the VirtualHeight is correct
        vheight = self.VirtualSize.height
        cheight = self.Sizer.MinSize.height
        if vheight != cheight:
            self.VirtualSize = (-1, cheight)
class ConnectionPanel(SimplePanel, UberWidget):
        This panel corresponds to an individual IM account and displays data about it.
        It displays the account's username, it's offline reason or state if no
        reason is available, a link that will connect if offline or disconnect
        if online, a dismiss button if the list is not set to ShowAll and the
        account is either online or offline, the protocol icon, and the state icon
        if the list is in ShowAll.
    def __dtor__(self):
        from gui.toolbox.refreshtimer import refreshtimer
    def __init__(self, parent, account):
        self.initover = False
        SimplePanel.__init__(self, parent)
        #Refernce for the timer for delayed hide
        self.willhide = None
        self.title = account.username
        #Becasue the width affects the content and the content effects the height
        #   this is used to store the last size so the widths can be compared
        #   perventing an infinit loop of size adjustments
        #These are used for faking a button
        self.buttonstate = 0 #visual state: Idle; 1: hover; 2: down
        self.buttonisdown = False #because if the button is down and shown down can differ
        self.buttonshown= False #Is the button visible?
        self.linkhovered = False #True if mouse is in the link rect
        self.linkdown = False #True if the mouse button was pushed while in link rect
        Bind = self.Bind
        Bind(wx.EVT_PAINT,        self.OnPaint)
        Bind(wx.EVT_SIZE,         self.OnSize)
        Bind(wx.EVT_ENTER_WINDOW, self.OnMouseIn)
        Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseOut)
        Bind(wx.EVT_MOTION,       self.OnMouseMotion)
        Bind(wx.EVT_LEFT_DOWN,    self.OnLeftDown)
        Bind(wx.EVT_LEFT_UP,      self.OnLeftUp)
        Bind(wx.EVT_RIGHT_UP,     self.OnMenu)
        Bind(wx.EVT_KEY_DOWN,     self.OnKey)
        self.initover = True
    def OnKey(self, event):
    def OnMenu(self, e = None):
        if e: e.Skip()
        menu    = self.Parent.menu
        account = self.account
        menu.AddItem(_('&Edit'), callback = lambda: profile.account_manager.edit(account))
        if account.connection:
            actions.menu(self, account.connection, menu)
            actions.menu(self, account, menu)
            menu.AddItem(_('&Connect'), callback = lambda: account.connect())
    def WhenStateChange(self, acct, attr, old, new):
        When the account state changes this calls everything that sets up the
        the elements of the panel and determines whether it shows or hides
        # An if self check becasue it's posible the callback could trigger between
        # the time the object is destroyed and the observer is removed
        account = self.account
        #When in the WILL_RECONNECT state, registered to refresh every 1 second
        from gui.toolbox.refreshtimer import refreshtimer
        if account.offline_reason == OfflineReason.WILL_RECONNECT:
            self.reason = lambda: profile.account_manager.state_desc(self.account)
            self.reason = profile.account_manager.state_desc(account) if account.state==ProtocolStatus.OFFLINE and account.offline_reason else _(account.state)
        self.stateicon = account.statusicon.ResizedSmaller(16)
        if not wx.IsDestroyed(self):
            #Tells DetermineShow to delay the hide if state changed to ONLINE or OFFLINE
            delayed = (account.state == ProtocolStatus.ONLINE) or (account.state == ProtocolStatus.OFFLINE and account.offline_reason == OfflineReason.NONE)
    def MakeLink(self):
            Gets the link information for the current state of the account
        self.link,self.linkcallback = self.account.get_link()
    def UpdateSkin(self):
            The usual update skin
        key = self.skinkey
        s = lambda k,d=None: skin.get('%s.%s'%(key,k),d)
        self.bg             = s('backgrounds.account',lambda: SkinColor(wx.WHITE))
        self.majorfont      = s('Fonts.Account', default_font)
        self.minorfont      = s('Fonts.StateAndLink',default_font)
        self.linkfont       =  CopyFont(self.minorfont, underline=True)
        self.majorfc        = s('FontColors.Account', wx.BLACK)
        self.statefc        = s('FontColors.State',lambda: wx.Colour(125,125,125))
        self.linkfc         = s('FontColors.Link', wx.BLUE)
        self.closeicon      =  [None]*3
        self.closeicon[0]   = s('Icons.Close',skin.get('AppDefaults.removeicon')).ResizedSmaller(16)
        self.closeicon[1]   = s('Icons.CloseHover',self.closeicon[0]).ResizedSmaller(16)
        self.closeicon[2]   = s('Icons.CloseDown',self.closeicon[1]).ResizedSmaller(16)
        self.liconsize      = s('ServiceIconSize',16)
        self.padding        = s('Padding',lambda: wx.Point(3,3))
        self.stateicon = self.account.statusicon.ResizedSmaller(16)
        self.licon = self.account.serviceicon.Resized(self.liconsize)
        if self.initover:
    def CalcLayout(self):
        Calculates the layout and height of the pannel based off of it's
        width and the size of the elements
        dc = MemoryDC()
        #x and y are the cursor for were the next item will be placed
        #padx and pady are just local refs to padding.x and padding.y
        x,y = padx, pady = self.padding
        sz = self.Size
        linkfont  = self.linkfont
        minorfont = self.minorfont
        majorfont = self.majorfont
        licon   = self.licon
        link    = self.link
        #Positiong the service icon and the status/close icon to the left or
        #   right respectivly
        self.liconpos = Point(x, y)
        self.riconpos = Point(sz.width - 16 - padx, y)
        #incriment x position to right of icon
        x += licon.Width + padx
        #Which is taller, the label or the icon
        h = max(majorfont.Height, 16)
        #Place title rect with the erlier calculated values and width whats left
        #   of the overall width for the width left for the label
        self.titlerect = Rect(x, y, sz.width-4*padx-licon.Width-16,h)
        #incriment y to below the filled space
        y += h+pady
        #Find the link width because it's needed to know how much room the
        #   reason rect needs
        linkwidth = GetTextWidth(link,linkfont)
        #Determine the horizantal space left for the reason, then wrap it
        reasonwidth = sz.width - x - (linkwidth + 2*padx)
        reason = self.reason() if callable(self.reason) else self.reason
        wrappedreason = self.wrappedreason = Wrap(reason, reasonwidth, minorfont, dc)
        #Get the height needed for the label from the wraped string
        reasonheight = dc.GetMultiLineTextExtent(wrappedreason,minorfont)[1]
        #combine into a rectangle
        self.reasonrect = Rect(x,y,reasonwidth,reasonheight)
        #Figure the rest of the link size information
        self.linksize = Size(linkwidth,linkfont.LineHeight)
        #figure out the height the panel has to be in order to fit the content
        h = max(y+reasonheight+pady,self.liconsize+2*padx)
        newSize = Size(self.MinSize.width,h)
        if self.MinSize != newSize:
            self.MinSize = newSize
    def DetermineShow(self, later=False):
            Decides weither the panel should show or hide.
            If 'later' is True, hiding is delayed by the pref connectionlist.online_linger
        account = self.account
        #Initialise or stop a delayed hide
        if later and not self.Parent.ShowAll and self.Shown:
            if not self.willhide:
                self.willhide = wx.CallLater(pref('connectionlist.online_linger',1000),self.WillShowFalse)
            if self.willhide:
                self.willhide = None
            #figures if it should show if parent is set to ShowAll or Offline with a reason and not achknowledged or the state is not OFFLINE or ONLINE
            shouldshow = self.Parent.ShowAll or ((account.state==ProtocolStatus.OFFLINE and account.offline_reason and not account.error_acked) or account.state not in (ProtocolStatus.OFFLINE,ProtocolStatus.ONLINE))
    def WillShowFalse(self):
        Callback for the delayed hide of DetermineShow
        Hides the panel and takes the stepss to correct the layout for the
    def OnSize(self,event):
        When the panel is width is change, runs CalcLayout to position elements
        and figure new height
        # Make sure the width actualy changed before calculating new positions and height
        if self.lastsizedto.width != self.Size.width:
            self.lastsizedto = self.Size
    def DoLinkPos(self):
        "Positions the link in the lower right hand corner of the panel."
        sz = self.Size
        linksz = self.linksize
        padx,pady = self.padding
        # Link goes in lower right hand corner
        self.linkrect = wx.Rect(sz.width - padx - linksz.width, sz.height - pady - linksz.height,*linksz)
    def OnPaint(self,event):
        dc   = AutoBufferedPaintDC(self)
        rect = RectS(self.Size)
        self.bg.Draw(dc, rect)
        # Draw service icon
        icon = self.licon if self.account.is_connected else self.licon.Greyed
        dc.DrawBitmapPoint(icon, self.liconpos, True)
        #The right icon can either be the close icon, the state icon, or nothing
        #Shows the state icon if the list is in ShowAll or nothing if state is CONNECTING, AUTHENTICATING,INITIALIZING, or LOADING_CONTACT_LIST
        #   Otherwise it shows the close button
        ricon = self.stateicon if self.Parent.ShowAll else (self.closeicon[self.buttonstate] if self.account.state not in ShowNothingStates else None)
        if ricon:
            rconpos = self.riconpos + Point(8 - ricon.Width // 2, 8 - ricon.Height // 2)
            dc.DrawBitmapPoint(ricon, rconpos, True)
        self.buttonshown = bool(ricon)
        # Draw the account name
        dc.Font = self.majorfont
        dc.TextForeground = self.majorfc
        dc.DrawTruncatedText(self.title, self.titlerect)
        # Draws the offline reason or state
        dc.Font = self.minorfont
        dc.TextForeground = self.statefc
        if callable(self.reason):
            self.wrappedreason = Wrap(self.reason(), self.reasonrect.Width, self.minorfont, dc )
        # Draw the link
        dc.Font = self.linkfont
        dc.TextForeground = self.linkfc
    def OnDestroy(self):
        When the panel is destroyed, this removes the panel as an observer
        of the account
    def OnLeftDown(self,event):
        Detects left mouse button down events for emulation of links and
        # if the button is being hovered
        if self.buttonstate == 1:
            # it is now down
            self.buttonstate = 2
            self.buttonisdown = True
        elif self.linkhovered:
            self.linkdown = True
    def OnLeftUp(self,event):
            Detects left mouse button up events for emulation of links and
        #if the buttons state is down, set it to idle and close the panel
        #   ackknowledging the the error
        if self.buttonstate == 2:
            self.buttonstate = 0
            self.account.error_acked = True
        self.buttonisdown = False
        #If link is down
        if self.linkdown:
            #And if the pointer is over the link
            if self.linkhovered:
                #call the callback for the link
            self.linkdown = False
    def OnMouseMotion(self,event):
            Ensures mouse in and mouse out capture events occure, also handels
            hover for the button and link
        #If the left mouse button is not down, make sure the panel gets/losses
        # capture accordingly
        if not event.LeftIsDown():
            mouseisin = wx.FindWindowAtPointer() is self
            if mouseisin and not self.HasCapture():
            elif not mouseisin and self.HasCapture():
        #Handle button statethings on mouse over
        if not self.Parent.ShowAll and self.HasCapture() and self.buttonshown:
            bstate = self.buttonstate
            bisdown = self.buttonisdown
            mouseinb = wx.RectPS(self.riconpos,(16,16)).Contains(event.Position)
            if bstate == 0 and mouseinb:
                self.buttonstate = 2 if bisdown else 1
            elif bstate in (1,2) and not mouseinb:
                self.buttonstate = 0
        #handle link mouseover
        mouseinl = self.linkhovered = self.linkrect.Contains(event.Position)
        self.SetCursor(wx.StockCursor(wx.CURSOR_HAND if mouseinl else wx.CURSOR_DEFAULT))
    def OnMouseIn(self,event):
            Captures the mouse when the cursor enters the panel
        if not self.HasCapture() and not event.LeftIsDown():
    def OnMouseOut(self,event):
            Releases the mouse when the cursor leaves the panle
        if not event.LeftIsDown():
            while self.HasCapture():