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

browser_side: classes reorganisation: - moved widgets in dedicated modules (base, contact, editor, libervia) and a widget module for single classes - same thing for panels (base, main, contact) - libervia_widget mix main panels and widget and drag n drop for technical reasons (see comments) - renamed WebPanel to WebWidget
author Goffi <goffi@goffi.org>
date Thu, 26 Feb 2015 18:10:54 +0100
parents src/browser/sat_browser/base_widget.py@e0021d571eef
children ccf95ec87005
comparison
equal deleted inserted replaced
647:e0021d571eef 648:6d3142b782c3
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 """Libervia base widget"""
20
21 import pyjd # this is dummy in pyjs
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24
25 from sat.core.i18n import _
26 from sat.core import exceptions
27 from sat_frontends.quick_frontend import quick_widgets
28
29 from pyjamas.ui.FlexTable import FlexTable
30 from pyjamas.ui.TabPanel import TabPanel
31 from pyjamas.ui.SimplePanel import SimplePanel
32 from pyjamas.ui.AbsolutePanel import AbsolutePanel
33 from pyjamas.ui.VerticalPanel import VerticalPanel
34 from pyjamas.ui.HorizontalPanel import HorizontalPanel
35 from pyjamas.ui.HTMLPanel import HTMLPanel
36 from pyjamas.ui.Label import Label
37 from pyjamas.ui.HTML import HTML
38 from pyjamas.ui.Button import Button
39 from pyjamas.ui.Widget import Widget
40 from pyjamas.ui.ClickListener import ClickHandler
41 from pyjamas.ui import HasAlignment
42 from pyjamas.ui.DragWidget import DragWidget
43 from pyjamas.ui.DropWidget import DropWidget
44 from pyjamas import DOM
45 from pyjamas import Window
46 from __pyjamas__ import doc
47
48 import dialog
49 import base_menu
50 import base_widget
51 import base_panel
52
53
54 # FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell).
55 # TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules
56
57
58 ### Drag n Drop ###
59
60
61 class DragLabel(DragWidget):
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 class DropCell(DropWidget):
110 """Cell in the middle grid which replace itself with the dropped widget on DnD"""
111 drop_keys = {}
112
113 def __init__(self, host):
114 DropWidget.__init__(self)
115 self.host = host
116 self.setStyleName('dropCell')
117
118 @classmethod
119 def addDropKey(cls, key, cb):
120 """Add a association between a key and a class to create on drop.
121
122 @param key: key to be associated (e.g. "CONTACT", "CHAT")
123 @param cb: a callable (either a class or method) returning a
124 LiberviaWidget instance
125 """
126 DropCell.drop_keys[key] = cb
127
128 def onDragEnter(self, event):
129 if self == LiberviaDragWidget.current:
130 return
131 self.addStyleName('dragover')
132 DOM.eventPreventDefault(event)
133
134 def onDragLeave(self, event):
135 if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
136 event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
137 # We check that we are inside widget's box, and we don't remove the style in this case because
138 # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
139 # don't want that
140 self.removeStyleName('dragover')
141
142 def onDragOver(self, event):
143 DOM.eventPreventDefault(event)
144
145 def _getCellAndRow(self, grid, event):
146 """Return cell and row index where the event is occuring"""
147 cell = grid.getEventTargetCell(event)
148 row = DOM.getParent(cell)
149 return (row.rowIndex, cell.cellIndex)
150
151 def onDrop(self, event):
152 """
153 @raise NoLiberviaWidgetException: something else than a LiberviaWidget
154 has been returned by the callback.
155 """
156 self.removeStyleName('dragover')
157 DOM.eventPreventDefault(event)
158 dt = event.dataTransfer
159 # 'text', 'text/plain', and 'Text' are equivalent.
160 try:
161 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
162 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
163 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
164 # item_type = dt.getData("type")
165 log.debug("message: %s" % item)
166 log.debug("type: %s" % item_type)
167 except:
168 log.debug("no message found")
169 item = '&nbsp;'
170 item_type = None
171 if item_type == "WIDGET":
172 if not LiberviaDragWidget.current:
173 log.error("No widget registered in LiberviaDragWidget !")
174 return
175 _new_panel = LiberviaDragWidget.current
176 if self == _new_panel: # We can't drop on ourself
177 return
178 # we need to remove the widget from the panel as it will be inserted elsewhere
179 widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
180 wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
181 row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
182 if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
183 # the dropped widget is the only one in the same row
184 # as the target widget (self), we don't do anything
185 return
186 widgets_panel.removeWidget(_new_panel)
187 elif item_type in self.drop_keys:
188 _new_panel = self.drop_keys[item_type](self.host, item)
189 if not isinstance(_new_panel, LiberviaWidget):
190 raise base_widget.NoLiberviaWidgetException
191 else:
192 log.warning("unmanaged item type")
193 return
194 if isinstance(self, LiberviaWidget):
195 # self.host.unregisterWidget(self) # FIXME
196 self.onQuit()
197 if not isinstance(_new_panel, LiberviaWidget):
198 log.warning("droping an object which is not a class of LiberviaWidget")
199 _flextable = self.getParent()
200 _widgetspanel = _flextable.getParent().getParent()
201 row_idx, cell_idx = self._getCellAndRow(_flextable, event)
202 if self.host.getSelected == self:
203 self.host.setSelected(None)
204 _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
205 """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
206 _width = 90/float(len(_unempty_panels) or 1)
207 #now we resize all the cell of the column
208 for panel in _unempty_panels:
209 td_elt = panel.getElement().parentNode
210 DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
211 if isinstance(self, quick_widgets.QuickWidget):
212 self.host.widgets.deleteWidget(self)
213
214
215 class EmptyWidget(DropCell, SimplePanel):
216 """Empty dropable panel"""
217
218 def __init__(self, host):
219 SimplePanel.__init__(self)
220 DropCell.__init__(self, host)
221 #self.setWidget(HTML(''))
222 self.setSize('100%', '100%')
223
224
225 class BorderWidget(EmptyWidget):
226 def __init__(self, host):
227 EmptyWidget.__init__(self, host)
228 self.addStyleName('borderPanel')
229
230
231 class LeftBorderWidget(BorderWidget):
232 def __init__(self, host):
233 BorderWidget.__init__(self, host)
234 self.addStyleName('leftBorderWidget')
235
236
237 class RightBorderWidget(BorderWidget):
238 def __init__(self, host):
239 BorderWidget.__init__(self, host)
240 self.addStyleName('rightBorderWidget')
241
242
243 class BottomBorderWidget(BorderWidget):
244 def __init__(self, host):
245 BorderWidget.__init__(self, host)
246 self.addStyleName('bottomBorderWidget')
247
248
249 class DropTab(Label, DropWidget):
250
251 def __init__(self, tab_panel, text):
252 Label.__init__(self, text)
253 DropWidget.__init__(self, tab_panel)
254 self.tab_panel = tab_panel
255 self.setStyleName('dropCell')
256 self.setWordWrap(False)
257 DOM.setStyleAttribute(self.getElement(), "min-width", "30px")
258
259 def _getIndex(self):
260 """ get current index of the DropTab """
261 # XXX: awful hack, but seems the only way to get index
262 return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
263
264 def onDragEnter(self, event):
265 #if self == LiberviaDragWidget.current:
266 # return
267 self.parent.addStyleName('dragover')
268 DOM.eventPreventDefault(event)
269
270 def onDragLeave(self, event):
271 self.parent.removeStyleName('dragover')
272
273 def onDragOver(self, event):
274 DOM.eventPreventDefault(event)
275
276 def onDrop(self, event):
277 DOM.eventPreventDefault(event)
278 self.parent.removeStyleName('dragover')
279 if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
280 # the widget come from the DragTab, so nothing to do, we let it there
281 return
282
283 # FIXME: quite the same stuff as in DropCell, need some factorisation
284 dt = event.dataTransfer
285 # 'text', 'text/plain', and 'Text' are equivalent.
286 try:
287 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
288 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
289 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
290 # item_type = dt.getData("type")
291 log.debug("message: %s" % item)
292 log.debug("type: %s" % item_type)
293 except:
294 log.debug("no message found")
295 item = '&nbsp;'
296 item_type = None
297 if item_type == "WIDGET":
298 if not LiberviaDragWidget.current:
299 log.error("No widget registered in LiberviaDragWidget !")
300 return
301 _new_panel = LiberviaDragWidget.current
302 _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel)
303 elif item_type in DropCell.drop_keys:
304 _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
305 else:
306 log.warning("unmanaged item type")
307 return
308
309 widgets_panel = self.tab_panel.getWidget(self._getIndex())
310 widgets_panel.addWidget(_new_panel)
311
312
313 ### Libervia Widget ###
314
315
316 class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
317
318 def __init__(self, parent, host, title, info=None):
319 """
320 @param parent (LiberviaWidget): LiberWidget instance
321 @param host (SatWebFrontend): SatWebFrontend instance
322 @param title (Label, HTML): text widget instance
323 @param info (Widget): text widget instance
324 """
325 AbsolutePanel.__init__(self)
326 self.add(title)
327 if info:
328 # FIXME: temporary design to display the info near the menu
329 button_group_wrapper = HorizontalPanel()
330 button_group_wrapper.add(info)
331 else:
332 button_group_wrapper = SimplePanel()
333 button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
334 button_group = base_widget.WidgetMenuBar(parent, host)
335 button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.MenuCmd(parent, 'onSetting'))
336 button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.MenuCmd(parent, 'onClose'))
337 button_group_wrapper.add(button_group)
338 self.add(button_group_wrapper)
339 self.addStyleName('widgetHeader')
340 LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
341
342
343 class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
344 """Libervia's widget which can replace itself with a dropped widget on DnD"""
345
346 def __init__(self, host, title='', info=None, selectable=False):
347 """Init the widget
348
349 @param host (SatWebFrontend): SatWebFrontend instance
350 @param title (str): title shown in the header of the widget
351 @param info (str, callable): info shown in the header of the widget
352 @param selectable (bool): True is widget can be selected by user
353 """
354 VerticalPanel.__init__(self)
355 DropCell.__init__(self, host)
356 ClickHandler.__init__(self)
357 self._selectable = selectable
358 self._title_id = HTMLPanel.createUniqueId()
359 self._setting_button_id = HTMLPanel.createUniqueId()
360 self._close_button_id = HTMLPanel.createUniqueId()
361 self._title = Label(title)
362 self._title.setStyleName('widgetHeader_title')
363 if info is not None:
364 if isinstance(info, str):
365 self._info = HTML(info)
366 else: # the info will be set by a callback
367 assert callable(info)
368 self._info = HTML()
369 info(self._info.setHTML)
370 self._info.setStyleName('widgetHeader_info')
371 else:
372 self._info = None
373 header = WidgetHeader(self, host, self._title, self._info)
374 self.add(header)
375 self.setSize('100%', '100%')
376 self.addStyleName('widget')
377 if self._selectable:
378 self.addClickListener(self)
379
380 # FIXME
381 # def onClose(sender):
382 # """Check dynamically if the unibox is enable or not"""
383 # if self.host.uni_box:
384 # self.host.uni_box.onWidgetClosed(sender)
385
386 # self.addCloseListener(onClose)
387 # self.host.registerWidget(self) # FIXME
388
389 def getDebugName(self):
390 return "%s (%s)" % (self, self._title.getText())
391
392 def getParent(self, class_=None, expect=True):
393 """Return the closest ancestor of the specified class.
394
395 Note: this method overrides pyjamas.ui.Widget.getParent
396
397 @param class_: class of the ancestor to look for or None to return the first parent
398 @param expect: set to True if the parent is expected (raise an error if not found)
399 @return: the parent/ancestor or None if it has not been found
400 @raise exceptions.InternalError: expect is True and no parent is found
401 """
402 current = Widget.getParent(self)
403 if class_ is None:
404 return current # this is the default behavior
405 while current is not None and not isinstance(current, class_):
406 current = Widget.getParent(current)
407 if current is None and expect:
408 raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self))
409 return current
410
411 def onClick(self, sender):
412 self.host.setSelected(self)
413
414 def onClose(self, sender):
415 """ Called when the close button is pushed """
416 widgets_panel = self.getParent(WidgetsPanel, expect=True)
417 widgets_panel.removeWidget(self)
418 self.onQuit()
419 self.host.widgets.deleteWidget(self)
420
421 def onQuit(self):
422 """ Called when the widget is actually ending """
423 pass
424
425 def refresh(self):
426 """This can be overwritten by a child class to refresh the display when,
427 instead of creating a new one, an existing widget is found and reused.
428 """
429 pass
430
431 def onSetting(self, sender):
432 widpanel = self.getParent(WidgetsPanel, expect=True)
433 row, col = widpanel.getIndex(self)
434 body = VerticalPanel()
435
436 # colspan & rowspan
437 colspan = widpanel.getColSpan(row, col)
438 rowspan = widpanel.getRowSpan(row, col)
439
440 def onColSpanChange(value):
441 widpanel.setColSpan(row, col, value)
442
443 def onRowSpanChange(value):
444 widpanel.setRowSpan(row, col, value)
445 colspan_setter = dialog.IntSetter("Columns span", colspan)
446 colspan_setter.addValueChangeListener(onColSpanChange)
447 colspan_setter.setWidth('100%')
448 rowspan_setter = dialog.IntSetter("Rows span", rowspan)
449 rowspan_setter.addValueChangeListener(onRowSpanChange)
450 rowspan_setter.setWidth('100%')
451 body.add(colspan_setter)
452 body.add(rowspan_setter)
453
454 # size
455 width_str = self.getWidth()
456 if width_str.endswith('px'):
457 width = int(width_str[:-2])
458 else:
459 width = 0
460 height_str = self.getHeight()
461 if height_str.endswith('px'):
462 height = int(height_str[:-2])
463 else:
464 height = 0
465
466 def onWidthChange(value):
467 if not value:
468 self.setWidth('100%')
469 else:
470 self.setWidth('%dpx' % value)
471
472 def onHeightChange(value):
473 if not value:
474 self.setHeight('100%')
475 else:
476 self.setHeight('%dpx' % value)
477 width_setter = dialog.IntSetter("width (0=auto)", width)
478 width_setter.addValueChangeListener(onWidthChange)
479 width_setter.setWidth('100%')
480 height_setter = dialog.IntSetter("height (0=auto)", height)
481 height_setter.addValueChangeListener(onHeightChange)
482 height_setter.setHeight('100%')
483 body.add(width_setter)
484 body.add(height_setter)
485
486 # reset
487 def onReset(sender):
488 colspan_setter.setValue(1)
489 rowspan_setter.setValue(1)
490 width_setter.setValue(0)
491 height_setter.setValue(0)
492
493 reset_bt = Button("Reset", onReset)
494 body.add(reset_bt)
495 body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
496
497 _dialog = dialog.GenericDialog("Widget setting", body)
498 _dialog.show()
499
500 def setTitle(self, text):
501 """change the title in the header of the widget
502 @param text: text of the new title"""
503 self._title.setText(text)
504
505 def setHeaderInfo(self, text):
506 """change the info in the header of the widget
507 @param text: text of the new title"""
508 try:
509 self._info.setHTML(text)
510 except TypeError:
511 log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
512
513 def isSelectable(self):
514 return self._selectable
515
516 def setSelectable(self, selectable):
517 if not self._selectable:
518 try:
519 self.removeClickListener(self)
520 except ValueError:
521 pass
522 if self.selectable and not self in self._clickListeners:
523 self.addClickListener(self)
524 self._selectable = selectable
525
526 def getWarningData(self):
527 """ Return exposition warning level when this widget is selected and something is sent to it
528 This method should be overriden by children
529 @return: tuple (warning level type/HTML msg). Type can be one of:
530 - PUBLIC
531 - GROUP
532 - ONE2ONE
533 - MISC
534 - NONE
535 """
536 if not self._selectable:
537 log.error("getWarningLevel must not be called for an unselectable widget")
538 raise Exception
539 # TODO: cleaner warning types (more general constants)
540 return ("NONE", None)
541
542 def setWidget(self, widget, scrollable=True):
543 """Set the widget that will be in the body of the LiberviaWidget
544 @param widget: widget to put in the body
545 @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
546 if scrollable:
547 _scrollpanelwrapper = base_panel.ScrollPanelWrapper()
548 _scrollpanelwrapper.setStyleName('widgetBody')
549 _scrollpanelwrapper.setWidget(widget)
550 body_wid = _scrollpanelwrapper
551 else:
552 body_wid = widget
553 self.add(body_wid)
554 self.setCellHeight(body_wid, '100%')
555
556 def doDetachChildren(self):
557 # We need to force the use of a panel subclass method here,
558 # for the same reason as doAttachChildren
559 VerticalPanel.doDetachChildren(self)
560
561 def doAttachChildren(self):
562 # We need to force the use of a panel subclass method here, else
563 # the event will not propagate to children
564 VerticalPanel.doAttachChildren(self)
565
566 def matchEntity(self, item):
567 """Check if this widget corresponds to the given entity.
568
569 This method should be overwritten by child classes.
570 @return: True if the widget matches the entity"""
571 raise NotImplementedError
572
573 def addMenus(self, menu_bar):
574 """Add menus to the header.
575
576 This method can be overwritten by child classes.
577 @param menu_bar (GenericMenuBar): menu bar of the widget's header
578 """
579 pass
580
581
582 # XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import
583
584
585 class WidgetsPanel(base_panel.ScrollPanelWrapper):
586 """The panel wanaging the widgets indide a tab"""
587
588 def __init__(self, host, locked=False):
589 """
590
591 @param host (SatWebFrontend): host instance
592 @param locked (bool): If True, the tab containing self will not be
593 removed when there are no more widget inside self. If False, the
594 tab will be removed with self's last widget.
595 """
596 base_panel.ScrollPanelWrapper.__init__(self)
597 self.setSize('100%', '100%')
598 self.host = host
599 self.locked = locked
600 self.selected = None
601 self.flextable = FlexTable()
602 self.flextable.setSize('100%', '100%')
603 self.setWidget(self.flextable)
604 self.setStyleName('widgetsPanel')
605 _bottom = BottomBorderWidget(self.host)
606 self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row,
607 # dropping a widget there will add a new row
608 td_elt = _bottom.getElement().parentNode
609 DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit)
610 self._max_cols = 1 # give the maximum number of columns in a raw
611
612 @property
613 def widgets(self):
614 return iter(self.flextable)
615
616 def isLocked(self):
617 return self.locked
618
619 def changeWidget(self, row, col, wid):
620 """Change the widget in the given location, add row or columns when necessary"""
621 log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col))
622 last_row = max(0, self.flextable.getRowCount() - 1)
623 # try: # FIXME: except without exception specified !
624 prev_wid = self.flextable.getWidget(row, col)
625 # except:
626 # log.error("Trying to change an unexisting widget !")
627 # return
628
629 cellFormatter = self.flextable.getFlexCellFormatter()
630
631 if isinstance(prev_wid, BorderWidget):
632 # We are on a border, we must create a row and/or columns
633 prev_wid.removeStyleName('dragover')
634
635 if isinstance(prev_wid, BottomBorderWidget):
636 # We are on the bottom border, we create a new row
637 self.flextable.insertRow(last_row)
638 self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
639 self.flextable.setWidget(last_row, 1, wid)
640 self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
641 cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
642 row = last_row
643
644 elif isinstance(prev_wid, LeftBorderWidget):
645 if col != 0:
646 log.error("LeftBorderWidget must be on the first column !")
647 return
648 self.flextable.insertCell(row, col + 1)
649 self.flextable.setWidget(row, 1, wid)
650
651 elif isinstance(prev_wid, RightBorderWidget):
652 if col != self.flextable.getCellCount(row) - 1:
653 log.error("RightBorderWidget must be on the last column !")
654 return
655 self.flextable.insertCell(row, col)
656 self.flextable.setWidget(row, col, wid)
657
658 else:
659 prev_wid.removeFromParent()
660 self.flextable.setWidget(row, col, wid)
661
662 _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
663 if _max_cols != self._max_cols:
664 self._max_cols = _max_cols
665 self._sizesAdjust()
666
667 def _sizesAdjust(self):
668 cellFormatter = self.flextable.getFlexCellFormatter()
669 width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders
670
671 for row_idx in xrange(self.flextable.getRowCount()):
672 for col_idx in xrange(self.flextable.getCellCount(row_idx)):
673 _widget = self.flextable.getWidget(row_idx, col_idx)
674 if not isinstance(_widget, BorderWidget):
675 td_elt = _widget.getElement().parentNode
676 DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
677
678 last_row = max(0, self.flextable.getRowCount() - 1)
679 cellFormatter.setColSpan(last_row, 0, self._max_cols)
680
681 def addWidget(self, wid):
682 """Add a widget to a new cell on the next to last row"""
683 last_row = max(0, self.flextable.getRowCount() - 1)
684 log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
685 self.changeWidget(last_row, 0, wid)
686
687 def removeWidget(self, wid):
688 """Remove a widget and the cell where it is"""
689 _row, _col = self.flextable.getIndex(wid)
690 self.flextable.remove(wid)
691 self.flextable.removeCell(_row, _col)
692 if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row
693 self.flextable.removeRow(_row)
694 _max_cols = 1
695 for row_idx in xrange(self.flextable.getRowCount()):
696 _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
697 if _max_cols != self._max_cols:
698 self._max_cols = _max_cols
699 self._sizesAdjust()
700 current = self
701
702 blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ?
703
704 if blank_page and not self.isLocked():
705 # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
706 while current is not None:
707 if isinstance(current, MainTabPanel):
708 current.onWidgetPanelRemove(self)
709 return
710 current = current.getParent()
711 log.error("no MainTabPanel found !")
712
713 def getWidgetCoords(self, wid):
714 return self.flextable.getIndex(wid)
715
716 def getLiberviaRowWidgets(self, row):
717 """ Return all the LiberviaWidget in the row """
718 return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
719
720 def getRowWidgets(self, row):
721 """ Return all the widgets in the row """
722 widgets = []
723 cols = self.flextable.getCellCount(row)
724 for col in xrange(cols):
725 widgets.append(self.flextable.getWidget(row, col))
726 return widgets
727
728 def getLiberviaWidgetsCount(self):
729 """ Get count of contained widgets """
730 return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
731
732 def getIndex(self, wid):
733 return self.flextable.getIndex(wid)
734
735 def getColSpan(self, row, col):
736 cellFormatter = self.flextable.getFlexCellFormatter()
737 return cellFormatter.getColSpan(row, col)
738
739 def setColSpan(self, row, col, value):
740 cellFormatter = self.flextable.getFlexCellFormatter()
741 return cellFormatter.setColSpan(row, col, value)
742
743 def getRowSpan(self, row, col):
744 cellFormatter = self.flextable.getFlexCellFormatter()
745 return cellFormatter.getRowSpan(row, col)
746
747 def setRowSpan(self, row, col, value):
748 cellFormatter = self.flextable.getFlexCellFormatter()
749 return cellFormatter.setRowSpan(row, col, value)
750
751
752 class MainTabPanel(TabPanel, ClickHandler):
753 """The panel managing the tabs"""
754
755 def __init__(self, host):
756 TabPanel.__init__(self)
757 ClickHandler.__init__(self)
758 self.host = host
759 self.setStyleName('liberviaTabPanel')
760 self.addStyleName('mainTabPanel')
761 Window.addWindowResizeListener(self)
762
763 self.tabBar.addTab(u'✚', True)
764
765 def onTabSelected(self, sender, tabIndex):
766 if tabIndex < self.getWidgetCount():
767 TabPanel.onTabSelected(self, sender, tabIndex)
768 return
769 # user clicked the "+" tab
770 default_label = _(u'new tab')
771 try:
772 label = Window.prompt(_(u'Name of the new tab'), default_label)
773 if not label:
774 label = default_label
775 except: # this happens when the user prevents the page to open the prompt dialog
776 label = default_label
777 self.addWidgetsTab(label, select=True)
778
779 def getCurrentPanel(self):
780 """ Get the panel of the currently selected tab
781
782 @return: WidgetsPanel
783 """
784 return self.deck.visibleWidget
785
786 def onWindowResized(self, width, height):
787 tab_panel_elt = self.getElement()
788 _elts = doc().getElementsByClassName('gwt-TabBar')
789 if not _elts.length:
790 log.error("no TabBar found, it should exist !")
791 tab_bar_h = 0
792 else:
793 tab_bar_h = _elts.item(0).offsetHeight
794 ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5
795 ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5
796 self.setWidth("%s%s" % (ideal_width, "px"))
797 self.setHeight("%s%s" % (ideal_height, "px"))
798
799 def addTab(self, widget, label, select=False):
800 """Create a new tab for the given widget.
801
802 @param widget (Widget): widget to associate to the tab
803 @param label (unicode): label of the tab
804 @param select (bool): True to select the added tab
805 """
806 TabPanel.add(self, widget, DropTab(self, label), False)
807 if select:
808 self.selectTab(self.getWidgetCount() - 1)
809
810 def addWidgetsTab(self, label, select=False, locked=False):
811 """Create a new tab for containing LiberviaWidgets.
812
813 @param label (unicode): label of the tab
814 @param select (bool): True to select the added tab
815 @param locked (bool): If True, the tab will not be removed when there
816 are no more widget inside. If False, the tab will be removed with
817 the last widget.
818 @return: WidgetsPanel
819 """
820 widgets_panel = WidgetsPanel(self, locked=locked)
821 self.addTab(widgets_panel, label, select)
822 return widgets_panel
823
824 def onWidgetPanelRemove(self, panel):
825 """ Called when a child WidgetsPanel is empty and need to be removed """
826 widget_index = self.getWidgetIndex(panel)
827 self.remove(panel)
828 widgets_count = self.getWidgetCount()
829 self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
830
831