Mercurial > libervia-web
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 = ' ' | |
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 = ' ' | |
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> " 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) |