""" 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 = "Please enter password for account %s." % 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()