diff options
28 files changed, 4425 insertions, 0 deletions
diff --git a/banner_tweaks/__init__.py b/banner_tweaks/__init__.py new file mode 100644 index 0000000..a328f68 --- /dev/null +++ b/banner_tweaks/__init__.py @@ -0,0 +1,2 @@ + +from plugin import BannerTweaksPlugin diff --git a/banner_tweaks/config_dialog.ui b/banner_tweaks/config_dialog.ui new file mode 100644 index 0000000..1994c1c --- /dev/null +++ b/banner_tweaks/config_dialog.ui @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkWindow" id="window1"> + <child> + <object class="GtkVBox" id="banner_tweaks_config_vbox"> + <property name="visible">True</property> + <property name="border_width">9</property> + <property name="orientation">vertical</property> + <property name="spacing">4</property> + <child> + <object class="GtkCheckButton" id="show_banner_image_checkbutton"> + <property name="label" translatable="yes">Display status icon</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, status icon will be displayed in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_banner_image_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_banner_online_msg_checkbutton"> + <property name="label" translatable="yes">Display status message of contact</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, status message of contact will be displayed in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_banner_online_msg_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_banner_resource_checkbutton"> + <property name="label" translatable="yes">Display resource name of contact</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, resource name of contact will be displayed in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_banner_resource_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="banner_small_fonts_checkbutton"> + <property name="label" translatable="yes">Use small fonts for contact name and resource name</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, smaller font will be used to display resource name and contact name in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_banner_small_fonts_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/banner_tweaks/manifest.ini b/banner_tweaks/manifest.ini new file mode 100644 index 0000000..4cc3652 --- /dev/null +++ b/banner_tweaks/manifest.ini @@ -0,0 +1,10 @@ +[info] +name: Banner Tweaks +short_name: banner_tweaks +version: 0.1 +description: Allows user to tweak chat window banner appearance (eg. make it compact). + Based on patch by pb in ticket #4133: + http://trac.gajim.org/attachment/ticket/4133. +authors = Mateusz Biliński <mateusz@bilinski.it> +homepage = http://blog.bilinski.it + diff --git a/banner_tweaks/plugin.py b/banner_tweaks/plugin.py new file mode 100644 index 0000000..6439dd5 --- /dev/null +++ b/banner_tweaks/plugin.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- + +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## + +''' +Adjustable chat window banner. + +Includes tweaks to make it compact. + +Based on patch by pb in ticket #4133: +http://trac.gajim.org/attachment/ticket/4133/gajim-chatbanneroptions-svn10008.patch + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 30 July 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +:license: GPL +''' + +import sys + +import gtk +import gobject +import message_control +from common import gajim +from common import helpers + +from plugins import GajimPlugin +from plugins.helpers import log, log_calls +from plugins.gui import GajimPluginConfigDialog + +class BannerTweaksPlugin(GajimPlugin): + + @log_calls('BannerTweaksPlugin') + def init(self): + self.description = _('Allows user to tweak chat window banner ' + 'appearance (eg. make it compact).\n' + 'Based on patch by pb in ticket #4133:\n' + 'http://trac.gajim.org/attachment/ticket/4133.') + self.config_dialog = BannerTweaksPluginConfigDialog(self) + + self.gui_extension_points = { + 'chat_control_base_draw_banner': (self.chat_control_base_draw_banner_called, + self.chat_control_base_draw_banner_deactivation) + } + + self.config_default_values = { + 'show_banner_image': (True, 'If True, Gajim will display a status icon in the banner of chat windows.'), + 'show_banner_online_msg': (True, 'If True, Gajim will display the status message of the contact in the banner of chat windows.'), + 'show_banner_resource': (False, 'If True, Gajim will display the resource name of the contact in the banner of chat windows.'), + 'banner_small_fonts': (False, 'If True, Gajim will use small fonts for contact name and resource name in the banner of chat windows.'), + 'old_chat_avatar_height': (52, 'chat_avatar_height value before plugin was activated'), + } + + @log_calls('BannerTweaksPlugin') + def activate(self): + self.config['old_chat_avatar_height'] = gajim.config.get('chat_avatar_height') + #gajim.config.set('chat_avatar_height', 28) + + @log_calls('BannerTweaksPlugin') + def deactivate(self): + gajim.config.set('chat_avatar_height', self.config['old_chat_avatar_height']) + + @log_calls('BannerTweaksPlugin') + def chat_control_base_draw_banner_called(self, chat_control): + if not self.config['show_banner_online_msg']: + chat_control.banner_status_label.hide() + chat_control.banner_status_label.set_no_show_all(True) + status_text = '' + chat_control.banner_status_label.set_markup(status_text) + + if not self.config['show_banner_image']: + if chat_control.TYPE_ID == message_control.TYPE_GC: + banner_status_img = chat_control.xml.get_object( + 'gc_banner_status_image') + else: + banner_status_img = chat_control.xml.get_object( + 'banner_status_image') + banner_status_img.clear() + + # TODO: part below repeats a lot of code from ChatControl.draw_banner_text() + # This could be rewritten using re module: getting markup text from + # banner_name_label and replacing some elements based on plugin config. + # Would it be faster? + if self.config['show_banner_resource'] or self.config['banner_small_fonts']: + banner_name_label = chat_control.xml.get_object('banner_name_label') + label_text = banner_name_label.get_label() + + contact = chat_control.contact + jid = contact.jid + + name = contact.get_shown_name() + if chat_control.resource: + name += '/' + chat_control.resource + elif contact.resource and self.config['show_banner_resource']: + name += '/' + contact.resource + + if chat_control.TYPE_ID == message_control.TYPE_PM: + name = _('%(nickname)s from group chat %(room_name)s') %\ + {'nickname': name, 'room_name': chat_control.room_name} + name = gobject.markup_escape_text(name) + + # 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 = '' + for account in gajim.contacts.get_accounts(): + if account == chat_control.account: + continue + if acct_info: # We already found a contact with same nick + break + for jid in gajim.contacts.get_jid_list(account): + other_contact_ = \ + gajim.contacts.get_first_contact_from_jid(account, jid) + if other_contact_.get_shown_name() == chat_control.contact.get_shown_name(): + acct_info = ' (%s)' % \ + gobject.markup_escape_text(chat_control.account) + break + + font_attrs, font_attrs_small = chat_control.get_font_attrs() + if self.config['banner_small_fonts']: + font_attrs = font_attrs_small + + st = gajim.config.get('displayed_chat_state_notifications') + cs = contact.chatstate + if cs and st in ('composing_only', 'all'): + if contact.show == 'offline': + chatstate = '' + elif st == 'all' or cs == 'composing': + chatstate = helpers.get_uf_chatstate(cs) + else: + chatstate = '' + + label_text = '<span %s>%s</span><span %s>%s %s</span>' % \ + (font_attrs, name, font_attrs_small, acct_info, chatstate) + else: + # weight="heavy" size="x-large" + label_text = '<span %s>%s</span><span %s>%s</span>' % \ + (font_attrs, name, font_attrs_small, acct_info) + + banner_name_label.set_markup(label_text) + + @log_calls('BannerTweaksPlugin') + def chat_control_base_draw_banner_deactivation(self, chat_control): + pass + #chat_control.draw_banner() + +class BannerTweaksPluginConfigDialog(GajimPluginConfigDialog): + def init(self): + self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path( + 'config_dialog.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain('gajim_plugins') + self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, + ['banner_tweaks_config_vbox']) + self.config_vbox = self.xml.get_object('banner_tweaks_config_vbox') + self.child.pack_start(self.config_vbox) + + self.show_banner_image_checkbutton = self.xml.get_object('show_banner_image_checkbutton') + self.show_banner_online_msg_checkbutton = self.xml.get_object('show_banner_online_msg_checkbutton') + self.show_banner_resource_checkbutton = self.xml.get_object('show_banner_resource_checkbutton') + self.banner_small_fonts_checkbutton = self.xml.get_object('banner_small_fonts_checkbutton') + + self.xml.connect_signals(self) + + def on_run(self): + self.show_banner_image_checkbutton.set_active(self.plugin.config['show_banner_image']) + self.show_banner_online_msg_checkbutton.set_active(self.plugin.config['show_banner_online_msg']) + self.show_banner_resource_checkbutton.set_active(self.plugin.config['show_banner_resource']) + self.banner_small_fonts_checkbutton.set_active(self.plugin.config['banner_small_fonts']) + + def on_show_banner_image_checkbutton_toggled(self, button): + self.plugin.config['show_banner_image'] = button.get_active() + + def on_show_banner_online_msg_checkbutton_toggled(self, button): + self.plugin.config['show_banner_online_msg'] = button.get_active() + + def on_show_banner_resource_checkbutton_toggled(self, button): + self.plugin.config['show_banner_resource'] = button.get_active() + + def on_banner_small_fonts_checkbutton_toggled(self, button): + self.plugin.config['banner_small_fonts'] = button.get_active() diff --git a/google_translation/__init__.py b/google_translation/__init__.py new file mode 100644 index 0000000..dc2c3bc --- /dev/null +++ b/google_translation/__init__.py @@ -0,0 +1 @@ +from plugin import GoogleTranslationPlugin diff --git a/google_translation/manifest.ini b/google_translation/manifest.ini new file mode 100644 index 0000000..2ad728b --- /dev/null +++ b/google_translation/manifest.ini @@ -0,0 +1,8 @@ +[info] +name: Google Translation +short_name: google_translation +version: 0.2 +description: Translates (currently only incoming) messages using Google Translate. +authors = Mateusz Biliński <mateusz@bilinski.it> +homepage = http://blog.bilinski.it + diff --git a/google_translation/plugin.py b/google_translation/plugin.py new file mode 100644 index 0000000..1aa5ed1 --- /dev/null +++ b/google_translation/plugin.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## +''' +Google Translation plugin. + +Translates (currently only incoming) messages using Google Translate. + +:note: consider this as proof-of-concept +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 25th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +:license: GPL +''' + +import json +import urllib2 +import HTMLParser +import gtk +from sys import getfilesystemencoding + +import chat_control +import groupchat_control + +from common import helpers +from common import gajim + +from plugins import GajimPlugin +from plugins.helpers import log_calls, log +from common import ged + +languages = { + _('Afrikaans'): 'af', + _('Albanian'): 'sq', + _('Armenian'): 'hy', + _('Azerbaijani'): 'az', + _('Arabic'): 'ar', + _('Basque'): 'eu', + _('Belarusian'): 'be', + _('Bulgarian'): 'bg', + _('Catalan'): 'ca', + _('Chinese (Simplified)'): 'zh-cn', + _('Chinese (Traditional)'): 'zh-tw', + _('Croatian'): 'hr', + _('Czech'): 'cs', + _('Danish'): 'da', + _('Dutch'): 'nl', + _('English'): 'en', + _('Estonian'): 'et', + _('Filipino'): 'tl', + _('Finnish'): 'fi', + _('French'): 'fr', + _('Galician'): 'gl', + _('Georgian'): 'ka', + _('German'): 'de', + _('Greek'): 'el', + _('Haitian Creole'): 'ht', + _('Hebrew'): 'iw', + _('Hindi'): 'hi', + _('Hungarian'): 'hu', + _('Icelandic'): 'is', + _('Indonesian'): 'id', + _('Italian'): 'it', + _('Irish'): 'da', + _('Japanese'): 'ja', + _('Korean'): 'ko', + _('Latvian'): 'lv', + _('Lithuanian'): 'lt', + _('Macedonian'): 'mk', + _('Malay'): 'ml', + _('Maltese'): 'mt', + _('Norwegian'): 'no', + _('Persian'): 'fa', + _('Polish'): 'pl', + _('Portuguese'): 'pt-BR', + _('Romanian'): 'ro', + _('Russian'): 'ru', + _('Serbian'): 'sr', + _('Slovak'): 'sk', + _('Slovenian'): 'sl', + _('Spanish'): 'es', + _('Swahili'): 'sw', + _('Swedish'): 'sv', + _('Thai'): 'th', + _('Turkish'): 'tr', + _('Ukrainian'): 'uk', + _('Urdu'): 'ur', + _('Vietnamese'): 'vi', + _('Welsh'): 'cy', + _('Yiddish'): 'yi', +} + +class GoogleTranslationPlugin(GajimPlugin): + + @log_calls('GoogleTranslationPlugin') + def init(self): + self.description = _('Translates (currently only incoming)' + 'messages using Google Translate.') + self.config_dialog = None + + self.config_default_values = { + 'per_jid_config': ({}, ''), + } + + self.events_handlers = {'decrypted-message-received': (ged.PREGUI, + self._nec_decrypted_message_received)} + + self.gui_extension_points = { + 'chat_control_base' : (self.connect_with_control, + self.disconnect_from_control), + } + + self.controls = [] + + @log_calls('GoogleTranslationPlugin') + def translate_text(self, account, text, from_lang, to_lang): + # Converts text so it can be used within URL as query to Google + # Translate. + quoted_text = urllib2.quote(text.encode(getfilesystemencoding())) + # prepare url + translation_url = u'https://ajax.googleapis.com/ajax/services/' \ + 'language/translate?q=%(quoted_text)s&' \ + 'langpair=%(from_lang)s%%7C%(to_lang)s&key=notsupplied&v=1.0' % \ + locals() + + results = helpers.download_image(account, {'src': translation_url})[0] + if not results: + return text + + result = json.loads(results) + + if result.get('responseStatus', '') != 200: + return text + + translated_text = result['responseData'].get('translatedText', '') + if translated_text: + try: + htmlparser = HTMLParser.HTMLParser() + translated_text = htmlparser.unescape(translated_text) + except Exception: + pass + return translated_text + return text + + @log_calls('GoogleTranslationPlugin') + def _nec_decrypted_message_received(self, obj): + if not obj.msgtxt: + return + if obj.jid not in self.config['per_jid_config']: + return + if not self.config['per_jid_config'][obj.jid]['enabled']: + return + from_lang = self.config['per_jid_config'][obj.jid]['from'] + to_lang = self.config['per_jid_config'][obj.jid]['to'] + translated_text = self.translate_text(obj.conn.name, obj.msgtxt, + from_lang, to_lang) + if translated_text: + obj.msgtxt = translated_text + '\n/' + _('Original text:') + '/ ' +\ + obj.msgtxt + + @log_calls('GoogleTranslationPlugin') + def activate(self): + pass + + @log_calls('GoogleTranslationPlugin') + def deactivate(self): + pass + + @log_calls('GoogleTranslationPlugin') + def connect_with_control(self, control): + base = Base(self, control) + self.controls.append(base) + + @log_calls('GoogleTranslationPlugin') + def disconnect_from_control(self, chat_control): + for base in self.controls: + base.disconnect_from_control() + self.controls = [] + +class Base(object): + def __init__(self, plugin, control): + self.plugin = plugin + self.control = control + self.contact = control.contact + self.account = control.account + self.jid = self.contact.jid + if self.jid in self.plugin.config['per_jid_config']: + self.config = self.plugin.config['per_jid_config'][self.jid] + else: + self.config = {'from': '', 'to': 'en', 'enabled': False} + self.create_buttons() + + def create_buttons(self): + if isinstance(self.control, chat_control.ChatControl): + vbox = self.control.xml.get_object('vbox106') + elif isinstance(self.control, groupchat_control.GroupchatControl): + vbox = self.control.xml.get_object('gc_textviews_vbox') + else: + return + + self.expander = gtk.Expander(_('Google translation')) + hbox = gtk.HBox(spacing=6) + self.expander.add(hbox) + label = gtk.Label(_('Translate from')) + hbox.pack_start(label, False, False) + liststore1 = gtk.ListStore(str, str) + liststore2 = gtk.ListStore(str, str) + cb1 = gtk.ComboBox(liststore1) + cb2 = gtk.ComboBox(liststore2) + cell = gtk.CellRendererText() + cb1.pack_start(cell, True) + cb1.add_attribute(cell, 'text', 0) + cell = gtk.CellRendererText() + cb2.pack_start(cell, True) + cb2.add_attribute(cell, 'text', 0) + #Language to translate from + liststore1.append([_('Auto'), '']) + if self.config['from'] == '': + cb1.set_active(0) + if self.config['from'] == '': + cb1.set_active(0) + i = 0 + ls = languages.items() + ls.sort() + for l in ls: + liststore1.append(l) + if l[1] == self.config['from']: + cb1.set_active(i+1) + liststore2.append(l) + if l[1] == self.config['to']: + cb2.set_active(i) + i += 1 + + hbox.pack_start(cb1, False, False) + label = gtk.Label(_('to')) + hbox.pack_start(label, False, False) + hbox.pack_start(cb2, False, False) + + cb = gtk.CheckButton(_('enable')) + if self.config['enabled']: + cb.set_active(True) + hbox.pack_start(cb, False, False) + vbox.pack_start(self.expander, False, False) + vbox.reorder_child(self.expander, 1) + + cb1.connect('changed', self.on_cb_changed, 'from') + cb2.connect('changed', self.on_cb_changed, 'to') + cb.connect('toggled', self.on_cb_toggled) + self.expander.show_all() + + def on_cb_changed(self, widget, option): + model = widget.get_model() + it = widget.get_active_iter() + self.config[option] = model[it][1] + self.plugin.config['per_jid_config'][self.jid] = self.config + self.plugin.config.save() + + def on_cb_toggled(self, widget): + self.config['enabled'] = widget.get_active() + self.plugin.config['per_jid_config'][self.jid] = self.config + self.plugin.config.save() + + def disconnect_from_control(self): + if isinstance(self.control, chat_control.ChatControl): + vbox = self.control.xml.get_object('vbox106') + elif isinstance(self.control, groupchat_control.GroupchatControl): + vbox = self.control.xml.get_object('gc_textviews_vbox') + else: + return + + vbox.remove(self.expander) diff --git a/length_notifier/__init__.py b/length_notifier/__init__.py new file mode 100644 index 0000000..67c8c61 --- /dev/null +++ b/length_notifier/__init__.py @@ -0,0 +1,2 @@ + +from length_notifier import LengthNotifierPlugin diff --git a/length_notifier/config_dialog.ui b/length_notifier/config_dialog.ui new file mode 100644 index 0000000..f06bfe1 --- /dev/null +++ b/length_notifier/config_dialog.ui @@ -0,0 +1,152 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkWindow" id="window1"> + <child> + <object class="GtkTable" id="length_notifier_config_table"> + <property name="visible">True</property> + <property name="border_width">9</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <property name="column_spacing">7</property> + <property name="row_spacing">5</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="tooltip_text" translatable="yes">Message length at which notification is invoked.</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Message length:</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="tooltip_text" translatable="yes">Background color of text entry field in chat window when notification is invoked.</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Notification color:</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="tooltip_text" translatable="yes">JabberIDs that plugin should be used with (eg. restrict only to one microblogging bot). Use comma (without space) as separator. If empty plugin is used with every JID. </property> + <property name="xalign">0</property> + <property name="label" translatable="yes">JabberIDs to include:</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="jids_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">JabberIDs that plugin should be used with (eg. restrict only to one microblogging bot). Use comma (without space) as separator. If empty plugin is used with every JID. </property> + <signal name="editing_done" handler="on_jids_entry_editing_done"/> + <signal name="changed" handler="on_jids_entry_changed"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <child> + <object class="GtkColorButton" id="notification_colorbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Background color of text entry field in chat window when notification is invoked.</property> + <property name="xalign">0</property> + <property name="title" translatable="yes">Pick a color for notification</property> + <property name="color">#000000000000</property> + <signal name="color_set" handler="on_notification_colorbutton_color_set"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <child> + <object class="GtkSpinButton" id="message_length_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">Message length at which notification is invoked.</property> + <property name="width_chars">6</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + <signal name="value_changed" handler="on_message_length_spinbutton_value_changed"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/length_notifier/length_notifier.py b/length_notifier/length_notifier.py new file mode 100644 index 0000000..3c9e9bc --- /dev/null +++ b/length_notifier/length_notifier.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## + +''' +Message length notifier plugin. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 1st June 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +:license: GPL +''' + +import sys + +import gtk + +from plugins import GajimPlugin +from plugins.helpers import log, log_calls +from plugins.gui import GajimPluginConfigDialog + +class LengthNotifierPlugin(GajimPlugin): + + @log_calls('LengthNotifierPlugin') + def init(self): + self.description = _('Highlights message entry field in chat window ' + 'when given length of message is exceeded.') + self.config_dialog = LengthNotifierPluginConfigDialog(self) + + self.gui_extension_points = { + 'chat_control' : (self.connect_with_chat_control, + self.disconnect_from_chat_control) + } + + self.config_default_values = { + 'MESSAGE_WARNING_LENGTH' : (140, 'Message length at which notification is invoked.'), + 'WARNING_COLOR' : ('#F0DB3E', 'Background color of text entry field in chat window when notification is invoked.'), + 'JIDS' : ([], 'JabberIDs that plugin should be used with (eg. restrict only to one microblogging bot). If empty plugin is used with every JID. [not implemented]') + } + + @log_calls('LengthNotifierPlugin') + def textview_length_warning(self, tb, chat_control): + tv = chat_control.msg_textview + d = chat_control.length_notifier_plugin_data + t = tb.get_text(tb.get_start_iter(), tb.get_end_iter()) + if t: + len_t = len(t) + #print("len_t: %d"%(len_t)) + if len_t>self.config['MESSAGE_WARNING_LENGTH']: + if not d['prev_color']: + d['prev_color'] = tv.style.copy().base[gtk.STATE_NORMAL] + tv.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(self.config['WARNING_COLOR'])) + elif d['prev_color']: + tv.modify_base(gtk.STATE_NORMAL, d['prev_color']) + d['prev_color'] = None + + @log_calls('LengthNotifierPlugin') + def connect_with_chat_control(self, chat_control): + jid = chat_control.contact.jid + if self.jid_is_ok(jid): + d = {'prev_color' : None} + tv = chat_control.msg_textview + tb = tv.get_buffer() + h_id = tb.connect('changed', self.textview_length_warning, chat_control) + d['h_id'] = h_id + + t = tb.get_text(tb.get_start_iter(), tb.get_end_iter()) + if t: + len_t = len(t) + if len_t>self.config['MESSAGE_WARNING_LENGTH']: + d['prev_color'] = tv.style.copy().base[gtk.STATE_NORMAL] + tv.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(self.config['WARNING_COLOR'])) + + chat_control.length_notifier_plugin_data = d + + return True + + return False + + @log_calls('LengthNotifierPlugin') + def disconnect_from_chat_control(self, chat_control): + try: + d = chat_control.length_notifier_plugin_data + tv = chat_control.msg_textview + tv.get_buffer().disconnect(d['h_id']) + if d['prev_color']: + tv.modify_base(gtk.STATE_NORMAL, d['prev_color']) + except AttributeError, error: + pass + #log.debug('Length Notifier Plugin was (probably) never connected with this chat window.\n Error: %s' % (error)) + + @log_calls('LengthNotifierPlugin') + def jid_is_ok(self, jid): + if jid in self.config['JIDS'] or not self.config['JIDS']: + return True + + return False + +class LengthNotifierPluginConfigDialog(GajimPluginConfigDialog): + def init(self): + self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path( + 'config_dialog.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain('gajim_plugins') + self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, + ['length_notifier_config_table']) + self.config_table = self.xml.get_object('length_notifier_config_table') + self.child.pack_start(self.config_table) + + self.message_length_spinbutton = self.xml.get_object( + 'message_length_spinbutton') + self.message_length_spinbutton.get_adjustment().set_all(140, 0, 500, 1, + 10, 0) + self.notification_colorbutton = self.xml.get_object( + 'notification_colorbutton') + self.jids_entry = self.xml.get_object('jids_entry') + + self.xml.connect_signals(self) + + def on_run(self): + self.message_length_spinbutton.set_value(self.plugin.config['MESSAGE_WARNING_LENGTH']) + self.notification_colorbutton.set_color(gtk.gdk.color_parse(self.plugin.config['WARNING_COLOR'])) + #self.jids_entry.set_text(self.plugin.config['JIDS']) + self.jids_entry.set_text(','.join(self.plugin.config['JIDS'])) + + @log_calls('LengthNotifierPluginConfigDialog') + def on_message_length_spinbutton_value_changed(self, spinbutton): + self.plugin.config['MESSAGE_WARNING_LENGTH'] = spinbutton.get_value() + + @log_calls('LengthNotifierPluginConfigDialog') + def on_notification_colorbutton_color_set(self, colorbutton): + self.plugin.config['WARNING_COLOR'] = colorbutton.get_color().to_string() + + @log_calls('LengthNotifierPluginConfigDialog') + def on_jids_entry_changed(self, entry): + text = entry.get_text() + if len(text)>0: + self.plugin.config['JIDS'] = entry.get_text().split(',') + else: + self.plugin.config['JIDS'] = [] + + @log_calls('LengthNotifierPluginConfigDialog') + def on_jids_entry_editing_done(self, entry): + pass diff --git a/length_notifier/manifest.ini b/length_notifier/manifest.ini new file mode 100644 index 0000000..d50feaa --- /dev/null +++ b/length_notifier/manifest.ini @@ -0,0 +1,9 @@ +[info] +name: Message Length Notifier +short_name: length_notifier +version: 0.1 +description: Highlights message entry field in chat window when given length of message is exceeded. +authors = Mateusz Biliński <mateusz@bilinski.it> +homepage = http://blog.bilinski.it + + diff --git a/plugin_installer/__init__.py b/plugin_installer/__init__.py new file mode 100644 index 0000000..a272f29 --- /dev/null +++ b/plugin_installer/__init__.py @@ -0,0 +1 @@ +from plugin_installer import PluginInstaller diff --git a/plugin_installer/config_dialog.ui b/plugin_installer/config_dialog.ui new file mode 100644 index 0000000..e637633 --- /dev/null +++ b/plugin_installer/config_dialog.ui @@ -0,0 +1,294 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkTextBuffer" id="textbuffer1"> + <property name="text">Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.</property> + </object> + <object class="GtkWindow" id="window1"> + <child> + <object class="GtkHPaned" id="hpaned2"> + <property name="width_request">600</property> + <property name="height_request">350</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="position">340</property> + <property name="position_set">True</property> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">6</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="available_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="search_column">1</property> + </object> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <property name="border_width">5</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="plugin_name_label1"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label"><empty></property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox8"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="label" translatable="yes">Authors:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="plugin_authors_label1"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="xpad">6</property> + <property name="label"><empty></property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox9"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="label" translatable="yes">Homepage:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLinkButton" id="plugin_homepage_linkbutton1"> + <property name="label">button</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + <property name="focus_on_click">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHBox" id="hbox10"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="label" translatable="yes">Description:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTextView" id="plugin_description_textview1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="pixels_above_lines">6</property> + <property name="wrap_mode">word</property> + <property name="left_margin">6</property> + <property name="right_margin">6</property> + <property name="indent">1</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox15"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox3"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="inslall_upgrade_button"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_inslall_upgrade_clicked"/> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-refresh</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Install/Upgrade</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="window2"> + <child> + <object class="GtkHBox" id="hbox111"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Ftp server:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="ftp_server"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/plugin_installer/manifest.ini b/plugin_installer/manifest.ini new file mode 100644 index 0000000..7a15081 --- /dev/null +++ b/plugin_installer/manifest.ini @@ -0,0 +1,8 @@ +[info] +name: Plugin Installer +short_name: plugin_installer +version: 0.5 +description: Install and upgrade plugins from ftp +authors: Denis Fomin <fominde@gmail.com> + Yann Leboulanger <asterix@lagaule.org> +homepage: http://www.gajim.org/ diff --git a/plugin_installer/plugin_installer.py b/plugin_installer/plugin_installer.py new file mode 100644 index 0000000..1fe1314 --- /dev/null +++ b/plugin_installer/plugin_installer.py @@ -0,0 +1,532 @@ +# -*- coding: utf-8 -*- +# +## plugins/plugin_installer/plugin_installer.py +## +## Copyright (C) 2010-2011 Denis Fomin <fominde AT gmail.com> +## Copyright (C) 2011 Yann Leboulanger <asterix AT lagaule.org> +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## +import gtk +import pango +import gobject +import ftplib +import io +import threading +import ConfigParser +import os +import fnmatch +import sys + +from common import gajim +from plugins import GajimPlugin +from plugins.helpers import log_calls, log +from dialogs import WarningDialog, HigDialog, YesNoDialog +from plugins.gui import GajimPluginConfigDialog + + +def convert_version_to_list(version_str): + version_list = version_str.split('.') + l = [] + while len(version_list): + l.append(int(version_list.pop(0))) + return l + +class PluginInstaller(GajimPlugin): + + @log_calls('PluginInstallerPlugin') + def init(self): + self.description = _('Install and upgrade plugins from ftp') + self.config_dialog = PluginInstallerPluginConfigDialog(self) + self.config_default_values = {'ftp_server': ('ftp.gajim.org', '')} + self.window = None + self.progressbar = None + self.available_plugins_model = None + self.upgrading = False # True when opened from upgrade popup dialog + + @log_calls('PluginInstallerPlugin') + def activate(self): + self.pl_menuitem = gajim.interface.roster.xml.get_object( + 'plugins_menuitem') + self.id_ = self.pl_menuitem.connect_after('activate', self.on_activate) + if 'plugins' in gajim.interface.instances: + self.on_activate(None) + gobject.timeout_add_seconds(30, self.check_update) + + @log_calls('PluginInstallerPlugin') + def warn_update(self, plugins): + def open_update(dummy): + self.upgrading = True + self.pl_menuitem.activate() + nb = gajim.interface.instances['plugins'].plugins_notebook + gobject.idle_add(nb.set_current_page, 1) + if plugins: + plugins_str = '\n'.join(plugins) + YesNoDialog(_('Plugins updates'), _('Some updates are available for' + ' your installer plugins. Do you want to update those plugins:' + '\n%s') % plugins_str, on_response_yes=open_update) + + @log_calls('PluginInstallerPlugin') + def check_update(self): + def _run(): + try: + to_update = [] + con = ftplib.FTP_TLS(ftp.server) + con.login() + con.prot_p() + con.cwd('plugins') + plugins_dirs = con.nlst() + for dir_ in plugins_dirs: + try: + con.retrbinary('RETR %s/manifest.ini' % dir_, + ftp.handleDownload) + except Exception, error: + if str(error).startswith('550'): + continue + ftp.config.readfp(io.BytesIO(ftp.buffer_.getvalue())) + local_version = ftp.get_plugin_version(ftp.config.get( + 'info', 'name')) + if local_version: + local = convert_version_to_list(local_version) + remote = convert_version_to_list(ftp.config.get('info', + 'version')) + if remote > local: + to_update.append(ftp.config.get('info', 'name')) + con.quit() + gobject.idle_add(self.warn_update, to_update) + except Exception, e: + log.debug('Ftp error when check updates: %s' % str(e)) + ftp = Ftp(self) + ftp.run = _run + ftp.start() + + @log_calls('PluginInstallerPlugin') + def deactivate(self): + self.pl_menuitem.disconnect(self.id_) + if hasattr(self, 'page_num'): + self.notebook.remove_page(self.page_num) + self.notebook.set_current_page(0) + if hasattr(self, 'ftp'): + del self.ftp + + def on_activate(self, widget): + if 'plugins' not in gajim.interface.instances: + return + self.installed_plugins_model = gajim.interface.instances[ + 'plugins'].installed_plugins_model + self.notebook = gajim.interface.instances['plugins'].plugins_notebook + self.id_n = self.notebook.connect('switch-page', + self.on_notebook_switch_page) + self.window = gajim.interface.instances['plugins'].window + self.window.connect('destroy', self.on_win_destroy) + self.GTK_BUILDER_FILE_PATH = self.local_file_path('config_dialog.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain('gajim_plugins') + self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['hpaned2']) + hpaned = self.xml.get_object('hpaned2') + self.page_num = self.notebook.append_page(hpaned, + gtk.Label(_('Available'))) + + widgets_to_extract = ('plugin_name_label1', + 'available_treeview', 'progressbar', 'inslall_upgrade_button', + 'plugin_authors_label1', 'plugin_authors_label1', + 'plugin_homepage_linkbutton1', 'plugin_description_textview1') + + for widget_name in widgets_to_extract: + setattr(self, widget_name, self.xml.get_object(widget_name)) + + attr_list = pango.AttrList() + attr_list.insert(pango.AttrWeight(pango.WEIGHT_BOLD, 0, -1)) + self.plugin_name_label1.set_attributes(attr_list) + + self.available_plugins_model = gtk.ListStore(gobject.TYPE_PYOBJECT, + gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN, gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT) + self.available_treeview.set_model(self.available_plugins_model) + + self.progressbar.set_property('no-show-all', True) + renderer = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Plugin'), renderer, text=1) + col.set_resizable(True) + col.set_property('expand', True) + col.set_sizing(gtk.TREE_VIEW_COLUMN_GROW_ONLY) + self.available_treeview.append_column(col) + col = gtk.TreeViewColumn(_('Installed\nversion'), renderer, text=2) + self.available_treeview.append_column(col) + col = gtk.TreeViewColumn(_('Available\nversion'), renderer, text=3) + col.set_property('expand', False) + self.available_treeview.append_column(col) + + renderer = gtk.CellRendererToggle() + renderer.set_property('activatable', True) + renderer.connect('toggled', self.available_plugins_toggled_cb) + col = gtk.TreeViewColumn(_('Install /\nUpgrade'), renderer, active=4) + self.available_treeview.append_column(col) + + if gobject.signal_lookup('error_signal', self.window) is 0: + gobject.signal_new('error_signal', self.window, + gobject.SIGNAL_RUN_LAST, gobject.TYPE_STRING, + (gobject.TYPE_STRING,)) + gobject.signal_new('plugin_downloaded', self.window, + gobject.SIGNAL_RUN_LAST, gobject.TYPE_STRING, + (gobject.TYPE_PYOBJECT,)) + self.window.connect('error_signal', self.on_some_ftp_error) + self.window.connect('plugin_downloaded', self.on_plugin_downloaded) + + selection = self.available_treeview.get_selection() + selection.connect('changed', + self.available_plugins_treeview_selection_changed) + selection.set_mode(gtk.SELECTION_SINGLE) + + self._clear_available_plugin_info() + self.xml.connect_signals(self) + self.window.show_all() + + def on_win_destroy(self, widget): + if hasattr(self, 'ftp'): + del self.ftp + + def available_plugins_toggled_cb(self, cell, path): + is_active = self.available_plugins_model[path][4] + self.available_plugins_model[path][4] = not is_active + dir_list = [] + for i in xrange(len(self.available_plugins_model)): + if self.available_plugins_model[i][4]: + dir_list.append(self.available_plugins_model[i][0]) + if not dir_list: + self.inslall_upgrade_button.set_property('sensitive', False) + else: + self.inslall_upgrade_button.set_property('sensitive', True) + + def on_notebook_switch_page(self, widget, page, page_num): + if not hasattr(self, 'ftp') and self.page_num == page_num: + self.available_plugins_model.clear() + self.progressbar.show() + self.ftp = Ftp(self) + self.ftp.remote_dirs = None + self.ftp.upgrading = True + self.ftp.start() + + def on_inslall_upgrade_clicked(self, widget): + self.inslall_upgrade_button.set_property('sensitive', False) + dir_list = [] + for i in xrange(len(self.available_plugins_model)): + if self.available_plugins_model[i][4]: + dir_list.append(self.available_plugins_model[i][0]) + + ftp = Ftp(self) + ftp.remote_dirs = dir_list + ftp.start() + + def on_some_ftp_error(self, widget, error_text): + for i in xrange(len(self.available_plugins_model)): + self.available_plugins_model[i][4] = False + self.progressbar.hide() + WarningDialog(_('Ftp error'), error_text, self.window) + + def on_plugin_downloaded(self, widget, plugin_dirs): + for _dir in plugin_dirs: + is_active = False + plugins = None + plugin_dir = os.path.join(gajim.PLUGINS_DIRS[1], _dir) + plugin = gajim.plugin_manager.get_plugin_by_path(plugin_dir) + if plugin: + if plugin.active and plugin.name != self.name: + is_active = True + gobject.idle_add(gajim.plugin_manager.deactivate_plugin, + plugin) + gajim.plugin_manager.plugins.remove(plugin) + + model = self.installed_plugins_model + for row in xrange(len(model)): + if plugin == model[row][0]: + model.remove(model.get_iter((row, 0))) + break + + plugins = self.scan_dir_for_plugin(plugin_dir) + if not plugins: + continue + gajim.plugin_manager.add_plugin(plugins[0]) + plugin = gajim.plugin_manager.plugins[-1] + for row in xrange(len(self.available_plugins_model)): + if plugin.name == self.available_plugins_model[row][1]: + self.available_plugins_model[row][2] = plugin.version + self.available_plugins_model[row][4] = False + continue + if is_active and plugin.name != self.name: + gobject.idle_add(gajim.plugin_manager.activate_plugin, plugin) + if plugin.name != 'Plugin Installer': + self.installed_plugins_model.append([plugin, plugin.name, + is_active]) + dialog = HigDialog(None, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, + '', _('All selected plugins downloaded')) + dialog.set_modal(False) + dialog.set_transient_for(self.window) + dialog.popup() + + def available_plugins_treeview_selection_changed(self, treeview_selection): + model, iter = treeview_selection.get_selected() + if iter: + self.plugin_name_label1.set_text(model.get_value(iter, 1)) + self.plugin_authors_label1.set_text(model.get_value(iter, 6)) + self.plugin_homepage_linkbutton1.set_uri(model.get_value(iter, 7)) + self.plugin_homepage_linkbutton1.set_label(model.get_value(iter, 7)) + label = self.plugin_homepage_linkbutton1.get_children()[0] + label.set_ellipsize(pango.ELLIPSIZE_END) + self.plugin_homepage_linkbutton1.set_property('sensitive', True) + desc_textbuffer = self.plugin_description_textview1.get_buffer() + desc_textbuffer.set_text(_(model.get_value(iter, 5))) + self.plugin_description_textview1.set_property('sensitive', True) + else: + self._clear_available_plugin_info() + + def _clear_available_plugin_info(self): + self.plugin_name_label1.set_text('') + self.plugin_authors_label1.set_text('') + self.plugin_homepage_linkbutton1.set_uri('') + self.plugin_homepage_linkbutton1.set_label('') + self.plugin_homepage_linkbutton1.set_property('sensitive', False) + + desc_textbuffer = self.plugin_description_textview1.get_buffer() + desc_textbuffer.set_text('') + self.plugin_description_textview1.set_property('sensitive', False) + + def scan_dir_for_plugin(self, path): + plugins_found = [] + conf = ConfigParser.ConfigParser() + fields = ('name', 'short_name', 'version', 'description', 'authors', + 'homepage') + if not os.path.isdir(path): + return plugins_found + + dir_list = os.listdir(path) + dir_, mod = os.path.split(path) + sys.path.insert(0, dir_) + + manifest_path = os.path.join(path, 'manifest.ini') + if not os.path.isfile(manifest_path): + return plugins_found + + for elem_name in dir_list: + file_path = os.path.join(path, elem_name) + module = None + + if os.path.isfile(file_path) and fnmatch.fnmatch(file_path, '*.py'): + module_name = os.path.splitext(elem_name)[0] + if module_name == '__init__': + continue + try: + module = __import__('%s.%s' % (mod, module_name)) + except ValueError, value_error: + pass + except ImportError, import_error: + pass + except AttributeError, attribute_error: + pass + if module is None: + continue + + for module_attr_name in [attr_name for attr_name in dir(module) + if not (attr_name.startswith('__') or attr_name.endswith('__'))]: + module_attr = getattr(module, module_attr_name) + try: + if not issubclass(module_attr, GajimPlugin) or \ + module_attr is GajimPlugin: + continue + module_attr.__path__ = os.path.abspath(os.path.dirname( + file_path)) + + # read metadata from manifest.ini + conf.readfp(open(manifest_path, 'r')) + for option in fields: + if conf.get('info', option) is '': + raise ConfigParser.NoOptionError, 'field empty' + setattr(module_attr, option, conf.get('info', option)) + conf.remove_section('info') + plugins_found.append(module_attr) + + except TypeError, type_error: + pass + except ConfigParser.NoOptionError, type_error: + # all fields are required + pass + return plugins_found + + +class Ftp(threading.Thread): + def __init__(self, plugin): + super(Ftp, self).__init__() + self.plugin = plugin + self.window = plugin.window + self.server = plugin.config['ftp_server'] + self.progressbar = plugin.progressbar + self.model = plugin.available_plugins_model + self.config = ConfigParser.ConfigParser() + self.buffer_ = io.BytesIO() + self.remote_dirs = None + self.append_to_model = True + self.upgrading = False + + def model_append(self, row): + self.model.append(row) + return False + + def progressbar_pulse(self): + self.progressbar.pulse() + return True + + def get_plugin_version(self, plugin_name): + for plugin in gajim.plugin_manager.plugins: + if plugin.name == plugin_name: + return plugin.version + + def run(self): + try: + gobject.idle_add(self.progressbar.set_text, + _('Connecting to server')) + self.ftp = ftplib.FTP_TLS(self.server) + self.ftp.login() + self.ftp.prot_p() + self.ftp.cwd('plugins') + if not self.remote_dirs: + self.plugins_dirs = self.ftp.nlst() + progress_step = 1.0 / len(self.plugins_dirs) + gobject.idle_add(self.progressbar.set_text, + _('Scan files on the server')) + for dir_ in self.plugins_dirs: + fract = self.progressbar.get_fraction() + progress_step + gobject.idle_add(self.progressbar.set_fraction, fract) + gobject.idle_add(self.progressbar.set_text, + _('Read "%s"') % dir_) + try: + self.ftp.retrbinary('RETR %s/manifest.ini' % dir_, + self.handleDownload) + except Exception, error: + if str(error).startswith('550'): + continue + self.config.readfp(io.BytesIO(self.buffer_.getvalue())) + local_version = self.get_plugin_version( + self.config.get('info', 'name')) + upgrade = False + if self.upgrading and local_version: + local = convert_version_to_list(local_version) + remote = convert_version_to_list(self.config.get('info', + 'version')) + if remote > local: + upgrade = True + gobject.idle_add( + self.plugin.inslall_upgrade_button.set_property, + 'sensitive', True) + gobject.idle_add(self.model_append, [dir_, + self.config.get('info', 'name'), local_version, + self.config.get('info', 'version'), upgrade, + self.config.get('info', 'description'), + self.config.get('info', 'authors'), + self.config.get('info', 'homepage'), ]) + self.plugins_dirs = None + self.ftp.quit() + gobject.idle_add(self.progressbar.set_fraction, 0) + if self.remote_dirs: + self.download_plugin() + gobject.idle_add(self.progressbar.hide) + except Exception, e: + self.window.emit('error_signal', str(e)) + + def handleDownload(self, block): + self.buffer_.write(block) + + def download_plugin(self): + gobject.idle_add(self.progressbar.show) + self.pulse = gobject.timeout_add(150, self.progressbar_pulse) + gobject.idle_add(self.progressbar.set_text, _('Create a list of files')) + for remote_dir in self.remote_dirs: + + def nlstr(dir_, subdir=None): + if subdir: + dir_ = dir_ + '/' + subdir + list_ = self.ftp.nlst(dir_) + for i in list_: + name = i.split('/')[-1] + if '.' not in name: + try: + if i == self.ftp.nlst(i)[0]: + files.append(i[1:]) + del dirs[i[1:]] + except Exception, e: + # empty dir or file + continue + dirs.append(i[1:]) + subdirs = name + nlstr(dir_, subdirs) + else: + files.append(i[1:]) + dirs, files = [], [] + nlstr('/plugins/' + remote_dir) + + if not os.path.isdir(gajim.PLUGINS_DIRS[1]): + os.mkdir(gajim.PLUGINS_DIRS[1]) + local_dir = ld = os.path.join(gajim.PLUGINS_DIRS[1], remote_dir) + if not os.path.isdir(local_dir): + os.mkdir(local_dir) + local_dir = os.path.split(gajim.PLUGINS_DIRS[1])[0] + + # creating dirs + for dir_ in dirs: + try: + os.mkdir(os.path.join(local_dir, dir_)) + except OSError, e: + if str(e).startswith('[Errno 17]'): + continue + raise + + # downloading files + for filename in files: + gobject.idle_add(self.progressbar.set_text, + _('Downloading "%s"') % filename) + full_filename = os.path.join(local_dir, filename) + try: + self.ftp.retrbinary('RETR /%s' % filename, + open(full_filename, 'wb').write) + #full_filename.close() + except ftplib.error_perm: + print 'ERROR: cannot read file "%s"' % filename + os.unlink(filename) + self.ftp.quit() + gobject.idle_add(self.window.emit, 'plugin_downloaded', + self.remote_dirs) + gobject.source_remove(self.pulse) + + +class PluginInstallerPluginConfigDialog(GajimPluginConfigDialog): + def init(self): + self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path( + 'config_dialog.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain('gajim_plugins') + self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['hbox111']) + hbox = self.xml.get_object('hbox111') + self.child.pack_start(hbox) + + self.xml.connect_signals(self) + self.connect('hide', self.on_hide) + + def on_run(self): + widget = self.xml.get_object('ftp_server') + widget.set_text(str(self.plugin.config['ftp_server'])) + + def on_hide(self, widget): + widget = self.xml.get_object('ftp_server') + self.plugin.config['ftp_server'] = widget.get_text() diff --git a/triggers/__init__.py b/triggers/__init__.py new file mode 100644 index 0000000..4f8d9f8 --- /dev/null +++ b/triggers/__init__.py @@ -0,0 +1 @@ +from triggers import Triggers diff --git a/triggers/config_dialog.ui b/triggers/config_dialog.ui new file mode 100644 index 0000000..ddc10c0 --- /dev/null +++ b/triggers/config_dialog.ui @@ -0,0 +1,910 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkListStore" id="liststore1"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">contact(s)</col> + </row> + <row> + <col id="0" translatable="yes">group(s)</col> + </row> + <row> + <col id="0" translatable="yes">everybody</col> + </row> + </data> + </object> + <object class="GtkListStore" id="liststore2"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Receive a Message</col> + </row> + <row> + <col id="0" translatable="yes">Contact Connects</col> + </row> + <row> + <col id="0" translatable="yes">Contact Disconnects</col> + </row> + <row> + <col id="0" translatable="yes">Contact Changes Status</col> + </row> + </data> + </object> + <object class="GtkWindow" id="advanced_notifications_window"> + <property name="border_width">6</property> + <property name="title" translatable="yes">Advanced Notifications Control</property> + <property name="role">Advanced Notifications Control</property> + <property name="resizable">False</property> + <property name="destroy_with_parent">True</property> + <child> + <object class="GtkVBox" id="vbox"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkVBox" id="vbox100"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow23"> + <property name="height_request">90</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="conditions_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <signal name="cursor_changed" handler="on_conditions_treeview_cursor_changed"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment99"> + <property name="visible">True</property> + <property name="xalign">1</property> + <property name="left_padding">212</property> + <child> + <object class="GtkHBox" id="hbox3045"> + <property name="visible">True</property> + <child> + <object class="GtkHButtonBox" id="hbuttonbox2"> + <property name="visible">True</property> + <property name="spacing">10</property> + <child> + <object class="GtkButton" id="new_button"> + <property name="label">gtk-new</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_new_button_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="up_button"> + <property name="label">gtk-go-up</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_up_button_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="down_button"> + <property name="label">gtk-go-down</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_down_button_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="delete_button"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_delete_button_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="config_vbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label391"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Conditions</b></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox101"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkHBox" id="hbox3042"> + <property name="visible">True</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label401"> + <property name="visible">True</property> + <property name="label" translatable="yes">When </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="event_combobox"> + <property name="visible">True</property> + <property name="model">liststore2</property> + <signal name="changed" handler="on_event_combobox_changed"/> + <child> + <object class="GtkCellRendererText" id="cellrenderertext2"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3048"> + <property name="visible">True</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label400"> + <property name="visible">True</property> + <property name="label" translatable="yes">for </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="recipient_type_combobox"> + <property name="visible">True</property> + <property name="model">liststore1</property> + <signal name="changed" handler="on_recipient_type_combobox_changed"/> + <child> + <object class="GtkCellRendererText" id="cellrenderertext1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="recipient_list_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="no_show_all">True</property> + <property name="tooltip_text" translatable="yes">comma separated list</property> + <signal name="changed" handler="on_recipient_list_entry_changed"/> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3049"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label402"> + <property name="visible">True</property> + <property name="label" translatable="yes">when I'm in</property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="status_hbox"> + <property name="visible">True</property> + <property name="spacing">3</property> + <child> + <object class="GtkRadioButton" id="all_status_rb"> + <property name="label" translatable="yes">All statuses</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_status_radiobutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="special_status_rb"> + <property name="label" translatable="yes">One or more special statuses...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">all_status_rb</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="online_cb"> + <property name="label" translatable="yes">Online / Free For Chat</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_status_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="away_cb"> + <property name="label" translatable="yes">Away</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_status_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="xa_cb"> + <property name="label" translatable="yes">Not Available</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_status_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="dnd_cb"> + <property name="label" translatable="yes">Busy </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_status_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="invisible_cb"> + <property name="label" translatable="yes">Invisible</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_status_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">6</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3053"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label408"> + <property name="visible">True</property> + <property name="label" translatable="yes">and I </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="tab_opened_cb"> + <property name="label" translatable="yes">Have </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_tab_opened_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="not_tab_opened_cb"> + <property name="label" translatable="yes">Don't have </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_not_tab_opened_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label409"> + <property name="visible">True</property> + <property name="label" translatable="yes"> a window/tab opened with that contact </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label392"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Actions</b></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame35"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkHBox" id="hbox3027"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="use_popup_cb"> + <property name="label" translatable="yes">_Inform me with a popup window</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_use_popup_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable_popup_cb"> + <property name="label" translatable="yes">_Disable existing popup window</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_disable_popup_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkFrame" id="frame38"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment93"> + <property name="visible">True</property> + <property name="border_width">6</property> + <property name="left_padding">12</property> + <child> + <object class="GtkVBox" id="vbox98"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkHBox" id="hbox3028"> + <property name="visible">True</property> + <property name="spacing">6</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkCheckButton" id="use_sound_cb"> + <property name="label" translatable="yes">Play a sound</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_use_sound_cb_toggled"/> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="sound_file_hbox"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkEntry" id="sound_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <signal name="changed" handler="on_sound_entry_changed"/> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button4"> + <property name="label">...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_browse_for_sounds_button_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="play_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <signal name="clicked" handler="on_play_button_clicked"/> + <child> + <object class="GtkImage" id="image1372"> + <property name="visible">True</property> + <property name="stock">gtk-media-play</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable_sound_cb"> + <property name="label" translatable="yes">_Disable existing sound for this event</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_disable_sound_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label394"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Sounds</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3032"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="use_auto_open_cb"> + <property name="label" translatable="yes">_Open chat window with user</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_use_auto_open_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable_auto_open_cb"> + <property name="label" translatable="yes">_Disable auto opening chat window</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_disable_auto_open_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkExpander" id="expander1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="expanded">True</property> + <child> + <object class="GtkVBox" id="vbox99"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkHBox" id="hbox3033"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="run_command_cb"> + <property name="label" translatable="yes">Launch a command</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_run_command_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="command_entry"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <signal name="changed" handler="on_command_entry_changed"/> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3035"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="use_systray_cb"> + <property name="label" translatable="yes">_Show event in notification area</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_use_systray_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable_systray_cb"> + <property name="label" translatable="yes">_Disable showing event in notification area</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_disable_systray_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3052"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="use_roster_cb"> + <property name="label" translatable="yes">_Show event in roster</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_use_roster_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable_roster_cb"> + <property name="label" translatable="yes">_Disable showing event in roster</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_disable_roster_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="use_urgency_hint_cb"> + <property name="label" translatable="yes">_Activate window manager's UrgencyHint to make chat window in taskbar flash</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_use_urgency_hint_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable_urgency_hint_cb"> + <property name="label" translatable="yes">_Deactivate window manager's UrgencyHint</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="no_show_all">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_disable_urgency_hint_cb_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label395"> + <property name="visible">True</property> + <property name="label" translatable="yes">Advanced Actions</property> + </object> + </child> + </object> + <packing> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/triggers/manifest.ini b/triggers/manifest.ini new file mode 100644 index 0000000..173bd10 --- /dev/null +++ b/triggers/manifest.ini @@ -0,0 +1,7 @@ +[info] +name: Triggers +short_name: triggers +version: 0.0.3 +description: Configure Gajim's behaviour for each contact +authors: Yann Leboulanger <asterix@lagaule.org> +homepage: http://trac.gajim.org/wiki/ diff --git a/triggers/triggers.py b/triggers/triggers.py new file mode 100644 index 0000000..1cfdd97 --- /dev/null +++ b/triggers/triggers.py @@ -0,0 +1,677 @@ +# -*- coding: utf-8 -*- +# +## plugins/triggers/triggers.py +## +## Copyright (C) 2011 Yann Leboulanger <asterix AT lagaule.org> +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## + +import gtk +import sys + +from common import gajim +from plugins import GajimPlugin +from plugins.helpers import log_calls +from plugins.gui import GajimPluginConfigDialog +from common import ged +from common import helpers + + +class Triggers(GajimPlugin): + + @log_calls('TriggersPlugin') + def init(self): + self.description = _('Configure Gajim\'s behaviour for each contact') + self.config_dialog = TriggersPluginConfigDialog(self) + self.config_default_values = {} + + self.events_handlers = {'notification': (ged.PREGUI, self._nec_notif), + 'decrypted-message-received': (ged.PREGUI2, + self._nec_decrypted_message_received), + 'presence-received': (ged.PREGUI, self._nec_presence_received)} + + def _check_rule_recipients(self, obj, rule): + rule_recipients = [t.strip() for t in rule['recipients'].split(',')] + if rule['recipient_type'] == 'contact' and obj.jid not in \ + rule_recipients: + return False + contact = gajim.contacts.get_first_contact_from_jid(obj.conn.name, obj.jid) + if not contact: # PM? + return False + contact_groups = contact.groups + group_found = False + for group in contact_groups: + if group in rule_recipients: + group_found = True + break + if rule['recipient_type'] == 'group' and not group_found: + return False + + return True + + def _check_rule_status(self, obj, rule): + rule_statuses = rule['status'].split() + our_status = gajim.SHOW_LIST[obj.conn.connected] + if rule['status'] != 'all' and our_status not in rule_statuses: + return False + + return True + + def _check_rule_tab_opened(self, obj, rule): + if rule['tab_opened'] == 'both': + return True + tab_opened = False + if gajim.interface.msg_win_mgr.get_control(obj.jid, obj.conn.name): + tab_opened = True + if tab_opened and rule['tab_opened'] == 'no': + return False + elif not tab_opened and rule['tab_opened'] == 'yes': + return False + + return True + + def check_rule_all(self, event, obj, rule): + # Check notification type + if rule['event'] != event: + return False + + # notification type is ok. Now check recipient + if not self._check_rule_recipients(obj, rule): + return False + + # recipient is ok. Now check our status + if not self._check_rule_status(obj, rule): + return False + + # our_status is ok. Now check opened chat window + if not self._check_rule_tab_opened(obj, rule): + return False + + # All is ok + return True + + def check_rule_apply_notif(self, obj, rule): + # Check notification type + notif_type = '' + if obj.notif_type == 'msg': + notif_type = 'message_received' + elif obj.notif_type == 'pres': + if obj.base_event.old_show < 2 and obj.base_event.new_show > 1: + notif_type = 'contact_connected' + elif obj.base_event.old_show > 1 and obj.base_event.new_show < 2: + notif_type = 'contact_disconnected' + else: + notif_type = 'contact_status_change' + + return self.check_rule_all(notif_type, obj, rule) + + def check_rule_apply_decrypted_msg(self, obj, rule): + return self.check_rule_all('message_received', obj, rule) + + def check_rule_apply_connected(self, obj, rule): + return self.check_rule_all('contact_connected', obj, rule) + + def check_rule_apply_disconnected(self, obj, rule): + return self.check_rule_all('contact_disconnected', obj, rule) + + def check_rule_apply_status_changed(self, obj, rule): + return self.check_rule_all('contact_status_change', obj, rule) + + def apply_rule_notif(self, obj, rule): + if rule['sound'] == 'no': + obj.do_sound = False + elif rule['sound'] == 'yes': + obj.do_sound = True + obj.sound_event = '' + obj.sound_file = rule['sound_file'] + + if rule['popup'] == 'no': + obj.do_popup = False + elif rule['popup'] == 'yes': + obj.do_popup = True + + if rule['run_command']: + obj.do_command = True + obj.command = rule['command'] + else: + obj.do_command = False + + if rule['systray'] == 'no': + obj.show_in_notification_area = False + elif rule['systray'] == 'yes': + obj.show_in_notification_area = True + + if rule['roster'] == 'no': + obj.show_in_roster = False + elif rule['roster'] == 'yes': + obj.show_in_roster = True + +# if rule['urgency_hint'] == 'no': +# ?? not in obj actions +# elif rule['urgency_hint'] == 'yes': + + def apply_rule_decrypted_message(self, obj, rule): + if rule['auto_open'] == 'no': + obj.popup = False + elif rule['auto_open'] == 'yes': + obj.popup = True + + def apply_rule_presence(self, obj, rule): + if rule['auto_open'] == 'no': + obj.popup = False + elif rule['auto_open'] == 'yes': + obj.popup = True + + def _nec_all(self, obj, check_func, apply_func): + # check rules in order + rules_num = [int(i) for i in self.config.keys()] + rules_num.sort() + for num in rules_num: + rule = self.config[str(num)] + if check_func(obj, rule): + apply_func(obj, rule) + # Should we stop after first valid rule ? + # break + + def _nec_notif(self, obj): + self._nec_all(obj, self.check_rule_apply_notif, self.apply_rule_notif) + + def _nec_decrypted_message_received(self, obj): + self._nec_all(obj, self.check_rule_apply_decrypted_msg, + self.apply_rule_decrypted_message) + + def _nec_presence_received(self, obj): + if obj.old_show < 2 and obj.new_show > 1: + check_func = self.check_rule_apply_connected + elif obj.old_show > 1 and obj.new_show < 2: + check_func = self.check_rule_apply_disconnected + else: + check_func = self.check_rule_apply_status_changed + self._nec_all(obj, check_func, self.apply_rule_presence) + + +class TriggersPluginConfigDialog(GajimPluginConfigDialog): + # {event: widgets_to_disable, } + events_list = { + 'message_received': [], + 'contact_connected': ['use_systray_cb', 'disable_systray_cb', + 'use_roster_cb', 'disable_roster_cb'], + 'contact_disconnected': ['use_systray_cb', 'disable_systray_cb', + 'use_roster_cb', 'disable_roster_cb'], + 'contact_status_change': ['use_systray_cb', 'disable_systray_cb', + 'use_roster_cb', 'disable_roster_cb'] + #, 'gc_msg_highlight': [], 'gc_msg': []} + } + recipient_types_list = ['contact', 'group', 'all'] + config_options = ['event', 'recipient_type', 'recipients', 'status', + 'tab_opened', 'sound', 'sound_file', 'popup', 'auto_open', + 'run_command', 'command', 'systray', 'roster', 'urgency_hint'] + + def init(self): + self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path( + 'config_dialog.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain('gajim_plugins') + self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, + ['vbox', 'liststore1', 'liststore2']) + vbox = self.xml.get_object('vbox') + self.child.pack_start(vbox) + + self.xml.connect_signals(self) + self.connect('hide', self.on_hide) + + def on_run(self): + # fill window + for w in ('conditions_treeview', 'config_vbox', 'event_combobox', + 'recipient_type_combobox', 'recipient_list_entry', 'delete_button', + 'status_hbox', 'use_sound_cb', 'disable_sound_cb', 'use_popup_cb', + 'disable_popup_cb', 'use_auto_open_cb', 'disable_auto_open_cb', + 'use_systray_cb', 'disable_systray_cb', 'use_roster_cb', + 'disable_roster_cb', 'tab_opened_cb', 'not_tab_opened_cb', + 'sound_entry', 'sound_file_hbox', 'up_button', 'down_button', + 'run_command_cb', 'command_entry', 'use_urgency_hint_cb', + 'disable_urgency_hint_cb'): + self.__dict__[w] = self.xml.get_object(w) + + self.config = {} + for n in self.plugin.config: + self.config[int(n)] = self.plugin.config[n] + + # Contains status checkboxes + childs = self.status_hbox.get_children() + + self.all_status_rb = childs[0] + self.special_status_rb = childs[1] + self.online_cb = childs[2] + self.away_cb = childs[3] + self.xa_cb = childs[4] + self.dnd_cb = childs[5] + self.invisible_cb = childs[6] + + if not self.conditions_treeview.get_column(0): + # window never opened + model = gtk.ListStore(int, str) + model.set_sort_column_id(0, gtk.SORT_ASCENDING) + self.conditions_treeview.set_model(model) + + # means number + col = gtk.TreeViewColumn(_('#')) + self.conditions_treeview.append_column(col) + renderer = gtk.CellRendererText() + col.pack_start(renderer, expand=False) + col.set_attributes(renderer, text=0) + + col = gtk.TreeViewColumn(_('Condition')) + self.conditions_treeview.append_column(col) + renderer = gtk.CellRendererText() + col.pack_start(renderer, expand=True) + col.set_attributes(renderer, text=1) + else: + model = self.conditions_treeview.get_model() + + model.clear() + + # Fill conditions_treeview + num = 0 + while num in self.config: + iter_ = model.append((num, '')) + path = model.get_path(iter_) + self.conditions_treeview.set_cursor(path) + self.active_num = num + self.initiate_rule_state() + self.set_treeview_string() + num += 1 + + # No rule selected at init time + self.conditions_treeview.get_selection().unselect_all() + self.active_num = -1 + self.config_vbox.set_sensitive(False) + self.delete_button.set_sensitive(False) + self.down_button.set_sensitive(False) + self.up_button.set_sensitive(False) + + def initiate_rule_state(self): + """ + Set values for all widgets + """ + if self.active_num < 0: + return + # event + value = self.config[self.active_num]['event'] + if value: + self.event_combobox.set_active(self.events_list.keys().index(value)) + else: + self.event_combobox.set_active(-1) + # recipient_type + value = self.config[self.active_num]['recipient_type'] + if value: + self.recipient_type_combobox.set_active( + self.recipient_types_list.index(value)) + else: + self.recipient_type_combobox.set_active(-1) + # recipient + value = self.config[self.active_num]['recipients'] + if not value: + value = '' + self.recipient_list_entry.set_text(value) + # status + value = self.config[self.active_num]['status'] + if value == 'all': + self.all_status_rb.set_active(True) + else: + self.special_status_rb.set_active(True) + values = value.split() + for v in ('online', 'away', 'xa', 'dnd', 'invisible'): + if v in values: + self.__dict__[v + '_cb'].set_active(True) + else: + self.__dict__[v + '_cb'].set_active(False) + self.on_status_radiobutton_toggled(self.all_status_rb) + + # tab_opened + value = self.config[self.active_num]['tab_opened'] + self.tab_opened_cb.set_active(True) + self.not_tab_opened_cb.set_active(True) + if value == 'no': + self.tab_opened_cb.set_active(False) + elif value == 'yes': + self.not_tab_opened_cb.set_active(False) + + # sound_file + value = self.config[self.active_num]['sound_file'] + self.sound_entry.set_text(value) + + # sound, popup, auto_open, systray, roster + for option in ('sound', 'popup', 'auto_open', 'systray', 'roster', + 'urgency_hint'): + value = self.config[self.active_num][option] + if value == 'yes': + self.__dict__['use_' + option + '_cb'].set_active(True) + else: + self.__dict__['use_' + option + '_cb'].set_active(False) + if value == 'no': + self.__dict__['disable_' + option + '_cb'].set_active(True) + else: + self.__dict__['disable_' + option + '_cb'].set_active(False) + + # run_command + value = self.config[self.active_num]['run_command'] + self.run_command_cb.set_active(value) + + # command + value = self.config[self.active_num]['command'] + self.command_entry.set_text(value) + + def set_treeview_string(self): + (model, iter_) = self.conditions_treeview.get_selection().get_selected() + if not iter_: + return + event = self.event_combobox.get_active_text() + recipient_type = self.recipient_type_combobox.get_active_text() + recipient = '' + if recipient_type != 'everybody': + recipient = self.recipient_list_entry.get_text() + if self.all_status_rb.get_active(): + status = '' + else: + status = _('when I am ') + for st in ('online', 'away', 'xa', 'dnd', 'invisible'): + if self.__dict__[st + '_cb'].get_active(): + status += helpers.get_uf_show(st) + ' ' + model[iter_][1] = "When %s for %s %s %s" % (event, recipient_type, + recipient, status) + + def on_conditions_treeview_cursor_changed(self, widget): + (model, iter_) = widget.get_selection().get_selected() + if not iter_: + self.active_num = '' + return + self.active_num = model[iter_][0] + if self.active_num == '0': + self.up_button.set_sensitive(False) + else: + self.up_button.set_sensitive(True) + max = self.conditions_treeview.get_model().iter_n_children(None) + if self.active_num == max - 1: + self.down_button.set_sensitive(False) + else: + self.down_button.set_sensitive(True) + self.initiate_rule_state() + self.config_vbox.set_sensitive(True) + self.delete_button.set_sensitive(True) + + def on_new_button_clicked(self, widget): + model = self.conditions_treeview.get_model() + num = self.conditions_treeview.get_model().iter_n_children(None) + self.config[num] = {'event': '', 'recipient_type': 'all', + 'recipients': '', 'status': 'all', 'tab_opened': 'both', + 'sound': '', 'sound_file': '', 'popup': '', 'auto_open': '', + 'run_command': False, 'command': '', 'systray': '', 'roster': '', + 'urgency_hint': False} + iter_ = model.append((num, '')) + path = model.get_path(iter_) + self.conditions_treeview.set_cursor(path) + self.active_num = num + self.set_treeview_string() + self.config_vbox.set_sensitive(True) + + def on_delete_button_clicked(self, widget): + (model, iter_) = self.conditions_treeview.get_selection().get_selected() + if not iter_: + return + # up all others + iter2 = model.iter_next(iter_) + num = self.active_num + while iter2: + num = model[iter2][0] + model[iter2][0] = num - 1 + self.config[num - 1] = self.config[num].copy() + iter2 = model.iter_next(iter2) + model.remove(iter_) + del self.config[num] + self.active_num = '' + self.config_vbox.set_sensitive(False) + self.delete_button.set_sensitive(False) + self.up_button.set_sensitive(False) + self.down_button.set_sensitive(False) + + def on_up_button_clicked(self, widget): + (model, iter_) = self.conditions_treeview.get_selection().get_selected() + if not iter_: + return + conf = self.config[self.active_num].copy() + self.config[self.active_num] = self.config[self.active_num - 1] + self.config[self.active_num - 1] = conf + + model[iter_][0] = self.active_num - 1 + # get previous iter + path = model.get_path(iter_) + iter_ = model.get_iter((path[0] - 1,)) + model[iter_][0] = self.active_num + self.on_conditions_treeview_cursor_changed(self.conditions_treeview) + + def on_down_button_clicked(self, widget): + (model, iter_) = self.conditions_treeview.get_selection().get_selected() + if not iter_: + return + conf = self.config[self.active_num].copy() + self.config[self.active_num] = self.config[self.active_num + 1] + self.config[self.active_num + 1] = conf + + model[iter_][0] = self.active_num + 1 + iter_ = model.iter_next(iter_) + model[iter_][0] = self.active_num + self.on_conditions_treeview_cursor_changed(self.conditions_treeview) + + def on_event_combobox_changed(self, widget): + if self.active_num < 0: + return + active = self.event_combobox.get_active() + if active == -1: + event = '' + else: + event = self.events_list.keys()[active] + self.config[self.active_num]['event'] = event + for w in ('use_systray_cb', 'disable_systray_cb', 'use_roster_cb', + 'disable_roster_cb'): + self.__dict__[w].set_sensitive(True) + for w in self.events_list[event]: + self.__dict__[w].set_sensitive(False) + self.__dict__[w].set_state(False) + self.set_treeview_string() + + def on_recipient_type_combobox_changed(self, widget): + if self.active_num < 0: + return + recipient_type = self.recipient_types_list[ + self.recipient_type_combobox.get_active()] + self.config[self.active_num]['recipient_type'] = recipient_type + if recipient_type == 'all': + self.recipient_list_entry.hide() + else: + self.recipient_list_entry.show() + self.set_treeview_string() + + def on_recipient_list_entry_changed(self, widget): + if self.active_num < 0: + return + recipients = widget.get_text().decode('utf-8') + #TODO: do some check + self.config[self.active_num]['recipients'] = recipients + self.set_treeview_string() + + def set_status_config(self): + if self.active_num < 0: + return + status = '' + for st in ('online', 'away', 'xa', 'dnd', 'invisible'): + if self.__dict__[st + '_cb'].get_active(): + status += st + ' ' + if status: + status = status[:-1] + self.config[self.active_num]['status'] = status + self.set_treeview_string() + + def on_status_radiobutton_toggled(self, widget): + if self.active_num < 0: + return + if self.all_status_rb.get_active(): + self.config[self.active_num]['status'] = 'all' + # 'All status' clicked + for st in ('online', 'away', 'xa', 'dnd', 'invisible'): + self.__dict__[st + '_cb'].hide() + + self.special_status_rb.show() + else: + self.set_status_config() + # 'special status' clicked + for st in ('online', 'away', 'xa', 'dnd', 'invisible'): + self.__dict__[st + '_cb'].show() + + self.special_status_rb.hide() + self.set_treeview_string() + + def on_status_cb_toggled(self, widget): + if self.active_num < 0: + return + self.set_status_config() + + # tab_opened OR (not xor) not_tab_opened must be active + def on_tab_opened_cb_toggled(self, widget): + if self.active_num < 0: + return + if self.tab_opened_cb.get_active(): + if self.not_tab_opened_cb.get_active(): + self.config[self.active_num]['tab_opened'] = 'both' + else: + self.config[self.active_num]['tab_opened'] = 'yes' + else: + self.not_tab_opened_cb.set_active(True) + self.config[self.active_num]['tab_opened'] = 'no' + + def on_not_tab_opened_cb_toggled(self, widget): + if self.active_num < 0: + return + if self.not_tab_opened_cb.get_active(): + if self.tab_opened_cb.get_active(): + self.config[self.active_num]['tab_opened'] = 'both' + else: + self.config[self.active_num]['tab_opened'] = 'no' + else: + self.tab_opened_cb.set_active(True) + self.config[self.active_num]['tab_opened'] = 'yes' + + def on_use_it_toggled(self, widget, oposite_widget, option): + if widget.get_active(): + if oposite_widget.get_active(): + oposite_widget.set_active(False) + self.config[self.active_num][option] = 'yes' + elif oposite_widget.get_active(): + self.config[self.active_num][option] = 'no' + else: + self.config[self.active_num][option] = '' + + def on_disable_it_toggled(self, widget, oposite_widget, option): + if widget.get_active(): + if oposite_widget.get_active(): + oposite_widget.set_active(False) + self.config[self.active_num][option] = 'no' + elif oposite_widget.get_active(): + self.config[self.active_num][option] = 'yes' + else: + self.config[self.active_num][option] = '' + + def on_use_sound_cb_toggled(self, widget): + self.on_use_it_toggled(widget, self.disable_sound_cb, 'sound') + if widget.get_active(): + self.sound_file_hbox.set_sensitive(True) + else: + self.sound_file_hbox.set_sensitive(False) + + def on_browse_for_sounds_button_clicked(self, widget, data=None): + if self.active_num < 0: + return + + def on_ok(widget, path_to_snd_file): + dialog.destroy() + if not path_to_snd_file: + path_to_snd_file = '' + self.config[self.active_num]['sound_file'] = path_to_snd_file + self.sound_entry.set_text(path_to_snd_file) + + path_to_snd_file = self.sound_entry.get_text().decode('utf-8') + path_to_snd_file = os.path.join(os.getcwd(), path_to_snd_file) + dialog = SoundChooserDialog(path_to_snd_file, on_ok) + + def on_play_button_clicked(self, widget): + helpers.play_sound_file(self.sound_entry.get_text().decode('utf-8')) + + def on_disable_sound_cb_toggled(self, widget): + self.on_disable_it_toggled(widget, self.use_sound_cb, 'sound') + + def on_sound_entry_changed(self, widget): + self.config[self.active_num]['sound_file'] = widget.get_text().\ + decode('utf-8') + + def on_use_popup_cb_toggled(self, widget): + self.on_use_it_toggled(widget, self.disable_popup_cb, 'popup') + + def on_disable_popup_cb_toggled(self, widget): + self.on_disable_it_toggled(widget, self.use_popup_cb, 'popup') + + def on_use_auto_open_cb_toggled(self, widget): + self.on_use_it_toggled(widget, self.disable_auto_open_cb, 'auto_open') + + def on_disable_auto_open_cb_toggled(self, widget): + self.on_disable_it_toggled(widget, self.use_auto_open_cb, 'auto_open') + + def on_run_command_cb_toggled(self, widget): + self.config[self.active_num]['run_command'] = widget.get_active() + if widget.get_active(): + self.command_entry.set_sensitive(True) + else: + self.command_entry.set_sensitive(False) + + def on_command_entry_changed(self, widget): + self.config[self.active_num]['command'] = widget.get_text().\ + decode('utf-8') + + def on_use_systray_cb_toggled(self, widget): + self.on_use_it_toggled(widget, self.disable_systray_cb, 'systray') + + def on_disable_systray_cb_toggled(self, widget): + self.on_disable_it_toggled(widget, self.use_systray_cb, 'systray') + + def on_use_roster_cb_toggled(self, widget): + self.on_use_it_toggled(widget, self.disable_roster_cb, 'roster') + + def on_disable_roster_cb_toggled(self, widget): + self.on_disable_it_toggled(widget, self.use_roster_cb, 'roster') + + def on_use_urgency_hint_cb_toggled(self, widget): + self.on_use_it_toggled(widget, self.disable_urgency_hint_cb, + 'uregency_hint') + + def on_disable_urgency_hint_cb_toggled(self, widget): + self.on_disable_it_toggled(widget, self.use_urgency_hint_cb, + 'uregency_hint') + + def on_hide(self, widget): + # save config + for n in self.plugin.config: + del self.plugin.config[n] + for n in self.config: + self.plugin.config[str(n)] = self.config[n] diff --git a/whiteboard/__init__.py b/whiteboard/__init__.py new file mode 100644 index 0000000..802d00c --- /dev/null +++ b/whiteboard/__init__.py @@ -0,0 +1 @@ +from plugin import WhiteboardPlugin diff --git a/whiteboard/brush_tool.png b/whiteboard/brush_tool.png Binary files differnew file mode 100644 index 0000000..266c321 --- /dev/null +++ b/whiteboard/brush_tool.png diff --git a/whiteboard/line_tool.png b/whiteboard/line_tool.png Binary files differnew file mode 100644 index 0000000..151f584 --- /dev/null +++ b/whiteboard/line_tool.png diff --git a/whiteboard/manifest.ini b/whiteboard/manifest.ini new file mode 100644 index 0000000..a9f6708 --- /dev/null +++ b/whiteboard/manifest.ini @@ -0,0 +1,7 @@ +[info] +name: Whiteboard +short_name: whiteboard +version: 0.1 +description: Shows a whiteboard in chat. python-pygoocanvas is required. +authors = Yann Leboulanger <asterix@lagaule.org> +homepage = www.gajim.org diff --git a/whiteboard/oval_tool.png b/whiteboard/oval_tool.png Binary files differnew file mode 100644 index 0000000..efd6f0c --- /dev/null +++ b/whiteboard/oval_tool.png diff --git a/whiteboard/plugin.py b/whiteboard/plugin.py new file mode 100644 index 0000000..9132e9a --- /dev/null +++ b/whiteboard/plugin.py @@ -0,0 +1,483 @@ +## plugins/whiteboard/plugin.py +## +## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com> +## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org> +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## + +''' +Whiteboard plugin. + +:author: Yann Leboulanger <asterix@lagaule.org> +:since: 1st November 2010 +:copyright: Copyright (2010) Yann Leboulanger <asterix@lagaule.org> +:license: GPL +''' + + +from common import helpers +from common import gajim +from plugins import GajimPlugin +from plugins.plugin import GajimPluginException +from plugins.helpers import log_calls, log +import common.xmpp +import gtk +import chat_control +from common import ged +from common.jingle_session import JingleSession +from common.jingle_content import JingleContent +from common.jingle_transport import JingleTransport, TransportType +import dialogs +from whiteboard_widget import Whiteboard, HAS_GOOCANVAS +from common import xmpp +from common import caps_cache + +NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml' +NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe' +NS_SXE = 'urn:xmpp:sxe:0' + +class WhiteboardPlugin(GajimPlugin): + @log_calls('WhiteboardPlugin') + def init(self): + self.description = _('Shows a whiteboard in chat.' + ' python-pygoocanvas is required.') + self.config_dialog = None + self.events_handlers = { + 'jingle-request-received': (ged.GUI1, self._nec_jingle_received), + 'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected), + 'jingle-disconnected-received': (ged.GUI1, + self._nec_jingle_disconnected), + 'raw-message-received': (ged.GUI1, self._nec_raw_message), + } + self.gui_extension_points = { + 'chat_control_base' : (self.connect_with_chat_control, + self.disconnect_from_chat_control), + 'chat_control_base_update_toolbar': (self.update_button_state, + None), + } + self.controls = [] + self.sid = None + + @log_calls('WhiteboardPlugin') + def _compute_caps_hash(self): + for a in gajim.connections: + gajim.caps_hash[a] = caps_cache.compute_caps_hash([ + gajim.gajim_identity], gajim.gajim_common_features + \ + gajim.gajim_optional_features[a]) + # re-send presence with new hash + connected = gajim.connections[a].connected + if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible': + gajim.connections[a].change_status(gajim.SHOW_LIST[connected], + gajim.connections[a].status) + + @log_calls('WhiteboardPlugin') + def activate(self): + if not HAS_GOOCANVAS: + raise GajimPluginException('python-pygoocanvas is missing!') + if NS_JINGLE_SXE not in gajim.gajim_common_features: + gajim.gajim_common_features.append(NS_JINGLE_SXE) + if NS_SXE not in gajim.gajim_common_features: + gajim.gajim_common_features.append(NS_SXE) + self._compute_caps_hash() + + @log_calls('WhiteboardPlugin') + def deactivate(self): + if NS_JINGLE_SXE in gajim.gajim_common_features: + gajim.gajim_common_features.remove(NS_JINGLE_SXE) + if NS_SXE in gajim.gajim_common_features: + gajim.gajim_common_features.remove(NS_SXE) + self._compute_caps_hash() + + @log_calls('WhiteboardPlugin') + def connect_with_chat_control(self, control): + if isinstance(control, chat_control.ChatControl): + base = Base(self, control) + self.controls.append(base) + + @log_calls('WhiteboardPlugin') + def disconnect_from_chat_control(self, chat_control): + for base in self.controls: + base.disconnect_from_chat_control() + self.controls = [] + + @log_calls('WhiteboardPlugin') + def update_button_state(self, control): + for base in self.controls: + if base.chat_control == control: + if control.contact.supports(NS_JINGLE_SXE) and \ + control.contact.supports(NS_SXE): + base.button.set_sensitive(True) + tooltip_text = _('Show whiteboard') + else: + base.button.set_sensitive(False) + tooltip_text = _('Client on the other side ' + 'does not support the whiteboard') + base.button.set_tooltip_text(tooltip_text) + + @log_calls('WhiteboardPlugin') + def show_request_dialog(self, account, fjid, jid, sid, content_types): + def on_ok(): + session = gajim.connections[account].get_jingle_session(fjid, sid) + self.sid = session.sid + if not session.accepted: + session.approve_session() + for content in content_types: + session.approve_content(content) + for _jid in (fjid, jid): + ctrl = gajim.interface.msg_win_mgr.get_control(_jid, account) + if ctrl: + break + if not ctrl: + # create it + gajim.interface.new_chat_from_jid(account, jid) + ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) + session = session.contents[('initiator', 'xhtml')] + ctrl.draw_whiteboard(session) + + def on_cancel(): + session = gajim.connections[account].get_jingle_session(fjid, sid) + session.decline_session() + + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + if contact: + name = contact.get_shown_name() + else: + name = jid + pritext = _('Incoming Whiteboard') + sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with ' + 'you. Do you want to accept?') % {'name': name, 'jid': jid} + dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext, + on_response_ok=on_ok, on_response_cancel=on_cancel) + dialog.popup() + + @log_calls('WhiteboardPlugin') + def _nec_jingle_received(self, obj): + if not HAS_GOOCANVAS: + return + content_types = set(c[0] for c in obj.contents) + if 'xhtml' not in content_types: + return + self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid, + content_types) + + @log_calls('WhiteboardPlugin') + def _nec_jingle_connected(self, obj): + if not HAS_GOOCANVAS: + return + account = obj.conn.name + ctrl = (gajim.interface.msg_win_mgr.get_control(obj.fjid, account) + or gajim.interface.msg_win_mgr.get_control(obj.jid, account)) + if not ctrl: + return + session = gajim.connections[obj.conn.name].get_jingle_session(obj.fjid, + obj.sid) + + if ('initiator', 'xhtml') not in session.contents: + return + + session = session.contents[('initiator', 'xhtml')] + ctrl.draw_whiteboard(session) + + @log_calls('WhiteboardPlugin') + def _nec_jingle_disconnected(self, obj): + for base in self.controls: + if base.sid == obj.sid: + base.stop_whiteboard(reason = obj.reason) + + @log_calls('WhiteboardPlugin') + def _nec_raw_message(self, obj): + if not HAS_GOOCANVAS: + return + if obj.stanza.getTag('sxe', namespace=NS_SXE): + account = obj.conn.name + + try: + fjid = helpers.get_full_jid_from_iq(obj.stanza) + except helpers.InvalidFormat: + obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'), + _('A message from a non-valid JID arrived, it has been ' + 'ignored.'))) + + jid = gajim.get_jid_without_resource(fjid) + ctrl = (gajim.interface.msg_win_mgr.get_control(fjid, account) + or gajim.interface.msg_win_mgr.get_control(jid, account)) + if not ctrl: + return + sxe = obj.stanza.getTag('sxe') + if not sxe: + return + sid = sxe.getAttr('session') + if (jid, sid) not in obj.conn._sessions: + pass +# newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) +# self.addJingle(newjingle) + + # we already have such session in dispatcher... + session = obj.conn.get_jingle_session(fjid, sid) + cn = session.contents[('initiator', 'xhtml')] + error = obj.stanza.getTag('error') + if error: + action = 'iq-error' + else: + action = 'edit' + + cn.on_stanza(obj.stanza, sxe, error, action) +# def __editCB(self, stanza, content, error, action): + #new_tags = sxe.getTags('new') + #remove_tags = sxe.getTags('remove') + + #if new_tags is not None: + ## Process new elements + #for tag in new_tags: + #if tag.getAttr('type') == 'element': + #ctrl.whiteboard.recieve_element(tag) + #elif tag.getAttr('type') == 'attr': + #ctrl.whiteboard.recieve_attr(tag) + #ctrl.whiteboard.apply_new() + + #if remove_tags is not None: + ## Delete rids + #for tag in remove_tags: + #target = tag.getAttr('target') + #ctrl.whiteboard.image.del_rid(target) + + # Stop propagating this event, it's handled + return True + + +class Base(object): + def __init__(self, plugin, chat_control): + self.plugin = plugin + self.chat_control = chat_control + self.chat_control.draw_whiteboard = self.draw_whiteboard + self.contact = self.chat_control.contact + self.account = self.chat_control.account + self.jid = self.contact.get_full_jid() + self.create_buttons() + self.whiteboard = None + self.sid = None + + def create_buttons(self): + # create whiteboard button + actions_hbox = self.chat_control.xml.get_object('actions_hbox') + self.button = gtk.ToggleButton(label=None, use_underline=True) + self.button.set_property('relief', gtk.RELIEF_NONE) + self.button.set_property('can-focus', False) + img = gtk.Image() + img_path = self.plugin.local_file_path('whiteboard.png') + pixbuf = gtk.gdk.pixbuf_new_from_file(img_path) + iconset = gtk.IconSet(pixbuf=pixbuf) + factory = gtk.IconFactory() + factory.add('whiteboard', iconset) + img_path = self.plugin.local_file_path('brush_tool.png') + pixbuf = gtk.gdk.pixbuf_new_from_file(img_path) + iconset = gtk.IconSet(pixbuf=pixbuf) + factory.add('brush_tool', iconset) + img_path = self.plugin.local_file_path('line_tool.png') + pixbuf = gtk.gdk.pixbuf_new_from_file(img_path) + iconset = gtk.IconSet(pixbuf=pixbuf) + factory.add('line_tool', iconset) + img_path = self.plugin.local_file_path('oval_tool.png') + pixbuf = gtk.gdk.pixbuf_new_from_file(img_path) + iconset = gtk.IconSet(pixbuf=pixbuf) + factory.add('oval_tool', iconset) + factory.add_default() + img.set_from_stock('whiteboard', gtk.ICON_SIZE_MENU) + self.button.set_image(img) + send_button = self.chat_control.xml.get_object('send_button') + send_button_pos = actions_hbox.child_get_property(send_button, + 'position') + actions_hbox.add_with_properties(self.button, 'position', + send_button_pos - 1, 'expand', False) + id_ = self.button.connect('toggled', self.on_whiteboard_button_toggled) + self.chat_control.handlers[id_] = self.button + self.button.show() + + def draw_whiteboard(self, content): + hbox = self.chat_control.xml.get_object('chat_control_hbox') + if len(hbox.get_children()) == 1: + self.whiteboard = Whiteboard(self.account, self.contact, content, + self.plugin) + # set minimum size + self.whiteboard.hbox.set_size_request(300, 0) + hbox.pack_start(self.whiteboard.hbox, expand=False, fill=False) + self.whiteboard.hbox.show_all() + self.button.set_active(True) + content.control = self + self.sid = content.session.sid + + def on_whiteboard_button_toggled(self, widget): + """ + Popup whiteboard + """ + if widget.get_active(): + if not self.whiteboard: + self.start_whiteboard() + else: + self.stop_whiteboard() + + def start_whiteboard(self): + conn = gajim.connections[self.chat_control.account] + jingle = JingleSession(conn, weinitiate=True, jid=self.jid) + self.sid = jingle.sid + conn._sessions[jingle.sid] = jingle + content = JingleWhiteboard(jingle) + content.control = self + jingle.add_content('xhtml', content) + jingle.start_session() + + def stop_whiteboard(self, reason=None): + conn = gajim.connections[self.chat_control.account] + self.sid = None + session = conn.get_jingle_session(self.jid, media='xhtml') + if session: + session.end_session() + self.button.set_active(False) + if reason: + txt = _('Whiteboard stopped: %(reason)s') % {'reason': reason} + self.chat_control.print_conversation(txt, 'info') + if not self.whiteboard: + return + hbox = self.chat_control.xml.get_object('chat_control_hbox') + if self.whiteboard.hbox in hbox.get_children(): + if hasattr(self.whiteboard, 'hbox'): + hbox.remove(self.whiteboard.hbox) + self.whiteboard = None + + def disconnect_from_chat_control(self): + actions_hbox = self.chat_control.xml.get_object('actions_hbox') + actions_hbox.remove(self.button) + +class JingleWhiteboard(JingleContent): + ''' Jingle Whiteboard sessions consist of xhtml content''' + def __init__(self, session, transport=None): + if not transport: + transport = JingleTransportSXE() + JingleContent.__init__(self, session, transport) + self.media = 'xhtml' + self.negotiated = True # there is nothing to negotiate + self.last_rid = 0 + self.callbacks['session-accept'] += [self._sessionAcceptCB] + self.callbacks['session-terminate'] += [self._stop] + self.callbacks['session-terminate-sent'] += [self._stop] + self.callbacks['edit'] = [self._EditCB] + + def _EditCB(self, stanza, content, error, action): + new_tags = content.getTags('new') + remove_tags = content.getTags('remove') + + if new_tags is not None: + # Process new elements + for tag in new_tags: + if tag.getAttr('type') == 'element': + self.control.whiteboard.recieve_element(tag) + elif tag.getAttr('type') == 'attr': + self.control.whiteboard.recieve_attr(tag) + self.control.whiteboard.apply_new() + + if remove_tags is not None: + # Delete rids + for tag in remove_tags: + target = tag.getAttr('target') + self.control.whiteboard.image.del_rid(target) + + def _sessionAcceptCB(self, stanza, content, error, action): + log.debug('session accepted') + self.session.connection.dispatch('WHITEBOARD_ACCEPTED', + (self.session.peerjid, self.session.sid)) + + def generate_rids(self, x): + # generates x number of rids and returns in list + rids = [] + for x in range(x): + rids.append(str(self.last_rid)) + self.last_rid += 1 + return rids + + def send_whiteboard_node(self, items, rids): + # takes int rid and dict items and sends it as a node + # sends new item + jid = self.session.peerjid + sid = self.session.sid + message = xmpp.Message(to=jid) + sxe = message.addChild(name='sxe', attrs={'session': sid}, + namespace=NS_SXE) + + for x in rids: + if items[x]['type'] == 'element': + parent = x + attrs = {'rid': x, + 'name': items[x]['data'][0].getName(), + 'type': items[x]['type']} + sxe.addChild(name='new', attrs=attrs) + if items[x]['type'] == 'attr': + attr_name = items[x]['data'] + chdata = items[parent]['data'][0].getAttr(attr_name) + attrs = {'rid': x, + 'name': attr_name, + 'type': items[x]['type'], + 'chdata': chdata, + 'parent': parent} + sxe.addChild(name='new', attrs=attrs) + self.session.connection.connection.send(message) + + def delete_whiteboard_node(self, rids): + message = xmpp.Message(to=self.session.peerjid) + sxe = message.addChild(name='sxe', attrs={'session': self.session.sid}, + namespace=NS_SXE) + + for x in rids: + sxe.addChild(name='remove', attrs = {'target': x}) + self.session.connection.connection.send(message) + + def send_items(self, items, rids): + # recieves dict items and a list of rids of items to send + # TODO: is there a less clumsy way that doesn't involve passing + # whole list + self.send_whiteboard_node(items, rids) + + def del_item(self, rids): + self.delete_whiteboard_node(rids) + + def encode(self, xml): + # encodes it sendable string + return 'data:text/xml,' + urllib.quote(xml) + + def _fill_content(self, content): + content.addChild(NS_JINGLE_XHTML + ' description') + + def _stop(self, *things): + pass + + def __del__(self): + pass + +def get_content(desc): + return JingleWhiteboard + +common.jingle_content.contents[NS_JINGLE_XHTML] = get_content + +class JingleTransportSXE(JingleTransport): + def __init__(self): + JingleTransport.__init__(self, TransportType.streaming) + + def make_transport(self, candidates=None): + transport = JingleTransport.make_transport(self, candidates) + transport.setNamespace(NS_JINGLE_SXE) + transport.setTagData('host', 'TODO') + return transport + +common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE diff --git a/whiteboard/whiteboard.png b/whiteboard/whiteboard.png Binary files differnew file mode 100644 index 0000000..13318e3 --- /dev/null +++ b/whiteboard/whiteboard.png diff --git a/whiteboard/whiteboard_widget.py b/whiteboard/whiteboard_widget.py new file mode 100644 index 0000000..2435ae1 --- /dev/null +++ b/whiteboard/whiteboard_widget.py @@ -0,0 +1,418 @@ +## plugins/whiteboard/whiteboard_widget.py +## +## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com> +## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org> +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see <http://www.gnu.org/licenses/>. +## + +import gtk +import gtkgui_helpers +try: + import goocanvas + HAS_GOOCANVAS = True +except: + HAS_GOOCANVAS = False +from common.xmpp import Node +from common import gajim +from dialogs import FileChooserDialog + +''' +A whiteboard widget made for Gajim. +- Ummu +''' + +class Whiteboard(object): + def __init__(self, account, contact, session, plugin): + self.plugin = plugin + file_path = plugin.local_file_path('whiteboard_widget.ui') + xml = gtk.Builder() + xml.set_translation_domain('gajim_plugins') + xml.add_from_file(file_path) + self.hbox = xml.get_object('whiteboard_hbox') + self.canevas = goocanvas.Canvas() + self.hbox.pack_start(self.canevas) + self.hbox.reorder_child(self.canevas, 0) + self.canevas.set_flags(gtk.CAN_FOCUS) + self.fg_color_select_button = xml.get_object('fg_color_button') + self.root = self.canevas.get_root_item() + self.tool_buttons = [] + for tool in ('brush', 'oval', 'line', 'delete'): + self.tool_buttons.append(xml.get_object(tool + '_button')) + xml.get_object('brush_button').set_active(True) + + # Events + self.canevas.connect('button-press-event', self.button_press_event) + self.canevas.connect('button-release-event', self.button_release_event) + self.canevas.connect('motion-notify-event', self.motion_notify_event) + self.canevas.connect('item-created', self.item_created) + + # Config + self.line_width = 2 + xml.get_object('size_scale').set_value(2) + self.color = str(self.fg_color_select_button.get_color()) + + # SVG Storage + self.image = SVGObject(self.root, session) + + xml.connect_signals(self) + + # Temporary Variables for items + self.item_temp = None + self.item_temp_coords = (0, 0) + self.item_data = None + + # Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance + self.recieving = {} + + def on_tool_button_toggled(self, widget): + for btn in self.tool_buttons: + if btn == widget: + continue + btn.set_active(False) + + def on_brush_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'brush' + self.on_tool_button_toggled(widget) + + def on_oval_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'oval' + self.on_tool_button_toggled(widget) + + def on_line_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'line' + self.on_tool_button_toggled(widget) + + def on_delete_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'delete' + self.on_tool_button_toggled(widget) + + def on_clear_button_clicked(self, widget): + self.image.clear_canvas() + + def on_export_button_clicked(self, widget): + SvgChooserDialog(self.image.export_svg) + + def on_fg_color_button_color_set(self, widget): + self.color = str(self.fg_color_select_button.get_color()) + + def item_created(self, canvas, item, model): + print 'item created' + item.connect('button-press-event', self.item_button_press_events) + + def item_button_press_events(self, item, target_item, event): + if self.image.draw_tool == 'delete': + self.image.del_item(item) + + def on_size_scale_format_value(self, widget): + self.line_width = int(widget.get_value()) + + def button_press_event(self, widget, event): + x = event.x + y = event.y + state = event.state + self.item_temp_coords = (x, y) + + if self.image.draw_tool == 'brush': + self.item_temp = goocanvas.Ellipse(parent=self.root, + center_x=x, + center_y=y, + radius_x=1, + radius_y=1, + stroke_color=self.color, + fill_color=self.color, + line_width=self.line_width) + self.item_data = 'M %s,%s L ' % (x, y) + + elif self.image.draw_tool == 'oval': + self.item_data = True + + if self.image.draw_tool == 'line': + self.item_data = 'M %s,%s L' % (x, y) + + def motion_notify_event(self, widget, event): + x = event.x + y = event.y + state = event.state + if self.item_temp is not None: + self.item_temp.remove() + + if self.item_data is not None: + if self.image.draw_tool == 'brush': + self.item_data = self.item_data + '%s,%s ' % (x, y) + self.item_temp = goocanvas.Path(parent=self.root, + data=self.item_data, line_width=self.line_width, + stroke_color=self.color) + elif self.image.draw_tool == 'oval': + self.item_temp = goocanvas.Ellipse(parent=self.root, + center_x=self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2, + center_y=self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2, + radius_x=abs(x - self.item_temp_coords[0]) / 2, + radius_y=abs(y - self.item_temp_coords[1]) / 2, + stroke_color=self.color, + line_width=self.line_width) + elif self.image.draw_tool == 'line': + self.item_data = 'M %s,%s L' % self.item_temp_coords + self.item_data = self.item_data + ' %s,%s' % (x, y) + self.item_temp = goocanvas.Path(parent=self.root, + data=self.item_data, line_width=self.line_width, + stroke_color=self.color) + + def button_release_event(self, widget, event): + x = event.x + y = event.y + state = event.state + + if self.image.draw_tool == 'brush': + self.item_data = self.item_data + '%s,%s' % (x, y) + if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]: + goocanvas.Ellipse(parent=self.root, + center_x=x, + center_y=y, + radius_x=1, + radius_y=1, + stroke_color=self.color, + fill_color=self.color, + line_width=self.line_width) + self.image.add_path(self.item_data, self.line_width, self.color) + + if self.image.draw_tool == 'oval': + cx = self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2 + cy = self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2 + rx = abs(x - self.item_temp_coords[0]) / 2 + ry = abs(y - self.item_temp_coords[1]) / 2 + self.image.add_ellipse(cx, cy, rx, ry, self.line_width, self.color) + + if self.image.draw_tool == 'line': + self.item_data = 'M %s,%s L' % self.item_temp_coords + self.item_data = self.item_data + ' %s,%s' % (x, y) + if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]: + goocanvas.Ellipse(parent=self.root, + center_x=x, + center_y=y, + radius_x=1, + radius_y=1, + stroke_color='black', + fill_color='black', + line_width=self.line_width) + self.image.add_path(self.item_data, self.line_width, self.color) + + if self.image.draw_tool == 'delete': + pass + + self.item_data = None + if self.item_temp is not None: + self.item_temp.remove() + self.item_temp = None + + def recieve_element(self, element): + node = self.image.g.addChild(name=element.getAttr('name')) + self.image.g.addChild(node=node) + self.recieving[element.getAttr('rid')] = {'type':'element', + 'data':[node], + 'children':[]} + + def recieve_attr(self, element): + node = self.recieving[element.getAttr('parent')]['data'][0] + node.setAttr(element.getAttr('name'), element.getAttr('chdata')) + + self.recieving[element.getAttr('rid')] = {'type':'attr', + 'data':element.getAttr('name'), + 'parent':node} + self.recieving[element.getAttr('parent')]['children'].append(element.getAttr('rid')) + + def apply_new(self): + for x in self.recieving.keys(): + if self.recieving[x]['type'] == 'element': + self.image.add_recieved(x, self.recieving) + + self.recieving = {} + +class SvgChooserDialog(FileChooserDialog): + def __init__(self, on_response_ok=None, on_response_cancel=None): + ''' + Choose in which SVG file to store the image + ''' + def on_ok(widget, callback): + ''' + check if file exists and call callback + ''' + path_to_file = self.get_filename() + path_to_file = gtkgui_helpers.decode_filechooser_file_paths( + (path_to_file,))[0] + widget.destroy() + callback(path_to_file) + + FileChooserDialog.__init__(self, + title_text=_('Save Image as...'), + action=gtk.FILE_CHOOSER_ACTION_SAVE, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, + gtk.RESPONSE_OK), + current_folder='', + default_response=gtk.RESPONSE_OK, + on_response_ok=(on_ok, on_response_ok), + on_response_cancel=on_response_cancel) + + filter_ = gtk.FileFilter() + filter_.set_name(_('All files')) + filter_.add_pattern('*') + self.add_filter(filter_) + + filter_ = gtk.FileFilter() + filter_.set_name(_('SVG Files')) + filter_.add_pattern('*.svg') + self.add_filter(filter_) + self.set_filter(filter_) + + +class SVGObject(): + ''' A class to store the svg document and make changes to it.''' + + def __init__(self, root, session, height=300, width=300): + # Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance + self.items = {} + self.root = root + self.draw_tool = 'brush' + + # sxe session + self.session = session + + # initialize svg document + self.svg = Node(node='<svg/>') + self.svg.setAttr('version', '1.1') + self.svg.setAttr('height', str(height)) + self.svg.setAttr('width', str(width)) + self.svg.setAttr('xmlns', 'http://www.w3.org/2000/svg') + # TODO: make this settable + self.g = self.svg.addChild(name='g') + self.g.setAttr('fill', 'none') + self.g.setAttr('stroke-linecap', 'round') + + def add_path(self, data, line_width, color): + ''' adds the path to the items listing, both minidom node and goocanvas + object in a tuple ''' + + goocanvas_obj = goocanvas.Path(parent=self.root, data=data, + line_width=line_width, stroke_color=color) + goocanvas_obj.connect('button-press-event', self.item_button_press_events) + + node = self.g.addChild(name='path') + node.setAttr('d', data) + node.setAttr('stroke-width', str(line_width)) + node.setAttr('stroke', color) + self.g.addChild(node=node) + + rids = self.session.generate_rids(4) + self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]} + self.items[rids[1]] = {'type':'attr', 'data':'d', 'parent':node} + self.items[rids[2]] = {'type':'attr', 'data':'stroke-width', 'parent':node} + self.items[rids[3]] = {'type':'attr', 'data':'stroke', 'parent':node} + + self.session.send_items(self.items, rids) + + def add_recieved(self, parent_rid, new_items): + ''' adds the path to the items listing, both minidom node and goocanvas + object in a tuple ''' + node = new_items[parent_rid]['data'][0] + + self.items[parent_rid] = new_items[parent_rid] + for x in new_items[parent_rid]['children']: + self.items[x] = new_items[x] + + if node.getName() == 'path': + goocanvas_obj = goocanvas.Path(parent=self.root, + data=node.getAttr('d'), + line_width=int(node.getAttr('stroke-width')), + stroke_color=node.getAttr('stroke')) + + if node.getName() == 'ellipse': + goocanvas_obj = goocanvas.Ellipse(parent=self.root, + center_x=float(node.getAttr('cx')), + center_y=float(node.getAttr('cy')), + radius_x=float(node.getAttr('rx')), + radius_y=float(node.getAttr('ry')), + stroke_color=node.getAttr('stroke'), + line_width=float(node.getAttr('stroke-width'))) + + self.items[parent_rid]['data'].append(goocanvas_obj) + goocanvas_obj.connect('button-press-event', self.item_button_press_events) + + def add_ellipse(self, cx, cy, rx, ry, line_width, stroke_color): + ''' adds the ellipse to the items listing, both minidom node and goocanvas + object in a tuple ''' + + goocanvas_obj = goocanvas.Ellipse(parent=self.root, + center_x=cx, + center_y=cy, + radius_x=rx, + radius_y=ry, + stroke_color=stroke_color, + line_width=line_width) + goocanvas_obj.connect('button-press-event', self.item_button_press_events) + + node = self.g.addChild(name='ellipse') + node.setAttr('cx', str(cx)) + node.setAttr('cy', str(cy)) + node.setAttr('rx', str(rx)) + node.setAttr('ry', str(ry)) + node.setAttr('stroke-width', str(line_width)) + node.setAttr('stroke', stroke_color) + self.g.addChild(node=node) + + rids = self.session.generate_rids(7) + self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]} + self.items[rids[1]] = {'type':'attr', 'data':'cx', 'parent':node} + self.items[rids[2]] = {'type':'attr', 'data':'cy', 'parent':node} + self.items[rids[3]] = {'type':'attr', 'data':'rx', 'parent':node} + self.items[rids[4]] = {'type':'attr', 'data':'ry', 'parent':node} + self.items[rids[5]] = {'type':'attr', 'data':'stroke-width', 'parent':node} + self.items[rids[6]] = {'type':'attr', 'data':'stroke', 'parent':node} + + self.session.send_items(self.items, rids) + + def del_item(self, item): + rids = [] + for x in self.items.keys(): + if self.items[x]['type'] == 'element': + if self.items[x]['data'][1] == item: + for y in self.items[x]['children']: + rids.append(y) + self.del_rid(y) + rids.append(x) + self.del_rid(x) + break + self.session.del_item(rids) + + def clear_canvas(self): + for x in self.items.keys(): + if self.items[x]['type'] == 'element': + self.del_rid(x) + + def del_rid(self, rid): + if self.items[rid]['type'] == 'element': + self.items[rid]['data'][1].remove() + del self.items[rid] + + def export_svg(self, filename): + f = open(filename, 'w') + f.writelines(str(self.svg)) + f.close() + + def item_button_press_events(self, item, target_item, event): + self.del_item(item) diff --git a/whiteboard/whiteboard_widget.ui b/whiteboard/whiteboard_widget.ui new file mode 100644 index 0000000..fddd2cb --- /dev/null +++ b/whiteboard/whiteboard_widget.ui @@ -0,0 +1,192 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkHBox" id="whiteboard_hbox"> + <property name="visible">True</property> + <property name="border_width">3</property> + <property name="spacing">6</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkVBox" id="vbuttonbox1"> + <property name="visible">True</property> + <property name="border_width">6</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkToggleButton" id="brush_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Brush Tool: Draw freehand lines</property> + <signal name="toggled" handler="on_brush_button_toggled"/> + <child> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="stock">brush_tool</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="oval_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Oval Tool: Draw circles and ellipses</property> + <signal name="toggled" handler="on_oval_button_toggled"/> + <child> + <object class="GtkImage" id="image6"> + <property name="visible">True</property> + <property name="stock">oval_tool</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="line_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Line Tool: Draw straight lines</property> + <signal name="toggled" handler="on_line_button_toggled"/> + <child> + <object class="GtkImage" id="image7"> + <property name="visible">True</property> + <property name="stock">line_tool</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="delete_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Delete Tool: Remove individual figures</property> + <signal name="toggled" handler="on_delete_button_toggled"/> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="stock">gtk-delete</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkButton" id="clear_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Clear Canvas: Cleanup canvas</property> + <signal name="clicked" handler="on_clear_button_clicked"/> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="stock">gtk-clear</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkButton" id="export_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Export Image: Save image to svg file</property> + <signal name="clicked" handler="on_export_button_clicked"/> + <child> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="stock">gtk-save-as</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkVScale" id="size_scale"> + <property name="height_request">68</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">Line width</property> + <property name="orientation">vertical</property> + <property name="adjustment">adjustment1</property> + <property name="inverted">True</property> + <property name="digits">0</property> + <property name="value_pos">bottom</property> + <signal name="value_changed" handler="on_size_scale_format_value"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="fg_color_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Foreground color</property> + <property name="color">#000000000000</property> + <signal name="color_set" handler="on_fg_color_button_color_set"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">7</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-delete</property> + </object> + <object class="GtkAdjustment" id="adjustment1"> + <property name="value">2</property> + <property name="lower">1</property> + <property name="upper">110</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <property name="page_size">10</property> + </object> +</interface> |