Welcome, guest | Sign In | My Account | Store | Cart
import functools
from collections import defaultdict

from chatbox import Chatbox, User_Message, Notification_Message, Notification_Of_Private_Message
from ordered_listbox import Tagged_and_Ordered_Dictbox

try:
    from Tkinter import StringVar, Text, Frame, Button, PanedWindow, Scrollbar, Label, Entry, Menu, TclError
    from Tkconstants import *
    import ttk
    import tkFont
except ImportError:
    from tkinter import StringVar, Text, Frame, Button, PanedWindow, Scrollbar, Label, Entry, Menu, TclError
    from tkinter.constants import *
    import tkinter.ttk as ttk
    from tkinter import font as tkFont


def autoscroll(sbar, first, last):
    """Hide and show scrollbar as needed."""
    first, last = float(first), float(last)
    if first <= 0 and last >= 1:
        sbar.grid_remove()
    else:
        sbar.grid()
    sbar.set(first, last)

def to_tkFont(font_spec):
    if isinstance(font_spec, tkFont.Font):
        return font_spec

    font_spec_length = len(font_spec)
    
    if font_spec_length == 1:
        family, = font
        return tkFont.Font(family=family)
    elif font_spec_length == 2:
        family, size = font_spec
        return tkFont.Font(family=family, size=size)
    elif font_spec_length == 3:
        family, size, weight = font_spec
        return tkFont.Font(family=family, size=size, weight=weight)

def _nested_value(obj, *list_of_keys):
    for key in list_of_keys:
        if isinstance(obj, dict):            
            if key in obj:
                obj = obj[key]
            else:
                return None
        else:
            return None
    return obj

# TODO: Add frame topic
class Channel(object):
    def __init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, entry_controls, style):
        self._my_nick_var = my_nick_var
        my_nick_var.trace_variable('w', lambda name, index, mode: self._chatbox.set_nick(self._my_nick_var.get()))
        
        self._channel_ID = channel_ID
        self._name = channel_name
        self._messenger = messenger

        self.interior = Frame(master, class_="Channel")
        self.interior.channel = self
        
        self._is_closed = False
        
        self._connection = connection

    def _build_chatbox(self, master, logging_file, style):
        kwargs = _nested_value(style, "Channel", "Chatbox")
        
        if not kwargs:
            kwargs = {}

        scrollbar_style = _nested_value(style, "Scrollbar")
        
        if scrollbar_style:
            for style_name in ("background", "troughcolor"):
                if style_name in scrollbar_style:
                    kwargs.setdefault("scrollbar_" +style_name, scrollbar_style[style_name])

        kwargs["logging_file"]=logging_file
        kwargs["my_nick"] = self._my_nick_var.get()
        kwargs["command"] = self._on_message_sent

        self._chatbox = Chatbox(master, **kwargs)
        return self._chatbox

    @property
    def channel_ID(self):
        return self._channel_ID
    
    @property
    def name(self):
        return self._name
    
    @property
    def messenger(self):
        return self._messenger

    @property
    def logging_file(self):
        return self._chatbox.logging_file
        
    @property
    def is_closed(self):
        return self._is_closed
        
    def bind_entry(self, event, handler):
        self._chatbox.bind_entry(event, handler)
        
    def bind_textarea(self, event, handler):
        self._chatbox.bind_textarea(event, handler)
        
    def bind_tag(self, tagName, sequence, func, add=None):
        self._chatbox.bind_tag(tagName, sequence, func, add=add) 

    def user_message(self, content, user):
        if self._is_closed:
            raise Exception("Channel is closed: %s"%self._channel_ID)
        else:
            self._chatbox.user_message(content, user)
    
    def notification_message(self, content, tag=None):
        if self._is_closed:
            raise Exception("Channel is closed: %s"%self._channel_ID)
        else:
            self._chatbox.notification(content, tag)
            
    def notification_of_private_message(self, content, from_, to):
        if self._is_closed:
            raise Exception("Channel is closed: %s"%self._channel_ID)
        else:
            if to is None:
                to = self._my_nick_var.get()
                
            if from_ is None:
                from_ = self._my_nick_var.get()

            self._chatbox.notification_of_private_message(content, from_, to)

    def send(self, content):
        if self._is_closed:
            raise Exception("Channel is closed: %s"%self.channel_ID)
        else:
            self._chatbox.send(content)

    def _on_message_sent(self, content):
        self._connection.trigger("i-send-a-message", {"channel_ID":self._channel_ID, "channel_name":self._name, "content":content})

    def add_messages(self, list_of_messages):
        if self._is_closed:
            raise Exception("Channel is closed: %s"%self.channel_ID)
        else:
            self._history.add_list_of_messages(list_of_messages)

    def close(self):
        self._is_closed = True
        self._connection.trigger("channel-closed", {"channel_ID":self._channel_ID, "channel_name":self._name})
        
    def set_nick(self, nick):
        self._my_nick_var.set(nick)
        
    def get_nick(self):
        return self._my_nick_var.get()
    
    def focus_entry(self):
        self._chatbox.focus_entry()

class Public_Channel(Channel):
    def __init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, panedwindow_ratio=0.8, topic=None, logging_file=None, maximum_lines=None, entry_controls=None, style=None):
        Channel.__init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, entry_controls, style)

        self._panedwindow = panedwindow = PanedWindow(self.interior, orient=HORIZONTAL, opaqueresize= False, sashpad =1)
        panedwindow.pack(expand=True, fill=BOTH)

        chatbox = self._build_chatbox(panedwindow, logging_file=logging_file, style=style)
        left_pane = chatbox.interior
            
        chatbox.interior.pack(expand=True, fill=BOTH)

        self._user_list = User_List(panedwindow, connection, style=style)
        right_pane = self._user_list.interior
    
        panedwindow.add(left_pane)    
        panedwindow.add(right_pane)
        
        panedwindow.update_idletasks()
        panedwindow.sash_place(0, int(round(panedwindow.winfo_reqwidth()*panedwindow_ratio)), 1)

        self.interior.bind('<Configure>', self._adjust_panedwindow)
        
    def _adjust_panedwindow(self, event):
        x = event.width - self._user_list.interior.winfo_reqwidth()

        self.interior.update_idletasks()
        self._panedwindow.sash_place(0, x, 1)
        
    def add_user(self, user):
        self._user_list.add_user(user)
        
    def delete_user(self, user):
        self._user_list.delete_user(user)
    
    def deselect_user(self):
        self._user_list.deselect()

    def set_tag(self, user, tag):
        self._user_list.set_tag(user, tag)

    def delete_tag(self, user, tag):
        self._user_list.delete_tag(user, tag)

    @property
    def users(self):
        return self._user_list.user_names

    @property
    def topic(self):
        return self._chatbox.topic

    @topic.setter
    def topic(self, topic):
        self._chatbox.topic = topic

class Private_Channel(Channel):
    def __init__(self, master, connection, messenger, channel_ID,  channel_name, my_nick_var, logging_file=None, entry_controls=None, style=None):
        Channel.__init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, entry_controls, style)

        chatbox = self._build_chatbox(self.interior, logging_file=logging_file, style=style)
        chatbox.interior.pack(expand=True, fill=BOTH)

# Modificar envio de mensajes. COntrolar mensajes no leidos
# NO existe mensajes no leidos
# Llevar registro de canales que son mostrados
# Modificar mensaje en entry

