示例#1
0
文件: imtabs.py 项目: sgricci/digsby
class TitleBarTimer(wx.Timer):
    '''
    Manages titles of tab and window titles for conversations.

    Shows flashing strings like (New IM) and (5 New IMs).
    '''

    shouldFlash = prefprop('conversation_window.notify_flash')
    cyclePause = prefprop('conversation_window.unread_cycle_pause')

    def __init__(self, win, tabs):

        wx.Timer.__init__(self)

        self.win = win
        self.tabs = tabs
        self.index = 0

    def Start(self):
        self.title = self.win.Title

        wx.Timer.Start(self, self.cyclePause)

    def Notify(self):
        win = self.win
        tabs = self.tabs

        if wx.IsDestroyed(win):
            self.Stop()
            return

        # (>")>[New IMs]<("<)
        if not win.IsActive() and len(tabs):
            tabNum = len(tabs)
            if self.index >= tabNum:
                self.index = 0
            tab = tabs[self.index]
            if not wx.IsDestroyed(tab):
                win.SetTitle('*' + tab.label1)
                self.index += 1
            else:
                tabs.remove(tab)

            if self.shouldFlash:
                FlashOnce(
                    win
                )  # hack until we figure out how to set the title without clearing the notify state
        else:
            self.Stop()

    def Stop(self):
        wx.Timer.Stop(self)
        if not wx.IsDestroyed(self.win):
            self.win.SetTitle(self.title)
        self.index = 0
示例#2
0
class FakeProtocol(common.protocol):

    name = 'fake'
    NUM_BUDDIES = common.prefprop('fake.num_buddies', default=20)

    def __init__(self, username, password, hub, server=None, **options):
        common.protocol.__init__(self, username, password, hub)

        self.root_group = Group('Root', self, 'Root')
        self.buddies = ObservableDict()
        self.self_buddy = FakeBuddy('self', self)
        self.buddies['self'] = self.self_buddy
        self.conversations = {}

    def Connect(self, invisible=False):
        self.change_state(self.Statuses.ONLINE)
        g1 = Group('Foo', self, 'Foo')
        g2 = Group('Bar', self, 'Bar')
        self.buddies['foobar'] = FakeBuddy('foobar', self)
        #g1.append(Contact(self.buddies['foobar'], 'foobar'))
        self.root_group.append(g1)
        self.root_group.append(g2)

        for i in range(int(self.NUM_BUDDIES)):
            g = random.choice((g1, g2))
            buddy = FakeBuddy('FakeBuddy #%d' % (i % 3), self)
            buddy.status = random.choice(('away', 'available', 'offline'))
            g.append(buddy)

        self.root_group.notify()

    #needs to be added to Protocol NotImplemented
    def set_message(self, message, status, format=None, default_status='away'):
        pass

    #needs to be added to Protocol NotImplemented
    def set_buddy_icon(self, icondata):
        pass

    def Disconnect(self):
        self.change_state(self.Statuses.OFFLINE)

    @property
    def caps(self):
        return []

    def convo_for(self, contact):
        try:
            return self.conversations[contact.id]
        except KeyError:
            c = FakeConversation(self)
            self.conversations[contact.id] = c
            return c

    def incomming_message(self, buddy, message):
        self.conversations.values()[0].received_message(buddy, message)
示例#3
0
            attrs = self.magicattrs
        except AttributeError:
            self.setup_string_substitution()
            attrs = self.magicattrs

        try:
            try:
                return attrs[attr](m, b) if args is None else attrs[attr](m, b,
                                                                          args)
            except KeyError:
                return extra[attr]
        except Exception:
            #print_exc() #uncomment me to find out why there are %strayvariables% in your HTML
            return '%' + attr + '%'

    show_tstamp = prefprop('conversation_window.timestamp', True)
    tstamp_fmt = prefprop('conversation_window.timestamp_format', '%#I:%M')

    def setup_string_substitution(self):
        # TODO: this should probably be done once, at the class level...not in a function

        icon = get_buddy_icon_url

        def strf(t, fmt=None):
            if not self.show_tstamp and not self.should_always_show_timestamp:
                return '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'

            fmt = self.tstamp_fmt if fmt is None else fmt

            for s, repl in strftime_replacements:
                fmt = fmt.replace(s, repl)
示例#4
0
文件: iminput.py 项目: sgricci/digsby
class IMInput(FormattedInput, FormatPrefsMixin, SplitterEventMixin):
    def __init__(
        self,
        parent,
        value='',
        autosize=True,
        formatOptions=None,
        multiFormat=True,
        showFormattingBar=True,
        rtl=False,
        skin=None,
        entercallback=None,
        validator=wx.DefaultValidator,
    ):

        FormattedInput.__init__(self,
                                parent,
                                value=value,
                                autosize=autosize,
                                formatOptions=formatOptions,
                                multiFormat=multiFormat,
                                showFormattingBar=showFormattingBar,
                                rtl=rtl,
                                skin=skin,
                                validator=validator)

        self.LoadStyle('messaging.default_style')

        self.entercallback = entercallback

        self.tc.Bind(wx.EVT_KEY_DOWN, self.OnEnterKey)

        self.sendbutton = None
        if self.showSendButton:
            self.CreateSendButton()

    def ShowSendButton(self, show):
        sendbutton = self.sendbutton
        hasSendButton = sendbutton is not None

        if hasSendButton:
            self.spacer.Show(show)
            sendbutton.Show(show)
        elif show:
            self.CreateSendButton()

        self.Layout()

    def CreateSendButton(self):

        self.spacer = SpacerPanel(self, skinkey='inputspacer')
        self.inputsizer.Add(self.spacer, 0, wx.EXPAND)

        sendbutton = self.sendbutton = UberButton(
            self, label=_('Send'),
            skin='InputButton')  #wx.Button(self, label = _('Send'))
        self.inputsizer.Add(sendbutton, 0, wx.EXPAND)
        sendbutton.Bind(wx.EVT_BUTTON, lambda e: self.entercallback(self))

    shiftToSend = prefprop("messaging.shift_to_send", False)
    showSendButton = prefprop("messaging.show_send_button", False)

    def OnEnterKey(self, event):
        """
            This detects key presses, runs entercallback if enter or return is pressed
            Any other key continues as normal, then refreshes the font and size info
        """
        keycode = event.KeyCode

        shiftToSend = self.shiftToSend
        hasModifiers = event.HasModifiers()
        shiftIsDown = event.Modifiers == wx.MOD_SHIFT

        if keycode in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
            # if there is a enter callback and no modifiers are down or if
            # shift should send and shift is down, call the callback
            if self.entercallback and \
                    (not (shiftToSend or hasModifiers or shiftIsDown) or \
                     (shiftToSend and shiftIsDown)):
                return self.entercallback(self)

        event.Skip()
示例#5
0
class PopupStack(list):
    'A popup stack is in charge of positioning popups for one corner of a display.'

    def __init__(self, monitor, position, padding=None, border=None):

        self.monitor = monitor

        self.corner = position
        assert position in valid_corners, position

        if padding is None:
            padding = (0, 10)
        if border is None:
            border = (10, 10)

        self.padding = Point(*padding)
        self.border = Point(*border)

        self.NextRect = self.Down if TOP & self.corner else self.Up
        self.OppositeRect = self.Up if TOP & self.corner else self.Down

    def __repr__(self):
        return '<PopupStack %s monitor %d (%d popups)>' % (alignment_to_string(
            self.corner), self.monitor, len(self))

    @property
    def ScreenRect(self):
        return screenArea(self.monitor)

    offset = prefprop('notifications.popups.offset', (0, 0))

    def Up(self, prevRect, newSize, user_action):
        border, padding = self.border, self.padding

        if prevRect is None:
            if LEFT & self.corner:
                pt = self.ScreenRect.BottomLeft + (border.x + padding.x +
                                                   self.offset[0],
                                                   -border.y - self.offset[1])
            else:
                pt = self.ScreenRect.BottomRight - (newSize.width + border.x +
                                                    padding.x + self.offset[0],
                                                    border.y + self.offset[1])
        else:
            pt = prevRect.TopLeft - Point(0, border.y + padding.y)

        r = RectPS(pt - Point(0, newSize.height), newSize)
        if AVOID_MOUSE and not user_action and r.Contains(GetMousePosition()):
            r.y -= r.Bottom - GetMousePosition().y + AVOID_MOUSE_PIXELS

        return r

    def Down(self, prevRect, newSize, user_action):
        border, padding = self.border, self.padding

        if prevRect is None:
            if LEFT & self.corner:
                pt = self.ScreenRect.TopLeft + (border.x + padding.x +
                                                self.offset[0],
                                                border.y + self.offset[1])
            else:
                pt = self.ScreenRect.TopRight - (newSize.width + border.x +
                                                 padding.x + self.offset[0],
                                                 -border.y - self.offset[1])
        else:
            pt = prevRect.BottomLeft + Point(0, border.y + padding.y)

        r = RectPS(pt, newSize)

        if AVOID_MOUSE and not user_action and r.Contains(GetMousePosition()):
            r.y += GetMousePosition().y - r.y + AVOID_MOUSE_PIXELS

        return r

    def InitialPos(self, size):
        return self.NextRect(None, size).Position

    def Add(self, popup):
        assert popup not in self
        self.append(popup)
        popup.OnClose += lambda userClose: self.Remove(popup,
                                                       user_action=userClose)
        self.DoPositions(popup)
        popup._infader = fadein(popup, 0, popup.opacity_normal, 30)

    def Remove(self, popup, user_action=False):
        try:
            self.remove(popup)
        except ValueError:
            pass
        self.DoPositions(user_action=user_action)

    def DoPositions(self, paging=None, user_action=False):
        prevRect = None

        for popup in self[:]:
            try:
                oldrect = popup.Rect
            except wx.PyDeadObjectError:
                self.remove(popup)
                log.critical('dead Popup object in %r' % self)
                continue

            quick = False
            desired = popup.DesiredSize

            if popup.Hover or popup.has_focus:
                rect = RectPS(popup.Position, desired)

                # if the popup is in one of the bottom corners and it's
                # expanding, keep the bottom of its rectangle in the same
                # place since that's where the interactable GUI is
                if paging is popup and BOTTOM & self.corner:
                    rect.y -= rect.height - oldrect.height
            else:
                rect = self.NextRect(prevRect,
                                     desired,
                                     user_action=user_action)

            popup._moved = True

            self.SetPopupRect(popup, rect, quick=paging is popup)
            prevRect = rect

    slidetime = prefprop('notifications.popups.slide_ms', 140)

    def SetPopupRect(self, popup, rect, quick=False):
        t = int(self.slidetime)
        if t == 0: quick = True

        oldrect = popup.Rect

        if quick:
            popup.SetRect(rect)
        else:
            if oldrect.Size != rect.Size:
                popup.SetRect(wx.RectPS(oldrect.Position, rect.Size))

            move_smoothly(popup, rect.Position, time=t)
示例#6
0
class GroupCellRenderer(Renderer):
    def __init__( self, parent ):
        Renderer.__init__(self, parent)

        layout_attrs = '''
        name_font_face
        name_font_size
        padding
        '''.strip().split()

        do(self.attrlink(attr) for attr in layout_attrs)

        self.UpdateSkin()

    def UpdateSkin(self):
        Renderer.UpdateSkin(self)
        s = self.skin

        s.margins          = skin.get('BuddiesPanel.GroupMargins')
        s.padding          = skin.get('BuddiesPanel.GroupPadding', lambda: Point(4,4))

        # Expanded/Collapsed icons next to group names
        g = lambda k, default = sentinel: skin.get('BuddiesPanel.GroupIcons.' + k, default)
        s.expanded         = g('Expanded',         lambda: None)
        s.expandedhover    = g('ExpandedHover',    lambda: s.expanded)
        s.expandedselected = g('ExpandedSelected', lambda: s.expanded)

        s.collapsed         = g('Collapsed',         lambda: None)
        s.collapsedhover    = g('CollapsedHover',    s.collapsed)
        s.collapsedselected = g('CollapsedSelected', s.collapsed)

        # Group backgrounds (default to Buddy backgrounds if not specified)
        g = lambda k, default: skin.get('BuddiesPanel.Backgrounds.' + k, default)
        s.bg         = g('Group',         lambda: g('Buddy'))
        s.hoverbg    = g('GroupHover',    lambda: g('BuddyHover'))
        s.selectedbg = g('GroupSelected', lambda: g('BuddySelected'))

        # Group font colors (default to Buddy font colors if not specified)
        f = s.fontcolors
        g = lambda k, default: skin.get('BuddiesPanel.FontColors.' + k, default)
        f.group         = g('Group',         lambda: g('Buddy',         lambda: syscol(wx.SYS_COLOUR_WINDOWTEXT)))
        f.grouphover    = g('GroupHover',    lambda: g('BuddyHover',    lambda: syscol(wx.SYS_COLOUR_WINDOWTEXT)))
        f.groupselected = g('GroupSelected', lambda: g('BuddySelected', lambda: syscol(wx.SYS_COLOUR_HIGHLIGHTTEXT)))

        self.calcsizes()

    def item_height( self, obj ):
        return int(self.group_height)

    def calcsizes(self):
        p = self.getpref
        margins = self.skin.margins
        padding = self.skin.padding

        # Main Font: contact's name
        self.mainfont        = safefont(p('name_font_face', None), try_this(lambda: int(p('name_font_size')), 10))
        self.mainfont_height = self.mainfont.LineHeight

        # group_height is reported via OnMeasureItem to VListBox
        self.group_height = int(self.mainfont_height) + margins.top + margins.bottom + (padding.y * 2)

        self.depth_indent = p('indent', 5)

    font_face    = prefprop('buddylist.layout.name_font_face', None)
    font_size    = prefprop('buddylist.layout.name_font_size', None)
    group_indent = prefprop('buddylist.layout.indent', 0)

    def Draw( self, dc, rect, selected, obj, depth, expanded, index, hover ):
        s = self.skin

        # apply margins
        rect = rect.AddMargins(wx.Rect(*s.margins)).AddMargins(wx.Rect(0, s.padding.y, 0, s.padding.y))

        # Group font is drawn with the same as the buddies.
        fontface = self.font_face
        font = safefont(fontface, try_this(lambda: int(self.font_size), 10), bold = True)
        dc.SetFont( font )

        # indent for depth
        rect = rect.Subtract(left = self.group_indent * depth)

        # Expander triangles.
        if isiterable( obj ):
            triangle = self.get_expander(selected, expanded, hover)

            if triangle is not None:
                dc.DrawBitmap(triangle, rect.x, rect.VCenter(triangle), True)
                rect = rect.Subtract(left = triangle.Width + s.padding.x)

        # decide on a foreground text color
        if selected: fg = s.fontcolors.groupselected
        elif hover:  fg = s.fontcolors.grouphover
        else:        fg = s.fontcolors.group
        dc.SetTextForeground( fg )

        # the actual text label
        dc.DrawTruncatedText(obj.display_string, rect, alignment = lmiddle)

    def get_expander(self, selected, expanded, hover):
        iconname = 'expanded' if expanded else 'collapsed'
        if selected: iconname += 'selected'
        elif hover:  iconname += 'hover'
        return getattr(self.skin, iconname, None)
示例#7
0
        try:
            attrs = self.magicattrs
        except AttributeError:
            self.setup_string_substitution()
            attrs = self.magicattrs

        try:
            try:
                return attrs[attr](m, b) if args is None else attrs[attr](m, b, args)
            except KeyError:
                return extra[attr]
        except Exception:
            #print_exc() #uncomment me to find out why there are %strayvariables% in your HTML
            return '%' + attr + '%'

    show_tstamp = prefprop('conversation_window.timestamp', True)
    tstamp_fmt  = prefprop('conversation_window.timestamp_format', '%#I:%M')

    def setup_string_substitution(self):
        # TODO: this should probably be done once, at the class level...not in a function

        icon = get_buddy_icon_url

        def strf(t, fmt = None):
            if not self.show_tstamp and not self.should_always_show_timestamp:
                return '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'

            fmt = self.tstamp_fmt if fmt is None else fmt

            for s, repl in strftime_replacements:
                fmt = fmt.replace(s, repl)
示例#8
0
class TabBar(SimplePanel, UberWidget):
    """
        Where the tabs live, handles all display and organization functionality
    """
    def __init__(self, parent, skinkey):
        SimplePanel.__init__(self, parent)

        self.tabs     = [] # a list of all the tabs
        self.rows     = [] # a list of all the visible row, each a list of all the tabs in that row
        self.rowindex = 0  # the first visible row
        self.tabindex = 0  # the first tab of the first visible row
        self.tabendex = 0  # the last tab of the last visible row

        events = [(wx.EVT_PAINT, self.OnPaint),
                  (wx.EVT_SIZE, self.OnSize),
                  (wx.EVT_BUTTON, self.OnButton),
                  (wx.EVT_MOUSEWHEEL, self.OnWheel),
                  (wx.EVT_MOTION,self.OnMotion)]

        for event, method in events: self.Bind(event, method)


        self.flagedrows = set()
        self.lastsize=self.Size

        self.rowheight=0#height of a row in pixels

        self.SetSkinKey(skinkey,True)

        #buttons for verticle alignment
        self.cupb = UberButton(self, CUPID, skin=self.scrollbuttonskin, icon=self.upicon)
        self.cupb.Show(False)

        self.cdownb = UberButton(self, CDOWNID, skin=self.scrollbuttonskin, icon=self.downicon)
        self.cdownb.Show(False)

        #the navigation box
        self.navi=Navi(self)


        self.dragorigin = None#when draging the tab that you are dragging
        self.dragtarget = None#when dragging the mouse is over and at that point released on

        # the arrow image shown when dragging tabs
        self.dropmarker=OverlayImage(self, self.dropmarkerimage)

        # self.dropmarker = Storage(Show = lambda v=True: None)
        self.dragside=None#was the tab droped on the left or right of the target tab

        #linking prefs
        link = profile.prefs.link #@UndefinedVariable
        link('tabs.rows',      self.Generate, False)
        link('tabs.tabbar_x',  self.Generate, False)
        link('tabs.hide_at_1', self.Generate, False)
        link('tabs.side_tabs', self.SkinRedirect, False)

        self.Top.Bind(wx.EVT_MENU, self.OnMenuEvent)

    side_tabs = prefprop('tabs.side_tabs')
    tab_rows = prefprop('tabs.rows', 2)

    def UpdateSkin(self):
        key = self.tabskin = self.skinkey
        g   = lambda k, default = sentinel: skin.get(key + '.' + k, default)
        sg   = lambda k, default = sentinel: skin.get('side' + key + '.' + k, default)

        elems = (('spacing', 'spacing',          2),
                 ('bg', 'backgrounds.bar'),
                 ('dropmarkerimage', 'dropmarker.image'),
#                 ('dropmarkeroverlay', 'dropmarker.overlay', 0),
                 ('dropmarkeroffset', 'dropmarker.offset', 0),
                 ('closebuttonskin', 'closebuttonskin', ''),
                 ('closeicon', 'icons.close', None),
                 ('scrollbuttonskin', 'scrollbuttonskin', ''),
                 ('lefticon', 'icons.left', ''),
                 ('righticon', 'icons.right', ''),
                 ('upicon', 'icons.up', ''),
                 ('downicon', 'icons.down', ''))

        for elem in elems:
            setattr(self, 'top' + elem[0], g(*elem[1:]))
            setattr(self, 'side' + elem[0], sg(elem[1],getattr(self,'top' + elem[0])))
            setattr(self, elem[0], getattr(self, ('side' if self.side_tabs else 'top') + elem[0]))


        if hasattr(self,'dropmarker'):
            self.dropmarker.SetImage(self.dropmarkerimage)
            self.dropmarker.SetRotation((self.side_tabs and not self.dropmarkerimage))

        navi = getattr(self, 'navi', None)
        if navi is not None:
            self.cdownb.SetSkinKey(self.scrollbuttonskin)
            self.cupb.SetSkinKey(self.scrollbuttonskin)
            self.cdownb.SetIcon(self.downicon)
            self.cupb.SetIcon(self.upicon)

            self.navi.closebutton.SetSkinKey(self.closebuttonskin)

            self.navi.closebutton.SetIcon(self.closeicon)

            scrollskin = self.scrollbuttonskin

            navi.prevb.SetSkinKey(scrollskin)
            navi.nextb.SetSkinKey(scrollskin)
            navi.upb.SetSkinKey(scrollskin)
            navi.downb.SetSkinKey(scrollskin)

            navi.prevb.SetIcon(self.lefticon)
            navi.nextb.SetIcon(self.righticon)
            navi.upb.SetIcon(self.upicon)
            navi.downb.SetIcon(self.downicon)

        wx.CallAfter(self.Generate)


    def SkinRedirect(self,val=None):
        elems = ('spacing',
                 'bg',
                 'dropmarkerimage',
                 #'dropmarkeroverlay',
                 'closebuttonskin',
                 'closeicon',
                 'scrollbuttonskin',
                 'lefticon',
                 'righticon',
                 'upicon',
                 'downicon'
                 )

        for elem in elems:
            setattr(self, elem, getattr(self,('side' if self.side_tabs else 'top') + elem))

        self.UpdateChildSkins()

    def UpdateChildSkins(self):
        self.cdownb.SetSkinKey(self.scrollbuttonskin,True)
        self.cupb.SetSkinKey(self.scrollbuttonskin,True)

        navi, sbs = self.navi, self.scrollbuttonskin
        navi.closebutton.SetSkinKey(self.closebuttonskin,True)
        navi.prevb.SetSkinKey(sbs, True)
        navi.nextb.SetSkinKey(sbs, True)
        navi.upb.SetSkinKey(sbs, True)
        navi.downb.SetSkinKey(sbs, True)

        self.UpdateChildrenIcons()

        for tab in self.tabs:
            tab.UpdateMode()

        self.Generate()

    def __repr__(self):
        return '<TabBar %r>' % self.tabs

    def OnDragStart(self, tab):
        'Catches the tab drag event and starts the tab dragging system.'

        self.NotifyDrag(tab)

    def OnMotion(self,event):
        'Positioning updates during drag and drop'

        if event.LeftIsDown() and (self.dragorigin or self.Manager.source):
            self.DragCalc(event.Position)

    def __getitem__(self, index):
        return self.tabs[index]

    def OnPaint(self, event):
        dc   = wx.PaintDC(self)
        rect = RectS(self.Size)
        if not self.side_tabs:
            rcount = min(len(self.rows), pref('tabs.rows', 2))
            height = self.tabs[0].Size.height

            y=0
            for unused_i in xrange(rcount):
                self.bg.Draw(dc, Rect(rect.x, y, rect.width, height))
                y += height

        else:
            self.bg.Draw(dc,rect)

    def Add(self, page, focus, resort = True):
        """
            Adds a tab to the bar.  Should only be used by parent NoteBook.
            page - page in PageContainer the tab is to be associated with
            focus - whether that tab should steal focus from current tab
        """
        tab = Tab(self, page, skinkey = self.tabskin)
        tab.Bind(wx.EVT_CONTEXT_MENU, self.ShowMenu)
        tab.Show(False)
        self.tabs.append(tab)

        if focus:
            wx.CallAfter(tab.SetActive, True)

        elif resort:
            if self.side_tabs:
                self.ReVgenerate()
            else:
                self.Regenerate(True)

        return tab

    def ShowMenu(self, e):
        self._menutab = e.EventObject

        try:
            menu = self._tabmenu
        except AttributeError:
            from gui.uberwidgets.umenu import UMenu
            menu = self._tabmenu = UMenu(self)
            menu.AddItem('Close &Other Tabs', id = CLOSE_OTHER_TABS)
            menu.AddSep()
            menu.AddItem('&Close Tab', id = CLOSE_TAB)

        menu.PopupMenu()

    def OnMenuEvent(self, e):
        '''Invoked when a tab context menu item is clicked.'''

        if e.Id == CLOSE_TAB:
            self._menutab.CloseTab()
        elif e.Id == CLOSE_OTHER_TABS:
            menutab = self._menutab

            # switch to that tab first
            menutab.active = True

            with self.Frozen():
                for tab in self.tabs[:]:
                    if tab is not menutab:
                        tab.CloseTab()
        else:
            e.Skip()

    def Generate(self, val=None):
        self.navi.closebutton.Show(pref('tabs.tabbar_x', False))

        if self.side_tabs:
            self.ReVgenerate(True)
        else:
            self.Regenerate()

    def ReVgenerate(self,total=False, safe=False, dotoggle=True):
        """
        It's like Doo... err.. Regenerate, only vertical
        """

