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='&nbsp;'
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