comparison browser/sat_browser/libervia_widget.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/browser/sat_browser/libervia_widget.py@f2170536ba23
children 2af117bfe6cc
comparison
equal deleted inserted replaced
1123:63a4b8fe9782 1124:28e3eb3bb217
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011-2018 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 item, item_type = eventGetData(event)
162 if item_type == "WIDGET":
163 if not LiberviaDragWidget.current:
164 log.error("No widget registered in LiberviaDragWidget !")
165 return
166 _new_panel = LiberviaDragWidget.current
167 if self == _new_panel: # We can't drop on ourself
168 return
169 # we need to remove the widget from the panel as it will be inserted elsewhere
170 widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
171 wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
172 row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
173 if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
174 # the dropped widget is the only one in the same row
175 # as the target widget (self), we don't do anything
176 return
177 widgets_panel.removeWidget(_new_panel)
178 elif item_type in self.drop_keys:
179 _new_panel = self.drop_keys[item_type](self.host, item)
180 if not isinstance(_new_panel, LiberviaWidget):
181 raise base_widget.NoLiberviaWidgetException
182 else:
183 log.warning("unmanaged item type")
184 return
185 if isinstance(self, LiberviaWidget):
186 # self.host.unregisterWidget(self) # FIXME
187 self.onQuit()
188 if not isinstance(_new_panel, LiberviaWidget):
189 log.warning("droping an object which is not a class of LiberviaWidget")
190 _flextable = self.getParent()
191 _widgetspanel = _flextable.getParent().getParent()
192 row_idx, cell_idx = self._getCellAndRow(_flextable, event)
193 if self.host.getSelected() == self:
194 self.host.setSelected(None)
195 _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
196 """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
197 _width = 90/float(len(_unempty_panels) or 1)
198 #now we resize all the cell of the column
199 for panel in _unempty_panels:
200 td_elt = panel.getElement().parentNode
201 DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
202 if isinstance(self, quick_widgets.QuickWidget):
203 self.host.widgets.deleteWidget(self)
204
205
206 class EmptyWidget(DropCell, SimplePanel):
207 """Empty dropable panel"""
208
209 def __init__(self, host):
210 SimplePanel.__init__(self)
211 DropCell.__init__(self, host)
212 #self.setWidget(HTML(''))
213 self.setSize('100%', '100%')
214
215
216 class BorderWidget(EmptyWidget):
217 def __init__(self, host):
218 EmptyWidget.__init__(self, host)
219 self.addStyleName('borderPanel')
220
221
222 class LeftBorderWidget(BorderWidget):
223 def __init__(self, host):
224 BorderWidget.__init__(self, host)
225 self.addStyleName('leftBorderWidget')
226
227
228 class RightBorderWidget(BorderWidget):
229 def __init__(self, host):
230 BorderWidget.__init__(self, host)
231 self.addStyleName('rightBorderWidget')
232
233
234 class BottomBorderWidget(BorderWidget):
235 def __init__(self, host):
236 BorderWidget.__init__(self, host)
237 self.addStyleName('bottomBorderWidget')
238
239
240 class DropTab(Label, DropWidget):
241
242 def __init__(self, tab_panel, text):
243 Label.__init__(self, text)
244 DropWidget.__init__(self, tab_panel)
245 self.tab_panel = tab_panel
246 self.setStyleName('dropCell')
247 self.setWordWrap(False)
248
249 def _getIndex(self):
250 """ get current index of the DropTab """
251 # XXX: awful hack, but seems the only way to get index
252 return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
253
254 def onDragEnter(self, event):
255 #if self == LiberviaDragWidget.current:
256 # return
257 self.parent.addStyleName('dragover')
258 DOM.eventPreventDefault(event)
259
260 def onDragLeave(self, event):
261 self.parent.removeStyleName('dragover')
262
263 def onDragOver(self, event):
264 DOM.eventPreventDefault(event)
265
266 def onDrop(self, event):
267 DOM.eventPreventDefault(event)
268 self.parent.removeStyleName('dragover')
269 if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
270 # the widget comes from the same tab, so nothing to do, we let it there
271 return
272
273 item, item_type = eventGetData(event)
274 if item_type == "WIDGET":
275 if not LiberviaDragWidget.current:
276 log.error("No widget registered in LiberviaDragWidget !")
277 return
278 _new_panel = LiberviaDragWidget.current
279 elif item_type in DropCell.drop_keys:
280 pass # create the widget when we are sure there's a tab for it
281 else:
282 log.warning("unmanaged item type")
283 return
284
285 # XXX: when needed, new tab creation must be done exactly here to not mess up with LiberviaDragWidget.onDragEnd
286 try:
287 widgets_panel = self.tab_panel.getWidget(self._getIndex())
288 except IndexError: # widgets panel doesn't exist, e.g. user dropped in "+" tab
289 widgets_panel = self.tab_panel.addWidgetsTab(None)
290 if widgets_panel is None: # user cancelled
291 return
292
293 if item_type == "WIDGET":
294 _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel)
295 else:
296 _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
297
298 widgets_panel.addWidget(_new_panel)
299
300
301 ### Libervia Widget ###
302
303
304 class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
305
306 def __init__(self, parent, host, title, info=None):
307 """
308 @param parent (LiberviaWidget): LiberWidget instance
309 @param host (SatWebFrontend): SatWebFrontend instance
310 @param title (Label, HTML): text widget instance
311 @param info (Widget): text widget instance
312 """
313 AbsolutePanel.__init__(self)
314 self.add(title)
315 if info:
316 # FIXME: temporary design to display the info near the menu
317 button_group_wrapper = HorizontalPanel()
318 button_group_wrapper.add(info)
319 else:
320 button_group_wrapper = SimplePanel()
321 button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
322 button_group = base_widget.WidgetMenuBar(parent, host)
323 button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.SimpleCmd(parent.onSetting))
324 button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.SimpleCmd(parent.onClose))
325 button_group_wrapper.add(button_group)
326 self.add(button_group_wrapper)
327 self.addStyleName('widgetHeader')
328 LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
329
330
331 class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
332 """Libervia's widget which can replace itself with a dropped widget on DnD"""
333
334 def __init__(self, host, title='', info=None, selectable=False, plugin_menu_context=None):
335 """Init the widget
336
337 @param host (SatWebFrontend): SatWebFrontend instance
338 @param title (unicode): title shown in the header of the widget
339 @param info (unicode): info shown in the header of the widget
340 @param selectable (bool): True is widget can be selected by user
341 @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
342 """
343 VerticalPanel.__init__(self)
344 DropCell.__init__(self, host)
345 ClickHandler.__init__(self)
346 self._selectable = selectable
347 self._plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
348 self._title_id = HTMLPanel.createUniqueId()
349 self._setting_button_id = HTMLPanel.createUniqueId()
350 self._close_button_id = HTMLPanel.createUniqueId()
351 self._title = Label(title)
352 self._title.setStyleName('widgetHeader_title')
353 if info is not None:
354 self._info = HTML(info)
355 self._info.setStyleName('widgetHeader_info')
356 else:
357 self._info = None
358 header = WidgetHeader(self, host, self._title, self._info)
359 self.add(header)
360 self.setSize('100%', '100%')
361 self.addStyleName('widget')
362 if self._selectable:
363 self.addClickListener(self)
364
365 @property
366 def plugin_menu_context(self):
367 return self._plugin_menu_context
368
369 def getDebugName(self):
370 return "%s (%s)" % (self, self._title.getText())
371
372 def getParent(self, class_=None, expect=True):
373 """Return the closest ancestor of the specified class.
374
375 Note: this method overrides pyjamas.ui.Widget.getParent
376
377 @param class_: class of the ancestor to look for or None to return the first parent
378 @param expect: set to True if the parent is expected (raise an error if not found)
379 @return: the parent/ancestor or None if it has not been found
380 @raise exceptions.InternalError: expect is True and no parent is found
381 """
382 current = Widget.getParent(self)
383 if class_ is None:
384 return current # this is the default behavior
385 while current is not None and not isinstance(current, class_):
386 current = Widget.getParent(current)
387 if current is None and expect:
388 raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self))
389 return current
390
391 def onClick(self, sender):
392 self.host.setSelected(self)
393
394 def onClose(self, sender):
395 """ Called when the close button is pushed """
396 widgets_panel = self.getParent(WidgetsPanel, expect=True)
397 widgets_panel.removeWidget(self)
398 self.onQuit()
399 self.host.widgets.deleteWidget(self)
400
401 def onQuit(self):
402 """ Called when the widget is actually ending """
403 pass
404
405 def refresh(self):
406 """This can be overwritten by a child class to refresh the display when,
407 instead of creating a new one, an existing widget is found and reused.
408 """
409 pass
410
411 def onSetting(self, sender):
412 widpanel = self.getParent(WidgetsPanel, expect=True)
413 row, col = widpanel.getIndex(self)
414 body = VerticalPanel()
415
416 # colspan & rowspan
417 colspan = widpanel.getColSpan(row, col)
418 rowspan = widpanel.getRowSpan(row, col)
419
420 def onColSpanChange(value):
421 widpanel.setColSpan(row, col, value)
422
423 def onRowSpanChange(value):
424 widpanel.setRowSpan(row, col, value)
425 colspan_setter = dialog.IntSetter("Columns span", colspan)
426 colspan_setter.addValueChangeListener(onColSpanChange)
427 colspan_setter.setWidth('100%')
428 rowspan_setter = dialog.IntSetter("Rows span", rowspan)
429 rowspan_setter.addValueChangeListener(onRowSpanChange)
430 rowspan_setter.setWidth('100%')
431 body.add(colspan_setter)
432 body.add(rowspan_setter)
433
434 # size
435 width_str = self.getWidth()
436 if width_str.endswith('px'):
437 width = int(width_str[:-2])
438 else:
439 width = 0
440 height_str = self.getHeight()
441 if height_str.endswith('px'):
442 height = int(height_str[:-2])
443 else:
444 height = 0
445
446 def onWidthChange(value):
447 if not value:
448 self.setWidth('100%')
449 else:
450 self.setWidth('%dpx' % value)
451
452 def onHeightChange(value):
453 if not value:
454 self.setHeight('100%')
455 else:
456 self.setHeight('%dpx' % value)
457 width_setter = dialog.IntSetter("width (0=auto)", width)
458 width_setter.addValueChangeListener(onWidthChange)
459 width_setter.setWidth('100%')
460 height_setter = dialog.IntSetter("height (0=auto)", height)
461 height_setter.addValueChangeListener(onHeightChange)
462 height_setter.setHeight('100%')
463 body.add(width_setter)
464 body.add(height_setter)
465
466 # reset
467 def onReset(sender):
468 colspan_setter.setValue(1)
469 rowspan_setter.setValue(1)
470 width_setter.setValue(0)
471 height_setter.setValue(0)
472
473 reset_bt = Button("Reset", onReset)
474 body.add(reset_bt)
475 body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
476
477 _dialog = dialog.GenericDialog("Widget setting", body)
478 _dialog.show()
479
480 def setTitle(self, text):
481 """change the title in the header of the widget
482 @param text: text of the new title"""
483 self._title.setText(text)
484
485 def setHeaderInfo(self, text):
486 """change the info in the header of the widget
487 @param text: text of the new title"""
488 try:
489 self._info.setHTML(text)
490 except TypeError:
491 log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
492
493 def isSelectable(self):
494 return self._selectable
495
496 def setSelectable(self, selectable):
497 if not self._selectable:
498 try:
499 self.removeClickListener(self)
500 except ValueError:
501 pass
502 if self.selectable and not self in self._clickListeners:
503 self.addClickListener(self)
504 self._selectable = selectable
505
506 def getWarningData(self):
507 """ Return exposition warning level when this widget is selected and something is sent to it
508 This method should be overriden by children
509 @return: tuple (warning level type/HTML msg). Type can be one of:
510 - PUBLIC
511 - GROUP
512 - ONE2ONE
513 - MISC
514 - NONE
515 """
516 if not self._selectable:
517 log.error("getWarningLevel must not be called for an unselectable widget")
518 raise Exception
519 # TODO: cleaner warning types (more general constants)
520 return ("NONE", None)
521
522 def setWidget(self, widget, scrollable=True):
523 """Set the widget that will be in the body of the LiberviaWidget
524 @param widget: widget to put in the body
525 @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
526 if scrollable:
527 _scrollpanelwrapper = base_panel.ScrollPanelWrapper()
528 _scrollpanelwrapper.setStyleName('widgetBody')
529 _scrollpanelwrapper.setWidget(widget)
530 body_wid = _scrollpanelwrapper
531 else:
532 body_wid = widget
533 self.add(body_wid)
534 self.setCellHeight(body_wid, '100%')
535
536 def doDetachChildren(self):
537 # We need to force the use of a panel subclass method here,
538 # for the same reason as doAttachChildren
539 VerticalPanel.doDetachChildren(self)
540
541 def doAttachChildren(self):
542 # We need to force the use of a panel subclass method here, else
543 # the event will not propagate to children
544 VerticalPanel.doAttachChildren(self)
545
546
547 # XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import
548
549
550 class WidgetsPanel(base_panel.ScrollPanelWrapper):
551 """The panel wanaging the widgets indide a tab"""
552
553 def __init__(self, host, locked=False):
554 """
555
556 @param host (SatWebFrontend): host instance
557 @param locked (bool): If True, the tab containing self will not be
558 removed when there are no more widget inside self. If False, the
559 tab will be removed with self's last widget.
560 """
561 base_panel.ScrollPanelWrapper.__init__(self)
562 self.setSize('100%', '100%')
563 self.host = host
564 self.locked = locked
565 self.selected = None
566 self.flextable = FlexTable()
567 self.flextable.setSize('100%', '100%')
568 self.setWidget(self.flextable)
569 self.setStyleName('widgetsPanel')
570 _bottom = BottomBorderWidget(self.host)
571 self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row,
572 # dropping a widget there will add a new row
573 td_elt = _bottom.getElement().parentNode
574 DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit)
575 self._max_cols = 1 # give the maximum number of columns in a raw
576
577 @property
578 def widgets(self):
579 return iter(self.flextable)
580
581 def isLocked(self):
582 return self.locked
583
584 def changeWidget(self, row, col, wid):
585 """Change the widget in the given location, add row or columns when necessary"""
586 log.debug(u"changing widget: %s %s %s" % (wid.getDebugName(), row, col))
587 last_row = max(0, self.flextable.getRowCount() - 1)
588 # try: # FIXME: except without exception specified !
589 prev_wid = self.flextable.getWidget(row, col)
590 # except:
591 # log.error("Trying to change an unexisting widget !")
592 # return
593
594 cellFormatter = self.flextable.getFlexCellFormatter()
595
596 if isinstance(prev_wid, BorderWidget):
597 # We are on a border, we must create a row and/or columns
598 prev_wid.removeStyleName('dragover')
599
600 if isinstance(prev_wid, BottomBorderWidget):
601 # We are on the bottom border, we create a new row
602 self.flextable.insertRow(last_row)
603 self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
604 self.flextable.setWidget(last_row, 1, wid)
605 self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
606 cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
607 row = last_row
608
609 elif isinstance(prev_wid, LeftBorderWidget):
610 if col != 0:
611 log.error("LeftBorderWidget must be on the first column !")
612 return
613 self.flextable.insertCell(row, col + 1)
614 self.flextable.setWidget(row, 1, wid)
615
616 elif isinstance(prev_wid, RightBorderWidget):
617 if col != self.flextable.getCellCount(row) - 1:
618 log.error("RightBorderWidget must be on the last column !")
619 return
620 self.flextable.insertCell(row, col)
621 self.flextable.setWidget(row, col, wid)
622
623 else:
624 prev_wid.removeFromParent()
625 self.flextable.setWidget(row, col, wid)
626
627 _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
628 if _max_cols != self._max_cols:
629 self._max_cols = _max_cols
630 self._sizesAdjust()
631
632 def _sizesAdjust(self):
633 cellFormatter = self.flextable.getFlexCellFormatter()
634 width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders
635
636 for row_idx in xrange(self.flextable.getRowCount()):
637 for col_idx in xrange(self.flextable.getCellCount(row_idx)):
638 _widget = self.flextable.getWidget(row_idx, col_idx)
639 if _widget and not isinstance(_widget, BorderWidget):
640 td_elt = _widget.getElement().parentNode
641 DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
642
643 last_row = max(0, self.flextable.getRowCount() - 1)
644 cellFormatter.setColSpan(last_row, 0, self._max_cols)
645
646 def addWidget(self, wid):
647 """Add a widget to a new cell on the next to last row"""
648 last_row = max(0, self.flextable.getRowCount() - 1)
649 log.debug(u"putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
650 self.changeWidget(last_row, 0, wid)
651
652 def removeWidget(self, wid):
653 """Remove a widget and the cell where it is"""
654 _row, _col = self.flextable.getIndex(wid)
655 self.flextable.remove(wid)
656 self.flextable.removeCell(_row, _col)
657 if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row
658 self.flextable.removeRow(_row)
659 _max_cols = 1
660 for row_idx in xrange(self.flextable.getRowCount()):
661 _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
662 if _max_cols != self._max_cols:
663 self._max_cols = _max_cols
664 self._sizesAdjust()
665 current = self
666
667 blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ?
668
669 if blank_page and not self.isLocked():
670 # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
671 while current is not None:
672 if isinstance(current, MainTabPanel):
673 current.onWidgetPanelRemove(self)
674 return
675 current = current.getParent()
676 log.error("no MainTabPanel found !")
677
678 def getWidgetCoords(self, wid):
679 return self.flextable.getIndex(wid)
680
681 def getLiberviaRowWidgets(self, row):
682 """ Return all the LiberviaWidget in the row """
683 return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
684
685 def getRowWidgets(self, row):
686 """ Return all the widgets in the row """
687 widgets = []
688 cols = self.flextable.getCellCount(row)
689 for col in xrange(cols):
690 widgets.append(self.flextable.getWidget(row, col))
691 return widgets
692
693 def getLiberviaWidgetsCount(self):
694 """ Get count of contained widgets """
695 return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
696
697 def getIndex(self, wid):
698 return self.flextable.getIndex(wid)
699
700 def getColSpan(self, row, col):
701 cellFormatter = self.flextable.getFlexCellFormatter()
702 return cellFormatter.getColSpan(row, col)
703
704 def setColSpan(self, row, col, value):
705 cellFormatter = self.flextable.getFlexCellFormatter()
706 return cellFormatter.setColSpan(row, col, value)
707
708 def getRowSpan(self, row, col):
709 cellFormatter = self.flextable.getFlexCellFormatter()
710 return cellFormatter.getRowSpan(row, col)
711
712 def setRowSpan(self, row, col, value):
713 cellFormatter = self.flextable.getFlexCellFormatter()
714 return cellFormatter.setRowSpan(row, col, value)
715
716
717 class MainTabPanel(TabPanel, ClickHandler):
718 """The panel managing the tabs"""
719
720 def __init__(self, host):
721 TabPanel.__init__(self, FloatingTab=True)
722 ClickHandler.__init__(self)
723 self.host = host
724 self.setStyleName('liberviaTabPanel')
725 self.tabBar.addTab(DropTab(self, u'✚'), asHTML=False)
726 self.tabBar.setVisible(False) # set to True when profile is logged
727 self.tabBar.addStyleDependentName('oneTab')
728
729 def onTabSelected(self, sender, tabIndex):
730 if tabIndex < self.getWidgetCount():
731 TabPanel.onTabSelected(self, sender, tabIndex)
732 self.host.selected_widget = self.getCurrentPanel().selected
733 return
734 # user clicked the "+" tab
735 self.addWidgetsTab(None, select=True)
736
737 def getCurrentPanel(self):
738 """ Get the panel of the currently selected tab
739
740 @return: WidgetsPanel
741 """
742 return self.deck.visibleWidget
743
744 def addTab(self, widget, label, select=False):
745 """Create a new tab for the given widget.
746
747 @param widget (Widget): widget to associate to the tab
748 @param label (unicode): label of the tab
749 @param select (bool): True to select the added tab
750 """
751 TabPanel.add(self, widget, DropTab(self, label), False)
752 if self.getWidgetCount() > 1:
753 self.tabBar.removeStyleDependentName('oneTab')
754 self.host.resize()
755 if select:
756 self.selectTab(self.getWidgetCount() - 1)
757
758 def addWidgetsTab(self, label, select=False, locked=False):
759 """Create a new tab for containing LiberviaWidgets.
760
761 @param label (unicode): label of the tab (None or '' for user prompt)
762 @param select (bool): True to select the added tab
763 @param locked (bool): If True, the tab will not be removed when there
764 are no more widget inside. If False, the tab will be removed with
765 the last widget.
766 @return: WidgetsPanel
767 """
768 widgets_panel = WidgetsPanel(self.host, locked=locked)
769
770 if not label:
771 default_label = _(u'new tab')
772 try:
773 label = Window.prompt(_(u'Name of the new tab'), default_label)
774 if not label: # empty label or user pressed "cancel"
775 return None
776 except: # this happens when the user prevents the page to open the prompt dialog
777 label = default_label
778
779 self.addTab(widgets_panel, label, select)
780 return widgets_panel
781
782 def onWidgetPanelRemove(self, panel):
783 """ Called when a child WidgetsPanel is empty and need to be removed """
784 widget_index = self.getWidgetIndex(panel)
785 self.remove(panel)
786 widgets_count = self.getWidgetCount()
787 if widgets_count == 1:
788 self.tabBar.addStyleDependentName('oneTab')
789 self.host.resize()
790 self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
791
792
793 def eventGetData(event):
794 """Retrieve the event data.
795
796 @param event(EventObject)
797 @return tuple: (event_text, event_type)
798 """
799 dt = event.dataTransfer
800 # 'text', 'text/plain', and 'Text' are equivalent.
801 try:
802 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
803 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
804 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
805 # item_type = dt.getData("type")
806 log.debug(u"event data: %s (type %s)" % (item, item_type))
807 except:
808 log.debug("event data not found")
809 item = '&nbsp;'
810 item_type = None
811 return item, item_type