#        print "Starting: Regenerate",self.Top.Title,'\n'#,'='*80,'\n','\n'.join(format_stack())
#        print '='*80

        #TODO: Should we be careful about the tab leaving the bar?
        tabs = self.tabs

        if not tabs: return

        do(tab.Show(False) for tab in self.tabs)
        for tab in self.tabs: tab.row = None
        del self.rows[:]

        # Safty precautions prevent list access errors
        if self.tabindex < 0 or self.tabindex >= len(tabs):
            self.tabindex = 0

        # Preset variables
        n = self.tabindex   # the first tab shown
        self.rowheight = tabs[0].GetMinHeight()
        area = self.Notebook.Size.height - 32 # Height in pixels of the tabbar

        # number of fully visible rows in the given area at the given height
        i = area//self.rowheight

        count = len(tabs)

        #one tab per row
        for r in xrange(count): tabs[r].row=r

        rows = self.rows
        size = self.Size

        #Sets navimode and position
        navi = self.navi
        navi.ShowNav(4)
        navi.Hide()
        navi.Position = wx.Point(size.width - navi.Size.width,0)

        # Totally reconstructs the list if it's told to or there are not tabs in the rows or
        # if there isn't one more tab than there is room for and there is enough room to fit
        # them all and number of tabs in the row equals the number of tabs
        if total or not rows or (i + 1 != len(rows[0])) and not (i > len(rows[0])) and len(rows[0]) == len(tabs):
            rows.append([])
            col = rows[0]

            #if all tabs fit
            if i >= count:
                n=0
                self.tabindex=0
                do(col.append(tab) for tab in tabs)
                av=col[0].MinSize.height
            #calculate and show range
            else:
                for t in xrange(n,n+i+1):
                    if t < len(tabs):col.append(tabs[t])

                # populate with earlier stuff
                while len(col) < i and n > 0:
                    n-=1
                    col.insert(0,tabs[n])

                if col: av = col[0].MinSize.height
        else:
            #just leave the new values the same as the old
            col = rows[0]
            av  = col[0].MinSize.height

        # Show all tabs in the bar
        count = 16
        for t in col:
            t.Size = (self.Size.width,av)
            t.Position = (0,count)
            count += av
            t.Show()

        self.tabindex=n
        endex = self.tabendex=n+len(col)

        if dotoggle:
            self.Toggle()

        cupb, cdownb = self.cupb, self.cdownb


        cupb.Enable(self.tabindex != 0)
        cdownb.Enable(endex < len(tabs) or tabs[endex - 1].Position.y +
                           tabs[endex-1].Size.height > size.height - 16)


        self.UpdateNotify()

    def Regenerate(self, safe = False, dotoggle=True):
        '''
        Regenerates layout information.

        safe is a flag to indicate if we should try to keep the currently active
        tab in view at all times. (This doesn't occur when scrolling, for
        instance.)
        '''

#        print "Starting: Regenerate",self.Top.Title,'\n','='*80,'\n','\n'.join(format_stack())
#        print '='*80
        # early exit for when the tabbar isn't visible.
        if not self.IsShown() and len(self.tabs) == 1:
            return

        with self.Frozen():
            self._Regenerate(safe = safe, dotoggle = dotoggle)

        self.Refresh(False)

    def _Regenerate(self, safe = False, dotoggle = True):
        self.cupb.Show(False)
        self.cdownb.Show(False)
        parentpage = self.Parent.pagecontainer

        # style is the number of rows (or 0 for single)
        style = self.tab_rows

        # Should we be careful about the tab leaving the bar?
        careful = not safe and parentpage.active

        # Hide all tabs preparation for refilling
        for tab in self.tabs:
            tab.Show(False)
            tab.row = None
        del self.rows[:]

        # navi set up, see if arrows are needed and placement
        tally = sum(tab.MinSize.width for tab in self.tabs) #total size of tabs

        navi = self.navi
        tabs = self.tabs
        rows = self.rows

        if not tabs: return

        # Tab alignment calculations

        # Saftey precautions prevent list access errors
        if self.tabindex < 0 or self.tabindex >= len(tabs):
            self.tabindex = 0

        # Preset variables
        n = self.tabindex   # the first tab shown
        i = n
        row = 0
        self.rowheight = tabs[0].MinHeight


        my_w, nav_w = self.Size.width, navi.Size.width

        # Decide what kind of navigation panel, if any, to use...
        if tally >= my_w - nav_w and not style:
            navi.ShowNav(1) # arrows left and right
        elif tally >= (my_w - nav_w):
            navi.ShowNav(3) # arrows up and down next to the X
        else:
            navi.ShowNav(0)

        #Where to put navigation panel.
        navi.Freeze()
        navi.Show(True)
        navi.Fit()
        navi.Position = wx.Point(self.Size.width-navi.Size.width,0)
        navi.Size = wx.Size(-1,self.Size.height)
        navi.Thaw()

        #More preparing vars
        area = self.Notebook.Size.width - navi.Size.width

        #While more tabs are not in a row
        while len(tabs) > i:
            tally = tabs[i].MinSize.width
            rows.append([])

            # Loop through each visible tab, fitting tabs on the right.
            while i < len(tabs) and tally < area:
                i += 1
                if i < len(tabs):
                    tally += tabs[i].MinSize.width

            #Be carefull that the active tab doesn't scroll off the bar
            if careful and not style:
                activeindex = tabs.index(parentpage.active.tab)
                change=False

                #add tabs until the active tab is visible
                while activeindex>=i and n!=i:
                    i += 1
                    tally += tabs[i].MinSize.width
                    change = True

                #Remove tab if more tabs than room
                if tally >= area and change:
                    tally -= tabs[n].MinSize.width
                    n += 1
                    self.tabindex=n




            # If extra space, fit tabs to the right of the row
            if not style: # if single row,
                while n > 0 and area - tally > tabs[n-1].MinSize.width:
                    n -= 1
                    self.tabindex = n
                    tally += tabs[n].MinSize.width


            # Injects tabs calculated to fit in that row into that row
            if range(n, i):
                rows[row] = [tabs[t] for t in xrange(n, i)]
                for tab in rows[row]:
                    tab.row=row
            else:
                rows[row].append(tabs[i])
                i += 1

            if not style: break # If we're in single row, break now.
            row += 1
            n = i
        # Row calculation

        if self.rowindex >= len(rows):
            self.rowindex = len(rows) - 1

        #cycle through visible rows
        row = self.rowindex
        visible = self.tab_rows or 1

        if careful and style:
            #print "Being Careful"
            active = parentpage.active.tab
            #print active
            for ir,r in enumerate(rows):
                #print ir,r
                if active in r:
                    if ir<row:
                        #print "moving index down"
                        row = self.rowindex = ir
                    elif ir >= row + visible:
                        #print "moving index up"
                        row = ir - (visible - 1)


        # If we're closing tabs above where is visible, keep the visible
        # index "where it is"
        if len(rows) - (row + 1) < visible and len(rows) >= visible:
            row = len(rows) - visible
            self.rowindex = row

        # Place tabs!
        while row < len(rows) and row < self.rowindex + visible and len( rows[row] ) != 0:

            # if this is a row that needs to be scrunched...
            if rows.index(rows[row]) == len(rows)-1 and \
                (style or len(rows[row]) == len(tabs)):

                for t in xrange(0,len(rows[row])):
                    thistab = rows[row][t]
                    thistab.SetSize(thistab.MinSize)
                    if not t:
                        # The first tab is set to it's minimum width at x: 0
                        thistab.SetPosition((0, self.rowheight*(row-self.rowindex)))
                    else:
                        # Every other tab is placed right next to the tab
                        # before it.
                        thistab.SetPosition((rows[row][t-1].Position.x \
                                             + rows[row][t-1].Size.width,
                                             self.rowheight*(row-self.rowindex)))
                    thistab.Show(True)

            # If there are more rows than the current row...
            elif len(rows) > row:
                # Get a list of tab indices, widest to smallest.
                ordered = [rows[row].index(t)
                           for t in sorted(rows[row],
                                           key=lambda o: o.MinSize.width,
                                                         reverse=True) ]
                length = len(ordered)
                reserved=0
                o=0 # o_O ?

                # Average width of tab if all tabs are the same size, and
                # fill up all the area.
                av = (area - reserved) / (length - o)
                mark = 0
                while o < length:
                    # Loop from "current" tab to the end
                    for t in xrange(o, length):
                        tab = rows[row][ordered[t]]

                        # If this tab is larger than average...
                        if tab.GetMinSize()[0] > av:
                            # Make it it's minimum, and keep track of it
                            tab.SetSize(tab.MinSize)
                            reserved += tab.MinSize.width
                            o += 1
                            mark = o

                            # If we're not on the last tab, recalc average
                            if (length - o):
                                av=(area-reserved)/(length-o)
                        else:
                            o += 1
                            break

                # For tabs less than the average, set them to average
                for t in xrange(mark, length):
                    tab = rows[row][ordered[t]]
                    tab.SetSize((av, tab.MinSize.height))

                # For every tab in the row
                for t, tab in enumerate(rows[row]):
                    if not t: # If it's the first tab:
                        if length==1:
                            # If the row is so small it can only fit one tab,
                            # make due.
                            tab.Size = wx.Size(area, tab.MinSize.height)
                        tab.Position = wx.Point(0, self.rowheight * (row - self.rowindex))
                    else:
                        tab.Position = wx.Point(rows[row][t-1].Position.x + rows[row][t-1].Size.width,
                                                self.rowheight * (row - self.rowindex))

                    tab.Show(True)
            row += 1
        if dotoggle:
            self.Toggle()

        # If total rows is less than total rows being shown, shrink the
        # tab area so that it's only just big enough.
        if len(rows) < style or not style:
            rows_shown = len(rows)
        else:
            rows_shown = style
        if  self.Parent.SashPosition != rows_shown * self.rowheight:#self.MinSize.height
            self.MinSize = wx.Size(-1, rows_shown * self.rowheight)
            self.Parent.SetSashPosition(self.MinSize.height)
#            self.Size=self.MinSize

        # Determine if the Navi needs to enable or show arrows
        navi.Enabler()
        #self.Parent.Layout()    # Relayout self
        self.tabendex = i-1     # final tab being shown

        self.UpdateNotify()

        navi.Size = wx.Size(-1,rows_shown * self.rowheight)

    def Remove(self, target):
        'Removes the tab specified from the bar.'

        index=self.tabs.index(target)
        self.tabs.remove(target)
        #if no more tabs close window
        if len(self.tabs)==0:
            self.Notebook.window.Close()
        else:
            #if index is between index and endex and bring one tab from the left
            if index>self.tabindex and index<self.tabendex and self.tabindex>0:
                self.tabindex-=1

            if self.side_tabs:
                self.ReVgenerate(total=True)
            else:
                self.Regenerate(safe = True)


    def OnSize(self, event):
        'ReLayout the tabs if the bar on event of a resize'
        event.Skip()

        if self.side_tabs and self.tabs:
            cupb   = self.cupb
            cdownb = self.cdownb
            size   = self.Size
            tabs = self.tabs
            endex = self.tabendex

            # position and size buttons
            cupb.Position = (0,0)
            cupb.Size = (size.width, 16)
            cupb.Show()
            cupb.Enable(self.tabindex != 0)

            cdownb.Position = (0, size.height - 16)
            cdownb.Size     = (size.width, 16)
            cdownb.Show()
            cdownb.Enable(endex < len(tabs) or tabs[endex - 1].Position.y +
                           tabs[endex-1].Size.height > size.height - 16)


        sz = self.Size
        if ((sz.width != self.lastsize.width and not self.side_tabs) or (sz != self.lastsize and self.side_tabs)) and self.IsShownOnScreen():
            self.lastsize = sz
            if self.side_tabs:
                self.ReVgenerate(dotoggle = False)
            else:
                self.Regenerate(False,dotoggle = False)

        try:
            wx.CallAfter(wx.CallAfter,self.Parent.pagecontainer.active.panel.input_area.expandEvent)
        except AttributeError:
            pass

        self.Refresh(False)


    def GetTabCount(self):
        """
            Returns the number of tabs in the bar
        """
        return len([t for t in self if t])

    def NextTab(self):
        self.SetNextActive(self.ActiveTab, wrap = True)

    def PrevTab(self):
        self.SetLastActive(self.ActiveTab, wrap = True)


    def SetNextActive(self, origin,wrap=False):
        """
            Sets the tab after the curent active
            -if it does not exist does the previbus
            -or the first if wrap is true
        """
        if origin in self.tabs:
            index=self.tabs.index(origin)
            if not index < len(self.tabs)-1 and wrap:
                self.tabs[0].SetActive(True)
            elif index < len(self.tabs)-1:
                self.tabs[index+1].SetActive(True)
            elif index>0:
                self.tabs[index-1].SetActive(True)
            self.Refresh(False)

    def SetLastActive(self, origin, wrap = False):
        """
            Sets the tab before the curent active
            -if it does not exist does the next
            -or the last if wrap is true
        """
        if origin in self.tabs:
            index=self.tabs.index(origin)
            if not index > 0 and wrap:
                self.tabs[len(self.tabs)-1].SetActive(True)
            elif index >0:
                self.tabs[index-1].SetActive(True)
            elif index<0:
                self.tabs[index+1].SetActive(True)
            self.Refresh(False)

    def SyncActive(self,atab):
        """
            Moves the index and endex so that the active tab is in the bar
        """
        if not atab: return

        if self.side_tabs:
            if atab < self.tabindex:
                self.tabindex=atab
                self.ReVgenerate(True)
            else:
                thetab=self.tabs[atab]
                while atab >= self.tabendex or thetab.Position.y+thetab.Size.height > self.Size.height-16:
                    self.tabindex+=1
                    self.ReVgenerate(True)
        else:
            style = self.tab_rows
            if atab < self.rowindex:
                self.rowindex=atab
                self.Regenerate()
            elif atab > self.rowindex+style-1:
                self.rowindex=atab-style+1
                self.Regenerate()

    def OnWheel(self,event):
        """
            Event that handles mouse wheeling,
            maps the events to SetNextActive and SetLastActive
        """
        if RectS(self.Size).Contains(event.Position):
            direction = event.GetWheelRotation()
            if direction<0:
                self.SetNextActive(self.ActiveTab, True)
            elif direction>0:
                self.SetLastActive(self.ActiveTab, True)

    @property
    def Notebook(self):
        return self.Parent

    @property
    def Manager(self):
        return self.Notebook.manager

    @property
    def ActiveTab(self):
        active = self.Notebook.pagecontainer.active
        if active is not None:
            return active.tab

    def OnButton(self,event):
        """
            The button events for vertical alignment for up and down
        """
        if event.GetId()==CUPID:
            if self.tabindex > 0:
                self.tabindex -= 1
                self.ReVgenerate(total = True)
        elif event.GetId()==CDOWNID:
            if self.tabendex<len(self.tabs) or self.tabs[self.tabendex-1].Position.y+self.tabs[self.tabendex-1].Size.height>self.Size.height-16:
                self.tabindex+=1
                self.ReVgenerate(total=True)

        self.UpdateNotify()

    def NotifyDrag(self, origin):
        """
            When a tab is dragged this is called to start the tabbar handling dragging
            origin - tab being dragged
        """
        # Lets the TabMan know a tab as been dragged
        self.dragorigin = origin
        origin.SetCursor(wx.StockCursor(wx.CURSOR_NO_ENTRY))
        self.Manager.Notify(self.Notebook)


    def DragCalc(self,point):
        """
            This does the dragging calculations for the tabs
        """

        sidetabs = self.side_tabs

        # if here is no local origin tab but there is a remote source identified in TabMan
        if not self.dragorigin and self.Manager.source:
            #setting a local drag origin
            master = self.Manager.source
            dragorigin=master.tabbar.dragorigin
            # announcing to TabMan it is expecting a tab
            self.Manager.Request(self.Notebook)
        # if there is a local origin use that
        else:
            dragorigin=self.dragorigin

        # if dragtarget is out of date find what you're dragging to
        if not self.dragtarget or not self.dragtarget.Rect.Contains(point):

            wap = wx.FindWindowAtPointer()
            self.dragtarget = wap if isinstance(wap,Tab) else None
            self.dragside   = None

        # if there is a tab as target
        if self.dragtarget and self.dragtarget != dragorigin:
            dtrect=self.dragtarget.Rect
            # data to decide what side the tab would be dropped on
            if not sidetabs:
                x  = point[0] - dtrect.x
                x2 = dtrect.width / 2
            else:
                x  = point[1] - dtrect.y
                x2 = dtrect.height / 2
            # make the left/top or right/bottom decision
            if x <= x2:#left/top
                if self.dragside!=False:
                    self.dragside=False
                    if not sidetabs:
                        self.DrawDropMarker(dtrect.x, dtrect.y)# + (dtrect.height // 2)
                    else:
                        self.DrawDropMarker(dtrect.x, dtrect.y)# + dtrect.width // 2

            elif not self.dragside:#right/bottom
                self.dragside=True
                if not sidetabs:
                    self.DrawDropMarker(dtrect.x+dtrect.width,dtrect.y)#+(dtrect.height//2)
                else: self.DrawDropMarker(dtrect.x,dtrect.y+dtrect.height)#+dtrect.width//2
            self.SetFocus()
        # if being dropped in the whitespace of the TabBar
        elif (dragorigin and self.dragtarget!=dragorigin) or (dragorigin==None and self.dragtarget==None) and self.Rect.Contains(point):
            # find what row the tab is being dropped in
            if not sidetabs:
                row=self.rows[(point[1]//self.rowheight)+self.rowindex]
                tab=row[len(row)-1]
                self.dragside=True
            # or in vertical if at the beginning or end
            else:
                if point.y>self.rowheight:
                    tab=self.rows[0][len(self.rows[0])-1]
                    self.dragside=True
                else:
                    tab=self.rows[0][0]
                    self.dragside=False
            dtrect=tab.Rect
            #Place marker
            if not sidetabs: self.DrawDropMarker(dtrect.x+dtrect.width,dtrect.y)#+(dtrect.height//2)
            elif self.dragside==True: self.DrawDropMarker(dtrect.x,dtrect.y+dtrect.height)#+dtrect.width//2
            else: self.DrawDropMarker(dtrect.x+dtrect.width/2,dtrect.y)
            self.SetFocus()
            #cleanup
            self.dragtarget=tab

        else:#if not in tabbar anymore don't show arrow
            self.dropmarker.Show(False)

    def DragFinish(self,new=False):
        """
        Ends dragging and does any rearranging if required
        """

        if not wx.IsDestroyed(self.dragorigin):
            self.dragorigin.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))

        if self.dragorigin and self.dragorigin.previewtabs:
            # Destroy the preview tab
            self.dragorigin.previewtabs.Stop()
            self.dragorigin.previewtabs = None

        rect       = RectPS(self.Notebook.ClientToScreen(self.Position), self.Size)
        parentrect = self.Notebook.window.Rect
        mousepos   = wx.GetMousePosition()
        manager    = self.Manager

        #if released out of the window...
        if not new and ((manager.destination and not parentrect.Contains(mousepos)) or not rect.Contains(mousepos)):
            if self.ActiveTab==self.dragorigin:
                self.SetNextActive(self.dragorigin)
            self.dragorigin.Show(False)

            #If no or invalid destination in manager create a new window and sets it destination
            dest = manager.destination
            if not dest or not dest.tabbar.Rect.Contains(dest.ScreenToClient(wx.GetMousePosition())):
                # FIXME: SWIG doesn't like subtracting a wx.Size from a wx.Point, so do it the hard way
                # until the SIP migration is finished.
                originsize = self.dragorigin.GetSize()
                newpoint = wx.Point(mousepos[0] - originsize[0], mousepos[1] - originsize[1])
                destination = self.Notebook.winman.NewWindow(newpoint, self.Notebook.window.GetSize()).notebook
            #else set the destination to the manager's destination
            else:
                destination = dest

            #clear tabman's destination
            manager.Request()

            # Grab a reference to the tab's page
            page = self.dragorigin.page

            # Make the tab "forget" about the page
            self.Notebook.did_remove(page.panel)
            del self.dragorigin.page
            del page.tab

            # Remove the tab from the tabs list, and destroy the wxWindow
            self.tabs.remove(self.dragorigin)

            self.dragorigin.Close()

            # remove page from this notebook and insert it into the target notebook
            #page.Parent.RemoveChild(page)
            destination.Insert(page, False)

            # cleanup
            manager.Notify()
            self.dragorigin=None

            # re-sort tabs
            if self.side_tabs:
                self.ReVgenerate(True)
            else:
                self.Regenerate(safe = True)

        # if released inside of the window
        # used both for moving within a window and as the last step of a
        # interwindow move in case of interwindow tab has already been moved to
        # this window at the end of the list and all that is left is to move it
        # to the correct position.
        elif self.dragtarget and self.dragorigin and self.dragorigin!=self.dragtarget and self.Rect.Contains(self.Notebook.ScreenToClient(mousepos)):
            #remove the tab from the list
            self.tabs.remove(self.dragorigin)

            #decide which side of the target the tab should be dropped
            pos = self.tabs.index(self.dragtarget) + (1 if self.dragside else 0)

            # Reinsert the tab in it's new position
            self.tabs.insert(pos, self.dragorigin)

            after = self.tabs[pos+1] if pos+1 < len(self.tabs) else None
            if after is not None:
                after = after.page.panel

            # call after so that windows are all in their correct places
            wx.CallAfter(self.Notebook.did_rearrange, self.dragorigin.page.panel, after)

#            # Resort
            if self.side_tabs:
                self.ReVgenerate(True)
            else:
                self.Regenerate()
        elif new:
            if self.side_tabs:
                self.ReVgenerate(True)
            else:
                self.Regenerate()

        #if there is a dragorigin run onMouseLeave on it to reset it's look
        if self.dragorigin:
            self.dragorigin.OnMouseLeave()

        #local cleanup
        self.dropmarker.Show(False)
        self.dragorigin=None
        self.dragtarget=None

        #destination and manager cleanup
        dest = manager.destination
        if dest:
            dest.tabbar.dragorigin=None
            dest.tabbar.dragtarget=None

        manager.Request()
        manager.Notify()

        if len(self.tabs)==0: self.Notebook.window.Close()

    def DrawDropMarker(self,x,y):
        """
        Places the marker for where to drop the tab
        x and y are center position, saves calculation that way
        """

#        if self.side_tabs:
#            self.dropmarker.SetSize((self.Size.width - self.dropmarkeroverlay, -1))
#        else:
#            self.dropmarker.SetSize((-1, self.rowheight - self.dropmarkeroverlay))
        self.dropmarker.Teleport(self.ClientToScreen((x, y+self.dropmarkeroffset)))

        if not self.dropmarker.IsShown():
            self.Manager.ShowDropMarker(self.dropmarker)

    def Toggle(self, switch = None):
        'Toggle whether the tabbar is hidden or shown.'

        if pref('tabs.hide_at_1', True) and len(self.tabs) <= 1 and not switch:
            self.Notebook.Split(False)

            #make sure the content of the IM win resizes if tabbaar hides
            self.ProcessEvent(wx.CommandEvent(wx.wxEVT_SIZE))
        else:
            self.Notebook.Split(True)


    def UpdateNotify(self):

        frows = self.flagedrows
        frows.clear()

        # Resort
        if self.side_tabs:
            tabs=self.tabs
            for i, tab in enumerate(tabs):
                if tab.notified:
                    frows.add(i)
                elif i in self.flagedrows:
                    frows.remove(i)

            self.cupb.SetNotify(len(frows) and min(frows) < self.tabindex)
            self.cdownb.SetNotify(len(frows) and (max(frows) >= self.tabendex or
                                                  tabs[max(frows)].Position.y + tabs[max(frows)].Size.height > self.Size.height - 16))
        else:
            for i, row in enumerate(self.rows):
                flaged = False
                for tab in row:
                    if tab and tab.notified:
                        flaged = True
                        frows.add(i)
            self.navi.upb.SetNotify(len(frows) and min(frows)<self.rowindex)
            self.navi.downb.SetNotify(len(frows) and max(frows)>self.rowindex + self.tab_rows - 1)
