comparison src/browser/sat_browser/panels.py @ 467:97c72fe4a5f2

browser_side: import fixes: - moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly - refactored bad import practices: classes are most of time not imported directly, module is imported instead.
author Goffi <goffi@goffi.org>
date Mon, 09 Jun 2014 22:15:26 +0200
parents src/browser/panels.py@b62c1cf0dbf7
children 992b900ab876
comparison
equal deleted inserted replaced
466:01880aa8ea2d 467:97c72fe4a5f2
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 import pyjd # this is dummy in pyjs
21 from sat.core.log import getLogger
22 log = getLogger(__name__)
23
24 from sat_frontends.tools.strings import addURLToText
25 from sat_frontends.tools.games import SYMBOLS
26 from sat.core.i18n import _
27
28 from pyjamas.ui.SimplePanel import SimplePanel
29 from pyjamas.ui.AbsolutePanel import AbsolutePanel
30 from pyjamas.ui.VerticalPanel import VerticalPanel
31 from pyjamas.ui.HorizontalPanel import HorizontalPanel
32 from pyjamas.ui.HTMLPanel import HTMLPanel
33 from pyjamas.ui.Frame import Frame
34 from pyjamas.ui.TextArea import TextArea
35 from pyjamas.ui.Label import Label
36 from pyjamas.ui.Button import Button
37 from pyjamas.ui.HTML import HTML
38 from pyjamas.ui.Image import Image
39 from pyjamas.ui.ClickListener import ClickHandler
40 from pyjamas.ui.FlowPanel import FlowPanel
41 from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler
42 from pyjamas.ui.MouseListener import MouseHandler
43 from pyjamas.ui.FocusListener import FocusHandler
44 from pyjamas.Timer import Timer
45 from pyjamas import DOM
46 from pyjamas import Window
47 from __pyjamas__ import doc
48
49 from datetime import datetime
50 from time import time
51
52 import jid
53 import html_tools
54 import base_panels
55 import card_game
56 import radiocol
57 import menu
58 import dialog
59 import base_widget
60 import richtext
61 import contact
62 from constants import Const as C
63 import plugin_xep_0085
64
65
66 # TODO: at some point we should decide which behaviors to keep and remove these two constants
67 TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button
68 NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry
69
70
71 class UniBoxPanel(HorizontalPanel):
72 """Panel containing the UniBox"""
73
74 def __init__(self, host):
75 HorizontalPanel.__init__(self)
76 self.host = host
77 self.setStyleName('uniBoxPanel')
78 self.unibox = None
79
80 def refresh(self):
81 """Enable or disable this panel. Contained widgets are created when necessary."""
82 enable = self.host.getUIParam('unibox')
83 self.setVisible(enable)
84 if enable and not self.unibox:
85 self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
86 self.button.setTitle('Open the rich text editor')
87 self.button.addStyleName('uniBoxButton')
88 self.add(self.button)
89 self.unibox = UniBox(self.host)
90 self.add(self.unibox)
91 self.setCellWidth(self.unibox, '100%')
92 self.button.addClickListener(self.openRichMessageEditor)
93 self.unibox.addKey("@@: ")
94 self.unibox.onSelectedChange(self.host.getSelected())
95
96 def openRichMessageEditor(self):
97 """Open the rich text editor."""
98 self.button.setVisible(False)
99 self.unibox.setVisible(False)
100 self.setCellWidth(self.unibox, '0px')
101 self.host.panel._contactsMove(self)
102
103 def afterEditCb():
104 Window.removeWindowResizeListener(self)
105 self.host.panel._contactsMove(self.host.panel._hpanel)
106 self.setCellWidth(self.unibox, '100%')
107 self.button.setVisible(True)
108 self.unibox.setVisible(True)
109 self.host.resize()
110
111 richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
112 Window.addWindowResizeListener(self)
113 self.host.resize()
114
115 def onWindowResized(self, width, height):
116 right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
117 left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth()
118 ideal_width = right - left - 40
119 self.host.richtext.setWidth("%spx" % ideal_width)
120
121
122 class MessageBox(TextArea):
123 """A basic text area for entering messages"""
124
125 def __init__(self, host):
126 TextArea.__init__(self)
127 self.host = host
128 self.__size = (0, 0)
129 self.setStyleName('messageBox')
130 self.addKeyboardListener(self)
131 MouseHandler.__init__(self)
132 self.addMouseListener(self)
133 self._selected_cache = None
134
135 def onBrowserEvent(self, event):
136 # XXX: woraroung a pyjamas bug: self.currentEvent is not set
137 # so the TextBox's cancelKey doens't work. This is a workaround
138 # FIXME: fix the bug upstream
139 self.currentEvent = event
140 TextArea.onBrowserEvent(self, event)
141
142 def onKeyPress(self, sender, keycode, modifiers):
143 _txt = self.getText()
144
145 def history_cb(text):
146 self.setText(text)
147 Timer(5, lambda timer: self.setCursorPos(len(text)))
148
149 if keycode == KEY_ENTER:
150 if _txt:
151 self._selected_cache.onTextEntered(_txt)
152 self.host._updateInputHistory(_txt)
153 self.setText('')
154 sender.cancelKey()
155 elif keycode == KEY_UP:
156 self.host._updateInputHistory(_txt, -1, history_cb)
157 elif keycode == KEY_DOWN:
158 self.host._updateInputHistory(_txt, +1, history_cb)
159 else:
160 self.__onComposing()
161
162 def __onComposing(self):
163 """Callback when the user is composing a text."""
164 if hasattr(self._selected_cache, "target"):
165 self._selected_cache.state_machine._onEvent("composing")
166
167 def onMouseUp(self, sender, x, y):
168 size = (self.getOffsetWidth(), self.getOffsetHeight())
169 if size != self.__size:
170 self.__size = size
171 self.host.resize()
172
173 def onSelectedChange(self, selected):
174 self._selected_cache = selected
175
176
177 class UniBox(MessageBox, MouseHandler): # AutoCompleteTextBox):
178 """This text box is used as a main typing point, for message, microblog, etc"""
179
180 def __init__(self, host):
181 MessageBox.__init__(self, host)
182 #AutoCompleteTextBox.__init__(self)
183 self.setStyleName('uniBox')
184 host.addSelectedListener(self.onSelectedChange)
185
186 def addKey(self, key):
187 return
188 #self.getCompletionItems().completions.append(key)
189
190 def removeKey(self, key):
191 return
192 # TODO: investigate why AutoCompleteTextBox doesn't work here,
193 # maybe it can work on a TextBox but no TextArea. Remove addKey
194 # and removeKey methods if they don't serve anymore.
195 try:
196 self.getCompletionItems().completions.remove(key)
197 except KeyError:
198 log.warning("trying to remove an unknown key")
199
200 def _getTarget(self, txt):
201 """ Say who will receive the messsage
202 @return: a tuple (selected, target_type, target info) with:
203 - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None))
204 - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
205 - msg: HTML message which will appear in the privacy warning banner """
206 target = self._selected_cache
207
208 def getSelectedOrStatus():
209 if target and target.isSelectable():
210 _type, msg = target.getWarningData()
211 target_hook = None # we use the selected widget, not a hook
212 else:
213 _type, msg = "STATUS", "This will be your new status message"
214 target_hook = (txt, None)
215 return (target_hook, _type, msg)
216
217 if not txt.startswith('@'):
218 target_hook, _type, msg = getSelectedOrStatus()
219 elif txt.startswith('@@: '):
220 _type = "PUBLIC"
221 msg = MicroblogPanel.warning_msg_public
222 target_hook = (txt[4:], None)
223 elif txt.startswith('@'):
224 _end = txt.find(': ')
225 if _end == -1:
226 target_hook, _type, msg = getSelectedOrStatus()
227 else:
228 group = txt[1:_end] # only one target group is managed for the moment
229 if not group or not group in self.host.contact_panel.getGroups():
230 # the group doesn't exists, we ignore the key
231 group = None
232 target_hook, _type, msg = getSelectedOrStatus()
233 else:
234 _type = "GROUP"
235 msg = MicroblogPanel.warning_msg_group % group
236 target_hook = (txt[_end + 2:], group)
237 else:
238 log.error("Unknown target")
239 target_hook, _type, msg = getSelectedOrStatus()
240
241 return (target_hook, _type, msg)
242
243 def onKeyPress(self, sender, keycode, modifiers):
244 _txt = self.getText()
245 target_hook, type_, msg = self._getTarget(_txt)
246
247 if keycode == KEY_ENTER:
248 if _txt:
249 if target_hook:
250 parsed_txt, data = target_hook
251 self.host.send([(type_, data)], parsed_txt)
252 self.host._updateInputHistory(_txt)
253 self.setText('')
254 self.host.showWarning(None, None)
255 else:
256 self.host.showWarning(type_, msg)
257 MessageBox.onKeyPress(self, sender, keycode, modifiers)
258
259 def getTargetAndData(self):
260 """For external use, to get information about the (hypothetical) message
261 that would be sent if we press Enter right now in the unibox.
262 @return a tuple (target, data) with:
263 - data: what would be the content of the message (body)
264 - target: JID, group with the prefix "@" or the public entity "@@"
265 """
266 _txt = self.getText()
267 target_hook, _type, _msg = self._getTarget(_txt)
268 if target_hook:
269 data, target = target_hook
270 if target is None:
271 return target_hook
272 return (data, "@%s" % (target if target != "" else "@"))
273 if isinstance(self._selected_cache, MicroblogPanel):
274 groups = self._selected_cache.accepted_groups
275 target = "@%s" % (groups[0] if len(groups) > 0 else "@")
276 if len(groups) > 1:
277 Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0])
278 elif isinstance(self._selected_cache, ChatPanel):
279 target = self._selected_cache.target
280 else:
281 target = None
282 return (_txt, target)
283
284 def onWidgetClosed(self, lib_wid):
285 """Called when a libervia widget is closed"""
286 if self._selected_cache == lib_wid:
287 self.onSelectedChange(None)
288
289 """def complete(self):
290
291 #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
292 #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
293 return AutoCompleteTextBox.complete(self)"""
294
295
296 class WarningPopup():
297
298 def __init__(self):
299 self._popup = None
300 self._timer = Timer(notify=self._timeCb)
301
302 def showWarning(self, type_=None, msg=None, duration=2000):
303 """Display a popup information message, e.g. to notify the recipient of a message being composed.
304 If type_ is None, a popup being currently displayed will be hidden.
305 @type_: a type determining the CSS style to be applied (see __showWarning)
306 @msg: message to be displayed
307 """
308 if type_ is None:
309 self.__removeWarning()
310 return
311 if not self._popup:
312 self.__showWarning(type_, msg)
313 elif (type_, msg) != self._popup.target_data:
314 self._timeCb(None) # we remove the popup
315 self.__showWarning(type_, msg)
316
317 self._timer.schedule(duration)
318
319 def __showWarning(self, type_, msg):
320 """Display a popup information message, e.g. to notify the recipient of a message being composed.
321 @type_: a type determining the CSS style to be applied. For now the defined styles are
322 "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE".
323 @msg: message to be displayed
324 """
325 if type_ == "NONE":
326 return
327 if not msg:
328 log.warning("no msg set uniBox warning")
329 return
330 if type_ == "PUBLIC":
331 style = "targetPublic"
332 elif type_ == "GROUP":
333 style = "targetGroup"
334 elif type_ == "STATUS":
335 style = "targetStatus"
336 elif type_ == "ONE2ONE":
337 style = "targetOne2One"
338 else:
339 log.error("unknown message type")
340 return
341 contents = HTML(msg)
342
343 self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
344 self._popup.target_data = (type_, msg)
345 self._popup.add(contents)
346 self._popup.setStyleName("warningPopup")
347 if style:
348 self._popup.addStyleName(style)
349
350 left = 0
351 top = 0 # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
352 self._popup.setPopupPosition(left, top)
353 self._popup.show()
354
355 def _timeCb(self, timer):
356 if self._popup:
357 self._popup.hide()
358 del self._popup
359 self._popup = None
360
361 def __removeWarning(self):
362 """Remove the popup"""
363 self._timeCb(None)
364
365
366 class MicroblogItem():
367 # XXX: should be moved in a separated module
368
369 def __init__(self, data):
370 self.id = data['id']
371 self.type = data.get('type', 'main_item')
372 self.empty = data.get('new', False)
373 self.title = data.get('title', '')
374 self.title_xhtml = data.get('title_xhtml', '')
375 self.content = data.get('content', '')
376 self.content_xhtml = data.get('content_xhtml', '')
377 self.author = data['author']
378 self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here
379 self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here
380 self.service = data.get('service', '')
381 self.node = data.get('node', '')
382 self.comments = data.get('comments', False)
383 self.comments_service = data.get('comments_service', '')
384 self.comments_node = data.get('comments_node', '')
385
386
387 class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler):
388
389 def __init__(self, blog_panel, data):
390 """
391 @param blog_panel: the parent panel
392 @param data: dict containing the blog item data, or a MicroblogItem instance.
393 """
394 self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data)
395 for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml',
396 'author', 'updated', 'published', 'comments', 'service', 'node',
397 'comments_service', 'comments_node']:
398 getter = lambda attr: lambda inst: getattr(inst._base_item, attr)
399 setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value)
400 setattr(MicroblogEntry, attr, property(getter(attr), setter(attr)))
401
402 SimplePanel.__init__(self)
403 self._blog_panel = blog_panel
404
405 self.panel = FlowPanel()
406 self.panel.setStyleName('mb_entry')
407
408 self.header = HTMLPanel('')
409 self.panel.add(self.header)
410
411 self.entry_actions = VerticalPanel()
412 self.entry_actions.setStyleName('mb_entry_actions')
413 self.panel.add(self.entry_actions)
414
415 entry_avatar = SimplePanel()
416 entry_avatar.setStyleName('mb_entry_avatar')
417 self.avatar = Image(self._blog_panel.host.getAvatar(self.author))
418 entry_avatar.add(self.avatar)
419 self.panel.add(entry_avatar)
420
421 if TOGGLE_EDITION_USE_ICON:
422 self.entry_dialog = HorizontalPanel()
423 else:
424 self.entry_dialog = VerticalPanel()
425 self.entry_dialog.setStyleName('mb_entry_dialog')
426 self.panel.add(self.entry_dialog)
427
428 self.add(self.panel)
429 ClickHandler.__init__(self)
430 self.addClickListener(self)
431
432 self.__pub_data = (self.service, self.node, self.id)
433 self.__setContent()
434
435 def __setContent(self):
436 """Actually set the entry content (header, icons, bubble...)"""
437 self.delete_label = self.update_label = self.comment_label = None
438 self.bubble = self._current_comment = None
439 self.__setHeader()
440 self.__setBubble()
441 self.__setIcons()
442
443 def __setHeader(self):
444 """Set the entry header"""
445 if self.empty:
446 return
447 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
448 self.header.setHTML("""<div class='mb_entry_header'>
449 <span class='mb_entry_author'>%(author)s</span> on
450 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
451 </div>""" % {'author': html_tools.html_sanitize(self.author),
452 'published': datetime.fromtimestamp(self.published),
453 'updated': update_text if self.published != self.updated else ''
454 }
455 )
456
457 def __setIcons(self):
458 """Set the entry icons (delete, update, comment)"""
459 if self.empty:
460 return
461
462 def addIcon(label, title):
463 label = Label(label)
464 label.setTitle(title)
465 label.addClickListener(self)
466 self.entry_actions.add(label)
467 return label
468
469 if self.comments:
470 self.comment_label = addIcon(u"↶", "Comment this message")
471 self.comment_label.setStyleName('mb_entry_action_larger')
472 is_publisher = self.author == self._blog_panel.host.whoami.bare
473 if is_publisher:
474 self.update_label = addIcon(u"✍", "Edit this message")
475 if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare):
476 self.delete_label = addIcon(u"✗", "Delete this message")
477
478 def updateAvatar(self, new_avatar):
479 """Change the avatar of the entry
480 @param new_avatar: path to the new image"""
481 self.avatar.setUrl(new_avatar)
482
483 def onClick(self, sender):
484 if sender == self:
485 try: # prevent re-selection of the main entry after a comment has been focused
486 if self.__ignoreNextEvent:
487 self.__ignoreNextEvent = False
488 return
489 except AttributeError:
490 pass
491 self._blog_panel.setSelectedEntry(self)
492 elif sender == self.delete_label:
493 self._delete()
494 elif sender == self.update_label:
495 self.edit(True)
496 elif sender == self.comment_label:
497 self.__ignoreNextEvent = True
498 self._comment()
499
500 def __modifiedCb(self, content):
501 """Send the new content to the backend
502 @return: False to restore the original content if a deletion has been cancelled
503 """
504 if not content['text']: # previous content has been emptied
505 self._delete(True)
506 return False
507 extra = {'published': str(self.published)}
508 if isinstance(self.bubble, richtext.RichTextEditor):
509 # TODO: if the user change his parameters after the message edition started,
510 # the message syntax could be different then the current syntax: pass the
511 # message syntax in extra for the frontend to use it instead of current syntax.
512 extra.update({'content_rich': content['text'], 'title': content['title']})
513 if self.empty:
514 if self.type == 'main_item':
515 self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra)
516 else:
517 self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
518 else:
519 self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra)
520 return True
521
522 def __afterEditCb(self, content):
523 """Remove the entry if it was an empty one (used for creating a new blog post).
524 Data for the actual new blog post will be received from the bridge"""
525 if self.empty:
526 self._blog_panel.removeEntry(self.type, self.id)
527 if self.type == 'main_item': # restore the "New message" button
528 self._blog_panel.refresh()
529 else: # allow to create a new comment
530 self._parent_entry._current_comment = None
531 self.entry_dialog.setWidth('auto')
532 try:
533 self.toggle_syntax_button.removeFromParent()
534 except TypeError:
535 pass
536
537 def __setBubble(self, edit=False):
538 """Set the bubble displaying the initial content."""
539 content = {'text': self.content_xhtml if self.content_xhtml else self.content,
540 'title': self.title_xhtml if self.title_xhtml else self.title}
541 if self.content_xhtml:
542 content.update({'syntax': C.SYNTAX_XHTML})
543 if self.author != self._blog_panel.host.whoami.bare:
544 options = ['read_only']
545 else:
546 options = [] if self.empty else ['update_msg']
547 self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
548 else: # assume raw text message have no title
549 self.bubble = base_panels.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
550 self.bubble.addStyleName("bubble")
551 try:
552 self.toggle_syntax_button.removeFromParent()
553 except TypeError:
554 pass
555 self.entry_dialog.add(self.bubble)
556 self.edit(edit)
557 self.bubble.addEditListener(self.__showWarning)
558
559 def __showWarning(self, sender, keycode):
560 if keycode == KEY_ENTER:
561 self._blog_panel.host.showWarning(None, None)
562 else:
563 self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment'))
564
565 def _delete(self, empty=False):
566 """Ask confirmation for deletion.
567 @return: False if the deletion has been cancelled."""
568 def confirm_cb(answer):
569 if answer:
570 self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments)
571 else: # restore the text if it has been emptied during the edition
572 self.bubble.setContent(self.bubble._original_content)
573
574 if self.empty:
575 text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
576 dialog.InfoDialog(_("Information"), text).show()
577 return
578 text = ""
579 if empty:
580 text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
581 target = _('message and all its comments') if self.comments else _('comment')
582 text += _("Do you really want to delete this %s?") % target
583 dialog.ConfirmDialog(confirm_cb, text=text).show()
584
585 def _comment(self):
586 """Add an empty entry for a new comment"""
587 if self._current_comment:
588 self._current_comment.bubble.setFocus(True)
589 self._blog_panel.setSelectedEntry(self._current_comment)
590 return
591 data = {'id': str(time()),
592 'new': True,
593 'type': 'comment',
594 'author': self._blog_panel.host.whoami.bare,
595 'service': self.comments_service,
596 'node': self.comments_node
597 }
598 entry = self._blog_panel.addEntry(data)
599 if entry is None:
600 log.info("The entry of id %s can not be commented" % self.id)
601 return
602 entry._parent_entry = self
603 self._current_comment = entry
604 self.edit(True, entry)
605 self._blog_panel.setSelectedEntry(entry)
606
607 def edit(self, edit, entry=None):
608 """Toggle the bubble between display and edit mode
609 @edit: boolean value
610 @entry: MicroblogEntry instance, or None to use self
611 """
612 if entry is None:
613 entry = self
614 try:
615 entry.toggle_syntax_button.removeFromParent()
616 except TypeError:
617 pass
618 entry.bubble.edit(edit)
619 if edit:
620 if isinstance(entry.bubble, richtext.RichTextEditor):
621 image = '<a class="richTextIcon">A</a>'
622 html = '<a style="color: blue;">raw text</a>'
623 title = _('Switch to raw text edition')
624 else:
625 image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
626 html = '<a style="color: blue;">rich text</a>'
627 title = _('Switch to rich text edition')
628 if TOGGLE_EDITION_USE_ICON:
629 entry.entry_dialog.setWidth('80%')
630 entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax)
631 entry.toggle_syntax_button.setTitle(title)
632 entry.entry_dialog.add(entry.toggle_syntax_button)
633 else:
634 entry.toggle_syntax_button = HTML(html)
635 entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax)
636 entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
637 entry.entry_dialog.add(entry.toggle_syntax_button)
638 entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS
639 entry.toggle_syntax_button.setStyleAttribute('left', '-20px')
640
641 def toggleContentSyntax(self):
642 """Toggle the editor between raw and rich text"""
643 original_content = self.bubble.getOriginalContent()
644 rich = not isinstance(self.bubble, richtext.RichTextEditor)
645 if rich:
646 original_content['syntax'] = C.SYNTAX_XHTML
647
648 def setBubble(text):
649 self.content = text
650 self.content_xhtml = text if rich else ''
651 self.content_title = self.content_title_xhtml = ''
652 self.bubble.removeFromParent()
653 self.__setBubble(True)
654 self.bubble.setOriginalContent(original_content)
655 if rich:
656 self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble
657
658 text = self.bubble.getContent()['text']
659 if not text:
660 setBubble(' ') # something different than empty string is needed to initialize the rich text editor
661 return
662 if not rich:
663 def confirm_cb(answer):
664 if answer:
665 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT)
666 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
667 else:
668 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
669
670
671 class MicroblogPanel(base_widget.LiberviaWidget):
672 warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know"
673 warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"
674
675 def __init__(self, host, accepted_groups):
676 """Panel used to show microblog
677 @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
678 """
679 base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
680 self.setAcceptedGroup(accepted_groups)
681 self.host = host
682 self.entries = {}
683 self.comments = {}
684 self.selected_entry = None
685 self.vpanel = VerticalPanel()
686 self.vpanel.setStyleName('microblogPanel')
687 self.setWidget(self.vpanel)
688
689 def refresh(self):
690 """Refresh the display of this widget. If the unibox is disabled,
691 display the 'New message' button or an empty bubble on top of the panel"""
692 if hasattr(self, 'new_button'):
693 self.new_button.setVisible(self.host.uni_box is None)
694 return
695 if self.host.uni_box is None:
696 def addBox():
697 if hasattr(self, 'new_button'):
698 self.new_button.setVisible(False)
699 data = {'id': str(time()),
700 'new': True,
701 'author': self.host.whoami.bare,
702 }
703 entry = self.addEntry(data)
704 entry.edit(True)
705 if NEW_MESSAGE_USE_BUTTON:
706 self.new_button = Button("New message", listener=addBox)
707 self.new_button.setStyleName("microblogNewButton")
708 self.vpanel.insert(self.new_button, 0)
709 elif not self.getNewMainEntry():
710 addBox()
711
712 def getNewMainEntry(self):
713 """Get the new entry being edited, or None if it doesn't exists.
714
715 @return (MicroblogEntry): the new entry being edited.
716 """
717 try:
718 first = self.vpanel.children[0]
719 except IndexError:
720 return None
721 assert(first.type == 'main_item')
722 return first if first.empty else None
723
724 @classmethod
725 def registerClass(cls):
726 base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel)
727 base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel)
728
729 @classmethod
730 def createPanel(cls, host, item):
731 """Generic panel creation for one, several or all groups (meta).
732 @parem host: the SatWebFrontend instance
733 @param item: single group as a string, list of groups
734 (as an array) or None (for the meta group = "all groups")
735 @return: the created MicroblogPanel
736 """
737 _items = item if isinstance(item, list) else ([] if item is None else [item])
738 _type = 'ALL' if _items == [] else 'GROUP'
739 # XXX: pyjamas doesn't support use of cls directly
740 _new_panel = MicroblogPanel(host, _items)
741 host.FillMicroblogPanel(_new_panel)
742 host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
743 host.setSelected(_new_panel)
744 _new_panel.refresh()
745 return _new_panel
746
747 @classmethod
748 def createMetaPanel(cls, host, item):
749 """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group"""
750 return MicroblogPanel.createPanel(host, None)
751
752 @property
753 def accepted_groups(self):
754 return self._accepted_groups
755
756 def matchEntity(self, entity):
757 """
758 @param entity: single group as a string, list of groups
759 (as an array) or None (for the meta group = "all groups")
760 @return: True if self matches the given entity
761 """
762 entity = entity if isinstance(entity, list) else ([] if entity is None else [entity])
763 entity.sort() # sort() do not return the sorted list: do it here, not on the "return" line
764 return self.accepted_groups == entity
765
766 def getWarningData(self, comment=None):
767 """
768 @param comment: True if the composed message is a comment. If None, consider we are
769 composing from the unibox and guess the message type from self.selected_entry
770 @return: a couple (type, msg) for calling self.host.showWarning"""
771 if comment is None: # composing from the unibox
772 if self.selected_entry and not self.selected_entry.comments:
773 log.error("an item without comment is selected")
774 return ("NONE", None)
775 comment = self.selected_entry is not None
776 if comment:
777 return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
778 elif not self._accepted_groups:
779 # we have a meta MicroblogPanel, we publish publicly
780 return ("PUBLIC", self.warning_msg_public)
781 else:
782 # we only accept one group at the moment
783 # FIXME: manage several groups
784 return ("GROUP", self.warning_msg_group % self._accepted_groups[0])
785
786 def onTextEntered(self, text):
787 if self.selected_entry:
788 # we are entering a comment
789 comments_url = self.selected_entry.comments
790 if not comments_url:
791 raise Exception("ERROR: the comments URL is empty")
792 target = ("COMMENT", comments_url)
793 elif not self._accepted_groups:
794 # we are entering a public microblog
795 target = ("PUBLIC", None)
796 else:
797 # we are entering a microblog restricted to a group
798 # FIXME: manage several groups
799 target = ("GROUP", self._accepted_groups[0])
800 self.host.send([target], text)
801
802 def accept_all(self):
803 return not self._accepted_groups # we accept every microblog only if we are not filtering by groups
804
805 def getEntries(self):
806 """Ask all the entries for the currenly accepted groups,
807 and fill the panel"""
808
809 def massiveInsert(self, mblogs):
810 """Insert several microblogs at once
811 @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
812 """
813 log.debug("Massive insertion of %d microblogs" % len(mblogs))
814 for publisher in mblogs:
815 log.debug("adding blogs for [%s]" % publisher)
816 for mblog in mblogs[publisher]:
817 if not "content" in mblog:
818 log.warning("No content found in microblog [%s]", mblog)
819 continue
820 self.addEntry(mblog)
821
822 def mblogsInsert(self, mblogs):
823 """ Insert several microblogs at once
824 @param mblogs: list of microblogs
825 """
826 for mblog in mblogs:
827 if not "content" in mblog:
828 log.warning("No content found in microblog [%s]", mblog)
829 continue
830 self.addEntry(mblog)
831
832 def _chronoInsert(self, vpanel, entry, reverse=True):
833 """ Insert an entry in chronological order
834 @param vpanel: VerticalPanel instance
835 @param entry: MicroblogEntry
836 @param reverse: more recent entry on top if True, chronological order else"""
837 assert(isinstance(reverse, bool))
838 if entry.empty:
839 entry.published = time()
840 # we look for the right index to insert our entry:
841 # if reversed, we insert the entry above the first entry
842 # in the past
843 idx = 0
844
845 for child in vpanel.children:
846 if not isinstance(child, MicroblogEntry):
847 idx += 1
848 continue
849 condition_to_stop = child.empty or (child.published > entry.published)
850 if condition_to_stop != reverse: # != is XOR
851 break
852 idx += 1
853
854 vpanel.insert(entry, idx)
855
856 def addEntry(self, data):
857 """Add an entry to the panel
858 @param data: dict containing the item data
859 @return: the added entry, or None
860 """
861 _entry = MicroblogEntry(self, data)
862 if _entry.type == "comment":
863 comments_hash = (_entry.service, _entry.node)
864 if not comments_hash in self.comments:
865 # The comments node is not known in this panel
866 return None
867 parent = self.comments[comments_hash]
868 parent_idx = self.vpanel.getWidgetIndex(parent)
869 # we find or create the panel where the comment must be inserted
870 try:
871 sub_panel = self.vpanel.getWidget(parent_idx + 1)
872 except IndexError:
873 sub_panel = None
874 if not sub_panel or not isinstance(sub_panel, VerticalPanel):
875 sub_panel = VerticalPanel()
876 sub_panel.setStyleName('microblogPanel')
877 sub_panel.addStyleName('subPanel')
878 self.vpanel.insert(sub_panel, parent_idx + 1)
879 for idx in xrange(0, len(sub_panel.getChildren())):
880 comment = sub_panel.getIndexedChild(idx)
881 if comment.id == _entry.id:
882 # update an existing comment
883 sub_panel.remove(comment)
884 sub_panel.insert(_entry, idx)
885 return _entry
886 # we want comments to be inserted in chronological order
887 self._chronoInsert(sub_panel, _entry, reverse=False)
888 return _entry
889
890 if _entry.id in self.entries: # update
891 idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
892 self.vpanel.remove(self.entries[_entry.id])
893 self.vpanel.insert(_entry, idx)
894 else: # new entry
895 self._chronoInsert(self.vpanel, _entry)
896 self.entries[_entry.id] = _entry
897
898 if _entry.comments:
899 # entry has comments, we keep the comments service/node as a reference
900 comments_hash = (_entry.comments_service, _entry.comments_node)
901 self.comments[comments_hash] = _entry
902 self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
903
904 return _entry
905
906 def removeEntry(self, type_, id_):
907 """Remove an entry from the panel
908 @param type_: entry type ('main_item' or 'comment')
909 @param id_: entry id
910 """
911 for child in self.vpanel.getChildren():
912 if isinstance(child, MicroblogEntry) and type_ == 'main_item':
913 if child.id == id_:
914 main_idx = self.vpanel.getWidgetIndex(child)
915 try:
916 sub_panel = self.vpanel.getWidget(main_idx + 1)
917 if isinstance(sub_panel, VerticalPanel):
918 sub_panel.removeFromParent()
919 except IndexError:
920 pass
921 child.removeFromParent()
922 self.selected_entry = None
923 break
924 elif isinstance(child, VerticalPanel) and type_ == 'comment':
925 for comment in child.getChildren():
926 if comment.id == id_:
927 comment.removeFromParent()
928 self.selected_entry = None
929 break
930
931 def setSelectedEntry(self, entry):
932 try:
933 self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry
934 except AttributeError:
935 log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!")
936 removeStyle = lambda entry: entry.removeStyleName('selected_entry')
937 if not self.host.uni_box or not entry.comments:
938 entry.addStyleName('selected_entry') # blink the clicked entry
939 clicked_entry = entry # entry may be None when the timer is done
940 Timer(500, lambda timer: removeStyle(clicked_entry))
941 if not self.host.uni_box:
942 return # unibox is disabled
943 # from here the previous behavior (toggle main item selection) is conserved
944 entry = entry if entry.comments else None
945 if self.selected_entry == entry:
946 entry = None
947 if self.selected_entry:
948 removeStyle(self.selected_entry)
949 if entry:
950 log.debug("microblog entry selected (author=%s)" % entry.author)
951 entry.addStyleName('selected_entry')
952 self.selected_entry = entry
953
954 def updateValue(self, type_, jid, value):
955 """Update a jid value in entries
956 @param type_: one of 'avatar', 'nick'
957 @param jid: jid concerned
958 @param value: new value"""
959 def updateVPanel(vpanel):
960 for child in vpanel.children:
961 if isinstance(child, MicroblogEntry) and child.author == jid:
962 child.updateAvatar(value)
963 elif isinstance(child, VerticalPanel):
964 updateVPanel(child)
965 if type_ == 'avatar':
966 updateVPanel(self.vpanel)
967
968 def setAcceptedGroup(self, group):
969 """Add one or more group(s) which can be displayed in this panel.
970 Prevent from duplicate values and keep the list sorted.
971 @param group: string of the group, or list of string
972 """
973 if not hasattr(self, "_accepted_groups"):
974 self._accepted_groups = []
975 groups = group if isinstance(group, list) else [group]
976 for _group in groups:
977 if _group not in self._accepted_groups:
978 self._accepted_groups.append(_group)
979 self._accepted_groups.sort()
980
981 def isJidAccepted(self, jid):
982 """Tell if a jid is actepted and shown in this panel
983 @param jid: jid
984 @return: True if the jid is accepted"""
985 if self.accept_all():
986 return True
987 for group in self._accepted_groups:
988 if self.host.contact_panel.isContactInGroup(group, jid):
989 return True
990 return False
991
992
993 class StatusPanel(base_panels.HTMLTextEditor):
994
995 EMPTY_STATUS = '&lt;click to set a status&gt;'
996
997 def __init__(self, host, status=''):
998 self.host = host
999 modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
1000 base_panels.HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True})
1001 self.edit(False)
1002 self.setStyleName('statusPanel')
1003
1004 @property
1005 def status(self):
1006 return self._original_content['text']
1007
1008 def __cleanContent(self, content):
1009 status = content['text']
1010 if status == self.EMPTY_STATUS or status in C.PRESENCE.values():
1011 content['text'] = ''
1012 return content
1013
1014 def getContent(self):
1015 return self.__cleanContent(base_panels.HTMLTextEditor.getContent(self))
1016
1017 def setContent(self, content):
1018 content = self.__cleanContent(content)
1019 base_panels.BaseTextEditor.setContent(self, content)
1020
1021 def setDisplayContent(self):
1022 status = self._original_content['text']
1023 try:
1024 presence = self.host.status_panel.presence
1025 except AttributeError: # during initialization
1026 presence = None
1027 if not status:
1028 if presence and presence in C.PRESENCE:
1029 status = C.PRESENCE[presence]
1030 else:
1031 status = self.EMPTY_STATUS
1032 self.display.setHTML(addURLToText(status))
1033
1034
1035 class PresenceStatusPanel(HorizontalPanel, ClickHandler):
1036
1037 def __init__(self, host, presence="", status=""):
1038 self.host = host
1039 HorizontalPanel.__init__(self, Width='100%')
1040 self.presence_button = Label(u"◉")
1041 self.presence_button.setStyleName("presence-button")
1042 self.status_panel = StatusPanel(host, status=status)
1043 self.setPresence(presence)
1044 entries = {}
1045 for value in C.PRESENCE.keys():
1046 entries.update({C.PRESENCE[value]: {"value": value}})
1047
1048 def callback(sender, key):
1049 self.setPresence(entries[key]["value"]) # order matters
1050 self.host.send([("STATUS", None)], self.status_panel.status)
1051
1052 self.presence_list = base_panels.PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"})
1053 self.presence_list.registerClickSender(self.presence_button)
1054
1055 panel = HorizontalPanel()
1056 panel.add(self.presence_button)
1057 panel.add(self.status_panel)
1058 panel.setCellVerticalAlignment(self.presence_button, 'baseline')
1059 panel.setCellVerticalAlignment(self.status_panel, 'baseline')
1060 panel.setStyleName("marginAuto")
1061 self.add(panel)
1062
1063 self.status_panel.edit(False)
1064
1065 ClickHandler.__init__(self)
1066 self.addClickListener(self)
1067
1068 @property
1069 def presence(self):
1070 return self._presence
1071
1072 @property
1073 def status(self):
1074 return self.status_panel._original_content['text']
1075
1076 def setPresence(self, presence):
1077 self._presence = presence
1078 contact.setPresenceStyle(self.presence_button, self._presence)
1079
1080 def setStatus(self, status):
1081 self.status_panel.setContent({'text': status})
1082 self.status_panel.setDisplayContent()
1083
1084 def onClick(self, sender):
1085 # As status is the default target of uniBar, we don't want to select anything if click on it
1086 self.host.setSelected(None)
1087
1088
1089 class ChatPanel(base_widget.LiberviaWidget):
1090
1091 def __init__(self, host, target, type_='one2one'):
1092 """Panel used for conversation (one 2 one or group chat)
1093 @param host: SatWebFrontend instance
1094 @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
1095 @param type: one2one for simple conversation, group for MUC"""
1096 base_widget.LiberviaWidget.__init__(self, host, title=target.bare, selectable=True)
1097 self.vpanel = VerticalPanel()
1098 self.vpanel.setSize('100%', '100%')
1099 self.type = type_
1100 self.nick = None
1101 if not target:
1102 log.error("Empty target !")
1103 return
1104 self.target = target
1105 self.__body = AbsolutePanel()
1106 self.__body.setStyleName('chatPanel_body')
1107 chat_area = HorizontalPanel()
1108 chat_area.setStyleName('chatArea')
1109 if type_ == 'group':
1110 self.occupants_list = base_panels.OccupantsList()
1111 chat_area.add(self.occupants_list)
1112 self.__body.add(chat_area)
1113 self.content = AbsolutePanel()
1114 self.content.setStyleName('chatContent')
1115 self.content_scroll = base_widget.ScrollPanelWrapper(self.content)
1116 chat_area.add(self.content_scroll)
1117 chat_area.setCellWidth(self.content_scroll, '100%')
1118 self.vpanel.add(self.__body)
1119 self.vpanel.setCellHeight(self.__body, '100%')
1120 self.addStyleName('chatPanel')
1121 self.setWidget(self.vpanel)
1122 self.state_machine = plugin_xep_0085.ChatStateMachine(self.host, str(self.target))
1123 self._state = None
1124
1125 @classmethod
1126 def registerClass(cls):
1127 base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
1128
1129 @classmethod
1130 def createPanel(cls, host, item):
1131 _contact = item if isinstance(item, jid.JID) else jid.JID(item)
1132 host.contact_panel.setContactMessageWaiting(_contact.bare, False)
1133 _new_panel = ChatPanel(host, _contact) # XXX: pyjamas doesn't seems to support creating with cls directly
1134 _new_panel.historyPrint()
1135 host.setSelected(_new_panel)
1136 _new_panel.refresh()
1137 return _new_panel
1138
1139 def refresh(self):
1140 """Refresh the display of this widget. If the unibox is disabled,
1141 add a message box at the bottom of the panel"""
1142 self.host.contact_panel.setContactMessageWaiting(self.target.bare, False)
1143 self.content_scroll.scrollToBottom()
1144
1145 enable_box = self.host.uni_box is None
1146 if hasattr(self, 'message_box'):
1147 self.message_box.setVisible(enable_box)
1148 return
1149 if enable_box:
1150 self.message_box = MessageBox(self.host)
1151 self.message_box.onSelectedChange(self)
1152 self.vpanel.add(self.message_box)
1153
1154 def matchEntity(self, entity):
1155 """
1156 @param entity: target jid as a string or jid.JID instance.
1157 Could also be a couple with a type in the second element.
1158 @return: True if self matches the given entity
1159 """
1160 if isinstance(entity, tuple):
1161 entity, type_ = entity if len(entity) > 1 else (entity[0], self.type)
1162 else:
1163 type_ = self.type
1164 entity = entity if isinstance(entity, jid.JID) else jid.JID(entity)
1165 try:
1166 return self.target.bare == entity.bare and self.type == type_
1167 except AttributeError as e:
1168 e.include_traceback()
1169 return False
1170
1171 def getWarningData(self):
1172 if self.type not in ["one2one", "group"]:
1173 raise Exception("Unmanaged type !")
1174 if self.type == "one2one":
1175 msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
1176 elif self.type == "group":
1177 msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
1178 return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg)
1179
1180 def onTextEntered(self, text):
1181 self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text)
1182 self.state_machine._onEvent("active")
1183
1184 def onQuit(self):
1185 base_widget.LiberviaWidget.onQuit(self)
1186 if self.type == 'group':
1187 self.host.bridge.call('mucLeave', None, self.target.bare)
1188
1189 def setUserNick(self, nick):
1190 """Set the nick of the user, usefull for e.g. change the color of the user"""
1191 self.nick = nick
1192
1193 def setPresents(self, nicks):
1194 """Set the users presents in this room
1195 @param occupants: list of nicks (string)"""
1196 self.occupants_list.clear()
1197 for nick in nicks:
1198 self.occupants_list.addOccupant(nick)
1199
1200 def userJoined(self, nick, data):
1201 self.occupants_list.addOccupant(nick)
1202 self.printInfo("=> %s has joined the room" % nick)
1203
1204 def userLeft(self, nick, data):
1205 self.occupants_list.removeOccupant(nick)
1206 self.printInfo("<= %s has left the room" % nick)
1207
1208 def changeUserNick(self, old_nick, new_nick):
1209 assert(self.type == "group")
1210 self.occupants_list.removeOccupant(old_nick)
1211 self.occupants_list.addOccupant(new_nick)
1212 self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick})
1213
1214 def historyPrint(self, size=20):
1215 """Print the initial history"""
1216 def getHistoryCB(history):
1217 # display day change
1218 day_format = "%A, %d %b %Y"
1219 previous_day = datetime.now().strftime(day_format)
1220 for line in history:
1221 timestamp, from_jid, to_jid, message, mess_type, extra = line
1222 message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
1223 if previous_day != message_day:
1224 self.printInfo("* " + message_day)
1225 previous_day = message_day
1226 self.printMessage(from_jid, message, extra, timestamp)
1227 self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
1228
1229 def printInfo(self, msg, type_='normal', link_cb=None):
1230 """Print general info
1231 @param msg: message to print
1232 @param type_: one of:
1233 "normal": general info like "toto has joined the room"
1234 "link": general info that is clickable like "click here to join the main room"
1235 "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
1236 @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
1237 """
1238 _wid = HTML(msg) if type_ == 'link' else Label(msg)
1239 if type_ == 'normal':
1240 _wid.setStyleName('chatTextInfo')
1241 elif type_ == 'link':
1242 _wid.setStyleName('chatTextInfo-link')
1243 if link_cb:
1244 _wid.addClickListener(link_cb)
1245 elif type_ == 'me':
1246 _wid.setStyleName('chatTextMe')
1247 else:
1248 _wid.setStyleName('chatTextInfo')
1249 self.content.add(_wid)
1250
1251 def printMessage(self, from_jid, msg, extra, timestamp=None):
1252 """Print message in chat window. Must be implemented by child class"""
1253 _jid = jid.JID(from_jid)
1254 nick = _jid.node if self.type == 'one2one' else _jid.resource
1255 mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare # mymess = True if message comes from local user
1256 if msg.startswith('/me '):
1257 self.printInfo('* %s %s' % (nick, msg[4:]), type_='me')
1258 return
1259 self.content.add(base_panels.ChatText(timestamp, nick, mymess, msg, extra.get('xhtml')))
1260 self.content_scroll.scrollToBottom()
1261
1262 def startGame(self, game_type, waiting, referee, players, *args):
1263 """Configure the chat window to start a game"""
1264 classes = {"Tarot": card_game.CardPanel, "RadioCol": radiocol.RadioColPanel}
1265 if game_type not in classes.keys():
1266 return # unknown game
1267 attr = game_type.lower()
1268 self.occupants_list.updateSpecials(players, SYMBOLS[attr])
1269 if waiting or not self.nick in players:
1270 return # waiting for player or not playing
1271 attr = "%s_panel" % attr
1272 if hasattr(self, attr):
1273 return
1274 log.info("%s Game Started \o/" % game_type)
1275 panel = classes[game_type](self, referee, self.nick, players, *args)
1276 setattr(self, attr, panel)
1277 self.vpanel.insert(panel, 0)
1278 self.vpanel.setCellHeight(panel, panel.getHeight())
1279
1280 def getGame(self, game_type):
1281 """Return class managing the game type"""
1282 # TODO: check that the game is launched, and manage errors
1283 if game_type == "Tarot":
1284 return self.tarot_panel
1285 elif game_type == "RadioCol":
1286 return self.radiocol_panel
1287
1288 def setState(self, state, nick=None):
1289 """Set the chat state (XEP-0085) of the contact. Leave nick to None
1290 to set the state for a one2one conversation, or give a nickname or
1291 C.ALL_OCCUPANTS to set the state of a participant within a MUC.
1292 @param state: the new chat state
1293 @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS
1294 """
1295 if nick:
1296 assert(self.type == 'group')
1297 occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick]
1298 for occupant in occupants:
1299 self.occupants_list.occupants_list[occupant].setState(state)
1300 else:
1301 assert(self.type == 'one2one')
1302 self._state = state
1303 self.refreshTitle()
1304 self.state_machine.started = not not state # start to send "composing" state from now
1305
1306 def refreshTitle(self):
1307 """Refresh the title of this ChatPanel dialog"""
1308 if self._state:
1309 self.setTitle(self.target.bare + " (" + self._state + ")")
1310 else:
1311 self.setTitle(self.target.bare)
1312
1313
1314 class WebPanel(base_widget.LiberviaWidget):
1315 """ (mini)browser like widget """
1316
1317 def __init__(self, host, url=None):
1318 """
1319 @param host: SatWebFrontend instance
1320 """
1321 base_widget.LiberviaWidget.__init__(self, host)
1322 self._vpanel = VerticalPanel()
1323 self._vpanel.setSize('100%', '100%')
1324 self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
1325 self._url.setText(url or "")
1326 self._url.setWidth('100%')
1327 hpanel = HorizontalPanel()
1328 hpanel.add(self._url)
1329 btn = Button("Go", self.onUrlClick)
1330 hpanel.setCellWidth(self._url, "100%")
1331 #self.setCellWidth(btn, "10%")
1332 hpanel.add(self._url)
1333 hpanel.add(btn)
1334 self._vpanel.add(hpanel)
1335 self._vpanel.setCellHeight(hpanel, '20px')
1336 self._frame = Frame(url or "")
1337 self._frame.setSize('100%', '100%')
1338 DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
1339 self._vpanel.add(self._frame)
1340 self.setWidget(self._vpanel)
1341
1342 def onUrlClick(self, sender):
1343 self._frame.setUrl(self._url.getText())
1344
1345
1346 class MainPanel(AbsolutePanel):
1347
1348 def __init__(self, host):
1349 self.host = host
1350 AbsolutePanel.__init__(self)
1351
1352 # menu
1353 self.menu = menu.Menu(host)
1354
1355 # unibox
1356 self.unibox_panel = UniBoxPanel(host)
1357 self.unibox_panel.setVisible(False)
1358
1359 # contacts
1360 self._contacts = HorizontalPanel()
1361 self._contacts.addStyleName('globalLeftArea')
1362 self.contacts_switch = Button(u'«', self._contactsSwitch)
1363 self.contacts_switch.addStyleName('contactsSwitch')
1364 self._contacts.add(self.contacts_switch)
1365 self._contacts.add(self.host.contact_panel)
1366
1367 # tabs
1368 self.tab_panel = base_widget.MainTabPanel(host)
1369 self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
1370 self.tab_panel.add(self.discuss_panel, "Discussions")
1371 self.tab_panel.selectTab(0)
1372
1373 self.header = AbsolutePanel()
1374 self.header.add(self.menu)
1375 self.header.add(self.unibox_panel)
1376 self.header.add(self.host.status_panel)
1377 self.header.setStyleName('header')
1378 self.add(self.header)
1379
1380 self._hpanel = HorizontalPanel()
1381 self._hpanel.add(self._contacts)
1382 self._hpanel.add(self.tab_panel)
1383 self.add(self._hpanel)
1384
1385 self.setWidth("100%")
1386 Window.addWindowResizeListener(self)
1387
1388 def _contactsSwitch(self, btn=None):
1389 """ (Un)hide contacts panel """
1390 if btn is None:
1391 btn = self.contacts_switch
1392 cpanel = self.host.contact_panel
1393 cpanel.setVisible(not cpanel.getVisible())
1394 btn.setText(u"«" if cpanel.getVisible() else u"»")
1395 self.host.resize()
1396
1397 def _contactsMove(self, parent):
1398 """Move the contacts container (containing the contact list and
1399 the "hide/show" button) to another parent, but always as the
1400 first child position (insert at index 0).
1401 """
1402 if self._contacts.getParent():
1403 if self._contacts.getParent() == parent:
1404 return
1405 self._contacts.removeFromParent()
1406 parent.insert(self._contacts, 0)
1407
1408 def onWindowResized(self, width, height):
1409 _elts = doc().getElementsByClassName('gwt-TabBar')
1410 if not _elts.length:
1411 tab_bar_h = 0
1412 else:
1413 tab_bar_h = _elts.item(0).offsetHeight
1414 ideal_height = Window.getClientHeight() - tab_bar_h
1415 self.setHeight("%s%s" % (ideal_height, "px"))
1416
1417 def refresh(self):
1418 """Refresh the main panel"""
1419 self.unibox_panel.refresh()