Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--banner_tweaks/__init__.py2
-rw-r--r--banner_tweaks/config_dialog.ui75
-rw-r--r--banner_tweaks/manifest.ini10
-rw-r--r--banner_tweaks/plugin.py194
-rw-r--r--google_translation/__init__.py1
-rw-r--r--google_translation/manifest.ini8
-rw-r--r--google_translation/plugin.py284
-rw-r--r--length_notifier/__init__.py2
-rw-r--r--length_notifier/config_dialog.ui152
-rw-r--r--length_notifier/length_notifier.py157
-rw-r--r--length_notifier/manifest.ini9
-rw-r--r--plugin_installer/__init__.py1
-rw-r--r--plugin_installer/config_dialog.ui294
-rw-r--r--plugin_installer/manifest.ini8
-rw-r--r--plugin_installer/plugin_installer.py532
-rw-r--r--triggers/__init__.py1
-rw-r--r--triggers/config_dialog.ui910
-rw-r--r--triggers/manifest.ini7
-rw-r--r--triggers/triggers.py677
-rw-r--r--whiteboard/__init__.py1
-rw-r--r--whiteboard/brush_tool.pngbin0 -> 806 bytes
-rw-r--r--whiteboard/line_tool.pngbin0 -> 1054 bytes
-rw-r--r--whiteboard/manifest.ini7
-rw-r--r--whiteboard/oval_tool.pngbin0 -> 989 bytes
-rw-r--r--whiteboard/plugin.py483
-rw-r--r--whiteboard/whiteboard.pngbin0 -> 1550 bytes
-rw-r--r--whiteboard/whiteboard_widget.py418
-rw-r--r--whiteboard/whiteboard_widget.ui192
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">&lt;empty&gt;</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">&lt;empty&gt;</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">&#x25CF;</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">&lt;b&gt;Conditions&lt;/b&gt;</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">&lt;b&gt;Actions&lt;/b&gt;</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">&lt;b&gt;Sounds&lt;/b&gt;</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
new file mode 100644
index 0000000..266c321
--- /dev/null
+++ b/whiteboard/brush_tool.png
Binary files differ
diff --git a/whiteboard/line_tool.png b/whiteboard/line_tool.png
new file mode 100644
index 0000000..151f584
--- /dev/null
+++ b/whiteboard/line_tool.png
Binary files differ
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
new file mode 100644
index 0000000..efd6f0c
--- /dev/null
+++ b/whiteboard/oval_tool.png
Binary files differ
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
new file mode 100644
index 0000000..13318e3
--- /dev/null
+++ b/whiteboard/whiteboard.png
Binary files differ
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>