示例#9
0
class Protocol(StateMixin):
    __metaclass__ = ObservableActionMeta
    __slots__ = ['name',
                 'username',
                 'password',
                 'hub',
                 'self_buddy',
                 'buddies',
                 'rdv_sessions']

    name = 'AbstractProtocol'

    # Gives a possible "best-guess" email address for a contact.
    @classmethod
    def email_hint(cls, contact): return None

    # server-side ordering of buddylist?
    is_ordered = False
    contact_order = False
    supports_group_chat = False

    # Message Formatting Defaults
    message_format = 'html'
    message_fg = True
    message_bg = True
    message_sizes = [8, 10, 12, 14, 18, 24, 36]

    def __repr__(self):
        return '<%s %s (%s)>' % \
               (self.__class__.__name__, self.username, self.state)



    def __init__(self, username, password, msgHub):
        StateMixin.__init__(self)
        self.username, \
        self.password, \
        self.hub       = str(username), \
                         str(password), \
                         ThreadsafeGUIProxy(msgHub)

        self.self_buddy = None
        self.buddies = None
        self.rdv_sessions = observe.ObservableDict()
        self.state = self.Statuses.OFFLINE
        self.offline_reason = self.Reasons.NONE

        self.add_observer(self.connection_status_monitor, 'state')
        self.add_observer(self.connection_status_monitor, 'offline_reason')

    FAKEROOT_NAME = prefprop('buddylist.fakeroot_name', default='Contacts')

    @property
    def serviceicon(self):
        from gui import skin
        return skin.get('serviceicons.%s' % self.service)


    @property
    def service(self):
        '''An overridable property used for displaying to the user, which can
        differ from the "name," which is the impl. (like 'jabber').'''

        return self.name

    def _unregister_buddies(self):
        log.info('_unregister_buddies: %r', self)

        try:
            unregister = profile.account_manager.buddywatcher.unregister
        except AttributeError:
            unregister = lambda b: None

        for bdict in self.buddy_dictionaries():
            if bdict is None:
                continue

            for buddy in bdict.values():
                unregister(buddy)
                try:
                    buddy.setnotifyif('status', 'unknown')
                except Exception:
                    pass # jabber buddies...
                buddy.observers.clear()

            bdict.observers.clear()
            bdict.clear()

        self.remove_observer(self.connection_status_monitor, 'state', 'offline_reason')

    def silence_notifications(self, timeSecs = None):
        'silences notifications on this protocol for timeSecs seconds'

        old_silence_timer = getattr(self, '_silence_notifications_timer', None)
        if old_silence_timer is not None:
            old_silence_timer.stop()

        from common import silence_notifications
        self._silence_notifications_timer = silence_notifications(self, timeSecs)

    def connection_status_monitor(self, src, attr, old, new):
        assert src is self

        if new == self.Statuses.OFFLINE:
            self._unregister_buddies()

        from plugin_manager import plugin_hub
        plugin_hub.act('digsby.protocol.statechange.async', self, attr, old, new)

        state_log.critical('%r: %r changed from <%r> to <%r>', self, attr, old, new)

    def allow_message(self, buddy, mobj):
        if buddy is self.self_buddy:
            return True

        if mobj.type == 'outgoing':
            return True # don't ever block outgoing messages.

        if getattr(mobj, 'sms', False):
            return True #ticket #3869 don't filter sms (which are usually not on a buddy list, nor an allow list).

    def set_and_size_icon(self, icondata):
        '''
        Sets the buddy icon in the protocol.

        icondata should be raw image file data

        If the given data is too big (in bytes or size), the image
        is resized before being passed to the protocol.
        '''

        from gui.toolbox.imagefx import pil_to_white_gif, has_transparency

        img = Image.open(StringIO(icondata)) # will raise IOError if icondata isn't an image

        max     = getattr(self, 'max_icon_bytes', sys.maxint)
        formats = getattr(self, 'icon_formats', ['PNG', 'JPEG', 'BMP', 'GIF'])
        format  = img.format

        needsResize   = len(icondata) > max
        needsReformat = format not in formats

        log.info('set_and_size_icon')
        log.info('  %d bytes of icondata', len(icondata))
        log.info('  format: %s, formats: %r', format, formats)
        log.info('  needs:')
        log.info('    resize: %s', needsResize)
        log.info('    reformat: %s', needsReformat)

        if needsReformat:
            if has_transparency(img) and 'GIF' in formats:
                # img.getcolors returns None if there are more than 256 colors in the image
                # GIF is probably a good guess for images with less than 256 colors
                format = 'GIF'
            else:
                format = formats[0]

        if needsReformat or needsResize:
            if format == 'GIF':
                # reduces dithering by adapting image palette
                icondata = pil_to_white_gif(img)
                imgFile = StringIO(icondata)
                img = Image.open(imgFile)
                imgFile = StringIO(icondata)
            else:
                opts = {'PNG': {'optimize': True}} # for PNGs, optimize for space

                if format == 'BMP':
                    img = img.convert('RGB')

                imgFile = StringIO()
                img.save(imgFile, format, **opts.get(format, {}))
                icondata = imgFile.getvalue()

        if needsResize:
            # too big, we need to resize
            w, h = getattr(self, 'max_icon_size', (48, 48)); assert w == h
            log.info('set_icon got %d bytes but %s only allows %d, resizing to %dx%d...',
                     len(icondata), self.name, max, w, w)
            img = img.Resized(w)
            if format == 'GIF':
                icondata = pil_to_white_gif(img)
                imgFile = StringIO(icondata)

        if needsReformat or needsResize:
            log.info('encoded icon as %s, size %d: %s...', format, len(icondata), repr(icondata[:16]))

            if len(icondata) > max:
                return log.warning('still too big, not setting')

            icondata = imgFile.getvalue()

        import wx
        wx.CallAfter(lambda : self.set_buddy_icon(icondata))


    @property
    def caps(self):
        raise NotImplementedError('"caps" property (a sequence of Buddy '
                                  'capabilities) is not implemented in '
                                  '%s' % self.__class__.__name__)

    def simple_status(self, status_string):
        'Returns a simple status string. (available or away)'

        from common.statusmessage import simple
        return simple(status_string, self)


    def _set_status_object(self, statusobj):
        'Given a status object, sets the status message for this protocol.'

        acctstatus = statusobj.for_account(self)

        # apply default formatting if none was specified in the status message
        if acctstatus.format is None:

            import wx
            @wx.CallAfter
            def gui_thread(st=acctstatus):
                from common import profile
                if profile and profile.prefs:
                    from gui.uberwidgets.formattedinput import get_default_format
                    st = st.copy(editable=None)
                    st.format = get_default_format()

                netcall(lambda: self.set_message_object(st))

            return

        self.set_message_object(acctstatus)

    @callsback
    def set_message_object(self, message, callback = None):
        msg, status, format = message.message, message.status.lower(), message.format
        if message.editable:
            #needs a hook!
            from digsby_status.status_tag_urls import tag_status
            msg = tag_status(msg, self.service, message.status)
        self.set_message(msg, status, format=format)

    @property
    def is_connected(self):
        return self.state == self.Statuses.ONLINE

    def Connect(self):
        raise NotImplementedError

    def Disconnect(self):
        log.info('Protocol.Disconnect')

    def authorize_buddy(self, buddy_obj, allow=True, username_added=None):
        raise NotImplementedError

    def block_buddy(self, buddy_obj):
        raise NotImplementedError

    def unblock_buddy(self, buddy_obj):
        raise NotImplementedError

    def set_remote_alias(self, btuple, new_name):
        raise NotImplementedError

    def chat_with(self, buddyobj):
        raise NotImplementedError

    def send_message(self, bname, msg, formatting):
        raise NotImplementedError

    def send_typing_status(self, bname, status):
        raise NotImplementedError

    def send_im(self, buddyname, message):
        self.convo_for(self.get_buddy(buddyname)).send_message(message)

    def set_status_message(self):
        raise NotImplementedError

    def send_direct_im_req(self, *btuple):
        raise NotImplementedError

    @callsback
    def rejoin_chat(self, old_conversation, callback=None):
        '''
        given a Conversation object belonging to an "old," disconnected
        protocol, joins the same chat room on self (a new, connected protocol).
        '''
        if old_conversation.did_explicit_exit:
            return log.info('not rejoining chat: conversation was exited')

        _original_success = callback.success
        def success(convo):
            if old_conversation.did_explicit_exit:
                log.info('leaving chat: old convo was exited')
                convo.exit()
            else:
                _original_success(convo)
        callback.success = success

        self._do_rejoin_chat(old_conversation, callback=callback)

    @callsback
    def _do_rejoin_chat(self, old_conversation, callback=None):
        self.join_chat(room_name = old_conversation.chat_room_name, notify_profile=False, callback=callback)

    @callsback
    def make_chat_and_invite(self, buddies_to_invite, convo=None, room_name=None, server=None, notify=False, callback=None):
        log.info('make_chat_and_invite')
        log.info('  connection: %r', self)
        log.info('  buddies_to_invite: %r', buddies_to_invite)

        def success(convo):
            callback.success(convo)
            buds = filter(lambda b: b != self.self_buddy, buddies_to_invite)
            convo.add_pending_contacts(buds)
            for b in buds:
                self.invite_to_chat(b, convo)

        self.join_chat(room_name=room_name, server=server, success=success, error=callback.error, notify_profile=notify)

    @callsback
    def invite_to_chat(self, buddy, conversation, callback=None):

        log.warning('invite_to_chat: %r to %r', buddy, conversation)

        orig_error = callback.error
        def on_error(exc=None):
            with traceguard:
                if isinstance(exc, ChatInviteException):
                    name = buddy.name
                    if exc.reason == ChatInviteException.REASON_OFFLINE:
                        msg = _('Error inviting {name}: they are offline').format(name=name)
                    elif exc.reason == ChatInviteException.REASON_GROUPCHAT_UNSUPPORTED:
                        msg = _('Error inviting {name}: they do not support group chat').format(name=name)
                    conversation.system_message(msg)

            orig_error(exc)

        callback.error = on_error
        return conversation.invite(buddy, callback=callback)

    def send_chat_req(self, btuple, room_name, invitemessage):
        raise NotImplementedError

    def send_buddy_list_request(self, btuple):
        raise NotImplementedError

    def set_invisible(self, invisible):
        raise NotImplementedError

    def accept_file(self, file_obj):
        raise NotImplementedError

    def add_buddy(self, bname, group_id=None, pos=0, service=None):
        raise NotImplementedError

    def remove_buddy(self, btuple, group_id=None):
        raise NotImplementedError

    def move_buddy(self, buddy, to_group, from_group=None, pos=0):
        raise NotImplementedError

    def get_group(self, groupname):
        raise NotImplementedError, "should return a Group or None"

    def get_groups(self):
        'Returns a list of group names.'

        return [g.name for g in self.root_group]

    def get_buddy(self, buddyname):
        return self.buddies[buddyname]

    def get_protocol_buddy(self, buddy):
        'Given a buddy, returns the protocol-specific buddy object stored in this Protocol.'
        return self.get_buddy(buddy.name)

    def has_buddy_on_list(self,buddy):
        for group in self.root_group:
            for listbuddy in group:
                if buddy.name == listbuddy.name and buddy.service == listbuddy.service:
                    return True
        return False

    def has_buddy(self, buddyname):
        return buddyname in self.buddies

    def add_new_buddy(self, buddyname, groupname, service = None, alias = None):
        g     = self.get_group(groupname)

        def doit(*res):
            grp = self.get_group(groupname)
            buddy = self.get_buddy(buddyname)
            grp.add_buddy(buddyname, service = service)

            if alias is not None:
                profile.set_contact_info(buddy, 'alias', alias)

        if g is None:
            self.add_group(groupname, success = doit)
        else:
            doit()


    @callsback
    def move_buddy_creating_group(self, contact, groupname, fromgroupname, index = 0, callback = None):
        def do_move():
            self.move_buddy(contact, groupname, fromgroupname, index,
                            callback = callback)

        group = self.get_group(groupname)
        if group is None:
            self.add_group(groupname, success = lambda newgroup,*a,**k: netcall(do_move))
        else:
            do_move()


    def add_contact(self, contactname, group, service = None):
        self.add_buddy(contactname, group, service = service)

    @property
    def protocol(self):
        return self.name

    def should_log(self, messageobj):
        return True

    def when_reconnect(self, callback):
        return self.account.when_reconnect(callback)
示例#10
0
class MessageArea(FrozenLoopScrollMixin, ScrollWinMixin, WheelShiftScrollFastMixin, WheelScrollCtrlZoomMixin, WebKitWindow):
    def __init__(self, parent, header_enabled = True, prevent_align_to_bottom=False):
        super(MessageArea, self).__init__(parent)

        self.inited = False
        self.header_enabled = header_enabled
        self.prevent_align_to_bottom = prevent_align_to_bottom

        self.Bind(webview.EVT_WEBVIEW_BEFORE_LOAD, self._before_load)

    date_context_format = '%A, %B %d, %Y'

    show_fonts     = prefprop('appearance.conversations.show_message_fonts', True)
    show_colors    = prefprop('appearance.conversations.show_message_colors', True)
    show_emoticons = prefprop('appearance.conversations.emoticons.enabled', True)
    htmlize_links  = prefprop('appearance.conversations.htmlize_links', True)

    def _before_load(self, e):
        e.Skip()
        if e.NavigationType == webview.WEBVIEW_NAV_LINK_CLICKED:
            url = e.URL
            if url.startswith('digsby://'):
                from common import urlhandler
                handle_result = urlhandler.handle(url)
                if handle_result.cancel_navigation:
                    e.Cancel()
                else:
                    if url != handle_result.url:
                        e.SetURL(url)

    def init_content(self, theme, chatName = None, buddy = None, show_history = None, prevent_align_to_bottom=False):
        # grab initial theme contents
        self.theme = theme

        # the message area keeps a timestamp, updated with each message, to
        # know when to insert a "date status" line
        now = datetime.now()
        self.tstamp  = toutc(datetime(now.year, now.month, now.day))

        show_header = self.header_enabled and pref('appearance.conversations.show_header', False)
        initialContents = theme.initialContents(chatName, buddy, show_header, prevent_align_to_bottom=prevent_align_to_bottom)

        self.SetPageSource(initialContents, theme.baseUrl)

        if show_history is None:
            show_history = True

        if show_history and history_enabled() and buddy is not None:
            with traceguard:
                self.show_history(buddy)

        self.inited = True

    def show_history(self, buddy):
        num_lines = max(0, pref('conversation_window.num_lines', 5, int))

        if num_lines > 0:
            logsource = buddy
            if pref('conversation_window.merge_metacontact_history', False):
                from common import profile
                metacontact = profile.metacontacts.forbuddy(buddy)
                if metacontact: logsource = list(metacontact).pop()

            msgobjs   = reversed(list(takemany(num_lines, logsource.history)))
            self.replay_messages(msgobjs, buddy)

    def replay_messages(self, msgobjs, buddy, context = True):
        'Displays a sequence of message objects.'

        next         = False
        oldname      = None
        olddirection = None

        num_messages = 0
        skipped_messages = 0

        for msg in msgobjs:
            num_messages += 1

            if msg in quiet_log_messages.messages:
                skipped_messages += 1
                if __debug__: log.info('skipping message: %r', msg)
                continue

            # the theme needs to know which messages to glue together as "adjacent"--
            # so we check the buddy name and the direction attribute for changes.
            name        = msg.buddy.name
            direction   = msg.type
            next        = oldname == name and direction == olddirection

            msg.buddy = buddy_lookup(buddy, name)
            msg.content_type = 'text/html'

            # context means "history" here
            self.format_message(direction, msg, next, context = context)

            oldname = name
            olddirection = direction

        log.info('replay_messages: %d total messages (%d skipped) for buddy %r', num_messages, skipped_messages, buddy)

    def show_header(self, show):
        'Show or hide the message header.'

        return self.RunScript(self.theme.show_header_script(show))

    def date_status(self, dt):
        # "Status" messages are reused as a form of date context for displaying
        # old messages in the history, and for when an IM window is open for
        # more than a day.

        # displayed timestamps need to be converted from UTC->local
        format_dt = fromutc(dt)

        return S(message   = format_dt.strftime(self.date_context_format),
                 timestamp = dt,
                 buddy     = None,
                 type      = None)

    def format_message(self, messagetype, messageobj, next = False, context = False):
        '''
        messagetype    status, incoming, or outgoing
        messageobj     a storage with buddy, message, timestamp, conversation
        next           True if this message is from the same sender as the last one
        context        True if this message is being displayed as message history
        '''
        if messagetype != 'status':

            # don't show date for the first message in the window
            msgtime = messageobj.timestamp

            if msgtime is None:
                msgtime = datetime.utcnow()

            # do the timestamps differ enough to show a date context line?
            if should_show_time(self.tstamp, msgtime):
                self.theme.set_always_show_timestamp(True)
                try:
                    self.format_message('status', self.date_status(msgtime), next = False, context = context)
                finally:
                    self.theme.set_always_show_timestamp(False)

                # This can't be a "next" message, since we just showed a status line.
                next = False

            self.tstamp = msgtime

        content_type = getattr(messageobj, 'content_type', 'text/plain')

        if content_type == 'text/plain':
            messageobj.message = messageobj.message.encode('xml')
            messageobj.message = preserve_whitespace(messageobj.message)
            messageobj.content_type = 'text/html'

        # apply text transformations, including emoticons and stripping
        # colors and formatting (if enabled)
        show_colors = self.theme.allow_text_colors and self.show_colors
        show_emoticons = pref('appearance.conversations.emoticons.pack') if self.show_emoticons else None

        transforms = dict(emoticons = show_emoticons,
                          links     = self.htmlize_links,
                          spaces    = True)

        if not getattr(messageobj, 'linkify', True):
            transforms['links'] = False

        stripped, strip_values = strip(messageobj.message,
                                       formatting           = not self.show_fonts,
                                       colors               = not show_colors,
                                       plaintext_transforms = transforms)

        messageobj = messageobj.copy()
        messageobj.message = stripped

        if getattr(messageobj, 'has_autotext', False):
            extra = {}
        else:
            extra = dict(has_autotext = True, autotext = AUTORESP)

        # If there is one background color, pass it to the conversation theme
        if self.show_colors:
            extra.update(handle_colors(strip_values))

        # get the JS function and complete message (including theme elements)
        func, msg = self.theme.format_message(messagetype, messageobj, next, context, **extra)

        if func:
            script = "%s('%s');" % (func, js_escape(msg))
            self.RunScript(script)
示例#11
0
class FormattedInput(cgui.SimplePanel):
    '''
    An RTF2 based input box with a formatting bar and auto sizing,
    '''
    def __init__(self,
                 parent,
                 pos       = wx.DefaultPosition,
                 size      = wx.DefaultSize,
                 value     = '',
                 entercallback = None,
                 font      = True,
                 pointsize = True,
                 bold      = True,
                 italic    = True,
                 underline = True,
                 textcolor = True,
                 bgcolor   = True,
                 sizes     = DEFAULT_SIZES,
                 emots     = None,

                 singleFormat = False,
                 show_formatting_bar = True,
                 rtl_input = False,
                 prefmode = False,
                 outlined = False,
                 aimprofile = False,
                 autosize = True,
                 default_skin = False,

                 validator = None,

                 format = None,):

        cgui.SimplePanel.__init__(self, parent)

        self.prefmode   = prefmode
        self.aimprofile = aimprofile
        self.autosize   = autosize

        self.UseAppDefaultSkin = (self.prefmode or self.aimprofile) or default_skin

        self.initover = False
        self.entercallback = entercallback

        self.sizes = sizes
        self.emots = emots

        # create the Formatting bar, buttons, and menus
        self.Sizer = wx.BoxSizer(wx.VERTICAL)
        csizer = self.csizer = wx.BoxSizer(wx.VERTICAL)
        self.Sizer.Add(csizer, 1, wx.EXPAND | wx.ALL, 1 if outlined else 0)

        self.UpdateSkin()

        if validator is None:
            validator = LengthLimit(20480)

        if self.autosize:
            tc = self.tc = FormattedExpandoTextCtrl(self, style=txtFlags, value = value, validator=validator)
            tc.Bind(EVT_ETC_LAYOUT_NEEDED, self.expandEvent)
            tc.Bind(wx.EVT_SIZE, self.OnSize)
            if config.platform == 'win':
                tc.Bind(wx.EVT_COMMAND_TEXT_PASTE, self.OnPaste)
            tc.SetMaxHeight(100)

            loadedHeight = pref('conversation_window.input_base_height')
            baseHeight = self.tc.GetCharHeight() + getattr(self.tc, 'GetExtraHeight', lambda: 0)()

            tc.SetMinHeight(loadedHeight or baseHeight)

            if loadedHeight:
                log.info("input_base_height loaded as %s", loadedHeight)
            else:
                log.info("input_base_height not used, set to baseHeight as %s", baseHeight)

            tc.MinSize = wx.Size(tc.MinSize.width, tc.GetMinHeight())
#            wx.CallAfter(self.tc.HideScrollbars)
            self.expandEvent()
        else:
            tc = self.tc = FormattedTextCtrl(self, style = txtFlags, value = value, validator=validator)

        self.tc.LayoutDirection = wx.Layout_RightToLeft if rtl_input else wx.Layout_LeftToRight

        tc.Bind(wx.EVT_CONTEXT_MENU, lambda e: default_umenu(tc).PopupMenu(event = e))

        # bind textfield events
        tc.Bind(wx.EVT_KEY_DOWN,  self.OnKey)

        self.shownbuttons = dict(
            font      = font,
            pointsize = pointsize,
            bold      = bold,
            italic    = italic,
            underline = underline,
            textcolor = textcolor,
            bgcolor   = bgcolor)


        if config.platform != 'mac' and show_formatting_bar:
            self.construct_formatting_bar()
        else:
            self.formatbar = None

        csizer.Add(self.tc, 1, wx.EXPAND)
        self.single_format = singleFormat

        self.initover = True

        if format is None:
            wx.CallAfter(self.LoadStyle)
            profile.prefs.add_observer(self.WhenDefaultLayoutChange, 'messaging.default_style')
        else:
            self.SetFormatStorage(format)

        Bind = self.Bind
        if outlined:
            Bind(wx.EVT_PAINT,self.OnPaint)

        Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)

    def OnPaste(self, event):
        text = clipboard.get_text()
        if text is None:
            return

        # our rich text control adds a newline to the end of copied text--
        # strip one out if it's there
        if text.endswith('\n'):
            text = text[:-1]

        self.tc.WriteText(text)

    def OnSize(self, event):
        # this is an ugly hack, but unavoidable. For some reason, whenever this is called it
        # resets the Mac spell checker state. (i.e. all misspelled words are no longer underlined)
        # this code makes sure that doesn't happen.
        if config.platform == 'mac':
            self.tc.OnTextChanged(event)
            self.tc.MacCheckSpelling(True)

    def BindSplitter(self, splitter):
        splitter.Bind(wx.EVT_LEFT_DOWN, self.OnSplitterStart)
        splitter.Bind(wx.EVT_LEFT_UP, self.OnSplitterSet)
        self.sizeID = wx.EVT_SIZE
        #SWIG HAX: In Robin's bindings, wx.EVT_SIZE is a function, and wx.wxEVT_SIZE is the int id
        if not config.platform == "win":
            self.sizeID = wx.wxEVT_SIZE
        splitter.Connect(splitter.Id, splitter.Id, self.sizeID, self.OnFirstSplitterSize)

        self.splitter = splitter

    def OnFirstSplitterSize(self, event):

        #HAX: make sure the splitter lays out the first time it get's a real size

        event.Skip()

        splitter = self.splitter

        if splitter.Size.height and splitter.Size.width:
            splitter.Disconnect(splitter.Id, splitter.Id, self.sizeID)
            wx.CallAfter(self.expandEvent)

    def OnSplitterStart(self, event):
        event.Skip()
        tc = self.tc

        baseh = min(tc.GetMaxHeight(), tc.GetCharHeight() * tc.GetNumLines() + tc.GetExtraHeight())

        tc.MinSize = wx.Size(tc.MinSize.width, baseh)
        tc.SetMinHeight(baseh)
        self.MinSize = wx.Size(-1, (self.formatbar.Size.height if self.FormattingBarIsShown() else 0) + tc.MinSize.height)

    def OnSplitterSet(self,event):
        event.Skip()
        tc = self.tc

        baseh = min(tc.GetMaxHeight(), self.tc.GetCharHeight() * self.tc.GetNumLines() + self.tc.GetExtraHeight())
        h = tc.Size.height if tc.Size.height != baseh else 0

        log.info("input_base_height set to %s", h)

        setpref('conversation_window.input_base_height', h)
        tc.SetMinHeight(h)

    def OnPaint(self, event):
        'Draws an outline around the control.'
        dc = BufferedPaintDC(self)
        dc.Brush = wx.TRANSPARENT_BRUSH
        dc.Pen = wx.Pen(OUTLINE_COLOR)
        dc.DrawRectangleRect(RectS(self.ClientSize))

    def OnDestroy(self, event):
        event.Skip()
        profile.prefs.remove_observer(self.WhenDefaultLayoutChange, 'messaging.default_style')

    def expandEvent(self, e=None):
        height = (self.formatbar.Size.height if self.FormattingBarIsShown() else 0) + \
            max(self.tc.MinSize.height, self.tc.GetMinHeight())

        self.MinSize = wx.Size(-1, height)

        # If BindSplitter was called, we have .splitter
        if hasattr(self, 'splitter'):
            self.splitter.SetSashPosition(self.splitter.ClientSize.height - height)

    def on_fbar_context_menu(self, e):
        # TODO: why isn't the IM window's formatting bar its own subclass!?

        from gui.uberwidgets.umenu import UMenu
        m = UMenu(self)
        m.AddItem(_('Hide Formatting Bar'), callback = lambda: wx.CallAfter(setpref, 'messaging.show_formatting_bar', False))
        m.PopupMenu()

    def construct_formatting_bar(self):
        fb = self.formatbar = UberBar(self, skinkey = self.toolbarskin, alignment = wx.ALIGN_LEFT)

        if not self.prefmode and not self.aimprofile:
            fb.Bind(wx.EVT_CONTEXT_MENU, self.on_fbar_context_menu)

        self.fontdd = FontDropDown(fb, skinkey = fb.buttonskin)
        self.fontdd.Bind(wx.EVT_COMMAND_CHOICE_SELECTED, self.OnFontSelect)

        self.msize = SimpleMenu(self, self.menuskin, maxheight = 10) # TODO: max and min width?
        self.msize.SetItems(self.SetSizes(self.sizes))
        self.bsize = UberButton(fb, -1, '10', menu = self.msize, type = 'menu')
        self.bsize.SetStaticWidth(self.sizeddwidth)
        self.msize.SetWidth(self.sizeddwidth)
        self.bbold = UberButton(fb, -1, icon = self.bicon, type = 'toggle')
        self.bbold.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleBold)

        self.bitalic = UberButton(fb, -1, icon = self.iicon, type="toggle")
        self.bitalic.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleItalic)

        self.bunderline = UberButton(fb, -1, icon = self.uicon,  type="toggle")
        self.bunderline.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleUnderline)

        self.bcolor = UberButton(fb, -1, icon = self.fcicon )
        self.bcolor.Bind(wx.EVT_BUTTON, self.OnColor)

        self.bbgcolor = UberButton(fb,-1, icon = self.bcicon)
        self.bbgcolor.Bind(wx.EVT_BUTTON, self.OnBGColor)

        self.bemote = UberButton(fb, -1, icon = self.eicon)
        self.bemote.Bind(wx.EVT_BUTTON, self.on_emote_button)

        self.EnableFormattingButtons(getattr(self, 'formatting_enabled', True))

        #Add all the buttons to the formating bar
        fb.AddMany([self.fontdd,
                    self.bsize,
                    self.bbold,
                    self.bitalic,
                    self.bunderline,
                    self.bcolor,
                    self.bbgcolor,
                    self.bemote])

        self.csizer.Insert(0, fb, 0, wx.EXPAND, 0)
        self.ShowButtons(**self.shownbuttons)

        self.UpdateDisplay()

        self.formatbar.Bind(wx.EVT_SIZE, lambda e: (e.Skip(), wx.CallAfter(self.expandEvent)))

    def on_emote_button(self, e):
        self.display_emotibox(self.bemote.ScreenRect)

    def display_emotibox(self, rect):
        ebox = self.get_emotibox()
        # position and display the emotibox
        ebox.Display(rect)

    def get_emotibox(self):
        'Shares the emoticon box between all instances of this class.'

        b = None
        old_name, new_name = getattr(self, '_emotipack_name', None), pref('appearance.conversations.emoticons.pack', type = unicode, default = u'default')
        self._emotipack_name = new_name

        try:
            b = self.__class__.emotibox
            if not wx.IsDestroyed(b):
                if old_name != new_name:
                    b.Destroy()
                elif b.Parent is not self:
                    b.Reparent(self)

        except AttributeError:
            pass

        if b is None or wx.IsDestroyed(b):
            from gui.imwin.emoticons import get_emoticon_bitmaps
            b = self.__class__.emotibox = UberEmotiBox(self, get_emoticon_bitmaps(self._emotipack_name), self.tc, maxwidth = 12)
        else:
            b.SetTextCtrl(self.tc)

        return b

    def UpdateSkin(self):
        s = lambda name, d = None: skin.get('%sFormattingBar.%s' % ('AppDefaults.' if self.UseAppDefaultSkin else '',name),d)

        iconsize = self.iconsize = s('iconsize')

        icons = s('icons').get

        bicon   = self.bicon   =             icons('bold').Resized(iconsize)
        iicon   = self.iicon   =           icons('italic').Resized(iconsize)
        uicon   = self.uicon   =        icons('underline').Resized(iconsize)
        fcicon  = self.fcicon  =  icons('foregroundcolor').Resized(iconsize)
        bcicon  = self.bcicon  =  icons('backgroundcolor').Resized(iconsize)
        eicon   = self.eicon   =            icons('emote').Resized(iconsize)

        fontddwidth = self.fontddwidth = s('FontDropDownWidth')
        sizeddwidth = self.sizeddwidth = s('SizeDropDownWidth')
        menuskin    = self.menuskin    = s('MenuSkin', None)
        toolbarskin = self.toolbarskin = s('toolbarskin', None)

        if self.initover and getattr(self, 'formatbar', None) is not None:
            self.formatbar.SetSkinKey(toolbarskin)
            # new font drop down does not have SetMinWidth
            # self.fontdd.SetMinWidth(fontddwidth)
            self.bsize.SetStaticWidth(sizeddwidth)
            self.msize.SetWidth(sizeddwidth)
            self.bbold.SetIcon(bicon)
            self.bitalic.SetIcon(iicon)
            self.bunderline.SetIcon(uicon)
            self.bcolor.SetIcon(fcicon)
            self.bbgcolor.SetIcon(bcicon)
            self.bemote.SetIcon(eicon)

        wx.CallAfter(self.Layout)

    def __repr__(self):
        try:
            return '<%s under %r>' % (self.__class__.__name__, self.Parent)
        except Exception:
            return object.__repr__(self)

    def SetSingleFormat(self, singleformat = True):
        '''
        Sets this control to only have one style for all of its contents. The
        style used is the first one found.
        '''

        return

    def GetSingleFormat(self):
        return True#self.single_format

    SingleFormat = property(GetSingleFormat, SetSingleFormat, None, doc = SetSingleFormat.__doc__)

    def SaveStyle(self):
        style = self.GetStyleAsDict()
        from pprint import pformat
        print 'saving style:\n%s' % pformat(style)
        setpref('profile.formatting' if self.aimprofile else 'messaging.default_style',style)

    def GetStyleAsDict(self):

        tc = self.tc

        return dict(BackgroundColour = tuple(tc.BackgroundColour),
                     TextColour = tuple(tc.ForegroundColour),
                     Font = font_to_tuple(tc.Font))

    def WhenDefaultLayoutChange(self,src,pref,old,new):