class Panel_Of_Channel_Names(object):

    def __init__(self, master, connection, style=None, add_at_the_beginning=True):
        self.interior = Frame(master, takefocus=1, class_="Panel_Of_Channel_Names")
        
        self._connection = connection
        
        self.interior.grid_columnconfigure(0, weight=1)
        self.interior.grid_rowconfigure(0, weight=1)

        self._vsb = Scrollbar(self.interior, takefocus=0)
        self._vsb.grid(column=1, row=0, sticky=N+S)

        #self._treeview = ttk.Treeview(self.interior, yscrollcommand=lambda f, l: autoscroll(self._vsb, f, l), takefocus=0,selectmode="browse", show="headings", columns=("#1"), style="Panel_Of_Channels.Treeview")
        self._treeview = ttk.Treeview(self.interior, yscrollcommand=lambda f, l: autoscroll(self._vsb, f, l), takefocus=0,selectmode="browse", show="headings", columns=("#1"), style="Panel_Of_Channels.Treeview")
        self._treeview.grid(column=0, row=0, sticky=N+S+W+E)

        self._treeview.column("#0", stretch= True, anchor="w")
        self._treeview.heading("#0",text="messenger")

        self._treeview.column("#1", stretch= True, anchor="w")
        self._treeview.heading("#1",text="channels")

        self._treeview.bind("<Double-Button-1>", self._on_select_item)
        self._treeview.bind('<Return>', self._on_select_item)

        self._treeview.bind('<Escape>', lambda event: self.deselect())

        self._vsb["command"]=self._treeview.yview
        
        if style is not None:
            ttk_style = ttk.Style()

            if "Panel_Of_Channel_Names" in style:
                panel_style = style["Panel_Of_Channel_Names"]

                if "messenger_column" in panel_style:
                    messenger_column_style = panel_style["messenger_column"]
                    self._treeview.column("#0", **messenger_column_style)
                    
                if "channel_column" in panel_style:
                    channel_column_style = panel_style["channel_column"]
                    self._treeview.column("#1", **channel_column_style)

                if "background" in panel_style:
                    background = panel_style["background"]
                    ttk_style.configure("Panel_Of_Channels.Treeview", background=background, fieldbackground=background)
                
                if "foreground" in panel_style:
                    foreground = panel_style["foreground"]
                    ttk_style.configure("Panel_Of_Channels.Treeview", foreground=foreground)
                
                if "font" in panel_style:
                    font = panel_style["font"]

                    ttk_style.configure("Panel_Of_Channels.Treeview", font=font, rowheight=to_tkFont(font).metrics("linespace"))
                    
                if "tags" in panel_style:
                   style_tags = panel_style["tags"]
                   for tag, config_tag in style_tags.items():
                       self._treeview.tag_configure(tag, **config_tag)
            
            if "Scrollbar" in style:
                scrollbar_style = style["Scrollbar"]
                
                if "background" in style:
                    self._vsb.configure(background=scrollbar_style["background"])
                
                if "throughcolor" in style:
                    self._vsb.configure(throughcolor=scrollbar_style["throughcolor"])

        self._unread_messages = defaultdict(int)
        
        self._show_messengers = False
        self._add_at_the_beginning = add_at_the_beginning

        self._treeview.bind("<Button-3>", self._generate_context_menu_event)

    def add_messenger(self, messenger, open=True):
        if not self._show_messengers:
            self._treeview.configure(show="tree headings")
            self._show_messengers = True

        messenger_IID = self._messenger_IID(messenger)

        self._treeview.insert("", END, messenger_IID, text=messenger)
        self._treeview.item(messenger_IID, open=open)

        return messenger_IID

    def delete_messenger(self, messenger):
        messenger_IID = self._messenger_IID(messenger)
        self._treeview.delete(messenger_IID)
        
    def _messenger_IID(self, messenger):
        return "+"+messenger

    def add_channel_name(self, channel_name, channel_ID, messenger=None, tags=None, image=None):
        if channel_ID[0] == "+":
            raise ValueError("Channel ID can't start with '+'")

        kwargs = {}
        if tags is not None:
            kwargs["tags"] = tags
            
        if image is not None:
            kwargs["image"] = image

        if messenger is None:
            iid = ""
        else:
            iid = self.add_messenger(messenger)

        self._treeview.insert(iid, END, channel_ID, values=(channel_name,), **kwargs)
        self._treeview.update_idletasks()

    def delete_channel_name(self, channel_ID):
        if channel_ID[0] == "+":
            raise ValueError("Channel ID can't start with '+'")

        self._treeview.delete(channel_ID)

    def set_tag(self, channel_ID, tag):
        tags = list(self._treeview.item(channel_ID, option="tags"))
        if not tags:
            tags = []

        if not tag in tags:
            tags.append(tag)
        
        self._treeview.item(channel_ID, tags=tags)

    def delete_tag(self, channel_ID, tag):
        tags = list(self._treeview.item(channel_ID, option="tags"))
        
        try:
            index = tags.index(tag)
        except ValueError:
            return

        tags.pop(index)
        self._treeview.item(channel_ID, tags=tags)

    def select_first_on_list(self):
        list_of_items = self._treeview.get_children()
        if len(list_of_items) ==0:
            return
            
        first_item = list_of_items[0]
        self.select(first_item)

    def select_next(self):
        selection = self._treeview.selection()
    
        if len(selection) ==0:
            self.select_first_on_list()
        else:
            item = selection[0]
            
            next_item = self._treeview.next(item)
            if next_item != "":
                self.select(next_item)
        
    def select_previous(self):
        selection = self._treeview.selection()
    
        if len(selection) ==0:
            self.select_first_on_list()
        else:
            item = selection[0]
            
            prev_item = self._treeview.prev(item)
            if prev_item != "":
                self.select(prev_item)

    def deselect(self):
        selection = self._treeview.selection()
    
        for item in selection:
            self._treeview.selection_remove(item)
            
    def select(self, channel_ID):
        self._treeview.focus_set()
        self._treeview.selection_set((channel_ID,channel_ID))
        self._treeview.focus(channel_ID)

    def clear(self):
        self._treeview.delete(*self._treeview.get_children())
        
    def bind(self, event, handler):
        self._treeview.bind(event, handler)

    def __iter__(self):
        return self._treeview.get(0, END)

    def _on_select_item(self, event):
        selected_items = self._treeview.selection()
        if selected_items == "": return

        item_ID = selected_items[0]
        if item_ID[0] == "+":
            self._connection.trigger('messenger-selected',item_ID[1:])
        else:
            self._connection.trigger('channel-selected',item_ID)
        
    def _generate_context_menu_event(self, event):
        # http://stackoverflow.com/questions/12014210/python-tkinter-app-adding-a-right-click-context-menu
        iid = self.tree.identify('item',event.x, event.y)
        # se tiene que capturar el channel ID
        # channel_ID = 

        self._connection.trigger('panel-of-channel-names-context-menu',{"metadata":{"iid": iid}, "x_root":event.x_root, "y_root":event.y_root})

    def notifify_messages_are_unread(self, channel_ID):
        if self._unread_messages[channel_ID] == 0:
            self.set_tag(channel_ID, "unread_messages")

        self._unread_messages[channel_ID] += 1

        if self._add_at_the_beginning:
            self._treeview.move(channel_ID,self._treeview.parent(channel_ID),0)

    def notifify_messages_are_read(self, channel_ID):
        if self._unread_messages[channel_ID] > 0:
            self.delete_tag(channel_ID, "unread_messages")

        self._unread_messages[channel_ID] = 0

def validate_channel(func):
    @functools.wraps(func)
    def wrapped(self, channel_ID, *args, **kwargs):
        if channel_ID in self._channels:
            return func(self, channel_ID, *args, **kwargs)
        else:
            raise ValueError("Not a valid channel ID: %s"%channel_ID)
    return wrapped

# Guardar tambien variable nick por cada messenger
# Se debe retocar la parte del nick

class Messenger(object):
    def __init__(self, name=None, nick=None):
        self.name = name
        self.nick_var = StringVar()
        self.channels = set()
        
        if nick is not None:
            self.set_nick(nick)

    def get_nick(self, nick):
        return self.nick_var.get()

    def set_nick(self, nick):
        self.nick_var.set(nick)
        
    def add_channel(self, channel_ID):
        self.channels.add(channel_ID)
        
    def delete_channel(self, channel_ID):
        self.channels.remove(channel_ID)
        
    def __iter__(self):
        return self.channels

class Connection(object):
    def __init__(self):
        self._callbacks = defaultdict(list)

    def on(self, event_name, callback):
        self._callbacks[event_name].append(callback)
        
    def off(self, event_name, *args):
        if len(args) == 0:
            del self._callbacks[event_name]
        else:
            list_of_callbacks = self._callbacks[event_name]
            for callback in args:
                try:
                    index = list_of_callbacks.index(callback)
                except ValueError:
                    continue
                    
                list_of_callbacks.pop(index)
                
    def trigger(self, event_name, data=None):
        for callback in self._callbacks[event_name]:
            callback(data)

def _menu_builder(f):
    
    def wrapped(data):
    
        menu = Menu(tearoff=0)
        f(menu, data["metadata"])

        menu.tk_popup(data["x_root"], data["y_root"], 0)
        menu.destroy()
    
    return wrapped

# Utilizar conexion para notificar mensajes leidos

