#!/usr/bin/env python
#
# $Id: wcc_dialog.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $
#
# Copyright 2001 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Messaging dialog.
"""
__rcs_info__ = {
#
# Creation Information
#
'module_name' : '$RCSfile: wcc_dialog.py,v $',
'rcs_id' : '$Id: wcc_dialog.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $',
'creator' : 'Doug Hellmann <doug@hellfly.net>',
'project' : 'PmwContribD',
'created' : 'Sun, 01-Apr-2001 13:20:32 EDT',
#
# Current Information
#
'author' : '$Author: doughellmann $',
'version' : '$Revision: 1.3 $',
'date' : '$Date: 2001/11/03 11:05:22 $',
}
#
# Import system modules
#
import string
import socket
import sys
import time
import re
import os
import Pmw
from Tkinter import *
#
# Import Local modules
#
import GuiAppD
import Table
import tkMessageBox
import wcc_protocol
#
# Module
#
def updateDialogSettings(**kw):
"""
Works for:
title
ringbell
<tag style attrs>
"""
for dialog in _message_dialogs.values():
#print 'Setting prefs %s for %s' % (kw, dialog)
apply(dialog.configure, (), kw)
return
def getMessageDialogIDFromUsers(users):
ud={}
for u in users:
ud[u] = None
users = ud.keys()
users.sort()
if len(users) > 1:
dlg_id = 'Chatting with %s' % string.join( users[:-1],
', ')
dlg_id = dlg_id + ' and %s' % users[-1]
else:
dlg_id = 'Chatting with %s' % users[0]
return dlg_id
_message_dialogs = {}
def getMessageDialog(
parent, uid, uname, dialogID, addressees, addressee_ids, app,
**kw):
"""
Find a dialog using the given id, or create a new
one and assign the id to it.
"""
if _message_dialogs.has_key(dialogID):
return _message_dialogs[dialogID]
else:
kw['dialogID'] = dialogID
kw['addressMessagesToIDs'] = addressee_ids
kw['addressMessagesToNames'] = addressees
kw['userName'] = uname
kw['userID'] = uid
kw['app'] = app
dlg = apply(MessageDialog, (parent,), kw)
_message_dialogs[dialogID] = dlg
return dlg
def _removeMessageDialog(dialogID):
try:
del _message_dialogs[dialogID]
except KeyError:
pass
return
def getRangeAndText(w, x, y, tag_name):
"""
Given the x,y coordinates on a text widget,
and the name of a face type or tag, returns
the range of text with that tag in which
the x,y point occurs.
"""
index_loc = '@%d,%d' % (x, y)
clicked_on = w.index(index_loc)
flat_range_list = w.tag_ranges(tag_name)
range_list = []
while flat_range_list:
range_list.append( flat_range_list[0], flat_range_list[1] )
flat_range_list = flat_range_list[2:]
for first, last in range_list:
if (w.compare(first, '<=', clicked_on) and
w.compare(clicked_on, '<=', last)):
return ( (first, last), w.get(first, last) )
return None
class MessageDialog(Pmw.Dialog):
#
# Compute some font information
#
precomputedFonts = {
'Helvetica':Pmw.logicalfont('Helvetica', -1, weight='bold'),
'Times':Pmw.logicalfont('Times', 0),
'Courier':Pmw.logicalfont('Courier', -1),
'CourierBold':Pmw.logicalfont('Courier', -1, weight='bold'),
}
SEND_BTN = 'Send'
ADD_USER_BTN = 'Invite'
CANCEL_BTN = 'Cancel'
CLEAR_BTN = 'Clear'
CLOSE_BTN = 'Close'
button_ids = (
SEND_BTN,
ADD_USER_BTN,
CANCEL_BTN,
#CLEAR_BTN,
CLOSE_BTN,
)
def __init__(self, parent=None,
dialogID='Unknown',
addressMessagesToIDs=(),
addressMessagesToNames=(),
userName='',
userID=0,
app=None,
**kw):
"""
Create a dialog to show an incoming message.
"""
self._dialog_id = dialogID
self._address_messages_to = addressMessagesToIDs
self._address_messages_to_names = addressMessagesToNames
self._name_lut = {}
for id, name in map(None,
addressMessagesToIDs, addressMessagesToNames):
self._name_lut[id] = name
self._user_name = userName
self._uid = userID
#
# Get some service methods from the application
#
self._balloon = app.balloon()
self._error_handler = app.showError
margin1 = 10
margin2 = 130
helvetica = self.precomputedFonts['Helvetica']
button_help = {
self.SEND_BTN:'Send the current message',
self.ADD_USER_BTN:'Add a user to this chat dialog',
self.CANCEL_BTN:'Discard the current message',
self.CLEAR_BTN:'Clear the entire chat dialog',
self.CLOSE_BTN:'Close this dialog',
}
optiondefs = (
# closing the window
('command', self.callback, Pmw.INITOPT),
('buttons', self.button_ids, self._buttons),
('defaultbutton', self.SEND_BTN, self._defaultButton),
('ringbell', 1, None),
('clearonclose', None, None),
('webbrowser', 'netscape -remote "openURL(%s, new-window)"', None),
('app', app, Pmw.INITOPT),
# text widget to hold chats
('chat_text_height', 15, None),
('chat_text_width', 80, None),
('chat_text_background', 'lightgrey', None),
('chat_labelpos', 'nw', Pmw.INITOPT),
('chat_label_text', 'Chatting as %s' % self._user_name,
Pmw.INITOPT),
('chat_text_wrap', 'word', None),
('chat_text_font', helvetica, None),
# text widget to hold outgoing messages
('message_text_height', 5, None),
('message_text_width', 80, None),
('message_labelpos', 'nw', Pmw.INITOPT),
('message_label_text', 'Message:', Pmw.INITOPT),
('message_text_wrap', 'word', None),
('message_text_font', helvetica, None),
('message_text_background', 'white', None),
# dialog title
('title', dialogID, self._settitle),
# date tag style (how date is displayed)
('dateface', '(("Helvetica",),{"foreground":"green","background":"black",})', self._tagValueReconfigure),
('datelmargin1', margin1, None),
('datelmargin2', margin2, None),
# from tag style (how from name is displayed)
('fromface', '(("Helvetica",),{"foreground":"red","background":"lightgrey",})', self._tagValueReconfigure),
('fromlmargin1', margin1, None),
('fromlmargin2', margin2, None),
# message tag style (how the message is displayed)
('messageface', '(("Helvetica",),{"foreground":"black","background":"lightgrey",})', self._tagValueReconfigure),
('messagelmargin1', margin2, None),
('messagelmargin2', margin2, None),
# url tag style (how the url is displayed)
('urlface', '(("Helvetica",),{"foreground":"blue","background":"lightgrey",})', self._tagValueReconfigure),
('urllmargin1', margin2, None),
('urllmargin2', margin2, None),
)
self.defineoptions(kw, optiondefs)
Pmw.Dialog.__init__(self, parent=parent)
#
# Create the dialog internals
#
self.withdraw()
self.bind('<Control-w>', self.closeCB)
inside = self.component('dialogchildsite')
#
# Create a text widget to hold the chat
#
chat = self.createcomponent(
'chat',
(), None,
Pmw.ScrolledText,
(inside,),
#label_text=dialogID,
text_state='disabled',
)
#
# Define the styles for the parts of the chats
#
self.configureStyles()
chat.pack(
side=TOP,
expand=YES,
fill=BOTH,
)
#
# Create the text widget that is going to be
# used for sending messages.
#
msg = self.createcomponent(
'message',
(), None,
Pmw.ScrolledText,
(inside,),
)
msg.pack(
side=TOP,
expand=NO,
fill=BOTH,
)
#
# Check keywords and initialise options.
#
self.initialiseoptions(self.__class__)
#
# Attach balloon help messages to all of the
# button box buttons.
#
if app and app.balloon():
bbox = self.component('buttonbox')
balloon = self._balloon
for id in self.button_ids:
balloon.bind(bbox.component(id),
button_help[id], button_help[id])
return
def configureStyles(self):
txt = self.component('chat_text')
for tag_name in ( 'date', 'from', 'message', 'url' ):
#print 'looking for %sfont' % tag_name
#print '\tand ', self['%sfont' % tag_name]
face = self['%sface' % tag_name]
if type(face) == type(''):
face = eval(face)
#print 'face=', face
#print 'type(face)=', type(face)
if not face:
continue
fontspec, tagspec = face
tagspec['font'] = fontspec
apply(txt.tag_configure, (tag_name,), tagspec)
txt.tag_configure(
tag_name,
lmargin1=self['%slmargin1' % tag_name],
lmargin2=self['%slmargin2' % tag_name],
)
txt.tag_bind(tag_name, '<Button-1>',
lambda e, t=tag_name, s=self: s.tagCB_click(t, e))
txt.tag_bind(tag_name, '<Enter>',
lambda e, t=tag_name, s=self: s.tagCB_enter(t, e))
txt.tag_bind(tag_name, '<Leave>',
lambda e, t=tag_name, s=self: s.tagCB_leave(t, e))
return
##
## Callbacks for text tags
##
def tagCB(self, tag_name, event, event_name):
"""
General dispatch method.
"""
try:
callback = getattr(self, 'tagCB_%s_%s' % (event_name, tag_name))
except AttributeError:
return
range_and_text = getRangeAndText(event.widget,
event.x, event.y,
tag_name)
if range_and_text:
callback(event, range_and_text[0], range_and_text[1])
return
def tagCB_enter(self, tag_name, event):
"""
Callback registered for enter events.
"""
self.tagCB(tag_name, event, 'enter')
return
def tagCB_leave(self, tag_name, event):
"""
Callback registered for leave events.
"""
self.tagCB(tag_name, event, 'leave')
return
def tagCB_click(self, tag_name, event):
"""
Callback registered for click events.
"""
self.tagCB(tag_name, event, 'click')
return
#
# Use the balloon manager to display the email
# address for the user that sent this message.
#
def tagCB_enter_from(self, event, span, text):
self.tagCB_leave_url(event, span, text)
user = string.strip(text)[:-1]
user_info = self['app'].statusInfo().infoFromName(user)
email = user_info[-1]
self._balloon._enter(event.widget, email, email, 1)
return
def tagCB_leave_from(self, event, span, text):
self._balloon._leave(event)
return
#
# Register a bunch of event handlers
# to switch the cursor around. We should
# be able to just use enter and leave event
# handlers on url text, but that does not
# see sufficient.
#
def tagCB_enter_url(self, event, span, text):
self.text_cursor = event.widget.cget('cursor')
event.widget.configure(cursor='hand2')
return
def tagCB_leave_url(self, event, span, text):
event.widget.configure(cursor='xterm')
return
tagCB_enter_message = tagCB_leave_url
def tagCB_click_url(self, event, span, text):
"""
Called when user clicks on url text.
"""
#print 'clicked on ', span, text
try:
browsercmd = '%s &' % (self['webbrowser'] % text)
except TypeError:
browsercmd = '%s %s &' % (self['webbrowser'], text)
#print 'browser command = ', browsercmd
os.system(browsercmd)
return
def _tagValueReconfigure(self, *args, **kw):
self.configureStyles()
return
##
## Dialog level callbacks
##
def errorMessage(self, message):
"""
Display an error message, if we can.
"""
if self._error_handler:
self._error_handler(message)
else:
print 'ERROR MESSAGE: %s' % message
return
def callback(self, button):
"""
This callback will be called when the window is
deleted (passing None for the button) or when a button
is pressed in the button box.
"""
if (button is None) or (button == self.CLOSE_BTN):
#
# Window was closed
#
if self['clearonclose'] == 'Always Clear':
clearwindow = 1
elif self['clearonclose'] == 'Always Keep':
clearwindow = 0
else:
response = tkMessageBox.askyesno(
title='Clear window?',
message='Clear this chat window now?',
)
clearwindow = response
if clearwindow:
self.clear()
self.withdraw()
elif button == self.SEND_BTN:
self.sendMessageCB()
elif button == self.CANCEL_BTN:
self.clearInputArea()
elif button == self.CLEAR_BTN:
self.clear()
elif button == self.ADD_USER_BTN:
inside = self.component('dialogchildsite')
new_user_name, new_user_id = self['app'].pick_user(
inside, self['app'].connection())
if (new_user_name and new_user_id and
new_user_id not in self._address_messages_to):
# add user to our addressee list
self._address_messages_to.append(new_user_id)
self._address_messages_to_names.append(new_user_name)
# update our id
newId = getMessageDialogIDFromUsers(
self._address_messages_to_names)
del _message_dialogs[self._dialog_id]
self._dialog_id = newId
_message_dialogs[newId] = self
# reset our title
self.configure(title=self._dialog_id)
else:
print 'button %s pressed' % button
return
def closeCB(self, *args):
self.callback(self.CLOSE_BTN)
return
def clear(self):
self.clearInputArea()
self.component('chat').settext('')
return
def sendMessageCB(self, event=None):
"""
This callback is called when the user is ready to
send a message.
"""
# get widgets
msg = self.component('message')
# get message text
outgoing_message = string.rstrip(msg.get())
self.clearInputArea()
self.sendMessage(outgoing_message)
#
# Schedule an event to have the input area cleared.
# We can't just do this, because if the user
# pressed a key to get to this callback, the regular
# binding for that key will happen after we clear
# the body of the text here. That means they get
# an extra carriage return in the text widget when
# they hit <Return>.
#
#self.after_idle(self.clearInputArea)
return
##
## Service functions
##
def sendMessage(self, outgoing_message):
"""
This method is called to
send a message.
"""
#
# Check to verify that there is a message.
#
if not outgoing_message:
return
#print 'OUTGOING Message: ', outgoing_message
# add the prefix for sending messages to >1 person
original_message = outgoing_message
if len(self._address_messages_to) > 1:
to_prefix = 'To: %s\r\n' \
% string.join(self._address_messages_to_names,
', ')
outgoing_message = to_prefix + outgoing_message
# add the message to the chat history
self.addMessage(self._user_name, original_message, bell=0)
self.update_idletasks()
self.component('message').update_idletasks()
# send the message
if self._address_messages_to:
for uid in self._address_messages_to:
if str(uid) != str(self._uid):
try:
self['app'].connection().mesg(uid, outgoing_message)
except wcc_protocol.WackyProtocolError, msg:
self._handleProtocolError(msg)
else:
try:
self['app'].connection().cast(outgoing_message)
except wcc_protocol.WackyProtocolError, msg:
self._handleProtocolError(msg)
return
def _handleProtocolError(self, exc):
if exc[-1] == '-MESG Unknown user.':
# User isn't there or logged off
try:
self.errorMessage('User %s is no longer available.' \
% self._name_lut[exc[1]])
except KeyError:
self.errorMessage('User %s is no longer available.' \
% exc[1])
else:
self.errorMessage('Unhandled protocol exception: %s' % str(exc))
return
def clearInputArea(self, event=None):
"""
Clear the text from the input message area
"""
self.component('message').clear()
self.component('message').update_idletasks()
return
##
## Regular expressions for special parts of
## messages
##
urlre = re.compile('(\S+://\S+)', re.DOTALL | re.MULTILINE)
urlre.face = 'url'
def addMessage(self, fromName, message, bell=1):
"""
Add a message to the chat history.
"""
#
# Clean up the incoming message text
#
message = string.rstrip(message)
message = string.replace(message, '\r', ' ')
#
# Get the text widget, add time and user
# info
#
widget = self.component('chat_text')
widget.configure(state='normal')
widget.insert('end', time.strftime('%a %I:%M:%S %p',
time.localtime(time.time())),
'date')
widget.insert('end', ' ', 'date')
widget.insert('end', ' ')
widget.insert('end', '%s:' % fromName, 'from')
widget.insert('end', ' ')
#
# Insert the message
#
message_parts = []
matchObj = 1
while matchObj:
for pattern in ( self.urlre, ):
matchObj = pattern.search(message)
if matchObj:
#print matchObj.re.face, matchObj.re.pattern,
#print ' matched ', matchObj.groups()
start, end = matchObj.span()
before = message[:start]
match = message[start:end]
message = message[end:]
if before:
message_parts.append( (before, 'message') )
message_parts.append( (match, matchObj.re.face) )
#print '\t', before
#print '\t', match
#print '\t', message
break
#break
if message:
message_parts.append( (message, 'message') )
#print 'parts:', message_parts
for text, face in message_parts:
widget.insert('end', text, face)
widget.insert('end', '\n', 'message')
widget.yview_pickplace('end')
widget.configure(state='disabled')
widget.update_idletasks()
if bell and self['ringbell']:
self._hull.bell()
self.show()
return
def show(self):
self.component('message_text').focus_set()
Pmw.Dialog.show(self)
return
|