#        if self.GetStyleAsDict() == old:
        self.LoadStyle()

    def LoadStyle(self):
        self.SetFormat(*load_pref_style('profile.formatting' if self.aimprofile else 'messaging.default_style'))


    def SetFormatStorage(self, format_storage):
        return self.SetFormat(*font_attrs_from_storage(format_storage))

    def SetFormat(self, font, fgc, bgc):
        tc = self.tc
        tc.Font = font
        tc.SetFont(font)
        tc.ForegroundColour = wx.BLACK
        tc.ForegroundColour = wx.Color(*fgc)
        tc.BackgroundColour = wx.Color(*bgc)
        self.UpdateDisplay()

    def ShowFormattingBar(self, val):
        'Shows or hides the formatting bar.'

        if val and self.formatbar is None:
            self.construct_formatting_bar()

        if self.formatbar is not None:
            self.csizer.Show(self.formatbar, val, True)
            self.Layout()

        if self.autosize:
            self.expandEvent()

    def FormattingBarIsShown(self):
        if getattr(self, 'formatbar', None) is not None:
            return self.formatbar.IsShown()

        return False

    def EnableFormattingButtons(self, enable):
        self.formatting_enabled = enable

        if hasattr(self, 'fontdd'): # the formatting bar might not have been constructed yet
            self.fontdd.Enable(enable)
            self.bsize.Enable(enable)
            self.bbold.Enable(enable)
            self.bitalic.Enable(enable)
            self.bunderline.Enable(enable)
            self.bcolor.Enable(enable)
            self.bbgcolor.Enable(enable)

    def ShowButtons(self,
                 font = True,
                 pointsize = True,
                 bold = True,
                 italic = True,
                 underline = True,
                 textcolor = True,
                 bgcolor = True):

        'Show or hide each button on the format bar.'

        self.fontdd.Show(font)
        self.bsize.Show(pointsize)
        self.bbold.Show(bold)
        self.bitalic.Show(italic)
        self.bunderline.Show(underline)
        self.bcolor.Show(textcolor)
        self.bbgcolor.Show(bgcolor)
        #self.bemote.Show(not self.prefmode)

    def GetStyleAsStorage(self):
        tc = self.tc
        font = tc.Font

        return Storage(
            backgroundcolor = tuple(tc.BackgroundColour),
            foregroundcolor = tuple(tc.ForegroundColour),
            family = FamilyNameFromFont(font),
            face = font.FaceName,
            size = font.PointSize,
            underline = font.Underlined,
            bold = font.Weight == wx.BOLD,
            italic = font.Style == wx.ITALIC)


    @property
    def Format(self):
        return self.GetStyleAsStorage()


    def GetHTML(self):
        'Does the necessary steps to convert the textfield value into HTML.'

        s = ''
        value = self.tc.Value

        s =  cgi.escape(value)

        s = s.replace('\n','<br>').replace('\t','&nbsp;&nbsp;')
        return s

    HTML = property(GetHTML)

    def GetValue(self):
        'Returns the string in the textfield.'

        return self.tc.GetValue()

    def SetValue(self, value):
        'Sets the string in the textfield.'

        return self.tc.SetValue(value)

    Value = property(GetValue, SetValue)

    @property
    def FontData(self):
        d = wx.FontData()
        tc = self.tc
        d.SetInitialFont(tc.GetFont())
        d.SetColour(tc.GetForegroundColour())
        return d

    def ShowModalFontDialog(self, e = None):
        '''
        Uses the native Mac font dialog to allow the user to select a font
        and a color.
        '''
        diag = wx.FontDialog(self, self.FontData)
        if wx.ID_OK == diag.ShowModal():
            font_data = diag.GetFontData()
            font = font_data.GetChosenFont()
            color = font_data.GetColour()

            tc = self.tc
            if color.IsOk():
                tc.ForegroundColour = color
            if font.IsOk():
                tc.Font = font

            if self.prefmode or self.aimprofile:
                wx.CallAfter(self.SaveStyle)

            tc.Refresh()
            tc.SetFocus()

    def CreateFontButton(self, parent, label = _('Set Font...')):
        '''
        Create a small button that will spawn a font dialog for setting
        the properties of this text control.
        '''
        font_button = wx.Button(parent, -1, label)
        font_button.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
        font_button.Bind(wx.EVT_BUTTON, self.ShowModalFontDialog)
        return font_button

    def OnColor(self, event = None):
        "Calls the color chooser for setting font color."

        newcolor = wx.GetColourFromUser(self, self.tc.ForegroundColour, _('Choose a foreground color'))
        self.update_color(newcolor)

    def update_color(self, newcolor):

        if newcolor.IsOk():
            self.tc.ForegroundColour = newcolor

        if self.prefmode or self.aimprofile:
            wx.CallAfter(self.SaveStyle)

        #Insure the focus goes back to the TextField
        self.tc.Refresh()
        self.tc.SetFocus()


    def OnBGColor(self,event):
        'Calls the color chooser for setting font background color.'

        newcolor = wx.GetColourFromUser(self, self.tc.BackgroundColour, _('Choose a background color'))
        self.update_bgcolor(newcolor)

    def update_bgcolor(self, newcolor):

        if newcolor.IsOk():
            self.tc.BackgroundColour = newcolor


        if self.prefmode or self.aimprofile:
            wx.CallAfter(self.SaveStyle)

        self.tc.Refresh()
        self.tc.SetFocus()


    def ApplyStyleGUIless(self, flag=None):
        tc = self.tc

        font     = tc.GetFont()
        fontsize = font.GetPointSize()

        weight = font.Weight == wx.FONTWEIGHT_BOLD
        style = font.Style == wx.FONTSTYLE_ITALIC
        underline = font.Underlined

        if flag == 'bold': weight = not weight
        if flag == 'italic': style = not style
        if flag == 'underline':  underline = not underline

        font= CopyFont(font,
                       pointSize = fontsize,
                       style     = wx.ITALIC if style else wx.NORMAL,
                       weight    = wx.FONTWEIGHT_BOLD if weight else wx.NORMAL,
                       underline = underline)

        # setting the font twices fixes a bug.
        tc.Font = font
        tc.SetFont(font)

        fgc = tc.ForegroundColour
        tc.ForegroundColour = wx.BLACK
        tc.ForegroundColour = fgc

        if self.prefmode or self.aimprofile:
            wx.CallAfter(self.SaveStyle)

        self.tc.SetFocus()

    shiftToSend = prefprop("messaging.shift_to_send", False)

    def OnKey(self, event):
        """
            This detects key presses, runs entercallback if enter or return is pressed
            Any other key continues as normal, then refreshes the font and size info
        """
        keycode = event.KeyCode


        shiftToSend = self.shiftToSend
        hasModifiers = event.HasModifiers()
        shiftIsDown = event.Modifiers == wx.MOD_SHIFT
        ctrlIsDown = event.Modifiers == wx.MOD_CONTROL

        if keycode in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
            # if there is a enter callback and no modifiers are down or if
            # shift should send and shift is down, call the callback
            if self.entercallback and \
                    (not (shiftToSend or hasModifiers or shiftIsDown) or \
                     (shiftToSend and shiftIsDown)):
                return self.entercallback(self)
            else:
                event.Skip()

        if not ctrlIsDown:
            return event.Skip()

        if wxMSW:
            # make Ctrl+R and Ctrl+L modify the RTL setting of the rich edit
            # control, not just the alignment.
            from gui.toolbox import set_rich_layoutdirection
            if keycode == ord('R'):
                set_rich_layoutdirection(self.tc, wx.Layout_RightToLeft)
                return
            elif keycode == ord('L'):
                set_rich_layoutdirection(self.tc, wx.Layout_LeftToRight)
                return

        if keycode == ord('B'):
            if hasattr(self, 'bbold'):
                self.bbold.Active()
                self.ApplyStyle()
            else:
                self.ApplyStyleGUIless('bold')
            return

        elif keycode == ord('I'):
            if hasattr(self, 'bitalic'):
                self.bitalic.Active()
                self.ApplyStyle()
            else:
                self.ApplyStyleGUIless('italic')
            return

        elif keycode == ord('U'):
            if hasattr(self, 'bunderline'):
                self.bunderline.Active()
                self.ApplyStyle()
            else:
                self.ApplyStyleGUIless('underline')
            return

        event.Skip()

    def Clear(self):
        """
            Clears the text from the text field
        """
        tc = self.tc

        if 'wxMSW' in wx.PlatformInfo:
            # Clear() removes any alignment flags that are set in the text control, so
            # reset them
            alignment = cgui.GetRichEditParagraphAlignment(tc)
            tc.Clear()
            if cgui.GetRichEditParagraphAlignment(tc) != alignment:
                cgui.SetRichEditParagraphAlignment(tc, alignment)
        else:
            tc.Clear()

    def UpdateDisplay(self):
        """
            Update the values of Font and Size buttons and the states of
            the bold, italic, and underline buttons based on selection or
            cursor posirion
        """
        with self.tc.Frozen():

            tc = self.tc

            font     = tc.GetFont()
            fontface = font.GetFaceName()
            fontsize = font.GetPointSize()


            bold = font.Weight == wx.FONTWEIGHT_BOLD
            italic = font.Style == wx.FONTSTYLE_ITALIC
            underline = font.Underlined

            # Set Bold, Italic, and Underline buttons
            if self.formatbar is not None:
                self.bbold.Active(bold)
                self.bitalic.Active(italic)
                self.bunderline.Active(underline)

                i = self.fontdd.FindString(fontface, False)
                self.fontdd.SetSelection(i)

                self.bsize.SetLabel(str(fontsize))
                self.bsize.Refresh()

    def ToggleBold(self, event):
        """
            Toggles the bold value of the selection
            or calls Applystyle in no selection
        """
        self.ApplyStyle()

    def ToggleItalic(self, event):
        """
            Toggles the italics state of the selection
            or calls Applystyle in no selection
        """
        self.ApplyStyle()

    def ToggleUnderline(self,event):
        'Toggles underline state of selection or calls Applystyle in no selection.'

        self.ApplyStyle()

    def ApplyStyle(self):
        'Sets the style at the cursor.'

        tc = self.tc

        style     = self.bitalic.active
        weight    = self.bbold.active
        underline = self.bunderline.active

        font= CopyFont(self.fontdd.GetClientData(self.fontdd.GetSelection()),
                       pointSize = int(self.bsize.label),
                       style     = wx.ITALIC if style else wx.NORMAL,
                       weight    = wx.FONTWEIGHT_BOLD if weight else wx.NORMAL,
                       underline = underline)

        tc.Font = font
        tc.SetFont(font)

        fgc = tc.ForegroundColour
        tc.ForegroundColour = wx.BLACK
        tc.ForegroundColour=fgc

        if self.prefmode or self.aimprofile:
            wx.CallAfter(self.SaveStyle)

        self.tc.SetFocus()

    def OnFontSelect(self, event):
        """
            Updates the button to the new font and applies it to the selection
            or calls ApplyStlye
        """
        self.ApplyStyle()

    def OnSizeMenu(self,item):
        """
            Updates the Size button to the new size and apllies it to the selection
            or calls ApplyStyle
        """
        self.bsize.SetLabel(item.content[0])

        self.ApplyStyle()

    def SetSizes(self, sizes = DEFAULT_SIZES):
        """
            Sets the list of selectable sizes
            If not set sizes default to ['8', '10', '12', '14', '18', '24', '36']
        """
        return [SimpleMenuItem([size], method = self.OnSizeMenu) for size in sizes]
示例#12
0
文件: imtabs.py 项目: sgricci/digsby
class ImFrame(wx.Frame):
    '''
    The frame around conversation tabs.
    '''

    WindowName = u'IM Window'

    def __init__(self, pos=None, size=None, startMinimized=False, posId=''):
        if pref('imwin.ads', type=bool, default=False):
            defaultSize = wx.Size(490, 470)
        else:
            defaultSize = wx.Size(470, 390)

        wininfo, placement = preLoadWindowPos(ImFrame.WindowName,
                                              uniqueId=posId,
                                              defaultPos=wx.Point(200, 200),
                                              defaultSize=defaultSize)
        wininfo['style'] |= wx.DEFAULT_FRAME_STYLE

        setPos = pos is not None
        setSize = size is not None

        if setPos or setSize:
            wininfo['style'] &= ~wx.MAXIMIZE

        if startMinimized:
            wininfo['style'] |= wx.ICONIZE
            self._starting_minimized = True  # see comment in imhub.py's frame_show function

        wininfo['style'] |= wx.FULL_REPAINT_ON_RESIZE

        wx.Frame.__init__(self,
                          parent=None,
                          name=ImFrame.WindowName,
                          **wininfo)

        self.on_engaged_start = Delegate()
        self.on_engaged_end = Delegate()
        self.on_sent_message = Delegate()

        # FIXME: Currently the IM window appearance is set by a load-time switch, as I want to first test
        # to ensure altering appearance.skin for Mac doesn't have other side-effects.
        if config.nativeIMWindow:
            import gui.imwin.imwin_native
            self.notebookPanel = gui.imwin.imwin_native.NativeNotebookPanel(
                self, -1)
            self.eventHandler = gui.imwin.imwin_native.NativeIMFrameEventHandler(
                self)
        else:
            preview = None
            if config.platform == 'win':
                preview = ImTabController
            self.notebookPanel = SkinnedNotebookPanel(self,
                                                      -1,
                                                      preview=preview)
            self.eventHandler = SkinnedIMFrameEventHandler(self)

        from gui.imwin.imwin_ads import construct_ad_panel
        ad_panel = construct_ad_panel(self, self.notebookPanel,
                                      self.notebookPanel.notebook.did_add)
        self.notebook.winman = TabWindowManager(
            lambda pos, size=None: ImFrame(pos=pos, size=size))

        if placement is not None and not (setPos or setSize):
            with traceguard:
                from gui.toolbox import SetWindowPlacement
                SetWindowPlacement(self, placement)

        if setPos:
            self.Position = pos
        if setSize:
            self.Size = size

        if not config.nativeIMWindow:
            self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self.posId = posId

        self.EnsureInScreen()
        if pos is not None:
            wx.CallAfter(self.EnsureNotStacked)

        # obey always on top and snapping prefs
        profile.prefs.link(IMWIN_ALWAYS_ON_TOP_PREF,
                           self.on_always_on_top,
                           callnow=not startMinimized)
        snap_pref(self)

        self.iconizecallbacks = set()

        Bind = self.Bind
        Bind(wx.EVT_ICONIZE, self.OnIconize)

        memory_event()

        def gainfocus(e):
            if e.Active:
                self._startengage()
            else:
                self._endengage()

            e.Skip()

        Bind(wx.EVT_ACTIVATE, gainfocus)

        self.register_hooks()

    def register_hooks(self):
        if getattr(ImFrame, 'did_register_hooks', False):
            return

        ImFrame.did_register_hooks = True
        ImFrame.engage_is_idle = False

        def goidle():
            ImFrame.engage_is_idle = True
            for win in all_imframes():
                win._endengage()
            return True

        def unidle():
            ImFrame.engage_is_idle = False
            for win in all_imframes():
                if win.IsActive():
                    win._startengage()
            return True

        def idle_ms(ms_idle_time):
            if not ImFrame.engage_is_idle and ms_idle_time > IMWIN_STAT_IDLE_TIME:
                goidle()
            elif ImFrame.engage_is_idle and ms_idle_time < IMWIN_STAT_IDLE_TIME:
                unidle()
            return 15 * 1000  #request check in 15 second intervals

        hooks.register('digsby.app.idle', idle_ms)

        # win7 taskbar hook
        import cgui
        if config.platform == 'win' and cgui.isWin7OrHigher():
            from gui.native.win.taskbar import set_overlay_icon, get_tab_notebook

            def icon_updated(imwin):
                get_tab_notebook(imwin.Top).InvalidateThumbnails(imwin)

                def later():
                    if wx.IsDestroyed(imwin): return
                    set_overlay_icon(im_badge(), tlw=imwin.Top)

                wx.CallLater(300, later)

            hooks.register('digsby.overlay_icon_updated', icon_updated)

    def _startengage(self):
        self._endengage()
        #CAS: crossplatform?
        self.start_time = default_timer()
        self.on_engaged_start()

    def _endengage(self):
        if hasattr(self, 'start_time'):
            diff = max(int(default_timer() - self.start_time), 0)
            del self.start_time
            hooks.notify('digsby.statistics.imwin.imwin_engage', diff)
            self.on_engaged_end()

    cycleTitle = prefprop('conversation_window.cycle_unread_ims')
    shouldFlash = prefprop('conversation_window.notify_flash')

    def OnIconize(self, event=None):
        self._didoniconize = True
        if event is not None:
            event.Skip()

        if event is not None and not event.Iconized(
        ) and not self.OnTop and pref(IMWIN_ALWAYS_ON_TOP_PREF, False):
            self.OnTop = True

        for callback in set(self.iconizecallbacks):
            try:
                callback()
            except PyDeadObjectError:
                self.iconizecallbacks.remove(callback)

    @property
    def notebook(self):
        if hasattr(self, "notebookPanel") and self.notebookPanel:
            return self.notebookPanel.notebook

        return None

    @property
    def AnyNotified(self):
        return any(iwin.Tab.notified for iwin in self)

    # FIXME: These iter methods aren't compatible with the native IM Window, and IMHO if we use
    # them somewhere we should replace them with a ImFrame.Tabs accessor rather than making the
    # frame itself an iterator for its tabs.
    def __iter__(self):
        if config.nativeIMWindow:
            assert True  # just assert so we know where we hit this from
        return iter(p.Content for p in self.notebook.Pages())

    def __getitem__(self, n):
        if config.nativeIMWindow:
            assert True  # just assert so we know where we hit this from
        return self.notebook.Pages()[n].Content

    def GetTabCount(self):
        return self.notebook.GetTabCount()

    def AddTab(self, ctrl, focus=None):
        return self.notebook.Add(ctrl, focus=focus)

    @bind('ImFrame.Tabs.CloseIfNotLast')
    def CloseTabIfNotLast(self):
        if self.notebook.GetTabCount() > 1:
            tab = self.notebook.ActiveTab
            self.notebook.CloseTab(tab)

    @bind('ImFrame.Tabs.CloseActive')
    def CloseActiveTab(self):
        tab = self.notebook.ActiveTab
        self.notebook.CloseTab(tab)

        if self.notebook.GetTabCount() < 1:
            self.Close()

    @bind('ImFrame.Tabs.NextTab')
    def NextTab(self):
        self.notebook.NextTab()

    @bind('ImFrame.Tabs.PrevTab')
    def PrevTab(self):
        self.notebook.PrevTab()

    @bind('ImFrame.ChatWindow.IncreaseTextSize')
    def IncreaseTextSize(self):
        self.ActiveMessageArea.IncreaseTextSize()

    @bind('ImFrame.ChatWindow.DecreaseTextSize')
    def DescreaseTextSize(self):
        self.ActiveMessageArea.DecreaseTextSize()

    @bind('ImFrame.ChatWindow.ResetTextSize')
    def ResetTextSize(self):
        self.ActiveMessageArea.ResetTextSize()

    @property
    def ActiveMessageArea(self):
        # TODO: no.
        return self.notebook.ActiveTab.page.Content.message_area

    def CloseAndSaveState(self, e):
        # Confirm multiple tab close
        tabcount = self.GetTabCount()

        if e.CanVeto() and tabcount > 1 and pref(WARN_PREF, True):
            with CloseTabsDialog(self, tabcount, True) as diag:
                diag.CenterOnParent()
                res = diag.ShowModal()

                if not diag.WarnMe:
                    setpref(WARN_PREF, False)

                if res == wx.ID_CANCEL:
                    return False

        self.Hide()
        saveWindowPos(self, uniqueId=self.posId)  # Save our window position

        # Call each IMWin's on_close method.
        if not config.nativeIMWindow:
            for page in self.notebook.Pages():
                page.Children[0].on_close()

        memory_event()
        return True

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, id(self))

    def on_always_on_top(self, val):
        'Invoked when "conversation_window.always_on_top" preference changes.'

        self.OnTop = val

    def UpdateSkin(self):
        wx.CallAfter(self.Layout)
