pypsyc/mjacob2/pypsyc/client/view.py

687 lines
23 KiB
Python

"""
pypsyc.client.view
~~~~~~~~~~~~~~~~~~
pypsyc GTK views
:copyright: 2010 by Manuel Jacob
:license: MIT
"""
import pygtk
pygtk.require('2.0')
import gtk
import pango
from gobject import idle_add
from pkg_resources import resource_filename
def _(message):
return message
class Form(gtk.HBox):
def __init__(self, dict_, attrs):
gtk.HBox.__init__(self)
self.dict = dict_
self.fields = []
left = gtk.VBox()
self.pack_start(left, False, False, 10)
right = gtk.VBox()
self.pack_start(right)
for l, a in attrs:
x = dict_[a]
le, ri, value_func = getattr(self, '_' + type(x).__name__)(l, x)
left.pack_start(le, padding=10)
right.pack_start(ri)
self.fields.append((a, value_func))
def _str(self, l, s):
label = gtk.Label(_(l) + ":")
label.set_alignment(0, 0.5)
entry = gtk.Entry()
entry.set_text(s)
if l.lower() == 'password':
entry.set_visibility(False)
return label, entry, entry.get_text
def _bool(self, l, b):
check = gtk.CheckButton(_(l))
check.set_active(b)
return gtk.Label(), check, check.get_active
def _list(self, l, list_):
label = gtk.Label(_(l) + ":")
label.set_alignment(0, 0.5)
combobox = gtk.combo_box_new_text()
for i in list_:
combobox.append_text(i)
combobox.set_active(0)
return label, combobox, combobox.get_active
_generator = _list
def save(self):
self.dict.update((a, f()) for a, f in self.fields)
return self.dict
def _open_form_dialog(title, window, dict_, attrs, buttons, func, markup=None):
dialog = gtk.Dialog(title, window,
(gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT |
gtk.DIALOG_NO_SEPARATOR), buttons)
if markup is not None:
label = gtk.Label()
label.set_markup(markup)
dialog.vbox.pack_start(label)
form = Form(dict_, attrs)
dialog.vbox.pack_start(form)
dialog.connect('response', _dialog_response, form, func)
dialog.show_all()
def _dialog_response(w, response, form, func):
if response == gtk.RESPONSE_ACCEPT:
func(form.save())
if response != gtk.RESPONSE_DELETE_EVENT:
w.destroy()
class AccountsView(object):
def __init__(self):
self.window = self.create_window()
self.window.show_all()
def create_window(self):
win = gtk.Window()
win.set_title(_("Accounts"))
win.connect('destroy', lambda w: self.controller.closed())
vbox = gtk.VBox()
win.add(vbox)
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
scroll.add(self.create_tree_view())
vbox.pack_start(scroll)
buttons = gtk.HButtonBox()
vbox.pack_start(buttons)
add_button = gtk.Button(stock=gtk.STOCK_ADD)
buttons.pack_start(add_button)
remove_button = self.remove_button = gtk.Button(stock=gtk.STOCK_REMOVE)
remove_button.set_sensitive(False)
buttons.pack_start(remove_button)
edit_button = self.edit_button = gtk.Button(stock=gtk.STOCK_EDIT)
edit_button.set_sensitive(False)
buttons.pack_start(edit_button)
add_button.connect('clicked', lambda w: self.controller.add_account())
remove_button.connect('clicked', self.on_remove)
edit_button.connect('clicked', self.on_edit)
return win
def create_tree_view(self):
self.accounts = gtk.ListStore(str, bool)
tv = gtk.TreeView(self.accounts)
tv.set_size_request(400, 200)
self.selection = tv.get_selection()
self.selection.connect('changed', self.on_selection)
renderer = gtk.CellRendererToggle()
renderer.connect('toggled', self.on_toggle)
column = gtk.TreeViewColumn(_("Active"), renderer, active=1)
tv.append_column(column)
column = gtk.TreeViewColumn("UNI", gtk.CellRendererText(), text=0)
tv.append_column(column)
return tv
def on_selection(self, selection):
selected = selection.get_selected()[1] is not None
self.remove_button.set_sensitive(selected)
self.edit_button.set_sensitive(selected)
def on_remove(self, w):
model, iter = self.selection.get_selected()
self.controller.remove_account(model.get_path(iter)[0])
def on_edit(self, w):
model, iter = self.selection.get_selected()
self.controller.edit_account(model.get_path(iter)[0])
def on_toggle(self, w, path):
self.controller.set_active(int(path), not self.accounts[path][1])
def show_addedit_dialog(self, account, add, callback):
attrs = (('Server', 'server'),
('User', 'person'),
('Password', 'password'),
('Save password', 'save_password'))
_open_form_dialog(_("Add Account" if add else "Edit Account"),
self.window, account, attrs,
(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT),
lambda d: callback())
class ConversationView(gtk.VBox):
def __init__(self, tabs_view):
self.tabs_view = tabs_view
gtk.VBox.__init__(self)
self.pack_start(self.create_content())
self.entry = gtk.Entry()
self.entry.connect('activate', self._enter)
self.pack_start(self.entry, False)
def create_content(self):
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
self.vadjustment = scroll.get_vadjustment()
tv = gtk.TextView()
self.text_buffer = tv.get_buffer()
tv.set_editable(False)
tv.set_cursor_visible(False)
tv.set_wrap_mode(gtk.WRAP_WORD_CHAR)
scroll.add(tv)
return scroll
def _enter(self, w):
text = w.get_text()
if not text:
return
self.controller.enter(text)
w.set_text("")
def show_message(self, message):
self._show_line(message)
self.tabs_view.showed_message(self)
def show_unknown_command(self):
self._show_line(_("Unknown command"))
def show_unknown_target(self):
self._show_line(_("Unknown user"))
def show_delivery_failed(self, message):
self._show_line(_("Delivery failed") + ': ' + message)
def _show_line(self, line):
va = self.vadjustment
must_scroll = (va.value == va.upper - va.page_size)
self.text_buffer.insert(self.text_buffer.get_end_iter(), '\n' + line)
if must_scroll:
idle_add(self._scroll)
def _scroll(self):
va = self.vadjustment
va.set_value(va.upper - va.page_size)
class ConferenceView(ConversationView):
def create_content(self):
hpane = gtk.HPaned()
hpane.pack1(ConversationView.create_content(self), True)
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
scroll.add(self.create_tree_view())
hpane.pack2(scroll, True)
hpane.set_position(450)
return hpane
def create_tree_view(self):
self.members = gtk.ListStore(str, str)
tv = gtk.TreeView(self.members)
tv.set_headers_visible(False)
tv.set_tooltip_column(0)
cell = gtk.CellRendererText()
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
column = gtk.TreeViewColumn(None, cell, text=1)
tv.append_column(column)
tv.connect('row-activated', self._double_click)
tv.connect('button-press-event', self._button_press)
tv.connect('popup-menu', self._popup_menu)
return tv
def _double_click(self, tv, path, view_column):
self.controller.open_conversation(path[0])
def _button_press(self, widget, event):
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
model = widget.get_model()
path = widget.get_path_at_pos(int(event.x), int(event.y))[0]
menu = self.create_context_menu(model, model.get_iter(path))
menu.popup(None, None, None, event.button, event.time)
def _popup_menu(self, widget):
model, iter = widget.get_selection().get_selected()
menu = self.create_context_menu(model, iter)
menu.popup(None, None, None, 3, 0)
return True
def create_context_menu(self, model, iter):
menu = gtk.Menu()
conv = gtk.MenuItem(_("Open Conversation"))
menu.append(conv)
conv.connect('activate', lambda w:
self.controller.open_conversation(model.get_path(iter)[0]))
if model.get_value(iter, 2) == 'offered':
accept = gtk.MenuItem(_("Accept Friendship Request"))
menu.append(accept)
accept.connect('activate', lambda w:
self.controller.accept_friendship(model.get_path(iter)[0]))
menu.show_all()
return menu
def show_unknown_target(self):
self._show_line(_("Unknown place"))
class TabsView(object):
def __init__(self, status_icon):
self.status_icon = status_icon
self.window = self.create_window()
self.window.set_default_size(600, 400)
self.unread_tabs = []
settings = gtk.Entry().get_settings()
settings.set_property('gtk-entry-select-on-focus', False)
def create_window(self):
win = gtk.Window()
win.set_title("pypsyc")
win.connect("delete_event", self._delete_event)
win.connect('focus-in-event', self._window_focus)
self.notebook = gtk.Notebook()
self.notebook.set_scrollable(True)
self.notebook.connect('switch-page', self._switched_page)
win.add(self.notebook)
self.notebook.show()
return win
def _delete_event(self, w, e):
self.window.hide()
for tab in self.notebook:
self.controller.close_tab(tab)
return True
def _window_focus(self, w, e):
current_page = self.notebook.get_current_page()
if current_page != -1:
self._switched_page(None, None, current_page)
def _switched_page(self, w, page_ptr, page_num):
tab = self.notebook.get_nth_page(page_num)
if tab in self.unread_tabs:
self._change_tab_color(tab, 'black')
self.unread_tabs.remove(tab)
if not self.unread_tabs:
self.status_icon.set_blinking(False)
idle_add(tab.entry.grab_focus)
def showed_message(self, tab):
if (not self.window.is_active() or
self.notebook.get_current_page() != self.notebook.page_num(tab)):
self._change_tab_color(tab, 'red')
if tab not in self.unread_tabs:
if not self.unread_tabs:
self.status_icon.set_blinking(True)
self.unread_tabs.append(tab)
def _change_tab_color(self, tab, color_spec):
tab_label = self.notebook.get_tab_label(tab).get_children()[0]
color = gtk.gdk.color_parse(color_spec)
tab_label.modify_fg(gtk.STATE_NORMAL, color)
tab_label.modify_fg(gtk.STATE_ACTIVE, color)
def on_status_icon_click(self):
if not self.unread_tabs:
return True
self.focus_tab(self.unread_tabs[0])
def show_conversation(self, label):
view = ConversationView(self)
self._add_tab(view, label)
return view
def show_conference(self, label):
view = ConferenceView(self)
self._add_tab(view, label)
return view
def _add_tab(self, tab, label):
tab_label = gtk.HBox()
tab_label.pack_start(gtk.Label(label))
image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
button = gtk.Button()
button.set_image(image)
button.set_relief(gtk.RELIEF_NONE)
button.connect('clicked', lambda w: self.controller.close_tab(tab))
tab_label.pack_start(button)
tab.show_all()
tab_label.show_all()
self.notebook.append_page(tab, tab_label)
def focus_tab(self, tab):
self.notebook.set_current_page(self.notebook.page_num(tab))
self.window.present()
def remove_tab(self, tab):
self.notebook.remove_page(self.notebook.page_num(tab))
if self.notebook.get_n_pages() == 0:
self.window.hide()
if tab in self.unread_tabs:
self.unread_tabs.remove(tab)
if not self.unread_tabs:
self.status_icon.set_blinking(False)
class FriendsView(object):
def __init__(self):
self.list_view = self.create_tree_view()
def create_tree_view(self):
self.friends = gtk.ListStore(str, bool, str)
tv = gtk.TreeView(self.friends)
tv.set_headers_visible(False)
tv.set_size_request(200, 400)
tv.set_property('has-tooltip', True)
cell = gtk.CellRendererText()
cell.set_property('foreground-set', True)
column = gtk.TreeViewColumn(None, cell, text=0)
column.set_cell_data_func(cell, self._foreground)
tv.append_column(column)
tv.connect('row-activated', self._double_click)
tv.connect('query-tooltip', self._tooltip)
tv.connect('button-press-event', self._button_press)
tv.connect('popup-menu', self._popup_menu)
return tv
def _foreground(self, column, cell, model, iter):
if model.get_value(iter, 1):
cell.set_property('foreground', 'black')
else:
cell.set_property('foreground', 'grey')
def _double_click(self, tv, path, view_column):
self.controller.open_conversation(path[0])
def _tooltip(self, widget, x, y, keyboard_mode, tooltip):
tooltip_context = widget.get_tooltip_context(x, y, keyboard_mode)
if tooltip_context is None:
return False
model, path, iter = tooltip_context
tooltip.set_text(_("Friendship state") + ": %s" % model.get(iter, 2))
widget.set_tooltip_row(tooltip, path)
return True
def _button_press(self, widget, event):
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
model = widget.get_model()
path = widget.get_path_at_pos(int(event.x), int(event.y))[0]
menu = self.create_context_menu(model, model.get_iter(path))
menu.popup(None, None, None, event.button, event.time)
def _popup_menu(self, widget):
model, iter = widget.get_selection().get_selected()
menu = self.create_context_menu(model, iter)
menu.popup(None, None, None, 3, 0)
return True
def create_context_menu(self, model, iter):
menu = gtk.Menu()
conv = gtk.MenuItem(_("Open Conversation"))
menu.append(conv)
conv.connect('activate', lambda w:
self.controller.open_conversation(model.get_path(iter)[0]))
if model.get_value(iter, 2) == 'offered':
accept = gtk.MenuItem(_("Accept Friendship Request"))
menu.append(accept)
accept.connect('activate', lambda w:
self.controller.accept_friendship(model.get_path(iter)[0]))
deny = gtk.MenuItem(_("Deny Friendship Request"))
menu.append(deny)
deny.connect('activate', lambda w:
self.controller.cancel_friendship(model.get_path(iter)[0]))
elif model.get_value(iter, 2) == 'pending':
cancel = gtk.MenuItem(_("Cancel Friendship Request"))
menu.append(cancel)
cancel.connect('activate', lambda w:
self.controller.cancel_friendship(model.get_path(iter)[0]))
else:
cancel = gtk.MenuItem(_("Cancel Friendship"))
menu.append(cancel)
cancel.connect('activate', lambda w:
self.controller.cancel_friendship(model.get_path(iter)[0]))
menu.show_all()
return menu
class DumpView(object):
def __init__(self):
self.buffers = {}
self.window = self.create_window()
self.window.show_all()
def create_window(self):
win = gtk.Window()
win.set_title(_("pypsyc protocol dump"))
win.set_default_size(500, 300)
win.connect('destroy', lambda w: self.controller.closed())
self.notebook = gtk.Notebook()
win.add(self.notebook)
return win
def show_line(self, direction, line, account):
if account not in self.buffers:
scroll = gtk.ScrolledWindow()
tv = gtk.TextView()
tv.set_editable(False)
scroll.add(tv)
scroll.show_all()
self.notebook.append_page(scroll, gtk.Label(account))
self.buffers[account] = tv.get_buffer(), scroll.get_vadjustment()
buffer, va = self.buffers[account]
must_scroll = (va.value == va.upper - va.page_size)
line = repr(line)[1:-1]
buffer.insert(buffer.get_end_iter(),
('\n< ' if direction == 'o' else '\n> ') + line)
if must_scroll:
idle_add(self.scroll, va)
def scroll(self, va):
va.set_value(va.upper - va.page_size)
class MainView(object):
def __init__(self):
self.window = self.create_window()
self.window.show_all()
self.position = self.window.get_position()
self.status_icon = self.create_status_icon()
self.tabs_view = TabsView(self.status_icon)
def create_window(self):
win = gtk.Window()
win.set_title("pypsyc")
win.connect("delete_event", lambda w, e: win.hide() or True)
vbox = gtk.VBox()
win.add(vbox)
menu_bar = gtk.MenuBar()
vbox.pack_start(menu_bar, False)
menu_bar.append(self.create_file_menu())
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
vbox.pack_start(scroll)
self.friends_view = FriendsView()
scroll.add(self.friends_view.list_view)
return win
def create_file_menu(self):
menu = gtk.Menu()
accounts = gtk.MenuItem(_("Accounts"))
menu.append(accounts)
accounts.connect('activate', lambda w: self.controller.open_accounts())
conv = gtk.MenuItem(_("Open Conversation"))
menu.append(conv)
conv.connect('activate', lambda w: self.controller.open_conversation())
conf = gtk.MenuItem(_("Open Conference"))
menu.append(conf)
conf.connect('activate', lambda w: self.controller.open_conference())
add_friend = gtk.MenuItem(_("Add Friend"))
menu.append(add_friend)
add_friend.connect('activate', lambda w: self.controller.add_friend())
self.account_buttons = (conv, conf, add_friend)
self.show_active_accounts(False)
dump = gtk.MenuItem(_("Protocol Dump"))
menu.append(dump)
dump.connect('activate', lambda w: self.controller.open_dump())
menu.append(gtk.MenuItem())
quit = gtk.MenuItem(_("Quit"))
menu.append(quit)
quit.connect('activate', lambda w: self.controller.quit())
item = gtk.MenuItem(_("File"))
item.set_submenu(menu)
return item
def create_status_icon(self):
status_icon = gtk.StatusIcon()
status_icon.set_from_file(resource_filename(__name__, 'psyc.ico'))
status_icon.connect('activate', self._activated_status_icon)
return status_icon
def _activated_status_icon(self, w):
if self.tabs_view.on_status_icon_click():
if self.window.is_active():
self.position = self.window.get_position()
self.window.hide()
else:
self.window.move(*self.position)
self.window.present()
def show_active_accounts(self, active):
for i in self.account_buttons:
i.set_sensitive(active)
def show_password_dialog(self, uni, account, callback):
title = _("Enter Password")
attrs = (('Password', 'password'),
('Save password', 'save_password'))
text = "<big><b>Please enter password for account %s.</b></big>" % uni
_open_form_dialog(title, self.window, account, attrs,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT),
lambda d: callback(), markup=text)
def show_conn_error(self, account, message):
text = "%s - %s" % (_("Connection error"), account)
dialog = gtk.MessageDialog(self.window, (gtk.DIALOG_MODAL |
gtk.DIALOG_DESTROY_WITH_PARENT),
gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
dialog.format_secondary_text(message)
dialog.connect('response', lambda w, r: dialog.destroy())
dialog.show()
def show_no_such_user(self, account):
text = "No such user"
dialog = gtk.MessageDialog(self.window, (gtk.DIALOG_MODAL |
gtk.DIALOG_DESTROY_WITH_PARENT),
gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
dialog.format_secondary_text(account)
dialog.connect('response', lambda w, r: dialog.destroy())
dialog.show()
def show_auth_error(self, account, message):
text = "%s - %s" % (_("Authentication error"), account)
dialog = gtk.MessageDialog(self.window, (gtk.DIALOG_MODAL |
gtk.DIALOG_DESTROY_WITH_PARENT),
gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
dialog.format_secondary_text(message)
dialog.connect('response', lambda w, r: dialog.destroy())
dialog.show()
def show_accounts(self):
return AccountsView()
def show_open_conv_dialog(self, accounts, callback):
data = {'account': accounts, 'server': "", 'person': ""}
attrs = (('Account', 'account'),
('Server', 'server'),
('Person', 'person'))
_open_form_dialog(_("Open Conversation"),
self.window, data, attrs,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT),
lambda d: callback(**d))
def show_open_conf_dialog(self, accounts, callback):
data = {'account': accounts, 'server': "", 'place': ""}
attrs = (('Account', 'account'),
('Server', 'server'),
('Place', 'place'))
_open_form_dialog(_("Open Conference"),
self.window, data, attrs,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT),
lambda d: callback(**d))
def show_add_friend_dialog(self, accounts, callback):
data = {'account': accounts, 'server': "", 'person': ""}
attrs = (('Account', 'account'),
('Server', 'server'),
('Person', 'person'))
_open_form_dialog(_("Add Friend"),
self.window, data, attrs,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT),
lambda d: callback(**d))
def show_dump_win(self):
return DumpView()