fixes for when disabling emoticons in prefs [gajim did not boot], add_remove_emoticons_window and class, better popups [needs more attention {me is on it}], no more emoticons tab, introducing version.py, hide os information included, and fixes allover.
This commit is contained in:
parent
2640cac0f6
commit
1c7076cfb0
8 changed files with 923 additions and 747 deletions
|
@ -28,13 +28,12 @@ import common.jabber
|
||||||
import socket
|
import socket
|
||||||
import select
|
import select
|
||||||
import pickle
|
import pickle
|
||||||
|
import version
|
||||||
from tempfile import *
|
from tempfile import *
|
||||||
|
|
||||||
from common import i18n
|
from common import i18n
|
||||||
_ = i18n._
|
_ = i18n._
|
||||||
|
|
||||||
VERSION = '0.6.1'
|
|
||||||
|
|
||||||
log = logging.getLogger('core.core')
|
log = logging.getLogger('core.core')
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
@ -632,7 +631,7 @@ class GajimCore:
|
||||||
iq_obj.setType('result')
|
iq_obj.setType('result')
|
||||||
qp = iq_obj.getTag('query')
|
qp = iq_obj.getTag('query')
|
||||||
qp.insertTag('name').insertData('Gajim')
|
qp.insertTag('name').insertData('Gajim')
|
||||||
qp.insertTag('version').insertData(VERSION)
|
qp.insertTag('version').insertData(version.version)
|
||||||
qp.insertTag('os').insertData(get_os_info())
|
qp.insertTag('os').insertData(get_os_info())
|
||||||
con.send(iq_obj)
|
con.send(iq_obj)
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,18 @@ class Preferences_window:
|
||||||
|
|
||||||
def on_preferences_window_show(self, widget):
|
def on_preferences_window_show(self, widget):
|
||||||
self.notebook.set_current_page(0)
|
self.notebook.set_current_page(0)
|
||||||
|
|
||||||
|
def on_checkbutton_toggled(self, widget, config_name, \
|
||||||
|
extra_function = None, change_sensitivity_widgets = None):
|
||||||
|
if widget.get_active():
|
||||||
|
self.plugin.config[config_name] = 1
|
||||||
|
if extra_function != None:
|
||||||
|
apply(extra_function)
|
||||||
|
else:
|
||||||
|
self.plugin.config[config_name] = 0
|
||||||
|
if change_sensitivity_widgets != None:
|
||||||
|
for w in change_sensitivity_widgets:
|
||||||
|
w.set_sensitive(widget.get_active())
|
||||||
|
|
||||||
def on_tray_icon_checkbutton_toggled(self, widget):
|
def on_tray_icon_checkbutton_toggled(self, widget):
|
||||||
if widget.get_active():
|
if widget.get_active():
|
||||||
|
@ -313,39 +325,17 @@ class Preferences_window:
|
||||||
self.update_text_tags()
|
self.update_text_tags()
|
||||||
|
|
||||||
def on_use_emoticons_checkbutton_toggled(self, widget):
|
def on_use_emoticons_checkbutton_toggled(self, widget):
|
||||||
self.on_checkbutton_toggled(widget, 'useemoticons',\
|
self.on_checkbutton_toggled(widget, 'useemoticons', None, \
|
||||||
[self.xml.get_widget('button_new_emoticon'),\
|
self.xml.get_widget('add_remove_emoticons_button'))
|
||||||
self.xml.get_widget('button_remove_emoticon'),\
|
|
||||||
self.xml.get_widget('treeview_emoticons'),\
|
def on_add_remove_emoticons_button_clicked(self, widget):
|
||||||
self.xml.get_widget('set_image_button'),\
|
Add_remove_emoticons_window(self.plugin)
|
||||||
self.xml.get_widget('emoticons_image')])
|
|
||||||
|
|
||||||
def on_emoticons_treemodel_row_deleted(self, model, path):
|
def on_auto_popup_checkbutton_toggled(self, widget):
|
||||||
iter = model.get_iter_first()
|
|
||||||
emots = []
|
|
||||||
while iter:
|
|
||||||
emots.append(model.get_value(iter, 0))
|
|
||||||
emots.append(model.get_value(iter, 1))
|
|
||||||
iter = model.iter_next(iter)
|
|
||||||
self.plugin.config['emoticons'] = '\t'.join(emots)
|
|
||||||
self.plugin.init_regexp()
|
|
||||||
|
|
||||||
def on_emoticons_treemodel_row_changed(self, model, path, iter):
|
|
||||||
if model[path][1] != None and len(model[path][1]) != 0:
|
|
||||||
iter = model.get_iter_first()
|
|
||||||
emots = []
|
|
||||||
while iter:
|
|
||||||
emots.append(model.get_value(iter, 0))
|
|
||||||
emots.append(model.get_value(iter, 1))
|
|
||||||
iter = model.iter_next(iter)
|
|
||||||
self.plugin.config['emoticons'] = '\t'.join(emots)
|
|
||||||
self.plugin.init_regexp()
|
|
||||||
|
|
||||||
def on_auto_pop_up_checkbutton_toggled(self, widget):
|
|
||||||
self.on_checkbutton_toggled(widget, 'autopopup', None,\
|
self.on_checkbutton_toggled(widget, 'autopopup', None,\
|
||||||
[self.auto_pp_away_checkbutton])
|
[self.auto_popup_away_checkbutton])
|
||||||
|
|
||||||
def on_auto_pop_up_away_checkbutton_toggled(self, widget):
|
def on_auto_popup_away_checkbutton_toggled(self, widget):
|
||||||
self.on_checkbutton_toggled(widget, 'autopopupaway')
|
self.on_checkbutton_toggled(widget, 'autopopupaway')
|
||||||
|
|
||||||
def on_ignore_events_from_unknown_contacts_checkbutton_toggled(self, widget):
|
def on_ignore_events_from_unknown_contacts_checkbutton_toggled(self, widget):
|
||||||
|
@ -355,11 +345,9 @@ class Preferences_window:
|
||||||
self.plugin.config['soundplayer'] = widget.get_text()
|
self.plugin.config['soundplayer'] = widget.get_text()
|
||||||
|
|
||||||
def on_prompt_online_status_message_checkbutton_toggled(self, widget):
|
def on_prompt_online_status_message_checkbutton_toggled(self, widget):
|
||||||
"""On Prompt Online Status Message Checkbutton Toggled"""
|
|
||||||
self.on_checkbutton_toggled(widget, 'ask_online_status')
|
self.on_checkbutton_toggled(widget, 'ask_online_status')
|
||||||
|
|
||||||
def on_prompt_offline_status_message_checkbutton_toggled(self, widget):
|
def on_prompt_offline_status_message_checkbutton_toggled(self, widget):
|
||||||
"""On Prompt Offline Status Message Checkbutton Toggled"""
|
|
||||||
self.on_checkbutton_toggled(widget, 'ask_offline_status')
|
self.on_checkbutton_toggled(widget, 'ask_offline_status')
|
||||||
|
|
||||||
def on_sounds_treemodel_row_changed(self, model, path, iter):
|
def on_sounds_treemodel_row_changed(self, model, path, iter):
|
||||||
|
@ -454,6 +442,14 @@ class Preferences_window:
|
||||||
self.config_logger['lognotsep'] = 0
|
self.config_logger['lognotsep'] = 0
|
||||||
self.plugin.send('CONFIG', None, ('Logger', self.config_logger, 'GtkGui'))
|
self.plugin.send('CONFIG', None, ('Logger', self.config_logger, 'GtkGui'))
|
||||||
|
|
||||||
|
def on_do_not_send_os_info_checkbutton_toggled(self, widget):
|
||||||
|
if widget.get_active():
|
||||||
|
#FIXME: when threads are removed, make sure this work
|
||||||
|
self.plugin.config['do_not_send_os_info'] = 1
|
||||||
|
else:
|
||||||
|
self.plugin.config['do_not_send_os_info'] = 0
|
||||||
|
|
||||||
|
|
||||||
def fill_msg_treeview(self):
|
def fill_msg_treeview(self):
|
||||||
i = 0
|
i = 0
|
||||||
self.xml.get_widget('delete_msg_button').set_sensitive(False)
|
self.xml.get_widget('delete_msg_button').set_sensitive(False)
|
||||||
|
@ -502,138 +498,6 @@ class Preferences_window:
|
||||||
if event.keyval == gtk.keysyms.Delete:
|
if event.keyval == gtk.keysyms.Delete:
|
||||||
self.on_delete_msg_button_clicked(widget)
|
self.on_delete_msg_button_clicked(widget)
|
||||||
|
|
||||||
def image_is_ok(self, image):
|
|
||||||
if not os.path.exists(image):
|
|
||||||
return 0
|
|
||||||
img = gtk.Image()
|
|
||||||
try:
|
|
||||||
img.set_from_file(image)
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
if img.get_storage_type() == gtk.IMAGE_PIXBUF:
|
|
||||||
pix = img.get_pixbuf()
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
if pix.get_width() > 24 or pix.get_height() > 24:
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def load_emots(self):
|
|
||||||
emots = {}
|
|
||||||
split_line = self.plugin.config['emoticons'].split('\t')
|
|
||||||
for i in range(0, len(split_line)/2):
|
|
||||||
if not self.image_is_ok(split_line[2*i+1]):
|
|
||||||
continue
|
|
||||||
emots[split_line[2*i]] = split_line[2*i+1]
|
|
||||||
return emots
|
|
||||||
|
|
||||||
def fill_emot_treeview(self):
|
|
||||||
model = self.emot_tree.get_model()
|
|
||||||
model.clear()
|
|
||||||
emots = self.load_emots()
|
|
||||||
for i in emots:
|
|
||||||
file = emots[i]
|
|
||||||
iter = model.append((i, file, None))
|
|
||||||
if not os.path.exists(file):
|
|
||||||
continue
|
|
||||||
img = gtk.Image()
|
|
||||||
img.show()
|
|
||||||
if file.find('.gif') != -1:
|
|
||||||
pix = gtk.gdk.PixbufAnimation(file)
|
|
||||||
img.set_from_animation(pix)
|
|
||||||
else:
|
|
||||||
pix = gtk.gdk.pixbuf_new_from_file(file)
|
|
||||||
img.set_from_pixbuf(pix)
|
|
||||||
model.set(iter, 2, img)
|
|
||||||
|
|
||||||
def on_emot_cell_edited(self, cell, row, new_text):
|
|
||||||
model = self.emot_tree.get_model()
|
|
||||||
iter = model.get_iter_from_string(row)
|
|
||||||
model.set_value(iter, 0, new_text)
|
|
||||||
|
|
||||||
def on_set_image_button_clicked(self, widget, data=None):
|
|
||||||
(model, iter) = self.emot_tree.get_selection().get_selected()
|
|
||||||
if not iter:
|
|
||||||
return
|
|
||||||
file = model.get_value(iter, 1)
|
|
||||||
dialog = gtk.FileChooserDialog("Choose image",
|
|
||||||
None,
|
|
||||||
gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN, gtk.RESPONSE_OK))
|
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
filter = gtk.FileFilter()
|
|
||||||
filter.set_name("All files")
|
|
||||||
filter.add_pattern("*")
|
|
||||||
dialog.add_filter(filter)
|
|
||||||
|
|
||||||
filter = gtk.FileFilter()
|
|
||||||
filter.set_name("Images")
|
|
||||||
filter.add_mime_type("image/png")
|
|
||||||
filter.add_mime_type("image/jpeg")
|
|
||||||
filter.add_mime_type("image/gif")
|
|
||||||
filter.add_pattern("*.png")
|
|
||||||
filter.add_pattern("*.jpg")
|
|
||||||
filter.add_pattern("*.gif")
|
|
||||||
filter.add_pattern("*.tif")
|
|
||||||
filter.add_pattern("*.xpm")
|
|
||||||
dialog.add_filter(filter)
|
|
||||||
dialog.set_filter(filter)
|
|
||||||
|
|
||||||
file = os.path.join(os.getcwd(), file)
|
|
||||||
dialog.set_filename(file)
|
|
||||||
file = ''
|
|
||||||
ok = 0
|
|
||||||
while(ok == 0):
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
file = dialog.get_filename()
|
|
||||||
if self.image_is_ok(file):
|
|
||||||
ok = 1
|
|
||||||
else:
|
|
||||||
ok = 1
|
|
||||||
dialog.destroy()
|
|
||||||
if file:
|
|
||||||
model.set_value(iter, 1, file)
|
|
||||||
img = gtk.Image()
|
|
||||||
img.show()
|
|
||||||
if file.find('.gif') != -1:
|
|
||||||
pix = gtk.gdk.PixbufAnimation(file)
|
|
||||||
img.set_from_animation(pix)
|
|
||||||
else:
|
|
||||||
pix = gtk.gdk.pixbuf_new_from_file(file)
|
|
||||||
img.set_from_pixbuf(pix)
|
|
||||||
model.set(iter, 2, img)
|
|
||||||
|
|
||||||
def on_button_new_emoticon_clicked(self, widget, data=None):
|
|
||||||
model = self.emot_tree.get_model()
|
|
||||||
iter = model.append()
|
|
||||||
model.set(iter, 0, 'smeiley', 1, '')
|
|
||||||
col = self.emot_tree.get_column(0)
|
|
||||||
self.emot_tree.set_cursor(model.get_path(iter), col, True)
|
|
||||||
|
|
||||||
def on_button_remove_emoticon_clicked(self, widget, data=None):
|
|
||||||
(model, iter) = self.emot_tree.get_selection().get_selected()
|
|
||||||
if not iter:
|
|
||||||
return
|
|
||||||
model.remove(iter)
|
|
||||||
|
|
||||||
def on_checkbutton_toggled(self, widget, config_name, \
|
|
||||||
extra_function = None, change_sensitivity_widgets = None):
|
|
||||||
if widget.get_active():
|
|
||||||
self.plugin.config[config_name] = 1
|
|
||||||
if extra_function != None:
|
|
||||||
apply(extra_function)
|
|
||||||
else:
|
|
||||||
self.plugin.config[config_name] = 0
|
|
||||||
if change_sensitivity_widgets != None:
|
|
||||||
for w in change_sensitivity_widgets:
|
|
||||||
w.set_sensitive(widget.get_active())
|
|
||||||
|
|
||||||
def on_treeview_emoticons_key_press_event(self, widget, event):
|
|
||||||
if event.keyval == gtk.keysyms.Delete:
|
|
||||||
self.on_button_remove_emoticon_clicked(widget)
|
|
||||||
|
|
||||||
def sound_toggled_cb(self, cell, path):
|
def sound_toggled_cb(self, cell, path):
|
||||||
model = self.sound_tree.get_model()
|
model = self.sound_tree.get_model()
|
||||||
model[path][1] = not model[path][1]
|
model[path][1] = not model[path][1]
|
||||||
|
@ -707,12 +571,10 @@ class Preferences_window:
|
||||||
self.xml = gtk.glade.XML(GTKGUI_GLADE, 'preferences_window', APP)
|
self.xml = gtk.glade.XML(GTKGUI_GLADE, 'preferences_window', APP)
|
||||||
self.window = self.xml.get_widget('preferences_window')
|
self.window = self.xml.get_widget('preferences_window')
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.xml.get_widget('emoticons_image').set_from_file(\
|
|
||||||
'plugins/gtkgui/pixmaps/smile.png')
|
|
||||||
self.iconset_combobox = self.xml.get_widget('iconset_combobox')
|
self.iconset_combobox = self.xml.get_widget('iconset_combobox')
|
||||||
self.auto_pp_checkbutton = self.xml.get_widget('auto_pop_up_checkbutton')
|
self.auto_popup_checkbutton = self.xml.get_widget('auto_popup_checkbutton')
|
||||||
self.auto_pp_away_checkbutton = self.xml.get_widget \
|
self.auto_popup_away_checkbutton = self.xml.get_widget \
|
||||||
('auto_pop_up_away_checkbutton')
|
('auto_popup_away_checkbutton')
|
||||||
self.auto_away_checkbutton = self.xml.get_widget('auto_away_checkbutton')
|
self.auto_away_checkbutton = self.xml.get_widget('auto_away_checkbutton')
|
||||||
self.auto_away_time_spinbutton = self.xml.get_widget \
|
self.auto_away_time_spinbutton = self.xml.get_widget \
|
||||||
('auto_away_time_spinbutton')
|
('auto_away_time_spinbutton')
|
||||||
|
@ -840,39 +702,16 @@ class Preferences_window:
|
||||||
#Use emoticons
|
#Use emoticons
|
||||||
st = self.plugin.config['useemoticons']
|
st = self.plugin.config['useemoticons']
|
||||||
self.xml.get_widget('use_emoticons_checkbutton').set_active(st)
|
self.xml.get_widget('use_emoticons_checkbutton').set_active(st)
|
||||||
self.xml.get_widget('button_new_emoticon').set_sensitive(st)
|
self.xml.get_widget('add_remove_emoticons_button').set_sensitive(st)
|
||||||
self.xml.get_widget('button_remove_emoticon').set_sensitive(st)
|
|
||||||
self.xml.get_widget('treeview_emoticons').set_sensitive(st)
|
|
||||||
self.xml.get_widget('set_image_button').set_sensitive(st)
|
|
||||||
|
|
||||||
#emoticons
|
#autopopup
|
||||||
self.emot_tree = self.xml.get_widget('treeview_emoticons')
|
|
||||||
model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gtk.Image)
|
|
||||||
self.emot_tree.set_model(model)
|
|
||||||
col = gtk.TreeViewColumn(_('Name'))
|
|
||||||
self.emot_tree.append_column(col)
|
|
||||||
renderer = gtk.CellRendererText()
|
|
||||||
renderer.connect('edited', self.on_emot_cell_edited)
|
|
||||||
renderer.set_property('editable', True)
|
|
||||||
col.pack_start(renderer, True)
|
|
||||||
col.set_attributes(renderer, text=0)
|
|
||||||
|
|
||||||
col = gtk.TreeViewColumn(_('Image'))
|
|
||||||
self.emot_tree.append_column(col)
|
|
||||||
renderer = gtkgui.CellRendererImage()
|
|
||||||
col.pack_start(renderer, expand = False)
|
|
||||||
col.add_attribute(renderer, 'image', 2)
|
|
||||||
|
|
||||||
self.fill_emot_treeview()
|
|
||||||
|
|
||||||
#Autopopup
|
|
||||||
st = self.plugin.config['autopopup']
|
st = self.plugin.config['autopopup']
|
||||||
self.auto_pp_checkbutton.set_active(st)
|
self.auto_popup_checkbutton.set_active(st)
|
||||||
|
|
||||||
#Autopopupaway
|
#autopopupaway
|
||||||
st = self.plugin.config['autopopupaway']
|
st = self.plugin.config['autopopupaway']
|
||||||
self.auto_pp_away_checkbutton.set_active(st)
|
self.auto_popup_away_checkbutton.set_active(st)
|
||||||
self.auto_pp_away_checkbutton.set_sensitive(self.plugin.config['autopopup'])
|
self.auto_popup_away_checkbutton.set_sensitive(self.plugin.config['autopopup'])
|
||||||
|
|
||||||
#Ignore messages from unknown contacts
|
#Ignore messages from unknown contacts
|
||||||
self.xml.get_widget('ignore_events_from_unknown_contacts_checkbutton').\
|
self.xml.get_widget('ignore_events_from_unknown_contacts_checkbutton').\
|
||||||
|
@ -982,19 +821,18 @@ class Preferences_window:
|
||||||
st = self.config_logger['lognotsep']
|
st = self.config_logger['lognotsep']
|
||||||
self.xml.get_widget('log_in_extern_checkbutton').set_active(st)
|
self.xml.get_widget('log_in_extern_checkbutton').set_active(st)
|
||||||
|
|
||||||
self.emot_tree.get_model().connect('row-changed', \
|
# don't send os info
|
||||||
self.on_emoticons_treemodel_row_changed)
|
st = self.plugin.config['do_not_send_os_info']
|
||||||
self.emot_tree.get_model().connect('row-deleted', \
|
self.xml.get_widget('do_not_send_os_info_checkbutton').set_active(st)
|
||||||
self.on_emoticons_treemodel_row_deleted)
|
|
||||||
self.sound_tree.get_model().connect('row-changed', \
|
|
||||||
self.on_sounds_treemodel_row_changed)
|
|
||||||
self.msg_tree.get_model().connect('row-changed', \
|
|
||||||
self.on_msg_treemodel_row_changed)
|
|
||||||
self.msg_tree.get_model().connect('row-deleted', \
|
|
||||||
self.on_msg_treemodel_row_deleted)
|
|
||||||
|
|
||||||
#self.notebook.set_current_page(0)
|
|
||||||
self.xml.signal_autoconnect(self)
|
self.xml.signal_autoconnect(self)
|
||||||
|
|
||||||
|
self.sound_tree.get_model().connect('row-changed', \
|
||||||
|
self.on_sounds_treemodel_row_changed)
|
||||||
|
self.msg_tree.get_model().connect('row-changed', \
|
||||||
|
self.on_msg_treemodel_row_changed)
|
||||||
|
self.msg_tree.get_model().connect('row-deleted', \
|
||||||
|
self.on_msg_treemodel_row_deleted)
|
||||||
|
|
||||||
|
|
||||||
class Account_modification_window:
|
class Account_modification_window:
|
||||||
"""Class for account informations"""
|
"""Class for account informations"""
|
||||||
|
@ -1006,6 +844,11 @@ class Account_modification_window:
|
||||||
"""When Close button is clicked"""
|
"""When Close button is clicked"""
|
||||||
widget.get_toplevel().destroy()
|
widget.get_toplevel().destroy()
|
||||||
|
|
||||||
|
def on_checkbutton_toggled(self, widget, widgets):
|
||||||
|
"""set or unset sensitivity of widgets when widget is toggled"""
|
||||||
|
for w in widgets:
|
||||||
|
w.set_sensitive(widget.get_active())
|
||||||
|
|
||||||
def init_account(self, infos):
|
def init_account(self, infos):
|
||||||
"""Initialize window with defaults values"""
|
"""Initialize window with defaults values"""
|
||||||
if infos.has_key('accname'):
|
if infos.has_key('accname'):
|
||||||
|
@ -1315,11 +1158,6 @@ class Account_modification_window:
|
||||||
gpg_save_password_checkbutton.set_sensitive(True)
|
gpg_save_password_checkbutton.set_sensitive(True)
|
||||||
gpg_save_password_checkbutton.set_active(False)
|
gpg_save_password_checkbutton.set_active(False)
|
||||||
self.xml.get_widget('gpg_password_entry').set_text('')
|
self.xml.get_widget('gpg_password_entry').set_text('')
|
||||||
|
|
||||||
def on_checkbutton_toggled(self, widget, widgets):
|
|
||||||
"""set or unset sensitivity of widgets when widget is toggled"""
|
|
||||||
for w in widgets:
|
|
||||||
w.set_sensitive(widget.get_active())
|
|
||||||
|
|
||||||
def on_checkbutton_toggled_and_clear(self, widget, widgets):
|
def on_checkbutton_toggled_and_clear(self, widget, widgets):
|
||||||
self.on_checkbutton_toggled(widget, widgets)
|
self.on_checkbutton_toggled(widget, widgets)
|
||||||
|
@ -1519,6 +1357,189 @@ class Service_registration_window:
|
||||||
self.window.show_all()
|
self.window.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
class Add_remove_emoticons_window:
|
||||||
|
def __init__(self, plugin):
|
||||||
|
self.xml = gtk.glade.XML(GTKGUI_GLADE, 'add_remove_emoticons_window', APP)
|
||||||
|
self.window = self.xml.get_widget('add_remove_emoticons_window')
|
||||||
|
self.plugin = plugin
|
||||||
|
|
||||||
|
#emoticons
|
||||||
|
self.emot_tree = self.xml.get_widget('emoticons_treeview')
|
||||||
|
model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gtk.Image)
|
||||||
|
self.emot_tree.set_model(model)
|
||||||
|
col = gtk.TreeViewColumn(_('Name'))
|
||||||
|
self.emot_tree.append_column(col)
|
||||||
|
renderer = gtk.CellRendererText()
|
||||||
|
renderer.connect('edited', self.on_emot_cell_edited)
|
||||||
|
renderer.set_property('editable', True)
|
||||||
|
col.pack_start(renderer, True)
|
||||||
|
col.set_attributes(renderer, text=0)
|
||||||
|
|
||||||
|
col = gtk.TreeViewColumn(_('Image'))
|
||||||
|
self.emot_tree.append_column(col)
|
||||||
|
renderer = gtkgui.CellRendererImage()
|
||||||
|
col.pack_start(renderer, expand = False)
|
||||||
|
col.add_attribute(renderer, 'image', 2)
|
||||||
|
|
||||||
|
self.fill_emot_treeview()
|
||||||
|
self.emot_tree.get_model().connect('row-changed', \
|
||||||
|
self.on_emoticons_treemodel_row_changed)
|
||||||
|
self.emot_tree.get_model().connect('row-deleted', \
|
||||||
|
self.on_emoticons_treemodel_row_deleted)
|
||||||
|
|
||||||
|
self.plugin = plugin
|
||||||
|
self.xml.signal_autoconnect(self)
|
||||||
|
self.window.show_all()
|
||||||
|
|
||||||
|
def on_add_remove_emoticons_window_delete_event(self, widget, event):
|
||||||
|
self.window.hide()
|
||||||
|
return True # do NOT destroy the window
|
||||||
|
|
||||||
|
def on_close_button_clicked(self, widget):
|
||||||
|
self.window.hide()
|
||||||
|
|
||||||
|
def on_emoticons_treemodel_row_deleted(self, model, path):
|
||||||
|
iter = model.get_iter_first()
|
||||||
|
emots = []
|
||||||
|
while iter:
|
||||||
|
emots.append(model.get_value(iter, 0))
|
||||||
|
emots.append(model.get_value(iter, 1))
|
||||||
|
iter = model.iter_next(iter)
|
||||||
|
self.plugin.config['emoticons'] = '\t'.join(emots)
|
||||||
|
self.plugin.init_regexp()
|
||||||
|
|
||||||
|
def on_emoticons_treemodel_row_changed(self, model, path, iter):
|
||||||
|
if model[path][1] != None and len(model[path][1]) != 0:
|
||||||
|
iter = model.get_iter_first()
|
||||||
|
emots = []
|
||||||
|
while iter:
|
||||||
|
emots.append(model.get_value(iter, 0))
|
||||||
|
emots.append(model.get_value(iter, 1))
|
||||||
|
iter = model.iter_next(iter)
|
||||||
|
self.plugin.config['emoticons'] = '\t'.join(emots)
|
||||||
|
self.plugin.init_regexp()
|
||||||
|
|
||||||
|
def image_is_ok(self, image):
|
||||||
|
if not os.path.exists(image):
|
||||||
|
return 0
|
||||||
|
img = gtk.Image()
|
||||||
|
try:
|
||||||
|
img.set_from_file(image)
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
if img.get_storage_type() == gtk.IMAGE_PIXBUF:
|
||||||
|
pix = img.get_pixbuf()
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
if pix.get_width() > 24 or pix.get_height() > 24:
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def load_emots(self):
|
||||||
|
emots = {}
|
||||||
|
split_line = self.plugin.config['emoticons'].split('\t')
|
||||||
|
for i in range(0, len(split_line)/2):
|
||||||
|
if not self.image_is_ok(split_line[2*i+1]):
|
||||||
|
continue
|
||||||
|
emots[split_line[2*i]] = split_line[2*i+1]
|
||||||
|
return emots
|
||||||
|
|
||||||
|
def fill_emot_treeview(self):
|
||||||
|
model = self.emot_tree.get_model()
|
||||||
|
model.clear()
|
||||||
|
emots = self.load_emots()
|
||||||
|
for i in emots:
|
||||||
|
file = emots[i]
|
||||||
|
iter = model.append((i, file, None))
|
||||||
|
if not os.path.exists(file):
|
||||||
|
continue
|
||||||
|
img = gtk.Image()
|
||||||
|
img.show()
|
||||||
|
if file.find('.gif') != -1:
|
||||||
|
pix = gtk.gdk.PixbufAnimation(file)
|
||||||
|
img.set_from_animation(pix)
|
||||||
|
else:
|
||||||
|
pix = gtk.gdk.pixbuf_new_from_file(file)
|
||||||
|
img.set_from_pixbuf(pix)
|
||||||
|
model.set(iter, 2, img)
|
||||||
|
|
||||||
|
def on_emot_cell_edited(self, cell, row, new_text):
|
||||||
|
model = self.emot_tree.get_model()
|
||||||
|
iter = model.get_iter_from_string(row)
|
||||||
|
model.set_value(iter, 0, new_text)
|
||||||
|
|
||||||
|
def on_set_image_button_clicked(self, widget, data=None):
|
||||||
|
(model, iter) = self.emot_tree.get_selection().get_selected()
|
||||||
|
if not iter:
|
||||||
|
return
|
||||||
|
file = model.get_value(iter, 1)
|
||||||
|
dialog = gtk.FileChooserDialog("Choose image",
|
||||||
|
None,
|
||||||
|
gtk.FILE_CHOOSER_ACTION_OPEN,
|
||||||
|
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
||||||
|
gtk.STOCK_OPEN, gtk.RESPONSE_OK))
|
||||||
|
dialog.set_default_response(gtk.RESPONSE_OK)
|
||||||
|
filter = gtk.FileFilter()
|
||||||
|
filter.set_name("All files")
|
||||||
|
filter.add_pattern("*")
|
||||||
|
dialog.add_filter(filter)
|
||||||
|
|
||||||
|
filter = gtk.FileFilter()
|
||||||
|
filter.set_name("Images")
|
||||||
|
filter.add_mime_type("image/png")
|
||||||
|
filter.add_mime_type("image/jpeg")
|
||||||
|
filter.add_mime_type("image/gif")
|
||||||
|
filter.add_pattern("*.png")
|
||||||
|
filter.add_pattern("*.jpg")
|
||||||
|
filter.add_pattern("*.gif")
|
||||||
|
filter.add_pattern("*.tif")
|
||||||
|
filter.add_pattern("*.xpm")
|
||||||
|
dialog.add_filter(filter)
|
||||||
|
dialog.set_filter(filter)
|
||||||
|
|
||||||
|
file = os.path.join(os.getcwd(), file)
|
||||||
|
dialog.set_filename(file)
|
||||||
|
file = ''
|
||||||
|
ok = 0
|
||||||
|
while(ok == 0):
|
||||||
|
response = dialog.run()
|
||||||
|
if response == gtk.RESPONSE_OK:
|
||||||
|
file = dialog.get_filename()
|
||||||
|
if self.image_is_ok(file):
|
||||||
|
ok = 1
|
||||||
|
else:
|
||||||
|
ok = 1
|
||||||
|
dialog.destroy()
|
||||||
|
if file:
|
||||||
|
model.set_value(iter, 1, file)
|
||||||
|
img = gtk.Image()
|
||||||
|
img.show()
|
||||||
|
if file.find('.gif') != -1:
|
||||||
|
pix = gtk.gdk.PixbufAnimation(file)
|
||||||
|
img.set_from_animation(pix)
|
||||||
|
else:
|
||||||
|
pix = gtk.gdk.pixbuf_new_from_file(file)
|
||||||
|
img.set_from_pixbuf(pix)
|
||||||
|
model.set(iter, 2, img)
|
||||||
|
|
||||||
|
def on_button_new_emoticon_clicked(self, widget, data=None):
|
||||||
|
model = self.emot_tree.get_model()
|
||||||
|
iter = model.append()
|
||||||
|
model.set(iter, 0, 'emoticon', 1, '')
|
||||||
|
col = self.emot_tree.get_column(0)
|
||||||
|
self.emot_tree.set_cursor(model.get_path(iter), col, True)
|
||||||
|
|
||||||
|
def on_button_remove_emoticon_clicked(self, widget, data=None):
|
||||||
|
(model, iter) = self.emot_tree.get_selection().get_selected()
|
||||||
|
if not iter:
|
||||||
|
return
|
||||||
|
model.remove(iter)
|
||||||
|
|
||||||
|
def on_emoticons_treeview_key_press_event(self, widget, event):
|
||||||
|
if event.keyval == gtk.keysyms.Delete:
|
||||||
|
self.on_button_remove_emoticon_clicked(widget)
|
||||||
|
|
||||||
|
|
||||||
class Service_discovery_window:
|
class Service_discovery_window:
|
||||||
"""Class for Service Discovery Window:
|
"""Class for Service Discovery Window:
|
||||||
to know the services on the selected server"""
|
to know the services on the selected server"""
|
||||||
|
|
|
@ -27,6 +27,7 @@ gtk.glade.bindtextdomain (APP, i18n.DIR)
|
||||||
gtk.glade.textdomain (APP)
|
gtk.glade.textdomain (APP)
|
||||||
|
|
||||||
import gtkgui
|
import gtkgui
|
||||||
|
import version
|
||||||
|
|
||||||
GTKGUI_GLADE='plugins/gtkgui/gtkgui.glade'
|
GTKGUI_GLADE='plugins/gtkgui/gtkgui.glade'
|
||||||
|
|
||||||
|
@ -555,7 +556,7 @@ class About_dialog:
|
||||||
|
|
||||||
dlg = gtk.AboutDialog()
|
dlg = gtk.AboutDialog()
|
||||||
dlg.set_name('Gajim')
|
dlg.set_name('Gajim')
|
||||||
dlg.set_version('0.6.1')
|
dlg.set_version(version.version)
|
||||||
s = u'Copyright \xa9 2003-2005 Gajim Team'
|
s = u'Copyright \xa9 2003-2005 Gajim Team'
|
||||||
dlg.set_copyright(s)
|
dlg.set_copyright(s)
|
||||||
text = open('COPYING').read()
|
text = open('COPYING').read()
|
||||||
|
@ -778,21 +779,32 @@ class Change_password_dialog:
|
||||||
return message
|
return message
|
||||||
|
|
||||||
class Popup_window:
|
class Popup_window:
|
||||||
def __init__(self, plugin=None, account=None):
|
def __init__(self, plugin, event_type, event_desc):
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.account = account
|
|
||||||
|
|
||||||
xml = gtk.glade.XML(GTKGUI_GLADE, 'popup_window', APP)
|
xml = gtk.glade.XML(GTKGUI_GLADE, 'popup_window', APP)
|
||||||
self.window = xml.get_widget('popup_window')
|
self.window = xml.get_widget('popup_window')
|
||||||
close_button = xml.get_widget('close_button')
|
close_button = xml.get_widget('close_button')
|
||||||
event_label = xml.get_widget('event_label')
|
event_type_label = xml.get_widget('event_type_label')
|
||||||
|
event_description_label = xml.get_widget('event_description_label')
|
||||||
|
eventbox = xml.get_widget('eventbox')
|
||||||
|
|
||||||
event_label.set_text(str(len(self.plugin.roster.popup_windows)))
|
event_type_label.set_markup('<b>'+event_type+'</b>')
|
||||||
|
event_description_label.set_text(event_desc)
|
||||||
self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('green'))
|
|
||||||
|
|
||||||
|
# set colors [ http://www.w3schools.com/html/html_colornames.asp ]
|
||||||
|
self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('black'))
|
||||||
|
if event_type == 'Contact Online':
|
||||||
|
close_button.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('forestgreen'))
|
||||||
|
eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('forestgreen'))
|
||||||
|
elif event_type == 'Contact Offline':
|
||||||
|
close_button.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('firebrick'))
|
||||||
|
eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('firebrick'))
|
||||||
|
elif event_type == 'New Message':
|
||||||
|
close_button.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('dodgerblue'))
|
||||||
|
eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('dodgerblue'))
|
||||||
|
|
||||||
# position the window to bottom-right of screen
|
# position the window to bottom-right of screen
|
||||||
gtk.gdk.flush()
|
|
||||||
window_width, window_height = self.window.get_size()
|
window_width, window_height = self.window.get_size()
|
||||||
self.plugin.roster.popups_height += window_height
|
self.plugin.roster.popups_height += window_height
|
||||||
self.window.move(gtk.gdk.screen_width() - window_width, \
|
self.window.move(gtk.gdk.screen_width() - window_width, \
|
||||||
|
@ -805,24 +817,19 @@ class Popup_window:
|
||||||
gobject.timeout_add(5000, self.on_timeout, window_height)
|
gobject.timeout_add(5000, self.on_timeout, window_height)
|
||||||
|
|
||||||
def on_close_button_clicked(self, widget, window_height):
|
def on_close_button_clicked(self, widget, window_height):
|
||||||
print 'window h', window_height
|
|
||||||
self.adjust_height_and_move_popup_windows(window_height)
|
self.adjust_height_and_move_popup_windows(window_height)
|
||||||
|
|
||||||
def on_timeout(self, window_height):
|
def on_timeout(self, window_height):
|
||||||
self.adjust_height_and_move_popup_windows(window_height)
|
self.adjust_height_and_move_popup_windows(window_height)
|
||||||
print 'window h', window_height
|
|
||||||
|
|
||||||
def adjust_height_and_move_popup_windows(self, window_height):
|
def adjust_height_and_move_popup_windows(self, window_height):
|
||||||
#remove
|
#remove
|
||||||
print 'self.plugin.roster.popups_height before', self.plugin.roster.popups_height
|
|
||||||
self.plugin.roster.popups_height -= window_height
|
self.plugin.roster.popups_height -= window_height
|
||||||
print 'self.plugin.roster.popups_height now', self.plugin.roster.popups_height
|
|
||||||
print 'removing', self.window
|
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
|
|
||||||
if len(self.plugin.roster.popup_windows) > 0:
|
if len(self.plugin.roster.popup_windows) > 0:
|
||||||
# we want to remove the first window added in the list
|
# we want to remove the first window added in the list
|
||||||
self.plugin.roster.popup_windows.pop(0) # remove
|
self.plugin.roster.popup_windows.pop(0) # remove first item
|
||||||
|
|
||||||
# move the rest of popup windows
|
# move the rest of popup windows
|
||||||
self.plugin.roster.popups_height = 0
|
self.plugin.roster.popups_height = 0
|
||||||
|
@ -831,3 +838,6 @@ class Popup_window:
|
||||||
self.plugin.roster.popups_height += window_height
|
self.plugin.roster.popups_height += window_height
|
||||||
window_instance.window.move(gtk.gdk.screen_width() - window_width, \
|
window_instance.window.move(gtk.gdk.screen_width() - window_width, \
|
||||||
gtk.gdk.screen_height() - self.plugin.roster.popups_height)
|
gtk.gdk.screen_height() - self.plugin.roster.popups_height)
|
||||||
|
|
||||||
|
def on_popup_window_button_press_event(self, widget, event):
|
||||||
|
print 'IN YOUR DREAMS ONLY..'
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -325,7 +325,7 @@ class plugin:
|
||||||
def handle_event_error(self, unused, msg):
|
def handle_event_error(self, unused, msg):
|
||||||
Error_dialog(msg)
|
Error_dialog(msg)
|
||||||
|
|
||||||
def handle_event_status(self, account, status):
|
def handle_event_status(self, account, status): # OUR status
|
||||||
#('STATUS', account, status)
|
#('STATUS', account, status)
|
||||||
self.roster.on_status_changed(account, status)
|
self.roster.on_status_changed(account, status)
|
||||||
|
|
||||||
|
@ -398,9 +398,19 @@ class plugin:
|
||||||
if old_show < 2 and statuss.index(user1.show) > 1 and \
|
if old_show < 2 and statuss.index(user1.show) > 1 and \
|
||||||
self.config['sound_contact_connected']:
|
self.config['sound_contact_connected']:
|
||||||
self.play_sound('sound_contact_connected')
|
self.play_sound('sound_contact_connected')
|
||||||
|
if not self.windows[account]['chats'].has_key(jid) and \
|
||||||
|
not self.queues[account].has_key(jid) and \
|
||||||
|
not self.config['autopopup']:
|
||||||
|
instance = Popup_window(self, 'Contact Online', jid )
|
||||||
|
self.roster.popup_windows.append(instance)
|
||||||
elif old_show > 1 and statuss.index(user1.show) < 2 and \
|
elif old_show > 1 and statuss.index(user1.show) < 2 and \
|
||||||
self.config['sound_contact_disconnected']:
|
self.config['sound_contact_disconnected']:
|
||||||
self.play_sound('sound_contact_disconnected')
|
self.play_sound('sound_contact_disconnected')
|
||||||
|
if not self.windows[account]['chats'].has_key(jid) and \
|
||||||
|
not self.queues[account].has_key(jid) and \
|
||||||
|
not self.config['autopopup']:
|
||||||
|
instance = Popup_window(self, 'Contact Offline', jid )
|
||||||
|
self.roster.popup_windows.append(instance)
|
||||||
|
|
||||||
elif self.windows[account]['gc'].has_key(ji):
|
elif self.windows[account]['gc'].has_key(ji):
|
||||||
#it is a groupchat presence
|
#it is a groupchat presence
|
||||||
|
@ -416,10 +426,14 @@ class plugin:
|
||||||
if self.config['ignore_unknown_contacts'] and \
|
if self.config['ignore_unknown_contacts'] and \
|
||||||
not self.roster.contacts[account].has_key(jid):
|
not self.roster.contacts[account].has_key(jid):
|
||||||
return
|
return
|
||||||
first = 0
|
|
||||||
|
first = False
|
||||||
if not self.windows[account]['chats'].has_key(jid) and \
|
if not self.windows[account]['chats'].has_key(jid) and \
|
||||||
not self.queues[account].has_key(jid):
|
not self.queues[account].has_key(jid):
|
||||||
first = 1
|
first = True
|
||||||
|
if not self.config['autopopup']:
|
||||||
|
instance = Popup_window(self, 'New Message', 'From '+ jid )
|
||||||
|
self.roster.popup_windows.append(instance)
|
||||||
self.roster.on_message(jid, array[1], array[2], account)
|
self.roster.on_message(jid, array[1], array[2], account)
|
||||||
if self.config['sound_first_message_received'] and first:
|
if self.config['sound_first_message_received'] and first:
|
||||||
self.play_sound('sound_first_message_received')
|
self.play_sound('sound_first_message_received')
|
||||||
|
@ -772,7 +786,7 @@ class plugin:
|
||||||
self.basic_pattern_re = sre.compile(basic_pattern, sre.IGNORECASE)
|
self.basic_pattern_re = sre.compile(basic_pattern, sre.IGNORECASE)
|
||||||
|
|
||||||
emoticons_pattern = ''
|
emoticons_pattern = ''
|
||||||
for emoticon in self.emoticons: # travel tru emoticons list
|
for emoticon in self.emoticons: # travel thru emoticons list
|
||||||
emoticon_escaped = sre.escape(emoticon) # espace regexp metachars
|
emoticon_escaped = sre.escape(emoticon) # espace regexp metachars
|
||||||
emoticons_pattern += emoticon_escaped + '|'# | means or in regexp
|
emoticons_pattern += emoticon_escaped + '|'# | means or in regexp
|
||||||
|
|
||||||
|
@ -787,17 +801,17 @@ class plugin:
|
||||||
self.launch_browser_mailer(kind, url)
|
self.launch_browser_mailer(kind, url)
|
||||||
|
|
||||||
def init_regexp(self):
|
def init_regexp(self):
|
||||||
if self.config['useemoticons']:
|
#initialize emoticons dictionary
|
||||||
"""initialize emoticons dictionary"""
|
self.emoticons = dict()
|
||||||
self.emoticons = dict()
|
split_line = self.config['emoticons'].split('\t')
|
||||||
split_line = self.config['emoticons'].split('\t')
|
for i in range(0, len(split_line)/2):
|
||||||
for i in range(0, len(split_line)/2):
|
emot_file = split_line[2*i+1]
|
||||||
emot_file = split_line[2*i+1]
|
if not self.image_is_ok(emot_file):
|
||||||
if not self.image_is_ok(emot_file):
|
continue
|
||||||
continue
|
pix = gtk.gdk.pixbuf_new_from_file(emot_file)
|
||||||
pix = gtk.gdk.pixbuf_new_from_file(emot_file)
|
self.emoticons[split_line[2*i]] = pix
|
||||||
self.emoticons[split_line[2*i]] = pix
|
|
||||||
|
# update regular expressions
|
||||||
self.make_regexps()
|
self.make_regexps()
|
||||||
|
|
||||||
def __init__(self, quIN, quOUT):
|
def __init__(self, quIN, quOUT):
|
||||||
|
@ -814,8 +828,8 @@ class plugin:
|
||||||
'MYVCARD', 'VCARD', 'LOG_NB_LINE', 'LOG_LINE', 'VISUAL', 'GC_MSG', \
|
'MYVCARD', 'VCARD', 'LOG_NB_LINE', 'LOG_LINE', 'VISUAL', 'GC_MSG', \
|
||||||
'GC_SUBJECT', 'BAD_PASSPHRASE', 'GPG_SECRETE_KEYS', 'ROSTER_INFO', \
|
'GC_SUBJECT', 'BAD_PASSPHRASE', 'GPG_SECRETE_KEYS', 'ROSTER_INFO', \
|
||||||
'MSGSENT'])
|
'MSGSENT'])
|
||||||
self.default_config = {'autopopup':1,\
|
self.default_config = {'autopopup':0,\
|
||||||
'autopopupaway':1,\
|
'autopopupaway':0,\
|
||||||
'ignore_unknown_contacts':0,\
|
'ignore_unknown_contacts':0,\
|
||||||
'showoffline':0,\
|
'showoffline':0,\
|
||||||
'autoaway':1,\
|
'autoaway':1,\
|
||||||
|
@ -882,6 +896,7 @@ class plugin:
|
||||||
'after_time': ']',\
|
'after_time': ']',\
|
||||||
'before_nickname': '<',\
|
'before_nickname': '<',\
|
||||||
'after_nickname': '>',\
|
'after_nickname': '>',\
|
||||||
|
'do_not_send_os_info': 0,\
|
||||||
}
|
}
|
||||||
self.send('ASK_CONFIG', None, ('GtkGui', 'GtkGui', self.default_config))
|
self.send('ASK_CONFIG', None, ('GtkGui', 'GtkGui', self.default_config))
|
||||||
self.config = self.wait('CONFIG')
|
self.config = self.wait('CONFIG')
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 770 B |
|
@ -917,8 +917,9 @@ class Roster_window:
|
||||||
New_message_dialog(self.plugin, account)
|
New_message_dialog(self.plugin, account)
|
||||||
|
|
||||||
def on_about_menuitem_activate(self, widget):
|
def on_about_menuitem_activate(self, widget):
|
||||||
#About_dialog()
|
About_dialog()
|
||||||
self.popup_windows.append( Popup_window(self.plugin) )
|
#inst = Popup_window(self.plugin, 'Fake Message', 'nkour@')
|
||||||
|
#self.popup_windows.append( inst )
|
||||||
|
|
||||||
def on_accounts_menuitem_activate(self, widget):
|
def on_accounts_menuitem_activate(self, widget):
|
||||||
if self.plugin.windows.has_key('accounts'):
|
if self.plugin.windows.has_key('accounts'):
|
||||||
|
|
1
version.py
Normal file
1
version.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
version='0.7'
|
Loading…
Add table
Reference in a new issue