示例#13
0
文件: imtabs.py 项目: sgricci/digsby
class SkinnedIMFrameEventHandler(wx.EvtHandler):
    '''
    A generic frame that can hold tabs. Sets up the TabManager and WinManager for uberbook

    TODO: move this logic/functionality to uberbook.py where it belongs
    '''
    def __init__(self, frame):
        wx.EvtHandler.__init__(self)

        self.frame = frame
        self.notebook = self.frame.notebookPanel.notebook

        self.mergetimer = None
        self.notifiedtabs = oset()
        self.titletimer = TitleBarTimer(self.frame, self.notifiedtabs)

        self.BindEventsToFrame()

    def BindEventsToFrame(self):
        Bind = self.frame.Bind
        Bind(wx.EVT_CLOSE, self.OnClose)
        Bind(wx.EVT_MOVE, self.OnMove)
        Bind(wx.EVT_SIZE, self.OnSize)
        Bind(wx.EVT_ACTIVATE, self.OnActivate)
        Bind(EVT_TAB_NOTIFIED, self.OnTabNotify)

        publisher = pubsub.Publisher()
        publisher.subscribe(self.OnPageTitleUpdated, 'tab.title.updated')
        publisher.subscribe(self.OnPageIconUpdated, 'tab.icon.updated')

    def OnClose(self, event):
        if self.frame.CloseAndSaveState(event):
            self.frame.Destroy()
        else:
            event.Veto()

    def OnActivate(self, e):
        # when window activates, focus the IM input area
        e.Skip()
        if e.GetActive():
            tab = self.notebook.ActiveTab
            if tab is not None:
                # ensure the active tab becomes unnotified
                tab.SetNotify(False)

                # focus the input area
                tab.page.Content.FocusTextCtrl()

        if self.titletimer.IsRunning():
            self.titletimer.Stop()

    def OnPageIconUpdated(self, message):
        """
        Update the notebook when a convo's icon changes.
        """

        page, icon = message.data
        if not self.frame or wx.IsDestroyed(self.frame):
            return

        assert getattr(self.frame.notebook, "_name", "") != "[unknown]"
        for mypage in self.frame.notebook.Pages():
            # TODO: on windows, this comparison will never be True because
            # we are comparing Pages and ImWinPanels.
            if mypage == page and self.frame.notebook.ActiveTab == mypage.tab:
                self.frame.SetFrameIcon(icon)

    def OnPageTitleUpdated(self, message):
        """
        Update the frame and notebook when a convo's name and/or typing status changes
        """
        if not self.frame or wx.IsDestroyed(self.frame):
            return

        imwin, title, window_title = message.data

        imwin.SetName(title)

        assert getattr(self.frame.notebook, "_name", "") != "[unknown]"

        page = None
        for mypage in self.frame.notebook.Pages():
            if mypage.Content is imwin:
                page = mypage

        if page is None or page.tab is not self.frame.notebook.ActiveTab:
            return

        if window_title is not None:
            frame_title = window_title
        else:
            frame_title = title

        if self.titletimer.IsRunning():
            self.titletimer.title = frame_title
        else:
            self.frame.SetTitle(frame_title)

    flashTime = prefprop('conversation_window.flash_time')
    flashCount = prefprop('conversation_window.flash_count')

    def OnTabNotify(self, event):
        tab = event.tab

        if tab.notified:
            self.notifiedtabs.add(tab)
            notify_unread_message_hook()
        elif tab in self.notifiedtabs:
            self.notifiedtabs.remove(tab)
            notify_unread_message_hook()
            return
        else:
            return

        if not self.frame.cycleTitle:
            if self.frame.shouldFlash:
                Flash(self.frame,
                      timeout=self.flashTime,
                      count=self.flashCount)
            return

        if len(self.notifiedtabs) and not self.frame.IsActive():
            if not self.titletimer.IsRunning():
                self.titletimer.Start()
        elif self.titletimer.IsRunning():
            self.titletimer.Stop()

    def OnMove(self, event):
        event.Skip(True)

        # check if we need to move to a window underneath
        if pref('messaging.tabs.enabled', True):

            mt = self.mergetimer
            if mt is None:
                self.mergetimer = wx.CallLater(10,
                                               self.notebook.StartWindowDrag)
            else:
                mt.Start(10)

        event.Skip(True)

    def OnSize(self, event):
        mt = self.mergetimer
        if mt is not None:
            # cancel the move check if we're resizing the top left corner
            mt.Stop()

        event.Skip(True)
示例#14
0
文件: umenu.py 项目: sgricci/digsby
class MenuListBox(SkinVListBox):
    'VListBox acting as a view for a wxMenu.'

    def __init__(self, parent, menu):
        SkinVListBox.__init__(self,
                              parent,
                              style=wx.NO_BORDER | wx.FULL_REPAINT_ON_RESIZE
                              | wx.WANTS_CHARS)

        self.menu = menu
        self.UpdateSkin()

        self.timer = wx.PyTimer(self._on_submenu_timer)

        Bind = self.Bind
        Bind(wx.EVT_MOUSE_EVENTS, self._mouseevents)
        Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._capturechanged)
        Bind(wx.EVT_LISTBOX, self._listbox)
        Bind(wx.EVT_KEY_DOWN, self._keydown)

        self.mouseCallbacks = {
            wx.wxEVT_MOTION: self._motion,
            wx.wxEVT_RIGHT_DOWN: self._rdown,
            wx.wxEVT_LEFT_DOWN: self._ldown,
            wx.wxEVT_LEFT_UP: self._lup
        }

        MenuListBox._lastvlist = ref(self)

    def reassign(self, menu):
        self.menu = menu

    menuOpenDelayMs = prefprop('menus.submenu_delay', 250)

    def __repr__(self):
        return '<%s for %r>' % (self.__class__.__name__, self.menu)

    @property
    def ParentPopup(self):
        pmenu = self.menu._parentmenu
        return None if pmenu is None else pmenu.popup.vlist

    def CalcSize(self):
        'Calculates the width and height of this menu.'

        self.SetItemCount(len(self.menu))

        height = 0
        dc = wx.MemoryDC()
        dc.Font = self.font
        s = self.skin
        padx = self.padding[0]
        iconsize = s.iconsize
        subw = s.submenuicon.Width
        sepheight = s.separatorimage.Size.height
        itemheight = self.itemheight
        textExtent = dc.GetTextExtent

        labelw = accelw = 0

        for item in self.menu:
            if item.Kind == ITEM_SEPARATOR:
                height += sepheight
            else:
                height += itemheight

                # keep the heights for the widest label and accelerator
                labelw = max(labelw, textExtent(item.Label)[0])
                accelw = max(accelw, textExtent(item.AccelText)[0])

        # store an x coordinate for where to draw the accelerator
        self.accelColumnX = padx + iconsize + padx + padx + labelw + padx

        # sum those biggest widths with the other elements to come up with a total width
        width = self.accelColumnX + padx + max(accelw, subw) + padx
        self.MinSize = self.Size = (wx.Size(width, height))

    def OnDrawItem(self, dc, rect, n):
        'Invoked by VListBox to draw one menu item'

        item = self.menu[n]
        kind = item.Kind
        s = self.skin
        iconsize = s.iconsize
        submenuicon = s.submenuicon
        padx = self.padding.x
        selected = self.IsSelected(n)

        drawbitmap, drawlabel = dc.DrawBitmap, dc.DrawLabel

        if kind == ITEM_SEPARATOR:
            s.separatorimage.Draw(dc, rect, n)
        else:
            dc.Font = self.font

            if not item.IsEnabled(): fg = 'disabled'
            elif selected: fg = 'selection'
            else: fg = 'normal'
            dc.TextForeground = getattr(s.fontcolors, fg)

            grect = Rect(*rect)
            grect.width = padx + iconsize + padx

            # icon bitmap
            bmap = item.Bitmap
            if bmap and bmap.Ok():
                bmap = bmap.ResizedSmaller(iconsize)
                drawbitmap(bmap, grect.HCenter(bmap), rect.VCenter(bmap), True)

            # checks and radio circles
            if item.IsCheckable() and item.IsChecked():
                # if there is a menu icon, show the check in the bottom right
                # otherwise center it
                if bmap: checkx = grect.Right - s.checkedicon.Width
                else: checkx = grect.HCenter(s.checkedicon)

                if kind == ITEM_CHECK:
                    drawbitmap(s.checkedicon, checkx,
                               rect.VCenter(s.checkedicon), True)
                elif kind == ITEM_RADIO:
                    drawbitmap(s.checkedicon, checkx,
                               rect.VCenter(s.checkedicon), True)

            # main label
            rect.Subtract(left=iconsize + 3 * padx)
            drawlabel(item.Label,
                      rect,
                      indexAccel=item.Text.split('\t')[0].find('&'),
                      alignment=ALIGN_CENTER_VERTICAL)

            # submenu icon
            rect.Subtract(right=submenuicon.Width + padx)
            if item.SubMenu is not None:
                drawbitmap(submenuicon, rect.Right, rect.VCenter(submenuicon),
                           True)

            # accelerator text
            acceltext = item.AccelText
            if acceltext:
                rect.x = self.accelColumnX + padx
                drawlabel(acceltext, rect, alignment=ALIGN_CENTER_VERTICAL)

    def OnDrawBackground(self, dc, rect, n):
        s = self.skin
        bgs = s.backgrounds

        bgname = 'selection' if self.menu[
            n].Kind != ITEM_SEPARATOR and self.IsSelected(n) else 'item'
        bg = getattr(bgs, bgname, None)
        if bg: bg.Draw(dc, rect, n)

    def PaintMoreBackground(self, dc, rect):
        'Invoked by SkinnedVList, used to draw the gutter.'

        # draw a gutter down the length of the menu
        g = self.skin.backgrounds.gutter
        if g:
            g.Draw(
                dc,
                Rect(rect.x, rect.y, self.skin.iconsize + self.padding.x * 2,
                     rect.height))

    def OnMeasureItem(self, n):
        item = self.menu[n]
        kind = item.Kind

        if kind == ITEM_SEPARATOR:
            return self.sepheight
        else:
            return self.itemheight

    def OnPopup(self):
        parentless = self.TopMenu == self.menu

        if parentless:
            # call ReleaseCapture here on windows to prevent the control that
            # spawned this menu from keeping capture--another right click
            # should open a new menu, not the same menu (see #1995)
            if wxMSW:
                ReleaseCapture_win32()

            if self.menu.Windowless:
                if not hasattr(self, 'traytimer'):
                    self.traytimer = UMenuTrayTimer(self)
                self.traytimer.Start(50)

        if not self.menu._parentmenu:
            self._grabkeyboard()

        if wx.LeftDown():
            self._leftbuttondown = True

        if not self.HasCapture():
            self.CaptureMouse()
        self.SetCursor(StockCursor(CURSOR_DEFAULT))
        self.SetFocus()

    def Dismiss(self):

        if hasattr(self, 'traytimer'):
            self.traytimer.Stop()

        if self.menu._childmenu:
            self.menu._childmenu.Dismiss()
            self.menu._childmenu = None

        while self.HasCapture():
            self.ReleaseMouse()

        self.Parent.Hide()

        m = self.menu
        if m._parentmenu is None:

            if hasattr(self, 'focusHandler'):
                self.focusHandler.close()
                del self.focusHandler

            wx.CallAfter(self.menu.OnDismiss)

        else:
            m._parentmenu._childmenu = None

    def DismissRoot(self):
        self.TopMenu.Dismiss()

    @property
    def TopMenu(self):
        m = self.menu
        while m._parentmenu is not None:
            m = m._parentmenu

        return m

    def UpdateSkin(self):
        self.SetMargins(wx.Point(0, 0))
        s = self.skin = self.menu.skin
        self.sepheight = s.separatorimage.Size.height

        try:
            self.font = s.font
        except KeyError:
            self.font = default_font()

        self.fontheight = s.font.Height

        try:
            self.padding = s.padding
            assert isinstance(self.padding.x, int) and isinstance(
                self.padding.y, int)
        except Exception:
            self.padding = wx.Point(3, 3)

        self.itemheight = int(self.fontheight + self.padding.y * 2)

        self.Background = s.backgrounds.menu

    @property
    def Window(self):
        return self.menu.Window

    def _grabkeyboard(self):
        if 'wxMSW' in wx.PlatformInfo:
            f = wx.Window.FindFocus()
        elif "wxGTK" in wx.Platform:
            f = self
        else:
            f = None
        if f:
            self.focusHandler = FocusHandler(self, f)

    def _showsubmenu(self, i, highlight=False):
        item = self.menu[i]
        submenu = item.SubMenu
        child = self.menu._childmenu

        if child is submenu:  # early exit if menu is already open
            return

        if child is not None:  # dismiss the current child submenu if there is one
            if child: child.Dismiss()
            self.menu._childmenu = None

        # open the new submenu if there is one
        if i != -1 and submenu is not None:
            r = self.ClientRect
            r.Y = self.GetItemY(i)
            r.Height = self.OnMeasureItem(i)
            self.menu._childmenu = submenu
            submenu._parentmenu = self.menu
            submenu._parentindex = i
            submenu.PopupMenu(r.ToScreen(self), submenu=True)
            if highlight: submenu.popup.vlist.Selection = 0

    def _on_submenu_timer(self):
        'Invoked after a certain duration of mouse hover time over a menu item.'

        i = self.Selection
        if i != -1 and self.IsShown() and FindWindowAtPointer() is self:
            self._showsubmenu(i)

    def _listbox(self, e):
        '''
        Invoked on listbox selection (i.e., the mouse moving up or down the
        list.
        '''

        self.timer.Start(self.menuOpenDelayMs, True)

    def _emit_menuevent(self, id, type=wx.wxEVT_COMMAND_MENU_SELECTED):
        event = wx.CommandEvent(type, id)
        self.menu.Handler.AddPendingEvent(event)

    # mouse handling

    def _mouseevents(self,
                     e,
                     wxEVT_MOTION=wxEVT_MOTION,
                     FindWindowAtPoint=FindWindowAtPoint,
                     UberButton=UberButton):
        rect = self.ClientRect
        pt_original = pt = e.Position

        if not rect.Contains(pt):
            menu = self.ParentPopup

            # forward mouse events to Parent, since we're capturing the mouse
            oldmenu = self
            while menu:
                pt = menu.ScreenToClient(oldmenu.ClientToScreen(pt))

                if menu.ClientRect.Contains(pt):
                    e.m_x, e.m_y = pt
                    return menu._mouseevents(e)

                oldmenu, menu = menu, menu.ParentPopup

            # when mousing over a menubar, we need to trigger other buttons
            try:
                button = self.TopMenu._button
            except AttributeError:
                # not in a menubar
                pass
            else:
                if e.GetEventType() == wxEVT_MOTION:
                    ctrl = FindWindowAtPoint(self.ClientToScreen(e.Position))
                    if ctrl is not None:
                        if getattr(self, '_motionswitch', -1) is ctrl:
                            # use _motionswitch to make sure the mouse actually /moves/
                            # over the button, and we're not just receiving an event for when
                            # it appears.
                            self._motionswitch = None
                            self.DismissRoot()
                            ctrl.menu._showquick = True
                            ctrl.OnLeftDown()
                        elif isinstance(
                                ctrl,
                                UberButton) and ctrl.menubarmode and hasattr(
                                    ctrl, 'menu'):
                            # check to see if the button being hovered over is for a menu
                            # on the same menubar
                            if ctrl.Parent is button.Parent and not ctrl is button:
                                self._motionswitch = ctrl

        e.m_x, e.m_y = pt_original
        try:
            cb = self.mouseCallbacks[e.EventType]
        except KeyError:
            pass
        else:
            cb(e)

    def _motion(self, e):
        # changes selection as mouse moves over the list
        p = e.Position
        i = self.HitTest(p) if self.ClientRect.Contains(p) else -1
        s = self.Selection

        if i != s:
            # if we're a submenu and our parent's selection doesn't point to us,
            # make it so
            p = self.ParentPopup
            if p is not None:
                pi = getattr(self.menu, '_parentindex', None)
                if pi is not None and p.Selection != pi:
                    p.Selection = pi

            self.SetSelection(i)
            self._emit_lbox_selection(i)

    @property
    def LeafMenu(self):
        s = self.menu

        while s._childmenu:
            s = s._childmenu

        return s

    def _keydown(self, e):
        # keys always go to the deepest child menu
        self = self.LeafMenu.popup.vlist

        code = e.KeyCode
        i, j, m = self.Selection, -1, self.menu

        if code == wx.WXK_DOWN:
            j = (i + 1) % len(m)
            while j != i and m[j].Kind == wx.ITEM_SEPARATOR:
                j = (j + 1) % len(m)

        elif code == wx.WXK_UP:
            if i == -1: i = len(m)
            j = (i - 1) % len(m)
            while j != i and m[j].Kind == wx.ITEM_SEPARATOR:
                j = (j - 1) % len(m)

        elif code == wx.WXK_RETURN:
            return self._activate_item(i, submenu=True, highlight=True)

        elif code == wx.WXK_RIGHT:
            if i == -1:
                pass  # skip menus
            elif m[i].SubMenu is not None:
                self.timer.Stop()
                return self._showsubmenu(i, highlight=True)

            # see if the top level menu has a "_next" menu
            while m._parentmenu:
                m = m._parentmenu
            next = getattr(m, '_next', None)

            if next is not None:
                wx.CallAfter(self.DismissRoot)
                next._showquick = True
                wx.CallAfter(next._button.OnLeftDown)

        elif code == wx.WXK_ESCAPE:
            self.Dismiss()

        elif code == wx.WXK_LEFT:
            if m._parentmenu:
                self.Dismiss()
            else:
                prev = getattr(self.menu, '_prev', None)
                if prev is not None:
                    wx.CallAfter(self.DismissRoot)
                    prev._showquick = True
                    wx.CallAfter(prev._button.OnLeftDown)

        elif code < 256:
            self._on_char(unichr(e.UnicodeKey))

        if j != -1:
            self.SetSelection(j)

    def _on_char(self, char):
        '''
        Pressing a printable character on an opened menu...
          a) there is an underlined character--pressing the key immediately activates that item
          b) there is one item that begins with that key--it is immediately activated
          c) there are multiple items beginning with that key--pressing the key repeatedly cycles
             selection between them
        '''
        char = char.lower()
        items = []

        # Find the first shortcut key preceeded by an &
        for item in self.menu:
            amp_char = GetAmpChar(item)
            if amp_char is not None and char == amp_char.lower():
                return self._activate_item(item, submenu=True, highlight=True)

        # Instead, find all menu items whose first character begins with the
        # one we're looking for.
        items = []

        # Get a range of indexes. If there's already a selection, rotate the
        # range so that we see items underneath the selection first.
        for i in rotated(range(0, len(self.menu)), -self.Selection - 1):
            item = self.menu[i]
            label_text = item.GetItemLabelText()
            if label_text and label_text[0].lower() == char:
                items.append(i)

        if len(items) == 1:
            # only one item--activate it immediately
            self._activate_item(items[0], submenu=True, highlight=True)
        elif len(items) > 1:
            # more than one item--select the first.
            self.SetSelection(items[0])

    def _rdown(self, e):
        p, rect = e.Position, self.ClientRect

        # right mouse click outside of any menu dismisses them all
        if not rect.Contains(p): return self.DismissRoot()

    def _ldown(self, e):
        p, rect = e.Position, self.ClientRect

        # a mouse click outside of a menu causes all the menus to close from
        # the top
        if not rect.Contains(p):
            return self.DismissRoot()

        i = self.HitTest(p)

        if i != -1:
            if self.menu[i].SubMenu is not None:
                # if clicking a submenu item, show the submenu
                self.timer.Stop()
                return self._showsubmenu(i)

    def _lup(self, e):
        p = e.Position

        i = self.HitTest(e.Position)
        if self.ClientRect.Contains(p):
            self._activate_item(i)
        else:
            ctrl = FindWindowAtPointer()
            if not isinstance(ctrl, UberButton) or not ctrl.type == 'menu':
                self.DismissRoot()

    def _activate_item(self, i, submenu=False, highlight=False):
        'Triggers item i (or item at position i).'

        if isinstance(i, int):
            if i == -1: return
            item = self.menu[i]
        else:
            item = i
            i = self.menu.IndexOf(item)

        if submenu:
            if item.SubMenu is not None:
                # if clicking a submenu item, show the submenu
                self.timer.Stop()
                if not self.Selection == i:
                    self.SetSelection(i)
                return self._showsubmenu(i, highlight=highlight)

        if item.Kind != ITEM_SEPARATOR and item.IsEnabled(
        ) and item.SubMenu is None:
            # clicking anything else that's enabled and not a separator
            # emits an event.
            if item.IsCheckable(): item.Check(not item.IsChecked())
            self._emit_menuevent(item.Id)
            self.DismissRoot()

    def _capturechanged(self, e):
        # MouseCaptureChangeEvent never seems to have the captured window...so this
        # hack (check for the active window later) will have to do.

        def active():
            try:
                if self.menu.Windowless or not hasattr(self.menu.Window.Top,
                                                       'IsActive'):
                    return True  # wx.GetApp().IsActive()
                else:
                    return self.menu.Window.Top.IsActive()
            except Exception:
                print_exc()
                return True

        if not active():
            wx.CallAfter(lambda: self.DismissRoot())
