| 1 | # -*- coding:utf-8 -*- |
|---|
| 2 | ## src/message_textview.py |
|---|
| 3 | ## |
|---|
| 4 | ## Copyright (C) 2003-2007 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 | ## |
|---|
| 8 | ## This file is part of Gajim. |
|---|
| 9 | ## |
|---|
| 10 | ## Gajim is free software; you can redistribute it and/or modify |
|---|
| 11 | ## it under the terms of the GNU General Public License as published |
|---|
| 12 | ## by the Free Software Foundation; version 3 only. |
|---|
| 13 | ## |
|---|
| 14 | ## Gajim is distributed in the hope that it will be useful, |
|---|
| 15 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 16 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 17 | ## GNU General Public License for more details. |
|---|
| 18 | ## |
|---|
| 19 | ## You should have received a copy of the GNU General Public License |
|---|
| 20 | ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 21 | ## |
|---|
| 22 | |
|---|
| 23 | import gtk |
|---|
| 24 | import gobject |
|---|
| 25 | import pango |
|---|
| 26 | import gtkgui_helpers |
|---|
| 27 | from common import gajim |
|---|
| 28 | |
|---|
| 29 | class MessageTextView(gtk.TextView): |
|---|
| 30 | '''Class for the message textview (where user writes new messages) |
|---|
| 31 | for chat/groupchat windows''' |
|---|
| 32 | __gsignals__ = dict( |
|---|
| 33 | mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, |
|---|
| 34 | None, # return value |
|---|
| 35 | (int, gtk.gdk.ModifierType ) # arguments |
|---|
| 36 | ) |
|---|
| 37 | ) |
|---|
| 38 | |
|---|
| 39 | def __init__(self): |
|---|
| 40 | gtk.TextView.__init__(self) |
|---|
| 41 | |
|---|
| 42 | # set properties |
|---|
| 43 | self.set_border_width(1) |
|---|
| 44 | self.set_accepts_tab(True) |
|---|
| 45 | self.set_editable(True) |
|---|
| 46 | self.set_cursor_visible(True) |
|---|
| 47 | self.set_wrap_mode(gtk.WRAP_WORD_CHAR) |
|---|
| 48 | self.set_left_margin(2) |
|---|
| 49 | self.set_right_margin(2) |
|---|
| 50 | self.set_pixels_above_lines(2) |
|---|
| 51 | self.set_pixels_below_lines(2) |
|---|
| 52 | |
|---|
| 53 | self.lang = None # Lang used for spell checking |
|---|
| 54 | buffer = self.get_buffer() |
|---|
| 55 | self.begin_tags = {} |
|---|
| 56 | self.end_tags = {} |
|---|
| 57 | self.color_tags = [] |
|---|
| 58 | self.fonts_tags = [] |
|---|
| 59 | self.other_tags = {} |
|---|
| 60 | self.other_tags['bold'] = buffer.create_tag('bold') |
|---|
| 61 | self.other_tags['bold'].set_property('weight', pango.WEIGHT_BOLD) |
|---|
| 62 | self.begin_tags['bold'] = '<strong>' |
|---|
| 63 | self.end_tags['bold'] = '</strong>' |
|---|
| 64 | self.other_tags['italic'] = buffer.create_tag('italic') |
|---|
| 65 | self.other_tags['italic'].set_property('style', pango.STYLE_ITALIC) |
|---|
| 66 | self.begin_tags['italic'] = '<em>' |
|---|
| 67 | self.end_tags['italic'] = '</em>' |
|---|
| 68 | self.other_tags['underline'] = buffer.create_tag('underline') |
|---|
| 69 | self.other_tags['underline'].set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 70 | self.begin_tags['underline'] = '<span style="text-decoration: underline;">' |
|---|
| 71 | self.end_tags['underline'] = '</span>' |
|---|
| 72 | self.other_tags['strike'] = buffer.create_tag('strike') |
|---|
| 73 | self.other_tags['strike'].set_property('strikethrough', True) |
|---|
| 74 | self.begin_tags['strike'] = '<span style="text-decoration: line-through;">' |
|---|
| 75 | self.end_tags['strike'] = '</span>' |
|---|
| 76 | |
|---|
| 77 | def make_clickable_urls(self, text): |
|---|
| 78 | buffer = self.get_buffer() |
|---|
| 79 | |
|---|
| 80 | start = 0 |
|---|
| 81 | end = 0 |
|---|
| 82 | index = 0 |
|---|
| 83 | |
|---|
| 84 | new_text = '' |
|---|
| 85 | iterator = gajim.interface.link_pattern_re.finditer(text) |
|---|
| 86 | for match in iterator: |
|---|
| 87 | start, end = match.span() |
|---|
| 88 | url = text[start:end] |
|---|
| 89 | if start != 0: |
|---|
| 90 | text_before_special_text = text[index:start] |
|---|
| 91 | else: |
|---|
| 92 | text_before_special_text = '' |
|---|
| 93 | end_iter = buffer.get_end_iter() |
|---|
| 94 | # we insert normal text |
|---|
| 95 | new_text += text_before_special_text + \ |
|---|
| 96 | '<a href="'+ url +'">' + url + '</a>' |
|---|
| 97 | |
|---|
| 98 | index = end # update index |
|---|
| 99 | |
|---|
| 100 | if end < len(text): |
|---|
| 101 | new_text += text[end:] |
|---|
| 102 | |
|---|
| 103 | return new_text # the position after *last* special text |
|---|
| 104 | |
|---|
| 105 | def get_active_tags(self): |
|---|
| 106 | buffer = self.get_buffer() |
|---|
| 107 | return_val = buffer.get_selection_bounds() |
|---|
| 108 | if return_val: # if sth was selected |
|---|
| 109 | start, finish = return_val[0], return_val[1] |
|---|
| 110 | else: |
|---|
| 111 | start, finish = buffer.get_bounds() |
|---|
| 112 | active_tags = [] |
|---|
| 113 | for tag in start.get_tags(): |
|---|
| 114 | active_tags.append(tag.get_property('name')) |
|---|
| 115 | return active_tags |
|---|
| 116 | |
|---|
| 117 | def set_tag(self, widget, tag): |
|---|
| 118 | buffer = self.get_buffer() |
|---|
| 119 | return_val = buffer.get_selection_bounds() |
|---|
| 120 | if return_val: # if sth was selected |
|---|
| 121 | start, finish = return_val[0], return_val[1] |
|---|
| 122 | else: |
|---|
| 123 | start, finish = buffer.get_bounds() |
|---|
| 124 | if start.has_tag(self.other_tags[tag]): |
|---|
| 125 | buffer.remove_tag_by_name(tag, start, finish) |
|---|
| 126 | else: |
|---|
| 127 | if tag == 'underline': |
|---|
| 128 | buffer.remove_tag_by_name('strike', start, finish) |
|---|
| 129 | elif tag == 'strike': |
|---|
| 130 | buffer.remove_tag_by_name('underline', start, finish) |
|---|
| 131 | buffer.apply_tag_by_name(tag, start, finish) |
|---|
| 132 | |
|---|
| 133 | def clear_tags(self, widget): |
|---|
| 134 | buffer = self.get_buffer() |
|---|
| 135 | return_val = buffer.get_selection_bounds() |
|---|
| 136 | if return_val: # if sth was selected |
|---|
| 137 | start, finish = return_val[0], return_val[1] |
|---|
| 138 | else: |
|---|
| 139 | start, finish = buffer.get_bounds() |
|---|
| 140 | buffer.remove_all_tags(start, finish) |
|---|
| 141 | |
|---|
| 142 | def color_set(self, widget, response, color): |
|---|
| 143 | if response == -6: |
|---|
| 144 | widget.destroy() |
|---|
| 145 | return |
|---|
| 146 | buffer = self.get_buffer() |
|---|
| 147 | color = color.get_current_color() |
|---|
| 148 | widget.destroy() |
|---|
| 149 | color_string = gtkgui_helpers.make_color_string(color) |
|---|
| 150 | tag_name = 'color' + color_string |
|---|
| 151 | if not tag_name in self.color_tags: |
|---|
| 152 | tagColor = buffer.create_tag(tag_name) |
|---|
| 153 | tagColor.set_property('foreground', color_string) |
|---|
| 154 | self.begin_tags[tag_name] = '<span style="color: ' + color_string + ';">' |
|---|
| 155 | self.end_tags[tag_name] = '</span>' |
|---|
| 156 | self.color_tags.append(tag_name) |
|---|
| 157 | |
|---|
| 158 | return_val = buffer.get_selection_bounds() |
|---|
| 159 | if return_val: # if sth was selected |
|---|
| 160 | start, finish = return_val[0], return_val[1] |
|---|
| 161 | else: |
|---|
| 162 | start, finish = buffer.get_bounds() |
|---|
| 163 | |
|---|
| 164 | for tag in self.color_tags: |
|---|
| 165 | buffer.remove_tag_by_name(tag, start, finish) |
|---|
| 166 | |
|---|
| 167 | buffer.apply_tag_by_name(tag_name, start, finish) |
|---|
| 168 | |
|---|
| 169 | def font_set(self, widget, response, font): |
|---|
| 170 | if response == -6: |
|---|
| 171 | widget.destroy() |
|---|
| 172 | return |
|---|
| 173 | |
|---|
| 174 | buffer = self.get_buffer() |
|---|
| 175 | |
|---|
| 176 | font = font.get_font_name() |
|---|
| 177 | font_desc = pango.FontDescription(font) |
|---|
| 178 | family = font_desc.get_family() |
|---|
| 179 | size = font_desc.get_size() |
|---|
| 180 | size = size / pango.SCALE |
|---|
| 181 | weight = font_desc.get_weight() |
|---|
| 182 | style = font_desc.get_style() |
|---|
| 183 | |
|---|
| 184 | widget.destroy() |
|---|
| 185 | |
|---|
| 186 | tag_name = 'font' + font |
|---|
| 187 | if not tag_name in self.fonts_tags: |
|---|
| 188 | tagFont = buffer.create_tag(tag_name) |
|---|
| 189 | tagFont.set_property('font', family + ' ' + str(size)) |
|---|
| 190 | self.begin_tags[tag_name] = \ |
|---|
| 191 | '<span style="font-family: ' + family + '; ' + \ |
|---|
| 192 | 'font-size: ' + str(size) + 'px">' |
|---|
| 193 | self.end_tags[tag_name] = '</span>' |
|---|
| 194 | self.fonts_tags.append(tag_name) |
|---|
| 195 | |
|---|
| 196 | return_val = buffer.get_selection_bounds() |
|---|
| 197 | if return_val: # if sth was selected |
|---|
| 198 | start, finish = return_val[0], return_val[1] |
|---|
| 199 | else: |
|---|
| 200 | start, finish = buffer.get_bounds() |
|---|
| 201 | |
|---|
| 202 | for tag in self.fonts_tags: |
|---|
| 203 | buffer.remove_tag_by_name(tag, start, finish) |
|---|
| 204 | |
|---|
| 205 | buffer.apply_tag_by_name(tag_name, start, finish) |
|---|
| 206 | |
|---|
| 207 | if weight == pango.WEIGHT_BOLD: |
|---|
| 208 | buffer.apply_tag_by_name('bold', start, finish) |
|---|
| 209 | else: |
|---|
| 210 | buffer.remove_tag_by_name('bold', start, finish) |
|---|
| 211 | |
|---|
| 212 | if style == pango.STYLE_ITALIC: |
|---|
| 213 | buffer.apply_tag_by_name('italic', start, finish) |
|---|
| 214 | else: |
|---|
| 215 | buffer.remove_tag_by_name('italic', start, finish) |
|---|
| 216 | |
|---|
| 217 | def get_xhtml(self): |
|---|
| 218 | buffer = self.get_buffer() |
|---|
| 219 | old = buffer.get_start_iter() |
|---|
| 220 | tags = {} |
|---|
| 221 | tags['bold'] = False |
|---|
| 222 | iter = buffer.get_start_iter() |
|---|
| 223 | old = buffer.get_start_iter() |
|---|
| 224 | texte = '' |
|---|
| 225 | modified = False |
|---|
| 226 | def xhtml_special(text): |
|---|
| 227 | text = text.replace('<', '<') |
|---|
| 228 | text = text.replace('>', '>') |
|---|
| 229 | text = text.replace('\n', '<br />') |
|---|
| 230 | return text |
|---|
| 231 | |
|---|
| 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 | texte += self.begin_tags[tag_name] |
|---|
| 237 | modified = True |
|---|
| 238 | while (iter.forward_to_tag_toggle(None) and not iter.is_end()): |
|---|
| 239 | modified = True |
|---|
| 240 | texte += xhtml_special(buffer.get_text(old, iter)) |
|---|
| 241 | old.forward_to_tag_toggle(None) |
|---|
| 242 | new_tags = [] |
|---|
| 243 | old_tags = [] |
|---|
| 244 | end_tags = [] |
|---|
| 245 | for tag in iter.get_toggled_tags(True): |
|---|
| 246 | tag_name = tag.get_property('name') |
|---|
| 247 | if tag_name not in self.begin_tags: |
|---|
| 248 | continue |
|---|
| 249 | new_tags.append(tag_name) |
|---|
| 250 | |
|---|
| 251 | for tag in iter.get_tags(): |
|---|
| 252 | tag_name = tag.get_property('name') |
|---|
| 253 | if tag_name not in self.begin_tags or tag_name not in self.end_tags: |
|---|
| 254 | continue |
|---|
| 255 | if tag_name not in new_tags: |
|---|
| 256 | old_tags.append(tag_name) |
|---|
| 257 | |
|---|
| 258 | for tag in iter.get_toggled_tags(False): |
|---|
| 259 | tag_name = tag.get_property('name') |
|---|
| 260 | if tag_name not in self.end_tags: |
|---|
| 261 | continue |
|---|
| 262 | end_tags.append(tag_name) |
|---|
| 263 | |
|---|
| 264 | for tag in old_tags: |
|---|
| 265 | texte += self.end_tags[tag] |
|---|
| 266 | for tag in end_tags: |
|---|
| 267 | texte += self.end_tags[tag] |
|---|
| 268 | for tag in new_tags: |
|---|
| 269 | texte += self.begin_tags[tag] |
|---|
| 270 | for tag in old_tags: |
|---|
| 271 | texte += self.begin_tags[tag] |
|---|
| 272 | |
|---|
| 273 | texte += xhtml_special(buffer.get_text(old, buffer.get_end_iter())) |
|---|
| 274 | for tag in iter.get_toggled_tags(False): |
|---|
| 275 | tag_name = tag.get_property('name') |
|---|
| 276 | if tag_name not in self.end_tags: |
|---|
| 277 | continue |
|---|
| 278 | texte += self.end_tags[tag_name] |
|---|
| 279 | |
|---|
| 280 | if modified: |
|---|
| 281 | return '<p>' + self.make_clickable_urls(texte) + '</p>' |
|---|
| 282 | else: |
|---|
| 283 | return None |
|---|
| 284 | |
|---|
| 285 | |
|---|
| 286 | def destroy(self): |
|---|
| 287 | import gc |
|---|
| 288 | gobject.idle_add(lambda:gc.collect()) |
|---|
| 289 | |
|---|
| 290 | def clear(self, widget = None): |
|---|
| 291 | '''clear text in the textview''' |
|---|
| 292 | buffer = self.get_buffer() |
|---|
| 293 | start, end = buffer.get_bounds() |
|---|
| 294 | buffer.delete(start, end) |
|---|
| 295 | |
|---|
| 296 | |
|---|
| 297 | # We register depending on keysym and modifier some bindings |
|---|
| 298 | # but we also pass those as param so we can construct fake Event |
|---|
| 299 | # Here we register bindings for those combinations that there is NO DEFAULT |
|---|
| 300 | # action to be done by gtk TextView. In such case we should not add a binding |
|---|
| 301 | # as the default action comes first and our bindings is useless. In that case |
|---|
| 302 | # we catch and do stuff before default action in normal key_press_event |
|---|
| 303 | # and we also return True there to stop the default action from running |
|---|
| 304 | |
|---|
| 305 | # CTRL + SHIFT + TAB |
|---|
| 306 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.ISO_Left_Tab, |
|---|
| 307 | gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.ISO_Left_Tab, |
|---|
| 308 | gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK) |
|---|
| 309 | |
|---|
| 310 | # CTRL + TAB |
|---|
| 311 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Tab, |
|---|
| 312 | gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Tab, |
|---|
| 313 | gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK) |
|---|
| 314 | |
|---|
| 315 | # TAB |
|---|
| 316 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Tab, |
|---|
| 317 | 0, 'mykeypress', int, gtk.keysyms.Tab, gtk.gdk.ModifierType, 0) |
|---|
| 318 | |
|---|
| 319 | # CTRL + UP |
|---|
| 320 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Up, |
|---|
| 321 | gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Up, |
|---|
| 322 | gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK) |
|---|
| 323 | |
|---|
| 324 | # CTRL + DOWN |
|---|
| 325 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Down, |
|---|
| 326 | gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Down, |
|---|
| 327 | gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK) |
|---|
| 328 | |
|---|
| 329 | # ENTER |
|---|
| 330 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Return, |
|---|
| 331 | 0, 'mykeypress', int, gtk.keysyms.Return, |
|---|
| 332 | gtk.gdk.ModifierType, 0) |
|---|
| 333 | |
|---|
| 334 | # Ctrl + Enter |
|---|
| 335 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.Return, |
|---|
| 336 | gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.Return, |
|---|
| 337 | gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK) |
|---|
| 338 | |
|---|
| 339 | # Keypad Enter |
|---|
| 340 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.KP_Enter, |
|---|
| 341 | 0, 'mykeypress', int, gtk.keysyms.KP_Enter, |
|---|
| 342 | gtk.gdk.ModifierType, 0) |
|---|
| 343 | |
|---|
| 344 | # Ctrl + Keypad Enter |
|---|
| 345 | gtk.binding_entry_add_signal(MessageTextView, gtk.keysyms.KP_Enter, |
|---|
| 346 | gtk.gdk.CONTROL_MASK, 'mykeypress', int, gtk.keysyms.KP_Enter, |
|---|
| 347 | gtk.gdk.ModifierType, gtk.gdk.CONTROL_MASK) |
|---|
| 348 | |
|---|
| 349 | # vim: se ts=3: |
|---|