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