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}