class Messenger_MegaWidget(object):
    _ID = 0

    def __init__(self, master, my_nick, style=None, entry_controls=None):
        self.interior = interior = Frame(master, class_="Chat_MegaWidget")
        
        self._style = style
        
        self._entry_controls = entry_controls

        self._connection = Connection()
        self._connection.on("channel-selected", self._on_select_channel)

        top_panel = PanedWindow(interior, orient=HORIZONTAL, opaqueresize= False, sashpad =1)
        top_panel.pack(expand=YES, fill=BOTH)

        self._notebook = ttk.Notebook(top_panel, style="Channels_Notebook.TNotebook")
        self._notebook.enable_traversal()
        self._notebook.bind('<3>', self._open_tab_context_menu)

        if "Tabs" in style:
            notebook_style = style["Tabs"]
            if "width" in notebook_style:
                self._notebook.configure(width=notebook_style["width"])
                
            if "height" in notebook_style:
                self._notebook.configure(height=notebook_style["height"])
            
            if "font" in notebook_style:
                ttk.Style().configure("Channels_Notebook.TNotebook.Tab", font=notebook_style["font"])

        self._notebook.bind("<<NotebookTabChanged>>", lambda event: self._on_tab_changed())

        self._panel_of_channel_names = Panel_Of_Channel_Names(top_panel, self._connection, style=style)
        self._panel_of_channel_names.interior.pack(expand=True, fill=BOTH)


        top_panel.add(self._panel_of_channel_names.interior)
        top_panel.add(self._notebook)

        self._channels = {}
        
        self._panel_of_channel_names.bind("<Escape>", lambda event: (self.deselect_chats(),self._entry.focus()))

        interior.bind_all('<Control-KeyPress-m>', lambda event: self._entry.focus())
        interior.bind_all('<Control-KeyPress-M>', lambda event: self._entry.focus())

        self._messengers = {
            None: Messenger(nick=my_nick)
        }

    def _open_tab_context_menu(self, event):
        if event.widget.identify(event.x, event.y) == 'label':
            index = event.widget.index('@%d,%d' % (event.x, event.y))
            #obtener aqui el canal
            #tab_rint event.widget.tab(index, 'text')
            self._connection.on("tab-context-menu", {"x_root":event.x_root, "y_root":event.y_root})
        return "break"

    def _on_select_channel(self, channel_ID):
        channel = self._channels[channel_ID]
        
        try:
            self._notebook.select(channel.interior)
        except TclError:
            self._notebook.add(channel.interior, text=channel.name)
            self._notebook.select(channel.interior)

        channel.focus_entry()

    def deselect_channel_on_panel(self):
        self._panel_of_channel_names.deselect()
        
    @validate_channel
    def select_channel_on_panel(self, channel_ID):
        self._panel_of_channel_names.select(channel_ID)
    
    @validate_channel        
    def untag_channel(self, channel_ID, tag):
        self._panel_of_channel_names.delete_tag(channel_ID, tag)

    @validate_channel
    def tag_channel(self, channel_ID, tag):
        self._panel_of_channel_names.set_tag(channel_ID, tag)

    @validate_channel
    def select_channel_on_notebook(self, channel_ID):
        channel = self._channels[channel_ID]
        self._notebook.select(channel.interior)
        
    def add_messenger(self, name):
        if name in self._messengers:
            raise ValueError("Messenger already created: %s"%name)

        self._messengers[name] = Messenger(name=name)
        self._panel_of_channel_names.add_messenger(name)

    def delete_messenger(self, name):
        if name not in self._messengers:
            raise ValueError("Messenger not exists: %s"%name)

        for channel_ID in self._messengers[name]:
            self.close_channel(channel_ID)

        del self._messengers[name]
        self._panel_of_channel_names.delete_messenger(name)

    def new_channel(self, channel_name, channel_ID=None, messenger=None, start_open=None, tags=None, image=None, public=False):
        if channel_ID is None:
            channel_ID = "channel"+str(self._ID)
            self._ID += 1
        else:
            if channel_ID[0] == "+":
                raise ValueError("Channel ID can not start with '+': %s"%channel_ID)
            
            if channel_ID in self._channels:
                raise ValueError("Channel is already created: %s"%channel_ID)

        if messenger in self._messengers:
            messenger_instance = self._messengers[messenger]
        else:
            messenger_instance = self._messengers[messenger] = Messenger(name=messenger)
            self._panel_of_channel_names.add_messenger(messenger)

        my_nick_var = messenger_instance.nick_var

        if public:
            if start_open is None:
                start_open = True
            channel = Public_Channel(self._notebook, self._connection, messenger_instance, channel_ID, channel_name, my_nick_var, style=self._style, entry_controls=self._entry_controls)
        else:
            if start_open is None:
                start_open = False

            channel = Private_Channel(self._notebook, self._connection, messenger_instance, channel_ID, channel_name, my_nick_var, style=self._style, entry_controls=self._entry_controls)

        channel.bind_entry("<Control-W>", lambda event, channel_ID=channel.channel_ID: self.hide_channel(channel_ID))
        channel.bind_entry("<Control-w>", lambda event, channel_ID=channel.channel_ID: self.hide_channel(channel_ID))
        channel.bind_entry('<Up>', lambda event:  self._panel_of_channel_names.select_previous())
        channel.bind_entry('<Down>', lambda event: self._panel_of_channel_names.select_next())

        self._channels[channel_ID] = channel

        self._panel_of_channel_names.add_channel_name(channel_name, channel_ID, messenger=messenger, tags=tags, image=image)
        messenger_instance.add_channel(channel_ID)

        if start_open:
            self._notebook.add(channel.interior, text=channel_name)

        return channel_ID

    def on(self, event_name, callback):
        self._connection.on(event_name, callback)
        
    def trigger(self, event_name, data):
        self._connection.trigger(event_name, data)

    @validate_channel
    def show_channel(self, channel_ID, select=False):
        channel = self._channels[channel_ID]
        self._notebook.add(channel.interior)

        if select:
            self._notebook.select(channel.interior)                

    def hide_channel(self, channel_ID=None):
        if channel_ID is None:
            channel = self.current_channel
        else:
            if channel_ID in self._channels:
                channel = self._channels[channel_ID]
            else:
                raise ValueError("Not a valid channel ID: %s"%channel_ID)

        self._notebook.hide(channel.interior)

    @validate_channel
    def close_channel(self, channel_ID=None):
        if channel_ID is None:
            channel = self.current_channel
            channel_ID = channel.channel_ID
        else:
            channel = self._channels[channel_ID]

        self._panel_of_channel_names.delete_channel_name(channel_ID)

        messenger = channel.messenger
        channel.interior.destroy()
        del self._channels[channel_ID]
        self._messengers[messenger].delete_channel(channel_ID)
        
        channel.close()

    def focus_entry(self):
        channel = self.current_channel
        if channel is not None:
            channel.focus_entry_set()

    @property
    def my_nick(self):
        return self._messengers[None].get_nick()

    @my_nick.setter
    def my_nick(self, new_nick):
        return self._messengers[None].set_nick(new_nick)

    def set_nick_on_messenger(self, messenger, nick):
        for channel_ID in self._messengers[messenger]:
            channel = self._channels[channel_ID]
            channel.set_nick(nick)
        
    def send(self, message, channel_ID=None):
        if channel_ID is None:
            channel = self.current_channel
            if self.current_channel is None:
                return
        else:
            channel = self._channels[channel_ID]

        channel.send(message)

    @validate_channel
    def user_message(self, channel_ID, nick, content):
        channel = self._channels[channel_ID]
        channel.user_message(nick, content)
        
        if channel != self.current_channel:
            self._notifify_messages_are_unread(channel_ID)

    @validate_channel
    def notification_message(self, channel_ID, content, notification_type=None):
        channel = self._channels[channel_ID]
        channel.notification_message(content, notification_type)
        
        if channel != self.current_channel:
            self._notifify_messages_are_unread(channel_ID)

    @validate_channel
    def notification_of_private_message(self, channel_ID, content, from_=None, to=None):
        channel = self._channels[channel_ID]
        channel.notification_of_private_message(content, from_=from_, to=to)
        
        if channel != self.current_channel:
            self._notifify_messages_are_unread(channel_ID)

    def change_nick(self, new_nick, messenger=None):
        self._messengers[messenger].set_nick(new_nick)

    @validate_channel
    def __getitem__(self, channel_ID):
        return self._channels[channel_ID]

    @validate_channel
    def __delitem__(self, channel_ID):
        self.close_channel(channel_ID)
        
    def __contains__(self, channel_ID):
        return channel_ID in self._channels
        
    @property
    def current_channel(self):
        widget_name = self._notebook.select()
        if widget_name:
            return self.interior.nametowidget(widget_name).channel
        else:
            return None

    def _on_tab_changed(self):
        channel = self.current_channel
        channel_ID = channel.channel_ID
        
        channel.focus_entry()
        
        self._notifify_messages_are_read(channel_ID)

    def _notifify_messages_are_read(self, channel_ID):
        self._panel_of_channel_names.notifify_messages_are_read(channel_ID)
        
        channel = self._channels[channel_ID]
        self._notebook.tab(channel.interior, text= channel.name)

    def _notifify_messages_are_unread(self, channel_ID):
        self._panel_of_channel_names.notifify_messages_are_unread(channel_ID)
        
        channel = self._channels[channel_ID]
        try:
            self._notebook.tab(channel.interior, text= "[*] "+channel.name)
        except TclError:
            pass
        
