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.ade.contenteditor.client;
029
030import org.opencms.acacia.client.CmsAttributeHandler;
031import org.opencms.acacia.client.CmsEditorBase;
032import org.opencms.acacia.client.CmsUndoRedoHandler;
033import org.opencms.acacia.client.CmsUndoRedoHandler.UndoRedoState;
034import org.opencms.acacia.client.CmsValidationContext;
035import org.opencms.acacia.client.CmsValueFocusHandler;
036import org.opencms.acacia.client.I_CmsEntityRenderer;
037import org.opencms.acacia.client.I_CmsInlineFormParent;
038import org.opencms.acacia.client.entity.CmsEntityBackend;
039import org.opencms.acacia.shared.CmsEntity;
040import org.opencms.acacia.shared.CmsEntityAttribute;
041import org.opencms.acacia.shared.CmsEntityChangeEvent;
042import org.opencms.acacia.shared.CmsEntityChangeEvent.ChangeType;
043import org.opencms.acacia.shared.CmsTabInfo;
044import org.opencms.acacia.shared.CmsType;
045import org.opencms.acacia.shared.CmsValidationResult;
046import org.opencms.ade.containerpage.client.CmsContainerpageController;
047import org.opencms.ade.contenteditor.client.css.I_CmsLayoutBundle;
048import org.opencms.ade.contenteditor.shared.CmsComplexWidgetData;
049import org.opencms.ade.contenteditor.shared.CmsContentDefinition;
050import org.opencms.ade.contenteditor.shared.CmsEditHandlerData;
051import org.opencms.ade.contenteditor.shared.CmsSaveResult;
052import org.opencms.ade.contenteditor.shared.rpc.I_CmsContentService;
053import org.opencms.ade.contenteditor.shared.rpc.I_CmsContentServiceAsync;
054import org.opencms.ade.contenteditor.widgetregistry.client.WidgetRegistry;
055import org.opencms.ade.publish.client.CmsPublishDialog;
056import org.opencms.ade.publish.shared.CmsPublishOptions;
057import org.opencms.gwt.client.CmsCoreProvider;
058import org.opencms.gwt.client.I_CmsEditableData;
059import org.opencms.gwt.client.rpc.CmsRpcAction;
060import org.opencms.gwt.client.rpc.CmsRpcPrefetcher;
061import org.opencms.gwt.client.ui.CmsConfirmDialog;
062import org.opencms.gwt.client.ui.CmsErrorDialog;
063import org.opencms.gwt.client.ui.CmsInfoHeader;
064import org.opencms.gwt.client.ui.CmsModelSelectDialog;
065import org.opencms.gwt.client.ui.CmsNotification;
066import org.opencms.gwt.client.ui.CmsNotification.Type;
067import org.opencms.gwt.client.ui.CmsPushButton;
068import org.opencms.gwt.client.ui.CmsToggleButton;
069import org.opencms.gwt.client.ui.CmsToolbar;
070import org.opencms.gwt.client.ui.I_CmsButton;
071import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
072import org.opencms.gwt.client.ui.I_CmsButton.Size;
073import org.opencms.gwt.client.ui.I_CmsConfirmDialogHandler;
074import org.opencms.gwt.client.ui.I_CmsModelSelectHandler;
075import org.opencms.gwt.client.ui.input.CmsSelectBox;
076import org.opencms.gwt.client.util.CmsDebugLog;
077import org.opencms.gwt.client.util.CmsDomUtil;
078import org.opencms.gwt.client.util.I_CmsSimpleCallback;
079import org.opencms.gwt.shared.CmsGwtConstants;
080import org.opencms.gwt.shared.CmsListInfoBean;
081import org.opencms.util.CmsStringUtil;
082import org.opencms.util.CmsUUID;
083
084import java.util.ArrayList;
085import java.util.Collection;
086import java.util.Collections;
087import java.util.HashMap;
088import java.util.HashSet;
089import java.util.List;
090import java.util.Map;
091import java.util.Map.Entry;
092import java.util.Set;
093
094import com.google.gwt.core.client.GWT;
095import com.google.gwt.core.client.JavaScriptObject;
096import com.google.gwt.core.client.Scheduler;
097import com.google.gwt.core.client.Scheduler.ScheduledCommand;
098import com.google.gwt.dom.client.Element;
099import com.google.gwt.dom.client.NodeList;
100import com.google.gwt.dom.client.Style;
101import com.google.gwt.dom.client.Style.Overflow;
102import com.google.gwt.dom.client.Style.Position;
103import com.google.gwt.dom.client.Style.Unit;
104import com.google.gwt.dom.client.Style.VerticalAlign;
105import com.google.gwt.event.dom.client.ClickEvent;
106import com.google.gwt.event.dom.client.ClickHandler;
107import com.google.gwt.event.dom.client.KeyCodes;
108import com.google.gwt.event.logical.shared.CloseEvent;
109import com.google.gwt.event.logical.shared.CloseHandler;
110import com.google.gwt.event.logical.shared.SelectionEvent;
111import com.google.gwt.event.logical.shared.SelectionHandler;
112import com.google.gwt.event.logical.shared.ValueChangeEvent;
113import com.google.gwt.event.logical.shared.ValueChangeHandler;
114import com.google.gwt.event.shared.HandlerRegistration;
115import com.google.gwt.user.client.Command;
116import com.google.gwt.user.client.Event;
117import com.google.gwt.user.client.Event.NativePreviewEvent;
118import com.google.gwt.user.client.Event.NativePreviewHandler;
119import com.google.gwt.user.client.Window;
120import com.google.gwt.user.client.Window.ClosingEvent;
121import com.google.gwt.user.client.Window.ClosingHandler;
122import com.google.gwt.user.client.rpc.AsyncCallback;
123import com.google.gwt.user.client.rpc.SerializationException;
124import com.google.gwt.user.client.rpc.ServiceDefTarget;
125import com.google.gwt.user.client.ui.FlowPanel;
126import com.google.gwt.user.client.ui.Label;
127import com.google.gwt.user.client.ui.PopupPanel;
128import com.google.gwt.user.client.ui.RootPanel;
129import com.google.gwt.user.client.ui.SimplePanel;
130import com.google.gwt.user.client.ui.TextBox;
131
132/**
133 * The content editor.<p>
134 */
135public final class CmsContentEditor extends CmsEditorBase {
136
137    /**
138     * CmsEntity change handler to watch for changes within given scopes and call the editor change handlers accordingly.<p>
139     */
140    class EditorChangeHandler implements ValueChangeHandler<CmsEntity> {
141
142        /** The scope values. */
143        Map<String, String> m_scopeValues;
144
145        private Set<String> m_changedScopes = new HashSet<>();
146
147        /** The change handler registration. */
148        private HandlerRegistration m_handlerRegistration;
149
150        /** The observed entity. */
151        private CmsEntity m_observerdEntity;
152
153        /**
154         * Constructor.<p>
155         *
156         * @param entity the entity to observe
157         * @param changeScopes the value scopes to watch for changes
158         */
159        public EditorChangeHandler(CmsEntity entity, Collection<String> changeScopes) {
160
161            m_observerdEntity = entity;
162            m_handlerRegistration = entity.addValueChangeHandler(this);
163            m_scopeValues = new HashMap<String, String>();
164            for (String scope : changeScopes) {
165                m_scopeValues.put(scope, CmsContentDefinition.getValueForPath(m_observerdEntity, scope));
166            }
167        }
168
169        /**
170         * Removes this observer from the entities change handler registration and clears registered listeners.<p>
171         */
172        public void clear() {
173
174            if (m_handlerRegistration != null) {
175                m_handlerRegistration.removeHandler();
176                m_handlerRegistration = null;
177            }
178            m_scopeValues.clear();
179            m_observerdEntity = null;
180        }
181
182        /**
183         * @see com.google.gwt.event.logical.shared.ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)
184         */
185        public void onValueChange(ValueChangeEvent<CmsEntity> event) {
186
187            final CmsEntity entity = event.getValue();
188            if (event instanceof CmsEntityChangeEvent) {
189
190                CmsEntityChangeEvent.ChangeType type = ((CmsEntityChangeEvent)event).getChangeType();
191                for (String scope : m_scopeValues.keySet()) {
192                    String scopeValue = CmsContentDefinition.getValueForPath(entity, scope);
193                    String previousValue = m_scopeValues.get(scope);
194                    if (((scopeValue != null) && !scopeValue.equals(previousValue))
195                        || ((scopeValue == null) && (previousValue != null))) {
196                        if (type == ChangeType.change) {
197                            m_changedScopes.add(scope);
198                        }
199                        m_scopeValues.put(scope, scopeValue);
200                    }
201                }
202
203            }
204            Scheduler.get().scheduleFinally(new ScheduledCommand() {
205
206                public void execute() {
207
208                    if (m_changedScopes.size() > 0) {
209                        Set<String> changedScopesCopy = new HashSet<>(m_changedScopes);
210                        m_changedScopes.clear();
211                        if (!changedScopesCopy.isEmpty()) {
212                            callEditorChangeHandlers(changedScopesCopy);
213                        }
214                    }
215                }
216            });
217        }
218    }
219
220    /** CSS marker class added to the html element when  the editor is active. */
221    public static final String EDITOR_MARKER_CLASS = "opencms-editor-active";
222
223    /** The add change listener method name. */
224    private static final String ADD_CHANGE_LISTENER_METHOD = "cmsAddEntityChangeListener";
225
226    /** The entity id selector prefix. */
227    private static final String ENTITY_ID_SELECTOR_PREFIX = "[" + CmsGwtConstants.ATTR_DATA_ID + "*=\"";
228
229    /** The entity id selector suffix. */
230    private static final String ENTITY_ID_SELECTOR_SUFFIX = "\"]";
231
232    /** The editable field selector. */
233    private static final String FIELD_SELECTOR = "[" + CmsGwtConstants.ATTR_DATA_FIELD + "*=\"opencms://\"]";
234
235    /** The get current entity method name. */
236    private static final String GET_CURRENT_ENTITY_METHOD = "cmsGetCurrentEntity";
237
238    /** The in-line editor instance. */
239    private static CmsContentEditor INSTANCE;
240
241    /** Flag indicating that an AJAX call for the editor change handler is running. */
242    protected boolean m_callingChangeHandlers;
243
244    /** The current content locale. */
245    protected String m_locale;
246
247    /** The on close call back. */
248    protected I_CmsEditorCloseHandler m_onClose;
249
250    /** The edit tool-bar. */
251    protected CmsToolbar m_toolbar;
252
253    /** The container element client id if available. */
254    String m_clientId;
255
256    /** The editor context. */
257    CmsEditorContext m_context;
258
259    /** Indicates if any settings attributes have been changed. */
260    boolean m_hasChangedSettings;
261
262    /** Value of the auto-unlock option from the configuration. */
263    private boolean m_autoUnlock;
264
265    /** The available locales. */
266    private Map<String, String> m_availableLocales;
267
268    /** The form editing base panel. */
269    private FlowPanel m_basePanel;
270
271    /** The cancel button. */
272    private CmsPushButton m_cancelButton;
273
274    /** The id's of the changed entities. */
275    private Set<String> m_changedEntityIds;
276
277    /** Changed scopes which still haven't been sent to the editor change handlers. */
278    private Set<String> m_changedScopes = new HashSet<String>();
279
280    /** The window closing handler registration. */
281    private HandlerRegistration m_closingHandlerRegistration;
282
283    /** The content info panel. */
284    private CmsInfoHeader m_contentInfoHeader;
285
286    /** The locales present within the edited content. */
287    private Set<String> m_contentLocales;
288
289    /** The copy locale button. */
290    private CmsPushButton m_copyLocaleButton;
291
292    /** The loaded content definitions by locale. */
293    private Map<String, CmsContentDefinition> m_definitions;
294
295    /** The entities to delete. */
296    private Set<String> m_deletedEntities;
297
298    /** The delete locale button. */
299    private CmsPushButton m_deleteLocaleButton;
300
301    /** Flag indicating the resource needs to removed on cancel. */
302    private boolean m_deleteOnCancel;
303
304    /** The entity value change handler calling configured editor change handlers. */
305    private EditorChangeHandler m_editorChangeHandler;
306
307    /** The entity observer instance. */
308    private CmsEntityObserver m_entityObserver;
309
310    /** The hide help bubbles button. */
311    private CmsToggleButton m_hideHelpBubblesButton;
312
313    /** The resource icon classes. */
314    private String m_iconClasses;
315
316    /** Flag which indicate whether the directedit parameter was set to true when loading the editor. */
317    private boolean m_isDirectEdit;
318
319    /** Flag indicating the editor was opened as the stand alone version, not from within any other module. */
320    private boolean m_isStandAlone;
321
322    /** The locale select box. */
323    private CmsSelectBox m_localeSelect;
324
325    /** The open form button. */
326    private CmsPushButton m_openFormButton;
327
328    /** The page info panel. */
329    private CmsInfoHeader m_pageInfoHeader;
330
331    /** The event preview handler registration. */
332    private HandlerRegistration m_previewHandlerRegistration;
333
334    /** The publish button. */
335    private CmsPushButton m_publishButton;
336
337    /** The redo button. */
338    private CmsPushButton m_redoButton;
339
340    /** The registered entity id's. */
341    private Set<String> m_registeredEntities;
342
343    /** The resource type name. */
344    private String m_resourceTypeName;
345
346    /** The save button. */
347    private CmsPushButton m_saveButton;
348
349    /** The save and exit button. */
350    private CmsPushButton m_saveExitButton;
351
352    /** The content service. */
353    private I_CmsContentServiceAsync m_service;
354
355    /** The resource site path. */
356    private String m_sitePath;
357
358    /** The tab informations for this form. */
359    private List<CmsTabInfo> m_tabInfos;
360
361    /** The resource title. */
362    private String m_title;
363
364    /** The undo button. */
365    private CmsPushButton m_undoButton;
366
367    /** The undo redo event handler registration. */
368    private HandlerRegistration m_undoRedoHandlerRegistration;
369
370    /**
371     * Constructor.<p>
372     */
373    private CmsContentEditor() {
374
375        super((I_CmsContentServiceAsync)GWT.create(I_CmsContentService.class), new CmsDefaultWidgetService());
376        m_service = (I_CmsContentServiceAsync)super.getService();
377        String serviceUrl = CmsCoreProvider.get().link("org.opencms.ade.contenteditor.CmsContentService.gwt");
378        ((ServiceDefTarget)m_service).setServiceEntryPoint(serviceUrl);
379        getWidgetService().setWidgetFactories(WidgetRegistry.getInstance().getWidgetFactories());
380        for (I_CmsEntityRenderer renderer : WidgetRegistry.getInstance().getRenderers()) {
381            getWidgetService().addRenderer(renderer);
382        }
383        // set the acacia editor message bundle
384        setDictionary(Messages.get().getDictionary());
385        I_CmsLayoutBundle.INSTANCE.editorCss().ensureInjected();
386        m_changedEntityIds = new HashSet<String>();
387        m_registeredEntities = new HashSet<String>();
388        m_availableLocales = new HashMap<String, String>();
389        m_contentLocales = new HashSet<String>();
390        m_deletedEntities = new HashSet<String>();
391        m_definitions = new HashMap<String, CmsContentDefinition>();
392        addValidationChangeHandler(new ValueChangeHandler<CmsValidationContext>() {
393
394            public void onValueChange(ValueChangeEvent<CmsValidationContext> event) {
395
396                handleValidationChange(event.getValue());
397            }
398        });
399    }
400
401    /**
402     * Adds an entity change listener.<p>
403     *
404     * @param changeListener the change listener
405     * @param changeScope the change scope
406     */
407    public static void addEntityChangeListener(I_CmsEntityChangeListener changeListener, String changeScope) {
408
409        CmsDebugLog.getInstance().printLine("trying to ad change listener for scope: " + changeScope);
410        if ((INSTANCE == null) || (INSTANCE.m_entityObserver == null)) {
411            CmsDebugLog.getInstance().printLine("handling external registration");
412            if (isObserverExported()) {
413                CmsDebugLog.getInstance().printLine("registration is available");
414                try {
415                    addNativeListener(changeListener, changeScope);
416                } catch (Exception e) {
417
418                    CmsDebugLog.getInstance().printLine(
419                        "Exception occured during listener registration" + e.getMessage());
420                }
421            } else {
422                throw new RuntimeException("Editor is not initialized yet.");
423            }
424        } else {
425            INSTANCE.m_entityObserver.addEntityChangeListener(changeListener, changeScope);
426        }
427    }
428
429    /**
430     * Gets the client id for the editable element.<p>
431     *
432     * @param editableData the editable element
433     *
434     * @return the client id for the editable element
435     */
436    public static String getClientIdForEditable(final I_CmsEditableData editableData) {
437
438        return ((editableData.getElementName() != null)
439            && editableData.getElementName().startsWith(editableData.getStructureId().toString()))
440            ? editableData.getElementName()
441            : editableData.getStructureId().toString();
442    }
443
444    /**
445     * Returns the currently edited entity.<p>
446     *
447     * @return the currently edited entity
448     */
449    public static CmsEntity getEntity() {
450
451        if ((INSTANCE == null) || (INSTANCE.m_entityObserver == null)) {
452            CmsDebugLog.getInstance().printLine("handling external registration");
453            if (isObserverExported()) {
454                return CmsEntityBackend.createFromNativeWrapper(nativeGetEntity());
455            } else {
456                throw new RuntimeException("Editor is not initialized yet.");
457            }
458        } else {
459            return INSTANCE.getCurrentEntity();
460        }
461    }
462
463    /**
464     * Returns the in-line editor instance.<p>
465     *
466     * @return the in-line editor instance
467     */
468    public static CmsContentEditor getInstance() {
469
470        if (INSTANCE == null) {
471            INSTANCE = new CmsContentEditor();
472        }
473        return INSTANCE;
474    }
475
476    /**
477     * Returns if the given element or it's descendants are inline editable.<p>
478     *
479     * @param element the element
480     *
481     * @return <code>true</code> if the element has editable descendants
482     */
483    public static boolean hasEditable(Element element) {
484
485        NodeList<Element> children = CmsDomUtil.querySelectorAll(FIELD_SELECTOR, element);
486        return (children != null) && (children.getLength() > 0);
487    }
488
489    /**
490     * Checks whether the given element is annotated for inline editing.<p>
491     *
492     * @param element the element to check
493     *
494     * @return <code>true</code> if the given element is annotated for inline editing
495     */
496    public static boolean isEditable(Element element) {
497
498        String field = element.getAttribute(CmsGwtConstants.ATTR_DATA_FIELD);
499        return (field != null) && field.contains("opencms://");
500    }
501
502    /**
503     * Replaces the id's within data-oc-id attributes of the given element and all it's children.<p>
504     *
505     * @param element the element
506     * @param oldId the old id
507     * @param newId the new id
508     */
509    public static void replaceResourceIds(Element element, String oldId, String newId) {
510
511        NodeList<Element> children = CmsDomUtil.querySelectorAll(
512            FIELD_SELECTOR + ENTITY_ID_SELECTOR_PREFIX + oldId + ENTITY_ID_SELECTOR_SUFFIX,
513            element);
514        if (children.getLength() > 0) {
515            for (int i = 0; i < children.getLength(); i++) {
516                Element child = children.getItem(i);
517                String idData = child.getAttribute(CmsGwtConstants.ATTR_DATA_ID);
518                idData = idData.replace(oldId, newId);
519                child.setAttribute(CmsGwtConstants.ATTR_DATA_ID, idData);
520            }
521        }
522    }
523
524    /**
525     * Sets all annotated child elements editable.<p>
526     *
527     * @param element the element
528     * @param serverId the editable resource structure id
529     * @param editable <code>true</code> to enable editing
530     *
531     * @return <code>true</code> if the element had editable elements
532     */
533    public static boolean setEditable(Element element, String serverId, boolean editable) {
534
535        I_CmsLayoutBundle.INSTANCE.editorCss().ensureInjected();
536        NodeList<Element> children = CmsDomUtil.querySelectorAll(
537            FIELD_SELECTOR + ENTITY_ID_SELECTOR_PREFIX + serverId + ENTITY_ID_SELECTOR_SUFFIX,
538            element);
539        if (children.getLength() > 0) {
540            for (int i = 0; i < children.getLength(); i++) {
541                Element child = children.getItem(i);
542                if (editable) {
543                    child.addClassName(I_CmsLayoutBundle.INSTANCE.editorCss().inlineEditable());
544                } else {
545                    child.removeClassName(I_CmsLayoutBundle.INSTANCE.editorCss().inlineEditable());
546                }
547            }
548            return true;
549        }
550        return false;
551    }
552
553    /**
554     * Adds the change listener.<p>
555     *
556     * @param changeListener the change listener
557     * @param changeScope the change scope
558     */
559    static native void addNativeListener(I_CmsEntityChangeListener changeListener, String changeScope)/*-{
560        var instance = changeListener;
561        var nat = {
562            onChange : function(entity) {
563                var cmsEntity = @org.opencms.acacia.client.entity.CmsEntityBackend::createFromNativeWrapper(Lcom/google/gwt/core/client/JavaScriptObject;)(entity);
564                instance.@org.opencms.ade.contenteditor.client.I_CmsEntityChangeListener::onEntityChange(Lorg/opencms/acacia/shared/CmsEntity;)(cmsEntity);
565            }
566        }
567        var method = $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::ADD_CHANGE_LISTENER_METHOD];
568        if (typeof method == 'function') {
569            method(nat, changeScope);
570        }
571    }-*/;
572
573    /**
574     * Checks whether the add entity change listener method has been exported.<p>
575     *
576     * @return <code>true</code> if the add entity change listener method has been exported
577     */
578    private static native boolean isObserverExported()/*-{
579        var method = $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::ADD_CHANGE_LISTENER_METHOD];
580        if (typeof method == 'function') {
581            return true;
582        } else {
583            return false;
584        }
585    }-*/;
586
587    /**
588     * Returns the current entity.<p>
589     *
590     * @return the current entity
591     */
592    private static native JavaScriptObject nativeGetEntity()/*-{
593        return $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::GET_CURRENT_ENTITY_METHOD]
594                ();
595    }-*/;
596
597    /**
598     * Closes the editor.<p>
599     * May be used from outside the editor module.<p>
600     */
601    public void closeEditor() {
602
603        if (m_saveButton != null) {
604            if (m_saveButton.isEnabled()) {
605                CmsConfirmDialog dialog = new CmsConfirmDialog(
606                    org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_DIALOG_RESET_TITLE_0),
607                    Messages.get().key(Messages.GUI_CONFIRM_LEAVING_EDITOR_0));
608                dialog.setHandler(new I_CmsConfirmDialogHandler() {
609
610                    public void onClose() {
611
612                        cancelEdit();
613                    }
614
615                    public void onOk() {
616
617                        saveAndExit();
618                    }
619                });
620                dialog.center();
621            } else {
622                cancelEdit();
623            }
624        }
625    }
626
627    /**
628     * Bypasses a focus bug in IE which can happen if the user opens the HTML code editor from the WYSIWYG editor.<p>
629     *
630     * The next time they open the editor form from the same container page, the user may be unable to focus on any input
631     * fields. To prevent this, we create a dummy input field outside the visible screen region and focus it when opening
632     * the editor.
633     */
634    public void fixFocus() {
635
636        TextBox invisibleTextBox = new TextBox();
637        Style style = invisibleTextBox.getElement().getStyle();
638        style.setPosition(Position.FIXED);
639        style.setLeft(-99999, Unit.PX);
640        style.setTop(-99999, Unit.PX);
641        m_basePanel.add(invisibleTextBox);
642        // base panel is already attached at this point, so we can just set the focus
643        invisibleTextBox.setFocus(true);
644    }
645
646    /**
647     * @see org.opencms.acacia.client.CmsEditorBase#getService()
648     */
649    @Override
650    public I_CmsContentServiceAsync getService() {
651
652        return m_service;
653    }
654
655    /**
656     * Loads the content definition for the given entity and executes the callback on success.<p>
657     *
658     * @param entityId the entity id
659     * @param editedEntity the currently edited entity
660     * @param callback the callback
661     */
662    public void loadDefinition(
663        final String entityId,
664        final CmsEntity editedEntity,
665        final I_CmsSimpleCallback<CmsContentDefinition> callback) {
666
667        CmsRpcAction<CmsContentDefinition> action = new CmsRpcAction<CmsContentDefinition>() {
668
669            @Override
670            public void execute() {
671
672                start(0, true);
673                getService().loadDefinition(
674                    entityId,
675                    m_clientId,
676                    editedEntity,
677                    getSkipPaths(),
678                    m_context.getSettingPresets(),
679                    this);
680            }
681
682            @Override
683            protected void onResponse(final CmsContentDefinition result) {
684
685                registerContentDefinition(result);
686                WidgetRegistry.getInstance().registerExternalWidgets(
687                    result.getExternalWidgetConfigurations(),
688                    new Command() {
689
690                        public void execute() {
691
692                            stop(false);
693                            callback.execute(result);
694                        }
695                    });
696            }
697        };
698        action.execute();
699    }
700
701    /**
702     * Loads the content definition for the given entity and executes the callback on success.<p>
703     *
704     * @param entityId the entity id
705     * @param newLink the new link
706     * @param modelFileId  the model file id
707     * @param postCreateHandler the post-create handler class name (optional)
708     * @param mode the content creation mode
709     * @param mainLocale the main language to copy in case the element language node does not exist yet
710     * @param editHandlerData the data for the edit handler, if one is used to create a new content; null otherwise
711     * @param settingPresets the presets for container element settings
712     * @param editorStylesheet the path for the editor style sheet (may be null)
713     * @param callback the callback
714     */
715    public void loadInitialDefinition(
716        final String entityId,
717        final String newLink,
718        final CmsUUID modelFileId,
719        final String postCreateHandler,
720        final String mode,
721        final String mainLocale,
722        final CmsEditHandlerData editHandlerData,
723        Map<String, String> settingPresets,
724        String editorStylesheet,
725        final I_CmsSimpleCallback<CmsContentDefinition> callback) {
726
727        CmsRpcAction<CmsContentDefinition> action = new CmsRpcAction<CmsContentDefinition>() {
728
729            @Override
730            public void execute() {
731
732                start(0, true);
733                getService().loadInitialDefinition(
734                    entityId,
735                    m_clientId,
736                    newLink,
737                    modelFileId,
738                    CmsCoreProvider.get().getUri(),
739                    mainLocale,
740                    postCreateHandler,
741                    mode,
742                    editHandlerData,
743                    settingPresets,
744                    editorStylesheet,
745                    this);
746            }
747
748            @Override
749            protected void onResponse(final CmsContentDefinition result) {
750
751                if (result.isModelInfo()) {
752                    stop(false);
753                    callback.execute(result);
754                } else {
755                    registerContentDefinition(result);
756                    WidgetRegistry.getInstance().registerExternalWidgets(
757                        result.getExternalWidgetConfigurations(),
758                        new Command() {
759
760                            public void execute() {
761
762                                stop(false);
763                                callback.execute(result);
764                            }
765                        });
766                }
767            }
768        };
769        action.execute();
770    }
771
772    /**
773     * Loads the content definition for the given entity and executes the callback on success.<p>
774     *
775     * @param entityId the entity id
776     * @param editedEntity the currently edited entity
777     * @param callback the callback
778     */
779    public void loadNewDefinition(
780        final String entityId,
781        final CmsEntity editedEntity,
782        final I_CmsSimpleCallback<CmsContentDefinition> callback) {
783
784        final Map<String, String> presets = new HashMap<>();
785        if ((m_context != null) && (m_context.getSettingPresets() != null)) {
786            presets.putAll(m_context.getSettingPresets());
787        }
788        CmsRpcAction<CmsContentDefinition> action = new CmsRpcAction<CmsContentDefinition>() {
789
790            @Override
791            public void execute() {
792
793                start(0, true);
794                getService().loadNewDefinition(entityId, m_clientId, editedEntity, getSkipPaths(), presets, this);
795            }
796
797            @Override
798            protected void onResponse(final CmsContentDefinition result) {
799
800                registerContentDefinition(result);
801                WidgetRegistry.getInstance().registerExternalWidgets(
802                    result.getExternalWidgetConfigurations(),
803                    new Command() {
804
805                        public void execute() {
806
807                            stop(false);
808                            callback.execute(result);
809                        }
810                    });
811            }
812        };
813        action.execute();
814    }
815
816    /**
817     * Opens the content editor dialog.<p>
818     *
819     * @param context the editor context
820     * @param locale the content locale
821     * @param elementId the element id
822     * @param clientId the container element client id if available
823     * @param newLink the new link
824     * @param modelFileId the model file id
825     * @param postCreateHandler the post-create handler class (optional)
826     * @param mode the content creation mode
827     * @param mainLocale the main language to copy in case the element language node does not exist yet
828     * @param editHandlerData the edit handler data, if we are using an edit handler to create a new element; null otherwise
829     * @param onClose the command executed on dialog close
830     */
831    public void openFormEditor(
832        final CmsEditorContext context,
833        final String locale,
834        String elementId,
835        String clientId,
836        final String newLink,
837        final CmsUUID modelFileId,
838        final String postCreateHandler,
839        final String mode,
840        final String mainLocale,
841        final CmsEditHandlerData editHandlerData,
842        final I_CmsEditorCloseHandler onClose) {
843
844        m_onClose = onClose;
845        m_clientId = clientId;
846        initEventPreviewHandler();
847        final CmsUUID structureId = new CmsUUID(elementId);
848
849        I_CmsSimpleCallback<Boolean> callback = new I_CmsSimpleCallback<Boolean>() {
850
851            public void execute(Boolean arg) {
852
853                if (arg.booleanValue()) {
854                    loadInitialDefinition(
855                        CmsContentDefinition.uuidToEntityId(structureId, locale),
856                        newLink,
857                        modelFileId,
858                        mode,
859                        postCreateHandler,
860                        mainLocale,
861                        editHandlerData,
862                        context.getSettingPresets(),
863                        context.getEditorStylesheet(),
864                        new I_CmsSimpleCallback<CmsContentDefinition>() {
865
866                            public void execute(CmsContentDefinition contentDefinition) {
867
868                                if (contentDefinition.isModelInfo()) {
869                                    openModelSelectDialog(context, contentDefinition);
870                                } else {
871                                    initEditor(context, contentDefinition, null, false, null);
872                                }
873                            }
874                        });
875                } else {
876                    showLockedResourceMessage();
877                }
878
879            }
880        };
881        // make sure the resource is locked, if we are not creating a new one
882        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(newLink)) {
883            callback.execute(Boolean.TRUE);
884        } else {
885            CmsCoreProvider.get().lock(structureId, callback);
886        }
887    }
888
889    /**
890     * Renders the in-line editor for the given element.<p>
891     *
892     * @param context the editor context
893     * @param elementId the element id
894     * @param locale the content locale
895     * @param panel the element panel
896     * @param mainLocale the main language to copy in case the element language node does not exist yet
897     * @param loadTime the time when the requested resource was loaded
898     * @param onClose the command to execute on close
899     */
900    public void openInlineEditor(
901        final CmsEditorContext context,
902        CmsUUID elementId,
903        String locale,
904        final I_CmsInlineFormParent panel,
905        final String mainLocale,
906        long loadTime,
907        I_CmsEditorCloseHandler onClose) {
908
909        initEventPreviewHandler();
910        final String entityId = CmsContentDefinition.uuidToEntityId(elementId, locale);
911        m_locale = locale;
912        m_onClose = onClose;
913        CmsCoreProvider.get().lock(elementId, loadTime, new I_CmsSimpleCallback<Boolean>() {
914
915            public void execute(Boolean arg) {
916
917                if (arg.booleanValue()) {
918                    loadInitialDefinition(
919                        entityId,
920                        null,
921                        null,
922                        null,
923                        null,
924                        mainLocale,
925                        null,
926                        Collections.emptyMap(),
927                        context.getEditorStylesheet(),
928                        new I_CmsSimpleCallback<CmsContentDefinition>() {
929
930                            public void execute(CmsContentDefinition contentDefinition) {
931
932                                initEditor(context, contentDefinition, panel, true, mainLocale);
933                            }
934                        });
935                } else {
936                    showLockedResourceMessage();
937                }
938            }
939        });
940    }
941
942    /**
943     * Opens the form based editor. Used within the stand alone acacia/editor.jsp.<p>
944     *
945     * @param context the editor context
946     */
947    public void openStandAloneFormEditor(final CmsEditorContext context) {
948
949        initEventPreviewHandler();
950        final CmsContentDefinition definition;
951        try {
952            definition = (CmsContentDefinition)CmsRpcPrefetcher.getSerializedObjectFromDictionary(
953                getService(),
954                I_CmsContentService.DICT_CONTENT_DEFINITION);
955        } catch (SerializationException e) {
956            RootPanel.get().add(new Label(e.getMessage()));
957            return;
958        }
959        m_isStandAlone = true;
960        if (definition.isModelInfo()) {
961            openModelSelectDialog(context, definition);
962        } else {
963            CmsCoreProvider.get().lock(
964                CmsContentDefinition.entityIdToUuid(definition.getEntityId()),
965                new I_CmsSimpleCallback<Boolean>() {
966
967                    public void execute(Boolean arg) {
968
969                        if (arg.booleanValue()) {
970
971                            registerContentDefinition(definition);
972                            // register all external widgets
973                            WidgetRegistry.getInstance().registerExternalWidgets(
974                                definition.getExternalWidgetConfigurations(),
975                                new Command() {
976
977                                    public void execute() {
978
979                                        initEditor(context, definition, null, false, null);
980                                    }
981                                });
982
983                        } else {
984                            showLockedResourceMessage();
985                        }
986                    }
987                });
988        }
989    }
990
991    /**
992     * Registers a deep copy of the source entity with the given target entity id.<p>
993     *
994     * @param sourceEntityId the source entity id
995     * @param targetEntityId the target entity id
996     */
997    public void registerClonedEntity(String sourceEntityId, String targetEntityId) {
998
999        CmsEntityBackend.getInstance().getEntity(sourceEntityId).createDeepCopy(targetEntityId);
1000    }
1001
1002    /**
1003     * Registers the given content definition.<p>
1004     *
1005     * @param definition the content definition
1006     */
1007    public void registerContentDefinition(CmsContentDefinition definition) {
1008
1009        getWidgetService().addConfigurations(definition.getConfigurations());
1010        CmsType baseType = definition.getTypes().get(definition.getEntityTypeName());
1011        m_entityBackend.registerTypes(baseType, definition.getTypes());
1012        for (CmsEntity entity : definition.getEntities().values()) {
1013            CmsEntity previousValue = m_entityBackend.getEntity(entity.getId());
1014            if (previousValue != null) {
1015                m_entityBackend.changeEntityContentValues(previousValue, entity);
1016            } else {
1017                m_entityBackend.registerEntity(entity);
1018                m_registeredEntities.add(entity.getId());
1019            }
1020        }
1021        for (Map.Entry<String, CmsComplexWidgetData> entry : definition.getComplexWidgetData().entrySet()) {
1022            String attrName = entry.getKey();
1023            CmsComplexWidgetData widgetData = entry.getValue();
1024            getWidgetService().registerComplexWidgetAttribute(
1025                attrName,
1026                widgetData.getRendererName(),
1027                widgetData.getConfiguration());
1028        }
1029    }
1030
1031    /**
1032     * Saves the given entities.<p>
1033     *
1034     * @param clearOnSuccess <code>true</code> to clear the VIE instance on success
1035     * @param callback the call back command
1036     */
1037    public void saveAndDeleteEntities(final boolean clearOnSuccess, final I_CmsSimpleCallback<Boolean> callback) {
1038
1039        final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
1040        saveAndDeleteEntities(entity, new ArrayList<String>(m_deletedEntities), clearOnSuccess, callback);
1041    }
1042
1043    /**
1044     * Saves the given entities.<p>
1045     *
1046     * @param lastEditedEntity the last edited entity
1047     * @param deletedEntites the deleted entity id's
1048     * @param clearOnSuccess <code>true</code> to clear the VIE instance on success
1049     * @param callback the call back command
1050     */
1051    public void saveAndDeleteEntities(
1052        final CmsEntity lastEditedEntity,
1053        final List<String> deletedEntites,
1054        final boolean clearOnSuccess,
1055        final I_CmsSimpleCallback<Boolean> callback) {
1056
1057        CmsRpcAction<CmsSaveResult> asyncCallback = new CmsRpcAction<CmsSaveResult>() {
1058
1059            @Override
1060            public void execute() {
1061
1062                start(200, true);
1063                getService().saveAndDeleteEntities(
1064                    lastEditedEntity,
1065                    m_clientId,
1066                    deletedEntites,
1067                    getSkipPaths(),
1068                    m_locale,
1069                    clearOnSuccess,
1070                    this);
1071            }
1072
1073            @Override
1074            protected void onResponse(CmsSaveResult result) {
1075
1076                stop(false);
1077                if ((result != null) && result.hasErrors()) {
1078                    showValidationErrorDialog(result.getValidationResult());
1079                } else {
1080                    callback.execute(Boolean.valueOf((result != null) && result.isHasChangedSettings()));
1081                    if (clearOnSuccess) {
1082                        destroyForm(true);
1083                    }
1084                }
1085            }
1086        };
1087        asyncCallback.execute();
1088    }
1089
1090    /**
1091     * Saves a value in an Xml content.<p>
1092     *
1093     * @param contentId the structure id of the content
1094     * @param contentPath the xpath for which to set the value
1095     * @param locale the locale for which to set the value
1096     * @param value the new value
1097     * @param asyncCallback the callback for the result
1098     */
1099    public void saveValue(
1100        final String contentId,
1101        final String contentPath,
1102        final String locale,
1103        final String value,
1104        final AsyncCallback<String> asyncCallback) {
1105
1106        CmsRpcAction<String> action = new CmsRpcAction<String>() {
1107
1108            @Override
1109            public void execute() {
1110
1111                start(0, false);
1112                getService().saveValue(contentId, contentPath, locale, value, this);
1113
1114            }
1115
1116            @Override
1117            protected void onResponse(String result) {
1118
1119                stop(false);
1120                asyncCallback.onSuccess(result);
1121
1122            }
1123        };
1124        action.execute();
1125    }
1126
1127    /**
1128     * Sets the show editor help flag to the user session.<p>
1129     *
1130     * @param show the show editor help flag
1131     */
1132    public void setShowEditorHelp(final boolean show) {
1133
1134        CmsCoreProvider.get().setShowEditorHelp(show);
1135    }
1136
1137    /**
1138     * Removes the given entity from the entity VIE store.<p>
1139     *
1140     * @param entityId the entity id
1141     */
1142    public void unregistereEntity(String entityId) {
1143
1144        CmsEntityBackend.getInstance().removeEntity(entityId);
1145    }
1146
1147    /**
1148     * @see org.opencms.acacia.client.CmsEditorBase#clearEditor()
1149     */
1150    @Override
1151    protected void clearEditor() {
1152
1153        super.clearEditor();
1154        m_context = null;
1155        if (m_undoRedoHandlerRegistration != null) {
1156            m_undoRedoHandlerRegistration.removeHandler();
1157        }
1158        if (m_toolbar != null) {
1159            m_toolbar.removeFromParent();
1160            m_toolbar = null;
1161        }
1162        m_cancelButton = null;
1163        m_localeSelect = null;
1164        m_deleteLocaleButton = null;
1165        m_copyLocaleButton = null;
1166        m_openFormButton = null;
1167        m_saveButton = null;
1168        m_onClose = null;
1169        m_locale = null;
1170        if (m_basePanel != null) {
1171            m_basePanel.removeFromParent();
1172            m_basePanel = null;
1173        }
1174        if (m_entityObserver != null) {
1175            m_entityObserver.clear();
1176            m_entityObserver = null;
1177        }
1178        m_changedEntityIds.clear();
1179        m_registeredEntities.clear();
1180        m_availableLocales.clear();
1181        m_contentLocales.clear();
1182        m_deletedEntities.clear();
1183        m_definitions.clear();
1184        m_title = null;
1185        m_sitePath = null;
1186        m_resourceTypeName = null;
1187        if (m_closingHandlerRegistration != null) {
1188            m_closingHandlerRegistration.removeHandler();
1189            m_closingHandlerRegistration = null;
1190        }
1191        if (m_isStandAlone) {
1192            closeEditorWidow();
1193        } else {
1194            RootPanel.getBodyElement().getParentElement().getStyle().clearOverflow();
1195        }
1196        if (m_previewHandlerRegistration != null) {
1197            m_previewHandlerRegistration.removeHandler();
1198            m_previewHandlerRegistration = null;
1199        }
1200        CmsDomUtil.getHtmlElement().removeClassName(EDITOR_MARKER_CLASS);
1201    }
1202
1203    /**
1204     * Gets the editor context.<p>
1205     *
1206     * @return the editor context
1207     */
1208    protected CmsEditorContext getContext() {
1209
1210        return m_context;
1211    }
1212
1213    /**
1214     * @see org.opencms.acacia.client.CmsEditorBase#getContextUri()
1215     */
1216    @Override
1217    protected String getContextUri() {
1218
1219        return CmsCoreProvider.get().getUri();
1220    }
1221
1222    /**
1223     * Gets the entity id.<p>
1224     *
1225     * @return the entity id
1226     */
1227    protected String getEntityId() {
1228
1229        return m_entityId;
1230    }
1231
1232    /**
1233     * @see org.opencms.acacia.client.CmsEditorBase#getHtmlContextInfo()
1234     */
1235    @Override
1236    protected String getHtmlContextInfo() {
1237
1238        return m_context.getHtmlContextInfo();
1239    }
1240
1241    /**
1242     * Returns the paths to be skipped when synchronizing locale independent fields.<p>
1243     *
1244     * @return the paths to be skipped when synchronizing locale independent fields
1245     */
1246    protected Collection<String> getSkipPaths() {
1247
1248        return ((CmsDefaultWidgetService)getWidgetService()).getSkipPaths();
1249    }
1250
1251    /**
1252     * Adds a content definition to the internal store.<p>
1253     *
1254     * @param definition the definition to add
1255     */
1256    void addContentDefinition(CmsContentDefinition definition) {
1257
1258        m_definitions.put(definition.getLocale(), definition);
1259        m_contentLocales.add(definition.getLocale());
1260    }
1261
1262    /**
1263     * Calls the editor change handlers.<p>
1264     *
1265     * @param changedScopes the changed content value scopes
1266     */
1267    void callEditorChangeHandlers(final Set<String> changedScopes) {
1268
1269        m_changedScopes.addAll(changedScopes);
1270        if (!m_callingChangeHandlers && (m_changedScopes.size() > 0)) {
1271            m_callingChangeHandlers = true;
1272            final Set<String> scopesToSend = new HashSet<String>(m_changedScopes);
1273            m_changedScopes.clear();
1274            final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
1275            final org.opencms.acacia.shared.CmsEntity currentState = entity.createDeepCopy(m_entityId);
1276            CmsRpcAction<CmsContentDefinition> action = new CmsRpcAction<CmsContentDefinition>() {
1277
1278                @Override
1279                public void execute() {
1280
1281                    start(200, true);
1282                    getService().callEditorChangeHandlers(
1283                        getEntityId(),
1284                        currentState,
1285                        getSkipPaths(),
1286                        scopesToSend,
1287                        this);
1288                }
1289
1290                @Override
1291                public void onFailure(Throwable t) {
1292
1293                    m_callingChangeHandlers = false;
1294                    super.onFailure(t);
1295
1296                }
1297
1298                @Override
1299                protected void onResponse(CmsContentDefinition result) {
1300
1301                    m_callingChangeHandlers = false;
1302                    stop(false);
1303                    updateEditorValues(currentState, result.getEntity());
1304                    callEditorChangeHandlers(new HashSet<String>());
1305                }
1306            };
1307            action.execute();
1308        }
1309    }
1310
1311    /**
1312     * Cancels the editing process.<p>
1313     */
1314    void cancelEdit() {
1315
1316        // store the scroll position
1317        int scrollTop = RootPanel.getBodyElement().getOwnerDocument().getScrollTop();
1318        setEditorState(false);
1319        unlockResource();
1320        if (m_onClose != null) {
1321            m_onClose.onClose(m_hasChangedSettings, /*publishDialog=*/false);
1322        }
1323        destroyForm(true);
1324        clearEditor();
1325        // restore the scroll position
1326        RootPanel.getBodyElement().getOwnerDocument().setScrollTop(scrollTop);
1327    }
1328
1329    /**
1330     * Checks if the content is valid and sends a notification if not.<p>
1331     * This will not trigger a validation run, but will use the latest state.<p>
1332     *
1333     * @return <code>true</code> in case there are no validation issues
1334     */
1335    boolean checkValidation() {
1336
1337        boolean result;
1338        if (m_changedEntityIds.isEmpty()) {
1339            result = true;
1340        } else if (m_saveButton.isEnabled()) {
1341            result = true;
1342        } else {
1343            result = false;
1344            CmsNotification.get().send(Type.ERROR, m_saveButton.getDisabledReason());
1345        }
1346        return result;
1347    }
1348
1349    /**
1350     * Asks the user to confirm resetting all changes.<p>
1351     */
1352    void confirmCancel() {
1353
1354        if (m_saveButton.isEnabled()) {
1355            CmsConfirmDialog dialog = new CmsConfirmDialog(
1356                org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_DIALOG_RESET_TITLE_0),
1357                org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_DIALOG_RESET_TEXT_0));
1358            dialog.setHandler(new I_CmsConfirmDialogHandler() {
1359
1360                public void onClose() {
1361
1362                    // nothing to do
1363                }
1364
1365                public void onOk() {
1366
1367                    cancelEdit();
1368                }
1369            });
1370            dialog.center();
1371        } else {
1372            cancelEdit();
1373        }
1374    }
1375
1376    /**
1377     * Opens the confirm delete locale dialog.<p>
1378     */
1379    void confirmDeleteLocale() {
1380
1381        CmsConfirmDialog dialog = new CmsConfirmDialog(
1382            Messages.get().key(Messages.GUI_CONFIRM_DELETE_LOCALE_TITLE_0),
1383            Messages.get().key(Messages.GUI_CONFIRM_DELETE_LOCALE_TEXT_0));
1384        dialog.setHandler(new I_CmsConfirmDialogHandler() {
1385
1386            public void onClose() {
1387
1388                // nothing to do
1389            }
1390
1391            public void onOk() {
1392
1393                deleteCurrentLocale();
1394            }
1395        });
1396        dialog.center();
1397    }
1398
1399    /**
1400     * Copies the current entity values to the given locales.<p>
1401     *
1402     * @param targetLocales the target locales
1403     */
1404    void copyLocales(final Set<String> targetLocales) {
1405
1406        final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
1407        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
1408
1409            @Override
1410            public void execute() {
1411
1412                start(200, true);
1413                getService().copyLocale(targetLocales, entity, this);
1414            }
1415
1416            @Override
1417            protected void onResponse(Void result) {
1418
1419                stop(false);
1420            }
1421        };
1422        action.execute();
1423        for (String targetLocale : targetLocales) {
1424            String targetId = getIdForLocale(targetLocale);
1425            if (!m_entityId.equals(targetId)) {
1426                if (m_registeredEntities.contains(targetId)) {
1427                    unregistereEntity(targetId);
1428                }
1429                registerClonedEntity(m_entityId, targetId);
1430                m_registeredEntities.add(targetId);
1431                m_changedEntityIds.add(targetId);
1432                m_contentLocales.add(targetLocale);
1433                m_deletedEntities.remove(targetId);
1434                enableSave();
1435            }
1436        }
1437        initLocaleSelect();
1438    }
1439
1440    /**
1441     * Deferrers the save action to the end of the browser event queue.<p>
1442     */
1443    void deferSave() {
1444
1445        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
1446
1447            public void execute() {
1448
1449                save();
1450            }
1451        });
1452    }
1453
1454    /**
1455     * Deferrers the save and exit action to the end of the browser event queue.<p>
1456     */
1457    void deferSaveAndExit() {
1458
1459        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
1460
1461            public void execute() {
1462
1463                saveAndExit();
1464            }
1465        });
1466    }
1467
1468    /**
1469     * Deletes the current locale.<p>
1470     */
1471    void deleteCurrentLocale() {
1472
1473        // there has to remain at least one content locale
1474        if (m_contentLocales.size() > 1) {
1475            String deletedLocale = m_locale;
1476            m_contentLocales.remove(deletedLocale);
1477            m_registeredEntities.remove(m_entityId);
1478            m_changedEntityIds.remove(m_entityId);
1479            m_deletedEntities.add(m_entityId);
1480            getValidationHandler().getValidationContext().removeEntityId(m_entityId);
1481            unregistereEntity(m_entityId);
1482            enableSave();
1483            String nextLocale = null;
1484            if (m_registeredEntities.isEmpty()) {
1485                nextLocale = m_contentLocales.iterator().next();
1486            } else {
1487                nextLocale = CmsContentDefinition.getLocaleFromId(m_registeredEntities.iterator().next());
1488            }
1489            switchLocale(nextLocale);
1490        }
1491    }
1492
1493    /**
1494     * Disables the save buttons with the given message.<p>
1495     *
1496     * @param message the disabled message
1497     */
1498    void disableSave(String message) {
1499
1500        m_saveButton.disable(message);
1501        m_saveExitButton.disable(message);
1502    }
1503
1504    /**
1505     * Leaves the editor saving the content if necessary.<p>
1506     */
1507    void exitWithSaving() {
1508
1509        if (checkValidation()) {
1510            if (m_saveExitButton.isEnabled()) {
1511                saveAndExit();
1512            } else {
1513                cancelEdit();
1514            }
1515        }
1516    }
1517
1518    /**
1519     * Handles validation changes.<p>
1520     *
1521     * @param validationContext the changed validation context
1522     */
1523    void handleValidationChange(CmsValidationContext validationContext) {
1524
1525        if (validationContext.hasValidationErrors()) {
1526            String locales = "";
1527            for (String id : validationContext.getInvalidEntityIds()) {
1528
1529                locales += "\n";
1530
1531                String locale = CmsContentDefinition.getLocaleFromId(id);
1532                if (m_availableLocales.containsKey(locale)) {
1533                    locales += m_availableLocales.get(locale);
1534                    locales += ": " + validationContext.getInvalidFields(id);
1535                }
1536            }
1537            disableSave(Messages.get().key(Messages.GUI_TOOLBAR_VALIDATION_ERRORS_1, locales));
1538        } else if (!m_changedEntityIds.isEmpty()) {
1539            enableSave();
1540        }
1541    }
1542
1543    /**
1544     * Hides the editor help bubbles.<p>
1545     *
1546     * @param hide <code>true</code> to hide the help bubbles
1547     */
1548    void hideHelpBubbles(boolean hide) {
1549
1550        setShowEditorHelp(!hide);
1551        CmsValueFocusHandler.getInstance().hideHelpBubbles(RootPanel.get(), hide);
1552        if (!hide) {
1553            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_SHOWN_0));
1554        } else {
1555            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_HIDDEN_0));
1556        }
1557    }
1558
1559    /**
1560     * Initializes the editor.<p>
1561     *
1562     * @param context the editor context
1563     * @param contentDefinition the content definition
1564     * @param formParent the inline form parent panel, used for inline editing only
1565     * @param inline <code>true</code> to render the editor for inline editing
1566     * @param mainLocale the main language to copy in case the element language node does not exist yet
1567     */
1568    void initEditor(
1569        CmsEditorContext context,
1570        CmsContentDefinition contentDefinition,
1571        I_CmsInlineFormParent formParent,
1572        boolean inline,
1573        String mainLocale) {
1574
1575        m_context = context;
1576        m_locale = contentDefinition.getLocale();
1577        m_entityId = contentDefinition.getEntityId();
1578        m_deleteOnCancel = contentDefinition.isDeleteOnCancel();
1579        m_autoUnlock = contentDefinition.isAutoUnlock();
1580        m_isDirectEdit = contentDefinition.isDirectEdit();
1581        CmsDomUtil.getHtmlElement().addClassName(EDITOR_MARKER_CLASS);
1582
1583        initClosingHandler();
1584        setContentDefinition(contentDefinition);
1585        initToolbar();
1586        if (inline && (formParent != null)) {
1587            if ((mainLocale != null)
1588                && (CmsDomUtil.querySelector(
1589                    "[" + CmsGwtConstants.ATTR_DATA_ID + "^='" + m_entityId + "']",
1590                    formParent.getElement()) == null)) {
1591                // in case a main locale is given and there are not any HTML elements attributed to the current entity id,
1592                // check if the content was rendered for the main locale
1593                CmsUUID structureId = CmsContentDefinition.entityIdToUuid(m_entityId);
1594                String mainLocaleEntityId = CmsContentDefinition.uuidToEntityId(structureId, mainLocale);
1595                NodeList<Element> elements = CmsDomUtil.querySelectorAll(
1596                    "[" + CmsGwtConstants.ATTR_DATA_ID + "^='" + mainLocaleEntityId + "']",
1597                    formParent.getElement());
1598                if (elements.getLength() > 0) {
1599                    for (int i = 0; i < elements.getLength(); i++) {
1600                        Element element = elements.getItem(i);
1601                        element.setAttribute(
1602                            CmsGwtConstants.ATTR_DATA_ID,
1603                            element.getAttribute(CmsGwtConstants.ATTR_DATA_ID).replace(mainLocaleEntityId, m_entityId));
1604                    }
1605                }
1606
1607            }
1608            initEditOverlay(formParent.getElement());
1609            addOverlayClickHandler(new ClickHandler() {
1610
1611                public void onClick(ClickEvent event) {
1612
1613                    exitWithSaving();
1614                }
1615            });
1616            m_hideHelpBubblesButton.setVisible(false);
1617            setNativeResourceInfo(m_sitePath, m_locale);
1618            initEntityObserver();
1619            if (m_definitions.get(m_locale).hasEditorChangeHandlers()) {
1620                initEditorChangeHandlers(m_definitions.get(m_locale).getEditorChangeScopes());
1621            }
1622            renderInlineEntity(m_entityId, formParent);
1623        } else {
1624            initFormPanel();
1625            renderFormContent();
1626            fixFocus();
1627        }
1628        if (contentDefinition.isPerformedAutocorrection()) {
1629            CmsNotification.get().send(
1630                CmsNotification.Type.NORMAL,
1631                Messages.get().key(Messages.GUI_WARN_INVALID_XML_STRUCTURE_0));
1632            setChanged();
1633        }
1634    }
1635
1636    /**
1637     * Initializes the editor change handler.<p>
1638     *
1639     * @param changeScopes the scopes to watch for changes
1640     */
1641    void initEditorChangeHandlers(Collection<String> changeScopes) {
1642
1643        if (m_editorChangeHandler != null) {
1644            m_editorChangeHandler.clear();
1645        }
1646        m_editorChangeHandler = new EditorChangeHandler(getEntity(), changeScopes);
1647    }
1648
1649    /**
1650     * Initializes the entity observer.<p>
1651     */
1652    void initEntityObserver() {
1653
1654        if (m_entityObserver != null) {
1655            m_entityObserver.clear();
1656        }
1657        m_entityObserver = new CmsEntityObserver(getCurrentEntity());
1658        exportObserver();
1659    }
1660
1661    /**
1662     * Opens the form based editor.<p>
1663     */
1664    void initFormPanel() {
1665
1666        removeEditOverlays();
1667        m_openFormButton.setVisible(false);
1668        m_saveButton.setVisible(true);
1669        m_hideHelpBubblesButton.setVisible(true);
1670        m_undoButton.setVisible(true);
1671        m_redoButton.setVisible(true);
1672        m_basePanel = new FlowPanel();
1673        m_basePanel.addStyleName(I_CmsLayoutBundle.INSTANCE.editorCss().basePanel());
1674        m_basePanel.addStyleName(I_CmsLayoutBundle.INSTANCE.generalCss().opencms());
1675        // insert base panel before the tool bar to keep the tool bar visible
1676        RootPanel.get().add(m_basePanel);
1677        if (m_isStandAlone) {
1678            RootPanel.getBodyElement().addClassName(I_CmsLayoutBundle.INSTANCE.editorCss().standAloneEditor());
1679        } else {
1680            RootPanel.getBodyElement().getParentElement().getStyle().setOverflow(Overflow.HIDDEN);
1681        }
1682    }
1683
1684    /**
1685     * Opens the copy locale dialog.<p>
1686     */
1687    void openCopyLocaleDialog() {
1688
1689        CmsCopyLocaleDialog dialog = new CmsCopyLocaleDialog(
1690            m_availableLocales,
1691            m_contentLocales,
1692            m_locale,
1693            m_definitions.get(m_locale).hasSynchronizedElements(),
1694            this);
1695        dialog.center();
1696    }
1697
1698    /**
1699     * Opens the model file select dialog.<p>
1700     *
1701     * @param context the editor context
1702     * @param definition the content definition
1703     */
1704    void openModelSelectDialog(final CmsEditorContext context, final CmsContentDefinition definition) {
1705
1706        I_CmsModelSelectHandler handler = new I_CmsModelSelectHandler() {
1707
1708            public void onModelSelect(CmsUUID modelStructureId) {
1709
1710                if (modelStructureId == null) {
1711                    modelStructureId = CmsUUID.getNullUUID();
1712                }
1713                openFormEditor(
1714                    context,
1715                    definition.getLocale(),
1716                    definition.getReferenceResourceId().toString(),
1717                    m_clientId,
1718                    definition.getNewLink(),
1719                    modelStructureId,
1720                    null,
1721                    null,
1722                    null,
1723
1724                    null,
1725                    m_onClose);
1726            }
1727        };
1728        String title = org.opencms.gwt.client.Messages.get().key(
1729            org.opencms.gwt.client.Messages.GUI_MODEL_SELECT_TITLE_0);
1730        String message = org.opencms.gwt.client.Messages.get().key(
1731            org.opencms.gwt.client.Messages.GUI_MODEL_SELECT_MESSAGE_0);
1732        CmsModelSelectDialog dialog = new CmsModelSelectDialog(handler, definition.getModelInfos(), title, message);
1733        dialog.center();
1734    }
1735
1736    /**
1737     * Previews the native event to enable keyboard short cuts.<p>
1738     *
1739     * @param event the event
1740     */
1741    void previewNativeEvent(NativePreviewEvent event) {
1742
1743        Event nativeEvent = Event.as(event.getNativeEvent());
1744        if (event.getTypeInt() == Event.ONKEYDOWN) {
1745            int keyCode = nativeEvent.getKeyCode();
1746            if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
1747                // look for short cuts
1748                if (nativeEvent.getShiftKey()) {
1749                    if (keyCode == KeyCodes.KEY_S) {
1750
1751                        exitWithSaving();
1752                        nativeEvent.preventDefault();
1753                        nativeEvent.stopPropagation();
1754                    } else if (keyCode == KeyCodes.KEY_X) {
1755                        confirmCancel();
1756                        nativeEvent.preventDefault();
1757                        nativeEvent.stopPropagation();
1758                    }
1759                } else if (keyCode == KeyCodes.KEY_S) {
1760                    if (checkValidation()) {
1761                        save();
1762                    }
1763                    nativeEvent.preventDefault();
1764                    nativeEvent.stopPropagation();
1765                }
1766
1767            }
1768        }
1769    }
1770
1771    /**
1772     * Renders the form content.<p>
1773     */
1774    void renderFormContent() {
1775
1776        initLocaleSelect();
1777        setNativeResourceInfo(m_sitePath, m_locale);
1778        m_contentInfoHeader = new CmsInfoHeader(m_title, null, m_sitePath, m_locale, m_iconClasses);
1779        m_basePanel.add(m_contentInfoHeader);
1780        SimplePanel content = new SimplePanel();
1781        content.setStyleName(org.opencms.acacia.client.css.I_CmsLayoutBundle.INSTANCE.form().formParent());
1782        m_basePanel.add(content);
1783        initEntityObserver();
1784        if (m_definitions.get(m_locale).hasEditorChangeHandlers()) {
1785            initEditorChangeHandlers(m_definitions.get(m_locale).getEditorChangeScopes());
1786        }
1787        renderEntityForm(m_entityId, m_tabInfos, content, m_basePanel.getElement());
1788        if ((m_clientId != null) && (getFormTabs() != null)) {
1789
1790            CmsListInfoBean pageInfo = CmsContainerpageController.get().getData().getPageInfo();
1791            m_pageInfoHeader = new CmsInfoHeader(
1792                pageInfo.getTitle(),
1793                null,
1794                pageInfo.getSubTitle(),
1795                CmsContainerpageController.get().getData().getLocale(),
1796                pageInfo.getBigIconClasses());
1797            updateInfoHeader(CmsContentDefinition.SETTINGS_TAB_ID.equals(getFormTabs().getSelectedId()));
1798            getFormTabs().addSelectionHandler(new SelectionHandler<Integer>() {
1799
1800                public void onSelection(SelectionEvent<Integer> event) {
1801
1802                    updateInfoHeader(CmsContentDefinition.SETTINGS_TAB_ID.equals(getFormTabs().getSelectedId()));
1803                }
1804            });
1805        }
1806    }
1807
1808    /**
1809     * Saves the content and closes the editor.<p>
1810     */
1811    void save() {
1812
1813        saveAndDeleteEntities(false, new I_CmsSimpleCallback<Boolean>() {
1814
1815            public void execute(Boolean hasChangedSettings) {
1816
1817                setSaved();
1818                setUnchanged();
1819                m_hasChangedSettings = m_hasChangedSettings || hasChangedSettings.booleanValue();
1820            }
1821        });
1822    }
1823
1824    /**
1825     * Saves the content and closes the editor.<p>
1826     */
1827    void saveAndExit() {
1828
1829        boolean unlock = shouldUnlockAutomatically();
1830        // store the scroll position
1831        final int scrollTop = RootPanel.getBodyElement().getOwnerDocument().getScrollTop();
1832        saveAndDeleteEntities(unlock, new I_CmsSimpleCallback<Boolean>() {
1833
1834            public void execute(final Boolean hasChangedSettings) {
1835
1836                setSaved();
1837                if (m_onClose != null) {
1838                    m_onClose.onClose(
1839                        m_hasChangedSettings || hasChangedSettings.booleanValue(),
1840                        /*publishDialog=*/false);
1841                }
1842                clearEditor();
1843                // restore the scroll position
1844                RootPanel.getBodyElement().getOwnerDocument().setScrollTop(scrollTop);
1845            }
1846        });
1847    }
1848
1849    /**
1850     * Sets the has changed flag and enables the save button.<p>
1851     */
1852    void setChanged() {
1853
1854        enableSave();
1855        m_changedEntityIds.add(m_entityId);
1856        m_deletedEntities.remove(m_entityId);
1857        updateOverlayPosition();
1858        setEditorState(true);
1859    }
1860
1861    /**
1862     * Sets the content definition.<p>
1863     *
1864     * @param definition the content definition
1865     */
1866    void setContentDefinition(CmsContentDefinition definition) {
1867
1868        if (m_availableLocales.isEmpty()) {
1869            // only set the locales when initially setting the content definition
1870            m_availableLocales.putAll(definition.getAvailableLocales());
1871            m_contentLocales.addAll(definition.getContentLocales());
1872        } else {
1873            m_contentLocales.add(definition.getLocale());
1874        }
1875        m_title = definition.getTitle();
1876        m_sitePath = definition.getSitePath();
1877        m_resourceTypeName = definition.getResourceType();
1878        m_registeredEntities.add(definition.getEntityId());
1879        m_tabInfos = definition.getTabInfos();
1880        m_iconClasses = definition.getIconClasses();
1881        addContentDefinition(definition);
1882        CmsDefaultWidgetService service = (CmsDefaultWidgetService)getWidgetService();
1883        service.addConfigurations(definition.getConfigurations());
1884        service.setSyncValues(definition.getSyncValues());
1885        service.setSkipPaths(definition.getSkipPaths());
1886        addEntityChangeHandler(definition.getEntityId(), new ValueChangeHandler<CmsEntity>() {
1887
1888            public void onValueChange(ValueChangeEvent<CmsEntity> event) {
1889
1890                setChanged();
1891            }
1892        });
1893    }
1894
1895    /**
1896     * Removes the delete on cancel flag for new resources.<p>
1897     */
1898    void setSaved() {
1899
1900        setEditorState(false);
1901        m_deleteOnCancel = false;
1902    }
1903
1904    /**
1905     * Call after save.<p>
1906     */
1907    void setUnchanged() {
1908
1909        m_changedEntityIds.clear();
1910        m_deletedEntities.clear();
1911        disableSave(Messages.get().key(Messages.GUI_TOOLBAR_NOTHING_CHANGED_0));
1912    }
1913
1914    /**
1915     * Enables and disabler the undo redo buttons according to the state.<p>
1916     *
1917     * @param state the undo redo state
1918     */
1919    void setUndoRedoState(UndoRedoState state) {
1920
1921        if (state.hasUndo()) {
1922            m_undoButton.enable();
1923        } else {
1924            m_undoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_UNDO_DISABLED_0));
1925        }
1926        if (state.hasRedo()) {
1927            m_redoButton.enable();
1928        } else {
1929            m_redoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_REDO_DISABLED_0));
1930        }
1931    }
1932
1933    /**
1934     * Returns true if the edited resource should be unlocked automatically after pressing Save/Exit.<p>
1935     *
1936     * @return true if the edited resource should be unlocked automatically
1937     */
1938    boolean shouldUnlockAutomatically() {
1939
1940        if (m_isStandAlone) {
1941            if (m_isDirectEdit) {
1942                // Classic direct edit case - always unlock
1943                return true;
1944            } else {
1945                // Workplace case - determined by configuration
1946                return m_autoUnlock;
1947            }
1948        }
1949        // Container page case - always unlock
1950        return true;
1951    }
1952
1953    /**
1954     * Shows the locked resource error message.<p>
1955     */
1956    void showLockedResourceMessage() {
1957
1958        CmsErrorDialog dialog = new CmsErrorDialog(
1959            Messages.get().key(Messages.ERR_RESOURCE_ALREADY_LOCKED_BY_OTHER_USER_0),
1960            null);
1961        dialog.addCloseHandler(new CloseHandler<PopupPanel>() {
1962
1963            public void onClose(CloseEvent<PopupPanel> event) {
1964
1965                cancelEdit();
1966            }
1967        });
1968        dialog.center();
1969    }
1970
1971    /**
1972     * Shows the validation error dialog.<p>
1973     *
1974     * @param validationResult the validation result
1975     */
1976    void showValidationErrorDialog(CmsValidationResult validationResult) {
1977
1978        if (validationResult.getErrors().keySet().contains(m_entityId)) {
1979            getValidationHandler().displayValidation(m_entityId, validationResult);
1980        }
1981        String errorLocales = "";
1982        for (String entityId : validationResult.getErrors().keySet()) {
1983            String locale = CmsContentDefinition.getLocaleFromId(entityId);
1984            errorLocales += m_availableLocales.get(locale) + ", ";
1985        }
1986        // remove trailing ','
1987        errorLocales = errorLocales.substring(0, errorLocales.length() - 2);
1988        CmsErrorDialog dialog = new CmsErrorDialog(
1989            Messages.get().key(Messages.GUI_VALIDATION_ERROR_1, errorLocales),
1990            null);
1991        dialog.center();
1992    }
1993
1994    /**
1995     * Switches to the selected locale. Will save changes first.<p>
1996     *
1997     * @param locale the locale to switch to
1998     */
1999    void switchLocale(final String locale) {
2000
2001        if (locale.equals(m_locale)) {
2002            return;
2003        }
2004        final Integer oldTabIndex = getTabIndex();
2005        m_locale = locale;
2006        m_basePanel.clear();
2007        destroyForm(false);
2008        final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
2009        m_entityId = getIdForLocale(locale);
2010        // if the content does not contain the requested locale yet, a new node will be created
2011        final boolean addedNewLocale = !m_contentLocales.contains(locale);
2012        if (m_registeredEntities.contains(m_entityId)) {
2013            unregistereEntity(m_entityId);
2014        }
2015        if (addedNewLocale) {
2016            loadNewDefinition(m_entityId, entity, new I_CmsSimpleCallback<CmsContentDefinition>() {
2017
2018                public void execute(final CmsContentDefinition contentDefinition) {
2019
2020                    setContentDefinition(contentDefinition);
2021                    renderFormContent();
2022                    if (oldTabIndex != null) {
2023                        if (oldTabIndex.intValue() < getFormTabs().getTabCount()) {
2024                            getFormTabs().selectTab(oldTabIndex.intValue());
2025                        }
2026                    }
2027                    setChanged();
2028
2029                }
2030            });
2031        } else {
2032            loadDefinition(m_entityId, entity, new I_CmsSimpleCallback<CmsContentDefinition>() {
2033
2034                public void execute(CmsContentDefinition contentDefinition) {
2035
2036                    setContentDefinition(contentDefinition);
2037                    renderFormContent();
2038                    if (oldTabIndex != null) {
2039                        if (oldTabIndex.intValue() < getFormTabs().getTabCount()) {
2040                            getFormTabs().selectTab(oldTabIndex.intValue());
2041                        }
2042                    }
2043                }
2044
2045            });
2046        }
2047    }
2048
2049    /**
2050     * Synchronizes the locale independent fields to the other locales.<p>
2051     */
2052    void synchronizeCurrentLocale() {
2053
2054        m_basePanel.clear();
2055        destroyForm(false);
2056        CmsEntity entity = m_entityBackend.getEntity(m_entityId);
2057        m_entityId = getIdForLocale(m_locale);
2058        ((CmsDefaultWidgetService)getWidgetService()).setSkipPaths(Collections.<String> emptyList());
2059        loadDefinition(m_entityId, entity, new I_CmsSimpleCallback<CmsContentDefinition>() {
2060
2061            public void execute(CmsContentDefinition contentDefinition) {
2062
2063                setContentDefinition(contentDefinition);
2064                renderFormContent();
2065                setChanged();
2066            }
2067        });
2068    }
2069
2070    /**
2071     * Unlocks the edited resource.<p>
2072     */
2073    void unlockResource() {
2074
2075        if (!shouldUnlockAutomatically()) {
2076            return;
2077        }
2078        if (m_entityId != null) {
2079            final CmsUUID structureId = CmsContentDefinition.entityIdToUuid(m_entityId);
2080            if (m_deleteOnCancel) {
2081                CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
2082
2083                    @Override
2084                    public void execute() {
2085
2086                        CmsCoreProvider.getVfsService().syncDeleteResource(structureId, this);
2087                    }
2088
2089                    @Override
2090                    protected void onResponse(Void result) {
2091
2092                        // nothing to do
2093                    }
2094                };
2095                action.executeSync();
2096            } else {
2097                CmsCoreProvider.get().unlock(structureId);
2098            }
2099        }
2100    }
2101
2102    /**
2103     * Updates the editor values.<p>
2104     *
2105     * @param previous the previous entity state
2106     * @param updated the updated entity state
2107     */
2108    void updateEditorValues(CmsEntity previous, CmsEntity updated) {
2109
2110        if (!m_isDirectEdit && updated.getId().equals(m_entityId)) {
2111            // only apply the changes to the same locale entity
2112            updateEditorValues(previous, updated, getEntity(), Collections.<String> emptyList());
2113        }
2114    }
2115
2116    /**
2117     * Updates the info header.<p>
2118     *
2119     * @param isSettingsTabSelected <code>true</code> if the element settings tab is selected
2120     */
2121    void updateInfoHeader(boolean isSettingsTabSelected) {
2122
2123        m_basePanel.remove(0);
2124        m_basePanel.insert(isSettingsTabSelected ? m_pageInfoHeader : m_contentInfoHeader, 0);
2125    }
2126
2127    /**
2128     * Adds the change listener to the observer.<p>
2129     *
2130     * @param changeListener the change listener
2131     * @param changeScope the change scope
2132     */
2133    private void addChangeListener(JavaScriptObject changeListener, String changeScope) {
2134
2135        try {
2136            System.out.println("Adding native listener for scope " + changeScope);
2137            m_entityObserver.addEntityChangeListener(new CmsEntityChangeListenerWrapper(changeListener), changeScope);
2138        } catch (Exception e) {
2139
2140            CmsDebugLog.getInstance().printLine("Exception occured during listener registration" + e.getMessage());
2141        }
2142    }
2143
2144    /**
2145     * Changes a simple type entity value.<p>
2146     *
2147     * @param attributeName the attribute name
2148     * @param index the value index
2149     * @param value the value
2150     * @param parentPathElements the parent path elements
2151     */
2152    private void changeSimpleValue(String attributeName, int index, String value, List<String> parentPathElements) {
2153
2154        CmsAttributeHandler handler = getAttributeHandler(attributeName, parentPathElements);
2155        handler.changeValue(value, index);
2156    }
2157
2158    /**
2159     * Closes the editor.<p>
2160     */
2161    private native void closeEditorWidow() /*-{
2162        if ($wnd.top.cms_ade_closeEditorDialog) {
2163            $wnd.top.cms_ade_closeEditorDialog();
2164        } else if ($wnd.parent.parent.cms_ade_closeEditorDialog) {
2165            $wnd.parent.parent.cms_ade_closeEditorDialog();
2166        } else {
2167            var backlink = $wnd[@org.opencms.ade.contenteditor.shared.rpc.I_CmsContentService::PARAM_BACKLINK];
2168            if (backlink) {
2169                $wnd.top.location.href = backlink;
2170            }
2171        }
2172    }-*/;
2173
2174    /**
2175     * Creates a push button for the edit tool-bar.<p>
2176     *
2177     * @param title the button title
2178     * @param imageClass the image class
2179     *
2180     * @return the button
2181     */
2182    private CmsPushButton createButton(String title, String imageClass) {
2183
2184        CmsPushButton result = new CmsPushButton();
2185        result.setTitle(title);
2186        result.setImageClass(imageClass);
2187        result.setButtonStyle(ButtonStyle.FONT_ICON, null);
2188        result.setSize(Size.big);
2189        return result;
2190    }
2191
2192    /**
2193     * Enables the save buttons.<p>
2194     */
2195    private void enableSave() {
2196
2197        m_saveButton.enable();
2198        m_saveExitButton.enable();
2199    }
2200
2201    /**
2202     * Exports the add entity change listener method.<p>
2203     */
2204    private native void exportObserver()/*-{
2205        var self = this;
2206        $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::ADD_CHANGE_LISTENER_METHOD] = function(
2207                listener, scope) {
2208            var wrapper = {
2209                onChange : listener.onChange
2210            }
2211            self.@org.opencms.ade.contenteditor.client.CmsContentEditor::addChangeListener(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;)(wrapper, scope);
2212        }
2213        $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::GET_CURRENT_ENTITY_METHOD] = function() {
2214            return new $wnd.acacia.CmsEntityWrapper(
2215                    self.@org.opencms.ade.contenteditor.client.CmsContentEditor::getCurrentEntity()());
2216        }
2217    }-*/;
2218
2219    /**
2220     * Returns the attribute handler for the given attribute.<p>
2221     *
2222     * @param attributeName the attribute name
2223     * @param parentPathElements the parent path elements
2224     *
2225     * @return the attribute handler
2226     */
2227    private CmsAttributeHandler getAttributeHandler(String attributeName, List<String> parentPathElements) {
2228
2229        List<String> childPathElements = new ArrayList<String>(parentPathElements);
2230        childPathElements.add(attributeName);
2231        CmsAttributeHandler handler = getRootAttributeHandler().getHandlerByPath(
2232            childPathElements.toArray(new String[childPathElements.size()]));
2233        return handler;
2234    }
2235
2236    /**
2237     * Returns the entity id for the given locale.<p>
2238     *
2239     * @param locale the locale
2240     *
2241     * @return the entity id
2242     */
2243    private String getIdForLocale(String locale) {
2244
2245        return CmsContentDefinition.uuidToEntityId(CmsContentDefinition.entityIdToUuid(m_entityId), locale);
2246    }
2247
2248    /**
2249     * Returns the selected tab index, or null if there are no tabs.
2250     *
2251     * @return the selected tab index or null
2252     */
2253    private Integer getTabIndex() {
2254
2255        if (getFormTabs() != null) {
2256            return Integer.valueOf(getFormTabs().getSelectedIndex());
2257        }
2258        return null;
2259    }
2260
2261    /**
2262     * Initializes the window closing handler to ensure the resource will be unlocked when leaving the editor.<p>
2263     */
2264    private void initClosingHandler() {
2265
2266        m_closingHandlerRegistration = Window.addWindowClosingHandler(new ClosingHandler() {
2267
2268            /**
2269             * @see com.google.gwt.user.client.Window.ClosingHandler#onWindowClosing(com.google.gwt.user.client.Window.ClosingEvent)
2270             */
2271            public void onWindowClosing(ClosingEvent event) {
2272
2273                unlockResource();
2274            }
2275        });
2276    }
2277
2278    /**
2279     * Initializes the event preview handler.<p>
2280     */
2281    private void initEventPreviewHandler() {
2282
2283        m_previewHandlerRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() {
2284
2285            public void onPreviewNativeEvent(NativePreviewEvent event) {
2286
2287                previewNativeEvent(event);
2288            }
2289        });
2290    }
2291
2292    /**
2293     * Initializes the locale selector.<p>
2294     */
2295    private void initLocaleSelect() {
2296
2297        if (m_availableLocales.size() < 2) {
2298            return;
2299        }
2300        Map<String, String> selectOptions = new HashMap<String, String>();
2301        for (Entry<String, String> localeEntry : m_availableLocales.entrySet()) {
2302            if (m_contentLocales.contains(localeEntry.getKey())) {
2303                selectOptions.put(localeEntry.getKey(), localeEntry.getValue());
2304            } else {
2305                selectOptions.put(localeEntry.getKey(), localeEntry.getValue() + " [-]");
2306            }
2307        }
2308        if (m_localeSelect == null) {
2309            m_localeSelect = new CmsSelectBox(selectOptions);
2310            m_toolbar.insertRight(m_localeSelect, 1);
2311            m_localeSelect.addStyleName(I_CmsLayoutBundle.INSTANCE.generalCss().inlineBlock());
2312            m_localeSelect.getElement().getStyle().setWidth(100, Unit.PX);
2313            m_localeSelect.getElement().getStyle().setVerticalAlign(VerticalAlign.MIDDLE);
2314            m_localeSelect.addValueChangeHandler(new ValueChangeHandler<String>() {
2315
2316                public void onValueChange(ValueChangeEvent<String> event) {
2317
2318                    switchLocale(event.getValue());
2319                }
2320            });
2321        } else {
2322            m_localeSelect.setItems(selectOptions);
2323        }
2324        m_localeSelect.setFormValueAsString(m_locale);
2325        if (m_deleteLocaleButton == null) {
2326            m_deleteLocaleButton = createButton(
2327                Messages.get().key(Messages.GUI_TOOLBAR_DELETE_LOCALE_0),
2328                "opencms-icon-remove-locale");
2329            m_deleteLocaleButton.addClickHandler(new ClickHandler() {
2330
2331                public void onClick(ClickEvent event) {
2332
2333                    confirmDeleteLocale();
2334                }
2335            });
2336            m_toolbar.insertRight(m_deleteLocaleButton, 2);
2337        }
2338        if (m_contentLocales.size() > 1) {
2339            m_deleteLocaleButton.enable();
2340        } else {
2341            m_deleteLocaleButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_CANT_DELETE_LAST_LOCALE_0));
2342        }
2343        if (m_copyLocaleButton == null) {
2344            m_copyLocaleButton = createButton(
2345                I_CmsButton.ButtonData.COPY_LOCALE_BUTTON.getTitle(),
2346                I_CmsButton.ButtonData.COPY_LOCALE_BUTTON.getIconClass());
2347            m_copyLocaleButton.addClickHandler(new ClickHandler() {
2348
2349                public void onClick(ClickEvent event) {
2350
2351                    openCopyLocaleDialog();
2352                }
2353            });
2354            m_toolbar.insertRight(m_copyLocaleButton, 3);
2355        }
2356
2357    }
2358
2359    /**
2360     * Generates the button bar displayed beneath the editable fields.<p>
2361     */
2362    private void initToolbar() {
2363
2364        m_toolbar = new CmsToolbar();
2365        m_toolbar.setAppTitle(Messages.get().key(Messages.GUI_CONTENT_EDITOR_TITLE_0));
2366        m_publishButton = createButton(
2367            I_CmsButton.ButtonData.PUBLISH_BUTTON.getTitle(),
2368            I_CmsButton.ButtonData.PUBLISH_BUTTON.getIconClass());
2369        m_toolbar.addLeft(m_publishButton);
2370        m_publishButton.addClickHandler(new ClickHandler() {
2371
2372            public void onClick(ClickEvent event) {
2373
2374                boolean unlock = shouldUnlockAutomatically();
2375
2376                saveAndDeleteEntities(unlock, new I_CmsSimpleCallback<Boolean>() {
2377
2378                    public void execute(final Boolean hasChangedSeetings) {
2379
2380                        setSaved();
2381                        HashMap<String, String> params = new HashMap<String, String>(
2382                            getContext().getPublishParameters());
2383                        CmsUUID structureId = CmsContentDefinition.entityIdToUuid(getEntityId());
2384                        params.put(CmsPublishOptions.PARAM_CONTENT, "" + structureId);
2385                        params.put(CmsPublishOptions.PARAM_START_WITH_CURRENT_PAGE, "");
2386                        CmsPublishDialog.showPublishDialog(params, new CloseHandler<PopupPanel>() {
2387
2388                            public void onClose(CloseEvent<PopupPanel> closeEvent) {
2389
2390                                if (m_onClose != null) {
2391                                    m_onClose.onClose(
2392                                        m_hasChangedSettings || hasChangedSeetings.booleanValue(),
2393                                        /*publishDialog=*/true);
2394                                }
2395                                clearEditor();
2396                            }
2397                        }, new Runnable() {
2398
2399                            public void run() {
2400
2401                                // ignore
2402                            }
2403
2404                        }, null);
2405
2406                    }
2407                });
2408
2409            }
2410        });
2411        m_saveExitButton = createButton(
2412            Messages.get().key(Messages.GUI_TOOLBAR_SAVE_AND_EXIT_0),
2413            "opencms-icon-save-exit");
2414        m_saveExitButton.addClickHandler(new ClickHandler() {
2415
2416            public void onClick(ClickEvent event) {
2417
2418                deferSaveAndExit();
2419            }
2420        });
2421        m_toolbar.addLeft(m_saveExitButton);
2422        m_saveButton = createButton(
2423            Messages.get().key(Messages.GUI_TOOLBAR_SAVE_0),
2424            I_CmsButton.ButtonData.SAVE_BUTTON.getIconClass());
2425        m_saveButton.addClickHandler(new ClickHandler() {
2426
2427            public void onClick(ClickEvent event) {
2428
2429                deferSave();
2430            }
2431        });
2432        m_saveButton.setVisible(false);
2433        m_toolbar.addLeft(m_saveButton);
2434        disableSave(Messages.get().key(Messages.GUI_TOOLBAR_NOTHING_CHANGED_0));
2435        m_undoButton = createButton(Messages.get().key(Messages.GUI_TOOLBAR_UNDO_0), "opencms-icon-undo");
2436        m_undoButton.addClickHandler(new ClickHandler() {
2437
2438            public void onClick(ClickEvent event) {
2439
2440                if (CmsUndoRedoHandler.getInstance().isIntitalized()) {
2441                    CmsUndoRedoHandler.getInstance().undo();
2442                }
2443            }
2444        });
2445        m_undoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_UNDO_DISABLED_0));
2446        m_undoButton.setVisible(false);
2447        m_toolbar.addLeft(m_undoButton);
2448        m_redoButton = createButton(Messages.get().key(Messages.GUI_TOOLBAR_REDO_0), "opencms-icon-redo");
2449        m_redoButton.addClickHandler(new ClickHandler() {
2450
2451            public void onClick(ClickEvent event) {
2452
2453                if (CmsUndoRedoHandler.getInstance().isIntitalized()) {
2454                    CmsUndoRedoHandler.getInstance().redo();
2455                }
2456            }
2457        });
2458        m_redoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_REDO_DISABLED_0));
2459        m_redoButton.setVisible(false);
2460        m_toolbar.addLeft(m_redoButton);
2461
2462        m_undoRedoHandlerRegistration = CmsUndoRedoHandler.getInstance().addValueChangeHandler(
2463            new ValueChangeHandler<CmsUndoRedoHandler.UndoRedoState>() {
2464
2465                public void onValueChange(ValueChangeEvent<UndoRedoState> event) {
2466
2467                    setUndoRedoState(event.getValue());
2468                }
2469            });
2470        m_openFormButton = createButton(
2471            Messages.get().key(Messages.GUI_TOOLBAR_OPEN_FORM_0),
2472            I_CmsButton.ButtonData.EDIT.getIconClass());
2473        m_openFormButton.addClickHandler(new ClickHandler() {
2474
2475            public void onClick(ClickEvent event) {
2476
2477                initFormPanel();
2478                renderFormContent();
2479            }
2480        });
2481        m_toolbar.addLeft(m_openFormButton);
2482
2483        m_hideHelpBubblesButton = new CmsToggleButton();
2484
2485        m_hideHelpBubblesButton.setImageClass(I_CmsButton.ButtonData.TOGGLE_HELP.getIconClass());
2486        m_hideHelpBubblesButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
2487        m_hideHelpBubblesButton.setSize(Size.big);
2488        m_hideHelpBubblesButton.addClickHandler(new ClickHandler() {
2489
2490            public void onClick(ClickEvent event) {
2491
2492                CmsToggleButton button = (CmsToggleButton)event.getSource();
2493                hideHelpBubbles(!button.isDown());
2494            }
2495        });
2496        m_hideHelpBubblesButton.setDown(CmsCoreProvider.get().isShowEditorHelp());
2497        CmsValueFocusHandler.getInstance().hideHelpBubbles(RootPanel.get(), !CmsCoreProvider.get().isShowEditorHelp());
2498        if (!CmsCoreProvider.get().isShowEditorHelp()) {
2499            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_HIDDEN_0));
2500        } else {
2501            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_SHOWN_0));
2502        }
2503        m_toolbar.addRight(m_hideHelpBubblesButton);
2504
2505        m_cancelButton = createButton(
2506            Messages.get().key(Messages.GUI_TOOLBAR_RESET_0),
2507            I_CmsButton.ButtonData.RESET_BUTTON.getIconClass());
2508        m_cancelButton.addClickHandler(new ClickHandler() {
2509
2510            public void onClick(ClickEvent event) {
2511
2512                confirmCancel();
2513            }
2514        });
2515        m_toolbar.addRight(m_cancelButton);
2516        RootPanel.get().add(m_toolbar);
2517    }
2518
2519    /**
2520     * Sets the editor state.<p>
2521     *
2522     * @param changed if the content has been changed
2523     */
2524    private native void setEditorState(boolean changed)/*-{
2525        if (typeof $wnd.cmsSetEditorChangedState === 'function') {
2526            $wnd.cmsSetEditorChangedState(changed);
2527        }
2528    }-*/;
2529
2530    /**
2531     * Sets the resource info to native window context variables.<p>
2532     *
2533     * @param sitePath the site path
2534     * @param locale the content locale
2535     */
2536    private native void setNativeResourceInfo(String sitePath, String locale)/*-{
2537        $wnd._editResource = sitePath;
2538        $wnd._editLanguage = locale;
2539    }-*/;
2540
2541    /**
2542     * Updates the editor values according to the given entity.<p>
2543     *
2544     * @param previous the previous entity state
2545     * @param updated the updated entity state
2546     * @param target the target entity
2547     * @param parentPathElements the parent path elements
2548     */
2549    private void updateEditorValues(
2550        CmsEntity previous,
2551        CmsEntity updated,
2552        CmsEntity target,
2553        List<String> parentPathElements) {
2554
2555        for (String attributeName : m_entityBackend.getType(target.getTypeName()).getAttributeNames()) {
2556            CmsAttributeHandler handler = getAttributeHandler(attributeName, parentPathElements);
2557            if (handler == null) {
2558                // non visible attribute, skip it
2559                continue;
2560            }
2561            if (previous.hasAttribute(attributeName)
2562                && updated.hasAttribute(attributeName)
2563                && target.hasAttribute(attributeName)) {
2564                CmsEntityAttribute updatedAttribute = updated.getAttribute(attributeName);
2565                CmsEntityAttribute previousAttribute = previous.getAttribute(attributeName);
2566                CmsEntityAttribute targetAttribute = target.getAttribute(attributeName);
2567                if (updatedAttribute.isSimpleValue()) {
2568                    if ((updatedAttribute.getValueCount() == previousAttribute.getValueCount())
2569                        && (updatedAttribute.getValueCount() == targetAttribute.getValueCount())) {
2570                        for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2571                            if (!updatedAttribute.getSimpleValues().get(i).equals(
2572                                previousAttribute.getSimpleValues().get(i))
2573                                && previousAttribute.getSimpleValues().get(i).equals(
2574                                    targetAttribute.getSimpleValues().get(i))) {
2575
2576                                changeSimpleValue(
2577                                    attributeName,
2578                                    i,
2579                                    updatedAttribute.getSimpleValues().get(i),
2580                                    parentPathElements);
2581                            }
2582                        }
2583                    } else {
2584                        if (targetAttribute.getValueCount() == previousAttribute.getValueCount()) {
2585                            // only act, if the value count has not been altered while executing the server request
2586                            if (updatedAttribute.getValueCount() > previousAttribute.getValueCount()) {
2587                                // new values have been added
2588                                for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2589                                    if (i >= previousAttribute.getSimpleValues().size()) {
2590                                        handler.addNewAttributeValue(updatedAttribute.getSimpleValues().get(i));
2591                                    } else if (!updatedAttribute.getSimpleValues().get(i).equals(
2592                                        previousAttribute.getSimpleValues().get(i))
2593                                        && previousAttribute.getSimpleValues().get(i).equals(
2594                                            targetAttribute.getSimpleValues().get(i))) {
2595                                        changeSimpleValue(
2596                                            attributeName,
2597                                            i,
2598                                            updatedAttribute.getSimpleValues().get(i),
2599                                            parentPathElements);
2600                                    }
2601                                }
2602                            } else {
2603                                // values have been removed
2604                                for (int i = previousAttribute.getValueCount() - 1; i >= 0; i--) {
2605                                    if (i >= updatedAttribute.getSimpleValues().size()) {
2606                                        handler.removeAttributeValue(i);
2607                                    } else if (!updatedAttribute.getSimpleValues().get(i).equals(
2608                                        previousAttribute.getSimpleValues().get(i))
2609                                        && previousAttribute.getSimpleValues().get(i).equals(
2610                                            targetAttribute.getSimpleValues().get(i))) {
2611                                        changeSimpleValue(
2612                                            attributeName,
2613                                            i,
2614                                            updatedAttribute.getSimpleValues().get(i),
2615                                            parentPathElements);
2616                                    }
2617                                }
2618                            }
2619                        }
2620                    }
2621                } else {
2622                    if (targetAttribute.getValueCount() == previousAttribute.getValueCount()) {
2623                        // only act, if the value count has not been altered while executing the server request
2624                        if (updatedAttribute.getValueCount() > previousAttribute.getValueCount()) {
2625                            // new values have been added
2626                            for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2627                                if (i >= previousAttribute.getSimpleValues().size()) {
2628                                    handler.addNewAttributeValue(
2629                                        m_entityBackend.registerEntity(
2630                                            updatedAttribute.getComplexValues().get(i),
2631                                            true));
2632                                } else {
2633                                    List<String> childPathElements = new ArrayList<String>(parentPathElements);
2634                                    childPathElements.add(attributeName + "[" + i + "]");
2635                                    updateEditorValues(
2636                                        previousAttribute.getComplexValues().get(i),
2637                                        updatedAttribute.getComplexValues().get(i),
2638                                        targetAttribute.getComplexValues().get(i),
2639                                        childPathElements);
2640                                }
2641                            }
2642                        } else {
2643                            // values have been removed
2644                            for (int i = previousAttribute.getValueCount() - 1; i >= 0; i--) {
2645                                if (i >= updatedAttribute.getValueCount()) {
2646
2647                                    handler.removeAttributeValue(i);
2648                                } else {
2649                                    List<String> childPathElements = new ArrayList<String>(parentPathElements);
2650                                    childPathElements.add(attributeName + "[" + i + "]");
2651                                    updateEditorValues(
2652                                        previousAttribute.getComplexValues().get(i),
2653                                        updatedAttribute.getComplexValues().get(i),
2654                                        targetAttribute.getComplexValues().get(i),
2655                                        childPathElements);
2656                                }
2657                            }
2658                        }
2659                    }
2660                }
2661            } else if (previous.hasAttribute(attributeName) && target.hasAttribute(attributeName)) {
2662                for (int i = target.getAttribute(attributeName).getValueCount() - 1; i >= 0; i--) {
2663                    handler.removeAttributeValue(i);
2664                }
2665
2666            } else if (!previous.hasAttribute(attributeName)
2667                && !target.hasAttribute(attributeName)
2668                && updated.hasAttribute(attributeName)) //
2669            {
2670                CmsEntityAttribute updatedAttribute = updated.getAttribute(attributeName);
2671                for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2672                    if (updatedAttribute.isSimpleValue()) {
2673                        handler.addNewAttributeValue(updatedAttribute.getSimpleValues().get(i));
2674                    } else {
2675                        handler.addNewAttributeValue(
2676                            m_entityBackend.registerEntity(updatedAttribute.getComplexValues().get(i), true));
2677                    }
2678                }
2679            }
2680        }
2681    }
2682}