001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.ui.components.editablegroup; 029 030import java.util.List; 031 032import com.google.common.base.Supplier; 033import com.google.common.collect.Lists; 034import com.vaadin.ui.AbstractComponent; 035import com.vaadin.ui.AbstractOrderedLayout; 036import com.vaadin.ui.Button; 037import com.vaadin.ui.Component; 038import com.vaadin.ui.Component.ErrorEvent; 039import com.vaadin.ui.Component.Event; 040import com.vaadin.ui.Component.Listener; 041import com.vaadin.ui.Layout; 042import com.vaadin.v7.ui.Label; 043 044/** 045 * Manages a group of widgets used as a multivalue input.<p> 046 * 047 * This class is not itself a widget, it just coordinates the other widgets actually used to display the multivalue widget group. 048 */ 049public class CmsEditableGroup { 050 051 /** 052 * Empty handler which shows or hides an 'Add' button to add new rows, depending on whether the group is empty. 053 */ 054 public static class AddButtonEmptyHandler implements CmsEditableGroup.I_EmptyHandler { 055 056 /** The 'Add' button. */ 057 private Button m_addButton; 058 059 /** The group. */ 060 private CmsEditableGroup m_group; 061 062 /** 063 * Creates a new instance. 064 * 065 * @param addButtonText the text for the Add button 066 */ 067 public AddButtonEmptyHandler(String addButtonText) { 068 069 m_addButton = new Button(addButtonText); 070 m_addButton.addClickListener(evt -> { 071 Component component = m_group.getNewComponentFactory().get(); 072 m_group.addRow(component); 073 }); 074 } 075 076 /** 077 * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#init(org.opencms.ui.components.editablegroup.CmsEditableGroup) 078 */ 079 public void init(CmsEditableGroup group) { 080 081 m_group = group; 082 } 083 084 /** 085 * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#setEmpty(boolean) 086 */ 087 public void setEmpty(boolean empty) { 088 089 if (empty) { 090 m_group.getContainer().addComponent(m_addButton); 091 } else { 092 m_group.getContainer().removeComponent(m_addButton); 093 } 094 } 095 096 } 097 098 /** 099 * Default implementation for row builder. 100 */ 101 public static class DefaultRowBuilder implements CmsEditableGroup.I_RowBuilder { 102 103 public I_CmsEditableGroupRow buildRow(CmsEditableGroup group, Component component) { 104 105 if (component instanceof Layout.MarginHandler) { 106 // Since the row is a HorizontalLayout with the edit buttons positioned next to the original 107 // widget, a margin on the widget causes it to be vertically offset from the buttons too much 108 Layout.MarginHandler marginHandler = (Layout.MarginHandler)component; 109 marginHandler.setMargin(false); 110 } 111 if (component instanceof AbstractComponent) { 112 component.addListener(group.getErrorListener()); 113 } 114 I_CmsEditableGroupRow row = new CmsEditableGroupRow(group, component); 115 if (group.getRowCaption() != null) { 116 row.setCaption(group.getRowCaption()); 117 } 118 return row; 119 } 120 } 121 122 /** 123 * Handles state changes when the group becomes empty/not empty. 124 */ 125 public interface I_EmptyHandler { 126 127 /** 128 * Needs to be called initially with the group for which this is used. 129 * 130 * @param group the group 131 */ 132 public void init(CmsEditableGroup group); 133 134 /** 135 * Called when the group changes from empty to not empty, or vice versa. 136 * 137 * @param empty true if the group is empty 138 */ 139 public void setEmpty(boolean empty); 140 } 141 142 /** 143 * Interface for group row components that can have errors. 144 */ 145 public interface I_HasError { 146 147 /** 148 * Check if there is an error. 149 * 150 * @return true if there is an error 151 */ 152 public boolean hasEditableGroupError(); 153 } 154 155 /** 156 * Builds editable group rows by wrapping other components. 157 */ 158 public interface I_RowBuilder { 159 160 /** 161 * Builds a row for the given group by wrapping the given component. 162 * 163 * @param group the group 164 * @param component the component 165 * @return the new row 166 */ 167 public I_CmsEditableGroupRow buildRow(CmsEditableGroup group, Component component); 168 } 169 170 /** The container in which to render the individual rows of the multivalue widget group. */ 171 private AbstractOrderedLayout m_container; 172 173 private I_EmptyHandler m_emptyHandler; 174 175 /** The error label. */ 176 private Label m_errorLabel = new Label(); 177 178 /** The error listener. */ 179 private Listener m_errorListener; 180 181 /** The error message. */ 182 private String m_errorMessage; 183 184 /**Should the add option be hidden?*/ 185 private boolean m_hideAdd; 186 187 /** Factory for creating new input fields. */ 188 private Supplier<Component> m_newComponentFactory; 189 190 /** The builder to use for creating new rows. */ 191 private I_RowBuilder m_rowBuilder = new DefaultRowBuilder(); 192 193 /** The row caption. */ 194 private String m_rowCaption; 195 196 /** 197 * Creates a new instance.<p> 198 * 199 * @param container the container in which to render the individual rows 200 * @param componentFactory the factory used to create new input fields 201 * @param placeholder the placeholder to display when there are no rows 202 */ 203 public CmsEditableGroup( 204 AbstractOrderedLayout container, 205 Supplier<Component> componentFactory, 206 I_EmptyHandler emptyHandler) { 207 208 m_hideAdd = false; 209 m_emptyHandler = emptyHandler; 210 m_container = container; 211 m_newComponentFactory = componentFactory; 212 m_emptyHandler = emptyHandler; 213 m_emptyHandler.init(this); 214 m_errorListener = new Listener() { 215 216 private static final long serialVersionUID = 1L; 217 218 @SuppressWarnings("synthetic-access") 219 public void componentEvent(Event event) { 220 221 if (event instanceof ErrorEvent) { 222 updateGroupValidation(); 223 } 224 } 225 }; 226 m_errorLabel.setValue(m_errorMessage); 227 m_errorLabel.addStyleName("o-editablegroup-errorlabel"); 228 setErrorVisible(false); 229 } 230 231 /** 232 * Creates a new instance.<p> 233 * 234 * @param container the container in which to render the individual rows 235 * @param componentFactory the factory used to create new input fields 236 * @param addButtonCaption the caption for the button which is used to add a new row to an empty list 237 */ 238 public CmsEditableGroup( 239 AbstractOrderedLayout container, 240 Supplier<Component> componentFactory, 241 String addButtonCaption) { 242 243 this(container, componentFactory, new AddButtonEmptyHandler(addButtonCaption)); 244 } 245 246 /** 247 * Adds a row for the given component at the end of the group. 248 * 249 * @param component the component to wrap in the row to be added 250 */ 251 public void addRow(Component component) { 252 253 Component actualComponent = component == null ? m_newComponentFactory.get() : component; 254 I_CmsEditableGroupRow row = m_rowBuilder.buildRow(this, actualComponent); 255 m_container.addComponent(row); 256 updatePlaceholder(); 257 updateButtonBars(); 258 updateGroupValidation(); 259 } 260 261 /** 262 * Adds a new row after the given one. 263 * 264 * @param row the row after which a new one should be added 265 */ 266 public void addRowAfter(I_CmsEditableGroupRow row) { 267 268 int index = m_container.getComponentIndex(row); 269 if (index >= 0) { 270 Component component = m_newComponentFactory.get(); 271 I_CmsEditableGroupRow newRow = m_rowBuilder.buildRow(this, component); 272 m_container.addComponent(newRow, index + 1); 273 } 274 updatePlaceholder(); 275 updateButtonBars(); 276 updateGroupValidation(); 277 } 278 279 /** 280 * Gets the row container. 281 * 282 * @return the row container 283 */ 284 public AbstractOrderedLayout getContainer() { 285 286 return m_container; 287 } 288 289 /** 290 * Gets the error listener. 291 * 292 * @return t 293 */ 294 public Listener getErrorListener() { 295 296 return m_errorListener; 297 } 298 299 /** 300 * Gets the factory used for creating new components. 301 * 302 * @return the factory used for creating new components 303 */ 304 public Supplier<Component> getNewComponentFactory() { 305 306 return m_newComponentFactory; 307 } 308 309 /** 310 * Returns the row caption.<p> 311 * 312 * @return the row caption 313 */ 314 public String getRowCaption() { 315 316 return m_rowCaption; 317 } 318 319 /** 320 * Gets all rows. 321 * 322 * @return the list of all rows 323 */ 324 public List<I_CmsEditableGroupRow> getRows() { 325 326 List<I_CmsEditableGroupRow> result = Lists.newArrayList(); 327 for (Component component : m_container) { 328 if (component instanceof I_CmsEditableGroupRow) { 329 result.add((I_CmsEditableGroupRow)component); 330 } 331 } 332 return result; 333 } 334 335 /** 336 * Initializes the multivalue group.<p> 337 */ 338 public void init() { 339 340 m_container.removeAllComponents(); 341 m_container.addComponent(m_errorLabel); 342 updatePlaceholder(); 343 } 344 345 /** 346 * Moves the given row down. 347 * 348 * @param row the row to move 349 */ 350 public void moveDown(I_CmsEditableGroupRow row) { 351 352 int index = m_container.getComponentIndex(row); 353 if ((index >= 0) && (index < (m_container.getComponentCount() - 1))) { 354 m_container.removeComponent(row); 355 m_container.addComponent(row, index + 1); 356 } 357 updateButtonBars(); 358 } 359 360 /** 361 * Moves the given row up. 362 * 363 * @param row the row to move 364 */ 365 public void moveUp(I_CmsEditableGroupRow row) { 366 367 int index = m_container.getComponentIndex(row); 368 if (index > 0) { 369 m_container.removeComponent(row); 370 m_container.addComponent(row, index - 1); 371 } 372 updateButtonBars(); 373 } 374 375 /** 376 * Removes the given row. 377 * 378 * @param row the row to remove 379 */ 380 public void remove(I_CmsEditableGroupRow row) { 381 382 int index = m_container.getComponentIndex(row); 383 if (index >= 0) { 384 m_container.removeComponent(row); 385 } 386 updatePlaceholder(); 387 updateButtonBars(); 388 updateGroupValidation(); 389 } 390 391 /** 392 * @see org.opencms.ui.components.editablegroup.I_CmsEditableGroup#setAddButtonVisible(boolean) 393 */ 394 public void setAddButtonVisible(boolean visible) { 395 396 m_hideAdd = !visible; 397 398 } 399 400 /** 401 * Sets the error message.<p> 402 * 403 * @param errorMessage the error message 404 */ 405 public void setErrorMessage(String errorMessage) { 406 407 m_errorMessage = errorMessage; 408 m_errorLabel.setValue(errorMessage != null ? errorMessage : ""); 409 } 410 411 /** 412 * Sets the row builder. 413 * 414 * @param rowBuilder the row builder 415 */ 416 public void setRowBuilder(I_RowBuilder rowBuilder) { 417 418 m_rowBuilder = rowBuilder; 419 } 420 421 /** 422 * Sets the row caption.<p> 423 * 424 * @param rowCaption the row caption to set 425 */ 426 public void setRowCaption(String rowCaption) { 427 428 m_rowCaption = rowCaption; 429 } 430 431 /** 432 * Checks if the given group component has an error.<p> 433 * 434 * @param component the component to check 435 * @return true if the component has an error 436 */ 437 protected boolean hasError(Component component) { 438 439 if (component instanceof AbstractComponent) { 440 if (((AbstractComponent)component).getComponentError() != null) { 441 return true; 442 } 443 } 444 if (component instanceof I_HasError) { 445 if (((I_HasError)component).hasEditableGroupError()) { 446 return true; 447 } 448 449 } 450 return false; 451 } 452 453 /** 454 * Shows or hides the error label.<p> 455 * 456 * @param hasError true if we have an error 457 */ 458 private void setErrorVisible(boolean hasError) { 459 460 m_errorLabel.setVisible(hasError && (m_errorMessage != null)); 461 } 462 463 /** 464 * Updates the button bars.<p> 465 */ 466 private void updateButtonBars() { 467 468 List<I_CmsEditableGroupRow> rows = getRows(); 469 int i = 0; 470 for (I_CmsEditableGroupRow row : rows) { 471 boolean first = i == 0; 472 boolean last = i == (rows.size() - 1); 473 row.getButtonBar().setFirstLast(first, last, m_hideAdd); 474 i += 1; 475 } 476 } 477 478 /** 479 * Updates the visibility of the error label based on errors in the group components.<p> 480 */ 481 private void updateGroupValidation() { 482 483 boolean hasError = false; 484 for (I_CmsEditableGroupRow row : getRows()) { 485 if (hasError(row.getComponent())) { 486 hasError = true; 487 break; 488 } 489 } 490 setErrorVisible(hasError); 491 } 492 493 /** 494 * Updates the button visibility.<p> 495 */ 496 private void updatePlaceholder() { 497 498 boolean empty = getRows().size() == 0; 499 m_emptyHandler.setEmpty(empty); 500 501 } 502}