comparison src/browser/sat_browser/base_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 e0021d571eef
children 849ffb24d5bf
comparison
equal deleted inserted replaced
647:e0021d571eef 648:6d3142b782c3
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 import pyjd # this is dummy in pyjs 20 import pyjd # this is dummy in pyjs
21 from sat.core.log import getLogger 21 from sat.core.log import getLogger
22 log = getLogger(__name__) 22 log = getLogger(__name__)
23 from sat.core import exceptions 23 import base_menu
24 from sat.core.i18n import _
25 from sat_frontends.quick_frontend import quick_widgets
26 24
27 from pyjamas.ui.SimplePanel import SimplePanel
28 from pyjamas.ui.AbsolutePanel import AbsolutePanel
29 from pyjamas.ui.VerticalPanel import VerticalPanel
30 from pyjamas.ui.HorizontalPanel import HorizontalPanel
31 from pyjamas.ui.ScrollPanel import ScrollPanel
32 from pyjamas.ui.FlexTable import FlexTable
33 from pyjamas.ui.TabPanel import TabPanel
34 from pyjamas.ui.HTMLPanel import HTMLPanel
35 from pyjamas.ui.Label import Label
36 from pyjamas.ui.HTML import HTML
37 from pyjamas.ui.Image import Image
38 from pyjamas.ui.Button import Button
39 from pyjamas.ui.Widget import Widget
40 from pyjamas.ui.DragWidget import DragWidget
41 from pyjamas.ui.DropWidget import DropWidget
42 from pyjamas.ui.ClickListener import ClickHandler
43 from pyjamas.ui import HasAlignment
44 from pyjamas import DOM
45 from pyjamas import Window
46 from constants import Const as C
47 25
48 from __pyjamas__ import doc 26 ### Exceptions ###
49
50 import dialog
51 import base_menu
52 import html_tools
53
54 unicode = str # XXX: pyjama doesn't manage unicode
55 27
56 28
57 class NoLiberviaWidgetException(Exception): 29 class NoLiberviaWidgetException(Exception):
30 """A Libervia widget was expected"""
58 pass 31 pass
59 32
60 33
61 class DragLabel(DragWidget): 34 ### Menus ###
62
63 def __init__(self, text, type_, host=None):
64 """Base of Drag n Drop mecanism in Libervia
65
66 @param text: data embedded with in drag n drop operation
67 @param type_: type of data that we are dragging
68 @param host: if not None, the host will be use to highlight BorderWidgets
69 """
70 DragWidget.__init__(self)
71 self.host = host
72 self._text = text
73 self.type_ = type_
74
75 def onDragStart(self, event):
76 dt = event.dataTransfer
77 dt.setData('text/plain', "%s\n%s" % (self._text, self.type_))
78 dt.setDragImage(self.getElement(), 15, 15)
79 if self.host is not None:
80 current_panel = self.host.tab_panel.getCurrentPanel()
81 for widget in current_panel.widgets:
82 if isinstance(widget, BorderWidget):
83 widget.addStyleName('borderWidgetOnDrag')
84
85 def onDragEnd(self, event):
86 if self.host is not None:
87 current_panel = self.host.tab_panel.getCurrentPanel()
88 for widget in current_panel.widgets:
89 if isinstance(widget, BorderWidget):
90 widget.removeStyleName('borderWidgetOnDrag')
91
92
93 class LiberviaDragWidget(DragLabel):
94 """ A DragLabel which keep the widget being dragged as class value """
95 current = None # widget currently dragged
96
97 def __init__(self, text, type_, widget):
98 DragLabel.__init__(self, text, type_, widget.host)
99 self.widget = widget
100
101 def onDragStart(self, event):
102 LiberviaDragWidget.current = self.widget
103 DragLabel.onDragStart(self, event)
104
105 def onDragEnd(self, event):
106 DragLabel.onDragEnd(self, event)
107 LiberviaDragWidget.current = None
108
109
110 class DropCell(DropWidget):
111 """Cell in the middle grid which replace itself with the dropped widget on DnD"""
112 drop_keys = {}
113
114 def __init__(self, host):
115 DropWidget.__init__(self)
116 self.host = host
117 self.setStyleName('dropCell')
118
119 @classmethod
120 def addDropKey(cls, key, cb):
121 """Add a association between a key and a class to create on drop.
122
123 @param key: key to be associated (e.g. "CONTACT", "CHAT")
124 @param cb: a callable (either a class or method) returning a
125 LiberviaWidget instance
126 """
127 DropCell.drop_keys[key] = cb
128
129 def onDragEnter(self, event):
130 if self == LiberviaDragWidget.current:
131 return
132 self.addStyleName('dragover')
133 DOM.eventPreventDefault(event)
134
135 def onDragLeave(self, event):
136 if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
137 event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
138 # We check that we are inside widget's box, and we don't remove the style in this case because
139 # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
140 # don't want that
141 self.removeStyleName('dragover')
142
143 def onDragOver(self, event):
144 DOM.eventPreventDefault(event)
145
146 def _getCellAndRow(self, grid, event):
147 """Return cell and row index where the event is occuring"""
148 cell = grid.getEventTargetCell(event)
149 row = DOM.getParent(cell)
150 return (row.rowIndex, cell.cellIndex)
151
152 def onDrop(self, event):
153 """
154 @raise NoLiberviaWidgetException: something else than a LiberviaWidget
155 has been returned by the callback.
156 """
157 self.removeStyleName('dragover')
158 DOM.eventPreventDefault(event)
159 dt = event.dataTransfer
160 # 'text', 'text/plain', and 'Text' are equivalent.
161 try:
162 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
163 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
164 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
165 # item_type = dt.getData("type")
166 log.debug("message: %s" % item)
167 log.debug("type: %s" % item_type)
168 except:
169 log.debug("no message found")
170 item = '&nbsp;'
171 item_type = None
172 if item_type == "WIDGET":
173 if not LiberviaDragWidget.current:
174 log.error("No widget registered in LiberviaDragWidget !")
175 return
176 _new_panel = LiberviaDragWidget.current
177 if self == _new_panel: # We can't drop on ourself
178 return
179 # we need to remove the widget from the panel as it will be inserted elsewhere
180 widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
181 wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
182 row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
183 if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
184 # the dropped widget is the only one in the same row
185 # as the target widget (self), we don't do anything
186 return
187 widgets_panel.removeWidget(_new_panel)
188 elif item_type in self.drop_keys:
189 _new_panel = self.drop_keys[item_type](self.host, item)
190 if not isinstance(_new_panel, LiberviaWidget):
191 raise NoLiberviaWidgetException
192 else:
193 log.warning("unmanaged item type")
194 return
195 if isinstance(self, LiberviaWidget):
196 # self.host.unregisterWidget(self) # FIXME
197 self.onQuit()
198 if not isinstance(_new_panel, LiberviaWidget):
199 log.warning("droping an object which is not a class of LiberviaWidget")
200 _flextable = self.getParent()
201 _widgetspanel = _flextable.getParent().getParent()
202 row_idx, cell_idx = self._getCellAndRow(_flextable, event)
203 if self.host.getSelected == self:
204 self.host.setSelected(None)
205 _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
206 """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
207 _width = 90/float(len(_unempty_panels) or 1)
208 #now we resize all the cell of the column
209 for panel in _unempty_panels:
210 td_elt = panel.getElement().parentNode
211 DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
212 if isinstance(self, quick_widgets.QuickWidget):
213 self.host.widgets.deleteWidget(self)
214 35
215 36
216 class WidgetMenuBar(base_menu.GenericMenuBar): 37 class WidgetMenuBar(base_menu.GenericMenuBar):
217 38
218 ITEM_TPL = "<img src='media/icons/misc/%s.png' />" 39 ITEM_TPL = "<img src='media/icons/misc/%s.png' />"
257 base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, flat_level=1) 78 base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, flat_level=1)
258 79
259 @classmethod 80 @classmethod
260 def getCategoryHTML(cls, menu_name_i18n, type_): 81 def getCategoryHTML(cls, menu_name_i18n, type_):
261 return menu_name_i18n 82 return menu_name_i18n
262
263
264 class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
265
266 def __init__(self, parent, host, title, info=None):
267 """
268 @param parent (LiberviaWidget): LiberWidget instance
269 @param host (SatWebFrontend): SatWebFrontend instance
270 @param title (Label, HTML): text widget instance
271 @param info (Widget): text widget instance
272 """
273 AbsolutePanel.__init__(self)
274 self.add(title)
275 if info:
276 # FIXME: temporary design to display the info near the menu
277 button_group_wrapper = HorizontalPanel()
278 button_group_wrapper.add(info)
279 else:
280 button_group_wrapper = SimplePanel()
281 button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
282 button_group = WidgetMenuBar(parent, host)
283 button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.MenuCmd(parent, 'onSetting'))
284 button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.MenuCmd(parent, 'onClose'))
285 button_group_wrapper.add(button_group)
286 self.add(button_group_wrapper)
287 self.addStyleName('widgetHeader')
288 LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
289
290
291 class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
292 """Libervia's widget which can replace itself with a dropped widget on DnD"""
293
294 def __init__(self, host, title='', info=None, selectable=False):
295 """Init the widget
296
297 @param host (SatWebFrontend): SatWebFrontend instance
298 @param title (str): title shown in the header of the widget
299 @param info (str, callable): info shown in the header of the widget
300 @param selectable (bool): True is widget can be selected by user
301 """
302 VerticalPanel.__init__(self)
303 DropCell.__init__(self, host)
304 ClickHandler.__init__(self)
305 self._selectable = selectable
306 self._title_id = HTMLPanel.createUniqueId()
307 self._setting_button_id = HTMLPanel.createUniqueId()
308 self._close_button_id = HTMLPanel.createUniqueId()
309 self._title = Label(title)
310 self._title.setStyleName('widgetHeader_title')
311 if info is not None:
312 if isinstance(info, str):
313 self._info = HTML(info)
314 else: # the info will be set by a callback
315 assert callable(info)
316 self._info = HTML()
317 info(self._info.setHTML)
318 self._info.setStyleName('widgetHeader_info')
319 else:
320 self._info = None
321 header = WidgetHeader(self, host, self._title, self._info)
322 self.add(header)
323 self.setSize('100%', '100%')
324 self.addStyleName('widget')
325 if self._selectable:
326 self.addClickListener(self)
327
328 # FIXME
329 # def onClose(sender):
330 # """Check dynamically if the unibox is enable or not"""
331 # if self.host.uni_box:
332 # self.host.uni_box.onWidgetClosed(sender)
333
334 # self.addCloseListener(onClose)
335 # self.host.registerWidget(self) # FIXME
336
337 def getDebugName(self):
338 return "%s (%s)" % (self, self._title.getText())
339
340 def getParent(self, class_=None, expect=True):
341 """Return the closest ancestor of the specified class.
342
343 Note: this method overrides pyjamas.ui.Widget.getParent
344
345 @param class_: class of the ancestor to look for or None to return the first parent
346 @param expect: set to True if the parent is expected (raise an error if not found)
347 @return: the parent/ancestor or None if it has not been found
348 @raise exceptions.InternalError: expect is True and no parent is found
349 """
350 current = Widget.getParent(self)
351 if class_ is None:
352 return current # this is the default behavior
353 while current is not None and not isinstance(current, class_):
354 current = Widget.getParent(current)
355 if current is None and expect:
356 raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self))
357 return current
358
359 def onClick(self, sender):
360 self.host.setSelected(self)
361
362 def onClose(self, sender):
363 """ Called when the close button is pushed """
364 widgets_panel = self.getParent(WidgetsPanel, expect=True)
365 widgets_panel.removeWidget(self)
366 self.onQuit()
367 self.host.widgets.deleteWidget(self)
368
369 def onQuit(self):
370 """ Called when the widget is actually ending """
371 pass
372
373 def refresh(self):
374 """This can be overwritten by a child class to refresh the display when,
375 instead of creating a new one, an existing widget is found and reused.
376 """
377 pass
378
379 def onSetting(self, sender):
380 widpanel = self.getParent(WidgetsPanel, expect=True)
381 row, col = widpanel.getIndex(self)
382 body = VerticalPanel()
383
384 # colspan & rowspan
385 colspan = widpanel.getColSpan(row, col)
386 rowspan = widpanel.getRowSpan(row, col)
387
388 def onColSpanChange(value):
389 widpanel.setColSpan(row, col, value)
390
391 def onRowSpanChange(value):
392 widpanel.setRowSpan(row, col, value)
393 colspan_setter = dialog.IntSetter("Columns span", colspan)
394 colspan_setter.addValueChangeListener(onColSpanChange)
395 colspan_setter.setWidth('100%')
396 rowspan_setter = dialog.IntSetter("Rows span", rowspan)
397 rowspan_setter.addValueChangeListener(onRowSpanChange)
398 rowspan_setter.setWidth('100%')
399 body.add(colspan_setter)
400 body.add(rowspan_setter)
401
402 # size
403 width_str = self.getWidth()
404 if width_str.endswith('px'):
405 width = int(width_str[:-2])
406 else:
407 width = 0
408 height_str = self.getHeight()
409 if height_str.endswith('px'):
410 height = int(height_str[:-2])
411 else:
412 height = 0
413
414 def onWidthChange(value):
415 if not value:
416 self.setWidth('100%')
417 else:
418 self.setWidth('%dpx' % value)
419
420 def onHeightChange(value):
421 if not value:
422 self.setHeight('100%')
423 else:
424 self.setHeight('%dpx' % value)
425 width_setter = dialog.IntSetter("width (0=auto)", width)
426 width_setter.addValueChangeListener(onWidthChange)
427 width_setter.setWidth('100%')
428 height_setter = dialog.IntSetter("height (0=auto)", height)
429 height_setter.addValueChangeListener(onHeightChange)
430 height_setter.setHeight('100%')
431 body.add(width_setter)
432 body.add(height_setter)
433
434 # reset
435 def onReset(sender):
436 colspan_setter.setValue(1)
437 rowspan_setter.setValue(1)
438 width_setter.setValue(0)
439 height_setter.setValue(0)
440
441 reset_bt = Button("Reset", onReset)
442 body.add(reset_bt)
443 body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
444
445 _dialog = dialog.GenericDialog("Widget setting", body)
446 _dialog.show()
447
448 def setTitle(self, text):
449 """change the title in the header of the widget
450 @param text: text of the new title"""
451 self._title.setText(text)
452
453 def setHeaderInfo(self, text):
454 """change the info in the header of the widget
455 @param text: text of the new title"""
456 try:
457 self._info.setHTML(text)
458 except TypeError:
459 log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
460
461 def isSelectable(self):
462 return self._selectable
463
464 def setSelectable(self, selectable):
465 if not self._selectable:
466 try:
467 self.removeClickListener(self)
468 except ValueError:
469 pass
470 if self.selectable and not self in self._clickListeners:
471 self.addClickListener(self)
472 self._selectable = selectable
473
474 def getWarningData(self):
475 """ Return exposition warning level when this widget is selected and something is sent to it
476 This method should be overriden by children
477 @return: tuple (warning level type/HTML msg). Type can be one of:
478 - PUBLIC
479 - GROUP
480 - ONE2ONE
481 - MISC
482 - NONE
483 """
484 if not self._selectable:
485 log.error("getWarningLevel must not be called for an unselectable widget")
486 raise Exception
487 # TODO: cleaner warning types (more general constants)
488 return ("NONE", None)
489
490 def setWidget(self, widget, scrollable=True):
491 """Set the widget that will be in the body of the LiberviaWidget
492 @param widget: widget to put in the body
493 @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
494 if scrollable:
495 _scrollpanelwrapper = ScrollPanelWrapper()
496 _scrollpanelwrapper.setStyleName('widgetBody')
497 _scrollpanelwrapper.setWidget(widget)
498 body_wid = _scrollpanelwrapper
499 else:
500 body_wid = widget
501 self.add(body_wid)
502 self.setCellHeight(body_wid, '100%')
503
504 def doDetachChildren(self):
505 # We need to force the use of a panel subclass method here,
506 # for the same reason as doAttachChildren
507 VerticalPanel.doDetachChildren(self)
508
509 def doAttachChildren(self):
510 # We need to force the use of a panel subclass method here, else
511 # the event will not propagate to children
512 VerticalPanel.doAttachChildren(self)
513
514 def matchEntity(self, item):
515 """Check if this widget corresponds to the given entity.
516
517 This method should be overwritten by child classes.
518 @return: True if the widget matches the entity"""
519 raise NotImplementedError
520
521 def addMenus(self, menu_bar):
522 """Add menus to the header.
523
524 This method can be overwritten by child classes.
525 @param menu_bar (GenericMenuBar): menu bar of the widget's header
526 """
527 pass
528
529
530 class ScrollPanelWrapper(SimplePanel):
531 """Scroll Panel like component, wich use the full available space
532 to work around percent size issue, it use some of the ideas found
533 here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
534 specially in code given at comment #46, thanks to Stefan Bachert"""
535
536 def __init__(self, *args, **kwargs):
537 SimplePanel.__init__(self)
538 self.spanel = ScrollPanel(*args, **kwargs)
539 SimplePanel.setWidget(self, self.spanel)
540 DOM.setStyleAttribute(self.getElement(), "position", "relative")
541 DOM.setStyleAttribute(self.getElement(), "top", "0px")
542 DOM.setStyleAttribute(self.getElement(), "left", "0px")
543 DOM.setStyleAttribute(self.getElement(), "width", "100%")
544 DOM.setStyleAttribute(self.getElement(), "height", "100%")
545 DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
546 DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
547 DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
548
549 def setWidget(self, widget):
550 self.spanel.setWidget(widget)
551
552 def setScrollPosition(self, position):
553 self.spanel.setScrollPosition(position)
554
555 def scrollToBottom(self):
556 self.setScrollPosition(self.spanel.getElement().scrollHeight)
557
558
559 class EmptyWidget(DropCell, SimplePanel):
560 """Empty dropable panel"""
561
562 def __init__(self, host):
563 SimplePanel.__init__(self)
564 DropCell.__init__(self, host)
565 #self.setWidget(HTML(''))
566 self.setSize('100%', '100%')
567
568
569 class BorderWidget(EmptyWidget):
570 def __init__(self, host):
571 EmptyWidget.__init__(self, host)
572 self.addStyleName('borderPanel')
573
574
575 class LeftBorderWidget(BorderWidget):
576 def __init__(self, host):
577 BorderWidget.__init__(self, host)
578 self.addStyleName('leftBorderWidget')
579
580
581 class RightBorderWidget(BorderWidget):
582 def __init__(self, host):
583 BorderWidget.__init__(self, host)
584 self.addStyleName('rightBorderWidget')
585
586
587 class BottomBorderWidget(BorderWidget):
588 def __init__(self, host):
589 BorderWidget.__init__(self, host)
590 self.addStyleName('bottomBorderWidget')
591
592
593 class WidgetsPanel(ScrollPanelWrapper):
594
595 def __init__(self, host, locked=False):
596 """
597
598 @param host (SatWebFrontend): host instance
599 @param locked (bool): If True, the tab containing self will not be
600 removed when there are no more widget inside self. If False, the
601 tab will be removed with self's last widget.
602 """
603 ScrollPanelWrapper.__init__(self)
604 self.setSize('100%', '100%')
605 self.host = host
606 self.locked = locked
607 self.selected = None
608 self.flextable = FlexTable()
609 self.flextable.setSize('100%', '100%')
610 self.setWidget(self.flextable)
611 self.setStyleName('widgetsPanel')
612 _bottom = BottomBorderWidget(self.host)
613 self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row,
614 # dropping a widget there will add a new row
615 td_elt = _bottom.getElement().parentNode
616 DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit)
617 self._max_cols = 1 # give the maximum number of columns in a raw
618
619 @property
620 def widgets(self):
621 return iter(self.flextable)
622
623 def isLocked(self):
624 return self.locked
625
626 def changeWidget(self, row, col, wid):
627 """Change the widget in the given location, add row or columns when necessary"""
628 log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col))
629 last_row = max(0, self.flextable.getRowCount() - 1)
630 # try: # FIXME: except without exception specified !
631 prev_wid = self.flextable.getWidget(row, col)
632 # except:
633 # log.error("Trying to change an unexisting widget !")
634 # return
635
636 cellFormatter = self.flextable.getFlexCellFormatter()
637
638 if isinstance(prev_wid, BorderWidget):
639 # We are on a border, we must create a row and/or columns
640 prev_wid.removeStyleName('dragover')
641
642 if isinstance(prev_wid, BottomBorderWidget):
643 # We are on the bottom border, we create a new row
644 self.flextable.insertRow(last_row)
645 self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
646 self.flextable.setWidget(last_row, 1, wid)
647 self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
648 cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
649 row = last_row
650
651 elif isinstance(prev_wid, LeftBorderWidget):
652 if col != 0:
653 log.error("LeftBorderWidget must be on the first column !")
654 return
655 self.flextable.insertCell(row, col + 1)
656 self.flextable.setWidget(row, 1, wid)
657
658 elif isinstance(prev_wid, RightBorderWidget):
659 if col != self.flextable.getCellCount(row) - 1:
660 log.error("RightBorderWidget must be on the last column !")
661 return
662 self.flextable.insertCell(row, col)
663 self.flextable.setWidget(row, col, wid)
664
665 else:
666 prev_wid.removeFromParent()
667 self.flextable.setWidget(row, col, wid)
668
669 _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
670 if _max_cols != self._max_cols:
671 self._max_cols = _max_cols
672 self._sizesAdjust()
673
674 def _sizesAdjust(self):
675 cellFormatter = self.flextable.getFlexCellFormatter()
676 width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders
677
678 for row_idx in xrange(self.flextable.getRowCount()):
679 for col_idx in xrange(self.flextable.getCellCount(row_idx)):
680 _widget = self.flextable.getWidget(row_idx, col_idx)
681 if not isinstance(_widget, BorderWidget):
682 td_elt = _widget.getElement().parentNode
683 DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
684
685 last_row = max(0, self.flextable.getRowCount() - 1)
686 cellFormatter.setColSpan(last_row, 0, self._max_cols)
687
688 def addWidget(self, wid):
689 """Add a widget to a new cell on the next to last row"""
690 last_row = max(0, self.flextable.getRowCount() - 1)
691 log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
692 self.changeWidget(last_row, 0, wid)
693
694 def removeWidget(self, wid):
695 """Remove a widget and the cell where it is"""
696 _row, _col = self.flextable.getIndex(wid)
697 self.flextable.remove(wid)
698 self.flextable.removeCell(_row, _col)
699 if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row
700 self.flextable.removeRow(_row)
701 _max_cols = 1
702 for row_idx in xrange(self.flextable.getRowCount()):
703 _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
704 if _max_cols != self._max_cols:
705 self._max_cols = _max_cols
706 self._sizesAdjust()
707 current = self
708
709 blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ?
710
711 if blank_page and not self.isLocked():
712 # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
713 while current is not None:
714 if isinstance(current, MainTabPanel):
715 current.onWidgetPanelRemove(self)
716 return
717 current = current.getParent()
718 log.error("no MainTabPanel found !")
719
720 def getWidgetCoords(self, wid):
721 return self.flextable.getIndex(wid)
722
723 def getLiberviaRowWidgets(self, row):
724 """ Return all the LiberviaWidget in the row """
725 return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
726
727 def getRowWidgets(self, row):
728 """ Return all the widgets in the row """
729 widgets = []
730 cols = self.flextable.getCellCount(row)
731 for col in xrange(cols):
732 widgets.append(self.flextable.getWidget(row, col))
733 return widgets
734
735 def getLiberviaWidgetsCount(self):
736 """ Get count of contained widgets """
737 return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
738
739 def getIndex(self, wid):
740 return self.flextable.getIndex(wid)
741
742 def getColSpan(self, row, col):
743 cellFormatter = self.flextable.getFlexCellFormatter()
744 return cellFormatter.getColSpan(row, col)
745
746 def setColSpan(self, row, col, value):
747 cellFormatter = self.flextable.getFlexCellFormatter()
748 return cellFormatter.setColSpan(row, col, value)
749
750 def getRowSpan(self, row, col):
751 cellFormatter = self.flextable.getFlexCellFormatter()
752 return cellFormatter.getRowSpan(row, col)
753
754 def setRowSpan(self, row, col, value):
755 cellFormatter = self.flextable.getFlexCellFormatter()
756 return cellFormatter.setRowSpan(row, col, value)
757
758
759 class DropTab(Label, DropWidget):
760
761 def __init__(self, tab_panel, text):
762 Label.__init__(self, text)
763 DropWidget.__init__(self, tab_panel)
764 self.tab_panel = tab_panel
765 self.setStyleName('dropCell')
766 self.setWordWrap(False)
767 DOM.setStyleAttribute(self.getElement(), "min-width", "30px")
768
769 def _getIndex(self):
770 """ get current index of the DropTab """
771 # XXX: awful hack, but seems the only way to get index
772 return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
773
774 def onDragEnter(self, event):
775 #if self == LiberviaDragWidget.current:
776 # return
777 self.parent.addStyleName('dragover')
778 DOM.eventPreventDefault(event)
779
780 def onDragLeave(self, event):
781 self.parent.removeStyleName('dragover')
782
783 def onDragOver(self, event):
784 DOM.eventPreventDefault(event)
785
786 def onDrop(self, event):
787 DOM.eventPreventDefault(event)
788 self.parent.removeStyleName('dragover')
789 if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
790 # the widget come from the DragTab, so nothing to do, we let it there
791 return
792
793 # FIXME: quite the same stuff as in DropCell, need some factorisation
794 dt = event.dataTransfer
795 # 'text', 'text/plain', and 'Text' are equivalent.
796 try:
797 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
798 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
799 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
800 # item_type = dt.getData("type")
801 log.debug("message: %s" % item)
802 log.debug("type: %s" % item_type)
803 except:
804 log.debug("no message found")
805 item = '&nbsp;'
806 item_type = None
807 if item_type == "WIDGET":
808 if not LiberviaDragWidget.current:
809 log.error("No widget registered in LiberviaDragWidget !")
810 return
811 _new_panel = LiberviaDragWidget.current
812 _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel)
813 elif item_type in DropCell.drop_keys:
814 _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
815 else:
816 log.warning("unmanaged item type")
817 return
818
819 widgets_panel = self.tab_panel.getWidget(self._getIndex())
820 widgets_panel.addWidget(_new_panel)
821
822
823 class MainTabPanel(TabPanel, ClickHandler):
824
825 def __init__(self, host):
826 TabPanel.__init__(self)
827 ClickHandler.__init__(self)
828 self.host = host
829 self.setStyleName('liberviaTabPanel')
830 self.addStyleName('mainTabPanel')
831 Window.addWindowResizeListener(self)
832
833 self.tabBar.addTab(u'✚', True)
834
835 def onTabSelected(self, sender, tabIndex):
836 if tabIndex < self.getWidgetCount():
837 TabPanel.onTabSelected(self, sender, tabIndex)
838 return
839 # user clicked the "+" tab
840 default_label = _(u'new tab')
841 try:
842 label = Window.prompt(_(u'Name of the new tab'), default_label)
843 if not label:
844 label = default_label
845 except: # this happens when the user prevents the page to open the prompt dialog
846 label = default_label
847 self.addWidgetsTab(label, select=True)
848
849 def getCurrentPanel(self):
850 """ Get the panel of the currently selected tab
851
852 @return: WidgetsPanel
853 """
854 return self.deck.visibleWidget
855
856 def onWindowResized(self, width, height):
857 tab_panel_elt = self.getElement()
858 _elts = doc().getElementsByClassName('gwt-TabBar')
859 if not _elts.length:
860 log.error("no TabBar found, it should exist !")
861 tab_bar_h = 0
862 else:
863 tab_bar_h = _elts.item(0).offsetHeight
864 ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5
865 ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5
866 self.setWidth("%s%s" % (ideal_width, "px"))
867 self.setHeight("%s%s" % (ideal_height, "px"))
868
869 def addTab(self, widget, label, select=False):
870 """Create a new tab for the given widget.
871
872 @param widget (Widget): widget to associate to the tab
873 @param label (unicode): label of the tab
874 @param select (bool): True to select the added tab
875 """
876 TabPanel.add(self, widget, DropTab(self, label), False)
877 if select:
878 self.selectTab(self.getWidgetCount() - 1)
879
880 def addWidgetsTab(self, label, select=False, locked=False):
881 """Create a new tab for containing LiberviaWidgets.
882
883 @param label (unicode): label of the tab
884 @param select (bool): True to select the added tab
885 @param locked (bool): If True, the tab will not be removed when there
886 are no more widget inside. If False, the tab will be removed with
887 the last widget.
888 @return: WidgetsPanel
889 """
890 widgets_panel = WidgetsPanel(self, locked=locked)
891 self.addTab(widgets_panel, label, select)
892 return widgets_panel
893
894 def onWidgetPanelRemove(self, panel):
895 """ Called when a child WidgetsPanel is empty and need to be removed """
896 widget_index = self.getWidgetIndex(panel)
897 self.remove(panel)
898 widgets_count = self.getWidgetCount()
899 self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
900
901
902 class ContactLabel(HTML):
903 """Display a contact in HTML, selecting best display (jid/nick/etc)"""
904
905 def __init__(self, host, jid_):
906 # TODO: add a listener for nick changes
907 HTML.__init__(self)
908 self.host = host
909 self.jid = jid_.bare
910 self.nick = self.host.contact_lists[C.PROF_KEY_NONE].getCache(self.jid, "nick")
911 self.alert = False
912 self.refresh()
913 self.setStyleName('contactLabel')
914
915 def refresh(self):
916 alert_html = "<strong>(*)</strong>&nbsp;" if self.alert else ""
917 contact_html = html_tools.html_sanitize(self.nick or unicode(self.jid))
918 html = "%(alert)s%(contact)s" % {'alert': alert_html,
919 'contact': contact_html}
920 self.setHTML(html)
921
922 def updateNick(self, new_nick):
923 """Change the current nick
924
925 @param new_nick(unicode): new nick to use
926 """
927 self.nick = new_nick
928 self.refresh()
929
930 def setAlert(self, alert):
931 """Show a visual indicator
932
933 @param alert: True if alert must be shown
934 """
935 self.alert = alert
936 self.refresh()
937
938
939 class ContactMenuBar(WidgetMenuBar):
940
941 def onBrowserEvent(self, event):
942 WidgetMenuBar.onBrowserEvent(self, event)
943 event.stopPropagation() # prevent opening the chat dialog
944
945 @classmethod
946 def getCategoryHTML(cls, menu_name_i18n, type_):
947 return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL
948
949 def setUrl(self, url):
950 """Set the URL of the contact avatar."""
951 self.items[0].setHTML('<img src="%s" />' % url)
952
953
954 class ContactBox(VerticalPanel, ClickHandler, DragLabel):
955
956 def __init__(self, parent, jid_):
957 """
958 @param parent (ContactPanel): ContactPanel hosting this box
959 @param jid_ (jid.JID): contact JID
960 """
961 VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
962 ClickHandler.__init__(self)
963 DragLabel.__init__(self, jid_, "CONTACT", parent.host)
964 self.jid = jid_.bare
965 self.label = ContactLabel(parent.host, self.jid)
966 self.avatar = ContactMenuBar(self, parent.host) if parent.handle_menu else Image()
967 self.updateAvatar(parent.host.getAvatarURL(self.jid))
968 self.add(self.avatar)
969 self.add(self.label)
970 self.addClickListener(self)
971
972 def addMenus(self, menu_bar):
973 menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': unicode(self.jid)})
974 menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': unicode(self.jid)})
975
976 def setAlert(self, alert):
977 """Show a visual indicator
978
979 @param alert: True if alert indicator show be shown"""
980 self.label.setAlert(alert)
981
982 def updateAvatar(self, url):
983 """Update the avatar.
984
985 @param url (unicode): image url
986 """
987 self.avatar.setUrl(url)
988
989 def updateNick(self, new_nick):
990 """Update the nickname.
991
992 @param new_nick (unicode): new nickname to use
993 """
994 self.label.updateNick(new_nick)
995
996 def onClick(self, sender):
997 try:
998 self.parent.onClick(self.jid)
999 except AttributeError:
1000 pass
1001 else:
1002 self.setAlert(False)