if __name__ == "__main__":
    
    style = {
        "Tabs":{
            "width":600,
            "height": 600,
            "font": ("TkDefaultFont", 11)
        },
        "Scrollbar": {
            "background":"#7f0303", 
            "troughcolor" :"#cc2b00"
        },
        "Panel_Of_Channel_Names": {
            "messenger_column": {
                "width": 100
            },
            "channel_column": {
                "width": 100
            },
            "background":"#676760", 
            "foreground":"#f7f7f7",
            "font": ("Courier New", 12),
            "tags": {
                "unread_messages": {
                    "foreground": "red"
                }
            }
        },
        "Channel": {
            "Chatbox": {
                "timestamp_template": "[%H:%M:%S]",
                "history_background":"#676760",
                "history_font": ("Courier New", 12),
                "label_font": ("TkDefaultFont", 10, "bold"),
                "entry_font": ("Courier New", 12),
                "entry_background": "#676760", 
                "entry_foreground":"#f7f7f7",
                "tags": {
                    "timestamp": {
                        "foreground":"#f7f7f7"
                    },
                    "nick": {
                        "foreground":"#f7f7f7"
                    },
                    "notification": {
                        "foreground":"#f7f7f7"
                    },
                    "notification_of_private_message": {
                        "foreground":"#f7f7f7"
                    },
                    "user_message": {
                        "foreground":"#f7f7f7"
                    }
                }
            },
            "User_List": {
                "width": 100,
                "background":"#676760", 
                "foreground":"#f7f7f7",
                "font": ("Courier New", 12)
            }
        }
    }

    try:
        from Tkinter import Tk
    except ImportError:
        from tkinter import Tk

    root = Tk()
    root.title("Chat megawidget")

    chat = Messenger_MegaWidget(root, my_nick="user123", style=style)
    chat.interior.pack(expand=True, fill=BOTH)


    channel_ID = chat.new_channel("#barcelona", start_open=True)

    chat.new_channel("johny23")
    chat.new_channel("Samatha32")

    chat.user_message(channel_ID, "john", "hi everybody")
    for i in range(100): chat.send("hola caracola")

    root.mainloop()

Diff to Previous Revision

--- revision 1 2017-02-23 22:44:38
+++ revision 2 2017-02-23 22:46:27
@@ -1,257 +1,898 @@
-# Author: Miguel Martinez Lopez
-# Uncomment the next line to see my email
-# print("Author's email: %s"%"61706c69636163696f6e616d656469646140676d61696c2e636f6d".decode("hex"))
-
-import datetime
-import collections
+import functools
+from collections import defaultdict
+
+from chatbox import Chatbox, User_Message, Notification_Message, Notification_Of_Private_Message
+from ordered_listbox import Tagged_and_Ordered_Dictbox
 
 try:
-    from Tkinter import StringVar, Text, Frame, PanedWindow, Scrollbar, Label, Entry
+    from Tkinter import StringVar, Text, Frame, Button, PanedWindow, Scrollbar, Label, Entry, Menu, TclError
     from Tkconstants import *
     import ttk
+    import tkFont
 except ImportError:
-    from tkinter import StringVar, Text, Frame, PanedWindow, Scrollbar, Label, Entry
+    from tkinter import StringVar, Text, Frame, Button, PanedWindow, Scrollbar, Label, Entry, Menu, TclError
     from tkinter.constants import *
     import tkinter.ttk as ttk
-
-User_Message = collections.namedtuple('User_Message', 'nick content')
-Notification_Message = collections.namedtuple('Notification_Message', 'content tag')
-Notification_Message.__new__.__defaults__ = ('notification',)
-
-Notification_Of_Private_Message = collections.namedtuple('Notification_Message', 'content from_ to')
-
-# Dar la posibilidad de agregar frame de tema
-class Chatbox(object):
-    def __init__(self, master, my_nick=None, command=None, topic=None, entry_controls=None, maximum_lines=None, timestamp_template=None, scrollbar_background=None, scrollbar_troughcolor=None, history_background=None, history_font=None, history_padx=None, history_pady=None, history_width=None, entry_font=None, entry_background=None, entry_foreground=None, label_template=u"{nick}", label_font=None, logging_file=None, tags=None):
-        self.interior = Frame(master, class_="Chatbox")
-
-        self._command = command
-
-        self._is_empty = True
-
-        self._maximum_lines = maximum_lines
-        self._timestamp_template = timestamp_template
-        
-        self._command = command
-
-        self._label_template = label_template
-        
-        self._logging_file = logging_file
-        
-        if logging_file is None:
-            self._log = None
-        else:
-            try:
-                self._log = open(logging_file, "r")
-            except:
-                self._log = None
-        
-        top_frame = Frame(self.interior, class_="Top")
-        top_frame.pack(expand=True, fill=BOTH)
+    from tkinter import font as tkFont
+
+
+def autoscroll(sbar, first, last):
+    """Hide and show scrollbar as needed."""
+    first, last = float(first), float(last)
+    if first <= 0 and last >= 1:
+        sbar.grid_remove()
+    else:
+        sbar.grid()
+    sbar.set(first, last)
+
+def to_tkFont(font_spec):
+    if isinstance(font_spec, tkFont.Font):
+        return font_spec
+
+    font_spec_length = len(font_spec)
+    
+    if font_spec_length == 1:
+        family, = font
+        return tkFont.Font(family=family)
+    elif font_spec_length == 2:
+        family, size = font_spec
+        return tkFont.Font(family=family, size=size)
+    elif font_spec_length == 3:
+        family, size, weight = font_spec
+        return tkFont.Font(family=family, size=size, weight=weight)
+
+def _nested_value(obj, *list_of_keys):
+    for key in list_of_keys:
+        if isinstance(obj, dict):            
+            if key in obj:
+                obj = obj[key]
+            else:
+                return None
+        else:
+            return None
+    return obj
+
+# TODO: Add frame topic
+class Channel(object):
+    def __init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, entry_controls, style):
+        self._my_nick_var = my_nick_var
+        my_nick_var.trace_variable('w', lambda name, index, mode: self._chatbox.set_nick(self._my_nick_var.get()))
+        
+        self._channel_ID = channel_ID
+        self._name = channel_name
+        self._messenger = messenger
+
+        self.interior = Frame(master, class_="Channel")
+        self.interior.channel = self
+        
+        self._is_closed = False
+        
+        self._connection = connection
+
+    def _build_chatbox(self, master, logging_file, style):
+        kwargs = _nested_value(style, "Channel", "Chatbox")
+        
+        if not kwargs:
+            kwargs = {}
+
+        scrollbar_style = _nested_value(style, "Scrollbar")
+        
+        if scrollbar_style:
+            for style_name in ("background", "troughcolor"):
+                if style_name in scrollbar_style:
+                    kwargs.setdefault("scrollbar_" +style_name, scrollbar_style[style_name])
+
+        kwargs["logging_file"]=logging_file
+        kwargs["my_nick"] = self._my_nick_var.get()
+        kwargs["command"] = self._on_message_sent
+
+        self._chatbox = Chatbox(master, **kwargs)
+        return self._chatbox
+
+    @property
+    def channel_ID(self):
+        return self._channel_ID
+    
+    @property
+    def name(self):
+        return self._name
+    
+    @property
+    def messenger(self):
+        return self._messenger
+
+    @property
+    def logging_file(self):
+        return self._chatbox.logging_file
+        
+    @property
+    def is_closed(self):
+        return self._is_closed
+        
+    def bind_entry(self, event, handler):
+        self._chatbox.bind_entry(event, handler)
+        
+    def bind_textarea(self, event, handler):
+        self._chatbox.bind_textarea(event, handler)
+        
+    def bind_tag(self, tagName, sequence, func, add=None):
+        self._chatbox.bind_tag(tagName, sequence, func, add=add) 
+
+    def user_message(self, content, user):
+        if self._is_closed:
+            raise Exception("Channel is closed: %s"%self._channel_ID)
+        else:
+            self._chatbox.user_message(content, user)
+    
+    def notification_message(self, content, tag=None):
+        if self._is_closed:
+            raise Exception("Channel is closed: %s"%self._channel_ID)
+        else:
+            self._chatbox.notification(content, tag)
+            
+    def notification_of_private_message(self, content, from_, to):
+        if self._is_closed:
+            raise Exception("Channel is closed: %s"%self._channel_ID)
+        else:
+            if to is None:
+                to = self._my_nick_var.get()
                 