示例#15
0
class SpellCheckTextCtrlMixin(object):
    def __init__(self):

        #these hold a set of information for the last time the field has been
        #spellchecked to check against to prevent redundant checks
        self.lastvalue = self.Value
        self.lastcharcount = len(self.Value)
        self.lastinsertion = self.InsertionPoint
        self.lastcurrentword = None
        self.needscheck = False
        self.lastkeycode = None
        self.lastchar = None

        self.regex_ignores = []

        self.spellcheckon = frozenset(punc + ' ')
        self.spellerrors = dict()

        Bind = self.Bind
        Bind(wx.EVT_KEY_DOWN, self.OnKey)
        Bind(wx.EVT_TEXT, self.OnText)
        Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)

        WM_PAINT = 0x000F  #Native Windows Paint Message, thanks MSDN or a source file, I don't remember
        BindWin32 = self.BindWin32
        BindWin32(WM_PAINT, self.WMPaint)

        wx.CallAfter(self.SpellCheckAll)

        link = profile.prefs.link
        link('messaging.spellcheck.enabled', self.UpdateEnablement)
        link('messaging.spellcheck.engineoptions.lang', self.UpdateEnablement)
        link('messaging.spellcheck.engineoptions.keyboard',
             self.UpdateEnablement)
        self.UpdateEnablement()

    def HasSpellingErrors(self):
        return bool(self.spellerrors)

    def AddRegexIgnore(self, regex):
        self.regex_ignores.append(regex)

    def RemoveRegexIgnore(self, regex):
        try:
            self.regex_ignores.remove(regex)
        except ValueError:
            return False
        else:
            return True

    def UpdateEnablement(self, *a):
        """
            Updates the display and spell errors when the spellcheck options are changed
        """

        #FIXME: Need a way to un-observe after and object is destroyed
        if not wx.IsDestroyed(self):
            spenabled = self.spenabled = pref('messaging.spellcheck.enabled')

            if spenabled:
                self.SpellCheckAll()
            else:
                self.spellerrors.clear()
                self.Refresh()

        else:
            log.error(
                'UpdateEnablement observer called on a SpellCheckTextCtrlMixin that has been destroyed'
            )

    def WMPaint(self, hWnd, msg, wParam, lParam):
        wx.CallAfter(self.PostPaint)

    spsize = 2  #The thickness of the line
    spstyle = wx.DOT  #The type of line used for spellcheck

    def PostPaint(self):
        """
            After the text is done painting, draw the spellcheck markup over it
        """

        try:
            # text control must implement GetReqHeight
            bottom = self.GetReqHeight()
        except AttributeError:
            return

        dc = wx.ClientDC(self)

        dc.Brush = wx.TRANSPARENT_BRUSH  #@UndefinedVariable
        dc.Pen = wx.Pen(wx.RED, self.spsize, self.spstyle)  #@UndefinedVariable

        lastline = self.GetNumberOfLines() - 1

        Point = wx.Point
        PosXY = lambda i: self.PositionToXY(i)[1:]
        XYPos = self.XYToPosition
        IndexToCoords = self.IndexToCoords
        value = self.Value

        for word in self.spellerrors:
            wordlen = len(word)
            for i in self.spellerrors[word]:

                i1 = i
                start = IndexToCoords(i)
                end = IndexToCoords(i + wordlen)

                if PosXY(i)[1] != lastline:
                    ymod = IndexToCoords(XYPos(0,
                                               PosXY(i)[1] + 1))[1] - start.y
                else:
                    ymod = (bottom - start.y) - 8 + IndexToCoords(0).y

                points = []
                while start.y != end.y:
                    r1 = PosXY(i1)[1]
                    i2 = XYPos(0, r1 + 1)
                    e1 = i2 - 1

                    lineend = self.IndexToCoords(e1) + Point(
                        dc.GetTextExtent(value[e1])[0], 0)
                    points.append((start, lineend))

                    i1 = i2
                    start = IndexToCoords(i1)

                points.append((start, end))

                for s, e in points:
                    dc.DrawLine(s.x, s.y + ymod, e.x, e.y + ymod)

    def AddSpellError(self, word, index, refresh=True):
        """
            Adds a misspelled word, or another index of that word to the dictionary of misspelled
            words for lookup later
        """

        if not word in self.spellerrors:
            self.spellerrors[word] = []

        if not index in self.spellerrors[word]:
            self.spellerrors[word].append(index)

        if refresh: self.Refresh()

    def UpdateSpellErrorIndexes(self):
        """
            Offsets all the spellerror indexes by the number of characters that
            have been typed since the last check if the index is higher then the
            last insertion index
        """

        lastinsert = self.lastinsertion
        lastcount = self.lastcharcount

        deltachar = len(self.Value) - lastcount
        spellerrors = self.spellerrors

        for word in spellerrors:
            for n in list(spellerrors[word]):
                if n > lastinsert:
                    i = spellerrors[word].index(n)
                    spellerrors[word][i] = n + deltachar

        self.Refresh()

    def ClearSpellErrors(self):
        """
            Clears all the spell errors
            Note that this does not update the display
        """

        self.spellerrors.clear()

    def RemoveExpiredSpellError(self, start, end, value=None):
        """
            This does various checks to try and dismiss marked errors that are no longer valid
        """

        if value is None:
            value = self.Value

        #URLs are not misspelled so lets get them out of the string
        #urllessWords
        purewords = [
            word for word in value.split() if not self.ignore_word(word)
        ]
        #purewords = ' '.join(urllessWords)

        #validate all the words in the spellerrors dictionary
        for word in set(self.spellerrors):
            wordlen = len(word)

            #if the word is't in the splash zone, don't bother checking it
            if not any(start <= i <= end for i in self.spellerrors[word]):
                continue

            #Remove the mark if the word is in the dictionary or no longer in the textctrl
            if word not in purewords or self.Check(word):
                self.spellerrors.pop(word)

                continue

            #if the word is still marked wrong but the number of instances have changed validate individual indexes
            elif len(self.spellerrors[word]) != purewords.count(word):
                for i in list(self.spellerrors[word]):
                    if value[i:i + wordlen] != word:
                        self.spellerrors[word].remove(i)

        self.Refresh()

    kb_shortcut_fixes = prefprop('messaging.spellcheck.kb_shortcut_fixes',
                                 default=False)

    def OnKey(self, e):
        """
            Key press event handling
            records last key code and the unicode character for that key
        """

        e.Skip()

        if not self.spenabled:
            return

        if self.kb_shortcut_fixes and \
                e.GetModifiers() == wx.MOD_CMD and e.KeyCode == wx.WXK_SPACE:
            e.Skip(False)
            return self.ReplaceWordWithBestSuggestion()

        self.lastchar = e.UnicodeKey
        self.lastvalue = self.Value
        self.lastcharcount = len(self.lastvalue)

        wx.CallAfter(self.PostOnKey)

    def PostOnKey(self):
        """
            Activates spellcheck on last change if a
            navigation or other non-editing key is hit
        """
        val = self.Value
        charcount = len(val)
        if self.needscheck and charcount and val == self.lastvalue:
            self.SpellCheckLastChange()

        self.lastinsertion = self.InsertionPoint
        self.lastcurrentword = self.GetCurrentWord()

    def OnText(self, event):
        """
            Text insertion event, flags the textfield as dirty if the text actually changed
        """

        event.Skip()

        if not self.spenabled or self.Value == self.lastvalue:
            self.Refresh()
            return

        self.needscheck = True

        self.UpdateSpellErrorIndexes()

        charcount = len(self.Value)

        if abs(charcount - self.lastcharcount) == 1:
            keypressed = unichr(self.lastchar) if self.lastchar else None
            currentword = self.GetCurrentWord()

            if ((keypressed in self.spellcheckon)
                    or self.lastcurrentword in self.spellerrors
                    and currentword != self.lastcurrentword):
                self.SpellCheckLastChange()
        else:
            self.SpellCheckAll()

    def UpdateLasts(self):
        """
            Updates variables used to determine if the string should be checked again
        """
        pass

    def Check(self, word):
        '''Returns True if a word should be considered spelled correctly.'''

        return self.ignore_word(word) or spellchecker.Check(word)

    def ignore_word(self, word):
        if word and isurl(word):
            return True

        for regex in self.regex_ignores:
            if regex.match(word):
                return True

        return False

    def SpellCheckAll(self):
        """
            Clears all the errors and re-spellchecks the entire text field
        """

        self.ClearSpellErrors()

        val = self.Value
        span = [word for word in val.split() if not self.ignore_word(word)]
        words = tokenizer_re.split(' '.join(span))

        index = 0

        for word in words:
            word = word.strip(string.punctuation)
            correct = self.Check(word)

            index = val.find(word, index)

            if not correct: self.AddSpellError(word, index, False)

            index += len(word)

        self.needscheck = False

        self.Refresh()

    def OnLeftDown(self, event):
        """
            Event handler to trigger spellcheck on click
        """
        event.Skip()

        if not self.spenabled:
            return

        charcount = len(self.Value)

        if charcount and self.needscheck:
            self.SpellCheckLastChange()

        self.lastinsertion = self.InsertionPoint
        self.lastcurrentword = self.GetCurrentWord()

    def GetCurrentWord(self):
        """
            Get word at cursor position
        """
        return self.GetWordAtPosition(self.InsertionPoint)

    def GetWordRangeAtPosition(self, ip):
        """
            For suggestion menu, only returns the word range using punctuation
            as a word breaking character
        """
        s = self.Value

        # for misspellings like "audio/vedio" we will underline
        # only "vedio." make sure that if you right click it, you
        # get only "vedio" -- the word the cursor is under.
        split_on = word_splitting_chars

        end = FindAny(s, split_on, ip)
        if end == -1: end = len(s)

        start = max(rFindAny(s, split_on, 0, end), 0)
        return start, end

    def GetWordAtPosition(self, ip):
        """
            For suggestion menu, only returns the word range using punctuation
            as a word breaking character
        """
        start, end = self.GetWordRangeAtPosition(ip)
        word = self.Value[start:end].split()
        if word:
            return word[0].strip(string.punctuation)

    def AddWordToDictionary(self, word):
        """
            Add a word, duh
        """
        spellchecker.Add(word)

        self.SpellCheckAll()

    def HitTestSuggestions(self, pos):
        """
            Returns the word under the mouse
        """
        result, col, row = self.HitTest(pos)
        if result == wx.TE_HT_UNKNOWN:
            return -1, []

        i = self.XYToPosition(col, row)
        return i, self.GetSuggestionsForPosition(i)

    def GetSuggestionsForPosition(self, i):
        if i == -1: return []

        word = self.GetWordAtPosition(i)

        if word and word in self.spellerrors:
            return spellchecker.Suggest(word)
        else:
            return []

    def AdjustSpellerrorIndex(self, index, diff):
        """
            This offsets all spellerror index that are highr than index by diff
        """
        spellerrors = self.spellerrors

        for word in spellerrors:
            for n in list(spellerrors[word]):
                if n > index:
                    i = spellerrors[word].index(n)
                    spellerrors[word][i] = n + diff

    def ReplaceWord(self, position, new_word):
        """
            Replaces the word at the position with the new word, and spell checks he area
        """
        oldip = self.InsertionPoint

        old_value = self.Value
        i, j = self.GetWordRangeAtPosition(position)

        old_word = old_value[i:j]

        # Since GetWordRangeAtPosition returns punctuation and whitespace...
        l = len(old_word)
        old_word = old_word.lstrip(nonword_characters)
        i += l - len(old_word)

        l = len(old_word)
        old_word = old_word.rstrip(nonword_characters)
        j -= l - len(old_word)

        self.Replace(i, j, new_word)

        diff = (len(new_word) - len(old_word))
        self.AdjustSpellerrorIndex(position, diff)
        self.InsertionPoint = oldip + diff

        #        self.UpdateSpellErrorIndexes()

        self.SpellCheckLastChange(position)
        self.ProcessEvent(wx.CommandEvent(wx.EVT_TEXT[0], self.Id))

    def ReplaceWordWithBestSuggestion(self, pos=None):
        '''Replaces the word at pos with the spellchecker's best suggestion. Has no
        effect if there are no suggestions.

        If pos is None, the cursor position is used.'''

        pos = pos if pos is not None else self.InsertionPoint
        suggestions = self.GetSuggestionsForPosition(pos)
        if suggestions:
            self.ReplaceWord(pos, suggestions[0])

    # {'word': [1, 56, 34]}

    def SpellCheckLastChange(self, ip=None):
        """
            Figure out a splash area for the last edit and spellcheck everything in it
        """

        # find the start and the end of the possibly relevant area
        val = self.Value
        end = val.find(' ', ip or self.lastinsertion)
        if end != -1: end = val.find(' ', end + 1)
        if end == -1: end = len(val)
        start = val.rfind(' ', 0, end)
        start = val.rfind(' ', 0, start)
        start = max(start, 0)
        start = val.rfind(' ', 0, start)
        start = max(start, 0)

        self.RemoveExpiredSpellError(start, end, value=val)

        # filter out irrelevant words
        span = [
            word for word in val[start:end].split()
            if not self.ignore_word(word)
        ]
        words = tokenizer_re.split(' '.join(span))

        # check all the words
        index = start
        for word in words:
            if not word: continue

            word = word.strip(string.punctuation)
            correct = self.Check(word)

            index = val.find(word, index, end)
            if not correct: self.AddSpellError(word, index)
            index += len(word)

        self.needscheck = False

    def AddSuggestionsToMenu(self, menu):
        return add_spelling_suggestions(self, menu)
示例#16
0
class UberButton(UberWidget, wx.PyControl):
    '''
    Skinnable buttons.

    Also acts as a wrapper for wx.Bitmap button to draw native looking buttons
    with icons and labels can be also used to make menu buttons and checkbox
    like toggle buttons.
    '''
    def __init__(self,
                 parent,
                 id=-1,
                 label='',
                 skin=None,
                 icon=None,
                 pos=wx.DefaultPosition,
                 size=None,
                 style=HORIZONTAL,
                 type=None,
                 menu=None,
                 menubarmode=False,
                 onclick=None):
        """
        Usage:
            UberButton(parent,id,label,skin,icon,pos,size,style,type,menu)
            -skin  - instead of detecting skins presence lke most UberGUI
                     this takes the skin as an argument from the parent
                     this allows different skins to be set to different
                     buttons at the same time
                     if not assigned will look OS native

            -icon  - The icon to show up on the button
                     Note: the button resizes to fit the icon, not vice versa

            -pos   - position of the button

            -size  - size of the button, this is actualy ignored and will
                     likely be adjusted to affect restraint size later

            -style - wx.HORIZONTAL - Icon centered over label centered on button
                     wx.VERTICAL - Icon spaced from left with label to the right

            -type  - None - normal button
                     combo - no behavior changes, only changes the drawing code
                             on native butons
                     toggle - button toggles True and False, needs visual
                              notification in native mode
                     menu - does not return an event, adds a dropdown icon
                            to the rightside of the button, when toggled on
                            displays the menu associated to it
            -menu  - the menu to drop down when the button is clicked if
                     type is menu
        """

        wx.PyControl.__init__(self,
                              parent,
                              id=id,
                              pos=pos,
                              style=wx.NO_BORDER | wx.FULL_REPAINT_ON_RESIZE)

        if type == "combo":
            UberWidget.__init__(self, 'COMBOBOX')
        else:
            UberWidget.__init__(self, "button")

        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self.label = label
        if label:
            self.RegAccel(label)

        self.notified = False
        self.active = False
        self.isdown = False
        self.hovered = False

        self.state = 1
        self.menu = menu if type == 'menu' else None
        if menu is not None and hasattr(menu, 'OnDismiss'):
            menu.OnDismiss += lambda: self.AddPendingEvent(
                wx.MenuEvent(wx.wxEVT_MENU_CLOSE, menu.Id))

        if icon:
            self.icon = icon
            self.MakeDicon()
        else:
            self.icon = None

        self.style = style
        self.type = type
        self.menubarmode = menubarmode
        self.ssize = wx.Size(0, 0)
        self.staticwidth = None

        if size:
            self.autoscale = False
        else:
            self.autoscale = True

        self.native = None
        self.Sizer = wx.BoxSizer(HORIZONTAL)

        self.SetSkinKey(skin, True)

        Bind = self.Bind
        Bind(wx.EVT_MOVE, lambda e: self.Refresh()),
        Bind(wx.EVT_PAINT, self.OnPaint),
        Bind(wx.EVT_SET_FOCUS, lambda e: self.Refresh()),
        Bind(wx.EVT_KEY_DOWN, self.OnKeyDown),
        Bind(wx.EVT_KEY_UP, self.OnKeyUp),
        Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus),
        Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown),
        Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown),
        Bind(wx.EVT_LEFT_UP, self.OnLeftUp),
        Bind(wx.EVT_ENTER_WINDOW, self.OnMouseIn),
        Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseOut),
        Bind(wx.EVT_MOTION, self.OnMotion),
        Bind(wx.EVT_MENU_CLOSE, lambda e:
             (self.Active(False), self.OnMouseOut()))
        Bind(wx.EVT_SHOW, self.OnShow)

        if onclick is not None:
            if not hasattr(onclick, '__call__'):
                raise TypeError('onclick must be callable')
            self.Bind(wx.EVT_BUTTON, lambda e: (e.Skip(), onclick()))

    def __repr__(self):
        try:
            return '<UberButton \'%s\'>' % self.label
        except:
            return '<UberButton %s>' % id(self)

    def SetAlignment(self, alignment):
        self.style = alignment
        self.Calcumalate()

    def GetAlignment(self):
        return self.style

    Alignment = property(GetAlignment, SetAlignment)

    #    def SetSize(self,size = None):
    #
    #        self.autoscale = not size
    #
    #        wx.Window.SetSize(self,size or self.ssize)
    #
    #    Size = property(wx.Window.GetSize,SetSize)

    def SetStaticWidth(self, width=None):
        """This forces the button to not auto resize it's width."""

        self.staticwidth = width
        self.Calcumalate()

    def OnMotion(self, event):
        """Ensures mouse release."""

        if self.native:
            event.Skip()
            return

        r = RectS(self.Size)
        if self.HasCapture() and not event.LeftIsDown() and \
            not r.Contains(event.Position):
            self.ReleaseAllCapture()

    @property
    def KeyCatcher(self):
        try:
            return self.Top._keycatcher
        except AttributeError:
            k = self.Top._keycatcher = KeyCatcher(self.Top)
            return k

    def UpdateSkin(self):
        'Simply gets a skin and sets it all up.'

        key = self.skinkey
        native = not key

        if native and self.uxthemeable:
            self.rendernative = True
            self.destroy_native()
            self.OpenNativeTheme()

            skinget = skin.get

            self.menuicon = skinget('appdefaults.dropdownicon')
            self.padding = (5, 5)
            self.margins = Margins([0, 0, 0, 0])
            self.Font = default_font()

            self.fontcolors = [wx.BLACK] * 7

            self.backgrounds = NATIVE_BACKGROUNDS
            self.Cut()

        elif native:
            if not self.native:
                self.native = wx.Button(self, style=wx.BU_EXACTFIT)
                self.native.SetLabel(self.label)
                self.Sizer.Add(self.native, 1, wx.EXPAND)
                self.Layout()
                self.Cut()

        else:
            self.rendernative = False
            self.destroy_native()
            skinget = skin.get
            skinroot = skin.get(key)
            #s = lambda k, default = sentinel: skinget('%s.%s' % (key, k), default)

            s = skinroot.get
            self.menuicon = s('menuicon', skinget('appdefaults.dropdownicon'))
            self.padding = s('padding', (5, 5))
            self.margins = s('margins', Margins([0, 0, 0, 0]))
            self.Font = s('font', default_font())

            fc = skinroot.get('fontcolors', {})
            s = fc.get
            self.fontcolors = [
                s('disabled', Color(125, 125, 125)),
                s('normal', wx.BLACK),
                s('active', wx.BLACK),
                s('hover', wx.BLACK),
                s('activehover', wx.BLACK),
                s('down', wx.BLACK),
                s('notify', wx.WHITE)
            ]

            bgs = skinroot.get('backgrounds', {})

            def s(key, default):
                try:
                    return bgs[key]
                except:
                    return default()

            disabled = s(
                'disabled', lambda: SkinGradient(
                    'vertical', [Color(125, 125, 125),
                                 Color(237, 237, 237)]))
            normal = s(
                'normal', lambda: SkinGradient(
                    'vertical', [Color(200, 255, 200),
                                 Color(85, 255, 85)]))
            active = s(
                'active', lambda: SkinGradient(
                    'vertical', [Color(200, 255, 238),
                                 Color(85, 255, 238)]))
            hover = s('hover', lambda: normal)
            activehover = s('activehover', lambda: active)
            down = s(
                'down', lambda: SkinGradient('vertical', [
                    Color(0, 125, 0), Color(00, 204, 00)
                ]))
            notify = s(
                'notify', lambda: SkinGradient(
                    'vertical', [Color(255, 255, 200),
                                 Color(255, 255, 85)]))

            self.backgrounds = [
                disabled, normal, active, hover, activehover, down, notify
            ]

        self.Calcumalate()
        self.Refresh()

    def destroy_native(self):
        '''Destroys the native peer button, if there is one.'''

        if self.native:
            self.native.Show(False)
            self.Sizer.Detach(self.native)
            self.native.Destroy()
            self.native = None

    def Enable(self, switch=True):
        """
        Enables or disables the button
        Enables by default
        (maybe change to toggle?)
        """
        if not self.native:
            wx.PyControl.Enable(self, switch)
            if switch:
                events = [(wx.EVT_LEFT_DOWN, self.OnLeftDown),
                          (wx.EVT_LEFT_DCLICK, self.OnLeftDown),
                          (wx.EVT_LEFT_UP, self.OnLeftUp),
                          (wx.EVT_ENTER_WINDOW, self.OnMouseIn),
                          (wx.EVT_LEAVE_WINDOW, self.OnMouseOut)]
                do(self.Bind(event, method) for (event, method) in events)

            else:
                events = [
                    wx.EVT_LEFT_DOWN, wx.EVT_LEFT_DCLICK, wx.EVT_LEFT_UP,
                    wx.EVT_ENTER_WINDOW, wx.EVT_LEAVE_WINDOW
                ]
                do(self.Unbind(event) for event in events)

            self.state = (1 if switch else 0)

            if self.ScreenRect.Contains(wx.GetMousePosition()):
                self.GetHover()
            else:
                self.ReleaseHover()

        else:
            self.native.Enable(switch)

        self.Refresh()

    def SetMenuBarMode(self, switch=None):
        """
        This turns on menubarmode

        This effects
            - the showing of the dropmenu icon
            - Ampersan denoted underlining
            - Informing a menubar about focus events
            - and some size and mouse capture logic differences logic
        """
        if switch:
            self.menubarmode = switch
        else:
            self.menubarmode = not self.menubarmode

    def MakeDicon(self):
        'Greys out an icon.'

        self.dicon = self.icon.Greyed.WXB

    def SetIcon(self, icon=None):
        """
        Changes icon for the button
        if no arguments speified or None, removes the icon from the button
        """
        if icon:
            self.icon = icon
            self.MakeDicon()
        else:
            self.icon = self.dicon = None

        self.Calcumalate()
        self.Refresh()

    def SetNotify(self, switch):
        self.notified = switch
        self.Refresh()

    def RegAccel(self, label=""):
        #        oldLabel = self.label
        #        i = oldLabel.find('&')
        #        if i != -1 and i < len(oldLabel) - 1 and oldLabel[i+1] != '&':
        #            self.KeyCatcher.RemoveDown('alt+%s' % oldLabel[i+1], self.FakeClick)

        i = label.find('&')
        if i != -1 and i < len(label) - 1 and label[i + 1] != '&':
            #            print self, 'adding alt+%s' % label[i+1]
            self.KeyCatcher.OnDown('alt+%s' % label[i + 1], self.FakeClick)

    def SetLabel(self, label=""):
        """
        Changes icon for the button
        if no arguments speified or an empty string, removes the label from the button
        """

        self.RegAccel(label)

        self.label = label
        if self.native:
            self.native.SetLabel(label)
        self.Calcumalate()
        self.Refresh()

    def GetLabel(self):
        return self.label

    Label = property(GetLabel, SetLabel)

    def FakeClick(self, event=None):
        if self.type == 'toggle':
            # If this button is a toggle button, we need to switch
            # states and throw a different type of command event.
            self.Active()
            self.SendButtonEvent(wx.wxEVT_COMMAND_TOGGLEBUTTON_CLICKED)
        if self.type == 'menu' and not self.active:
            self.CallMenu()
        elif not self.type == 'menu':
            self.SendButtonEvent()

    def Calcumalate(self):
        """
        Calculates the positioning of all content of the button and
        sets the size of the button based off of these calculations
        """
        if self.native:
            self.Size = self.MinSize = self.native.BestSize
            #self.Fit()
            self.Parent.Layout()
            ApplySmokeAndMirrors(self)
            return

        label = DeAmp(self.label)

        if self.icon:
            iconwidth, iconheight = self.icon.Width, self.icon.Height
        else:
            iconwidth, iconheight = (0, 0)
        if label != '':
            labelwidth, labelheight = GetTextExtent(
                label, self.Font)[0], self.Font.Height
        else:
            labelwidth, labelheight = (0, 0)

        self.labelsize = Size(labelwidth, labelheight)

        margins = self.margins

        sx, sy = self.padding
        swidth = 0
        sheight = 0

        #icon & label caclulations
        if self.icon and label != '':
            if self.style == HORIZONTAL:
                swidth += iconwidth + labelwidth + 3 * sx
                if iconheight > labelheight:
                    sheight += iconheight + 2 * sy
                    iy = sy
                else:
                    sheight += labelheight + 2 * sy
                    iy = sy + labelheight // 2 - iconheight // 2

                self.iconcurser = wx.Point(sx, iy)
                self.labelcurser = wx.Point(
                    self.iconcurser.x + iconwidth + sx,
                    self.iconcurser.y + iconheight / 2 - labelheight / 2)

                if self.menu and not self.menubarmode and self.menuicon:
                    swidth += self.menuicon.Width + sx

                self.ssize = Size(margins.left + swidth + margins.right,
                                  margins.top + sheight + margins.bottom)

            elif self.style == VERTICAL:
                labelwin = labelwidth > iconwidth
                if labelwin:
                    swidth += labelwidth + 2 * sx
                else:
                    swidth += iconwidth + 2 * sx
                sheight += iconheight + labelheight + 3 * sy

                lx = sx
                if labelwin:
                    self.iconcurser = wx.Point(
                        lx + labelwidth / 2 - iconwidth / 2, sy)
                    self.labelcurser = wx.Point(
                        lx, self.iconcurser.y + iconheight + sy)
                else:
                    self.iconcurser = wx.Point(lx, sy)
                    self.labelcurser = wx.Point(
                        lx + iconwidth / 2 - labelwidth / 2,
                        self.iconcurser.y + iconheight + sy)

                if self.menu and not self.menubarmode:
                    swidth += self.menuicon.Width + sx

                self.ssize = Size(margins.left + swidth + margins.right,
                                  margins.top + sheight + margins.bottom)

        # Just icon caclulations
        elif self.icon:
            swidth += iconwidth + 2 * sx
            sheight += iconheight + 2 * sy

            self.iconcurser = wx.Point(sx, sy)

            if self.menu and not self.menubarmode:
                swidth += self.menuicon.Width + sx

            self.ssize = Size(margins.left + swidth + margins.right,
                              margins.top + sheight + margins.bottom)

        # Just label caclulations
        elif label != '':
            swidth += labelwidth + 2 * sx
            sheight += labelheight + 2 * sy

            self.labelcurser = wx.Point(sx, sy)

            if self.menu and not self.menubarmode:
                swidth += self.menuicon.Width + sx

            self.ssize = Size(margins.left + swidth + margins.right,
                              margins.top + sheight + margins.bottom)

        elif self.menu is not None and not self.menubarmode:
            swidth += self.menuicon.Width
            sheight += self.menuicon.Height

            self.ssize = Size(swidth + 2 * sx + margins.x,
                              sheight + 2 * sy + margins.y)
        else:
            self.ssize = Size(16, 16)

        if self.staticwidth:
            self.ssize.width = self.staticwidth

        self.MinSize = self.ssize
        self.InvalidateBestSize()

        self.Parent.Layout()

    def DoGetBestSize(self):
        self.CacheBestSize(self.ssize)
        return self.ssize

    side_tabs = prefprop('tabs.side_tabs', False)

    def OnPaint(self, event):
        "Draws the button's background."

        # excape if native
        if self.native:
            event.Skip()
            return

        rect = RectS(self.Size)

        parent, grandparent = self.Parent, self.GrandParent
        if parent.__class__.__name__ == "Tab" and self.side_tabs:
            #Clipping the button for when partualy hidden
            cliptangle = RectS(self.Size)
            sy = self.Rect.y
            sw, sh = self.Size
            py = parent.Position.y
            pph = grandparent.Size.height - 16

            if parent.Position.y + parent.Size.height > grandparent.Size.height-16 and \
               sy + sh + py > pph:
                cliptangle = Rect(0, 0, sw, pph - (sy + py))
                dc = wx.PaintDC(self)
                dc.SetClippingRect(cliptangle)
        else:
            dc = AutoBufferedPaintDC(self)

        # actual drawing of background
        currentstate=5 if self.isdown else \
                     6 if self.state and self.notified else \
                     self.state+(self.hovered*2) if self.state else \
                     0

        if self.rendernative:

            nrect = rect if self.type == 'combo' else rect.Inflate(1, 1)
            #wx.RendererNative.Get().DrawPushButton(self,dc,nrect,self.backgrounds[currentstate])
            part = 1
            state = self.backgrounds[currentstate]
            self.DrawNativeLike(dc, part, state, nrect)
        else:
            background = self.backgrounds[currentstate]
            from cgui import SplitImage4
            if isinstance(background, SplitImage4):
                ApplySmokeAndMirrors(self, background.GetBitmap(self.Size))
            else:
                ApplySmokeAndMirrors(self)
            background.Draw(dc, rect)

        #TODO: Backgroundless icon buttons

        if not (self.type == 'combo' and self.rendernative):
            self.DrawContent(dc)

    def OnShow(self, e):
        'Invoked on EVT_SHOW'

        # Don't leave menus stranded if the button is being hidden.
        if not e.GetShow() and self.menu and not wx.IsDestroyed(
                self.menu) and self.menu.IsShown():
            self.menu.Hide()

    def DrawContent(self, dc):
        'Draws the contents of the button to the DC provided.'
        offset = ((self.Size.width / 2 - self.ssize[0] / 2) if
                  (not self.menu or self.label != '' and self.icon
                   and self.style == VERTICAL or self.label == '')
                  and not (self.label != '' and self.icon
                           and self.style == HORIZONTAL) else 0,
                  self.Size.height / 2 - self.ssize[1] / 2)
        ml, mt, mr, mb = self.margins

        #Draw Icons
        if self.icon:
            dc.DrawBitmap(self.icon if self.IsEnabled() else self.dicon,
                          self.iconcurser[0] + offset[0] + ml,
                          self.iconcurser[1] + offset[1] + mt, True)
        #Draw Labels
        if self.label != '':

            dc.Font = self.Font

            #Set the font color
            currentstate = 6 if  self.state and self.notified else \
                           5 if self.isdown else \
                           self.state + (self.hovered * 2) if self.state else \
                           0

            dc.TextForeground = self.fontcolors[currentstate]

            dc.Font = self.Font
            labelcurser = self.labelcurser

            loffset = (max(offset[0], 0), max(offset[1], 0))
            w = min(
                self.labelsize.x, self.Rect.Width - labelcurser.x -
                loffset[0] - self.padding[0] - mr -
                ((self.menuicon.Width + self.padding[0])
                 if self.menu and not self.menubarmode else 0)) + 2
            lrect = Rect(labelcurser.x + loffset[0] + ml,
                         labelcurser.y + loffset[1] + mt, w, self.labelsize[1])

            #            dc.Pen=wx.Pen(wx.Color(0,255,0))
            #            dc.Brush=wx.TRANSPARENT_BRUSH
            #            dc.DrawRectangleRect(lrect)

            dc.DrawLabel(TruncateText(self.label.replace('&', ''), w, None,
                                      dc),
                         lrect,
                         LEFT_VCENTER,
                         indexAccel=self.label.find('&'))

        #Lastly draw the dropmenu icon if there is a menu
        if self.type == 'menu' and not self.menubarmode:
            dc.DrawBitmapPoint(
                self.menuicon,
                wx.Point(
                    self.Size.width - self.menuicon.Width - self.padding[0] -
                    mr, (self.Size.height -
                         (mt + mb)) / 2 + mt - self.menuicon.Height / 2), True)

    def Active(self, switch=None):
        """
        Used for menu and toggle buttons.

        Toggles active state or sets it to the provided state.
        """

        if self.state:
            old_active = self.active
            self.active = not self.active if switch is None else switch
            self.state = 2 if self.active else 1

            if old_active != self.active:
                self.Refresh(False)

    def IsActive(self):
        return self.active

    def OnKillFocus(self, event):
        """
        Part of an attempted tab-traversal fix, might work, might not.
        """

        if self.native:
            event.Skip()
            self.Refresh()
            return

        if self.isdown:
            self.OnMouseOut(event)
        self.Refresh()

    def OnKeyDown(self, event):
        """
        Used to allow spacebar to emulate mousedown
        """

        if self.native:
            event.Skip()
            self.Refresh()
            return

        if event.KeyCode == wx.WXK_SPACE:
            if not self.isdown and not wx.LeftDown(): self.OnLeftDown(event)
        else:
            event.Skip()
            self.Refresh()

    def OnKeyUp(self, event):
        """
        Allow spacebar to emulate mouse up
        """

        if self.native:
            event.Skip()
            self.Refresh()
            return

        if event.KeyCode == wx.WXK_SPACE:
            if self.isdown and not wx.LeftDown(): self.OnLeftUp(event)
        else:
            event.Skip()

    def OnLeftDown(self, event=None):
        'Left mouse down handling.'

        if self.native:
            self.isdown = True
            event.Skip()
            self.Refresh()
            return

        if self.isdown:
            self.OnMouseOut(event)
            return

        self.isdown = True

        if not self.HasCapture():
            self.CaptureMouse()

        self.SetFocus()

        if self.type == 'menu':
            if not self.active:
                self.CallMenu()
            elif self.menu:
                self.Active(False)
                self.menu.Dismiss()

        self.Refresh()

    def SendButtonEvent(self, type=None):
        """
        fakes a button event
        """
        eventType = type or wx.wxEVT_COMMAND_BUTTON_CLICKED
        evt = wx.CommandEvent(eventType, self.Id)
        evt.EventObject = self
        self.AddPendingEvent(evt)

    def OnLeftUp(self, event):
        'Left mouse up handling.'

        if self.native:
            self.isdown = False
            event.Skip()
            self.Refresh()
            return

        self.ReleaseAllCapture()

        if self.isdown:
            self.isdown = False

            #If mouse is released over the button
            if (event.m_x >= 0 and event.m_x <= self.Size.x and event.m_y >= 0
                    and event.m_y <= self.Size.y) or hasattr(
                        event, 'KeyCode') and event.KeyCode == wx.WXK_SPACE:
                if self.type == 'toggle':
                    # If this button is a toggle button, we need to switch
                    # states and throw a different type of command event.
                    self.Active()
                    self.SendButtonEvent(wx.wxEVT_COMMAND_TOGGLEBUTTON_CLICKED)
                elif not self.type == 'menu':
                    self.SendButtonEvent()

            self.Refresh()

    def GetHover(self):
        """
        Mouse over detection for hover state and menubar mouse over open
        """

        self.hovered = True
        self.Refresh()

        event = UberEvents.UBOver(source=self)
        self.Parent.AddPendingEvent(event)

    def OnMouseIn(self, event):
        "Mouse over handling."

        if not event.LeftIsDown() or self.menubarmode:
            self.GetHover()

        if self.native:
            event.Skip()
            return

        elif self.HasCapture():
            # Mouse left is still down, has come back to our rectangle: depress
            self.isdown = True
            self.Refresh()

    def OnMouseOut(self, event=None):
        'Mouse out handling.'

        self.isdown = False

        if self.native:
            event.Skip()
            return

        if self.menubarmode:
            self.ReleaseAllCapture()

