Mercurial > libervia-web
comparison browser_side/base_widget.py @ 195:dd27072d8ae0
browser side: widgets refactoring:
- moved base widgets in a base_widget module
- widgets class now register themselves their Drag/Drop type
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 04 Mar 2013 23:01:57 +0100 |
parents | browser_side/panels.py@6198be95a39c |
children | c2639c9f86ea |
comparison
equal
deleted
inserted
replaced
194:6198be95a39c | 195:dd27072d8ae0 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 Libervia: a Salut à Toi frontend | |
6 Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org> | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU Affero General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU Affero General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU Affero General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 import pyjd # this is dummy in pyjs | |
23 from pyjamas.ui.SimplePanel import SimplePanel | |
24 from pyjamas.ui.AbsolutePanel import AbsolutePanel | |
25 from pyjamas.ui.VerticalPanel import VerticalPanel | |
26 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
27 from pyjamas.ui.ScrollPanel import ScrollPanel | |
28 from pyjamas.ui.FlexTable import FlexTable | |
29 from pyjamas.ui.TabPanel import TabPanel | |
30 from pyjamas.ui.HTMLPanel import HTMLPanel | |
31 from pyjamas.ui.Label import Label | |
32 from pyjamas.ui.Button import Button | |
33 from pyjamas.ui.Image import Image | |
34 from pyjamas.ui.DropWidget import DropWidget | |
35 from pyjamas.ui.ClickListener import ClickHandler | |
36 from pyjamas.ui import HasAlignment | |
37 from pyjamas import DOM | |
38 import dialog | |
39 from pyjamas import Window | |
40 from __pyjamas__ import doc | |
41 | |
42 class DropCell(DropWidget): | |
43 """Cell in the middle grid which replace itself with the dropped widget on DnD""" | |
44 drop_keys = {} | |
45 | |
46 def __init__(self, host): | |
47 DropWidget.__init__(self) | |
48 self.host = host | |
49 self.setStyleName('dropCell') | |
50 | |
51 @classmethod | |
52 def addDropKey(cls, key, callback): | |
53 DropCell.drop_keys[key] = callback | |
54 | |
55 def onDragEnter(self, event): | |
56 self.addStyleName('dragover') | |
57 DOM.eventPreventDefault(event) | |
58 | |
59 def onDragLeave(self, event): | |
60 if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\ | |
61 event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth()-1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight()-1: | |
62 #We check that we are inside widget's box, and we don't remove the style in this case because | |
63 #if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we | |
64 #don't want that | |
65 self.removeStyleName('dragover') | |
66 | |
67 def onDragOver(self, event): | |
68 DOM.eventPreventDefault(event) | |
69 | |
70 def _getCellAndRow(self, grid, event): | |
71 """Return cell and row index where the event is occuring""" | |
72 cell = grid.getEventTargetCell(event) | |
73 row = DOM.getParent(cell) | |
74 return (row.rowIndex, cell.cellIndex) | |
75 | |
76 | |
77 def onDrop(self, event): | |
78 dt = event.dataTransfer | |
79 #'text', 'text/plain', and 'Text' are equivalent. | |
80 try: | |
81 item, item_type = dt.getData("text/plain").split('\n') #Workaround for webkit, only text/plain seems to be managed | |
82 if item_type and item_type[-1] == '\0': #Workaround for what looks like a pyjamas bug: the \0 should not be there, and | |
83 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report | |
84 #item_type = dt.getData("type") | |
85 print "message: %s" % item | |
86 print "type: %s" % item_type | |
87 except: | |
88 print "no message found" | |
89 item=' ' | |
90 item_type = None | |
91 DOM.eventPreventDefault(event) | |
92 if item_type in self.drop_keys: | |
93 _new_panel = self.drop_keys[item_type](self.host,item) | |
94 else: | |
95 return False | |
96 if isinstance(self, LiberviaWidget): | |
97 self.host.unregisterWidget(self) | |
98 self.onQuit() | |
99 if not isinstance(_new_panel, LiberviaWidget): | |
100 print ('WARNING: droping an object which is not a class of LiberviaWidget') | |
101 _flextable = self.getParent() | |
102 _widgetspanel = _flextable.getParent().getParent() | |
103 row_idx, cell_idx = self._getCellAndRow(_flextable, event) | |
104 if self.host.getSelected == self: | |
105 self.host.setSelected(None) | |
106 _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) | |
107 """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) | |
108 _width = 90/float(len(_unempty_panels) or 1) | |
109 #now we resize all the cell of the column | |
110 for panel in _unempty_panels: | |
111 td_elt = panel.getElement().parentNode | |
112 DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)""" | |
113 #FIXME: delete object ? Check the right way with pyjamas | |
114 | |
115 class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): | |
116 """Libervia's widget which can replace itself with a dropped widget on DnD""" | |
117 | |
118 def __init__(self, host, title='', selectable=False): | |
119 """Init the widget | |
120 @param host: SatWebFrontend object | |
121 @param title: title show in the header of the widget | |
122 @param selectable: True is widget can be selected by user""" | |
123 VerticalPanel.__init__(self) | |
124 DropCell.__init__(self, host) | |
125 ClickHandler.__init__(self) | |
126 self.__selectable = selectable | |
127 self.__title_id = HTMLPanel.createUniqueId() | |
128 self.__setting_button_id = HTMLPanel.createUniqueId() | |
129 self.__close_button_id = HTMLPanel.createUniqueId() | |
130 header = AbsolutePanel() | |
131 self.__title = Label(title) | |
132 self.__title.setStyleName('widgetHeader_title') | |
133 header.add(self.__title) | |
134 button_group_wrapper = SimplePanel() | |
135 button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') | |
136 button_group = HorizontalPanel() | |
137 button_group.setStyleName('widgetHeader_buttonGroup') | |
138 setting_button = Image("media/icons/misc/settings.png") | |
139 setting_button.setStyleName('widgetHeader_settingButton') | |
140 setting_button.addClickListener(self.onSetting) | |
141 close_button = Image("media/icons/misc/close.png") | |
142 close_button.setStyleName('widgetHeader_closeButton') | |
143 close_button.addClickListener(self.onClose) | |
144 button_group.add(setting_button) | |
145 button_group.add(close_button) | |
146 button_group_wrapper.setWidget(button_group) | |
147 header.add(button_group_wrapper) | |
148 self.add(header) | |
149 header.addStyleName('widgetHeader') | |
150 self.setSize('100%', '100%') | |
151 self.addStyleName('widget') | |
152 if self.__selectable: | |
153 self.addClickListener(self) | |
154 self.host.registerWidget(self) | |
155 | |
156 def _getWidgetsPanel(self): | |
157 current = self | |
158 while current is not None and current.__class__ != WidgetsPanel: | |
159 current = current.getParent() | |
160 if current is None: | |
161 print "Error: can't find WidgetsPanel" | |
162 return current | |
163 | |
164 def onClick(self, sender): | |
165 self.host.setSelected(self) | |
166 | |
167 def onClose(self, sender): | |
168 """ Called when the close button is pushed """ | |
169 _widgetspanel = self._getWidgetsPanel() | |
170 _widgetspanel.removeWidget(self) | |
171 self.onQuit() | |
172 | |
173 def onQuit(self): | |
174 """ Called when the widget is actually ending """ | |
175 pass | |
176 | |
177 def onSetting(self, sender): | |
178 widpanel = self._getWidgetsPanel() | |
179 row, col = widpanel.getIndex(self) | |
180 body = VerticalPanel() | |
181 | |
182 #colspan & rowspan | |
183 colspan = widpanel.getColSpan(row, col) | |
184 rowspan = widpanel.getRowSpan(row, col) | |
185 def onColSpanChange(value): | |
186 widpanel.setColSpan(row, col, value) | |
187 def onRowSpanChange(value): | |
188 widpanel.setRowSpan(row, col, value) | |
189 colspan_setter = dialog.IntSetter("Columns span", colspan) | |
190 colspan_setter.addValueChangeListener(onColSpanChange) | |
191 colspan_setter.setWidth('100%') | |
192 rowspan_setter = dialog.IntSetter("Rows span", rowspan) | |
193 rowspan_setter.addValueChangeListener(onRowSpanChange) | |
194 rowspan_setter.setWidth('100%') | |
195 body.add(colspan_setter) | |
196 body.add(rowspan_setter) | |
197 | |
198 #size | |
199 width_str = self.getWidth() | |
200 if width_str.endswith('px'): | |
201 width=int(width_str[:-2]) | |
202 else: | |
203 width = 0 | |
204 height_str = self.getHeight() | |
205 if height_str.endswith('px'): | |
206 height=int(height_str[:-2]) | |
207 else: | |
208 height = 0 | |
209 def onWidthChange(value): | |
210 if not value: | |
211 self.setWidth('100%') | |
212 else: | |
213 self.setWidth('%dpx' % value) | |
214 def onHeightChange(value): | |
215 if not value: | |
216 self.setHeight('100%') | |
217 else: | |
218 self.setHeight('%dpx' % value) | |
219 width_setter = dialog.IntSetter("width (0=auto)", width) | |
220 width_setter.addValueChangeListener(onWidthChange) | |
221 width_setter.setWidth('100%') | |
222 height_setter = dialog.IntSetter("height (0=auto)", height) | |
223 height_setter.addValueChangeListener(onHeightChange) | |
224 height_setter.setHeight('100%') | |
225 body.add(width_setter) | |
226 body.add(height_setter) | |
227 | |
228 #reset | |
229 def onReset(sender): | |
230 colspan_setter.setValue(1) | |
231 rowspan_setter.setValue(1) | |
232 width_setter.setValue(0) | |
233 height_setter.setValue(0) | |
234 | |
235 reset_bt = Button("Reset", onReset) | |
236 body.add(reset_bt) | |
237 body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER) | |
238 | |
239 _dialog = dialog.GenericDialog("Widget setting", body) | |
240 _dialog.show() | |
241 | |
242 def setTitle(self, text): | |
243 """change the title in the header of the widget | |
244 @param text: text of the new title""" | |
245 self.__title.setText(text) | |
246 | |
247 def isSelectable(self): | |
248 return self.__selectable | |
249 | |
250 def setSelectable(self, selectable): | |
251 if not self.__selectable: | |
252 try: | |
253 self.removeClickListener(self) | |
254 except ValueError: | |
255 pass | |
256 if self.selectable and not self in self._clickListeners: | |
257 self.addClickListener(self) | |
258 self.__selectable = selectable | |
259 | |
260 def setWidget(self, widget, scrollable=True): | |
261 """Set the widget that will be in the body of the LiberviaWidget | |
262 @param widget: widget to put in the body | |
263 @param scrollable: if true, the widget will be in a ScrollPanelWrapper""" | |
264 if scrollable: | |
265 _scrollpanelwrapper = ScrollPanelWrapper() | |
266 _scrollpanelwrapper.setStyleName('widgetBody') | |
267 _scrollpanelwrapper.setWidget(widget) | |
268 body_wid = _scrollpanelwrapper | |
269 else: | |
270 body_wid = widget | |
271 self.add(body_wid) | |
272 self.setCellHeight(body_wid, '100%') | |
273 | |
274 def doDetachChildren(self): | |
275 #We need to force the use of a panel subclass method here, | |
276 #for the same reason as doAttachChildren | |
277 VerticalPanel.doDetachChildren(self) | |
278 | |
279 def doAttachChildren(self): | |
280 #We need to force the use of a panel subclass method here, else | |
281 #the event will not propagate to children | |
282 VerticalPanel.doAttachChildren(self) | |
283 | |
284 class ScrollPanelWrapper(SimplePanel): | |
285 """Scroll Panel like component, wich use the full available space | |
286 to work around percent size issue, it use some of the ideas found | |
287 here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316 | |
288 specially in code given at comment #46, thanks to Stefan Bachert""" | |
289 | |
290 def __init__(self, *args, **kwargs): | |
291 SimplePanel.__init__(self) | |
292 self.spanel = ScrollPanel(*args, **kwargs) | |
293 SimplePanel.setWidget(self, self.spanel) | |
294 DOM.setStyleAttribute(self.getElement(), "position", "relative") | |
295 DOM.setStyleAttribute(self.getElement(), "top", "0px") | |
296 DOM.setStyleAttribute(self.getElement(), "left", "0px") | |
297 DOM.setStyleAttribute(self.getElement(), "width", "100%") | |
298 DOM.setStyleAttribute(self.getElement(), "height", "100%") | |
299 DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute") | |
300 DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%") | |
301 DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%") | |
302 | |
303 def setWidget(self, widget): | |
304 self.spanel.setWidget(widget) | |
305 | |
306 def setScrollPosition(self, position): | |
307 self.spanel.setScrollPosition(position) | |
308 | |
309 def scrollToBottom(self): | |
310 self.setScrollPosition(self.spanel.getElement().scrollHeight) | |
311 | |
312 class EmptyWidget(DropCell, SimplePanel): | |
313 """Empty dropable panel""" | |
314 | |
315 def __init__(self, host): | |
316 SimplePanel.__init__(self) | |
317 DropCell.__init__(self, host) | |
318 #self.setWidget(HTML('')) | |
319 self.setSize('100%','100%') | |
320 | |
321 class BorderWidget(EmptyWidget): | |
322 def __init__(self, host): | |
323 EmptyWidget.__init__(self, host) | |
324 self.addStyleName('borderPanel') | |
325 | |
326 class LeftBorderWidget(BorderWidget): | |
327 def __init__(self, host): | |
328 BorderWidget.__init__(self, host) | |
329 self.addStyleName('leftBorderWidget') | |
330 | |
331 class RightBorderWidget(BorderWidget): | |
332 def __init__(self, host): | |
333 BorderWidget.__init__(self, host) | |
334 self.addStyleName('rightBorderWidget') | |
335 | |
336 class BottomBorderWidget(BorderWidget): | |
337 def __init__(self, host): | |
338 BorderWidget.__init__(self, host) | |
339 self.addStyleName('bottomBorderWidget') | |
340 | |
341 class WidgetsPanel(ScrollPanelWrapper): | |
342 | |
343 def __init__(self, host, locked = False): | |
344 ScrollPanelWrapper.__init__(self) | |
345 self.setSize('100%', '100%') | |
346 self.host = host | |
347 self.locked = locked #if True: tab will not be removed when there are no more widgets inside | |
348 self.selected = None | |
349 self.flextable = FlexTable() | |
350 self.flextable.setSize('100%','100%') | |
351 self.setWidget(self.flextable) | |
352 self.setStyleName('widgetsPanel') | |
353 _bottom = BottomBorderWidget(self.host) | |
354 self.flextable.setWidget(0, 0, _bottom) #There will be always an Empty widget on the last row, | |
355 #dropping a widget there will add a new row | |
356 td_elt = _bottom.getElement().parentNode | |
357 DOM.setStyleAttribute(td_elt, "height", "1px") #needed so the cell adapt to the size of the border (specially in webkit) | |
358 self._max_cols = 1 #give the maximum number of columns i a raw | |
359 | |
360 def isLocked(self): | |
361 return self.locked | |
362 | |
363 def changeWidget(self, row, col, wid): | |
364 """Change the widget in the given location, add row or columns when necessary""" | |
365 print "changing widget:", wid, row, col | |
366 last_row = max(0, self.flextable.getRowCount()-1) | |
367 try: | |
368 prev_wid = self.flextable.getWidget(row, col) | |
369 except: | |
370 print "ERROR: Trying to change an unexisting widget !" | |
371 return | |
372 | |
373 | |
374 cellFormatter = self.flextable.getFlexCellFormatter() | |
375 | |
376 if isinstance(prev_wid, BorderWidget): | |
377 #We are on a border, we must create a row and/or columns | |
378 print "BORDER WIDGET" | |
379 prev_wid.removeStyleName('dragover') | |
380 | |
381 if isinstance(prev_wid, BottomBorderWidget): | |
382 #We are on the bottom border, we create a new row | |
383 self.flextable.insertRow(last_row) | |
384 self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host)) | |
385 self.flextable.setWidget(last_row, 1, wid) | |
386 self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host)) | |
387 cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT) | |
388 row = last_row | |
389 | |
390 elif isinstance(prev_wid, LeftBorderWidget): | |
391 if col!=0: | |
392 print "ERROR: LeftBorderWidget must be on the first column !" | |
393 return | |
394 self.flextable.insertCell(row, col+1) | |
395 self.flextable.setWidget(row, 1, wid) | |
396 | |
397 elif isinstance(prev_wid, RightBorderWidget): | |
398 if col!=self.flextable.getCellCount(row)-1: | |
399 print "ERROR: RightBorderWidget must be on the last column !" | |
400 return | |
401 self.flextable.insertCell(row, col) | |
402 self.flextable.setWidget(row, col, wid) | |
403 | |
404 else: | |
405 prev_wid.removeFromParent() | |
406 self.flextable.setWidget(row, col, wid) | |
407 | |
408 _max_cols = max(self._max_cols, self.flextable.getCellCount(row)) | |
409 if _max_cols != self._max_cols: | |
410 self._max_cols = _max_cols | |
411 self._sizesAdjust() | |
412 | |
413 def _sizesAdjust(self): | |
414 cellFormatter = self.flextable.getFlexCellFormatter() | |
415 width = 100.0/max(1, self._max_cols-2) #we don't count the borders | |
416 | |
417 for row_idx in xrange(self.flextable.getRowCount()): | |
418 for col_idx in xrange(self.flextable.getCellCount(row_idx)): | |
419 _widget = self.flextable.getWidget(row_idx, col_idx) | |
420 if not isinstance(_widget, BorderWidget): | |
421 td_elt = _widget.getElement().parentNode | |
422 DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width) | |
423 | |
424 last_row = max(0, self.flextable.getRowCount()-1) | |
425 cellFormatter.setColSpan(last_row, 0, self._max_cols) | |
426 | |
427 def addWidget(self, wid): | |
428 """Add a widget to a new cell on the next to last row""" | |
429 last_row = max(0, self.flextable.getRowCount()-1) | |
430 print "putting widget %s at %d, %d" % (wid, last_row, 0) | |
431 self.changeWidget(last_row, 0, wid) | |
432 | |
433 def removeWidget(self, wid): | |
434 """Remove a widget and the cell where it is""" | |
435 _row, _col = self.flextable.getIndex(wid) | |
436 self.flextable.remove(wid) | |
437 self.flextable.removeCell(_row, _col) | |
438 if self.flextable.getCellCount(_row) == 2: #we have only the borders left, we remove the row | |
439 self.flextable.removeRow(_row) | |
440 _max_cols = 1 | |
441 for row_idx in xrange(self.flextable.getRowCount()): | |
442 _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx)) | |
443 if _max_cols != self._max_cols: | |
444 self._max_cols = _max_cols | |
445 self._sizesAdjust() | |
446 current = self | |
447 | |
448 blank_page = not [wid for wid in self.flextable if isinstance(wid, LiberviaWidget)] # do we still have widgets on the page ? | |
449 | |
450 if blank_page and not self.isLocked(): | |
451 #we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed | |
452 while current is not None: | |
453 if isinstance(current, MainTabPanel): | |
454 current.onWidgetPanelRemove(self) | |
455 return | |
456 current = current.getParent() | |
457 print "Error: no MainTabPanel found !" | |
458 | |
459 def getIndex(self, wid): | |
460 return self.flextable.getIndex(wid) | |
461 | |
462 def getColSpan(self, row, col): | |
463 cellFormatter = self.flextable.getFlexCellFormatter() | |
464 return cellFormatter.getColSpan(row, col) | |
465 | |
466 def setColSpan(self, row, col, value): | |
467 cellFormatter = self.flextable.getFlexCellFormatter() | |
468 return cellFormatter.setColSpan(row, col, value) | |
469 | |
470 def getRowSpan(self, row, col): | |
471 cellFormatter = self.flextable.getFlexCellFormatter() | |
472 return cellFormatter.getRowSpan(row, col) | |
473 | |
474 def setRowSpan(self, row, col, value): | |
475 cellFormatter = self.flextable.getFlexCellFormatter() | |
476 return cellFormatter.setRowSpan(row, col, value) | |
477 | |
478 class MainTabPanel(TabPanel): | |
479 | |
480 def __init__(self, host): | |
481 TabPanel.__init__(self) | |
482 self.host=host | |
483 self.tabBar.setVisible(False) | |
484 self.setStyleName('liberviaTabPanel') | |
485 self.addStyleName('mainTabPanel') | |
486 Window.addWindowResizeListener(self) | |
487 | |
488 def getCurrentPanel(self): | |
489 """ Get the panel of the currently selected tab """ | |
490 return self.deck.visibleWidget | |
491 | |
492 def onWindowResized(self, width, height): | |
493 tab_panel_elt = self.getElement() | |
494 _elts = doc().getElementsByClassName('gwt-TabBar') | |
495 if not _elts.length: | |
496 print ("ERROR: no TabBar found, it should exist !") | |
497 tab_bar_h = 0 | |
498 else: | |
499 tab_bar_h = _elts.item(0).offsetHeight | |
500 ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5 | |
501 ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5 | |
502 self.setWidth("%s%s" % (ideal_width, "px")); | |
503 self.setHeight("%s%s" % (ideal_height, "px")); | |
504 | |
505 def add(self, widget, tabText=None, asHTML=False): | |
506 TabPanel.add(self, widget, tabText, asHTML) | |
507 if self.getWidgetCount()>1: | |
508 self.tabBar.setVisible(True) | |
509 self.host.resize() | |
510 | |
511 def onWidgetPanelRemove(self, panel): | |
512 """ Called when a child WidgetsPanel is empty and need to be removed """ | |
513 self.remove(panel) | |
514 widgets_count = self.getWidgetCount() | |
515 if widgets_count == 1: | |
516 self.tabBar.setVisible(False) | |
517 self.host.resize() | |
518 self.selectTab(0) | |
519 else: | |
520 self.selectTab(widgets_count - 1) | |
521 |