[kepi] ability to use subkeys. Fixes #6051
[gajim.git] / src / message_textview.py
blob70cf3afea6d9e8f377c4e13c34bfaa1eae7c1387
1 # -*- coding:utf-8 -*-
2 ## src/message_textview.py
3 ##
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
6 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
7 ## Copyright (C) 2008-2009 Julien Pivotto <roidelapluie AT gmail.com>
8 ##
9 ## This file is part of Gajim.
11 ## Gajim is free software; you can redistribute it and/or modify
12 ## it under the terms of the GNU General Public License as published
13 ## by the Free Software Foundation; version 3 only.
15 ## Gajim is distributed in the hope that it will be useful,
16 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ## GNU General Public License for more details.
20 ## You should have received a copy of the GNU General Public License
21 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
24 import gc
26 import gtk
27 import gobject
28 import pango
30 import gtkgui_helpers
31 from common import gajim
33 class MessageTextView(gtk.TextView):
34 """
35 Class for the message textview (where user writes new messages) for
36 chat/groupchat windows
37 """
38 UNDO_LIMIT = 20
39 __gsignals__ = dict(
40 mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION,
41 None, # return value
42 (int, gtk.gdk.ModifierType ) # arguments
46 def __init__(self):
47 gtk.TextView.__init__(self)
49 # set properties
50 self.set_border_width(1)
51 self.set_accepts_tab(True)
52 self.set_editable(True)
53 self.set_cursor_visible(True)
54 self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
55 self.set_left_margin(2)
56 self.set_right_margin(2)
57 self.set_pixels_above_lines(2)
58 self.set_pixels_below_lines(2)
60 # set undo list
61 self.undo_list = []
62 # needed to know if we undid something
63 self.undo_pressed = False
64 self.lang = None # Lang used for spell checking
65 _buffer = self.get_buffer()
66 self.begin_tags = {}
67 self.end_tags = {}
68 self.color_tags = []
69 self.fonts_tags = []
70 self.other_tags = {}
71 self.other_tags['bold'] = _buffer.create_tag('bold')
72 self.other_tags['bold'].set_property('weight', pango.WEIGHT_BOLD)
73 self.begin_tags['bold'] = '<strong>'
74 self.end_tags['bold'] = '</strong>'
75 self.other_tags['italic'] = _buffer.create_tag('italic')
76 self.other_tags['italic'].set_property('style', pango.STYLE_ITALIC)
77 self.begin_tags['italic'] = '<em>'
78 self.end_tags['italic'] = '</em>'
79 self.other_tags['underline'] = _buffer.create_tag('underline')
80 self.other_tags['underline'].set_property('underline', pango.UNDERLINE_SINGLE)
81 self.begin_tags['underline'] = '<span style="text-decoration: underline;">'
82 self.end_tags['underline'] = '</span>'
83 self.other_tags['strike'] = _buffer.create_tag('strike')
84 self.other_tags['strike'].set_property('strikethrough', True)
85 self.begin_tags['strike'] = '<span style="text-decoration: line-through;">'
86 self.end_tags['strike'] = '</span>'
88 def make_clickable_urls(self, text):
89 _buffer = self.get_buffer()
91 start = 0
92 end = 0
93 index = 0
95 new_text = ''
96 iterator = gajim.interface.link_pattern_re.finditer(text)
97 for match in iterator:
98 start, end = match.span()
99 url = text[start:end]
100 if start != 0:
101 text_before_special_text = text[index:start]
102 else:
103 text_before_special_text = ''
104 # we insert normal text
105 new_text += text_before_special_text + \
106 '<a href="'+ url +'">' + url + '</a>'
108 index = end # update index
110 if end < len(text):
111 new_text += text[end:]
113 return new_text # the position after *last* special text
115 def get_active_tags(self):
116 start, finish = self.get_active_iters()
117 active_tags = []
118 for tag in start.get_tags():
119 active_tags.append(tag.get_property('name'))
120 return active_tags
122 def get_active_iters(self):
123 _buffer = self.get_buffer()
124 return_val = _buffer.get_selection_bounds()
125 if return_val: # if sth was selected
126 start, finish = return_val[0], return_val[1]
127 else:
128 start, finish = _buffer.get_bounds()
129 return (start, finish)
131 def set_tag(self, widget, tag):
132 _buffer = self.get_buffer()
133 start, finish = self.get_active_iters()
134 if start.has_tag(self.other_tags[tag]):
135 _buffer.remove_tag_by_name(tag, start, finish)
136 else:
137 if tag == 'underline':
138 _buffer.remove_tag_by_name('strike', start, finish)
139 elif tag == 'strike':
140 _buffer.remove_tag_by_name('underline', start, finish)
141 _buffer.apply_tag_by_name(tag, start, finish)
143 def clear_tags(self, widget):
144 _buffer = self.get_buffer()
145 start, finish = self.get_active_iters()
146 _buffer.remove_all_tags(start, finish)
148 def color_set(self, widget, response, color):
149 if response == -6:
150 widget.destroy()
151 return
152 _buffer = self.get_buffer()
153 color = color.get_current_color()
154 widget.destroy()
155 color_string = gtkgui_helpers.make_color_string(color)
156 tag_name = 'color' + color_string
157 if not tag_name in self.color_tags:
158 tagColor = _buffer.create_tag(tag_name)
159 tagColor.set_property('foreground', color_string)
160 self.begin_tags[tag_name] = '<span style="color: ' + color_string + ';">'
161 self.end_tags[tag_name] = '</span>'
162 self.color_tags.append(tag_name)
164 start, finish = self.get_active_iters()
166 for tag in self.color_tags:
167 _buffer.remove_tag_by_name(tag, start, finish)
169 _buffer.apply_tag_by_name(tag_name, start, finish)
171 def font_set(self, widget, response, font):
172 if response == -6:
173 widget.destroy()
174 return
176 _buffer = self.get_buffer()
178 font = font.get_font_name()
179 font_desc = pango.FontDescription(font)
180 family = font_desc.get_family()
181 size = font_desc.get_size()
182 size = size / pango.SCALE
183 weight = font_desc.get_weight()
184 style = font_desc.get_style()
186 widget.destroy()
188 tag_name = 'font' + font
189 if not tag_name in self.fonts_tags:
190 tagFont = _buffer.create_tag(tag_name)
191 tagFont.set_property('font', family + ' ' + str(size))
192 self.begin_tags[tag_name] = \
193 '<span style="font-family: ' + family + '; ' + \
194 'font-size: ' + str(size) + 'px">'
195 self.end_tags[tag_name] = '</span>'
196 self.fonts_tags.append(tag_name)
198 start, finish = self.get_active_iters()
200 for tag in self.fonts_tags:
201 _buffer.remove_tag_by_name(tag, start, finish)
203 _buffer.apply_tag_by_name(tag_name, start, finish)
205 if weight == pango.WEIGHT_BOLD:
206 _buffer.apply_tag_by_name('bold', start, finish)
207 else:
208 _buffer.remove_tag_by_name('bold', start, finish)
210 if style == pango.STYLE_ITALIC:
211 _buffer.apply_tag_by_name('italic', start, finish)
212 else:
213 _buffer.remove_tag_by_name('italic', start, finish)
215 def get_xhtml(self):
216 _buffer = self.get_buffer()
217 old = _buffer.get_start_iter()
218 tags = {}
219 tags['bold'] = False
220 iter = _buffer.get_start_iter()
221 old = _buffer.get_start_iter()
222 text = ''
223 modified = False
225 def xhtml_special(text):
226 text = text.replace('<', '&lt;')
227 text = text.replace('>', '&gt;')
228 text = text.replace('&', '&amp;')
229 text = text.replace('\n', '<br />')
230 return text
232 for tag in iter.get_toggled_tags(True):
233 tag_name = tag.get_property('name')
234 if tag_name not in self.begin_tags:
235 continue
236 text += self.begin_tags[tag_name]
237 modified = True
238 while (iter.forward_to_tag_toggle(None) and not iter.is_end()):
239 text += xhtml_special(_buffer.get_text(old, iter))
240 old.forward_to_tag_toggle(None)
241 new_tags, old_tags, end_tags = [], [], []
242 for tag in iter.get_toggled_tags(True):
243 tag_name = tag.get_property('name')
244 if tag_name not in self.begin_tags:
245 continue
246 new_tags.append(tag_name)
247 modified = True
249 for tag in iter.get_tags():
250 tag_name = tag.get_property('name')
251 if tag_name not in self.begin_tags or tag_name not in self.end_tags:
252 continue
253 if tag_name not in new_tags:
254 old_tags.append(tag_name)
256 for tag in iter.get_toggled_tags(False):
257 tag_name = tag.get_property('name')
258 if tag_name not in self.end_tags:
259 continue
260 end_tags.append(tag_name)
262 for tag in old_tags:
263 text += self.end_tags[tag]
264 for tag in end_tags:
265 text += self.end_tags[tag]
266 for tag in new_tags:
267 text += self.begin_tags[tag]
268 for tag in old_tags:
269 text += self.begin_tags[tag]
271 text += xhtml_special(_buffer.get_text(old, _buffer.get_end_iter()))
272 for tag in iter.get_toggled_tags(False):
273 tag_name = tag.get_property('name')
274 if tag_name not in self.end_tags:
275 continue
276 text += self.end_tags[tag_name]
278 if modified:
279 return '<p>' + self.make_clickable_urls(text) + '</p>'
280 else:
281 return None
283 def destroy(self):
284 gobject.idle_add(gc.collect)
286 def clear(self, widget = None):
288 Clear text in the textview
290 _buffer = self.get_buffer()
291 start, end = _buffer.get_bounds()
292 _buffer.delete(start, end)
294 def save_undo(self, text):
295 self.undo_list.append(text)
296 if len(self.undo_list) > self.UNDO_LIMIT:
297 del self.undo_list[0]
298 self.undo_pressed = False
300 def undo(self, widget=None):
302 Undo text in the textview
304 _buffer = self.get_buffer()
305 if self.undo_list:
306 _buffer.set_text(self.undo_list.pop())
307 self.undo_pressed = True
309 def get_sensitive(self):
310 # get sensitive is not in GTK < 2.18
311 try:
312 return super(MessageTextView, self).get_sensitive()
313 except AttributeError:
314 return self.get_property('sensitive')
316 # We register depending on keysym and modifier some bindings
317 # but we also pass those as param so we can construct fake Event
318 # Here we register bindings for those combinations that there is NO DEFAULT
319 # action to be done by gtk TextView. In such case we should not add a binding
320 # as the default action comes first and our bindings is useless. In that case
321 # we catch and do stuff before default action in normal key_press_event
322 # and we also return True there to stop the default action from running
324 # CTRL + SHIFT + TAB
325 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.ISO_Left_Tab,
326 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.ISO_Left_Tab,
327 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)
329 # CTRL + TAB
330 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Tab,
331 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Tab,
332 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)
334 # TAB
335 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Tab,
336 0, 'mykeypress', int, gtk.keysyms.Tab, gtk.gdk.ModifierType, 0)
338 # CTRL + UP
339 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Up,
340 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Up,
341 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)
343 # CTRL + DOWN
344 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Down,
345 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Down,
346 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)
348 # CTRL + SHIFT + UP
349 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Up,
350 gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK, 'mykeypress', int,
351 gtk.keysyms.Up, gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK |
352 gtk.gdk.SHIFT_MASK)
354 # CTRL + SHIFT + DOWN
355 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Down,
356 gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK, 'mykeypress', int,
357 gtk.keysyms.Down, gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK |
358 gtk.gdk.SHIFT_MASK)
360 # ENTER
361 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Return,
362 0, 'mykeypress', int, gtk.keysyms.Return,
363 gtk.gdk.ModifierType, 0)
365 # Ctrl + Enter
366 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Return,
367 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Return,
368 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)
370 # Keypad Enter
371 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.KP_Enter,
372 0, 'mykeypress', int, gtk.keysyms.KP_Enter,
373 gtk.gdk.ModifierType, 0)
375 # Ctrl + Keypad Enter
376 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.KP_Enter,
377 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.KP_Enter,
378 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)
380 # Ctrl + z
381 gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.z,
382 gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.z,
383 gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK)