comparison src/browser/sat_browser/editor_widget.py @ 648:6d3142b782c3 frontends_multi_profiles

browser_side: classes reorganisation: - moved widgets in dedicated modules (base, contact, editor, libervia) and a widget module for single classes - same thing for panels (base, main, contact) - libervia_widget mix main panels and widget and drag n drop for technical reasons (see comments) - renamed WebPanel to WebWidget
author Goffi <goffi@goffi.org>
date Thu, 26 Feb 2015 18:10:54 +0100
parents src/browser/sat_browser/base_panels.py@e0021d571eef
children e876f493dccc
comparison
equal deleted inserted replaced
647:e0021d571eef 648:6d3142b782c3
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.log import getLogger
21 log = getLogger(__name__)
22 from sat_frontends.tools import strings
23
24 from pyjamas.ui.HTML import HTML
25 from pyjamas.ui.SimplePanel import SimplePanel
26 from pyjamas.ui.TextArea import TextArea
27 from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler
28 from pyjamas.ui.FocusListener import FocusHandler
29 from pyjamas.ui.ClickListener import ClickHandler
30 from pyjamas.ui.MouseListener import MouseHandler
31 from pyjamas.Timer import Timer
32 from pyjamas import DOM
33
34 import html_tools
35
36
37 class MessageBox(TextArea):
38 """A basic text area for entering messages"""
39
40 def __init__(self, host):
41 TextArea.__init__(self)
42 self.host = host
43 self.size = (0, 0)
44 self.setStyleName('messageBox')
45 self.addKeyboardListener(self)
46 MouseHandler.__init__(self)
47 self.addMouseListener(self)
48
49 def onBrowserEvent(self, event):
50 # XXX: woraroung a pyjamas bug: self.currentEvent is not set
51 # so the TextBox's cancelKey doens't work. This is a workaround
52 # FIXME: fix the bug upstream
53 self.currentEvent = event
54 TextArea.onBrowserEvent(self, event)
55
56 def onKeyPress(self, sender, keycode, modifiers):
57 _txt = self.getText()
58
59 def history_cb(text):
60 self.setText(text)
61 Timer(5, lambda timer: self.setCursorPos(len(text)))
62
63 if keycode == KEY_ENTER:
64 if _txt:
65 self.host.selected_widget.onTextEntered(_txt)
66 self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
67 self.setText('')
68 sender.cancelKey()
69 elif keycode == KEY_UP:
70 self.host._updateInputHistory(_txt, -1, history_cb)
71 elif keycode == KEY_DOWN:
72 self.host._updateInputHistory(_txt, +1, history_cb)
73 else:
74 self._onComposing()
75
76 def _onComposing(self):
77 """Callback when the user is composing a text."""
78 self.host.selected_widget.state_machine._onEvent("composing")
79
80 def onMouseUp(self, sender, x, y):
81 size = (self.getOffsetWidth(), self.getOffsetHeight())
82 if size != self.size:
83 self.size = size
84 self.host.resize()
85
86 def onSelectedChange(self, selected):
87 self._selected_cache = selected
88
89
90 class BaseTextEditor(object):
91 """Basic definition of a text editor. The method edit gets a boolean parameter which
92 should be set to True when you want to edit the text and False to only display it."""
93
94 def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
95 """
96 Remark when inheriting this class: since the setContent method could be
97 overwritten by the child class, you should consider calling this __init__
98 after all the parameters affecting this setContent method have been set.
99 @param content: dict with at least a 'text' key
100 @param strproc: method to be applied on strings to clean the content
101 @param modifiedCb: method to be called when the text has been modified.
102 If this method returns:
103 - True: the modification will be saved and afterEditCb called;
104 - False: the modification won't be saved and afterEditCb called;
105 - None: the modification won't be saved and afterEditCb not called.
106 @param afterEditCb: method to be called when the edition is done
107 """
108 if content is None:
109 content = {'text': ''}
110 assert('text' in content)
111 if strproc is None:
112 def strproc(text):
113 try:
114 return text.strip()
115 except (TypeError, AttributeError):
116 return text
117 self.strproc = strproc
118 self.__modifiedCb = modifiedCb
119 self._afterEditCb = afterEditCb
120 self.initialized = False
121 self.edit_listeners = []
122 self.setContent(content)
123
124 def setContent(self, content=None):
125 """Set the editable content. The displayed content, which is set from the child class, could differ.
126 @param content: dict with at least a 'text' key
127 """
128 if content is None:
129 content = {'text': ''}
130 elif not isinstance(content, dict):
131 content = {'text': content}
132 assert('text' in content)
133 self._original_content = {}
134 for key in content:
135 self._original_content[key] = self.strproc(content[key])
136
137 def getContent(self):
138 """Get the current edited or editable content.
139 @return: dict with at least a 'text' key
140 """
141 raise NotImplementedError
142
143 def setOriginalContent(self, content):
144 """Use this method with care! Content initialization should normally be
145 done with self.setContent. This method exists to let you trick the editor,
146 e.g. for self.modified to return True also when nothing has been modified.
147 @param content: dict
148 """
149 self._original_content = content
150
151 def getOriginalContent(self):
152 """
153 @return the original content before modification (dict)
154 """
155 return self._original_content
156
157 def modified(self, content=None):
158 """Check if the content has been modified.
159 Remark: we don't use the direct comparison because we want to ignore empty elements
160 @content: content to be check against the original content or None to use the current content
161 @return: True if the content has been modified.
162 """
163 if content is None:
164 content = self.getContent()
165 # the following method returns True if one non empty element exists in a but not in b
166 diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
167 # the following method returns True if the values for the common keys are not equals
168 diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
169 # finally the combination of both to return True if a difference is found
170 diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
171
172 return diff(content, self._original_content)
173
174 def edit(self, edit, abort=False, sync=False):
175 """
176 Remark: the editor must be visible before you call this method.
177 @param edit: set to True to edit the content or False to only display it
178 @param abort: set to True to cancel the edition and loose the changes.
179 If edit and abort are both True, self.abortEdition can be used to ask for a
180 confirmation. When edit is False and abort is True, abortion is actually done.
181 @param sync: set to True to cancel the edition after the content has been saved somewhere else
182 """
183 if edit:
184 if not self.initialized:
185 self.syncToEditor() # e.g.: use the selected target and unibox content
186 self.setFocus(True)
187 if abort:
188 content = self.getContent()
189 if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation
190 self.edit(False, True, sync)
191 return
192 if sync:
193 self.syncFromEditor(content) # e.g.: save the content to unibox
194 return
195 else:
196 if not self.initialized:
197 return
198 content = self.getContent()
199 if abort:
200 self._afterEditCb(content)
201 return
202 if self.__modifiedCb and self.modified(content):
203 result = self.__modifiedCb(content) # e.g.: send a message or update something
204 if result is not None:
205 if self._afterEditCb:
206 self._afterEditCb(content) # e.g.: restore the display mode
207 if result is True:
208 self.setContent(content)
209 elif self._afterEditCb:
210 self._afterEditCb(content)
211
212 self.initialized = True
213
214 def setFocus(self, focus):
215 """
216 @param focus: set to True to focus the editor
217 """
218 raise NotImplementedError
219
220 def syncToEditor(self):
221 pass
222
223 def syncFromEditor(self, content):
224 pass
225
226 def abortEdition(self, content):
227 return True
228
229 def addEditListener(self, listener):
230 """Add a method to be called whenever the text is edited.
231 @param listener: method taking two arguments: sender, keycode"""
232 self.edit_listeners.append(listener)
233
234
235 class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
236 """Base class for manage a simple text editor."""
237
238 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
239 """
240 @param content
241 @param modifiedCb
242 @param afterEditCb
243 @param options: dict with the following value:
244 - no_xhtml: set to True to clean any xhtml content.
245 - enhance_display: if True, the display text will be enhanced with strings.addURLToText
246 - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
247 - listen_focus: set to True to terminate the edition when the focus is lost.
248 - listen_click: set to True to start the edition when you click on the widget.
249 """
250 self.options = {'no_xhtml': False,
251 'enhance_display': True,
252 'listen_keyboard': True,
253 'listen_focus': False,
254 'listen_click': False
255 }
256 if options:
257 self.options.update(options)
258 self.__shift_down = False
259 if self.options['listen_focus']:
260 FocusHandler.__init__(self)
261 if self.options['listen_click']:
262 ClickHandler.__init__(self)
263 KeyboardHandler.__init__(self)
264 strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
265 BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
266 self.textarea = self.display = None
267
268 def setContent(self, content=None):
269 BaseTextEditor.setContent(self, content)
270
271 def getContent(self):
272 raise NotImplementedError
273
274 def edit(self, edit, abort=False, sync=False):
275 BaseTextEditor.edit(self, edit)
276 if edit:
277 if self.options['listen_focus'] and self not in self.textarea._focusListeners:
278 self.textarea.addFocusListener(self)
279 if self.options['listen_click']:
280 self.display.clearClickListener()
281 if self not in self.textarea._keyboardListeners:
282 self.textarea.addKeyboardListener(self)
283 else:
284 self.setDisplayContent()
285 if self.options['listen_focus']:
286 try:
287 self.textarea.removeFocusListener(self)
288 except ValueError:
289 pass
290 if self.options['listen_click'] and self not in self.display._clickListeners:
291 self.display.addClickListener(self)
292 try:
293 self.textarea.removeKeyboardListener(self)
294 except ValueError:
295 pass
296
297 def setDisplayContent(self):
298 text = self._original_content['text']
299 if not self.options['no_xhtml']:
300 text = strings.addURLToImage(text)
301 if self.options['enhance_display']:
302 text = strings.addURLToText(text)
303 self.display.setHTML(html_tools.convertNewLinesToXHTML(text))
304
305 def setFocus(self, focus):
306 raise NotImplementedError
307
308 def onKeyDown(self, sender, keycode, modifiers):
309 for listener in self.edit_listeners:
310 listener(self.textarea, keycode)
311 if not self.options['listen_keyboard']:
312 return
313 if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter>
314 self.__shift_down = True
315 return
316 if keycode == KEY_ENTER: # finish the edition
317 self.textarea.setFocus(False)
318 if not self.options['listen_focus']:
319 self.edit(False)
320
321 def onKeyUp(self, sender, keycode, modifiers):
322 if keycode == KEY_SHIFT:
323 self.__shift_down = False
324
325 def onLostFocus(self, sender):
326 """Finish the edition when focus is lost"""
327 if self.options['listen_focus']:
328 self.edit(False)
329
330 def onClick(self, sender=None):
331 """Start the edition when the widget is clicked"""
332 if self.options['listen_click']:
333 self.edit(True)
334
335 def onBrowserEvent(self, event):
336 if self.options['listen_focus']:
337 FocusHandler.onBrowserEvent(self, event)
338 if self.options['listen_click']:
339 ClickHandler.onBrowserEvent(self, event)
340 KeyboardHandler.onBrowserEvent(self, event)
341
342
343 class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
344 """Manage a simple text editor with the HTML 5 "contenteditable" property."""
345
346 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
347 HTML.__init__(self)
348 SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
349 self.textarea = self.display = self
350
351 def getContent(self):
352 text = DOM.getInnerHTML(self.getElement())
353 return {'text': self.strproc(text) if text else ''}
354
355 def edit(self, edit, abort=False, sync=False):
356 if edit:
357 self.textarea.setHTML(self._original_content['text'])
358 self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
359 SimpleTextEditor.edit(self, edit, abort, sync)
360
361 def setFocus(self, focus):
362 if focus:
363 self.getElement().focus()
364 else:
365 self.getElement().blur()
366
367
368 class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
369 """Manage a simple text editor with a TextArea for editing, HTML for display."""
370
371 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
372 SimplePanel.__init__(self)
373 SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
374 self.textarea = TextArea()
375 self.display = HTML()
376
377 def getContent(self):
378 text = self.textarea.getText()
379 return {'text': self.strproc(text) if text else ''}
380
381 def edit(self, edit, abort=False, sync=False):
382 if edit:
383 self.textarea.setText(self._original_content['text'])
384 self.setWidget(self.textarea if edit else self.display)
385 SimpleTextEditor.edit(self, edit, abort, sync)
386
387 def setFocus(self, focus):
388 if focus and self.isAttached():
389 self.textarea.setCursorPos(len(self.textarea.getText()))
390 self.textarea.setFocus(focus)