-        self._textarea = Text(top_frame, state=DISABLED)
-
-        self._vsb = Scrollbar(top_frame, takefocus=0, command=self._textarea.yview)
-        self._vsb.pack(side=RIGHT, fill=Y)
-
-        self._textarea.pack(side=RIGHT, expand=YES, fill=BOTH)
-        self._textarea["yscrollcommand"]=self._vsb.set
-        
-        entry_frame = Frame(self.interior, class_="Chatbox_Entry")
-        entry_frame.pack(fill=X, anchor=N)
-        
-        if entry_controls is not None:
-            controls_frame = Frame(entry_frame, class_="Controls")
-            controls_frame.pack(fill=X)
-            entry_controls(controls_frame, chatbox=self)
+            if from_ is None:
+                from_ = self._my_nick_var.get()
+
+            self._chatbox.notification_of_private_message(content, from_, to)
+
+    def send(self, content):
+        if self._is_closed:
+            raise Exception("Channel is closed: %s"%self.channel_ID)
+        else:
+            self._chatbox.send(content)
+
+    def _on_message_sent(self, content):
+        self._connection.trigger("i-send-a-message", {"channel_ID":self._channel_ID, "channel_name":self._name, "content":content})
+
+    def add_messages(self, list_of_messages):
+        if self._is_closed:
+            raise Exception("Channel is closed: %s"%self.channel_ID)
+        else:
+            self._history.add_list_of_messages(list_of_messages)
+
+    def close(self):
+        self._is_closed = True
+        self._connection.trigger("channel-closed", {"channel_ID":self._channel_ID, "channel_name":self._name})
+        
+    def set_nick(self, nick):
+        self._my_nick_var.set(nick)
+        
+    def get_nick(self):
+        return self._my_nick_var.get()
+    
+    def focus_entry(self):
+        self._chatbox.focus_entry()
+
+class Public_Channel(Channel):
+    def __init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, panedwindow_ratio=0.8, topic=None, logging_file=None, maximum_lines=None, entry_controls=None, style=None):
+        Channel.__init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, entry_controls, style)
+
+        self._panedwindow = panedwindow = PanedWindow(self.interior, orient=HORIZONTAL, opaqueresize= False, sashpad =1)
+        panedwindow.pack(expand=True, fill=BOTH)
+
+        chatbox = self._build_chatbox(panedwindow, logging_file=logging_file, style=style)
+        left_pane = chatbox.interior
             
-            bottom_of_entry_frame = Frame(entry_frame)
-            self._entry_label = Label(bottom_of_entry_frame)
-            self._entry = Entry(bottom_of_entry_frame)
-        else:            
-            self._entry_label = Label(entry_frame)
-            self._entry = Entry(entry_frame)
-        
-        self._entry.pack(side=LEFT, expand=YES, fill = X)
-        self._entry.bind("<Return>", self._on_message_sent)
-        
-        self._entry.focus()
-
-        if history_background:
-            self._textarea.configure(background=history_background)
-        
-        if history_font:
-            self._textarea.configure(font=history_font)
-
-        if history_padx:
-             self._textarea.configure(padx=history_padx)
-             
-        if history_width:
-             self._textarea.configure(width=history_width)
-
-        if history_pady:
-            self._textarea.configure(pady=history_pady)
-
-        if scrollbar_background:
-            self._vsb.configure(background = scrollbar_background)
-
-        if scrollbar_troughcolor:
-            self._vsb.configure(troughcolor = scrollbar_troughcolor)
-
-        if entry_font:
-            self._entry.configure(font=entry_font)
-
-        if entry_background:
-            self._entry.configure(background=entry_background)
-            
-        if entry_foreground:
-            self._entry.configure(foreground=entry_foreground)
-        
-        if label_font:
-            self._entry_label.configure(font=label_font)
-
-        if tags:
-            for tag, tag_config in tags.items():
-                self._textarea.tag_config(tag, **tag_config)
-                
-        self.set_nick(my_nick)
+        chatbox.interior.pack(expand=True, fill=BOTH)
+
+        self._user_list = User_List(panedwindow, connection, style=style)
+        right_pane = self._user_list.interior
+    
+        panedwindow.add(left_pane)    
+        panedwindow.add(right_pane)
+        
+        panedwindow.update_idletasks()
+        panedwindow.sash_place(0, int(round(panedwindow.winfo_reqwidth()*panedwindow_ratio)), 1)
+
+        self.interior.bind('<Configure>', self._adjust_panedwindow)
+        
+    def _adjust_panedwindow(self, event):
+        x = event.width - self._user_list.interior.winfo_reqwidth()
+
+        self.interior.update_idletasks()
+        self._panedwindow.sash_place(0, x, 1)
+        
+    def add_user(self, user):
+        self._user_list.add_user(user)
+        
+    def delete_user(self, user):
+        self._user_list.delete_user(user)
+    
+    def deselect_user(self):
+        self._user_list.deselect()
+
+    def set_tag(self, user, tag):
+        self._user_list.set_tag(user, tag)
+
+    def delete_tag(self, user, tag):
+        self._user_list.delete_tag(user, tag)
+
+    @property
+    def users(self):
+        return self._user_list.user_names
 
     @property
     def topic(self):
-        return
-        
+        return self._chatbox.topic
+
     @topic.setter
     def topic(self, topic):