#        if not self.menubarmode:
        self.ReleaseHover()

        self.Refresh()

    def ReleaseHover(self):
        'Un-hover the button.'

        self.hovered = False
        self.Refresh()

        event = UberEvents.UBOut(source=self)
        self.Parent.AddPendingEvent(event)

    def CallMenu(self):
        'Click (down then up) handling.'

        self.Active()
        if self.active:
            event = wx.MenuEvent(wx.wxEVT_MENU_OPEN, -1)
            event.SetEventObject(self.menu)
            self.Top.ProcessEvent(event)
            self.ReleaseAllCapture()
            self.menu.Display(self)

    def GetValue(self):
        return self.active

    def SetValue(self, v):
        self.active = v
        self.Refresh()

    Value = property(GetValue, SetValue)

    def AcceptsFocusFromKeyboard(self):
        return not self.menubarmode

    def AcceptsFocus(self):
        return not self.menubarmode
示例#17
0
class ImWinCtrl(object):
    '''
    IM window logic.

    - incoming messages and new conversations go to the "message" function
    - the on_mode_change[d] functions are called when changing modes
    '''
    def __init__(self):
        # "controllers" in charge of the contents of the To/From combos
        self.IMControl = IMControl(self, self.capsbar, self.To, self.From)
        self.IMControl.OnSelection += self.FocusTextCtrl
        self.IMControl.OnSwitchContact += lambda c: self.set_conversation(
            c.protocol.convo_for(c))

        self.EmailControl = EmailControl(self.To, self.From)
        self.EmailControl.OnLoseFocus += lambda: self._emailpanel.subject_input.SetFocus(
        )

        self.SMSControl = SMSControl(self.To, self.From)
        self.SMSControl.OnLoseFocus += self.FocusTextCtrl

        self.controllers = {
            'im': self.IMControl,
            'email': self.EmailControl,
            'sms': self.SMSControl
        }

        self.mode = None
        self.convo = None

        self.typing = None  # buddy's typing state
        self.typing_status = None  # your typing state
        self.typing_timer = None  # timer for keeping track of your typing state
        self.clear_typing_timer = wx.PyTimer(self.on_clear_typing_timer)

        self.GetButton('im').Bind(
            EVT_BUTTON, lambda e: self.set_mode('im', toggle_tofrom=True))

        for mode in ('email', 'sms', 'info'):
            self.GetButton(mode).Bind(EVT_BUTTON,
                                      lambda e, mode=mode: self.set_mode(mode))

        self.GetButton('video').Bind(EVT_BUTTON, lambda e: self.on_video())

        multichat = self.GetButton('multichat')
        if multichat is not None:
            multichat.Bind(wx.EVT_TOGGLEBUTTON,
                           lambda e: self.toggle_roomlist())

    send_typing = prefprop('privacy.send_typing_notifications', True)
    typed_delay = prefprop('messaging.typed_delay', 5)

    def message(self, messageobj, convo=None, mode='im', meta=None):
        "Called by imhub.py with incoming messages."

        info('%r', self)
        info_s('  messageobj: %r', messageobj)
        info('       convo: %r', convo)
        info('        mode: %r', mode)
        info('        meta: %r', meta)

        assert wx.IsMainThread()

        if messageobj is None:
            # starting a new conversation--no message
            self.set_conversation(convo)
            self.set_mode(mode)
            self.IMControl.SetConvo(convo)
            if convo.ischat:
                self.show_roomlist(True)

        elif (messageobj.get('sms', False) or getattr(convo.buddy, 'sms', None)
              ) and not profile.blist.on_buddylist(convo.buddy):
            # an incoming SMS message
            if self.convo is None:
                self.set_conversation(convo)

            if self.mode != 'sms':
                self.set_mode('sms')

            # just show it
            self.show_message(messageobj)
        else:
            convo = messageobj.conversation

            if self.mode is None:
                self.set_mode(mode)
            self.show_message(messageobj)
            self.set_conversation(convo)
#            self.IMControl.SetConvo(convo)

    def set_mode(self, mode, toggle_tofrom=False):
        with self.Frozen():
            oldmode = getattr(self, 'mode', None)
            self.on_mode_change(oldmode, mode, toggle_tofrom)
            self.show_controls(mode)
            self.on_mode_changed(mode)

    Mode = property(attrgetter('mode'), set_mode)

    def on_mode_change(self, oldmode, mode, toggle_tofrom=False):
        'Invoked before the GUI is shown for a new mode.'

        # To/From showing and hiding
        if oldmode != mode and mode in ('email', 'sms'):
            self.ShowToFrom(True)
        elif mode == 'info':
            self.ShowToFrom(False)

        # Enabling/disabling formatting buttons
        if mode == 'sms' and oldmode != 'sms':
            wx.CallAfter(
                lambda: self.input_area.tc.SetMaxLength(SMS_MAX_LENGTH))
        elif oldmode == 'sms' and mode != 'sms':
            wx.CallAfter(lambda: self.input_area.tc.SetMaxLength(0))

        # If going to IM, Email, or SMS--show that mode's to/from combo
        for m in ('im', 'email', 'sms'):
            if mode == m and oldmode != m:
                wx.CallAfter(self.controllers[mode].Apply)
                break

        # If going to IM mode...
        if mode == 'im':
            # toggle if clicking IM button
            #   or
            # ask IM control if there is more than one choice for the To/From choices.
            if self.convo.ischat:
                self.ShowToFrom(False)
            elif oldmode != 'im':
                self.ShowToFrom(self.IMControl.HasChoices)
            elif toggle_tofrom:
                self.ShowToFrom(not self.capsbar.ToFromShown)

            #show = self.IMControl.HasChoices if oldmode != 'im' else not self.capsbar.ToFromShown
            #self.ShowToFrom(show)

        if oldmode is not None:
            self.GetButton(oldmode).Active(False)

        self.GetButton(mode).Active(True)

    def on_mode_changed(self, mode):
        'Invoked after the GUI is shown for a new mode.'

        if mode in ('im', 'sms'):
            self.on_message_area_shown()
            if self.IsActive():
                # for some reason, if we don't use wx.CallAfter on Mac, the call happens to early.
                if sys.platform.startswith('darwin'):
                    wx.CallAfter(self.FocusTextCtrl)
                else:
                    self.FocusTextCtrl()

        elif mode == 'info':
            self.set_profile_html(self.Buddy)
            self.profile_html.SetFocus()  # so mousewheel works immediately

        elif mode == 'email':
            self._emailpanel.subject_input.SetFocus()

    def on_send_message(self):
        if getattr(self, 'on_send_message_' + self.mode, lambda a: None)():
            self.Top.on_sent_message(self.mode, self)

    def on_send_message_im(self):
        'Invoked when enter is pressed in the message input box during IM mode.'

        val = self.input_area.GetFormattedValue()

        # Early exit if there is no message to send.
        if not val.format_as('plaintext'):
            return

        self.history.commit(val.format_as('plaintext'))

        # If the user has selected different to/from accounts, change
        # our conversation object.
        if self.set_conversation_from_combos():
            self.convo.send_message(val)
            self.ClearAndFocus()
            return True

    def on_send_email(self, *a):
        'Invoked when the "Send" button is pressed. (For Emails)'

        to, frm = self.EmailControl.ToEmail, self.EmailControl.FromAccount

        if to is None:
            return wx.MessageBox(
                _('Please add an email address for this buddy by '
                  'clicking the "To:" box.'),
                _('Compose email to {name}').format(name=self.Buddy.name))

        epanel = self._emailpanel

        def success(*a):
            log.info('Email send success')
            # store history
            profile.blist.add_tofrom('email', to, frm)

            epanel.Clear()
            epanel.SetStatusMessage(_('Message Sent'))
            epanel.send_button.Enable(True)
            epanel.openin.Enable(True)

            import hooks
            hooks.notify('digsby.statistics.email.sent_from_imwindow')

        def error(*a):
            log.info('Email send error')
            epanel.SetStatusMessage(_('Failed to Send Email'))
            epanel.send_button.Enable(True)
            epanel.openin.Enable(True)

        epanel.SetStatusMessage(_('Sending...'))
        epanel.send_button.Enable(False)
        epanel.openin.Enable(False)

        subject = self._get_email_subject()
        body = self._get_email_body()

        frm.OnClickSend(to=to,
                        subject=subject,
                        body=body,
                        success=success,
                        error=error)

    def _get_email_subject(self):
        return self._emailpanel.subject_input.Value

    def _get_email_body(self):

        body = self._emailpanel.email_input_area.Value
        if pref('email.signature.enabled', type=bool, default=False):
            footer = u'\r\n' + pref(
                'email.signature.value',
                type=unicode,
                default=
                u'\r\n_______________________________________________________________'
                '\r\nSent using Digsby - http://email.digsby.com')
        else:
            footer = ''

        return body + footer

    def on_send_message_sms(self):
        'Invoked when enter is pressed in the message input box during SMS mode.'

        # Early exit if there is no message to send.
        if not self.input_area.Value: return

        to, frm = self.SMSControl.ToSMS, self.SMSControl.FromAccount
        if to is None:
            MessageBox(_('Please add an SMS number first.'),
                       _('Send SMS Message'))
        elif frm is None:
            MessageBox(
                _('You are not signed in to any accounts which can send SMS messages.'
                  ), _('Send SMS Message'))
        else:

            message = self.input_area.Value

            def on_success():
                self.show_message(
                    Message(buddy=frm.self_buddy,
                            message=message[:SMS_MAX_LENGTH],
                            conversation=self.convo,
                            type='outgoing'))
                self.ClearAndFocus()

            def on_error(errstr=None):
                if errstr is not None:
                    more = '\n' + _(
                        'The error message received was:') + '\n\t%s' % errstr
                else:
                    more = ''

                MessageBox(
                    _('There was an error in sending your SMS message.') +
                    more,
                    _('Send SMS Message Error'),
                    style=wx.ICON_ERROR)

            # Check the length--even though we limit the number of characters in SMS mode, the input box
            # may already have had too many characters.
            if len(message) > SMS_MAX_LENGTH:
                sms_line1 = _(
                    'Only the first {max_length:d} characters of your message can be sent over SMS:'
                ).format(max_length=SMS_MAX_LENGTH)
                sms_line2 = _('Do you want to send this message now?')

                if wx.NO == wx.MessageBox(u'%s\n\n"%s"\n\n%s' %
                                          (sms_line1, message, sms_line2),
                                          _('Send SMS - Character Limit'),
                                          style=wx.YES_NO):
                    return

            import hooks
            hooks.notify('digsby.statistics.sms.sent')
            frm.send_sms(to,
                         message[:SMS_MAX_LENGTH],
                         success=on_success,
                         error=on_error)

    def on_edit_email(self):
        '''
        Uses the email account's mail client to edit the currently entered email.

        Invoked when the "Edit In..." button is clicked in the email panel.
        '''

        to, frm = self.EmailControl.ToEmail, self.EmailControl.FromAccount

        if to is not None and frm is not None:
            frm.OnComposeEmail(to=to,
                               subject=self._emailpanel.subject_input.Value,
                               body=self._emailpanel.email_input_area.Value)

    def set_conversation_from_combos(self):
        '''
        If our current conversation doesn't match the to and from accounts
        chosen by the combos, obtains a new conversation.

        Returns False if there are no accounts to send the IM with.
        '''
        if self.ischat:
            return self.convo.protocol.connected

        to, frm = self.IMControl.Buddy, self.IMControl.Account

        # If IMControl's Account object is None, all accounts which can message
        # the buddy we're talking to have signed off. We can't send the message.
        if frm is None: return False

        convo = self.convo
        if convo.protocol is not frm or convo.buddy is not to:
            log.info(
                'asking protocol %r for a new convo for buddy %r with service %r',
                frm, to, to.service)
            convo = frm.convo_for(to)
            log.info('got conversation %r with buddy/service %r %r:', convo,
                     convo.buddy, convo.buddy.service)
            self.set_conversation(convo)

        return True

    def on_close(self):
        if getattr(self, '_closed', False):
            log.warning(
                'FIXME: imwin_ctrl.on_close was called more than once!!!')
            return

        self._closed = True

        del self.capsbar.buddy_callback
        del self.capsbar

        import hooks
        hooks.notify('digsby.overlay_icon_updated', self)

        from plugin_manager import plugin_hub

        plugin_hub.act('digsby.im.conversation.close.async', self.convo)

        self.unlink_observers()
        if self.convo is not None:
            self.unwatch_conversation(self.convo)
            try:
                self.convo.explicit_exit()
            except Exception:
                print_exc()

    @property
    def Conversation(self):
        return self.convo

    @property
    def Buddy(self):
        return self.IMControl.Buddy

    @property
    def SMS(self):
        return self.SMSControl.get_contact_sms()

    def set_conversation(self, convo, meta=None):
        if convo is self.convo:
            return

        # watch/unwatch
        shouldShowToFrom = False
        if self.convo is not None:
            self.unwatch_conversation(self.convo)
            self.convo.exit()
            if not self.convo.ischat:
                shouldShowToFrom = True

        self.convo = convo
        self.watch_conversation(convo)

        if self.is_roomlist_constructed():
            self.roomlist.SetConversation(convo)

        contact = meta if meta is not None else convo.buddy

        self.capsbar.ApplyCaps(convo=convo)
        self.IMControl.SetConvo(convo, meta)
        if shouldShowToFrom and not self.convo.ischat:
            self.ShowToFrom(shouldShowToFrom)

        self.EmailControl.SetContact(contact)
        self.SMSControl.SetContact(contact)
        self.update_icon()
        self.update_title()

        self.choose_message_formatting()
        self.convo.play_queued_messages()

        if convo.ischat:

            @wx.CallAfter
            def after():
                self.show_roomlist(True)

    def _update_caps_and_title(self, *a):
        @wx.CallAfter
        def after():
            self.capsbar.ApplyCaps(convo=self.convo)
            self.update_title()

            if self.convo.ischat and not getattr(
                    self, 'roomlist_has_been_shown', False):
                log.info("showing roomlist...")
                self.toggle_roomlist()

    def watch_conversation(self, convo):

        from plugin_manager import plugin_hub

        plugin_hub.act('digsby.im.conversation.open.async', convo)

        convo.typing_status.add_observer(self.typing_status_changed)
        convo.add_observer(self._update_caps_and_title, 'ischat')

        buddy = convo.buddy

        buddy.add_observer(self.buddy_status_changed, 'status')
        buddy.add_observer(self.buddy_info_changed)

        if convo.ischat:
            convo.room_list.add_observer(self.chat_buddies_changed)
            convo.conversation_reconnected.add_unique(
                self.on_conversation_reconnected)

        convo.protocol.add_observer(self._on_convo_proto_state_change, 'state')

        #profile.account_manager.buddywatcher.watch_status(buddy, self.on_status_change)

    def on_conversation_reconnected(self, convo):
        @wx.CallAfter
        def gui():
            log.warning('on_conversation_reconnected: %r', convo)
            self.set_conversation(convo)
            convo.system_message(_('Reconnected'))

    def chat_buddies_changed(self, *a):
        wx.CallAfter(self.update_title)

    def unwatch_conversation(self, convo=None):
        if convo is None: convo = self.convo
        if convo is not None:
            buddy = convo.buddy

            convo.remove_observer(self._update_caps_and_title, 'ischat')
            convo.typing_status.remove_observer(self.typing_status_changed)
            if buddy is not None:
                buddy.remove_observer(self.buddy_status_changed, 'status')
                buddy.remove_observer(self.buddy_info_changed)

            convo.room_list.remove_observer(self.chat_buddies_changed)
            convo.conversation_reconnected.remove_maybe(
                self.on_conversation_reconnected)

            convo.protocol.remove_observer(self._on_convo_proto_state_change,
                                           'state')
            #profile.account_manager.buddywatcher.unwatch_status(buddy, self.on_status_change)

    def _on_convo_proto_state_change(self, proto, attr, old, new):
        @wx.CallAfter
        def after():
            if self.convo.ischat and new == proto.Statuses.OFFLINE:
                # chats include roomlist count, and need to be updated on disconnect.
                self.update_title()

    def show_status(self, update, ondone=None):
        cb = lambda u=update: self.show_message(u, ondone)

        try:
            timer = self._statustimer
        except AttributeError:
            timer = self._statustimer = wx.PyTimer(cb)
        else:
            timer.SetCallback(cb)

        if not self._statustimer.IsRunning():
            self._statustimer.Start(250, True)

    @calllimit(1)
    def buddy_info_changed(self, *a):
        '''
        This method is called anytime the buddy's information changes.

        If we're in "info" mode, the HTML profile box is updated.
        '''
        if self.mode == 'info':
            self.set_profile_html(self.Buddy)

    def buddy_status_changed(self, *a):
        wx.CallAfter(self._buddy_status_changed)

    def _buddy_status_changed(self):
        if check_destroyed(self):
            return

        if self.convo.ischat:
            return

        # if the buddy's online status changes, we may need to add/remove the
        # Files button
        self.capsbar.ApplyCaps(self.Buddy)

        # if we're showing the buddy's status orb in the tab/window title,
        # update those icons now.
        if self.icontype == 'status':
            self.update_icon()

    def typing_status_changed(self, *a):
        "Called when the conversation's typing status changes."

        typing = self.convo.typing_status.get(self.convo.buddy, None)

        # this pref indicates how long after not receiving typing notifications
        # we wait until clearing them, in seconds. 0 means never clear.
        typing_clear_time_secs = pref(
            'messaging.typing_notifications.clear_after', default=30, type=int)
        if typing_clear_time_secs > 0:
            if typing is not None:
                self.clear_typing_timer.StartOneShot(1000 *
                                                     typing_clear_time_secs)
            else:
                self.clear_typing_timer.Stop()

        self.on_typing(typing)

    def on_typing(self, typing):
        self.typing = typing
        self.update_title()
        self.update_icon()

    def on_clear_typing_timer(self):
        '''
        Called after a set period of time with no typing updates.
        '''

        if not wx.IsDestroyed(self):
            self.on_typing(None)

    def choose_message_formatting(self):
        '''
        Gives a chance for both the conversation and the protocol to expose
        an attribute "message_formatting", which if set to 'plaintext'
        will cause us only to extract (and send as IMs) text from the input
        box, not HTML.
        '''
        plain = False
        conv = self.convo

        try:
            plain = conv.message_formatting == 'plaintext'
        except AttributeError:
            try:
                plain = conv.protocol.message_formatting == 'plaintext'
            except AttributeError:
                pass

        self.plainttext = plain

    def show_message(self, messageobj, ondone=None):
        "Shows a message object in the message area."

        c = messageobj.conversation
        b = messageobj.buddy
        t = messageobj.type

        # used to remember incoming<->outgoing for whether to "glue"
        # consecutive messages together visually
        buddyid = (b.idstr(), messageobj.type) if b is not None else None

        if buddyid is None:
            next = False
        else:
            next = getattr(self, 'last_buddy', None) == buddyid
        self.last_buddy = buddyid

        self.message_area.format_message(t, messageobj, next=next)

        if ondone is not None:
            #ondone()
            pass

    def set_profile_html(self, buddy):
        "Sets the HTML info window's contents to buddy's profile."

        profilewindow = self.profile_html

        # don't generate HTML for the same buddy twice.
        try:
            html = GetInfo(self.Buddy,
                           showprofile=True,
                           showhide=False,
                           overflow_hidden=False)
        except Exception:
            print_exc()
            html = buddy.name

        # freeze/thaw since HTML window is flickery when updating contents
        with self.Frozen():
            profilewindow.SetHTML(html)

    def on_text_changed(self, e):
        'Called when the main text input box changes.'

        e.Skip()

        # change conversations if we need to
        oldConvo = self.convo
        if not self.send_typing or self.Mode != 'im' or not self.set_conversation_from_combos(
        ):
            return

        # end typing notifications for the old conversation
        if oldConvo is not None and oldConvo is not self.convo:
            with traceguard:
                oldConvo.send_typing_status(None)

        txt = self.input_area.Value

        if len(txt) == 0:
            if self.typing_status != None:
                self.typing_status = None
                self.convo.send_typing_status(None)

            if self.typing_timer:
                self.typing_timer.Stop()
                self.typing_timer = None

        else:
            if self.typing_status != 'typing':
                self.typing_status = 'typing'
                self.convo.send_typing_status(self.typing_status)

            self.cancel_timer()
            self.typing_timer = PyTimer(self.send_typed)
            self.typing_timer.Start(self.typed_delay * 1000, True)

    def send_typed(self, *e):
        if self.typing_status != 'typed':
            self.typing_status = 'typed'
            self.convo.send_typing_status(self.typing_status)

    def cancel_timer(self):
        if self.typing_timer:
            self.typing_timer.Stop()
            self.typing_timer = None

    def on_message_area_shown(self):
        'Sets up the MessageStyle for the IM area.'

        if hasattr(self, 'message_area') and not self.message_area.inited:
            if hasattr(self, 'convo'):
                self.init_message_area(self.convo.name,
                                       self.convo.buddy,
                                       show_history=not self.convo.ischat)
            else:
                self.init_message_area('', None)

            self.input_area.tc.Bind(wx.EVT_TEXT, self.on_text_changed)

            from gui.imwin.imwindnd import ImWinDropTarget
            self.SetDropTarget(ImWinDropTarget(self))

    def on_video(self):

        import hooks
        hooks.notify('digsby.video_chat.requested')

        buddy = self.Buddy

        from gui.video.webvideo import VideoChatWindow

        if VideoChatWindow.RaiseExisting():
            self.convo.system_message(
                _('You can only have one audio/video call at a time.'))
            log.info('video window already up')
        else:
            log.info('requesting video chat')

            from digsby.videochat import VideoChat
            VideoChat(buddy)
