#!/usr/bin/env python # coding: utf-8 """ Chatwisted client gui with Tkinter """ import Tkinter as tk import tkMessageBox import tkSimpleDialog from twisted.internet import tksupport from chatwisted.gui import Gui from chatwisted.client import PluginClient from chatwisted.plugins import PLUGINS from chatwisted import ABOUT class OptQuestionier(tkSimpleDialog.Dialog): """ Dialog Class to ask the user for some options """ def __init__(self, parent, title, options, callback): """ :Parameters: parent : widget parent widget title : string title for the dialog window options : list options to ask for as (name, type, default) tuples, eg. ("name", str, "Guest") :todo: bind kp_enter like return """ self.options = options self.entries = dict() self.out = {} self.callback = callback tkSimpleDialog.Dialog.__init__(self, parent, title) #self.bind("", self.ok) def body(self, master): """ Create the option enties and labels. """ row = 1 focus = None for name, dummy, default in self.options: tk.Label(master, text="%s:" % name).grid(row=row) self.entries[name] = entry = tk.Entry(master) entry.insert("end", str(default)) entry.grid(row=row, column=1) if not focus: focus = entry row += 1 return focus # initial focus def validate(self): """ Validate the input and save converted values in self.out """ try: for name, convert, dummy in self.options: value = self.entries[name].get() if not value: raise ValueError self.out[name] = convert(value) return 1 except ValueError: tkMessageBox.showwarning( "Bad input", "Illegal values, please try again" ) return 0 def apply(self): """ Call the callback function with validated data """ self.callback(self.out) class TkPluginToplevel(tk.Toplevel): """ tk.Toplevel widget for TkPluginClients """ def __init__(self, master): """ Define a plugin attribute """ tk.Toplevel.__init__(self, master) self.plugin = None self.visible = True def set_plugin(self, plugin): """ Put an TkPluginClient object on the window. Packs the plugin on the window, sets the title to the group_name of the plugin """ plugin.pack() self.plugin = plugin self.title(plugin.group_name) self.protocol("WM_DELETE_WINDOW", self.on_quit) self.plugin.on_quit = self.on_quit def on_quit(self): """ If a plugin is set, call its on_quit method. Then destroy yourself. """ if self.plugin: self.plugin.__class__.on_quit(self.plugin) self.destroy() return True def toggle(self): """ Toggles between visible and invisible """ if self.visible: self.wm_withdraw() self.visible = False else: self.wm_deiconify() self.visible = True class TkGui(Gui): """ Chat GUI with Tkinter """ name = "tk" def __init__(self, factory): """ Build up the GUI """ Gui.__init__(self, factory) self.root = tk.Tk() self.main_frame = tk.Frame(self.root) self.main_frame.pack(side="top") main = TkPluginClient(self.main_frame, factory, "main") main.pack(side="right") factory.parser.default_func = main.log self.factory.parser.get_cmds_from_obj(main) #: dict of all plugin toplevel windows self.group_windows = {} self.menu = {} self.root_menu = tk.Menu(self.root, tearoff=0) self.root["menu"] = self.root_menu self.make_menu() self.root.protocol("WM_DELETE_WINDOW", self.on_quit) self.root.title("chatwisted") self.status = tk.StringVar() self.status.set("not connected") self.status_bar = tk.Frame(relief="sunken", borderwidth=1) self.status_bar.pack(fill="x", side="bottom") self.statuslab = tk.Label(self.status_bar, textvariable=self.status, relief="sunken", bd=1, padx=3, pady=1) self.statuslab.pack(side="right") tksupport.install(self.root) #{ Host/Connect def start_client(self, ip, port, name): """ Start a client and connect to a server :Parameters: ip : string ip to connect to port : int port to use name : string name to connect as """ ip = ip self.status.set("connecting...") if self.factory.start_client(ip, port, name): self.status.set("connected") else: self.status.set("error") def start_server(self, port, name): """ Start a server and connect to it as client :Parameters: port : int port to use name : string name to connect as """ self.status.set("starting server...") if self.factory.start_server(port, name): self.status.set("server is running") else: self.status.set("error") def host_dialog(self): """ Creates an dialog with entries for hosting options (name and port). """ options = (("Port", int, self.default_port), ("Your Name", str, "Admin")) OptQuestionier(self.root, "Host...", options, self.host) def host(self, opt): """ Callback for host_dialog """ self.start_server(opt["Port"], opt["Your Name"]) def connect_dialog(self): """ Creates an dialog with entries for connecting options (ip, name and port). """ options = (("IP", str, self.default_ip), ("Port", int, self.default_port), ("Your Name", str, "Guest")) OptQuestionier(self.root, "Connect...", options, self.connect) def connect(self, opt): """ Callback for connect_dialog """ self.start_client(opt["IP"], opt["Port"], opt["Your Name"]) #{ Commands def cmd_error(self, err): """ Shows an error message """ tkMessageBox.showerror("ERROR", err) def cmd_plugin_list(self, *plugins): """ update plugin list """ self.update_menu("new", plugins, self.new_group) def cmd_group_list(self, *groups): """ update group list :todo: view-checkbuttons must fit window states """ mygroups = set(self.factory.groups.keys()) print "CLIENT: gui: group_list | groups:", groups, print "|mygroups:", mygroups self.update_menu("enter", set(groups)-mygroups, self.factory.enter_group) self.update_menu("leave", mygroups, lambda name: self.factory.groups[name].on_quit()) self.menu["view"].delete(0, "end") for group_name, window in self.group_windows.iteritems(): self.menu["view"].add_checkbutton(label=group_name, command=window.toggle) #{ Interface Methods def new_group(self, plugin_class): """ create a new plugin group :Parameters: plugin_class : TkPluginClient plugin class """ name = tkSimpleDialog.askstring("Enter Groupname", "Name:") if name: #plugin_class(self.root, self.factory, name) self.factory.create_group(plugin_class, name) #print "new group", plugin_class def add_group(self, plugin_class, group_name): """ create a new plugin obj. and connect to a group :Parameters: group_name : string name of the group to connect to plugin_class : TkPluginClient plugin class """ print "add group", group_name, plugin_class top = TkPluginToplevel(self.root) top.set_plugin(plugin_class(top, self.factory, group_name)) self.group_windows[group_name] = top self.factory.send_cmd("group_list", "") #{ Helper Methods def show_about(self): """ Pop up an info message with the docstring of this module """ tkMessageBox.showinfo("About", ABOUT, parent=self.root) def update_menu(self, menu, list_, func): """ Updates a menu :Parameters: menu : string name of the menu to update list_ : list names of new menu entries func : callable callback function or method that takes one argument # (the name of the clicked entry) """ self.menu[menu].delete(0, "end") for value in list_: self.menu[menu].add_command(label=value, command=lambda v=value: func(v)) def on_quit(self): """ Called when a user tries to close the window ask for confirmation """ # if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"): self.factory.stop_reactor() def make_menu(self): """ Generates the menu """ def make_submenu(menu, menuitems): """ Generates a submenu """ for name, value in menuitems: if type(value) == tuple: submenu = tk.Menu(menu, tearoff=0) menu.add_cascade(menu=submenu, label=name) self.menu[name] = submenu if value != ("", ""): make_submenu(submenu, value) elif name == "---": menu.add_separator() else: menu.add_command(label=name, command=value) structure = (("file", (("quit", self.on_quit), )), ("connection", (("connect...", self.connect_dialog), ("host...", self.host_dialog))), ("groups", (("new", ("", ""), ), ("enter", ("",""), ), ("leave", ("",""), ))), ("view", (("---", ""), )), ("about", self.show_about), ) make_submenu(self.root_menu, structure) class TkPluginClient(PluginClient, tk.Frame): """ Defines a simple chat that nearly every plugin should have. To write your own plugin inherit from this class and put your contents into the `main_frame`-Frame. """ name = "chat" gui = "tk" def __init__(self, master, factory, name): """ Build up the chat gui. """ # call parent class constructors PluginClient.__init__(self, factory, name) tk.Frame.__init__(self, master) # add a client list at the left self.client_list = tk.Listbox(self, width=10) self.client_list.grid(column=0, row=0, rowspan=3, sticky="news") # add a centered #: frame for plugin contents self.main_frame = tk.Frame(self) self.main_frame.grid(column=1, row=0, sticky="news") # add a frame with a textbox and scrollbar self.chat_frame = tk.Frame(self) self.chat_frame.grid(column=1, row=1, sticky="news") self.scrollbar = tk.Scrollbar(self.chat_frame) self.scrollbar.pack(side="right", fill="y") self.textbox = tk.Text(self.chat_frame, yscrollcommand=self.scrollbar.set, state=tk.DISABLED, width=30, height=8) self.textbox.pack(fill=tk.BOTH, expand=1, side="right") self.scrollbar.config(command=self.textbox.yview) # add a frame with an entry and a "send"-Button self.bottomframe = tk.Frame(self) self.bottomframe.grid(column=1, row=2, sticky="ew") self.entry = tk.Entry(self.bottomframe) self.entry.pack(side="left", fill=tk.BOTH, expand=1) self.entry.bind("", self.send_message) self.entry.bind("", self.send_message) self.send_button = tk.Button(self.bottomframe, text="Send", command=self.send_message) self.send_button.pack(side="right") def log(self, line): """ Appends a line (without linebreak) to the textbox (and adds a lb) """ self.textbox.config(state=tk.NORMAL) self.textbox.insert(tk.END, line+"\n") self.textbox.see(tk.END) self.textbox.config(state=tk.DISABLED) def cmd_list(self, *clients): """ Updates the client list """ self.client_list.delete(0, "end") for client_name in clients: self.client_list.insert("end", client_name) def cmd_error(self, err): """ Shows an error """ tkMessageBox.showerror("ERROR", err) def send_message(self, dummy=None): """ Sends a line to the server """ line = self.entry.get() if line: if PluginClient.send_message(self, line): self.entry.delete(0, "end") def on_quit(self): """ Destroys this Frame """ PluginClient.on_quit(self) self.destroy() PLUGINS["chat"]["tk"] = TkPluginClient