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 /** Flag which controls whether the edit option is enabled. */ 174 private boolean m_editEnabled; 175 176 private I_EmptyHandler m_emptyHandler; 177 178 /** The error label. */ 179 private Label m_errorLabel = new Label(); 180 181 /** The error listener. */ 182 private Listener m_errorListener; 183 184 /** The error message. */ 185 private String m_errorMessage; 186 187 /**Should the add option be hidden?*/ 188 private boolean m_hideAdd; 189 190 /** Factory for creating new input fields. */ 191 private Supplier<Component> m_newComponentFactory; 192 193 /** The builder to use for creating new rows. */ 194 private I_RowBuilder m_rowBuilder = new DefaultRowBuilder(); 195 196 /** The row caption. */ 197 private String m_rowCaption; 198 199 /** 200 * Creates a new instance.<p> 201 * 202 * @param container the container in which to render the individual rows 203 * @param componentFactory the factory used to create new input fields 204 * @param placeholder the placeholder to display when there are no rows 205 */ 206 public CmsEditableGroup( 207 AbstractOrderedLayout container, 208 Supplier<Component> componentFactory, 209 I_EmptyHandler emptyHandler) { 210 211 m_hideAdd = false; 212 m_emptyHandler = emptyHandler; 213 m_container = container; 214 m_newComponentFactory = componentFactory; 215 m_emptyHandler = emptyHandler; 216 m_emptyHandler.init(this); 217 m_errorListener = new Listener() { 218 219 private static final long serialVersionUID = 1L; 220 221 @SuppressWarnings("synthetic-access") 222 public void componentEvent(Event event) { 223 224 if (event instanceof ErrorEvent) { 225 updateGroupValidation(); 226 } 227 } 228 }; 229 m_errorLabel.setValue(m_errorMessage); 230 m_errorLabel.addStyleName("o-editablegroup-errorlabel"); 231 setErrorVisible(false); 232 } 233 234 /** 235 * Creates a new instance.<p> 236 * 237 * @param container the container in which to render the individual rows 238 * @param componentFactory the factory used to create new input fields 239 * @param addButtonCaption the caption for the button which is used to add a new row to an empty list 240 */ 241 public CmsEditableGroup( 242 AbstractOrderedLayout container, 243 Supplier<Component> componentFactory, 244 String addButtonCaption) { 245 246 this(container, componentFactory, new AddButtonEmptyHandler(addButtonCaption)); 247 } 248 249 /** 250 * Adds a row for the given component at the end of the group. 251 * 252 * @param component the component to wrap in the row to be added 253 */ 254 public void addRow(Component component) { 255 256 Component actualComponent = component == null ? m_newComponentFactory.get() : component; 257 I_CmsEditableGroupRow row = m_rowBuilder.buildRow(this, actualComponent); 258 m_container.addComponent(row); 259 updatePlaceholder(); 260 updateButtonBars(); 261 updateGroupValidation(); 262 } 263 264 /** 265 * Adds a new row after the given one. 266 * 267 * @param row the row after which a new one should be added 268 */ 269 public void addRowAfter(I_CmsEditableGroupRow row) { 270 271 int index = m_container.getComponentIndex(row); 272 if (index >= 0) { 273 Component component = m_newComponentFactory.get(); 274 I_CmsEditableGroupRow newRow = m_rowBuilder.buildRow(this, component); 275 m_container.addComponent(newRow, index + 1); 276 } 277 updatePlaceholder(); 278 updateButtonBars(); 279 updateGroupValidation(); 280 } 281 282 /** 283 * Gets the row container. 284 * 285 * @return the row container 286 */ 287 public AbstractOrderedLayout getContainer() { 288 289 return m_container; 290 } 291 292 /** 293 * Gets the error listener. 294 * 295 * @return t 296 */ 297 public Listener getErrorListener() { 298 299 return m_errorListener; 300 } 301 302 /** 303 * Gets the factory used for creating new components. 304 * 305 * @return the factory used for creating new components 306 */ 307 public Supplier<Component> getNewComponentFactory() { 308 309 return m_newComponentFactory; 310 } 311 312 /** 313 * Returns the row caption.<p> 314 * 315 * @return the row caption 316 */ 317 public String getRowCaption() { 318 319 return m_rowCaption; 320 } 321 322 /** 323 * Gets all rows. 324 * 325 * @return the list of all rows 326 */ 327 public List<I_CmsEditableGroupRow> getRows() { 328 329 List<I_CmsEditableGroupRow> result = Lists.newArrayList(); 330 for (Component component : m_container) { 331 if (component instanceof I_CmsEditableGroupRow) { 332 result.add((I_CmsEditableGroupRow)component); 333 } 334 } 335 return result; 336 } 337 338 /** 339 * Initializes the multivalue group.<p> 340 */ 341 public void init() { 342 343 m_container.removeAllComponents(); 344 m_container.addComponent(m_errorLabel); 345 updatePlaceholder(); 346 } 347 348 /** 349 * Moves the given row down. 350 * 351 * @param row the row to move 352 */ 353 public void moveDown(I_CmsEditableGroupRow row) { 354 355 int index = m_container.getComponentIndex(row); 356 if ((index >= 0) && (index < (m_container.getComponentCount() - 1))) { 357 m_container.removeComponent(row); 358 m_container.addComponent(row, index + 1); 359 } 360 updateButtonBars(); 361 } 362 363 /** 364 * Moves the given row up. 365 * 366 * @param row the row to move 367 */ 368 public void moveUp(I_CmsEditableGroupRow row) { 369 370 int index = m_container.getComponentIndex(row); 371 if (index > 0) { 372 m_container.removeComponent(row); 373 m_container.addComponent(row, index - 1); 374 } 375 updateButtonBars(); 376 } 377 378 public void onEdit(I_CmsEditableGroupRow row) { 379 380 } 381 382 /** 383 * Removes the given row. 384 * 385 * @param row the row to remove 386 */ 387 public void remove(I_CmsEditableGroupRow row) { 388 389 int index = m_container.getComponentIndex(row); 390 if (index >= 0) { 391 m_container.removeComponent(row); 392 } 393 updatePlaceholder(); 394 updateButtonBars(); 395 updateGroupValidation(); 396 } 397 398 public void removeAll() { 399 400 m_container.removeAllComponents(); 401 updatePlaceholder(); 402 updateButtonBars(); 403 updateGroupValidation(); 404 405 } 406 407 /** 408 * @see org.opencms.ui.components.editablegroup.I_CmsEditableGroup#setAddButtonVisible(boolean) 409 */ 410 public void setAddButtonVisible(boolean visible) { 411 412 m_hideAdd = !visible; 413 414 } 415 416 /** 417 * Enables / disables edit button. 418 * 419 * @param enabled true if edit button should be enabled 420 */ 421 public void setEditEnabled(boolean enabled) { 422 423 m_editEnabled = enabled; 424 } 425 426 /** 427 * Sets the error message.<p> 428 * 429 * @param errorMessage the error message 430 */ 431 public void setErrorMessage(String errorMessage) { 432 433 m_errorMessage = errorMessage; 434 m_errorLabel.setValue(errorMessage != null ? errorMessage : ""); 435 } 436 437 /** 438 * Sets the row builder. 439 * 440 * @param rowBuilder the row builder 441 */ 442 public void setRowBuilder(I_RowBuilder rowBuilder) { 443 444 m_rowBuilder = rowBuilder; 445 } 446 447 /** 448 * Sets the row caption.<p> 449 * 450 * @param rowCaption the row caption to set 451 */ 452 public void setRowCaption(String rowCaption) { 453 454 m_rowCaption = rowCaption; 455 } 456 457 /** 458 * Checks if the given group component has an error.<p> 459 * 460 * @param component the component to check 461 * @return true if the component has an error 462 */ 463 protected boolean hasError(Component component) { 464 465 if (component instanceof AbstractComponent) { 466 if (((AbstractComponent)component).getComponentError() != null) { 467 return true; 468 } 469 } 470 if (component instanceof I_HasError) { 471 if (((I_HasError)component).hasEditableGroupError()) { 472 return true; 473 } 474 475 } 476 return false; 477 } 478 479 /** 480 * Shows or hides the error label.<p> 481 * 482 * @param hasError true if we have an error 483 */ 484 private void setErrorVisible(boolean hasError) { 485 486 m_errorLabel.setVisible(hasError && (m_errorMessage != null)); 487 } 488 489 /** 490 * Updates the button bars.<p> 491 */ 492 private void updateButtonBars() { 493 494 List<I_CmsEditableGroupRow> rows = getRows(); 495 int i = 0; 496 for (I_CmsEditableGroupRow row : rows) { 497 boolean first = i == 0; 498 boolean last = i == (rows.size() - 1); 499 row.getButtonBar().setFirstLast(first, last, m_hideAdd); 500 row.getButtonBar().getState().setEditEnabled(m_editEnabled); 501 i += 1; 502 } 503 } 504 505 /** 506 * Updates the visibility of the error label based on errors in the group components.<p> 507 */ 508 private void updateGroupValidation() { 509 510 boolean hasError = false; 511 for (I_CmsEditableGroupRow row : getRows()) { 512 if (hasError(row.getComponent())) { 513 hasError = true; 514 break; 515 } 516 } 517 setErrorVisible(hasError); 518 } 519 520 /** 521 * Updates the button visibility.<p> 522 */ 523 private void updatePlaceholder() { 524 525 boolean empty = getRows().size() == 0; 526 m_emptyHandler.setEmpty(empty); 527 528 } 529}