You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VCustomLayout.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. /*
  2. @ITMillApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client.ui;
  5. import java.util.HashMap;
  6. import java.util.HashSet;
  7. import java.util.Iterator;
  8. import java.util.Set;
  9. import com.google.gwt.dom.client.ImageElement;
  10. import com.google.gwt.dom.client.NodeList;
  11. import com.google.gwt.user.client.DOM;
  12. import com.google.gwt.user.client.Element;
  13. import com.google.gwt.user.client.Event;
  14. import com.google.gwt.user.client.ui.ComplexPanel;
  15. import com.google.gwt.user.client.ui.Widget;
  16. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  17. import com.vaadin.terminal.gwt.client.BrowserInfo;
  18. import com.vaadin.terminal.gwt.client.Container;
  19. import com.vaadin.terminal.gwt.client.ContainerResizedListener;
  20. import com.vaadin.terminal.gwt.client.Paintable;
  21. import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
  22. import com.vaadin.terminal.gwt.client.RenderSpace;
  23. import com.vaadin.terminal.gwt.client.UIDL;
  24. import com.vaadin.terminal.gwt.client.Util;
  25. import com.vaadin.terminal.gwt.client.VCaption;
  26. import com.vaadin.terminal.gwt.client.VCaptionWrapper;
  27. /**
  28. * Custom Layout implements complex layout defined with HTML template.
  29. *
  30. * @author IT Mill
  31. *
  32. */
  33. public class VCustomLayout extends ComplexPanel implements Paintable,
  34. Container, ContainerResizedListener {
  35. public static final String CLASSNAME = "v-customlayout";
  36. /** Location-name to containing element in DOM map */
  37. private final HashMap locationToElement = new HashMap();
  38. /** Location-name to contained widget map */
  39. private final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>();
  40. /** Widget to captionwrapper map */
  41. private final HashMap widgetToCaptionWrapper = new HashMap();
  42. /** Name of the currently rendered style */
  43. String currentTemplateName;
  44. /** Unexecuted scripts loaded from the template */
  45. private String scripts = "";
  46. /** Paintable ID of this paintable */
  47. private String pid;
  48. private ApplicationConnection client;
  49. /** Has the template been loaded from contents passed in UIDL **/
  50. private boolean hasTemplateContents = false;
  51. private Element elementWithNativeResizeFunction;
  52. private String height = "";
  53. private String width = "";
  54. private HashMap<String, FloatSize> locationToExtraSize = new HashMap<String, FloatSize>();
  55. public VCustomLayout() {
  56. setElement(DOM.createDiv());
  57. // Clear any unwanted styling
  58. DOM.setStyleAttribute(getElement(), "border", "none");
  59. DOM.setStyleAttribute(getElement(), "margin", "0");
  60. DOM.setStyleAttribute(getElement(), "padding", "0");
  61. if (BrowserInfo.get().isIE()) {
  62. DOM.setStyleAttribute(getElement(), "position", "relative");
  63. }
  64. setStyleName(CLASSNAME);
  65. }
  66. /**
  67. * Sets widget to given location.
  68. *
  69. * If location already contains a widget it will be removed.
  70. *
  71. * @param widget
  72. * Widget to be set into location.
  73. * @param location
  74. * location name where widget will be added
  75. *
  76. * @throws IllegalArgumentException
  77. * if no such location is found in the layout.
  78. */
  79. public void setWidget(Widget widget, String location) {
  80. if (widget == null) {
  81. return;
  82. }
  83. // If no given location is found in the layout, and exception is throws
  84. Element elem = (Element) locationToElement.get(location);
  85. if (elem == null && hasTemplate()) {
  86. throw new IllegalArgumentException("No location " + location
  87. + " found");
  88. }
  89. // Get previous widget
  90. final Widget previous = locationToWidget.get(location);
  91. // NOP if given widget already exists in this location
  92. if (previous == widget) {
  93. return;
  94. }
  95. if (previous != null) {
  96. remove(previous);
  97. }
  98. // if template is missing add element in order
  99. if (!hasTemplate()) {
  100. elem = getElement();
  101. }
  102. // Add widget to location
  103. super.add(widget, elem);
  104. locationToWidget.put(location, widget);
  105. }
  106. /** Update the layout from UIDL */
  107. public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
  108. this.client = client;
  109. // ApplicationConnection manages generic component features
  110. if (client.updateComponent(this, uidl, true)) {
  111. return;
  112. }
  113. pid = uidl.getId();
  114. if (!hasTemplate()) {
  115. // Update HTML template only once
  116. initializeHTML(uidl, client);
  117. }
  118. // Evaluate scripts
  119. eval(scripts);
  120. scripts = null;
  121. iLayout();
  122. // TODO Check if this is needed
  123. client.runDescendentsLayout(this);
  124. Set oldWidgets = new HashSet();
  125. oldWidgets.addAll(locationToWidget.values());
  126. // For all contained widgets
  127. for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
  128. final UIDL uidlForChild = (UIDL) i.next();
  129. if (uidlForChild.getTag().equals("location")) {
  130. final String location = uidlForChild.getStringAttribute("name");
  131. final Paintable child = client.getPaintable(uidlForChild
  132. .getChildUIDL(0));
  133. try {
  134. setWidget((Widget) child, location);
  135. child.updateFromUIDL(uidlForChild.getChildUIDL(0), client);
  136. } catch (final IllegalArgumentException e) {
  137. // If no location is found, this component is not visible
  138. }
  139. oldWidgets.remove(child);
  140. }
  141. }
  142. for (Iterator iterator = oldWidgets.iterator(); iterator.hasNext();) {
  143. Widget oldWidget = (Widget) iterator.next();
  144. if (oldWidget.isAttached()) {
  145. // slot of this widget is emptied, remove it
  146. remove(oldWidget);
  147. }
  148. }
  149. iLayout();
  150. // TODO Check if this is needed
  151. client.runDescendentsLayout(this);
  152. }
  153. /** Initialize HTML-layout. */
  154. private void initializeHTML(UIDL uidl, ApplicationConnection client) {
  155. final String newTemplateContents = uidl
  156. .getStringAttribute("templateContents");
  157. final String newTemplate = uidl.getStringAttribute("template");
  158. currentTemplateName = null;
  159. hasTemplateContents = false;
  160. String template = "";
  161. if (newTemplate != null) {
  162. // Get the HTML-template from client
  163. template = client.getResource("layouts/" + newTemplate + ".html");
  164. if (template == null) {
  165. template = "<em>Layout file layouts/"
  166. + newTemplate
  167. + ".html is missing. Components will be drawn for debug purposes.</em>";
  168. } else {
  169. currentTemplateName = newTemplate;
  170. }
  171. } else {
  172. hasTemplateContents = true;
  173. template = newTemplateContents;
  174. }
  175. // Connect body of the template to DOM
  176. template = extractBodyAndScriptsFromTemplate(template);
  177. // TODO prefix img src:s here with a regeps, cannot work further with IE
  178. String themeUri = client.getThemeUri();
  179. String relImgPrefix = themeUri + "/layouts/";
  180. // prefix all relative image elements to point to theme dir with a
  181. // regexp search
  182. template = template.replaceAll(
  183. "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"",
  184. "<$1 $2src=\"" + relImgPrefix + "$3\"");
  185. // also support src attributes without quotes
  186. template = template
  187. .replaceAll(
  188. "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]",
  189. "<$1 $2src=\"" + relImgPrefix + "$3\"");
  190. // also prefix relative style="...url(...)..."
  191. template = template
  192. .replaceAll(
  193. "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)",
  194. "$1 " + relImgPrefix + "$2 $3");
  195. getElement().setInnerHTML(template);
  196. // Remap locations to elements
  197. locationToElement.clear();
  198. scanForLocations(getElement());
  199. initImgElements();
  200. elementWithNativeResizeFunction = DOM.getFirstChild(getElement());
  201. if (elementWithNativeResizeFunction == null) {
  202. elementWithNativeResizeFunction = getElement();
  203. }
  204. publishResizedFunction(elementWithNativeResizeFunction);
  205. }
  206. private native boolean uriEndsWithSlash()
  207. /*-{
  208. var path = $wnd.location.pathname;
  209. if(path.charAt(path.length - 1) == "/")
  210. return true;
  211. return false;
  212. }-*/;
  213. private boolean hasTemplate() {
  214. if (currentTemplateName == null && !hasTemplateContents) {
  215. return false;
  216. } else {
  217. return true;
  218. }
  219. }
  220. /** Collect locations from template */
  221. private void scanForLocations(Element elem) {
  222. final String location = elem.getAttribute("location");
  223. if (!"".equals(location)) {
  224. locationToElement.put(location, elem);
  225. elem.setInnerHTML("");
  226. int x = Util.measureHorizontalPaddingAndBorder(elem, 0);
  227. int y = Util.measureVerticalPaddingAndBorder(elem, 0);
  228. FloatSize fs = new FloatSize(x, y);
  229. locationToExtraSize.put(location, fs);
  230. } else {
  231. final int len = DOM.getChildCount(elem);
  232. for (int i = 0; i < len; i++) {
  233. scanForLocations(DOM.getChild(elem, i));
  234. }
  235. }
  236. }
  237. /** Evaluate given script in browser document */
  238. private static native void eval(String script)
  239. /*-{
  240. try {
  241. if (script != null)
  242. eval("{ var document = $doc; var window = $wnd; "+ script + "}");
  243. } catch (e) {
  244. }
  245. }-*/;
  246. /**
  247. * Img elements needs some special handling in custom layout. Img elements
  248. * will get their onload events sunk. This way custom layout can notify
  249. * parent about possible size change.
  250. */
  251. private void initImgElements() {
  252. NodeList<com.google.gwt.dom.client.Element> nodeList = getElement()
  253. .getElementsByTagName("IMG");
  254. for (int i = 0; i < nodeList.getLength(); i++) {
  255. com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList
  256. .getItem(i);
  257. DOM.sinkEvents((Element) img.cast(), Event.ONLOAD);
  258. }
  259. }
  260. /**
  261. * Extract body part and script tags from raw html-template.
  262. *
  263. * Saves contents of all script-tags to private property: scripts. Returns
  264. * contents of the body part for the html without script-tags. Also replaces
  265. * all _UID_ tags with an unique id-string.
  266. *
  267. * @param html
  268. * Original HTML-template received from server
  269. * @return html that is used to create the HTMLPanel.
  270. */
  271. private String extractBodyAndScriptsFromTemplate(String html) {
  272. // Replace UID:s
  273. html = html.replaceAll("_UID_", pid + "__");
  274. // Exctract script-tags
  275. scripts = "";
  276. int endOfPrevScript = 0;
  277. int nextPosToCheck = 0;
  278. String lc = html.toLowerCase();
  279. String res = "";
  280. int scriptStart = lc.indexOf("<script", nextPosToCheck);
  281. while (scriptStart > 0) {
  282. res += html.substring(endOfPrevScript, scriptStart);
  283. scriptStart = lc.indexOf(">", scriptStart);
  284. final int j = lc.indexOf("</script>", scriptStart);
  285. scripts += html.substring(scriptStart + 1, j) + ";";
  286. nextPosToCheck = endOfPrevScript = j + "</script>".length();
  287. scriptStart = lc.indexOf("<script", nextPosToCheck);
  288. }
  289. res += html.substring(endOfPrevScript);
  290. // Extract body
  291. html = res;
  292. lc = html.toLowerCase();
  293. int startOfBody = lc.indexOf("<body");
  294. if (startOfBody < 0) {
  295. res = html;
  296. } else {
  297. res = "";
  298. startOfBody = lc.indexOf(">", startOfBody) + 1;
  299. final int endOfBody = lc.indexOf("</body>", startOfBody);
  300. if (endOfBody > startOfBody) {
  301. res = html.substring(startOfBody, endOfBody);
  302. } else {
  303. res = html.substring(startOfBody);
  304. }
  305. }
  306. return res;
  307. }
  308. /** Replace child components */
  309. public void replaceChildComponent(Widget from, Widget to) {
  310. final String location = getLocation(from);
  311. if (location == null) {
  312. throw new IllegalArgumentException();
  313. }
  314. setWidget(to, location);
  315. }
  316. /** Does this layout contain given child */
  317. public boolean hasChildComponent(Widget component) {
  318. return locationToWidget.containsValue(component);
  319. }
  320. /** Update caption for given widget */
  321. public void updateCaption(Paintable component, UIDL uidl) {
  322. VCaptionWrapper wrapper = (VCaptionWrapper) widgetToCaptionWrapper
  323. .get(component);
  324. if (VCaption.isNeeded(uidl)) {
  325. if (wrapper == null) {
  326. final String loc = getLocation((Widget) component);
  327. super.remove((Widget) component);
  328. wrapper = new VCaptionWrapper(component, client);
  329. super.add(wrapper, (Element) locationToElement.get(loc));
  330. widgetToCaptionWrapper.put(component, wrapper);
  331. }
  332. wrapper.updateCaption(uidl);
  333. } else {
  334. if (wrapper != null) {
  335. final String loc = getLocation((Widget) component);
  336. super.remove(wrapper);
  337. super.add((Widget) wrapper.getPaintable(),
  338. (Element) locationToElement.get(loc));
  339. widgetToCaptionWrapper.remove(component);
  340. }
  341. }
  342. }
  343. /** Get the location of an widget */
  344. public String getLocation(Widget w) {
  345. for (final Iterator i = locationToWidget.keySet().iterator(); i
  346. .hasNext();) {
  347. final String location = (String) i.next();
  348. if (locationToWidget.get(location) == w) {
  349. return location;
  350. }
  351. }
  352. return null;
  353. }
  354. /** Removes given widget from the layout */
  355. @Override
  356. public boolean remove(Widget w) {
  357. client.unregisterPaintable((Paintable) w);
  358. final String location = getLocation(w);
  359. if (location != null) {
  360. locationToWidget.remove(location);
  361. }
  362. final VCaptionWrapper cw = (VCaptionWrapper) widgetToCaptionWrapper
  363. .get(w);
  364. if (cw != null) {
  365. widgetToCaptionWrapper.remove(w);
  366. return super.remove(cw);
  367. } else if (w != null) {
  368. return super.remove(w);
  369. }
  370. return false;
  371. }
  372. /** Adding widget without specifying location is not supported */
  373. @Override
  374. public void add(Widget w) {
  375. throw new UnsupportedOperationException();
  376. }
  377. /** Clear all widgets from the layout */
  378. @Override
  379. public void clear() {
  380. super.clear();
  381. locationToWidget.clear();
  382. widgetToCaptionWrapper.clear();
  383. }
  384. public void iLayout() {
  385. iLayoutJS(DOM.getFirstChild(getElement()));
  386. }
  387. /**
  388. * This method is published to JS side with the same name into first DOM
  389. * node of custom layout. This way if one implements some resizeable
  390. * containers in custom layout he/she can notify children after resize.
  391. */
  392. public void notifyChildrenOfSizeChange() {
  393. client.runDescendentsLayout(this);
  394. }
  395. @Override
  396. public void onDetach() {
  397. super.onDetach();
  398. if (elementWithNativeResizeFunction != null) {
  399. detachResizedFunction(elementWithNativeResizeFunction);
  400. }
  401. }
  402. private native void detachResizedFunction(Element element)
  403. /*-{
  404. element.notifyChildrenOfSizeChange = null;
  405. }-*/;
  406. private native void publishResizedFunction(Element element)
  407. /*-{
  408. var self = this;
  409. element.notifyChildrenOfSizeChange = function() {
  410. self.@com.vaadin.terminal.gwt.client.ui.VCustomLayout::notifyChildrenOfSizeChange()();
  411. };
  412. }-*/;
  413. /**
  414. * In custom layout one may want to run layout functions made with
  415. * JavaScript. This function tests if one exists (with name "iLayoutJS" in
  416. * layouts first DOM node) and runs et. Return value is used to determine if
  417. * children needs to be notified of size changes.
  418. *
  419. * Note! When implementing a JS layout function you most likely want to call
  420. * notifyChildrenOfSizeChange() function on your custom layouts main
  421. * element. That method is used to control whether child components layout
  422. * functions are to be run.
  423. *
  424. * @param el
  425. * @return true if layout function exists and was run successfully, else
  426. * false.
  427. */
  428. private native boolean iLayoutJS(Element el)
  429. /*-{
  430. if(el && el.iLayoutJS) {
  431. try {
  432. el.iLayoutJS();
  433. return true;
  434. } catch (e) {
  435. return false;
  436. }
  437. } else {
  438. return false;
  439. }
  440. }-*/;
  441. public boolean requestLayout(Set<Paintable> child) {
  442. updateRelativeSizedComponents(true, true);
  443. if (width.equals("") || height.equals("")) {
  444. /* Automatically propagated upwards if the size can change */
  445. return false;
  446. }
  447. return true;
  448. }
  449. public RenderSpace getAllocatedSpace(Widget child) {
  450. com.google.gwt.dom.client.Element pe = child.getElement()
  451. .getParentElement();
  452. FloatSize extra = locationToExtraSize.get(getLocation(child));
  453. return new RenderSpace(pe.getOffsetWidth() - (int) extra.getWidth(),
  454. pe.getOffsetHeight() - (int) extra.getHeight(),
  455. Util.mayHaveScrollBars(pe));
  456. }
  457. @Override
  458. public void onBrowserEvent(Event event) {
  459. super.onBrowserEvent(event);
  460. if (event.getTypeInt() == Event.ONLOAD) {
  461. Util.notifyParentOfSizeChange(this, true);
  462. event.cancelBubble(true);
  463. }
  464. }
  465. @Override
  466. public void setHeight(String height) {
  467. if (this.height.equals(height)) {
  468. return;
  469. }
  470. boolean shrinking = true;
  471. if (isLarger(height, this.height)) {
  472. shrinking = false;
  473. }
  474. this.height = height;
  475. super.setHeight(height);
  476. /*
  477. * If the height shrinks we must remove all components with relative
  478. * height from the DOM, update their height when they do not affect the
  479. * available space and finally restore them to the original state
  480. */
  481. if (shrinking) {
  482. updateRelativeSizedComponents(false, true);
  483. }
  484. }
  485. @Override
  486. public void setWidth(String width) {
  487. if (this.width.equals(width)) {
  488. return;
  489. }
  490. boolean shrinking = true;
  491. if (isLarger(width, this.width)) {
  492. shrinking = false;
  493. }
  494. super.setWidth(width);
  495. this.width = width;
  496. /*
  497. * If the width shrinks we must remove all components with relative
  498. * width from the DOM, update their width when they do not affect the
  499. * available space and finally restore them to the original state
  500. */
  501. if (shrinking) {
  502. updateRelativeSizedComponents(true, false);
  503. }
  504. }
  505. private void updateRelativeSizedComponents(boolean relativeWidth,
  506. boolean relativeHeight) {
  507. Set<Widget> relativeSizeWidgets = new HashSet<Widget>();
  508. for (Widget widget : locationToWidget.values()) {
  509. FloatSize relativeSize = client.getRelativeSize(widget);
  510. if (relativeSize != null) {
  511. if ((relativeWidth && (relativeSize.getWidth() >= 0.0f))
  512. || (relativeHeight && (relativeSize.getHeight() >= 0.0f))) {
  513. relativeSizeWidgets.add(widget);
  514. widget.getElement().getStyle()
  515. .setProperty("position", "absolute");
  516. }
  517. }
  518. }
  519. for (Widget widget : relativeSizeWidgets) {
  520. client.handleComponentRelativeSize(widget);
  521. widget.getElement().getStyle().setProperty("position", "");
  522. }
  523. }
  524. /**
  525. * Compares newSize with currentSize and returns true if it is clear that
  526. * newSize is larger than currentSize. Returns false if newSize is smaller
  527. * or if it is unclear which one is smaller.
  528. *
  529. * @param newSize
  530. * @param currentSize
  531. * @return
  532. */
  533. private boolean isLarger(String newSize, String currentSize) {
  534. if (newSize.equals("") || currentSize.equals("")) {
  535. return false;
  536. }
  537. if (!newSize.endsWith("px") || !currentSize.endsWith("px")) {
  538. return false;
  539. }
  540. int newSizePx = Integer.parseInt(newSize.substring(0,
  541. newSize.length() - 2));
  542. int currentSizePx = Integer.parseInt(currentSize.substring(0,
  543. currentSize.length() - 2));
  544. boolean larger = newSizePx > currentSizePx;
  545. return larger;
  546. }
  547. }