content: /*
   * This library is part of OpenCms -
   * the Open Source Content Management System
   *
   * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
   *
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
   * Lesser General Public License for more details.
   *
   * For further information about Alkacon Software, please see the
   * company website: https://www.alkacon.com
   *
   * For further information about OpenCms, please see the
   * project website: https://www.opencms.org
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package org.opencms.gwt.client.property;
  
  import org.opencms.gwt.client.Messages;
  import org.opencms.gwt.client.ui.CmsPopup;
  import org.opencms.gwt.client.ui.CmsScrollPanel;
  import org.opencms.gwt.client.ui.input.CmsTextArea;
  import org.opencms.gwt.client.ui.input.CmsTextBox;
  import org.opencms.gwt.client.ui.input.I_CmsFormWidget;
  import org.opencms.gwt.client.ui.input.I_CmsHasGhostValue;
  import org.opencms.gwt.client.ui.input.I_CmsStringModel;
  import org.opencms.gwt.client.ui.input.form.CmsBasicFormField;
  import org.opencms.gwt.client.util.CmsDebugLog;
  import org.opencms.gwt.client.util.CmsDomUtil;
  import org.opencms.gwt.client.util.CmsDomUtil.Style;
  import org.opencms.gwt.shared.CmsListInfoBean;
  import org.opencms.gwt.shared.property.CmsClientProperty;
  import org.opencms.gwt.shared.property.CmsClientProperty.Mode;
  import org.opencms.gwt.shared.property.CmsPathValue;
  import org.opencms.util.CmsPair;
  import org.opencms.util.CmsStringUtil;
  import org.opencms.util.CmsUUID;
  import org.opencms.xml.content.CmsXmlContentProperty;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import com.google.common.base.Joiner;
  import com.google.common.base.Objects;
  import com.google.common.base.Optional;
  import com.google.common.collect.BiMap;
  import com.google.common.collect.HashBiMap;
  import com.google.gwt.core.client.Scheduler;
  import com.google.gwt.core.client.Scheduler.RepeatingCommand;
  import com.google.gwt.dom.client.Element;
  import com.google.gwt.event.dom.client.FocusEvent;
  import com.google.gwt.event.dom.client.FocusHandler;
  import com.google.gwt.event.dom.client.HasFocusHandlers;
  import com.google.gwt.event.logical.shared.BeforeSelectionEvent;
  import com.google.gwt.event.logical.shared.BeforeSelectionHandler;
  import com.google.gwt.event.logical.shared.ValueChangeEvent;
  import com.google.gwt.event.logical.shared.ValueChangeHandler;
  import com.google.gwt.event.shared.EventBus;
  import com.google.gwt.event.shared.GwtEvent;
  import com.google.gwt.event.shared.HandlerRegistration;
  import com.google.gwt.event.shared.SimpleEventBus;
  import com.google.gwt.user.client.ui.Widget;
  
  /**
   * The sitemap entry editor class for the VFS mode.<p>
   *
   * @since 8.0.0
   */
  public class CmsVfsModePropertyEditor extends A_CmsPropertyEditor {
  
      /** The interval used for updating the height. */
      public static final int UPDATE_HEIGHT_INTERVAL = 70;
  
      /** True when resizing of the dialog is disabled. */
      protected static boolean m_resizeDisabled;
  
      /** The map of tab names. */
      private static BiMap<CmsClientProperty.Mode, String> tabs;
  
      static {
          tabs = HashBiMap.create();
          tabs.put(Mode.effective, CmsPropertyPanel.TAB_SIMPLE);
          tabs.put(Mode.structure, CmsPropertyPanel.TAB_INDIVIDUAL);
          tabs.put(Mode.resource, CmsPropertyPanel.TAB_SHARED);
      }
  
      /** The map of models of the fields. */
      Map<String, I_CmsStringModel> m_models = new HashMap<String, I_CmsStringModel>();
  
      /** Active field data. */
      private CmsActiveFieldData m_activeFieldData;
  
      /** Field data which should be restored. */
      private CmsActiveFieldData m_fieldDataToBeRestored;
  
      /** Flag to control whether the properties should be editable. */
      private boolean m_isReadOnly;
  
      /** The previous tab index. */
      private int m_oldTabIndex = -1;
  
      /** The panel used for editing the properties. */
      private CmsPropertyPanel m_panel;
  
      /** The properties of the entry. */
      private Map<String, CmsClientProperty> m_properties;
  
      /** Flag which indicates whether the resource properties should be editable. */
      private boolean m_showResourceProperties;
  
      /**
       * Creates a new sitemap entry editor instance for the VFS mode.<p>
       *
       * @param propConfig the property configuration
       * @param handler the sitemap entry editor handler
       */
      public CmsVfsModePropertyEditor(Map<String, CmsXmlContentProperty> propConfig, I_CmsPropertyEditorHandler handler) {
  
          super(propConfig, handler);
          m_properties = CmsClientProperty.makeLazyCopy(handler.getOwnProperties());
      }
  
      /**
       * Disables resizing.<p>
       *
       * @param disabled true if resizing should be disabled
       */
      public static void disableResize(boolean disabled) {
  
          m_resizeDisabled = disabled;
      }
  
      /**
       * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#buildFields()
       */
      @Override
      public void buildFields() {
  
          internalBuildConfiguredFields();
          internalBuildFields(Mode.structure);
          if (m_showResourceProperties) {
              internalBuildFields(Mode.resource);
          }
      }
  
      /**
       * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#createFormWidget(java.lang.String, java.util.Map, com.google.common.base.Optional)
       */
      @Override
      public I_CmsFormWidget createFormWidget(
          String key,
          Map<String, String> widgetParams,
          Optional<String> defaultValue) {
  
          I_CmsFormWidget widget = super.createFormWidget(key, widgetParams, defaultValue);
          if (m_isReadOnly) {
              widget.setEnabled(false);
          }
          return widget;
      }
  
      /**
       * Focuses the file name field (delayed).<p>
       */
      public void focusNameField() {
  
          m_activeFieldData = new CmsActiveFieldData(null, "simple", A_CmsPropertyEditor.FIELD_URLNAME);
          m_panel.tryToRestoreFieldData(m_activeFieldData);
  
      }
  
      /**
       * Gets the active field data.<p>
       *
       * @return the active field data
       */
      public CmsActiveFieldData getActiveFieldData() {
  
          return m_activeFieldData;
      }
  
      /**
       * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#initializeWidgets(org.opencms.gwt.client.ui.CmsPopup)
       */
      @Override
      public void initializeWidgets(final CmsPopup dialog) {
  
          super.initializeWidgets(dialog);
          dialog.setCaption(null);
          dialog.removePadding();
          m_panel.tryToRestoreFieldData(m_fieldDataToBeRestored);
  
          Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
  
              public boolean execute() {
  
                  if (!getPropertyPanel().getTabPanel().isAttached() || !getPropertyPanel().getTabPanel().isVisible()) {
                      return false;
                  }
                  if (!m_resizeDisabled) {
                      updateHeight(dialog);
                  }
                  return true;
              }
          }, UPDATE_HEIGHT_INTERVAL);
      }
  
      /**
       * Sets the active field data to be restored.<p>
       *
       * @param fieldData the active field data to be restored
       */
      public void restoreActiveFieldData(CmsActiveFieldData fieldData) {
  
          m_fieldDataToBeRestored = fieldData;
  
      }
  
      /**
       * Sets the "readonly" mode.<p>
       *
       * @param readonly if true, readonly mode will be enabled
       */
      public void setReadOnly(boolean readonly) {
  
          m_isReadOnly = readonly;
      }
  
      /**
       * Sets the "show resource properties" flag which controls whether the resource value fields should be built.<p>
       *
       * @param showResourceProperties if true, the resource value fields will be build
       */
      public void setShowResourceProperties(boolean showResourceProperties) {
  
          m_showResourceProperties = showResourceProperties;
      }
  
      /**
       * Returns the property panel.<p>
       *
       * @return the property panel
       */
      protected CmsPropertyPanel getPropertyPanel() {
  
          return m_panel;
      }
  
      /**
       * Method which is called when the tab is switched.<p>
       *
       * @param toTab the tab to which the user is switching
       */
      protected void handleSwitchTab(int toTab) {
  
          switch (toTab) {
              case 0:
                  rebuildSimpleTab();
                  break;
              case 1:
                  rebuildIndividualTab();
                  break;
              case 2:
                  rebuildSharedTab();
                  break;
              default:
                  break;
          }
          if ((m_disabledReason != null) && !m_nameOnlyDisabled) {
              disableInput(m_disabledReason, m_nameOnlyDisabled);
          } else if (m_nameOnlyDisabled) {
              disableInput(m_disabledReason, true);
              m_form.validateAllFields();
          } else {
              m_form.validateAllFields();
          }
      }
  
      /**
       * @see org.opencms.gwt.client.property.A_CmsPropertyEditor#setupFieldContainer()
       */
      @Override
      protected void setupFieldContainer() {
  
          CmsListInfoBean info = m_handler.getPageInfo();
          m_panel = new CmsPropertyPanel(m_showResourceProperties, info);
          String modeClass = m_handler.getModeClass();
          if (modeClass != null) {
              m_panel.addStyleName(modeClass);
          }
          m_panel.addBeforeSelectionHandler(new BeforeSelectionHandler<Integer>() {
  
              public void onBeforeSelection(BeforeSelectionEvent<Integer> event) {
  
                  int target = event.getItem().intValue();
                  handleSwitchTab(target);
              }
          });
          m_form.setWidget(m_panel);
  
      }
  
      /**
       * Updates the panel height depending on the content of the current tab.<p>
       *
       * @param dialog the dialog for which the height should be updated
       */
      protected void updateHeight(CmsPopup dialog) {
  
          int tabIndex = m_panel.getTabPanel().getSelectedIndex();
          boolean changedTab = tabIndex != m_oldTabIndex;
          m_oldTabIndex = tabIndex;
          CmsScrollPanel tabWidget = m_panel.getTabPanel().getWidget(tabIndex).getScrollPanel();
          Element innerElement = tabWidget.getWidget().getElement();
          int contentHeight = CmsDomUtil.getCurrentStyleInt(innerElement, Style.height);
          int spaceLeft = dialog.getAvailableHeight(0);
          int extraSpaceOutsideOfTab = 50;
          int extraSpaceInTabOutsideOfScrollPanel = 38;
          // The second and later tabs start out with a collapsed fieldset at the bottom, which takes more vertical space which is not included in the offsetHeight of the scroll panel content.
          // So to not make things look weird, we use different sizing of the scroll panel for the first tab than for the others.
          int extraSpaceInScrollPanel = tabIndex == 0 ? 4 : 10;
          int newTabPanelHeight = Math.min(
              spaceLeft,
              contentHeight + extraSpaceOutsideOfTab + extraSpaceInTabOutsideOfScrollPanel + extraSpaceInScrollPanel);
          int newTabHeight = newTabPanelHeight - extraSpaceOutsideOfTab;
          int newScrollPanelHeight = newTabHeight - extraSpaceInTabOutsideOfScrollPanel;
          boolean changedHeight = m_panel.getTabPanel().getOffsetHeight() != newTabPanelHeight;
          if (changedHeight || changedTab) {
              m_panel.getTabPanel().setHeight(newTabPanelHeight + "px");
              int selectedIndex = m_panel.getTabPanel().getSelectedIndex();
              CmsScrollPanel widget = m_panel.getTabPanel().getWidget(selectedIndex).getScrollPanel();
              widget.setHeight(newScrollPanelHeight + "px");
              widget.onResizeDescendant();
          }
      }
  
      /**
       * Builds a single form field.<p>
       *
       * @param ownProps the entry's own properties
       * @param propName the property name
       * @param mode the mode which controls which kind of field will be built
       */
      private void buildField(
          Map<String, CmsClientProperty> ownProps,
          final String propName,
          CmsClientProperty.Mode mode) {
  
          String entryId = m_handler.getId().toString();
          CmsXmlContentProperty propDef = m_propertyConfig.get(propName);
  
          if (propDef == null) {
              String widget = CmsClientProperty.PROPERTY_TEMPLATE.equals(propName) ? "template" : "string";
              propDef = new CmsXmlContentProperty(
                  propName,
                  "string",
                  widget,
                  "",
                  null,
                  null,
                  null,
                  null,
                  null,
                  null,
                  null);
          }
  
          if (mode != Mode.effective) {
              propDef = propDef.withNiceName(propName);
          }
  
          CmsClientProperty ownProp = m_properties.get(propName);
          CmsPathValue pathValue = CmsClientProperty.getPathValue(ownProp, mode).prepend(entryId + "/" + propName + "/");
  
          //CHECK: should fields other than NavText be really automatically allowed to be empty in navigation mode?
          final String tab = tabs.get(mode);
          final CmsBasicFormField field = CmsBasicFormField.createField(
              propDef,
              pathValue.getPath() + "#" + tab,
              this,
              Collections.<String, String> emptyMap(),
              true);
  
          CmsPair<String, String> defaultValueAndOrigin = getDefaultValueToDisplay(ownProp, mode);
          String defaultValue = "";
          String origin = "";
          if (defaultValueAndOrigin != null) {
              defaultValue = defaultValueAndOrigin.getFirst();
              origin = defaultValueAndOrigin.getSecond();
          }
          final Widget w = (Widget)field.getWidget();
          I_CmsStringModel model = getStringModel(pathValue);
          field.bind(model);
          boolean ghost = CmsStringUtil.isEmptyOrWhitespaceOnly(pathValue.getValue());
          String initialValue = pathValue.getValue();
          if (w instanceof I_CmsHasGhostValue) {
              ((I_CmsHasGhostValue)w).setGhostValue(defaultValue, ghost);
              if (ghost) {
                  initialValue = null;
              }
          }
          if (w instanceof HasFocusHandlers) {
              try {
                  ((HasFocusHandlers)w).addFocusHandler(new FocusHandler() {
  
                      @SuppressWarnings("synthetic-access")
                      public void onFocus(FocusEvent event) {
  
                          if ((w instanceof CmsTextBox) || (w instanceof CmsTextArea)) {
                              m_activeFieldData = new CmsActiveFieldData(field, tab, propName);
                          } else {
                              // field received focus, but it doesn't make sense to restore it later
                              m_activeFieldData = null;
                          }
                      }
                  });
              } catch (Exception e) {
                  CmsDebugLog.consoleLog("" + e);
              }
  
          }
  
          boolean isShowingGhost = ghost && !CmsStringUtil.isEmpty(defaultValue);
  
          if (isShowingGhost) {
              field.getLayoutData().put("info", origin);
          }
          if (!ghost || isShowingGhost) {
              field.getLayoutData().put(CmsPropertyPanel.LD_DISPLAY_VALUE, "true");
          }
          field.getLayoutData().put(CmsPropertyPanel.LD_PROPERTY, propName);
          field.getLayoutData().put("tab", tab);
          m_form.addField(tab, field, initialValue);
      }
  
      /**
       * Creates a string model which uses a field of a CmsClientProperty for storing its value.<p>
       *
       * @param id the structure id
       * @param propName the property id
       * @param isStructure if true, the structure value field should be used, else the resource value field
       *
       *
       * @return the new model object
       */
      private I_CmsStringModel createStringModel(final CmsUUID id, final String propName, final boolean isStructure) {
  
          final CmsClientProperty property = m_properties.get(propName);
  
          return new I_CmsStringModel() {
  
              private boolean m_active;
  
              private EventBus m_eventBus = new SimpleEventBus();
  
              public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
  
                  return m_eventBus.addHandler(ValueChangeEvent.getType(), handler);
              }
  
              /**
               * @see com.google.gwt.event.shared.HasHandlers#fireEvent(com.google.gwt.event.shared.GwtEvent)
               */
              public void fireEvent(GwtEvent<?> event) {
  
                  m_eventBus.fireEvent(event);
              }
  
              public String getId() {
  
                  return Joiner.on("/").join(id.toString(), propName, isStructure ? "S" : "R");
              }
  
              public String getValue() {
  
                  if (isStructure) {
                      return property.getStructureValue();
                  } else {
                      return property.getResourceValue();
                  }
              }
  
              public void setValue(String value, boolean notify) {
  
                  if (!m_active) {
                      m_active = true;
                      try {
                          String oldValue = getValue();
                          boolean changed = !Objects.equal(value, oldValue);
                          if (isStructure) {
                              property.setStructureValue(value);
                          } else {
                              property.setResourceValue(value);
                          }
                          if (notify && changed) {
                              ValueChangeEvent.fire(this, value);
                          }
                      } finally {
                          m_active = false;
                      }
                  }
              }
          };
      }
  
      /**
       * Gets a pair of strings containing the default value to display for a given property and its source.<p>
       *
       * @param prop the property
       * @param mode the mode
       *
       * @return a pair of the form (defaultValue, origin)
       */
      private CmsPair<String, String> getDefaultValueToDisplay(CmsClientProperty prop, Mode mode) {
  
          if ((mode == Mode.structure) && !CmsStringUtil.isEmpty(prop.getResourceValue())) {
              String message = Messages.get().key(Messages.GUI_ORIGIN_SHARED_0);
  
              return CmsPair.create(prop.getResourceValue(), message);
          }
          CmsClientProperty inheritedProperty = m_handler.getInheritedProperty(prop.getName());
          if (CmsClientProperty.isPropertyEmpty(inheritedProperty)) {
              return null;
          }
          CmsPathValue pathValue = inheritedProperty.getPathValue(mode);
          String message = Messages.get().key(Messages.GUI_ORIGIN_INHERITED_1, inheritedProperty.getOrigin());
          return CmsPair.create(pathValue.getValue(), message);
      }
  
      /**
       * Creates a string model for a given property path value, and returns the same model if the same path value is passed in.<p>
       *
       * @param pathValue the path value
       *
       * @return the model for that path value
       */
      private I_CmsStringModel getStringModel(CmsPathValue pathValue) {
  
          String path = pathValue.getPath();
          I_CmsStringModel model = m_models.get(path);
          if (model == null) {
              String[] tokens = path.split("/");
              String id = tokens[0];
              String propName = tokens[1];
              boolean isStructure = tokens[2].equals("S");
              model = createStringModel(new CmsUUID(id), propName, isStructure);
              m_models.put(path, model);
          }
          return model;
      }
  
      /**
       * Builds the fields for the configured properties in the first tab.<p>
       */
      private void internalBuildConfiguredFields() {
  
          Map<String, CmsClientProperty> ownProps = m_handler.getOwnProperties();
          List<String> keys = new ArrayList<String>(m_propertyConfig.keySet());
          for (String propName : keys) {
              buildField(ownProps, propName, Mode.effective);
          }
      }
  
      /**
       *
       * Builds the fields for a given mode.<p>
       *
       * @param mode the mode
       */
      private void internalBuildFields(Mode mode) {
  
          Map<String, CmsClientProperty> ownProps = m_handler.getOwnProperties();
          for (String propName : m_handler.getAllPropertyNames()) {
              buildField(ownProps, propName, mode);
          }
      }
  
      /**
       * Moves the given property name to the top of the keys if present.<p>
       *
       * @param keys the list of keys
       * @param propertyName the property name to move
       */
      private void moveToTop(List<String> keys, String propertyName) {
  
          if (keys.contains(propertyName)) {
              keys.remove(propertyName);
              keys.add(0, propertyName);
          }
      }
  
      /**
       * Rebuilds the "individual" tab.<p>
       */
      private void rebuildIndividualTab() {
  
          m_form.removeGroup(CmsPropertyPanel.TAB_INDIVIDUAL);
          CmsPropertyPanel panel = ((CmsPropertyPanel)m_form.getWidget());
          panel.clearTab(CmsPropertyPanel.TAB_INDIVIDUAL);
          internalBuildFields(Mode.structure);
          m_form.renderGroup(CmsPropertyPanel.TAB_INDIVIDUAL);
      }
  
      /**
       * Rebuilds the "shared" tab.<p>
       */
      private void rebuildSharedTab() {
  
          m_form.removeGroup(CmsPropertyPanel.TAB_SHARED);
          CmsPropertyPanel panel = ((CmsPropertyPanel)m_form.getWidget());
          panel.clearTab(CmsPropertyPanel.TAB_SHARED);
          internalBuildFields(Mode.resource);
          m_form.renderGroup(CmsPropertyPanel.TAB_SHARED);
      }
  
      /**
       * Rebuilds the simple tab.<p>
       */
      private void rebuildSimpleTab() {
  
          m_form.removeGroup(CmsPropertyPanel.TAB_SIMPLE);
          CmsPropertyPanel panel = ((CmsPropertyPanel)m_form.getWidget());
          panel.clearTab(CmsPropertyPanel.TAB_SIMPLE);
          if (m_handler.hasEditableName()) {
              m_form.addField(CmsPropertyPanel.TAB_SIMPLE, createUrlNameField());
          }
          internalBuildConfiguredFields();
          m_form.renderGroup(CmsPropertyPanel.TAB_SIMPLE);
      }
  }
  