示例#18
0
class BuddyList(WheelScrollMixin, BuddyListBase):
    'Main buddy list control.'

    # The amount of space given to "borders" between buddy list elements during
    # drag and drop.
    def __init__(self, parent, infobox, collapsed=None, keyhandler=None):
        # load expanded groups
        try:
            collapsed = eval(profile.localprefs[EXPANSION_STATE_KEY])
        except:
            collapsed = [
                u'OfflineGroup_Offline'
            ]  # TODO: actually run expanded_id on the offline group to obtain this string

        self.dragResult = wx.DragMove
        self.infobox_scrollers = set()

        # an empty group forms the root of the hierarchy
        model = TreeListModel(collapsed=collapsed)

        # store idle indices for a periodical refresh
        self.idle_indices = []
        self.idle_timer = wx.PyTimer(self.refresh_idle_buddies)

        # save expansion after EXPANSION_SAVE_TIME seconds
        def save_expansion():
            profile.localprefs[EXPANSION_STATE_KEY] = repr(
                list(model.collapsed))

        profile.PreDisconnectHooks.append(save_expansion)
        model.expansion_state_changed += delayed_call(
            save_expansion, EXPANSION_SAVE_TIME_SECS)
        model.expansion_state_changed += lambda: self.renderers_cache.clear()

        from jabber.JabberContact import JabberContact
        model.donotexpand += [MetaContact, JabberContact]

        super(BuddyList, self).__init__(parent, model)

        Bind = self.Bind
        Bind(wx.EVT_MOTION, self.motion)
        Bind(wx.EVT_ENTER_WINDOW, self.enter_window)
        Bind(wx.EVT_LEAVE_WINDOW, self.leave_window)
        Bind(wx.EVT_MIDDLE_UP, self.on_middle_up)
        self.BindWheel(self)
        Bind(wx.EVT_RIGHT_DOWN, self.on_right_down)
        Bind(wx.EVT_CONTEXT_MENU, self.on_context_menu)
        Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
        Bind(wx.EVT_KEY_DOWN, self.__onkey)

        if keyhandler is not None:
            Bind(wx.EVT_CHAR, keyhandler)

        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self.update_renderers()

        self.dragging_obj = None
        self.shownbuddies = defaultdict(list)

        with traceguard:
            self.SetDropTarget(BuddyListDropTarget(self))

        # On Mac OS X, we don't get mouse moved events when the
        # window is inactive or not focused, so poll the mouse state
        # whenever we enter the window
        self.mouse_tracker = None
        if config.platform == 'mac':
            self.mouse_tracker = wx.PyTimer(self.motion)

        blist = profile.blist

        # Observe changes to the buddylist structure.
        blist.add_observer(self.on_blist_update)

        self.infobox = infobox
        infobox.Befriend(self)

        top_bind = self.Top.Bind
        top_bind(wx.EVT_SHOW, self.on_frame_show_or_iconized)
        top_bind(wx.EVT_ICONIZE, self.on_frame_show_or_iconized)

    def on_frame_show_or_iconized(self, e):
        # Since this control is the only observer of the sorter, we can
        # add a hack that disables sorting when the buddylist isn't shown.
        #
        # When you bring the buddylist back, a sort will happen.
        e.Skip()
        frame = self.Top

        def later():
            visible = frame.IsShown() and not frame.IsIconized()
            profile.blist.set_sort_paused(not visible)

        # allow adjacent iconize/show to catch up
        wx.CallLater(50, later)

    @property
    def context_menu(self):
        try:
            return self._context_menu
        except AttributeError:
            self._context_menu = UMenu(self, _('BuddyList Popup'))
            return self._context_menu

    def on_blist_update(self, blist, *a):
        if not self: return
        self.set_root(blist.view)

    showing_idle = prefprop('buddylist.layout.extra_info')

    def PostPaint(self, dc):
        self.paint_add_link(dc)

        # reset the idle timer when painting
        if self.showing_idle in ('both', 'idle'):
            self.idle_timer.Start(IDLE_UPDATE_SECS * 1000)

        profile.blist.set_sort_paused(False)

        if not getattr(self, '_did_startup_time', False):
            self._did_startup_time = True
            import sys
            from time import clock
            sys._startup_time = clock()

            if getattr(getattr(sys, 'opts', None), 'measure') == 'startup':
                print 'startup time', sys._startup_time
                import os
                os._exit(0)

    def refresh_idle_buddies(self):
        for i in self.idle_indices:
            self.RefreshLine(i)

    def paint_add_link(self, dc):
        if len(self.model) == 0:
            amgr = profile.account_manager

            if amgr.accounts_loaded and len(amgr.accounts) == 0:
                try:
                    f = skin.get('BuddiesPanel.Fonts.AddAccountsLink',
                                 default_font)
                    f.SetUnderlined(True)

                    dc.SetTextForeground(
                        skin.get('BuddiesPanel.FontColors.AddAccountsLink',
                                 wx.BLUE))
                except Exception:
                    print_exc()
                    f = self.Font

                dc.SetFont(f)

                # Center an "Add Accounts" link in the client area.
                s = _('Add Accounts')

                w, h = dc.GetTextExtent(s)

                # position the link 1/4 down the list
                r = self.ClientRect
                r.Height = r.Height / 2
                x, y = r.HCenterW(w), r.VCenterH(h)

                dc.DrawText(s, x, y)
                self._linkrect = wx.Rect(x, y, w, h)

                # Draw a "Need Help?" link as well.
                help_string = _('Need Help?')
                w, new_height = dc.GetTextExtent(help_string)
                x, y = r.HCenterW(w), y + h * 1.5
                dc.DrawText(help_string, x, y)
                self._helplinkrect = wx.Rect(x, y, w, h)
                return

        try:
            del self._linkrect
            del self._helplinkrect
        except AttributeError:
            pass

    def set_root(self, root):
        return TreeList.set_root(self, root)

    def rename_selected(self):
        obj = self.SelectedItem
        if obj is not None:
            # check the action precondition
            if obj.rename_gui.action_allowed(obj):
                obj.rename_gui()

    def __onkey(self, e):
        c = e.KeyCode

        if c == wx.WXK_SPACE:
            self.RotateContact(not e.ShiftDown())
        elif c == wx.WXK_F2:
            self.rename_selected()
        elif c == wx.WXK_DELETE:
            self.delete_blist_item(self.SelectedItem)
        elif c in activate_keys:
            self.on_doubleclick()
        elif c == wx.WXK_HOME:
            self.SetSelection(0) if len(self.model) else None
        elif c == wx.WXK_END:
            self.SetSelection(len(self.model) - 1) if len(self.model) else None
        else:
            e.Skip(True)

    def UpdateSkin(self):
        self.dragimgs = S(
            bar=skin.get('BuddiesPanel.Dragimages.Bar'),  #TODO: stretch?
            box=skin.get('BuddiesPanel.Dragimages.Box'))

        for renderer in self.renderers.itervalues():
            renderer.UpdateSkin()

        bg = skin.get('BuddiesPanel.Backgrounds.List',
                      lambda: SkinColor(wx.WHITE))
        self.SetBackground(bg)

        self.RefreshAll()

    def update_renderers(self):
        import gui.buddylist.renderers as r

        self.UpdateSkin()

        contact_renderer = r.ContactCellRenderer(self)
        group_renderer = r.GroupCellRenderer(self)
        search_renderer = r.SearchCellRenderer(self)

        self.renderers.update(
            dict(Group=group_renderer,
                 DGroup=group_renderer,
                 SortGroup=group_renderer,
                 JabberContact=contact_renderer,
                 Contact=contact_renderer,
                 JabberResource=contact_renderer,
                 YahooContact=contact_renderer,
                 MetaContact=r.MetaContactCellRenderer(self),
                 SearchEntry=search_renderer,
                 SearchOptionsEntry=r.SearchCellOptionsRenderer(self),
                 SearchWebGroup=group_renderer))

    def dragon_allowed(self, i):
        'Is dragging on to the ith buddy allowed.'

        dragging = self.dragging_obj
        target = self.model[i]

        if isinstance(target, SpecialGroup):
            return False

        if isinstance(dragging, GroupTypes):
            return False

        from digsby import iswidget
        if iswidget(target):
            return False

        return True

    def on_middle_up(self, e):
        self.RotateContact(not e.ShiftDown())

    def RotateContact(self, forward=True):
        ib = self.infobox
        if forward:
            ib.SelectNext()
        else:
            ib.SelectLast()

    def _on_mousewheel(self, e):
        if e.RightIsDown():
            self._menu_ok = False
            ib = self.infobox
            rot = e.WheelRotation
            if rot < 0:
                ib.SelectNext()
            elif rot > 0:
                ib.SelectLast()
        else:
            # forward the mousewheel event to the infobox
            win = wx.FindWindowAtPointer()
            if isinstance(win, tuple(self.infobox_scrollers)):
                self.infobox.on_mousewheel(e)
            else:
                super(BuddyList, self)._on_mousewheel(e)

    def on_right_down(self, e):
        i = self.HitTest(e.Position)
        self.SetSelection(i)
        e.Skip()

    def on_context_menu(self, e):
        # ensure the selection is underneath the mouse cursor
        self.context_menu_event_selection(e)

        menu = self.context_menu
        menu.RemoveAllItems()

        i = self.GetSelection()

        if i != -1:
            obj = self.model[i]
            if hasattr(obj, '_disallow_actions'):
                return

            # The mouse is over a group or buddy
            import common.actions as actions
            actions.menu(self, self.model[i], self.context_menu)
        else:
            # The mouse is over an empty space in the buddylist
            #
            # Only show "Add Group" if you are connected to an IM account (other than Digsby)
            # OR if you have the "digsby.allow_add" preference set.
            if not any(x.allow_contact_add
                       for x in profile.account_manager.connected_accounts):
                return

            from gui.protocols import add_group
            menu.AddItem(_('&Add Group'), callback=add_group)

        menu.PopupMenu()

    def context_menu_event_selection(self, e):
        # EVT_CONTEXT_MENU has .Position == (-1, -1) when caused by the keyboard
        # if that's not the case, and the mouse is over an item that isn't
        # selected, select it.
        if e and e.Position != (-1, -1):
            # watch out! EVT_RIGHT_DOWN's event.Position is in client coordinates
            # but EVT_CONTEXT_MENU gets screen coordinates (at least on windows)
            self.SetSelection(self.HitTest(self.ScreenToClient(e.Position)))

    def open_convo_with_selected(self):
        'Opens a conversation window with the buddy currently selected.'

        i = self.GetSelection()
        if i != -1:
            obj = self.model[i]
            if self.can_chat(obj):
                chat_with(obj)
                self.infobox.DoubleclickHide()
                return True
            elif hasattr(obj, 'activate'):
                with traceguard:
                    obj.activate()
                return True

    def enter_window(self, e):
        if self.mouse_tracker:
            self.mouse_tracker.Start(10)

    def leave_window(self, e):
        if self.mouse_tracker:
            self.mouse_tracker.Stop()
        e.Skip()
        self.infobox.InvalidateDoubleclickHide()

    def can_chat(self, obj):
        return isinstance(obj, (Contact, MetaContact))

    def on_doubleclick(self, e=None):
        if not self.open_convo_with_selected():
            TreeList.on_doubleclick(self, e)

    activate_selected_item = on_doubleclick

    def expand_all(self):
        with self.save_selection():
            model = self.model
            isexp = model.is_expanded

            for i, obj in enumerate(model):
                if isinstance(obj, GroupTypes) and not isexp(i):
                    model.expand(obj)
                    return self.expand_all()

    def collapse_all(self):
        with self.save_selection():
            model = self.model
            isexp = model.is_expanded

            for i, obj in enumerate(model):
                if isinstance(obj, GroupTypes) and isexp(i):
                    model.collapse(obj)
                    return self.collapse_all()

    def on_left_down(self, e):
        self.drag_point = e.Position

        try:
            linkrect = self._linkrect
            helprect = self._helplinkrect
        except AttributeError:
            pass
        else:
            if linkrect.Contains(e.Position):
                import gui.pref.prefsdialog
                gui.pref.prefsdialog.show('accounts')
            elif helprect.Contains(e.Position):
                wx.LaunchDefaultBrowser(NEED_HELP_LINK)

        i = self.HitTest((e.GetX(), e.GetY()))

        if i == -1:
            self.SetSelection(-1)
            return e.Skip(True)

        if self.model.expandable(self[i]) and e.GetX() < 15:
            # If we're clicking a Group expander triangle, toggle the group
            # expansion but do not select.
            self.toggle_expand(self[i])
        else:
            self.SetSelection(i)
            e.Skip(True)

            self.infobox.quickshow = True
            self.infobox_hittest(e.Position)

    def motion(self, e=None):
        'Invoked on mouse motion over the buddy list.'
        pos = None
        if e:
            e.Skip(True)
            pos = e.Position
        else:
            pos = self.Parent.ScreenToClient(wx.GetMousePosition())

        try:
            linkrect = self._linkrect
            helprect = self._helplinkrect
        except AttributeError:
            pass
        else:
            # If the cursor is over a custom drawn link show a hand
            if any(r.Contains(pos) for r in (linkrect, helprect)):
                self.SetCursor(StockCursor(CURSOR_HAND))
            else:
                self.SetCursor(StockCursor(CURSOR_ARROW))

        # Drag "distance" must be at least ten pixels
        if e and hasattr(self, 'drag_point') and e.LeftIsDown() and e.Dragging() \
            and _dist2(e.GetPosition(), self.drag_point) > 100:

            i = self.GetSelection()
            if i != -1 and i < len(self.model):
                data = self.make_drag_data(self.model[i])

                ds = BuddyListDropSource(self)
                ds.SetData(data)
                unused_result = ds.DoDragDrop(wx.Drag_AllowMove)
                self.hilite = None
                self.Refresh()
                self.dragging = False
        elif e and not e.LeftIsDown() and hasattr(self, 'drag_point'):
            del self.drag_point

        else:
            self.infobox_hittest(pos)

    show_infobox = prefprop('infobox.show')

    def infobox_hittest(self, pos):
        i = self.HitTest(pos)

        if i != -1:
            obj = self[i]
            if not isinstance(obj, GroupTypes):
                if self.show_infobox and isinstance(obj,
                                                    (Contact, MetaContact)):
                    p = self.Parent
                    pl = p.ClientToScreen(
                        wx.Point(0, self.Position.y + self.GetItemY(i)))
                    pr = pl + wx.Point(p.Size.width, 0)
                    return self.infobox.Display(pl, pr, obj)

        # hide infobox when over search entries, or empty space
        self._hide_infobox()

    def _hide_infobox(self):
        if self.infobox.Shown:
            self.infobox.DelayedHide()
        else:
            self.infobox.Hide()

    def make_drag_data(self, blist_item):
        data = wx.DataObjectComposite()

        import contacts.contactsdnd as contactsdnd
        contactsdnd.add_to_dataobject(data, blist_item)

        self.dragging_obj = blist_item
        return data

    def on_drop_buddylistitem(self, clist_obj):
        if not getattr(self, 'hilite', None):
            return

        # hilite is a tuple of (area, index)
        area, _i, drop_to = self.hilite

        if area == 'below_group':
            area = 'below'

        if area == 'disallow': return
        #        if self.model[i] is clist_obj: return # early exit for dropping to same

        from gui.searchgui import SearchEntry

        if isinstance(clist_obj, Contact):
            return self.on_drop_contact(clist_obj, area, drop_to)
        elif isinstance(clist_obj, MetaContact):
            return self.on_drop_metacontact(clist_obj, area, drop_to)
        elif isinstance(clist_obj, GroupTypes):
            return self.on_drop_dgroup(clist_obj, area, drop_to)
        elif isinstance(clist_obj, SearchEntry):
            return self.on_drop_search(clist_obj, area, drop_to)
        self.hilite = None

    def on_drop_dgroup(self, group, area, togroup):
        assert isinstance(
            togroup,
            GroupTypes), "dragging above or below something that isn't a group"
        assert area in ('above', 'below')
        profile.blist.rearrange_group(group, area, togroup)

    def on_drop_metacontact(self, clist_obj, area, drop_to):
        if isinstance(drop_to, GroupTypes):
            return self.on_drop_metacontact_dgroup(clist_obj, area, drop_to)
        elif isinstance(drop_to, MetaContact):
            return self.on_drop_metacontact_metacontact(
                clist_obj, area, drop_to)
        elif isinstance(drop_to, Contact):
            return self.on_drop_metacontact_contact(clist_obj, area, drop_to)

    def on_drop_metacontact_dgroup(self, clist_obj, area, drop_to):
        assert area in ('box', 'below')
        assert isinstance(clist_obj, MetaContact)
        blist = profile.blist
        if area == 'below':
            position = blist.DROP_BEGINNING
        else:
            position = blist.DROP_END
        blist.rearrange(clist_obj, area, drop_to, position)
        if not in_same_group(clist_obj, drop_to):
            clist_obj.move_to_group(drop_to.name)

    def do_relative_metacontact(self, clist_obj, area, drop_to):
        drop_group = self.model.parent_of(drop_to)
        profile.blist.rearrange(clist_obj, area, drop_group, drop_to)
        if not in_same_group(clist_obj, drop_group):
            clist_obj.move_to_group(drop_group.name)

    def on_drop_search(self, entry, area, drop_to):
        'rearrange search web items'
        from common.search import searches
        from common import setpref
        entry = entry.searchengine.dict()
        drop_to = drop_to.searchengine.dict()
        engines = [s.dict() for s in searches[:]]

        i = engines.index(entry)
        j = engines.index(drop_to) + (1 if area == 'below' else 0)

        if j > i: j -= 1

        if i != len(engines):
            engines.pop(i)
        engines.insert(j, entry)

        setpref('search.external', engines)

    def on_drop_metacontact_metacontact(self, clist_obj, area, drop_to):
        assert area in ('above', 'box', 'below')
        if area == 'box':
            contacts = list(drop_to) + list(clist_obj)
            diag = MetaContactDialog(self,
                                     contacts,
                                     metacontact=drop_to,
                                     title=_('Merge Contacts'),
                                     order=None)
            try:
                diag.Prompt(
                    ondone=lambda *a, **k: clist_obj.explode(ask=False))
            finally:
                diag.Destroy()
        else:
            self.do_relative_metacontact(clist_obj, area, drop_to)

    def on_drop_metacontact_contact(self, clist_obj, area, drop_to):
        assert area in ('above', 'box', 'below')
        if area == 'box':
            contacts = [drop_to] + list(clist_obj)
            diag = MetaContactDialog(self,
                                     contacts,
                                     metacontact=clist_obj,
                                     title=_('Merge Contacts'),
                                     order=None)
            drop_group = self.model.parent_of(drop_to)

            def morelater(*a, **k):
                profile.blist.rearrange(clist_obj, 'above', drop_group,
                                        drop_to)
                if not in_same_group(clist_obj, drop_group):
                    clist_obj.move_to_group(drop_group.name)

            try:
                diag.Prompt(ondone=morelater)
            finally:
                diag.Destroy()
        else:
            self.do_relative_metacontact(clist_obj, area, drop_to)

    def delete_blist_item(self, item):

        if isinstance(item, Contact):
            from gui.protocols import remove_contact
            remove_contact(item, item.remove)
        elif isinstance(item, MetaContact):
            item.explode()
        elif isinstance(item, GroupTypes):
            from gui.protocols import remove_group
            remove_group(item, item.delete)

    def on_drop_contact(self, clist_obj, area, drop_to):
        if isinstance(drop_to, GroupTypes):
            return self.on_drop_contact_dgroup(clist_obj, area, drop_to)
        elif isinstance(drop_to, MetaContact):
            return self.on_drop_contact_metacontact(clist_obj, area, drop_to)
        elif isinstance(drop_to, Contact):
            return self.on_drop_contact_contact(clist_obj, area, drop_to)

    def on_drop_contact_dgroup(self, clist_obj, area, drop_to):
        assert area in ('box', 'below')
        assert isinstance(clist_obj, Contact)
        blist = profile.blist
        if area == 'below':
            position = blist.DROP_BEGINNING
        else:
            position = blist.DROP_END
        success = lambda *a: blist.rearrange(clist_obj, area, drop_to, position
                                             )
        if clist_obj not in drop_to:

            @callsback
            def do_move(result=None, callback=None):
                clist_obj.move_to_group(drop_to.name, callback=callback)

            do_move(success=success)
        else:
            success()

    def do_relative_contact(self, clist_obj, area, drop_to):
        drop_group = self.model.parent_of(drop_to)
        blist = profile.blist
        success = lambda *a: blist.rearrange(clist_obj, area, drop_group,
                                             drop_to)
        if clist_obj not in drop_group:

            @callsback
            def do_move(result=None, callback=None):
                clist_obj.move_to_group(drop_group.name, callback=callback)

            do_move(success=success)
        else:
            success()

    def on_drop_contact_metacontact(self, clist_obj, area, drop_to):
        assert area in ('above', 'box', 'below')
        if area == 'box':
            diag = MetaContactDialog.add_contact(self, drop_to, clist_obj, -1)
            diag.Prompt(ondone=lambda *a: None)
            diag.Destroy()
        else:
            self.do_relative_contact(clist_obj, area, drop_to)

    def on_drop_contact_contact(self, clist_obj, area, drop_to):
        assert area in ('above', 'box', 'below')
        if area == 'box':
            order = ('__meta__', 'above', self.model.parent_of(drop_to),
                     drop_to)
            diag = MetaContactDialog(self, [drop_to, clist_obj], order=order)
            diag.Prompt(ondone=lambda *a: None)
            diag.Destroy()
        else:
            self.do_relative_contact(clist_obj, area, drop_to)

    def get_feedback(self, clientPt):
        # Percent will be the percentage of vertical space the cursor has
        # passed over the item it's on.
        i, percent = self.hit_test_ex(clientPt)
        dragging = self.dragging_obj
        drop_to = self.model[i]
        if i == -1:
            # We must be dragging off into the "void" below the
            # buddylist: put it below the last item.
            i = len(self.model) - 1
            percent = 1
            parent_percent = ('foo', 1)
        else:
            parent_percent = self.hit_test_parent(clientPt)

        from .buddylistrules import target, feedback
        new_to, position = target(self.model, dragging, drop_to, i, percent,
                                  parent_percent)
        feedback_result = feedback(self.model, dragging, new_to, position)
        return new_to, feedback_result

    def GiveFeedback(self, effect, dragsource):
        'Logic for drawing drag and drop indication marks.'

        mousepos = wx.GetMousePosition()
        clientPt = self.ScreenToClient(mousepos)
        new_to, feedback_result = self.get_feedback(clientPt)

        #        ITEM_BOX    = 'box'
        #        GROUP_BOX   = 'group_box'
        #        ABOVE       = 'above'
        #        BELOW       = 'below'
        #        BELOW_GROUP = 'below_group'
        #        DISALLOW    = 'disallow'

        if feedback_result == 'group_box':
            feedback_result = 'box'
#        if feedback_result == 'below_group':
#            feedback_result = 'below'

#        if feedback_result not in ('above', 'below', 'box'):
#            feedback_result = None

        old_hilite = self.hilite
        if feedback_result is not None:
            self.hilite = (feedback_result, self.model.index_of(new_to),
                           new_to)
        else:
            self.hilite = None

        # Is there a better way to do this? Keep track of previous mouse
        # positions, perhaps?


#        if self.hilite is not None:
#            area, i, drop_to = self.hilite
#            if area == 'disallow':
#                dragsource.SetCursor(effect, wx.StockCursor(wx.CURSOR_NO_ENTRY))
#            else:
#                dragsource.SetCursor(effect, wx.StockCursor(wx.CURSOR_COPY_ARROW))

# only refresh lines that need it
        if self.hilite != old_hilite:
            #            print self.hilite, old_hilite
            #            print [old_hilite[1] if old_hilite else -1, self.hilite[1] if self.hilite else -1]
            hilites = filter(lambda a: a != -1, [
                old_hilite[1] if old_hilite else -1,
                self.hilite[1] if self.hilite else -1
            ])
            i, j = min(hilites), max(hilites)
            self.RefreshLines(max(0, i - 1), min(self.GetItemCount() - 1,
                                                 j + 1))
            if old_hilite and old_hilite[0] == 'below_group':
                if self.model.is_expanded(old_hilite[1]):
                    self.RefreshLine(old_hilite[1] + len(old_hilite[2]))
            if self.hilite and self.hilite[0] == 'below_group':
                if self.model.is_expanded(self.hilite[1]):
                    self.RefreshLine(self.hilite[1] + len(self.hilite[2]))

        self.dragResult = wx.DragNone if not self.hilite or self.hilite[
            0] == 'disallow' else wx.DragMove

        return False

    def search(self, search_string):
        self.fallback_selection = 1
        profile.blist.search(search_string, self._on_search_cb)

    def _on_search_cb(self, results):
        # results is num contacts in (prevSearch, thisSearch)
        # if we went from finding no contacts to finding some contacts, select the first one.
        prev, this = results
        if prev == -1 and this > 0:
            wx.CallAfter(self.SetSelection, 1)

    def clear_search(self):
        profile.blist.search('')