-        return
-        
+        self._chatbox.topic = topic
+
+class Private_Channel(Channel):
+    def __init__(self, master, connection, messenger, channel_ID,  channel_name, my_nick_var, logging_file=None, entry_controls=None, style=None):
+        Channel.__init__(self, master, connection, messenger, channel_ID, channel_name, my_nick_var, entry_controls, style)
+
+        chatbox = self._build_chatbox(self.interior, logging_file=logging_file, style=style)
+        chatbox.interior.pack(expand=True, fill=BOTH)
+
+# Modificar envio de mensajes. COntrolar mensajes no leidos
+# NO existe mensajes no leidos
+# Llevar registro de canales que son mostrados
+# Modificar mensaje en entry
+
+class Panel_Of_Channel_Names(object):
+
+    def __init__(self, master, connection, style=None, add_at_the_beginning=True):
+        self.interior = Frame(master, takefocus=1, class_="Panel_Of_Channel_Names")
+        
+        self._connection = connection
+        
+        self.interior.grid_columnconfigure(0, weight=1)
+        self.interior.grid_rowconfigure(0, weight=1)
+
+        self._vsb = Scrollbar(self.interior, takefocus=0)
+        self._vsb.grid(column=1, row=0, sticky=N+S)
+
+        #self._treeview = ttk.Treeview(self.interior, yscrollcommand=lambda f, l: autoscroll(self._vsb, f, l), takefocus=0,selectmode="browse", show="headings", columns=("#1"), style="Panel_Of_Channels.Treeview")
+        self._treeview = ttk.Treeview(self.interior, yscrollcommand=lambda f, l: autoscroll(self._vsb, f, l), takefocus=0,selectmode="browse", show="headings", columns=("#1"), style="Panel_Of_Channels.Treeview")
+        self._treeview.grid(column=0, row=0, sticky=N+S+W+E)
+
+        self._treeview.column("#0", stretch= True, anchor="w")
+        self._treeview.heading("#0",text="messenger")
+
+        self._treeview.column("#1", stretch= True, anchor="w")
+        self._treeview.heading("#1",text="channels")
+
+        self._treeview.bind("<Double-Button-1>", self._on_select_item)
+        self._treeview.bind('<Return>', self._on_select_item)
+
+        self._treeview.bind('<Escape>', lambda event: self.deselect())
+
+        self._vsb["command"]=self._treeview.yview
+        
+        if style is not None:
+            ttk_style = ttk.Style()
+
+            if "Panel_Of_Channel_Names" in style:
+                panel_style = style["Panel_Of_Channel_Names"]
+
+                if "messenger_column" in panel_style:
+                    messenger_column_style = panel_style["messenger_column"]
+                    self._treeview.column("#0", **messenger_column_style)
+                    
+                if "channel_column" in panel_style:
+                    channel_column_style = panel_style["channel_column"]
+                    self._treeview.column("#1", **channel_column_style)
+
+                if "background" in panel_style:
+                    background = panel_style["background"]
+                    ttk_style.configure("Panel_Of_Channels.Treeview", background=background, fieldbackground=background)
+                
+                if "foreground" in panel_style:
+                    foreground = panel_style["foreground"]
+                    ttk_style.configure("Panel_Of_Channels.Treeview", foreground=foreground)
+                
+                if "font" in panel_style:
+                    font = panel_style["font"]
+
+                    ttk_style.configure("Panel_Of_Channels.Treeview", font=font, rowheight=to_tkFont(font).metrics("linespace"))
+                    
+                if "tags" in panel_style:
+                   style_tags = panel_style["tags"]
+                   for tag, config_tag in style_tags.items():
+                       self._treeview.tag_configure(tag, **config_tag)
+            
+            if "Scrollbar" in style:
+                scrollbar_style = style["Scrollbar"]
+                
+                if "background" in style:
+                    self._vsb.configure(background=scrollbar_style["background"])
+                
+                if "throughcolor" in style:
+                    self._vsb.configure(throughcolor=scrollbar_style["throughcolor"])
+
+        self._unread_messages = defaultdict(int)
+        
+        self._show_messengers = False
+        self._add_at_the_beginning = add_at_the_beginning
+
+        self._treeview.bind("<Button-3>", self._generate_context_menu_event)
+
+    def add_messenger(self, messenger, open=True):
+        if not self._show_messengers:
+            self._treeview.configure(show="tree headings")
+            self._show_messengers = True
+
+        messenger_IID = self._messenger_IID(messenger)
+
+        self._treeview.insert("", END, messenger_IID, text=messenger)
+        self._treeview.item(messenger_IID, open=open)
+
+        return messenger_IID
+
+    def delete_messenger(self, messenger):
+        messenger_IID = self._messenger_IID(messenger)
+        self._treeview.delete(messenger_IID)
+        
+    def _messenger_IID(self, messenger):
+        return "+"+messenger
+
+    def add_channel_name(self, channel_name, channel_ID, messenger=None, tags=None, image=None):
+        if channel_ID[0] == "+":
+            raise ValueError("Channel ID can't start with '+'")
+
+        kwargs = {}
+        if tags is not None:
+            kwargs["tags"] = tags
+            
+        if image is not None:
+            kwargs["image"] = image
+
+        if messenger is None:
+            iid = ""
+        else:
+            iid = self.add_messenger(messenger)
+
+        self._treeview.insert(iid, END, channel_ID, values=(channel_name,), **kwargs)
+        self._treeview.update_idletasks()
+
+    def delete_channel_name(self, channel_ID):
+        if channel_ID[0] == "+":
+            raise ValueError("Channel ID can't start with '+'")
+
+        self._treeview.delete(channel_ID)
+
+    def set_tag(self, channel_ID, tag):
+        tags = list(self._treeview.item(channel_ID, option="tags"))
+        if not tags:
+            tags = []
+
+        if not tag in tags:
+            tags.append(tag)
+        
+        self._treeview.item(channel_ID, tags=tags)
+
+    def delete_tag(self, channel_ID, tag):
+        tags = list(self._treeview.item(channel_ID, option="tags"))
+        
+        try:
+            index = tags.index(tag)
+        except ValueError:
+            return
+
+        tags.pop(index)
+        self._treeview.item(channel_ID, tags=tags)
+
+    def select_first_on_list(self):
+        list_of_items = self._treeview.get_children()
+        if len(list_of_items) ==0:
+            return
+            
+        first_item = list_of_items[0]
+        self.select(first_item)
+
+    def select_next(self):
+        selection = self._treeview.selection()
+    
+        if len(selection) ==0:
+            self.select_first_on_list()
+        else:
+            item = selection[0]
+            
+            next_item = self._treeview.next(item)
+            if next_item != "":
+                self.select(next_item)
+        
+    def select_previous(self):
+        selection = self._treeview.selection()
+    
+        if len(selection) ==0:
+            self.select_first_on_list()
+        else:
+            item = selection[0]
+            
+            prev_item = self._treeview.prev(item)
+            if prev_item != "":
+                self.select(prev_item)
+
+    def deselect(self):
+        selection = self._treeview.selection()
+    
+        for item in selection:
+            self._treeview.selection_remove(item)
+            
+    def select(self, channel_ID):
+        self._treeview.focus_set()
+        self._treeview.selection_set((channel_ID,channel_ID))
+        self._treeview.focus(channel_ID)
+
+    def clear(self):
+        self._treeview.delete(*self._treeview.get_children())
+        
+    def bind(self, event, handler):
+        self._treeview.bind(event, handler)
+
+    def __iter__(self):
+        return self._treeview.get(0, END)
+
+    def _on_select_item(self, event):
+        selected_items = self._treeview.selection()
+        if selected_items == "": return
+
+        item_ID = selected_items[0]
+        if item_ID[0] == "+":
+            self._connection.trigger('messenger-selected',item_ID[1:])
+        else:
+            self._connection.trigger('channel-selected',item_ID)
+        
+    def _generate_context_menu_event(self, event):
+        # http://stackoverflow.com/questions/12014210/python-tkinter-app-adding-a-right-click-context-menu
+        iid = self.tree.identify('item',event.x, event.y)
+        # se tiene que capturar el channel ID
+        # channel_ID = 
+
+        self._connection.trigger('panel-of-channel-names-context-menu',{"metadata":{"iid": iid}, "x_root":event.x_root, "y_root":event.y_root})
+
+    def notifify_messages_are_unread(self, channel_ID):
+        if self._unread_messages[channel_ID] == 0:
+            self.set_tag(channel_ID, "unread_messages")
+
+        self._unread_messages[channel_ID] += 1
+
+        if self._add_at_the_beginning:
+            self._treeview.move(channel_ID,self._treeview.parent(channel_ID),0)
+
+    def notifify_messages_are_read(self, channel_ID):
+        if self._unread_messages[channel_ID] > 0:
+            self.delete_tag(channel_ID, "unread_messages")
+
+        self._unread_messages[channel_ID] = 0
+
+def validate_channel(func):
+    @functools.wraps(func)
+    def wrapped(self, channel_ID, *args, **kwargs):
+        if channel_ID in self._channels:
+            return func(self, channel_ID, *args, **kwargs)
+        else:
+            raise ValueError("Not a valid channel ID: %s"%channel_ID)
+    return wrapped
+
+# Guardar tambien variable nick por cada messenger
+# Se debe retocar la parte del nick
+
+class Messenger(object):
+    def __init__(self, name=None, nick=None):
+        self.name = name
+        self.nick_var = StringVar()
+        self.channels = set()
+        
+        if nick is not None:
+            self.set_nick(nick)
+
+    def get_nick(self, nick):
+        return self.nick_var.get()
+
+    def set_nick(self, nick):
+        self.nick_var.set(nick)
+        
+    def add_channel(self, channel_ID):
+        self.channels.add(channel_ID)
+        
+    def delete_channel(self, channel_ID):
+        self.channels.remove(channel_ID)
+        
+    def __iter__(self):
+        return self.channels
+
+class Connection(object):
+    def __init__(self):
+        self._callbacks = defaultdict(list)
+
+    def on(self, event_name, callback):
+        self._callbacks[event_name].append(callback)
+        
+    def off(self, event_name, *args):
+        if len(args) == 0:
+            del self._callbacks[event_name]
+        else:
+            list_of_callbacks = self._callbacks[event_name]
+            for callback in args:
+                try:
+                    index = list_of_callbacks.index(callback)
+                except ValueError:
+                    continue
+                    
+                list_of_callbacks.pop(index)
+                
+    def trigger(self, event_name, data=None):
+        for callback in self._callbacks[event_name]:
+            callback(data)
+
+def _menu_builder(f):
+    
+    def wrapped(data):
+    
+        menu = Menu(tearoff=0)
+        f(menu, data["metadata"])
+
+        menu.tk_popup(data["x_root"], data["y_root"], 0)
+        menu.destroy()
+    
+    return wrapped
+
+# Utilizar conexion para notificar mensajes leidos
+
+class Messenger_MegaWidget(object):
+    _ID = 0
+
+    def __init__(self, master, my_nick, style=None, entry_controls=None):
+        self.interior = interior = Frame(master, class_="Chat_MegaWidget")
+        
+        self._style = style
+        
+        self._entry_controls = entry_controls
+
+        self._connection = Connection()
+        self._connection.on("channel-selected", self._on_select_channel)
+
+        top_panel = PanedWindow(interior, orient=HORIZONTAL, opaqueresize= False, sashpad =1)
+        top_panel.pack(expand=YES, fill=BOTH)
+
+        self._notebook = ttk.Notebook(top_panel, style="Channels_Notebook.TNotebook")
+        self._notebook.enable_traversal()
+        self._notebook.bind('<3>', self._open_tab_context_menu)
+
+        if "Tabs" in style:
+            notebook_style = style["Tabs"]
+            if "width" in notebook_style:
+                self._notebook.configure(width=notebook_style["width"])
+                
+            if "height" in notebook_style:
+                self._notebook.configure(height=notebook_style["height"])
+            
+            if "font" in notebook_style:
+                ttk.Style().configure("Channels_Notebook.TNotebook.Tab", font=notebook_style["font"])
+
+        self._notebook.bind("<<NotebookTabChanged>>", lambda event: self._on_tab_changed())
+
+        self._panel_of_channel_names = Panel_Of_Channel_Names(top_panel, self._connection, style=style)
+        self._panel_of_channel_names.interior.pack(expand=True, fill=BOTH)
+
+
+        top_panel.add(self._panel_of_channel_names.interior)
+        top_panel.add(self._notebook)
+
+        self._channels = {}
+        
+        self._panel_of_channel_names.bind("<Escape>", lambda event: (self.deselect_chats(),self._entry.focus()))
+
+        interior.bind_all('<Control-KeyPress-m>', lambda event: self._entry.focus())
+        interior.bind_all('<Control-KeyPress-M>', lambda event: self._entry.focus())
+
+        self._messengers = {
+            None: Messenger(nick=my_nick)
+        }
+
+    def _open_tab_context_menu(self, event):
+        if event.widget.identify(event.x, event.y) == 'label':
+            index = event.widget.index('@%d,%d' % (event.x, event.y))
+            #obtener aqui el canal
+            #tab_rint event.widget.tab(index, 'text')
+            self._connection.on("tab-context-menu", {"x_root":event.x_root, "y_root":event.y_root})
+        return "break"
+
+    def _on_select_channel(self, channel_ID):
+        channel = self._channels[channel_ID]
+        
+        try:
+            self._notebook.select(channel.interior)
+        except TclError:
+            self._notebook.add(channel.interior, text=channel.name)
+            self._notebook.select(channel.interior)
+
+        channel.focus_entry()
+
+    def deselect_channel_on_panel(self):
+        self._panel_of_channel_names.deselect()
+        
+    @validate_channel
+    def select_channel_on_panel(self, channel_ID):
+        self._panel_of_channel_names.select(channel_ID)
+    
+    @validate_channel        
+    def untag_channel(self, channel_ID, tag):
+        self._panel_of_channel_names.delete_tag(channel_ID, tag)
+
+    @validate_channel
+    def tag_channel(self, channel_ID, tag):
+        self._panel_of_channel_names.set_tag(channel_ID, tag)
+
+    @validate_channel
+    def select_channel_on_notebook(self, channel_ID):
+        channel = self._channels[channel_ID]
+        self._notebook.select(channel.interior)
+        
+    def add_messenger(self, name):
+        if name in self._messengers:
+            raise ValueError("Messenger already created: %s"%name)
+
+        self._messengers[name] = Messenger(name=name)
+        self._panel_of_channel_names.add_messenger(name)
+
+    def delete_messenger(self, name):
+        if name not in self._messengers:
+            raise ValueError("Messenger not exists: %s"%name)
+
+        for channel_ID in self._messengers[name]:
+            self.close_channel(channel_ID)
+
+        del self._messengers[name]
+        self._panel_of_channel_names.delete_messenger(name)
+
+    def new_channel(self, channel_name, channel_ID=None, messenger=None, start_open=None, tags=None, image=None, public=False):
+        if channel_ID is None:
+            channel_ID = "channel"+str(self._ID)
+            self._ID += 1
+        else:
+            if channel_ID[0] == "+":
+                raise ValueError("Channel ID can not start with '+': %s"%channel_ID)
+            
+            if channel_ID in self._channels:
+                raise ValueError("Channel is already created: %s"%channel_ID)
+
+        if messenger in self._messengers:
+            messenger_instance = self._messengers[messenger]
+        else:
+            messenger_instance = self._messengers[messenger] = Messenger(name=messenger)
+            self._panel_of_channel_names.add_messenger(messenger)
+
+        my_nick_var = messenger_instance.nick_var
+
+        if public:
+            if start_open is None:
+                start_open = True
+            channel = Public_Channel(self._notebook, self._connection, messenger_instance, channel_ID, channel_name, my_nick_var, style=self._style, entry_controls=self._entry_controls)
+        else:
+            if start_open is None:
+                start_open = False
+
+            channel = Private_Channel(self._notebook, self._connection, messenger_instance, channel_ID, channel_name, my_nick_var, style=self._style, entry_controls=self._entry_controls)
+
+        channel.bind_entry("<Control-W>", lambda event, channel_ID=channel.channel_ID: self.hide_channel(channel_ID))
+        channel.bind_entry("<Control-w>", lambda event, channel_ID=channel.channel_ID: self.hide_channel(channel_ID))
+        channel.bind_entry('<Up>', lambda event:  self._panel_of_channel_names.select_previous())
+        channel.bind_entry('<Down>', lambda event: self._panel_of_channel_names.select_next())
+
+        self._channels[channel_ID] = channel
+
+        self._panel_of_channel_names.add_channel_name(channel_name, channel_ID, messenger=messenger, tags=tags, image=image)
+        messenger_instance.add_channel(channel_ID)
+
+        if start_open:
+            self._notebook.add(channel.interior, text=channel_name)
+
+        return channel_ID
+
+    def on(self, event_name, callback):
+        self._connection.on(event_name, callback)
+        
+    def trigger(self, event_name, data):
+        self._connection.trigger(event_name, data)
+
+    @validate_channel
+    def show_channel(self, channel_ID, select=False):
+        channel = self._channels[channel_ID]
+        self._notebook.add(channel.interior)
+
+        if select:
+            self._notebook.select(channel.interior)                
+
+    def hide_channel(self, channel_ID=None):
+        if channel_ID is None:
+            channel = self.current_channel
+        else:
+            if channel_ID in self._channels:
+                channel = self._channels[channel_ID]
+            else:
+                raise ValueError("Not a valid channel ID: %s"%channel_ID)
+
+        self._notebook.hide(channel.interior)
+
+    @validate_channel
+    def close_channel(self, channel_ID=None):
+        if channel_ID is None:
+            channel = self.current_channel
+            channel_ID = channel.channel_ID
+        else:
+            channel = self._channels[channel_ID]
+
+        self._panel_of_channel_names.delete_channel_name(channel_ID)
+
+        messenger = channel.messenger
+        channel.interior.destroy()
+        del self._channels[channel_ID]
+        self._messengers[messenger].delete_channel(channel_ID)
+        
+        channel.close()
+
     def focus_entry(self):
