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()