comparison src/browser/sat_browser/libervia_widget.py @ 679:a90cc8fc9605

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 16:15:18 +0100
parents 849ffb24d5bf
children 9877607c719a
comparison
equal deleted inserted replaced
590:1bffc4c244c3 679:a90cc8fc9605
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
47 import dialog
48 import base_menu
49 import base_widget
50 import base_panel
51
52
53 unicode = str # FIXME: pyjamas workaround
54
55
56 # 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).
57 # 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
58
59
60 ### Drag n Drop ###
61
62
63 class DragLabel(DragWidget):
64
65 def __init__(self, text, type_, host=None):
66 """Base of Drag n Drop mecanism in Libervia
67
68 @param text: data embedded with in drag n drop operation
69 @param type_: type of data that we are dragging
70 @param host: if not None, the host will be use to highlight BorderWidgets
71 """
72 DragWidget.__init__(self)
73 self.host = host
74 self._text = text
75 self.type_ = type_
76
77 def onDragStart(self, event):
78 dt = event.dataTransfer
79 dt.setData('text/plain', "%s\n%s" % (self._text, self.type_))
80 dt.setDragImage(self.getElement(), 15, 15)
81 if self.host is not None:
82 current_panel = self.host.tab_panel.getCurrentPanel()
83 for widget in current_panel.widgets:
84 if isinstance(widget, BorderWidget):
85 widget.addStyleName('borderWidgetOnDrag')
86
87 def onDragEnd(self, event):
88 if self.host is not None:
89 current_panel = self.host.tab_panel.getCurrentPanel()
90 for widget in current_panel.widgets:
91 if isinstance(widget, BorderWidget):
92 widget.removeStyleName('borderWidgetOnDrag')
93
94
95 class LiberviaDragWidget(DragLabel):
96 """ A DragLabel which keep the widget being dragged as class value """
97 current = None # widget currently dragged
98
99 def __init__(self, text, type_, widget):
100 DragLabel.__init__(self, text, type_, widget.host)
101 self.widget = widget
102
103 def onDragStart(self, event):
104 LiberviaDragWidget.current = self.widget
105 DragLabel.onDragStart(self, event)
106
107 def onDragEnd(self, event):
108 DragLabel.onDragEnd(self, event)
109 LiberviaDragWidget.current = None
110
111
112 class DropCell(DropWidget):
113 """Cell in the middle grid which replace itself with the dropped widget on DnD"""
114 drop_keys = {}
115
116 def __init__(self, host):
117 DropWidget.__init__(self)
118 self.host = host
119 self.setStyleName('dropCell')
120
121 @classmethod
122 def addDropKey(cls, key, cb):
123 """Add a association between a key and a class to create on drop.
124
125 @param key: key to be associated (e.g. "CONTACT", "CHAT")
126 @param cb: a callable (either a class or method) returning a
127 LiberviaWidget instance
128 """
129 DropCell.drop_keys[key] = cb
130
131 def onDragEnter(self, event):
132 if self == LiberviaDragWidget.current:
133 return
134 self.addStyleName('dragover')
135 DOM.eventPreventDefault(event)
136
137 def onDragLeave(self, event):
138 if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
139 event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
140 # We check that we are inside widget's box, and we don't remove the style in this case because
141 # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
142 # don't want that
143 self.removeStyleName('dragover')
144
145 def onDragOver(self, event):
146 DOM.eventPreventDefault(event)
147
148 def _getCellAndRow(self, grid, event):
149 """Return cell and row index where the event is occuring"""
150 cell = grid.getEventTargetCell(event)
151 row = DOM.getParent(cell)
152 return (row.rowIndex, cell.cellIndex)
153
154 def onDrop(self, event):
155 """
156 @raise NoLiberviaWidgetException: something else than a LiberviaWidget
157 has been returned by the callback.
158 """
159 self.removeStyleName('dragover')
160 DOM.eventPreventDefault(event)
161 dt = event.dataTransfer
162 # 'text', 'text/plain', and 'Text' are equivalent.
163 try:
164 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
165 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
166 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
167 # item_type = dt.getData("type")
168 log.debug("message: %s" % item)
169 log.debug("type: %s" % item_type)
170 except:
171 log.debug("no message found")
172 item = '&nbsp;'
173 item_type = None
174 if item_type == "WIDGET":
175 if not LiberviaDragWidget.current:
176 log.error("No widget registered in LiberviaDragWidget !")
177 return
178 _new_panel = LiberviaDragWidget.current
179 if self == _new_panel: # We can't drop on ourself
180 return
181 # we need to remove the widget from the panel as it will be inserted elsewhere
182 widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
183 wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
184 row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
185 if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
186 # the dropped widget is the only one in the same row
187 # as the target widget (self), we don't do anything
188 return
189 widgets_panel.removeWidget(_new_panel)
190 elif item_type in self.drop_keys:
191 _new_panel = self.drop_keys[item_type](self.host, item)
192 if not isinstance(_new_panel, LiberviaWidget):
193 raise base_widget.NoLiberviaWidgetException
194 else:
195 log.warning("unmanaged item type")
196 return
197 if isinstance(self, LiberviaWidget):
198 # self.host.unregisterWidget(self) # FIXME
199 self.onQuit()
200 if not isinstance(_new_panel, LiberviaWidget):
201 log.warning("droping an object which is not a class of LiberviaWidget")
202 _flextable = self.getParent()
203 _widgetspanel = _flextable.getParent().getParent()
204 row_idx, cell_idx = self._getCellAndRow(_flextable, event)
205 if self.host.getSelected() == self:
206 self.host.setSelected(None)
207 _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
208 """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
209 _width = 90/float(len(_unempty_panels) or 1)
210 #now we resize all the cell of the column
211 for panel in _unempty_panels:
212 td_elt = panel.getElement().parentNode
213 DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
214 if isinstance(self, quick_widgets.QuickWidget):
215 self.host.widgets.deleteWidget(self)
216
217
218 class EmptyWidget(DropCell, SimplePanel):
219 """Empty dropable panel"""
220
221 def __init__(self, host):
222 SimplePanel.__init__(self)
223 DropCell.__init__(self, host)
224 #self.setWidget(HTML(''))
225 self.setSize('100%', '100%')
226
227
228 class BorderWidget(EmptyWidget):
229 def __init__(self, host):
230 EmptyWidget.__init__(self, host)
231 self.addStyleName('borderPanel')
232
233
234 class LeftBorderWidget(BorderWidget):
235 def __init__(self, host):
236 BorderWidget.__init__(self, host)
237 self.addStyleName('leftBorderWidget')
238
239
240 class RightBorderWidget(BorderWidget):
241 def __init__(self, host):
242 BorderWidget.__init__(self, host)
243 self.addStyleName('rightBorderWidget')
244
245
246 class BottomBorderWidget(BorderWidget):
247 def __init__(self, host):
248 BorderWidget.__init__(self, host)
249 self.addStyleName('bottomBorderWidget')
250
251
252 class DropTab(Label, DropWidget):
253
254 def __init__(self, tab_panel, text):
255 Label.__init__(self, text)
256 DropWidget.__init__(self, tab_panel)
257 self.tab_panel = tab_panel
258 self.setStyleName('dropCell')
259 self.setWordWrap(False)
260
261 def _getIndex(self):
262 """ get current index of the DropTab """
263 # XXX: awful hack, but seems the only way to get index
264 return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
265
266 def onDragEnter(self, event):
267 #if self == LiberviaDragWidget.current:
268 # return
269 self.parent.addStyleName('dragover')
270 DOM.eventPreventDefault(event)
271
272 def onDragLeave(self, event):
273 self.parent.removeStyleName('dragover')
274
275 def onDragOver(self, event):
276 DOM.eventPreventDefault(event)
277
278 def onDrop(self, event):
279 DOM.eventPreventDefault(event)
280 self.parent.removeStyleName('dragover')
281 if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
282 # the widget comes from the same tab, so nothing to do, we let it there
283 return
284
285 # FIXME: quite the same stuff as in DropCell, need some factorisation
286 dt = event.dataTransfer
287 # 'text', 'text/plain', and 'Text' are equivalent.
288 try:
289 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
290 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
291 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
292 # item_type = dt.getData("type")
293 log.debug("message: %s" % item)
294 log.debug("type: %s" % item_type)
295 except:
296 log.debug("no message found")
297 item = '&nbsp;'
298 item_type = None
299
300 if item_type == "WIDGET":
301 if not LiberviaDragWidget.current:
302 log.error("No widget registered in LiberviaDragWidget !")
303 return
304 _new_panel = LiberviaDragWidget.current
305 elif item_type in DropCell.drop_keys:
306 pass # create the widget when we are sure there's a tab for it
307 else:
308 log.warning("unmanaged item type")
309 return
310
311 # XXX: when needed, new tab creation must be done exactly here to not mess up with LiberviaDragWidget.onDragEnd
312 try:
313 widgets_panel = self.tab_panel.getWidget(self._getIndex())
314 except IndexError: # widgets panel doesn't exist, e.g. user dropped in "+" tab
315 widgets_panel = self.tab_panel.addWidgetsTab(None)
316 if widgets_panel is None: # user cancelled
317 return
318
319 if item_type == "WIDGET":
320 _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel)
321 else:
322 _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
323
324 widgets_panel.addWidget(_new_panel)
325
326
327 ### Libervia Widget ###
328
329
330 class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
331
332 def __init__(self, parent, host, title, info=None):
333 """
334 @param parent (LiberviaWidget): LiberWidget instance
335 @param host (SatWebFrontend): SatWebFrontend instance
336 @param title (Label, HTML): text widget instance
337 @param info (Widget): text widget instance
338 """
339 AbsolutePanel.__init__(self)
340 self.add(title)
341 if info:
342 # FIXME: temporary design to display the info near the menu
343 button_group_wrapper = HorizontalPanel()
344 button_group_wrapper.add(info)
345 else:
346 button_group_wrapper = SimplePanel()
347 button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
348 button_group = base_widget.WidgetMenuBar(parent, host)
349 button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.SimpleCmd(parent.onSetting))
350 button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.SimpleCmd(parent.onClose))
351 button_group_wrapper.add(button_group)
352 self.add(button_group_wrapper)
353 self.addStyleName('widgetHeader')
354 LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
355
356
357 class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
358 """Libervia's widget which can replace itself with a dropped widget on DnD"""
359
360 def __init__(self, host, title='', info=None, selectable=False, plugin_menu_context=None):
361 """Init the widget
362
363 @param host (SatWebFrontend): SatWebFrontend instance
364 @param title (unicode): title shown in the header of the widget
365 @param info (unicode): info shown in the header of the widget
366 @param selectable (bool): True is widget can be selected by user
367 @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
368 """
369 VerticalPanel.__init__(self)
370 DropCell.__init__(self, host)
371 ClickHandler.__init__(self)
372 self._selectable = selectable
373 self._plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
374 self._title_id = HTMLPanel.createUniqueId()
375 self._setting_button_id = HTMLPanel.createUniqueId()
376 self._close_button_id = HTMLPanel.createUniqueId()
377 self._title = Label(title)
378 self._title.setStyleName('widgetHeader_title')
379 if info is not None:
380 self._info = HTML(info)
381 self._info.setStyleName('widgetHeader_info')
382 else:
383 self._info = None
384 header = WidgetHeader(self, host, self._title, self._info)
385 self.add(header)
386 self.setSize('100%', '100%')
387 self.addStyleName('widget')
388 if self._selectable:
389 self.addClickListener(self)
390
391 # FIXME
392 # def onClose(sender):
393 # """Check dynamically if the unibox is enable or not"""
394 # if self.host.uni_box:
395 # self.host.uni_box.onWidgetClosed(sender)
396
397 # self.addCloseListener(onClose)
398 # self.host.registerWidget(self) # FIXME
399
400 @property
401 def plugin_menu_context(self):
402 return self._plugin_menu_context
403
404 def getDebugName(self):
405 return "%s (%s)" % (self, self._title.getText())
406
407 def getParent(self, class_=None, expect=True):
408 """Return the closest ancestor of the specified class.
409
410 Note: this method overrides pyjamas.ui.Widget.getParent
411
412 @param class_: class of the ancestor to look for or None to return the first parent
413 @param expect: set to True if the parent is expected (raise an error if not found)
414 @return: the parent/ancestor or None if it has not been found
415 @raise exceptions.InternalError: expect is True and no parent is found
416 """
417 current = Widget.getParent(self)
418 if class_ is None:
419 return current # this is the default behavior
420 while current is not None and not isinstance(current, class_):
421 current = Widget.getParent(current)
422 if current is None and expect:
423 raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self))
424 return current
425
426 def onClick(self, sender):
427 self.host.setSelected(self)
428
429 def onClose(self, sender):
430 """ Called when the close button is pushed """
431 widgets_panel = self.getParent(WidgetsPanel, expect=True)
432 widgets_panel.removeWidget(self)
433 self.onQuit()
434 self.host.widgets.deleteWidget(self)
435
436 def onQuit(self):
437 """ Called when the widget is actually ending """
438 pass
439
440 def refresh(self):
441 """This can be overwritten by a child class to refresh the display when,
442 instead of creating a new one, an existing widget is found and reused.
443 """
444 pass
445
446 def onSetting(self, sender):
447 widpanel = self.getParent(WidgetsPanel, expect=True)
448 row, col = widpanel.getIndex(self)
449 body = VerticalPanel()
450
451 # colspan & rowspan
452 colspan = widpanel.getColSpan(row, col)
453 rowspan = widpanel.getRowSpan(row, col)
454
455 def onColSpanChange(value):
456 widpanel.setColSpan(row, col, value)
457
458 def onRowSpanChange(value):
459 widpanel.setRowSpan(row, col, value)
460 colspan_setter = dialog.IntSetter("Columns span", colspan)
461 colspan_setter.addValueChangeListener(onColSpanChange)
462 colspan_setter.setWidth('100%')
463 rowspan_setter = dialog.IntSetter("Rows span", rowspan)
464 rowspan_setter.addValueChangeListener(onRowSpanChange)
465 rowspan_setter.setWidth('100%')
466 body.add(colspan_setter)
467 body.add(rowspan_setter)
468
469 # size
470 width_str = self.getWidth()
471 if width_str.endswith('px'):
472 width = int(width_str[:-2])
473 else:
474 width = 0
475 height_str = self.getHeight()
476 if height_str.endswith('px'):
477 height = int(height_str[:-2])
478 else:
479 height = 0
480
481 def onWidthChange(value):
482 if not value:
483 self.setWidth('100%')
484 else:
485 self.setWidth('%dpx' % value)
486
487 def onHeightChange(value):
488 if not value:
489 self.setHeight('100%')
490 else:
491 self.setHeight('%dpx' % value)
492 width_setter = dialog.IntSetter("width (0=auto)", width)
493 width_setter.addValueChangeListener(onWidthChange)
494 width_setter.setWidth('100%')
495 height_setter = dialog.IntSetter("height (0=auto)", height)
496 height_setter.addValueChangeListener(onHeightChange)
497 height_setter.setHeight('100%')
498 body.add(width_setter)
499 body.add(height_setter)
500
501 # reset
502 def onReset(sender):
503 colspan_setter.setValue(1)
504 rowspan_setter.setValue(1)
505 width_setter.setValue(0)
506 height_setter.setValue(0)
507
508 reset_bt = Button("Reset", onReset)
509 body.add(reset_bt)
510 body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
511
512 _dialog = dialog.GenericDialog("Widget setting", body)
513 _dialog.show()
514
515 def setTitle(self, text):
516 """change the title in the header of the widget
517 @param text: text of the new title"""
518 self._title.setText(text)
519
520 def setHeaderInfo(self, text):
521 """change the info in the header of the widget
522 @param text: text of the new title"""
523 try:
524 self._info.setHTML(text)
525 except TypeError:
526 log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
527
528 def isSelectable(self):
529 return self._selectable
530
531 def setSelectable(self, selectable):
532 if not self._selectable:
533 try:
534 self.removeClickListener(self)
535 except ValueError:
536 pass
537 if self.selectable and not self in self._clickListeners:
538 self.addClickListener(self)
539 self._selectable = selectable
540
541 def getWarningData(self):
542 """ Return exposition warning level when this widget is selected and something is sent to it
543 This method should be overriden by children
544 @return: tuple (warning level type/HTML msg). Type can be one of:
545 - PUBLIC
546 - GROUP
547 - ONE2ONE
548 - MISC
549 - NONE
550 """
551 if not self._selectable:
552 log.error("getWarningLevel must not be called for an unselectable widget")
553 raise Exception
554 # TODO: cleaner warning types (more general constants)
555 return ("NONE", None)
556
557 def setWidget(self, widget, scrollable=True):
558 """Set the widget that will be in the body of the LiberviaWidget
559 @param widget: widget to put in the body
560 @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
561 if scrollable:
562 _scrollpanelwrapper = base_panel.ScrollPanelWrapper()
563 _scrollpanelwrapper.setStyleName('widgetBody')
564 _scrollpanelwrapper.setWidget(widget)
565 body_wid = _scrollpanelwrapper
566 else:
567 body_wid = widget
568 self.add(body_wid)
569 self.setCellHeight(body_wid, '100%')
570
571 def doDetachChildren(self):
572 # We need to force the use of a panel subclass method here,
573 # for the same reason as doAttachChildren
574 VerticalPanel.doDetachChildren(self)
575
576 def doAttachChildren(self):
577 # We need to force the use of a panel subclass method here, else
578 # the event will not propagate to children
579 VerticalPanel.doAttachChildren(self)
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 _widget and 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, FloatingTab=True)
757 ClickHandler.__init__(self)
758 self.host = host
759 self.setStyleName('liberviaTabPanel')
760 self.tabBar.addTab(DropTab(self, u'✚'), asHTML=False)
761 self.tabBar.setVisible(False) # set to True when profile is logged
762 self.tabBar.addStyleDependentName('oneTab')
763
764 def onTabSelected(self, sender, tabIndex):
765 if tabIndex < self.getWidgetCount():
766 TabPanel.onTabSelected(self, sender, tabIndex)
767 return
768 # user clicked the "+" tab
769 self.addWidgetsTab(None, select=True)
770
771 def getCurrentPanel(self):
772 """ Get the panel of the currently selected tab
773
774 @return: WidgetsPanel
775 """
776 return self.deck.visibleWidget
777
778 def addTab(self, widget, label, select=False):
779 """Create a new tab for the given widget.
780
781 @param widget (Widget): widget to associate to the tab
782 @param label (unicode): label of the tab
783 @param select (bool): True to select the added tab
784 """
785 TabPanel.add(self, widget, DropTab(self, label), False)
786 if self.getWidgetCount() > 1:
787 self.tabBar.removeStyleDependentName('oneTab')
788 self.host.resize()
789 if select:
790 self.selectTab(self.getWidgetCount() - 1)
791
792 def addWidgetsTab(self, label, select=False, locked=False):
793 """Create a new tab for containing LiberviaWidgets.
794
795 @param label (unicode): label of the tab (None or '' for user prompt)
796 @param select (bool): True to select the added tab
797 @param locked (bool): If True, the tab will not be removed when there
798 are no more widget inside. If False, the tab will be removed with
799 the last widget.
800 @return: WidgetsPanel
801 """
802 widgets_panel = WidgetsPanel(self.host, locked=locked)
803
804 if not label:
805 default_label = _(u'new tab')
806 try:
807 label = Window.prompt(_(u'Name of the new tab'), default_label)
808 if not label: # empty label or user pressed "cancel"
809 return None
810 except: # this happens when the user prevents the page to open the prompt dialog
811 label = default_label
812
813 self.addTab(widgets_panel, label, select)
814 return widgets_panel
815
816 def onWidgetPanelRemove(self, panel):
817 """ Called when a child WidgetsPanel is empty and need to be removed """
818 widget_index = self.getWidgetIndex(panel)
819 self.remove(panel)
820 widgets_count = self.getWidgetCount()
821 if widgets_count == 1:
822 self.tabBar.addStyleDependentName('oneTab')
823 self.host.resize()
824 self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)