-        self._entry.focus()
-
-    def bind_entry(self, event, handler):
-        self._entry.bind(event, handler)
-        
-    def bind_textarea(self, event, handler):
-        self._textarea.bind(event, handler)
-        
-    def bind_tag(self, tagName, sequence, func, add=None):
-        self._textarea.tag_bind(tagName, sequence, func, add=add) 
-        
-    def focus(self):
-        self._entry.focus()
-
-    def user_message(self, nick, content):
-        if self._timestamp_template is None:
-            self._write((u"%s:"%nick, "nick"), " ", (content, "user_message"))
-        else:
-            timestamp = datetime.datetime.now().strftime(self._timestamp_template)
-            self._write((timestamp, "timestamp"), " ", (u"%s:"%nick, "nick"), " ", (content, "user_message"))
-
-    def notification_message(self, content, tag=None):
-        if tag is None:
-            tag = "notification"
-
-        self._write((content, tag))
-        
-    notification = notification_message
-    
-    def notification_of_private_message(self, content, from_, to):
-        if self._timestamp_template is None:
-            self.notification_message(u"{from_} -> {to}: {content}".format(from_=from_, to=to, content=content), "notification_of_private_message")
-        else:
-            timestamp = datetime.datetime.now().strftime(self._timestamp_template)
-            self.notification_message(u"{timestamp} {from_} -> {to}: {content}".format(timestamp=timestamp, from_=from_, to=to, content=content), "notification_of_private_message")
-        
-    def new_message(self, message):
-        if isinstance(message, User_Message):
-            self.user_message(message.content, message.nick)
-        elif isinstance(message, Notification_Message):
-            self.notification(message.content, message.tag)
-        elif isinstance(message, Notification_Of_Private_Message):
-            self.notification_of_private_message(message.from_, message.to, message.content)
-        else:
-            raise Exception("Bad message")
-
-    def tag(self, tag_name, **kwargs):
-        self._textarea.tag_config(tag_name, **kwargs)
-
-    def clear(self):
-        self._is_empty = True
-        self._textarea.delete('1.0', END)
+        channel = self.current_channel
+        if channel is not None:
+            channel.focus_entry_set()
 
     @property
