pressing a char while popup is open will close the popup and insert the char
[emesene_autocomplete.git] / Completion.py
blob522cd62da6be91cdd98c60ec63e663c3158fa7b7
1 # -*- coding: utf-8 -*-
2 '''An autocompletion plug-in for emesene. Currently it will autocomplete only
3 slash commands, but I'm planning to allow emoticons completion, too
4 '''
6 VERSION = '0.1'
7 import os
8 import re
10 import gobject
11 import gtk
13 import Plugin
15 class CompletionEngine:
16 def __init__(self):
17 self.complete_functions = []
19 def add_complete_function(self, function):
20 self.complete_functions.append(function)
22 def complete_word(self, buffer):
23 complete = []
24 for func in self.complete_functions:
25 complete.extend(func(buffer))
26 return complete
28 class CompletionWindow(gtk.Window):
29 """Window for displaying a list of completions."""
31 def __init__(self, parent, callback):
32 gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
33 self.set_decorated(False)
34 self.store = None
35 self.view = None
36 self.completions = None
37 self.complete_callback = callback
38 self.set_transient_for(parent)
39 self.set_border_width(1)
40 self.text = gtk.TextView()
41 self.text_buffer = gtk.TextBuffer()
42 self.text.set_buffer(self.text_buffer)
43 self.text.set_size_request(300, 200)
44 self.text.set_sensitive(False)
45 self.init_tree_view()
46 self.init_frame()
47 self.cb_ids = {}
48 self.cb_ids['focus-out'] = self.connect('focus-out-event', self.focus_out_event)
49 self.cb_ids['key-press'] = self.connect('key-press-event', self.key_press_event)
50 self.grab_focus()
53 def key_press_event(self, widget, event):
54 print 'pressed', event.keyval
55 if event.keyval == gtk.keysyms.Escape:
56 self.hide()
57 return True
58 if event.keyval == gtk.keysyms.BackSpace:
59 self.hide()
60 return True
61 if event.keyval in (gtk.keysyms.Return, gtk.keysyms.Right):
62 self.complete()
63 return True
64 if event.keyval == gtk.keysyms.Up:
65 self.select_previous()
66 return True
67 if event.keyval == gtk.keysyms.Down:
68 self.select_next()
69 return True
71 char = gtk.gdk.keyval_to_unicode(event.keyval)
72 if char:
73 self.complete_callback(chr(char))
74 return False
76 def complete(self):
77 self.complete_callback(self.completions[self.get_selected()]['completion'])
79 def focus_out_event(self, *args):
80 self.hide()
82 def get_selected(self):
83 """Get the selected row."""
85 selection = self.view.get_selection()
86 return selection.get_selected_rows()[1][0][0]
88 def init_frame(self):
89 """Initialize the frame and scroller around the tree view."""
91 scroller = gtk.ScrolledWindow()
92 scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_NEVER)
93 scroller.add(self.view)
94 frame = gtk.Frame()
95 frame.set_shadow_type(gtk.SHADOW_OUT)
96 hbox = gtk.HBox()
97 hbox.add(scroller)
99 scroller_text = gtk.ScrolledWindow()
100 scroller_text.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
101 scroller_text.add(self.text)
102 hbox.add(scroller_text)
103 frame.add(hbox)
104 self.add(frame)
107 def init_tree_view(self):
108 """Initialize the tree view listing the completions."""
110 self.store = gtk.ListStore(gobject.TYPE_STRING)
111 self.view = gtk.TreeView(self.store)
112 renderer = gtk.CellRendererText()
113 column = gtk.TreeViewColumn("", renderer, text=0)
114 self.view.append_column(column)
115 self.view.set_enable_search(False)
116 self.view.set_headers_visible(False)
117 self.view.set_rules_hint(True)
118 selection = self.view.get_selection()
119 selection.set_mode(gtk.SELECTION_SINGLE)
120 self.view.set_size_request(200, 200)
121 self.view.connect('row-activated', self.row_activated)
124 def row_activated(self, tree, path, view_column, data = None):
125 self.complete()
128 def select_next(self):
129 """Select the next completion."""
131 row = min(self.get_selected() + 1, len(self.store) - 1)
132 selection = self.view.get_selection()
133 selection.unselect_all()
134 selection.select_path(row)
135 self.view.scroll_to_cell(row)
136 self.text_buffer.set_text(self.completions[self.get_selected()]['info'])
138 def select_previous(self):
139 """Select the previous completion."""
141 row = max(self.get_selected() - 1, 0)
142 selection = self.view.get_selection()
143 selection.unselect_all()
144 selection.select_path(row)
145 self.view.scroll_to_cell(row)
146 self.text_buffer.set_text(self.completions[self.get_selected()]['info'])
148 def set_completions(self, completions):
149 """Set the completions to display."""
151 self.completions = completions
152 self.completions.reverse()
153 self.resize(1, 1)
154 self.store.clear()
155 for completion in completions:
156 self.store.append([unicode(completion['abbr'])])
157 self.view.columns_autosize()
158 self.view.get_selection().select_path(0)
159 self.text_buffer.set_text(self.completions[self.get_selected()]['info'])
161 def set_font_description(self, font_desc):
162 """Set the label's font description."""
164 self.view.modify_font(font_desc)
166 class Completer:
167 '''This class provides completion feature for ONE conversation'''
168 def __init__(self, engine, view, window):
169 self.engine = engine
170 self.view = view
171 self.window = window
172 self.popup = CompletionWindow(None, self.complete)
174 self.key_press_id = view.connect("key-press-event", self.on_key_pressed)
176 def disconnect(self):
177 self.view.disconnect(self.key_press_id)
179 def complete(self, completion):
180 """Complete the current word."""
182 doc = self.view.get_buffer()
183 doc.insert_at_cursor(completion)
184 self.hide_popup()
186 def cancel(self):
187 self.hide_popup()
188 return False
190 def hide_popup(self):
191 """Hide the completion window."""
193 self.popup.hide()
194 self.completes = None
196 def show_popup(self, completions, x, y):
197 """Show the completion window."""
199 root_x, root_y = self.window.get_position()
200 self.popup.move(root_x + x + 24, root_y + y + 44)
201 self.popup.set_completions(completions)
202 self.popup.show_all()
204 def display_completions(self, view, event):
205 doc = view.get_buffer()
206 insert = doc.get_iter_at_mark(doc.get_insert())
208 window = gtk.TEXT_WINDOW_TEXT
209 rect = view.get_iter_location(insert)
210 x, y = view.buffer_to_window_coords(window, rect.x, rect.y)
211 x, y = view.translate_coordinates(self.window, x, y)
212 completes = self.engine.complete_word(doc)
213 if completes:
214 self.show_popup(completes, x, y)
215 return True
216 else:
217 return False
219 def on_key_pressed(self, view, event):
220 if event.keyval == gtk.keysyms.Tab:
221 #if event.state & gtk.gdk.CONTROL_MASK and event.state & gtk.gdk.MOD1_MASK and event.keyval == gtk.keysyms.Enter:
222 return self.display_completions(view, event)
223 if event.state & gtk.gdk.CONTROL_MASK:
224 return self.cancel()
225 if event.state & gtk.gdk.MOD1_MASK:
226 return self.cancel()
227 return self.cancel()
231 class MainClass( Plugin.Plugin ):
232 '''Main plugin class'''
233 def __init__(self, controller, msn):
234 '''constructor'''
235 Plugin.Plugin.__init__( self, controller, msn )
236 self.description = _('Autocompletion for slash commands in the inputBox')
237 self.authors = { 'BoySka' : 'boyska gmail com' }
238 self.website = 'http://emesene.org'
239 self.displayName = _('Autocomplete')
240 self.name = 'Completion'
243 self.controller = controller
244 self.msn = msn
245 self.engine = None
247 self.completers = {}
249 self.new_conv_id = None
250 self.close_conv_id = None
252 self.config = controller.config
253 self.config.readPluginConfig(self.name)
255 #Plugin methods
257 def start( self ):
258 '''start the plugin'''
259 conv_manager = self.controller.conversationManager
260 self.new_conv_id = conv_manager.connect(
261 'new-conversation-ui', self.on_new_conversation)
262 self.close_conv_id = conv_manager.connect(
263 'close-conversation-ui', self.on_close_conversation)
265 self.engine = CompletionEngine()
267 for conv in self.getOpenConversations():
268 self.completers[conv] = Completer(
269 self.engine,
270 conv.ui.input.input,
271 conv.parentConversationWindow)
273 self.engine.add_complete_function(self.slash_complete)
274 #we can add multiple complete functions
275 #self.engine.add_complete_function(self.simple_complete)
276 self.enabled = True
278 def stop( self ):
279 '''stop the plugin'''
280 for conversation in self.completers.keys():
281 self.completers[conversation].disconnect()
282 conv_manager = self.controller.conversationManager
283 conv_manager.disconnect(self.new_conv_id)
284 conv_manager.disconnect(self.close_conv_id)
285 self.enabled = False
287 def check( self ):
288 '''check if the plugin can be enabled'''
289 return ( True, 'Ok' )
291 def on_new_conversation(self, conversationManager, conversation, window):
292 self.completers[conversation] = Completer(
293 self.engine,
294 conversation.ui.input.input,
295 conversation.parentConversationWindow)
297 def on_close_conversation(self, conversationManager, conversation, window):
298 self.completers[conversation].disconnect()
299 del self.completers[conversation]
301 def slash_complete(self, buffer):
302 complete = []
303 c = buffer.get_start_iter().get_char()
304 if c != '/':
305 return complete
306 start = buffer.get_iter_at_mark(buffer.get_insert()).copy()
307 while start.backward_char():
308 char = unicode(start.get_char())
309 if char in [' ', '\n']:
310 return complete
312 incomplete = _get_last_word(buffer) #exclude the slash
314 commands = self.controller.Slash.commands
315 for x in commands.keys():
316 if x.startswith(incomplete):
317 complete.append({'completion':x[len(incomplete):],
318 'abbr':x,
319 'info':commands[x][1] or 'no description'})
320 complete.sort()
321 complete.reverse()
322 return complete
324 def simple_complete(self, buffer):
325 '''just a test function'''
326 words = ['foobar', 'peloponneso', 'pelonelluovo', 'pelota', 'pescasseroli']
327 incomplete = _get_last_word(buffer)
328 complete = []
329 for x in words:
330 if x.startswith(incomplete):
331 complete.append({'completion':x[len(incomplete):], 'abbr':x, 'info':'simple'})
332 return complete
334 def _get_last_word(buffer):
335 re_alpha = re.compile(r"\w+", re.UNICODE | re.MULTILINE)
336 insert = buffer.get_iter_at_mark(buffer.get_insert())
337 start = insert.copy()
339 while start.backward_char():
340 char = unicode(start.get_char())
341 if not re_alpha.match(char) and not char == ".":
342 start.forward_char()
343 break
344 incomplete = unicode(buffer.get_text(start, insert))
345 #incomplete += unicode(event.string)
346 return incomplete