diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index 7d5053885ba1b3e234282ff7929c7bfe5664fc8d..0000000000000000000000000000000000000000 --- a/src/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# Set the C flags to include the GTK+ and Python libraries -PYTHON ?= python -PYTHONVER = `$(PYTHON) -c 'import sys; print sys.version[:3]'` -gtk_CFLAGS = `pkg-config --cflags gtk+-2.0 pygtk-2.0` -fPIC -I/usr/include/python$(PYTHONVER) -I. -gtk_LDFLAGS = `pkg-config --libs gtk+-2.0 pygtk-2.0` -lpython$(PYTHONVER) - -all: trayicon.so gtkspell.so - -# Build the shared objects -trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o - $(CC) -shared $^ -o $@ $(LDFLAGS) $(gtk_LDFLAGS) - -gtkspell.so: - $(CC) $(OPTFLAGS) $(CFLAGS) $(LDFLAGS) $(gtk_CFLAGS) $(gtk_LDFLAGS) `pkg-config --libs --cflags gtkspell-2.0` -shared gtkspellmodule.c $^ -o $@ - -# The path to the GTK+ python types -DEFS=`pkg-config --variable=defsdir pygtk-2.0` - -%.o: %.c - $(CC) -o $@ -c $< $(CFLAGS) $(gtk_CFLAGS) - -# Generate the C wrapper from the defs and our override file -trayicon.c: trayicon.defs trayicon.override - pygtk-codegen-2.0 --prefix trayicon \ - --register $(DEFS)/gdk-types.defs \ - --register $(DEFS)/gtk-types.defs \ - --override trayicon.override \ - trayicon.defs > $@ - - -# A rule to clean the generated files -clean: - rm -f trayicon.so *.o trayicon.c gtkspell.so *~ - -.PHONY: clean diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..30eb995a95f44390ba2784d6328cc48a304b3c88 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,73 @@ +SUBDIRS = common + +CLEANFILES = \ + trayicon.c +INCLUDES = \ + $(PYTHON_INCLUDES) + +if BUILD_GTKSPELL +gtkspelllib_LTLIBRARIES = gtkspell.la +gtkspelllibdir = $(libdir)/gajim + +gtkspell_la_LIBADD = \ + $(GTKSPELL_LIBS) $(PYGTK_LIBS) + +gtkspell_la_SOURCES = \ + gtkspellmodule.c + +gtkspell_la_LDFLAGS = \ + -module -avoid-version + +gtkspell_la_CFLAGS = $(GTKSPELL_CFLAGS) $(PYGTK_CFLAGS) +endif +if BUILD_TRAYICON +trayiconlib_LTLIBRARIES = trayicon.la +trayiconlibdir = $(libdir)/gajim +trayicon_la_LIBADD = $(PYGTK_LIBS) +trayicon_la_SOURCES = \ + eggtrayicon.c \ + trayiconmodule.c + +nodist_trayicon_la_SOURCES = \ + trayicon.c + +trayicon_la_LDFLAGS = \ + -module -avoid-version +trayicon_la_CFLAGS = $(PYGTK_CFLAGS) + +trayicon.c: + pygtk-codegen-2.0 --prefix trayicon \ + --register $(PYGTK_DEFS)/gdk-types.defs \ + --register $(PYGTK_DEFS)/gtk-types.defs \ + --override $(srcdir)/trayicon.override \ + $(srcdir)/trayicon.defs > $@ +endif +gajimsrcdir = $(pkgdatadir)/src +gajimsrc_DATA = $(srcdir)/*.py + +gajimsrc1dir = $(pkgdatadir)/src/common +gajimsrc1_DATA = \ + $(srcdir)/common/*.py + +gajimsrc2dir = $(pkgdatadir)/src/common/xmpp +gajimsrc2_DATA = \ + $(srcdir)/common/xmpp/*.py + +gajimsrc3dir = $(pkgdatadir)/src/common/zeroconf +gajimsrc3_DATA = \ + $(srcdir)/common/zeroconf/*.py + +DISTCLEANFILES = + +EXTRA_DIST = $(gajimsrc_DATA) \ + $(gajimsrc1_DATA) \ + $(gajimsrc2_DATA) \ + $(gajimsrc3_DATA) \ + gtkspellmodule.c \ + eggtrayicon.c \ + trayiconmodule.c \ + eggtrayicon.h \ + trayicon.defs \ + trayicon.override + +MAINTAINERCLEANFILES = Makefile.in diff --git a/src/advanced.py b/src/advanced.py index 76bf6ed4f783f298f87841ccbfd42cf23fa1e4cb..0640c02d70036e096b102c30fdb75b77bc2948ba 100644 --- a/src/advanced.py +++ b/src/advanced.py @@ -1,18 +1,8 @@ ## advanced.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Vincent Hanquez <tab@snarc.org> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005 Vincent Hanquez <tab@snarc.org> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -42,7 +32,7 @@ C_TYPE GTKGUI_GLADE = 'manage_accounts_window.glade' -class AdvancedConfigurationWindow: +class AdvancedConfigurationWindow(object): def __init__(self): self.xml = gtkgui_helpers.get_glade('advanced_configuration_window.glade') self.window = self.xml.get_widget('advanced_configuration_window') diff --git a/src/cell_renderer_image.py b/src/cell_renderer_image.py index f09dfa7ac7206d4cb7471bc83c3fbdc0e7e575df..d6a9fbc63902bacd6a3651347718efe6151b3219 100644 --- a/src/cell_renderer_image.py +++ b/src/cell_renderer_image.py @@ -8,7 +8,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> diff --git a/src/chat_control.py b/src/chat_control.py index b9cf730a7918a091839395a3d46a8c6d211c4a99..7a0a9effcdcccb81721a37dadcc8a4d53b398b81 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -34,6 +34,8 @@ from message_textview import MessageTextView from common.contacts import GC_Contact from common.logger import Constants constants = Constants() +from common.rst_xhtml_generator import create_xhtml +from common.xmpp.protocol import NS_XHTML try: import gtkspell @@ -112,7 +114,7 @@ class ChatControlBase(MessageControl): acct, resource = None): MessageControl.__init__(self, type_id, parent_win, widget_name, display_names, contact, acct, resource = resource); - # when/if we do XHTML we will but formatting buttons back + # when/if we do XHTML we will put formatting buttons back widget = self.xml.get_widget('emoticons_button') id = widget.connect('clicked', self.on_emoticons_button_clicked) self.handlers[id] = widget @@ -196,7 +198,6 @@ class ChatControlBase(MessageControl): self.msg_textview.lang = lang spell.set_language(lang) except (gobject.GError, RuntimeError), msg: - #FIXME: add a ui for this use spell.set_language() dialogs.ErrorDialog(unicode(msg), _('If that is not your language ' 'for which you want to highlight misspelled words, then please ' 'set your $LANG as appropriate. Eg. for French do export ' @@ -229,6 +230,11 @@ class ChatControlBase(MessageControl): item = gtk.SeparatorMenuItem() menu.prepend(item) + item = gtk.ImageMenuItem(gtk.STOCK_CLEAR) + menu.prepend(item) + id = item.connect('activate', self.msg_textview.clear) + self.handlers[id] = item + if gajim.config.get('use_speller') and HAS_GTK_SPELL: item = gtk.MenuItem(_('Spelling language')) menu.prepend(item) @@ -242,11 +248,6 @@ class ChatControlBase(MessageControl): id = item.connect('activate', _on_select_dictionary, langs[lang]) self.handlers[id] = item - item = gtk.ImageMenuItem(gtk.STOCK_CLEAR) - menu.prepend(item) - id = item.connect('activate', self.msg_textview.clear) - self.handlers[id] = item - menu.show_all() # moved from ChatControl @@ -425,7 +426,8 @@ class ChatControlBase(MessageControl): message_textview = widget message_buffer = message_textview.get_buffer() start_iter, end_iter = message_buffer.get_bounds() - message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8') + message = message_buffer.get_text(start_iter, end_iter, False).decode( + 'utf-8') # construct event instance from binding event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here @@ -471,10 +473,11 @@ class ChatControlBase(MessageControl): self.send_message(message) # send the message else: # Give the control itself a chance to process - self.handle_message_textview_mykey_press(widget, event_keyval, event_keymod) + self.handle_message_textview_mykey_press(widget, event_keyval, + event_keymod) def _process_command(self, message): - if not message: + if not message or message[0] != '/': return False message = message[1:] @@ -534,7 +537,7 @@ class ChatControlBase(MessageControl): def print_conversation_line(self, text, kind, name, tim, other_tags_for_name = [], other_tags_for_time = [], other_tags_for_text = [], count_as_new = True, - subject = None, old_kind = None): + subject = None, old_kind = None, xhtml = None): '''prints 'chat' type messages''' jid = self.contact.jid full_jid = self.get_full_jid() @@ -544,20 +547,26 @@ class ChatControlBase(MessageControl): end = True textview.print_conversation_line(text, jid, kind, name, tim, other_tags_for_name, other_tags_for_time, other_tags_for_text, - subject, old_kind) + subject, old_kind, xhtml) if not count_as_new: return if kind == 'incoming': gajim.last_message_time[self.account][full_jid] = time.time() - urgent = True if (not self.parent_win.get_active_jid() or \ full_jid != self.parent_win.get_active_jid() or \ not self.parent_win.is_active() or not end) and \ kind in ('incoming', 'incoming_queue'): - if self.notify_on_new_messages(): + gc_message = False + if self.type_id == message_control.TYPE_GC: + gc_message = True + if not gc_message or \ + (gc_message and (other_tags_for_text == ['marked'] or \ + gajim.config.get('notify_on_all_muc_messages'))): + # we want to have save this message in events list + # other_tags_for_text == ['marked'] --> highlighted gc message type_ = 'printed_' + self.type_id - if self.type_id == message_control.TYPE_GC: + if gc_message: type_ = 'printed_gc_msg' show_in_roster = notify.get_show_in_roster('message_received', self.account, self.contact) @@ -572,10 +581,11 @@ class ChatControlBase(MessageControl): gajim.interface.roster.draw_contact(self.contact.jid, self.account) self.parent_win.redraw_tab(self) + ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account) if not self.parent_win.is_active(): - ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, - self.account) - self.parent_win.show_title(urgent, ctrl) + self.parent_win.show_title(True, ctrl) # Enabled Urgent hint + else: + self.parent_win.show_title(False, ctrl) # Disabled Urgent hint def toggle_emoticons(self): '''hide show emoticons_button and make sure emoticons_menu is always there @@ -647,17 +657,7 @@ class ChatControlBase(MessageControl): if not gajim.events.remove_events(self.account, self.get_full_jid(), types = [type_]): # There were events to remove - self.parent_win.redraw_tab(self) - self.parent_win.show_title() - # redraw roster - if self.type_id == message_control.TYPE_PM: - room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - groupchat_control = gajim.interface.msg_win_mgr.get_control( - room_jid, self.account) - groupchat_control.draw_contact(nick) - else: - gajim.interface.roster.draw_contact(jid, self.account) - gajim.interface.roster.show_title() + self.redraw_after_event_removed(jid) self.msg_textview.grab_focus() # Note, we send None chatstate to preserve current self.parent_win.redraw_tab(self) @@ -750,8 +750,22 @@ class ChatControlBase(MessageControl): if not gajim.events.remove_events(self.account, self.get_full_jid(), types = ['printed_' + type_, type_]): # There were events to remove - self.parent_win.redraw_tab(self) - self.parent_win.show_title() + self.redraw_after_event_removed(jid) + + def redraw_after_event_removed(self, jid): + ''' We just removed a 'printed_*' event, redraw contact in roster or + gc_roster and titles in roster and msg_win ''' + self.parent_win.redraw_tab(self) + self.parent_win.show_title() + # TODO : get the contact and check notify.get_show_in_roster() + if self.type_id == message_control.TYPE_PM: + room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) + groupchat_control = gajim.interface.msg_win_mgr.get_control( + room_jid, self.account) + groupchat_control.draw_contact(nick) + else: + gajim.interface.roster.draw_contact(jid, self.account) + gajim.interface.roster.show_title() def sent_messages_scroll(self, direction, conv_buf): size = len(self.sent_history) @@ -760,7 +774,8 @@ class ChatControlBase(MessageControl): #whatever is already typed start_iter = conv_buf.get_start_iter() end_iter = conv_buf.get_end_iter() - self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode('utf-8') + self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode( + 'utf-8') self.typing_new = False if direction == 'up': if self.sent_history_pos == 0: @@ -820,8 +835,8 @@ class ChatControl(ChatControlBase): old_msg_kind = None # last kind of the printed message def __init__(self, parent_win, contact, acct, resource = None): - ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox', - (_('Chat'), _('Chats')), contact, acct, resource) + ChatControlBase.__init__(self, self.TYPE_ID, parent_win, + 'chat_child_vbox', (_('Chat'), _('Chats')), contact, acct, resource) # for muc use: # widget = self.xml.get_widget('muc_window_actions_button') @@ -829,13 +844,16 @@ class ChatControl(ChatControlBase): id = widget.connect('clicked', self.on_actions_button_clicked) self.handlers[id] = widget - self.hide_chat_buttons_always = gajim.config.get('always_hide_chat_buttons') + self.hide_chat_buttons_always = gajim.config.get( + 'always_hide_chat_buttons') self.chat_buttons_set_visible(self.hide_chat_buttons_always) - self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_chat_banner')) + self.widget_set_visible(self.xml.get_widget('banner_eventbox'), + gajim.config.get('hide_chat_banner')) # Initialize drag-n-drop self.TARGET_TYPE_URI_LIST = 80 self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ] - id = self.widget.connect('drag_data_received', self._on_drag_data_received) + id = self.widget.connect('drag_data_received', + self._on_drag_data_received) self.handlers[id] = self.widget self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | @@ -857,17 +875,21 @@ class ChatControl(ChatControlBase): self._on_window_motion_notify) self.handlers[id] = self.parent_win.window message_tv_buffer = self.msg_textview.get_buffer() - id = message_tv_buffer.connect('changed', self._on_message_tv_buffer_changed) + id = message_tv_buffer.connect('changed', + self._on_message_tv_buffer_changed) self.handlers[id] = message_tv_buffer widget = self.xml.get_widget('avatar_eventbox') - id = widget.connect('enter-notify-event', self.on_avatar_eventbox_enter_notify_event) + id = widget.connect('enter-notify-event', + self.on_avatar_eventbox_enter_notify_event) self.handlers[id] = widget - id = widget.connect('leave-notify-event', self.on_avatar_eventbox_leave_notify_event) + id = widget.connect('leave-notify-event', + self.on_avatar_eventbox_leave_notify_event) self.handlers[id] = widget - id = widget.connect('button-press-event', self.on_avatar_eventbox_button_press_event) + id = widget.connect('button-press-event', + self.on_avatar_eventbox_button_press_event) self.handlers[id] = widget widget = self.xml.get_widget('gpg_togglebutton') @@ -881,12 +903,7 @@ class ChatControl(ChatControlBase): self.update_ui() # restore previous conversation self.restore_conversation() - # is account displayed after nick in banner ? - self.account_displayed= False - def notify_on_new_messages(self): - return gajim.config.get('trayicon_notification_on_new_messages') - def on_avatar_eventbox_enter_notify_event(self, widget, event): '''we enter the eventbox area so we under conditions add a timeout to show a bigger avatar after 0.5 sec''' @@ -924,7 +941,8 @@ class ChatControl(ChatControlBase): menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) id = menuitem.connect('activate', gtkgui_helpers.on_avatar_save_as_menuitem_activate, - self.contact.jid, self.account, self.contact.name + '.jpeg') + self.contact.jid, self.account, self.contact.get_shown_name() + + '.jpeg') self.handlers[id] = menuitem menu.append(menuitem) menu.show_all() @@ -1001,39 +1019,33 @@ class ChatControl(ChatControlBase): banner_name_label = self.xml.get_widget('banner_name_label') name = contact.get_shown_name() - avoid_showing_account_too = False if self.resource: name += '/' + self.resource - avoid_showing_account_too = True if self.TYPE_ID == message_control.TYPE_PM: - room_jid = self.contact.jid.split('/')[0] - room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, - self.account) - name = _('%s from room %s') % (name, room_ctrl.name) + name = _('%(nickname)s from group chat %(room_name)s') %\ + {'nickname': name, 'room_name': self.room_name} name = gtkgui_helpers.escape_for_pango_markup(name) - # We know our contacts nick, but if there are any other controls - # with the same nick we need to also display the account + # We know our contacts nick, but if another contact has the same nick + # in another account we need to also display the account. # except if we are talking to two different resources of the same contact acct_info = '' - self.account_displayed = False - for ctrl in self.parent_win.controls(): - if ctrl == self: + for account in gajim.contacts.get_accounts(): + if account == self.account: continue - if self.contact.get_shown_name() == ctrl.contact.get_shown_name()\ - and not avoid_showing_account_too: - self.account_displayed = True - if not ctrl.account_displayed: - # do that after this instance exists - gobject.idle_add(ctrl.draw_banner) - acct_info = ' (%s)' % \ - gtkgui_helpers.escape_for_pango_markup(self.account) + if acct_info: # We already found a contact with same nick break + for jid in gajim.contacts.get_jid_list(account): + contact_ = gajim.contacts.get_first_contact_from_jid(account, jid) + if contact_.get_shown_name() == self.contact.get_shown_name(): + acct_info = ' (%s)' % \ + gtkgui_helpers.escape_for_pango_markup(self.account) + break status = contact.status if status is not None: banner_name_label.set_ellipsize(pango.ELLIPSIZE_END) - status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 2) + status = helpers.reduce_chars_newlines(status, max_lines = 2) status_escaped = gtkgui_helpers.escape_for_pango_markup(status) font_attrs, font_attrs_small = self.get_font_attrs() @@ -1071,8 +1083,8 @@ class ChatControl(ChatControlBase): banner_name_label.set_markup(label_text) def on_toggle_gpg_togglebutton(self, widget): - gajim.config.set_per('contacts', self.contact.get_full_jid(), - 'gpg_enabled', widget.get_active()) + gajim.config.set_per('contacts', self.contact.jid, 'gpg_enabled', + widget.get_active()) def _update_gpg(self): tb = self.xml.get_widget('gpg_togglebutton') @@ -1166,8 +1178,8 @@ class ChatControl(ChatControlBase): if current_state == 'composing': self.send_chatstate('paused') # pause composing - # assume no activity and let the motion-notify or 'insert-text' make them True - # refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! + # assume no activity and let the motion-notify or 'insert-text' make them + # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! self.reset_kbd_mouse_timeout_vars() return True # loop forever @@ -1186,11 +1198,12 @@ class ChatControl(ChatControlBase): if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs: return True # loop forever - if not self.mouse_over_in_last_30_secs or self.kbd_activity_in_last_30_secs: + if not self.mouse_over_in_last_30_secs or \ + self.kbd_activity_in_last_30_secs: self.send_chatstate('inactive', contact) - # assume no activity and let the motion-notify or 'insert-text' make them True - # refresh 30 seconds too or else it's 30 - 5 = 25 seconds! + # assume no activity and let the motion-notify or 'insert-text' make them + # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds! self.reset_kbd_mouse_timeout_vars() return True # loop forever @@ -1201,7 +1214,7 @@ class ChatControl(ChatControlBase): self.kbd_activity_in_last_30_secs = False def print_conversation(self, text, frm = '', tim = None, - encrypted = False, subject = None): + encrypted = False, subject = None, xhtml = None): '''Print a line in the conversation: if contact is set to status: it's a status message if contact is set to another value: it's an outgoing message @@ -1240,8 +1253,12 @@ class ChatControl(ChatControlBase): else: kind = 'outgoing' name = gajim.nicks[self.account] + if not xhtml and not encrypted and gajim.config.get('rst_formatting_outgoing_messages'): + xhtml = create_xhtml(text) + if xhtml: + xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml) ChatControlBase.print_conversation_line(self, text, kind, name, tim, - subject = subject, old_kind = self.old_msg_kind) + subject = subject, old_kind = self.old_msg_kind, xhtml = xhtml) if text.startswith('/me ') or text.startswith('/me\n'): self.old_msg_kind = None else: @@ -1276,9 +1293,6 @@ class ChatControl(ChatControlBase): elif chatstate == 'paused': color = gajim.config.get_per('themes', theme, 'state_paused_color') - else: - color = gajim.config.get_per('themes', theme, - 'state_active_color') if color: # We set the color for when it's the current tab or not color = gtk.gdk.colormap_get_system().alloc_color(color) @@ -1287,6 +1301,9 @@ class ChatControl(ChatControlBase): if chatstate in ('inactive', 'gone') and\ self.parent_win.get_active_control() != self: color = self.lighten_color(color) + elif chatstate == 'active' : # active, get color from gtk + color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE] + name = self.contact.get_shown_name() if self.resource: @@ -1459,18 +1476,20 @@ class ChatControl(ChatControlBase): # prevent going paused if we we were not composing (JEP violation) if state == 'paused' and not contact.our_chatstate == 'composing': - MessageControl.send_message(self, None, chatstate = 'active') # go active before + # go active before + MessageControl.send_message(self, None, chatstate = 'active') contact.our_chatstate = 'active' self.reset_kbd_mouse_timeout_vars() # if we're inactive prevent composing (JEP violation) elif contact.our_chatstate == 'inactive' and state == 'composing': - MessageControl.send_message(self, None, chatstate = 'active') # go active before + # go active before + MessageControl.send_message(self, None, chatstate = 'active') contact.our_chatstate = 'active' self.reset_kbd_mouse_timeout_vars() - MessageControl.send_message(self, None, chatstate = state, msg_id = contact.msg_id, - composing_jep = contact.composing_jep) + MessageControl.send_message(self, None, chatstate = state, + msg_id = contact.msg_id, composing_jep = contact.composing_jep) contact.our_chatstate = state if contact.our_chatstate == 'active': self.reset_kbd_mouse_timeout_vars() @@ -1501,7 +1520,7 @@ class ChatControl(ChatControlBase): self.msg_textview.destroy() - def allow_shutdown(self): + def allow_shutdown(self, method): if time.time() - gajim.last_message_time[self.account]\ [self.get_full_jid()] < 2: # 2 seconds @@ -1593,13 +1612,16 @@ class ChatControl(ChatControlBase): return timeout = gajim.config.get('restore_timeout') # in minutes - events = gajim.events.get_events(self.account, jid, ['chat']) # number of messages that are in queue and are already logged, we want # to avoid duplication - pending_how_many = len(events) + pending_how_many = len(gajim.events.get_events(self.account, jid, + ['chat', 'pm'])) + if self.resource: + pending_how_many += len(gajim.events.get_events(self.account, + self.contact.get_full_jid(), ['chat', 'pm'])) rows = gajim.logger.get_last_conversation_lines(jid, restore_how_many, - pending_how_many, timeout, self.account) + pending_how_many, timeout, self.account) local_old_kind = None for row in rows: # row[0] time, row[1] has kind, row[2] the message if not row[2]: # message is empty, we don't print it @@ -1657,7 +1679,7 @@ class ChatControl(ChatControlBase): else: kind = 'print_queue' self.print_conversation(data[0], kind, tim = data[3], - encrypted = data[4], subject = data[1]) + encrypted = data[4], subject = data[1], xhtml = data[7]) if len(data) > 6 and isinstance(data[6], int): message_ids.append(data[6]) if message_ids: @@ -1665,30 +1687,19 @@ class ChatControl(ChatControlBase): gajim.events.remove_events(self.account, jid_with_resource, types = [self.type_id]) - self.parent_win.show_title() - self.parent_win.redraw_tab(self) - # redraw roster - gajim.interface.roster.show_title() - typ = 'chat' # Is it a normal chat or a pm ? # reset to status image in gc if it is a pm if is_pm: control.update_ui() typ = 'pm' - if is_pm: - room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - groupchat_control = gajim.interface.msg_win_mgr.get_control( - room_jid, self.account) - groupchat_control.draw_contact(nick) - else: - gajim.interface.roster.draw_contact(jid, self.account) - # Redraw parent too - gajim.interface.roster.draw_parent_contact(jid, self.account) - if (self.contact.show == 'offline' or self.contact.show == 'error'): - showOffline = gajim.config.get('showoffline') - if not showOffline and typ == 'chat' and \ - len(gajim.contacts.get_contact(self.account, jid)) < 2: + self.redraw_after_event_removed(jid) + if (self.contact.show in ('offline', 'error')): + show_offline = gajim.config.get('showoffline') + show_transports = gajim.config.get('show_transports_group') + if (not show_transports and gajim.jid_is_transport(jid)) or \ + (not show_offline and typ == 'chat' and \ + len(gajim.contacts.get_contact(self.account, jid)) < 2): gajim.interface.roster.really_remove_contact(self.contact, self.account) elif typ == 'pm': diff --git a/src/common/GnuPG.py b/src/common/GnuPG.py index bb6753fca8e401295778ae74d948a4aff7e4d379..af38cddf9132bb946368e50bfaf98d0bfff38b7b 100644 --- a/src/common/GnuPG.py +++ b/src/common/GnuPG.py @@ -2,13 +2,13 @@ ## ## Contributors for this file: ## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <nkour@jabber.org> +## - Nikos Kouremenos <kourem@gmail.com> ## ## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> diff --git a/src/common/Makefile b/src/common/Makefile deleted file mode 100644 index c6d05a2d4e24af78bbb4f91bfbd2b99ac7cdc6a3..0000000000000000000000000000000000000000 --- a/src/common/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -# Set the C flags to include the GTK+ and Python libraries -PYTHON ?= python -PYTHONVER = `$(PYTHON) -c 'import sys; print sys.version[:3]'` - -HAVE_XSCRNSAVER = $(shell pkg-config --exists xscrnsaver && echo 'YES') - -ifeq ($(HAVE_XSCRNSAVER),YES) -# We link with libXScrnsaver from modular X.Org X11 -gtk_and_x_CFLAGS = `pkg-config --cflags gtk+-2.0 pygtk-2.0 xscrnsaver` -fpic -I/usr/include/python$(PYTHONVER) -I. -gtk_and_x_LDFLAGS = `pkg-config --libs gtk+-2.0 pygtk-2.0 xscrnsaver` -lpython$(PYTHONVER) -else -# # We link with libXScrnsaver from monolithic X.Org X11 -gtk_and_x_CFLAGS = `pkg-config --cflags gtk+-2.0 pygtk-2.0` -fpic -I/usr/include/python$(PYTHONVER) -I. -gtk_and_x_LDFLAGS = `pkg-config --libs gtk+-2.0 pygtk-2.0` \ - -L/usr/X11R6$(LIBDIR) -lX11 -lXss -lXext -lpython$(PYTHONVER) -endif - -all: idle.so - -idle.so: - $(CC) $(OPTFLAGS) $(CFLAGS) $(LDFLAGS) $(gtk_and_x_CFLAGS) $(gtk_and_x_LDFLAGS) -shared idle.c $^ -o $@ - -clean: - rm -f *.so - rm -rf build diff --git a/src/common/Makefile.am b/src/common/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..ff75958c0b3c427b06b6c4782674d089f68e27c4 --- /dev/null +++ b/src/common/Makefile.am @@ -0,0 +1,22 @@ + +INCLUDES = \ + $(PYTHON_INCLUDES) +if BUILD_IDLE +idlelib_LTLIBRARIES = idle.la +idlelibdir = $(libdir)/gajim + +idle_la_LIBADD = $(XSCRNSAVER_LIBS) + +idle_la_SOURCES = idle.c + +idle_la_LDFLAGS = \ + -module -avoid-version + +idle_la_CFLAGS = $(XSCRNSAVER_CFLAGS) $(PYTHON_INCLUDES) +endif + +DISTCLEANFILES = + +EXTRA_DIST = + +MAINTAINERCLEANFILES = Makefile.in diff --git a/src/common/check_paths.py b/src/common/check_paths.py index 03913473d4837cbff1ecf2ca71384b22fda15b0a..1008e8c4f514ff74e4c6cecf55b1bcd56735676b 100644 --- a/src/common/check_paths.py +++ b/src/common/check_paths.py @@ -1,16 +1,7 @@ -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Travis Shirk <travis@pobox.com> ## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -29,7 +20,14 @@ import stat from common import gajim import logger -from pysqlite2 import dbapi2 as sqlite # DO NOT MOVE ABOVE OF import gajim +# DO NOT MOVE ABOVE OF import gajim +try: + import sqlite3 as sqlite # python 2.5 +except ImportError: + try: + from pysqlite2 import dbapi2 as sqlite + except ImportError: + raise exceptions.PysqliteNotAvailable def create_log_db(): print _('creating logs database') @@ -57,11 +55,13 @@ def create_log_db(): jid_id INTEGER ); + CREATE INDEX idx_unread_messages_jid_id ON unread_messages (jid_id); + CREATE TABLE transports_cache ( transport TEXT UNIQUE, type INTEGER ); - + CREATE TABLE logs( log_line_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, jid_id INTEGER, @@ -72,6 +72,8 @@ def create_log_db(): message TEXT, subject TEXT ); + + CREATE INDEX idx_logs_jid_id_kind ON logs (jid_id, kind); ''' ) diff --git a/src/common/config.py b/src/common/config.py index 96f51ea6b72eee868a748850bf1cda88d1a42d65..23b997d47af3dab4e3e5f35a567055b15511afed 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -6,7 +6,8 @@ ## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com> ## Copyright (C) 2005 Travis Shirk <travis@pobox.com> ## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za> -## +## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> +## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published ## by the Free Software Foundation; version 2 only. @@ -20,6 +21,7 @@ import sre import copy +import defs ( @@ -37,6 +39,8 @@ opt_bool = [ 'boolean', 0 ] opt_color = [ 'color', '^(#[0-9a-fA-F]{6})|()$' ] opt_one_window_types = ['never', 'always', 'peracct', 'pertype'] +DEFAULT_ICONSET = 'dcraven' + class Config: __options = { @@ -50,7 +54,8 @@ class Config: 'autopopupaway': [ opt_bool, False ], 'use_notif_daemon': [ opt_bool, True , _('Use D-Bus and Notification-Daemon to show notifications') ], 'ignore_unknown_contacts': [ opt_bool, False ], - 'showoffline': [ opt_bool, False, '', True ], + 'showoffline': [ opt_bool, False ], + 'show_transports_group': [ opt_bool, True ], 'autoaway': [ opt_bool, True ], 'autoawaytime': [ opt_int, 5, _('Time in minutes, after which your status changes to away.') ], 'autoaway_message': [ opt_str, _('Away as a result of being idle') ], @@ -67,7 +72,7 @@ class Config: 'last_status_msg_invisible': [ opt_str, '' ], 'last_status_msg_offline': [ opt_str, '' ], 'trayicon': [ opt_bool, True, '', True ], - 'iconset': [ opt_str, 'dcraven', '', True ], + 'iconset': [ opt_str, DEFAULT_ICONSET, '', True ], 'use_transports_iconsets': [ opt_bool, True, '', True ], 'inmsgcolor': [ opt_color, '#a34526', '', True ], 'outmsgcolor': [ opt_color, '#164e6f', '', True ], @@ -79,15 +84,19 @@ class Config: 'saveposition': [ opt_bool, True ], 'mergeaccounts': [ opt_bool, False, '', True ], 'sort_by_show': [ opt_bool, True, '', True ], + 'enable_zeroconf': [opt_bool, False, _('Enable link-local/zeroconf messaging')], 'use_speller': [ opt_bool, False, ], + 'ignore_incoming_xhtml': [ opt_bool, False, ], 'speller_language': [ opt_str, '', _('Language used by speller')], 'print_time': [ opt_str, 'always', _('\'always\' - print time for every message.\n\'sometimes\' - print time every print_ichat_every_foo_minutes minute.\n\'never\' - never print time.')], - 'print_time_fuzzy': [ opt_int, 0, _('Value of fuzziness from 1 to 4 or 0 to disable fuzzyclock. 1 is the most precise clock, 4 the less precise one.') ], + 'print_time_fuzzy': [ opt_int, 0, _('Print time in chats using Fuzzy Clock. Value of fuzziness from 1 to 4, or 0 to disable fuzzyclock. 1 is the most precise clock, 4 the less precise one. This is used only if print_time is \'sometimes\'.') ], 'emoticons_theme': [opt_str, 'static', '', True ], 'ascii_formatting': [ opt_bool, True, _('Treat * / _ pairs as possible formatting characters.'), True], 'show_ascii_formatting_chars': [ opt_bool, True , _('If True, do not ' 'remove */_ . So *abc* will be bold but with * * not removed.')], + 'rst_formatting_outgoing_messages': [ opt_bool, False, + _('Uses ReStructured text markup for HTML, plus ascii formatting if selected. (If you want to use this, install docutils)')], 'sounds_on': [ opt_bool, True ], # 'aplay', 'play', 'esdplay', 'artsplay' detected first time only 'soundplayer': [ opt_str, '' ], @@ -125,6 +134,7 @@ class Config: 'before_nickname': [ opt_str, '' ], 'after_nickname': [ opt_str, ':' ], 'send_os_info': [ opt_bool, True ], + 'set_status_msg_from_current_music_track': [ opt_bool, False ], 'notify_on_new_gmail_email': [ opt_bool, True ], 'notify_on_new_gmail_email_extra': [ opt_bool, False ], 'usegpg': [ opt_bool, False, '', True ], @@ -135,25 +145,26 @@ class Config: 'send_on_ctrl_enter': [opt_bool, False, _('Send message on Ctrl+Enter and with Enter make new line (Mirabilis ICQ Client default behaviour).')], 'show_roster_on_startup': [opt_bool, True], 'key_up_lines': [opt_int, 25, _('How many lines to store for Ctrl+KeyUP.')], - 'version': [ opt_str, '0.10.1.3' ], # which version created the config + 'version': [ opt_str, defs.version ], # which version created the config 'search_engine': [opt_str, 'http://www.google.com/search?&q=%s&sourceid=gajim'], 'dictionary_url': [opt_str, 'WIKTIONARY', _("Either custom url with %s in it where %s is the word/phrase or 'WIKTIONARY' which means use wiktionary.")], 'always_english_wikipedia': [opt_bool, False], 'always_english_wiktionary': [opt_bool, True], 'remote_control': [opt_bool, True, _('If checked, Gajim can be controlled remotely using gajim-remote.'), True], + 'networkmanager_support': [opt_bool, True, _('If True, listen to D-Bus signals from NetworkManager and change the status of accounts (provided they do not have listen_to_network_manager set to False and they sync with global status) based upon the status of the network connection.'), True], 'chat_state_notifications': [opt_str, 'all'], # 'all', 'composing_only', 'disabled' 'autodetect_browser_mailer': [opt_bool, False, '', True], 'print_ichat_every_foo_minutes': [opt_int, 5, _('When not printing time for every message (print_time==sometimes), print it every x minutes.')], 'confirm_close_muc': [opt_bool, True, _('Ask before closing a group chat tab/window.')], - 'confirm_close_muc_rooms': [opt_str, '', _('Always ask before closing group chat tab/window in this space separated list of room jids.')], - 'noconfirm_close_muc_rooms': [opt_str, '', _('Never ask before closing group chat tab/window in this space separated list of room jids.')], + 'confirm_close_muc_rooms': [opt_str, '', _('Always ask before closing group chat tab/window in this space separated list of group chat jids.')], + 'noconfirm_close_muc_rooms': [opt_str, '', _('Never ask before closing group chat tab/window in this space separated list of group chat jids.')], 'notify_on_file_complete': [opt_bool, True], 'file_transfers_port': [opt_int, 28011], 'ft_override_host_to_send': [opt_str, '', _('Overrides the host we send for File Transfer in case of address translation/port forwarding.')], 'conversation_font': [opt_str, ''], 'use_kib_mib': [opt_bool, False, _('IEC standard says KiB = 1024 bytes, KB = 1000 bytes.')], 'notify_on_all_muc_messages': [opt_bool, False], - 'trayicon_notification_on_new_messages': [opt_bool, True], + 'trayicon_notification_on_events': [opt_bool, True, _('Notify of events in the system trayicon.')], 'last_save_dir': [opt_str, ''], 'last_send_dir': [opt_str, ''], 'last_emoticons_dir': [opt_str, ''], @@ -174,7 +185,7 @@ class Config: 'notification_position_y': [opt_int, -1], 'notification_avatar_width': [opt_int, 48], 'notification_avatar_height': [opt_int, 48], - 'muc_highlight_words': [opt_str, '', _('A semicolon-separated list of words that will be highlighted in multi-user chat.')], + 'muc_highlight_words': [opt_str, '', _('A semicolon-separated list of words that will be highlighted in group chats.')], 'quit_on_roster_x_button': [opt_bool, False, _('If True, quits Gajim when X button of Window Manager is clicked. This setting is taken into account only if trayicon is used.')], 'set_xmpp://_handler_everytime': [opt_bool, False, _('If True, Gajim registers for xmpp:// on each startup.')], 'show_unread_tab_icon': [opt_bool, False, _('If True, Gajim will display an icon on each tab containing unread messages. Depending on the theme, this icon may be animated.')], @@ -182,15 +193,15 @@ class Config: 'show_avatars_in_roster': [opt_bool, True, '', True], 'ask_avatars_on_startup': [opt_bool, True, _('If True, Gajim will ask for avatar each contact that did not have an avatar last time or has one cached that is too old.')], 'print_status_in_chats': [opt_bool, True, _('If False, Gajim will no longer print status line in chats when a contact changes his or her status and/or his or her status message.')], - 'print_status_in_muc': [opt_str, 'in_and_out', _('can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes his or her status and/or his or her status message. If "all" Gajim will print all status messages. If "in_and_out", gajim will only print FOO enters/leaves room.')], + 'print_status_in_muc': [opt_str, 'in_and_out', _('can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes his or her status and/or his or her status message. If "all" Gajim will print all status messages. If "in_and_out", gajim will only print FOO enters/leaves group chat.')], 'log_contact_status_changes': [opt_bool, False], 'restored_messages_color': [opt_str, 'grey'], 'restored_messages_small': [opt_bool, True, _('If True, restored messages will use a smaller font than the default one.')], 'hide_avatar_of_transport': [opt_bool, False, _('Don\'t show avatar for the transport itself.')], - 'roster_window_skip_taskbar': [opt_bool, False], + 'roster_window_skip_taskbar': [opt_bool, False, _('Don\'t show roster in the system taskbar.')], 'use_urgency_hint': [opt_bool, True, _('If True and installed GTK+ and PyGTK versions are at least 2.8, make the window flash (the default behaviour in most Window Managers) when holding pending events.')], 'notification_timeout': [opt_int, 5], - 'send_sha_in_gc_presence': [opt_bool, True, _('Jabberd1.4 does not like sha info when one join a password protected room. Turn this option to False to stop sending sha info in group chat presences.')], + 'send_sha_in_gc_presence': [opt_bool, True, _('Jabberd1.4 does not like sha info when one join a password protected group chat. Turn this option to False to stop sending sha info in group chat presences.')], 'one_message_window': [opt_str, 'always', #always, never, peracct, pertype should not be translated _('Controls the window where new messages are placed.\n\'always\' - All messages are sent to a single window.\n\'never\' - All messages get their own window.\n\'peracct\' - Messages for each account are sent to a specific window.\n\'pertype\' - Each message type (e.g., chats vs. groupchats) are sent to a specific window. Note, changing this option requires restarting Gajim before the changes will take effect.')], @@ -200,11 +211,12 @@ class Config: 'always_hide_chat_buttons': [opt_bool, False, _('Hides the buttons in two persons chat window.')], 'hide_groupchat_banner': [opt_bool, False, _('Hides the banner in a group chat window')], 'hide_chat_banner': [opt_bool, False, _('Hides the banner in two persons chat window')], - 'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the room occupants list in group chat window.')], - 'chat_merge_consecutive_nickname': [opt_bool, False, _('Merge consecutive nickname in chat window.')], + 'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the group chat occupants list in group chat window.')], + 'chat_merge_consecutive_nickname': [opt_bool, False, _('In a chat, show the nickname at the beginning of a line only when it\'s not the same person talking than in previous message.')], 'chat_merge_consecutive_nickname_indent': [opt_str, ' ', _('Indentation when using merge consecutive nickame.')], 'gc_nicknames_colors': [ opt_str, '#a34526:#c000ff:#0012ff:#388a99:#38995d:#519938:#ff8a00:#94452d:#244b5a:#32645a', _('List of colors that will be used to color nicknames in group chats.'), True ], 'ctrl_tab_go_to_next_composing': [opt_bool, True, _('Ctrl-Tab go to next composing tab when none is unread.')], + 'confirm_metacontacts': [ opt_str, '', _('Should we show the confirm metacontacts creation dialog or not? Empty string means we never show the dialog.')], } __options_per_key = { @@ -215,6 +227,13 @@ class Config: 'password': [ opt_str, '' ], 'resource': [ opt_str, 'gajim', '', True ], 'priority': [ opt_int, 5, '', True ], + 'adjust_priority_with_status': [ opt_bool, True, _('Priority will change automatically according to your status. Priorities are defined in autopriority_* options.') ], + 'autopriority_online': [ opt_int, 50], + 'autopriority_chat': [ opt_int, 50], + 'autopriority_away': [ opt_int, 40], + 'autopriority_xa': [ opt_int, 30], + 'autopriority_dnd': [ opt_int, 20], + 'autopriority_invisible': [ opt_int, 10], 'autoconnect': [ opt_bool, False, '', True ], 'autoreconnect': [ opt_bool, True ], 'active': [ opt_bool, True], @@ -246,6 +265,12 @@ class Config: 'msgwin-y-position': [opt_int, -1], # Default is to let the wm decide 'msgwin-width': [opt_int, 480], 'msgwin-height': [opt_int, 440], + 'listen_to_network_manager' : [opt_bool, True], + 'is_zeroconf': [opt_bool, False], + 'zeroconf_first_name': [ opt_str, '', '', True ], + 'zeroconf_last_name': [ opt_str, '', '', True ], + 'zeroconf_jabber_id': [ opt_str, '', '', True ], + 'zeroconf_email': [ opt_str, '', '', True ], }, {}), 'statusmsg': ({ 'message': [ opt_str, '' ], @@ -284,8 +309,6 @@ class Config: 'bannerfontattrs': [ opt_str, 'B', '', True ], # http://www.pitt.edu/~nisg/cis/web/cgi/rgb.html - # FIXME: not black but the default color from gtk+ theme - 'state_active_color': [ opt_color, 'black' ], 'state_inactive_color': [ opt_color, 'grey62' ], 'state_composing_color': [ opt_color, 'green4' ], 'state_paused_color': [ opt_color, 'mediumblue' ], @@ -327,15 +350,15 @@ class Config: _('Movie'): _("I'm watching a movie."), _('Working'): _("I'm working."), _('Phone'): _("I'm on the phone."), - _('Out'): _("I'm out enjoying life"), + _('Out'): _("I'm out enjoying life."), } defaultstatusmsg_default = { - 'online': [ False, _("I'm available") ], - 'chat': [ False, _("I'm free for chat") ], - 'away': [ False, _('Be right back') ], - 'xa': [ False, _("I'm not available") ], - 'dnd': [ False, _('Do not disturb') ], + 'online': [ False, _("I'm available.") ], + 'chat': [ False, _("I'm free for chat.") ], + 'away': [ False, _('Be right back.') ], + 'xa': [ False, _("I'm not available.") ], + 'dnd': [ False, _('Do not disturb.') ], 'invisible': [ False, _('Bye!') ], 'offline': [ False, _('Bye!') ], } @@ -346,8 +369,8 @@ class Config: 'contact_connected': [ True, '../data/sounds/connected.wav' ], 'contact_disconnected': [ True, '../data/sounds/disconnected.wav' ], 'message_sent': [ True, '../data/sounds/sent.wav' ], - 'muc_message_highlight': [ True, '../data/sounds/gc_message1.wav', _('Sound to play when a MUC message contains one of the words in muc_highlight_words, or when a MUC message contains your nickname.')], - 'muc_message_received': [ True, '../data/sounds/gc_message2.wav', _('Sound to play when any MUC message arrives. (This setting is taken into account only if notify_on_all_muc_messages is True)') ], + 'muc_message_highlight': [ True, '../data/sounds/gc_message1.wav', _('Sound to play when a group chat message contains one of the words in muc_highlight_words, or when a group chat message contains your nickname.')], + 'muc_message_received': [ False, '../data/sounds/gc_message2.wav', _('Sound to play when any MUC message arrives.') ], } themes_default = { diff --git a/src/common/configpaths.py b/src/common/configpaths.py new file mode 100644 index 0000000000000000000000000000000000000000..0fce3d1dac646d310c47137834f3339ff1c074d7 --- /dev/null +++ b/src/common/configpaths.py @@ -0,0 +1,115 @@ +import os +import sys +import tempfile + +# Note on path and filename encodings: +# +# In general it is very difficult to do this correctly. +# We may pull information from environment variables, and what encoding that is +# in is anyone's guess. Any information we request directly from the file +# system will be in filesystemencoding, and (parts of) paths that we write in +# this source code will be in whatever encoding the source is in. (I hereby +# declare this file to be UTF-8 encoded.) +# +# To make things more complicated, modern Windows filesystems use UTF-16, but +# the API tends to hide this from us. +# +# I tried to minimize problems by passing Unicode strings to OS functions as +# much as possible. Hopefully this makes the function return an Unicode string +# as well. If not, we get an 8-bit string in filesystemencoding, which we can +# happily pass to functions that operate on files and directories, so we can +# just leave it as is. Since these paths are meant to be internal to Gajim and +# not displayed to the user, Unicode is not really necessary here. + +def fse(s): + '''Convert from filesystem encoding if not already Unicode''' + return unicode(s, sys.getfilesystemencoding()) + +class ConfigPaths: + def __init__(this, root=None): + this.root = root + this.paths = {} + + if this.root is None: + if os.name == 'nt': + try: + # Documents and Settings\[User Name]\Application Data\Gajim + + # How are we supposed to know what encoding the environment + # variable 'appdata' is in? Assuming it to be in filesystem + # encoding. + this.root = os.path.join(fse(os.environ[u'appdata']), u'Gajim') + except KeyError: + # win9x, in cwd + this.root = u'' + else: # Unices + # Pass in an Unicode string, and hopefully get one back. + this.root = os.path.expanduser(u'~/.gajim') + + def add_from_root(this, name, path): + this.paths[name] = (True, path) + + def add(this, name, path): + this.paths[name] = (False, path) + + def __getitem__(this, key): + relative, path = this.paths[key] + if not relative: + return path + return os.path.join(this.root, path) + + def get(this, key, default=None): + try: + return this[key] + except KeyError: + return default + + def iteritems(this): + for key in this.paths.iterkeys(): + yield (key, this[key]) + +def windowsify(s): + if os.name == 'nt': + return s.capitalize() + return s + +def init(): + paths = ConfigPaths() + + # LOG is deprecated + k = ( 'LOG', 'LOG_DB', 'VCARD', 'AVATAR', 'MY_EMOTS' ) + v = (u'logs', u'logs.db', u'vcards', u'avatars', u'emoticons') + + if os.name == 'nt': + v = map(lambda x: x.capitalize(), v) + + for n, p in zip(k, v): + paths.add_from_root(n, p) + + paths.add('DATA', os.path.join(u'..', windowsify(u'data'))) + paths.add('HOME', os.path.expanduser(u'~')) + paths.add('TMP', fse(tempfile.gettempdir())) + + try: + import svn_config + svn_config.configure(paths) + except (ImportError, AttributeError): + pass + + #for k, v in paths.iteritems(): + # print "%s: %s" % (k, v) + + return paths + +gajimpaths = init() + +def init_profile(profile, paths=gajimpaths): + conffile = windowsify(u'config') + pidfile = windowsify(u'gajim') + + if len(profile) > 0: + conffile += u'.' + profile + pidfile += u'.' + profile + pidfile += u'.pid' + paths.add_from_root('CONFIG_FILE', conffile) + paths.add_from_root('PID_FILE', pidfile) diff --git a/src/common/connection.py b/src/common/connection.py index f270cceef9ea75d65a7ae28042b23efeef0507a1..771fb31b9d9ed04827f18b8fec99f6454a0bf32b 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -1,19 +1,11 @@ ## common/connection.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <nkour@jabber.org> -## - Dimitur Kirov <dkirov@gmail.com> -## - Travis Shirk <travis@pobox.com> ## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org> +## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com> +## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -38,10 +30,13 @@ import common.xmpp from common import helpers from common import gajim from common import GnuPG +from common import passwords from connection_handlers import * USE_GPG = GnuPG.USE_GPG +from common.rst_xhtml_generator import create_xhtml + class Connection(ConnectionHandlers): '''Connection class''' def __init__(self, name): @@ -51,8 +46,10 @@ class Connection(ConnectionHandlers): self.connection = None # xmpppy ClientCommon instance # this property is used to prevent double connections self.last_connection = None # last ClientCommon instance + self.is_zeroconf = False self.gpg = None self.status = '' + self.priority = gajim.get_priority(name, 'offline') self.old_show = '' # increase/decrease default timeout for server responses self.try_connecting_for_foo_secs = 45 @@ -61,11 +58,12 @@ class Connection(ConnectionHandlers): self.time_to_reconnect = None self.new_account_info = None self.bookmarks = [] + self.annotations = {} self.on_purpose = False self.last_io = gajim.idlequeue.current_time() self.last_sent = [] self.last_history_line = {} - self.password = gajim.config.get_per('accounts', name, 'password') + self.password = passwords.get_password(name) self.server_resource = gajim.config.get_per('accounts', name, 'resource') if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'): self.keepalives = gajim.config.get_per('accounts', self.name,'keep_alive_every_foo_secs') @@ -88,6 +86,7 @@ class Connection(ConnectionHandlers): self.available_transports = {} # list of available transports on this # server {'icq': ['icq.server.com', 'icq2.server.com'], } self.vcard_supported = True + self.metacontacts_supported = True # END __init__ def put_event(self, ev): @@ -118,6 +117,7 @@ class Connection(ConnectionHandlers): self.on_purpose = on_purpose self.connected = 0 self.time_to_reconnect = None + self.privacy_rules_supported = False if self.connection: # make sure previous connection is completely closed gajim.proxy65_manager.disconnect(self.connection) @@ -128,23 +128,22 @@ class Connection(ConnectionHandlers): def _disconnectedReconnCB(self): '''Called when we are disconnected''' gajim.log.debug('disconnectedReconnCB') - if self.connected > 1: - # we cannot change our status to offline or connectiong + if gajim.account_is_connected(self.name): + # we cannot change our status to offline or connecting # after we auth to server self.old_show = STATUS_LIST[self.connected] self.connected = 0 self.dispatch('STATUS', 'offline') if not self.on_purpose: self.disconnect() - if gajim.config.get_per('accounts', self.name, 'autoreconnect') \ - and self.retrycount <= 10: + if gajim.config.get_per('accounts', self.name, 'autoreconnect'): self.connected = 1 self.dispatch('STATUS', 'connecting') # this check has moved from _reconnect method if self.retrycount > 5: - self.time_to_reconnect = 20 + self.time_to_reconnect = random.randint(15, 25) else: - self.time_to_reconnect = 10 + self.time_to_reconnect = random.randint(5, 15) gajim.idlequeue.set_alarm(self._reconnect_alarm, self.time_to_reconnect) elif self.on_connect_failure: @@ -163,7 +162,7 @@ class Connection(ConnectionHandlers): self.dispatch('STATUS', 'offline') self.dispatch('CONNECTION_LOST', (_('Connection with account "%s" has been lost') % self.name, - _('To continue sending and receiving messages, you will need to reconnect.'))) + _('Reconnect manually.'))) def _event_dispatcher(self, realm, event, data): if realm == common.xmpp.NS_REGISTER: @@ -185,7 +184,6 @@ class Connection(ConnectionHandlers): if not common.xmpp.isResultNode(result): self.dispatch('ACC_NOT_OK', (result.getError())) return - self.connected = 0 self.password = self.new_account_info['password'] if USE_GPG: self.gpg = GnuPG.GnuPG() @@ -195,7 +193,9 @@ class Connection(ConnectionHandlers): gajim.connections[self.name] = self self.dispatch('ACC_OK', (self.new_account_info)) self.new_account_info = None - self.connection = None + if self.connection: + self.connection.UnregisterDisconnectHandler(self._on_new_account) + self.disconnect(on_purpose=True) common.xmpp.features_nb.register(self.connection, data[0], req, _on_register_result) return @@ -368,8 +368,7 @@ class Connection(ConnectionHandlers): secure = self._secure) return else: - if not retry or self.retrycount > 10: - self.retrycount = 0 + if not retry and self.retrycount == 0: self.time_to_reconnect = None if self.on_connect_failure: self.on_connect_failure() @@ -404,8 +403,6 @@ class Connection(ConnectionHandlers): con.RegisterDisconnectHandler(self._disconnectedReconnCB) gajim.log.debug(_('Connected to server %s:%s with %s') % (self._current_host['host'], self._current_host['port'], con_type)) - # Ask metacontacts before roster - self.get_metacontacts() self._register_handlers(con, con_type) return True @@ -459,7 +456,7 @@ class Connection(ConnectionHandlers): # END connect def quit(self, kill_core): - if kill_core and self.connected > 1: + if kill_core and gajim.account_is_connected(self.name): self.disconnect(on_purpose = True) def get_privacy_lists(self): @@ -531,14 +528,15 @@ class Connection(ConnectionHandlers): # active the privacy rule self.privacy_rules_supported = True self.activate_privacy_rule('invisible') - prio = unicode(gajim.config.get_per('accounts', self.name, 'priority')) - p = common.xmpp.Presence(typ = ptype, priority = prio, show = show) + priority = unicode(gajim.get_priority(self.name, show)) + p = common.xmpp.Presence(typ = ptype, priority = priority, show = show) p = self.add_sha(p, ptype != 'unavailable') if msg: p.setStatus(msg) if signed: p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) self.connection.send(p) + self.priority = priority self.dispatch('STATUS', 'invisible') if initial: #ask our VCard @@ -546,6 +544,9 @@ class Connection(ConnectionHandlers): #Get bookmarks from private namespace self.get_bookmarks() + + #Get annotations + self.get_annotations() #Inform GUI we just signed in self.dispatch('SIGNED_IN', ()) @@ -591,8 +592,11 @@ class Connection(ConnectionHandlers): if self.connection: con.set_send_timeout(self.keepalives, self.send_keepalive) self.connection.onreceive(None) - # Ask metacontacts before roster - self.get_metacontacts() + iq = common.xmpp.Iq('get', common.xmpp.NS_PRIVACY, xmlns = '') + id = self.connection.getAnID() + iq.setID(id) + self.awaiting_answers[id] = (PRIVACY_ARRIVED, ) + self.connection.send(iq) def change_status(self, show, msg, auto = False): if not show in STATUS_LIST: @@ -646,9 +650,8 @@ class Connection(ConnectionHandlers): iq = self.build_privacy_rule('visible', 'allow') self.connection.send(iq) self.activate_privacy_rule('visible') - prio = unicode(gajim.config.get_per('accounts', self.name, - 'priority')) - p = common.xmpp.Presence(typ = None, priority = prio, show = sshow) + priority = unicode(gajim.get_priority(self.name, sshow)) + p = common.xmpp.Presence(typ = None, priority = priority, show = sshow) p = self.add_sha(p) if msg: p.setStatus(msg) @@ -656,6 +659,7 @@ class Connection(ConnectionHandlers): p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) if self.connection: self.connection.send(p) + self.priority = priority self.dispatch('STATUS', show) def _on_disconnected(self): @@ -666,17 +670,22 @@ class Connection(ConnectionHandlers): def get_status(self): return STATUS_LIST[self.connected] - def send_motd(self, jid, subject = '', msg = ''): + + def send_motd(self, jid, subject = '', msg = '', xhtml = None): if not self.connection: return - msg_iq = common.xmpp.Message(to = jid, body = msg, subject = subject) + msg_iq = common.xmpp.Message(to = jid, body = msg, subject = subject, + xhtml = xhtml) + self.connection.send(msg_iq) def send_message(self, jid, msg, keyID, type = 'chat', subject='', chatstate = None, msg_id = None, composing_jep = None, resource = None, - user_nick = None): + user_nick = None, xhtml = None): if not self.connection: return + if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'): + xhtml = create_xhtml(msg) if not msg and chatstate is None: return fjid = jid @@ -690,18 +699,24 @@ class Connection(ConnectionHandlers): if msgenc: msgtxt = '[This message is encrypted]' lang = os.getenv('LANG') - if lang is not None or lang != 'en': # we're not english - msgtxt = _('[This message is encrypted]') +\ - ' ([This message is encrypted])' # one in locale and one en + if lang is not None and lang != 'en': # we're not english + # one in locale and one en + msgtxt = _('[This message is *encrypted* (See :JEP:`27`]') +\ + ' ([This message is *encrypted* (See :JEP:`27`])' + if msgtxt and not xhtml and gajim.config.get( + 'rst_formatting_outgoing_messages'): + # Generate a XHTML part using reStructured text markup + xhtml = create_xhtml(msgtxt) if type == 'chat': - msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type) + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type, + xhtml = xhtml) else: if subject: msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, - typ = 'normal', subject = subject) + typ = 'normal', subject = subject, xhtml = xhtml) else: msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, - typ = 'normal') + typ = 'normal', xhtml = xhtml) if msgenc: msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) @@ -719,7 +734,8 @@ class Connection(ConnectionHandlers): msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES) if composing_jep == 'JEP-0022' or not composing_jep: # JEP-0022 - chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT) + chatstate_node = msg_iq.setTag('x', + namespace = common.xmpp.NS_EVENT) if not msgtxt: # when no <body>, add <id> if not msg_id: # avoid putting 'None' in <id> tag msg_id = '' @@ -729,7 +745,8 @@ class Connection(ConnectionHandlers): chatstate_node.addChild(name = 'composing') self.connection.send(msg_iq) - no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') + no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\ + .split() ji = gajim.get_jid_without_resource(jid) if self.name not in no_log_for and ji not in no_log_for: log_msg = msg @@ -832,7 +849,7 @@ class Connection(ConnectionHandlers): if self.connection: self.connection.getRoster().setItem(jid = jid, name = name, groups = groups) - + def new_account(self, name, config, sync = False): # If a connection already exist we cannot create a new account if self.connection: @@ -851,6 +868,8 @@ class Connection(ConnectionHandlers): return self.on_connect_failure = None self.connection = con + if con: + con.RegisterDisconnectHandler(self._on_new_account) common.xmpp.features_nb.getRegInfo(con, self._hostname) def account_changed(self, new_name): @@ -914,14 +933,39 @@ class Connection(ConnectionHandlers): # Only add optional elements if not empty # Note: need to handle both None and '' as empty # thus shouldn't use "is not None" - if bm['nick']: + if bm.get('nick', None): iq5 = iq4.setTagData('nick', bm['nick']) - if bm['password']: + if bm.get('password', None): iq5 = iq4.setTagData('password', bm['password']) - if bm['print_status']: + if bm.get('print_status', None): iq5 = iq4.setTagData('print_status', bm['print_status']) self.connection.send(iq) + def get_annotations(self): + '''Get Annonations from storage as described in XEP 0048, and XEP 0145''' + self.annotations = {} + if not self.connection: + return + iq = common.xmpp.Iq(typ='get') + iq2 = iq.addChild(name='query', namespace='jabber:iq:private') + iq2.addChild(name='storage', namespace='storage:rosternotes') + self.connection.send(iq) + + def store_annotations(self): + '''Set Annonations in private storage as described in XEP 0048, and XEP 0145''' + if not self.connection: + return + iq = common.xmpp.Iq(typ='set') + iq2 = iq.addChild(name='query', namespace='jabber:iq:private') + iq3 = iq2.addChild(name='storage', namespace='storage:rosternotes') + for jid in self.annotations.keys(): + if self.annotations[jid]: + iq4 = iq3.addChild(name = "note") + iq4.setAttr('jid', jid) + iq4.setData(self.annotations[jid]) + self.connection.send(iq) + + def get_metacontacts(self): '''Get metacontacts list from storage as described in JEP 0049''' if not self.connection: @@ -958,14 +1002,15 @@ class Connection(ConnectionHandlers): p = self.add_sha(p, ptype != 'unavailable') self.connection.send(p) - def join_gc(self, nick, room, server, password): + def join_gc(self, nick, room_jid, password): + # FIXME: This room JID needs to be normalized; see #1364 if not self.connection: return show = helpers.get_xmpp_show(STATUS_LIST[self.connected]) if show == 'invisible': # Never join a room when invisible return - p = common.xmpp.Presence(to = '%s@%s/%s' % (room, server, nick), + p = common.xmpp.Presence(to = '%s/%s' % (room_jid, nick), show = show, status = self.status) if gajim.config.get('send_sha_in_gc_presence'): p = self.add_sha(p) @@ -974,17 +1019,18 @@ class Connection(ConnectionHandlers): t.setTagData('password', password) self.connection.send(p) #last date/time in history to avoid duplicate - # FIXME: This JID needs to be normalized; see #1364 - jid='%s@%s' % (room, server) - last_log = gajim.logger.get_last_date_that_has_logs(jid, is_room = True) + last_log = gajim.logger.get_last_date_that_has_logs(room_jid, + is_room = True) if last_log is None: last_log = 0 - self.last_history_line[jid]= last_log + self.last_history_line[room_jid]= last_log - def send_gc_message(self, jid, msg): + def send_gc_message(self, jid, msg, xhtml = None): if not self.connection: return - msg_iq = common.xmpp.Message(jid, msg, typ = 'groupchat') + if not xhtml and gajim.config.get('rst_formatting_outgoing_messages'): + xhtml = create_xhtml(msg) + msg_iq = common.xmpp.Message(jid, msg, typ = 'groupchat', xhtml = xhtml) self.connection.send(msg_iq) self.dispatch('MSGSENT', (jid, msg)) @@ -1110,11 +1156,11 @@ class Connection(ConnectionHandlers): self.connection.send(iq) def unregister_account(self, on_remove_success): - # no need to write this as a class method and keep the value of on_remove_success - # as a class property as pass it as an argument + # no need to write this as a class method and keep the value of + # on_remove_success as a class property as pass it as an argument def _on_unregister_account_connect(con): self.on_connect_auth = None - if self.connected > 1: + if gajim.account_is_connected(self.name): hostname = gajim.config.get_per('accounts', self.name, 'hostname') iq = common.xmpp.Iq(typ = 'set', to = hostname) q = iq.setTag(common.xmpp.NS_REGISTER + ' query').setTag('remove') diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index d87883bbb41592886cc54c28575beaa1247cf2f4..9c56796fd31f366ea3013a40d730de5f87ce0f0d 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -3,7 +3,7 @@ ## ## Contributors for this file: ## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <nkour@jabber.org> +## - Nikos Kouremenos <kourem@gmail.com> ## - Dimitur Kirov <dkirov@gmail.com> ## - Travis Shirk <travis@pobox.com> ## @@ -45,15 +45,13 @@ VCARD_PUBLISHED = 'vcard_published' VCARD_ARRIVED = 'vcard_arrived' AGENT_REMOVED = 'agent_removed' METACONTACTS_ARRIVED = 'metacontacts_arrived' +PRIVACY_ARRIVED = 'privacy_arrived' HAS_IDLE = True try: - import common.idle as idle # when we launch gajim from sources + import idle except: - try: - import idle # when Gajim is installed - except: - gajim.log.debug(_('Unable to load idle module')) - HAS_IDLE = False + gajim.log.debug(_('Unable to load idle module')) + HAS_IDLE = False class ConnectionBytestream: def __init__(self): @@ -94,7 +92,7 @@ class ConnectionBytestream: if contact.jid == receiver_jid: file_props['error'] = -5 self.remove_transfer(file_props) - self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props)) + self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, '')) sender_jid = unicode(file_props['sender']).split('/')[0] if contact.jid == sender_jid: file_props['error'] = -3 @@ -179,11 +177,12 @@ class ConnectionBytestream: except socket.gaierror: self.dispatch('ERROR', (_('Wrong host'), _('The host you configured as the ft_override_host_to_send advanced option is not valid, so ignored.'))) ft_override_host_to_send = self.peerhost[0] - listener = gajim.socks5queue.start_listener(self.peerhost[0], port, + listener = gajim.socks5queue.start_listener(port, sha_str, self._result_socks5_sid, file_props['sid']) if listener == None: file_props['error'] = -5 - self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props)) + self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props, + '')) self._connect_error(unicode(receiver), file_props['sid'], file_props['sid'], code = 406) return @@ -225,8 +224,8 @@ class ConnectionBytestream: iq = common.xmpp.Protocol(name = 'iq', to = unicode(file_props['sender']), typ = 'error') iq.setAttr('id', file_props['request-id']) - err = common.xmpp.ErrorNode(code = '406', typ = 'auth', name = - 'not-acceptable') + err = common.xmpp.ErrorNode(code = '403', typ = 'cancel', name = + 'forbidden', text = 'Offer Declined') iq.addChild(node=err) self.connection.send(iq) @@ -318,8 +317,8 @@ class ConnectionBytestream: if file_props is not None: self.disconnect_transfer(file_props) file_props['error'] = -3 - self.dispatch('FILE_REQUEST_ERROR', (to, file_props)) - + self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg)) + def _proxy_auth_ok(self, proxy): '''cb, called after authentication to proxy server ''' file_props = self.files_props[proxy['sid']] @@ -348,7 +347,7 @@ class ConnectionBytestream: return file_props = self.files_props[id] file_props['error'] = -4 - self.dispatch('FILE_REQUEST_ERROR', (jid, file_props)) + self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) raise common.xmpp.NodeProcessed def _bytestreamSetCB(self, con, iq_obj): @@ -565,7 +564,7 @@ class ConnectionBytestream: return jid = helpers.get_jid_from_iq(iq_obj) file_props['error'] = -3 - self.dispatch('FILE_REQUEST_ERROR', (jid, file_props)) + self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) raise common.xmpp.NodeProcessed class ConnectionDisco: @@ -696,6 +695,13 @@ class ConnectionDisco: attr = {} for key in i.getAttrs(): attr[key] = i.getAttrs()[key] + if 'jid' not in attr: + continue + try: + helpers.parse_jid(attr['jid']) + except common.helpers.InvalidFormat: + # jid is not conform + continue items.append(attr) jid = helpers.get_full_jid_from_iq(iq_obj) hostname = gajim.config.get_per('accounts', self.name, @@ -732,6 +738,7 @@ class ConnectionDisco: q.addChild('feature', attrs = {'var': common.xmpp.NS_SI}) q.addChild('feature', attrs = {'var': common.xmpp.NS_FILE}) q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC}) + q.addChild('feature', attrs = {'var': common.xmpp.NS_XHTML_IM}) self.connection.send(iq) raise common.xmpp.NodeProcessed @@ -839,6 +846,8 @@ class ConnectionVcard: puny_jid = helpers.sanitize_filename(jid) path = os.path.join(gajim.VCARD_PATH, puny_jid) if jid in self.room_jids or os.path.isdir(path): + if not nick: + return # remove room_jid file if needed if os.path.isfile(path): os.remove(path) @@ -976,9 +985,7 @@ class ConnectionVcard: 'invisible': self.vcard_sha = new_sha sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected]) - prio = unicode(gajim.config.get_per('accounts', self.name, - 'priority')) - p = common.xmpp.Presence(typ = None, priority = prio, + p = common.xmpp.Presence(typ = None, priority = self.priority, show = sshow, status = self.status) p = self.add_sha(p) self.connection.send(p) @@ -1023,8 +1030,15 @@ class ConnectionVcard: else: meta_list[tag] = [data] self.dispatch('METACONTACTS', meta_list) + else: + self.metacontacts_supported = False # We can now continue connection by requesting the roster self.connection.initRoster() + elif self.awaiting_answers[id][0] == PRIVACY_ARRIVED: + if iq_obj.getType() != 'error': + self.privacy_rules_supported = True + # Ask metacontacts before roster + self.get_metacontacts() del self.awaiting_answers[id] @@ -1104,10 +1118,8 @@ class ConnectionVcard: if STATUS_LIST[self.connected] == 'invisible': return sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected]) - prio = unicode(gajim.config.get_per('accounts', self.name, - 'priority')) - p = common.xmpp.Presence(typ = None, priority = prio, show = sshow, - status = self.status) + p = common.xmpp.Presence(typ = None, priority = self.priority, + show = sshow, status = self.status) p = self.add_sha(p) self.connection.send(p) else: @@ -1137,11 +1149,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def build_http_auth_answer(self, iq_obj, answer): if answer == 'yes': - iq = iq_obj.buildReply('result') + self.connection.send(iq_obj.buildReply('result')) elif answer == 'no': - iq = iq_obj.buildReply('error') - iq.setError('not-authorized', 401) - self.connection.send(iq) + err = common.xmpp.Error(iq_obj, + common.xmpp.protocol.ERR_NOT_AUTHORIZED) + self.connection.send(err) def _HttpAuthCB(self, con, iq_obj): gajim.log.debug('HttpAuthCB') @@ -1156,7 +1168,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, raise common.xmpp.NodeProcessed def _ErrorCB(self, con, iq_obj): - errmsg = iq_obj.getError() + gajim.log.debug('ErrorCB') + if iq_obj.getQueryNS() == common.xmpp.NS_VERSION: + who = helpers.get_full_jid_from_iq(iq_obj) + jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who) + self.dispatch('OS_INFO', (jid_stripped, resource, '', '')) + return + errmsg = iq_obj.getErrorMsg() errcode = iq_obj.getErrorCode() jid_from = helpers.get_full_jid_from_iq(iq_obj) id = unicode(iq_obj.getID()) @@ -1197,6 +1215,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # http://www.jabber.org/jeps/jep-0049.html #TODO: implement this pass + elif ns == 'storage:rosternotes': + # Annotations + # http://www.xmpp.org/extensions/xep-0145.html + notes = storage.getTags('note') + for note in notes: + jid = note.getAttr('jid') + annotation = note.getData() + self.annotations[jid] = annotation def _PrivateErrorCB(self, con, iq_obj): gajim.log.debug('PrivateErrorCB') @@ -1205,6 +1231,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if storage_tag: ns = storage_tag.getNamespace() if ns == 'storage:metacontacts': + self.metacontacts_supported = False # Private XML Storage (JEP49) is not supported by server # Continue connecting self.connection.initRoster() @@ -1312,7 +1339,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if gm.getTag('mailbox').getTag('mail-thread-info'): gmail_messages = gm.getTag('mailbox').getTags('mail-thread-info') for gmessage in gmail_messages: - gmail_from = gmessage.getTag('senders').getTag('sender').getAttr('address') + sender = gmessage.getTag('senders').getTag('sender') + if not sender: + continue + gmail_from = sender.getAttr('address') gmail_subject = gmessage.getTag('subject').getData() gmail_snippet = gmessage.getTag('snippet').getData() gmail_messages_list.append({ \ @@ -1331,6 +1361,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self._pubsubEventCB(con, msg) return msgtxt = msg.getBody() + msghtml = msg.getXHTML() mtype = msg.getType() subject = msg.getSubject() # if not there, it's None tim = msg.getTimestamp() @@ -1405,15 +1436,18 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt, tim)) elif mtype == 'groupchat': + has_timestamp = False + if msg.timestamp: + has_timestamp = True if subject: - self.dispatch('GC_SUBJECT', (frm, subject, msgtxt)) + self.dispatch('GC_SUBJECT', (frm, subject, msgtxt, has_timestamp)) else: if not msg.getTag('body'): #no <body> return # Ignore message from room in which we are not if not self.last_history_line.has_key(jid): return - self.dispatch('GC_MSG', (frm, msgtxt, tim)) + self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msghtml)) if self.name not in no_log_for and not int(float(time.mktime(tim))) <= \ self.last_history_line[jid] and msgtxt: gajim.logger.write('gc_msg', frm, msgtxt, tim = tim) @@ -1425,11 +1459,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, subject = subject) self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject, - chatstate, msg_id, composing_jep, user_nick)) + chatstate, msg_id, composing_jep, user_nick, msghtml)) else: # it's single message - if self.name not in no_log_for and jid not in no_log_for and msgtxt: - gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, - subject = subject) if invite is not None: item = invite.getTag('invite') jid_from = item.getAttr('from') @@ -1437,9 +1468,12 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, item = invite.getTag('password') password = invite.getTagData('password') self.dispatch('GC_INVITATION',(frm, jid_from, reason, password)) - else: - self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal', - subject, chatstate, msg_id, composing_jep, user_nick)) + return + if self.name not in no_log_for and jid not in no_log_for and msgtxt: + gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, + subject = subject) + self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal', + subject, chatstate, msg_id, composing_jep, user_nick, msghtml)) # END messageCB def _pubsubEventCB(self, con, msg): @@ -1482,8 +1516,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # one who = str(prs.getFrom()) jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who) - self.dispatch('GC_MSG', (jid_stripped, _('Nickname not allowed: %s') % \ - resource, None)) + self.dispatch('GC_MSG', (jid_stripped, + _('Nickname not allowed: %s') % resource, None, False, None)) return jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who) timestamp = None @@ -1545,22 +1579,22 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource, prio, keyID, timestamp)) elif errcode == '401': # password required to join - self.dispatch('ERROR', (_('Unable to join room'), - _('A password is required to join this room.'))) + self.dispatch('ERROR', (_('Unable to join group chat'), + _('A password is required to join this group chat.'))) elif errcode == '403': # we are banned - self.dispatch('ERROR', (_('Unable to join room'), - _('You are banned from this room.'))) - elif errcode == '404': # room does not exist - self.dispatch('ERROR', (_('Unable to join room'), - _('Such room does not exist.'))) + self.dispatch('ERROR', (_('Unable to join group chat'), + _('You are banned from this group chat.'))) + elif errcode == '404': # group chat does not exist + self.dispatch('ERROR', (_('Unable to join group chat'), + _('Such group chat does not exist.'))) elif errcode == '405': - self.dispatch('ERROR', (_('Unable to join room'), - _('Room creation is restricted.'))) + self.dispatch('ERROR', (_('Unable to join group chat'), + _('Group chat creation is restricted.'))) elif errcode == '406': - self.dispatch('ERROR', (_('Unable to join room'), + self.dispatch('ERROR', (_('Unable to join group chat'), _('Your registered nickname must be used.'))) elif errcode == '407': - self.dispatch('ERROR', (_('Unable to join room'), + self.dispatch('ERROR', (_('Unable to join group chat'), _('You are not in the members list.'))) elif errcode == '409': # nick conflict # the jid_from in this case is FAKE JID: room_jid/nick @@ -1568,7 +1602,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, proposed_nickname = resource + \ gajim.config.get('gc_proposed_nick_char') room_jid = gajim.get_room_from_fjid(who) - self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join room'), + self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join group chat'), _('Your desired nickname is in use or registered by another occupant.\nPlease specify another nickname below:'), proposed_nickname)) else: # print in the window the error self.dispatch('ERROR_ANSWER', ('', jid_stripped, @@ -1839,12 +1873,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if show == 'invisible': self.send_invisible_presence(msg, signed, True) return - prio = unicode(gajim.config.get_per('accounts', self.name, - 'priority')) + priority = gajim.get_priority(self.name, sshow) vcard = self.get_cached_vcard(jid) if vcard and vcard.has_key('PHOTO') and vcard['PHOTO'].has_key('SHA'): self.vcard_sha = vcard['PHOTO']['SHA'] - p = common.xmpp.Presence(typ = None, priority = prio, show = sshow) + p = common.xmpp.Presence(typ = None, priority = priority, show = sshow) p = self.add_sha(p) if msg: p.setStatus(msg) @@ -1853,6 +1886,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if self.connection: self.connection.send(p) + self.priority = priority self.dispatch('STATUS', show) # ask our VCard self.request_vcard(None) @@ -1860,6 +1894,9 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # Get bookmarks from private namespace self.get_bookmarks() + # Get annotations from private namespace + self.get_annotations() + # If it's a gmail account, # inform the server that we want e-mail notifications if gajim.get_server_from_jid(our_jid) in gajim.gmail_domains: diff --git a/src/common/contacts.py b/src/common/contacts.py index e6d095641402951ad075a6285b9d68ad469352cb..eac033c8c69b3fef77174d260780be4af383dfc1 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -1,16 +1,8 @@ ## common/contacts.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com> ## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -27,8 +19,8 @@ import common.gajim class Contact: '''Information concerning each contact''' def __init__(self, jid='', name='', groups=[], show='', status='', sub='', - ask='', resource='', priority=0, keyID='', our_chatstate=None, - chatstate=None, last_status_time=None, msg_id = None, composing_jep = None): + ask='', resource='', priority=0, keyID='', our_chatstate=None, + chatstate=None, last_status_time=None, msg_id = None, composing_jep = None): self.jid = jid self.name = name self.groups = groups @@ -67,10 +59,40 @@ class Contact: return self.name return self.jid.split('@')[0] + def is_hidden_from_roster(self): + '''if contact should not be visible in roster''' + # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html + if self.is_transport(): + return False + if self.sub in ('both', 'to'): + return False + if self.sub in ('none', 'from') and self.ask == 'subscribe': + return False + if self.sub in ('none', 'from') and (self.name or len(self.groups)): + return False + if _('Not in Roster') in self.groups: + return False + return True + + def is_observer(self): + # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html + is_observer = False + if self.sub == 'from' and not self.is_transport()\ + and self.is_hidden_from_roster(): + is_observer = True + return is_observer + + def is_transport(self): + # if not '@' or '@' starts the jid then contact is transport + if self.jid.find('@') <= 0: + return True + return False + + class GC_Contact: '''Information concerning each groupchat contact''' def __init__(self, room_jid='', name='', show='', status='', role='', - affiliation='', jid = '', resource = ''): + affiliation='', jid = '', resource = ''): self.room_jid = room_jid self.name = name self.show = show @@ -220,6 +242,51 @@ class Contacts: return self._contacts[account][jid][0] return None + def get_contacts_from_group(self, account, group): + '''Returns all contacts in the given group''' + group_contacts = [] + for jid in self._contacts[account]: + contacts = self.get_contacts_from_jid(account, jid) + if group in contacts[0].groups: + group_contacts += contacts + return group_contacts + + def get_nb_online_total_contacts(self, accounts = [], groups = []): + '''Returns the number of online contacts and the total number of + contacts''' + if accounts == []: + accounts = self.get_accounts() + nbr_online = 0 + nbr_total = 0 + for account in accounts: + our_jid = common.gajim.get_jid_from_account(account) + for jid in self.get_jid_list(account): + if jid == our_jid: + continue + if common.gajim.jid_is_transport(jid) and not \ + common.gajim.config.get('show_transports_group'): + # do not count transports + continue + contact = self.get_contact_with_highest_priority(account, jid) + in_groups = False + if groups == []: + in_groups = True + else: + contact_groups = contact.groups + if not contact_groups: + # Contact is not in a group, so count it in General group + contact_groups.append(_('General')) + for group in groups: + if group in contact_groups: + in_groups = True + break + + if in_groups: + if contact.show not in ('offline', 'error'): + nbr_online += 1 + nbr_total += 1 + return nbr_online, nbr_total + def define_metacontacts(self, account, tags_list): self._metacontacts_tags[account] = tags_list diff --git a/src/dbus_support.py b/src/common/dbus_support.py similarity index 68% rename from src/dbus_support.py rename to src/common/dbus_support.py index 59e751c417ee84b0b3975ec51adcb2fb5d6d7971..d554f7ab23e2b17193470225a77771f43275df6d 100644 --- a/src/dbus_support.py +++ b/src/common/dbus_support.py @@ -16,32 +16,56 @@ ## import os -import sys from common import gajim from common import exceptions +_GAJIM_ERROR_IFACE = 'org.gajim.dbus.Error' + try: import dbus - version = getattr(dbus, 'version', (0, 20, 0)) - supported = True + import dbus.service + import dbus.glib + supported = True # does use have D-Bus bindings? except ImportError: - version = (0, 0, 0) supported = False if not os.name == 'nt': # only say that to non Windows users print _('D-Bus python bindings are missing in this computer') print _('D-Bus capabilities of Gajim cannot be used') + +class SystemBus: + '''A Singleton for the DBus SystemBus''' + def __init__(self): + self.system_bus = None -# dbus 0.23 leads to segfault with threads_init() -if sys.version[:4] >= '2.4' and version[1] < 30: - supported = False + def SystemBus(self): + if not supported: + raise exceptions.DbusNotSupported -if version >= (0, 41, 0): - import dbus.service - import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it + if not self.present(): + raise exceptions.SystemBusNotPresent + return self.system_bus + + def bus(self): + return self.SystemBus() + + def present(self): + if not supported: + return False + if self.system_bus is None: + try: + self.system_bus = dbus.SystemBus() + except dbus.dbus_bindings.DBusException: + self.system_bus = None + return False + if self.system_bus is None: + return False + return True + +system_bus = SystemBus() class SessionBus: - '''A Singleton for the DBus SessionBus''' + '''A Singleton for the D-Bus SessionBus''' def __init__(self): self.session_bus = None @@ -102,4 +126,13 @@ def get_interface(interface, path): def get_notifications_interface(): '''Returns the notifications interface.''' - return get_interface('org.freedesktop.Notifications','/org/freedesktop/Notifications') + return get_interface('org.freedesktop.Notifications', + '/org/freedesktop/Notifications') + +if supported: + class MissingArgument(dbus.DBusException): + _dbus_error_name = _GAJIM_ERROR_IFACE + '.MissingArgument' + + class InvalidArgument(dbus.DBusException): + '''Raised when one of the provided arguments is invalid.''' + _dbus_error_name = _GAJIM_ERROR_IFACE + '.InvalidArgument' diff --git a/src/common/defs.py b/src/common/defs.py new file mode 100644 index 0000000000000000000000000000000000000000..82d11b301fcf3b45bcce1a03cc851162ecc406e7 --- /dev/null +++ b/src/common/defs.py @@ -0,0 +1,9 @@ +docdir = '../' + +datadir = '../' + +version = '0.10.1.7' + +import sys, os.path +for base in ('.', 'common'): + sys.path.append(os.path.join(base, '.libs')) diff --git a/src/common/events.py b/src/common/events.py index 4a7ba9e87f4dd69aa6fee6b587bdcb7bc1cc85f8..99cd98cfc3a761c7257d5bf1ad64ab730baa0c59 100644 --- a/src/common/events.py +++ b/src/common/events.py @@ -5,7 +5,7 @@ ## ## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -81,7 +81,7 @@ class Events: gajim.interface.systray.set_img() def remove_events(self, account, jid, event = None, types = []): - '''if event is not speficied, remove all events from this jid, + '''if event is not specified, remove all events from this jid, optionnaly only from given type return True if no such event found''' if not self._events.has_key(account): @@ -118,11 +118,11 @@ class Events: if gajim.interface.systray_capabilities: gajim.interface.systray.set_img() - def get_nb_events(self, types = []): - return self._get_nb_events(types = types) + def get_nb_events(self, types = [], account = None): + return self._get_nb_events(types = types, account = account) def get_events(self, account, jid = None, types = []): - '''if event is not speficied, remove all events from this jid, + '''if event is not specified, get all events from this jid, optionnaly only from given type''' if not self._events.has_key(account): return [] @@ -149,7 +149,7 @@ class Events: return first_event def _get_nb_events(self, account = None, jid = None, attribute = None, types = []): - '''return the number of events''' + '''return the number of pending events''' nb = 0 if account: accounts = [account] @@ -223,7 +223,7 @@ class Events: return self._get_first_event_with_attribute(events) def get_nb_roster_events(self, account = None, jid = None, types = []): - '''returns the number of events displayedin roster''' + '''returns the number of events displayed in roster''' return self._get_nb_events(attribute = 'roster', account = account, jid = jid, types = types) diff --git a/src/common/exceptions.py b/src/common/exceptions.py index 0b1bc8c4a72f5825f29609e727b5df2b2a7fe6cc..bceef79a4e3743f82532e3605ac6281f2d96e19c 100644 --- a/src/common/exceptions.py +++ b/src/common/exceptions.py @@ -1,17 +1,7 @@ ## exceptions.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -54,3 +44,12 @@ class SessionBusNotPresent(Exception): def __str__(self): return _('Session bus is not available.\nTry reading http://trac.gajim.org/wiki/GajimDBus') + +class GajimGeneralException(Exception): + '''This exception ir our general exception''' + def __init__(self, text=''): + Exception.__init__(self) + self.text = text + + def __str__(self): + return self.text diff --git a/src/common/gajim.py b/src/common/gajim.py index dc7794e6e3fa0536fa681fc184c8718489016379..9f03b09d4913024e8469b96a642ea739d88bd279 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -15,9 +15,7 @@ ## GNU General Public License for more details. ## -import os import sys -import tempfile import logging import locale @@ -25,10 +23,36 @@ import config from contacts import Contacts from events import Events +try: + import defs +except ImportError: + print >> sys.stderr, '''defs.py is missing! + +If you start gajim from svn: + * Make sure you have GNU autotools installed. + This includes the following packages: + automake >= 1.8 + autoconf >= 2.59 + intltool-0.35 + libtool + * Run + $ sh autogen.sh + * Optionally, install gajim + $ make + $ sudo make install + +**** Note for translators **** + You can get the latest string updates, by running: + $ cd po/ + $ make update-po + +''' + sys.exit(1) + interface = None # The actual interface (the gtk one for the moment) config = config.Config() version = config.get('version') -connections = {} +connections = {} # 'account name': 'account (connection.Connection) instance' verbose = False h = logging.StreamHandler() @@ -40,38 +64,17 @@ log.addHandler(h) import logger logger = logger.Logger() # init the logger -if os.name == 'nt': - DATA_DIR = os.path.join('..', 'data') - try: - # Documents and Settings\[User Name]\Application Data\Gajim - LOGPATH = os.path.join(os.environ['appdata'], 'Gajim', 'Logs') # deprecated - VCARD_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'Vcards') - AVATAR_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'Avatars') - MY_EMOTS_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'Emoticons') - except KeyError: - # win9x, in cwd - LOGPATH = 'Logs' # deprecated - VCARD_PATH = 'Vcards' - AVATAR_PATH = 'Avatars' - MY_EMOTS_PATH = 'Emoticons' -else: # Unices - DATA_DIR = '../data' - LOGPATH = os.path.expanduser('~/.gajim/logs') # deprecated - VCARD_PATH = os.path.expanduser('~/.gajim/vcards') - AVATAR_PATH = os.path.expanduser('~/.gajim/avatars') - MY_EMOTS_PATH = os.path.expanduser('~/.gajim/emoticons') - -HOME_DIR = os.path.expanduser('~') -TMP = tempfile.gettempdir() +import configpaths +gajimpaths = configpaths.gajimpaths + +LOGPATH = gajimpaths['LOG'] # deprecated +VCARD_PATH = gajimpaths['VCARD'] +AVATAR_PATH = gajimpaths['AVATAR'] +MY_EMOTS_PATH = gajimpaths['MY_EMOTS'] +TMP = gajimpaths['TMP'] +DATA_DIR = gajimpaths['DATA'] +HOME_DIR = gajimpaths['HOME'] -try: - LOGPATH = LOGPATH.decode(sys.getfilesystemencoding()) - VCARD_PATH = VCARD_PATH.decode(sys.getfilesystemencoding()) - TMP = TMP.decode(sys.getfilesystemencoding()) - AVATAR_PATH = AVATAR_PATH.decode(sys.getfilesystemencoding()) - MY_EMOTS_PATH = MY_EMOTS_PATH.decode(sys.getfilesystemencoding()) -except: - pass try: LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc.. except (ValueError, locale.Error): @@ -120,6 +123,12 @@ status_before_autoaway = {} SHOW_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible'] +# zeroconf account name +ZEROCONF_ACC_NAME = 'Local' +priority_dict = {} +for status in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): + priority_dict[status] = config.get('autopriority' + status) + def get_nick_from_jid(jid): pos = jid.find('@') return jid[:pos] @@ -133,10 +142,10 @@ def get_nick_from_fjid(jid): # gaim@conference.jabber.no/nick/nick-continued return jid.split('/', 1)[1] -def get_room_name_and_server_from_room_jid(jid): - room_name = get_nick_from_jid(jid) +def get_name_and_server_from_jid(jid): + name = get_nick_from_jid(jid) server = get_server_from_jid(jid) - return room_name, server + return name, server def get_room_and_nick_from_fjid(jid): # fake jid is the jid for a contact in a room @@ -207,11 +216,36 @@ def get_number_of_connected_accounts(accounts_list = None): accounts = connections.keys() else: accounts = accounts_list - for acct in accounts: - if connections[acct].connected > 1: + for account in accounts: + if account_is_connected(account): connected_accounts = connected_accounts + 1 return connected_accounts +def account_is_connected(account): + if account not in connections: + return False + if connections[account].connected > 1: # 0 is offline, 1 is connecting + return True + else: + return False + +def account_is_disconnected(account): + return not account_is_connected(account) + +def get_number_of_securely_connected_accounts(): + '''returns the number of the accounts that are SSL/TLS connected''' + num_of_secured = 0 + for account in connections: + if account_is_securely_connected(account): + num_of_secured += 1 + return num_of_secured + +def account_is_securely_connected(account): + if account in con_types and con_types[account] in ('tls', 'ssl'): + return True + else: + return False + def get_transport_name_from_jid(jid, use_config_setting = True): '''returns 'aim', 'gg', 'irc' etc if JID is not from transport returns None''' @@ -299,3 +333,13 @@ def get_name_from_jid(account, jid): else: actor = jid return actor + +def get_priority(account, show): + '''return the priority an account must have''' + if not show: + show = 'online' + + if show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible') and \ + config.get_per('accounts', account, 'adjust_priority_with_status'): + return config.get_per('accounts', account, 'autopriority_' + show) + return config.get_per('accounts', account, 'priority') diff --git a/src/common/helpers.py b/src/common/helpers.py index 1226f2cd54f6dd8216b93ff00affef4c88f3d27f..5dc224ff26529f9ef44223c785abadce4d708e69 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -17,19 +17,21 @@ ## import sre +import locale import os import subprocess import urllib import errno import select -import sys import sha from encodings.punycode import punycode_encode import gajim from i18n import Q_ +from i18n import ngettext from xmpp_stringprep import nodeprep, resourceprep, nameprep + try: import winsound # windows-only built-in module for playing wav import win32api @@ -290,6 +292,21 @@ def get_uf_role(role, plural = False): else: role_name = _('Visitor') return role_name + +def get_uf_affiliation(affiliation): + '''Get a nice and translated affilition for muc''' + if affiliation == 'none': + affiliation_name = Q_('?Group Chat Contact Affiliation:None') + elif affiliation == 'owner': + affiliation_name = _('Owner') + elif affiliation == 'admin': + affiliation_name = _('Administrator') + elif affiliation == 'member': + affiliation_name = _('Member') + else: # Argl ! An unknown affiliation ! + affiliation_name = affiliation.capitalize() + return affiliation_name + def get_sorted_keys(adict): keys = adict.keys() @@ -362,9 +379,14 @@ def is_in_path(name_of_command, return_abs_path = False): return is_in_dir def exec_command(command): - '''command is a string that contain arguments''' -# os.system(command) - subprocess.Popen(command.split()) + subprocess.Popen(command, shell = True) + +def build_command(executable, parameter): + # we add to the parameter (can hold path with spaces) + # "" so we have good parsing from shell + parameter = parameter.replace('"', '\\"') # but first escape " + command = '%s "%s"' % (executable, parameter) + return command def launch_browser_mailer(kind, uri): #kind = 'url' or 'mail' @@ -382,6 +404,8 @@ def launch_browser_mailer(kind, uri): command = 'gnome-open' elif gajim.config.get('openwith') == 'kfmclient exec': command = 'kfmclient exec' + elif gajim.config.get('openwith') == 'exo-open': + command = 'exo-open' elif gajim.config.get('openwith') == 'custom': if kind == 'url': command = gajim.config.get('custombrowser') @@ -389,7 +413,8 @@ def launch_browser_mailer(kind, uri): command = gajim.config.get('custommailapp') if command == '': # if no app is configured return - command = command + ' ' + uri + + command = build_command(command, uri) try: exec_command(command) except: @@ -406,11 +431,13 @@ def launch_file_manager(path_to_open): command = 'gnome-open' elif gajim.config.get('openwith') == 'kfmclient exec': command = 'kfmclient exec' + elif gajim.config.get('openwith') == 'exo-open': + command = 'exo-open' elif gajim.config.get('openwith') == 'custom': command = gajim.config.get('custom_file_manager') if command == '': # if no app is configured return - command = command + ' ' + path_to_open + command = build_command(command, path_to_open) try: exec_command(command) except: @@ -438,7 +465,7 @@ def play_sound_file(path_to_soundfile): if gajim.config.get('soundplayer') == '': return player = gajim.config.get('soundplayer') - command = player + ' ' + path_to_soundfile + command = build_command(player, path_to_soundfile) exec_command(command) def get_file_path_from_dnd_dropped_uri(uri): @@ -523,8 +550,10 @@ def get_icon_name_to_show(contact, account = None): def decode_string(string): '''try to decode (to make it Unicode instance) given string''' + if isinstance(string, unicode): + return string # by the time we go to iso15 it better be the one else we show bad characters - encodings = (sys.getfilesystemencoding(), 'utf-8', 'iso-8859-15') + encodings = (locale.getpreferredencoding(), 'utf-8', 'iso-8859-15') for encoding in encodings: try: string = string.decode(encoding) @@ -599,7 +628,6 @@ def get_documents_path(): path = os.path.expanduser('~') return path -# moved from connection.py def get_full_jid_from_iq(iq_obj): '''return the full jid (with resource) from an iq as unicode''' return parse_jid(str(iq_obj.getFrom())) @@ -674,6 +702,7 @@ def get_os_info(): output = temp_failure_retry(child_stdout.readline).strip() child_stdout.close() child_stdin.close() + os.wait() # some distros put n/a in places, so remove those output = output.replace('n/a', '').replace('N/A', '') return output @@ -726,23 +755,21 @@ def sanitize_filename(filename): return filename -def allow_showing_notification(account, type = None, advanced_notif_num = None, -first = True): +def allow_showing_notification(account, type = 'notify_on_new_message', +advanced_notif_num = None, is_first_message = True): '''is it allowed to show nofication? check OUR status and if we allow notifications for that status - type is the option that need to be True ex: notify_on_signing - first: set it to false when it's not the first message''' - if advanced_notif_num != None: + type is the option that need to be True e.g.: notify_on_signing + is_first_message: set it to false when it's not the first message''' + if advanced_notif_num is not None: popup = gajim.config.get_per('notifications', str(advanced_notif_num), 'popup') if popup == 'yes': return True if popup == 'no': return False - if type and (not gajim.config.get(type) or not first): + if type and (not gajim.config.get(type) or not is_first_message): return False - if type and gajim.config.get(type) and first: - return True if gajim.config.get('autopopupaway'): # always show notification return True if gajim.connections[account].connected in (2, 3): # we're online or chat @@ -766,7 +793,7 @@ def allow_popup_window(account, advanced_notif_num = None): return False def allow_sound_notification(sound_event, advanced_notif_num = None): - if advanced_notif_num != None: + if advanced_notif_num is not None: sound = gajim.config.get_per('notifications', str(advanced_notif_num), 'sound') if sound == 'yes': @@ -796,3 +823,110 @@ def get_chat_control(account, contact): highest_contact.resource: return None return gajim.interface.msg_win_mgr.get_control(contact.jid, account) + +def reduce_chars_newlines(text, max_chars = 0, max_lines = 0): + '''Cut the chars after 'max_chars' on each line + and show only the first 'max_lines'. + If any of the params is not present (None or 0) the action + on it is not performed''' + + def _cut_if_long(string): + if len(string) > max_chars: + string = string[:max_chars - 3] + '...' + return string + + if isinstance(text, str): + text = text.decode('utf-8') + + if max_lines == 0: + lines = text.split('\n') + else: + lines = text.split('\n', max_lines)[:max_lines] + if max_chars > 0: + if lines: + lines = map(lambda e: _cut_if_long(e), lines) + if lines: + reduced_text = reduce(lambda e, e1: e + '\n' + e1, lines) + else: + reduced_text = '' + return reduced_text + +def get_notification_icon_tooltip_text(): + text = None + unread_chat = gajim.events.get_nb_events(types = ['printed_chat', + 'chat']) + unread_single_chat = gajim.events.get_nb_events(types = ['normal']) + unread_gc = gajim.events.get_nb_events(types = ['printed_gc_msg', + 'gc_msg']) + unread_pm = gajim.events.get_nb_events(types = ['printed_pm', 'pm']) + + accounts = get_accounts_info() + + if unread_chat or unread_single_chat or unread_gc or unread_pm: + text = 'Gajim ' + awaiting_events = unread_chat + unread_single_chat + unread_gc + unread_pm + if awaiting_events == unread_chat or awaiting_events == unread_single_chat \ + or awaiting_events == unread_gc or awaiting_events == unread_pm: + # This condition is like previous if but with xor... + # Print in one line + text += '-' + else: + # Print in multiple lines + text += '\n ' + if unread_chat: + text += ngettext( + ' %d unread message', + ' %d unread messages', + unread_chat, unread_chat, unread_chat) + text += '\n ' + if unread_single_chat: + text += ngettext( + ' %d unread single message', + ' %d unread single messages', + unread_single_chat, unread_single_chat, unread_single_chat) + text += '\n ' + if unread_gc: + text += ngettext( + ' %d unread group chat message', + ' %d unread group chat messages', + unread_gc, unread_gc, unread_gc) + text += '\n ' + if unread_pm: + text += ngettext( + ' %d unread private message', + ' %d unread private messages', + unread_pm, unread_pm, unread_pm) + text += '\n ' + text = text[:-4] # remove latest '\n ' + elif len(accounts) > 1: + text = _('Gajim') + elif len(accounts) == 1: + message = accounts[0]['status_line'] + message = reduce_chars_newlines(message, 100, 1) + text = _('Gajim - %s') % message + else: + text = _('Gajim - %s') % get_uf_show('offline') + + return text + +def get_accounts_info(): + '''helper for notification icon tooltip''' + accounts = [] + accounts_list = gajim.contacts.get_accounts() + accounts_list.sort() + for account in accounts_list: + status_idx = gajim.connections[account].connected + # uncomment the following to hide offline accounts + # if status_idx == 0: continue + status = gajim.SHOW_LIST[status_idx] + message = gajim.connections[account].status + single_line = get_uf_show(status) + if message is None: + message = '' + else: + message = message.strip() + if message != '': + single_line += ': ' + message + accounts.append({'name': account, 'status_line': single_line, + 'show': status, 'message': message}) + return accounts diff --git a/src/common/i18n.py b/src/common/i18n.py index ae23f0e2ffcccec577b6b68a60ec9360915f85a2..8eae356213d8abedba1505a4708edec8839f2300 100644 --- a/src/common/i18n.py +++ b/src/common/i18n.py @@ -8,7 +8,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> diff --git a/src/common/logger.py b/src/common/logger.py index 11f78440659485854d139bf536b5287a6d44c646..9de23a017d65252649fcbae49648ce4dfddb9143 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -1,17 +1,7 @@ ## logger.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -32,24 +22,15 @@ import exceptions import gajim try: - from pysqlite2 import dbapi2 as sqlite + import sqlite3 as sqlite # python 2.5 except ImportError: - raise exceptions.PysqliteNotAvailable - -if os.name == 'nt': try: - # Documents and Settings\[User Name]\Application Data\Gajim\logs.db - LOG_DB_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'logs.db') - except KeyError: - # win9x, ./logs.db - LOG_DB_PATH = 'logs.db' -else: # Unices - LOG_DB_PATH = os.path.expanduser('~/.gajim/logs.db') + from pysqlite2 import dbapi2 as sqlite + except ImportError: + raise exceptions.PysqliteNotAvailable -try: - LOG_DB_PATH = LOG_DB_PATH.decode(sys.getfilesystemencoding()) -except: - pass +import configpaths +LOG_DB_PATH = configpaths.gajimpaths['LOG_DB'] class Constants: def __init__(self): @@ -107,15 +88,33 @@ class Logger: return self.init_vars() - def init_vars(self): - # if locked, wait up to 20 sec to unlock - # before raise (hopefully should be enough) + def close_db(self): if self.con: self.con.close() + self.con = None + self.cur = None + + def open_db(self): + self.close_db() + + # if locked, wait up to 20 sec to unlock + # before raise (hopefully should be enough) self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0, isolation_level = 'IMMEDIATE') self.cur = self.con.cursor() + self.set_synchronous(False) + + def set_synchronous(self, sync): + try: + if sync: + self.cur.execute("PRAGMA synchronous = NORMAL") + else: + self.cur.execute("PRAGMA synchronous = OFF") + except sqlite.Error, e: + gajim.log.debug("Failed to set_synchronous(%s): %s" % (sync, str(e))) + def init_vars(self): + self.open_db() self.get_jids_already_in_db() def get_jids_already_in_db(self): @@ -136,9 +135,11 @@ class Logger: and after that all okay''' possible_room_jid, possible_nick = jid.split('/', 1) + return self.jid_is_room_jid(possible_room_jid) + def jid_is_room_jid(self, jid): self.cur.execute('SELECT jid_id FROM jids WHERE jid=? AND type=?', - (possible_room_jid, constants.JID_ROOM_TYPE)) + (jid, constants.JID_ROOM_TYPE)) row = self.cur.fetchone() if row is None: return False @@ -344,10 +345,8 @@ class Logger: ROOM_JID/nick if pm-related.''' if self.jids_already_in == []: # only happens if we just created the db - self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0, - isolation_level = 'IMMEDIATE') - self.cur = self.con.cursor() - + self.open_db() + jid = jid.lower() contact_name_col = None # holds nickname for kinds gcstatus, gc_msg # message holds the message unless kind is status or gcstatus, diff --git a/src/common/optparser.py b/src/common/optparser.py index 5459905b7b29380b0d27140bd071732296c2d93e..c64446f9045e7ed3063520fe2989198d9fa013c2 100644 --- a/src/common/optparser.py +++ b/src/common/optparser.py @@ -1,16 +1,6 @@ ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -27,11 +17,21 @@ import sys import locale from common import gajim +import exceptions +try: + import sqlite3 as sqlite # python 2.5 +except ImportError: + try: + from pysqlite2 import dbapi2 as sqlite + except ImportError: + raise exceptions.PysqliteNotAvailable +import logger + class OptionsParser: def __init__(self, filename): self.__filename = filename - self.old_values = {} # values that are saved in the file and maybe - # no longer valid + self.old_values = {} # values that are saved in the file and maybe + # no longer valid def read_line(self, line): index = line.find(' = ') @@ -126,8 +126,7 @@ class OptionsParser: os.chmod(self.__filename, 0600) def update_config(self, old_version, new_version): - # Convert '0.x.y' to (0, x, y) - old_version_list = old_version.split('.') + old_version_list = old_version.split('.') # convert '0.x.y' to (0, x, y) old = [] while len(old_version_list): old.append(int(old_version_list.pop(0))) @@ -146,7 +145,15 @@ class OptionsParser: self.update_config_to_01012() if old < [0, 10, 1, 3] and new >= [0, 10, 1, 3]: self.update_config_to_01013() - + if old < [0, 10, 1, 4] and new >= [0, 10, 1, 4]: + self.update_config_to_01014() + if old < [0, 10, 1, 5] and new >= [0, 10, 1, 5]: + self.update_config_to_01015() + if old < [0, 10, 1, 6] and new >= [0, 10, 1, 6]: + self.update_config_to_01016() + if old < [0, 10, 1, 7] and new >= [0, 10, 1, 7]: + self.update_config_to_01017() + gajim.logger.init_vars() gajim.config.set('version', new_version) @@ -202,13 +209,6 @@ class OptionsParser: def assert_unread_msgs_table_exists(self): '''create table unread_messages if there is no such table''' - import exceptions - try: - from pysqlite2 import dbapi2 as sqlite - except ImportError: - raise exceptions.PysqliteNotAvailable - import logger - con = sqlite.connect(logger.LOG_DB_PATH) cur = con.cursor() try: @@ -278,13 +278,6 @@ class OptionsParser: def update_config_to_01013(self): '''create table transports_cache if there is no such table''' - import exceptions - try: - from pysqlite2 import dbapi2 as sqlite - except ImportError: - raise exceptions.PysqliteNotAvailable - import logger - con = sqlite.connect(logger.LOG_DB_PATH) cur = con.cursor() try: @@ -301,3 +294,56 @@ class OptionsParser: pass con.close() gajim.config.set('version', '0.10.1.3') + + def update_config_to_01014(self): + '''apply indeces to the logs database''' + print _('migrating logs database to indeces') + con = sqlite.connect(logger.LOG_DB_PATH) + cur = con.cursor() + # apply indeces + try: + cur.executescript( + ''' + CREATE INDEX idx_logs_jid_id_kind ON logs (jid_id, kind); + CREATE INDEX idx_unread_messages_jid_id ON unread_messages (jid_id); + ''' + ) + + con.commit() + except: + pass + con.close() + gajim.config.set('version', '0.10.1.4') + + def update_config_to_01015(self): + '''clean show values in logs database''' + con = sqlite.connect(logger.LOG_DB_PATH) + cur = con.cursor() + status = dict((i[5:].lower(), logger.constants.__dict__[i]) for i in \ + logger.constants.__dict__.keys() if i.startswith('SHOW_')) + for show in status: + cur.execute('update logs set show = ? where show = ?;', (status[show], + show)) + cur.execute('update logs set show = NULL where show not in (0, 1, 2, 3, 4, 5);') + con.commit() + cur.close() # remove this in 2007 [pysqlite old versions need this] + con.close() + gajim.config.set('version', '0.10.1.5') + + def update_config_to_01016(self): + '''#2494 : Now we play gc_received_message sound even if + notify_on_all_muc_messages is false. Keep precedent behaviour.''' + if self.old_values.has_key('notify_on_all_muc_messages') and \ + self.old_values['notify_on_all_muc_messages'] == 'False' and \ + gajim.config.get_per('soundevents', 'muc_message_received', 'enabled'): + gajim.config.set_per('soundevents',\ + 'muc_message_received', 'enabled', False) + gajim.config.set('version', '0.10.1.6') + + def update_config_to_01017(self): + '''trayicon_notification_on_new_messages -> + trayicon_notification_on_events ''' + if self.old_values.has_key('trayicon_notification_on_new_messages'): + gajim.config.set('trayicon_notification_on_events', + self.old_values['trayicon_notification_on_new_messages']) + gajim.config.set('version', '0.10.1.7') diff --git a/src/common/passwords.py b/src/common/passwords.py new file mode 100644 index 0000000000000000000000000000000000000000..b6050bc6a7ea01fecaa6ac11820f1546a8764c5e --- /dev/null +++ b/src/common/passwords.py @@ -0,0 +1,117 @@ +## +## Copyright (C) 2006 Gustavo J. A. M. Carneiro <gjcarneiro@gmail.com> +## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +__all__ = ['get_password', 'save_password'] + +import gobject + +from common import gajim + +try: + import gnomekeyring +except ImportError: + USER_USES_GNOMEKEYRING = False +else: + if gnomekeyring.is_available(): + USER_USES_GNOMEKEYRING = True + else: + USER_USES_GNOMEKEYRING = False + +class PasswordStorage(object): + def get_password(self, account_name): + raise NotImplementedError + def save_password(self, account_name, password): + raise NotImplementedError + + +class SimplePasswordStorage(PasswordStorage): + def get_password(self, account_name): + return gajim.config.get_per('accounts', account_name, 'password') + + def save_password(self, account_name, password): + gajim.config.set_per('accounts', account_name, 'password', password) + gajim.connections[account_name].password = password + + +class GnomePasswordStorage(PasswordStorage): + def __init__(self): + # self.keyring = gnomekeyring.get_default_keyring_sync() + + ## above line commented and code below inserted as workaround + ## for the bug http://bugzilla.gnome.org/show_bug.cgi?id=363019 + self.keyring = "default" + try: + gnomekeyring.create_sync(self.keyring, None) + except gnomekeyring.AlreadyExistsError: + pass + + def get_password(self, account_name): + conf = gajim.config.get_per('accounts', account_name, 'password') + if conf is None: + return None + try: + unused, auth_token = conf.split('gnomekeyring:') + auth_token = int(auth_token) + except ValueError: + password = conf + ## migrate the password over to keyring + try: + self.save_password(account_name, password, update=False) + except gnomekeyring.NoKeyringDaemonError: + ## no keyring daemon: in the future, stop using it + set_storage(SimplePasswordStorage()) + return password + try: + return gnomekeyring.item_get_info_sync(self.keyring, + auth_token).get_secret() + except gnomekeyring.DeniedError: + return None + except gnomekeyring.NoKeyringDaemonError: + ## no keyring daemon: in the future, stop using it + set_storage(SimplePasswordStorage()) + return None + + def save_password(self, account_name, password, update=True): + display_name = _('Gajim account %s') % account_name + attributes = dict(account_name=str(account_name), gajim=1) + auth_token = gnomekeyring.item_create_sync( + self.keyring, gnomekeyring.ITEM_GENERIC_SECRET, + display_name, attributes, password, update) + token = 'gnomekeyring:%i' % auth_token + gajim.config.set_per('accounts', account_name, 'password', token) + + +storage = None +def get_storage(): + global storage + if storage is None: # None is only in first time get_storage is called + if USER_USES_GNOMEKEYRING: + try: + storage = GnomePasswordStorage() + except gnomekeyring.NoKeyringDaemonError: + storage = SimplePasswordStorage() + else: + storage = SimplePasswordStorage() + return storage + +def set_storage(storage_): + global storage + storage = storage_ + + +def get_password(account_name): + return get_storage().get_password(account_name) + +def save_password(account_name, password): + return get_storage().save_password(account_name, password) diff --git a/src/common/proxy65_manager.py b/src/common/proxy65_manager.py index 5f2a9f528edcb5a17a0e8ea021d095de1f0b3689..5145632db5eacc9bedd7b586b6fda5ec9ccff9f6 100644 --- a/src/common/proxy65_manager.py +++ b/src/common/proxy65_manager.py @@ -186,6 +186,9 @@ class HostTester(Socks5, IdleObject): def connect(self): ''' create the socket and plug it to the idlequeue ''' + if self.host is None: + self.on_failure() + return None self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.setblocking(False) self.fd = self._sock.fileno() diff --git a/src/common/rst_xhtml_generator.py b/src/common/rst_xhtml_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..c69771506ee2185804b527b2b7d8d3ab251fdef8 --- /dev/null +++ b/src/common/rst_xhtml_generator.py @@ -0,0 +1,126 @@ +## rst_xhtml_generator.py +## +## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2006 Santiago Gala +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +try: + from docutils import io + from docutils.core import Publisher + from docutils.parsers.rst import roles + from docutils import nodes,utils + from docutils.parsers.rst.roles import set_classes +except: + def create_xhtml(text): + return None +else: + def jep_reference_role(role, rawtext, text, lineno, inliner, + options={}, content=[]): + '''Role to make handy references to Jabber Enhancement Proposals (JEP). + + Use as :JEP:`71` (or jep, or jep-reference). + Modeled after the sample in docutils documentation. + ''' + + jep_base_url = 'http://www.jabber.org/jeps/' + jep_url = 'jep-%04d.html' + try: + jepnum = int(text) + if jepnum <= 0: + raise ValueError + except ValueError: + msg = inliner.reporter.error( + 'JEP number must be a number greater than or equal to 1; ' + '"%s" is invalid.' % text, line=lineno) + prb = inliner.problematic(rawtext, rawtext, msg) + return [prb], [msg] + ref = jep_base_url + jep_url % jepnum + set_classes(options) + node = nodes.reference(rawtext, 'JEP ' + utils.unescape(text), refuri=ref, + **options) + return [node], [] + + roles.register_canonical_role('jep-reference', jep_reference_role) + from docutils.parsers.rst.languages.en import roles + roles['jep-reference'] = 'jep-reference' + roles['jep'] = 'jep-reference' + + class HTMLGenerator: + '''Really simple HTMLGenerator starting from publish_parts. + + It reuses the docutils.core.Publisher class, which means it is *not* + threadsafe. + ''' + def __init__(self, + settings_spec=None, + settings_overrides=dict(report_level=5, halt_level=5), + config_section='general'): + self.pub = Publisher(reader=None, parser=None, writer=None, + settings=None, + source_class=io.StringInput, + destination_class=io.StringOutput) + self.pub.set_components(reader_name='standalone', + parser_name='restructuredtext', + writer_name='html') + # hack: JEP-0071 does not allow HTML char entities, so we hack our way + # out of it. + # — == u"\u2014" + # a setting to only emit charater entities in the writer would be nice + # FIXME: several are emitted, and they are explicitly forbidden + # in the JEP + # == u"\u00a0" + self.pub.writer.translator_class.attribution_formats['dash'] = ( + u'\u2014', '') + self.pub.process_programmatic_settings(settings_spec, + settings_overrides, + config_section) + + + def create_xhtml(self, text, + destination=None, + destination_path=None, + enable_exit_status=None): + ''' Create xhtml for a fragment of IM dialog. + We can use the source_name to store info about + the message.''' + self.pub.set_source(text, None) + self.pub.set_destination(destination, destination_path) + output = self.pub.publish(enable_exit_status=enable_exit_status) + # kludge until we can get docutils to stop generating (rare) + # entities + return u'\u00a0'.join(self.pub.writer.parts['fragment'].strip().split( + ' ')) + + Generator = HTMLGenerator() + + def create_xhtml(text): + return Generator.create_xhtml(text) + + +if __name__ == '__main__': + print Generator.create_xhtml(''' +test:: + + >>> print 1 + 1 + +*I* like it. It is for :JEP:`71` + +this `` should trigger`` should trigger the problem. + +''') + print Generator.create_xhtml(''' +*test1 + +test2_ +''') diff --git a/src/common/sleepy.py b/src/common/sleepy.py index fa6315c4dd85a755cb7180e851ebb4dfcdb28f5e..a0ce13ae7204c0ac8aa55418860df873dcfb0599 100644 --- a/src/common/sleepy.py +++ b/src/common/sleepy.py @@ -8,7 +8,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -33,13 +33,10 @@ STATE_AWAKE = 'awake' SUPPORTED = True try: - import common.idle as idle # when we launch gajim from sources + import idle except: - try: - import idle # when Gajim is installed - except: - gajim.log.debug('Unable to load idle module') - SUPPORTED = False + gajim.log.debug('Unable to load idle module') + SUPPORTED = False class Sleepy: diff --git a/src/common/socks5.py b/src/common/socks5.py index 9039dab5473517671991ef78da4cd394e7275d05..80c9a9a67fc0c80d521d4d35f40b15308a9aaa4d 100644 --- a/src/common/socks5.py +++ b/src/common/socks5.py @@ -3,14 +3,14 @@ ## ## Contributors for this file: ## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <nkour@jabber.org> +## - Nikos Kouremenos <kourem@gmail.com> ## - Dimitur Kirov <dkirov@gmail.com> ## ## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -27,11 +27,8 @@ import socket -import select -import os import struct import sha -import time from dialogs import BindPortError from errno import EWOULDBLOCK @@ -74,13 +71,13 @@ class SocksQueue: self.on_success = None self.on_failure = None - def start_listener(self, host, port, sha_str, sha_handler, sid): + def start_listener(self, port, sha_str, sha_handler, sid): ''' start waiting for incomming connections on (host, port) and do a socks5 authentication using sid for generated sha ''' self.sha_handlers[sha_str] = (sha_handler, sid) if self.listener == None: - self.listener = Socks5Listener(self.idlequeue, host, port) + self.listener = Socks5Listener(self.idlequeue, port) self.listener.queue = self self.listener.bind() if self.listener.started is False: @@ -213,7 +210,7 @@ class SocksQueue: sender = self.senders[file_props['hash']] sender.account = account - result = get_file_contents(0) + result = self.get_file_contents(0) self.process_result(result, sender) def result_sha(self, sha_str, idx): @@ -350,7 +347,10 @@ class SocksQueue: class Socks5: def __init__(self, idlequeue, host, port, initiator, target, sid): if host is not None: - self.host = socket.gethostbyname(host) + try: + self.host = socket.gethostbyname(host) + except socket.gaierror: + self.host = None self.idlequeue = idlequeue self.fd = -1 self.port = port @@ -787,12 +787,12 @@ class Socks5Sender(Socks5, IdleObject): self.queue.remove_sender(self.queue_idx, False) class Socks5Listener(IdleObject): - def __init__(self, idlequeue, host, port): - ''' handle all incomming connections on (host, port) + def __init__(self, idlequeue, port): + ''' handle all incomming connections on (0.0.0.0, port) This class implements IdleObject, but we will expect only pollin events though ''' - self.host, self.port = host, port + self.port = port self.queue_idx = -1 self.idlequeue = idlequeue self.queue = None diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 40753ef4e014213d059e86b9f2132eae61b9914d..cf8fa67be595fd976b68b60af093963615da5913 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -74,10 +74,8 @@ class NBCommonClient(CommonClient): ''' Called on disconnection. Calls disconnect handlers and cleans things up. ''' self.connected='' self.DEBUG(self.DBG,'Disconnect detected','stop') - self.disconnect_handlers.reverse() - for i in self.disconnect_handlers: + for i in reversed(self.disconnect_handlers): i() - self.disconnect_handlers.reverse() if self.__dict__.has_key('NonBlockingRoster'): self.NonBlockingRoster.PlugOut() if self.__dict__.has_key('NonBlockingBind'): @@ -125,6 +123,8 @@ class NBCommonClient(CommonClient): self.on_connect_failure(retry) def _on_connected(self): + # connect succeded, so no need of this callback anymore + self.on_connect_failure = None self.connected = 'tcp' if self._Ssl: transports_nb.NonBlockingTLS().PlugIn(self, now=1) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index ca13af184327e9ff6c0091dfbb6d6bd7f0ca0674..30eb811b0d6c5899d9580977bd345a0adadc6df0 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -74,7 +74,6 @@ class Dispatcher(PlugIn): self.RegisterProtocol('presence', Presence) self.RegisterProtocol('message', Message) self.RegisterDefaultHandler(self.returnStanzaHandler) - # Register Gajim's event handler as soon as dispatcher begins self.RegisterEventHandler(self._owner._caller._event_dispatcher) self.on_responses = {} @@ -84,7 +83,10 @@ class Dispatcher(PlugIn): self._owner.lastErrNode = None self._owner.lastErr = None self._owner.lastErrCode = None - self.StreamInit() + if hasattr(self._owner, 'StreamInit'): + self._owner.StreamInit() + else: + self.StreamInit() def plugout(self): ''' Prepares instance to be destructed. ''' @@ -129,17 +131,18 @@ class Dispatcher(PlugIn): try: self.Stream.Parse(data) # end stream:stream tag received - if self.Stream and self.Stream._NodeBuilder__depth == 0: + if self.Stream and self.Stream.has_received_endtag(): self._owner.Connection.disconnect() return 0 except ExpatError: sys.exc_clear() - self.DEBUG('Invalid XML received from server. Forcing disconnect.') + self.DEBUG('Invalid XML received from server. Forcing disconnect.', 'error') self._owner.Connection.pollend() return 0 if len(self._pendingExceptions) > 0: _pendingException = self._pendingExceptions.pop() raise _pendingException[0], _pendingException[1], _pendingException[2] + if len(data) == 0: return '0' return len(data) def RegisterNamespace(self, xmlns, order='info'): @@ -396,7 +399,7 @@ class Dispatcher(PlugIn): Additional callback arguments can be specified in args. ''' self.SendAndWaitForResponse(stanza, 0, func, args) - def send(self, stanza): + def send(self, stanza, is_message = False): ''' Serialise stanza and put it on the wire. Assign an unique ID to it before send. Returns assigned ID.''' if type(stanza) in [type(''), type(u'')]: @@ -423,7 +426,10 @@ class Dispatcher(PlugIn): stanza=route stanza.setNamespace(self._owner.Namespace) stanza.setParent(self._metastream) - self._owner.Connection.send(stanza) + if is_message: + self._owner.Connection.send(stanza, True) + else: + self._owner.Connection.send(stanza) return _ID def disconnect(self): diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index 03b79109396067aa41b84ba4ad05ba750bb93fa2..db0c575bdd28f340977dcda39002fd9d5ff3dbc3 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -53,7 +53,6 @@ class IdleQueue: self.selector = select.poll() def remove_timeout(self, fd): - ''' self explanatory, remove the timeout from 'read_timeouts' dict ''' if self.read_timeouts.has_key(fd): del(self.read_timeouts[fd]) diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 1add2a8e4ee73c600355fc4024e7abda3d018a3a..daf3097738572b77707716f51c8a8a3675fa81ec 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -19,7 +19,7 @@ Protocol module contains tools that is needed for processing of xmpp-related data structures. """ -from simplexml import Node,ustr +from simplexml import Node,NodeBuilder,ustr import time NS_ACTIVITY ='http://jabber.org/protocol/activity' # JEP-0108 NS_ADDRESS ='http://jabber.org/protocol/address' # JEP-0033 @@ -94,6 +94,7 @@ NS_VCARD_UPDATE =NS_VCARD+':x:update' NS_VERSION ='jabber:iq:version' NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # JEP-0130 NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # JEP-0071 +NS_XHTML = 'http://www.w3.org/1999/xhtml' # " NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # JEP-0141 NS_DATA_VALIDATE='http://jabber.org/protocol/xdata-validate' # JEP-0122 NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams' @@ -348,11 +349,18 @@ class Protocol(Node): for tag in errtag.getChildren(): if tag.getName()<>'text': return tag.getName() return errtag.getData() + def getErrorMsg(self): + """ Return the textual description of the error (if present) or the error condition """ + errtag=self.getTag('error') + if errtag: + for tag in errtag.getChildren(): + if tag.getName()=='text': return tag.getData() + return self.getError() def getErrorCode(self): - """ Return the error code. Obsolette. """ + """ Return the error code. Obsolete. """ return self.getTagAttr('error','code') def setError(self,error,code=None): - """ Set the error code. Obsolette. Use error-conditions instead. """ + """ Set the error code. Obsolete. Use error-conditions instead. """ if code: if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) @@ -378,16 +386,29 @@ class Protocol(Node): class Message(Protocol): """ XMPP Message stanza - "push" mechanism.""" - def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): + def __init__(self, to=None, body=None, xhtml=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): """ Create message object. You can specify recipient, text of message, type of message any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) if body: self.setBody(body) + if xhtml: self.setXHTML(xhtml) if subject: self.setSubject(subject) def getBody(self): """ Returns text of the message. """ return self.getTagData('body') + def getXHTML(self, xmllang=None): + """ Returns serialized xhtml-im element text of the message. + + TODO: Returning a DOM could make rendering faster.""" + xhtml = self.getTag('html') + if xhtml: + if xmllang: + body = xhtml.getTag('body', attrs={'xml:lang':xmllang}) + else: + body = xhtml.getTag('body') + return str(body) + return None def getSubject(self): """ Returns subject of the message. """ return self.getTagData('subject') @@ -397,6 +418,22 @@ class Message(Protocol): def setBody(self,val): """ Sets the text of the message. """ self.setTagData('body',val) + + def setXHTML(self,val,xmllang=None): + """ Sets the xhtml text of the message (JEP-0071). + The parameter is the "inner html" to the body.""" + try: + if xmllang: + dom = NodeBuilder('<body xmlns="'+NS_XHTML+'" xml:lang="'+xmllang+'" >' + val + '</body>').getDom() + else: + dom = NodeBuilder('<body xmlns="'+NS_XHTML+'">'+val+'</body>',0).getDom() + if self.getTag('html'): + self.getTag('html').addChild(node=dom) + else: + self.setTag('html',namespace=NS_XHTML_IM).addChild(node=dom) + except Exception, e: + print "Error", e + pass #FIXME: log. we could not set xhtml (parse error, whatever) def setSubject(self,val): """ Sets the subject of the message. """ self.setTagData('subject',val) diff --git a/src/common/xmpp/session.py b/src/common/xmpp/session.py index 3921937ed95f5b05dd60f3e01c88eb4f53e86dd4..b61e4f6deeb7ed7db18d7c3cab18fa919c4e6e25 100644 --- a/src/common/xmpp/session.py +++ b/src/common/xmpp/session.py @@ -183,7 +183,7 @@ class Session: if self.sendbuffer: try: # LOCK_QUEUE - sent=self._send(self.sendbuffer) # âÌÏËÉÒÕÀÝÁÑ ÛÔÕÞËÁ! + sent=self._send(self.sendbuffer) # blocking socket except: # UNLOCK_QUEUE self.set_socket_state(SOCKET_DEAD) diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index af304af805d51455006a2aa3173893765303ed90..8aa8321ddf6844bde159accf4d4820e285ed4831 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -302,6 +302,8 @@ class NodeBuilder: self.Parse = self._parser.Parse self.__depth = 0 + self.__last_depth = 0 + self.__max_depth = 0 self._dispatch_depth = 1 self._document_attrs = None self._mini_dom=initial_node @@ -338,7 +340,7 @@ class NodeBuilder: ns=attr[:sp] # attrs[self.namespaces[ns]+attr[sp+1:]]=attrs[attr] del attrs[attr] # - self.__depth += 1 + self._inc_depth() self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`), 'down') if self.__depth == self._dispatch_depth: if not self._mini_dom : @@ -366,7 +368,7 @@ class NodeBuilder: self._ptr = self._ptr.parent else: self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", 'stop') - self.__depth -= 1 + self._dec_depth() self.last_is_data = 0 if self.__depth == 0: self.stream_footer_received() @@ -374,7 +376,7 @@ class NodeBuilder: if self.last_is_data: if self.data_buffer: self.data_buffer.append(data) - else: + elif self._ptr: self.data_buffer = [data] self.last_is_data = 1 @@ -399,6 +401,19 @@ class NodeBuilder: """ Method called when stream just closed. """ self.check_data_buffer() + def has_received_endtag(self, level=0): + """ Return True if at least one end tag was seen (at level) """ + return self.__depth <= level and self.__max_depth > level + + def _inc_depth(self): + self.__last_depth = self.__depth + self.__depth += 1 + self.__max_depth = max(self.__depth, self.__max_depth) + + def _dec_depth(self): + self.__last_depth = self.__depth + self.__depth -= 1 + def XML2Node(xml): """ Converts supplied textual string into XML node. Handy f.e. for reading configuration file. Raises xml.parser.expat.parsererror if provided string is not well-formed XML. """ diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index ad3456e6a6a0f8cd922d651aa7b9afd5a4529d53..528b9e08f1d537f27ac79b2e03e263411536cb4c 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -143,11 +143,11 @@ class NonBlockingTcp(PlugIn, IdleObject): def pollin(self): self._do_receive() - def pollend(self): + def pollend(self, retry = False): conn_failure_cb = self.on_connect_failure self.disconnect() if conn_failure_cb: - conn_failure_cb() + conn_failure_cb(retry) def disconnect(self): if self.state == -2: # already disconnected @@ -216,15 +216,19 @@ class NonBlockingTcp(PlugIn, IdleObject): # "received" will be empty anyhow if errnum == socket.SSL_ERROR_WANT_READ: pass - elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]: + elif errnum == errno.ECONNRESET: + self.pollend(True) + # don't proccess result, caus it will raise error + return + elif errnum in [errno.ENOTCONN, errno.ESHUTDOWN]: self.pollend() - # don't proccess result, cas it will raise error + # don't proccess result, caus it will raise error return elif not received : if errnum != socket.SSL_ERROR_EOF: # 8 EOF occurred in violation of protocol self.DEBUG('Socket error while receiving data', 'error') - self.pollend() + self.pollend(True) if self.state >= 0: self.disconnect() return diff --git a/src/common/zeroconf/__init__.py b/src/common/zeroconf/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py new file mode 100644 index 0000000000000000000000000000000000000000..da15b7ad5ec730d0de0ce7b9d44e435e67c925c3 --- /dev/null +++ b/src/common/zeroconf/client_zeroconf.py @@ -0,0 +1,647 @@ +## common/zeroconf/client_zeroconf.py +## +## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> +## 2006 Dimitur Kirov <dkirov@gmail.com> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +from common import gajim +import common.xmpp +from common.xmpp.idlequeue import IdleObject +from common.xmpp import dispatcher_nb, simplexml +from common.xmpp.client import * +from common.xmpp.simplexml import ustr +from common.zeroconf import zeroconf + +from common.xmpp.protocol import * +import socket +import errno +import sys + +from common.zeroconf import roster_zeroconf + +MAX_BUFF_LEN = 65536 +DATA_RECEIVED='DATA RECEIVED' +DATA_SENT='DATA SENT' +TYPE_SERVER, TYPE_CLIENT = range(2) + +# wait XX sec to establish a connection +CONNECT_TIMEOUT_SECONDS = 10 + +# after XX sec with no activity, close the stream +ACTIVITY_TIMEOUT_SECONDS = 30 + +class ZeroconfListener(IdleObject): + def __init__(self, port, conn_holder): + ''' handle all incomming connections on ('0.0.0.0', port)''' + self.port = port + self.queue_idx = -1 + #~ self.queue = None + self.started = False + self._sock = None + self.fd = -1 + self.caller = conn_holder.caller + self.conn_holder = conn_holder + + def bind(self): + self._serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # will fail when port is busy, or we don't have rights to bind + try: + self._serv.bind(('0.0.0.0', self.port)) + except Exception, e: + # unable to bind, show error dialog + return None + self._serv.listen(socket.SOMAXCONN) + self._serv.setblocking(False) + self.fd = self._serv.fileno() + gajim.idlequeue.plug_idle(self, False, True) + self.started = True + + def pollend(self): + ''' called when we stop listening on (host, port) ''' + self.disconnect() + + def pollin(self): + ''' accept a new incomming connection and notify queue''' + sock = self.accept_conn() + P2PClient(sock[0], sock[1][0], sock[1][1], self.conn_holder) + + def disconnect(self): + ''' free all resources, we are not listening anymore ''' + gajim.idlequeue.remove_timeout(self.fd) + gajim.idlequeue.unplug_idle(self.fd) + self.fd = -1 + self.started = False + try: + self._serv.close() + except: + pass + self.conn_holder.kill_all_connections() + + def accept_conn(self): + ''' accepts a new incoming connection ''' + _sock = self._serv.accept() + _sock[0].setblocking(False) + return _sock + +class P2PClient(IdleObject): + def __init__(self, _sock, host, port, conn_holder, stanzaqueue = [], to = None): + self._owner = self + self.Namespace = 'jabber:client' + self.defaultNamespace = self.Namespace + self._component = 0 + self._registered_name = None + self._caller = conn_holder.caller + self.conn_holder = conn_holder + self.stanzaqueue = stanzaqueue + self.to = to + self.Server = host + self.DBG = 'client' + self.Connection = None + if gajim.verbose: + debug = ['always', 'nodebuilder'] + else: + debug = [] + self._DEBUG = Debug.Debug(debug) + self.DEBUG = self._DEBUG.Show + self.debug_flags = self._DEBUG.debug_flags + self.debug_flags.append(self.DBG) + self.sock_hash = None + if _sock: + self.sock_type = TYPE_SERVER + else: + self.sock_type = TYPE_CLIENT + conn = P2PConnection('', _sock, host, port, self._caller, self.on_connect, self) + self.sock_hash = conn._sock.__hash__ + self.fd = conn.fd + self.conn_holder.add_connection(self, self.Server, port, self.to) + # count messages in queue + for val in self.stanzaqueue: + stanza, is_message = val + if is_message: + if self.conn_holder.number_of_awaiting_messages.has_key(self.fd): + self.conn_holder.number_of_awaiting_messages[self.fd]+=1 + else: + self.conn_holder.number_of_awaiting_messages[self.fd]=1 + + def add_stanza(self, stanza, is_message = False): + if self.Connection: + if self.Connection.state == -1: + return False + self.send(stanza, is_message) + else: + self.stanzaqueue.append((stanza, is_message)) + + if is_message: + if self.conn_holder.number_of_awaiting_messages.has_key(self.fd): + self.conn_holder.number_of_awaiting_messages[self.fd]+=1 + else: + self.conn_holder.number_of_awaiting_messages[self.fd] = 1 + + return True + + def on_message_sent(self, connection_id): + self.conn_holder.number_of_awaiting_messages[connection_id]-=1 + + def on_connect(self, conn): + self.Connection = conn + self.Connection.PlugIn(self) + dispatcher_nb.Dispatcher().PlugIn(self) + self._register_handlers() + if self.sock_type == TYPE_CLIENT: + while self.stanzaqueue: + stanza, is_message = self.stanzaqueue.pop(0) + self.send(stanza, is_message) + + def StreamInit(self): + ''' Send an initial stream header. ''' + self.Dispatcher.Stream = simplexml.NodeBuilder() + self.Dispatcher.Stream._dispatch_depth = 2 + self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch + self.Dispatcher.Stream.stream_header_received = self._check_stream_start + self.debug_flags.append(simplexml.DBG_NODEBUILDER) + self.Dispatcher.Stream.DEBUG = self.DEBUG + self.Dispatcher.Stream.features = None + if self.sock_type == TYPE_CLIENT: + self.send_stream_header() + + def send_stream_header(self): + self.Dispatcher._metastream = Node('stream:stream') + self.Dispatcher._metastream.setNamespace(self.Namespace) + # XXX TLS support + #~ self._metastream.setAttr('version', '1.0') + self.Dispatcher._metastream.setAttr('xmlns:stream', NS_STREAMS) + self.Dispatcher.send("<?xml version='1.0'?>%s>" % str(self.Dispatcher._metastream)[:-2]) + + def _check_stream_start(self, ns, tag, attrs): + if ns<>NS_STREAMS or tag<>'stream': + self._caller.dispatch('MSGERROR',[unicode(self.to), -1, \ + _('Connection to host could not be established: Incorrect answer from server.'), None, None]) + self.Connection.DEBUG('Incorrect stream start: (%s,%s).Terminating! ' % (tag, ns), 'error') + self.Connection.disconnect() + return + if self.sock_type == TYPE_SERVER: + self.send_stream_header() + while self.stanzaqueue: + stanza, is_message = self.stanzaqueue.pop(0) + self.send(stanza, is_message) + + + def on_disconnect(self): + if self.conn_holder: + if self.conn_holder.number_of_awaiting_messages.has_key(self.fd): + if self.conn_holder.number_of_awaiting_messages[self.fd] > 0: + self._caller.dispatch('MSGERROR',[unicode(self.to), -1, \ + _('Connection to host could not be established'), None, None]) + del self.conn_holder.number_of_awaiting_messages[self.fd] + self.conn_holder.remove_connection(self.sock_hash) + if self.__dict__.has_key('Dispatcher'): + self.Dispatcher.PlugOut() + if self.__dict__.has_key('P2PConnection'): + self.P2PConnection.PlugOut() + self.Connection = None + self._caller = None + self.conn_holder = None + + def force_disconnect(self): + if self.Connection: + self.disconnect() + else: + self.on_disconnect() + + def _on_receive_document_attrs(self, data): + if data: + self.Dispatcher.ProcessNonBlocking(data) + if not hasattr(self, 'Dispatcher') or \ + self.Dispatcher.Stream._document_attrs is None: + return + self.onreceive(None) + if self.Dispatcher.Stream._document_attrs.has_key('version') and \ + self.Dispatcher.Stream._document_attrs['version'] == '1.0': + #~ self.onreceive(self._on_receive_stream_features) + #XXX continue with TLS + return + self.onreceive(None) + return True + + def _register_handlers(self): + self.RegisterHandler('message', lambda conn, data:self._caller._messageCB(self.Server, conn, data)) + self.RegisterHandler('iq', self._caller._siSetCB, 'set', + common.xmpp.NS_SI) + self.RegisterHandler('iq', self._caller._siErrorCB, 'error', + common.xmpp.NS_SI) + self.RegisterHandler('iq', self._caller._siResultCB, 'result', + common.xmpp.NS_SI) + self.RegisterHandler('iq', self._caller._bytestreamSetCB, 'set', + common.xmpp.NS_BYTESTREAM) + self.RegisterHandler('iq', self._caller._bytestreamResultCB, 'result', + common.xmpp.NS_BYTESTREAM) + self.RegisterHandler('iq', self._caller._bytestreamErrorCB, 'error', + common.xmpp.NS_BYTESTREAM) + +class P2PConnection(IdleObject, PlugIn): + ''' class for sending file to socket over socks5 ''' + def __init__(self, sock_hash, _sock, host = None, port = None, caller = None, on_connect = None, client = None): + IdleObject.__init__(self) + self._owner = client + PlugIn.__init__(self) + self.DBG_LINE='socket' + self.sendqueue = [] + self.sendbuff = None + self.buff_is_message = False + self._sock = _sock + self.sock_hash = None + self.host, self.port = host, port + self.on_connect = on_connect + self.client = client + self.writable = False + self.readable = False + self._exported_methods=[self.send, self.disconnect, self.onreceive] + self.on_receive = None + if _sock: + self._sock = _sock + self.state = 1 + self._sock.setblocking(False) + self.fd = self._sock.fileno() + self.on_connect(self) + else: + self.state = 0 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.setblocking(False) + self.fd = self._sock.fileno() + gajim.idlequeue.plug_idle(self, True, False) + self.set_timeout(CONNECT_TIMEOUT_SECONDS) + self.do_connect() + + def set_timeout(self, timeout): + gajim.idlequeue.remove_timeout(self.fd) + if self.state >= 0: + gajim.idlequeue.set_read_timeout(self.fd, timeout) + + def plugin(self, owner): + self.onreceive(owner._on_receive_document_attrs) + self._plug_idle() + return True + + def plugout(self): + ''' Disconnect from the remote server and unregister self.disconnected method from + the owner's dispatcher. ''' + self.disconnect() + self._owner = None + + def onreceive(self, recv_handler): + if not recv_handler: + if hasattr(self._owner, 'Dispatcher'): + self.on_receive = self._owner.Dispatcher.ProcessNonBlocking + else: + self.on_receive = None + return + _tmp = self.on_receive + # make sure this cb is not overriden by recursive calls + if not recv_handler(None) and _tmp == self.on_receive: + self.on_receive = recv_handler + + def send(self, packet, is_message = False): + '''Append stanza to the queue of messages to be send. + If supplied data is unicode string, encode it to utf-8. + ''' + if self.state <= 0: + return + + r = packet + + if isinstance(r, unicode): + r = r.encode('utf-8') + elif not isinstance(r, str): + r = ustr(r).encode('utf-8') + + self.sendqueue.append((r, is_message)) + self._plug_idle() + + def read_timeout(self): + if self.client.conn_holder.number_of_awaiting_messages.has_key(self.fd) \ + and self.client.conn_holder.number_of_awaiting_messages[self.fd] > 0: + self.client._caller.dispatch('MSGERROR',[unicode(self.client.to), -1, \ + _('Connection to host could not be established: Timeout while sending data.'), None, None]) + self.client.conn_holder.number_of_awaiting_messages[self.fd] = 0 + self.pollend() + + def do_connect(self): + errnum = 0 + try: + self._sock.connect((self.host, self.port)) + self._sock.setblocking(False) + except Exception, ee: + (errnum, errstr) = ee + if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): + return + # win32 needs this + elif errnum not in (0, 10056, errno.EISCONN) or self.state != 0: + self.disconnect() + return None + else: # socket is already connected + self._sock.setblocking(False) + self.state = 1 # connected + self.on_connect(self) + return 1 # we are connected + + + def pollout(self): + if self.state == 0: + self.do_connect() + return + gajim.idlequeue.remove_timeout(self.fd) + self._do_send() + + def pollend(self): + self.state = -1 + self.disconnect() + + def pollin(self): + ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' + received = '' + errnum = 0 + try: + # get as many bites, as possible, but not more than RECV_BUFSIZE + received = self._sock.recv(MAX_BUFF_LEN) + except Exception, e: + if len(e.args) > 0 and isinstance(e.args[0], int): + errnum = e[0] + sys.exc_clear() + # "received" will be empty anyhow + if errnum == socket.SSL_ERROR_WANT_READ: + pass + elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]: + self.pollend() + # don't proccess result, cas it will raise error + return + elif not received : + if errnum != socket.SSL_ERROR_EOF: + # 8 EOF occurred in violation of protocol + self.pollend() + if self.state >= 0: + self.disconnect() + return + + if self.state < 0: + return + if self.on_receive: + if self._owner.sock_type == TYPE_CLIENT: + self.set_timeout(ACTIVITY_TIMEOUT_SECONDS) + if received.strip(): + self.DEBUG(received, 'got') + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_RECEIVED, received) + self.on_receive(received) + else: + # This should never happed, so we need the debug + self.DEBUG('Unhandled data received: %s' % received,'error') + self.disconnect() + return True + + def onreceive(self, recv_handler): + if not recv_handler: + if hasattr(self._owner, 'Dispatcher'): + self.on_receive = self._owner.Dispatcher.ProcessNonBlocking + else: + self.on_receive = None + return + _tmp = self.on_receive + # make sure this cb is not overriden by recursive calls + if not recv_handler(None) and _tmp == self.on_receive: + self.on_receive = recv_handler + + def disconnect(self): + ''' Closes the socket. ''' + gajim.idlequeue.remove_timeout(self.fd) + gajim.idlequeue.unplug_idle(self.fd) + try: + self._sock.shutdown(socket.SHUT_RDWR) + self._sock.close() + except: + # socket is already closed + pass + self.fd = -1 + self.state = -1 + if self._owner: + self._owner.on_disconnect() + + def _do_send(self): + if not self.sendbuff: + if not self.sendqueue: + return None # nothing to send + self.sendbuff, self.buff_is_message = self.sendqueue.pop(0) + self.sent_data = self.sendbuff + try: + send_count = self._sock.send(self.sendbuff) + if send_count: + self.sendbuff = self.sendbuff[send_count:] + if not self.sendbuff and not self.sendqueue: + if self.state < 0: + gajim.idlequeue.unplug_idle(self.fd) + self._on_send() + self.disconnect() + return + # we are not waiting for write + self._plug_idle() + self._on_send() + + except socket.error, e: + sys.exc_clear() + if e[0] == socket.SSL_ERROR_WANT_WRITE: + return True + if self.state < 0: + self.disconnect() + return + self._on_send_failure() + return + if self._owner.sock_type == TYPE_CLIENT: + self.set_timeout(ACTIVITY_TIMEOUT_SECONDS) + return True + + def _plug_idle(self): + readable = self.state != 0 + if self.sendqueue or self.sendbuff: + writable = True + else: + writable = False + if self.writable != writable or self.readable != readable: + gajim.idlequeue.plug_idle(self, writable, readable) + + + def _on_send(self): + if self.sent_data and self.sent_data.strip(): + self.DEBUG(self.sent_data,'sent') + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_SENT, self.sent_data) + self.sent_data = None + if self.buff_is_message: + self._owner.on_message_sent(self.fd) + self.buff_is_message = False + + def _on_send_failure(self): + self.DEBUG("Socket error while sending data",'error') + self._owner.disconnected() + self.sent_data = None + + +class ClientZeroconf: + def __init__(self, caller): + self.caller = caller + self.zeroconf = None + self.roster = None + self.last_msg = '' + self.connections = {} + self.recipient_to_hash = {} + self.ip_to_hash = {} + self.hash_to_port = {} + self.listener = None + self.number_of_awaiting_messages = {} + + def test_avahi(self): + try: + import avahi + except ImportError: + return False + return True + + def connect(self, show, msg): + self.port = self.start_listener(self.caller.port) + if not self.port: + return False + self.zeroconf_init(show, msg) + if not self.zeroconf.connect(): + self.disconnect() + return None + self.roster = roster_zeroconf.Roster(self.zeroconf) + return True + + def remove_announce(self): + if self.zeroconf: + return self.zeroconf.remove_announce() + + def announce(self): + if self.zeroconf: + return self.zeroconf.announce() + + def set_show_msg(self, show, msg): + if self.zeroconf: + self.zeroconf.txt['msg'] = msg + self.last_msg = msg + return self.zeroconf.update_txt(show) + + def resolve_all(self): + if self.zeroconf: + self.zeroconf.resolve_all() + + def reannounce(self, txt): + self.remove_announce() + self.zeroconf.txt = txt + self.zeroconf.port = self.port + self.zeroconf.username = self.caller.username + return self.announce() + + + def zeroconf_init(self, show, msg): + self.zeroconf = zeroconf.Zeroconf(self.caller._on_new_service, + self.caller._on_remove_service, self.caller._on_name_conflictCB, + self.caller._on_disconnected, self.caller._on_error, self.caller.username, self.caller.host, self.port) + self.zeroconf.txt['msg'] = msg + self.zeroconf.txt['status'] = show + self.zeroconf.txt['1st'] = self.caller.first + self.zeroconf.txt['last'] = self.caller.last + self.zeroconf.txt['jid'] = self.caller.jabber_id + self.zeroconf.txt['email'] = self.caller.email + self.zeroconf.username = self.caller.username + self.zeroconf.host = self.caller.host + self.zeroconf.port = self.port + self.last_msg = msg + + def disconnect(self): + if self.listener: + self.listener.disconnect() + self.listener = None + if self.zeroconf: + self.zeroconf.disconnect() + self.zeroconf = None + if self.roster: + self.roster.zeroconf = None + self.roster._data = None + self.roster = None + + def kill_all_connections(self): + for connection in self.connections.values(): + connection.force_disconnect() + + def add_connection(self, connection, ip, port, recipient): + sock_hash = connection.sock_hash + if sock_hash not in self.connections: + self.connections[sock_hash] = connection + self.ip_to_hash[ip] = sock_hash + self.hash_to_port[sock_hash] = port + if recipient: + self.recipient_to_hash[recipient] = sock_hash + + def remove_connection(self, sock_hash): + if sock_hash in self.connections: + del self.connections[sock_hash] + for i in self.recipient_to_hash: + if self.recipient_to_hash[i] == sock_hash: + del self.recipient_to_hash[i] + break + for i in self.ip_to_hash: + if self.ip_to_hash[i] == sock_hash: + del self.ip_to_hash[i] + break + if self.hash_to_port.has_key(sock_hash): + del self.hash_to_port[sock_hash] + + def start_listener(self, port): + for p in range(port, port + 5): + self.listener = ZeroconfListener(p, self) + self.listener.bind() + if self.listener.started: + return p + self.listener = None + return False + + def getRoster(self): + if self.roster: + return self.roster.getRoster() + return {} + + def send(self, stanza, is_message = False): + stanza.setFrom(self.roster.zeroconf.name) + to = stanza.getTo() + + try: + item = self.roster[to] + except KeyError: + self.caller.dispatch('MSGERROR', [unicode(to), '-1', _('Contact is offline. Your message could not be sent.'), None, None]) + return False + + # look for hashed connections + if to in self.recipient_to_hash: + conn = self.connections[self.recipient_to_hash[to]] + if conn.add_stanza(stanza, is_message): + return + + if item['address'] in self.ip_to_hash: + hash = self.ip_to_hash[item['address']] + if self.hash_to_port[hash] == item['port']: + conn = self.connections[hash] + if conn.add_stanza(stanza, is_message): + return + + # otherwise open new connection + P2PClient(None, item['address'], item['port'], self, [(stanza, is_message)], to) diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py new file mode 100644 index 0000000000000000000000000000000000000000..238de3ee8ce82b58b575080e05cdc54335d21a57 --- /dev/null +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -0,0 +1,906 @@ +## +## Copyright (C) 2006 Gajim Team +## +## Contributors for this file: +## - Yann Le Boulanger <asterix@lagaule.org> +## - Nikos Kouremenos <nkour@jabber.org> +## - Dimitur Kirov <dkirov@gmail.com> +## - Travis Shirk <travis@pobox.com> +## - Stefan Bethge <stefan@lanpartei.de> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +import os +import time +import base64 +import sha +import socket +import sys + +from calendar import timegm + +#import socks5 +import common.xmpp + +from common import GnuPG +from common import helpers +from common import gajim +from common.zeroconf import zeroconf +STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', + 'invisible'] +# kind of events we can wait for an answer +VCARD_PUBLISHED = 'vcard_published' +VCARD_ARRIVED = 'vcard_arrived' +AGENT_REMOVED = 'agent_removed' +HAS_IDLE = True +try: + import idle +except: + gajim.log.debug(_('Unable to load idle module')) + HAS_IDLE = False + + +class ConnectionBytestream: + def __init__(self): + self.files_props = {} + + def is_transfer_stoped(self, file_props): + if file_props.has_key('error') and file_props['error'] != 0: + return True + if file_props.has_key('completed') and file_props['completed']: + return True + if file_props.has_key('connected') and file_props['connected'] == False: + return True + if not file_props.has_key('stopped') or not file_props['stopped']: + return False + return True + + def send_success_connect_reply(self, streamhost): + ''' send reply to the initiator of FT that we + made a connection + ''' + if streamhost is None: + return None + iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result', + frm = streamhost['target']) + iq.setAttr('id', streamhost['id']) + query = iq.setTag('query') + query.setNamespace(common.xmpp.NS_BYTESTREAM) + stream_tag = query.setTag('streamhost-used') + stream_tag.setAttr('jid', streamhost['jid']) + self.connection.send(iq) + + def remove_transfers_for_contact(self, contact): + ''' stop all active transfer for contact ''' + for file_props in self.files_props.values(): + if self.is_transfer_stoped(file_props): + continue + receiver_jid = unicode(file_props['receiver']).split('/')[0] + if contact.jid == receiver_jid: + file_props['error'] = -5 + self.remove_transfer(file_props) + self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, '')) + sender_jid = unicode(file_props['sender']) + if contact.jid == sender_jid: + file_props['error'] = -3 + self.remove_transfer(file_props) + + def remove_all_transfers(self): + ''' stops and removes all active connections from the socks5 pool ''' + for file_props in self.files_props.values(): + self.remove_transfer(file_props, remove_from_list = False) + del(self.files_props) + self.files_props = {} + + def remove_transfer(self, file_props, remove_from_list = True): + if file_props is None: + return + self.disconnect_transfer(file_props) + sid = file_props['sid'] + gajim.socks5queue.remove_file_props(self.name, sid) + + if remove_from_list: + if self.files_props.has_key('sid'): + del(self.files_props['sid']) + + def disconnect_transfer(self, file_props): + if file_props is None: + return + if file_props.has_key('hash'): + gajim.socks5queue.remove_sender(file_props['hash']) + + if file_props.has_key('streamhosts'): + for host in file_props['streamhosts']: + if host.has_key('idx') and host['idx'] > 0: + gajim.socks5queue.remove_receiver(host['idx']) + gajim.socks5queue.remove_sender(host['idx']) + + def send_socks5_info(self, file_props, fast = True, receiver = None, + sender = None): + ''' send iq for the present streamhosts and proxies ''' + if type(self.peerhost) != tuple: + return + port = gajim.config.get('file_transfers_port') + ft_override_host_to_send = gajim.config.get('ft_override_host_to_send') + if receiver is None: + receiver = file_props['receiver'] + if sender is None: + sender = file_props['sender'] + proxyhosts = [] + sha_str = helpers.get_auth_sha(file_props['sid'], sender, + receiver) + file_props['sha_str'] = sha_str + if not ft_override_host_to_send: + ft_override_host_to_send = self.peerhost[0] + try: + ft_override_host_to_send = socket.gethostbyname( + ft_override_host_to_send) + except socket.gaierror: + self.dispatch('ERROR', (_('Wrong host'), _('The host you configured as the ft_override_host_to_send advanced option is not valid, so ignored.'))) + ft_override_host_to_send = self.peerhost[0] + listener = gajim.socks5queue.start_listener(port, + sha_str, self._result_socks5_sid, file_props['sid']) + if listener == None: + file_props['error'] = -5 + self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props, + '')) + self._connect_error(unicode(receiver), file_props['sid'], + file_props['sid'], code = 406) + return + + iq = common.xmpp.Protocol(name = 'iq', to = unicode(receiver), + typ = 'set') + file_props['request-id'] = 'id_' + file_props['sid'] + iq.setID(file_props['request-id']) + query = iq.setTag('query') + query.setNamespace(common.xmpp.NS_BYTESTREAM) + query.setAttr('mode', 'tcp') + query.setAttr('sid', file_props['sid']) + streamhost = query.setTag('streamhost') + streamhost.setAttr('port', unicode(port)) + streamhost.setAttr('host', ft_override_host_to_send) + streamhost.setAttr('jid', sender) + self.connection.send(iq) + + def send_file_rejection(self, file_props): + ''' informs sender that we refuse to download the file ''' + # user response to ConfirmationDialog may come after we've disconneted + if not self.connection or self.connected < 2: + return + iq = common.xmpp.Protocol(name = 'iq', + to = unicode(file_props['sender']), typ = 'error') + iq.setAttr('id', file_props['request-id']) + err = common.xmpp.ErrorNode(code = '403', typ = 'cancel', name = + 'forbidden', text = 'Offer Declined') + iq.addChild(node=err) + self.connection.send(iq) + + def send_file_approval(self, file_props): + ''' send iq, confirming that we want to download the file ''' + # user response to ConfirmationDialog may come after we've disconneted + if not self.connection or self.connected < 2: + return + iq = common.xmpp.Protocol(name = 'iq', + to = unicode(file_props['sender']), typ = 'result') + iq.setAttr('id', file_props['request-id']) + si = iq.setTag('si') + si.setNamespace(common.xmpp.NS_SI) + if file_props.has_key('offset') and file_props['offset']: + file_tag = si.setTag('file') + file_tag.setNamespace(common.xmpp.NS_FILE) + range_tag = file_tag.setTag('range') + range_tag.setAttr('offset', file_props['offset']) + feature = si.setTag('feature') + feature.setNamespace(common.xmpp.NS_FEATURE) + _feature = common.xmpp.DataForm(typ='submit') + feature.addChild(node=_feature) + field = _feature.setField('stream-method') + field.delAttr('type') + field.setValue(common.xmpp.NS_BYTESTREAM) + self.connection.send(iq) + + def send_file_request(self, file_props): + ''' send iq for new FT request ''' + if not self.connection or self.connected < 2: + return + our_jid = gajim.get_jid_from_account(self.name) + frm = our_jid + file_props['sender'] = frm + fjid = file_props['receiver'].jid + iq = common.xmpp.Protocol(name = 'iq', to = fjid, + typ = 'set') + iq.setID(file_props['sid']) + self.files_props[file_props['sid']] = file_props + si = iq.setTag('si') + si.setNamespace(common.xmpp.NS_SI) + si.setAttr('profile', common.xmpp.NS_FILE) + si.setAttr('id', file_props['sid']) + file_tag = si.setTag('file') + file_tag.setNamespace(common.xmpp.NS_FILE) + file_tag.setAttr('name', file_props['name']) + file_tag.setAttr('size', file_props['size']) + desc = file_tag.setTag('desc') + if file_props.has_key('desc'): + desc.setData(file_props['desc']) + file_tag.setTag('range') + feature = si.setTag('feature') + feature.setNamespace(common.xmpp.NS_FEATURE) + _feature = common.xmpp.DataForm(typ='form') + feature.addChild(node=_feature) + field = _feature.setField('stream-method') + field.setAttr('type', 'list-single') + field.addOption(common.xmpp.NS_BYTESTREAM) + self.connection.send(iq) + + def _result_socks5_sid(self, sid, hash_id): + ''' store the result of sha message from auth. ''' + if not self.files_props.has_key(sid): + return + file_props = self.files_props[sid] + file_props['hash'] = hash_id + return + + def _connect_error(self, to, _id, sid, code = 404): + ''' cb, when there is an error establishing BS connection, or + when connection is rejected''' + msg_dict = { + 404: 'Could not connect to given hosts', + 405: 'Cancel', + 406: 'Not acceptable', + } + msg = msg_dict[code] + iq = None + iq = common.xmpp.Protocol(name = 'iq', to = to, + typ = 'error') + iq.setAttr('id', _id) + err = iq.setTag('error') + err.setAttr('code', unicode(code)) + err.setData(msg) + self.connection.send(iq) + if code == 404: + file_props = gajim.socks5queue.get_file_props(self.name, sid) + if file_props is not None: + self.disconnect_transfer(file_props) + file_props['error'] = -3 + self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg)) + + def _proxy_auth_ok(self, proxy): + '''cb, called after authentication to proxy server ''' + file_props = self.files_props[proxy['sid']] + iq = common.xmpp.Protocol(name = 'iq', to = proxy['initiator'], + typ = 'set') + auth_id = "au_" + proxy['sid'] + iq.setID(auth_id) + query = iq.setTag('query') + query.setNamespace(common.xmpp.NS_BYTESTREAM) + query.setAttr('sid', proxy['sid']) + activate = query.setTag('activate') + activate.setData(file_props['proxy_receiver']) + iq.setID(auth_id) + self.connection.send(iq) + + # register xmpppy handlers for bytestream and FT stanzas + def _bytestreamErrorCB(self, con, iq_obj): + gajim.log.debug('_bytestreamErrorCB') + id = unicode(iq_obj.getAttr('id')) + frm = unicode(iq_obj.getFrom()) + query = iq_obj.getTag('query') + gajim.proxy65_manager.error_cb(frm, query) + jid = unicode(iq_obj.getFrom()) + id = id[3:] + if not self.files_props.has_key(id): + return + file_props = self.files_props[id] + file_props['error'] = -4 + self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) + raise common.xmpp.NodeProcessed + + def _bytestreamSetCB(self, con, iq_obj): + gajim.log.debug('_bytestreamSetCB') + target = unicode(iq_obj.getAttr('to')) + id = unicode(iq_obj.getAttr('id')) + query = iq_obj.getTag('query') + sid = unicode(query.getAttr('sid')) + file_props = gajim.socks5queue.get_file_props( + self.name, sid) + streamhosts=[] + for item in query.getChildren(): + if item.getName() == 'streamhost': + host_dict={ + 'state': 0, + 'target': target, + 'id': id, + 'sid': sid, + 'initiator': unicode(iq_obj.getFrom()) + } + for attr in item.getAttrs(): + host_dict[attr] = item.getAttr(attr) + streamhosts.append(host_dict) + if file_props is None: + if self.files_props.has_key(sid): + file_props = self.files_props[sid] + file_props['fast'] = streamhosts + if file_props['type'] == 's': + if file_props.has_key('streamhosts'): + file_props['streamhosts'].extend(streamhosts) + else: + file_props['streamhosts'] = streamhosts + if not gajim.socks5queue.get_file_props(self.name, sid): + gajim.socks5queue.add_file_props(self.name, file_props) + gajim.socks5queue.connect_to_hosts(self.name, sid, + self.send_success_connect_reply, None) + raise common.xmpp.NodeProcessed + + file_props['streamhosts'] = streamhosts + if file_props['type'] == 'r': + gajim.socks5queue.connect_to_hosts(self.name, sid, + self.send_success_connect_reply, self._connect_error) + raise common.xmpp.NodeProcessed + + def _ResultCB(self, con, iq_obj): + gajim.log.debug('_ResultCB') + # if we want to respect jep-0065 we have to check for proxy + # activation result in any result iq + real_id = unicode(iq_obj.getAttr('id')) + if real_id[:3] != 'au_': + return + frm = unicode(iq_obj.getFrom()) + id = real_id[3:] + if self.files_props.has_key(id): + file_props = self.files_props[id] + if file_props['streamhost-used']: + for host in file_props['proxyhosts']: + if host['initiator'] == frm and host.has_key('idx'): + gajim.socks5queue.activate_proxy(host['idx']) + raise common.xmpp.NodeProcessed + + def _bytestreamResultCB(self, con, iq_obj): + gajim.log.debug('_bytestreamResultCB') + frm = unicode(iq_obj.getFrom()) + real_id = unicode(iq_obj.getAttr('id')) + query = iq_obj.getTag('query') + gajim.proxy65_manager.resolve_result(frm, query) + + try: + streamhost = query.getTag('streamhost-used') + except: # this bytestream result is not what we need + pass + id = real_id[3:] + if self.files_props.has_key(id): + file_props = self.files_props[id] + else: + raise common.xmpp.NodeProcessed + if streamhost is None: + # proxy approves the activate query + if real_id[:3] == 'au_': + id = real_id[3:] + if not file_props.has_key('streamhost-used') or \ + file_props['streamhost-used'] is False: + raise common.xmpp.NodeProcessed + if not file_props.has_key('proxyhosts'): + raise common.xmpp.NodeProcessed + for host in file_props['proxyhosts']: + if host['initiator'] == frm and \ + unicode(query.getAttr('sid')) == file_props['sid']: + gajim.socks5queue.activate_proxy(host['idx']) + break + raise common.xmpp.NodeProcessed + jid = streamhost.getAttr('jid') + if file_props.has_key('streamhost-used') and \ + file_props['streamhost-used'] is True: + raise common.xmpp.NodeProcessed + + if real_id[:3] == 'au_': + gajim.socks5queue.send_file(file_props, self.name) + raise common.xmpp.NodeProcessed + + proxy = None + if file_props.has_key('proxyhosts'): + for proxyhost in file_props['proxyhosts']: + if proxyhost['jid'] == jid: + proxy = proxyhost + + if proxy != None: + file_props['streamhost-used'] = True + if not file_props.has_key('streamhosts'): + file_props['streamhosts'] = [] + file_props['streamhosts'].append(proxy) + file_props['is_a_proxy'] = True + receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy, file_props['sid'], file_props) + gajim.socks5queue.add_receiver(self.name, receiver) + proxy['idx'] = receiver.queue_idx + gajim.socks5queue.on_success = self._proxy_auth_ok + raise common.xmpp.NodeProcessed + + else: + gajim.socks5queue.send_file(file_props, self.name) + if file_props.has_key('fast'): + fasts = file_props['fast'] + if len(fasts) > 0: + self._connect_error(frm, fasts[0]['id'], file_props['sid'], + code = 406) + + raise common.xmpp.NodeProcessed + + def _siResultCB(self, con, iq_obj): + gajim.log.debug('_siResultCB') + self.peerhost = con._owner.Connection._sock.getsockname() + id = iq_obj.getAttr('id') + if not self.files_props.has_key(id): + # no such jid + return + file_props = self.files_props[id] + if file_props is None: + # file properties for jid is none + return + if file_props.has_key('request-id'): + # we have already sent streamhosts info + return + file_props['receiver'] = unicode(iq_obj.getFrom()) + si = iq_obj.getTag('si') + file_tag = si.getTag('file') + range_tag = None + if file_tag: + range_tag = file_tag.getTag('range') + if range_tag: + offset = range_tag.getAttr('offset') + if offset: + file_props['offset'] = int(offset) + length = range_tag.getAttr('length') + if length: + file_props['length'] = int(length) + feature = si.setTag('feature') + if feature.getNamespace() != common.xmpp.NS_FEATURE: + return + form_tag = feature.getTag('x') + form = common.xmpp.DataForm(node=form_tag) + field = form.getField('stream-method') + if field.getValue() != common.xmpp.NS_BYTESTREAM: + return + self.send_socks5_info(file_props, fast = True) + raise common.xmpp.NodeProcessed + + def _siSetCB(self, con, iq_obj): + gajim.log.debug('_siSetCB') + jid = unicode(iq_obj.getFrom()) + si = iq_obj.getTag('si') + profile = si.getAttr('profile') + mime_type = si.getAttr('mime-type') + if profile != common.xmpp.NS_FILE: + return + file_tag = si.getTag('file') + file_props = {'type': 'r'} + for attribute in file_tag.getAttrs(): + if attribute in ('name', 'size', 'hash', 'date'): + val = file_tag.getAttr(attribute) + if val is None: + continue + file_props[attribute] = val + file_desc_tag = file_tag.getTag('desc') + if file_desc_tag is not None: + file_props['desc'] = file_desc_tag.getData() + + if mime_type is not None: + file_props['mime-type'] = mime_type + our_jid = gajim.get_jid_from_account(self.name) + file_props['receiver'] = our_jid + file_props['sender'] = unicode(iq_obj.getFrom()) + file_props['request-id'] = unicode(iq_obj.getAttr('id')) + file_props['sid'] = unicode(si.getAttr('id')) + gajim.socks5queue.add_file_props(self.name, file_props) + self.dispatch('FILE_REQUEST', (jid, file_props)) + raise common.xmpp.NodeProcessed + + def _siErrorCB(self, con, iq_obj): + gajim.log.debug('_siErrorCB') + si = iq_obj.getTag('si') + profile = si.getAttr('profile') + if profile != common.xmpp.NS_FILE: + return + id = iq_obj.getAttr('id') + if not self.files_props.has_key(id): + # no such jid + return + file_props = self.files_props[id] + if file_props is None: + # file properties for jid is none + return + jid = unicode(iq_obj.getFrom()) + file_props['error'] = -3 + self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) + raise common.xmpp.NodeProcessed + + + +class ConnectionVcard: + def __init__(self): + self.vcard_sha = None + self.vcard_shas = {} # sha of contacts + self.room_jids = [] # list of gc jids so that vcard are saved in a folder + + def add_sha(self, p, send_caps = True): + ''' + c = p.setTag('x', namespace = common.xmpp.NS_VCARD_UPDATE) + if self.vcard_sha is not None: + c.setTagData('photo', self.vcard_sha) + if send_caps: + return self.add_caps(p) + return p + ''' + pass + + def add_caps(self, p): + ''' + # advertise our capabilities in presence stanza (jep-0115) + c = p.setTag('c', namespace = common.xmpp.NS_CAPS) + c.setAttr('node', 'http://gajim.org/caps') + c.setAttr('ext', 'ftrans') + c.setAttr('ver', gajim.version) + return p + ''' + pass + + def node_to_dict(self, node): + dict = {} + + for info in node.getChildren(): + name = info.getName() + if name in ('ADR', 'TEL', 'EMAIL'): # we can have several + if not dict.has_key(name): + dict[name] = [] + entry = {} + for c in info.getChildren(): + entry[c.getName()] = c.getData() + dict[name].append(entry) + elif info.getChildren() == []: + dict[name] = info.getData() + else: + dict[name] = {} + for c in info.getChildren(): + dict[name][c.getName()] = c.getData() + + return dict + + def save_vcard_to_hd(self, full_jid, card): + jid, nick = gajim.get_room_and_nick_from_fjid(full_jid) + puny_jid = helpers.sanitize_filename(jid) + path = os.path.join(gajim.VCARD_PATH, puny_jid) + if jid in self.room_jids or os.path.isdir(path): + # remove room_jid file if needed + if os.path.isfile(path): + os.remove(path) + # create folder if needed + if not os.path.isdir(path): + os.mkdir(path, 0700) + puny_nick = helpers.sanitize_filename(nick) + path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick) + else: + path_to_file = path + fil = open(path_to_file, 'w') + fil.write(str(card)) + fil.close() + + def get_cached_vcard(self, fjid, is_fake_jid = False): + '''return the vcard as a dict + return {} if vcard was too old + return None if we don't have cached vcard''' + jid, nick = gajim.get_room_and_nick_from_fjid(fjid) + puny_jid = helpers.sanitize_filename(jid) + if is_fake_jid: + puny_nick = helpers.sanitize_filename(nick) + path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick) + else: + path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid) + if not os.path.isfile(path_to_file): + return None + # We have the vcard cached + f = open(path_to_file) + c = f.read() + f.close() + card = common.xmpp.Node(node = c) + vcard = self.node_to_dict(card) + if vcard.has_key('PHOTO'): + if not isinstance(vcard['PHOTO'], dict): + del vcard['PHOTO'] + elif vcard['PHOTO'].has_key('SHA'): + cached_sha = vcard['PHOTO']['SHA'] + if self.vcard_shas.has_key(jid) and self.vcard_shas[jid] != \ + cached_sha: + # user change his vcard so don't use the cached one + return {} + vcard['jid'] = jid + vcard['resource'] = gajim.get_resource_from_jid(fjid) + return vcard + + def request_vcard(self, jid = None, is_fake_jid = False): + '''request the VCARD. If is_fake_jid is True, it means we request a vcard + to a fake jid, like in private messages in groupchat''' + if not self.connection: + return + ''' + iq = common.xmpp.Iq(typ = 'get') + if jid: + iq.setTo(jid) + iq.setTag(common.xmpp.NS_VCARD + ' vCard') + + id = self.connection.getAnID() + iq.setID(id) + self.awaiting_answers[id] = (VCARD_ARRIVED, jid) + if is_fake_jid: + room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) + if not room_jid in self.room_jids: + self.room_jids.append(room_jid) + self.connection.send(iq) + #('VCARD', {entry1: data, entry2: {entry21: data, ...}, ...}) + ''' + pass + + def send_vcard(self, vcard): + if not self.connection: + return + ''' + iq = common.xmpp.Iq(typ = 'set') + iq2 = iq.setTag(common.xmpp.NS_VCARD + ' vCard') + for i in vcard: + if i == 'jid': + continue + if isinstance(vcard[i], dict): + iq3 = iq2.addChild(i) + for j in vcard[i]: + iq3.addChild(j).setData(vcard[i][j]) + elif type(vcard[i]) == type([]): + for j in vcard[i]: + iq3 = iq2.addChild(i) + for k in j: + iq3.addChild(k).setData(j[k]) + else: + iq2.addChild(i).setData(vcard[i]) + + id = self.connection.getAnID() + iq.setID(id) + self.connection.send(iq) + + # Add the sha of the avatar + if vcard.has_key('PHOTO') and isinstance(vcard['PHOTO'], dict) and \ + vcard['PHOTO'].has_key('BINVAL'): + photo = vcard['PHOTO']['BINVAL'] + photo_decoded = base64.decodestring(photo) + our_jid = gajim.get_jid_from_account(self.name) + gajim.interface.save_avatar_files(our_jid, photo_decoded) + avatar_sha = sha.sha(photo_decoded).hexdigest() + iq2.getTag('PHOTO').setTagData('SHA', avatar_sha) + + self.awaiting_answers[id] = (VCARD_PUBLISHED, iq2) + ''' + pass + +class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream): + def __init__(self): + ConnectionVcard.__init__(self) + ConnectionBytestream.__init__(self) + # List of IDs we are waiting answers for {id: (type_of_request, data), } + self.awaiting_answers = {} + # List of IDs that will produce a timeout is answer doesn't arrive + # {time_of_the_timeout: (id, message to send to gui), } + self.awaiting_timeouts = {} + # keep the jids we auto added (transports contacts) to not send the + # SUBSCRIBED event to gui + self.automatically_added = [] + try: + idle.init() + except: + HAS_IDLE = False + + def _messageCB(self, ip, con, msg): + '''Called when we receive a message''' + msgtxt = msg.getBody() + msghtml = msg.getXHTML() + mtype = msg.getType() + subject = msg.getSubject() # if not there, it's None + tim = msg.getTimestamp() + tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') + tim = time.localtime(timegm(tim)) + frm = msg.getFrom() + if frm == None: + for key in self.connection.zeroconf.contacts: + if ip == self.connection.zeroconf.contacts[key][zeroconf.C_ADDRESS]: + frm = key + frm = unicode(frm) + jid = frm + no_log_for = gajim.config.get_per('accounts', self.name, + 'no_log_for').split() + encrypted = False + chatstate = None + encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED) + decmsg = '' + # invitations + invite = None + if not encTag: + invite = msg.getTag('x', namespace = common.xmpp.NS_MUC_USER) + if invite and not invite.getTag('invite'): + invite = None + delayed = msg.getTag('x', namespace = common.xmpp.NS_DELAY) != None + msg_id = None + composing_jep = None + xtags = msg.getTags('x') + # chatstates - look for chatstate tags in a message if not delayed + if not delayed: + composing_jep = False + children = msg.getChildren() + for child in children: + if child.getNamespace() == 'http://jabber.org/protocol/chatstates': + chatstate = child.getName() + composing_jep = 'JEP-0085' + break + # No JEP-0085 support, fallback to JEP-0022 + if not chatstate: + chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT) + if chatstate_child: + chatstate = 'active' + composing_jep = 'JEP-0022' + if not msgtxt and chatstate_child.getTag('composing'): + chatstate = 'composing' + # JEP-0172 User Nickname + user_nick = msg.getTagData('nick') + if not user_nick: + user_nick = '' + + if encTag and GnuPG.USE_GPG: + #decrypt + encmsg = encTag.getData() + + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + if keyID: + decmsg = self.gpg.decrypt(encmsg, keyID) + if decmsg: + msgtxt = decmsg + encrypted = True + if mtype == 'error': + error_msg = msg.getError() + if not error_msg: + error_msg = msgtxt + msgtxt = None + if self.name not in no_log_for: + gajim.logger.write('error', frm, error_msg, tim = tim, + subject = subject) + self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt, + tim)) + elif mtype == 'chat': # it's type 'chat' + if not msg.getTag('body') and chatstate is None: #no <body> + return + if msg.getTag('body') and self.name not in no_log_for and jid not in\ + no_log_for and msgtxt: + msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, + subject = subject) + self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject, + chatstate, msg_id, composing_jep, user_nick, msghtml)) + elif mtype == 'normal': # it's single message + if self.name not in no_log_for and jid not in no_log_for and msgtxt: + gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, + subject = subject) + if invite: + self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal', + subject, chatstate, msg_id, composing_jep, user_nick)) + # END messageCB + ''' + def build_http_auth_answer(self, iq_obj, answer): + if answer == 'yes': + iq = iq_obj.buildReply('result') + elif answer == 'no': + iq = iq_obj.buildReply('error') + iq.setError('not-authorized', 401) + self.connection.send(iq) + ''' + + def parse_data_form(self, node): + dic = {} + tag = node.getTag('title') + if tag: + dic['title'] = tag.getData() + tag = node.getTag('instructions') + if tag: + dic['instructions'] = tag.getData() + i = 0 + for child in node.getChildren(): + if child.getName() != 'field': + continue + var = child.getAttr('var') + ctype = child.getAttr('type') + label = child.getAttr('label') + if not var and ctype != 'fixed': # We must have var if type != fixed + continue + dic[i] = {} + if var: + dic[i]['var'] = var + if ctype: + dic[i]['type'] = ctype + if label: + dic[i]['label'] = label + tags = child.getTags('value') + if len(tags): + dic[i]['values'] = [] + for tag in tags: + data = tag.getData() + if ctype == 'boolean': + if data in ('yes', 'true', 'assent', '1'): + data = True + else: + data = False + dic[i]['values'].append(data) + tag = child.getTag('desc') + if tag: + dic[i]['desc'] = tag.getData() + option_tags = child.getTags('option') + if len(option_tags): + dic[i]['options'] = {} + j = 0 + for option_tag in option_tags: + dic[i]['options'][j] = {} + label = option_tag.getAttr('label') + tags = option_tag.getTags('value') + dic[i]['options'][j]['values'] = [] + for tag in tags: + dic[i]['options'][j]['values'].append(tag.getData()) + if not label: + label = dic[i]['options'][j]['values'][0] + dic[i]['options'][j]['label'] = label + j += 1 + if not dic[i].has_key('values'): + dic[i]['values'] = [dic[i]['options'][0]['values'][0]] + i += 1 + return dic + + def store_metacontacts(self, tags): + ''' fake empty method ''' + # serverside metacontacts are not supported with zeroconf + # (there is no server) + pass + def remove_transfers_for_contact(self, contact): + ''' stop all active transfer for contact ''' + '''for file_props in self.files_props.values(): + if self.is_transfer_stoped(file_props): + continue + receiver_jid = unicode(file_props['receiver']).split('/')[0] + if contact.jid == receiver_jid: + file_props['error'] = -5 + self.remove_transfer(file_props) + self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props)) + sender_jid = unicode(file_props['sender']).split('/')[0] + if contact.jid == sender_jid: + file_props['error'] = -3 + self.remove_transfer(file_props) + ''' + pass + + def remove_all_transfers(self): + ''' stops and removes all active connections from the socks5 pool ''' + ''' + for file_props in self.files_props.values(): + self.remove_transfer(file_props, remove_from_list = False) + del(self.files_props) + self.files_props = {} + ''' + pass + + def remove_transfer(self, file_props, remove_from_list = True): + ''' + if file_props is None: + return + self.disconnect_transfer(file_props) + sid = file_props['sid'] + gajim.socks5queue.remove_file_props(self.name, sid) + + if remove_from_list: + if self.files_props.has_key('sid'): + del(self.files_props['sid']) + ''' + pass + diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py new file mode 100644 index 0000000000000000000000000000000000000000..b5dfd5090e71d68a96195522465a8bbe1d50e167 --- /dev/null +++ b/src/common/zeroconf/connection_zeroconf.py @@ -0,0 +1,500 @@ +## common/zeroconf/connection_zeroconf.py +## +## Contributors for this file: +## - Yann Le Boulanger <asterix@lagaule.org> +## - Nikos Kouremenos <nkour@jabber.org> +## - Dimitur Kirov <dkirov@gmail.com> +## - Travis Shirk <travis@pobox.com> +## - Stefan Bethge <stefan@lanpartei.de> +## +## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> +## Vincent Hanquez <tab@snarc.org> +## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org> +## Vincent Hanquez <tab@snarc.org> +## Nikos Kouremenos <nkour@jabber.org> +## Dimitur Kirov <dkirov@gmail.com> +## Travis Shirk <travis@pobox.com> +## Norman Rasmussen <norman@rasmussen.co.za> +## Stefan Bethge <stefan@lanpartei.de> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +import os +import random +random.seed() + +import signal +if os.name != 'nt': + signal.signal(signal.SIGPIPE, signal.SIG_DFL) +import getpass +import gobject +import notify + +from common import helpers +from common import gajim +from common import GnuPG +from common.zeroconf import connection_handlers_zeroconf +from common.zeroconf import client_zeroconf +from connection_handlers_zeroconf import * + +USE_GPG = GnuPG.USE_GPG + +class ConnectionZeroconf(ConnectionHandlersZeroconf): + '''Connection class''' + def __init__(self, name): + ConnectionHandlersZeroconf.__init__(self) + # system username + self.username = None + self.name = name + self.connected = 0 # offline + self.connection = None + self.gpg = None + self.is_zeroconf = True + self.privacy_rules_supported = False + self.status = '' + self.old_show = '' + self.priority = 0 + + self.call_resolve_timeout = False + + #self.time_to_reconnect = None + #self.new_account_info = None + self.bookmarks = [] + + #we don't need a password, but must be non-empty + self.password = 'zeroconf' + + self.autoconnect = False + self.sync_with_global_status = True + self.no_log_for = False + + # Do we continue connection when we get roster (send presence,get vcard...) + self.continue_connect_info = None + if USE_GPG: + self.gpg = GnuPG.GnuPG() + gajim.config.set('usegpg', True) + else: + gajim.config.set('usegpg', False) + + self.get_config_values_or_default() + + self.muc_jid = {} # jid of muc server for each transport type + self.vcard_supported = False + + def get_config_values_or_default(self): + ''' get name, host, port from config, or + create zeroconf account with default values''' + + if not gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name'): + gajim.log.debug('Creating zeroconf account') + gajim.config.add_per('accounts', gajim.ZEROCONF_ACC_NAME) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for', '') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'password', 'zeroconf') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status', True) + + #XXX make sure host is US-ASCII + self.host = unicode(socket.gethostname()) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', self.host) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port', 5298) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'is_zeroconf', True) + self.host = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname') + self.port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') + self.autoconnect = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect') + self.sync_with_global_status = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status') + self.no_log_for = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for') + self.first = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') + self.last = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') + self.jabber_id = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') + self.email = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + + if not self.username: + self.username = unicode(getpass.getuser()) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name', self.username) + else: + self.username = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name') + # END __init__ + + def dispatch(self, event, data): + if gajim.handlers.has_key(event): + gajim.handlers[event](self.name, data) + + def _reconnect(self): + gajim.log.debug('reconnect') + + signed = self.get_signed_msg(self.status) + self.reconnect() + + def quit(self, kill_core): + if kill_core and self.connected > 1: + self.disconnect() + + def disable_account(self): + self.disconnect() + + def test_gpg_passphrase(self, password): + self.gpg.passphrase = password + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + signed = self.gpg.sign('test', keyID) + self.gpg.password = None + return signed != 'BAD_PASSPHRASE' + + def get_signed_msg(self, msg): + signed = '' + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + if keyID and USE_GPG: + use_gpg_agent = gajim.config.get('use_gpg_agent') + if self.connected < 2 and self.gpg.passphrase is None and \ + not use_gpg_agent: + # We didn't set a passphrase + self.dispatch('ERROR', (_('OpenPGP passphrase was not given'), + #%s is the account name here + _('You will be connected to %s without OpenPGP.') % self.name)) + elif self.gpg.passphrase is not None or use_gpg_agent: + signed = self.gpg.sign(msg, keyID) + if signed == 'BAD_PASSPHRASE': + signed = '' + if self.connected < 2: + self.dispatch('BAD_PASSPHRASE', ()) + return signed + + def _on_resolve_timeout(self): + if self.connected: + self.connection.resolve_all() + diffs = self.roster.getDiffs() + for key in diffs: + self.roster.setItem(key) + self.dispatch('ROSTER_INFO', (key, self.roster.getName(key), + 'both', 'no', self.roster.getGroups(key))) + self.dispatch('NOTIFY', (key, self.roster.getStatus(key), + self.roster.getMessage(key), 'local', 0, None, 0)) + #XXX open chat windows don't get refreshed (full name), add that + return self.call_resolve_timeout + + # callbacks called from zeroconf + def _on_new_service(self,jid): + self.roster.setItem(jid) + self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid))) + self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0)) + + def _on_remove_service(self, jid): + self.roster.delItem(jid) + # 'NOTIFY' (account, (jid, status, status message, resource, priority, + # keyID, timestamp)) + self.dispatch('NOTIFY', (jid, 'offline', '', 'local', 0, None, 0)) + + def _on_disconnected(self): + self.disconnect() + self.dispatch('STATUS', 'offline') + self.dispatch('CONNECTION_LOST', + (_('Connection with account "%s" has been lost') % self.name, + _('To continue sending and receiving messages, you will need to reconnect.'))) + self.status = 'offline' + self.disconnect() + + def _on_name_conflictCB(self, alt_name): + self.disconnect() + self.dispatch('STATUS', 'offline') + self.dispatch('ZC_NAME_CONFLICT', alt_name) + + def _on_error(self, message): + self.dispatch('ERROR', (_('Avahi error'), _("%s\nLink-local messaging might not work properly.") % message)) + + def connect(self, show = 'online', msg = ''): + self.get_config_values_or_default() + if not self.connection: + self.connection = client_zeroconf.ClientZeroconf(self) + if not self.connection.test_avahi(): + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not connect to "%s"') % self.name, + _('Please check if Avahi is installed.'))) + self.disconnect() + return + result = self.connection.connect(show, msg) + if not result: + self.dispatch('STATUS', 'offline') + self.status = 'offline' + if result is False: + self.dispatch('CONNECTION_LOST', + (_('Could not start local service'), + _('Unable to bind to port %d.' % self.port))) + else: # result is None + self.dispatch('CONNECTION_LOST', + (_('Could not start local service'), + _('Please check if avahi-daemon is running.'))) + self.disconnect() + return + else: + self.connection.announce() + self.roster = self.connection.getRoster() + self.dispatch('ROSTER', self.roster) + + #display contacts already detected and resolved + for jid in self.roster.keys(): + self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid))) + self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0)) + + self.connected = STATUS_LIST.index(show) + + # refresh all contacts data every five seconds + self.call_resolve_timeout = True + gobject.timeout_add(5000, self._on_resolve_timeout) + return True + + def disconnect(self, on_purpose = False): + self.connected = 0 + self.time_to_reconnect = None + if self.connection: + self.connection.disconnect() + self.connection = None + # stop calling the timeout + self.call_resolve_timeout = False + + def reannounce(self): + if self.connected: + txt = {} + txt['1st'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') + txt['last'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') + txt['jid'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') + txt['email'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + self.connection.reannounce(txt) + + def update_details(self): + if self.connection: + port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') + if port != self.port: + self.port = port + last_msg = self.connection.last_msg + self.disconnect() + if not self.connect(self.status, last_msg): + return + if self.status != 'invisible': + self.connection.announce() + else: + self.reannounce() + + def change_status(self, show, msg, sync = False, auto = False): + if not show in STATUS_LIST: + return -1 + self.status = show + + check = True #to check for errors from zeroconf + # 'connect' + if show != 'offline' and not self.connected: + if not self.connect(show, msg): + return + if show != 'invisible': + check = self.connection.announce() + else: + self.connected = STATUS_LIST.index(show) + + # 'disconnect' + elif show == 'offline' and self.connected: + self.disconnect() + + # update status + elif show != 'offline' and self.connected: + was_invisible = self.connected == STATUS_LIST.index('invisible') + self.connected = STATUS_LIST.index(show) + if show == 'invisible': + check = check and self.connection.remove_announce() + elif was_invisible: + if not self.connected: + check = check and self.connect(show, msg) + check = check and self.connection.announce() + if self.connection and not show == 'invisible': + check = check and self.connection.set_show_msg(show, msg) + + #stay offline when zeroconf does something wrong + if check: + self.dispatch('STATUS', show) + else: + # show notification that avahi or system bus is down + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not change status of account "%s"') % self.name, + _('Please check if avahi-daemon is running.'))) + + def get_status(self): + return STATUS_LIST[self.connected] + + def send_message(self, jid, msg, keyID, type = 'chat', subject='', + chatstate = None, msg_id = None, composing_jep = None, resource = None, + user_nick = None): + fjid = jid + + if not self.connection: + return + if not msg and chatstate is None: + return + + if self.status in ('invisible', 'offline'): + self.dispatch('MSGERROR', [unicode(jid), '-1', _('You are not connected or not visible to others. Your message could not be sent.'), None, None]) + return + + msgtxt = msg + msgenc = '' + if keyID and USE_GPG: + #encrypt + msgenc = self.gpg.encrypt(msg, [keyID]) + if msgenc: + msgtxt = '[This message is encrypted]' + lang = os.getenv('LANG') + if lang is not None or lang != 'en': # we're not english + msgtxt = _('[This message is encrypted]') +\ + ' ([This message is encrypted])' # one in locale and one en + + + if type == 'chat': + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type) + + else: + if subject: + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, + typ = 'normal', subject = subject) + else: + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, + typ = 'normal') + + if msgenc: + msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) + + # chatstates - if peer supports jep85 or jep22, send chatstates + # please note that the only valid tag inside a message containing a <body> + # tag is the active event + if chatstate is not None: + if composing_jep == 'JEP-0085' or not composing_jep: + # JEP-0085 + msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES) + if composing_jep == 'JEP-0022' or not composing_jep: + # JEP-0022 + chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT) + if not msgtxt: # when no <body>, add <id> + if not msg_id: # avoid putting 'None' in <id> tag + msg_id = '' + chatstate_node.setTagData('id', msg_id) + # when msgtxt, requests JEP-0022 composing notification + if chatstate is 'composing' or msgtxt: + chatstate_node.addChild(name = 'composing') + + if not self.connection.send(msg_iq, msg != None): + return + + no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') + ji = gajim.get_jid_without_resource(jid) + if self.name not in no_log_for and ji not in no_log_for: + log_msg = msg + if subject: + log_msg = _('Subject: %s\n%s') % (subject, msg) + if log_msg: + if type == 'chat': + kind = 'chat_msg_sent' + else: + kind = 'single_msg_sent' + gajim.logger.write(kind, jid, log_msg) + + self.dispatch('MSGSENT', (jid, msg, keyID)) + + def send_stanza(self, stanza): + # send a stanza untouched + if not self.connection: + return + self.connection.send(stanza) + + def ack_subscribed(self, jid): + gajim.log.debug('This should not happen (ack_subscribed)') + + def ack_unsubscribed(self, jid): + gajim.log.debug('This should not happen (ack_unsubscribed)') + + def request_subscription(self, jid, msg = '', name = '', groups = [], + auto_auth = False): + gajim.log.debug('This should not happen (request_subscription)') + + def send_authorization(self, jid): + gajim.log.debug('This should not happen (send_authorization)') + + def refuse_authorization(self, jid): + gajim.log.debug('This should not happen (refuse_authorization)') + + def unsubscribe(self, jid, remove_auth = True): + gajim.log.debug('This should not happen (unsubscribe)') + + def unsubscribe_agent(self, agent): + gajim.log.debug('This should not happen (unsubscribe_agent)') + + def update_contact(self, jid, name, groups): + if self.connection: + self.connection.getRoster().setItem(jid = jid, name = name, + groups = groups) + + def new_account(self, name, config, sync = False): + gajim.log.debug('This should not happen (new_account)') + + def _on_new_account(self, con = None, con_type = None): + gajim.log.debug('This should not happen (_on_new_account)') + + def account_changed(self, new_name): + self.name = new_name + + def request_last_status_time(self, jid, resource): + gajim.log.debug('This should not happen (request_last_status_time)') + + def request_os_info(self, jid, resource): + gajim.log.debug('This should not happen (request_os_info)') + + def get_settings(self): + gajim.log.debug('This should not happen (get_settings)') + + def get_bookmarks(self): + gajim.log.debug('This should not happen (get_bookmarks)') + + def store_bookmarks(self): + gajim.log.debug('This should not happen (store_bookmarks)') + + def get_metacontacts(self): + gajim.log.debug('This should not happen (get_metacontacts)') + + def send_agent_status(self, agent, ptype): + gajim.log.debug('This should not happen (send_agent_status)') + + def gpg_passphrase(self, passphrase): + if USE_GPG: + use_gpg_agent = gajim.config.get('use_gpg_agent') + if use_gpg_agent: + self.gpg.passphrase = None + else: + self.gpg.passphrase = passphrase + + def ask_gpg_keys(self): + if USE_GPG: + keys = self.gpg.get_keys() + return keys + return None + + def ask_gpg_secrete_keys(self): + if USE_GPG: + keys = self.gpg.get_secret_keys() + return keys + return None + + def _event_dispatcher(self, realm, event, data): + if realm == '': + if event == common.xmpp.transports.DATA_RECEIVED: + self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) + elif event == common.xmpp.transports.DATA_SENT: + self.dispatch('STANZA_SENT', unicode(data)) + +# END ConnectionZeroconf diff --git a/src/common/zeroconf/roster_zeroconf.py b/src/common/zeroconf/roster_zeroconf.py new file mode 100644 index 0000000000000000000000000000000000000000..269d3dbb2355e156b525143a68da0126c5142de4 --- /dev/null +++ b/src/common/zeroconf/roster_zeroconf.py @@ -0,0 +1,156 @@ +## common/zeroconf/roster_zeroconf.py +## +## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from common.zeroconf import zeroconf + +class Roster: + def __init__(self, zeroconf): + self._data = None + self.zeroconf = zeroconf # our zeroconf instance + + def update_roster(self): + for val in self.zeroconf.contacts.values(): + self.setItem(val[zeroconf.C_NAME]) + + def getRoster(self): + #print 'roster_zeroconf.py: getRoster' + if self._data is None: + self._data = {} + self.update_roster() + return self + + def getDiffs(self): + ''' update the roster with new data and return dict with + jid -> new status pairs to do notifications and stuff ''' + + diffs = {} + old_data = self._data.copy() + self.update_roster() + for key in old_data.keys(): + if self._data.has_key(key): + if old_data[key] != self._data[key]: + diffs[key] = self._data[key]['status'] + #print 'roster_zeroconf.py: diffs:' + str(diffs) + return diffs + + def setItem(self, jid, name = '', groups = ''): + #print 'roster_zeroconf.py: setItem %s' % jid + contact = self.zeroconf.get_contact(jid) + if not contact: + return + + (service_jid, domain, interface, protocol, host, address, port, bare_jid, txt) \ + = contact + + self._data[jid]={} + self._data[jid]['ask'] = 'no' #? + self._data[jid]['subscription'] = 'both' + self._data[jid]['groups'] = [] + self._data[jid]['resources'] = {} + self._data[jid]['address'] = address + self._data[jid]['host'] = host + self._data[jid]['port'] = port + txt_dict = self.zeroconf.txt_array_to_dict(txt) + if txt_dict.has_key('status'): + status = txt_dict['status'] + else: + status = '' + nm = '' + if txt_dict.has_key('1st'): + nm = txt_dict['1st'] + if txt_dict.has_key('last'): + if nm != '': + nm += ' ' + nm += txt_dict['last'] + if nm: + self._data[jid]['name'] = nm + else: + self._data[jid]['name'] = jid + if status == 'avail': + status = 'online' + self._data[jid]['txt_dict'] = txt_dict + if not self._data[jid]['txt_dict'].has_key('msg'): + self._data[jid]['txt_dict']['msg'] = '' + self._data[jid]['status'] = status + self._data[jid]['show'] = status + + def delItem(self, jid): + #print 'roster_zeroconf.py: delItem %s' % jid + if self._data.has_key(jid): + del self._data[jid] + + def getItem(self, jid): + #print 'roster_zeroconf.py: getItem: %s' % jid + if self._data.has_key(jid): + return self._data[jid] + + def __getitem__(self,jid): + #print 'roster_zeroconf.py: __getitem__' + return self._data[jid] + + def getItems(self): + #print 'roster_zeroconf.py: getItems' + # Return list of all [bare] JIDs that the roster currently tracks. + return self._data.keys() + + def keys(self): + #print 'roster_zeroconf.py: keys' + return self._data.keys() + + def getRaw(self): + #print 'roster_zeroconf.py: getRaw' + return self._data + + def getResources(self, jid): + #print 'roster_zeroconf.py: getResources(%s)' % jid + return {} + + def getGroups(self, jid): + return self._data[jid]['groups'] + + def getName(self, jid): + if self._data.has_key(jid): + return self._data[jid]['name'] + + def getStatus(self, jid): + if self._data.has_key(jid): + return self._data[jid]['status'] + + def getMessage(self, jid): + if self._data.has_key(jid): + return self._data[jid]['txt_dict']['msg'] + + def getShow(self, jid): + #print 'roster_zeroconf.py: getShow' + return getStatus(jid) + + def getPriority(jid): + return 5 + + def getSubscription(self,jid): + #print 'roster_zeroconf.py: getSubscription' + return 'both' + + def Subscribe(self,jid): + pass + + def Unsubscribe(self,jid): + pass + + def Authorize(self,jid): + pass + + def Unauthorize(self,jid): + pass diff --git a/src/common/zeroconf/zeroconf.py b/src/common/zeroconf/zeroconf.py new file mode 100755 index 0000000000000000000000000000000000000000..fbaadfd1d83fc4487454f9708a625444a46f1eca --- /dev/null +++ b/src/common/zeroconf/zeroconf.py @@ -0,0 +1,421 @@ +## common/zeroconf/zeroconf.py +## +## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +import os +import sys +import socket +from common import gajim +from common import xmpp + +try: + import dbus.glib +except ImportError, e: + pass + + +C_NAME, C_DOMAIN, C_INTERFACE, C_PROTOCOL, C_HOST, \ +C_ADDRESS, C_PORT, C_BARE_NAME, C_TXT = range(9) + +class Zeroconf: + def __init__(self, new_serviceCB, remove_serviceCB, name_conflictCB, + disconnected_CB, error_CB, name, host, port): + self.avahi = None + self.domain = None # specific domain to browse + self.stype = '_presence._tcp' + self.port = port # listening port that gets announced + self.username = name + self.host = host + self.txt = {} # service data + + #XXX these CBs should be set to None when we destroy the object + # (go offline), because they create a circular reference + self.new_serviceCB = new_serviceCB + self.remove_serviceCB = remove_serviceCB + self.name_conflictCB = name_conflictCB + self.disconnected_CB = disconnected_CB + self.error_CB = error_CB + + self.service_browser = None + self.domain_browser = None + self.bus = None + self.server = None + self.contacts = {} # all current local contacts with data + self.entrygroup = None + self.connected = False + self.announced = False + self.invalid_self_contact = {} + + + ## handlers for dbus callbacks + def entrygroup_commit_error_CB(self, err): + # left blank for possible later usage + pass + + def error_callback1(self, err): + gajim.log.debug('Error while resolving: ' + str(err)) + + def error_callback(self, err): + gajim.log.debug(str(err)) + # timeouts are non-critical + if str(err) != 'Timeout reached': + self.disconnect() + self.disconnected_CB() + + def new_service_callback(self, interface, protocol, name, stype, domain, flags): + gajim.log.debug('Found service %s in domain %s on %i.%i.' % (name, domain, interface, protocol)) + if not self.connected: + return + + # synchronous resolving + self.server.ResolveService( int(interface), int(protocol), name, stype, \ + domain, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), \ + reply_handler=self.service_resolved_callback, error_handler=self.error_callback1) + + def remove_service_callback(self, interface, protocol, name, stype, domain, flags): + gajim.log.debug('Service %s in domain %s on %i.%i disappeared.' % (name, domain, interface, protocol)) + if not self.connected: + return + if name != self.name: + for key in self.contacts.keys(): + if self.contacts[key][C_BARE_NAME] == name: + del self.contacts[key] + self.remove_serviceCB(key) + return + + def new_service_type(self, interface, protocol, stype, domain, flags): + # Are we already browsing this domain for this type? + if self.service_browser: + return + + object_path = self.server.ServiceBrowserNew(interface, protocol, \ + stype, domain, dbus.UInt32(0)) + + self.service_browser = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, \ + object_path) , self.avahi.DBUS_INTERFACE_SERVICE_BROWSER) + self.service_browser.connect_to_signal('ItemNew', self.new_service_callback) + self.service_browser.connect_to_signal('ItemRemove', self.remove_service_callback) + self.service_browser.connect_to_signal('Failure', self.error_callback) + + def new_domain_callback(self,interface, protocol, domain, flags): + if domain != "local": + self.browse_domain(interface, protocol, domain) + + def txt_array_to_dict(self, txt_array): + txt_dict = {} + for els in txt_array: + key, val = '', None + for c in els: + #FIXME: remove when outdated, this is for avahi < 0.6.14 + if c < 0 or c > 255: + c = '.' + else: + c = chr(c) + if val is None: + if c == '=': + val = '' + else: + key += c + else: + val += c + if val is None: # missing '=' + val = '' + txt_dict[key] = val.decode('utf-8') + return txt_dict + + def service_resolved_callback(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + gajim.log.debug('Service data for service %s in domain %s on %i.%i:' + % (name, domain, interface, protocol)) + gajim.log.debug('Host %s (%s), port %i, TXT data: %s' % (host, address, port, + self.txt_array_to_dict(txt))) + if not self.connected: + return + bare_name = name + if name.find('@') == -1: + name = name + '@' + name + + # we don't want to see ourselves in the list + if name != self.name: + self.contacts[name] = (name, domain, interface, protocol, host, address, port, + bare_name, txt) + self.new_serviceCB(name) + else: + # remember data + # In case this is not our own record but of another + # gajim instance on the same machine, + # it will be used when we get a new name. + self.invalid_self_contact[name] = (name, domain, interface, protocol, host, address, port, bare_name, txt) + + + # different handler when resolving all contacts + def service_resolved_all_callback(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + if not self.connected: + return + bare_name = name + if name.find('@') == -1: + name = name + '@' + name + self.contacts[name] = (name, domain, interface, protocol, host, address, port, bare_name, txt) + + def service_added_callback(self): + gajim.log.debug('Service successfully added') + + def service_committed_callback(self): + gajim.log.debug('Service successfully committed') + + def service_updated_callback(self): + gajim.log.debug('Service successfully updated') + + def service_add_fail_callback(self, err): + gajim.log.debug('Error while adding service. %s' % str(err)) + if str(err) == 'Local name collision': + alternative_name = self.server.GetAlternativeServiceName(self.username) + self.name_conflictCB(alternative_name) + return + self.error_CB(_('Error while adding service. %s') % str(err)) + self.disconnect() + + def server_state_changed_callback(self, state, error): + if state == self.avahi.SERVER_RUNNING: + self.create_service() + elif state == self.avahi.SERVER_COLLISION: + self.entrygroup.Reset() + elif state == self.avahi.CLIENT_FAILURE: + # does it ever go here? + gajim.log.debug('CLIENT FAILURE') + + def entrygroup_state_changed_callback(self, state, error): + # the name is already present, so recreate + if state == self.avahi.ENTRY_GROUP_COLLISION: + self.service_add_fail_callback('Local name collision') + elif state == self.avahi.ENTRY_GROUP_FAILURE: + gajim.log.debug('zeroconf.py: ENTRY_GROUP_FAILURE reached(that' + ' should not happen)') + + # make zeroconf-valid names + def replace_show(self, show): + if show in ['chat', 'online', '']: + return 'avail' + elif show == 'xa': + return 'away' + return show + + def avahi_txt(self): + utf8_dict = {} + for key in self.txt: + val = self.txt[key] + if isinstance(val, unicode): + utf8_dict[key] = val.encode('utf-8') + else: + utf8_dict[key] = val + return self.avahi.dict_to_txt_array(utf8_dict) + + def create_service(self): + try: + if not self.entrygroup: + # create an EntryGroup for publishing + self.entrygroup = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, self.server.EntryGroupNew()), self.avahi.DBUS_INTERFACE_ENTRY_GROUP) + self.entrygroup.connect_to_signal('StateChanged', self.entrygroup_state_changed_callback) + + txt = {} + + #remove empty keys + for key,val in self.txt.iteritems(): + if val: + txt[key] = val + + txt['port.p2pj'] = self.port + txt['version'] = 1 + txt['txtvers'] = 1 + + # replace gajim's show messages with compatible ones + if self.txt.has_key('status'): + txt['status'] = self.replace_show(self.txt['status']) + else: + txt['status'] = 'avail' + + self.txt = txt + gajim.log.debug('Publishing service %s of type %s' % (self.name, self.stype)) + self.entrygroup.AddService(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype, '', '', self.port, self.avahi_txt(), reply_handler=self.service_added_callback, error_handler=self.service_add_fail_callback) + + self.entrygroup.Commit(reply_handler=self.service_committed_callback, + error_handler=self.entrygroup_commit_error_CB) + + return True + + except dbus.dbus_bindings.DBusException, e: + gajim.log.debug(str(e)) + return False + + def announce(self): + if not self.connected: + return False + + state = self.server.GetState() + if state == self.avahi.SERVER_RUNNING: + self.create_service() + self.announced = True + return True + + def remove_announce(self): + if self.announced == False: + return False + try: + if self.entrygroup.GetState() != self.avahi.ENTRY_GROUP_FAILURE: + self.entrygroup.Reset() + self.entrygroup.Free() + # .Free() has mem leaks + obj = self.entrygroup._obj + obj._bus = None + self.entrygroup._obj = None + self.entrygroup = None + self.announced = False + + return True + else: + return False + except dbus.dbus_bindings.DBusException, e: + gajim.log.debug("Can't remove service. That should not happen") + + def browse_domain(self, interface, protocol, domain): + self.new_service_type(interface, protocol, self.stype, domain, '') + + def avahi_dbus_connect_cb(self, a, connect, disconnect): + if connect != "": + gajim.log.debug('Lost connection to avahi-daemon') + self.disconnect() + if self.disconnected_CB: + self.disconnected_CB() + else: + gajim.log.debug('We are connected to avahi-daemon') + + # connect to dbus + def connect_dbus(self): + try: + import dbus + except ImportError: + gajim.log.debug('Error: python-dbus needs to be installed. No zeroconf support.') + return False + if self.bus: + return True + try: + self.bus = dbus.SystemBus() + self.bus.add_signal_receiver(self.avahi_dbus_connect_cb, + "NameOwnerChanged", "org.freedesktop.DBus", + arg0="org.freedesktop.Avahi") + except Exception, e: + # System bus is not present + self.bus = None + gajim.log.debug(str(e)) + return False + else: + return True + + # connect to avahi + def connect_avahi(self): + if not self.connect_dbus(): + return False + try: + import avahi + self.avahi = avahi + except ImportError: + gajim.log.debug('Error: python-avahi needs to be installed. No zeroconf support.') + return False + + if self.server: + return True + try: + self.server = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, \ + self.avahi.DBUS_PATH_SERVER), self.avahi.DBUS_INTERFACE_SERVER) + self.server.connect_to_signal('StateChanged', + self.server_state_changed_callback) + except Exception, e: + # Avahi service is not present + self.server = None + gajim.log.debug(str(e)) + return False + else: + return True + + def connect(self): + self.name = self.username + '@' + self.host # service name + if not self.connect_avahi(): + return False + + self.connected = True + # start browsing + if self.domain is None: + # Explicitly browse .local + self.browse_domain(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, "local") + + # Browse for other browsable domains + self.domain_browser = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, \ + self.server.DomainBrowserNew(self.avahi.IF_UNSPEC, \ + self.avahi.PROTO_UNSPEC, '', self.avahi.DOMAIN_BROWSER_BROWSE,\ + dbus.UInt32(0))), self.avahi.DBUS_INTERFACE_DOMAIN_BROWSER) + self.domain_browser.connect_to_signal('ItemNew', self.new_domain_callback) + self.domain_browser.connect_to_signal('Failure', self.error_callback) + else: + self.browse_domain(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, domain) + + return True + + def disconnect(self): + if self.connected: + self.connected = False + if self.service_browser: + self.service_browser.Free() + self.service_browser._obj._bus = None + self.service_browser._obj = None + if self.domain_browser: + self.domain_browser.Free() + self.domain_browser._obj._bus = None + self.domain_browser._obj = None + self.remove_announce() + self.server._obj._bus = None + self.server._obj = None + self.server = None + self.service_browser = None + self.domain_browser = None + + # refresh txt data of all contacts manually (no callback available) + def resolve_all(self): + if not self.connected: + return + for val in self.contacts.values(): + self.server.ResolveService(int(val[C_INTERFACE]), int(val[C_PROTOCOL]), val[C_BARE_NAME], \ + self.stype, val[C_DOMAIN], self.avahi.PROTO_UNSPEC, dbus.UInt32(0),\ + reply_handler=self.service_resolved_all_callback, error_handler=self.error_callback) + + def get_contacts(self): + if not jid in self.contacts: + return None + return self.contacts + + def get_contact(self, jid): + if not jid in self.contacts: + return None + return self.contacts[jid] + + def update_txt(self, show = None): + if show: + self.txt['status'] = self.replace_show(show) + + txt = self.avahi_txt() + if self.connected and self.entrygroup: + self.entrygroup.UpdateServiceTxt(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype,'', txt, reply_handler=self.service_updated_callback, error_handler=self.error_callback) + return True + else: + return False + + +# END Zeroconf diff --git a/src/config.py b/src/config.py index dd8e7bce3871c2be7f419c244c73d9c84433eb5d..080858aa3f1babf1386cee4c56c0f8dd95a0fdbf 100644 --- a/src/config.py +++ b/src/config.py @@ -1,9 +1,10 @@ ## config.py ## ## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> -## Copyright (C) 2005-2006 Nikos Kouremenos <nkour@jabber.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com> ## Copyright (C) 2003-2005 Vincent Hanquez <tab@snarc.org> +## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -23,7 +24,6 @@ import common.sleepy import gtkgui_helpers import dialogs -import vcard import cell_renderer_image import message_control import chat_control @@ -37,6 +37,11 @@ except: from common import helpers from common import gajim from common import connection +from common import passwords +from common import zeroconf +from common import dbus_support + +from common.exceptions import GajimGeneralException #---------- PreferencesWindow class -------------# class PreferencesWindow: @@ -116,6 +121,9 @@ class PreferencesWindow: emoticons_combobox.set_model(model) l = [] for dir in emoticons_list: + if not os.path.isdir(os.path.join(gajim.DATA_DIR, 'emoticons', dir)) \ + and not os.path.isdir(os.path.join(gajim.MY_EMOTS_PATH, dir)) : + continue if dir != '.svn': l.append(dir) l.append(_('Disabled')) @@ -140,6 +148,8 @@ class PreferencesWindow: self.iconset_combobox.set_model(model) l = [] for dir in iconsets_list: + if not os.path.isdir(os.path.join(gajim.DATA_DIR, 'iconsets', dir)): + continue if dir != '.svn' and dir != 'transports': l.append(dir) if l.count == 0: @@ -147,8 +157,10 @@ class PreferencesWindow: for i in xrange(len(l)): preview = gtk.Image() files = [] - files.append(os.path.join(gajim.DATA_DIR, 'iconsets', l[i], '16x16', 'online.png')) - files.append(os.path.join(gajim.DATA_DIR, 'iconsets', l[i], '16x16', 'online.gif')) + files.append(os.path.join(gajim.DATA_DIR, 'iconsets', l[i], '16x16', + 'online.png')) + files.append(os.path.join(gajim.DATA_DIR, 'iconsets', l[i], '16x16', + 'online.gif')) for file in files: if os.path.exists(file): preview.set_from_file(file) @@ -181,11 +193,10 @@ class PreferencesWindow: theme = config_theme.replace('_', ' ') model.append([theme]) if gajim.config.get('roster_theme') == config_theme: - theme_combobox.set_active(i) + theme_combobox.set_active(i) i += 1 - self.on_theme_combobox_changed(theme_combobox) - #use speller + # use speller if os.name == 'nt': self.xml.get_widget('speller_checkbutton').set_no_show_all(True) else: @@ -195,7 +206,11 @@ class PreferencesWindow: else: self.xml.get_widget('speller_checkbutton').set_sensitive(False) - #Print time + # Ignore XHTML + st = gajim.config.get('ignore_incoming_xhtml') + self.xml.get_widget('xhtml_checkbutton').set_active(st) + + # Print time st = gajim.config.get('print_ichat_every_foo_minutes') text = _('Every %s _minutes') % st self.xml.get_widget('time_sometimes_radiobutton').set_label(text) @@ -211,19 +226,23 @@ class PreferencesWindow: #before time st = gajim.config.get('before_time') - self.xml.get_widget('before_time_entry').set_text(st) + st = helpers.from_one_line(st) + self.xml.get_widget('before_time_textview').get_buffer().set_text(st) #after time st = gajim.config.get('after_time') - self.xml.get_widget('after_time_entry').set_text(st) + st = helpers.from_one_line(st) + self.xml.get_widget('after_time_textview').get_buffer().set_text(st) #before nickname st = gajim.config.get('before_nickname') - self.xml.get_widget('before_nickname_entry').set_text(st) + st = helpers.from_one_line(st) + self.xml.get_widget('before_nickname_textview').get_buffer().set_text(st) #after nickanme st = gajim.config.get('after_nickname') - self.xml.get_widget('after_nickname_entry').set_text(st) + st = helpers.from_one_line(st) + self.xml.get_widget('after_nickname_textview').get_buffer().set_text(st) #Color for incomming messages colSt = gajim.config.get('inmsgcolor') @@ -294,7 +313,8 @@ class PreferencesWindow: combo.set_active(2) #sounds - if os.name == 'nt': # if windows, player must not become visible on show_all + if os.name == 'nt': + # if windows, player must not become visible on show_all soundplayer_hbox = self.xml.get_widget('soundplayer_hbox') soundplayer_hbox.set_no_show_all(True) if gajim.config.get('sounds_on'): @@ -429,15 +449,16 @@ class PreferencesWindow: self.xml.get_widget('custom_apps_frame').set_no_show_all(True) if gajim.config.get('autodetect_browser_mailer'): self.applications_combobox.set_active(0) - gtkgui_helpers.autodetect_browser_mailer() - # autodetect_browser_mailer is now False. - # so user has 'Always Use GNOME/KDE' or Custom + # else autodetect_browser_mailer is False. + # so user has 'Always Use GNOME/KDE/XFCE4' or Custom elif gajim.config.get('openwith') == 'gnome-open': self.applications_combobox.set_active(1) elif gajim.config.get('openwith') == 'kfmclient exec': self.applications_combobox.set_active(2) + elif gajim.config.get('openwith') == 'exo-open': + self.applications_combobox.set_active(3) elif gajim.config.get('openwith') == 'custom': - self.applications_combobox.set_active(3) + self.applications_combobox.set_active(4) self.xml.get_widget('custom_apps_frame').show() self.xml.get_widget('custom_browser_entry').set_text( @@ -454,12 +475,25 @@ class PreferencesWindow: # send os info st = gajim.config.get('send_os_info') self.xml.get_widget('send_os_info_checkbutton').set_active(st) + + # set status msg from currently playing music track + widget = self.xml.get_widget( + 'set_status_msg_from_current_music_track_checkbutton') + if os.name == 'nt': + widget.set_no_show_all(True) + widget.hide() + elif dbus_support.supported: + st = gajim.config.get('set_status_msg_from_current_music_track') + widget.set_active(st) + else: + widget.set_sensitive(False) # Notify user of new gmail e-mail messages, # only show checkbox if user has a gtalk account frame_gmail = self.xml.get_widget('frame_gmail') notify_gmail_checkbutton = self.xml.get_widget('notify_gmail_checkbutton') - notify_gmail_extra_checkbutton = self.xml.get_widget('notify_gmail_extra_checkbutton') + notify_gmail_extra_checkbutton = self.xml.get_widget( + 'notify_gmail_extra_checkbutton') frame_gmail.set_no_show_all(True) for account in gajim.config.get_per('accounts'): @@ -511,8 +545,11 @@ class PreferencesWindow: gajim.interface.systray.change_status(show) else: gajim.config.set('trayicon', False) + if not gajim.interface.roster.window.get_property('visible'): + gajim.interface.roster.window.present() gajim.interface.hide_systray() - gajim.config.set('show_roster_on_startup', True) # no tray, show roster! + # no tray, show roster! + gajim.config.set('show_roster_on_startup', True) gajim.interface.roster.draw_roster() gajim.interface.save_config() @@ -543,7 +580,7 @@ class PreferencesWindow: else: gajim.config.set('emoticons_theme', emot_theme) - gajim.interface.init_emoticons() + gajim.interface.init_emoticons(need_reload = True) gajim.interface.make_regexps() self.toggle_emoticons() @@ -640,11 +677,14 @@ class PreferencesWindow: else: self.remove_speller() + def on_xhtml_checkbutton_toggled(self, widget): + self.on_checkbutton_toggled(widget, 'ignore_incoming_xhtml') + def _set_sensitivity_for_before_after_time_widgets(self, sensitive): self.xml.get_widget('before_time_label').set_sensitive(sensitive) - self.xml.get_widget('before_time_entry').set_sensitive(sensitive) + self.xml.get_widget('before_time_textview').set_sensitive(sensitive) self.xml.get_widget('after_time_label').set_sensitive(sensitive) - self.xml.get_widget('after_time_entry').set_sensitive(sensitive) + self.xml.get_widget('after_time_textview').set_sensitive(sensitive) def on_time_never_radiobutton_toggled(self, widget): if widget.get_active(): @@ -664,20 +704,33 @@ class PreferencesWindow: self._set_sensitivity_for_before_after_time_widgets(True) gajim.interface.save_config() - def on_before_time_entry_focus_out_event(self, widget, event): - gajim.config.set('before_time', widget.get_text().decode('utf-8')) + def _get_textview_text(self, tv): + buffer = tv.get_buffer() + begin, end = buffer.get_bounds() + return buffer.get_text(begin, end).decode('utf-8') + + def on_before_time_textview_focus_out_event(self, widget, event): + text = self._get_textview_text(widget) + text = helpers.to_one_line(text) + gajim.config.set('before_time', text) gajim.interface.save_config() - def on_after_time_entry_focus_out_event(self, widget, event): - gajim.config.set('after_time', widget.get_text().decode('utf-8')) + def on_after_time_textview_focus_out_event(self, widget, event): + text = self._get_textview_text(widget) + text = helpers.to_one_line(text) + gajim.config.set('after_time', text) gajim.interface.save_config() - def on_before_nickname_entry_focus_out_event(self, widget, event): - gajim.config.set('before_nickname', widget.get_text().decode('utf-8')) + def on_before_nickname_textview_focus_out_event(self, widget, event): + text = self._get_textview_text(widget) + text = helpers.to_one_line(text) + gajim.config.set('before_nickname', text) gajim.interface.save_config() - def on_after_nickname_entry_focus_out_event(self, widget, event): - gajim.config.set('after_nickname', widget.get_text().decode('utf-8')) + def on_after_nickname_textview_focus_out_event(self, widget, event): + text = self._get_textview_text(widget) + text = helpers.to_one_line(text) + gajim.config.set('after_nickname', text) gajim.interface.save_config() def update_text_tags(self): @@ -886,7 +939,7 @@ class PreferencesWindow: def on_applications_combobox_changed(self, widget): gajim.config.set('autodetect_browser_mailer', False) - if widget.get_active() == 3: + if widget.get_active() == 4: self.xml.get_widget('custom_apps_frame').show() gajim.config.set('openwith', 'custom') else: @@ -896,6 +949,8 @@ class PreferencesWindow: gajim.config.set('openwith', 'gnome-open') elif widget.get_active() == 2: gajim.config.set('openwith', 'kfmclient exec') + elif widget.get_active() == 3: + gajim.config.set('openwith', 'exo-open') self.xml.get_widget('custom_apps_frame').hide() gajim.interface.save_config() @@ -1063,13 +1118,21 @@ class PreferencesWindow: gajim.interface.instances['advanced_config'] = \ dialogs.AdvancedConfigurationWindow() + def set_status_msg_from_current_music_track_checkbutton_toggled(self, + widget): + self.on_checkbutton_toggled(widget, + 'set_status_msg_from_current_music_track') + gajim.interface.roster.enable_syncing_status_msg_from_current_music_track( + widget.get_active()) + #---------- AccountModificationWindow class -------------# class AccountModificationWindow: '''Class for account informations''' def on_account_modification_window_destroy(self, widget): '''close window''' if gajim.interface.instances.has_key(self.account): - if gajim.interface.instances[self.account].has_key('account_modification'): + if gajim.interface.instances[self.account].has_key( + 'account_modification'): del gajim.interface.instances[self.account]['account_modification'] return if gajim.interface.instances.has_key('account_modification'): @@ -1147,14 +1210,16 @@ class AccountModificationWindow: self.xml.get_widget('save_password_checkbutton').set_active( gajim.config.get_per('accounts', self.account, 'savepass')) if gajim.config.get_per('accounts', self.account, 'savepass'): - passstr = gajim.config.get_per('accounts', - self.account, 'password') + passstr = passwords.get_password(self.account) password_entry = self.xml.get_widget('password_entry') password_entry.set_sensitive(True) password_entry.set_text(passstr) self.xml.get_widget('resource_entry').set_text(gajim.config.get_per( 'accounts', self.account, 'resource')) + self.xml.get_widget('adjust_priority_with_status_checkbutton').set_active( + gajim.config.get_per('accounts', self.account, + 'adjust_priority_with_status')) self.xml.get_widget('priority_spinbutton').set_value(gajim.config.\ get_per('accounts', self.account, 'priority')) @@ -1219,6 +1284,10 @@ class AccountModificationWindow: return True return False + def on_adjust_priority_with_status_checkbutton_toggled(self, widget): + self.xml.get_widget('priority_spinbutton').set_sensitive( + not widget.get_active()) + def on_save_button_clicked(self, widget): '''When save button is clicked: Save information in config file''' config = {} @@ -1237,8 +1306,8 @@ class AccountModificationWindow: return if name in gajim.connections: dialogs.ErrorDialog(_('Account Name Already Used'), - _('This name is already used by another of your accounts. Please choose ' - 'another name.')) + _('This name is already used by another of your accounts. ' + 'Please choose another name.')) return if (name == ''): dialogs.ErrorDialog(_('Invalid account name'), @@ -1265,7 +1334,8 @@ class AccountModificationWindow: dialogs.ErrorDialog(pritext, sectext) return - resource = self.xml.get_widget('resource_entry').get_text().decode('utf-8') + resource = self.xml.get_widget('resource_entry').get_text().decode( + 'utf-8') try: resource = helpers.parse_resource(resource) except helpers.InvalidFormat, s: @@ -1278,16 +1348,18 @@ class AccountModificationWindow: config['password'] = self.xml.get_widget('password_entry').get_text().\ decode('utf-8') config['resource'] = resource + config['adjust_priority_with_status'] = self.xml.get_widget( + 'adjust_priority_with_status_checkbutton').get_active() config['priority'] = self.xml.get_widget('priority_spinbutton').\ - get_value_as_int() + get_value_as_int() config['autoconnect'] = self.xml.get_widget('autoconnect_checkbutton').\ - get_active() - config['autoreconnect'] = self.xml.get_widget('autoreconnect_checkbutton').\ - get_active() + get_active() + config['autoreconnect'] = self.xml.get_widget( + 'autoreconnect_checkbutton').get_active() if self.account: - list_no_log_for = gajim.config.get_per('accounts', - self.account, 'no_log_for').split() + list_no_log_for = gajim.config.get_per('accounts', self.account, + 'no_log_for').split() else: list_no_log_for = [] if self.account in list_no_log_for: @@ -1297,7 +1369,7 @@ class AccountModificationWindow: config['no_log_for'] = ' '.join(list_no_log_for) config['sync_with_global_status'] = self.xml.get_widget( - 'sync_with_global_status_checkbutton').get_active() + 'sync_with_global_status_checkbutton').get_active() config['use_ft_proxies'] = self.xml.get_widget( 'use_ft_proxies_checkbutton').get_active() @@ -1324,21 +1396,27 @@ class AccountModificationWindow: config['custom_host'] = self.xml.get_widget( 'custom_host_entry').get_text().decode('utf-8') - config['keyname'] = self.xml.get_widget('gpg_name_label').get_text().decode('utf-8') + # update in case the name was changed to local account's name + config['is_zeroconf'] = False + + config['keyname'] = self.xml.get_widget('gpg_name_label').get_text().\ + decode('utf-8') if config['keyname'] == '': #no key selected config['keyid'] = '' config['savegpgpass'] = False config['gpgpassword'] = '' else: - config['keyid'] = self.xml.get_widget('gpg_key_label').get_text().decode('utf-8') + config['keyid'] = self.xml.get_widget('gpg_key_label').get_text().\ + decode('utf-8') config['savegpgpass'] = self.xml.get_widget( - 'gpg_save_password_checkbutton').get_active() + 'gpg_save_password_checkbutton').get_active() config['gpgpassword'] = self.xml.get_widget('gpg_password_entry' ).get_text().decode('utf-8') #if we modify the name of the account if name != self.account: #update variables - gajim.interface.instances[name] = gajim.interface.instances[self.account] + gajim.interface.instances[name] = gajim.interface.instances[ + self.account] gajim.nicks[name] = gajim.nicks[self.account] gajim.block_signed_in_notifications[name] = \ gajim.block_signed_in_notifications[self.account] @@ -1358,8 +1436,7 @@ class AccountModificationWindow: gajim.events.change_account_name(self.account, name) # change account variable for chat / gc controls - for ctrl in gajim.interface.msg_win_mgr.get_controls(): - ctrl.account = name + gajim.interface.msg_win_mgr.change_account_name(self.account, name) # upgrade account variable in opened windows for kind in ('infos', 'disco', 'gc_config'): for j in gajim.interface.instances[name][kind]: @@ -1394,27 +1471,28 @@ class AccountModificationWindow: # check if relogin is needed relogin_needed = False if self.options_changed_need_relogin(config, - ('resource', 'proxy', 'usessl', 'keyname', - 'use_custom_host', 'custom_host')): + ('resource', 'proxy', 'usessl', 'keyname', + 'use_custom_host', 'custom_host')): relogin_needed = True elif config['use_custom_host'] and (self.option_changed(config, - 'custom_host') or self.option_changed(config, 'custom_port')): + 'custom_host') or self.option_changed(config, 'custom_port')): relogin_needed = True if self.option_changed(config, 'use_ft_proxies') and \ config['use_ft_proxies']: gajim.connections[self.account].discover_ft_proxies() - if self.option_changed(config, 'priority'): + if self.option_changed(config, 'priority') or self.option_changed( + config, 'adjust_priority_with_status'): resend_presence = True for opt in config: gajim.config.set_per('accounts', name, opt, config[opt]) if config['savepass']: - gajim.connections[name].password = config['password'] + passwords.save_password(name, config['password']) else: - gajim.connections[name].password = None + passwords.save_password(name, None) # refresh accounts window if gajim.interface.instances.has_key('accounts'): gajim.interface.instances['accounts'].init_accounts() @@ -1463,7 +1541,7 @@ class AccountModificationWindow: def on_change_password_button_clicked(self, widget): try: dialog = dialogs.ChangePasswordDialog(self.account) - except RuntimeError: + except GajimGeneralException: #if we showed ErrorDialog, there will not be dialog instance return @@ -1476,7 +1554,8 @@ class AccountModificationWindow: def on_edit_details_button_clicked(self, widget): if not gajim.interface.instances.has_key(self.account): dialogs.ErrorDialog(_('No such account available'), - _('You must create your account before editing your personal information.')) + _('You must create your account before editing your personal ' + 'information.')) return # show error dialog if account is newly created (not in gajim.connections) @@ -1639,7 +1718,8 @@ class ManageProxiesWindow: self.xml.get_widget('proxypass_entry').set_sensitive(act) def on_proxies_treeview_cursor_changed(self, widget): - #FIXME: check if off proxy settings are correct (see http://trac.gajim.org/changeset/1921#file2 line 1221 + #FIXME: check if off proxy settings are correct (see + # http://trac.gajim.org/changeset/1921#file2 line 1221 (model, iter) = widget.get_selection().get_selected() if not iter: return @@ -1758,6 +1838,23 @@ class AccountsWindow: st = gajim.config.get('mergeaccounts') self.xml.get_widget('merge_checkbutton').set_active(st) + import os + + avahi_error = False + try: + import avahi + except ImportError: + avahi_error = True + + # enable zeroconf + st = gajim.config.get('enable_zeroconf') + w = self.xml.get_widget('enable_zeroconf_checkbutton') + w.set_active(st) + if os.name == 'nt' or (avahi_error and not w.get_active()): + w.set_sensitive(False) + self.zeroconf_toggled_id = w.connect('toggled', + self.on_enable_zeroconf_checkbutton_toggled) + def on_accounts_window_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Escape: self.window.destroy() @@ -1770,7 +1867,8 @@ class AccountsWindow: model.clear() for account in gajim.connections: iter = model.append() - model.set(iter, 0, account, 1, gajim.get_hostname_from_account(account)) + model.set(iter, 0, account, 1, gajim.get_hostname_from_account( + account)) def on_accounts_treeview_cursor_changed(self, widget): '''Activate delete and modify buttons when a row is selected''' @@ -1797,11 +1895,47 @@ class AccountsWindow: dialogs.ErrorDialog(_('Unread events'), _('Read all pending events before removing this account.')) return - if gajim.interface.instances[account].has_key('remove_account'): - gajim.interface.instances[account]['remove_account'].window.present() + + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + w = self.xml.get_widget('enable_zeroconf_checkbutton') + w.set_active(False) + return else: - gajim.interface.instances[account]['remove_account'] = \ - RemoveAccountWindow(account) + if gajim.interface.instances[account].has_key('remove_account'): + gajim.interface.instances[account]['remove_account'].window.present( + ) + else: + gajim.interface.instances[account]['remove_account'] = \ + RemoveAccountWindow(account) + + win_opened = False + if gajim.interface.msg_win_mgr.get_controls(acct = account): + win_opened = True + else: + for key in gajim.interface.instances[account]: + if gajim.interface.instances[account][key] and key != \ + 'remove_account': + win_opened = True + break + # Detect if we have opened windows for this account + self.dialog = None + def remove(widget, account): + if self.dialog: + self.dialog.destroy() + if gajim.interface.instances[account].has_key('remove_account'): + gajim.interface.instances[account]['remove_account'].window.\ + present() + else: + gajim.interface.instances[account]['remove_account'] = \ + RemoveAccountWindow(account) + if win_opened: + self.dialog = dialogs.ConfirmationDialog( + _('You have opened chat in account %s') % account, + _('All chat and groupchat windows will be closed. Do you want to ' + 'continue?'), + on_response_ok = (remove, account)) + else: + remove(widget, account) def on_modify_button_clicked(self, widget): '''When modify button is clicked: @@ -1819,21 +1953,117 @@ class AccountsWindow: self.show_modification_window(account) def show_modification_window(self, account): - if gajim.interface.instances[account].has_key('account_modification'): - gajim.interface.instances[account]['account_modification'].window.present() + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + if gajim.interface.instances.has_key('zeroconf_properties'): + gajim.interface.instances['zeroconf_properties'].window.present() + else: + gajim.interface.instances['zeroconf_properties'] = \ + ZeroconfPropertiesWindow() else: - gajim.interface.instances[account]['account_modification'] = \ - AccountModificationWindow(account) + if gajim.interface.instances[account].has_key('account_modification'): + gajim.interface.instances[account]['account_modification'].window.\ + present() + else: + gajim.interface.instances[account]['account_modification'] = \ + AccountModificationWindow(account) - def on_merge_checkbutton_toggled(self, widget): - gajim.config.set('mergeaccounts', widget.get_active()) + def on_checkbutton_toggled(self, widget, config_name, + change_sensitivity_widgets = None): + gajim.config.set(config_name, widget.get_active()) + if change_sensitivity_widgets: + for w in change_sensitivity_widgets: + w.set_sensitive(widget.get_active()) gajim.interface.save_config() + + def on_merge_checkbutton_toggled(self, widget): + self.on_checkbutton_toggled(widget, 'mergeaccounts') if len(gajim.connections) >= 2: # Do not merge accounts if only one exists gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False gajim.interface.roster.draw_roster() + + def on_enable_zeroconf_checkbutton_toggled(self, widget): + # don't do anything if there is an account with the local name but is a + # normal account + if gajim.connections.has_key(gajim.ZEROCONF_ACC_NAME) and not \ + gajim.connections[gajim.ZEROCONF_ACC_NAME].is_zeroconf: + gajim.connections[gajim.ZEROCONF_ACC_NAME].dispatch('ERROR', + (_('Account Local already exists.'), + _('Please rename or remove it before enabling link-local messaging' + '.'))) + widget.disconnect(self.zeroconf_toggled_id) + widget.set_active(False) + self.zeroconf_toggled_id = widget.connect('toggled', + self.on_enable_zeroconf_checkbutton_toggled) + return + + if gajim.config.get('enable_zeroconf'): + #disable + gajim.interface.roster.close_all(gajim.ZEROCONF_ACC_NAME) + gajim.connections[gajim.ZEROCONF_ACC_NAME].disable_account() + del gajim.connections[gajim.ZEROCONF_ACC_NAME] + gajim.interface.save_config() + del gajim.interface.instances[gajim.ZEROCONF_ACC_NAME] + del gajim.nicks[gajim.ZEROCONF_ACC_NAME] + del gajim.block_signed_in_notifications[gajim.ZEROCONF_ACC_NAME] + del gajim.groups[gajim.ZEROCONF_ACC_NAME] + gajim.contacts.remove_account(gajim.ZEROCONF_ACC_NAME) + del gajim.gc_connected[gajim.ZEROCONF_ACC_NAME] + del gajim.automatic_rooms[gajim.ZEROCONF_ACC_NAME] + del gajim.to_be_removed[gajim.ZEROCONF_ACC_NAME] + del gajim.newly_added[gajim.ZEROCONF_ACC_NAME] + del gajim.sleeper_state[gajim.ZEROCONF_ACC_NAME] + del gajim.encrypted_chats[gajim.ZEROCONF_ACC_NAME] + del gajim.last_message_time[gajim.ZEROCONF_ACC_NAME] + del gajim.status_before_autoaway[gajim.ZEROCONF_ACC_NAME] + if len(gajim.connections) >= 2: + # Do not merge accounts if only one exists + gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') + else: + gajim.interface.roster.regroup = False + gajim.interface.roster.draw_roster() + gajim.interface.roster.actions_menu_needs_rebuild = True + if gajim.interface.instances.has_key('accounts'): + gajim.interface.instances['accounts'].init_accounts() + + else: + # enable (will create new account if not present) + gajim.connections[gajim.ZEROCONF_ACC_NAME] = common.zeroconf.\ + connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) + # update variables + gajim.interface.instances[gajim.ZEROCONF_ACC_NAME] = {'infos': {}, + 'disco': {}, 'gc_config': {}} + gajim.connections[gajim.ZEROCONF_ACC_NAME].connected = 0 + gajim.groups[gajim.ZEROCONF_ACC_NAME] = {} + gajim.contacts.add_account(gajim.ZEROCONF_ACC_NAME) + gajim.gc_connected[gajim.ZEROCONF_ACC_NAME] = {} + gajim.automatic_rooms[gajim.ZEROCONF_ACC_NAME] = {} + gajim.newly_added[gajim.ZEROCONF_ACC_NAME] = [] + gajim.to_be_removed[gajim.ZEROCONF_ACC_NAME] = [] + gajim.nicks[gajim.ZEROCONF_ACC_NAME] = gajim.ZEROCONF_ACC_NAME + gajim.block_signed_in_notifications[gajim.ZEROCONF_ACC_NAME] = True + gajim.sleeper_state[gajim.ZEROCONF_ACC_NAME] = 'off' + gajim.encrypted_chats[gajim.ZEROCONF_ACC_NAME] = [] + gajim.last_message_time[gajim.ZEROCONF_ACC_NAME] = {} + gajim.status_before_autoaway[gajim.ZEROCONF_ACC_NAME] = '' + # refresh accounts window + if gajim.interface.instances.has_key('accounts'): + gajim.interface.instances['accounts'].init_accounts() + # refresh roster + if len(gajim.connections) >= 2: + # Do not merge accounts if only one exists + gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') + else: + gajim.interface.roster.regroup = False + gajim.interface.roster.draw_roster() + gajim.interface.roster.actions_menu_needs_rebuild = True + gajim.interface.save_config() + gajim.connections[gajim.ZEROCONF_ACC_NAME].change_status('online', '') + + self.on_checkbutton_toggled(widget, 'enable_zeroconf') + class DataFormWindow: def __init__(self, account, config): self.account = account @@ -1988,7 +2218,8 @@ class ServiceRegistrationWindow(DataFormWindow): if self.is_form: DataFormWindow.__init__(self, account, infos) else: - self.xml = gtkgui_helpers.get_glade('service_registration_window.glade') + self.xml = gtkgui_helpers.get_glade( + 'service_registration_window.glade') self.window = self.xml.get_widget('service_registration_window') self.window.set_transient_for(gajim.interface.roster.window) if infos.has_key('registered'): @@ -2038,8 +2269,8 @@ class ServiceRegistrationWindow(DataFormWindow): else: gajim.interface.roster.add_transport_to_roster(self.account, self.service) - gajim.connections[self.account].register_agent(self.service, self.infos, - True) # True is for is_form + gajim.connections[self.account].register_agent(self.service, + self.infos, True) # True is for is_form else: # we pressed OK of service_registration_window # send registration info to the core @@ -2052,7 +2283,8 @@ class ServiceRegistrationWindow(DataFormWindow): else: gajim.interface.roster.add_transport_to_roster(self.account, self.service) - gajim.connections[self.account].register_agent(self.service, self.infos) + gajim.connections[self.account].register_agent(self.service, + self.infos) self.window.destroy() @@ -2281,7 +2513,8 @@ class RemoveAccountWindow: # We don't remove account cause we canceled pw window return gajim.connections[self.account].password = passphrase - gajim.connections[self.account].unregister_account(self._on_remove_success) + gajim.connections[self.account].unregister_account( + self._on_remove_success) else: self._on_remove_success(True) @@ -2300,7 +2533,7 @@ class RemoveAccountWindow: if not res: return # Close all opened windows - gajim.interface.roster.close_all(self.account) + gajim.interface.roster.close_all(self.account, force = True) gajim.connections[self.account].disconnect(on_purpose = True) del gajim.connections[self.account] gajim.config.del_per('accounts', self.account) @@ -2335,15 +2568,17 @@ class ManageBookmarksWindow: self.window = self.xml.get_widget('manage_bookmarks_window') self.window.set_transient_for(gajim.interface.roster.window) - #Account-JID, RoomName, Room-JID, Autojoin, Passowrd, Nick, Show_Status + # Account-JID, RoomName, Room-JID, Autojoin, Passowrd, Nick, Show_Status self.treestore = gtk.TreeStore(str, str, str, bool, str, str, str) - #Store bookmarks in treeview. + # Store bookmarks in treeview. for account in gajim.connections: if gajim.connections[account].connected <= 1: continue + if gajim.connections[account].is_zeroconf: + continue iter = self.treestore.append(None, [None, account,None, - None, None, None, None]) + None, None, None, None]) for bookmark in gajim.connections[account].bookmarks: if bookmark['name'] == '': @@ -2356,10 +2591,9 @@ class ManageBookmarksWindow: autojoin = helpers.from_xs_boolean_to_python_boolean( bookmark['autojoin']) - if bookmark.has_key('print_status'): - print_status = bookmark['print_status'] - if not print_status: - print_status = gajim.config.get('print_status_in_muc') + print_status = bookmark.get('print_status', '') + if print_status not in ('', 'all', 'in_and_out', 'none'): + print_status = '' self.treestore.append( iter, [ account, bookmark['name'], @@ -2372,7 +2606,7 @@ class ManageBookmarksWindow: self.print_status_combobox = self.xml.get_widget('print_status_combobox') model = gtk.ListStore(str, str) - self.option_list = {'all': _('All'), + self.option_list = {'': _('Default'), 'all': _('All'), 'in_and_out': _('Enter and leave only'), 'none': _('None')} opts = self.option_list.keys() opts.sort() @@ -2443,8 +2677,8 @@ class ManageBookmarksWindow: account = model[add_to][1].decode('utf-8') nick = gajim.nicks[account] - self.treestore.append(add_to, [account, _('New Room'), '', False, '', - nick, 'in_and_out']) + self.treestore.append(add_to, [account, _('New Group Chat'), '', False, + '', nick, 'in_and_out']) self.view.expand_row(model.get_path(add_to), True) @@ -2473,9 +2707,11 @@ class ManageBookmarksWindow: #Account data can't be changed return - if self.server_entry.get_text().decode('utf-8') == '' or self.room_entry.get_text().decode('utf-8') == '': + if self.server_entry.get_text().decode('utf-8') == '' or \ + self.room_entry.get_text().decode('utf-8') == '': dialogs.ErrorDialog(_('This bookmark has invalid data'), -_('Please be sure to fill out server and room fields or remove this bookmark.')) + _('Please be sure to fill out server and room fields or remove this' + ' bookmark.')) return False return True @@ -2627,7 +2863,8 @@ _('Please be sure to fill out server and room fields or remove this bookmark.')) class AccountCreationWizardWindow: def __init__(self): - self.xml = gtkgui_helpers.get_glade('account_creation_wizard_window.glade') + self.xml = gtkgui_helpers.get_glade( + 'account_creation_wizard_window.glade') self.window = self.xml.get_widget('account_creation_wizard_window') # Connect events from comboboxentry.child @@ -2736,7 +2973,8 @@ class AccountCreationWizardWindow: username = widgets['username_entry'].get_text().decode('utf-8') if not username: pritext = _('Invalid username') - sectext = _('You must provide a username to configure this account.') + sectext = _('You must provide a username to configure this account' + '.') dialogs.ErrorDialog(pritext, sectext) return server = widgets['server_comboboxentry'].child.get_text() @@ -2781,7 +3019,7 @@ class AccountCreationWizardWindow: self.account = server + str(i) i += 1 - username, server = gajim.get_room_name_and_server_from_room_jid(jid) + username, server = gajim.get_name_and_server_from_jid(jid) self.save_account(username, server, savepass, password) self.cancel_button.hide() self.back_button.hide() @@ -2789,7 +3027,9 @@ class AccountCreationWizardWindow: if self.modify: finish_text = '<big><b>%s</b></big>\n\n%s' % ( _('Account has been added successfully'), -_('You can set advanced account options by pressing Advanced button, or later by clicking in Accounts menuitem under Edit menu from the main window.')) + _('You can set advanced account options by pressing Advanced ' + 'button, or later by clicking in Accounts menuitem under Edit ' + 'menu from the main window.')) self.finish_label.set_markup(finish_text) self.finish_button.show() self.finish_button.set_property('has-default', True) @@ -2822,7 +3062,9 @@ _('You can set advanced account options by pressing Advanced button, or later by finish_text = '<big><b>%s</b></big>\n\n%s' % ( _('Your new account has been created successfully'), -_('You can set advanced account options by pressing Advanced button, or later by clicking in Accounts menuitem under Edit menu from the main window.')) + _('You can set advanced account options by pressing Advanced button, ' + 'or later by clicking in Accounts menuitem under Edit menu from the ' + 'main window.')) self.finish_label.set_markup(finish_text) self.notebook.set_current_page(3) # show finish page @@ -2837,7 +3079,8 @@ _('You can set advanced account options by pressing Advanced button, or later by self.show_vcard_checkbutton.hide() img = self.xml.get_widget('finish_image') img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_DIALOG) - finish_text = '<big><b>%s</b></big>\n\n%s' % (_('An error occured during account creation') , reason) + finish_text = '<big><b>%s</b></big>\n\n%s' % (_('An error occured during ' + 'account creation') , reason) self.finish_label.set_markup(finish_text) self.notebook.set_current_page(3) # show finish page @@ -2900,9 +3143,6 @@ _('You can set advanced account options by pressing Advanced button, or later by con = connection.Connection(self.account) con.password = password - if not savepass: - password = "" - config = {} config['name'] = login config['hostname'] = server @@ -2931,6 +3171,10 @@ _('You can set advanced account options by pressing Advanced button, or later by def create_vars(self, config): gajim.config.add_per('accounts', self.account) + + if not config['savepass']: + config['password'] = '' + for opt in config: gajim.config.set_per('accounts', self.account, opt, config[opt]) @@ -2961,3 +3205,210 @@ _('You can set advanced account options by pressing Advanced button, or later by gajim.interface.roster.draw_roster() gajim.interface.roster.actions_menu_needs_rebuild = True gajim.interface.save_config() + +#---------- ZeroconfPropertiesWindow class -------------# +class ZeroconfPropertiesWindow: + def __init__(self): + self.xml = gtkgui_helpers.get_glade('zeroconf_properties_window.glade') + self.window = self.xml.get_widget('zeroconf_properties_window') + self.window.set_transient_for(gajim.interface.roster.window) + self.xml.signal_autoconnect(self) + + self.init_account() + self.init_account_gpg() + + self.xml.get_widget('save_button').grab_focus() + self.window.show_all() + + def init_account(self): + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'autoconnect') + if st: + self.xml.get_widget('autoconnect_checkbutton').set_active(st) + + list_no_log_for = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME,'no_log_for').split() + if gajim.ZEROCONF_ACC_NAME in list_no_log_for: + self.xml.get_widget('log_history_checkbutton').set_active(0) + else: + self.xml.get_widget('log_history_checkbutton').set_active(1) + + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'sync_with_global_status') + if st: + self.xml.get_widget('sync_with_global_status_checkbutton').set_active( + st) + + for opt in ('first_name', 'last_name', 'jabber_id', 'email'): + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_' + opt) + if st: + self.xml.get_widget(opt + '_entry').set_text(st) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port') + if st: + self.xml.get_widget('custom_port_entry').set_text(str(st)) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'use_custom_host') + if st: + self.xml.get_widget('custom_port_checkbutton').set_active(st) + + self.xml.get_widget('custom_port_entry').set_sensitive(bool(st)) + + if not st: + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port', '5298') + + def init_account_gpg(self): + keyid = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'keyid') + keyname = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'keyname') + savegpgpass = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'savegpgpass') + + if not keyid or not gajim.config.get('usegpg'): + return + + self.xml.get_widget('gpg_key_label').set_text(keyid) + self.xml.get_widget('gpg_name_label').set_text(keyname) + gpg_save_password_checkbutton = \ + self.xml.get_widget('gpg_save_password_checkbutton') + gpg_save_password_checkbutton.set_sensitive(True) + gpg_save_password_checkbutton.set_active(savegpgpass) + + if savegpgpass: + entry = self.xml.get_widget('gpg_password_entry') + entry.set_sensitive(True) + gpgpassword = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'gpgpassword') + entry.set_text(gpgpassword) + + def on_zeroconf_properties_window_destroy(self, widget): + # close window + if gajim.interface.instances.has_key('zeroconf_properties'): + del gajim.interface.instances['zeroconf_properties'] + + def on_custom_port_checkbutton_toggled(self, widget): + st = self.xml.get_widget('custom_port_checkbutton').get_active() + self.xml.get_widget('custom_port_entry').set_sensitive(bool(st)) + + def on_cancel_button_clicked(self, widget): + self.window.destroy() + + def on_save_button_clicked(self, widget): + config = {} + + st = self.xml.get_widget('autoconnect_checkbutton').get_active() + config['autoconnect'] = st + list_no_log_for = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'no_log_for').split() + if gajim.ZEROCONF_ACC_NAME in list_no_log_for: + list_no_log_for.remove(gajim.ZEROCONF_ACC_NAME) + if not self.xml.get_widget('log_history_checkbutton').get_active(): + list_no_log_for.append(gajim.ZEROCONF_ACC_NAME) + config['no_log_for'] = ' '.join(list_no_log_for) + + st = self.xml.get_widget('sync_with_global_status_checkbutton').\ + get_active() + config['sync_with_global_status'] = st + + st = self.xml.get_widget('first_name_entry').get_text() + config['zeroconf_first_name'] = st.decode('utf-8') + + st = self.xml.get_widget('last_name_entry').get_text() + config['zeroconf_last_name'] = st.decode('utf-8') + + st = self.xml.get_widget('jabber_id_entry').get_text() + config['zeroconf_jabber_id'] = st.decode('utf-8') + + st = self.xml.get_widget('email_entry').get_text() + config['zeroconf_email'] = st.decode('utf-8') + + use_custom_port = self.xml.get_widget('custom_port_checkbutton').\ + get_active() + config['use_custom_host'] = use_custom_port + + old_port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port') + if use_custom_port: + port = self.xml.get_widget('custom_port_entry').get_text() + else: + port = 5298 + + config['custom_port'] = port + + config['keyname'] = self.xml.get_widget('gpg_name_label').get_text().\ + decode('utf-8') + if config['keyname'] == '': #no key selected + config['keyid'] = '' + config['savegpgpass'] = False + config['gpgpassword'] = '' + else: + config['keyid'] = self.xml.get_widget('gpg_key_label').get_text().\ + decode('utf-8') + config['savegpgpass'] = self.xml.get_widget( + 'gpg_save_password_checkbutton').get_active() + config['gpgpassword'] = self.xml.get_widget('gpg_password_entry' + ).get_text().decode('utf-8') + + reconnect = False + for opt in ('zeroconf_first_name','zeroconf_last_name', + 'zeroconf_jabber_id', 'zeroconf_email', 'custom_port'): + if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, opt) != \ + config[opt]: + reconnect = True + + for opt in config: + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, opt, + config[opt]) + + if gajim.connections.has_key(gajim.ZEROCONF_ACC_NAME): + if port != old_port or reconnect: + gajim.connections[gajim.ZEROCONF_ACC_NAME].update_details() + + self.window.destroy() + + def on_gpg_choose_button_clicked(self, widget, data = None): + if gajim.connections.has_key(gajim.ZEROCONF_ACC_NAME): + secret_keys = gajim.connections[gajim.ZEROCONF_ACC_NAME].\ + ask_gpg_secrete_keys() + + # self.account is None and/or gajim.connections is {} + else: + from common import GnuPG + if GnuPG.USE_GPG: + secret_keys = GnuPG.GnuPG().get_secret_keys() + else: + secret_keys = [] + if not secret_keys: + dialogs.ErrorDialog(_('Failed to get secret keys'), + _('There was a problem retrieving your OpenPGP secret keys.')) + return + secret_keys[_('None')] = _('None') + instance = dialogs.ChooseGPGKeyDialog(_('OpenPGP Key Selection'), + _('Choose your OpenPGP key'), secret_keys) + keyID = instance.run() + if keyID is None: + return + checkbutton = self.xml.get_widget('gpg_save_password_checkbutton') + gpg_key_label = self.xml.get_widget('gpg_key_label') + gpg_name_label = self.xml.get_widget('gpg_name_label') + if keyID[0] == _('None'): + gpg_key_label.set_text(_('No key selected')) + gpg_name_label.set_text('') + checkbutton.set_sensitive(False) + self.xml.get_widget('gpg_password_entry').set_sensitive(False) + else: + gpg_key_label.set_text(keyID[0]) + gpg_name_label.set_text(keyID[1]) + checkbutton.set_sensitive(True) + checkbutton.set_active(False) + self.xml.get_widget('gpg_password_entry').set_text('') + + def on_gpg_save_password_checkbutton_toggled(self, widget): + st = widget.get_active() + w = self.xml.get_widget('gpg_password_entry') + w.set_sensitive(bool(st)) diff --git a/src/conversation_textview.py b/src/conversation_textview.py index 888c9d2b367d72f7f37cc12181ac6e7fe3dc4880..834ec6acd62bb87e2b568720b635d37ef724cc73 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -1,17 +1,8 @@ ## conversation_textview.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -27,7 +18,6 @@ import gtk import pango import gobject import time -import sys import os import tooltips import dialogs @@ -39,12 +29,20 @@ from common import helpers from calendar import timegm from common.fuzzyclock import FuzzyClock +from htmltextview import HtmlTextView +from common.exceptions import GajimGeneralException + class ConversationTextview: '''Class for the conversation textview (where user reads already said messages) for chat/groupchat windows''' - def __init__(self, account): - # no need to inherit TextView, use it as property is safer - self.tv = gtk.TextView() + def __init__(self, account, used_in_history_window = False): + '''if used_in_history_window is True, then we do not show + Clear menuitem in context menu''' + self.used_in_history_window = used_in_history_window + + # no need to inherit TextView, use it as atrribute is safer + self.tv = HtmlTextView() + self.tv.html_hyperlink_handler = self.html_hyperlink_handler # set properties self.tv.set_border_width(1) @@ -57,11 +55,13 @@ class ConversationTextview: self.handlers = {} # connect signals - id = self.tv.connect('motion_notify_event', self.on_textview_motion_notify_event) + id = self.tv.connect('motion_notify_event', + self.on_textview_motion_notify_event) self.handlers[id] = self.tv id = self.tv.connect('populate_popup', self.on_textview_populate_popup) self.handlers[id] = self.tv - id = self.tv.connect('button_press_event', self.on_textview_button_press_event) + id = self.tv.connect('button_press_event', + self.on_textview_button_press_event) self.handlers[id] = self.tv self.account = account @@ -98,7 +98,7 @@ class ConversationTextview: tag.set_property('weight', pango.WEIGHT_BOLD) tag = buffer.create_tag('time_sometimes') - tag.set_property('foreground', 'grey') + tag.set_property('foreground', 'darkgrey') tag.set_property('scale', pango.SCALE_SMALL) tag.set_property('justification', gtk.JUSTIFY_CENTER) @@ -138,6 +138,11 @@ class ConversationTextview: self.focus_out_end_iter_offset = None self.line_tooltip = tooltips.BaseTooltip() + + path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png') + self.focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file) + # use it for hr too + self.tv.focus_out_line_pixbuf = self.focus_out_line_pixbuf def del_handlers(self): for i in self.handlers.keys(): @@ -145,7 +150,7 @@ class ConversationTextview: self.handlers[i].disconnect(i) del self.handlers self.tv.destroy() - #TODO + #FIXME: # self.line_tooltip.destroy() def update_tags(self): @@ -230,19 +235,14 @@ class ConversationTextview: end_iter_for_previous_line) # add the new focus out line - # FIXME: Why is this loaded from disk everytime - path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png') - focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file) end_iter = buffer.get_end_iter() buffer.insert(end_iter, '\n') - buffer.insert_pixbuf(end_iter, focus_out_line_pixbuf) + buffer.insert_pixbuf(end_iter, self.focus_out_line_pixbuf) end_iter = buffer.get_end_iter() before_img_iter = end_iter.copy() before_img_iter.backward_char() # one char back (an image also takes one char) buffer.apply_tag_by_name('focus-out-line', before_img_iter, end_iter) - #FIXME: remove this workaround when bug is fixed - # c http://bugzilla.gnome.org/show_bug.cgi?id=318569 self.allow_focus_out_line = False @@ -316,19 +316,29 @@ class ConversationTextview: def on_textview_populate_popup(self, textview, menu): '''we override the default context menu and we prepend Clear + (only if used_in_history_window is False) and if we have sth selected we show a submenu with actions on the phrase (see on_conversation_textview_button_press_event)''' - item = gtk.SeparatorMenuItem() - menu.prepend(item) - item = gtk.ImageMenuItem(gtk.STOCK_CLEAR) - menu.prepend(item) - id = item.connect('activate', self.clear) - self.handlers[id] = item + + separator_menuitem_was_added = False + if not self.used_in_history_window: + item = gtk.SeparatorMenuItem() + menu.prepend(item) + separator_menuitem_was_added = True + + item = gtk.ImageMenuItem(gtk.STOCK_CLEAR) + menu.prepend(item) + id = item.connect('activate', self.clear) + self.handlers[id] = item + if self.selected_phrase: - s = self.selected_phrase - if len(s) > 25: - s = s[:21] + '...' - item = gtk.MenuItem(_('Actions for "%s"') % s) + if not separator_menuitem_was_added: + item = gtk.SeparatorMenuItem() + menu.prepend(item) + + self.selected_phrase = helpers.reduce_chars_newlines( + self.selected_phrase, 25, 2) + item = gtk.MenuItem(_('_Actions for "%s"') % self.selected_phrase) menu.prepend(item) submenu = gtk.Menu() item.set_submenu(submenu) @@ -360,19 +370,20 @@ class ConversationTextview: self.handlers[id] = item else: if dict_link.find('%s') == -1: - #we must have %s in the url if not WIKTIONARY + # we must have %s in the url if not WIKTIONARY item = gtk.MenuItem(_('Dictionary URL is missing an "%s" and it is not WIKTIONARY')) item.set_property('sensitive', False) else: link = dict_link % self.selected_phrase - id = item.connect('activate', self.visit_url_from_menuitem, link) + id = item.connect('activate', self.visit_url_from_menuitem, + link) self.handlers[id] = item submenu.append(item) search_link = gajim.config.get('search_engine') if search_link.find('%s') == -1: - #we must have %s in the url + # we must have %s in the url item = gtk.MenuItem(_('Web Search URL is missing an "%s"')) item.set_property('sensitive', False) else: @@ -381,14 +392,18 @@ class ConversationTextview: id = item.connect('activate', self.visit_url_from_menuitem, link) self.handlers[id] = item submenu.append(item) + + item = gtk.MenuItem(_('Open as _Link')) + id = item.connect('activate', self.visit_url_from_menuitem, link) + self.handlers[id] = item + submenu.append(item) menu.show_all() def on_textview_button_press_event(self, widget, event): # If we clicked on a taged text do NOT open the standard popup menu # if normal text check if we have sth selected - - self.selected_phrase = '' + self.selected_phrase = '' # do not move belove event button check! if event.button != 3: # if not right click return False @@ -426,18 +441,16 @@ class ConversationTextview: def on_start_chat_activate(self, widget, jid): gajim.interface.roster.new_chat_from_jid(self.account, jid) - def on_join_group_chat_menuitem_activate(self, widget, jid): - room, server = jid.split('@') - if gajim.interface.instances[self.account].has_key('join_gc'): + def on_join_group_chat_menuitem_activate(self, widget, room_jid): + if 'join_gc' in gajim.interface.instances[self.account]: instance = gajim.interface.instances[self.account]['join_gc'] - instance.xml.get_widget('server_entry').set_text(server) - instance.xml.get_widget('room_entry').set_text(room) + instance.xml.get_widget('room_jid_entry').set_text(room_jid) gajim.interface.instances[self.account]['join_gc'].window.present() else: try: gajim.interface.instances[self.account]['join_gc'] = \ - dialogs.JoinGroupchatWindow(self.account, server, room) - except RuntimeError: + dialogs.JoinGroupchatWindow(self.account, room_jid) + except GajimGeneralException: pass def on_add_to_roster_activate(self, widget, jid): @@ -459,6 +472,7 @@ class ConversationTextview: childs[6].hide() # join group chat childs[7].hide() # add to roster else: # It's a mail or a JID + text = text.lower() id = childs[2].connect('activate', self.on_copy_link_activate, text) self.handlers[id] = childs[2] id = childs[3].connect('activate', self.on_open_link_activate, kind, text) @@ -506,6 +520,15 @@ class ConversationTextview: # we launch the correct application helpers.launch_browser_mailer(kind, word) + def html_hyperlink_handler(self, texttag, widget, event, iter, kind, href): + if event.type == gtk.gdk.BUTTON_PRESS: + if event.button == 3: # right click + self.make_link_menu(event, kind, href) + else: + # we launch the correct application + helpers.launch_browser_mailer(kind, href) + + def detect_and_print_special_text(self, otext, other_tags): '''detects special text (emots & links & formatting) prints normal text before any special text it founts, @@ -562,6 +585,7 @@ class ConversationTextview: img.show() #add with possible animation self.tv.add_child_at_anchor(img, anchor) + #FIXME: one day, somehow sync with regexp in gajim.py elif special_text.startswith('http://') or \ special_text.startswith('www.') or \ special_text.startswith('ftp://') or \ @@ -638,11 +662,11 @@ class ConversationTextview: def print_empty_line(self): buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() - buffer.insert(end_iter, '\n') + buffer.insert_with_tags_by_name(end_iter, '\n', 'eol') def print_conversation_line(self, text, jid, kind, name, tim, - other_tags_for_name = [], other_tags_for_time = [], - other_tags_for_text = [], subject = None, old_kind = None): + other_tags_for_name = [], other_tags_for_time = [], other_tags_for_text = [], + subject = None, old_kind = None, xhtml = None): '''prints 'chat' type messages''' buffer = self.tv.get_buffer() buffer.begin_user_action() @@ -652,7 +676,7 @@ class ConversationTextview: at_the_end = True if buffer.get_char_count() > 0: - buffer.insert(end_iter, '\n') + buffer.insert_with_tags_by_name(end_iter, '\n', 'eol') if kind == 'incoming_queue': kind = 'incoming' if old_kind == 'incoming_queue': @@ -664,7 +688,9 @@ class ConversationTextview: current_print_time = gajim.config.get('print_time') if current_print_time == 'always' and kind != 'info': before_str = gajim.config.get('before_time') + before_str = helpers.from_one_line(before_str) after_str = gajim.config.get('after_time') + after_str = helpers.from_one_line(after_str) # get difference in days since epoch (86400 = 24*3600) # number of days since epoch for current time (in GMT) - # number of days since epoch for message (in GMT) @@ -682,10 +708,10 @@ class ConversationTextview: format += day_str + ' ' format += '%X' + after_str tim_format = time.strftime(format, tim) - # if tim_format comes as unicode because of day_str. - # we convert it to the encoding that we want (and that is utf-8) - tim_format = helpers.ensure_utf8_string(tim_format) - tim_format = tim_format.encode('utf-8') + if locale.getpreferredencoding() == 'UTF-8': + # if tim_format comes as unicode because of day_str. + # we convert it to the encoding that we want (and that is utf-8) + tim_format = helpers.ensure_utf8_string(tim_format) buffer.insert_with_tags_by_name(end_iter, tim_format + ' ', *other_tags_for_time) elif current_print_time == 'sometimes' and kind != 'info': @@ -725,7 +751,7 @@ class ConversationTextview: else: self.print_name(name, kind, other_tags_for_name) self.print_subject(subject) - self.print_real_text(text, text_tags, name) + self.print_real_text(text, text_tags, name, xhtml) # scroll to the end of the textview if at_the_end or kind == 'outgoing': @@ -748,7 +774,9 @@ class ConversationTextview: name_tags = other_tags_for_name[:] # create a new list name_tags.append(kind) before_str = gajim.config.get('before_nickname') + before_str = helpers.from_one_line(before_str) after_str = gajim.config.get('after_nickname') + after_str = helpers.from_one_line(after_str) format = before_str + name + after_str + ' ' buffer.insert_with_tags_by_name(end_iter, format, *name_tags) @@ -760,8 +788,18 @@ class ConversationTextview: buffer.insert(end_iter, subject) self.print_empty_line() - def print_real_text(self, text, text_tags = [], name = None): + def print_real_text(self, text, text_tags = [], name = None, xhtml = None): '''this adds normal and special text. call this to add text''' + if xhtml: + try: + if name and (text.startswith('/me ') or text.startswith('/me\n')): + xhtml = xhtml.replace('/me', '<dfn>%s</dfn>'% (name,), 1) + self.tv.display_html(xhtml.encode('utf-8')) + return + except Exception, e: + gajim.log.debug(str("Error processing xhtml")+str(e)) + gajim.log.debug(str("with |"+xhtml+"|")) + buffer = self.tv.get_buffer() # /me is replaced by name if name is given if name and (text.startswith('/me ') or text.startswith('/me\n')): diff --git a/src/dialogs.py b/src/dialogs.py index 6a8ec5de3e8e71e9351486a72f43d9d4d559622d..afb53b83a7dc9e1e36f277e3dc47d24b081020a9 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -3,7 +3,7 @@ ## ## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> ## Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005-2006 Nikos Kouremenos <nkour@jabber.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com> ## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com> ## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za> @@ -25,6 +25,7 @@ import os import gtkgui_helpers import vcard import conversation_textview +import message_control try: import gtkspell @@ -40,6 +41,7 @@ from advanced import AdvancedConfigurationWindow from common import gajim from common import helpers +from common.exceptions import GajimGeneralException class EditGroupsDialog: '''Class for the edit group dialog window''' @@ -139,6 +141,9 @@ class EditGroupsDialog: group = self.xml.get_widget('group_entry').get_text().decode('utf-8') if not group: return + # Do not allow special groups + if group in helpers.special_groups: + return # check if it already exists model = self.list.get_model() iter = model.get_iter_root() @@ -180,14 +185,16 @@ class EditGroupsDialog: if account not in accounts: accounts.append(account) for g in gajim.groups[account].keys(): - if g in helpers.special_groups: - continue if g in groups: continue groups[g] = 0 for g in contact.groups: groups[g] += 1 - group_list = groups.keys() + group_list = [] + # Remove special groups if they are empty + for group in groups: + if group not in helpers.special_groups or groups[group] > 0: + group_list.append(group) group_list.sort() for group in group_list: iter = store.append() @@ -264,6 +271,7 @@ class ChooseGPGKeyDialog: renderer = gtk.CellRendererText() self.keys_treeview.insert_column_with_attributes(-1, _('Contact name'), renderer, text = 1) + self.keys_treeview.set_search_column(1) self.fill_tree(secret_keys, selected) self.window.show_all() @@ -405,12 +413,12 @@ class ChangeStatusMessageDialog: class AddNewContactWindow: '''Class for AddNewContactWindow''' - uid_labels = {'jabber': _('Jabber ID'), - 'aim': _('AIM Address'), - 'gadu-gadu': _('GG Number'), - 'icq': _('ICQ Number'), - 'msn': _('MSN Address'), - 'yahoo': _('Yahoo! Address')} + uid_labels = {'jabber': _('Jabber ID:'), + 'aim': _('AIM Address:'), + 'gadu-gadu': _('GG Number:'), + 'icq': _('ICQ Number:'), + 'msn': _('MSN Address:'), + 'yahoo': _('Yahoo! Address:')} def __init__(self, account = None, jid = None, user_nick = None, group = None): self.account = account @@ -441,7 +449,8 @@ class AddNewContactWindow: 'uid_label', 'uid_entry', 'protocol_combobox', 'protocol_jid_combobox', 'protocol_hbox', 'nickname_entry', 'message_scrolledwindow', 'register_hbox', 'subscription_table', 'add_button', - 'message_textview', 'connected_label', 'group_comboboxentry'): + 'message_textview', 'connected_label', 'group_comboboxentry', + 'auto_authorize_checkbutton'): self.__dict__[w] = self.xml.get_widget(w) if account and len(gajim.connections) >= 2: prompt_text =\ @@ -486,8 +495,10 @@ _('Please fill in the data of the contact you want to add in account %s') %accou liststore.append([type_, type_]) self.protocol_combobox.set_model(liststore) self.protocol_combobox.set_active(0) - self.protocol_jid_combobox.set_sensitive(False) + self.protocol_jid_combobox.set_no_show_all(True) + self.protocol_jid_combobox.hide() self.subscription_table.set_no_show_all(True) + self.auto_authorize_checkbutton.show() self.message_scrolledwindow.set_no_show_all(True) self.register_hbox.set_no_show_all(True) self.register_hbox.hide() @@ -497,11 +508,13 @@ _('Please fill in the data of the contact you want to add in account %s') %accou self.protocol_jid_combobox.set_model(liststore) self.xml.signal_autoconnect(self) if jid: - type_ = gajim.get_transport_name_from_jid(jid) or 'jabber' + type_ = gajim.get_transport_name_from_jid(jid) + if not type_: + type_ = 'jabber' if type_ == 'jabber': self.uid_entry.set_text(jid) else: - uid, transport = gajim.get_room_name_and_server_from_room_jid(jid) + uid, transport = gajim.get_name_and_server_from_jid(jid) self.uid_entry.set_text(uid.replace('%', '@', 1)) #set protocol_combobox model = self.protocol_combobox.get_model() @@ -515,13 +528,13 @@ _('Please fill in the data of the contact you want to add in account %s') %accou i += 1 # set protocol_jid_combobox - self.protocol_combobox.set_active(0) + self.protocol_jid_combobox.set_active(0) model = self.protocol_jid_combobox.get_model() iter = model.get_iter_first() i = 0 while iter: if model[iter][0] == transport: - self.protocol_combobox.set_active(i) + self.protocol_jid_combobox.set_active(i) break iter = model.iter_next(iter) i += 1 @@ -626,7 +639,7 @@ _('Please fill in the data of the contact you want to add in account %s') %accou else: message= '' group = self.group_comboboxentry.child.get_text().decode('utf-8') - auto_auth = self.xml.get_widget('auto_authorize_checkbutton').get_active() + auto_auth = self.auto_authorize_checkbutton.get_active() gajim.interface.roster.req_sub(self, jid, message, self.account, group = group, pseudo = nickname, auto_auth = auto_auth) self.window.destroy() @@ -641,13 +654,15 @@ _('Please fill in the data of the contact you want to add in account %s') %accou for jid_ in self.agents[type_]: model.append([jid_]) self.protocol_jid_combobox.set_active(0) - self.protocol_jid_combobox.set_sensitive(True) + if len(self.agents[type_]) > 1: + self.protocol_jid_combobox.set_no_show_all(False) + self.protocol_jid_combobox.show_all() else: - self.protocol_jid_combobox.set_sensitive(False) + self.protocol_jid_combobox.hide() if type_ in self.uid_labels: self.uid_label.set_text(self.uid_labels[type_]) else: - self.uid_label.set_text(_('User ID')) + self.uid_label.set_text(_('User ID:')) if type_ == 'jabber': self.message_scrolledwindow.show() else: @@ -655,6 +670,7 @@ _('Please fill in the data of the contact you want to add in account %s') %accou if type_ in self.available_types: self.register_hbox.set_no_show_all(False) self.register_hbox.show_all() + self.auto_authorize_checkbutton.hide() self.connected_label.hide() self.subscription_table.hide() self.add_button.set_sensitive(False) @@ -668,9 +684,11 @@ _('Please fill in the data of the contact you want to add in account %s') %accou self.subscription_table.hide() self.connected_label.show() self.add_button.set_sensitive(False) + self.auto_authorize_checkbutton.hide() return self.subscription_table.set_no_show_all(False) self.subscription_table.show_all() + self.auto_authorize_checkbutton.show() self.connected_label.hide() self.add_button.set_sensitive(True) @@ -697,8 +715,14 @@ class AboutDialog: dlg.set_version(gajim.version) s = u'Copyright © 2003-2006 Gajim Team' dlg.set_copyright(s) - text = open('../COPYING').read() - dlg.set_license(text) + copying_file_path = None + if os.path.isfile(os.path.join(gajim.defs.docdir, 'COPYING')): + copying_file_path = os.path.join(gajim.defs.docdir, 'COPYING') + elif os.path.isfile('../COPYING'): + copying_file_path = '../COPYING' + if copying_file_path: + text = open(copying_file_path).read() + dlg.set_license(text) dlg.set_comments('%s\n%s %s\n%s %s' % (_('A GTK+ jabber client'), \ @@ -706,28 +730,40 @@ class AboutDialog: _('PyGTK Version:'), self.tuple2str(gtk.pygtk_version))) dlg.set_website('http://www.gajim.org/') - authors = [] - authors_file = open('../AUTHORS').read() - authors_file = authors_file.split('\n') - for author in authors_file: - if author == 'CURRENT DEVELOPERS:': - authors.append(_('Current Developers:')) - elif author == 'PAST DEVELOPERS:': - authors.append('\n' + _('Past Developers:')) - elif author != '': # Real author line - authors.append(author) - - authors.append('\n' + _('THANKS:')) + authors_file_path = None + if os.path.isfile(os.path.join(gajim.defs.docdir, 'AUTHORS')): + authors_file_path = os.path.join(gajim.defs.docdir, 'AUTHORS') + elif os.path.isfile('../AUTHORS'): + authors_file_path = '../AUTHORS' + if authors_file_path: + authors = [] + authors_file = open(authors_file_path).read() + authors_file = authors_file.split('\n') + for author in authors_file: + if author == 'CURRENT DEVELOPERS:': + authors.append(_('Current Developers:')) + elif author == 'PAST DEVELOPERS:': + authors.append('\n' + _('Past Developers:')) + elif author != '': # Real author line + authors.append(author) + + thanks_file_path = None + if os.path.isfile(os.path.join(gajim.defs.docdir, 'THANKS')): + thanks_file_path = os.path.join(gajim.defs.docdir, 'THANKS') + elif os.path.isfile('../THANKS'): + thanks_file_path = '../THANKS' + if thanks_file_path: + authors.append('\n' + _('THANKS:')) - text = open('../THANKS').read() - text_splitted = text.split('\n') - text = '\n'.join(text_splitted[:-2]) # remove one english sentence - # and add it manually as translatable - text += '\n%s\n' % _('Last but not least, we would like to thank all ' - 'the package maintainers.') - authors.append(text) + text = open(thanks_file_path).read() + text_splitted = text.split('\n') + text = '\n'.join(text_splitted[:-2]) # remove one english sentence + # and add it manually as translatable + text += '\n%s\n' % _('Last but not least, we would like to thank all ' + 'the package maintainers.') + authors.append(text) - dlg.set_authors(authors) + dlg.set_authors(authors) if gtk.pygtk_version >= (2, 8, 0) and gtk.gtk_version >= (2, 8, 0): dlg.props.wrap_license = True @@ -837,27 +873,31 @@ class FileChooserDialog(gtk.FileChooserDialog): self.set_current_folder(current_folder) else: self.set_current_folder(helpers.get_documents_path()) - - buttons = self.action_area.get_children() - possible_responses = {gtk.STOCK_OPEN: on_response_ok, - gtk.STOCK_SAVE: on_response_ok, - gtk.STOCK_CANCEL: on_response_cancel} - for b in buttons: - for response in possible_responses: - if b.get_label() == response: - if not possible_responses[response]: - b.connect('clicked', self.just_destroy) - elif isinstance(possible_responses[response], tuple): - if len(possible_responses[response]) == 1: - b.connect('clicked', possible_responses[response][0]) - else: - b.connect('clicked', *possible_responses[response]) - else: - b.connect('clicked', possible_responses[response]) - break - + self.response_ok, self.response_cancel = \ + on_response_ok, on_response_cancel + # in gtk+-2.10 clicked signal on some of the buttons in a dialog + # is emitted twice, so we cannot rely on 'clicked' signal + self.connect('response', self.on_dialog_response) self.show_all() + def on_dialog_response(self, dialog, response): + if response in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_CLOSE): + if self.response_cancel: + if isinstance(self.response_cancel, tuple): + self.response_cancel[0](dialog, *self.response_cancel[1:]) + else: + self.response_cancel(dialog) + else: + self.just_destroy(dialog) + elif response == gtk.RESPONSE_OK: + if self.response_ok: + if isinstance(self.response_ok, tuple): + self.response_ok[0](dialog, *self.response_ok[1:]) + else: + self.response_ok(dialog) + else: + self.just_destroy(dialog) + def just_destroy(self, widget): self.destroy() @@ -1015,6 +1055,12 @@ class SubscriptionRequestWindow: xml.signal_autoconnect(self) self.window.show_all() + def prepare_popup_menu(self): + xml = gtkgui_helpers.get_glade('subscription_request_popup_menu.glade') + menu = xml.get_widget('subscription_request_popup_menu') + xml.signal_autoconnect(self) + return menu + def on_close_button_clicked(self, widget): self.window.destroy() @@ -1025,7 +1071,7 @@ class SubscriptionRequestWindow: if self.jid not in gajim.contacts.get_jid_list(self.account): AddNewContactWindow(self.account, self.jid, self.user_nick) - def on_contact_info_button_clicked(self, widget): + def on_contact_info_activate(self, widget): '''ask vcard''' if gajim.interface.instances[self.account]['infos'].has_key(self.jid): gajim.interface.instances[self.account]['infos'][self.jid].window.present() @@ -1039,38 +1085,50 @@ class SubscriptionRequestWindow: gajim.interface.instances[self.account]['infos'][self.jid].xml.\ get_widget('information_notebook').remove_page(0) + def on_start_chat_activate(self, widget): + '''open chat''' + gajim.interface.roster.new_chat_from_jid(self.account, self.jid) + def on_deny_button_clicked(self, widget): '''refuse the request''' gajim.connections[self.account].refuse_authorization(self.jid) self.window.destroy() + def on_actions_button_clicked(self, widget): + '''popup action menu''' + menu = self.prepare_popup_menu() + menu.show_all() + gtkgui_helpers.popup_emoticons_under_button(menu, widget, self.window.window) + + class JoinGroupchatWindow: - def __init__(self, account, server = '', room = '', nick = '', - automatic = False): + def __init__(self, account, room_jid = '', nick = '', automatic = False): '''automatic is a dict like {'invities': []} If automatic is not empty, this means room must be automaticaly configured and when done, invities must be automatically invited''' - if server and room: - jid = room + '@' + server - if jid in gajim.gc_connected[account] and gajim.gc_connected[account][jid]: - ErrorDialog(_('You are already in room %s') % jid) - raise RuntimeError, 'You are already in this room' + if room_jid != '': + if room_jid in gajim.gc_connected[account] and\ + gajim.gc_connected[account][room_jid]: + ErrorDialog(_('You are already in group chat %s') % room_jid) + raise GajimGeneralException, 'You are already in this group chat' self.account = account self.automatic = automatic if nick == '': nick = gajim.nicks[self.account] if gajim.connections[account].connected < 2: ErrorDialog(_('You are not connected to the server'), -_('You can not join a group chat unless you are connected.')) - raise RuntimeError, 'You must be connected to join a groupchat' + _('You can not join a group chat unless you are connected.')) + raise GajimGeneralException, 'You must be connected to join a groupchat' self._empty_required_widgets = [] self.xml = gtkgui_helpers.get_glade('join_groupchat_window.glade') self.window = self.xml.get_widget('join_groupchat_window') - self.xml.get_widget('server_entry').set_text(server) - self.xml.get_widget('room_entry').set_text(room) - self.xml.get_widget('nickname_entry').set_text(nick) + self._room_jid_entry = self.xml.get_widget('room_jid_entry') + self._nickname_entry = self.xml.get_widget('nickname_entry') + + self._room_jid_entry.set_text(room_jid) + self._nickname_entry.set_text(nick) self.xml.signal_autoconnect(self) gajim.interface.instances[account]['join_gc'] = self #now add us to open windows if len(gajim.connections) > 1: @@ -1090,19 +1148,14 @@ _('You can not join a group chat unless you are connected.')) self.recently_combobox.append_text(g) if len(self.recently_groupchat) == 0: self.recently_combobox.set_sensitive(False) - elif server == '' and room == '': + elif room_jid == '': self.recently_combobox.set_active(0) - self.xml.get_widget('room_entry').select_region(0, -1) - elif room and server: + self._room_jid_entry.select_region(0, -1) + elif room_jid != '': self.xml.get_widget('join_button').grab_focus() - self._server_entry = self.xml.get_widget('server_entry') - self._room_entry = self.xml.get_widget('room_entry') - self._nickname_entry = self.xml.get_widget('nickname_entry') - if not self._server_entry.get_text(): - self._empty_required_widgets.append(self._server_entry) - if not self._room_entry.get_text(): - self._empty_required_widgets.append(self._room_entry) + if not self._room_jid_entry.get_text(): + self._empty_required_widgets.append(self._room_jid_entry) if not self._nickname_entry.get_text(): self._empty_required_widgets.append(self._nickname_entry) if len(self._empty_required_widgets): @@ -1129,27 +1182,11 @@ _('You can not join a group chat unless you are connected.')) if len(self._empty_required_widgets) == 0: self.xml.get_widget('join_button').set_sensitive(True) - def on_room_entry_key_press_event(self, widget, event): - # Check for pressed @ and jump to server_entry if found - if event.keyval == gtk.keysyms.at: - self.xml.get_widget('server_entry').grab_focus() - return True - - def on_server_entry_key_press_event(self, widget, event): - # If backspace is pressed in empty server_entry, return to the room entry - backspace = event.keyval == gtk.keysyms.BackSpace - server_entry = self.xml.get_widget('server_entry') - empty = len(server_entry.get_text()) == 0 - if backspace and empty: - self.xml.get_widget('room_entry').grab_focus() - return True - def on_recently_combobox_changed(self, widget): model = widget.get_model() - iter = widget.get_active_iter() - gid = model[iter][0].decode('utf-8') - self.xml.get_widget('room_entry').set_text(gid.split('@')[0]) - self.xml.get_widget('server_entry').set_text(gid.split('@')[1]) + iter_ = widget.get_active_iter() + room_jid = model[iter_][0].decode('utf-8') + self._room_jid_entry.set_text(room_jid) def on_cancel_button_clicked(self, widget): '''When Cancel button is clicked''' @@ -1157,30 +1194,35 @@ _('You can not join a group chat unless you are connected.')) def on_join_button_clicked(self, widget): '''When Join button is clicked''' - nickname = self.xml.get_widget('nickname_entry').get_text().decode( - 'utf-8') - room = self.xml.get_widget('room_entry').get_text().decode('utf-8') - server = self.xml.get_widget('server_entry').get_text().decode('utf-8') + nickname = self._nickname_entry.get_text().decode('utf-8') + room_jid = self._room_jid_entry.get_text().decode('utf-8') password = self.xml.get_widget('password_entry').get_text().decode( 'utf-8') - jid = '%s@%s' % (room, server) try: - jid = helpers.parse_jid(jid) + room_jid = helpers.parse_jid(room_jid) except: - ErrorDialog(_('Invalid room or server name'), - _('The room name or server name has not allowed characters.')) + ErrorDialog(_('Invalid group chat Jabber ID'), + _('The group chat Jabber ID has not allowed characters.')) return - if jid in self.recently_groupchat: - self.recently_groupchat.remove(jid) - self.recently_groupchat.insert(0, jid) + if gajim.interface.msg_win_mgr.has_window(room_jid, self.account): + ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, self.account) + if ctrl.type_id != message_control.TYPE_GC: + ErrorDialog(_('This is not a group chat'), + _('%s is not the name of a group chat.') % room_jid) + return + if room_jid in self.recently_groupchat: + self.recently_groupchat.remove(room_jid) + self.recently_groupchat.insert(0, room_jid) if len(self.recently_groupchat) > 10: self.recently_groupchat = self.recently_groupchat[0:10] - gajim.config.set('recently_groupchat', ' '.join(self.recently_groupchat)) + gajim.config.set('recently_groupchat', + ' '.join(self.recently_groupchat)) if self.automatic: - gajim.automatic_rooms[self.account][jid] = self.automatic - gajim.interface.roster.join_gc_room(self.account, jid, nickname, password) + gajim.automatic_rooms[self.account][room_jid] = self.automatic + gajim.interface.roster.join_gc_room(self.account, room_jid, nickname, + password) self.window.destroy() @@ -1192,7 +1234,7 @@ class NewChatDialog(InputDialog): title = _('Start Chat with account %s') % account else: title = _('Start Chat') - prompt_text = _('Fill in the jid, or nick of the contact you would like\nto send a chat message to:') + prompt_text = _('Fill in the nickname or the Jabber ID of the contact you would like\nto send a chat message to:') InputDialog.__init__(self, title, prompt_text, is_modal = False) self.completion_dict = {} @@ -1240,7 +1282,7 @@ class ChangePasswordDialog: if not account or gajim.connections[account].connected < 2: ErrorDialog(_('You are not connected to the server'), _('Without a connection, you can not change your password.')) - raise RuntimeError, 'You are not connected to the server' + raise GajimGeneralException, 'You are not connected to the server' self.account = account self.xml = gtkgui_helpers.get_glade('change_password_dialog.glade') self.dialog = self.xml.get_widget('change_password_dialog') @@ -1302,7 +1344,8 @@ class PopupNotificationWindow: # default image if not path_to_image: path_to_image = os.path.abspath( - os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'chat_msg_recv.png')) # img to display + os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'chat_msg_recv.png')) # img to display if event_type == _('Contact Signed In'): bg_color = 'limegreen' @@ -1322,7 +1365,7 @@ class PopupNotificationWindow: bg_color = 'tan1' elif event_type == _('Contact Changed Status'): bg_color = 'thistle2' - else: # Unknown event ! Shouldn't happen but deal with it + else: # Unknown event! Shouldn't happen but deal with it bg_color = 'white' popup_bg_color = gtk.gdk.color_parse(bg_color) close_button.modify_bg(gtk.STATE_NORMAL, popup_bg_color) @@ -1421,8 +1464,12 @@ class SingleMessageWindow: self.cancel_button = self.xml.get_widget('cancel_button') self.close_button = self.xml.get_widget('close_button') self.message_tv_buffer.connect('changed', self.update_char_counter) - - self.to_entry.set_text(to) + if type(to) == type([]): + jid = ', '.join( [i[0].jid + '/' + i[0].resource for i in to]) + self.to_entry.set_text(jid) + self.to_entry.set_sensitive(False) + else: + self.to_entry.set_text(to) if gajim.config.get('use_speller') and HAS_GTK_SPELL and action == 'send': try: @@ -1574,22 +1621,27 @@ class SingleMessageWindow: ErrorDialog(_('Connection not available'), _('Please make sure you are connected with "%s".') % self.account) return - to_whom_jid = self.to_entry.get_text().decode('utf-8') - if self.completion_dict.has_key(to_whom_jid): - to_whom_jid = self.completion_dict[to_whom_jid].jid - subject = self.subject_entry.get_text().decode('utf-8') - begin, end = self.message_tv_buffer.get_bounds() - message = self.message_tv_buffer.get_text(begin, end).decode('utf-8') - - if to_whom_jid.find('/announce/') != -1: - gajim.connections[self.account].send_motd(to_whom_jid, subject, - message) - return + if type(self.to) == type([]): + sender_list = [i[0].jid + '/' + i[0].resource for i in self.to] + else: + sender_list = [self.to_entry.get_text().decode('utf-8')] + + for to_whom_jid in sender_list: + if self.completion_dict.has_key(to_whom_jid): + to_whom_jid = self.completion_dict[to_whom_jid].jid + subject = self.subject_entry.get_text().decode('utf-8') + begin, end = self.message_tv_buffer.get_bounds() + message = self.message_tv_buffer.get_text(begin, end).decode('utf-8') + + if to_whom_jid.find('/announce/') != -1: + gajim.connections[self.account].send_motd(to_whom_jid, subject, + message) + return - # FIXME: allow GPG message some day - gajim.connections[self.account].send_message(to_whom_jid, message, - keyID = None, type = 'normal', subject=subject) - + # FIXME: allow GPG message some day + gajim.connections[self.account].send_message(to_whom_jid, message, + keyID = None, type = 'normal', subject=subject) + self.subject_entry.set_text('') # we sent ok, clear the subject self.message_tv_buffer.set_text('') # we sent ok, clear the textview @@ -1725,10 +1777,13 @@ class XMLConsoleWindow: self.input_textview.grab_focus() class PrivacyListWindow: - def __init__(self, account, privacy_list, list_type): - '''list_type can be 0 if list is created or 1 if it id edited''' + '''Window that is used for creating NEW or EDITING already there privacy + lists''' + def __init__(self, account, privacy_list_name, action): + '''action is 'EDIT' or 'NEW' depending on if we create a new priv list + or edit an already existing one''' self.account = account - self.privacy_list = privacy_list + self.privacy_list_name = privacy_list_name # Dicts and Default Values self.active_rule = '' @@ -1740,7 +1795,7 @@ class PrivacyListWindow: self.allow_deny = 'allow' # Connect to glade - self.xml = gtkgui_helpers.get_glade('privacy_list_edit_window.glade') + self.xml = gtkgui_helpers.get_glade('privacy_list_window.glade') self.window = self.xml.get_widget('privacy_list_edit_window') # Add Widgets @@ -1762,10 +1817,9 @@ class PrivacyListWindow: 'privacy_list_default_checkbutton']: self.__dict__[widget_to_add] = self.xml.get_widget(widget_to_add) - # Send translations self.privacy_lists_title_label.set_label( _('Privacy List <b><i>%s</i></b>') % \ - gtkgui_helpers.escape_for_pango_markup(self.privacy_list)) + gtkgui_helpers.escape_for_pango_markup(self.privacy_list_name)) if len(gajim.connections) > 1: title = _('Privacy List for %s') % self.account @@ -1776,9 +1830,9 @@ class PrivacyListWindow: self.open_rule_button.set_sensitive(False) self.privacy_list_active_checkbutton.set_sensitive(False) self.privacy_list_default_checkbutton.set_sensitive(False) + self.list_of_rules_combobox.set_sensitive(False) - # Check if list is created (0) or edited (1) - if list_type == 1: + if action == 'EDIT': self.refresh_rules() count = 0 @@ -1793,22 +1847,20 @@ class PrivacyListWindow: self.add_edit_vbox.set_no_show_all(True) self.window.show_all() self.add_edit_vbox.hide() - + self.xml.signal_autoconnect(self) def on_privacy_list_edit_window_destroy(self, widget): - '''close window''' - if gajim.interface.instances[self.account].has_key('privacy_list_%s' % \ - self.privacy_list): - del gajim.interface.instances[self.account]['privacy_list_%s' % \ - self.privacy_list] + key_name = 'privacy_list_%s' % self.privacy_list_name + if key_name in gajim.interface.instances[self.account]: + del gajim.interface.instances[self.account][key_name] def check_active_default(self, a_d_dict): - if a_d_dict['active'] == self.privacy_list: + if a_d_dict['active'] == self.privacy_list_name: self.privacy_list_active_checkbutton.set_active(True) else: self.privacy_list_active_checkbutton.set_active(False) - if a_d_dict['default'] == self.privacy_list: + if a_d_dict['default'] == self.privacy_list_name: self.privacy_list_default_checkbutton.set_active(True) else: self.privacy_list_default_checkbutton.set_active(False) @@ -1818,11 +1870,10 @@ class PrivacyListWindow: self.global_rules = {} for rule in rules: if rule.has_key('type'): - text_item = 'Order: %s, action: %s, type: %s, value: %s' % \ - (rule['order'], rule['action'], rule['type'], - rule['value']) + text_item = _('Order: %s, action: %s, type: %s, value: %s') % \ + (rule['order'], rule['action'], rule['type'], rule['value']) else: - text_item = 'Order: %s, action: %s' % (rule['order'], + text_item = _('Order: %s, action: %s') % (rule['order'], rule['action']) self.global_rules[text_item] = rule self.list_of_rules_combobox.append_text(text_item) @@ -1845,22 +1896,26 @@ class PrivacyListWindow: gajim.connections[self.account].get_active_default_lists() def refresh_rules(self): - gajim.connections[self.account].get_privacy_list(self.privacy_list) + gajim.connections[self.account].get_privacy_list(self.privacy_list_name) def on_delete_rule_button_clicked(self, widget): tags = [] for rule in self.global_rules: - if rule != \ - self.list_of_rules_combobox.get_active_text().decode('utf-8'): + if rule != self.list_of_rules_combobox.get_active_text(): tags.append(self.global_rules[rule]) gajim.connections[self.account].set_privacy_list( - self.privacy_list, tags) + self.privacy_list_name, tags) self.privacy_list_received(tags) self.add_edit_vbox.hide() + if not tags: # we removed latest rule + if 'privacy_lists' in gajim.interface.instances[self.account]: + win = gajim.interface.instances[self.account]['privacy_lists'] + win.remove_privacy_list_from_combobox(self.privacy_list_name) + win.draw_widgets() def on_open_rule_button_clicked(self, widget): self.add_edit_rule_label.set_label( - _('<b>Edit a rule</b>')) + _('<b>Edit a rule</b>')) active_num = self.list_of_rules_combobox.get_active() if active_num == -1: self.active_rule = '' @@ -1909,29 +1964,31 @@ class PrivacyListWindow: self.edit_queries_send_checkbutton.set_active(True) elif child == 'message': self.edit_send_messages_checkbutton.set_active(True) - + if rule_info['action'] == 'allow': - self.edit_allow_radiobutton.set_active(True) + self.edit_allow_radiobutton.set_active(True) else: - self.edit_deny_radiobutton.set_active(True) + self.edit_deny_radiobutton.set_active(True) self.add_edit_vbox.show() - + def on_privacy_list_active_checkbutton_toggled(self, widget): if widget.get_active(): - gajim.connections[self.account].set_active_list(self.privacy_list) + gajim.connections[self.account].set_active_list( + self.privacy_list_name) else: gajim.connections[self.account].set_active_list(None) def on_privacy_list_default_checkbutton_toggled(self, widget): if widget.get_active(): - gajim.connections[self.account].set_default_list(self.privacy_list) + gajim.connections[self.account].set_default_list( + self.privacy_list_name) else: gajim.connections[self.account].set_default_list(None) def on_new_rule_button_clicked(self, widget): self.reset_fields() self.add_edit_vbox.show() - + def reset_fields(self): self.edit_type_jabberid_entry.set_text('') self.edit_allow_radiobutton.set_active(True) @@ -1950,12 +2007,10 @@ class PrivacyListWindow: def get_current_tags(self): if self.edit_type_jabberid_radiobutton.get_active(): edit_type = 'jid' - edit_value = \ - self.edit_type_jabberid_entry.get_text().decode('utf-8') + edit_value = self.edit_type_jabberid_entry.get_text() elif self.edit_type_group_radiobutton.get_active(): edit_type = 'group' - edit_value = \ - self.edit_type_group_combobox.get_active_text().decode('utf-8') + edit_value = self.edit_type_group_combobox.get_active_text() elif self.edit_type_subscription_radiobutton.get_active(): edit_type = 'subscription' subs = ['none', 'both', 'from', 'to'] @@ -1994,9 +2049,14 @@ class PrivacyListWindow: else: tags.append(current_tags) - gajim.connections[self.account].set_privacy_list(self.privacy_list, tags) + gajim.connections[self.account].set_privacy_list( + self.privacy_list_name, tags) self.privacy_list_received(tags) self.add_edit_vbox.hide() + if 'privacy_lists' in gajim.interface.instances[self.account]: + win = gajim.interface.instances[self.account]['privacy_lists'] + win.add_privacy_list_to_combobox(self.privacy_list_name) + win.draw_widgets() def on_list_of_rules_combobox_changed(self, widget): self.add_edit_vbox.hide() @@ -2011,32 +2071,28 @@ class PrivacyListWindow: if active_bool: self.allow_deny = radiobutton - def on_privacy_list_close_button_clicked(self, widget): + def on_close_button_clicked(self, widget): self.window.destroy() - - def on_privacy_list_refresh_button_clicked(self, widget): - self.refresh_rules() - self.add_edit_vbox.hide() class PrivacyListsWindow: -# To do: UTF-8 ??????? + '''Window that is the main window for Privacy Lists; + we can list there the privacy lists and ask to create a new one + or edit an already there one''' def __init__(self, account): self.account = account - - self.privacy_lists = [] - self.privacy_lists_save = [] - self.xml = gtkgui_helpers.get_glade('privacy_lists_first_window.glade') + self.xml = gtkgui_helpers.get_glade('privacy_lists_window.glade') self.window = self.xml.get_widget('privacy_lists_first_window') for widget_to_add in ['list_of_privacy_lists_combobox', - 'delete_privacy_list_button', 'open_privacy_list_button', - 'new_privacy_list_button', 'new_privacy_list_entry', 'buttons_hbox', - 'privacy_lists_refresh_button', 'close_privacy_lists_window_button']: - self.__dict__[widget_to_add] = self.xml.get_widget(widget_to_add) + 'delete_privacy_list_button', 'open_privacy_list_button', + 'new_privacy_list_button', 'new_privacy_list_entry', + 'privacy_lists_refresh_button', 'close_privacy_lists_window_button']: + self.__dict__[widget_to_add] = self.xml.get_widget( + widget_to_add) - self.draw_privacy_lists_in_combobox() + self.draw_privacy_lists_in_combobox([]) self.privacy_lists_refresh() self.enabled = True @@ -2053,31 +2109,40 @@ class PrivacyListsWindow: self.xml.signal_autoconnect(self) def on_privacy_lists_first_window_destroy(self, widget): - '''close window''' - if gajim.interface.instances[self.account].has_key('privacy_lists'): + if 'privacy_lists' in gajim.interface.instances[self.account]: del gajim.interface.instances[self.account]['privacy_lists'] - def draw_privacy_lists_in_combobox(self): + def remove_privacy_list_from_combobox(self, privacy_list): + if privacy_list not in self.privacy_lists_save: + return + privacy_list_index = self.privacy_lists_save.index(privacy_list) + self.list_of_privacy_lists_combobox.remove_text(privacy_list_index) + self.privacy_lists_save.remove(privacy_list) + + def add_privacy_list_to_combobox(self, privacy_list): + if privacy_list in self.privacy_lists_save: + return + self.list_of_privacy_lists_combobox.append_text(privacy_list) + self.privacy_lists_save.append(privacy_list) + + def draw_privacy_lists_in_combobox(self, privacy_lists): self.list_of_privacy_lists_combobox.set_active(-1) self.list_of_privacy_lists_combobox.get_model().clear() - self.privacy_lists_save = self.privacy_lists - for add_item in self.privacy_lists: - self.list_of_privacy_lists_combobox.append_text(add_item) - if len(self.privacy_lists) == 0: - self.list_of_privacy_lists_combobox.set_sensitive(False) - self.buttons_hbox.set_sensitive(False) - elif len(self.privacy_lists) == 1: - self.list_of_privacy_lists_combobox.set_active(0) + self.privacy_lists_save = [] + for add_item in privacy_lists: + self.add_privacy_list_to_combobox(add_item) + self.draw_widgets() + + def draw_widgets(self): + if len(self.privacy_lists_save) == 0: self.list_of_privacy_lists_combobox.set_sensitive(False) - self.buttons_hbox.set_sensitive(True) + self.open_privacy_list_button.set_sensitive(False) + self.delete_privacy_list_button.set_sensitive(False) else: self.list_of_privacy_lists_combobox.set_sensitive(True) - self.buttons_hbox.set_sensitive(True) self.list_of_privacy_lists_combobox.set_active(0) - self.privacy_lists = [] - - def on_privacy_lists_refresh_button_clicked(self, widget): - self.privacy_lists_refresh() + self.open_privacy_list_button.set_sensitive(True) + self.delete_privacy_list_button.set_sensitive(True) def on_close_button_clicked(self, widget): self.window.destroy() @@ -2087,27 +2152,31 @@ class PrivacyListsWindow: self.list_of_privacy_lists_combobox.get_active()] gajim.connections[self.account].del_privacy_list(active_list) self.privacy_lists_save.remove(active_list) - self.privacy_lists_received({'lists':self.privacy_lists_save}) + self.privacy_lists_received({'lists': self.privacy_lists_save}) def privacy_lists_received(self, lists): if not lists: return + privacy_lists = [] for privacy_list in lists['lists']: - self.privacy_lists += [privacy_list] - self.draw_privacy_lists_in_combobox() + privacy_lists.append(privacy_list) + self.draw_privacy_lists_in_combobox(privacy_lists) def privacy_lists_refresh(self): gajim.connections[self.account].get_privacy_lists() def on_new_privacy_list_button_clicked(self, widget): - name = self.new_privacy_list_entry.get_text().decode('utf-8') - if gajim.interface.instances[self.account].has_key( - 'privacy_list_%s' % name): - gajim.interface.instances[self.account]['privacy_list_%s' % name].\ - window.present() + name = self.new_privacy_list_entry.get_text() + if not name: + ErrorDialog(_('Invalid List Name'), + _('You must enter a name to create a privacy list.')) + return + key_name = 'privacy_list_%s' % name + if gajim.interface.instances[self.account].has_key(key_name): + gajim.interface.instances[self.account][key_name].window.present() else: - gajim.interface.instances[self.account]['privacy_list_%s' % name] = \ - PrivacyListWindow(self.account, name, 0) + gajim.interface.instances[self.account][key_name] = \ + PrivacyListWindow(self.account, name, 'NEW') self.new_privacy_list_entry.set_text('') def on_privacy_lists_refresh_button_clicked(self, widget): @@ -2116,16 +2185,17 @@ class PrivacyListsWindow: def on_open_privacy_list_button_clicked(self, widget): name = self.privacy_lists_save[ self.list_of_privacy_lists_combobox.get_active()] + key_name = 'privacy_list_%s' % name if gajim.interface.instances[self.account].has_key( - 'privacy_list_%s' % name): - gajim.interface.instances[self.account]['privacy_list_%s' % name].\ - window.present() + key_name): + gajim.interface.instances[self.account][key_name].window.present() else: - gajim.interface.instances[self.account]['privacy_list_%s' % name] = \ - PrivacyListWindow(self.account, name, 1) + gajim.interface.instances[self.account][key_name] = \ + PrivacyListWindow(self.account, name, 'EDIT') class InvitationReceivedDialog: - def __init__(self, account, room_jid, contact_jid, password = None, comment = None): + def __init__(self, account, room_jid, contact_jid, password = None, + comment = None): self.room_jid = room_jid self.account = account @@ -2133,8 +2203,8 @@ class InvitationReceivedDialog: self.dialog = xml.get_widget('invitation_received_dialog') #FIXME: use nickname instead of contact_jid - pritext = _('%(contact_jid)s has invited you to %(room_jid)s room') % { - 'room_jid': room_jid, 'contact_jid': contact_jid } + pritext = _('%(contact_jid)s has invited you to group chat %(room_jid)s')\ + % {'room_jid': room_jid, 'contact_jid': contact_jid } label_text = '<big><b>%s</b></big>' % pritext @@ -2155,10 +2225,9 @@ class InvitationReceivedDialog: def on_accept_button_clicked(self, widget): self.dialog.destroy() - room, server = gajim.get_room_name_and_server_from_room_jid(self.room_jid) try: - JoinGroupchatWindow(self.account, server = server, room = room) - except RuntimeError: + JoinGroupchatWindow(self.account, self.room_jid) + except GajimGeneralException: pass class ProgressDialog: @@ -2291,6 +2360,18 @@ class ImageChooserDialog(FileChooserDialog): return widget.get_preview_widget().set_from_pixbuf(pixbuf) +class AvatarChooserDialog(ImageChooserDialog): + def __init__(self, path_to_file = '', on_response_ok = None, + on_response_cancel = None, on_response_clear = None): + ImageChooserDialog.__init__(self, path_to_file, on_response_ok, + on_response_cancel) + button = gtk.Button(None, gtk.STOCK_CLEAR) + if on_response_clear: + button.connect('clicked', on_response_clear) + button.show_all() + self.action_area.pack_start(button) + self.action_area.reorder_child(button, 0) + class AddSpecialNotificationDialog: def __init__(self, jid): '''jid is the jid for which we want to add special notification diff --git a/src/disco.py b/src/disco.py index 1a23fcc467ad0f5714b24c2f6e758c72a30f70ee..7e94ef74e0857d383699f88c8622ab3cdadfd09b 100644 --- a/src/disco.py +++ b/src/disco.py @@ -1,19 +1,9 @@ # -*- coding: utf-8 -*- ## config.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Stéphan Kochen <stephan@kochen.nl> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Stéphan Kochen <stephan@kochen.nl> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -36,10 +26,10 @@ # - def update_actions(self) # - def default_action(self) # - def _find_item(self, jid, node) -# - def _add_item(self, model, jid, node, item, force) -# - def _update_item(self, model, iter, jid, node, item) -# - def _update_info(self, model, iter, jid, node, identities, features, data) -# - def _update_error(self, model, iter, jid, node) +# - def _add_item(self, jid, node, item, force) +# - def _update_item(self, iter, jid, node, item) +# - def _update_info(self, iter, jid, node, identities, features, data) +# - def _update_error(self, iter, jid, node) # # * Should call the super class for this method. # All others do not have to call back to the super class. (but can if they want @@ -60,6 +50,7 @@ import groups from common import gajim from common import xmpp +from common.exceptions import GajimGeneralException # Dictionary mapping category, type pairs to browser class, image pairs. # This is a function, so we can call it after the classes are declared. @@ -133,6 +124,13 @@ class CacheDictionary: def __call__(self): return self.value + def cleanup(self): + for key in self.cache.keys(): + item = self.cache[key] + if item.source: + gobject.source_remove(item.source) + del self.cache[key] + def _expire_timeout(self, key): '''The timeout has expired, remove the object.''' if key in self.cache: @@ -144,8 +142,9 @@ class CacheDictionary: item = self.cache[key] if item.source: gobject.source_remove(item.source) - source = gobject.timeout_add(self.lifetime, self._expire_timeout, key) - item.source = source + if self.lifetime: + source = gobject.timeout_add(self.lifetime, self._expire_timeout, key) + item.source = source def __getitem__(self, key): item = self.cache[key] @@ -217,11 +216,15 @@ class ServicesCache: ServiceCache instance.''' def __init__(self, account): self.account = account - self._items = CacheDictionary(15, getrefresh = False) - self._info = CacheDictionary(15, getrefresh = False) + self._items = CacheDictionary(0, getrefresh = False) + self._info = CacheDictionary(0, getrefresh = False) self._subscriptions = CacheDictionary(5, getrefresh=False) self._cbs = {} + def cleanup(self): + self._items.cleanup() + self._info.cleanup() + def _clean_closure(self, cb, type, addr): # A closure died, clean up cbkey = (type, addr) @@ -393,12 +396,12 @@ class ServicesCache: if self._cbs.has_key(cbkey): del self._cbs[cbkey] -# object is needed so that property() works +# object is needed so that @property works class ServiceDiscoveryWindow(object): '''Class that represents the Services Discovery window.''' def __init__(self, account, jid = '', node = '', address_entry = False, parent = None): - self._account = account + self.account = account self.parent = parent if not jid: jid = gajim.config.get_per('accounts', account, 'hostname') @@ -425,6 +428,7 @@ _('Without a connection, you can not browse available services')) self.xml = gtkgui_helpers.get_glade('service_discovery_window.glade') self.window = self.xml.get_widget('service_discovery_window') self.services_treeview = self.xml.get_widget('services_treeview') + self.model = None # This is more reliable than the cursor-changed signal. selection = self.services_treeview.get_selection() selection.connect_after('changed', @@ -455,7 +459,6 @@ _('Without a connection, you can not browse available services')) liststore = gtk.ListStore(str) self.address_comboboxentry.set_model(liststore) - self.address_comboboxentry.set_text_column(0) self.latest_addresses = gajim.config.get( 'latest_disco_addresses').split() if jid in self.latest_addresses: @@ -476,30 +479,35 @@ _('Without a connection, you can not browse available services')) self.travel(jid, node) self.window.show_all() + @property def _get_account(self): - return self._account + return self.account + @property def _set_account(self, value): - self._account = value + self.account = value self.cache.account = value if self.browser: self.browser.account = value - account = property(_get_account, _set_account) def _initial_state(self): '''Set some initial state on the window. Separated in a method because it's handy to use within browser's cleanup method.''' self.progressbar.hide() - self.window.set_title(_('Service Discovery using account %s') % self.account) + title_text = _('Service Discovery using account %s') % self.account + self.window.set_title(title_text) self._set_window_banner_text(_('Service Discovery')) - # FIXME: use self.banner_icon.clear() when we switch to GTK 2.8 - self.banner_icon.set_from_file(None) - self.banner_icon.hide() # Just clearing it doesn't work + if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0): + self.banner_icon.clear() + else: + self.banner_icon.set_from_file(None) + self.banner_icon.hide() # Just clearing it doesn't work def _set_window_banner_text(self, text, text_after = None): theme = gajim.config.get('roster_theme') bannerfont = gajim.config.get_per('themes', theme, 'bannerfont') - bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs') + bannerfontattrs = gajim.config.get_per('themes', theme, + 'bannerfontattrs') if bannerfont: font = pango.FontDescription(bannerfont) @@ -592,6 +600,7 @@ _('Without a connection, you can not browse available services')) self.browser = None self.window.destroy() + self.cache.cleanup() for child in self.children[:]: child.parent = None if chain: @@ -724,9 +733,9 @@ class AgentBrowser: note that the first two columns should ALWAYS be of type string and contain the JID and node of the item respectively.''' # JID, node, name, address - model = gtk.ListStore(str, str, str, str) - model.set_sort_column_id(3, gtk.SORT_ASCENDING) - self.window.services_treeview.set_model(model) + self.model = gtk.ListStore(str, str, str, str) + self.model.set_sort_column_id(3, gtk.SORT_ASCENDING) + self.window.services_treeview.set_model(self.model) # Name column col = gtk.TreeViewColumn(_('Name')) renderer = gtk.CellRendererText() @@ -744,7 +753,7 @@ class AgentBrowser: self.window.services_treeview.set_headers_visible(True) def _clean_treemodel(self): - self.window.services_treeview.get_model().clear() + self.model.clear() for col in self.window.services_treeview.get_columns(): self.window.services_treeview.remove_column(col) self.window.services_treeview.set_headers_visible(False) @@ -876,8 +885,7 @@ class AgentBrowser: def browse(self, force = False): '''Fill the treeview with agents, fetching the info if necessary.''' - model = self.window.services_treeview.get_model() - model.clear() + self.model.clear() self._total_items = self._progress = 0 self.window.progressbar.show() self._pulse_timeout = gobject.timeout_add(250, self._pulse_timeout_cb) @@ -894,21 +902,21 @@ class AgentBrowser: def _find_item(self, jid, node): '''Check if an item is already in the treeview. Return an iter to it if so, None otherwise.''' - model = self.window.services_treeview.get_model() - iter = model.get_iter_root() + iter = self.model.get_iter_root() while iter: - cjid = model.get_value(iter, 0).decode('utf-8') - cnode = model.get_value(iter, 1).decode('utf-8') + cjid = self.model.get_value(iter, 0).decode('utf-8') + cnode = self.model.get_value(iter, 1).decode('utf-8') if jid == cjid and node == cnode: break - iter = model.iter_next(iter) + iter = self.model.iter_next(iter) if iter: return iter return None def _agent_items(self, jid, node, items, force): '''Callback for when we receive a list of agent items.''' - model = self.window.services_treeview.get_model() + self.model.clear() + self._total_items = 0 gobject.source_remove(self._pulse_timeout) self.window.progressbar.hide() # The server returned an error @@ -920,53 +928,48 @@ class AgentBrowser: _('This service does not contain any items to browse.')) return # We got a list of items + self.window.services_treeview.set_model(None) for item in items: jid = item['jid'] node = item.get('node', '') - iter = self._find_item(jid, node) - if iter: - # Already in the treeview - self._update_item(model, iter, jid, node, item) - else: - # Not in the treeview - self._total_items += 1 - self._add_item(model, jid, node, item, force) + self._total_items += 1 + self._add_item(jid, node, item, force) + self.window.services_treeview.set_model(self.model) def _agent_info(self, jid, node, identities, features, data): '''Callback for when we receive info about an agent's item.''' addr = get_agent_address(jid, node) - model = self.window.services_treeview.get_model() iter = self._find_item(jid, node) if not iter: # Not in the treeview, stop return if identities == 0: # The server returned an error - self._update_error(model, iter, jid, node) + self._update_error(iter, jid, node) else: # We got our info - self._update_info(model, iter, jid, node, + self._update_info(iter, jid, node, identities, features, data) self.update_actions() - def _add_item(self, model, jid, node, item, force): + def _add_item(self, jid, node, item, force): '''Called when an item should be added to the model. The result of a disco#items query.''' - model.append((jid, node, item.get('name', ''), + self.model.append((jid, node, item.get('name', ''), get_agent_address(jid, node))) - def _update_item(self, model, iter, jid, node, item): + def _update_item(self, iter, jid, node, item): '''Called when an item should be updated in the model. The result of a disco#items query. (seldom)''' if item.has_key('name'): - model[iter][2] = item['name'] + self.model[iter][2] = item['name'] - def _update_info(self, model, iter, jid, node, identities, features, data): + def _update_info(self, iter, jid, node, identities, features, data): '''Called when an item should be updated in the model with further info. The result of a disco#info query.''' - model[iter][2] = identities[0].get('name', '') + self.model[iter][2] = identities[0].get('name', '') - def _update_error(self, model, iter, jid, node): + def _update_error(self, iter, jid, node): '''Called when a disco#info query failed for an item.''' pass @@ -1050,14 +1053,12 @@ class ToplevelAgentBrowser(AgentBrowser): # These are all callbacks to make tooltips work def on_treeview_leave_notify_event(self, widget, event): - model = widget.get_model() props = widget.get_path_at_pos(int(event.x), int(event.y)) if self.tooltip.timeout > 0: if not props or self.tooltip.id == props[0]: self.tooltip.hide_tooltip() def on_treeview_motion_notify_event(self, widget, event): - model = widget.get_model() props = widget.get_path_at_pos(int(event.x), int(event.y)) if self.tooltip.timeout > 0: if not props or self.tooltip.id != props[0]: @@ -1066,12 +1067,12 @@ class ToplevelAgentBrowser(AgentBrowser): [row, col, x, y] = props iter = None try: - iter = model.get_iter(row) + iter = self.model.get_iter(row) except: self.tooltip.hide_tooltip() return - jid = model[iter][0] - state = model[iter][4] + jid = self.model[iter][0] + state = self.model[iter][4] # Not a category, and we have something to say about state if jid and state > 0 and \ (self.tooltip.timeout == 0 or self.tooltip.id != props[0]): @@ -1088,10 +1089,10 @@ class ToplevelAgentBrowser(AgentBrowser): # JID, node, icon, description, state # State means 2 when error, 1 when fetching, 0 when succes. view = self.window.services_treeview - model = gtk.TreeStore(str, str, gtk.gdk.Pixbuf, str, int) - model.set_sort_func(4, self._treemodel_sort_func) - model.set_sort_column_id(4, gtk.SORT_ASCENDING) - view.set_model(model) + self.model = gtk.TreeStore(str, str, gtk.gdk.Pixbuf, str, int) + self.model.set_sort_func(4, self._treemodel_sort_func) + self.model.set_sort_column_id(4, gtk.SORT_ASCENDING) + view.set_model(self.model) col = gtk.TreeViewColumn() # Icon Renderer @@ -1195,16 +1196,10 @@ class ToplevelAgentBrowser(AgentBrowser): if not iter: return service = model[iter][0].decode('utf-8') - if service.find('@') != -1: - services = service.split('@', 1) - room = services[0] - service = services[1] - else: - room = '' if not gajim.interface.instances[self.account].has_key('join_gc'): try: - dialogs.JoinGroupchatWindow(self.account, service, room) - except RuntimeError: + dialogs.JoinGroupchatWindow(self.account, service) + except GajimGeneralException: pass else: gajim.interface.instances[self.account]['join_gc'].window.present() @@ -1251,10 +1246,11 @@ class ToplevelAgentBrowser(AgentBrowser): # We can register this agent registered_transports = [] jid_list = gajim.contacts.get_jid_list(self.account) - for j in jid_list: - contact = gajim.contacts.get_first_contact_from_jid(self.account, j) + for jid in jid_list: + contact = gajim.contacts.get_first_contact_from_jid( + self.account, jid) if _('Transports') in contact.groups: - registered_transports.append(j) + registered_transports.append(jid) if jid in registered_transports: self.register_button.set_label(_('_Edit')) else: @@ -1333,41 +1329,38 @@ class ToplevelAgentBrowser(AgentBrowser): def _create_category(self, cat, type=None): '''Creates a category row.''' - model = self.window.services_treeview.get_model() cat, prio = self._friendly_category(cat, type) - return model.append(None, ('', '', None, cat, prio)) + return self.model.append(None, ('', '', None, cat, prio)) def _find_category(self, cat, type=None): '''Looks up a category row and returns the iterator to it, or None.''' - model = self.window.services_treeview.get_model() cat, prio = self._friendly_category(cat, type) - iter = model.get_iter_root() + iter = self.model.get_iter_root() while iter: - if model.get_value(iter, 3).decode('utf-8') == cat: + if self.model.get_value(iter, 3).decode('utf-8') == cat: break - iter = model.iter_next(iter) + iter = self.model.iter_next(iter) if iter: return iter return None def _find_item(self, jid, node): - model = self.window.services_treeview.get_model() iter = None - cat_iter = model.get_iter_root() + cat_iter = self.model.get_iter_root() while cat_iter and not iter: - iter = model.iter_children(cat_iter) + iter = self.model.iter_children(cat_iter) while iter: - cjid = model.get_value(iter, 0).decode('utf-8') - cnode = model.get_value(iter, 1).decode('utf-8') + cjid = self.model.get_value(iter, 0).decode('utf-8') + cnode = self.model.get_value(iter, 1).decode('utf-8') if jid == cjid and node == cnode: break - iter = model.iter_next(iter) - cat_iter = model.iter_next(cat_iter) + iter = self.model.iter_next(iter) + cat_iter = self.model.iter_next(cat_iter) if iter: return iter return None - def _add_item(self, model, jid, node, item, force): + def _add_item(self, jid, node, item, force): # Row text addr = get_agent_address(jid, node) if item.has_key('name'): @@ -1391,21 +1384,21 @@ class ToplevelAgentBrowser(AgentBrowser): cat = self._find_category(*cat_args) if not cat: cat = self._create_category(*cat_args) - model.append(cat, (item['jid'], item.get('node', ''), pix, descr, 1)) + self.model.append(cat, (item['jid'], item.get('node', ''), pix, descr, 1)) self._expand_all() # Grab info on the service self.cache.get_info(jid, node, self._agent_info, force = force) self._update_progressbar() - def _update_item(self, model, iter, jid, node, item): + def _update_item(self, iter, jid, node, item): addr = get_agent_address(jid, node) if item.has_key('name'): descr = "<b>%s</b>\n%s" % (item['name'], addr) else: descr = "<b>%s</b>" % addr - model[iter][3] = descr + self.model[iter][3] = descr - def _update_info(self, model, iter, jid, node, identities, features, data): + def _update_info(self, iter, jid, node, identities, features, data): addr = get_agent_address(jid, node) name = identities[0].get('name', '') if name: @@ -1427,32 +1420,32 @@ class ToplevelAgentBrowser(AgentBrowser): break # Check if we have to move categories - old_cat_iter = model.iter_parent(iter) - old_cat = model.get_value(old_cat_iter, 3).decode('utf-8') - if model.get_value(old_cat_iter, 3) == cat: + old_cat_iter = self.model.iter_parent(iter) + old_cat = self.model.get_value(old_cat_iter, 3).decode('utf-8') + if self.model.get_value(old_cat_iter, 3) == cat: # Already in the right category, just update - model[iter][2] = pix - model[iter][3] = descr - model[iter][4] = 0 + self.model[iter][2] = pix + self.model[iter][3] = descr + self.model[iter][4] = 0 return # Not in the right category, move it. - model.remove(iter) + self.model.remove(iter) # Check if the old category is empty - if not model.iter_is_valid(old_cat_iter): + if not self.model.iter_is_valid(old_cat_iter): old_cat_iter = self._find_category(old_cat) - if not model.iter_children(old_cat_iter): - model.remove(old_cat_iter) + if not self.model.iter_children(old_cat_iter): + self.model.remove(old_cat_iter) cat_iter = self._find_category(cat, type) if not cat_iter: cat_iter = self._create_category(cat, type) - model.append(cat_iter, (jid, node, pix, descr, 0)) + self.model.append(cat_iter, (jid, node, pix, descr, 0)) self._expand_all() - def _update_error(self, model, iter, jid, node): + def _update_error(self, iter, jid, node): addr = get_agent_address(jid, node) - model[iter][4] = 2 + self.model[iter][4] = 2 self._progress += 1 self._update_progressbar() @@ -1466,11 +1459,13 @@ class MucBrowser(AgentBrowser): # JID, node, name, users, description, fetched # This is rather long, I'd rather not use a data_func here though. # Users is a string, because want to be able to leave it empty. - model = gtk.ListStore(str, str, str, str, str, bool) - model.set_sort_column_id(2, gtk.SORT_ASCENDING) - self.window.services_treeview.set_model(model) + self.model = gtk.ListStore(str, str, str, str, str, bool) + self.model.set_sort_column_id(2, gtk.SORT_ASCENDING) + self.window.services_treeview.set_model(self.model) # Name column col = gtk.TreeViewColumn(_('Name')) + col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + col.set_fixed_width(100) renderer = gtk.CellRendererText() col.pack_start(renderer) col.set_attributes(renderer, text = 2) @@ -1490,6 +1485,13 @@ class MucBrowser(AgentBrowser): col.set_attributes(renderer, text = 4) self.window.services_treeview.insert_column(col, -1) col.set_resizable(True) + # Id column + col = gtk.TreeViewColumn(_('Id')) + renderer = gtk.CellRendererText() + col.pack_start(renderer) + col.set_attributes(renderer, text = 0) + self.window.services_treeview.insert_column(col, -1) + col.set_resizable(True) self.window.services_treeview.set_headers_visible(True) # Source id for idle callback used to start disco#info queries. self._fetch_source = None @@ -1499,7 +1501,8 @@ class MucBrowser(AgentBrowser): self.vadj = self.window.services_scrollwin.get_property('vadjustment') self.vadj_cbid = self.vadj.connect('value-changed', self.on_scroll) # And to size changes - self.size_cbid = self.window.services_scrollwin.connect('size-allocate', self.on_scroll) + self.size_cbid = self.window.services_scrollwin.connect( + 'size-allocate', self.on_scroll) def _clean_treemodel(self): if self.size_cbid: @@ -1528,16 +1531,12 @@ class MucBrowser(AgentBrowser): if not iter: return service = model[iter][0].decode('utf-8') - if service.find('@') != -1: - services = service.split('@', 1) - room = services[0] - service = services[1] - else: - room = model[iter][1].decode('utf-8') - if not gajim.interface.instances[self.account].has_key('join_gc'): + room = model[iter][1].decode('utf-8') + if 'join_gc' not in gajim.interface.instances[self.account]: try: - dialogs.JoinGroupchatWindow(self.account, service, room) - except RuntimeError: + room_jid = '%s@%s' % (service, room) + dialogs.JoinGroupchatWindow(self.account, service) + except GajimGeneralException: pass else: gajim.interface.instances[self.account]['join_gc'].window.present() @@ -1572,7 +1571,6 @@ class MucBrowser(AgentBrowser): # Prevent a silly warning, try again in a bit. self._fetch_source = gobject.timeout_add(100, self._start_info_query) return - model = view.get_model() # We have to do this in a pygtk <2.8 compatible way :/ #start, end = self.window.services_treeview.get_visible_range() rect = view.get_visible_rect() @@ -1581,7 +1579,7 @@ class MucBrowser(AgentBrowser): try: sx, sy = view.tree_to_widget_coords(rect.x, rect.y) spath = view.get_path_at_pos(sx, sy)[0] - iter = model.get_iter(spath) + iter = self.model.get_iter(spath) except TypeError: self._fetch_source = None return @@ -1595,14 +1593,14 @@ class MucBrowser(AgentBrowser): except TypeError: # We're at the end of the model, we can leave end=None though. pass - while iter and model.get_path(iter) != end: - if not model.get_value(iter, 5): - jid = model.get_value(iter, 0).decode('utf-8') - node = model.get_value(iter, 1).decode('utf-8') + while iter and self.model.get_path(iter) != end: + if not self.model.get_value(iter, 5): + jid = self.model.get_value(iter, 0).decode('utf-8') + node = self.model.get_value(iter, 1).decode('utf-8') self.cache.get_info(jid, node, self._agent_info) self._fetch_source = True return - iter = model.iter_next(iter) + iter = self.model.iter_next(iter) self._fetch_source = None def _channel_altinfo(self, jid, node, items, name = None): @@ -1623,22 +1621,21 @@ class MucBrowser(AgentBrowser): self._fetch_source = None return else: - model = self.window.services_treeview.get_model() iter = self._find_item(jid, node) if iter: if name: - model[iter][2] = name - model[iter][3] = len(items) # The number of users - model[iter][5] = True + self.model[iter][2] = name + self.model[iter][3] = len(items) # The number of users + self.model[iter][5] = True self._fetch_source = None self._query_visible() - def _add_item(self, model, jid, node, item, force): - model.append((jid, node, item.get('name', ''), '', '', False)) + def _add_item(self, jid, node, item, force): + self.model.append((jid, node, item.get('name', ''), '', '', False)) if not self._fetch_source: self._fetch_source = gobject.idle_add(self._start_info_query) - def _update_info(self, model, iter, jid, node, identities, features, data): + def _update_info(self, iter, jid, node, identities, features, data): name = identities[0].get('name', '') for form in data: typefield = form.getField('FORM_TYPE') @@ -1648,14 +1645,14 @@ class MucBrowser(AgentBrowser): users = form.getField('muc#roominfo_occupants') descr = form.getField('muc#roominfo_description') if users: - model[iter][3] = users.getValue() + self.model[iter][3] = users.getValue() if descr: - model[iter][4] = descr.getValue() + self.model[iter][4] = descr.getValue() # Only set these when we find a form with additional info # Some servers don't support forms and put extra info in # the name attribute, so we preserve it in that case. - model[iter][2] = name - model[iter][5] = True + self.model[iter][2] = name + self.model[iter][5] = True break else: # We didn't find a form, switch to alternate query mode @@ -1665,7 +1662,7 @@ class MucBrowser(AgentBrowser): self._fetch_source = None self._query_visible() - def _update_error(self, model, iter, jid, node): + def _update_error(self, iter, jid, node): # switch to alternate query mode self.cache.get_items(jid, node, self._channel_altinfo) diff --git a/src/filetransfers_window.py b/src/filetransfers_window.py index fc6a3cf2567dd64e1ac07c3f74181951df1fb3fa..02ecc9cb9aff967bc84a6497612f301f96e2ad34 100644 --- a/src/filetransfers_window.py +++ b/src/filetransfers_window.py @@ -213,7 +213,7 @@ class FileTransfersWindow: _('Connection with peer cannot be established.')) self.tree.get_selection().unselect_all() - def show_stopped(self, jid, file_props): + def show_stopped(self, jid, file_props, error_msg = ''): self.window.present() self.window.window.focus() if file_props['type'] == 'r': @@ -222,6 +222,8 @@ _('Connection with peer cannot be established.')) file_name = file_props['name'] sectext = '\t' + _('Filename: %s') % file_name sectext += '\n\t' + _('Recipient: %s') % jid + if error_msg: + sectext += '\n\t' + _('Error message: %s') % error_msg dialogs.ErrorDialog(_('File transfer stopped by the contact of the other side'), \ sectext) self.tree.get_selection().unselect_all() @@ -244,11 +246,16 @@ _('Connection with peer cannot be established.')) gtk.RESPONSE_OK, True, # select multiple true as we can select many files to send gajim.config.get('last_send_dir'), + on_response_ok = on_ok, + on_response_cancel = lambda e:dialog.destroy() ) - btn = dialog.add_button(_('_Send'), gtk.RESPONSE_OK) - btn.set_use_stock(True) # FIXME: add send icon to this button (JUMP_TO) - btn.connect('clicked', on_ok) + btn = gtk.Button(_('_Send')) + btn.set_property('can-default', True) + # FIXME: add send icon to this button (JUMP_TO) + dialog.add_action_widget(btn, gtk.RESPONSE_OK) + dialog.set_default_response(gtk.RESPONSE_OK) + btn.show() def send_file(self, account, contact, file_path): ''' start the real transfer(upload) of the file ''' @@ -448,8 +455,10 @@ _('Connection with peer cannot be established.')) for ev_type in ('file-error', 'file-completed', 'file-request-error', 'file-send-error', 'file-stopped'): for event in gajim.events.get_events(account, jid, [ev_type]): - if event.parameters[1]['sid'] == file_props['sid']: + if event.parameters['sid'] == file_props['sid']: gajim.events.remove_events(account, jid, event) + gajim.interface.roster.draw_contact(jid, account) + gajim.interface.roster.show_title() del(self.files_props[sid[0]][sid[1:]]) del(file_props) @@ -558,7 +567,7 @@ _('Connection with peer cannot be established.')) (file_path, file_name) = os.path.split(file_props['file-name']) else: file_name = file_props['name'] - text_props = file_name + '\n' + text_props = gtkgui_helpers.escape_for_pango_markup(file_name) + '\n' text_props += contact.get_shown_name() self.model.set(iter, 1, text_labels, 2, text_props, C_SID, file_props['type'] + file_props['sid']) diff --git a/src/gajim-remote.py b/src/gajim-remote.py index 563cc121ac284eb3aa70e8a3106c6c8db183b7f8..5548b80aa63e26b5de4043552ad067c6de5998e9 100755 --- a/src/gajim-remote.py +++ b/src/gajim-remote.py @@ -1,22 +1,8 @@ -#!/bin/sh -''':' -exec python -OOt "$0" ${1+"$@"} -' ''' -## scripts/gajim-remote.py +#!/usr/bin/env python ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Dimitur Kirov <dkirov@gmail.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -28,7 +14,7 @@ exec python -OOt "$0" ${1+"$@"} ## GNU General Public License for more details. ## -# gajim-remote help will show you the DBUS API of Gajim +# gajim-remote help will show you the D-BUS API of Gajim import sys import locale @@ -51,13 +37,11 @@ def send_error(error_message): try: import dbus -except: - raise exceptions.DbusNotSupported - -_version = getattr(dbus, 'version', (0, 20, 0)) -if _version[1] >= 41: import dbus.service import dbus.glib +except: + print str(exceptions.DbusNotSupported()) + sys.exit(1) OBJ_PATH = '/org/gajim/dbus/RemoteObject' INTERFACE = 'org.gajim.dbus.RemoteInterface' @@ -90,8 +74,8 @@ class GajimRemote: _('Shows or hides the roster window'), [] ], - 'show_next_unread': [ - _('Popups a window with the next unread message'), + 'show_next_pending_event': [ + _('Popups a window with the next pending event'), [] ], 'list_contacts': [ @@ -320,14 +304,8 @@ class GajimRemote: except: raise exceptions.SessionBusNotPresent - if _version[1] >= 30: - obj = self.sbus.get_object(SERVICE, OBJ_PATH) - interface = dbus.Interface(obj, INTERFACE) - elif _version[1] < 30: - self.service = self.sbus.get_service(SERVICE) - interface = self.service.get_object(OBJ_PATH, INTERFACE) - else: - send_error(_('Unknown D-Bus version: %s') % _version[1]) + obj = self.sbus.get_object(SERVICE, OBJ_PATH) + interface = dbus.Interface(obj, INTERFACE) # get the function asked self.method = interface.__getattr__(self.command) @@ -447,10 +425,7 @@ class GajimRemote: ''' calls self.method with arguments from sys.argv[2:] ''' args = sys.argv[2:] args = [i.decode(PREFERRED_ENCODING) for i in sys.argv[2:]] - if _version[1] >= 41: - args = [dbus.String(i) for i in args] - else: - args = [i.encode('UTF-8') for i in sys.argv[2:]] + args = [dbus.String(i) for i in args] try: res = self.method(*args) return res diff --git a/src/gajim.py b/src/gajim.py index 1720d111a1666d0d829db2d3f372f36e110a7582..a2e66fd5e437736806c587c20632a4fa0d379562 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1,23 +1,11 @@ -#!/bin/sh -''':' -exec python -OOt "$0" ${1+"$@"} -' ''' +#!/usr/bin/env python ## gajim.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Dimitur Kirov <dkirov@gmail.com> -## - Travis Shirk <travis@pobox.com> ## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com> +## Copyright (C) 2005 Travis Shirk <travis@pobox.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -41,6 +29,8 @@ from chat_control import ChatControlBase from atom_window import AtomWindow from common import exceptions +from common.zeroconf import connection_zeroconf +from common import dbus_support if os.name == 'posix': # dl module is Unix Only try: # rename the process name to gajim @@ -82,16 +72,14 @@ except exceptions.PysqliteNotAvailable, e: if os.name == 'nt': try: import winsound # windows-only built-in module for playing wav - import win32api - import win32con except: pritext = _('Gajim needs pywin32 to run') sectext = _('Please make sure that Pywin32 is installed on your system. You can get it at %s') % 'http://sourceforge.net/project/showfiles.php?group_id=78018' if pritext: dlg = gtk.MessageDialog(None, - gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL, - gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext) + gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL, + gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext) dlg.format_secondary_text(sectext) dlg.run() @@ -144,23 +132,16 @@ for o, a in opts: elif o in ('-p', '--profile'): # gajim --profile name profile = a -pid_filename = os.path.expanduser('~/.gajim/gajim') -config_filename = os.path.expanduser('~/.gajim/config') -if os.name == 'nt': - try: - # Documents and Settings\[User Name]\Application Data\Gajim\logs - config_filename = os.environ['appdata'] + '/Gajim/config' - pid_filename = os.environ['appdata'] + '/Gajim/gajim' - except KeyError: - # win9x so ./config - config_filename = 'config' - pid_filename = 'gajim' - -if profile: - config_filename += '.%s' % profile - pid_filename += '.%s' % profile - -pid_filename += '.pid' +import locale +profile = unicode(profile, locale.getpreferredencoding()) + +import common.configpaths +common.configpaths.init_profile(profile) +gajimpaths = common.configpaths.gajimpaths + +pid_filename = gajimpaths['PID_FILE'] +config_filename = gajimpaths['CONFIG_FILE'] + import dialogs if os.path.exists(pid_filename): path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png') @@ -194,7 +175,6 @@ atexit.register(on_exit) parser = optparser.OptionsParser(config_filename) import roster_window -import systray import profile_window import config @@ -217,7 +197,7 @@ class GlibIdleQueue(idlequeue.IdleQueue): Start listening for events from fd ''' res = gobject.io_add_watch(fd, flags, self.process_events, - priority=gobject.PRIORITY_LOW) + priority=gobject.PRIORITY_LOW) # store the id of the watch, so that we can remove it on unplug self.events[fd] = res @@ -274,7 +254,7 @@ class Interface: on_response_no = (response, account, data[3], 'no')) def handle_event_error_answer(self, account, array): - #('ERROR_ANSWER', account, (id, jid_from. errmsg, errcode)) + #('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode)) id, jid_from, errmsg, errcode = array if unicode(errcode) in ('403', '406') and id: # show the error dialog @@ -286,7 +266,7 @@ class Interface: file_props = ft.files_props['s'][sid] file_props['error'] = -4 self.handle_event_file_request_error(account, - (jid_from, file_props)) + (jid_from, file_props, errmsg)) conn = gajim.connections[account] conn.disconnect_transfer(file_props) return @@ -341,7 +321,8 @@ class Interface: # Inform all controls for this account of the connection state change for ctrl in self.msg_win_mgr.get_controls(): if ctrl.account == account: - if status == 'offline': + if status == 'offline' or (status == 'invisible' and \ + gajim.connections[account].is_zeroconf): ctrl.got_disconnected() else: # Other code rejoins all GCs, so we don't do it here @@ -422,12 +403,13 @@ class Interface: elif contact1.show in statuss: old_show = statuss.index(contact1.show) if (resources != [''] and (len(lcontact) != 1 or - lcontact[0].show != 'offline')) and jid.find('@') > 0: + lcontact[0].show != 'offline')) and jid.find('@') > 0: old_show = 0 contact1 = gajim.contacts.copy_contact(contact1) lcontact.append(contact1) contact1.resource = resource - if contact1.jid.find('@') > 0 and len(lcontact) == 1: # It's not an agent + if contact1.jid.find('@') > 0 and len(lcontact) == 1: + # It's not an agent if old_show == 0 and new_show > 1: if not contact1.jid in gajim.newly_added[account]: gajim.newly_added[account].append(contact1.jid) @@ -459,6 +441,7 @@ class Interface: if ji in jid_list: # Update existing iter self.roster.draw_contact(ji, account) + self.roster.draw_group(_('Transports'), account) # transport just signed in/out, don't show popup notifications # for 30s account_ji = account + '/' + ji @@ -509,18 +492,23 @@ class Interface: def handle_event_msg(self, account, array): # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject, - # chatstate, msg_id, composing_jep, user_nick)) user_nick is JEP-0172 + # chatstate, msg_id, composing_jep, user_nick, xhtml)) + # user_nick is JEP-0172 full_jid_with_resource = array[0] jid = gajim.get_jid_without_resource(full_jid_with_resource) resource = gajim.get_resource_from_jid(full_jid_with_resource) message = array[1] + encrypted = array[3] msg_type = array[4] subject = array[5] chatstate = array[6] msg_id = array[7] composing_jep = array[8] + xhtml = array[10] + if gajim.config.get('ignore_incoming_xhtml'): + xhtml = None if gajim.jid_is_transport(jid): jid = jid.replace('@', '') @@ -530,6 +518,7 @@ class Interface: message_control.TYPE_GC: # It's a Private message pm = True + msg_type = 'pm' chat_control = None jid_of_control = full_jid_with_resource @@ -544,7 +533,8 @@ class Interface: # unknow contact or offline message jid_of_control = jid chat_control = self.msg_win_mgr.get_control(jid, account) - elif highest_contact and resource != highest_contact.resource: + elif highest_contact and resource != highest_contact.resource and \ + highest_contact.show != 'offline': jid_of_control = full_jid_with_resource chat_control = None elif not pm: @@ -575,39 +565,51 @@ class Interface: contact.msg_id = msg_id # THIS MUST BE AFTER chatstates handling - # AND BEFORE playsound (else we here sounding on chatstates!) + # AND BEFORE playsound (else we ear sounding on chatstates!) if not message: # empty message text return if gajim.config.get('ignore_unknown_contacts') and \ not gajim.contacts.get_contact(account, jid) and not pm: return - + if not contact: + # contact is not in the roster, create a fake one to display + # notification + contact = common.contacts.Contact(jid = jid, resource = resource) advanced_notif_num = notify.get_advanced_notification('message_received', account, contact) # Is it a first or next message received ? first = False - if not chat_control and not gajim.events.get_events(account, - jid_of_control, ['chat']): - # It's a first message and not a Private Message + if msg_type == 'normal': + if not gajim.events.get_events(account, jid, ['normal']): + first = True + elif not chat_control and not gajim.events.get_events(account, + jid_of_control, [msg_type]): # msg_type can be chat or pm first = True if pm: nickname = resource - msg_type = 'pm' - groupchat_control.on_private_message(nickname, message, array[2]) + groupchat_control.on_private_message(nickname, message, array[2], + xhtml) else: # array: (jid, msg, time, encrypted, msg_type, subject) - self.roster.on_message(jid, message, array[2], account, array[3], - msg_type, subject, resource, msg_id, array[9], advanced_notif_num) + if encrypted: + self.roster.on_message(jid, message, array[2], account, array[3], + msg_type, subject, resource, msg_id, array[9], + advanced_notif_num) + else: + # xhtml in last element + self.roster.on_message(jid, message, array[2], account, array[3], + msg_type, subject, resource, msg_id, array[9], + advanced_notif_num, xhtml = xhtml) nickname = gajim.get_name_from_jid(account, jid) # Check and do wanted notifications msg = message if subject: msg = _('Subject: %s') % subject + '\n' + msg - notify.notify('new_message', jid, account, [msg_type, first, nickname, - msg], advanced_notif_num) + notify.notify('new_message', jid_of_control, account, [msg_type, + first, nickname, msg], advanced_notif_num) if self.remote_ctrl: self.remote_ctrl.raise_signal('NewMessage', (account, array)) @@ -617,33 +619,35 @@ class Interface: full_jid_with_resource = array[0] jids = full_jid_with_resource.split('/', 1) jid = jids[0] - gcs = self.msg_win_mgr.get_controls(message_control.TYPE_GC) - for gc_control in gcs: - if jid == gc_control.contact.jid: - if len(jids) > 1: # it's a pm - nick = jids[1] - if not self.msg_win_mgr.get_control(full_jid_with_resource, account): - tv = gc_control.list_treeview - model = tv.get_model() - i = gc_control.get_contact_iter(nick) - if i: - show = model[i][3] - else: - show = 'offline' - gc_c = gajim.contacts.create_gc_contact(room_jid = jid, - name = nick, show = show) - c = gajim.contacts.contact_from_gc_contact(gc_c) - self.roster.new_chat(c, account, private_chat = True) - ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) - ctrl.print_conversation('Error %s: %s' % (array[1], array[2]), - 'status') - return - - gc_control.print_conversation('Error %s: %s' % (array[1], array[2])) - if gc_control.parent_win.get_active_jid() == jid: - gc_control.set_subject(gc_control.subject) + gc_control = self.msg_win_mgr.get_control(jid, account) + if gc_control and gc_control.type_id != message_control.TYPE_GC: + gc_control = None + if gc_control: + if len(jids) > 1: # it's a pm + nick = jids[1] + if not self.msg_win_mgr.get_control(full_jid_with_resource, + account): + tv = gc_control.list_treeview + model = tv.get_model() + iter = gc_control.get_contact_iter(nick) + if iter: + show = model[iter][3] + else: + show = 'offline' + gc_c = gajim.contacts.create_gc_contact(room_jid = jid, + name = nick, show = show) + c = gajim.contacts.contact_from_gc_contact(gc_c) + self.roster.new_chat(c, account, private_chat = True) + ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) + ctrl.print_conversation('Error %s: %s' % (array[1], array[2]), + 'status') return + gc_control.print_conversation('Error %s: %s' % (array[1], array[2])) + if gc_control.parent_win.get_active_jid() == jid: + gc_control.set_subject(gc_control.subject) + return + if gajim.jid_is_transport(jid): jid = jid.replace('@', '') msg = array[2] @@ -697,8 +701,8 @@ class Interface: self.remote_ctrl.raise_signal('Subscribed', (account, array)) def handle_event_unsubscribed(self, account, jid): - dialogs.InformationDialog(_('Contact "%s" removed subscription from you') % jid, - _('You will always see him or her as offline.')) + dialogs.InformationDialog(_('Contact "%s" removed subscription from you')\ + % jid, _('You will always see him or her as offline.')) # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does not show deny gajim.connections[account].ack_unsubscribed(jid) if self.remote_ctrl: @@ -741,8 +745,8 @@ class Interface: config.ServiceRegistrationWindow(array[0], array[1], account, array[2]) else: - dialogs.ErrorDialog(_('Contact with "%s" cannot be established')\ -% array[0], _('Check your connection or try again later.')) + dialogs.ErrorDialog(_('Contact with "%s" cannot be established') \ + % array[0], _('Check your connection or try again later.')) def handle_event_agent_info_items(self, account, array): #('AGENT_INFO_ITEMS', account, (agent, node, items)) @@ -851,6 +855,7 @@ class Interface: self.remote_ctrl.raise_signal('LastStatusTime', (account, array)) def handle_event_os_info(self, account, array): + #'OS_INFO' (account, (jid, resource, client_info, os_info)) win = None if self.instances[account]['infos'].has_key(array[0]): win = self.instances[account]['infos'][array[0]] @@ -875,8 +880,8 @@ class Interface: # Get the window and control for the updated status, this may be a PrivateChatControl control = self.msg_win_mgr.get_control(room_jid, account) if control: - control.chg_contact_status(nick, show, status, array[4], array[5], array[6], - array[7], array[8], array[9], array[10]) + control.chg_contact_status(nick, show, status, array[4], array[5], + array[6], array[7], array[8], array[9], array[10]) # print status in chat window and update status/GPG image if self.msg_win_mgr.has_window(fjid, account): @@ -892,38 +897,48 @@ class Interface: if self.remote_ctrl: self.remote_ctrl.raise_signal('GCPresence', (account, array)) - def handle_event_gc_msg(self, account, array): - # ('GC_MSG', account, (jid, msg, time)) + # ('GC_MSG', account, (jid, msg, time, has_timestamp, htmlmsg)) jids = array[0].split('/', 1) room_jid = jids[0] gc_control = self.msg_win_mgr.get_control(room_jid, account) if not gc_control: return + xhtml = array[4] + if gajim.config.get('ignore_incoming_xhtml'): + xhtml = None if len(jids) == 1: # message from server nick = '' else: # message from someone nick = jids[1] - gc_control.on_message(nick, array[1], array[2]) + gc_control.on_message(nick, array[1], array[2], array[3], xhtml) if self.remote_ctrl: self.remote_ctrl.raise_signal('GCMessage', (account, array)) def handle_event_gc_subject(self, account, array): - #('GC_SUBJECT', account, (jid, subject, body)) + #('GC_SUBJECT', account, (jid, subject, body, has_timestamp)) jids = array[0].split('/', 1) jid = jids[0] gc_control = self.msg_win_mgr.get_control(jid, account) if not gc_control: return gc_control.set_subject(array[1]) - # We can receive a subject with a body that contains "X has set the subject to Y" ... - if array[2]: - gc_control.print_conversation(array[2]) - # ... Or the message comes from the occupant who set the subject - elif len(jids) > 1: - gc_control.print_conversation('%s has set the subject to %s' % (jids[1], array[1])) + # Standard way, the message comes from the occupant who set the subject + text = None + if len(jids) > 1: + text = '%s has set the subject to %s' % (jids[1], array[1]) + # Workaround for psi bug http://flyspray.psi-im.org/task/595 , to be + # deleted one day. We can receive a subject with a body that contains + # "X has set the subject to Y" ... + elif array[2]: + text = array[2] + if text is not None: + if array[3]: + gc_control.print_old_conversation(text) + else: + gc_control.print_conversation(text) def handle_event_gc_config(self, account, array): #('GC_CONFIG', account, (jid, config)) config is a dict @@ -959,7 +974,7 @@ class Interface: self.add_event(account, jid, 'gc-invitation', (room_jid, array[2], array[3])) - if helpers.allow_showing_notification(account, 'notify_on_new_message'): + if helpers.allow_showing_notification(account): path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'gc_invitation.png') path = gtkgui_helpers.get_path_to_generic_or_avatar(path) @@ -1010,7 +1025,7 @@ class Interface: return # Add it to roster contact = gajim.contacts.create_contact(jid = jid, name = name, - groups = groups, show = 'offline', sub = sub, ask = ask) + groups = groups, show = 'offline', sub = sub, ask = ask) gajim.contacts.add_contact(account, contact) self.roster.add_contact_to_roster(jid, account) else: @@ -1075,13 +1090,17 @@ class Interface: gmail_messages_list = array[2] if gajim.config.get('notify_on_new_gmail_email'): img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'single_msg_recv.png') #FIXME: find a better image - title = _('New E-mail on %(gmail_mail_address)s') % \ + 'new_email_recv.png') + title = _('New mail on %(gmail_mail_address)s') % \ {'gmail_mail_address': jid} - text = i18n.ngettext('You have %d new E-mail message', 'You have %d new E-mail messages', gmail_new_messages, gmail_new_messages, gmail_new_messages) + text = i18n.ngettext('You have %d new mail conversation', + 'You have %d new mail conversations', gmail_new_messages, + gmail_new_messages, gmail_new_messages) if gajim.config.get('notify_on_new_gmail_email_extra'): for gmessage in gmail_messages_list: + #FIXME: emulate Gtalk client popups. find out what they parse and how + #they decide what to show # each message has a 'From', 'Subject' and 'Snippet' field text += _('\nFrom: %(from_address)s') % \ {'from_address': gmessage['From']} @@ -1149,8 +1168,15 @@ class Interface: self.roster.show_title() if no_queue: # We didn't have a queue: we change icons + if not gajim.contacts.get_contact_with_highest_priority(account, jid): + # add contact to roster ("Not In The Roster") if he is not + self.roster.add_to_not_in_the_roster(account, jid) self.roster.draw_contact(jid, account) + # Show contact in roster (if he is invisible for example) and select line + path = self.roster.get_path(jid, account) + self.roster.show_and_select_path(path, jid, account) + def remove_first_event(self, account, jid, type_ = None): event = gajim.events.get_first_event(account, jid, type_) self.remove_event(account, jid, event) @@ -1161,24 +1187,26 @@ class Interface: return # no other event? if not len(gajim.events.get_events(account, jid)): - if not gajim.config.get('showoffline'): - contact = gajim.contacts.get_contact_with_highest_priority(account, - jid) - if contact: - self.roster.really_remove_contact(contact, account) + contact = gajim.contacts.get_contact_with_highest_priority(account, + jid) + show_transport = gajim.config.get('show_transports_group') + if contact and (contact.show in ('error', 'offline') and \ + not gajim.config.get('showoffline') or ( + gajim.jid_is_transport(jid) and not show_transport)): + self.roster.really_remove_contact(contact, account) self.roster.show_title() self.roster.draw_contact(jid, account) def handle_event_file_request_error(self, account, array): - jid = array[0] - file_props = array[1] + # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg)) + jid, file_props, errmsg = array ft = self.instances['file_transfers'] ft.set_status(file_props['type'], file_props['sid'], 'stop') errno = file_props['error'] if helpers.allow_popup_window(account): if errno in (-4, -5): - ft.show_stopped(jid, file_props) + ft.show_stopped(jid, file_props, errmsg) else: ft.show_request_error(file_props) return @@ -1224,8 +1252,11 @@ class Interface: path_to_image = path, title = event_type, text = txt) def handle_event_file_progress(self, account, file_props): - self.instances['file_transfers'].set_progress(file_props['type'], - file_props['sid'], file_props['received-len']) + if time.time() - self.last_ftwindow_update > 0.5: + # update ft window every 500ms + self.last_ftwindow_update = time.time() + self.instances['file_transfers'].set_progress(file_props['type'], + file_props['sid'], file_props['received-len']) def handle_event_file_rcv_completed(self, account, file_props): ft = self.instances['file_transfers'] @@ -1328,7 +1359,9 @@ class Interface: self.instances[account]['xml_console'].print_stanza(stanza, 'outgoing') def handle_event_vcard_published(self, account, array): - dialogs.InformationDialog(_('vCard publication succeeded'), _('Your personal information has been published successfully.')) + if self.instances[account].has_key('profile'): + win = self.instances[account]['profile'] + win.vcard_published() for gc_control in self.msg_win_mgr.get_controls(message_control.TYPE_GC): if gc_control.account == account: show = gajim.SHOW_LIST[gajim.connections[account].connected] @@ -1337,7 +1370,9 @@ class Interface: gc_control.room_jid, show, status) def handle_event_vcard_not_published(self, account, array): - dialogs.InformationDialog(_('vCard publication failed'), _('There was an error while publishing your personal information, try again later.')) + if self.instances[account].has_key('profile'): + win = self.instances[account]['profile'] + win.vcard_not_published() def handle_event_signed_in(self, account, empty): '''SIGNED_IN event is emitted when we sign in, so handle it''' @@ -1362,12 +1397,11 @@ class Interface: if gajim.gc_connected[account].has_key(room_jid) and\ gajim.gc_connected[account][room_jid]: continue - room, server = gajim.get_room_name_and_server_from_room_jid(room_jid) nick = gc_control.nick password = '' if gajim.gc_passwords.has_key(room_jid): password = gajim.gc_passwords[room_jid] - gajim.connections[account].join_gc(nick, room, server, password) + gajim.connections[account].join_gc(nick, room_jid, password) def handle_event_metacontacts(self, account, tags_list): gajim.contacts.define_metacontacts(account, tags_list) @@ -1401,6 +1435,21 @@ class Interface: if win.startswith('privacy_list_'): self.instances[account][win].check_active_default(data) + def handle_event_zc_name_conflict(self, account, data): + dlg = dialogs.InputDialog(_('Username Conflict'), + _('Please type a new username for your local account'), + is_modal = True) + dlg.input_entry.set_text(data) + response = dlg.get_response() + if response == gtk.RESPONSE_OK: + new_name = dlg.input_entry.get_text() + gajim.config.set_per('accounts', account, 'name', new_name) + status = gajim.connections[account].status + gajim.connections[account].username = new_name + gajim.connections[account].change_status(status, '') + else: + gajim.connections[account].change_status('offline','') + def read_sleepy(self): '''Check idle status and change that status if needed''' if not self.sleeper.poll(): @@ -1614,25 +1663,28 @@ class Interface: menu.show_all() return menu - def init_emoticons(self): - if not gajim.config.get('emoticons_theme'): + def init_emoticons(self, need_reload = False): + emot_theme = gajim.config.get('emoticons_theme') + if not emot_theme: return #initialize emoticons dictionary and unique images list self.emoticons_images = list() self.emoticons = dict() - emot_theme = gajim.config.get('emoticons_theme') - if not emot_theme: - return path = os.path.join(gajim.DATA_DIR, 'emoticons', emot_theme) if not os.path.exists(path): # It's maybe a user theme path = os.path.join(gajim.MY_EMOTS_PATH, emot_theme) - if not os.path.exists(path): # theme doesn't exist + if not os.path.exists(path): # theme doesn't exist, disable emoticons + gajim.config.set('emoticons_theme', '') return sys.path.append(path) - from emoticons import emoticons as emots + import emoticons + if need_reload: + # we need to reload else that doesn't work when changing emoticon set + reload(emoticons) + emots = emoticons.emoticons for emot in emots: emot_file = os.path.join(path, emots[emot]) if not self.image_is_ok(emot_file): @@ -1646,7 +1698,7 @@ class Interface: self.emoticons_images.append((emot, pix)) self.emoticons[emot.upper()] = emot_file sys.path.remove(path) - del emots + del emoticons if self.emoticons_menu: self.emoticons_menu.destroy() self.emoticons_menu = self.prepare_emoticons_menu() @@ -1707,6 +1759,7 @@ class Interface: 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, 'PRIVACY_LISTS_ACTIVE_DEFAULT': \ self.handle_event_privacy_lists_active_default, + 'ZC_NAME_CONFLICT': self.handle_event_zc_name_conflict, } gajim.handlers = self.handlers @@ -1726,20 +1779,28 @@ class Interface: err_str) sys.exit() - def handle_event(self, account, jid, type_): + def handle_event(self, account, fjid, type_): w = None - fjid = jid - resource = gajim.get_resource_from_jid(jid) - jid = gajim.get_jid_without_resource(jid) + resource = gajim.get_resource_from_jid(fjid) + jid = gajim.get_jid_without_resource(fjid) if type_ in ('printed_gc_msg', 'gc_msg'): w = self.msg_win_mgr.get_window(jid, account) - elif type_ in ('printed_chat', 'chat'): + elif type_ in ('printed_chat', 'chat', ''): + # '' is for log in/out notifications if self.msg_win_mgr.has_window(fjid, account): w = self.msg_win_mgr.get_window(fjid, account) else: + highest_contact = gajim.contacts.get_contact_with_highest_priority( + account, jid) + # jid can have a window if this resource was lower when he sent + # message and is now higher because the other one is offline + if resource and highest_contact.resource == resource and \ + not self.msg_win_mgr.has_window(jid, account): + resource = None + fjid = jid contact = gajim.contacts.get_contact(account, jid, resource) - if isinstance(contact, list): - contact = gajim.contacts.get_first_contact_from_jid(account, jid) + if not contact or isinstance(contact, list): + contact = highest_contact self.roster.new_chat(contact, account, resource = resource) w = self.msg_win_mgr.get_window(fjid, account) gajim.last_message_time[account][jid] = 0 # long time ago @@ -1763,16 +1824,18 @@ class Interface: elif type_ in ('normal', 'file-request', 'file-request-error', 'file-send-error', 'file-error', 'file-stopped', 'file-completed'): # Get the first single message event - event = gajim.events.get_first_event(account, jid, type_) - # Open the window - self.roster.open_event(account, jid, event) - elif type_ == 'gmail': - if gajim.config.get_per('accounts', account, 'savepass'): - url = ('http://www.google.com/accounts/ServiceLoginAuth?service=mail&Email=%s&Passwd=%s&continue=https://mail.google.com/mail') %\ - (urllib.quote(gajim.config.get_per('accounts', account, 'name')), - urllib.quote(gajim.config.get_per('accounts', account, 'password'))) + event = gajim.events.get_first_event(account, fjid, type_) + if not event: + # default to jid without resource + event = gajim.events.get_first_event(account, jid, type_) + # Open the window + self.roster.open_event(account, jid, event) else: - url = ('http://mail.google.com/') + # Open the window + self.roster.open_event(account, fjid, event) + elif type_ == 'gmail': + url = 'http://mail.google.com/mail?account_id=%s' % urllib.quote( + gajim.config.get_per('accounts', account, 'name')) helpers.launch_browser_mailer('url', url) elif type_ == 'gc-invitation': event = gajim.events.get_first_event(account, jid, type_) @@ -1780,6 +1843,7 @@ class Interface: dialogs.InvitationReceivedDialog(account, data[0], jid, data[2], data[1]) gajim.events.remove_events(account, jid, event) + self.roster.draw_contact(jid, account) if w: w.set_active_tab(fjid, account) w.window.present() @@ -1857,11 +1921,17 @@ class Interface: self.handle_event_file_progress) gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue) self.register_handlers() + if gajim.config.get('enable_zeroconf'): + gajim.connections[gajim.ZEROCONF_ACC_NAME] = common.zeroconf.connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) for account in gajim.config.get_per('accounts'): - gajim.connections[account] = common.connection.Connection(account) - + if not gajim.config.get_per('accounts', account, 'is_zeroconf'): + gajim.connections[account] = common.connection.Connection(account) + + # gtk hooks gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail') gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url') + if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0): + gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url') self.instances = {'logs': {}} @@ -1891,6 +1961,12 @@ class Interface: else: self.remote_ctrl = None + if gajim.config.get('networkmanager_support') and dbus_support.supported: + try: + import network_manager_listener + except: + print >> sys.stderr, _('Network Manager support not available') + self.show_vcard_when_connect = [] path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png') @@ -1904,18 +1980,18 @@ class Interface: self.systray_enabled = False self.systray_capabilities = False - if os.name == 'nt': - try: - import systraywin32 - except: # user doesn't have trayicon capabilities - pass - else: - self.systray_capabilities = True - self.systray = systraywin32.SystrayWin32() - else: + if os.name == 'nt' and gtk.pygtk_version >= (2, 10, 0) and\ + gtk.gtk_version >= (2, 10, 0): + import statusicon + self.systray = statusicon.StatusIcon() + self.systray_capabilities = True + else: # use ours, not GTK+ one + # [FIXME: remove this when we migrate to 2.10 and we can do + # cool tooltips somehow and (not dying to keep) animation] + import systray self.systray_capabilities = systray.HAS_SYSTRAY_CAPABILITIES if self.systray_capabilities: - self.systray = systray.Systray() + self.systray = systray.Systray() if self.systray_capabilities and gajim.config.get('trayicon'): self.show_systray() @@ -1945,6 +2021,9 @@ class Interface: 'choose another language by setting the speller_language option.' ) % lang) gajim.config.set('use_speller', False) + + self.last_ftwindow_update = 0 + gobject.timeout_add(100, self.autoconnect) gobject.timeout_add(200, self.process_connections) gobject.timeout_add(500, self.read_sleepy) @@ -1968,7 +2047,8 @@ if __name__ == '__main__': cli = gnome.ui.master_client() cli.connect('die', die_cb) - path_to_gajim_script = gtkgui_helpers.get_abspath_for_script('gajim') + path_to_gajim_script = gtkgui_helpers.get_abspath_for_script( + 'gajim') if path_to_gajim_script: argv = [path_to_gajim_script] @@ -1983,5 +2063,6 @@ if __name__ == '__main__': gtkgui_helpers.possibly_set_gajim_as_xmpp_handler() check_paths.check_and_possibly_create_paths() + Interface() gtk.main() diff --git a/src/gajim_themes_window.py b/src/gajim_themes_window.py index cc9b1742ee36ea5489ade54c10b2cdcc1147d2d1..93e979cdaf5a3aa68ae2134b7356e7180bf9c03b 100644 --- a/src/gajim_themes_window.py +++ b/src/gajim_themes_window.py @@ -9,7 +9,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -51,7 +51,7 @@ class GajimThemesWindow: self.themes_tree = self.xml.get_widget('themes_treeview') self.theme_options_vbox = self.xml.get_widget('theme_options_vbox') self.colorbuttons = {} - for chatstate in ('active', 'inactive', 'composing', 'paused', 'gone', + for chatstate in ('inactive', 'composing', 'paused', 'gone', 'muc_msg', 'muc_directed_msg'): self.colorbuttons[chatstate] = self.xml.get_widget(chatstate + \ '_colorbutton') @@ -198,7 +198,7 @@ class GajimThemesWindow: self.no_update = False gajim.interface.roster.change_roster_style(None) - for chatstate in ('active', 'inactive', 'composing', 'paused', 'gone', + for chatstate in ('inactive', 'composing', 'paused', 'gone', 'muc_msg', 'muc_directed_msg'): color = gajim.config.get_per('themes', theme, 'state_' + chatstate + \ '_color') @@ -333,11 +333,6 @@ class GajimThemesWindow: font_props[1] = True return font_props - def on_active_colorbutton_color_set(self, widget): - self.no_update = True - self._set_color(True, widget, 'state_active_color') - self.no_update = False - def on_inactive_colorbutton_color_set(self, widget): self.no_update = True self._set_color(True, widget, 'state_inactive_color') diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 05432780619a95b315a94bcc1db9abbe2afabd9c..a78830a84a46a2db6372a6c6f15c955894b4bd83 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -4,7 +4,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -39,12 +39,13 @@ from common import helpers from chat_control import ChatControl from chat_control import ChatControlBase from conversation_textview import ConversationTextview +from common.exceptions import GajimGeneralException #(status_image, type, nick, shown_nick) ( C_IMG, # image to show state (online, new message etc) -C_TYPE, # type of the row ('contact' or 'group') -C_NICK, # contact nickame or group name +C_NICK, # contact nickame or ROLE name +C_TYPE, # type of the row ('contact' or 'role') C_TEXT, # text shown in the cellrenderer C_AVATAR, # avatar of the contact ) = range(5) @@ -94,6 +95,9 @@ class PrivateChatControl(ChatControl): TYPE_ID = message_control.TYPE_PM def __init__(self, parent_win, contact, acct): + room_jid = contact.jid.split('/')[0] + room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, acct) + self.room_name = room_ctrl.name ChatControl.__init__(self, parent_win, contact, acct) self.TYPE_ID = 'pm' self.display_names = (_('Private Chat'), _('Private Chats')) @@ -103,9 +107,10 @@ class PrivateChatControl(ChatControl): if not message: return - # We need to make sure that we can still send through the room and that the - # recipient did not go away - contact = gajim.contacts.get_first_contact_from_jid(self.account, self.contact.jid) + # We need to make sure that we can still send through the room and that + # the recipient did not go away + contact = gajim.contacts.get_first_contact_from_jid(self.account, + self.contact.jid) if contact is None: # contact was from pm in MUC room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid) @@ -114,7 +119,7 @@ class PrivateChatControl(ChatControl): dialogs.ErrorDialog( _('Sending private message failed'), #in second %s code replaces with nickname - _('You are no longer in room "%s" or "%s" has left.') % \ + _('You are no longer in group chat "%s" or "%s" has left.') % \ (room, nick)) return @@ -172,17 +177,20 @@ class GroupchatControl(ChatControlBase): self.nick = contact.name self.name = self.room_jid.split('@')[0] - self.hide_chat_buttons_always = gajim.config.get('always_hide_groupchat_buttons') + self.hide_chat_buttons_always = gajim.config.get( + 'always_hide_groupchat_buttons') self.chat_buttons_set_visible(self.hide_chat_buttons_always) - self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_groupchat_banner')) - self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'), gajim.config.get('hide_groupchat_occupants_list')) + self.widget_set_visible(self.xml.get_widget('banner_eventbox'), + gajim.config.get('hide_groupchat_banner')) + self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'), + gajim.config.get('hide_groupchat_occupants_list')) self.gc_refer_to_nick_char = gajim.config.get('gc_refer_to_nick_char') self._last_selected_contact = None # None or holds jid, account tuple # alphanum sorted self.muc_cmds = ['ban', 'chat', 'query', 'clear', 'close', 'compact', - 'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick', 'part', - 'names', 'say', 'topic'] + 'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick', + 'part', 'names', 'say', 'topic'] # muc attention flag (when we are mentioned in a muc) # if True, the room has mentioned us self.attention_flag = False @@ -200,7 +208,8 @@ class GroupchatControl(ChatControlBase): xm = gtkgui_helpers.get_glade('gc_control_popup_menu.glade') widget = xm.get_widget('bookmark_room_menuitem') - id = widget.connect('activate', self._on_bookmark_room_menuitem_activate) + id = widget.connect('activate', + self._on_bookmark_room_menuitem_activate) self.handlers[id] = widget widget = xm.get_widget('change_nick_menuitem') @@ -208,11 +217,13 @@ class GroupchatControl(ChatControlBase): self.handlers[id] = widget widget = xm.get_widget('configure_room_menuitem') - id = widget.connect('activate', self._on_configure_room_menuitem_activate) + id = widget.connect('activate', + self._on_configure_room_menuitem_activate) self.handlers[id] = widget widget = xm.get_widget('change_subject_menuitem') - id = widget.connect('activate', self._on_change_subject_menuitem_activate) + id = widget.connect('activate', + self._on_change_subject_menuitem_activate) self.handlers[id] = widget widget = xm.get_widget('compact_view_menuitem') @@ -226,29 +237,27 @@ class GroupchatControl(ChatControlBase): self.gc_popup_menu = xm.get_widget('gc_control_popup_menu') self.name_label = self.xml.get_widget('banner_name_label') - id = self.parent_win.window.connect('focus-in-event', - self._on_window_focus_in_event) - self.handlers[id] = self.parent_win.window # set the position of the current hpaned self.hpaned_position = gajim.config.get('gc-hpaned-position') self.hpaned = self.xml.get_widget('hpaned') self.hpaned.set_position(self.hpaned_position) - list_treeview = self.list_treeview = self.xml.get_widget('list_treeview') - selection = list_treeview.get_selection() + self.list_treeview = self.xml.get_widget('list_treeview') + selection = self.list_treeview.get_selection() id = selection.connect('changed', self.on_list_treeview_selection_changed) self.handlers[id] = selection - id = list_treeview.connect('style-set', self.on_list_treeview_style_set) - self.handlers[id] = list_treeview + id = self.list_treeview.connect('style-set', + self.on_list_treeview_style_set) + self.handlers[id] = self.list_treeview # we want to know when the the widget resizes, because that is # an indication that the hpaned has moved... # FIXME: Find a better indicator that the hpaned has moved. id = self.list_treeview.connect('size-allocate', self.on_treeview_size_allocate) self.handlers[id] = self.list_treeview - #status_image, type, nickname, shown_nick + #status_image, shown_nick, type, nickname, avatar store = gtk.TreeStore(gtk.Image, str, str, str, gtk.gdk.Pixbuf) store.set_sort_column_id(C_TEXT, gtk.SORT_ASCENDING) self.list_treeview.set_model(store) @@ -275,7 +284,8 @@ class GroupchatControl(ChatControlBase): renderer_text = gtk.CellRendererText() # nickname column.pack_start(renderer_text, expand = True) column.add_attribute(renderer_text, 'markup', C_TEXT) - column.set_cell_data_func(renderer_text, tree_cell_data_func, self.list_treeview) + column.set_cell_data_func(renderer_text, tree_cell_data_func, + self.list_treeview) self.list_treeview.append_column(column) @@ -316,15 +326,6 @@ class GroupchatControl(ChatControlBase): menu.show_all() - def notify_on_new_messages(self): - return gajim.config.get('notify_on_all_muc_messages') or \ - self.attention_flag - - def _on_window_focus_in_event(self, widget, event): - '''When window gets focus''' - if self.parent_win.get_active_jid() == self.room_jid: - self.conv_textview.allow_focus_out_line = True - def on_treeview_size_allocate(self, widget, allocation): '''The MUC treeview has resized. Move the hpaned in all tabs to match''' self.hpaned_position = self.hpaned.get_position() @@ -371,39 +372,44 @@ class GroupchatControl(ChatControlBase): has_focus = self.parent_win.window.get_property('has-toplevel-focus') current_tab = self.parent_win.get_active_control() == self + color_name = None color = None theme = gajim.config.get('roster_theme') if chatstate == 'attention' and (not has_focus or not current_tab): self.attention_flag = True - color = gajim.config.get_per('themes', theme, + color_name = gajim.config.get_per('themes', theme, 'state_muc_directed_msg_color') elif chatstate: if chatstate == 'active' or (current_tab and has_focus): self.attention_flag = False - color = gajim.config.get_per('themes', theme, - 'state_active_color') + # get active color from gtk + color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE] elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\ not self.attention_flag: - color = gajim.config.get_per('themes', theme, 'state_muc_msg_color') - if color: - color = gtk.gdk.colormap_get_system().alloc_color(color) - + color_name = gajim.config.get_per('themes', theme, + 'state_muc_msg_color') + if color_name: + color = gtk.gdk.colormap_get_system().alloc_color(color_name) + label_str = self.name + + # count waiting highlighted messages + unread = '' + num_unread = self.get_nb_unread() + if num_unread == 1: + unread = '*' + elif num_unread > 1: + unread = '[' + unicode(num_unread) + ']' + label_str = unread + label_str return (label_str, color) def get_tab_image(self): - # Set tab image (always 16x16); unread messages show the 'message' image - img_16 = gajim.interface.roster.get_appropriate_state_images( - self.room_jid, icon_name = 'message') - + # Set tab image (always 16x16) tab_image = None - if self.attention_flag and gajim.config.get('show_unread_tab_icon'): - tab_image = img_16['message'] + if gajim.gc_connected[self.account][self.room_jid]: + tab_image = gajim.interface.roster.load_icon('muc_active') else: - if gajim.gc_connected[self.account][self.room_jid]: - tab_image = img_16['muc_active'] - else: - tab_image = img_16['muc_inactive'] + tab_image = gajim.interface.roster.load_icon('muc_inactive') return tab_image def update_ui(self): @@ -430,15 +436,18 @@ class GroupchatControl(ChatControlBase): childs[3].set_sensitive(False) return menu - def on_message(self, nick, msg, tim): + def on_message(self, nick, msg, tim, has_timestamp = False, xhtml = None): if not nick: # message from server - self.print_conversation(msg, tim = tim) + self.print_conversation(msg, tim = tim, xhtml = xhtml) else: # message from someone - self.print_conversation(msg, nick, tim) + if has_timestamp: + self.print_old_conversation(msg, nick, tim, xhtml) + else: + self.print_conversation(msg, nick, tim, xhtml) - def on_private_message(self, nick, msg, tim): + def on_private_message(self, nick, msg, tim, xhtml): # Do we have a queue? fjid = self.room_jid + '/' + nick no_queue = len(gajim.events.get_events(self.account, fjid)) == 0 @@ -446,11 +455,11 @@ class GroupchatControl(ChatControlBase): # We print if window is opened pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account) if pm_control: - pm_control.print_conversation(msg, tim = tim) + pm_control.print_conversation(msg, tim = tim, xhtml = xhtml) return event = gajim.events.create_event('pm', (msg, '', 'incoming', tim, - False, '', None)) + False, '', None, xhtml)) gajim.events.add_event(self.account, fjid, event) autopopup = gajim.config.get('autopopup') @@ -458,7 +467,7 @@ class GroupchatControl(ChatControlBase): iter = self.get_contact_iter(nick) path = self.list_treeview.get_model().get_path(iter) if not autopopup or (not autopopupaway and \ - gajim.connections[self.account].connected > 2): + gajim.connections[self.account].connected > 2): if no_queue: # We didn't have a queue: we change icons model = self.list_treeview.get_model() state_images =\ @@ -467,6 +476,7 @@ class GroupchatControl(ChatControlBase): image = state_images['message'] model[iter][C_IMG] = image self.parent_win.show_title() + self.parent_win.redraw_tab(self) else: self._start_private_message(nick) # Scroll to line @@ -499,7 +509,26 @@ class GroupchatControl(ChatControlBase): gc_count_nicknames_colors = 0 gc_custom_colors = {} - def print_conversation(self, text, contact = '', tim = None): + def print_old_conversation(self, text, contact = '', tim = None, + xhtml = None): + if isinstance(text, str): + text = unicode(text, 'utf-8') + if contact: + if contact == self.nick: # it's us + kind = 'outgoing' + else: + kind = 'incoming' + else: + kind = 'status' + if gajim.config.get('restored_messages_small'): + small_attr = ['small'] + else: + small_attr = [] + ChatControlBase.print_conversation_line(self, text, kind, contact, tim, + small_attr, small_attr + ['restored_message'], + small_attr + ['restored_message'], xhtml = xhtml) + + def print_conversation(self, text, contact = '', tim = None, xhtml = None): '''Print a line in the conversation: if contact is set: it's a message from someone or an info message (contact = 'info' in such a case) @@ -553,11 +582,16 @@ class GroupchatControl(ChatControlBase): self.check_and_possibly_add_focus_out_line() ChatControlBase.print_conversation_line(self, text, kind, contact, tim, - other_tags_for_name, [], other_tags_for_text) + other_tags_for_name, [], other_tags_for_text, xhtml = xhtml) def get_nb_unread(self): nb = len(gajim.events.get_events(self.account, self.room_jid, ['printed_gc_msg'])) + nb += self.get_nb_unread_pm() + return nb + + def get_nb_unread_pm(self): + nb = 0 for nick in gajim.contacts.get_nick_list(self.account, self.room_jid): nb += len(gajim.events.get_events(self.account, self.room_jid + '/' + \ nick, ['pm'])) @@ -570,8 +604,7 @@ class GroupchatControl(ChatControlBase): # Do we play a sound on every muc message? if gajim.config.get_per('soundevents', 'muc_message_received', 'enabled'): - if gajim.config.get('notify_on_all_muc_messages'): - sound = 'received' + sound = 'received' # Are any of the defined highlighting words in the text? if self.needs_visual_notification(text): @@ -622,13 +655,16 @@ class GroupchatControl(ChatControlBase): word[char_position:char_position+1] if (refer_to_nick_char != ''): refer_to_nick_char_code = ord(refer_to_nick_char) - if ((refer_to_nick_char_code < 65 or refer_to_nick_char_code > 123)\ - or (refer_to_nick_char_code < 97 and refer_to_nick_char_code > 90)): + if ((refer_to_nick_char_code < 65 or \ + refer_to_nick_char_code > 123) or \ + (refer_to_nick_char_code < 97 and \ + refer_to_nick_char_code > 90)): return True else: - # This is A->Z or a->z, we can be sure our nick is the beginning - # of a real word, do not highlight. Note that we can probably - # do a better detection of non-punctuation characters + # This is A->Z or a->z, we can be sure our nick is the + # beginning of a real word, do not highlight. Note that we + # can probably do a better detection of non-punctuation + # characters return False else: # Special word == word, no char after in word return True @@ -638,7 +674,7 @@ class GroupchatControl(ChatControlBase): self.subject = subject self.name_label.set_ellipsize(pango.ELLIPSIZE_END) - subject = gtkgui_helpers.reduce_chars_newlines(subject, max_lines = 2) + subject = helpers.reduce_chars_newlines(subject, max_lines = 2) subject = gtkgui_helpers.escape_for_pango_markup(subject) font_attrs, font_attrs_small = self.get_font_attrs() text = '<span %s>%s</span>' % (font_attrs, self.room_jid) @@ -646,11 +682,10 @@ class GroupchatControl(ChatControlBase): text += '\n<span %s>%s</span>' % (font_attrs_small, subject) self.name_label.set_markup(text) event_box = self.name_label.get_parent() - if subject == '': - self.subject = _('This room has no subject') - # tooltip must always hold ALL the subject - self.subject_tooltip.set_tip(event_box, self.subject) + if self.subject: + # tooltip must always hold ALL the subject + self.subject_tooltip.set_tip(event_box, self.subject) def got_connected(self): gajim.gc_connected[self.account][self.room_jid] = True @@ -677,7 +712,8 @@ class GroupchatControl(ChatControlBase): gc_contact.affiliation, gc_contact.status, gc_contact.jid) - def on_send_pm(self, widget=None, model=None, iter=None, nick=None, msg=None): + def on_send_pm(self, widget = None, model = None, iter = None, nick = None, + msg = None): '''opens a chat window and msg is not None sends private message to a contact in a room''' if nick is None: @@ -686,14 +722,16 @@ class GroupchatControl(ChatControlBase): self._start_private_message(nick) if msg: - gajim.interface.msg_win_mgr.get_control(fjid, self.account).send_message(msg) + gajim.interface.msg_win_mgr.get_control(fjid, self.account).\ + send_message(msg) def draw_contact(self, nick, selected=False, focus=False): iter = self.get_contact_iter(nick) if not iter: return model = self.list_treeview.get_model() - gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, + nick) state_images = gajim.interface.roster.jabber_state_images['16'] if len(gajim.events.get_events(self.account, self.room_jid + '/' + nick)): image = state_images['message'] @@ -706,7 +744,7 @@ class GroupchatControl(ChatControlBase): if status and gajim.config.get('show_status_msgs_in_roster'): status = status.strip() if status != '': - status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 1) + status = helpers.reduce_chars_newlines(status, max_lines = 1) # escape markup entities and make them small italic and fg color color = gtkgui_helpers._get_fade_color(self.list_treeview, selected, focus) @@ -720,6 +758,8 @@ class GroupchatControl(ChatControlBase): def draw_avatar(self, nick): model = self.list_treeview.get_model() iter = self.get_contact_iter(nick) + if not iter: + return if gajim.config.get('show_avatars_in_roster'): pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(self.room_jid + \ '/' + nick, True) @@ -731,8 +771,8 @@ class GroupchatControl(ChatControlBase): scaled_pixbuf = None model[iter][C_AVATAR] = scaled_pixbuf - def chg_contact_status(self, nick, show, status, role, affiliation, jid, reason, actor, - statusCode, new_nick): + def chg_contact_status(self, nick, show, status, role, affiliation, jid, + reason, actor, statusCode, new_nick): '''When an occupant changes his or her status''' if show == 'invisible': return @@ -824,7 +864,8 @@ class GroupchatControl(ChatControlBase): self.add_contact_to_roster(nick, show, role, affiliation, status, jid) else: - c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + c = gajim.contacts.get_gc_contact(self.account, self.room_jid, + nick) if c.show == show and c.status == status and \ c.affiliation == affiliation: #no change return @@ -841,20 +882,22 @@ class GroupchatControl(ChatControlBase): print_status = None for bookmark in gajim.connections[self.account].bookmarks: if bookmark['jid'] == self.room_jid: - print_status = bookmark['print_status'] + print_status = bookmark.get('print_status', None) break - if print_status is None: + if not print_status: print_status = gajim.config.get('print_status_in_muc') nick_jid = nick if jid: - nick_jid += ' (%s)' % jid + # delete ressource + simple_jid = gajim.get_jid_without_resource(jid) + nick_jid += ' (%s)' % simple_jid if show == 'offline' and print_status in ('all', 'in_and_out'): st = _('%s has left') % nick_jid if reason: st += ' [%s]' % reason else: if newly_created and print_status in ('all', 'in_and_out'): - st = _('%s has joined the room') % nick_jid + st = _('%s has joined the group chat') % nick_jid elif print_status == 'all': st = _('%s is now %s') % (nick_jid, helpers.get_uf_show(show)) if st: @@ -881,9 +924,9 @@ class GroupchatControl(ChatControlBase): role_iter = self.get_role_iter(role) if not role_iter: role_iter = model.append(None, - (gajim.interface.roster.jabber_state_images['16']['closed'], 'role', - role, '<b>%s</b>' % role_name, None)) - iter = model.append(role_iter, (None, 'contact', nick, name, None)) + (gajim.interface.roster.jabber_state_images['16']['closed'], role, + 'role', '<b>%s</b>' % role_name, None)) + iter = model.append(role_iter, (None, nick, 'contact', name, None)) if not nick in gajim.contacts.get_nick_list(self.account, self.room_jid): gc_contact = gajim.contacts.create_gc_contact(room_jid = self.room_jid, name = nick, show = show, status = status, role = role, @@ -892,7 +935,7 @@ class GroupchatControl(ChatControlBase): self.draw_contact(nick) self.draw_avatar(nick) # Do not ask avatar to irc rooms as irc transports reply with messages - r, server = gajim.get_room_name_and_server_from_room_jid(self.room_jid) + server = gajim.get_server_from_jid(self.room_jid) if gajim.config.get('ask_avatars_on_startup') and \ not server.startswith('irc'): fjid = self.room_jid + '/' + nick @@ -925,7 +968,8 @@ class GroupchatControl(ChatControlBase): iter = self.get_contact_iter(nick) if not iter: return - gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, + nick) if gc_contact: gajim.contacts.remove_gc_contact(self.account, gc_contact) parent_iter = model.iter_parent(iter) @@ -1005,8 +1049,10 @@ class GroupchatControl(ChatControlBase): new_topic = message_array.pop(0) gajim.connections[self.account].send_gc_subject(self.room_jid, new_topic) - else: + elif self.subject is not '': self.print_conversation(self.subject, 'info') + else: + self.print_conversation(_('This group chat has no subject'), 'info') self.clear(self.msg_textview) return True elif command == 'invite': @@ -1034,15 +1080,13 @@ class GroupchatControl(ChatControlBase): elif command == 'join': # example: /join room@conference.example.com/nick if len(message_array): - message_array = message_array[0] - if message_array.find('@') >= 0: - room, servernick = message_array.split('@') - if servernick.find('/') >= 0: - server, nick = servernick.split('/', 1) + room_jid = message_array[0] + if room_jid.find('@') >= 0: + if room_jid.find('/') >= 0: + room_jid, nick = room_jid.split('/', 1) else: - server = servernick nick = '' - #join_gc window is needed in order to provide for password entry. + # join_gc window is needed in order to provide for password entry. if gajim.interface.instances[self.account].has_key('join_gc'): gajim.interface.instances[self.account]['join_gc'].\ window.present() @@ -1050,13 +1094,13 @@ class GroupchatControl(ChatControlBase): try: gajim.interface.instances[self.account]['join_gc'] =\ dialogs.JoinGroupchatWindow(self.account, - server = server, room = room, nick = nick) - except RuntimeError: + room_jid = room_jid, nick = nick) + except GajimGeneralException: pass self.clear(self.msg_textview) else: #%s is something the user wrote but it is not a jid so we inform - s = _('%s does not appear to be a valid JID') % message_array + s = _('%s does not appear to be a valid JID') % message_array[0] self.print_conversation(s, 'info') else: self.get_command_help(command) @@ -1066,21 +1110,21 @@ class GroupchatControl(ChatControlBase): reason = 'offline' if len(message_array): reason = message_array.pop(0) - self.parent_win.remove_tab(self,reason) + self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason) self.clear(self.msg_textview) return True elif command == 'ban': if len(message_array): message_array = message_array[0].split() nick = message_array.pop(0) - room_nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) + room_nicks = gajim.contacts.get_nick_list(self.account, + self.room_jid) reason = ' '.join(message_array) if nick in room_nicks: - ban_jid = gajim.construct_fjid(self.room_jid, nick) - gajim.connections[self.account].gc_set_affiliation(self.room_jid, - ban_jid, 'outcast', reason) - self.clear(self.msg_textview) - elif nick.find('@') >= 0: + gc_contact = gajim.contacts.get_gc_contact(self.account, + self.room_jid, nick) + nick = gc_contact.jid + if nick.find('@') >= 0: gajim.connections[self.account].gc_set_affiliation(self.room_jid, nick, 'outcast', reason) self.clear(self.msg_textview) @@ -1094,7 +1138,8 @@ class GroupchatControl(ChatControlBase): if len(message_array): message_array = message_array[0].split() nick = message_array.pop(0) - room_nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) + room_nicks = gajim.contacts.get_nick_list(self.account, + self.room_jid) reason = ' '.join(message_array) if nick in room_nicks: gajim.connections[self.account].gc_set_role(self.room_jid, nick, @@ -1154,7 +1199,8 @@ class GroupchatControl(ChatControlBase): if not self._process_command(message): # Send the message - gajim.connections[self.account].send_gc_message(self.room_jid, message) + gajim.connections[self.account].send_gc_message(self.room_jid, + message) self.msg_textview.get_buffer().set_text('') self.msg_textview.grab_focus() @@ -1162,10 +1208,11 @@ class GroupchatControl(ChatControlBase): if command == 'help': self.print_conversation(_('Commands: %s') % self.muc_cmds, 'info') elif command == 'ban': - s = _('Usage: /%s <nickname|JID> [reason], bans the JID from the room.' + s = _('Usage: /%s <nickname|JID> [reason], bans the JID from the group chat.' ' The nickname of an occupant may be substituted, but not if it ' - 'contains "@". If the JID is currently in the room, he/she/it will ' - 'also be kicked. Does NOT support spaces in nickname.') % command + 'contains "@". If the JID is currently in the group chat, ' + 'he/she/it will also be kicked. Does NOT support spaces in ' + 'nickname.') % command self.print_conversation(s, 'info') elif command == 'chat' or command == 'query': self.print_conversation(_('Usage: /%s <nickname>, opens a private chat' @@ -1177,10 +1224,11 @@ class GroupchatControl(ChatControlBase): self.print_conversation(_('Usage: /%s [reason], closes the current ' 'window or tab, displaying reason if specified.') % command, 'info') elif command == 'compact': - self.print_conversation(_('Usage: /%s, hide the chat buttons.') % command, 'info') + self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \ + command, 'info') elif command == 'invite': self.print_conversation(_('Usage: /%s <JID> [reason], invites JID to ' - 'the current room, optionally providing a reason.') % command, + 'the current group chat, optionally providing a reason.') % command, 'info') elif command == 'join': self.print_conversation(_('Usage: /%s <room>@<server>[/nickname], ' @@ -1188,12 +1236,12 @@ class GroupchatControl(ChatControlBase): % command, 'info') elif command == 'kick': self.print_conversation(_('Usage: /%s <nickname> [reason], removes ' - 'the occupant specified by nickname from the room and optionally ' - 'displays a reason. Does NOT support spaces in nickname.') % \ - command, 'info') + 'the occupant specified by nickname from the group chat and ' + 'optionally displays a reason. Does NOT support spaces in ' + 'nickname.') % command, 'info') elif command == 'me': self.print_conversation(_('Usage: /%s <action>, sends action to the ' - 'current room. Use third person. (e.g. /%s explodes.)') % \ + 'current group chat. Use third person. (e.g. /%s explodes.)') % \ (command, command), 'info') elif command == 'msg': s = _('Usage: /%s <nickname> [message], opens a private message window' @@ -1201,16 +1249,16 @@ class GroupchatControl(ChatControlBase): command self.print_conversation(s, 'info') elif command == 'nick': - s = _('Usage: /%s <nickname>, changes your nickname in current room.')\ - % command + s = _('Usage: /%s <nickname>, changes your nickname in current group ' + 'chat.') % command self.print_conversation(s, 'info') elif command == 'names': - s = _('Usage: /%s , display the names of room occupants.')\ + s = _('Usage: /%s , display the names of group chat occupants.')\ % command self.print_conversation(s, 'info') elif command == 'topic': self.print_conversation(_('Usage: /%s [topic], displays or updates the' - ' current room topic.') % command, 'info') + ' current group chat topic.') % command, 'info') elif command == 'say': self.print_conversation(_('Usage: /%s <message>, sends a message ' 'without looking for other commands.') % command, 'info') @@ -1218,7 +1266,8 @@ class GroupchatControl(ChatControlBase): self.print_conversation(_('No help info for /%s') % command, 'info') def get_role(self, nick): - gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, + nick) if gc_contact: return gc_contact.role else: @@ -1249,7 +1298,13 @@ class GroupchatControl(ChatControlBase): self.handlers[i].disconnect(i) del self.handlers[i] - def allow_shutdown(self): + def allow_shutdown(self, method): + '''If check_selection is True, ''' + if method == self.parent_win.CLOSE_ESC: + model, iter = self.list_treeview.get_selection().get_selected() + if iter: + self.list_treeview.get_selection().unselect_all() + return False retval = True includes = gajim.config.get('confirm_close_muc_rooms').split(' ') excludes = gajim.config.get('noconfirm_close_muc_rooms').split(' ') @@ -1257,9 +1312,10 @@ class GroupchatControl(ChatControlBase): if (gajim.config.get('confirm_close_muc') or self.room_jid in includes) \ and gajim.gc_connected[self.account][self.room_jid] and self.room_jid not\ in excludes: - pritext = _('Are you sure you want to leave room "%s"?') % self.name + pritext = _('Are you sure you want to leave group chat "%s"?')\ + % self.name sectext = _('If you close this window, you will be disconnected ' - 'from this room.') + 'from this group chat.') dialog = dialogs.ConfirmationDialogCheck(pritext, sectext, _('Do _not ask me again')) @@ -1275,6 +1331,7 @@ class GroupchatControl(ChatControlBase): return retval def set_control_active(self, state): + self.conv_textview.allow_focus_out_line = True self.attention_flag = False ChatControlBase.set_control_active(self, state) if not state: @@ -1299,7 +1356,8 @@ class GroupchatControl(ChatControlBase): _('Please specify the new subject:'), self.subject) response = instance.get_response() if response == gtk.RESPONSE_OK: - # Note, we don't update self.subject since we don't know whether it will work yet + # Note, we don't update self.subject since we don't know whether it + # will work yet subject = instance.input_entry.get_text().decode('utf-8') gajim.connections[self.account].send_gc_subject(self.room_jid, subject) @@ -1324,15 +1382,14 @@ class GroupchatControl(ChatControlBase): 'jid': self.room_jid, 'autojoin': '0', 'password': '', - 'nick': self.nick, - 'print_status' : gajim.config.get('print_status_in_muc') + 'nick': self.nick } for bookmark in gajim.connections[self.account].bookmarks: if bookmark['jid'] == bm['jid']: dialogs.ErrorDialog( _('Bookmark already set'), - _('Room "%s" is already in your bookmarks.') % bm['jid']) + _('Group Chat "%s" is already in your bookmarks.') % bm['jid']) return gajim.connections[self.account].bookmarks.append(bm) @@ -1344,7 +1401,8 @@ class GroupchatControl(ChatControlBase): _('Bookmark has been added successfully'), _('You can manage your bookmarks via Actions menu in your roster.')) - def handle_message_textview_mykey_press(self, widget, event_keyval, event_keymod): + def handle_message_textview_mykey_press(self, widget, event_keyval, + event_keymod): # NOTE: handles mykeypress which is custom signal connected to this # CB in new_room(). for this singal see message_textview.py @@ -1356,17 +1414,30 @@ class GroupchatControl(ChatControlBase): message_buffer = widget.get_buffer() start_iter, end_iter = message_buffer.get_bounds() - message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8') + message = message_buffer.get_text(start_iter, end_iter, False).decode( + 'utf-8') if event.keyval == gtk.keysyms.Tab: # TAB cursor_position = message_buffer.get_insert() end_iter = message_buffer.get_iter_at_mark(cursor_position) - text = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8') - if text.endswith(' '): - if not self.last_key_tabs: - return False + text = message_buffer.get_text(start_iter, end_iter, False).decode( + 'utf-8') splitted_text = text.split() + # topic completion + splitted_text2 = text.split(None, 1) + if text.startswith('/topic '): + if len(splitted_text2) == 2 and \ + self.subject.startswith(splitted_text2[1]) and\ + len(self.subject) > len(splitted_text2[1]): + message_buffer.insert_at_cursor( + self.subject[len(splitted_text2[1]):]) + return True + elif len(splitted_text2) == 1 and text.startswith('/topic '): + message_buffer.delete(start_iter, end_iter) + message_buffer.insert_at_cursor('/topic '+self.subject) + return True + # command completion if text.startswith('/') and len(splitted_text) == 1: text = splitted_text[0] @@ -1435,7 +1506,11 @@ class GroupchatControl(ChatControlBase): def on_list_treeview_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Escape: - widget.get_selection().unselect_all() + selection = widget.get_selection() + model, iter = selection.get_selected() + if iter: + widget.get_selection().unselect_all() + return True def on_list_treeview_row_expanded(self, widget, iter, path): '''When a row is expanded: change the icon of the arrow''' @@ -1473,8 +1548,8 @@ class GroupchatControl(ChatControlBase): # looking for user's affiliation and role user_nick = self.nick - user_affiliation = gajim.contacts.get_gc_contact(self.account, self.room_jid, - user_nick).affiliation + user_affiliation = gajim.contacts.get_gc_contact(self.account, + self.room_jid, user_nick).affiliation user_role = self.get_role(user_nick) # making menu from glade @@ -1483,9 +1558,10 @@ class GroupchatControl(ChatControlBase): # these conditions were taken from JEP 0045 item = xml.get_widget('kick_menuitem') if user_role != 'moderator' or \ - (user_affiliation == 'admin' and target_affiliation == 'owner') or \ - (user_affiliation == 'member' and target_affiliation in ('admin', 'owner')) or \ - (user_affiliation == 'none' and target_affiliation != 'none'): + (user_affiliation == 'admin' and target_affiliation == 'owner') or \ + (user_affiliation == 'member' and target_affiliation in ('admin', + 'owner')) or (user_affiliation == 'none' and target_affiliation != \ + 'none'): item.set_sensitive(False) id = item.connect('activate', self.kick, nick) self.handlers[id] = item @@ -1493,18 +1569,18 @@ class GroupchatControl(ChatControlBase): item = xml.get_widget('voice_checkmenuitem') item.set_active(target_role != 'visitor') if user_role != 'moderator' or \ - user_affiliation == 'none' or \ - (user_affiliation=='member' and target_affiliation!='none') or \ - target_affiliation in ('admin', 'owner'): + user_affiliation == 'none' or \ + (user_affiliation=='member' and target_affiliation!='none') or \ + target_affiliation in ('admin', 'owner'): item.set_sensitive(False) id = item.connect('activate', self.on_voice_checkmenuitem_activate, - nick) + nick) self.handlers[id] = item item = xml.get_widget('moderator_checkmenuitem') item.set_active(target_role == 'moderator') if not user_affiliation in ('admin', 'owner') or \ - target_affiliation in ('admin', 'owner'): + target_affiliation in ('admin', 'owner'): item.set_sensitive(False) id = item.connect('activate', self.on_moderator_checkmenuitem_activate, nick) @@ -1512,8 +1588,8 @@ class GroupchatControl(ChatControlBase): item = xml.get_widget('ban_menuitem') if not user_affiliation in ('admin', 'owner') or \ - (target_affiliation in ('admin', 'owner') and\ - user_affiliation != 'owner'): + (target_affiliation in ('admin', 'owner') and\ + user_affiliation != 'owner'): item.set_sensitive(False) id = item.connect('activate', self.ban, jid) self.handlers[id] = item @@ -1521,7 +1597,7 @@ class GroupchatControl(ChatControlBase): item = xml.get_widget('member_checkmenuitem') item.set_active(target_affiliation != 'none') if not user_affiliation in ('admin', 'owner') or \ - (user_affiliation != 'owner' and target_affiliation in ('admin','owner')): + (user_affiliation != 'owner' and target_affiliation in ('admin','owner')): item.set_sensitive(False) id = item.connect('activate', self.on_member_checkmenuitem_activate, jid) @@ -1711,19 +1787,23 @@ class GroupchatControl(ChatControlBase): def grant_voice(self, widget, nick): '''grant voice privilege to a user''' - gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') + gajim.connections[self.account].gc_set_role(self.room_jid, nick, + 'participant') def revoke_voice(self, widget, nick): '''revoke voice privilege to a user''' - gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'visitor') + gajim.connections[self.account].gc_set_role(self.room_jid, nick, + 'visitor') def grant_moderator(self, widget, nick): '''grant moderator privilege to a user''' - gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'moderator') + gajim.connections[self.account].gc_set_role(self.room_jid, nick, + 'moderator') def revoke_moderator(self, widget, nick): '''revoke moderator privilege to a user''' - gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') + gajim.connections[self.account].gc_set_role(self.room_jid, nick, + 'participant') def ban(self, widget, jid): '''ban a user''' @@ -1738,17 +1818,17 @@ class GroupchatControl(ChatControlBase): else: return # stop banning procedure gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, - 'outcast', reason) + 'outcast', reason) def grant_membership(self, widget, jid): '''grant membership privilege to a user''' gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, - 'member') + 'member') def revoke_membership(self, widget, jid): '''revoke membership privilege to a user''' gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, - 'none') + 'none') def grant_admin(self, widget, jid): '''grant administrative privilege to a user''' @@ -1772,10 +1852,11 @@ class GroupchatControl(ChatControlBase): c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) c2 = gajim.contacts.contact_from_gc_contact(c) if gajim.interface.instances[self.account]['infos'].has_key(c2.jid): - gajim.interface.instances[self.account]['infos'][c2.jid].window.present() + gajim.interface.instances[self.account]['infos'][c2.jid].window.\ + present() else: gajim.interface.instances[self.account]['infos'][c2.jid] = \ - vcard.VcardWindow(c2, self.account, is_fake = True) + vcard.VcardWindow(c2, self.account, c) def on_history(self, widget, nick): jid = gajim.construct_fjid(self.room_jid, nick) diff --git a/src/gtkexcepthook.py b/src/gtkexcepthook.py index 875da8ee4e7c5c0a0f89259ffa4d3b806b8269f3..7e398979dffa0adbadd16e2ea860b7c37446e70d 100644 --- a/src/gtkexcepthook.py +++ b/src/gtkexcepthook.py @@ -1,18 +1,7 @@ ## gtkexcepthook.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Dimitur Kirov <dkirov@gmail.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## ## Initially written and submitted by Gustavo J. A. M. Carneiro ## @@ -33,6 +22,7 @@ import threading import gtk import pango +from common import i18n import dialogs from cStringIO import StringIO diff --git a/src/gtkgui_helpers.py b/src/gtkgui_helpers.py index fb6e3eaa850c1738eaac52fa0644757b2d0b1964..39690f7fa81addf4bf78c414fd965876ba0a6178 100644 --- a/src/gtkgui_helpers.py +++ b/src/gtkgui_helpers.py @@ -2,7 +2,7 @@ ## ## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> ## Copyright (C) 2004-2005 Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005-2006 Nikos Kouremenos <nkour@jabber.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com> ## Copyright (C) 2005 Travis Shirk <travis@pobox.com> ## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za> @@ -169,30 +169,6 @@ def get_default_font(): return None -def reduce_chars_newlines(text, max_chars = 0, max_lines = 0): - '''Cut the chars after 'max_chars' on each line - and show only the first 'max_lines'. - If any of the params is not present (None or 0) the action - on it is not performed''' - - def _cut_if_long(str): - if len(str) > max_chars: - str = str[:max_chars - 3] + '...' - return str - - if max_lines == 0: - lines = text.split('\n') - else: - lines = text.split('\n', max_lines)[:max_lines] - if max_chars > 0: - if lines: - lines = map(lambda e: _cut_if_long(e), lines) - if lines: - reduced_text = reduce(lambda e, e1: e + '\n' + e1, lines) - else: - reduced_text = '' - return reduced_text - def escape_for_pango_markup(string): # escapes < > & ' " # for pango markup not to break @@ -207,7 +183,30 @@ def escape_for_pango_markup(string): return escaped_str def autodetect_browser_mailer(): - # recognize the environment for appropriate browser/mailer + # recognize the environment and set appropriate browser/mailer + if user_runs_gnome(): + gajim.config.set('openwith', 'gnome-open') + elif user_runs_kde(): + gajim.config.set('openwith', 'kfmclient exec') + elif user_runs_xfce(): + gajim.config.set('openwith', 'exo-open') + else: + gajim.config.set('openwith', 'custom') + +def user_runs_gnome(): + return 'gnome-session' in get_running_processes() + +def user_runs_kde(): + return 'startkde' in get_running_processes() + +def user_runs_xfce(): + procs = get_running_processes() + if 'startxfce4' in procs or 'xfce4-session' in procs: + return True + return False + +def get_running_processes(): + '''returns running processes or None (if not /proc exists)''' if os.path.isdir('/proc'): # under Linux: checking if 'gnome-session' or # 'startkde' programs were run before gajim, by @@ -237,12 +236,9 @@ def autodetect_browser_mailer(): # list of processes processes = [os.path.basename(os.readlink('/proc/' + f +'/exe')) for f in files] - if 'gnome-session' in processes: - gajim.config.set('openwith', 'gnome-open') - elif 'startkde' in processes: - gajim.config.set('openwith', 'kfmclient exec') - else: - gajim.config.set('openwith', 'custom') + + return processes + return [] def move_window(window, x, y): '''moves the window but also checks if out of screen''' @@ -390,7 +386,7 @@ def possibly_move_window_in_current_desktop(window): current virtual desktop window is GTK window''' if os.name == 'nt': - return + return False root_window = gtk.gdk.screen_get_default().get_root_window() # current user's vd @@ -406,6 +402,8 @@ def possibly_move_window_in_current_desktop(window): # we are in another VD that the window was # so show it in current VD window.present() + return True + return False def file_is_locked(path_to_file): '''returns True if file is locked (WINDOWS ONLY)''' @@ -680,6 +678,14 @@ default_name = ''): file_path = dialog.get_filename() file_path = decode_filechooser_file_paths((file_path,))[0] if os.path.exists(file_path): + # check if we have write permissions + if not os.access(file_path, os.W_OK): + file_name = os.path.basename(file_path) + dialogs.ErrorDialog(_('Cannot overwrite existing file "%s"' % + file_name), + _('A file with this name already exists and you do not have ' + 'permission to overwrite it.')) + return dialog2 = dialogs.FTOverwriteConfirmationDialog( _('This file already exists'), _('What do you want to do?'), False) @@ -688,6 +694,13 @@ default_name = ''): response = dialog2.get_response() if response < 0: return + else: + dirname = os.path.dirname(file_path) + if not os.access(dirname, os.W_OK): + dialogs.ErrorDialog(_('Directory "%s" is not writable') % \ + dirname, _('You do not have permission to create files in this' + ' directory.')) + return # Get pixbuf pixbuf = None @@ -710,8 +723,8 @@ default_name = ''): try: pixbuf.save(file_path, type_) except: - #XXX Check for permissions - os.remove(file_path) + if os.path.exists(file_path): + os.remove(file_path) new_file_path = '.'.join(file_path.split('.')[:-1]) + '.jpeg' dialog2 = dialogs.ConfirmationDialog(_('Extension not supported'), _('Image cannot be saved in %(type)s format. Save as %(new_filename)s?') % {'type': type_, 'new_filename': new_file_path}, @@ -735,3 +748,6 @@ default_name = ''): dialog.set_current_name(default_name) dialog.connect('delete-event', lambda widget, event: on_cancel(widget)) + +def on_bm_header_changed_state(widget, event): + widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state diff --git a/src/history_manager.py b/src/history_manager.py index 21d74491f81a84b0028fa9f64ec4b6d38e048c07..bde306c32639ebbb7d3503d62a7b45da4a4a34e9 100755 --- a/src/history_manager.py +++ b/src/history_manager.py @@ -1,7 +1,4 @@ -#!/bin/sh -''':' -exec python -OOt "$0" ${1+"$@"} -' ''' +#!/usr/bin/env python ## history_manager.py ## ## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com> @@ -33,6 +30,9 @@ import dialogs import gtkgui_helpers from common.logger import LOG_DB_PATH, constants +#FIXME: constants should implement 2 way mappings +status = dict((constants.__dict__[i], i[5:].lower()) for i in \ + constants.__dict__.keys() if i.startswith('SHOW_')) from common import gajim from common import helpers @@ -44,14 +44,17 @@ C_SUBJECT, C_NICKNAME ) = range(2, 6) + try: - from pysqlite2 import dbapi2 as sqlite + import sqlite3 as sqlite # python 2.5 except ImportError: - raise exceptions.PysqliteNotAvailable + try: + from pysqlite2 import dbapi2 as sqlite + except ImportError: + raise exceptions.PysqliteNotAvailable class HistoryManager: - def __init__(self): path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png') pix = gtk.gdk.pixbuf_new_from_file(path_to_file) @@ -193,7 +196,9 @@ class HistoryManager: gtk.main_quit() def _fill_jids_listview(self): - self.cur.execute('SELECT jid, jid_id FROM jids ORDER BY jid') + # get those jids that have at least one entry in logs + self.cur.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN (SELECT ' + 'distinct logs.jid_id FROM logs) ORDER BY jid') rows = self.cur.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)] for row in rows: self.jids_already_in.append(row[0]) # jid @@ -310,6 +315,7 @@ class HistoryManager: except ValueError: pass else: + color = None if kind in (constants.KIND_SINGLE_MSG_RECV, constants.KIND_CHAT_MSG_RECV, constants.KIND_GC_MSG): # it is the other side @@ -325,11 +331,14 @@ class HistoryManager: message = '' else: message = ' : ' + message - message = helpers.get_uf_show(show) + message - - message = '<span foreground="%s">%s</span>' % (color, - gtkgui_helpers.escape_for_pango_markup(message)) - self.logs_liststore.append((log_line_id, jid_id, time_, message, + message = helpers.get_uf_show(gajim.SHOW_LIST[show]) + message + + message_ = '<span' + if color: + message_ += ' foreground="%s"' % color + message_ += '>%s</span>' % \ + gtkgui_helpers.escape_for_pango_markup(message) + self.logs_liststore.append((log_line_id, jid_id, time_, message_, subject, nickname)) def _fill_search_results_listview(self, text): diff --git a/src/history_window.py b/src/history_window.py index fba1684ff6401e0a7a9c71b134cd25439cf72487..4a9bc9ceca357f9c6fc18021c73d7f5029a0febc 100644 --- a/src/history_window.py +++ b/src/history_window.py @@ -8,7 +8,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -59,7 +59,8 @@ class HistoryWindow: self.calendar = xml.get_widget('calendar') scrolledwindow = xml.get_widget('scrolledwindow') - self.history_textview = conversation_textview.ConversationTextview(account) + self.history_textview = conversation_textview.ConversationTextview( + account, used_in_history_window = True) scrolledwindow.add(self.history_textview.tv) self.history_buffer = self.history_textview.tv.get_buffer() self.history_buffer.create_tag('highlight', background = 'yellow') @@ -209,7 +210,9 @@ class HistoryWindow: if gajim.config.get('print_time') == 'always': before_str = gajim.config.get('before_time') + before_str = helpers.from_one_line(before_str) after_str = gajim.config.get('after_time') + after_str = helpers.from_one_line(after_str) format = before_str + '%X' + after_str + ' ' tim = time.strftime(format, time.localtime(float(tim))) buf.insert(end_iter, tim) # add time @@ -277,7 +280,9 @@ class HistoryWindow: if contact_name and kind != constants.KIND_GCSTATUS: # add stuff before and after contact name before_str = gajim.config.get('before_nickname') + before_str = helpers.from_one_line(before_str) after_str = gajim.config.get('after_nickname') + after_str = helpers.from_one_line(after_str) format = before_str + contact_name + after_str + ' ' buf.insert_with_tags_by_name(end_iter, format, tag_name) diff --git a/src/htmltextview.py b/src/htmltextview.py new file mode 100644 index 0000000000000000000000000000000000000000..378a4e2360ed97c33099d377d5fcfa19f69c7a0e --- /dev/null +++ b/src/htmltextview.py @@ -0,0 +1,963 @@ +### Copyright (C) 2005 Gustavo J. A. M. Carneiro +### Copyright (C) 2006 Santiago Gala +### +### This library is free software; you can redistribute it and/or +### modify it under the terms of the GNU Lesser General Public +### License as published by the Free Software Foundation; either +### version 2 of the License, or (at your option) any later version. +### +### This library is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### Lesser General Public License for more details. +### +### You should have received a copy of the GNU Lesser General Public +### License along with this library; if not, write to the +### Free Software Foundation, Inc., 59 Temple Place - Suite 330, +### Boston, MA 02111-1307, USA. + + +""" +A gtk.TextView-based renderer for XHTML-IM, as described in: + http://www.jabber.org/jeps/jep-0071.html + +Starting with the version posted by Gustavo Carneiro, +I (Santiago Gala) am trying to make it more compatible +with the markup that docutils generate, and also more +modular. + +""" + +import gobject +import pango +import gtk +import xml.sax, xml.sax.handler +import re +import warnings +from cStringIO import StringIO +import urllib2 +import operator + +from common import gajim +#from common import i18n + + +import tooltips + + +__all__ = ['HtmlTextView'] + +whitespace_rx = re.compile("\\s+") +allwhitespace_rx = re.compile("^\\s*$") + +## pixels = points * display_resolution +display_resolution = 0.3514598*(gtk.gdk.screen_height() / + float(gtk.gdk.screen_height_mm())) + +#embryo of CSS classes +classes = { + #'system-message':';display: none', + 'problematic':';color: red', +} + +#styles for elemens +element_styles = { + 'u' : ';text-decoration: underline', + 'em' : ';font-style: oblique', + 'cite' : '; background-color:rgb(170,190,250); font-style: oblique', + 'li' : '; margin-left: 1em; margin-right: 10%', + 'strong' : ';font-weight: bold', + 'pre' : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%', + 'kbd' : ';background-color:rgb(210,210,210);font-family: monospace', + 'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%', + 'dt' : ';font-weight: bold; font-style: oblique', + 'dd' : ';margin-left: 2em; font-style: oblique' +} +# no difference for the moment +element_styles['dfn'] = element_styles['em'] +element_styles['var'] = element_styles['em'] +# deprecated, legacy, presentational +element_styles['tt'] = element_styles['kbd'] +element_styles['i'] = element_styles['em'] +element_styles['b'] = element_styles['strong'] + +class_styles = { +} + +""" +========== + JEP-0071 +========== + +This Integration Set includes a subset of the modules defined for +XHTML 1.0 but does not redefine any existing modules, nor +does it define any new modules. Specifically, it includes the +following modules only: + +- Structure +- Text + + * Block + + phrasal + addr, blockquote, pre + Struc + div,p + Heading + h1, h2, h3, h4, h5, h6 + + * Inline + + phrasal + abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var + structural + br, span + +- Hypertext (a) +- List (ul, ol, dl) +- Image (img) +- Style Attribute + +Therefore XHTML-IM uses the following content models: + + Block.mix + Block-like elements, e.g., paragraphs + Flow.mix + Any block or inline elements + Inline.mix + Character-level elements + InlineNoAnchor.class + Anchor element + InlinePre.mix + Pre element + +XHTML-IM also uses the following Attribute Groups: + +Core.extra.attrib + TBD +I18n.extra.attrib + TBD +Common.extra + style + + +... +#block level: +#Heading h +# ( pres = h1 | h2 | h3 | h4 | h5 | h6 ) +#Block ( phrasal = address | blockquote | pre ) +#NOT ( presentational = hr ) +# ( structural = div | p ) +#other: section +#Inline ( phrasal = abbr | acronym | cite | code | dfn | em | kbd | q | samp | strong | var ) +#NOT ( presentational = b | big | i | small | sub | sup | tt ) +# ( structural = br | span ) +#Param/Legacy param, font, basefont, center, s, strike, u, dir, menu, isindex +# +""" + +BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', )) +BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', )) +BLOCK_PRES = set(( 'hr', )) #not in xhtml-im +BLOCK_STRUCT = set(( 'div', 'p', )) +BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;) +BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS) + +INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', ')) +INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im +INLINE_STRUCT = set('br, span'.split(', ')) +INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT) + +LIST_ELEMS = set( 'dl, ol, ul'.split(', ')) + +for name in BLOCK_HEAD: + num = eval(name[1]) + size = (num-1) // 2 + weigth = (num - 1) % 2 + element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size], + ('font-weight: bold', 'font-style: oblique')[weigth], + ) + + +def build_patterns(view, config, interface): + #extra, rst does not mark _underline_ or /it/ up + #actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild + basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\ + r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)' + view.basic_pattern_re = re.compile(basic_pattern) + #TODO: emoticons + emoticons_pattern = '' + if config.get('emoticons_theme'): + # When an emoticon is bordered by an alpha-numeric character it is NOT + # expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc. + # We still allow multiple emoticons side-by-side like :P:P:P + # sort keys by length so :qwe emot is checked before :q + keys = interface.emoticons.keys() + keys.sort(interface.on_emoticon_sort) + emoticons_pattern_prematch = '' + emoticons_pattern_postmatch = '' + emoticon_length = 0 + for emoticon in keys: # travel thru emoticons list + emoticon_escaped = re.escape(emoticon) # espace regexp metachars + emoticons_pattern += emoticon_escaped + '|'# | means or in regexp + if (emoticon_length != len(emoticon)): + # Build up expressions to match emoticons next to other emoticons + emoticons_pattern_prematch = emoticons_pattern_prematch[:-1] + ')|(?<=' + emoticons_pattern_postmatch = emoticons_pattern_postmatch[:-1] + ')|(?=' + emoticon_length = len(emoticon) + emoticons_pattern_prematch += emoticon_escaped + '|' + emoticons_pattern_postmatch += emoticon_escaped + '|' + # We match from our list of emoticons, but they must either have + # whitespace, or another emoticon next to it to match successfully + # [\w.] alphanumeric and dot (for not matching 8) in (2.8)) + emoticons_pattern = '|' + \ + '(?:(?<![\w.]' + emoticons_pattern_prematch[:-1] + '))' + \ + '(?:' + emoticons_pattern[:-1] + ')' + \ + '(?:(?![\w.]' + emoticons_pattern_postmatch[:-1] + '))' + + # because emoticons match later (in the string) they need to be after + # basic matches that may occur earlier + emot_and_basic_pattern = basic_pattern + emoticons_pattern + view.emot_and_basic_re = re.compile(emot_and_basic_pattern, re.IGNORECASE) + + +def _parse_css_color(color): + '''_parse_css_color(css_color) -> gtk.gdk.Color''' + if color.startswith("rgb(") and color.endswith(')'): + r, g, b = [int(c)*257 for c in color[4:-1].split(',')] + return gtk.gdk.Color(r, g, b) + else: + return gtk.gdk.color_parse(color) + + +class HtmlHandler(xml.sax.handler.ContentHandler): + + def __init__(self, textview, startiter): + xml.sax.handler.ContentHandler.__init__(self) + self.textbuf = textview.get_buffer() + self.textview = textview + self.iter = startiter + self.text = '' + self.starting=True + self.preserve = False + self.styles = [] # a gtk.TextTag or None, for each span level + self.list_counters = [] # stack (top at head) of list + # counters, or None for unordered list + + def _parse_style_color(self, tag, value): + color = _parse_css_color(value) + tag.set_property("foreground-gdk", color) + + def _parse_style_background_color(self, tag, value): + color = _parse_css_color(value) + tag.set_property("background-gdk", color) + if gtk.gtk_version >= (2, 8): + tag.set_property("paragraph-background-gdk", color) + + + if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1): + + def _get_current_attributes(self): + attrs = self.textview.get_default_attributes() + self.iter.backward_char() + self.iter.get_attributes(attrs) + self.iter.forward_char() + return attrs + + else: + + ## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455 + def _get_current_style_attr(self, propname, comb_oper=None): + tags = [tag for tag in self.styles if tag is not None] + tags.reverse() + is_set_name = propname + "-set" + value = None + for tag in tags: + if tag.get_property(is_set_name): + if value is None: + value = tag.get_property(propname) + if comb_oper is None: + return value + else: + value = comb_oper(value, tag.get_property(propname)) + return value + + class _FakeAttrs(object): + __slots__ = ("font", "font_scale") + + def _get_current_attributes(self): + attrs = self._FakeAttrs() + attrs.font_scale = self._get_current_style_attr("scale", + operator.mul) + if attrs.font_scale is None: + attrs.font_scale = 1.0 + attrs.font = self._get_current_style_attr("font-desc") + if attrs.font is None: + attrs.font = self.textview.style.font_desc + return attrs + + + def __parse_length_frac_size_allocate(self, textview, allocation, + frac, callback, args): + callback(allocation.width*frac, *args) + + def _parse_length(self, value, font_relative, callback, *args): + '''Parse/calc length, converting to pixels, calls callback(length, *args) + when the length is first computed or changes''' + if value.endswith('%'): + frac = float(value[:-1])/100 + if font_relative: + attrs = self._get_current_attributes() + font_size = attrs.font.get_size() / pango.SCALE + callback(frac*display_resolution*font_size, *args) + else: + ## CSS says "Percentage values: refer to width of the closest + ## block-level ancestor" + ## This is difficult/impossible to implement, so we use + ## textview width instead; a reasonable approximation.. + alloc = self.textview.get_allocation() + self.__parse_length_frac_size_allocate(self.textview, alloc, + frac, callback, args) + self.textview.connect("size-allocate", + self.__parse_length_frac_size_allocate, + frac, callback, args) + + elif value.endswith('pt'): # points + callback(float(value[:-2])*display_resolution, *args) + + elif value.endswith('em'): # ems, the height of the element's font + attrs = self._get_current_attributes() + font_size = attrs.font.get_size() / pango.SCALE + callback(float(value[:-2])*display_resolution*font_size, *args) + + elif value.endswith('ex'): # x-height, ~ the height of the letter 'x' + ## FIXME: figure out how to calculate this correctly + ## for now 'em' size is used as approximation + attrs = self._get_current_attributes() + font_size = attrs.font.get_size() / pango.SCALE + callback(float(value[:-2])*display_resolution*font_size, *args) + + elif value.endswith('px'): # pixels + callback(int(value[:-2]), *args) + + else: + warnings.warn("Unable to parse length value '%s'" % value) + + def __parse_font_size_cb(length, tag): + tag.set_property("size-points", length/display_resolution) + __parse_font_size_cb = staticmethod(__parse_font_size_cb) + + def _parse_style_display(self, tag, value): + if value == 'none': + tag.set_property('invisible','true') + #Fixme: display: block, inline + + def _parse_style_font_size(self, tag, value): + try: + scale = { + "xx-small": pango.SCALE_XX_SMALL, + "x-small": pango.SCALE_X_SMALL, + "small": pango.SCALE_SMALL, + "medium": pango.SCALE_MEDIUM, + "large": pango.SCALE_LARGE, + "x-large": pango.SCALE_X_LARGE, + "xx-large": pango.SCALE_XX_LARGE, + } [value] + except KeyError: + pass + else: + attrs = self._get_current_attributes() + tag.set_property("scale", scale / attrs.font_scale) + return + if value == 'smaller': + tag.set_property("scale", pango.SCALE_SMALL) + return + if value == 'larger': + tag.set_property("scale", pango.SCALE_LARGE) + return + self._parse_length(value, True, self.__parse_font_size_cb, tag) + + def _parse_style_font_style(self, tag, value): + try: + style = { + "normal": pango.STYLE_NORMAL, + "italic": pango.STYLE_ITALIC, + "oblique": pango.STYLE_OBLIQUE, + } [value] + except KeyError: + warnings.warn("unknown font-style %s" % value) + else: + tag.set_property("style", style) + + def __frac_length_tag_cb(self,length, tag, propname): + styles = self._get_style_tags() + if styles: + length += styles[-1].get_property(propname) + tag.set_property(propname, length) + #__frac_length_tag_cb = staticmethod(__frac_length_tag_cb) + + def _parse_style_margin_left(self, tag, value): + self._parse_length(value, False, self.__frac_length_tag_cb, + tag, "left-margin") + + def _parse_style_margin_right(self, tag, value): + self._parse_length(value, False, self.__frac_length_tag_cb, + tag, "right-margin") + + def _parse_style_font_weight(self, tag, value): + ## TODO: missing 'bolder' and 'lighter' + try: + weight = { + '100': pango.WEIGHT_ULTRALIGHT, + '200': pango.WEIGHT_ULTRALIGHT, + '300': pango.WEIGHT_LIGHT, + '400': pango.WEIGHT_NORMAL, + '500': pango.WEIGHT_NORMAL, + '600': pango.WEIGHT_BOLD, + '700': pango.WEIGHT_BOLD, + '800': pango.WEIGHT_ULTRABOLD, + '900': pango.WEIGHT_HEAVY, + 'normal': pango.WEIGHT_NORMAL, + 'bold': pango.WEIGHT_BOLD, + } [value] + except KeyError: + warnings.warn("unknown font-style %s" % value) + else: + tag.set_property("weight", weight) + + def _parse_style_font_family(self, tag, value): + tag.set_property("family", value) + + def _parse_style_text_align(self, tag, value): + try: + align = { + 'left': gtk.JUSTIFY_LEFT, + 'right': gtk.JUSTIFY_RIGHT, + 'center': gtk.JUSTIFY_CENTER, + 'justify': gtk.JUSTIFY_FILL, + } [value] + except KeyError: + warnings.warn("Invalid text-align:%s requested" % value) + else: + tag.set_property("justification", align) + + def _parse_style_text_decoration(self, tag, value): + if value == "none": + tag.set_property("underline", pango.UNDERLINE_NONE) + tag.set_property("strikethrough", False) + elif value == "underline": + tag.set_property("underline", pango.UNDERLINE_SINGLE) + tag.set_property("strikethrough", False) + elif value == "overline": + warnings.warn("text-decoration:overline not implemented") + tag.set_property("underline", pango.UNDERLINE_NONE) + tag.set_property("strikethrough", False) + elif value == "line-through": + tag.set_property("underline", pango.UNDERLINE_NONE) + tag.set_property("strikethrough", True) + elif value == "blink": + warnings.warn("text-decoration:blink not implemented") + else: + warnings.warn("text-decoration:%s not implemented" % value) + + def _parse_style_white_space(self, tag, value): + if value == 'pre': + tag.set_property("wrap_mode", gtk.WRAP_NONE) + elif value == 'normal': + tag.set_property("wrap_mode", gtk.WRAP_WORD) + elif value == 'nowrap': + tag.set_property("wrap_mode", gtk.WRAP_NONE) + + + ## build a dictionary mapping styles to methods, for greater speed + __style_methods = dict() + for style in ["background-color", "color", "font-family", "font-size", + "font-style", "font-weight", "margin-left", "margin-right", + "text-align", "text-decoration", "white-space", 'display' ]: + try: + method = locals()["_parse_style_%s" % style.replace('-', '_')] + except KeyError: + warnings.warn("Style attribute '%s' not yet implemented" % style) + else: + __style_methods[style] = method + del style + ## -- + + def _get_style_tags(self): + return [tag for tag in self.styles if tag is not None] + + def _create_url(self, href, title, type_, id_): + tag = self.textbuf.create_tag(id_) + if href and href[0] != '#': + tag.href = href + tag.type_ = type_ # to be used by the URL handler + tag.connect('event', self.textview.html_hyperlink_handler, 'url', href) + tag.set_property('foreground', '#0000ff') + tag.set_property('underline', pango.UNDERLINE_SINGLE) + tag.is_anchor = True + if title: + tag.title = title + return tag + + + def _begin_span(self, style, tag=None, id_=None): + if style is None: + self.styles.append(tag) + return None + if tag is None: + if id_: + tag = self.textbuf.create_tag(id_) + else: + tag = self.textbuf.create_tag() + for attr, val in [item.split(':', 1) for item in style.split(';') if len(item.strip())]: + attr = attr.strip().lower() + val = val.strip() + try: + method = self.__style_methods[attr] + except KeyError: + warnings.warn("Style attribute '%s' requested " + "but not yet implemented" % attr) + else: + method(self, tag, val) + self.styles.append(tag) + + def _end_span(self): + self.styles.pop() + + def _jump_line(self): + self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol') + self.starting = True + + def _insert_text(self, text): + if self.starting and text != '\n': + self.starting = (text[-1] == '\n') + tags = self._get_style_tags() + if tags: + self.textbuf.insert_with_tags(self.iter, text, *tags) + else: + self.textbuf.insert(self.iter, text) + + def _starts_line(self): + return self.starting or self.iter.starts_line() + + def _flush_text(self): + if not self.text: return + text, self.text = self.text, '' + if not self.preserve: + text = text.replace('\n', ' ') + self.handle_specials(whitespace_rx.sub(' ', text)) + else: + self._insert_text(text.strip("\n")) + + def _anchor_event(self, tag, textview, event, iter, href, type_): + if event.type == gtk.gdk.BUTTON_PRESS: + self.textview.emit("url-clicked", href, type_) + return True + return False + + def handle_specials(self, text): + index = 0 + se = self.textview.config.get('show_ascii_formatting_chars') + if self.textview.config.get('emoticons_theme'): + iterator = self.textview.emot_and_basic_re.finditer(text) + else: + iterator = self.textview.basic_pattern_re.finditer(text) + for match in iterator: + start, end = match.span() + special_text = text[start:end] + if start != 0: + self._insert_text(text[index:start]) + index = end # update index + #emoticons + possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS + if self.textview.config.get('emoticons_theme') and \ + possible_emot_ascii_caps in self.textview.interface.emoticons.keys(): + #it's an emoticon + emot_ascii = possible_emot_ascii_caps + anchor = self.textbuf.create_child_anchor(self.iter) + img = gtk.Image() + img.set_from_file(self.textview.interface.emoticons[emot_ascii]) + img.show() + # TODO: add alt/tooltip with the special_text (a11y) + self.textview.add_child_at_anchor(img, anchor) + else: + # now print it + if special_text.startswith('/'): # it's explicit italics + self.startElement('i', {}) + elif special_text.startswith('_'): # it's explicit underline + self.startElement("u", {}) + if se: self._insert_text(special_text[0]) + self.handle_specials(special_text[1:-1]) + if se: self._insert_text(special_text[0]) + if special_text.startswith('_'): # it's explicit underline + self.endElement('u') + if special_text.startswith('/'): # it's explicit italics + self.endElement('i') + if index < len(text): + self._insert_text(text[index:]) + + def characters(self, content): + if self.preserve: + self.text += content + return + if allwhitespace_rx.match(content) is not None and self._starts_line(): + return + self.text += content + self.starting = False + + + def startElement(self, name, attrs): + self._flush_text() + klass = [i for i in attrs.get('class',' ').split(' ') if i] + style = attrs.get('style','') + #Add styles defined for classes + #TODO: priority between class and style elements? + for k in klass: + if k in classes: + style += classes[k] + + tag = None + #FIXME: if we want to use id, it needs to be unique across + # the whole textview, so we need to add something like the + # message-id to it. + #id_ = attrs.get('id',None) + id_ = None + if name == 'a': + #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type + href = attrs.get('href', None) + title = attrs.get('title', attrs.get('rel',href)) + type_ = attrs.get('type', None) + tag = self._create_url(href, title, type_, id_) + elif name == 'blockquote': + cite = attrs.get('cite', None) + if cite: + tag = self.textbuf.create_tag(id_) + tag.title = title + tag.is_anchor = True + elif name in LIST_ELEMS: + style += ';margin-left: 2em' + if name in element_styles: + style += element_styles[name] + + if style == '': + style = None + self._begin_span(style, tag, id_) + + if name == 'br': + pass # handled in endElement + elif name == 'hr': + pass # handled in endElement + elif name in BLOCK: + if not self._starts_line(): + self._jump_line() + if name == 'pre': + self.preserve = True + elif name == 'span': + pass + elif name in ('dl', 'ul'): + if not self._starts_line(): + self._jump_line() + self.list_counters.append(None) + elif name == 'ol': + if not self._starts_line(): + self._jump_line() + self.list_counters.append(0) + elif name == 'li': + if self.list_counters[-1] is None: + li_head = unichr(0x2022) + else: + self.list_counters[-1] += 1 + li_head = "%i." % self.list_counters[-1] + self.text = ' '*len(self.list_counters)*4 + li_head + ' ' + self._flush_text() + self.starting = True + elif name == 'dd': + self._jump_line() + elif name == 'dt': + if not self.starting: + self._jump_line() + elif name == 'img': + try: + ## Max image size = 10 MB (to try to prevent DoS) + mem = urllib2.urlopen(attrs['src']).read(10*1024*1024) + ## Caveat: GdkPixbuf is known not to be safe to load + ## images from network... this program is now potentially + ## hackable ;) + loader = gtk.gdk.PixbufLoader() + loader.write(mem); loader.close() + pixbuf = loader.get_pixbuf() + except Exception, ex: + gajim.log.debug(str('Error loading image'+ex)) + pixbuf = None + alt = attrs.get('alt', "Broken image") + try: + loader.close() + except: pass + if pixbuf is not None: + tags = self._get_style_tags() + if tags: + tmpmark = self.textbuf.create_mark(None, self.iter, True) + + self.textbuf.insert_pixbuf(self.iter, pixbuf) + + if tags: + start = self.textbuf.get_iter_at_mark(tmpmark) + for tag in tags: + self.textbuf.apply_tag(tag, start, self.iter) + self.textbuf.delete_mark(tmpmark) + else: + self._insert_text("[IMG: %s]" % alt) + elif name == 'body' or name == 'html': + pass + elif name == 'a': + pass + elif name in INLINE: + pass + else: + warnings.warn("Unhandled element '%s'" % name) + + def endElement(self, name): + endPreserving = False + newLine = False + if name == 'br': + newLine = True + elif name == 'hr': + #FIXME: plenty of unused attributes (width, height,...) :) + self._jump_line() + try: + self.textbuf.insert_pixbuf(self.iter, self.textview.focus_out_line_pixbuf) + #self._insert_text(u"\u2550"*40) + self._jump_line() + except Exception, e: + gajim.log.debug(str("Error in hr"+e)) + elif name in LIST_ELEMS: + self.list_counters.pop() + elif name == 'li': + newLine = True + elif name == 'img': + pass + elif name == 'body' or name == 'html': + pass + elif name == 'a': + pass + elif name in INLINE: + pass + elif name in ('dd', 'dt', ): + pass + elif name in BLOCK: + if name == 'pre': + endPreserving = True + else: + warnings.warn("Unhandled element '%s'" % name) + self._flush_text() + if endPreserving: + self.preserve = False + if newLine: + self._jump_line() + self._end_span() + #if not self._starts_line(): + # self.text = ' ' + +class HtmlTextView(gtk.TextView): + __gtype_name__ = 'HtmlTextView' + __gsignals__ = { + 'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type + } + + def __init__(self): + gobject.GObject.__init__(self) + self.set_wrap_mode(gtk.WRAP_CHAR) + self.set_editable(False) + self._changed_cursor = False + self.connect("motion-notify-event", self.__motion_notify_event) + self.connect("leave-notify-event", self.__leave_event) + self.connect("enter-notify-event", self.__motion_notify_event) + self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL) + self.tooltip = tooltips.BaseTooltip() + self.config = gajim.config + self.interface = gajim.interface + # end big hack + build_patterns(self,gajim.config,gajim.interface) + + def __leave_event(self, widget, event): + if self._changed_cursor: + window = widget.get_window(gtk.TEXT_WINDOW_TEXT) + window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM)) + self._changed_cursor = False + + def show_tooltip(self, tag): + if not self.tooltip.win: + # check if the current pointer is still over the line + text = getattr(tag, 'title', False) + if text: + pointer = self.get_pointer() + position = self.window.get_origin() + win = self.get_toplevel() + self.tooltip.show_tooltip(text, 8, position[1] + pointer[1]) + + def __motion_notify_event(self, widget, event): + x, y, _ = widget.window.get_pointer() + x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y) + tags = widget.get_iter_at_location(x, y).get_tags() + is_over_anchor = False + for tag in tags: + if getattr(tag, 'is_anchor', False): + is_over_anchor = True + break + if self.tooltip.timeout != 0: + # Check if we should hide the line tooltip + if not is_over_anchor: + self.tooltip.hide_tooltip() + if not self._changed_cursor and is_over_anchor: + window = widget.get_window(gtk.TEXT_WINDOW_TEXT) + window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) + self._changed_cursor = True + gobject.timeout_add(500, + self.show_tooltip, tag) + elif self._changed_cursor and not is_over_anchor: + window = widget.get_window(gtk.TEXT_WINDOW_TEXT) + window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM)) + self._changed_cursor = False + return False + + def display_html(self, html): + buffer = self.get_buffer() + eob = buffer.get_end_iter() + ## this works too if libxml2 is not available + # parser = xml.sax.make_parser(['drv_libxml2']) + # parser.setFeature(xml.sax.handler.feature_validation, True) + parser = xml.sax.make_parser() + parser.setContentHandler(HtmlHandler(self, eob)) + parser.parse(StringIO(html)) + + #if not eob.starts_line(): + # buffer.insert(eob, "\n") + +if gobject.pygtk_version < (2, 8): + gobject.type_register(HtmlTextView) + +change_cursor = None + +if __name__ == '__main__': + + htmlview = HtmlTextView() + + tooltip = tooltips.BaseTooltip() + def on_textview_motion_notify_event(widget, event): + '''change the cursor to a hand when we are over a mail or an url''' + global change_cursor + pointer_x, pointer_y, spam = htmlview.window.get_pointer() + x, y = htmlview.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x, + pointer_y) + tags = htmlview.get_iter_at_location(x, y).get_tags() + if change_cursor: + htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( + gtk.gdk.Cursor(gtk.gdk.XTERM)) + change_cursor = None + tag_table = htmlview.get_buffer().get_tag_table() + over_line = False + for tag in tags: + try: + if tag.is_anchor: + htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( + gtk.gdk.Cursor(gtk.gdk.HAND2)) + change_cursor = tag + elif tag == tag_table.lookup('focus-out-line'): + over_line = True + except: pass + + #if line_tooltip.timeout != 0: + # Check if we should hide the line tooltip + # if not over_line: + # line_tooltip.hide_tooltip() + #if over_line and not line_tooltip.win: + # line_tooltip.timeout = gobject.timeout_add(500, + # show_line_tooltip) + # htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( + # gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) + # change_cursor = tag + + htmlview.connect('motion_notify_event', on_textview_motion_notify_event) + + def handler(texttag, widget, event, iter, kind, href): + if event.type == gtk.gdk.BUTTON_PRESS: + print href + + htmlview.html_hyperlink_handler = handler + + htmlview.display_html('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n' + ' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n' + ' <span style="font-size: 500%; font-family: serif">World</span>\n' + '</div>\n') + htmlview.display_html("<hr />") + htmlview.display_html(""" + <p style='font-size:large'> + <span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>, + I'm <span style='color:green'>green</span> + with <span style='font-weight: bold'>envy</span>! + </p> + """) + htmlview.display_html("<hr />") + htmlview.display_html(""" + <body xmlns='http://www.w3.org/1999/xhtml'> + <p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p> + <p style='margin-left: 5px; margin-right: 2%'> + "A foolish consistency is the hobgoblin of little minds." + </p> + </body> + """) + htmlview.display_html("<hr />") + htmlview.display_html(""" + <body xmlns='http://www.w3.org/1999/xhtml'> + <p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p> + <p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg' + alt='A License to Jabber' + height='261' + width='537'/></p> + </body> + """) + htmlview.display_html("<hr />") + htmlview.display_html(""" + <body xmlns='http://www.w3.org/1999/xhtml'> + <ul style='background-color:rgb(120,140,100)'> + <li> One </li> + <li> Two </li> + <li> Three </li> + </ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n): + def faciter(n,acc): + if n==0: return acc + return faciter(n-1, acc*n) + if n<0: raise ValueError("Must be non-negative") + return faciter(n,1)</pre> + </body> + """) + htmlview.display_html("<hr />") + htmlview.display_html(""" + <body xmlns='http://www.w3.org/1999/xhtml'> + <ol style='background-color:rgb(120,140,100)'> + <li> One </li> + <li> Two is nested: <ul style='background-color:rgb(200,200,100)'> + <li> One </li> + <li> Two </li> + <li> Three </li> + </ul></li> + <li> Three </li></ol> + </body> + """) + htmlview.show() + sw = gtk.ScrolledWindow() + sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC) + sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC) + sw.set_property("border-width", 0) + sw.add(htmlview) + sw.show() + frame = gtk.Frame() + frame.set_shadow_type(gtk.SHADOW_IN) + frame.show() + frame.add(sw) + w = gtk.Window() + w.add(frame) + w.set_default_size(400, 300) + w.show_all() + w.connect("destroy", lambda w: gtk.main_quit()) + gtk.main() diff --git a/src/message_control.py b/src/message_control.py index 1c1b0aa50ca930e009b8eee2457e5401ce0b929b..8c9c0612c61291f5c4429c84ad175c9361137cb3 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -57,7 +57,7 @@ class MessageControl: or inactive (state is False)''' pass # Derived types MUST implement this method - def allow_shutdown(self): + def allow_shutdown(self, method): '''Called to check is a control is allowed to shutdown. If a control is not in a suitable shutdown state this method should return False''' @@ -68,10 +68,6 @@ class MessageControl: # NOTE: Derived classes MUST implement this pass - def notify_on_new_messages(self): - # NOTE: Derived classes MUST implement this - return False - def repaint_themed_widgets(self, theme): pass # NOTE: Derived classes SHOULD implement this diff --git a/src/message_textview.py b/src/message_textview.py index cdf21ab54154d7a46df009d9a237ebb2baa51ebb..de5e61c5f3b84a37663b79a19111a74a97f7c773 100644 --- a/src/message_textview.py +++ b/src/message_textview.py @@ -8,7 +8,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> diff --git a/src/message_window.py b/src/message_window.py index f190956c8ae16831b04815411600d3c1792161dd..2ca82cef8fcc25027caa662d87e3ed5d1b17d4c0 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -4,7 +4,7 @@ ## Vincent Hanquez <tab@snarc.org> ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> ## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> +## Nikos Kouremenos <kourem@gmail.com> ## Dimitur Kirov <dkirov@gmail.com> ## Travis Shirk <travis@pobox.com> ## Norman Rasmussen <norman@rasmussen.co.za> @@ -40,6 +40,13 @@ class MessageWindow: # DND_TARGETS is the targets needed by drag_source_set and drag_dest_set DND_TARGETS = [('GAJIM_TAB', 0, 81)] hid = 0 # drag_data_received handler id + ( + CLOSE_TAB_MIDDLE_CLICK, + CLOSE_ESC, + CLOSE_CLOSE_BUTTON, + CLOSE_COMMAND, + CLOSE_CTRL_KEY + ) = range(5) def __init__(self, acct, type): # A dictionary of dictionaries where _contacts[account][jid] == A MessageControl @@ -104,6 +111,16 @@ class MessageWindow: self.notebook.drag_dest_set(gtk.DEST_DEFAULT_ALL, self.DND_TARGETS, gtk.gdk.ACTION_MOVE) + def change_account_name(self, old_name, new_name): + if self._controls.has_key(old_name): + self._controls[new_name] = self._controls[old_name] + del self._controls[old_name] + for ctrl in self.controls(): + if ctrl.account == old_name: + ctrl.account = new_name + if self.account == old_name: + self.account = new_name + def get_num_controls(self): n = 0 for dict in self._controls.values(): @@ -130,7 +147,7 @@ class MessageWindow: def _on_window_delete(self, win, event): # Make sure all controls are okay with being deleted for ctrl in self.controls(): - if not ctrl.allow_shutdown(): + if not ctrl.allow_shutdown(self.CLOSE_CLOSE_BUTTON): return True # halt the delete return False @@ -189,7 +206,7 @@ class MessageWindow: self.popup_menu(event) elif event.button == 2: # middle click ctrl = self._widget_to_control(child) - self.remove_tab(ctrl) + self.remove_tab(ctrl, self.CLOSE_TAB_MIDDLE_CLICK) def _on_message_textview_mykeypress_event(self, widget, event_keyval, event_keymod): @@ -214,15 +231,17 @@ class MessageWindow: def _on_close_button_clicked(self, button, control): '''When close button is pressed: close a tab''' - self.remove_tab(control) + self.remove_tab(control, self.CLOSE_CLOSE_BUTTON) def show_title(self, urgent = True, control = None): '''redraw the window's title''' unread = 0 for ctrl in self.controls(): if ctrl.type_id == message_control.TYPE_GC and not \ - gajim.config.get('notify_on_all_muc_messages') and not \ - ctrl.attention_flag: + gajim.config.get('notify_on_all_muc_messages') and not \ + ctrl.attention_flag: + # count only pm messages + unread += ctrl.get_nb_unread_pm() continue unread += ctrl.get_nb_unread() @@ -269,10 +288,11 @@ class MessageWindow: ctrl_page = self.notebook.page_num(ctrl.widget) self.notebook.set_current_page(ctrl_page) - def remove_tab(self, ctrl, reason = None): - '''reason is only for gc (offline status message)''' + def remove_tab(self, ctrl, mothod, reason = None, force = False): + '''reason is only for gc (offline status message) + if force is True, do not ask any confirmation''' # Shutdown the MessageControl - if not ctrl.allow_shutdown(): + if not force and not ctrl.allow_shutdown(mothod): return if reason is not None: # We are leaving gc with a status message ctrl.shutdown(reason) @@ -292,28 +312,22 @@ class MessageWindow: if len(self._controls[ctrl.account]) == 0: del self._controls[ctrl.account] - # Notify a dupicate nick to update their banner and clear account display - for c in self.controls(): - if c == self: - continue - if ctrl.contact.get_shown_name() == c.contact.get_shown_name(): - c.draw_banner() - - if self.get_num_controls() == 1: # we are going from two tabs to one - show_tabs_if_one_tab = gajim.config.get('tabs_always_visible') - self.notebook.set_show_tabs(show_tabs_if_one_tab) - if not show_tabs_if_one_tab: - self.alignment.set_property('top-padding', 0) - self.show_title() - elif self.get_num_controls() == 0: + if self.get_num_controls() == 0: # These are not called when the window is destroyed like this, fake it gajim.interface.msg_win_mgr._on_window_delete(self.window, None) gajim.interface.msg_win_mgr._on_window_destroy(self.window) # dnd clean up self.notebook.disconnect(self.hid) self.notebook.drag_dest_unset() - self.window.destroy() + return # don't show_title, we are dead + elif self.get_num_controls() == 1: # we are going from two tabs to one + show_tabs_if_one_tab = gajim.config.get('tabs_always_visible') + self.notebook.set_show_tabs(show_tabs_if_one_tab) + if not show_tabs_if_one_tab: + self.alignment.set_property('top-padding', 0) + self.show_title() + def redraw_tab(self, ctrl, chatstate = None): hbox = self.notebook.get_tab_label(ctrl.widget).get_children()[0] @@ -488,9 +502,9 @@ class MessageWindow: elif event.keyval == gtk.keysyms.Tab: # CTRL + TAB self.move_to_next_unread_tab(True) elif event.keyval == gtk.keysyms.F4: # CTRL + F4 - self.remove_tab(ctrl) + self.remove_tab(ctrl, self.CLOSE_CTRL_KEY) elif event.keyval == gtk.keysyms.w: # CTRL + W - self.remove_tab(ctrl) + self.remove_tab(ctrl, self.CLOSE_CTRL_KEY) # MOD1 (ALT) mask elif event.state & gtk.gdk.MOD1_MASK: @@ -513,7 +527,7 @@ class MessageWindow: # Close tab bindings elif event.keyval == gtk.keysyms.Escape and \ gajim.config.get('escape_key_closes'): # Escape - self.remove_tab(ctrl) + self.remove_tab(ctrl, self.CLOSE_ESC) else: # If the active control has a message_textview pass the event to it active_ctrl = self.get_active_control() @@ -618,7 +632,11 @@ class MessageWindowMgr: # Map the mode to a int constant for frequent compares mode = gajim.config.get('one_message_window') self.mode = common.config.opt_one_window_types.index(mode) - + + def change_account_name(self, old_name, new_name): + for win in self.windows(): + win.change_account_name(old_name, new_name) + def _new_window(self, acct, type): win = MessageWindow(acct, type) # we track the lifetime of this window diff --git a/src/music_track_listener.py b/src/music_track_listener.py new file mode 100644 index 0000000000000000000000000000000000000000..9cffd7a55e042e0f64769758313375e8821fc081 --- /dev/null +++ b/src/music_track_listener.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +## musictracklistener.py +## +## Copyright (C) 2006 Gustavo Carneiro <gjcarneiro@gmail.com> +## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +import gobject +if __name__ == '__main__': + # install _() func before importing dbus_support + from common import i18n + +from common import dbus_support +if dbus_support.supported: + import dbus + import dbus.glib + +class MusicTrackInfo(object): + __slots__ = ['title', 'album', 'artist', 'duration', 'track_number'] + + +class MusicTrackListener(gobject.GObject): + __gsignals__ = { + 'music-track-changed': (gobject.SIGNAL_RUN_LAST, None, (object,)), + } + + _instance = None + @classmethod + def get(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + super(MusicTrackListener, self).__init__() + self._last_playing_music = None + + bus = dbus.SessionBus() + + ## Muine + bus.add_signal_receiver(self._muine_music_track_change_cb, 'SongChanged', + 'org.gnome.Muine.Player') + bus.add_signal_receiver(self._player_name_owner_changed, + 'NameOwnerChanged', 'org.freedesktop.DBus', arg0='org.gnome.Muine') + bus.add_signal_receiver(self._player_playing_changed_cb, 'StateChanged', + 'org.gnome.Muine.Player') + + ## Rhythmbox + bus.add_signal_receiver(self._rhythmbox_music_track_change_cb, + 'playingUriChanged', 'org.gnome.Rhythmbox.Player') + bus.add_signal_receiver(self._player_name_owner_changed, + 'NameOwnerChanged', 'org.freedesktop.DBus', arg0='org.gnome.Rhythmbox') + bus.add_signal_receiver(self._player_playing_changed_cb, + 'playingChanged', 'org.gnome.Rhythmbox.Player') + + def do_music_track_changed(self, info): + if info is not None: + self._last_playing_music = info + + def _player_name_owner_changed(self, name, old, new): + if not new: + self.emit('music-track-changed', None) + + def _player_playing_changed_cb(self, playing): + if playing: + self.emit('music-track-changed', self._last_playing_music) + else: + self.emit('music-track-changed', None) + + def _muine_properties_extract(self, song_string): + d = dict((x.strip() for x in s1.split(':', 1)) for s1 in \ + song_string.split('\n')) + info = MusicTrackInfo() + info.title = d['title'] + info.album = d['album'] + info.artist = d['artist'] + info.duration = int(d['duration']) + info.track_number = int(d['track_number']) + return info + + def _muine_music_track_change_cb(self, arg): + info = self._muine_properties_extract(arg) + self.emit('music-track-changed', info) + + def _rhythmbox_properties_extract(self, props): + info = MusicTrackInfo() + info.title = props['title'] + info.album = props['album'] + info.artist = props['artist'] + info.duration = int(props['duration']) + info.track_number = int(props['track-number']) + return info + + def _rhythmbox_music_track_change_cb(self, uri): + bus = dbus.SessionBus() + rbshellobj = bus.get_object('org.gnome.Rhythmbox', + '/org/gnome/Rhythmbox/Shell') + rbshell = dbus.Interface(rbshellobj, 'org.gnome.Rhythmbox.Shell') + props = rbshell.getSongProperties(uri) + info = self._rhythmbox_properties_extract(props) + self.emit('music-track-changed', info) + + def get_playing_track(self): + '''Return a MusicTrackInfo for the currently playing + song, or None if no song is playing''' + + bus = dbus.SessionBus() + + ## Check Muine playing track + if dbus.dbus_bindings.bus_name_has_owner(bus.get_connection(), + 'org.gnome.Muine'): + obj = bus.get_object('org.gnome.Muine', '/org/gnome/Muine/Player') + player = dbus.Interface(obj, 'org.gnome.Muine.Player') + if player.GetPlaying(): + song_string = player.GetCurrentSong() + song = self._muine_properties_extract(song_string) + self._last_playing_music = song + return song + + ## Check Rhythmbox playing song + if dbus.dbus_bindings.bus_name_has_owner(bus.get_connection(), + 'org.gnome.Rhythmbox'): + rbshellobj = bus.get_object('org.gnome.Rhythmbox', + '/org/gnome/Rhythmbox/Shell') + player = dbus.Interface( + bus.get_object('org.gnome.Rhythmbox', + '/org/gnome/Rhythmbox/Player'), 'org.gnome.Rhythmbox.Player') + rbshell = dbus.Interface(rbshellobj, 'org.gnome.Rhythmbox.Shell') + uri = player.getPlayingUri() + props = rbshell.getSongProperties(uri) + info = self._rhythmbox_properties_extract(props) + self._last_playing_music = info + return info + + return None + +# here we test :) +if __name__ == '__main__': + def music_track_change_cb(listener, music_track_info): + if music_track_info is None: + print "Stop!" + else: + print music_track_info.title + listener = MusicTrackListener.get() + listener.connect('music-track-changed', music_track_change_cb) + track = listener.get_playing_track() + if track is None: + print 'Now not playing anything' + else: + print 'Now playing: "%s" by %s' % (track.title, track.artist) + gobject.MainLoop().run() diff --git a/src/network_manager_listener.py b/src/network_manager_listener.py new file mode 100644 index 0000000000000000000000000000000000000000..47c1a870e0f1dffef3e3c9052fd22ce9498616e0 --- /dev/null +++ b/src/network_manager_listener.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +## network_manager_listener.py +## Copyright (C) 2006 Jeffrey C. Ollie <jeff at ocjtech.us> +## Copyright (C) 2006 Stefan Bethge <stefan at lanpartei.de> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from common import gajim + +def device_now_active(self, *args): + for connection in gajim.connections.itervalues(): + if gajim.config.get_per('accounts', connection.name, 'listen_to_network_manager') and gajim.config.get_per('accounts', connection.name, 'sync_with_global_status'): + connection.change_status('online', '') + +def device_no_longer_active(self, *args): + for connection in gajim.connections.itervalues(): + if gajim.config.get_per('accounts', connection.name, 'listen_to_network_manager') and gajim.config.get_per('accounts', connection.name, 'sync_with_global_status'): + connection.change_status('offline', '') + + +from common.dbus_support import system_bus + +import dbus +import dbus.glib + +bus = system_bus.SystemBus() + +bus.add_signal_receiver(device_no_longer_active, + 'DeviceNoLongerActive', + 'org.freedesktop.NetworkManager', + 'org.freedesktop.NetworkManager', + '/org/freedesktop/NetworkManager') + +bus.add_signal_receiver(device_now_active, + 'DeviceNowActive', + 'org.freedesktop.NetworkManager', + 'org.freedesktop.NetworkManager', + '/org/freedesktop/NetworkManager') + diff --git a/src/notify.py b/src/notify.py index 908ff25d9de2e84fe1c94ce9c2e72dd6e70192d4..c79463a7b2d651ba4737145372bf63227e73b7a1 100644 --- a/src/notify.py +++ b/src/notify.py @@ -4,7 +4,7 @@ ## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> ## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com> ## -## DBUS/libnotify connection code: +## Notification daemon connection via D-Bus code: ## Copyright (C) 2005 by Sebastian Estienne ## ## This program is free software; you can redistribute it and/or modify @@ -25,12 +25,19 @@ import gtkgui_helpers from common import gajim from common import helpers -import dbus_support +from common import dbus_support if dbus_support.supported: import dbus - if dbus_support.version >= (0, 41, 0): - import dbus.glib - import dbus.service + import dbus.glib + import dbus.service + + +USER_HAS_PYNOTIFY = True # user has pynotify module +try: + import pynotify + pynotify.init('Gajim Notification') +except ImportError: + USER_HAS_PYNOTIFY = False def get_show_in_roster(event, account, contact): '''Return True if this event must be shown in roster, else False''' @@ -42,27 +49,23 @@ def get_show_in_roster(event, account, contact): return False if event == 'message_received': chat_control = helpers.get_chat_control(account, contact) - if not chat_control: - return True - elif event == 'ft_request': - return True - return False + if chat_control: + return False + return True def get_show_in_systray(event, account, contact): - '''Return True if this event must be shown in roster, else False''' + '''Return True if this event must be shown in systray, else False''' num = get_advanced_notification(event, account, contact) if num != None: if gajim.config.get_per('notifications', str(num), 'systray') == 'yes': return True if gajim.config.get_per('notifications', str(num), 'systray') == 'no': return False - if event in ('message_received', 'ft_request', 'gc_msg_highlight', - 'ft_request'): - return True - return False + return gajim.config.get('trayicon_notification_on_events') def get_advanced_notification(event, account, contact): - '''Returns the number of the first advanced notification or None''' + '''Returns the number of the first (top most) + advanced notification else None''' num = 0 notif = gajim.config.get_per('notifications', str(num)) while notif: @@ -98,7 +101,7 @@ def get_advanced_notification(event, account, contact): if tab_opened == 'both': tab_opened_ok = True else: - chat_control = helper.get_chat_control(account, contact) + chat_control = helpers.get_chat_control(account, contact) if (chat_control and tab_opened == 'yes') or (not chat_control and \ tab_opened == 'no'): tab_opened_ok = True @@ -109,17 +112,19 @@ def get_advanced_notification(event, account, contact): notif = gajim.config.get_per('notifications', str(num)) def notify(event, jid, account, parameters, advanced_notif_num = None): - '''Check what type of notifications we want, depending on basic configuration - of notifications and advanced one and do these notifications''' + '''Check what type of notifications we want, depending on basic + and the advanced configuration of notifications and do these notifications; + advanced_notif_num holds the number of the first (top most) advanced + notification''' # First, find what notifications we want do_popup = False do_sound = False do_cmd = False - if (event == 'status_change'): + if event == 'status_change': new_show = parameters[0] status_message = parameters[1] - # Default : No popup for status change - elif (event == 'contact_connected'): + # Default: No popup for status change + elif event == 'contact_connected': status_message = parameters j = gajim.get_jid_without_resource(jid) server = gajim.get_server_from_jid(j) @@ -135,43 +140,44 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): 'enabled') and not gajim.block_signed_in_notifications[account] and \ not block_transport: do_sound = True - elif (event == 'contact_disconnected'): + elif event == 'contact_disconnected': status_message = parameters if helpers.allow_showing_notification(account, 'notify_on_signout'): do_popup = True if gajim.config.get_per('soundevents', 'contact_disconnected', 'enabled'): do_sound = True - elif (event == 'new_message'): + elif event == 'new_message': message_type = parameters[0] - first = parameters[1] + is_first_message = parameters[1] nickname = parameters[2] message = parameters[3] if helpers.allow_showing_notification(account, 'notify_on_new_message', - advanced_notif_num, first): + advanced_notif_num, is_first_message): do_popup = True - if first and helpers.allow_sound_notification('first_message_received', - advanced_notif_num): + if is_first_message and helpers.allow_sound_notification( + 'first_message_received', advanced_notif_num): do_sound = True - elif not first and helpers.allow_sound_notification( + elif not is_first_message and helpers.allow_sound_notification( 'next_message_received', advanced_notif_num): do_sound = True else: print '*Event not implemeted yet*' - if advanced_notif_num != None and gajim.config.get_per('notifications', + if advanced_notif_num is not None and gajim.config.get_per('notifications', str(advanced_notif_num), 'run_command'): do_cmd = True # Do the wanted notifications - if (do_popup): - if (event == 'contact_connected' or event == 'contact_disconnected' or \ - event == 'status_change'): # Common code for popup for these 3 events - if (event == 'contact_disconnected'): + if do_popup: + if event in ('contact_connected', 'contact_disconnected', + 'status_change'): # Common code for popup for these three events + if event == 'contact_disconnected': show_image = 'offline.png' suffix = '_notif_size_bw.png' else: #Status Change or Connected - # TODO : for status change, we don't always 'online.png', but we + # FIXME: for status change, + # we don't always 'online.png', but we # first need 48x48 for all status show_image = 'online.png' suffix = '_notif_size_colored.png' @@ -186,7 +192,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): iconset, '48x48', show_image) path = gtkgui_helpers.get_path_to_generic_or_avatar(img, jid = jid, suffix = suffix) - if (event == 'status_change'): + if event == 'status_change': title = _('%(nick)s Changed Status') % \ {'nick': gajim.get_name_from_jid(account, jid)} text = _('%(nick)s is now %(status)s') % \ @@ -196,7 +202,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): text = text + " : " + status_message popup(_('Contact Changed Status'), jid, account, path_to_image = path, title = title, text = text) - elif (event == 'contact_connected'): + elif event == 'contact_connected': title = _('%(nickname)s Signed In') % \ {'nickname': gajim.get_name_from_jid(account, jid)} text = '' @@ -204,7 +210,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): text = status_message popup(_('Contact Signed In'), jid, account, path_to_image = path, title = title, text = text) - elif (event == 'contact_disconnected'): + elif event == 'contact_disconnected': title = _('%(nickname)s Signed Out') % \ {'nickname': gajim.get_name_from_jid(account, jid)} text = '' @@ -212,7 +218,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): text = status_message popup(_('Contact Signed Out'), jid, account, path_to_image = path, title = title, text = text) - elif (event == 'new_message'): + elif event == 'new_message': if message_type == 'normal': # single message event_type = _('New Single Message') img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', @@ -222,10 +228,10 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): text = message elif message_type == 'pm': # private message event_type = _('New Private Message') - room_name, t = gajim.get_room_name_and_server_from_room_jid(jid) + room_name = gajim.get_nick_from_jid(jid) img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'priv_msg_recv.png') - title = _('New Private Message from room %s') % room_name + title = _('New Private Message from group chat %s') % room_name text = _('%(nickname)s: %(message)s') % {'nickname': nickname, 'message': message} else: # chat message @@ -239,18 +245,18 @@ def notify(event, jid, account, parameters, advanced_notif_num = None): popup(event_type, jid, account, message_type, path_to_image = path, title = title, text = text) - if (do_sound): + if do_sound: snd_file = None snd_event = None # If not snd_file, play the event - if (event == 'new_message'): - if advanced_notif_num != None and gajim.config.get_per('notifications', - str(advanced_notif_num), 'sound') == 'yes': + if event == 'new_message': + if advanced_notif_num is not None and gajim.config.get_per( + 'notifications', str(advanced_notif_num), 'sound') == 'yes': snd_file = gajim.config.get_per('notifications', str(advanced_notif_num), 'sound_file') - elif advanced_notif_num != None and gajim.config.get_per( + elif advanced_notif_num is not None and gajim.config.get_per( 'notifications', str(advanced_notif_num), 'sound') == 'no': pass # do not set snd_event - elif first: + elif is_first_message: snd_event = 'first_message_received' else: snd_event = 'next_message_received' @@ -276,20 +282,61 @@ def popup(event_type, jid, account, msg_type = '', path_to_image = None, the older style PopupNotificationWindow method.''' text = gtkgui_helpers.escape_for_pango_markup(text) title = gtkgui_helpers.escape_for_pango_markup(title) + if gajim.config.get('use_notif_daemon') and dbus_support.supported: try: - DesktopNotification(event_type, jid, account, msg_type, path_to_image, - title, text) - return + DesktopNotification(event_type, jid, account, msg_type, + path_to_image, title, text) + return # sucessfully did D-Bus Notification procedure! except dbus.dbus_bindings.DBusException, e: - # Connection to D-Bus failed, try popup + # Connection to D-Bus failed gajim.log.debug(str(e)) except TypeError, e: # This means that we sent the message incorrectly gajim.log.debug(str(e)) - instance = dialogs.PopupNotificationWindow(event_type, jid, account, msg_type, \ - path_to_image, title, text) - gajim.interface.roster.popup_notification_windows.append(instance) + # we failed to speak to notification daemon via D-Bus + if USER_HAS_PYNOTIFY: # try via libnotify + if not text: + text = gajim.get_name_from_jid(account, jid) # default value of text + if not title: + title = event_type + # default image + if not path_to_image: + path_to_image = os.path.abspath( + os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'chat_msg_recv.png')) # img to display + + + notification = pynotify.Notification(title, text) + timeout = gajim.config.get('notification_timeout') * 1000 # make it ms + notification.set_timeout(timeout) + + notification.set_category(event_type) + notification.set_data('event_type', event_type) + notification.set_data('jid', jid) + notification.set_data('account', account) + notification.set_data('msg_type', event_type) + notification.set_data('path_to_image', path_to_image) + notification.add_action('default', 'Default Action', + on_pynotify_notification_clicked) + + notification.show() + + else: # go old style + instance = dialogs.PopupNotificationWindow(event_type, jid, + account, msg_type, path_to_image, title, text) + gajim.interface.roster.popup_notification_windows.append( + instance) + +def on_pynotify_notification_clicked(notification, action): + event_type = notification.get_data('event_type') + jid = notification.get_data('jid') + account = notification.get_data('account') + msg_type = notification.get_data('msg_type') + path_to_image = notification.get_data('path_to_image') + + notification.close() + gajim.interface.handle_event(account, jid, msg_type) class NotificationResponseManager: '''Collects references to pending DesktopNotifications and manages there @@ -342,7 +389,7 @@ class NotificationResponseManager: notification_response_manager = NotificationResponseManager() class DesktopNotification: - '''A DesktopNotification that interfaces with DBus via the Desktop + '''A DesktopNotification that interfaces with D-Bus via the Desktop Notification specification''' def __init__(self, event_type, jid, account, msg_type = '', path_to_image = None, title = None, text = None): diff --git a/src/profile_window.py b/src/profile_window.py index 41bbd8de3c329248d77205ef72de5ffb0cbab361..b5bd628095cb23d4a65be1bc544c6264b5e8a6b6 100644 --- a/src/profile_window.py +++ b/src/profile_window.py @@ -13,18 +13,17 @@ ## GNU General Public License for more details. ## +# THIS FILE IS FOR **OUR** PROFILE (when we edit our INFO) + import gtk import gobject import base64 import mimetypes import os -import time -import locale import gtkgui_helpers import dialogs -from common import helpers from common import gajim from common.i18n import Q_ @@ -60,17 +59,40 @@ class ProfileWindow: def __init__(self, account): self.xml = gtkgui_helpers.get_glade('profile_window.glade') self.window = self.xml.get_widget('profile_window') + self.progressbar = self.xml.get_widget('progressbar') + self.statusbar = self.xml.get_widget('statusbar') + self.context_id = self.statusbar.get_context_id('profile') self.account = account self.jid = gajim.get_jid_from_account(account) self.avatar_mime_type = None self.avatar_encoded = None + self.message_id = self.statusbar.push(self.context_id, + _('Retrieving profile...')) + self.update_progressbar_timeout_id = gobject.timeout_add(100, + self.update_progressbar) + self.remove_statusbar_timeout_id = None + # Create Image for avatar button + image = gtk.Image() + self.xml.get_widget('PHOTO_button').set_image(image) self.xml.signal_autoconnect(self) self.window.show_all() + def update_progressbar(self): + self.progressbar.pulse() + return True # loop forever + + def remove_statusbar(self, message_id): + self.statusbar.remove(self.context_id, message_id) + self.remove_statusbar_timeout_id = None + def on_profile_window_destroy(self, widget): + if self.update_progressbar_timeout_id is not None: + gobject.source_remove(self.update_progressbar_timeout_id) + if self.remove_statusbar_timeout_id is not None: + gobject.source_remove(self.remove_statusbar_timeout_id) del gajim.interface.instances[self.account]['profile'] def on_profile_window_key_press_event(self, widget, event): @@ -79,8 +101,10 @@ class ProfileWindow: def on_clear_button_clicked(self, widget): # empty the image - self.xml.get_widget('PHOTO_image').set_from_icon_name('stock_person', - gtk.ICON_SIZE_DIALOG) + button = self.xml.get_widget('PHOTO_button') + image = button.get_image() + image.set_from_pixbuf(None) + button.set_label(_('Click to set your avatar')) self.avatar_encoded = None self.avatar_mime_type = None @@ -124,24 +148,36 @@ class ProfileWindow: pixbuf = gtkgui_helpers.get_pixbuf_from_data(data) # rescale it pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard') - image = self.xml.get_widget('PHOTO_image') + button = self.xml.get_widget('PHOTO_button') + image = button.get_image() image.set_from_pixbuf(pixbuf) + button.set_label('') self.avatar_encoded = base64.encodestring(data) # returns None if unknown type self.avatar_mime_type = mimetypes.guess_type(path_to_file)[0] - self.dialog = dialogs.ImageChooserDialog(on_response_ok = on_ok) + def on_clear(widget): + self.dialog.destroy() + self.on_clear_button_clicked(widget) + + self.dialog = dialogs.AvatarChooserDialog(on_response_ok = on_ok, + on_response_clear = on_clear) def on_PHOTO_button_press_event(self, widget, event): '''If right-clicked, show popup''' if event.button == 3 and self.avatar_encoded: # right click menu = gtk.Menu() - nick = gajim.config.get_per('accounts', self.account, 'name') - menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) - menuitem.connect('activate', - gtkgui_helpers.on_avatar_save_as_menuitem_activate, - self.jid, None, nick + '.jpeg') - menu.append(menuitem) + + # Try to get pixbuf + pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(self.jid) + + if pixbuf: + nick = gajim.config.get_per('accounts', self.account, 'name') + menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) + menuitem.connect('activate', + gtkgui_helpers.on_avatar_save_as_menuitem_activate, + self.jid, None, nick + '.jpeg') + menu.append(menuitem) # show clear menuitem = gtk.ImageMenuItem(gtk.STOCK_CLEAR) menuitem.connect('activate', self.on_clear_button_clicked) @@ -162,18 +198,23 @@ class ProfileWindow: def set_values(self, vcard): if not 'PHOTO' in vcard: # set default image - image = self.xml.get_widget('PHOTO_image') - image.set_from_icon_name('stock_person', gtk.ICON_SIZE_DIALOG) + button = self.xml.get_widget('PHOTO_button') + image = button.get_image() + image.set_from_pixbuf(None) + button.set_label(_('Click to set your avatar')) for i in vcard.keys(): if i == 'PHOTO': pixbuf, self.avatar_encoded, self.avatar_mime_type = \ get_avatar_pixbuf_encoded_mime(vcard[i]) - image = self.xml.get_widget('PHOTO_image') + button = self.xml.get_widget('PHOTO_button') + image = button.get_image() if not pixbuf: - image.set_from_icon_name('stock_person', gtk.ICON_SIZE_DIALOG) + image.set_from_pixbuf(None) + button.set_label(_('Click to set your avatar')) continue pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard') image.set_from_pixbuf(pixbuf) + button.set_label('') continue if i == 'ADR' or i == 'TEL' or i == 'EMAIL': for entry in vcard[i]: @@ -191,6 +232,18 @@ class ProfileWindow: vcard[i], 0) else: self.set_value(i + '_entry', vcard[i]) + if self.update_progressbar_timeout_id is not None: + if self.message_id: + self.statusbar.remove(self.context_id, self.message_id) + self.message_id = self.statusbar.push(self.context_id, + _('Information received')) + self.remove_statusbar_timeout_id = gobject.timeout_add(3000, + self.remove_statusbar, self.message_id) + gobject.source_remove(self.update_progressbar_timeout_id) + # redraw progressbar after avatar is set so that windows is already + # resized. Else progressbar is not correctly redrawn + gobject.idle_add(self.progressbar.set_fraction, 0) + self.update_progressbar_timeout_id = None def add_to_vcard(self, vcard, entry, txt): '''Add an information to the vCard dictionary''' @@ -248,6 +301,9 @@ class ProfileWindow: return vcard def on_publish_button_clicked(self, widget): + if self.update_progressbar_timeout_id: + # Operation in progress + return if gajim.connections[self.account].connected < 2: dialogs.ErrorDialog(_('You are not connected to the server'), _('Without a connection you can not publish your contact ' @@ -261,8 +317,42 @@ class ProfileWindow: nick = gajim.config.get_per('accounts', self.account, 'name') gajim.nicks[self.account] = nick gajim.connections[self.account].send_vcard(vcard) + self.message_id = self.statusbar.push(self.context_id, + _('Sending profile...')) + self.update_progressbar_timeout_id = gobject.timeout_add(100, + self.update_progressbar) + + def vcard_published(self): + if self.message_id: + self.statusbar.remove(self.context_id, self.message_id) + self.message_id = self.statusbar.push(self.context_id, + _('Information published')) + self.remove_statusbar_timeout_id = gobject.timeout_add(3000, + self.remove_statusbar, self.message_id) + if self.update_progressbar_timeout_id is not None: + gobject.source_remove(self.update_progressbar_timeout_id) + self.progressbar.set_fraction(0) + self.update_progressbar_timeout_id = None + + def vcard_not_published(self): + if self.message_id: + self.statusbar.remove(self.context_id, self.message_id) + self.message_id = self.statusbar.push(self.context_id, + _('Information NOT published')) + self.remove_statusbar_timeout_id = gobject.timeout_add(3000, + self.remove_statusbar, self.message_id) + if self.update_progressbar_timeout_id is not None: + gobject.source_remove(self.update_progressbar_timeout_id) + self.progressbar.set_fraction(0) + self.update_progressbar_timeout_id = None + dialogs.InformationDialog(_('vCard publication failed'), + _('There was an error while publishing your personal information, ' + 'try again later.')) def on_retrieve_button_clicked(self, widget): + if self.update_progressbar_timeout_id: + # Operation in progress + return entries = ['FN', 'NICKNAME', 'BDAY', 'EMAIL_HOME_USERID', 'URL', 'TEL_HOME_NUMBER', 'N_FAMILY', 'N_GIVEN', 'N_MIDDLE', 'N_PREFIX', 'N_SUFFIX', 'ADR_HOME_STREET', 'ADR_HOME_EXTADR', 'ADR_HOME_LOCALITY', @@ -275,9 +365,18 @@ class ProfileWindow: for e in entries: self.xml.get_widget(e + '_entry').set_text('') self.xml.get_widget('DESC_textview').get_buffer().set_text('') - self.xml.get_widget('PHOTO_image').set_from_icon_name('stock_person', - gtk.ICON_SIZE_DIALOG) + button = self.xml.get_widget('PHOTO_button') + image = button.get_image() + image.set_from_pixbuf(None) + button.set_label(_('Click to set your avatar')) gajim.connections[self.account].request_vcard(self.jid) else: dialogs.ErrorDialog(_('You are not connected to the server'), - _('Without a connection, you can not get your contact information.')) + _('Without a connection, you can not get your contact information.')) + self.message_id = self.statusbar.push(self.context_id, + _('Retrieving profile...')) + self.update_progressbar_timeout_id = gobject.timeout_add(100, + self.update_progressbar) + + def on_close_button_clicked(self, widget): + self.window.destroy() diff --git a/src/remote_control.py b/src/remote_control.py index 82dbc54b63d3dfdf277e7d9cc8439c236e40f710..a87c892945bad6f624c8ca871148dc03f1f0b061 100644 --- a/src/remote_control.py +++ b/src/remote_control.py @@ -1,19 +1,9 @@ ## remote_control.py ## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Dimitur Kirov <dkirov@gmail.com> -## - Andrew Sayman <lorien420@myrealbox.com> -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> +## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com> +## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -33,60 +23,33 @@ from common import helpers from time import time from dialogs import AddNewContactWindow, NewChatDialog -import dbus_support +from common import dbus_support if dbus_support.supported: import dbus - if dbus_support.version >= (0, 41, 0): + if dbus_support: import dbus.service - import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it - DbusPrototype = dbus.service.Object - elif dbus_support.version >= (0, 20, 0): - DbusPrototype = dbus.Object - else: #dbus is not defined - DbusPrototype = str + import dbus.glib INTERFACE = 'org.gajim.dbus.RemoteInterface' OBJ_PATH = '/org/gajim/dbus/RemoteObject' SERVICE = 'org.gajim.dbus' -# type mapping, it is different in each version -ident = lambda e: e -if dbus_support.version[1] >= 43: - # in most cases it is a utf-8 string - DBUS_STRING = dbus.String - - # general type (for use in dicts, - # where all values should have the same type) - DBUS_VARIANT = dbus.Variant - DBUS_BOOLEAN = dbus.Boolean - DBUS_DOUBLE = dbus.Double - DBUS_INT32 = dbus.Int32 - # dictionary with string key and binary value - DBUS_DICT_SV = lambda : dbus.Dictionary({}, signature="sv") - # dictionary with string key and value - DBUS_DICT_SS = lambda : dbus.Dictionary({}, signature="ss") - # empty type - DBUS_NONE = lambda : dbus.Variant(0) - -else: # 33, 35, 36 - DBUS_DICT_SV = lambda : {} - DBUS_DICT_SS = lambda : {} - DBUS_STRING = lambda e: unicode(e).encode('utf-8') - # this is the only way to return lists and dicts of mixed types - DBUS_VARIANT = lambda e: (isinstance(e, (str, unicode)) and \ - DBUS_STRING(e)) or repr(e) - DBUS_NONE = lambda : '' - if dbus_support.version[1] >= 41: # 35, 36 - DBUS_BOOLEAN = dbus.Boolean - DBUS_DOUBLE = dbus.Double - DBUS_INT32 = dbus.Int32 - else: # 33 - DBUS_BOOLEAN = ident - DBUS_INT32 = ident - DBUS_DOUBLE = ident - -STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', - 'invisible'] +# type mapping + +# in most cases it is a utf-8 string +DBUS_STRING = dbus.String + +# general type (for use in dicts, where all values should have the same type) +DBUS_VARIANT = dbus.Variant +DBUS_BOOLEAN = dbus.Boolean +DBUS_DOUBLE = dbus.Double +DBUS_INT32 = dbus.Int32 +# dictionary with string key and binary value +DBUS_DICT_SV = lambda : dbus.Dictionary({}, signature="sv") +# dictionary with string key and value +DBUS_DICT_SS = lambda : dbus.Dictionary({}, signature="ss") +# empty type +DBUS_NONE = lambda : dbus.Variant(0) def get_dbus_struct(obj): ''' recursively go through all the items and replace @@ -123,65 +86,35 @@ class Remote: self.signal_object = None session_bus = dbus_support.session_bus.SessionBus() - if dbus_support.version[1] >= 41: - service = dbus.service.BusName(SERVICE, bus=session_bus) - self.signal_object = SignalObject(service) - elif dbus_support.version[1] <= 40 and dbus_support.version[1] >= 20: - service=dbus.Service(SERVICE, session_bus) - self.signal_object = SignalObject(service) + service = dbus.service.BusName(SERVICE, bus=session_bus) + self.signal_object = SignalObject(service) def raise_signal(self, signal, arg): if self.signal_object: self.signal_object.raise_signal(signal, - get_dbus_struct(arg)) + get_dbus_struct(arg)) -class SignalObject(DbusPrototype): - ''' Local object definition for /org/gajim/dbus/RemoteObject. This doc must - not be visible, because the clients can access only the remote object. ''' +class SignalObject(dbus.service.Object): + ''' Local object definition for /org/gajim/dbus/RemoteObject. + (This docstring is not be visible, because the clients can access only the remote object.)''' def __init__(self, service): self.first_show = True self.vcard_account = None # register our dbus API - if dbus_support.version[1] >= 41: - DbusPrototype.__init__(self, service, OBJ_PATH) - elif dbus_support.version[1] >= 30: - DbusPrototype.__init__(self, OBJ_PATH, service) - else: - DbusPrototype.__init__(self, OBJ_PATH, service, - [ self.toggle_roster_appearance, - self.show_next_unread, - self.list_contacts, - self.list_accounts, - self.account_info, - self.change_status, - self.open_chat, - self.send_message, - self.send_single_message, - self.contact_info, - self.send_file, - self.prefs_list, - self.prefs_store, - self.prefs_del, - self.prefs_put, - self.add_contact, - self.remove_contact, - self.get_status, - self.get_status_message, - self.start_chat, - self.send_xml, - ]) + dbus.service.Object.__init__(self, service, OBJ_PATH) def raise_signal(self, signal, arg): - ''' raise a signal, with a single string message ''' + '''raise a signal, with a single string message''' from dbus import dbus_bindings message = dbus_bindings.Signal(OBJ_PATH, INTERFACE, signal) i = message.get_iter(True) i.append(arg) self._connection.send(message) + @dbus.service.method(INTERFACE) def get_status(self, *args): '''get_status(account = None) returns status (show to be exact) which is the global one @@ -193,8 +126,9 @@ class SignalObject(DbusPrototype): return helpers.get_global_show() # return show for the given account index = gajim.connections[account].connected - return DBUS_STRING(STATUS_LIST[index]) + return DBUS_STRING(gajim.SHOW_LIST[index]) + @dbus.service.method(INTERFACE) def get_status_message(self, *args): '''get_status(account = None) returns status which is the global one @@ -208,7 +142,7 @@ class SignalObject(DbusPrototype): status = gajim.connections[account].status return DBUS_STRING(status) - + @dbus.service.method(INTERFACE) def get_account_and_contact(self, account, jid): ''' get the account (if not given) and contact instance from jid''' connected_account = None @@ -236,6 +170,7 @@ class SignalObject(DbusPrototype): return connected_account, contact + @dbus.service.method(INTERFACE) def send_file(self, *args): '''send_file(file_path, jid, account=None) send file, located at 'file_path' to 'jid', using account @@ -254,7 +189,7 @@ class SignalObject(DbusPrototype): return False def _send_message(self, jid, message, keyID, account, type = 'chat', subject = None): - ''' can be called from send_chat_message (default when send_message) + '''can be called from send_chat_message (default when send_message) or send_single_message''' if not jid or not message: return None # or raise error @@ -269,28 +204,31 @@ class SignalObject(DbusPrototype): return True return False + @dbus.service.method(INTERFACE) def send_chat_message(self, *args): - ''' send_message(jid, message, keyID=None, account=None) + '''send_message(jid, message, keyID=None, account=None) send chat 'message' to 'jid', using account (optional) 'account'. if keyID is specified, encrypt the message with the pgp key ''' jid, message, keyID, account = self._get_real_arguments(args, 4) jid = self._get_real_jid(jid, account) return self._send_message(jid, message, keyID, account) + @dbus.service.method(INTERFACE) def send_single_message(self, *args): - ''' send_single_message(jid, subject, message, keyID=None, account=None) + '''send_single_message(jid, subject, message, keyID=None, account=None) send single 'message' to 'jid', using account (optional) 'account'. if keyID is specified, encrypt the message with the pgp key ''' jid, subject, message, keyID, account = self._get_real_arguments(args, 5) jid = self._get_real_jid(jid, account) return self._send_message(jid, message, keyID, account, type, subject) + @dbus.service.method(INTERFACE) def open_chat(self, *args): ''' start_chat(jid, account=None) -> shows the tabbed window for new message to 'jid', using account(optional) 'account' ''' jid, account = self._get_real_arguments(args, 2) if not jid: - # FIXME: raise exception for missing argument (dbus0.35+) + raise MissingArgument return None jid = self._get_real_jid(jid, account) @@ -332,13 +270,14 @@ class SignalObject(DbusPrototype): return True return False + @dbus.service.method(INTERFACE) def change_status(self, *args, **keywords): ''' change_status(status, message, account). account is optional - if not specified status is changed for all accounts. ''' status, message, account = self._get_real_arguments(args, 3) if status not in ('offline', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible'): - # FIXME: raise exception for bad status (dbus0.35) + raise InvalidArgument return None if account: gobject.idle_add(gajim.interface.roster.send_status, account, @@ -346,25 +285,29 @@ class SignalObject(DbusPrototype): else: # account not specified, so change the status of all accounts for acc in gajim.contacts.get_accounts(): + if not gajim.config.get_per('accounts', acc, 'sync_with_global_status'): + continue gobject.idle_add(gajim.interface.roster.send_status, acc, status, message) return None - def show_next_unread(self, *args): - ''' Show the window(s) with next waiting messages in tabbed/group chats. ''' + @dbus.service.method(INTERFACE) + def show_next_pending_event(self, *args): + '''Show the window(s) with next pending event in tabbed/group chats.''' if gajim.events.get_nb_events(): gajim.interface.systray.handle_first_event() + @dbus.service.method(INTERFACE) def contact_info(self, *args): - ''' get vcard info for a contact. Return cached value of the vcard. + '''get vcard info for a contact. Return cached value of the vcard. ''' [jid] = self._get_real_arguments(args, 1) if not isinstance(jid, unicode): jid = unicode(jid) if not jid: - # FIXME: raise exception for missing argument (0.3+) + raise MissingArgument return None - jid = self._get_real_jid(jid, account) + jid = self._get_real_jid(jid) cached_vcard = gajim.connections.values()[0].get_cached_vcard(jid) if cached_vcard: @@ -373,8 +316,9 @@ class SignalObject(DbusPrototype): # return empty dict return DBUS_DICT_SV() + @dbus.service.method(INTERFACE) def list_accounts(self, *args): - ''' list register accounts ''' + '''list register accounts''' result = gajim.contacts.get_accounts() if result and len(result) > 0: result_array = [] @@ -383,8 +327,9 @@ class SignalObject(DbusPrototype): return result_array return None + @dbus.service.method(INTERFACE) def account_info(self, *args): - ''' show info on account: resource, jid, nick, prio, message ''' + '''show info on account: resource, jid, nick, prio, message''' [for_account] = self._get_real_arguments(args, 1) if not gajim.connections.has_key(for_account): # account is invalid @@ -392,19 +337,19 @@ class SignalObject(DbusPrototype): account = gajim.connections[for_account] result = DBUS_DICT_SS() index = account.connected - result['status'] = DBUS_STRING(STATUS_LIST[index]) + result['status'] = DBUS_STRING(gajim.SHOW_LIST[index]) result['name'] = DBUS_STRING(account.name) result['jid'] = DBUS_STRING(gajim.get_jid_from_account(account.name)) result['message'] = DBUS_STRING(account.status) - result['priority'] = DBUS_STRING(unicode(gajim.config.get_per('accounts', - account.name, 'priority'))) + result['priority'] = DBUS_STRING(unicode(account.priority)) result['resource'] = DBUS_STRING(unicode(gajim.config.get_per('accounts', - account.name, 'resource'))) + account.name, 'resource'))) return result + @dbus.service.method(INTERFACE) def list_contacts(self, *args): - ''' list all contacts in the roster. If the first argument is specified, - then return the contacts for the specified account ''' + '''list all contacts in the roster. If the first argument is specified, + then return the contacts for the specified account''' [for_account] = self._get_real_arguments(args, 1) result = [] accounts = gajim.contacts.get_accounts() @@ -426,6 +371,7 @@ class SignalObject(DbusPrototype): return None return result + @dbus.service.method(INTERFACE) def toggle_roster_appearance(self, *args): ''' shows/hides the roster window ''' win = gajim.interface.roster.window @@ -439,6 +385,7 @@ class SignalObject(DbusPrototype): else: win.window.focus(long(time())) + @dbus.service.method(INTERFACE) def prefs_list(self, *args): prefs_dict = DBUS_DICT_SS() def get_prefs(data, name, path, value): @@ -453,6 +400,7 @@ class SignalObject(DbusPrototype): gajim.config.foreach(get_prefs) return prefs_dict + @dbus.service.method(INTERFACE) def prefs_store(self, *args): try: gajim.interface.save_config() @@ -460,6 +408,7 @@ class SignalObject(DbusPrototype): return False return True + @dbus.service.method(INTERFACE) def prefs_del(self, *args): [key] = self._get_real_arguments(args, 1) if not key: @@ -473,6 +422,7 @@ class SignalObject(DbusPrototype): gajim.config.del_per(key_path[0], key_path[1], key_path[2]) return True + @dbus.service.method(INTERFACE) def prefs_put(self, *args): [key] = self._get_real_arguments(args, 1) if not key: @@ -486,6 +436,7 @@ class SignalObject(DbusPrototype): gajim.config.set_per(key_path[0], key_path[1], subname, value) return True + @dbus.service.method(INTERFACE) def add_contact(self, *args): [jid, account] = self._get_real_arguments(args, 2) if account: @@ -501,6 +452,7 @@ class SignalObject(DbusPrototype): AddNewContactWindow(account = None, jid = jid) return True + @dbus.service.method(INTERFACE) def remove_contact(self, *args): [jid, account] = self._get_real_arguments(args, 2) jid = self._get_real_jid(jid, account) @@ -595,9 +547,11 @@ class SignalObject(DbusPrototype): contact_dict['resources'] = DBUS_VARIANT(contact_dict['resources']) return contact_dict + @dbus.service.method(INTERFACE) def get_unread_msgs_number(self, *args): - return str(gajim.events.get_nb_events) + return str(gajim.events.get_nb_events()) + @dbus.service.method(INTERFACE) def start_chat(self, *args): [account] = self._get_real_arguments(args, 1) if not account: @@ -606,6 +560,7 @@ class SignalObject(DbusPrototype): NewChatDialog(account) return True + @dbus.service.method(INTERFACE) def send_xml(self, *args): xml, account = self._get_real_arguments(args, 2) if account: @@ -613,36 +568,3 @@ class SignalObject(DbusPrototype): else: for acc in gajim.contacts.get_accounts(): gajim.connections[acc].send_stanza(xml) - - if dbus_support.version[1] >= 30 and dbus_support.version[1] <= 40: - method = dbus.method - signal = dbus.signal - elif dbus_support.version[1] >= 41: - method = dbus.service.method - signal = dbus.service.signal - - # prevent using decorators, because they are not supported - # on python < 2.4 - # FIXME: use decorators when python2.3 (and dbus 0.23) is OOOOOOLD - toggle_roster_appearance = method(INTERFACE)(toggle_roster_appearance) - list_contacts = method(INTERFACE)(list_contacts) - list_accounts = method(INTERFACE)(list_accounts) - show_next_unread = method(INTERFACE)(show_next_unread) - change_status = method(INTERFACE)(change_status) - open_chat = method(INTERFACE)(open_chat) - contact_info = method(INTERFACE)(contact_info) - send_message = method(INTERFACE)(send_chat_message) - send_single_message = method(INTERFACE)(send_single_message) - send_file = method(INTERFACE)(send_file) - prefs_list = method(INTERFACE)(prefs_list) - prefs_put = method(INTERFACE)(prefs_put) - prefs_del = method(INTERFACE)(prefs_del) - prefs_store = method(INTERFACE)(prefs_store) - remove_contact = method(INTERFACE)(remove_contact) - add_contact = method(INTERFACE)(add_contact) - get_status = method(INTERFACE)(get_status) - get_status_message = method(INTERFACE)(get_status_message) - account_info = method(INTERFACE)(account_info) - get_unread_msgs_number = method(INTERFACE)(get_unread_msgs_number) - start_chat = method(INTERFACE)(start_chat) - send_xml = method(INTERFACE)(send_xml) diff --git a/src/roster_window.py b/src/roster_window.py index c14c467b95744e7119af2ac9b80b0e9a57f2a4d9..c773a7265d1bc497c47549018c058b10e323943b 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ## roster_window.py ## ## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> @@ -35,11 +36,18 @@ import notify from common import gajim from common import helpers +from common import passwords +from common.exceptions import GajimGeneralException + from message_window import MessageWindowMgr from chat_control import ChatControl from groupchat_control import GroupchatControl from groupchat_control import PrivateChatControl +from common import dbus_support +if dbus_support.supported: + from music_track_listener import MusicTrackListener + #(icon, name, type, jid, account, editable, second pixbuf) ( C_IMG, # image to show state (online, new message etc) @@ -51,11 +59,8 @@ C_EDITABLE, # cellrenderer text that holds name editable or not? C_SECPIXBUF, # secondary_pixbuf (holds avatar or padlock) ) = range(7) - -DEFAULT_ICONSET = 'dcraven' - class RosterWindow: - '''Class for main window of gtkgui interface''' + '''Class for main window of the GTK+ interface''' def get_account_iter(self, name): model = self.tree.get_model() @@ -76,9 +81,8 @@ class RosterWindow: root = self.get_account_iter(account) group_iter = model.iter_children(root) # C_NAME column contacts the pango escaped group name - name = gtkgui_helpers.escape_for_pango_markup(name) while group_iter: - group_name = model[group_iter][C_NAME].decode('utf-8') + group_name = model[group_iter][C_JID].decode('utf-8') if name == group_name: break group_iter = model.iter_next(group_iter) @@ -129,6 +133,31 @@ class RosterWindow: group_iter = model.iter_next(group_iter) return found + def get_path(self, jid, account): + ''' Try to get line of contact in roster ''' + iters = self.get_contact_iter(jid, account) + if iters: + path = self.tree.get_model().get_path(iters[0]) + else: + path = None + return path + + def show_and_select_path(self, path, jid, account): + '''Show contact in roster (if he is invisible for example) + and select line''' + if not path: + # contact is in roster but we curently don't see him online + # show him + self.add_contact_to_roster(jid, account) + iters = self.get_contact_iter(jid, account) + path = self.tree.get_model().get_path(iters[0]) + # popup == False so we show awaiting event in roster + # show and select contact line in roster (even if he is not in roster) + self.tree.expand_row(path[0:1], False) + self.tree.expand_row(path[0:2], False) + self.tree.scroll_to_cell(path) + self.tree.set_cursor(path) + def add_account_to_roster(self, account): model = self.tree.get_model() if self.get_account_iter(account): @@ -144,8 +173,7 @@ class RosterWindow: show = gajim.SHOW_LIST[gajim.connections[account].connected] tls_pixbuf = None - if gajim.con_types.has_key(account) and \ - gajim.con_types[account] in ('tls', 'ssl'): + if gajim.account_is_securely_connected(account): tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) # the only way to create a pixbuf from stock @@ -163,12 +191,8 @@ class RosterWindow: else: accounts = [account] num_of_accounts = len(accounts) - num_of_secured = 0 - for acct in accounts: - if gajim.con_types.has_key(acct) and \ - gajim.con_types[acct] in ('tls', 'ssl'): - num_of_secured += 1 - if num_of_secured: + num_of_secured = gajim.get_number_of_securely_connected_accounts() + if num_of_secured and gajim.con_types.has_key(account) and gajim.con_types[account] in ('tls', 'ssl'): tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) # the only way to create a pixbuf from stock if num_of_secured < num_of_accounts: @@ -193,12 +217,20 @@ class RosterWindow: else: model[iter][C_SECPIXBUF] = None path = model.get_path(iter) + account_name = account + accounts = [account] if self.regroup: - account = _('Merged accounts') + account_name = _('Merged accounts') + accounts = [] if not self.tree.row_expanded(path) and model.iter_has_child(iter): - model[iter][C_NAME] = '[%s]' % account - else: - model[iter][C_NAME] = account + # account row not expanded + account_name = '[%s]' % account_name + if gajim.account_is_connected(account) or (self.regroup and \ + gajim.get_number_of_connected_accounts()): + nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts( + accounts = accounts) + account_name += ' (%s/%s)' % (repr(nbr_on),repr(nbr_total)) + model[iter][C_NAME] = account_name def remove_newly_added(self, jid, account): if jid in gajim.newly_added[account]: @@ -207,13 +239,19 @@ class RosterWindow: def add_contact_to_roster(self, jid, account): '''Add a contact to the roster and add groups if they aren't in roster - force is about force to add it, even if it is offline and show offline + force is about force to add it, even if it is offline and show offline is False, because it has online children, so we need to show it. If add_children is True, we also add all children, even if they were not already drawn''' showOffline = gajim.config.get('showoffline') model = self.tree.get_model() contact = gajim.contacts.get_first_contact_from_jid(account, jid) + nb_events = gajim.events.get_nb_roster_events(account, contact.jid) + # count events from all resources + for contact_ in gajim.contacts.get_contact(account, jid): + if contact_.resource: + nb_events += gajim.events.get_nb_roster_events(account, + contact_.get_full_jid()) if not contact: return # If contact already in roster, do not add it @@ -223,23 +261,19 @@ class RosterWindow: self.add_self_contact(account) return if gajim.jid_is_transport(contact.jid): + # if jid is transport, check if we wanna show it in roster + if not gajim.config.get('show_transports_group') and not nb_events: + return contact.groups = [_('Transports')] + elif not showOffline and not gajim.account_is_connected(account) and \ + nb_events == 0: + return - # JEP-0162 - hide = True - if contact.sub in ('both', 'to'): - hide = False - elif contact.ask == 'subscribe': - hide = False - elif contact.name or len(contact.groups): - hide = False - - observer = False - if hide: - if contact.sub == 'from': - observer = True - else: - return + # XEP-0162 + hide = contact.is_hidden_from_roster() + if hide and contact.sub != 'from': + return + observer = contact.is_observer() if observer: # if he has a tag, remove it @@ -289,10 +323,10 @@ class RosterWindow: return if (contact.show in ('offline', 'error') or hide) and \ - not showOffline and (not _('Transports') in contact.groups or \ - gajim.connections[account].connected < 2) and \ - len(gajim.events.get_events(account, jid)) == 0 and \ - not _('Not in Roster') in contact.groups: + not showOffline and (not _('Transports') in contact.groups or \ + gajim.connections[account].connected < 2) and \ + len(gajim.contacts.get_contact(account, jid)) == 1 and nb_events == 0 and\ + not _('Not in Roster') in contact.groups: return # Remove brother contacts that are already in roster to add them @@ -307,25 +341,28 @@ class RosterWindow: groups = [_('Observers')] elif not groups: groups = [_('General')] - for g in groups: - iterG = self.get_group_iter(g, account) + for group in groups: + iterG = self.get_group_iter(group, account) if not iterG: IterAcct = self.get_account_iter(account) iterG = model.append(IterAcct, [ self.jabber_state_images['16']['closed'], - gtkgui_helpers.escape_for_pango_markup(g), 'group', g, account, - False, None]) - if not gajim.groups[account].has_key(g): # It can probably never append - if account + g in self.collapsed_rows: + gtkgui_helpers.escape_for_pango_markup(group), 'group', + group, account, False, None]) + self.draw_group(group, account) + if model.iter_n_children(IterAcct) == 1: # We added the first one + self.draw_account(account) + if group not in gajim.groups[account]: # It can probably never append + if account + group in self.collapsed_rows: ishidden = False else: ishidden = True - gajim.groups[account][g] = { 'expand': ishidden } + gajim.groups[account][group] = {'expand': ishidden} if not account in self.collapsed_rows: self.tree.expand_row((model.get_path(iterG)[0]), False) typestr = 'contact' - if g == _('Transports'): + if group == _('Transports'): typestr = 'agent' name = contact.get_shown_name() @@ -333,7 +370,7 @@ class RosterWindow: model.append(iterG, (None, name, typestr, contact.jid, account, False, None)) - if gajim.groups[account][g]['expand']: + if gajim.groups[account][group]['expand']: self.tree.expand_row(model.get_path(iterG), False) self.draw_contact(jid, account) self.draw_avatar(jid, account) @@ -343,6 +380,36 @@ class RosterWindow: data['jid']) self.add_contact_to_roster(data['jid'], data['account']) + def draw_group(self, group, account): + iter = self.get_group_iter(group, account) + if not iter: + return + if self.regroup: + accounts = [] + else: + accounts = [account] + nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts( + accounts = accounts, groups = [group]) + model = self.tree.get_model() + model.set_value(iter, 1 , gtkgui_helpers.escape_for_pango_markup( + '%s (%s/%s)' % (group, repr(nbr_on), repr(nbr_total)))) + + def add_to_not_in_the_roster(self, account, jid, nick = ''): + ''' add jid to group "not in the roster", he MUST not be in roster yet, + return contact ''' + keyID = '' + attached_keys = gajim.config.get_per('accounts', account, + 'attached_gpg_keys').split() + if jid in attached_keys: + keyID = attached_keys[attached_keys.index(jid) + 1] + contact = gajim.contacts.create_contact(jid = jid, + name = nick, groups = [_('Not in Roster')], + show = 'not in roster', status = '', sub = 'none', + keyID = keyID) + gajim.contacts.add_contact(account, contact) + self.add_contact_to_roster(contact.jid, account) + return contact + def get_self_contact_iter(self, account): model = self.tree.get_model() iterAcct = self.get_account_iter(account) @@ -388,25 +455,23 @@ class RosterWindow: return if contact.jid in gajim.newly_added[account]: return - if contact.jid.find('@') < 1 and gajim.connections[account].connected > 1: - # It's an agent + if gajim.jid_is_transport(contact.jid) and gajim.account_is_connected( + account) and gajim.config.get('show_transports_group'): + # It's an agent and we show them return if contact.jid in gajim.to_be_removed[account]: gajim.to_be_removed[account].remove(contact.jid) - # JEP-0162 - hide = True - if contact.sub in ('both', 'to', 'from'): - hide = False - elif contact.ask == 'subscribe': - hide = False - elif contact.name or len(contact.groups): - hide = False - - showOffline = gajim.config.get('showoffline') - if (contact.show in ('offline', 'error') or hide) and \ - not showOffline and (not _('Transports') in contact.groups or \ - gajim.connections[account].connected < 2) and \ - len(gajim.events.get_events(account, contact.jid, ['chat'])) == 0: + + + hide = contact.is_hidden_from_roster() + + show_offline = gajim.config.get('showoffline') + show_transports = gajim.config.get('show_transports_group') + if (_('Transports') in contact.groups and not show_transports) or \ + ((contact.show in ('offline', 'error') or hide) and not show_offline and \ + (not _('Transports') in contact.groups or \ + gajim.account_is_disconnected(account))) and \ + len(gajim.events.get_events(account, contact.jid, ['chat'])) == 0: self.remove_contact(contact, account) else: self.draw_contact(contact.jid, account) @@ -459,7 +524,7 @@ class RosterWindow: def get_appropriate_state_images(self, jid, size = '16', icon_name = 'online'): '''check jid and return the appropriate state images dict for - the demanded size. icon_name is taken into account when jis is from + the demanded size. icon_name is taken into account when jid is from transport: transport iconset doesn't contain all icons, so we fall back to jabber one''' transport = gajim.get_transport_name_from_jid(jid) @@ -483,18 +548,27 @@ class RosterWindow: return name = gtkgui_helpers.escape_for_pango_markup(contact.get_shown_name()) - if len(contact_instances) > 1: - name += ' (' + unicode(len(contact_instances)) + ')' + nb_connected_contact = 0 + for c in contact_instances: + if c.show not in ('error', 'offline'): + nb_connected_contact += 1 + if nb_connected_contact > 1: + name += ' (' + unicode(nb_connected_contact) + ')' # show (account_name) if there are 2 contact with same jid in merged mode if self.regroup: add_acct = False # look through all contacts of all accounts - for a in gajim.connections: - for j in gajim.contacts.get_jid_list(a): + for account_iter in gajim.connections: + if account_iter == account: # useless to add accout name + continue + for jid_iter in gajim.contacts.get_jid_list(account_iter): # [0] cause it'fster than highest_prio - c = gajim.contacts.get_first_contact_from_jid(a, j) - if c.name == contact.name and (j, a) != (jid, account): + contact_iter = gajim.contacts.\ + get_first_contact_from_jid(account_iter, jid_iter) + if contact_iter.get_shown_name() == \ + contact.get_shown_name() and\ + (jid_iter, account_iter) != (jid, account): add_acct = True break if add_acct: @@ -507,7 +581,7 @@ class RosterWindow: if contact.status and gajim.config.get('show_status_msgs_in_roster'): status = contact.status.strip() if status != '': - status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 1) + status = helpers.reduce_chars_newlines(status, max_lines = 1) # escape markup entities and make them small italic and fg color color = gtkgui_helpers._get_fade_color(self.tree, selected, focus) colorstring = "#%04x%04x%04x" % (color.red, color.green, color.blue) @@ -597,20 +671,19 @@ class RosterWindow: win = gajim.interface.msg_win_mgr.get_window(room_jid, account) win.window.present() win.set_active_tab(room_jid, account) - dialogs.ErrorDialog(_('You are already in room %s') % room_jid) + dialogs.ErrorDialog(_('You are already in group chat %s') % room_jid) return invisible_show = gajim.SHOW_LIST.index('invisible') if gajim.connections[account].connected == invisible_show: - dialogs.ErrorDialog(_('You cannot join a room while you are invisible') + dialogs.ErrorDialog(_('You cannot join a group chat while you are invisible') ) return - room, server = room_jid.split('@') if not gajim.interface.msg_win_mgr.has_window(room_jid, account): self.new_room(room_jid, nick, account) gc_win = gajim.interface.msg_win_mgr.get_window(room_jid, account) gc_win.set_active_tab(room_jid, account) gc_win.window.present() - gajim.connections[account].join_gc(nick, room, server, password) + gajim.connections[account].join_gc(nick, room_jid, password) if password: gajim.gc_passwords[room_jid] = password @@ -625,9 +698,6 @@ class RosterWindow: self.join_gc_room(account, bookmark['jid'], bookmark['nick'], bookmark['password']) - def on_bm_header_changed_state(self, widget, event): - widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state - def on_send_server_message_menuitem_activate(self, widget, account): server = gajim.config.get_per('accounts', account, 'hostname') server += '/announce/online' @@ -687,26 +757,37 @@ class RosterWindow: update_motd_menuitem = xml.get_widget('update_motd_menuitem') delete_motd_menuitem = xml.get_widget('delete_motd_menuitem') - send_single_message_menuitem.connect('activate', - self.on_send_single_message_menuitem_activate, account) - xml_console_menuitem.connect('activate', self.on_xml_console_menuitem_activate, account) - privacy_lists_menuitem.connect('activate', - self.on_privacy_lists_menuitem_activate, account) - - send_server_message_menuitem.connect('activate', - self.on_send_server_message_menuitem_activate, account) + if gajim.connections[account] and gajim.connections[account].privacy_rules_supported: + privacy_lists_menuitem.connect('activate', + self.on_privacy_lists_menuitem_activate, account) + else: + privacy_lists_menuitem.set_sensitive(False) + + if gajim.connections[account].is_zeroconf: + send_single_message_menuitem.set_sensitive(False) + administrator_menuitem.set_sensitive(False) + send_server_message_menuitem.set_sensitive(False) + set_motd_menuitem.set_sensitive(False) + update_motd_menuitem.set_sensitive(False) + delete_motd_menuitem.set_sensitive(False) + else: + send_single_message_menuitem.connect('activate', + self.on_send_single_message_menuitem_activate, account) + + send_server_message_menuitem.connect('activate', + self.on_send_server_message_menuitem_activate, account) - set_motd_menuitem.connect('activate', - self.on_set_motd_menuitem_activate, account) + set_motd_menuitem.connect('activate', + self.on_set_motd_menuitem_activate, account) - update_motd_menuitem.connect('activate', - self.on_update_motd_menuitem_activate, account) + update_motd_menuitem.connect('activate', + self.on_update_motd_menuitem_activate, account) - delete_motd_menuitem.connect('activate', - self.on_delete_motd_menuitem_activate, account) + delete_motd_menuitem.connect('activate', + self.on_delete_motd_menuitem_activate, account) advanced_menuitem_menu.show_all() @@ -718,6 +799,9 @@ class RosterWindow: return new_chat_menuitem = self.xml.get_widget('new_chat_menuitem') join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') + muc_icon = self.load_icon('muc_active') + if muc_icon: + join_gc_menuitem.set_image(muc_icon) add_new_contact_menuitem = self.xml.get_widget('add_new_contact_menuitem') service_disco_menuitem = self.xml.get_widget('service_disco_menuitem') advanced_menuitem = self.xml.get_widget('advanced_menuitem') @@ -777,23 +861,36 @@ class RosterWindow: disco_sub_menu = gtk.Menu() new_chat_sub_menu = gtk.Menu() - for account in gajim.connections: + accounts_list = gajim.contacts.get_accounts() + accounts_list.sort() + for account in accounts_list: if gajim.connections[account].connected <= 1: # if offline or connecting continue - + + # new chat + new_chat_item = gtk.MenuItem(_('using account %s') % account, + False) + new_chat_sub_menu.append(new_chat_item) + new_chat_item.connect('activate', + self.on_new_chat_menuitem_activate, account) + + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + continue + # join gc label = gtk.Label() label.set_markup('<u>' + account.upper() +'</u>') label.set_use_underline(False) gc_item = gtk.MenuItem() gc_item.add(label) - gc_item.connect('state-changed', self.on_bm_header_changed_state) + gc_item.connect('state-changed', + gtkgui_helpers.on_bm_header_changed_state) gc_sub_menu.append(gc_item) self.add_bookmarks_list(gc_sub_menu, account) - # the 'manage gc bookmarks' item is showed + # the 'manage gc bookmarks' item is shown # below to avoid duplicate code # add @@ -807,12 +904,6 @@ class RosterWindow: disco_item.connect('activate', self.on_service_disco_menuitem_activate, account) - # new chat - new_chat_item = gtk.MenuItem(_('using account %s') % account, - False) - new_chat_sub_menu.append(new_chat_item) - new_chat_item.connect('activate', - self.on_new_chat_menuitem_activate, account) add_new_contact_menuitem.set_submenu(add_sub_menu) add_sub_menu.show_all() @@ -823,7 +914,7 @@ class RosterWindow: elif connected_accounts == 1: # user has only one account for account in gajim.connections: - if gajim.connections[account].connected > 1: # THE connected account + if gajim.account_is_connected(account): # THE connected account # gc self.add_bookmarks_list(gc_sub_menu, account) # add @@ -851,22 +942,30 @@ class RosterWindow: if connected_accounts == 0: # no connected accounts, make the menuitems insensitive - new_chat_menuitem.set_sensitive(False) - join_gc_menuitem.set_sensitive(False) - add_new_contact_menuitem.set_sensitive(False) - service_disco_menuitem.set_sensitive(False) + for item in [new_chat_menuitem, join_gc_menuitem,\ + add_new_contact_menuitem, service_disco_menuitem]: + item.set_sensitive(False) else: # we have one or more connected accounts - new_chat_menuitem.set_sensitive(True) - join_gc_menuitem.set_sensitive(True) - add_new_contact_menuitem.set_sensitive(True) - service_disco_menuitem.set_sensitive(True) + for item in [new_chat_menuitem, join_gc_menuitem,\ + add_new_contact_menuitem, service_disco_menuitem]: + item.set_sensitive(True) + + # disable some fields if only local account is there + if connected_accounts == 1: + for account in gajim.connections: + if gajim.account_is_connected(account) and \ + gajim.connections[account].is_zeroconf: + for item in [join_gc_menuitem,\ + add_new_contact_menuitem, service_disco_menuitem]: + item.set_sensitive(False) + # show the 'manage gc bookmarks' item newitem = gtk.SeparatorMenuItem() # separator gc_sub_menu.append(newitem) connected_accounts_with_vcard = [] for account in gajim.connections: - if gajim.connections[account].connected > 1 and \ + if gajim.account_is_connected(account) and \ gajim.connections[account].vcard_supported: connected_accounts_with_vcard.append(account) if len(connected_accounts_with_vcard) > 1: @@ -894,11 +993,12 @@ class RosterWindow: else: profile_avatar_menuitem.set_sensitive(True) - newitem = gtk.ImageMenuItem(_('Manage Bookmarks...')) + newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...')) img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) newitem.set_image(img) - newitem.connect('activate', self.on_manage_bookmarks_menuitem_activate) + newitem.connect('activate', + self.on_manage_bookmarks_menuitem_activate) gc_sub_menu.append(newitem) gc_sub_menu.show_all() @@ -917,7 +1017,11 @@ class RosterWindow: advanced_menuitem_menu.show_all() else: # user has *more* than one account : build advanced submenus advanced_sub_menu = gtk.Menu() + accounts = [] # Put accounts in a list to sort them for account in gajim.connections: + accounts.append(account) + accounts.sort() + for account in accounts: advanced_item = gtk.MenuItem(_('for account %s') % account, False) advanced_sub_menu.append(advanced_item) advanced_menuitem_menu = self.get_and_connect_advanced_menuitem_menu( @@ -947,8 +1051,8 @@ class RosterWindow: item.connect('activate', self.on_history_manager_menuitem_activate) def add_bookmarks_list(self, gc_sub_menu, account): - '''Print join new room item and bookmarks list for an account''' - item = gtk.MenuItem(_('_Join New Room')) + '''Show join new group chat item and bookmarks list for an account''' + item = gtk.MenuItem(_('_Join New Group Chat')) item.connect('activate', self.on_join_gc_activate, account) gc_sub_menu.append(item) @@ -1069,24 +1173,32 @@ class RosterWindow: contact.show = show contact.status = status if show in ('offline', 'error') and \ - len(gajim.events.get_events(account, contact.jid)) == 0: + len(gajim.events.get_events(account, contact.get_full_jid())) == 0: if len(contact_instances) > 1: # if multiple resources gajim.contacts.remove_contact(account, contact) self.remove_contact(contact, account) self.add_contact_to_roster(contact.jid, account) # print status in chat window and update status/GPG image - for j in (contact.jid, contact.get_full_jid()): + jid_list = [contact.jid] + if contact.get_full_jid() != contact.jid: + jid_list.append(contact.get_full_jid()) + for j in jid_list: if gajim.interface.msg_win_mgr.has_window(j, account): jid = contact.jid win = gajim.interface.msg_win_mgr.get_window(j, account) ctrl = win.get_control(j, account) + ctrl.contact = contact ctrl.update_ui() win.redraw_tab(ctrl) name = contact.get_shown_name() - if contact.resource != '': + + # if multiple resources (or second one disconnecting) + if (len(contact_instances) > 1 or (len(contact_instances) == 1 and \ + show in ('offline', 'error'))) and contact.resource != '': name += '/' + contact.resource + uf_show = helpers.get_uf_show(show) if status: ctrl.print_conversation(_('%s is now %s (%s)') % (name, uf_show, @@ -1098,6 +1210,14 @@ class RosterWindow: account, contact.jid): ctrl.draw_banner() + if not contact.groups: + self.draw_group(_('General'), account) + else: + for group in contact.groups: + self.draw_group(group, account) + + self.draw_account(account) + def on_info(self, widget, contact, account): '''Call vcard_information_window class to display contact's information''' info = gajim.interface.instances[account]['infos'] @@ -1106,6 +1226,19 @@ class RosterWindow: else: info[contact.jid] = vcard.VcardWindow(contact, account) + def on_info_zeroconf(self, widget, contact, account): + info = gajim.interface.instances[account]['infos'] + if info.has_key(contact.jid): + info[contact.jid].window.present() + else: + contact = gajim.contacts.get_first_contact_from_jid(account, + contact.jid) + if contact.show in ('offline', 'error'): + # don't show info on offline contacts + return + info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account) + + def show_tooltip(self, contact): pointer = self.tree.get_pointer() props = self.tree.get_path_at_pos(pointer[0], pointer[1]) @@ -1149,8 +1282,15 @@ class RosterWindow: if self.tooltip.timeout == 0 or self.tooltip.id != props[0]: self.tooltip.id = row contacts = gajim.contacts.get_contact(account, jid) + connected_contacts = [] + for c in contacts: + if c.show not in ('offline', 'error'): + connected_contacts.append(c) + if not connected_contacts: + # no connected contacts, show the ofline one + connected_contacts = contacts self.tooltip.timeout = gobject.timeout_add(500, - self.show_tooltip, contacts) + self.show_tooltip, connected_contacts) elif model[iter][C_TYPE] == 'account': # we're on an account entry in the roster account = model[iter][C_ACCOUNT].decode('utf-8') @@ -1164,13 +1304,18 @@ class RosterWindow: contacts = [] connection = gajim.connections[account] # get our current contact info - contact = gajim.contacts.create_contact(jid = jid, name = account, - show = connection.get_status(), sub = '', + + nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts( + accounts = [account]) + account_name = account + if gajim.account_is_connected(account): + account_name += '(%s/%s)' % (repr(nbr_on), repr(nbr_total)) + contact = gajim.contacts.create_contact(jid = jid, + name = account_name, show = connection.get_status(), sub = '', status = connection.status, resource = gajim.config.get_per('accounts', connection.name, 'resource'), - priority = gajim.config.get_per('accounts', connection.name, - 'priority'), + priority = connection.priority, keyID = gajim.config.get_per('accounts', connection.name, 'keyid')) contacts.append(contact) @@ -1205,7 +1350,7 @@ class RosterWindow: gajim.connections[account].request_register_agent_info(contact.jid) def on_remove_agent(self, widget, list_): - '''When an agent is requested to log in or off. list_ is a list of + '''When an agent is requested to be removed. list_ is a list of (contact, account) tuple''' for (contact, account) in list_: if gajim.config.get_per('accounts', account, 'hostname') == \ @@ -1227,6 +1372,17 @@ class RosterWindow: gajim.contacts.remove_jid(account, contact.jid) gajim.contacts.remove_contact(account, contact) + # Check if there are unread events from some contacts + has_unread_events = False + for (contact, account) in list_: + for jid in gajim.events.get_events(account): + if jid.endswith(contact.jid): + has_unread_events = True + break + if has_unread_events: + dialogs.ErrorDialog(_('You have unread messages'), + _('You must read them before removing this transport.')) + return if len(list_) == 1: pritext = _('Transport "%s" will be removed') % contact.jid sectext = _('You will no longer be able to send and receive messages to contacts from this transport.') @@ -1269,6 +1425,22 @@ class RosterWindow: model[iter][C_EDITABLE] = True # set 'editable' to True self.tree.set_cursor(path, self.tree.get_column(0), True) + def on_remove_group_item_activated(self, widget, group, account): + dlg = dialogs.ConfirmationDialogCheck(_('Remove Group'), + _('Do you want to remove the group %s from the roster ?' % group), + _('Remove also all contacts in this group from your roster')) + dlg.set_default_response(gtk.BUTTONS_OK_CANCEL) + response = dlg.run() + if response == gtk.RESPONSE_OK: + for contact in gajim.contacts.get_contacts_from_group(account, group): + if not dlg.is_checked(): + self.remove_contact_from_group(account, contact, group) + gajim.connections[account].update_contact(contact.jid, + contact.name, contact.groups) + self.add_contact_to_roster(contact.jid, account) + else: + gajim.connections[account].unsubscribe(contact.jid) + def on_assign_pgp_key(self, widget, contact, account): attached_keys = gajim.config.get_per('accounts', account, 'attached_gpg_keys').split() @@ -1316,6 +1488,8 @@ class RosterWindow: contact = None): if contact is None: dialogs.SingleMessageWindow(account, action = 'send') + elif type(contact) == type([]): + dialogs.SingleMessageWindow(account, contact, 'send') else: jid = contact.jid if contact.jid == gajim.get_jid_from_account(account): @@ -1333,13 +1507,129 @@ class RosterWindow: '''Make contact's popup menu''' model = self.tree.get_model() jid = model[iter][C_JID].decode('utf-8') - path = model.get_path(iter) + tree_path = model.get_path(iter) account = model[iter][C_ACCOUNT].decode('utf-8') our_jid = jid == gajim.get_jid_from_account(account) contact = gajim.contacts.get_contact_with_highest_priority(account, jid) if not contact: return + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + xml = gtkgui_helpers.get_glade('zeroconf_contact_context_menu.glade') + zeroconf_contact_context_menu = xml.get_widget('zeroconf_contact_context_menu') + + start_chat_menuitem = xml.get_widget('start_chat_menuitem') + rename_menuitem = xml.get_widget('rename_menuitem') + edit_groups_menuitem = xml.get_widget('edit_groups_menuitem') + # separator has with send file, assign_openpgp_key_menuitem, etc.. + above_send_file_separator = xml.get_widget('above_send_file_separator') + send_file_menuitem = xml.get_widget('send_file_menuitem') + assign_openpgp_key_menuitem = xml.get_widget( + 'assign_openpgp_key_menuitem') + add_special_notification_menuitem = xml.get_widget( + 'add_special_notification_menuitem') + + add_special_notification_menuitem.hide() + add_special_notification_menuitem.set_no_show_all(True) + + if not our_jid: + # add a special img for rename menuitem + path_to_kbd_input_img = os.path.join(gajim.DATA_DIR, 'pixmaps', + 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path_to_kbd_input_img) + rename_menuitem.set_image(img) + + above_information_separator = xml.get_widget( + 'above_information_separator') + + # skip a separator + information_menuitem = xml.get_widget('information_menuitem') + history_menuitem = xml.get_widget('history_menuitem') + + contacts = gajim.contacts.get_contact(account, jid) + if len(contacts) > 1: # sevral resources + sub_menu = gtk.Menu() + start_chat_menuitem.set_submenu(sub_menu) + + iconset = gajim.config.get('iconset') + path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') + for c in contacts: + # icon MUST be different instance for every item + state_images = self.load_iconset(path) + item = gtk.ImageMenuItem(c.resource + ' (' + str(c.priority) + ')') + icon_name = helpers.get_icon_name_to_show(c, account) + icon = state_images[icon_name] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.on_open_chat_window, c, account, + c.resource) + + else: # one resource + start_chat_menuitem.connect('activate', + self.on_roster_treeview_row_activated, tree_path) + + if contact.resource: + send_file_menuitem.connect('activate', + self.on_send_file_menuitem_activate, account, contact) + else: # if we do not have resource we cannot send file + send_file_menuitem.hide() + send_file_menuitem.set_no_show_all(True) + + rename_menuitem.connect('activate', self.on_rename, iter, tree_path) + if contact.show in ('offline', 'error'): + information_menuitem.set_sensitive(False) + send_file_menuitem.set_sensitive(False) + else: + information_menuitem.connect('activate', self.on_info_zeroconf, contact, + account) + history_menuitem.connect('activate', self.on_history, contact, + account) + + if _('Not in Roster') not in contact.groups: + #contact is in normal group + edit_groups_menuitem.set_no_show_all(False) + assign_openpgp_key_menuitem.set_no_show_all(False) + edit_groups_menuitem.connect('activate', self.on_edit_groups, [( + contact,account)]) + + if gajim.config.get('usegpg'): + assign_openpgp_key_menuitem.connect('activate', + self.on_assign_pgp_key, contact, account) + + else: # contact is in group 'Not in Roster' + edit_groups_menuitem.hide() + edit_groups_menuitem.set_no_show_all(True) + # hide first of the two consecutive separators + above_send_file_separator.hide() + above_send_file_separator.set_no_show_all(True) + assign_openpgp_key_menuitem.hide() + assign_openpgp_key_menuitem.set_no_show_all(True) + + # Remove many items when it's self contact row + if our_jid: + for menuitem in (rename_menuitem, edit_groups_menuitem, + above_information_separator): + menuitem.set_no_show_all(True) + menuitem.hide() + + # Unsensitive many items when account is offline + if gajim.connections[account].connected < 2: + for widget in [start_chat_menuitem, rename_menuitem, edit_groups_menuitem, send_file_menuitem]: + widget.set_sensitive(False) + + event_button = gtkgui_helpers.get_possible_button_event(event) + + zeroconf_contact_context_menu.attach_to_widget(self.tree, None) + zeroconf_contact_context_menu.connect('selection-done', + gtkgui_helpers.destroy_widget) + zeroconf_contact_context_menu.show_all() + zeroconf_contact_context_menu.popup(None, None, None, event_button, + event.time) + return + + + # normal account xml = gtkgui_helpers.get_glade('roster_contact_context_menu.glade') roster_contact_context_menu = xml.get_widget( 'roster_contact_context_menu') @@ -1371,6 +1661,10 @@ class RosterWindow: img.set_from_file(path_to_kbd_input_img) rename_menuitem.set_image(img) + muc_icon = self.load_icon('muc_active') + if muc_icon: + invite_menuitem.set_image(muc_icon) + above_subscription_separator = xml.get_widget( 'above_subscription_separator') subscription_menuitem = xml.get_widget('subscription_menuitem') @@ -1434,7 +1728,7 @@ class RosterWindow: submenu = gtk.Menu() invite_menuitem.set_submenu(submenu) - menuitem = gtk.ImageMenuItem(_('_New room')) + menuitem = gtk.ImageMenuItem(_('_New group chat')) icon = gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU) menuitem.set_image(icon) menuitem.connect('activate', self.on_invite_to_new_room, [(contact, @@ -1461,7 +1755,7 @@ class RosterWindow: menuitem.connect('activate', self.on_invite_to_room, [(contact, account)], room_jid, acct) submenu.append(menuitem) - rename_menuitem.connect('activate', self.on_rename, iter, path) + rename_menuitem.connect('activate', self.on_rename, iter, tree_path) remove_from_roster_menuitem.connect('activate', self.on_req_usub, [(contact, account)]) information_menuitem.connect('activate', self.on_info, contact, @@ -1557,9 +1851,9 @@ class RosterWindow: try: gajim.interface.instances[account]['join_gc'] = \ dialogs.JoinGroupchatWindow(account, - server = gajim.connections[account].muc_jid[type_], + gajim.connections[account].muc_jid[type_], automatic = {'invities': jid_list}) - except RuntimeError: + except GajimGeneralException: continue break @@ -1609,7 +1903,7 @@ class RosterWindow: else: sub_menu = gtk.Menu() - menuitem = gtk.ImageMenuItem(_('_New room')) + menuitem = gtk.ImageMenuItem(_('_New group chat')) icon = gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU) menuitem.set_image(icon) menuitem.connect('activate', self.on_invite_to_new_room, list_) @@ -1645,6 +1939,19 @@ class RosterWindow: edit_groups_item = gtk.MenuItem(_('Edit _Groups')) menu.append(edit_groups_item) edit_groups_item.connect('activate', self.on_edit_groups, list_) + + account = None + for (contact, current_account) in list_: + # check that we use the same account for every sender + if account is not None and account != current_account: + account = None + break + account = current_account + if account is not None: + send_group_message_item = gtk.MenuItem(_('Send Group M_essage')) + menu.append(send_group_message_item) + send_group_message_item.connect('activate', + self.on_send_single_message_menuitem_activate, account, list_) # unsensitive if one account is not connected if one_account_offline: @@ -1663,24 +1970,58 @@ class RosterWindow: path = model.get_path(iter) group = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') - if group in helpers.special_groups + (_('General'),): - return menu = gtk.Menu() + if not group in helpers.special_groups + (_('General'),): - rename_item = gtk.ImageMenuItem(_('Re_name')) - # add a special img for rename menuitem - path_to_kbd_input_img = os.path.join(gajim.DATA_DIR, 'pixmaps', - 'kbd_input.png') - img = gtk.Image() - img.set_from_file(path_to_kbd_input_img) - rename_item.set_image(img) - menu.append(rename_item) - rename_item.connect('activate', self.on_rename, iter, path) + rename_item = gtk.ImageMenuItem(_('Re_name')) + # add a special img for rename menuitem + path_to_kbd_input_img = os.path.join(gajim.DATA_DIR, 'pixmaps', + 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path_to_kbd_input_img) + rename_item.set_image(img) + menu.append(rename_item) + rename_item.connect('activate', self.on_rename, iter, path) + + # Remove group + remove_item = gtk.ImageMenuItem(_('_Remove from Roster')) + icon = gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU) + remove_item.set_image(icon) + menu.append(remove_item) + remove_item.connect('activate', self.on_remove_group_item_activated, + group, account) + + # unsensitive if account is not connected + if gajim.connections[account].connected < 2: + rename_item.set_sensitive(False) + send_group_message_item = gtk.MenuItem(_('Send Group M_essage')) - # unsensitive if account is not connected - if gajim.connections[account].connected < 2: - rename_item.set_sensitive(False) + send_group_message_submenu = gtk.Menu() + send_group_message_item.set_submenu(send_group_message_submenu) + menu.append(send_group_message_item) + + group_message_to_all_item = gtk.MenuItem(_('To all users')) + send_group_message_submenu.append(group_message_to_all_item) + + group_message_to_all_online_item = gtk.MenuItem(_('To all online users')) + send_group_message_submenu.append(group_message_to_all_online_item) + list_ = [] # list of (jid, account) tuples + list_online = [] # list of (jid, account) tuples + + group = model[iter][C_NAME] + for jid in gajim.contacts.get_jid_list(account): + contact = gajim.contacts.get_contact_with_highest_priority(account, + jid) + if group in contact.groups or (contact.groups == [] and group == _('General')): + if contact.show not in ('offline', 'error'): + list_online.append((contact, account)) + list_.append((contact, account)) + + group_message_to_all_online_item.connect('activate', + self.on_send_single_message_menuitem_activate, account, list_online) + group_message_to_all_item.connect('activate', + self.on_send_single_message_menuitem_activate, account, list_) event_button = gtkgui_helpers.get_possible_button_event(event) @@ -1695,7 +2036,6 @@ class RosterWindow: jid = model[iter][C_JID].decode('utf-8') path = model.get_path(iter) account = model[iter][C_ACCOUNT].decode('utf-8') - is_connected = gajim.connections[account].connected > 1 contact = gajim.contacts.get_contact_with_highest_priority(account, jid) menu = gtk.Menu() @@ -1704,7 +2044,8 @@ class RosterWindow: item.set_image(icon) menu.append(item) show = contact.show - if (show != 'offline' and show != 'error') or not is_connected: + if (show != 'offline' and show != 'error') or\ + gajim.account_is_disconnected(account): item.set_sensitive(False) item.connect('activate', self.on_agent_logging, jid, None, account) @@ -1712,7 +2053,8 @@ class RosterWindow: icon = gtk.image_new_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) item.set_image(icon) menu.append(item) - if show in ('offline', 'error') or not is_connected: + if show in ('offline', 'error') or gajim.account_is_disconnected( + account): item.set_sensitive(False) item.connect('activate', self.on_agent_logging, jid, 'unavailable', account) @@ -1725,7 +2067,7 @@ class RosterWindow: item.set_image(icon) menu.append(item) item.connect('activate', self.on_edit_agent, contact, account) - if not is_connected: + if gajim.account_is_disconnected(account): item.set_sensitive(False) item = gtk.ImageMenuItem(_('Execute Command...')) @@ -1746,7 +2088,7 @@ class RosterWindow: item.set_image(img) menu.append(item) item.connect('activate', self.on_rename, iter, path) - if not is_connected: + if gajim.account_is_disconnected(account): item.set_sensitive(False) item = gtk.ImageMenuItem(_('_Remove from Roster')) @@ -1754,7 +2096,7 @@ class RosterWindow: item.set_image(icon) menu.append(item) item.connect('activate', self.on_remove_agent, [(contact, account)]) - if not is_connected: + if gajim.account_is_disconnected(account): item.set_sensitive(False) event_button = gtkgui_helpers.get_possible_button_event(event) @@ -1772,13 +2114,17 @@ class RosterWindow: gajim.interface.instances[account]['account_modification'] = \ config.AccountModificationWindow(account) - def on_open_gmail_inbox(self, widget, account): - if gajim.config.get_per('accounts', account, 'savepass'): - url = ('http://www.google.com/accounts/ServiceLoginAuth?service=mail&Email=%s&Passwd=%s&continue=https://mail.google.com/mail') %\ - (urllib.quote(gajim.config.get_per('accounts', account, 'name')), - urllib.quote(gajim.config.get_per('accounts', account, 'password'))) + def on_zeroconf_properties(self, widget, account): + if gajim.interface.instances.has_key('zeroconf_properties'): + gajim.interface.instances['zeroconf_properties'].\ + window.present() else: - url = ('http://mail.google.com/') + gajim.interface.instances['zeroconf_properties'] = \ + config.ZeroconfPropertiesWindow() + + def on_open_gmail_inbox(self, widget, account): + url = 'http://mail.google.com/mail?account_id=%s' % urllib.quote( + gajim.config.get_per('accounts', account, 'name')) helpers.launch_browser_mailer('url', url) def on_change_status_message_activate(self, widget, account): @@ -1792,82 +2138,132 @@ class RosterWindow: # we have to create our own set of icons for the menu # using self.jabber_status_images is poopoo iconset = gajim.config.get('iconset') - if not iconset: - iconset = DEFAULT_ICONSET path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') state_images = self.load_iconset(path) - xml = gtkgui_helpers.get_glade('account_context_menu.glade') - account_context_menu = xml.get_widget('account_context_menu') + if not gajim.config.get_per('accounts', account, 'is_zeroconf'): + xml = gtkgui_helpers.get_glade('account_context_menu.glade') + account_context_menu = xml.get_widget('account_context_menu') + + status_menuitem = xml.get_widget('status_menuitem') + join_group_chat_menuitem =xml.get_widget('join_group_chat_menuitem') + open_gmail_inbox_menuitem = xml.get_widget('open_gmail_inbox_menuitem') + new_message_menuitem = xml.get_widget('new_message_menuitem') + add_contact_menuitem = xml.get_widget('add_contact_menuitem') + service_discovery_menuitem = xml.get_widget('service_discovery_menuitem') + execute_command_menuitem = xml.get_widget('execute_command_menuitem') + edit_account_menuitem = xml.get_widget('edit_account_menuitem') + sub_menu = gtk.Menu() + status_menuitem.set_submenu(sub_menu) - status_menuitem = xml.get_widget('status_menuitem') - join_group_chat_menuitem =xml.get_widget('join_group_chat_menuitem') - open_gmail_inbox_menuitem = xml.get_widget('open_gmail_inbox_menuitem') - new_message_menuitem = xml.get_widget('new_message_menuitem') - add_contact_menuitem = xml.get_widget('add_contact_menuitem') - service_discovery_menuitem = xml.get_widget('service_discovery_menuitem') - execute_command_menuitem = xml.get_widget('execute_command_menuitem') - edit_account_menuitem = xml.get_widget('edit_account_menuitem') - sub_menu = gtk.Menu() - status_menuitem.set_submenu(sub_menu) + for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): + uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + icon = state_images[show] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.change_status, account, show) - for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): - uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.SeparatorMenuItem() + sub_menu.append(item) + + item = gtk.ImageMenuItem(_('_Change Status Message')) + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path) + item.set_image(img) + sub_menu.append(item) + item.connect('activate', self.on_change_status_message_activate, account) + if gajim.connections[account].connected < 2: + item.set_sensitive(False) + + uf_show = helpers.get_uf_show('offline', use_mnemonic = True) item = gtk.ImageMenuItem(uf_show) - icon = state_images[show] + icon = state_images['offline'] item.set_image(icon) sub_menu.append(item) - item.connect('activate', self.change_status, account, show) + item.connect('activate', self.change_status, account, 'offline') - item = gtk.SeparatorMenuItem() - sub_menu.append(item) - - item = gtk.ImageMenuItem(_('_Change Status Message')) - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') - img = gtk.Image() - img.set_from_file(path) - item.set_image(img) - sub_menu.append(item) - item.connect('activate', self.on_change_status_message_activate, account) - if gajim.connections[account].connected < 2: - item.set_sensitive(False) + if gajim.config.get_per('accounts', account, 'hostname') not in gajim.gmail_domains: + open_gmail_inbox_menuitem.set_no_show_all(True) + open_gmail_inbox_menuitem.hide() + else: + open_gmail_inbox_menuitem.connect('activate', self.on_open_gmail_inbox, + account) - uf_show = helpers.get_uf_show('offline', use_mnemonic = True) - item = gtk.ImageMenuItem(uf_show) - icon = state_images['offline'] - item.set_image(icon) - sub_menu.append(item) - item.connect('activate', self.change_status, account, 'offline') + edit_account_menuitem.connect('activate', self.on_edit_account, account) + add_contact_menuitem.connect('activate', self.on_add_new_contact, account) + service_discovery_menuitem.connect('activate', + self.on_service_disco_menuitem_activate, account) + hostname = gajim.config.get_per('accounts', account, 'hostname') + contact = gajim.contacts.create_contact(jid = hostname) # Fake contact + execute_command_menuitem.connect('activate', + self.on_execute_command, contact, account) + + gc_sub_menu = gtk.Menu() # gc is always a submenu + join_group_chat_menuitem.set_submenu(gc_sub_menu) + self.add_bookmarks_list(gc_sub_menu, account) + new_message_menuitem.connect('activate', + self.on_new_message_menuitem_activate, account) - if gajim.config.get_per('accounts', account, 'hostname') not in gajim.gmail_domains: - open_gmail_inbox_menuitem.set_no_show_all(True) - open_gmail_inbox_menuitem.hide() + # make some items insensitive if account is offline + if gajim.connections[account].connected < 2: + for widget in [add_contact_menuitem, service_discovery_menuitem, + join_group_chat_menuitem, new_message_menuitem, + execute_command_menuitem]: + widget.set_sensitive(False) else: - open_gmail_inbox_menuitem.connect('activate', self.on_open_gmail_inbox, - account) + xml = gtkgui_helpers.get_glade('zeroconf_context_menu.glade') + account_context_menu = xml.get_widget('zeroconf_context_menu') - edit_account_menuitem.connect('activate', self.on_edit_account, account) - add_contact_menuitem.connect('activate', self.on_add_new_contact, account) - service_discovery_menuitem.connect('activate', - self.on_service_disco_menuitem_activate, account) - hostname = gajim.config.get_per('accounts', account, 'hostname') - contact = gajim.contacts.create_contact(jid = hostname) # Fake contact - execute_command_menuitem.connect('activate', - self.on_execute_command, contact, account) + status_menuitem = xml.get_widget('status_menuitem') + #join_group_chat_menuitem =xml.get_widget('join_group_chat_menuitem') + new_message_menuitem = xml.get_widget('new_message_menuitem') + zeroconf_properties_menuitem = xml.get_widget('zeroconf_properties_menuitem') + sub_menu = gtk.Menu() + status_menuitem.set_submenu(sub_menu) - gc_sub_menu = gtk.Menu() # gc is always a submenu - join_group_chat_menuitem.set_submenu(gc_sub_menu) - self.add_bookmarks_list(gc_sub_menu, account) - new_message_menuitem.connect('activate', - self.on_new_message_menuitem_activate, account) + for show in ('online', 'away', 'dnd', 'invisible'): + uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + icon = state_images[show] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.change_status, account, show) - # make some items insensitive if account is offline - if gajim.connections[account].connected < 2: - for widget in [add_contact_menuitem, service_discovery_menuitem, - join_group_chat_menuitem, new_message_menuitem, - execute_command_menuitem]: - widget.set_sensitive(False) - + item = gtk.SeparatorMenuItem() + sub_menu.append(item) + + item = gtk.ImageMenuItem(_('_Change Status Message')) + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path) + item.set_image(img) + sub_menu.append(item) + item.connect('activate', self.on_change_status_message_activate, account) + if gajim.connections[account].connected < 2: + item.set_sensitive(False) + + uf_show = helpers.get_uf_show('offline', use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + icon = state_images['offline'] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.change_status, account, 'offline') + + zeroconf_properties_menuitem.connect('activate', self.on_zeroconf_properties, account) + #gc_sub_menu = gtk.Menu() # gc is always a submenu + #join_group_chat_menuitem.set_submenu(gc_sub_menu) + #self.add_bookmarks_list(gc_sub_menu, account) + #new_message_menuitem.connect('activate', + # self.on_new_message_menuitem_activate, account) + + # make some items insensitive if account is offline + #if gajim.connections[account].connected < 2: + # for widget in [join_group_chat_menuitem, new_message_menuitem]: + # widget.set_sensitive(False) + # new_message_menuitem.set_sensitive(False) + return account_context_menu def make_account_menu(self, event, iter): @@ -1875,13 +2271,11 @@ class RosterWindow: model = self.tree.get_model() account = model[iter][C_ACCOUNT].decode('utf-8') - if account != 'all': + if account != 'all': # not in merged mode menu = self.build_account_menu(account) else: menu = gtk.Menu() iconset = gajim.config.get('iconset') - if not iconset: - iconset = DEFAULT_ICONSET path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') accounts = [] # Put accounts in a list to sort them for account in gajim.connections: @@ -1980,6 +2374,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if not len(list_of_paths): return type = model[list_of_paths[0]][C_TYPE] + account = model[list_of_paths[0]][C_ACCOUNT] list_ = [] for path in list_of_paths: if model[path][C_TYPE] != type: @@ -1989,7 +2384,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) contact = gajim.contacts.get_contact_with_highest_priority(account, jid) list_.append((contact, account)) - if type in ('account', 'group', 'self_contact'): + if type in ('account', 'group', 'self_contact') or account == gajim.ZEROCONF_ACC_NAME: return if type == 'contact': self.on_req_usub(widget, list_) @@ -2083,7 +2478,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) return True for acct in gajim.connections: if not gajim.config.get_per('accounts', acct, - 'sync_with_global_status'): + 'sync_with_global_status'): continue current_show = gajim.SHOW_LIST[gajim.connections[acct].connected] self.send_status(acct, current_show, message) @@ -2141,7 +2536,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) account) ctrl = gajim.interface.msg_win_mgr.get_control(contact.jid, account) - msg_win.remove_tab(ctrl) + msg_win.remove_tab(ctrl, msg_win.CLOSE_CLOSE_BUTTON) else: need_readd = True if need_readd: @@ -2214,8 +2609,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) gajim.connections[account].password = passphrase if save: gajim.config.set_per('accounts', account, 'savepass', True) - gajim.config.set_per('accounts', account, 'password', - passphrase) + passwords.save_password(account, passphrase) keyid = None use_gpg_agent = gajim.config.get('use_gpg_agent') @@ -2270,7 +2664,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if gc_control.account == account: gajim.connections[account].send_gc_status(gc_control.nick, gc_control.room_jid, status, txt) - if gajim.connections[account].connected > 1: + if gajim.account_is_connected(account): if status == 'online' and gajim.interface.sleeper.getState() != \ common.sleepy.STATE_UNKNOWN: gajim.sleeper_state[account] = 'online' @@ -2287,6 +2681,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) show == 'invisible': return '' dlg = dialogs.ChangeStatusMessageDialog(show) + dlg.window.present() # show it on current workspace message = dlg.run() return message @@ -2337,14 +2732,16 @@ _('If "%s" accepts this request you will know his or her status.') % jid) dlg = dialogs.ChangeStatusMessageDialog(status) message = dlg.run() if message is not None: # None if user pressed Cancel - for acct in accounts: - if not gajim.config.get_per('accounts', acct, + for account in accounts: + if not gajim.config.get_per('accounts', account, 'sync_with_global_status'): continue - current_show = gajim.SHOW_LIST[gajim.connections[acct].connected] - self.send_status(acct, current_show, message) + current_show = gajim.SHOW_LIST[ + gajim.connections[account].connected] + self.send_status(account, current_show, message) self.combobox_callback_active = False - self.status_combobox.set_active(self.previous_status_combobox_active) + self.status_combobox.set_active( + self.previous_status_combobox_active) self.combobox_callback_active = True return # we are about to change show, so save this new show so in case @@ -2354,13 +2751,13 @@ _('If "%s" accepts this request you will know his or her status.') % jid) connected_accounts = gajim.get_number_of_connected_accounts() if status == 'invisible': bug_user = False - for acct in accounts: - if connected_accounts < 1 or gajim.connections[acct].connected > 1: - if not gajim.config.get_per('accounts', acct, + for account in accounts: + if connected_accounts < 1 or gajim.account_is_connected(account): + if not gajim.config.get_per('accounts', account, 'sync_with_global_status'): continue # We're going to change our status to invisible - if self.connected_rooms(acct): + if self.connected_rooms(account): bug_user = True break if bug_user: @@ -2380,17 +2777,58 @@ _('If "%s" accepts this request you will know his or her status.') % jid) global_sync_accounts.append(acct) global_sync_connected_accounts = gajim.get_number_of_connected_accounts( global_sync_accounts) - for acct in accounts: - if not gajim.config.get_per('accounts', acct, 'sync_with_global_status'): + for account in accounts: + if not gajim.config.get_per('accounts', account, + 'sync_with_global_status'): continue # we are connected (so we wanna change show and status) # or no account is connected and we want to connect with new show and status if not global_sync_connected_accounts > 0 or \ - gajim.connections[acct].connected > 1: - self.send_status(acct, status, message) + gajim.account_is_connected(account): + self.send_status(account, status, message) self.update_status_combobox() + ## enable setting status msg from currently playing music track + def enable_syncing_status_msg_from_current_music_track(self, enabled): + '''if enabled is True, we listen to events from music players about + currently played music track, and we update our + status message accordinly''' + if not dbus_support.supported: + # do nothing if user doesn't have D-Bus bindings + return + if enabled: + if self._music_track_changed_signal is None: + listener = MusicTrackListener.get() + self._music_track_changed_signal = listener.connect( + 'music-track-changed', self._music_track_changed) + track = listener.get_playing_track() + self._music_track_changed(listener, track) + else: + if self._music_track_changed_signal is not None: + listener = MusicTrackListener.get() + listener.disconnect(self._music_track_changed_signal) + self._music_track_changed_signal = None + self._music_track_changed(None, None) + + def _music_track_changed(self, unused_listener, music_track_info): + accounts = gajim.connections.keys() + if music_track_info is None: + status_message = '' + else: + status_message = _('♪ "%(title)s" by %(artist)s ♪') % \ + {'title': music_track_info.title, + 'artist': music_track_info.artist } + for account in accounts: + if not gajim.config.get_per('accounts', account, + 'sync_with_global_status'): + continue + if not gajim.connections[account].connected: + continue + current_show = gajim.SHOW_LIST[gajim.connections[account].connected] + self.send_status(account, current_show, status_message) + + def update_status_combobox(self): # table to change index in connection.connected to index in combobox table = {'offline':9, 'connecting':9, 'online':0, 'chat':1, 'away':2, @@ -2404,14 +2842,25 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if gajim.interface.systray_enabled: gajim.interface.systray.change_status(show) + def set_account_status_icon(self, account): + status = gajim.connections[account].connected + model = self.tree.get_model() + accountIter = self.get_account_iter(account) + if not accountIter: + return + if not self.regroup: + show = gajim.SHOW_LIST[status] + else: # accounts merged + show = helpers.get_global_show() + model[accountIter][C_IMG] = self.jabber_state_images['16'][show] + def on_status_changed(self, account, status): '''the core tells us that our status has changed''' if account not in gajim.contacts.get_accounts(): return model = self.tree.get_model() accountIter = self.get_account_iter(account) - if accountIter: - model[accountIter][0] = self.jabber_state_images['16'][status] + self.set_account_status_icon(account) if status == 'offline': if self.quit_on_next_offline > -1: self.quit_on_next_offline -= 1 @@ -2459,20 +2908,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def new_chat_from_jid(self, account, jid): jid = gajim.get_jid_without_resource(jid) contact = gajim.contacts.get_contact_with_highest_priority(account, jid) - no_contact = False + added_to_roster = False if not contact: - no_contact = True - keyID = '' - attached_keys = gajim.config.get_per('accounts', account, - 'attached_gpg_keys').split() - if jid in attached_keys: - keyID = attached_keys[attached_keys.index(jid) + 1] - contact = gajim.contacts.create_contact(jid = jid, - name = '', groups = [_('Not in Roster')], - show = 'not in roster', status = '', sub = 'none', - keyID = keyID) - gajim.contacts.add_contact(account, contact) - self.add_contact_to_roster(contact.jid, account) + added_to_roster = True + contact = self.add_to_not_in_the_roster(account, jid) if not gajim.interface.msg_win_mgr.has_window(contact.jid, account): self.new_chat(contact, account) @@ -2480,7 +2919,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) mw.set_active_tab(jid, account) mw.window.present() # For JEP-0172 - if no_contact: + if added_to_roster: mc = mw.get_control(jid, account) mc.user_nick = gajim.nicks[account] @@ -2496,11 +2935,12 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def on_message(self, jid, msg, tim, account, encrypted = False, msg_type = '', subject = None, resource = '', msg_id = None, - user_nick = '', advanced_notif_num = None): + user_nick = '', advanced_notif_num = None, xhtml = None): '''when we receive a message''' contact = None # if chat window will be for specific resource resource_for_chat = resource + fjid = jid # Try to catch the contact with correct resource if resource: fjid = jid + '/' + resource @@ -2508,33 +2948,30 @@ _('If "%s" accepts this request you will know his or her status.') % jid) highest_contact = gajim.contacts.get_contact_with_highest_priority( account, jid) if not contact: - # Default to highest prio - fjid = jid - resource_for_chat = None - contact = highest_contact + # If there is another resource, it may be a message from an invisible + # resource + lcontact = gajim.contacts.get_contacts_from_jid(account, jid) + if (len(lcontact) > 1 or (lcontact and lcontact[0].resource and \ + lcontact[0].show != 'offline')) and jid.find('@') > 0: + contact = gajim.contacts.copy_contact(highest_contact) + contact.resource = resource + if resource: + fjid = jid + '/' + resource + contact.priority = 0 + contact.show = 'offline' + contact.status = '' + gajim.contacts.add_contact(account, contact) + + else: + # Default to highest prio + fjid = jid + resource_for_chat = None + contact = highest_contact if not contact: # contact is not in roster - keyID = '' - attached_keys = gajim.config.get_per('accounts', account, - 'attached_gpg_keys').split() - if jid in attached_keys: - keyID = attached_keys[attached_keys.index(jid) + 1] - if user_nick: - nick = user_nick - else: - nick = jid.split('@')[0] - contact = gajim.contacts.create_contact(jid = jid, - name = nick, groups = [_('Not in Roster')], - show = 'not in roster', status = '', ask = 'none', - keyID = keyID, resource = resource) - gajim.contacts.add_contact(account, contact) - self.add_contact_to_roster(jid, account) + contact = self.add_to_not_in_the_roster(account, jid, user_nick) - iters = self.get_contact_iter(jid, account) - if iters: - path = self.tree.get_model().get_path(iters[0]) - else: - path = None + path = self.get_path(jid, account) # Try to get line of contact in roster # Look for a chat control that has the given resource ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) @@ -2564,21 +3001,21 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if msg_type == 'error': typ = 'status' ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, - subject = subject) + subject = subject, xhtml = xhtml) if msg_id: gajim.logger.set_read_messages([msg_id]) return # We save it in a queue type_ = 'chat' + event_type = 'message_received' if msg_type == 'normal': type_ = 'normal' - show_in_roster = notify.get_show_in_roster('message_received', account, - contact) - show_in_systray = notify.get_show_in_systray('message_received', account, - contact) + event_type = 'single_message_received' + show_in_roster = notify.get_show_in_roster(event_type, account, contact) + show_in_systray = notify.get_show_in_systray(event_type, account, contact) event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, - encrypted, resource, msg_id), show_in_roster = show_in_roster, + encrypted, resource, msg_id, xhtml), show_in_roster = show_in_roster, show_in_systray = show_in_systray) gajim.events.add_event(account, fjid, event) if popup: @@ -2595,21 +3032,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) else: if no_queue: # We didn't have a queue: we change icons self.draw_contact(jid, account) - # Redraw parent too - self.draw_parent_contact(jid, account) self.show_title() # we show the * or [n] - if not path: - # contact is in roster but we curently don't see him online - # show him - self.add_contact_to_roster(jid, account) - iters = self.get_contact_iter(jid, account) - path = self.tree.get_model().get_path(iters[0]) - # popup == False so we show awaiting event in roster - # show and select contact line in roster (even if he is not in roster) - self.tree.expand_row(path[0:1], False) - self.tree.expand_row(path[0:2], False) - self.tree.scroll_to_cell(path) - self.tree.set_cursor(path) + # Show contact in roster (if he is invisible for example) and select + # line + self.show_and_select_path(path, jid, account) def on_preferences_menuitem_activate(self, widget): if gajim.interface.instances.has_key('preferences'): @@ -2624,7 +3050,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) '''when the join gc menuitem is clicked, show the join gc window''' invisible_show = gajim.SHOW_LIST.index('invisible') if gajim.connections[account].connected == invisible_show: - dialogs.ErrorDialog(_('You cannot join a room while you are invisible') + dialogs.ErrorDialog(_('You cannot join a group chat while you are invisible') ) return if gajim.interface.instances[account].has_key('join_gc'): @@ -2634,7 +3060,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) try: gajim.interface.instances[account]['join_gc'] = \ dialogs.JoinGroupchatWindow(account) - except RuntimeError: + except GajimGeneralException: pass def on_new_message_menuitem_activate(self, widget, account): @@ -2664,6 +3090,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) else: gajim.interface.instances['file_transfers'].window.show_all() + def on_show_transports_menuitem_activate(self, widget): + gajim.config.set('show_transports_group', widget.get_active()) + self.draw_roster() + def on_manage_bookmarks_menuitem_activate(self, widget): config.ManageBookmarksWindow() @@ -2678,11 +3108,13 @@ _('If "%s" accepts this request you will know his or her status.') % jid) else: w.window.destroy() - def close_all(self, account): - '''close all the windows from an account''' + def close_all(self, account, force = False): + '''close all the windows from an account + if force is True, do not ask confirmation before closing chat/gc windows + ''' self.close_all_from_dict(gajim.interface.instances[account]) for ctrl in gajim.interface.msg_win_mgr.get_controls(acct = account): - ctrl.parent_win.remove_tab(ctrl) + ctrl.parent_win.remove_tab(ctrl, force = force) def on_roster_window_delete_event(self, widget, event): '''When we want to close the window''' @@ -2850,6 +3282,11 @@ _('If "%s" accepts this request you will know his or her status.') % jid) gajim.interface.remove_first_event(account, jid, event.type_) ft.show_completed(jid, data) return True + elif event.type_ == 'gc-invitation': + dialogs.InvitationReceivedDialog(account, data[0], jid, data[2], + data[1]) + gajim.interface.remove_first_event(account, jid, event.type_) + return True return False def on_execute_command(self, widget, contact, account, resource=None): @@ -2874,6 +3311,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) # last message is long time ago gajim.last_message_time[account][ctrl.get_full_jid()] = 0 win.set_active_tab(fjid, account) + if gajim.connections[account].is_zeroconf and \ + gajim.connections[account].status in ('offline', 'invisible'): + win.get_control(fjid, account).got_disconnected() + win.window.present() def on_roster_treeview_row_activated(self, widget, path, col = 0): @@ -2914,7 +3355,9 @@ _('If "%s" accepts this request you will know his or her status.') % jid) fjid += '/' + resource if self.open_event(account, fjid, first_ev): return - c = gajim.contacts.get_contact_with_highest_priority(account, jid) + c = gajim.contacts.get_contact(account, jid, resource) + if not c or isinstance(c, list): + c = gajim.contacts.get_contact_with_highest_priority(account, jid) if jid == gajim.get_jid_from_account(account): resource = c.resource self.on_open_chat_window(widget, c, account, resource = resource) @@ -2922,7 +3365,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def on_roster_treeview_row_expanded(self, widget, iter, path): '''When a row is expanded change the icon of the arrow''' model = self.tree.get_model() - if gajim.config.get('mergeaccounts'): + if self.regroup: # merged accounts accounts = gajim.connections.keys() else: accounts = [model[iter][C_ACCOUNT].decode('utf-8')] @@ -2954,7 +3397,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) '''When a row is collapsed : change the icon of the arrow''' model = self.tree.get_model() - if gajim.config.get('mergeaccounts'): + if self.regroup: # merged accounts accounts = gajim.connections.keys() else: accounts = [model[iter][C_ACCOUNT].decode('utf-8')] @@ -3071,13 +3514,12 @@ _('If "%s" accepts this request you will know his or her status.') % jid) try: # Object will add itself to the window dict disco.ServiceDiscoveryWindow(account, address_entry = True) - except RuntimeError: + except GajimGeneralException: pass def load_iconset(self, path, pixbuf2 = None, transport = False): - '''load an iconset from the given path, and add pixbuf2 on top left of - each static images''' - imgs = {} + '''load full iconset from the given path, and add + pixbuf2 on top left of each static images''' path += '/' if transport: list = ('online', 'chat', 'away', 'xa', 'dnd', 'offline', @@ -3089,15 +3531,28 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if pixbuf2: list = ('connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'offline', 'error', 'requested', 'message', 'not in roster') - for state in list: + return self._load_icon_list(list, path, pixbuf2) + + def load_icon(self, icon_name): + '''load an icon from the iconset in 16x16''' + iconset = gajim.config.get('iconset') + path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16'+ '/') + icon_list = self._load_icon_list([icon_name], path) + return icon_list[icon_name] + + def _load_icon_list(self, icons_list, path, pixbuf2 = None): + '''load icons in icons_list from the given path, + and add pixbuf2 on top left of each static images''' + imgs = {} + for icon in icons_list: # try to open a pixfile with the correct method - state_file = state.replace(' ', '_') + icon_file = icon.replace(' ', '_') files = [] - files.append(path + state_file + '.gif') - files.append(path + state_file + '.png') + files.append(path + icon_file + '.gif') + files.append(path + icon_file + '.png') image = gtk.Image() image.show() - imgs[state] = image + imgs[icon] = image for file in files: # loop seeking for either gif or png if os.path.exists(file): image.set_from_file(file) @@ -3115,8 +3570,13 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def make_jabber_state_images(self): '''initialise jabber_state_images dict''' iconset = gajim.config.get('iconset') - if not iconset: - iconset = 'dcraven' + if iconset: + path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') + if not os.path.exists(path): + iconset = gajim.config.DEFAULT_ICONSET + else: + iconset = gajim.config.DEFAULT_ICONSET + path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '32x32') self.jabber_state_images['32'] = self.load_iconset(path) @@ -3155,7 +3615,8 @@ _('If "%s" accepts this request you will know his or her status.') % jid) model[iter][1] = self.jabber_state_images['16'][model[iter][2]] iter = model.iter_next(iter) # Update the systray - gajim.interface.systray.set_img() + if gajim.interface.systray_enabled: + gajim.interface.systray.set_img() for win in gajim.interface.msg_win_mgr.windows(): for ctrl in win.controls(): @@ -3190,21 +3651,25 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def iconCellDataFunc(self, column, renderer, model, iter, data = None): '''When a row is added, set properties for icon renderer''' theme = gajim.config.get('roster_theme') - if model[iter][C_TYPE] == 'account': + type_ = model[iter][C_TYPE] + if type_ == 'account': color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: self.set_renderer_color(renderer, gtk.STATE_ACTIVE) renderer.set_property('xalign', 0) - elif model[iter][C_TYPE] == 'group': + elif type_ == 'group': color = gajim.config.get_per('themes', theme, 'groupbgcolor') if color: renderer.set_property('cell-background', color) else: self.set_renderer_color(renderer, gtk.STATE_PRELIGHT) renderer.set_property('xalign', 0.2) - else: + elif type_: # prevent type_ = None, see http://trac.gajim.org/ticket/2534 + if not model[iter][C_JID] or not model[iter][C_ACCOUNT]: + # This can append when at the moment we add the row + return jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') if jid in gajim.newly_added[account]: @@ -3227,7 +3692,8 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def nameCellDataFunc(self, column, renderer, model, iter, data = None): '''When a row is added, set properties for name renderer''' theme = gajim.config.get('roster_theme') - if model[iter][C_TYPE] == 'account': + type_ = model[iter][C_TYPE] + if type_ == 'account': color = gajim.config.get_per('themes', theme, 'accounttextcolor') if color: renderer.set_property('foreground', color) @@ -3242,7 +3708,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) gtkgui_helpers.get_theme_font_for_option(theme, 'accountfont')) renderer.set_property('xpad', 0) renderer.set_property('width', 3) - elif model[iter][C_TYPE] == 'group': + elif type_ == 'group': color = gajim.config.get_per('themes', theme, 'grouptextcolor') if color: renderer.set_property('foreground', color) @@ -3256,7 +3722,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont')) renderer.set_property('xpad', 4) - else: + elif type_: # prevent type_ = None, see http://trac.gajim.org/ticket/2534 + if not model[iter][C_JID] or not model[iter][C_ACCOUNT]: + # This can append when at the moment we add the row + return jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') color = gajim.config.get_per('themes', theme, 'contacttextcolor') @@ -3285,19 +3754,23 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def fill_secondary_pixbuf_rederer(self, column, renderer, model, iter, data=None): '''When a row is added, set properties for secondary renderer (avatar or padlock)''' theme = gajim.config.get('roster_theme') - if model[iter][C_TYPE] == 'account': + type_ = model[iter][C_TYPE] + if type_ == 'account': color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: self.set_renderer_color(renderer, gtk.STATE_ACTIVE) - elif model[iter][C_TYPE] == 'group': + elif type_ == 'group': color = gajim.config.get_per('themes', theme, 'groupbgcolor') if color: renderer.set_property('cell-background', color) else: self.set_renderer_color(renderer, gtk.STATE_PRELIGHT) - else: # contact + elif type_: # prevent type_ = None, see http://trac.gajim.org/ticket/2534 + if not model[iter][C_JID] or not model[iter][C_ACCOUNT]: + # This can append when at the moment we add the row + return jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') if jid in gajim.newly_added[account]: @@ -3418,34 +3891,55 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def on_drop_in_contact(self, widget, account_source, c_source, account_dest, c_dest, was_big_brother, context, etime): - # children must take the new tag too, so remember old tag - old_tag = gajim.contacts.get_metacontacts_tag(account_source, - c_source.jid) - # remove the source row - self.remove_contact(c_source, account_source) - # brother inherite big brother groups - c_source.groups = [] - for g in c_dest.groups: - c_source.groups.append(g) - gajim.connections[account_source].update_contact(c_source.jid, - c_source.name, c_source.groups) - gajim.contacts.add_metacontact(account_dest, c_dest.jid, account_source, - c_source.jid) - if was_big_brother: - # add brothers too - all_jid = gajim.contacts.get_metacontacts_jids(old_tag) - for _account in all_jid: - for _jid in all_jid[_account]: - gajim.contacts.add_metacontact(account_dest, c_dest.jid, - _account, _jid) - _c = gajim.contacts.get_first_contact_from_jid(_account, _jid) - self.remove_contact(_c, _account) - self.add_contact_to_roster(_jid, _account) - self.draw_contact(_jid, _account) - self.add_contact_to_roster(c_source.jid, account_source) - self.draw_contact(c_dest.jid, account_dest) - - context.finish(True, True, etime) + if not gajim.connections[account_source].metacontacts_supported or not \ + gajim.connections[account_dest].metacontacts_supported: + dialogs.WarningDialog(_('Metacontacts storage not supported by your server'), _('Your server does not support storing metacontacts information. So those information will not be save on next reconnection.')) + def merge_contacts(widget = None): + if widget: # dialog has been shown + dlg.destroy() + if dlg.is_checked(): # user does not want to be asked again + gajim.config.set('confirm_metacontacts', 'no') + else: + gajim.config.set('confirm_metacontacts', 'yes') + # children must take the new tag too, so remember old tag + old_tag = gajim.contacts.get_metacontacts_tag(account_source, + c_source.jid) + # remove the source row + self.remove_contact(c_source, account_source) + # brother inherite big brother groups + c_source.groups = [] + for g in c_dest.groups: + c_source.groups.append(g) + gajim.connections[account_source].update_contact(c_source.jid, + c_source.name, c_source.groups) + gajim.contacts.add_metacontact(account_dest, c_dest.jid, + account_source, c_source.jid) + if was_big_brother: + # add brothers too + all_jid = gajim.contacts.get_metacontacts_jids(old_tag) + for _account in all_jid: + for _jid in all_jid[_account]: + gajim.contacts.add_metacontact(account_dest, c_dest.jid, + _account, _jid) + _c = gajim.contacts.get_first_contact_from_jid(_account, _jid) + self.remove_contact(_c, _account) + self.add_contact_to_roster(_jid, _account) + self.draw_contact(_jid, _account) + self.add_contact_to_roster(c_source.jid, account_source) + self.draw_contact(c_dest.jid, account_dest) + + context.finish(True, True, etime) + + confirm_metacontacts = gajim.config.get('confirm_metacontacts') + if confirm_metacontacts == 'no': + merge_contacts() + return + pritext = _('You are about to create a metacontact. Are you sure you want to continue?') + sectext = _('Metacontacts are a way to regroup several contacts in one line. Generaly it is used when the same person has several Jabber accounts or transport accounts.') + dlg = dialogs.ConfirmationDialogCheck(pritext, sectext, + _('Do _not ask me again'), on_response_ok = merge_contacts) + if not confirm_metacontacts: # First time we see this window + dlg.checkbutton.set_active(True) def on_drop_in_group(self, widget, account, c_source, grp_dest, context, etime, grp_source = None): @@ -3523,6 +4017,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) account_dest, c_dest, path) return + if gajim.config.get_per('accounts', account_dest, 'is_zeroconf'): + # drop on zeroconf account, no contact adds possible + return + if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 2: # dropped before a group : we drop it in the previous group path_dest = (path_dest[0], path_dest[1]-1) @@ -3534,6 +4032,8 @@ _('If "%s" accepts this request you will know his or her status.') % jid) return if type_dest == 'account' and account_source == account_dest: return + if gajim.config.get_per('accounts', account_source, 'is_zeroconf'): + return it = iter_source while model[it][C_TYPE] == 'contact': it = model.iter_parent(it) @@ -3577,13 +4077,6 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if jid_source == jid_dest: if grp_source == grp_dest and account_source == account_dest: return - if context.action == gtk.gdk.ACTION_COPY: - self.on_drop_in_group(None, account_source, c_source, grp_dest, - context, etime) - return - self.on_drop_in_group(None, account_source, c_source, grp_dest, - context, etime, grp_source) - return if grp_source == grp_dest: # Add meta contact #FIXME: doesn't work under windows: @@ -3611,13 +4104,13 @@ _('If "%s" accepts this request you will know his or her status.') % jid) menu = gtk.Menu() item = gtk.MenuItem(_('Drop %s in group %s') % (c_source.name, grp_dest)) - item.connect('activate', self.on_drop_in_group, account_dest, c_source, + item.connect('activate', self.on_drop_in_group, account_source, c_source, grp_dest, context, etime, grp_source) menu.append(item) c_dest = gajim.contacts.get_contact_with_highest_priority( account_dest, jid_dest) - item = gtk.MenuItem(_('Make %s and %s metacontacts') % (c_source.name, - c_dest.name)) + item = gtk.MenuItem(_('Make %s and %s metacontacts') % + (c_source.get_shown_name(), c_dest.get_shown_name())) is_big_brother = False if model.iter_has_child(iter_source): is_big_brother = True @@ -3633,12 +4126,16 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def show_title(self): change_title_allowed = gajim.config.get('change_roster_title') + nb_unread = 0 if change_title_allowed: start = '' - nb_unread = gajim.events.get_nb_events(['chat', 'normal', - 'file-request', 'file-error', 'file-completed', - 'file-request-error', 'file-send-error', 'file-stopped', 'gc_msg', - 'printed_chat', 'printed_gc_msg']) + for account in gajim.connections: + # Count events in roster title only if we don't auto open them + if not helpers.allow_popup_window(account): + nb_unread += gajim.events.get_nb_events(['chat', 'normal', + 'file-request', 'file-error', 'file-completed', + 'file-request-error', 'file-send-error', 'file-stopped', + 'printed_chat'], account) if nb_unread > 1: start = '[' + str(nb_unread) + '] ' elif nb_unread == 1: @@ -3703,6 +4200,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def __init__(self): self.xml = gtkgui_helpers.get_glade('roster_window.glade') self.window = self.xml.get_widget('roster_window') + self._music_track_changed_signal = None gajim.interface.msg_win_mgr = MessageWindowMgr() self.advanced_menus = [] # We keep them to destroy them if gajim.config.get('roster_window_skip_taskbar'): @@ -3826,6 +4324,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) self.xml.get_widget('show_offline_contacts_menuitem').set_active( showOffline) + show_transports_group = gajim.config.get('show_transports_group') + self.xml.get_widget('show_transports_menuitem').set_active( + show_transports_group) + # columns # this col has 3 cells: @@ -3879,6 +4381,13 @@ _('If "%s" accepts this request you will know his or her status.') % jid) self.tooltip = tooltips.RosterTooltip() self.draw_roster() + ## Music Track notifications + ## FIXME: we use a timeout because changing status of + ## accounts has no effect until they are connected. + gobject.timeout_add(1000, + self.enable_syncing_status_msg_from_current_music_track, + gajim.config.get('set_status_msg_from_current_music_track')) + if gajim.config.get('show_roster_on_startup'): self.window.show_all() else: @@ -3891,4 +4400,3 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if len(gajim.connections) == 0: # if we have no account gajim.interface.instances['account_creation_wizard'] = \ config.AccountCreationWizardWindow() - diff --git a/src/statusicon.py b/src/statusicon.py new file mode 100644 index 0000000000000000000000000000000000000000..0201afe64991d2280fc14b3bdf2afd6b444d8661 --- /dev/null +++ b/src/statusicon.py @@ -0,0 +1,68 @@ +## statusicon.py +## +## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com> +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +import gtk +import systray + +from common import gajim +from common import helpers + +class StatusIcon(systray.Systray): + '''Class for the notification area icon''' + #FIXME: when we migrate to GTK 2.10 stick only to this class + # (move base stuff from systray.py and rm it) + #NOTE: gtk api does NOT allow: + # leave, enter motion notify + # and can't do cool tooltips we use + # and we could use blinking instead of unsupported animation + # or we could emulate animation by every foo ms chaning the image + def __init__(self): + systray.Systray.__init__(self) + self.status_icon = gtk.StatusIcon() + + def show_icon(self): + self.status_icon.connect('activate', self.on_status_icon_left_clicked) + self.status_icon.connect('popup-menu', self.on_status_icon_right_clicked) + + self.set_img() + self.status_icon.props.visible = True + + def on_status_icon_right_clicked(self, widget, event_button, event_time): + self.make_menu(event_button, event_time) + + def hide_icon(self): + self.status_icon.props.visible = False + + def on_status_icon_left_clicked(self, widget): + self.on_left_click() + + def set_img(self): + '''apart from image, we also update tooltip text here''' + if not gajim.interface.systray_enabled: + return + text = helpers.get_notification_icon_tooltip_text() + self.status_icon.set_tooltip(text) + if gajim.events.get_nb_systray_events(): + state = 'message' + else: + state = self.status + #FIXME: do not always use 16x16 (ask actually used size and use that) + image = gajim.interface.roster.jabber_state_images['16'][state] + if image.get_storage_type() == gtk.IMAGE_PIXBUF: + self.status_icon.props.pixbuf = image.get_pixbuf() + #FIXME: oops they forgot to support GIF animation? + #or they were lazy to get it to work under Windows! WTF! + #elif image.get_storage_type() == gtk.IMAGE_ANIMATION: + # self.img_tray.set_from_animation(image.get_animation()) diff --git a/src/systray.py b/src/systray.py index 926c077d1361fbfa1333c14df054c18880b14609..d17c5fc0b8c43983f6330a46138dd1d1e8bcfd79 100644 --- a/src/systray.py +++ b/src/systray.py @@ -43,7 +43,7 @@ except: class Systray: '''Class for icon in the notification area - This class is both base class (for systraywin32.py) and normal class + This class is both base class (for statusicon.py) and normal class for trayicon in GNU/Linux''' def __init__(self): @@ -94,18 +94,17 @@ class Systray: def on_new_chat(self, widget, account): dialogs.NewChatDialog(account) - def make_menu(self, event = None): - '''create chat with and new message (sub) menus/menuitems - event is None when we're in Windows - ''' - + def make_menu(self, event_button, event_time): + '''create chat with and new message (sub) menus/menuitems''' for m in self.popup_menus: m.destroy() chat_with_menuitem = self.xml.get_widget('chat_with_menuitem') - single_message_menuitem = self.xml.get_widget('single_message_menuitem') + single_message_menuitem = self.xml.get_widget( + 'single_message_menuitem') status_menuitem = self.xml.get_widget('status_menu') join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') + sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem') if self.single_message_handler_id: single_message_menuitem.handler_disconnect( @@ -124,11 +123,12 @@ class Systray: # We need our own set of status icons, let's make 'em! iconset = gajim.config.get('iconset') - if not iconset: - iconset = 'dcraven' path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') state_images = gajim.interface.roster.load_iconset(path) + if state_images.has_key('muc_active'): + join_gc_menuitem.set_image(state_images['muc_active']) + for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): uf_show = helpers.get_uf_show(show, use_mnemonic = True) item = gtk.ImageMenuItem(uf_show) @@ -159,7 +159,8 @@ class Systray: sub_menu.append(item) item.connect('activate', self.on_show_menuitem_activate, 'offline') - iskey = connected_accounts > 0 + iskey = connected_accounts > 0 and not (connected_accounts == 1 and + gajim.connections[gajim.connections.keys()[0]].is_zeroconf) chat_with_menuitem.set_sensitive(iskey) single_message_menuitem.set_sensitive(iskey) join_gc_menuitem.set_sensitive(iskey) @@ -170,12 +171,15 @@ class Systray: self.popup_menus.append(account_menu_for_chat_with) account_menu_for_single_message = gtk.Menu() - single_message_menuitem.set_submenu(account_menu_for_single_message) + single_message_menuitem.set_submenu( + account_menu_for_single_message) self.popup_menus.append(account_menu_for_single_message) accounts_list = gajim.contacts.get_accounts() accounts_list.sort() for account in accounts_list: + if gajim.connections[account].is_zeroconf: + continue if gajim.connections[account].connected > 1: #for chat_with item = gtk.MenuItem(_('using account %s') % account) @@ -194,8 +198,11 @@ class Systray: label.set_use_underline(False) gc_item = gtk.MenuItem() gc_item.add(label) + gc_item.connect('state-changed', + gtkgui_helpers.on_bm_header_changed_state) gc_sub_menu.append(gc_item) - gajim.interface.roster.add_bookmarks_list(gc_sub_menu, account) + gajim.interface.roster.add_bookmarks_list(gc_sub_menu, + account) elif connected_accounts == 1: # one account # one account connected, no need to show 'as jid' @@ -205,24 +212,25 @@ class Systray: 'activate', self.on_new_chat, account) # for single message single_message_menuitem.remove_submenu() - self.single_message_handler_id = single_message_menuitem.connect( - 'activate', self.on_single_message_menuitem_activate, account) + self.single_message_handler_id = single_message_menuitem.\ + connect('activate', + self.on_single_message_menuitem_activate, account) # join gc - gajim.interface.roster.add_bookmarks_list(gc_sub_menu, account) + gajim.interface.roster.add_bookmarks_list(gc_sub_menu, + account) break # No other connected account - if event is None: - # None means windows (we explicitly popup in systraywin32.py) - if self.added_hide_menuitem is False: - self.systray_context_menu.prepend(gtk.SeparatorMenuItem()) - item = gtk.MenuItem(_('Hide this menu')) - self.systray_context_menu.prepend(item) - self.added_hide_menuitem = True + sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on')) + if os.name == 'nt' and gtk.pygtk_version >= (2, 10, 0) and\ + gtk.gtk_version >= (2, 10, 0): + self.systray_context_menu.popup(None, None, + gtk.status_icon_position_menu, event_button, + event_time, self.status_icon) else: # GNU and Unices - self.systray_context_menu.popup(None, None, None, event.button, - event.time) + self.systray_context_menu.popup(None, None, None, event_button, + event_time) self.systray_context_menu.show_all() def on_show_all_events_menuitem_activate(self, widget): @@ -232,6 +240,10 @@ class Systray: for event in events[account][jid]: gajim.interface.handle_event(account, jid, event.type_) + def on_sounds_mute_menuitem_activate(self, widget): + gajim.config.set('sounds_on', not widget.get_active()) + gajim.interface.save_config() + def on_show_roster_menuitem_activate(self, widget): win = gajim.interface.roster.window win.present() @@ -250,11 +262,11 @@ class Systray: if len(gajim.events.get_systray_events()) == 0: # no pending events, so toggle visible/hidden for roster window if win.get_property('visible'): # visible in ANY virtual desktop? - win.hide() # we hide it from VD that was visible in - # but we could be in another VD right now. eg vd2 - # and we want not only to hide it in vd1 but also show it in vd2 - gtkgui_helpers.possibly_move_window_in_current_desktop(win) + # we could be in another VD right now. eg vd2 + # and we want to show it in vd2 + if not gtkgui_helpers.possibly_move_window_in_current_desktop(win): + win.hide() # else we hide it from VD that was visible in else: win.present() else: @@ -275,12 +287,14 @@ class Systray: def on_clicked(self, widget, event): self.on_tray_leave_notify_event(widget, None) - if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1: # Left click + if event.type != gtk.gdk.BUTTON_PRESS: + return + if event.button == 1: # Left click self.on_left_click() elif event.button == 2: # middle click self.on_middle_click() elif event.button == 3: # right click - self.make_menu(event) + self.make_menu(event.button, event.time) def on_show_menuitem_activate(self, widget, show): # we all add some fake (we cannot select those nor have them as show) @@ -295,6 +309,7 @@ class Systray: active = gajim.interface.roster.status_combobox.get_active() status = model[active][2].decode('utf-8') dlg = dialogs.ChangeStatusMessageDialog(status) + dlg.window.present() message = dlg.run() if message is not None: # None if user press Cancel accounts = gajim.connections.keys() diff --git a/src/systraywin32.py b/src/systraywin32.py deleted file mode 100644 index c758247e01d2a2c62945e98435f7c9b922e80f39..0000000000000000000000000000000000000000 --- a/src/systraywin32.py +++ /dev/null @@ -1,305 +0,0 @@ -## src/systraywin32.py -## -## Contributors for this file: -## - Yann Le Boulanger <asterix@lagaule.org> -## - Nikos Kouremenos <kourem@gmail.com> -## - Dimitur Kirov <dkirov@gmail.com> -## -## code initially based on -## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/334779 -## with some ideas/help from pysystray.sf.net -## -## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> -## Vincent Hanquez <tab@snarc.org> -## Nikos Kouremenos <nkour@jabber.org> -## Dimitur Kirov <dkirov@gmail.com> -## Travis Shirk <travis@pobox.com> -## Norman Rasmussen <norman@rasmussen.co.za> -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 2 only. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## - - -import win32gui -import pywintypes -import win32con # winapi constants -import systray -import gtk -import os - -WM_TASKBARCREATED = win32gui.RegisterWindowMessage('TaskbarCreated') -WM_TRAYMESSAGE = win32con.WM_USER + 20 - -import gtkgui_helpers -from common import gajim -from common import i18n - -class SystrayWINAPI: - def __init__(self, gtk_window): - self._window = gtk_window - self._hwnd = gtk_window.window.handle - self._message_map = {} - - self.notify_icon = None - - # Sublass the window and inject a WNDPROC to process messages. - self._oldwndproc = win32gui.SetWindowLong(self._hwnd, - win32con.GWL_WNDPROC, self._wndproc) - - - def add_notify_icon(self, menu, hicon=None, tooltip=None): - """ Creates a notify icon for the gtk window. """ - if not self.notify_icon: - if not hicon: - hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) - self.notify_icon = NotifyIcon(self._hwnd, hicon, tooltip) - - # Makes redraw if the taskbar is restarted. - self.message_map({WM_TASKBARCREATED: self.notify_icon._redraw}) - - - def message_map(self, msg_map={}): - """ Maps message processing to callback functions ala win32gui. """ - if msg_map: - if self._message_map: - duplicatekeys = [key for key in msg_map.keys() - if self._message_map.has_key(key)] - - for key in duplicatekeys: - new_value = msg_map[key] - - if isinstance(new_value, list): - raise TypeError('Dict cannot have list values') - - value = self._message_map[key] - - if new_value != value: - new_value = [new_value] - - if isinstance(value, list): - value += new_value - else: - value = [value] + new_value - - msg_map[key] = value - self._message_map.update(msg_map) - - def remove_notify_icon(self): - """ Removes the notify icon. """ - if self.notify_icon: - self.notify_icon.remove() - self.notify_icon = None - - def remove(self, *args): - """ Unloads the extensions. """ - self._message_map = {} - self.remove_notify_icon() - self = None - - def show_balloon_tooltip(self, title, text, timeout=10, - icon=win32gui.NIIF_NONE): - """ Shows a baloon tooltip. """ - if not self.notify_icon: - self.add_notifyicon() - self.notify_icon.show_balloon(title, text, timeout, icon) - - def _wndproc(self, hwnd, msg, wparam, lparam): - """ A WINDPROC to process window messages. """ - if self._message_map.has_key(msg): - callback = self._message_map[msg] - if isinstance(callback, list): - for cb in callback: - cb(hwnd, msg, wparam, lparam) - else: - callback(hwnd, msg, wparam, lparam) - - return win32gui.CallWindowProc(self._oldwndproc, hwnd, msg, wparam, - lparam) - - -class NotifyIcon: - - def __init__(self, hwnd, hicon, tooltip=None): - self._hwnd = hwnd - self._id = 0 - self._flags = win32gui.NIF_MESSAGE | win32gui.NIF_ICON - self._callbackmessage = WM_TRAYMESSAGE - self._hicon = hicon - - try: - win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self._get_nid()) - except pywintypes.error: - pass - if tooltip: self.set_tooltip(tooltip) - - - def _get_nid(self): - """ Function to initialise & retrieve the NOTIFYICONDATA Structure. """ - nid = [self._hwnd, self._id, self._flags, self._callbackmessage, self._hicon] - - if not hasattr(self, '_tip'): self._tip = '' - nid.append(self._tip) - - if not hasattr(self, '_info'): self._info = '' - nid.append(self._info) - - if not hasattr(self, '_timeout'): self._timeout = 0 - nid.append(self._timeout) - - if not hasattr(self, '_infotitle'): self._infotitle = '' - nid.append(self._infotitle) - - if not hasattr(self, '_infoflags'):self._infoflags = win32gui.NIIF_NONE - nid.append(self._infoflags) - - return tuple(nid) - - def remove(self): - """ Removes the tray icon. """ - try: - win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, self._get_nid()) - except pywintypes.error: - pass - - - def set_tooltip(self, tooltip): - """ Sets the tray icon tooltip. """ - self._flags = self._flags | win32gui.NIF_TIP - self._tip = tooltip - try: - win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self._get_nid()) - except pywintypes.error: - pass - - - def show_balloon(self, title, text, timeout=10, icon=win32gui.NIIF_NONE): - """ Shows a balloon tooltip from the tray icon. """ - self._flags = self._flags | win32gui.NIF_INFO - self._infotitle = title - self._info = text - self._timeout = timeout * 1000 - self._infoflags = icon - try: - win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self._get_nid()) - except pywintypes.error: - pass - - def _redraw(self, *args): - """ Redraws the tray icon. """ - self.remove() - try: - win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self._get_nid()) - except pywintypes.error: - pass - - -class SystrayWin32(systray.Systray): - def __init__(self): - # Note: gtk window must be realized before installing extensions. - systray.Systray.__init__(self) - self.jids = [] - self.status = 'offline' - self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade') - self.systray_context_menu = self.xml.get_widget('systray_context_menu') - self.added_hide_menuitem = False - -# self.tray_ico_imgs = self.load_icos() - - w = gtk.Window() # just a window to pass - w.realize() # realize it so gtk window exists - self.systray_winapi = SystrayWINAPI(w) - - self.xml.signal_autoconnect(self) - - # Set up the callback messages - self.systray_winapi.message_map({ - WM_TRAYMESSAGE: self.on_clicked - }) - - def show_icon(self): - #self.systray_winapi.add_notify_icon(self.systray_context_menu, tooltip = 'Gajim') - #self.systray_winapi.notify_icon.menu = self.systray_context_menu - # do not remove set_img does both above. - # maybe I can only change img without readding - # the notify icon? HOW?? - self.set_img() - - def hide_icon(self): - self.systray_winapi.remove() - - def on_clicked(self, hwnd, message, wparam, lparam): - if lparam == win32con.WM_RBUTTONUP: # Right click - self.make_menu() - self.systray_winapi.notify_icon.menu.popup(None, None, None, 0, 0) - elif lparam == win32con.WM_MBUTTONUP: # Middle click - self.on_middle_click() - elif lparam == win32con.WM_LBUTTONUP: # Left click - self.on_left_click() - - def set_img(self): - self.tray_ico_imgs = self.load_icos() #FIXME: do not do this here - # see gajim.interface.roster.reload_jabber_state_images() to merge - - if len(self.jids) > 0: - state = 'message' - else: - state = self.status - hicon = self.tray_ico_imgs[state] - if hicon is None: - return - - self.systray_winapi.remove_notify_icon() - self.systray_winapi.add_notify_icon(self.systray_context_menu, hicon, - 'Gajim') - self.systray_winapi.notify_icon.menu = self.systray_context_menu - - nb = gajim.events.get_nb_systray_events() - - if nb > 0: - text = i18n.ngettext( - 'Gajim - %d unread message', - 'Gajim - %d unread messages', - nb, nb, nb) - else: - text = 'Gajim' - self.systray_winapi.notify_icon.set_tooltip(text) - - def load_icos(self): - '''load .ico files and return them to a dic of SHOW --> img_obj''' - iconset = str(gajim.config.get('iconset')) - if not iconset: - iconset = 'dcraven' - - imgs = {} - path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16', 'icos') - # icon folder for missing icons - path_dcraven_iconset = os.path.join(gajim.DATA_DIR, 'iconsets', 'dcraven', - '16x16', 'icos') - states_list = gajim.SHOW_LIST - # trayicon apart from show holds message state too - states_list.append('message') - for state in states_list: - path_to_ico = os.path.join(path, state + '.ico') - if not os.path.isfile(path_to_ico): # fallback to dcraven iconset - path_to_ico = os.path.join(path_dcraven_iconset, state + '.ico') - if os.path.exists(path_to_ico): - hinst = win32gui.GetModuleHandle(None) - img_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE - try: - image = win32gui.LoadImage(hinst, path_to_ico, - win32con.IMAGE_ICON, 0, 0, img_flags) - except pywintypes.error: - imgs[state] = None - else: - imgs[state] = image - - return imgs diff --git a/src/tooltips.py b/src/tooltips.py index 53915df8ab78b3ecfbd110c8825cfd57dfad6110..a6e40fe2f177e7ba30c5535109ea8ab69b849a5b 100644 --- a/src/tooltips.py +++ b/src/tooltips.py @@ -21,7 +21,6 @@ import time import locale import gtkgui_helpers -import message_control from common import gajim from common import helpers @@ -38,7 +37,7 @@ class BaseTooltip: tooltip.hide_tooltip() * data - the text to be displayed (extenders override this argument and - dislpay more complex contents) + display more complex contents) * widget_height - the height of the widget on which we want to show tooltip * widget_y_position - the vertical position of the widget on the screen @@ -135,7 +134,7 @@ class BaseTooltip: preferred_y = widget_y_position + widget_height + 4 self.preferred_position = [pointer_x, preferred_y] - self.widget_height =widget_height + self.widget_height = widget_height self.win.ensure_style() self.win.show_all() @@ -177,10 +176,12 @@ class StatusTable: # make sure 'status' is unicode before we send to to reduce_chars if isinstance(status, str): status = unicode(status, encoding='utf-8') - # reduce to 200 chars, 1 line - status = gtkgui_helpers.reduce_chars_newlines(status, 200, 1) - str_status += ' - ' + status - return gtkgui_helpers.escape_for_pango_markup(str_status) + # reduce to 100 chars, 1 line + status = helpers.reduce_chars_newlines(status, 100, 1) + str_status = gtkgui_helpers.escape_for_pango_markup(str_status) + status = gtkgui_helpers.escape_for_pango_markup(status) + str_status += ' - <i>' + status + '</i>' + return str_status def add_status_row(self, file_path, show, str_status, status_time = None, show_lock = False): ''' appends a new row with status icon to the table ''' @@ -213,13 +214,6 @@ class StatusTable: gtk.ICON_SIZE_MENU) self.table.attach(lock_image, 4, 5, self.current_row, self.current_row + 1, 0, 0, 0, 0) - if status_time: - self.current_row += 1 - # decode locale encoded string, the same way as below (10x nk) - local_time = time.strftime("%c", status_time) - local_time = local_time.decode(locale.getpreferredencoding()) - status_time_label = gtk.Label(local_time) - status_time_label.set_alignment(0, 0) class NotificationAreaTooltip(BaseTooltip, StatusTable): ''' Tooltip that is shown in the notification area ''' @@ -227,27 +221,6 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): BaseTooltip.__init__(self) StatusTable.__init__(self) - def get_accounts_info(self): - accounts = [] - accounts_list = gajim.contacts.get_accounts() - accounts_list.sort() - for account in accounts_list: - status_idx = gajim.connections[account].connected - # uncomment the following to hide offline accounts - # if status_idx == 0: continue - status = gajim.SHOW_LIST[status_idx] - message = gajim.connections[account].status - single_line = helpers.get_uf_show(status) - if message is None: - message = '' - else: - message = message.strip() - if message != '': - single_line += ': ' + message - accounts.append({'name': account, 'status_line': single_line, - 'show': status, 'message': message}) - return accounts - def fill_table_with_accounts(self, accounts): iconset = gajim.config.get('iconset') if not iconset: @@ -259,7 +232,7 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): # there are possible pango TBs on 'set_markup' if isinstance(message, str): message = unicode(message, encoding = 'utf-8') - message = gtkgui_helpers.reduce_chars_newlines(message, 50, 1) + message = helpers.reduce_chars_newlines(message, 100, 1) message = gtkgui_helpers.escape_for_pango_markup(message) if gajim.con_types.has_key(acct['name']) and \ gajim.con_types[acct['name']] in ('tls', 'ssl'): @@ -278,67 +251,16 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): def populate(self, data): self.create_window() self.create_table() - self.hbox = gtk.HBox() - self.table.set_property('column-spacing', 1) - text, single_line = '', '' - - unread_chat = gajim.events.get_nb_events(types = ['printed_chat', 'chat']) - unread_single_chat = gajim.events.get_nb_events(types = ['normal']) - unread_gc = gajim.events.get_nb_events(types = ['printed_gc_msg', - 'gc_msg']) - unread_pm = gajim.events.get_nb_events(types = ['printed_pm', 'pm']) - - accounts = self.get_accounts_info() - - if unread_chat or unread_single_chat or unread_gc or unread_pm: - text = 'Gajim ' - awaiting_events = unread_chat + unread_single_chat + unread_gc + unread_pm - if awaiting_events == unread_chat or awaiting_events == unread_single_chat \ - or awaiting_events == unread_gc or awaiting_events == unread_pm: - # This condition is like previous if but with xor... - # Print in one line - text += '-' - else: - # Print in multiple lines - text += '\n ' - if unread_chat: - text += i18n.ngettext( - ' %d unread message', - ' %d unread messages', - unread_chat, unread_chat, unread_chat) - text += '\n ' - if unread_single_chat: - text += i18n.ngettext( - ' %d unread single message', - ' %d unread single messages', - unread_single_chat, unread_single_chat, unread_single_chat) - text += '\n ' - if unread_gc: - text += i18n.ngettext( - ' %d unread group chat message', - ' %d unread group chat messages', - unread_gc, unread_gc, unread_gc) - text += '\n ' - if unread_pm: - text += i18n.ngettext( - ' %d unread private message', - ' %d unread private messages', - unread_pm, unread_pm, unread_pm) - text += '\n ' - text = text[:-4] # remove latest '\n ' - elif len(accounts) > 1: - text = _('Gajim') - self.current_current_row = 1 + accounts = helpers.get_accounts_info() + if len(accounts) > 1: self.table.resize(2, 1) self.fill_table_with_accounts(accounts) + self.hbox = gtk.HBox() + self.table.set_property('column-spacing', 1) - elif len(accounts) == 1: - message = accounts[0]['status_line'] - message = gtkgui_helpers.reduce_chars_newlines(message, 50, 1) - message = gtkgui_helpers.escape_for_pango_markup(message) - text = _('Gajim - %s') % message - else: - text = _('Gajim - %s') % helpers.get_uf_show('offline') + text = helpers.get_notification_icon_tooltip_text() + text = gtkgui_helpers.escape_for_pango_markup(text) + self.add_text_row(text) self.hbox.add(self.table) self.win.add(self.hbox) @@ -364,27 +286,37 @@ class GCTooltip(BaseTooltip): vcard_table.set_homogeneous(False) vcard_current_row = 1 properties = [] - - if contact.jid.strip() != '': - jid_markup = '<span weight="bold">' + contact.jid + '</span>' - else: - jid_markup = '<span weight="bold">' + \ + + nick_markup = '<b>' + \ gtkgui_helpers.escape_for_pango_markup(contact.get_shown_name()) \ - + '</span>' - properties.append((jid_markup, None)) - properties.append((_('Role: '), helpers.get_uf_role(contact.role))) - properties.append((_('Affiliation: '), contact.affiliation.capitalize())) - if hasattr(contact, 'resource') and contact.resource.strip() != '': - properties.append((_('Resource: '), - gtkgui_helpers.escape_for_pango_markup(contact.resource) )) - show = helpers.get_uf_show(contact.show) - if contact.status: + + '</b>' + properties.append((nick_markup, None)) + + if contact.status: # status message status = contact.status.strip() if status != '': # escape markup entities - status = gtkgui_helpers.reduce_chars_newlines(status, 200, 5) - show += ' - ' + gtkgui_helpers.escape_for_pango_markup(status) - properties.append((_('Status: '), show)) + status = helpers.reduce_chars_newlines(status, 100, 5) + status = '<i>' +\ + gtkgui_helpers.escape_for_pango_markup(status) + '</i>' + properties.append((status, None)) + else: # no status message, show SHOW instead + show = helpers.get_uf_show(contact.show) + show = '<i>' + show + '</i>' + properties.append((show, None)) + + if contact.jid.strip() != '': + properties.append((_('JID: '), contact.jid)) + + if hasattr(contact, 'resource') and contact.resource.strip() != '': + properties.append((_('Resource: '), + gtkgui_helpers.escape_for_pango_markup(contact.resource) )) + if contact.affiliation != 'none': + uf_affiliation = helpers.get_uf_affiliation(contact.affiliation) + affiliation_str = \ + _('%(owner_or_admin_or_member)s of this group chat') %\ + {'owner_or_admin_or_member': uf_affiliation} + properties.append((affiliation_str, None)) # Add avatar puny_name = helpers.sanitize_filename(contact.name) @@ -410,22 +342,23 @@ class GCTooltip(BaseTooltip): label.set_alignment(0, 0) if property[1]: label.set_markup(property[0]) - vcard_table.attach(label, 1, 2, vcard_current_row, vcard_current_row + 1, - gtk.FILL, vertical_fill, 0, 0) + vcard_table.attach(label, 1, 2, vcard_current_row, + vcard_current_row + 1, gtk.FILL, vertical_fill, 0, 0) label = gtk.Label() label.set_alignment(0, 0) label.set_markup(property[1]) label.set_line_wrap(True) - vcard_table.attach(label, 2, 3, vcard_current_row, vcard_current_row + 1, - gtk.EXPAND | gtk.FILL, vertical_fill, 0, 0) + vcard_table.attach(label, 2, 3, vcard_current_row, + vcard_current_row + 1, gtk.EXPAND | gtk.FILL, + vertical_fill, 0, 0) else: label.set_markup(property[0]) - vcard_table.attach(label, 1, 3, vcard_current_row, vcard_current_row + 1, - gtk.FILL, vertical_fill, 0) + vcard_table.attach(label, 1, 3, vcard_current_row, + vcard_current_row + 1, gtk.FILL, vertical_fill, 0) self.avatar_image.set_alignment(0, 0) - vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row +1, - gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3) + vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row + 1, + gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3) self.win.add(vcard_table) class RosterTooltip(NotificationAreaTooltip): @@ -445,8 +378,7 @@ class RosterTooltip(NotificationAreaTooltip): self.create_table() if not contacts or len(contacts) == 0: # Tooltip for merged accounts row - accounts = self.get_accounts_info() - self.current_current_row = 0 + accounts = helpers.get_accounts_info() self.table.resize(2, 1) self.spacer_label = '' self.fill_table_with_accounts(accounts) @@ -458,15 +390,6 @@ class RosterTooltip(NotificationAreaTooltip): prim_contact = gajim.contacts.get_highest_prio_contact_from_contacts( contacts) - transport = gajim.get_transport_name_from_jid(prim_contact.jid) - if transport: - file_path = os.path.join(gajim.DATA_DIR, 'iconsets', 'transports', - transport , '16x16') - else: - iconset = gajim.config.get('iconset') - if not iconset: - iconset = 'dcraven' - file_path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') puny_jid = helpers.sanitize_filename(prim_contact.jid) table_size = 3 @@ -486,23 +409,12 @@ class RosterTooltip(NotificationAreaTooltip): vcard_table.set_homogeneous(False) vcard_current_row = 1 properties = [] - jid_markup = '<span weight="bold">' + prim_contact.jid + '</span>' - properties.append((jid_markup, None)) - properties.append((_('Name: '), gtkgui_helpers.escape_for_pango_markup( - prim_contact.get_shown_name()))) - if prim_contact.sub: - properties.append(( _('Subscription: '), - gtkgui_helpers.escape_for_pango_markup(helpers.get_uf_sub(prim_contact.sub)))) - if prim_contact.keyID: - keyID = None - if len(prim_contact.keyID) == 8: - keyID = prim_contact.keyID - elif len(prim_contact.keyID) == 16: - keyID = prim_contact.keyID[8:] - if keyID: - properties.append((_('OpenPGP: '), - gtkgui_helpers.escape_for_pango_markup(keyID))) + name_markup = u'<span weight="bold">' + \ + gtkgui_helpers.escape_for_pango_markup(prim_contact.get_shown_name())\ + + '</span>' + properties.append((name_markup, None)) + num_resources = 0 # put contacts in dict, where key is priority contacts_dict = {} @@ -514,12 +426,20 @@ class RosterTooltip(NotificationAreaTooltip): else: contacts_dict[contact.priority] = [contact] - if num_resources == 1 and contact.resource: - properties.append((_('Resource: '), - gtkgui_helpers.escape_for_pango_markup(contact.resource) + ' (' + \ - unicode(contact.priority) + ')')) if num_resources > 1: properties.append((_('Status: '), ' ')) + transport = gajim.get_transport_name_from_jid( + prim_contact.jid) + if transport: + file_path = os.path.join(gajim.DATA_DIR, 'iconsets', + 'transports', transport , '16x16') + else: + iconset = gajim.config.get('iconset') + if not iconset: + iconset = 'dcraven' + file_path = os.path.join(gajim.DATA_DIR, + 'iconsets', iconset, '16x16') + contact_keys = contacts_dict.keys() contact_keys.sort() contact_keys.reverse() @@ -535,29 +455,63 @@ class RosterTooltip(NotificationAreaTooltip): else: # only one resource if contact.show: show = helpers.get_uf_show(contact.show) + if contact.last_status_time: + vcard_current_row += 1 + if contact.show == 'offline': + text = ' - ' + _('Last status: %s') + else: + text = _(' since %s') + + if time.strftime('%j', time.localtime())== \ + time.strftime('%j', contact.last_status_time): + # it's today, show only the locale hour representation + local_time = time.strftime('%X', + contact.last_status_time) + else: + # time.strftime returns locale encoded string + local_time = time.strftime('%c', + contact.last_status_time) + local_time = local_time.decode( + locale.getpreferredencoding()) + text = text % local_time + show += text + show = '<i>' + show + '</i>' + # we append show below + if contact.status: status = contact.status.strip() if status: # reduce long status - # (no more than 200 chars on line and no more than 5 lines) - status = gtkgui_helpers.reduce_chars_newlines(status, 200, 5) + # (no more than 100 chars on line and no more than 5 lines) + status = helpers.reduce_chars_newlines(status, 100, 5) # escape markup entities. status = gtkgui_helpers.escape_for_pango_markup(status) - show += ' - ' + status - properties.append((_('Status: '), show)) - - if contact.last_status_time: - vcard_current_row += 1 - if contact.show == 'offline': - text = _('Last status on %s') - else: - text = _('Since %s') + properties.append(('<i>%s</i>' % status, None)) + properties.append((show, None)) + + properties.append((_('Jabber ID: '), prim_contact.jid )) - # time.strftime returns locale encoded string - local_time = time.strftime('%c', contact.last_status_time) - local_time = local_time.decode(locale.getpreferredencoding()) - text = text % local_time - properties.append(('<span style="italic">%s</span>' % text, None)) + # contact has only one ressource + if num_resources == 1 and contact.resource: + properties.append((_('Resource: '), + gtkgui_helpers.escape_for_pango_markup(contact.resource) +\ + ' (' + unicode(contact.priority) + ')')) + + if prim_contact.sub and prim_contact.sub != 'both': + # ('both' is the normal sub so we don't show it) + properties.append(( _('Subscription: '), + gtkgui_helpers.escape_for_pango_markup(helpers.get_uf_sub(prim_contact.sub)))) + + if prim_contact.keyID: + keyID = None + if len(prim_contact.keyID) == 8: + keyID = prim_contact.keyID + elif len(prim_contact.keyID) == 16: + keyID = prim_contact.keyID[8:] + if keyID: + properties.append((_('OpenPGP: '), + gtkgui_helpers.escape_for_pango_markup(keyID))) + while properties: property = properties.pop(0) vcard_current_row += 1 @@ -568,28 +522,26 @@ class RosterTooltip(NotificationAreaTooltip): label.set_alignment(0, 0) if property[1]: label.set_markup(property[0]) - vcard_table.attach(label, 1, 2, vcard_current_row, vcard_current_row + 1, - gtk.FILL, vertical_fill, 0, 0) + vcard_table.attach(label, 1, 2, vcard_current_row, + vcard_current_row + 1, gtk.FILL, vertical_fill, 0, 0) label = gtk.Label() - if num_resources > 1 and not properties: - label.set_alignment(0, 1) - else: - label.set_alignment(0, 0) + label.set_alignment(0, 0) label.set_markup(property[1]) label.set_line_wrap(True) - vcard_table.attach(label, 2, 3, vcard_current_row, vcard_current_row + 1, - gtk.EXPAND | gtk.FILL, vertical_fill, 0, 0) + vcard_table.attach(label, 2, 3, vcard_current_row, + vcard_current_row + 1, gtk.EXPAND | gtk.FILL, + vertical_fill, 0, 0) else: - if isinstance(property[0], unicode): + if isinstance(property[0], (unicode, str)): #FIXME: rm unicode? label.set_markup(property[0]) else: label = property[0] - vcard_table.attach(label, 1, 3, vcard_current_row, vcard_current_row + 1, - gtk.FILL, vertical_fill, 0) + vcard_table.attach(label, 1, 3, vcard_current_row, + vcard_current_row + 1, gtk.FILL, vertical_fill, 0) self.avatar_image.set_alignment(0, 0) if table_size == 4: - vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row +1, - gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3) + vcard_table.attach(self.avatar_image, 3, 4, 2, + vcard_current_row + 1, gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3) self.win.add(vcard_table) class FileTransfersTooltip(BaseTooltip): diff --git a/src/vcard.py b/src/vcard.py index 009605b3825b41b1b7bc1c102202eeea2a070a6d..a479d382bfc883b6872e06d246f751078c31de8c 100644 --- a/src/vcard.py +++ b/src/vcard.py @@ -2,6 +2,7 @@ ## ## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org> ## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -13,16 +14,15 @@ ## GNU General Public License for more details. ## +# THIS FILE IS FOR **OTHERS'** PROFILE (when we VIEW their INFO) + import gtk import gobject import base64 -import mimetypes -import os import time import locale import gtkgui_helpers -import dialogs from common import helpers from common import gajim @@ -57,25 +57,49 @@ def get_avatar_pixbuf_encoded_mime(photo): class VcardWindow: '''Class for contact's information window''' - def __init__(self, contact, account, is_fake = False): + def __init__(self, contact, account, gc_contact = None): # the contact variable is the jid if vcard is true self.xml = gtkgui_helpers.get_glade('vcard_information_window.glade') self.window = self.xml.get_widget('vcard_information_window') + self.progressbar = self.xml.get_widget('progressbar') self.contact = contact self.account = account - self.is_fake = is_fake + self.gc_contact = gc_contact self.avatar_mime_type = None self.avatar_encoded = None + self.vcard_arrived = False + self.os_info_arrived = False + self.update_progressbar_timeout_id = gobject.timeout_add(100, + self.update_progressbar) self.fill_jabber_page() + annotations = gajim.connections[self.account].annotations + if self.contact.jid in annotations: + buffer = self.xml.get_widget('textview_annotation').get_buffer() + buffer.set_text(annotations[self.contact.jid]) self.xml.signal_autoconnect(self) self.window.show_all() + self.xml.get_widget('close_button').grab_focus() + + def update_progressbar(self): + self.progressbar.pulse() + return True # loop forever def on_vcard_information_window_destroy(self, widget): + if self.update_progressbar_timeout_id is not None: + gobject.source_remove(self.update_progressbar_timeout_id) del gajim.interface.instances[self.account]['infos'][self.contact.jid] + buffer = self.xml.get_widget('textview_annotation').get_buffer() + annotation = buffer.get_text(buffer.get_start_iter(), + buffer.get_end_iter()) + connection = gajim.connections[self.account] + if annotation != connection.annotations.get(self.contact.jid, ''): + connection.annotations[self.contact.jid] = annotation + connection.store_annotations() + def on_vcard_information_window_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Escape: @@ -104,7 +128,8 @@ class VcardWindow: menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) menuitem.connect('activate', gtkgui_helpers.on_avatar_save_as_menuitem_activate, - self.contact.jid, self.account, self.contact.name + '.jpeg') + self.contact.jid, self.account, self.contact.get_shown_name() + + '.jpeg') menu.append(menuitem) menu.connect('selection-done', lambda w:w.destroy()) # show the menu @@ -113,13 +138,24 @@ class VcardWindow: def set_value(self, entry_name, value): try: - self.xml.get_widget(entry_name).set_text(value) + if value and entry_name == 'URL_label': + if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0): + widget = gtk.LinkButton(value, value) + else: + widget = gtk.Label(value) + widget.set_selectable(True) + widget.show() + table = self.xml.get_widget('personal_info_table') + table.attach(widget, 1, 4, 3, 4, yoptions = 0) + else: + self.xml.get_widget(entry_name).set_text(value) except AttributeError: pass def set_values(self, vcard): for i in vcard.keys(): - if i == 'PHOTO': + if i == 'PHOTO' and self.xml.get_widget('information_notebook').\ + get_n_pages() > 4: pixbuf, self.avatar_encoded, self.avatar_mime_type = \ get_avatar_pixbuf_encoded_mime(vcard[i]) image = self.xml.get_widget('PHOTO_image') @@ -130,7 +166,7 @@ class VcardWindow: pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard') image.set_from_pixbuf(pixbuf) continue - if i == 'ADR' or i == 'TEL' or i == 'EMAIL': + if i in ('ADR', 'TEL', 'EMAIL'): for entry in vcard[i]: add_on = '_HOME' if 'WORK' in entry: @@ -144,14 +180,23 @@ class VcardWindow: if i == 'DESC': self.xml.get_widget('DESC_textview').get_buffer().set_text( vcard[i], 0) - else: + elif i != 'jid': # Do not override jid_label self.set_value(i + '_label', vcard[i]) + self.vcard_arrived = True + self.test_remove_progressbar() + + def test_remove_progressbar(self): + if self.update_progressbar_timeout_id is not None and \ + self.vcard_arrived and self.os_info_arrived: + gobject.source_remove(self.update_progressbar_timeout_id) + self.progressbar.hide() + self.update_progressbar_timeout_id = None def set_last_status_time(self): self.fill_status_label() def set_os_info(self, resource, client_info, os_info): - if self.xml.get_widget('information_notebook').get_n_pages() < 4: + if self.xml.get_widget('information_notebook').get_n_pages() < 5: return i = 0 client = '' @@ -174,16 +219,25 @@ class VcardWindow: os = Q_('?OS:Unknown') self.xml.get_widget('client_name_version_label').set_text(client) self.xml.get_widget('os_label').set_text(os) + self.os_info_arrived = True + self.test_remove_progressbar() def fill_status_label(self): - if self.xml.get_widget('information_notebook').get_n_pages() < 4: + if self.xml.get_widget('information_notebook').get_n_pages() < 5: return contact_list = gajim.contacts.get_contact(self.account, self.contact.jid) + connected_contact_list = [] + for c in contact_list: + if c.show not in ('offline', 'error'): + connected_contact_list.append(c) + if not connected_contact_list: + # no connected contact, get the offline one + connected_contact_list = contact_list # stats holds show and status message stats = '' one = True # Are we adding the first line ? - if contact_list: - for c in contact_list: + if connected_contact_list: + for c in connected_contact_list: if not one: stats += '\n' stats += helpers.get_uf_show(c.show) @@ -212,26 +266,38 @@ class VcardWindow: self.contact.get_shown_name() + '</span></b>') self.xml.get_widget('jid_label').set_text(self.contact.jid) - uf_sub = helpers.get_uf_sub(self.contact.sub) - self.xml.get_widget('subscription_label').set_text(uf_sub) - eb = self.xml.get_widget('subscription_label_eventbox') - if self.contact.sub == 'from': - tt_text = _("This contact is interested in your presence information, but you are not interested in his/her presence") - elif self.contact.sub == 'to': - tt_text = _("You are interested in the contact's presence information, but he/she is not interested in yours") - elif self.contact.sub == 'both': - tt_text = _("You and the contact are interested in each other's presence information") - else: # None - tt_text = _("You are not interested in the contact's presence, and neither he/she is interested in yours") - tooltips.set_tip(eb, tt_text) - - label = self.xml.get_widget('ask_label') - uf_ask = helpers.get_uf_ask(self.contact.ask) - label.set_text(uf_ask) - eb = self.xml.get_widget('ask_label_eventbox') - if self.contact.ask == 'subscribe': - tooltips.set_tip(eb, - _("You are waiting contact's answer about your subscription request")) + + subscription_label = self.xml.get_widget('subscription_label') + ask_label = self.xml.get_widget('ask_label') + if self.gc_contact: + self.xml.get_widget('subscription_title_label').set_text(_("Role:")) + uf_role = helpers.get_uf_role(self.gc_contact.role) + subscription_label.set_text(uf_role) + + self.xml.get_widget('ask_title_label').set_text(_("Affiliation:")) + uf_affiliation = helpers.get_uf_affiliation(self.gc_contact.affiliation) + ask_label.set_text(uf_affiliation) + else: + uf_sub = helpers.get_uf_sub(self.contact.sub) + subscription_label.set_text(uf_sub) + eb = self.xml.get_widget('subscription_label_eventbox') + if self.contact.sub == 'from': + tt_text = _("This contact is interested in your presence information, but you are not interested in his/her presence") + elif self.contact.sub == 'to': + tt_text = _("You are interested in the contact's presence information, but he/she is not interested in yours") + elif self.contact.sub == 'both': + tt_text = _("You and the contact are interested in each other's presence information") + else: # None + tt_text = _("You are not interested in the contact's presence, and neither he/she is interested in yours") + tooltips.set_tip(eb, tt_text) + + uf_ask = helpers.get_uf_ask(self.contact.ask) + ask_label.set_text(uf_ask) + eb = self.xml.get_widget('ask_label_eventbox') + if self.contact.ask == 'subscribe': + tooltips.set_tip(eb, + _("You are waiting contact's answer about your subscription request")) + log = True if self.contact.jid in gajim.config.get_per('accounts', self.account, 'no_log_for').split(' '): @@ -251,8 +317,12 @@ class VcardWindow: gajim.connections[self.account].request_last_status_time(self.contact.jid, self.contact.resource) - # Request os info in contact is connected - if self.contact.show not in ('offline', 'error'): + # do not wait for os_info if contact is not connected or has error + # additional check for observer is needed, as show is offline for him + if self.contact.show in ('offline', 'error')\ + and not self.contact.is_observer(): + self.os_info_arrived = True + else: # Request os info if contact is connected gobject.idle_add(gajim.connections[self.account].request_os_info, self.contact.jid, self.contact.resource) self.os_info = {0: {'resource': self.contact.resource, 'client': '', @@ -282,4 +352,160 @@ class VcardWindow: self.fill_status_label() - gajim.connections[self.account].request_vcard(self.contact.jid, self.is_fake) + gajim.connections[self.account].request_vcard(self.contact.jid, + self.gc_contact is not None) + + def on_close_button_clicked(self, widget): + self.window.destroy() + + +class ZeroconfVcardWindow: + def __init__(self, contact, account, is_fake = False): + # the contact variable is the jid if vcard is true + self.xml = gtkgui_helpers.get_glade('zeroconf_information_window.glade') + self.window = self.xml.get_widget('zeroconf_information_window') + + self.contact = contact + self.account = account + self.is_fake = is_fake + + # self.avatar_mime_type = None + # self.avatar_encoded = None + + self.fill_contact_page() + self.fill_personal_page() + + self.xml.signal_autoconnect(self) + self.window.show_all() + + def on_zeroconf_information_window_destroy(self, widget): + del gajim.interface.instances[self.account]['infos'][self.contact.jid] + + def on_zeroconf_information_window_key_press_event(self, widget, event): + if event.keyval == gtk.keysyms.Escape: + self.window.destroy() + + def on_log_history_checkbutton_toggled(self, widget): + #log conversation history? + oldlog = True + no_log_for = gajim.config.get_per('accounts', self.account, + 'no_log_for').split() + if self.contact.jid in no_log_for: + oldlog = False + log = widget.get_active() + if not log and not self.contact.jid in no_log_for: + no_log_for.append(self.contact.jid) + if log and self.contact.jid in no_log_for: + no_log_for.remove(self.contact.jid) + if oldlog != log: + gajim.config.set_per('accounts', self.account, 'no_log_for', + ' '.join(no_log_for)) + + def on_PHOTO_eventbox_button_press_event(self, widget, event): + '''If right-clicked, show popup''' + if event.button == 3: # right click + menu = gtk.Menu() + menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) + menuitem.connect('activate', + gtkgui_helpers.on_avatar_save_as_menuitem_activate, + self.contact.jid, self.account, self.contact.get_shown_name() + + '.jpeg') + menu.append(menuitem) + menu.connect('selection-done', lambda w:w.destroy()) + # show the menu + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + def set_value(self, entry_name, value): + try: + if value and entry_name == 'URL_label': + if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0): + widget = gtk.LinkButton(value, value) + else: + widget = gtk.Label(value) + widget.set_selectable(True) + table = self.xml.get_widget('personal_info_table') + table.attach(widget, 1, 4, 3, 4, yoptions = 0) + else: + self.xml.get_widget(entry_name).set_text(value) + except AttributeError: + pass + + def fill_status_label(self): + if self.xml.get_widget('information_notebook').get_n_pages() < 2: + return + contact_list = gajim.contacts.get_contact(self.account, self.contact.jid) + # stats holds show and status message + stats = '' + one = True # Are we adding the first line ? + if contact_list: + for c in contact_list: + if not one: + stats += '\n' + stats += helpers.get_uf_show(c.show) + if c.status: + stats += ': ' + c.status + if c.last_status_time: + stats += '\n' + _('since %s') % time.strftime('%c', + c.last_status_time).decode(locale.getpreferredencoding()) + one = False + else: # Maybe gc_vcard ? + stats = helpers.get_uf_show(self.contact.show) + if self.contact.status: + stats += ': ' + self.contact.status + status_label = self.xml.get_widget('status_label') + status_label.set_max_width_chars(15) + status_label.set_text(stats) + + tip = gtk.Tooltips() + status_label_eventbox = self.xml.get_widget('status_label_eventbox') + tip.set_tip(status_label_eventbox, stats) + + def fill_contact_page(self): + tooltips = gtk.Tooltips() + self.xml.get_widget('nickname_label').set_markup( + '<b><span size="x-large">' + + self.contact.get_shown_name() + + '</span></b>') + self.xml.get_widget('local_jid_label').set_text(self.contact.jid) + + log = True + if self.contact.jid in gajim.config.get_per('accounts', self.account, + 'no_log_for').split(' '): + log = False + checkbutton = self.xml.get_widget('log_history_checkbutton') + checkbutton.set_active(log) + checkbutton.connect('toggled', self.on_log_history_checkbutton_toggled) + + resources = '%s (%s)' % (self.contact.resource, unicode( + self.contact.priority)) + uf_resources = self.contact.resource + _(' resource with priority ')\ + + unicode(self.contact.priority) + if not self.contact.status: + self.contact.status = '' + + # Request list time status + # gajim.connections[self.account].request_last_status_time(self.contact.jid, + # self.contact.resource) + + self.xml.get_widget('resource_prio_label').set_text(resources) + resource_prio_label_eventbox = self.xml.get_widget( + 'resource_prio_label_eventbox') + tooltips.set_tip(resource_prio_label_eventbox, uf_resources) + + self.fill_status_label() + + # gajim.connections[self.account].request_vcard(self.contact.jid, self.is_fake) + + def fill_personal_page(self): + contact = gajim.connections[gajim.ZEROCONF_ACC_NAME].roster.getItem(self.contact.jid) + for key in ('1st', 'last', 'jid', 'email'): + if not contact['txt_dict'].has_key(key): + contact['txt_dict'][key] = '' + self.xml.get_widget('first_name_label').set_text(contact['txt_dict']['1st']) + self.xml.get_widget('last_name_label').set_text(contact['txt_dict']['last']) + self.xml.get_widget('jabber_id_label').set_text(contact['txt_dict']['jid']) + self.xml.get_widget('email_label').set_text(contact['txt_dict']['email']) + + def on_close_button_clicked(self, widget): + self.window.destroy()