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
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)
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 ' ' fmt = self.tstamp_fmt if fmt is None else fmt for s, repl in strftime_replacements: fmt = fmt.replace(s, repl)
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()
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)
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)
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 ' ' fmt = self.tstamp_fmt if fmt is None else fmt for s, repl in strftime_replacements: fmt = fmt.replace(s, repl)
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)
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)
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)
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',' ') 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]
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)
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)
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())
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)
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
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)
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('')