-    def logging_file(self):
-        return self._logging_file
-
-    def send(self, content):
-        if self._my_nick is None:
-            raise Exception("Nick not set")
-
-        self.user_message(self._my_nick, content)
-
-    def _filter_text(self, text):
-        return "".join(ch for ch in text if ch <= u"\uFFFF")
-    
-    def _write(self, *args):
-        if len(args) == 0: return
-            
-        relative_position_of_scrollbar = self._vsb.get()[1]
-        
-        self._textarea.config(state=NORMAL)
-        
-        if self._is_empty:
-            self._is_empty = False
-        else:
-            self._textarea.insert(END, "\n")
-            if self._log is not None:
-                self._log.write("\n")
-
-        for arg in args:
-            if isinstance(arg, tuple):
-                text, tag = arg
-                        # Parsing not allowed characters
-                text = self._filter_text(text)
-                self._textarea.insert(END, text, tag)
-            else:
-                text = arg
-
-                text = self._filter_text(text)
-                self._textarea.insert(END, text)
-            
-            if self._log is not None:
-                self._log.write(text)
-
-        if self._maximum_lines is not None:
-            start_line = int(self._textarea.index('end-1c').split('.')[0]) -self._maximum_lines 
-            
-            if lines_to_delete >= 1:
-                self._textarea.delete('%s.0'%start_line, END)
-
-        self._textarea.config(state=DISABLED)
-        
-        if relative_position_of_scrollbar == 1:
-            self._textarea.yview_moveto(1)
-
-    def _on_message_sent(self, event):
-        message = self._entry.get()
-        self._entry.delete(0, END)
-        
-        self.send(message)
-
-        if self._command:
-            self._command(message)
-
-    def set_nick(self, my_nick):
-        self._my_nick = my_nick
-
-        if my_nick:
-            text = self._label_template.format(nick=my_nick)
-
-            self._entry_label["text"] = text
-            self._entry_label.pack(side=LEFT,padx=(5,5), before=self._entry)
-        else:
-            self._entry_label.pack_forget()
-
+    def my_nick(self):
+        return self._messengers[None].get_nick()
+
+    @my_nick.setter
+    def my_nick(self, new_nick):
+        return self._messengers[None].set_nick(new_nick)
+
+    def set_nick_on_messenger(self, messenger, nick):
+        for channel_ID in self._messengers[messenger]:
+            channel = self._channels[channel_ID]
+            channel.set_nick(nick)
+        
+    def send(self, message, channel_ID=None):
+        if channel_ID is None:
+            channel = self.current_channel
+            if self.current_channel is None:
+                return
+        else:
+            channel = self._channels[channel_ID]
+
+        channel.send(message)
+
+    @validate_channel
+    def user_message(self, channel_ID, nick, content):
+        channel = self._channels[channel_ID]
+        channel.user_message(nick, content)
+        
+        if channel != self.current_channel:
+            self._notifify_messages_are_unread(channel_ID)
+
+    @validate_channel
+    def notification_message(self, channel_ID, content, notification_type=None):
+        channel = self._channels[channel_ID]
+        channel.notification_message(content, notification_type)
+        
+        if channel != self.current_channel:
+            self._notifify_messages_are_unread(channel_ID)
+
+    @validate_channel
+    def notification_of_private_message(self, channel_ID, content, from_=None, to=None):
+        channel = self._channels[channel_ID]
+        channel.notification_of_private_message(content, from_=from_, to=to)
+        
+        if channel != self.current_channel:
+            self._notifify_messages_are_unread(channel_ID)
+
+    def change_nick(self, new_nick, messenger=None):
+        self._messengers[messenger].set_nick(new_nick)
+
+    @validate_channel
+    def __getitem__(self, channel_ID):
+        return self._channels[channel_ID]
+
+    @validate_channel
+    def __delitem__(self, channel_ID):
+        self.close_channel(channel_ID)
+        
+    def __contains__(self, channel_ID):
+        return channel_ID in self._channels
+        
+    @property
+    def current_channel(self):
+        widget_name = self._notebook.select()
+        if widget_name:
+            return self.interior.nametowidget(widget_name).channel
+        else:
+            return None
+
+    def _on_tab_changed(self):
+        channel = self.current_channel
+        channel_ID = channel.channel_ID
+        
+        channel.focus_entry()
+        
+        self._notifify_messages_are_read(channel_ID)
+
+    def _notifify_messages_are_read(self, channel_ID):
+        self._panel_of_channel_names.notifify_messages_are_read(channel_ID)
+        
+        channel = self._channels[channel_ID]
+        self._notebook.tab(channel.interior, text= channel.name)
+
+    def _notifify_messages_are_unread(self, channel_ID):
+        self._panel_of_channel_names.notifify_messages_are_unread(channel_ID)
+        
+        channel = self._channels[channel_ID]
+        try:
+            self._notebook.tab(channel.interior, text= "[*] "+channel.name)
+        except TclError:
+            pass
+        
 if __name__ == "__main__":
+    
+    style = {
+        "Tabs":{
+            "width":600,
+            "height": 600,
+            "font": ("TkDefaultFont", 11)
+        },
+        "Scrollbar": {
+            "background":"#7f0303", 
+            "troughcolor" :"#cc2b00"
+        },
+        "Panel_Of_Channel_Names": {
+            "messenger_column": {
+                "width": 100
+            },
+            "channel_column": {
+                "width": 100
+            },
+            "background":"#676760", 
+            "foreground":"#f7f7f7",
+            "font": ("Courier New", 12),
+            "tags": {
+                "unread_messages": {
+                    "foreground": "red"
+                }
+            }
+        },
+        "Channel": {
+            "Chatbox": {
+                "timestamp_template": "[%H:%M:%S]",
+                "history_background":"#676760",
+                "history_font": ("Courier New", 12),
+                "label_font": ("TkDefaultFont", 10, "bold"),
+                "entry_font": ("Courier New", 12),
+                "entry_background": "#676760", 
+                "entry_foreground":"#f7f7f7",
+                "tags": {
+                    "timestamp": {
+                        "foreground":"#f7f7f7"
+                    },
+                    "nick": {
+                        "foreground":"#f7f7f7"
+                    },
+                    "notification": {
+                        "foreground":"#f7f7f7"
+                    },
+                    "notification_of_private_message": {
+                        "foreground":"#f7f7f7"
+                    },
+                    "user_message": {
+                        "foreground":"#f7f7f7"
+                    }
+                }
+            },
+            "User_List": {
+                "width": 100,
+                "background":"#676760", 
+                "foreground":"#f7f7f7",
+                "font": ("Courier New", 12)
+            }
+        }
+    }
 
     try:
         from Tkinter import Tk
@@ -261,13 +902,16 @@
     root = Tk()
     root.title("Chat megawidget")
 
-    def command(txt):
-        print(txt)
-
-    chatbox = Chatbox(root, my_nick="user1", command=command)
-    chatbox.user_message("user2", "hello guys")
-    
-    chatbox.send("Hi, you are welcome!")
-    chatbox.interior.pack(expand=True, fill=BOTH)
+    chat = Messenger_MegaWidget(root, my_nick="user123", style=style)
+    chat.interior.pack(expand=True, fill=BOTH)
+
+
+    channel_ID = chat.new_channel("#barcelona", start_open=True)
+
+    chat.new_channel("johny23")
+    chat.new_channel("Samatha32")
+
+    chat.user_message(channel_ID, "john", "hi everybody")
+    for i in range(100): chat.send("hola caracola")
 
     root.mainloop()

History