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.

GridLayout.java 53KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui;
  17. import java.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.Iterator;
  24. import java.util.LinkedList;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Map.Entry;
  28. import java.util.Set;
  29. import org.jsoup.nodes.Attributes;
  30. import org.jsoup.nodes.Element;
  31. import org.jsoup.select.Elements;
  32. import com.vaadin.event.LayoutEvents.LayoutClickEvent;
  33. import com.vaadin.event.LayoutEvents.LayoutClickListener;
  34. import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
  35. import com.vaadin.shared.Connector;
  36. import com.vaadin.shared.EventId;
  37. import com.vaadin.shared.MouseEventDetails;
  38. import com.vaadin.shared.Registration;
  39. import com.vaadin.shared.ui.MarginInfo;
  40. import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc;
  41. import com.vaadin.shared.ui.gridlayout.GridLayoutState;
  42. import com.vaadin.shared.ui.gridlayout.GridLayoutState.ChildComponentData;
  43. import com.vaadin.ui.declarative.DesignAttributeHandler;
  44. import com.vaadin.ui.declarative.DesignContext;
  45. /**
  46. * A layout where the components are laid out on a grid using cell coordinates.
  47. *
  48. * <p>
  49. * The GridLayout also maintains a cursor for adding components in
  50. * left-to-right, top-to-bottom order.
  51. * </p>
  52. *
  53. * <p>
  54. * Each component in a <code>GridLayout</code> uses a defined
  55. * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The
  56. * components may not overlap with the existing components - if you try to do so
  57. * you will get an {@link OverlapsException}. Adding a component with cursor
  58. * automatically extends the grid by increasing the grid height.
  59. * </p>
  60. *
  61. * <p>
  62. * The grid coordinates, which are specified by a row and column index, always
  63. * start from 0 for the topmost row and the leftmost column.
  64. * </p>
  65. *
  66. * @author Vaadin Ltd.
  67. * @since 3.0
  68. */
  69. @SuppressWarnings("serial")
  70. public class GridLayout extends AbstractLayout
  71. implements Layout.AlignmentHandler, Layout.SpacingHandler,
  72. Layout.MarginHandler, LayoutClickNotifier {
  73. private GridLayoutServerRpc rpc = (MouseEventDetails mouseDetails,
  74. Connector clickedConnector) -> fireEvent(
  75. LayoutClickEvent.createEvent(GridLayout.this, mouseDetails,
  76. clickedConnector));
  77. /**
  78. * Cursor X position: this is where the next component with unspecified x,y
  79. * is inserted
  80. */
  81. private int cursorX = 0;
  82. /**
  83. * Cursor Y position: this is where the next component with unspecified x,y
  84. * is inserted
  85. */
  86. private int cursorY = 0;
  87. private final LinkedList<Component> components = new LinkedList<>();
  88. private Map<Integer, Float> columnExpandRatio = new HashMap<>();
  89. private Map<Integer, Float> rowExpandRatio = new HashMap<>();
  90. private Alignment defaultComponentAlignment = Alignment.TOP_LEFT;
  91. /**
  92. * Constructor for a grid of given size (number of columns and rows).
  93. *
  94. * The grid may grow or shrink later. Grid grows automatically if you add
  95. * components outside its area.
  96. *
  97. * @param columns
  98. * Number of columns in the grid.
  99. * @param rows
  100. * Number of rows in the grid.
  101. */
  102. public GridLayout(int columns, int rows) {
  103. setColumns(columns);
  104. setRows(rows);
  105. registerRpc(rpc);
  106. }
  107. /**
  108. * Constructs an empty (1x1) grid layout that is extended as needed.
  109. */
  110. public GridLayout() {
  111. this(1, 1);
  112. }
  113. /**
  114. * Constructs a GridLayout of given size (number of columns and rows) and
  115. * adds the given components in order to the grid.
  116. *
  117. * @see #addComponents(Component...)
  118. *
  119. * @param columns
  120. * Number of columns in the grid.
  121. * @param rows
  122. * Number of rows in the grid.
  123. * @param children
  124. * Components to add to the grid.
  125. */
  126. public GridLayout(int columns, int rows, Component... children) {
  127. this(columns, rows);
  128. addComponents(children);
  129. }
  130. @Override
  131. protected GridLayoutState getState() {
  132. return (GridLayoutState) super.getState();
  133. }
  134. @Override
  135. protected GridLayoutState getState(boolean markAsDirty) {
  136. return (GridLayoutState) super.getState(markAsDirty);
  137. }
  138. /**
  139. * <p>
  140. * Adds a component to the grid in the specified area. The area is defined
  141. * by specifying the upper left corner (column1, row1) and the lower right
  142. * corner (column2, row2) of the area. The coordinates are zero-based.
  143. * </p>
  144. *
  145. * <p>
  146. * If the area overlaps with any of the existing components already present
  147. * in the grid, the operation will fail and an {@link OverlapsException} is
  148. * thrown.
  149. * </p>
  150. *
  151. * @param component
  152. * the component to be added, not <code>null</code>.
  153. * @param column1
  154. * the column of the upper left corner of the area <code>c</code>
  155. * is supposed to occupy. The leftmost column has index 0.
  156. * @param row1
  157. * the row of the upper left corner of the area <code>c</code> is
  158. * supposed to occupy. The topmost row has index 0.
  159. * @param column2
  160. * the column of the lower right corner of the area
  161. * <code>c</code> is supposed to occupy.
  162. * @param row2
  163. * the row of the lower right corner of the area <code>c</code>
  164. * is supposed to occupy.
  165. * @throws OverlapsException
  166. * if the new component overlaps with any of the components
  167. * already in the grid.
  168. * @throws OutOfBoundsException
  169. * if the cells are outside the grid area.
  170. */
  171. public void addComponent(Component component, int column1, int row1,
  172. int column2, int row2)
  173. throws OverlapsException, OutOfBoundsException {
  174. if (component == null) {
  175. throw new NullPointerException("Component must not be null");
  176. }
  177. // Checks that the component does not already exist in the container
  178. if (components.contains(component)) {
  179. throw new IllegalArgumentException(
  180. "Component is already in the container");
  181. }
  182. // Creates the area
  183. final Area area = new Area(component, column1, row1, column2, row2);
  184. // Checks the validity of the coordinates
  185. if (column2 < column1 || row2 < row1) {
  186. throw new IllegalArgumentException(String.format(
  187. "Illegal coordinates for the component: %s!<=%s, %s!<=%s",
  188. column1, column2, row1, row2));
  189. }
  190. if (column1 < 0 || row1 < 0 || column2 >= getColumns()
  191. || row2 >= getRows()) {
  192. throw new OutOfBoundsException(area);
  193. }
  194. // Checks that newItem does not overlap with existing items
  195. checkExistingOverlaps(area);
  196. // Inserts the component to right place at the list
  197. // Respect top-down, left-right ordering
  198. // component.setParent(this);
  199. final Map<Connector, ChildComponentData> childDataMap = getState().childData;
  200. int index = 0;
  201. boolean done = false;
  202. for (Component c : components) {
  203. final ChildComponentData existingArea = childDataMap.get(c);
  204. if ((existingArea.row1 >= row1 && existingArea.column1 > column1)
  205. || existingArea.row1 > row1) {
  206. components.add(index, component);
  207. done = true;
  208. break;
  209. }
  210. index++;
  211. }
  212. if (!done) {
  213. components.addLast(component);
  214. }
  215. childDataMap.put(component, area.childData);
  216. // Attempt to add to super
  217. try {
  218. super.addComponent(component);
  219. } catch (IllegalArgumentException e) {
  220. childDataMap.remove(component);
  221. components.remove(component);
  222. throw e;
  223. }
  224. // update cursor position, if it's within this area; use first position
  225. // outside this area, even if it's occupied
  226. if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1
  227. && cursorY <= row2) {
  228. // cursor within area
  229. cursorX = column2 + 1; // one right of area
  230. if (cursorX >= getColumns()) {
  231. // overflowed columns
  232. cursorX = 0; // first col
  233. // move one row down, or one row under the area
  234. cursorY = (column1 == 0 ? row2 : row1) + 1;
  235. } else {
  236. cursorY = row1;
  237. }
  238. }
  239. }
  240. /**
  241. * Tests if the given area overlaps with any of the items already on the
  242. * grid.
  243. *
  244. * @param area
  245. * the Area to be checked for overlapping.
  246. * @throws OverlapsException
  247. * if <code>area</code> overlaps with any existing area.
  248. */
  249. private void checkExistingOverlaps(Area area) throws OverlapsException {
  250. for (Entry<Connector, ChildComponentData> entry : getState().childData
  251. .entrySet()) {
  252. if (componentsOverlap(entry.getValue(), area.childData)) {
  253. // Component not added, overlaps with existing component
  254. throw new OverlapsException(
  255. new Area(entry.getValue(), (Component) entry.getKey()));
  256. }
  257. }
  258. }
  259. /**
  260. * Adds the component to the grid in cells column1,row1 (NortWest corner of
  261. * the area.) End coordinates (SouthEast corner of the area) are the same as
  262. * column1,row1. The coordinates are zero-based. Component width and height
  263. * is 1.
  264. *
  265. * @param component
  266. * the component to be added, not <code>null</code>.
  267. * @param column
  268. * the column index, starting from 0.
  269. * @param row
  270. * the row index, starting from 0.
  271. * @throws OverlapsException
  272. * if the new component overlaps with any of the components
  273. * already in the grid.
  274. * @throws OutOfBoundsException
  275. * if the cell is outside the grid area.
  276. */
  277. public void addComponent(Component component, int column, int row)
  278. throws OverlapsException, OutOfBoundsException {
  279. this.addComponent(component, column, row, column, row);
  280. }
  281. /**
  282. * Forces the next component to be added at the beginning of the next line.
  283. *
  284. * <p>
  285. * Sets the cursor column to 0 and increments the cursor row by one.
  286. * </p>
  287. *
  288. * <p>
  289. * By calling this function you can ensure that no more components are added
  290. * right of the previous component.
  291. * </p>
  292. *
  293. * @see #space()
  294. */
  295. public void newLine() {
  296. cursorX = 0;
  297. cursorY++;
  298. }
  299. /**
  300. * Moves the cursor forward by one. If the cursor goes out of the right grid
  301. * border, it is moved to the first column of the next row.
  302. *
  303. * @see #newLine()
  304. */
  305. public void space() {
  306. cursorX++;
  307. if (cursorX >= getColumns()) {
  308. cursorX = 0;
  309. cursorY++;
  310. }
  311. }
  312. /**
  313. * Adds the component into this container to the cursor position. If the
  314. * cursor position is already occupied, the cursor is moved forwards to find
  315. * free position. If the cursor goes out from the bottom of the grid, the
  316. * grid is automatically extended.
  317. *
  318. * @param component
  319. * the component to be added, not <code>null</code>.
  320. */
  321. @Override
  322. public void addComponent(Component component) {
  323. if (component == null) {
  324. throw new IllegalArgumentException("Component must not be null");
  325. }
  326. // Finds first available place from the grid
  327. Area area;
  328. boolean done = false;
  329. while (!done) {
  330. try {
  331. area = new Area(component, cursorX, cursorY, cursorX, cursorY);
  332. checkExistingOverlaps(area);
  333. done = true;
  334. } catch (final OverlapsException e) {
  335. space();
  336. }
  337. }
  338. // Extends the grid if needed
  339. if (cursorX >= getColumns()) {
  340. setColumns(cursorX + 1);
  341. }
  342. if (cursorY >= getRows()) {
  343. setRows(cursorY + 1);
  344. }
  345. addComponent(component, cursorX, cursorY);
  346. }
  347. /**
  348. * Removes the specified component from the layout.
  349. *
  350. * @param component
  351. * the component to be removed.
  352. */
  353. @Override
  354. public void removeComponent(Component component) {
  355. // Check that the component is contained in the container
  356. if (component == null || !components.contains(component)) {
  357. return;
  358. }
  359. getState().childData.remove(component);
  360. components.remove(component);
  361. super.removeComponent(component);
  362. }
  363. /**
  364. * Removes the component specified by its cell coordinates.
  365. *
  366. * @param column
  367. * the component's column, starting from 0.
  368. * @param row
  369. * the component's row, starting from 0.
  370. */
  371. public void removeComponent(int column, int row) {
  372. // Finds the area
  373. for (final Component component : components) {
  374. final ChildComponentData childData = getState().childData
  375. .get(component);
  376. if (childData.column1 == column && childData.row1 == row) {
  377. removeComponent(component);
  378. return;
  379. }
  380. }
  381. }
  382. /**
  383. * Gets an Iterator for the components contained in the layout. By using the
  384. * Iterator it is possible to step through the contents of the layout.
  385. *
  386. * @return the Iterator of the components inside the layout.
  387. */
  388. @Override
  389. public Iterator<Component> iterator() {
  390. return Collections.unmodifiableCollection(components).iterator();
  391. }
  392. /**
  393. * Gets the number of components contained in the layout. Consistent with
  394. * the iterator returned by {@link #getComponentIterator()}.
  395. *
  396. * @return the number of contained components
  397. */
  398. @Override
  399. public int getComponentCount() {
  400. return components.size();
  401. }
  402. @Override
  403. public void beforeClientResponse(boolean initial) {
  404. super.beforeClientResponse(initial);
  405. getState().colExpand = new float[getColumns()];
  406. float colSum = getExpandRatioSum(columnExpandRatio);
  407. if (colSum == 0) {
  408. // no cols have been expanded
  409. for (int i = 0; i < getColumns(); i++) {
  410. getState().colExpand[i] = 1f;
  411. }
  412. } else {
  413. for (int i = 0; i < getColumns(); i++) {
  414. getState().colExpand[i] = getColumnExpandRatio(i);
  415. }
  416. }
  417. getState().rowExpand = new float[getRows()];
  418. float rowSum = getExpandRatioSum(rowExpandRatio);
  419. if (rowSum == 0) {
  420. // no rows have been expanded
  421. for (int i = 0; i < getRows(); i++) {
  422. getState().rowExpand[i] = 1f;
  423. }
  424. } else {
  425. for (int i = 0; i < getRows(); i++) {
  426. getState().rowExpand[i] = getRowExpandRatio(i);
  427. }
  428. }
  429. }
  430. private float getExpandRatioSum(Map<Integer, Float> ratioMap) {
  431. float sum = 0;
  432. for (Float expandRatio : ratioMap.values()) {
  433. sum += expandRatio;
  434. }
  435. return sum;
  436. }
  437. /*
  438. * (non-Javadoc)
  439. *
  440. * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com
  441. * .vaadin.ui.Component)
  442. */
  443. @Override
  444. public Alignment getComponentAlignment(Component childComponent) {
  445. ChildComponentData childComponentData = getState(false).childData
  446. .get(childComponent);
  447. if (childComponentData == null) {
  448. throw new IllegalArgumentException(
  449. "The given component is not a child of this layout");
  450. } else {
  451. return new Alignment(childComponentData.alignment);
  452. }
  453. }
  454. /**
  455. * Defines a rectangular area of cells in a GridLayout.
  456. *
  457. * <p>
  458. * Also maintains a reference to the component contained in the area.
  459. * </p>
  460. *
  461. * <p>
  462. * The area is specified by the cell coordinates of its upper left corner
  463. * (column1,row1) and lower right corner (column2,row2). As otherwise with
  464. * GridLayout, the column and row coordinates start from zero.
  465. * </p>
  466. *
  467. * @author Vaadin Ltd.
  468. * @since 3.0
  469. */
  470. public class Area implements Serializable {
  471. private final ChildComponentData childData;
  472. private final Component component;
  473. /**
  474. * <p>
  475. * Construct a new area on a grid.
  476. * </p>
  477. *
  478. * @param component
  479. * the component connected to the area.
  480. * @param column1
  481. * The column of the upper left corner cell of the area. The
  482. * leftmost column has index 0.
  483. * @param row1
  484. * The row of the upper left corner cell of the area. The
  485. * topmost row has index 0.
  486. * @param column2
  487. * The column of the lower right corner cell of the area. The
  488. * leftmost column has index 0.
  489. * @param row2
  490. * The row of the lower right corner cell of the area. The
  491. * topmost row has index 0.
  492. */
  493. public Area(Component component, int column1, int row1, int column2,
  494. int row2) {
  495. this.component = component;
  496. childData = new ChildComponentData();
  497. childData.alignment = getDefaultComponentAlignment().getBitMask();
  498. childData.column1 = column1;
  499. childData.row1 = row1;
  500. childData.column2 = column2;
  501. childData.row2 = row2;
  502. }
  503. public Area(ChildComponentData childData, Component component) {
  504. this.childData = childData;
  505. this.component = component;
  506. }
  507. /**
  508. * Tests if this Area overlaps with another Area.
  509. *
  510. * @param other
  511. * the other Area that is to be tested for overlap with this
  512. * area
  513. * @return <code>true</code> if <code>other</code> area overlaps with
  514. * this on, <code>false</code> if it does not.
  515. */
  516. public boolean overlaps(Area other) {
  517. return componentsOverlap(childData, other.childData);
  518. }
  519. /**
  520. * Gets the component connected to the area.
  521. *
  522. * @return the Component.
  523. */
  524. public Component getComponent() {
  525. return component;
  526. }
  527. /**
  528. * Gets the column of the top-left corner cell.
  529. *
  530. * @return the column of the top-left corner cell.
  531. */
  532. public int getColumn1() {
  533. return childData.column1;
  534. }
  535. /**
  536. * Gets the column of the bottom-right corner cell.
  537. *
  538. * @return the column of the bottom-right corner cell.
  539. */
  540. public int getColumn2() {
  541. return childData.column2;
  542. }
  543. /**
  544. * Gets the row of the top-left corner cell.
  545. *
  546. * @return the row of the top-left corner cell.
  547. */
  548. public int getRow1() {
  549. return childData.row1;
  550. }
  551. /**
  552. * Gets the row of the bottom-right corner cell.
  553. *
  554. * @return the row of the bottom-right corner cell.
  555. */
  556. public int getRow2() {
  557. return childData.row2;
  558. }
  559. @Override
  560. public String toString() {
  561. return String.format("Area{%s,%s - %s,%s}", getColumn1(), getRow1(),
  562. getColumn2(), getRow2());
  563. }
  564. }
  565. private static boolean componentsOverlap(ChildComponentData a,
  566. ChildComponentData b) {
  567. return a.column1 <= b.column2 && a.row1 <= b.row2
  568. && a.column2 >= b.column1 && a.row2 >= b.row1;
  569. }
  570. /**
  571. * Gridlayout does not support laying components on top of each other. An
  572. * <code>OverlapsException</code> is thrown when a component already exists
  573. * (even partly) at the same space on a grid with the new component.
  574. *
  575. * @author Vaadin Ltd.
  576. * @since 3.0
  577. */
  578. public class OverlapsException extends RuntimeException {
  579. private final Area existingArea;
  580. /**
  581. * Constructs an <code>OverlapsException</code>.
  582. *
  583. * @param existingArea
  584. */
  585. public OverlapsException(Area existingArea) {
  586. this.existingArea = existingArea;
  587. }
  588. @Override
  589. public String getMessage() {
  590. StringBuilder sb = new StringBuilder();
  591. Component component = existingArea.getComponent();
  592. sb.append(component);
  593. sb.append("( type = ");
  594. sb.append(component.getClass().getName());
  595. if (component.getCaption() != null) {
  596. sb.append(", caption = \"");
  597. sb.append(component.getCaption());
  598. sb.append("\"");
  599. }
  600. sb.append(')');
  601. sb.append(" is already added to ");
  602. sb.append(existingArea.childData.column1);
  603. sb.append(',');
  604. sb.append(existingArea.childData.column1);
  605. sb.append(',');
  606. sb.append(existingArea.childData.row1);
  607. sb.append(',');
  608. sb.append(existingArea.childData.row2);
  609. sb.append("(column1, column2, row1, row2).");
  610. return sb.toString();
  611. }
  612. /**
  613. * Gets the area .
  614. *
  615. * @return the existing area.
  616. */
  617. public Area getArea() {
  618. return existingArea;
  619. }
  620. }
  621. /**
  622. * An <code>Exception</code> object which is thrown when an area exceeds the
  623. * bounds of the grid.
  624. *
  625. * @author Vaadin Ltd.
  626. * @since 3.0
  627. */
  628. public class OutOfBoundsException extends RuntimeException {
  629. private final Area areaOutOfBounds;
  630. /**
  631. * Constructs an <code>OoutOfBoundsException</code> with the specified
  632. * detail message.
  633. *
  634. * @param areaOutOfBounds
  635. */
  636. public OutOfBoundsException(Area areaOutOfBounds) {
  637. super(String.format("%s, layout dimension: %sx%s", areaOutOfBounds,
  638. getColumns(), getRows()));
  639. this.areaOutOfBounds = areaOutOfBounds;
  640. }
  641. /**
  642. * Gets the area that is out of bounds.
  643. *
  644. * @return the area out of Bound.
  645. */
  646. public Area getArea() {
  647. return areaOutOfBounds;
  648. }
  649. }
  650. /**
  651. * Sets the number of columns in the grid. The column count can not be
  652. * reduced if there are any areas that would be outside of the shrunk grid.
  653. *
  654. * @param columns
  655. * the new number of columns in the grid.
  656. */
  657. public void setColumns(int columns) {
  658. // The the param
  659. if (columns < 1) {
  660. throw new IllegalArgumentException(
  661. "The number of columns and rows in the grid must be at least 1");
  662. }
  663. // In case of no change
  664. if (getColumns() == columns) {
  665. return;
  666. }
  667. // Checks for overlaps
  668. if (getColumns() > columns) {
  669. for (Entry<Connector, ChildComponentData> entry : getState().childData
  670. .entrySet()) {
  671. if (entry.getValue().column2 >= columns) {
  672. throw new OutOfBoundsException(new Area(entry.getValue(),
  673. (Component) entry.getKey()));
  674. }
  675. }
  676. }
  677. // Forget expands for removed columns
  678. if (columns < getColumns()) {
  679. for (int i = columns; i < getColumns(); i++) {
  680. columnExpandRatio.remove(i);
  681. getState().explicitColRatios.remove(i);
  682. }
  683. }
  684. getState().columns = columns;
  685. }
  686. /**
  687. * Get the number of columns in the grid.
  688. *
  689. * @return the number of columns in the grid.
  690. */
  691. public int getColumns() {
  692. return getState(false).columns;
  693. }
  694. /**
  695. * Sets the number of rows in the grid. The number of rows can not be
  696. * reduced if there are any areas that would be outside of the shrunk grid.
  697. *
  698. * @param rows
  699. * the new number of rows in the grid.
  700. */
  701. public void setRows(int rows) {
  702. // The the param
  703. if (rows < 1) {
  704. throw new IllegalArgumentException(
  705. "The number of columns and rows in the grid must be at least 1");
  706. }
  707. // In case of no change
  708. if (getRows() == rows) {
  709. return;
  710. }
  711. // Checks for overlaps
  712. if (getRows() > rows) {
  713. for (Entry<Connector, ChildComponentData> entry : getState().childData
  714. .entrySet()) {
  715. if (entry.getValue().row2 >= rows) {
  716. throw new OutOfBoundsException(new Area(entry.getValue(),
  717. (Component) entry.getKey()));
  718. }
  719. }
  720. }
  721. // Forget expands for removed rows
  722. if (rows < getRows()) {
  723. for (int i = rows; i < getRows(); i++) {
  724. rowExpandRatio.remove(i);
  725. getState().explicitRowRatios.remove(i);
  726. }
  727. }
  728. getState().rows = rows;
  729. }
  730. /**
  731. * Get the number of rows in the grid.
  732. *
  733. * @return the number of rows in the grid.
  734. */
  735. public int getRows() {
  736. return getState(false).rows;
  737. }
  738. /**
  739. * Gets the current x-position (column) of the cursor.
  740. *
  741. * <p>
  742. * The cursor position points the position for the next component that is
  743. * added without specifying its coordinates (grid cell). When the cursor
  744. * position is occupied, the next component will be added to first free
  745. * position after the cursor.
  746. * </p>
  747. *
  748. * @return the grid column the cursor is on, starting from 0.
  749. */
  750. public int getCursorX() {
  751. return cursorX;
  752. }
  753. /**
  754. * Sets the current cursor x-position. This is usually handled automatically
  755. * by GridLayout.
  756. *
  757. * @param cursorX
  758. */
  759. public void setCursorX(int cursorX) {
  760. this.cursorX = cursorX;
  761. }
  762. /**
  763. * Gets the current y-position (row) of the cursor.
  764. *
  765. * <p>
  766. * The cursor position points the position for the next component that is
  767. * added without specifying its coordinates (grid cell). When the cursor
  768. * position is occupied, the next component will be added to the first free
  769. * position after the cursor.
  770. * </p>
  771. *
  772. * @return the grid row the Cursor is on.
  773. */
  774. public int getCursorY() {
  775. return cursorY;
  776. }
  777. /**
  778. * Sets the current y-coordinate (row) of the cursor. This is usually
  779. * handled automatically by GridLayout.
  780. *
  781. * @param cursorY
  782. * the row number, starting from 0 for the topmost row.
  783. */
  784. public void setCursorY(int cursorY) {
  785. this.cursorY = cursorY;
  786. }
  787. /* Documented in superclass */
  788. @Override
  789. public void replaceComponent(Component oldComponent,
  790. Component newComponent) {
  791. // Gets the locations
  792. ChildComponentData oldLocation = getState().childData.get(oldComponent);
  793. ChildComponentData newLocation = getState().childData.get(newComponent);
  794. if (oldLocation == null) {
  795. addComponent(newComponent);
  796. } else if (newLocation == null) {
  797. removeComponent(oldComponent);
  798. addComponent(newComponent, oldLocation.column1, oldLocation.row1,
  799. oldLocation.column2, oldLocation.row2);
  800. } else {
  801. int oldAlignment = oldLocation.alignment;
  802. oldLocation.alignment = newLocation.alignment;
  803. newLocation.alignment = oldAlignment;
  804. getState().childData.put(newComponent, oldLocation);
  805. getState().childData.put(oldComponent, newLocation);
  806. }
  807. }
  808. /*
  809. * Removes all components from this container.
  810. *
  811. * @see com.vaadin.ui.ComponentContainer#removeAllComponents()
  812. */
  813. @Override
  814. public void removeAllComponents() {
  815. super.removeAllComponents();
  816. cursorX = 0;
  817. cursorY = 0;
  818. }
  819. @Override
  820. public void setComponentAlignment(Component childComponent,
  821. Alignment alignment) {
  822. ChildComponentData childComponentData = getState().childData
  823. .get(childComponent);
  824. if (childComponentData == null) {
  825. throw new IllegalArgumentException(
  826. "Component must be added to layout before using setComponentAlignment()");
  827. } else {
  828. if (alignment == null) {
  829. childComponentData.alignment = GridLayoutState.ALIGNMENT_DEFAULT
  830. .getBitMask();
  831. } else {
  832. childComponentData.alignment = alignment.getBitMask();
  833. }
  834. }
  835. }
  836. /*
  837. * (non-Javadoc)
  838. *
  839. * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean)
  840. */
  841. @Override
  842. public void setSpacing(boolean spacing) {
  843. getState().spacing = spacing;
  844. }
  845. /*
  846. * (non-Javadoc)
  847. *
  848. * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing()
  849. */
  850. @Override
  851. public boolean isSpacing() {
  852. return getState(false).spacing;
  853. }
  854. /**
  855. * Inserts an empty row at the specified position in the grid.
  856. *
  857. * @param row
  858. * Index of the row before which the new row will be inserted.
  859. * The leftmost row has index 0.
  860. */
  861. public void insertRow(int row) {
  862. if (row > getRows()) {
  863. throw new IllegalArgumentException("Cannot insert row at " + row
  864. + " in a gridlayout with height " + getRows());
  865. }
  866. for (ChildComponentData existingArea : getState().childData.values()) {
  867. // Areas ending below the row needs to be moved down or stretched
  868. if (existingArea.row2 >= row) {
  869. existingArea.row2++;
  870. // Stretch areas that span over the selected row
  871. if (existingArea.row1 >= row) {
  872. existingArea.row1++;
  873. }
  874. }
  875. }
  876. if (cursorY >= row) {
  877. cursorY++;
  878. }
  879. setRows(getRows() + 1);
  880. markAsDirty();
  881. }
  882. /**
  883. * Removes a row and all the components in the row.
  884. *
  885. * <p>
  886. * Components which span over several rows are removed if the selected row
  887. * is on the first row of such a component.
  888. * </p>
  889. *
  890. * <p>
  891. * If the last row is removed then all remaining components will be removed
  892. * and the grid will be reduced to one row. The cursor will be moved to the
  893. * upper left cell of the grid.
  894. * </p>
  895. *
  896. * @param row
  897. * Index of the row to remove. The leftmost row has index 0.
  898. */
  899. public void removeRow(int row) {
  900. if (row >= getRows()) {
  901. throw new IllegalArgumentException("Cannot delete row " + row
  902. + " from a gridlayout with height " + getRows());
  903. }
  904. // Remove all components in row
  905. for (int col = 0; col < getColumns(); col++) {
  906. removeComponent(col, row);
  907. }
  908. // Shrink or remove areas in the selected row
  909. for (ChildComponentData existingArea : getState().childData.values()) {
  910. if (existingArea.row2 >= row) {
  911. existingArea.row2--;
  912. if (existingArea.row1 > row) {
  913. existingArea.row1--;
  914. }
  915. }
  916. }
  917. if (getRows() == 1) {
  918. /*
  919. * Removing the last row means that the dimensions of the Grid
  920. * layout will be truncated to 1 empty row and the cursor is moved
  921. * to the first cell
  922. */
  923. cursorX = 0;
  924. cursorY = 0;
  925. } else {
  926. setRows(getRows() - 1);
  927. if (cursorY > row) {
  928. cursorY--;
  929. }
  930. }
  931. markAsDirty();
  932. }
  933. /**
  934. * Sets the expand ratio of given column.
  935. *
  936. * <p>
  937. * The expand ratio defines how excess space is distributed among columns.
  938. * Excess space means space that is left over from components that are not
  939. * sized relatively. By default, the excess space is distributed evenly.
  940. * </p>
  941. *
  942. * <p>
  943. * Note, that width of this GridLayout needs to be defined (fixed or
  944. * relative, as opposed to undefined height) for this method to have any
  945. * effect.
  946. * <p>
  947. * Note that checking for relative width for the child components is done on
  948. * the server so you cannot set a child component to have undefined width on
  949. * the server and set it to <code>100%</code> in CSS. You must set it to
  950. * <code>100%</code> on the server.
  951. *
  952. * @see #setWidth(float, Unit)
  953. *
  954. * @param columnIndex
  955. * @param ratio
  956. */
  957. public void setColumnExpandRatio(int columnIndex, float ratio) {
  958. columnExpandRatio.put(columnIndex, ratio);
  959. getState().explicitColRatios.add(columnIndex);
  960. markAsDirty();
  961. }
  962. /**
  963. * Returns the expand ratio of given column.
  964. *
  965. * @see #setColumnExpandRatio(int, float)
  966. *
  967. * @param columnIndex
  968. * @return the expand ratio, 0.0f by default
  969. */
  970. public float getColumnExpandRatio(int columnIndex) {
  971. Float r = columnExpandRatio.get(columnIndex);
  972. return r == null ? 0 : r.floatValue();
  973. }
  974. /**
  975. * Sets the expand ratio of given row.
  976. *
  977. * <p>
  978. * Expand ratio defines how excess space is distributed among rows. Excess
  979. * space means the space left over from components that are not sized
  980. * relatively. By default, the excess space is distributed evenly.
  981. * </p>
  982. *
  983. * <p>
  984. * Note, that height of this GridLayout needs to be defined (fixed or
  985. * relative, as opposed to undefined height) for this method to have any
  986. * effect.
  987. * <p>
  988. * Note that checking for relative height for the child components is done
  989. * on the server so you cannot set a child component to have undefined
  990. * height on the server and set it to <code>100%</code> in CSS. You must set
  991. * it to <code>100%</code> on the server.
  992. *
  993. * @see #setHeight(float, Unit)
  994. *
  995. * @param rowIndex
  996. * The row index, starting from 0 for the topmost row.
  997. * @param ratio
  998. */
  999. public void setRowExpandRatio(int rowIndex, float ratio) {
  1000. rowExpandRatio.put(rowIndex, ratio);
  1001. getState().explicitRowRatios.add(rowIndex);
  1002. markAsDirty();
  1003. }
  1004. /**
  1005. * Returns the expand ratio of given row.
  1006. *
  1007. * @see #setRowExpandRatio(int, float)
  1008. *
  1009. * @param rowIndex
  1010. * The row index, starting from 0 for the topmost row.
  1011. * @return the expand ratio, 0.0f by default
  1012. */
  1013. public float getRowExpandRatio(int rowIndex) {
  1014. Float r = rowExpandRatio.get(rowIndex);
  1015. return r == null ? 0 : r.floatValue();
  1016. }
  1017. /**
  1018. * Gets the Component at given index.
  1019. *
  1020. * @param x
  1021. * The column index, starting from 0 for the leftmost column.
  1022. * @param y
  1023. * The row index, starting from 0 for the topmost row.
  1024. * @return Component in given cell or null if empty
  1025. */
  1026. public Component getComponent(int x, int y) {
  1027. for (Entry<Connector, ChildComponentData> entry : getState(
  1028. false).childData.entrySet()) {
  1029. ChildComponentData childData = entry.getValue();
  1030. if (childData.column1 <= x && x <= childData.column2
  1031. && childData.row1 <= y && y <= childData.row2) {
  1032. return (Component) entry.getKey();
  1033. }
  1034. }
  1035. return null;
  1036. }
  1037. /**
  1038. * Returns information about the area where given component is laid in the
  1039. * GridLayout.
  1040. *
  1041. * @param component
  1042. * the component whose area information is requested.
  1043. * @return an Area object that contains information how component is laid in
  1044. * the grid
  1045. */
  1046. public Area getComponentArea(Component component) {
  1047. ChildComponentData childComponentData = getState(false).childData
  1048. .get(component);
  1049. if (childComponentData == null) {
  1050. return null;
  1051. } else {
  1052. return new Area(childComponentData, component);
  1053. }
  1054. }
  1055. @Override
  1056. public Registration addLayoutClickListener(LayoutClickListener listener) {
  1057. return addListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER,
  1058. LayoutClickEvent.class, listener,
  1059. LayoutClickListener.clickMethod);
  1060. }
  1061. @Override
  1062. @Deprecated
  1063. public void removeLayoutClickListener(LayoutClickListener listener) {
  1064. removeListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER,
  1065. LayoutClickEvent.class, listener);
  1066. }
  1067. /*
  1068. * (non-Javadoc)
  1069. *
  1070. * @see com.vaadin.ui.Layout.MarginHandler#setMargin(boolean)
  1071. */
  1072. @Override
  1073. public void setMargin(boolean enabled) {
  1074. setMargin(new MarginInfo(enabled));
  1075. }
  1076. /*
  1077. * (non-Javadoc)
  1078. *
  1079. * @see com.vaadin.ui.Layout.MarginHandler#setMargin(com.vaadin.shared.ui.
  1080. * MarginInfo )
  1081. */
  1082. @Override
  1083. public void setMargin(MarginInfo marginInfo) {
  1084. getState().marginsBitmask = marginInfo.getBitMask();
  1085. }
  1086. /*
  1087. * (non-Javadoc)
  1088. *
  1089. * @see com.vaadin.ui.Layout.MarginHandler#getMargin()
  1090. */
  1091. @Override
  1092. public MarginInfo getMargin() {
  1093. return new MarginInfo(getState(false).marginsBitmask);
  1094. }
  1095. /*
  1096. * (non-Javadoc)
  1097. *
  1098. * @see com.vaadin.ui.Layout.AlignmentHandler#getDefaultComponentAlignment()
  1099. */
  1100. @Override
  1101. public Alignment getDefaultComponentAlignment() {
  1102. return defaultComponentAlignment;
  1103. }
  1104. /*
  1105. * (non-Javadoc)
  1106. *
  1107. * @see
  1108. * com.vaadin.ui.Layout.AlignmentHandler#setDefaultComponentAlignment(com
  1109. * .vaadin.ui.Alignment)
  1110. */
  1111. @Override
  1112. public void setDefaultComponentAlignment(Alignment defaultAlignment) {
  1113. defaultComponentAlignment = defaultAlignment;
  1114. }
  1115. /**
  1116. * Sets whether empty rows and columns should be considered as non-existent
  1117. * when rendering or not. If this is set to true then the spacing between
  1118. * multiple empty columns (or rows) will be collapsed.
  1119. *
  1120. * The default behavior is to consider all rows and columns as visible
  1121. *
  1122. * NOTE that this must be set before the initial rendering takes place.
  1123. * Updating this on the fly is not supported.
  1124. *
  1125. * @since 7.3
  1126. * @param hideEmptyRowsAndColumns
  1127. * true to hide empty rows and columns, false to leave them as-is
  1128. */
  1129. public void setHideEmptyRowsAndColumns(boolean hideEmptyRowsAndColumns) {
  1130. getState().hideEmptyRowsAndColumns = hideEmptyRowsAndColumns;
  1131. }
  1132. /**
  1133. * Checks whether whether empty rows and columns should be considered as
  1134. * non-existent when rendering or not.
  1135. *
  1136. * @see #setHideEmptyRowsAndColumns(boolean)
  1137. * @since 7.3
  1138. * @return true if empty rows and columns are hidden, false otherwise
  1139. */
  1140. public boolean isHideEmptyRowsAndColumns() {
  1141. return getState(false).hideEmptyRowsAndColumns;
  1142. }
  1143. /**
  1144. * {@inheritDoc}
  1145. * <p>
  1146. * After reading the design, cursorY is set to point to a row outside of the
  1147. * GridLayout area. CursorX is reset to 0.
  1148. */
  1149. @Override
  1150. public void readDesign(Element design, DesignContext designContext) {
  1151. super.readDesign(design, designContext);
  1152. setMargin(readMargin(design, getMargin(), designContext));
  1153. if (design.childNodeSize() > 0) {
  1154. // Touch content only if there is some content specified. This is
  1155. // needed to be able to use extended GridLayouts which add
  1156. // components in the constructor (e.g. Designs based on GridLayout).
  1157. readChildComponents(design.children(), designContext);
  1158. }
  1159. // Set cursor position explicitly
  1160. setCursorY(getRows());
  1161. setCursorX(0);
  1162. }
  1163. private void readChildComponents(Elements childElements,
  1164. DesignContext designContext) {
  1165. List<Element> rowElements = new ArrayList<>();
  1166. List<Map<Integer, Component>> rows = new ArrayList<>();
  1167. // Prepare a 2D map for reading column contents
  1168. for (Element e : childElements) {
  1169. if (e.tagName().equalsIgnoreCase("row")) {
  1170. rowElements.add(e);
  1171. rows.add(new HashMap<>());
  1172. }
  1173. }
  1174. setRows(Math.max(rows.size(), 1));
  1175. Map<Component, Alignment> alignments = new HashMap<>();
  1176. List<Float> columnExpandRatios = new ArrayList<>();
  1177. for (int row = 0; row < rowElements.size(); ++row) {
  1178. Element rowElement = rowElements.get(row);
  1179. // Row Expand
  1180. if (rowElement.hasAttr("expand")) {
  1181. float expand = DesignAttributeHandler.readAttribute("expand",
  1182. rowElement.attributes(), float.class);
  1183. setRowExpandRatio(row, expand);
  1184. }
  1185. Elements cols = rowElement.children();
  1186. // Amount of skipped columns due to spanned components
  1187. int skippedColumns = 0;
  1188. for (int column = 0; column < cols.size(); ++column) {
  1189. while (rows.get(row).containsKey(column + skippedColumns)) {
  1190. // Skip any spanned components
  1191. skippedColumns++;
  1192. }
  1193. Element col = cols.get(column);
  1194. Component child = null;
  1195. if (!col.children().isEmpty()) {
  1196. Element childElement = col.child(0);
  1197. child = designContext.readDesign(childElement);
  1198. alignments.put(child, DesignAttributeHandler
  1199. .readAlignment(childElement.attributes()));
  1200. // TODO: Currently ignoring any extra children.
  1201. // Needs Error handling?
  1202. } // Else: Empty placeholder. No child component.
  1203. // Handle rowspan and colspan for this child component
  1204. Attributes attr = col.attributes();
  1205. int colspan = DesignAttributeHandler.readAttribute("colspan",
  1206. attr, 1, int.class);
  1207. int rowspan = DesignAttributeHandler.readAttribute("rowspan",
  1208. attr, 1, int.class);
  1209. for (int rowIndex = row; rowIndex < row + rowspan; ++rowIndex) {
  1210. for (int colIndex = column; colIndex < column
  1211. + colspan; ++colIndex) {
  1212. if (rowIndex == rows.size()) {
  1213. // Rowspan with not enough rows. Fix by adding rows.
  1214. rows.add(new HashMap<>());
  1215. }
  1216. rows.get(rowIndex).put(colIndex + skippedColumns,
  1217. child);
  1218. }
  1219. }
  1220. // Read column expand ratios if handling the first row.
  1221. if (row == 0) {
  1222. if (col.hasAttr("expand")) {
  1223. for (String expand : col.attr("expand").split(",")) {
  1224. columnExpandRatios.add(Float.parseFloat(expand));
  1225. }
  1226. } else {
  1227. for (int c = 0; c < colspan; ++c) {
  1228. columnExpandRatios.add(0f);
  1229. }
  1230. }
  1231. }
  1232. skippedColumns += (colspan - 1);
  1233. }
  1234. }
  1235. // Calculate highest column count and set columns
  1236. int colMax = 0;
  1237. for (Map<Integer, Component> cols : rows) {
  1238. if (colMax < cols.size()) {
  1239. colMax = cols.size();
  1240. }
  1241. }
  1242. setColumns(Math.max(colMax, 1));
  1243. for (int i = 0; i < columnExpandRatios.size(); ++i) {
  1244. setColumnExpandRatio(i, columnExpandRatios.get(i));
  1245. }
  1246. // Reiterate through the 2D map and add components to GridLayout
  1247. Set<Component> visited = new HashSet<>();
  1248. // Ignore any missing components
  1249. visited.add(null);
  1250. for (int i = 0; i < rows.size(); ++i) {
  1251. Map<Integer, Component> row = rows.get(i);
  1252. for (int j = 0; j < colMax; ++j) {
  1253. Component child = row.get(j);
  1254. if (visited.contains(child)) {
  1255. // Empty location or already handled child
  1256. continue;
  1257. }
  1258. visited.add(child);
  1259. // Figure out col and rowspan from 2D map
  1260. int colspan = 0;
  1261. while (j + colspan + 1 < row.size()
  1262. && row.get(j + colspan + 1) == child) {
  1263. ++colspan;
  1264. }
  1265. int rowspan = 0;
  1266. while (i + rowspan + 1 < rows.size()
  1267. && rows.get(i + rowspan + 1).get(j) == child) {
  1268. ++rowspan;
  1269. }
  1270. // Add component with area
  1271. addComponent(child, j, i, j + colspan, i + rowspan);
  1272. setComponentAlignment(child, alignments.get(child));
  1273. }
  1274. }
  1275. }
  1276. @Override
  1277. public void writeDesign(Element design, DesignContext designContext) {
  1278. super.writeDesign(design, designContext);
  1279. GridLayout def = designContext.getDefaultInstance(this);
  1280. writeMargin(design, getMargin(), def.getMargin(), designContext);
  1281. if (!designContext.shouldWriteChildren(this, def)) {
  1282. return;
  1283. }
  1284. if (components.isEmpty()) {
  1285. writeEmptyColsAndRows(design, designContext);
  1286. return;
  1287. }
  1288. final Map<Connector, ChildComponentData> childData = getState().childData;
  1289. // Make a 2D map of component areas.
  1290. Component[][] componentMap = new Component[getState().rows][getState().columns];
  1291. final Component dummyComponent = new Label("");
  1292. for (Component component : components) {
  1293. ChildComponentData coords = childData.get(component);
  1294. for (int row = coords.row1; row <= coords.row2; ++row) {
  1295. for (int col = coords.column1; col <= coords.column2; ++col) {
  1296. componentMap[row][col] = component;
  1297. }
  1298. }
  1299. }
  1300. // Go through the map and write only needed column tags
  1301. Set<Connector> visited = new HashSet<>();
  1302. // Skip the dummy placeholder
  1303. visited.add(dummyComponent);
  1304. for (int i = 0; i < componentMap.length; ++i) {
  1305. Element row = design.appendElement("row");
  1306. // Row Expand
  1307. DesignAttributeHandler.writeAttribute("expand", row.attributes(),
  1308. getRowExpandRatio(i), 0.0f, float.class, designContext);
  1309. int colspan = 1;
  1310. Element col;
  1311. for (int j = 0; j < componentMap[i].length; ++j) {
  1312. Component child = componentMap[i][j];
  1313. if (child != null) {
  1314. if (visited.contains(child)) {
  1315. // Child has already been written in the design
  1316. continue;
  1317. }
  1318. visited.add(child);
  1319. Element childElement = designContext.createElement(child);
  1320. col = row.appendElement("column");
  1321. // Write child data into design
  1322. ChildComponentData coords = childData.get(child);
  1323. Alignment alignment = getComponentAlignment(child);
  1324. DesignAttributeHandler.writeAlignment(childElement,
  1325. alignment);
  1326. col.appendChild(childElement);
  1327. if (coords.row1 != coords.row2) {
  1328. col.attr("rowspan",
  1329. "" + (1 + coords.row2 - coords.row1));
  1330. }
  1331. colspan = 1 + coords.column2 - coords.column1;
  1332. if (colspan > 1) {
  1333. col.attr("colspan", "" + colspan);
  1334. }
  1335. } else {
  1336. boolean hasExpands = false;
  1337. if (i == 0 && lastComponentOnRow(componentMap[i], j,
  1338. visited)) {
  1339. // A column with expand and no content in the end of
  1340. // first row needs to be present.
  1341. for (int c = j; c < componentMap[i].length; ++c) {
  1342. if (getColumnExpandRatio(c) > 0) {
  1343. hasExpands = true;
  1344. }
  1345. }
  1346. }
  1347. if (lastComponentOnRow(componentMap[i], j, visited)
  1348. && !hasExpands) {
  1349. continue;
  1350. }
  1351. // Empty placeholder tag.
  1352. col = row.appendElement("column");
  1353. // Use colspan to make placeholders more pleasant
  1354. while (j + colspan < componentMap[i].length
  1355. && componentMap[i][j + colspan] == child) {
  1356. ++colspan;
  1357. }
  1358. int rowspan = getRowSpan(componentMap, i, j, colspan,
  1359. child);
  1360. if (colspan > 1) {
  1361. col.attr("colspan", "" + colspan);
  1362. }
  1363. if (rowspan > 1) {
  1364. col.attr("rowspan", "" + rowspan);
  1365. }
  1366. for (int x = 0; x < rowspan; ++x) {
  1367. for (int y = 0; y < colspan; ++y) {
  1368. // Mark handled columns
  1369. componentMap[i + x][j + y] = dummyComponent;
  1370. }
  1371. }
  1372. }
  1373. // Column expands
  1374. if (i == 0) {
  1375. // Only do expands on first row
  1376. String expands = "";
  1377. boolean expandRatios = false;
  1378. for (int c = 0; c < colspan; ++c) {
  1379. float colExpand = getColumnExpandRatio(j + c);
  1380. if (colExpand > 0) {
  1381. expandRatios = true;
  1382. }
  1383. expands += (c > 0 ? "," : "") + colExpand;
  1384. }
  1385. if (expandRatios) {
  1386. col.attr("expand", expands);
  1387. }
  1388. }
  1389. j += colspan - 1;
  1390. }
  1391. }
  1392. }
  1393. /**
  1394. * Fills in the design with rows and empty columns. This needs to be done
  1395. * for empty {@link GridLayout}, because there's no other way to serialize
  1396. * info about number of columns and rows if there are absolutely no
  1397. * components in the {@link GridLayout}
  1398. *
  1399. * @param design
  1400. * @param designContext
  1401. */
  1402. private void writeEmptyColsAndRows(Element design,
  1403. DesignContext designContext) {
  1404. int rowCount = getState(false).rows;
  1405. int colCount = getState(false).columns;
  1406. // only write cols and rows tags if size is not 1x1
  1407. if (rowCount == 1 && colCount == 1) {
  1408. return;
  1409. }
  1410. for (int i = 0; i < rowCount; i++) {
  1411. Element row = design.appendElement("row");
  1412. for (int j = 0; j < colCount; j++) {
  1413. row.appendElement("column");
  1414. }
  1415. }
  1416. }
  1417. private int getRowSpan(Component[][] compMap, int i, int j, int colspan,
  1418. Component child) {
  1419. int rowspan = 1;
  1420. while (i + rowspan < compMap.length
  1421. && compMap[i + rowspan][j] == child) {
  1422. for (int k = 0; k < colspan; ++k) {
  1423. if (compMap[i + rowspan][j + k] != child) {
  1424. return rowspan;
  1425. }
  1426. }
  1427. rowspan++;
  1428. }
  1429. return rowspan;
  1430. }
  1431. private boolean lastComponentOnRow(Component[] componentArray, int j,
  1432. Set<Connector> visited) {
  1433. while ((++j) < componentArray.length) {
  1434. Component child = componentArray[j];
  1435. if (child != null && !visited.contains(child)) {
  1436. return false;
  1437. }
  1438. }
  1439. return true;
  1440. }
  1441. @Override
  1442. protected Collection<String> getCustomAttributes() {
  1443. Collection<String> result = super.getCustomAttributes();
  1444. result.add("cursor-x");
  1445. result.add("cursor-y");
  1446. result.add("rows");
  1447. result.add("columns");
  1448. result.add("margin");
  1449. result.add("margin-left");
  1450. result.add("margin-right");
  1451. result.add("margin-top");
  1452. result.add("margin-bottom");
  1453. return result;
  1454. }
  1455. }