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.gwt.client.property; 029 030import org.opencms.gwt.client.Messages; 031import org.opencms.gwt.client.ui.CmsPopup; 032import org.opencms.gwt.client.ui.CmsScrollPanel; 033import org.opencms.gwt.client.ui.input.CmsTextArea; 034import org.opencms.gwt.client.ui.input.CmsTextBox; 035import org.opencms.gwt.client.ui.input.I_CmsFormWidget; 036import org.opencms.gwt.client.ui.input.I_CmsHasGhostValue; 037import org.opencms.gwt.client.ui.input.I_CmsStringModel; 038import org.opencms.gwt.client.ui.input.form.CmsBasicFormField; 039import org.opencms.gwt.client.util.CmsDebugLog; 040import org.opencms.gwt.client.util.CmsDomUtil; 041import org.opencms.gwt.client.util.CmsDomUtil.Style; 042import org.opencms.gwt.shared.CmsListInfoBean; 043import org.opencms.gwt.shared.property.CmsClientProperty; 044import org.opencms.gwt.shared.property.CmsClientProperty.Mode; 045import org.opencms.gwt.shared.property.CmsPathValue; 046import org.opencms.util.CmsPair; 047import org.opencms.util.CmsStringUtil; 048import org.opencms.util.CmsUUID; 049import org.opencms.xml.content.CmsXmlContentProperty; 050 051import java.util.ArrayList; 052import java.util.Collections; 053import java.util.HashMap; 054import java.util.List; 055import java.util.Map; 056 057import com.google.common.base.Joiner; 058import com.google.common.base.Objects; 059import com.google.common.base.Optional; 060import com.google.common.collect.BiMap; 061import com.google.common.collect.HashBiMap; 062import com.google.gwt.core.client.Scheduler; 063import com.google.gwt.core.client.Scheduler.RepeatingCommand; 064import com.google.gwt.dom.client.Element; 065import com.google.gwt.event.dom.client.FocusEvent; 066import com.google.gwt.event.dom.client.FocusHandler; 067import com.google.gwt.event.dom.client.HasFocusHandlers; 068import com.google.gwt.event.logical.shared.BeforeSelectionEvent; 069import com.google.gwt.event.logical.shared.BeforeSelectionHandler; 070import com.google.gwt.event.logical.shared.ValueChangeEvent; 071import com.google.gwt.event.logical.shared.ValueChangeHandler; 072import com.google.gwt.event.shared.EventBus; 073import com.google.gwt.event.shared.GwtEvent; 074import com.google.gwt.event.shared.HandlerRegistration; 075import com.google.gwt.event.shared.SimpleEventBus; 076import com.google.gwt.user.client.ui.Widget; 077 078/** 079 * The sitemap entry editor class for the VFS mode.<p> 080 * 081 * @since 8.0.0 082 */ 083public class CmsVfsModePropertyEditor extends A_CmsPropertyEditor { 084 085 /** The interval used for updating the height. */ 086 public static final int UPDATE_HEIGHT_INTERVAL = 200; 087 088 /** True when resizing of the dialog is disabled. */ 089 protected static boolean m_resizeDisabled; 090 091 /** The map of tab names. */ 092 private static BiMap<CmsClientProperty.Mode, String> tabs; 093 094 static { 095 tabs = HashBiMap.create(); 096 tabs.put(Mode.effective, CmsPropertyPanel.TAB_SIMPLE); 097 tabs.put(Mode.structure, CmsPropertyPanel.TAB_INDIVIDUAL); 098 tabs.put(Mode.resource, CmsPropertyPanel.TAB_SHARED); 099 } 100 101 /** The map of models of the fields. */ 102 Map<String, I_CmsStringModel> m_models = new HashMap<String, I_CmsStringModel>(); 103 104 /** Active field data. */ 105 private CmsActiveFieldData m_activeFieldData; 106 107 /** Field data which should be restored. */ 108 private CmsActiveFieldData m_fieldDataToBeRestored; 109 110 /** Flag to control whether the properties should be editable. */ 111 private boolean m_isReadOnly; 112 113 /** The previous tab index. */ 114 private int m_oldTabIndex = -1; 115 116 /** The panel used for editing the properties. */ 117 private CmsPropertyPanel m_panel; 118 119 /** The properties of the entry. */ 120 private Map<String, CmsClientProperty> m_properties; 121 122 /** Flag which indicates whether the resource properties should be editable. */ 123 private boolean m_showResourceProperties; 124 125 /** 126 * Creates a new sitemap entry editor instance for the VFS mode.<p> 127 * 128 * @param propConfig the property configuration 129 * @param handler the sitemap entry editor handler 130 */ 131 public CmsVfsModePropertyEditor(Map<String, CmsXmlContentProperty> propConfig, I_CmsPropertyEditorHandler handler) { 132 133 super(propConfig, handler); 134 m_properties = CmsClientProperty.makeLazyCopy(handler.getOwnProperties()); 135 } 136 137 /** 138 * Disables resizing.<p> 139 * 140 * @param disabled true if resizing should be disabled 141 */ 142 public static void disableResize(boolean disabled) { 143 144 m_resizeDisabled = disabled; 145 } 146 147 /** 148 * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#buildFields() 149 */ 150 @Override 151 public void buildFields() { 152 153 internalBuildConfiguredFields(); 154 internalBuildFields(Mode.structure); 155 if (m_showResourceProperties) { 156 internalBuildFields(Mode.resource); 157 } 158 } 159 160 /** 161 * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#createFormWidget(java.lang.String, java.util.Map, com.google.common.base.Optional) 162 */ 163 @Override 164 public I_CmsFormWidget createFormWidget( 165 String key, 166 Map<String, String> widgetParams, 167 Optional<String> defaultValue) { 168 169 I_CmsFormWidget widget = super.createFormWidget(key, widgetParams, defaultValue); 170 if (m_isReadOnly) { 171 widget.setEnabled(false); 172 } 173 return widget; 174 } 175 176 /** 177 * Focuses the file name field (delayed).<p> 178 */ 179 public void focusNameField() { 180 181 m_activeFieldData = new CmsActiveFieldData(null, "simple", A_CmsPropertyEditor.FIELD_URLNAME); 182 m_panel.tryToRestoreFieldData(m_activeFieldData); 183 184 } 185 186 /** 187 * Gets the active field data.<p> 188 * 189 * @return the active field data 190 */ 191 public CmsActiveFieldData getActiveFieldData() { 192 193 return m_activeFieldData; 194 } 195 196 /** 197 * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#initializeWidgets(org.opencms.gwt.client.ui.CmsPopup) 198 */ 199 @Override 200 public void initializeWidgets(final CmsPopup dialog) { 201 202 super.initializeWidgets(dialog); 203 dialog.setCaption(null); 204 dialog.removePadding(); 205 m_panel.tryToRestoreFieldData(m_fieldDataToBeRestored); 206 207 Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { 208 209 public boolean execute() { 210 211 if (!getPropertyPanel().getTabPanel().isAttached() || !getPropertyPanel().getTabPanel().isVisible()) { 212 return false; 213 } 214 if (!m_resizeDisabled) { 215 updateHeight(dialog); 216 } 217 return true; 218 } 219 }, UPDATE_HEIGHT_INTERVAL); 220 } 221 222 /** 223 * Sets the active field data to be restored.<p> 224 * 225 * @param fieldData the active field data to be restored 226 */ 227 public void restoreActiveFieldData(CmsActiveFieldData fieldData) { 228 229 m_fieldDataToBeRestored = fieldData; 230 231 } 232 233 /** 234 * Sets the "readonly" mode.<p> 235 * 236 * @param readonly if true, readonly mode will be enabled 237 */ 238 public void setReadOnly(boolean readonly) { 239 240 m_isReadOnly = readonly; 241 } 242 243 /** 244 * Sets the "show resource properties" flag which controls whether the resource value fields should be built.<p> 245 * 246 * @param showResourceProperties if true, the resource value fields will be build 247 */ 248 public void setShowResourceProperties(boolean showResourceProperties) { 249 250 m_showResourceProperties = showResourceProperties; 251 } 252 253 /** 254 * Returns the property panel.<p> 255 * 256 * @return the property panel 257 */ 258 protected CmsPropertyPanel getPropertyPanel() { 259 260 return m_panel; 261 } 262 263 /** 264 * Method which is called when the tab is switched.<p> 265 * 266 * @param toTab the tab to which the user is switching 267 */ 268 protected void handleSwitchTab(int toTab) { 269 270 switch (toTab) { 271 case 0: 272 rebuildSimpleTab(); 273 break; 274 case 1: 275 rebuildIndividualTab(); 276 break; 277 case 2: 278 rebuildSharedTab(); 279 break; 280 default: 281 break; 282 } 283 if ((m_disabledReason != null) && !m_nameOnlyDisabled) { 284 disableInput(m_disabledReason, m_nameOnlyDisabled); 285 } else if (m_nameOnlyDisabled) { 286 disableInput(m_disabledReason, true); 287 m_form.validateAllFields(); 288 } else { 289 m_form.validateAllFields(); 290 } 291 } 292 293 /** 294 * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#setupFieldContainer() 295 */ 296 @Override 297 protected void setupFieldContainer() { 298 299 CmsListInfoBean info = m_handler.getPageInfo(); 300 m_panel = new CmsPropertyPanel(m_showResourceProperties, info); 301 String modeClass = m_handler.getModeClass(); 302 if (modeClass != null) { 303 m_panel.addStyleName(modeClass); 304 } 305 m_panel.addBeforeSelectionHandler(new BeforeSelectionHandler<Integer>() { 306 307 public void onBeforeSelection(BeforeSelectionEvent<Integer> event) { 308 309 int target = event.getItem().intValue(); 310 handleSwitchTab(target); 311 } 312 }); 313 m_form.setWidget(m_panel); 314 315 } 316 317 /** 318 * Updates the panel height depending on the content of the current tab.<p> 319 * 320 * @param dialog the dialog for which the height should be updated 321 */ 322 protected void updateHeight(CmsPopup dialog) { 323 324 int tabIndex = m_panel.getTabPanel().getSelectedIndex(); 325 boolean changedTab = tabIndex != m_oldTabIndex; 326 m_oldTabIndex = tabIndex; 327 CmsScrollPanel tabWidget = m_panel.getTabPanel().getWidget(tabIndex); 328 Element innerElement = tabWidget.getWidget().getElement(); 329 int contentHeight = CmsDomUtil.getCurrentStyleInt(innerElement, Style.height); 330 int spaceLeft = dialog.getAvailableHeight(0); 331 int newHeight = Math.min(spaceLeft, contentHeight + 47); 332 boolean changedHeight = m_panel.getTabPanel().getOffsetHeight() != newHeight; 333 if (changedHeight || changedTab) { 334 m_panel.getTabPanel().setHeight(newHeight + "px"); 335 int selectedIndex = m_panel.getTabPanel().getSelectedIndex(); 336 CmsScrollPanel widget = m_panel.getTabPanel().getWidget(selectedIndex); 337 338 widget.setHeight((newHeight - 34) + "px"); 339 widget.onResizeDescendant(); 340 //dialog.center(); 341 } 342 } 343 344 /** 345 * Builds a single form field.<p> 346 * 347 * @param ownProps the entry's own properties 348 * @param propName the property name 349 * @param mode the mode which controls which kind of field will be built 350 */ 351 private void buildField( 352 Map<String, CmsClientProperty> ownProps, 353 final String propName, 354 CmsClientProperty.Mode mode) { 355 356 String entryId = m_handler.getId().toString(); 357 CmsXmlContentProperty propDef = m_propertyConfig.get(propName); 358 359 if (propDef == null) { 360 String widget = CmsClientProperty.PROPERTY_TEMPLATE.equals(propName) ? "template" : "string"; 361 propDef = new CmsXmlContentProperty( 362 propName, 363 "string", 364 widget, 365 "", 366 null, 367 null, 368 null, 369 null, 370 null, 371 null, 372 null); 373 } 374 375 if (mode != Mode.effective) { 376 propDef = propDef.withNiceName(propName); 377 } 378 379 CmsClientProperty ownProp = m_properties.get(propName); 380 CmsPathValue pathValue = CmsClientProperty.getPathValue(ownProp, mode).prepend(entryId + "/" + propName + "/"); 381 382 //CHECK: should fields other than NavText be really automatically allowed to be empty in navigation mode? 383 final String tab = tabs.get(mode); 384 final CmsBasicFormField field = CmsBasicFormField.createField( 385 propDef, 386 pathValue.getPath() + "#" + tab, 387 this, 388 Collections.<String, String> emptyMap(), 389 true); 390 391 CmsPair<String, String> defaultValueAndOrigin = getDefaultValueToDisplay(ownProp, mode); 392 String defaultValue = ""; 393 String origin = ""; 394 if (defaultValueAndOrigin != null) { 395 defaultValue = defaultValueAndOrigin.getFirst(); 396 origin = defaultValueAndOrigin.getSecond(); 397 } 398 final Widget w = (Widget)field.getWidget(); 399 I_CmsStringModel model = getStringModel(pathValue); 400 field.bind(model); 401 boolean ghost = CmsStringUtil.isEmptyOrWhitespaceOnly(pathValue.getValue()); 402 String initialValue = pathValue.getValue(); 403 if (w instanceof I_CmsHasGhostValue) { 404 ((I_CmsHasGhostValue)w).setGhostValue(defaultValue, ghost); 405 if (ghost) { 406 initialValue = null; 407 } 408 } 409 if (w instanceof HasFocusHandlers) { 410 try { 411 ((HasFocusHandlers)w).addFocusHandler(new FocusHandler() { 412 413 @SuppressWarnings("synthetic-access") 414 public void onFocus(FocusEvent event) { 415 416 if ((w instanceof CmsTextBox) || (w instanceof CmsTextArea)) { 417 m_activeFieldData = new CmsActiveFieldData(field, tab, propName); 418 } else { 419 // field received focus, but it doesn't make sense to restore it later 420 m_activeFieldData = null; 421 } 422 } 423 }); 424 } catch (Exception e) { 425 CmsDebugLog.consoleLog("" + e); 426 } 427 428 } 429 430 boolean isShowingGhost = ghost && !CmsStringUtil.isEmpty(defaultValue); 431 432 if (isShowingGhost) { 433 field.getLayoutData().put("info", origin); 434 } 435 if (!ghost || isShowingGhost) { 436 field.getLayoutData().put(CmsPropertyPanel.LD_DISPLAY_VALUE, "true"); 437 } 438 field.getLayoutData().put(CmsPropertyPanel.LD_PROPERTY, propName); 439 field.getLayoutData().put("tab", tab); 440 m_form.addField(tab, field, initialValue); 441 } 442 443 /** 444 * Creates a string model which uses a field of a CmsClientProperty for storing its value.<p> 445 * 446 * @param id the structure id 447 * @param propName the property id 448 * @param isStructure if true, the structure value field should be used, else the resource value field 449 * 450 * 451 * @return the new model object 452 */ 453 private I_CmsStringModel createStringModel(final CmsUUID id, final String propName, final boolean isStructure) { 454 455 final CmsClientProperty property = m_properties.get(propName); 456 457 return new I_CmsStringModel() { 458 459 private boolean m_active; 460 461 private EventBus m_eventBus = new SimpleEventBus(); 462 463 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) { 464 465 return m_eventBus.addHandler(ValueChangeEvent.getType(), handler); 466 } 467 468 /** 469 * @see com.google.gwt.event.shared.HasHandlers#fireEvent(com.google.gwt.event.shared.GwtEvent) 470 */ 471 public void fireEvent(GwtEvent<?> event) { 472 473 m_eventBus.fireEvent(event); 474 } 475 476 public String getId() { 477 478 return Joiner.on("/").join(id.toString(), propName, isStructure ? "S" : "R"); 479 } 480 481 public String getValue() { 482 483 if (isStructure) { 484 return property.getStructureValue(); 485 } else { 486 return property.getResourceValue(); 487 } 488 } 489 490 public void setValue(String value, boolean notify) { 491 492 if (!m_active) { 493 m_active = true; 494 try { 495 String oldValue = getValue(); 496 boolean changed = !Objects.equal(value, oldValue); 497 if (isStructure) { 498 property.setStructureValue(value); 499 } else { 500 property.setResourceValue(value); 501 } 502 if (notify && changed) { 503 ValueChangeEvent.fire(this, value); 504 } 505 } finally { 506 m_active = false; 507 } 508 } 509 } 510 }; 511 } 512 513 /** 514 * Gets a pair of strings containing the default value to display for a given property and its source.<p> 515 * 516 * @param prop the property 517 * @param mode the mode 518 * 519 * @return a pair of the form (defaultValue, origin) 520 */ 521 private CmsPair<String, String> getDefaultValueToDisplay(CmsClientProperty prop, Mode mode) { 522 523 if ((mode == Mode.structure) && !CmsStringUtil.isEmpty(prop.getResourceValue())) { 524 String message = Messages.get().key(Messages.GUI_ORIGIN_SHARED_0); 525 526 return CmsPair.create(prop.getResourceValue(), message); 527 } 528 CmsClientProperty inheritedProperty = m_handler.getInheritedProperty(prop.getName()); 529 if (CmsClientProperty.isPropertyEmpty(inheritedProperty)) { 530 return null; 531 } 532 CmsPathValue pathValue = inheritedProperty.getPathValue(mode); 533 String message = Messages.get().key(Messages.GUI_ORIGIN_INHERITED_1, inheritedProperty.getOrigin()); 534 return CmsPair.create(pathValue.getValue(), message); 535 } 536 537 /** 538 * Creates a string model for a given property path value, and returns the same model if the same path value is passed in.<p> 539 * 540 * @param pathValue the path value 541 * 542 * @return the model for that path value 543 */ 544 private I_CmsStringModel getStringModel(CmsPathValue pathValue) { 545 546 String path = pathValue.getPath(); 547 I_CmsStringModel model = m_models.get(path); 548 if (model == null) { 549 String[] tokens = path.split("/"); 550 String id = tokens[0]; 551 String propName = tokens[1]; 552 boolean isStructure = tokens[2].equals("S"); 553 model = createStringModel(new CmsUUID(id), propName, isStructure); 554 m_models.put(path, model); 555 } 556 return model; 557 } 558 559 /** 560 * Builds the fields for the configured properties in the first tab.<p> 561 */ 562 private void internalBuildConfiguredFields() { 563 564 Map<String, CmsClientProperty> ownProps = m_handler.getOwnProperties(); 565 List<String> keys = new ArrayList<String>(m_propertyConfig.keySet()); 566 for (String propName : keys) { 567 buildField(ownProps, propName, Mode.effective); 568 } 569 } 570 571 /** 572 * 573 * Builds the fields for a given mode.<p> 574 * 575 * @param mode the mode 576 */ 577 private void internalBuildFields(Mode mode) { 578 579 Map<String, CmsClientProperty> ownProps = m_handler.getOwnProperties(); 580 for (String propName : m_handler.getAllPropertyNames()) { 581 buildField(ownProps, propName, mode); 582 } 583 } 584 585 /** 586 * Moves the given property name to the top of the keys if present.<p> 587 * 588 * @param keys the list of keys 589 * @param propertyName the property name to move 590 */ 591 private void moveToTop(List<String> keys, String propertyName) { 592 593 if (keys.contains(propertyName)) { 594 keys.remove(propertyName); 595 keys.add(0, propertyName); 596 } 597 } 598 599 /** 600 * Rebuilds the "individual" tab.<p> 601 */ 602 private void rebuildIndividualTab() { 603 604 m_form.removeGroup(CmsPropertyPanel.TAB_INDIVIDUAL); 605 CmsPropertyPanel panel = ((CmsPropertyPanel)m_form.getWidget()); 606 panel.clearTab(CmsPropertyPanel.TAB_INDIVIDUAL); 607 internalBuildFields(Mode.structure); 608 m_form.renderGroup(CmsPropertyPanel.TAB_INDIVIDUAL); 609 } 610 611 /** 612 * Rebuilds the "shared" tab.<p> 613 */ 614 private void rebuildSharedTab() { 615 616 m_form.removeGroup(CmsPropertyPanel.TAB_SHARED); 617 CmsPropertyPanel panel = ((CmsPropertyPanel)m_form.getWidget()); 618 panel.clearTab(CmsPropertyPanel.TAB_SHARED); 619 internalBuildFields(Mode.resource); 620 m_form.renderGroup(CmsPropertyPanel.TAB_SHARED); 621 } 622 623 /** 624 * Rebuilds the simple tab.<p> 625 */ 626 private void rebuildSimpleTab() { 627 628 m_form.removeGroup(CmsPropertyPanel.TAB_SIMPLE); 629 CmsPropertyPanel panel = ((CmsPropertyPanel)m_form.getWidget()); 630 panel.clearTab(CmsPropertyPanel.TAB_SIMPLE); 631 if (m_handler.hasEditableName()) { 632 m_form.addField(CmsPropertyPanel.TAB_SIMPLE, createUrlNameField()); 633 } 634 internalBuildConfiguredFields(); 635 m_form.renderGroup(CmsPropertyPanel.TAB_SIMPLE); 636 } 637}