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        m_context = context;
849
850        I_CmsSimpleCallback<Boolean> callback = new I_CmsSimpleCallback<Boolean>() {
851
852            public void execute(Boolean arg) {
853
854                if (arg.booleanValue()) {
855                    loadInitialDefinition(
856                        CmsContentDefinition.uuidToEntityId(structureId, locale),
857                        newLink,
858                        modelFileId,
859                        mode,
860                        postCreateHandler,
861                        mainLocale,
862                        editHandlerData,
863                        context.getSettingPresets(),
864                        context.getEditorStylesheet(),
865                        new I_CmsSimpleCallback<CmsContentDefinition>() {
866
867                            public void execute(CmsContentDefinition contentDefinition) {
868
869                                if (contentDefinition.isModelInfo()) {
870                                    openModelSelectDialog(context, contentDefinition);
871                                } else {
872                                    initEditor(context, contentDefinition, null, false, null);
873                                }
874                            }
875                        });
876                } else {
877                    showLockedResourceMessage();
878                }
879
880            }
881        };
882        // make sure the resource is locked, if we are not creating a new one
883        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(newLink)) {
884            callback.execute(Boolean.TRUE);
885        } else {
886            CmsCoreProvider.get().lock(structureId, callback);
887        }
888    }
889
890    /**
891     * Renders the in-line editor for the given element.<p>
892     *
893     * @param context the editor context
894     * @param elementId the element id
895     * @param locale the content locale
896     * @param panel the element panel
897     * @param mainLocale the main language to copy in case the element language node does not exist yet
898     * @param loadTime the time when the requested resource was loaded
899     * @param onClose the command to execute on close
900     */
901    public void openInlineEditor(
902        final CmsEditorContext context,
903        CmsUUID elementId,
904        String locale,
905        final I_CmsInlineFormParent panel,
906        final String mainLocale,
907        long loadTime,
908        I_CmsEditorCloseHandler onClose) {
909
910        initEventPreviewHandler();
911        m_context = context;
912        final String entityId = CmsContentDefinition.uuidToEntityId(elementId, locale);
913        m_locale = locale;
914        m_onClose = onClose;
915        CmsCoreProvider.get().lock(elementId, loadTime, new I_CmsSimpleCallback<Boolean>() {
916
917            public void execute(Boolean arg) {
918
919                if (arg.booleanValue()) {
920                    loadInitialDefinition(
921                        entityId,
922                        null,
923                        null,
924                        null,
925                        null,
926                        mainLocale,
927                        null,
928                        Collections.emptyMap(),
929                        context.getEditorStylesheet(),
930                        new I_CmsSimpleCallback<CmsContentDefinition>() {
931
932                            public void execute(CmsContentDefinition contentDefinition) {
933
934                                initEditor(context, contentDefinition, panel, true, mainLocale);
935                            }
936                        });
937                } else {
938                    showLockedResourceMessage();
939                }
940            }
941        });
942    }
943
944    /**
945     * Opens the form based editor. Used within the stand alone acacia/editor.jsp.<p>
946     *
947     * @param context the editor context
948     */
949    public void openStandAloneFormEditor(final CmsEditorContext context) {
950
951        initEventPreviewHandler();
952        final CmsContentDefinition definition;
953        try {
954            definition = (CmsContentDefinition)CmsRpcPrefetcher.getSerializedObjectFromDictionary(
955                getService(),
956                I_CmsContentService.DICT_CONTENT_DEFINITION);
957        } catch (SerializationException e) {
958            RootPanel.get().add(new Label(e.getMessage()));
959            return;
960        }
961        context.setReusedElement(definition.isReusedElement());
962        m_isStandAlone = true;
963        if (definition.isModelInfo()) {
964            openModelSelectDialog(context, definition);
965        } else {
966            CmsCoreProvider.get().lock(
967                CmsContentDefinition.entityIdToUuid(definition.getEntityId()),
968                new I_CmsSimpleCallback<Boolean>() {
969
970                    public void execute(Boolean arg) {
971
972                        if (arg.booleanValue()) {
973
974                            registerContentDefinition(definition);
975                            // register all external widgets
976                            WidgetRegistry.getInstance().registerExternalWidgets(
977                                definition.getExternalWidgetConfigurations(),
978                                new Command() {
979
980                                    public void execute() {
981
982                                        initEditor(context, definition, null, false, null);
983                                    }
984                                });
985
986                        } else {
987                            showLockedResourceMessage();
988                        }
989                    }
990                });
991        }
992    }
993
994    /**
995     * Registers a deep copy of the source entity with the given target entity id.<p>
996     *
997     * @param sourceEntityId the source entity id
998     * @param targetEntityId the target entity id
999     */
1000    public void registerClonedEntity(String sourceEntityId, String targetEntityId) {
1001
1002        CmsEntityBackend.getInstance().getEntity(sourceEntityId).createDeepCopy(targetEntityId);
1003    }
1004
1005    /**
1006     * Registers the given content definition.<p>
1007     *
1008     * @param definition the content definition
1009     */
1010    public void registerContentDefinition(CmsContentDefinition definition) {
1011
1012        getWidgetService().addConfigurations(definition.getConfigurations());
1013        CmsType baseType = definition.getTypes().get(definition.getEntityTypeName());
1014        m_entityBackend.registerTypes(baseType, definition.getTypes());
1015        for (CmsEntity entity : definition.getEntities().values()) {
1016            CmsEntity previousValue = m_entityBackend.getEntity(entity.getId());
1017            if (previousValue != null) {
1018                m_entityBackend.changeEntityContentValues(previousValue, entity);
1019            } else {
1020                m_entityBackend.registerEntity(entity);
1021                m_registeredEntities.add(entity.getId());
1022            }
1023        }
1024        for (Map.Entry<String, CmsComplexWidgetData> entry : definition.getComplexWidgetData().entrySet()) {
1025            String attrName = entry.getKey();
1026            CmsComplexWidgetData widgetData = entry.getValue();
1027            getWidgetService().registerComplexWidgetAttribute(
1028                attrName,
1029                widgetData.getRendererName(),
1030                widgetData.getConfiguration());
1031        }
1032    }
1033
1034    /**
1035     * Saves the given entities.<p>
1036     *
1037     * @param clearOnSuccess <code>true</code> to clear the VIE instance on success
1038     * @param callback the call back command
1039     */
1040    public void saveAndDeleteEntities(final boolean clearOnSuccess, final I_CmsSimpleCallback<Boolean> callback) {
1041
1042        final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
1043        saveAndDeleteEntities(entity, new ArrayList<String>(m_deletedEntities), clearOnSuccess, callback);
1044    }
1045
1046    /**
1047     * Saves the given entities.<p>
1048     *
1049     * @param lastEditedEntity the last edited entity
1050     * @param deletedEntites the deleted entity id's
1051     * @param clearOnSuccess <code>true</code> to clear the VIE instance on success
1052     * @param callback the call back command
1053     */
1054    public void saveAndDeleteEntities(
1055        final CmsEntity lastEditedEntity,
1056        final List<String> deletedEntites,
1057        final boolean clearOnSuccess,
1058        final I_CmsSimpleCallback<Boolean> callback) {
1059
1060        CmsRpcAction<CmsSaveResult> asyncCallback = new CmsRpcAction<CmsSaveResult>() {
1061
1062            @Override
1063            public void execute() {
1064
1065                start(200, true);
1066                getService().saveAndDeleteEntities(
1067                    lastEditedEntity,
1068                    m_clientId,
1069                    deletedEntites,
1070                    getSkipPaths(),
1071                    m_locale,
1072                    clearOnSuccess,
1073                    this);
1074            }
1075
1076            @Override
1077            protected void onResponse(CmsSaveResult result) {
1078
1079                stop(false);
1080                if ((result != null) && result.hasErrors()) {
1081                    showValidationErrorDialog(result.getValidationResult());
1082                } else {
1083                    callback.execute(Boolean.valueOf((result != null) && result.isHasChangedSettings()));
1084                    if (clearOnSuccess) {
1085                        destroyForm(true);
1086                    }
1087                }
1088            }
1089        };
1090        asyncCallback.execute();
1091    }
1092
1093    /**
1094     * Saves a value in an Xml content.<p>
1095     *
1096     * @param contentId the structure id of the content
1097     * @param contentPath the xpath for which to set the value
1098     * @param locale the locale for which to set the value
1099     * @param value the new value
1100     * @param asyncCallback the callback for the result
1101     */
1102    public void saveValue(
1103        final String contentId,
1104        final String contentPath,
1105        final String locale,
1106        final String value,
1107        final AsyncCallback<String> asyncCallback) {
1108
1109        CmsRpcAction<String> action = new CmsRpcAction<String>() {
1110
1111            @Override
1112            public void execute() {
1113
1114                start(0, false);
1115                getService().saveValue(contentId, contentPath, locale, value, this);
1116
1117            }
1118
1119            @Override
1120            protected void onResponse(String result) {
1121
1122                stop(false);
1123                asyncCallback.onSuccess(result);
1124
1125            }
1126        };
1127        action.execute();
1128    }
1129
1130    /**
1131     * Sets the show editor help flag to the user session.<p>
1132     *
1133     * @param show the show editor help flag
1134     */
1135    public void setShowEditorHelp(final boolean show) {
1136
1137        CmsCoreProvider.get().setShowEditorHelp(show);
1138    }
1139
1140    /**
1141     * Removes the given entity from the entity VIE store.<p>
1142     *
1143     * @param entityId the entity id
1144     */
1145    public void unregistereEntity(String entityId) {
1146
1147        CmsEntityBackend.getInstance().removeEntity(entityId);
1148    }
1149
1150    /**
1151     * @see org.opencms.acacia.client.CmsEditorBase#clearEditor()
1152     */
1153    @Override
1154    protected void clearEditor() {
1155
1156        super.clearEditor();
1157        m_context = null;
1158        if (m_undoRedoHandlerRegistration != null) {
1159            m_undoRedoHandlerRegistration.removeHandler();
1160        }
1161        if (m_toolbar != null) {
1162            m_toolbar.removeFromParent();
1163            m_toolbar = null;
1164        }
1165        m_cancelButton = null;
1166        m_localeSelect = null;
1167        m_deleteLocaleButton = null;
1168        m_copyLocaleButton = null;
1169        m_openFormButton = null;
1170        m_saveButton = null;
1171        m_onClose = null;
1172        m_locale = null;
1173        if (m_basePanel != null) {
1174            m_basePanel.removeFromParent();
1175            m_basePanel = null;
1176        }
1177        if (m_entityObserver != null) {
1178            m_entityObserver.clear();
1179            m_entityObserver = null;
1180        }
1181        m_changedEntityIds.clear();
1182        m_registeredEntities.clear();
1183        m_availableLocales.clear();
1184        m_contentLocales.clear();
1185        m_deletedEntities.clear();
1186        m_definitions.clear();
1187        m_title = null;
1188        m_sitePath = null;
1189        m_resourceTypeName = null;
1190        if (m_closingHandlerRegistration != null) {
1191            m_closingHandlerRegistration.removeHandler();
1192            m_closingHandlerRegistration = null;
1193        }
1194        if (m_isStandAlone) {
1195            closeEditorWidow();
1196        } else {
1197            RootPanel.getBodyElement().getParentElement().getStyle().clearOverflow();
1198        }
1199        if (m_previewHandlerRegistration != null) {
1200            m_previewHandlerRegistration.removeHandler();
1201            m_previewHandlerRegistration = null;
1202        }
1203        CmsDomUtil.getHtmlElement().removeClassName(EDITOR_MARKER_CLASS);
1204    }
1205
1206    /**
1207     * Gets the editor context.<p>
1208     *
1209     * @return the editor context
1210     */
1211    protected CmsEditorContext getContext() {
1212
1213        return m_context;
1214    }
1215
1216    /**
1217     * @see org.opencms.acacia.client.CmsEditorBase#getContextUri()
1218     */
1219    @Override
1220    protected String getContextUri() {
1221
1222        return CmsCoreProvider.get().getUri();
1223    }
1224
1225    /**
1226     * Gets the entity id.<p>
1227     *
1228     * @return the entity id
1229     */
1230    protected String getEntityId() {
1231
1232        return m_entityId;
1233    }
1234
1235    /**
1236     * @see org.opencms.acacia.client.CmsEditorBase#getHtmlContextInfo()
1237     */
1238    @Override
1239    protected String getHtmlContextInfo() {
1240
1241        return m_context.getHtmlContextInfo();
1242    }
1243
1244    /**
1245     * Returns the paths to be skipped when synchronizing locale independent fields.<p>
1246     *
1247     * @return the paths to be skipped when synchronizing locale independent fields
1248     */
1249    protected Collection<String> getSkipPaths() {
1250
1251        return ((CmsDefaultWidgetService)getWidgetService()).getSkipPaths();
1252    }
1253
1254    /**
1255     * Adds a content definition to the internal store.<p>
1256     *
1257     * @param definition the definition to add
1258     */
1259    void addContentDefinition(CmsContentDefinition definition) {
1260
1261        m_definitions.put(definition.getLocale(), definition);
1262        m_contentLocales.add(definition.getLocale());
1263    }
1264
1265    /**
1266     * Calls the editor change handlers.<p>
1267     *
1268     * @param changedScopes the changed content value scopes
1269     */
1270    void callEditorChangeHandlers(final Set<String> changedScopes) {
1271
1272        m_changedScopes.addAll(changedScopes);
1273        if (!m_callingChangeHandlers && (m_changedScopes.size() > 0)) {
1274            m_callingChangeHandlers = true;
1275            final Set<String> scopesToSend = new HashSet<String>(m_changedScopes);
1276            m_changedScopes.clear();
1277            final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
1278            final org.opencms.acacia.shared.CmsEntity currentState = entity.createDeepCopy(m_entityId);
1279            CmsRpcAction<CmsContentDefinition> action = new CmsRpcAction<CmsContentDefinition>() {
1280
1281                @Override
1282                public void execute() {
1283
1284                    start(200, true);
1285                    getService().callEditorChangeHandlers(
1286                        getEntityId(),
1287                        currentState,
1288                        getSkipPaths(),
1289                        scopesToSend,
1290                        this);
1291                }
1292
1293                @Override
1294                public void onFailure(Throwable t) {
1295
1296                    m_callingChangeHandlers = false;
1297                    super.onFailure(t);
1298
1299                }
1300
1301                @Override
1302                protected void onResponse(CmsContentDefinition result) {
1303
1304                    m_callingChangeHandlers = false;
1305                    stop(false);
1306                    updateEditorValues(currentState, result.getEntity());
1307                    callEditorChangeHandlers(new HashSet<String>());
1308                }
1309            };
1310            action.execute();
1311        }
1312    }
1313
1314    /**
1315     * Cancels the editing process.<p>
1316     */
1317    void cancelEdit() {
1318
1319        // store the scroll position
1320        int scrollTop = RootPanel.getBodyElement().getOwnerDocument().getScrollTop();
1321        setEditorState(false);
1322        unlockResource();
1323        if (m_onClose != null) {
1324            m_onClose.onClose(m_hasChangedSettings, /*publishDialog=*/false);
1325        }
1326        destroyForm(true);
1327        clearEditor();
1328        // restore the scroll position
1329        RootPanel.getBodyElement().getOwnerDocument().setScrollTop(scrollTop);
1330    }
1331
1332    /**
1333     * Checks if the content is valid and sends a notification if not.<p>
1334     * This will not trigger a validation run, but will use the latest state.<p>
1335     *
1336     * @return <code>true</code> in case there are no validation issues
1337     */
1338    boolean checkValidation() {
1339
1340        boolean result;
1341        if (m_changedEntityIds.isEmpty()) {
1342            result = true;
1343        } else if (m_saveButton.isEnabled()) {
1344            result = true;
1345        } else {
1346            result = false;
1347            CmsNotification.get().send(Type.ERROR, m_saveButton.getDisabledReason());
1348        }
1349        return result;
1350    }
1351
1352    /**
1353     * Asks the user to confirm resetting all changes.<p>
1354     */
1355    void confirmCancel() {
1356
1357        if (m_saveButton.isEnabled()) {
1358            CmsConfirmDialog dialog = new CmsConfirmDialog(
1359                org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_DIALOG_RESET_TITLE_0),
1360                org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_DIALOG_RESET_TEXT_0));
1361            dialog.setHandler(new I_CmsConfirmDialogHandler() {
1362
1363                public void onClose() {
1364
1365                    // nothing to do
1366                }
1367
1368                public void onOk() {
1369
1370                    cancelEdit();
1371                }
1372            });
1373            dialog.center();
1374        } else {
1375            cancelEdit();
1376        }
1377    }
1378
1379    /**
1380     * Opens the confirm delete locale dialog.<p>
1381     */
1382    void confirmDeleteLocale() {
1383
1384        CmsConfirmDialog dialog = new CmsConfirmDialog(
1385            Messages.get().key(Messages.GUI_CONFIRM_DELETE_LOCALE_TITLE_0),
1386            Messages.get().key(Messages.GUI_CONFIRM_DELETE_LOCALE_TEXT_0));
1387        dialog.setHandler(new I_CmsConfirmDialogHandler() {
1388
1389            public void onClose() {
1390
1391                // nothing to do
1392            }
1393
1394            public void onOk() {
1395
1396                deleteCurrentLocale();
1397            }
1398        });
1399        dialog.center();
1400    }
1401
1402    /**
1403     * Copies the current entity values to the given locales.<p>
1404     *
1405     * @param targetLocales the target locales
1406     */
1407    void copyLocales(final Set<String> targetLocales) {
1408
1409        final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
1410        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
1411
1412            @Override
1413            public void execute() {
1414
1415                start(200, true);
1416                getService().copyLocale(targetLocales, entity, this);
1417            }
1418
1419            @Override
1420            protected void onResponse(Void result) {
1421
1422                stop(false);
1423            }
1424        };
1425        action.execute();
1426        for (String targetLocale : targetLocales) {
1427            String targetId = getIdForLocale(targetLocale);
1428            if (!m_entityId.equals(targetId)) {
1429                if (m_registeredEntities.contains(targetId)) {
1430                    unregistereEntity(targetId);
1431                }
1432                registerClonedEntity(m_entityId, targetId);
1433                m_registeredEntities.add(targetId);
1434                m_changedEntityIds.add(targetId);
1435                m_contentLocales.add(targetLocale);
1436                m_deletedEntities.remove(targetId);
1437                enableSave();
1438            }
1439        }
1440        initLocaleSelect();
1441    }
1442
1443    /**
1444     * Deferrers the save action to the end of the browser event queue.<p>
1445     */
1446    void deferSave() {
1447
1448        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
1449
1450            public void execute() {
1451
1452                save();
1453            }
1454        });
1455    }
1456
1457    /**
1458     * Deferrers the save and exit action to the end of the browser event queue.<p>
1459     */
1460    void deferSaveAndExit() {
1461
1462        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
1463
1464            public void execute() {
1465
1466                saveAndExit();
1467            }
1468        });
1469    }
1470
1471    /**
1472     * Deletes the current locale.<p>
1473     */
1474    void deleteCurrentLocale() {
1475
1476        // there has to remain at least one content locale
1477        if (m_contentLocales.size() > 1) {
1478            String deletedLocale = m_locale;
1479            m_contentLocales.remove(deletedLocale);
1480            m_registeredEntities.remove(m_entityId);
1481            m_changedEntityIds.remove(m_entityId);
1482            m_deletedEntities.add(m_entityId);
1483            getValidationHandler().getValidationContext().removeEntityId(m_entityId);
1484            unregistereEntity(m_entityId);
1485            enableSave();
1486            String nextLocale = null;
1487            if (m_registeredEntities.isEmpty()) {
1488                nextLocale = m_contentLocales.iterator().next();
1489            } else {
1490                nextLocale = CmsContentDefinition.getLocaleFromId(m_registeredEntities.iterator().next());
1491            }
1492            switchLocale(nextLocale);
1493        }
1494    }
1495
1496    /**
1497     * Disables the save buttons with the given message.<p>
1498     *
1499     * @param message the disabled message
1500     */
1501    void disableSave(String message) {
1502
1503        m_saveButton.disable(message);
1504        m_saveExitButton.disable(message);
1505    }
1506
1507    /**
1508     * Leaves the editor saving the content if necessary.<p>
1509     */
1510    void exitWithSaving() {
1511
1512        if (checkValidation()) {
1513            if (m_saveExitButton.isEnabled()) {
1514                saveAndExit();
1515            } else {
1516                cancelEdit();
1517            }
1518        }
1519    }
1520
1521    /**
1522     * Handles validation changes.<p>
1523     *
1524     * @param validationContext the changed validation context
1525     */
1526    void handleValidationChange(CmsValidationContext validationContext) {
1527
1528        if (validationContext.hasValidationErrors()) {
1529            String locales = "";
1530            for (String id : validationContext.getInvalidEntityIds()) {
1531
1532                locales += "\n";
1533
1534                String locale = CmsContentDefinition.getLocaleFromId(id);
1535                if (m_availableLocales.containsKey(locale)) {
1536                    locales += m_availableLocales.get(locale);
1537                    locales += ": " + validationContext.getInvalidFields(id);
1538                }
1539            }
1540            disableSave(Messages.get().key(Messages.GUI_TOOLBAR_VALIDATION_ERRORS_1, locales));
1541        } else if (!m_changedEntityIds.isEmpty()) {
1542            enableSave();
1543        }
1544    }
1545
1546    /**
1547     * Hides the editor help bubbles.<p>
1548     *
1549     * @param hide <code>true</code> to hide the help bubbles
1550     */
1551    void hideHelpBubbles(boolean hide) {
1552
1553        setShowEditorHelp(!hide);
1554        CmsValueFocusHandler.getInstance().hideHelpBubbles(RootPanel.get(), hide);
1555        if (!hide) {
1556            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_SHOWN_0));
1557        } else {
1558            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_HIDDEN_0));
1559        }
1560    }
1561
1562    /**
1563     * Initializes the editor.<p>
1564     *
1565     * @param context the editor context
1566     * @param contentDefinition the content definition
1567     * @param formParent the inline form parent panel, used for inline editing only
1568     * @param inline <code>true</code> to render the editor for inline editing
1569     * @param mainLocale the main language to copy in case the element language node does not exist yet
1570     */
1571    void initEditor(
1572        CmsEditorContext context,
1573        CmsContentDefinition contentDefinition,
1574        I_CmsInlineFormParent formParent,
1575        boolean inline,
1576        String mainLocale) {
1577
1578        m_context = context;
1579        m_locale = contentDefinition.getLocale();
1580        m_entityId = contentDefinition.getEntityId();
1581        m_deleteOnCancel = contentDefinition.isDeleteOnCancel();
1582        m_autoUnlock = contentDefinition.isAutoUnlock();
1583        m_isDirectEdit = contentDefinition.isDirectEdit();
1584        CmsDomUtil.getHtmlElement().addClassName(EDITOR_MARKER_CLASS);
1585
1586        initClosingHandler();
1587        setContentDefinition(contentDefinition);
1588        initToolbar();
1589        if (inline && (formParent != null)) {
1590            if ((mainLocale != null)
1591                && (CmsDomUtil.querySelector(
1592                    "[" + CmsGwtConstants.ATTR_DATA_ID + "^='" + m_entityId + "']",
1593                    formParent.getElement()) == null)) {
1594                // in case a main locale is given and there are not any HTML elements attributed to the current entity id,
1595                // check if the content was rendered for the main locale
1596                CmsUUID structureId = CmsContentDefinition.entityIdToUuid(m_entityId);
1597                String mainLocaleEntityId = CmsContentDefinition.uuidToEntityId(structureId, mainLocale);
1598                NodeList<Element> elements = CmsDomUtil.querySelectorAll(
1599                    "[" + CmsGwtConstants.ATTR_DATA_ID + "^='" + mainLocaleEntityId + "']",
1600                    formParent.getElement());
1601                if (elements.getLength() > 0) {
1602                    for (int i = 0; i < elements.getLength(); i++) {
1603                        Element element = elements.getItem(i);
1604                        element.setAttribute(
1605                            CmsGwtConstants.ATTR_DATA_ID,
1606                            element.getAttribute(CmsGwtConstants.ATTR_DATA_ID).replace(mainLocaleEntityId, m_entityId));
1607                    }
1608                }
1609
1610            }
1611            initEditOverlay(formParent.getElement());
1612            addOverlayClickHandler(new ClickHandler() {
1613
1614                public void onClick(ClickEvent event) {
1615
1616                    exitWithSaving();
1617                }
1618            });
1619            m_hideHelpBubblesButton.setVisible(false);
1620            setNativeResourceInfo(m_sitePath, m_locale);
1621            initEntityObserver();
1622            if (m_definitions.get(m_locale).hasEditorChangeHandlers()) {
1623                initEditorChangeHandlers(m_definitions.get(m_locale).getEditorChangeScopes());
1624            }
1625            renderInlineEntity(m_entityId, formParent);
1626        } else {
1627            initFormPanel();
1628            renderFormContent();
1629            fixFocus();
1630        }
1631        if (contentDefinition.isPerformedAutocorrection()) {
1632            CmsNotification.get().send(
1633                CmsNotification.Type.NORMAL,
1634                Messages.get().key(Messages.GUI_WARN_INVALID_XML_STRUCTURE_0));
1635            setChanged();
1636        }
1637    }
1638
1639    /**
1640     * Initializes the editor change handler.<p>
1641     *
1642     * @param changeScopes the scopes to watch for changes
1643     */
1644    void initEditorChangeHandlers(Collection<String> changeScopes) {
1645
1646        if (m_editorChangeHandler != null) {
1647            m_editorChangeHandler.clear();
1648        }
1649        m_editorChangeHandler = new EditorChangeHandler(getEntity(), changeScopes);
1650    }
1651
1652    /**
1653     * Initializes the entity observer.<p>
1654     */
1655    void initEntityObserver() {
1656
1657        if (m_entityObserver != null) {
1658            m_entityObserver.clear();
1659        }
1660        m_entityObserver = new CmsEntityObserver(getCurrentEntity());
1661        exportObserver();
1662    }
1663
1664    /**
1665     * Opens the form based editor.<p>
1666     */
1667    void initFormPanel() {
1668
1669        removeEditOverlays();
1670        m_openFormButton.setVisible(false);
1671        m_saveButton.setVisible(true);
1672        m_hideHelpBubblesButton.setVisible(true);
1673        m_undoButton.setVisible(true);
1674        m_redoButton.setVisible(true);
1675        m_basePanel = new FlowPanel();
1676        m_basePanel.addStyleName(I_CmsLayoutBundle.INSTANCE.editorCss().basePanel());
1677        m_basePanel.addStyleName(I_CmsLayoutBundle.INSTANCE.generalCss().opencms());
1678        // insert base panel before the tool bar to keep the tool bar visible
1679        RootPanel.get().add(m_basePanel);
1680        if (m_isStandAlone) {
1681            RootPanel.getBodyElement().addClassName(I_CmsLayoutBundle.INSTANCE.editorCss().standAloneEditor());
1682        } else {
1683            RootPanel.getBodyElement().getParentElement().getStyle().setOverflow(Overflow.HIDDEN);
1684        }
1685    }
1686
1687    /**
1688     * Opens the copy locale dialog.<p>
1689     */
1690    void openCopyLocaleDialog() {
1691
1692        CmsCopyLocaleDialog dialog = new CmsCopyLocaleDialog(
1693            m_availableLocales,
1694            m_contentLocales,
1695            m_locale,
1696            m_definitions.get(m_locale).hasSynchronizedElements(),
1697            this);
1698        dialog.center();
1699    }
1700
1701    /**
1702     * Opens the model file select dialog.<p>
1703     *
1704     * @param context the editor context
1705     * @param definition the content definition
1706     */
1707    void openModelSelectDialog(final CmsEditorContext context, final CmsContentDefinition definition) {
1708
1709        I_CmsModelSelectHandler handler = new I_CmsModelSelectHandler() {
1710
1711            public void onModelSelect(CmsUUID modelStructureId) {
1712
1713                if (modelStructureId == null) {
1714                    modelStructureId = CmsUUID.getNullUUID();
1715                }
1716                openFormEditor(
1717                    context,
1718                    definition.getLocale(),
1719                    definition.getReferenceResourceId().toString(),
1720                    m_clientId,
1721                    definition.getNewLink(),
1722                    modelStructureId,
1723                    null,
1724                    null,
1725                    null,
1726
1727                    null,
1728                    m_onClose);
1729            }
1730        };
1731        String title = org.opencms.gwt.client.Messages.get().key(
1732            org.opencms.gwt.client.Messages.GUI_MODEL_SELECT_TITLE_0);
1733        String message = org.opencms.gwt.client.Messages.get().key(
1734            org.opencms.gwt.client.Messages.GUI_MODEL_SELECT_MESSAGE_0);
1735        CmsModelSelectDialog dialog = new CmsModelSelectDialog(handler, definition.getModelInfos(), title, message);
1736        dialog.center();
1737    }
1738
1739    /**
1740     * Previews the native event to enable keyboard short cuts.<p>
1741     *
1742     * @param event the event
1743     */
1744    void previewNativeEvent(NativePreviewEvent event) {
1745
1746        Event nativeEvent = Event.as(event.getNativeEvent());
1747        if (event.getTypeInt() == Event.ONKEYDOWN) {
1748            int keyCode = nativeEvent.getKeyCode();
1749            if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
1750                // look for short cuts
1751                if (nativeEvent.getShiftKey()) {
1752                    if (keyCode == KeyCodes.KEY_S) {
1753
1754                        exitWithSaving();
1755                        nativeEvent.preventDefault();
1756                        nativeEvent.stopPropagation();
1757                    } else if (keyCode == KeyCodes.KEY_X) {
1758                        confirmCancel();
1759                        nativeEvent.preventDefault();
1760                        nativeEvent.stopPropagation();
1761                    }
1762                } else if (keyCode == KeyCodes.KEY_S) {
1763                    if (checkValidation()) {
1764                        save();
1765                    }
1766                    nativeEvent.preventDefault();
1767                    nativeEvent.stopPropagation();
1768                }
1769
1770            }
1771        }
1772    }
1773
1774    /**
1775     * Renders the form content.<p>
1776     */
1777    void renderFormContent() {
1778
1779        initLocaleSelect();
1780        setNativeResourceInfo(m_sitePath, m_locale);
1781        m_contentInfoHeader = new CmsInfoHeader(m_title, null, m_sitePath, m_locale, m_iconClasses);
1782        m_basePanel.add(m_contentInfoHeader);
1783        if (m_context.isReusedElement()) {
1784            String message = Messages.get().key(Messages.GUI_CONTENT_EDITOR_REUSE_MARKER_0);
1785            Label label = new Label(message);
1786            label.addStyleName("oc-editor-reuse-marker");
1787            m_contentInfoHeader.addWidget(label);
1788        }
1789        SimplePanel content = new SimplePanel();
1790        content.setStyleName(org.opencms.acacia.client.css.I_CmsLayoutBundle.INSTANCE.form().formParent());
1791        m_basePanel.add(content);
1792        initEntityObserver();
1793        if (m_definitions.get(m_locale).hasEditorChangeHandlers()) {
1794            initEditorChangeHandlers(m_definitions.get(m_locale).getEditorChangeScopes());
1795        }
1796        renderEntityForm(m_entityId, m_tabInfos, content, m_basePanel.getElement());
1797        if ((m_clientId != null) && (getFormTabs() != null)) {
1798
1799            CmsListInfoBean pageInfo = CmsContainerpageController.get().getData().getPageInfo();
1800            m_pageInfoHeader = new CmsInfoHeader(
1801                pageInfo.getTitle(),
1802                null,
1803                pageInfo.getSubTitle(),
1804                CmsContainerpageController.get().getData().getLocale(),
1805                pageInfo.getBigIconClasses());
1806            updateInfoHeader(CmsContentDefinition.SETTINGS_TAB_ID.equals(getFormTabs().getSelectedId()));
1807            getFormTabs().addSelectionHandler(new SelectionHandler<Integer>() {
1808
1809                public void onSelection(SelectionEvent<Integer> event) {
1810
1811                    updateInfoHeader(CmsContentDefinition.SETTINGS_TAB_ID.equals(getFormTabs().getSelectedId()));
1812                }
1813            });
1814        }
1815    }
1816
1817    /**
1818     * Saves the content and closes the editor.<p>
1819     */
1820    void save() {
1821
1822        saveAndDeleteEntities(false, new I_CmsSimpleCallback<Boolean>() {
1823
1824            public void execute(Boolean hasChangedSettings) {
1825
1826                setSaved();
1827                setUnchanged();
1828                m_hasChangedSettings = m_hasChangedSettings || hasChangedSettings.booleanValue();
1829            }
1830        });
1831    }
1832
1833    /**
1834     * Saves the content and closes the editor.<p>
1835     */
1836    void saveAndExit() {
1837
1838        boolean unlock = shouldUnlockAutomatically();
1839        // store the scroll position
1840        final int scrollTop = RootPanel.getBodyElement().getOwnerDocument().getScrollTop();
1841        saveAndDeleteEntities(unlock, new I_CmsSimpleCallback<Boolean>() {
1842
1843            public void execute(final Boolean hasChangedSettings) {
1844
1845                setSaved();
1846                if (m_onClose != null) {
1847                    m_onClose.onClose(
1848                        m_hasChangedSettings || hasChangedSettings.booleanValue(),
1849                        /*publishDialog=*/false);
1850                }
1851                clearEditor();
1852                // restore the scroll position
1853                RootPanel.getBodyElement().getOwnerDocument().setScrollTop(scrollTop);
1854            }
1855        });
1856    }
1857
1858    /**
1859     * Sets the has changed flag and enables the save button.<p>
1860     */
1861    void setChanged() {
1862
1863        enableSave();
1864        m_changedEntityIds.add(m_entityId);
1865        m_deletedEntities.remove(m_entityId);
1866        updateOverlayPosition();
1867        setEditorState(true);
1868    }
1869
1870    /**
1871     * Sets the content definition.<p>
1872     *
1873     * @param definition the content definition
1874     */
1875    void setContentDefinition(CmsContentDefinition definition) {
1876
1877        if (m_availableLocales.isEmpty()) {
1878            // only set the locales when initially setting the content definition
1879            m_availableLocales.putAll(definition.getAvailableLocales());
1880            m_contentLocales.addAll(definition.getContentLocales());
1881        } else {
1882            m_contentLocales.add(definition.getLocale());
1883        }
1884        m_title = definition.getTitle();
1885        m_sitePath = definition.getSitePath();
1886        m_resourceTypeName = definition.getResourceType();
1887        m_registeredEntities.add(definition.getEntityId());
1888        m_tabInfos = definition.getTabInfos();
1889        m_iconClasses = definition.getIconClasses();
1890        addContentDefinition(definition);
1891        CmsDefaultWidgetService service = (CmsDefaultWidgetService)getWidgetService();
1892        service.addConfigurations(definition.getConfigurations());
1893        service.setSyncValues(definition.getSyncValues());
1894        service.setSkipPaths(definition.getSkipPaths());
1895        addEntityChangeHandler(definition.getEntityId(), new ValueChangeHandler<CmsEntity>() {
1896
1897            public void onValueChange(ValueChangeEvent<CmsEntity> event) {
1898
1899                setChanged();
1900            }
1901        });
1902    }
1903
1904    /**
1905     * Removes the delete on cancel flag for new resources.<p>
1906     */
1907    void setSaved() {
1908
1909        setEditorState(false);
1910        m_deleteOnCancel = false;
1911    }
1912
1913    /**
1914     * Call after save.<p>
1915     */
1916    void setUnchanged() {
1917
1918        m_changedEntityIds.clear();
1919        m_deletedEntities.clear();
1920        disableSave(Messages.get().key(Messages.GUI_TOOLBAR_NOTHING_CHANGED_0));
1921    }
1922
1923    /**
1924     * Enables and disabler the undo redo buttons according to the state.<p>
1925     *
1926     * @param state the undo redo state
1927     */
1928    void setUndoRedoState(UndoRedoState state) {
1929
1930        if (state.hasUndo()) {
1931            m_undoButton.enable();
1932        } else {
1933            m_undoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_UNDO_DISABLED_0));
1934        }
1935        if (state.hasRedo()) {
1936            m_redoButton.enable();
1937        } else {
1938            m_redoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_REDO_DISABLED_0));
1939        }
1940    }
1941
1942    /**
1943     * Returns true if the edited resource should be unlocked automatically after pressing Save/Exit.<p>
1944     *
1945     * @return true if the edited resource should be unlocked automatically
1946     */
1947    boolean shouldUnlockAutomatically() {
1948
1949        if (m_isStandAlone) {
1950            if (m_isDirectEdit) {
1951                // Classic direct edit case - always unlock
1952                return true;
1953            } else {
1954                // Workplace case - determined by configuration
1955                return m_autoUnlock;
1956            }
1957        }
1958        // Container page case - always unlock
1959        return true;
1960    }
1961
1962    /**
1963     * Shows the locked resource error message.<p>
1964     */
1965    void showLockedResourceMessage() {
1966
1967        CmsErrorDialog dialog = new CmsErrorDialog(
1968            Messages.get().key(Messages.ERR_RESOURCE_ALREADY_LOCKED_BY_OTHER_USER_0),
1969            null);
1970        dialog.addCloseHandler(new CloseHandler<PopupPanel>() {
1971
1972            public void onClose(CloseEvent<PopupPanel> event) {
1973
1974                cancelEdit();
1975            }
1976        });
1977        dialog.center();
1978    }
1979
1980    /**
1981     * Shows the validation error dialog.<p>
1982     *
1983     * @param validationResult the validation result
1984     */
1985    void showValidationErrorDialog(CmsValidationResult validationResult) {
1986
1987        if (validationResult.getErrors().keySet().contains(m_entityId)) {
1988            getValidationHandler().displayValidation(m_entityId, validationResult);
1989        }
1990        String errorLocales = "";
1991        for (String entityId : validationResult.getErrors().keySet()) {
1992            String locale = CmsContentDefinition.getLocaleFromId(entityId);
1993            errorLocales += m_availableLocales.get(locale) + ", ";
1994        }
1995        // remove trailing ','
1996        errorLocales = errorLocales.substring(0, errorLocales.length() - 2);
1997        CmsErrorDialog dialog = new CmsErrorDialog(
1998            Messages.get().key(Messages.GUI_VALIDATION_ERROR_1, errorLocales),
1999            null);
2000        dialog.center();
2001    }
2002
2003    /**
2004     * Switches to the selected locale. Will save changes first.<p>
2005     *
2006     * @param locale the locale to switch to
2007     */
2008    void switchLocale(final String locale) {
2009
2010        if (locale.equals(m_locale)) {
2011            return;
2012        }
2013        final Integer oldTabIndex = getTabIndex();
2014        m_locale = locale;
2015        m_basePanel.clear();
2016        destroyForm(false);
2017        final CmsEntity entity = m_entityBackend.getEntity(m_entityId);
2018        m_entityId = getIdForLocale(locale);
2019        // if the content does not contain the requested locale yet, a new node will be created
2020        final boolean addedNewLocale = !m_contentLocales.contains(locale);
2021        if (m_registeredEntities.contains(m_entityId)) {
2022            unregistereEntity(m_entityId);
2023        }
2024        if (addedNewLocale) {
2025            loadNewDefinition(m_entityId, entity, new I_CmsSimpleCallback<CmsContentDefinition>() {
2026
2027                public void execute(final CmsContentDefinition contentDefinition) {
2028
2029                    setContentDefinition(contentDefinition);
2030                    renderFormContent();
2031                    if (oldTabIndex != null) {
2032                        if (oldTabIndex.intValue() < getFormTabs().getTabCount()) {
2033                            getFormTabs().selectTab(oldTabIndex.intValue());
2034                        }
2035                    }
2036                    setChanged();
2037
2038                }
2039            });
2040        } else {
2041            loadDefinition(m_entityId, entity, new I_CmsSimpleCallback<CmsContentDefinition>() {
2042
2043                public void execute(CmsContentDefinition contentDefinition) {
2044
2045                    setContentDefinition(contentDefinition);
2046                    renderFormContent();
2047                    if (oldTabIndex != null) {
2048                        if (oldTabIndex.intValue() < getFormTabs().getTabCount()) {
2049                            getFormTabs().selectTab(oldTabIndex.intValue());
2050                        }
2051                    }
2052                }
2053
2054            });
2055        }
2056    }
2057
2058    /**
2059     * Synchronizes the locale independent fields to the other locales.<p>
2060     */
2061    void synchronizeCurrentLocale() {
2062
2063        m_basePanel.clear();
2064        destroyForm(false);
2065        CmsEntity entity = m_entityBackend.getEntity(m_entityId);
2066        m_entityId = getIdForLocale(m_locale);
2067        ((CmsDefaultWidgetService)getWidgetService()).setSkipPaths(Collections.<String> emptyList());
2068        loadDefinition(m_entityId, entity, new I_CmsSimpleCallback<CmsContentDefinition>() {
2069
2070            public void execute(CmsContentDefinition contentDefinition) {
2071
2072                setContentDefinition(contentDefinition);
2073                renderFormContent();
2074                setChanged();
2075            }
2076        });
2077    }
2078
2079    /**
2080     * Unlocks the edited resource.<p>
2081     */
2082    void unlockResource() {
2083
2084        if (!shouldUnlockAutomatically()) {
2085            return;
2086        }
2087        if (m_entityId != null) {
2088            final CmsUUID structureId = CmsContentDefinition.entityIdToUuid(m_entityId);
2089            if (m_deleteOnCancel) {
2090                CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
2091
2092                    @Override
2093                    public void execute() {
2094
2095                        CmsCoreProvider.getVfsService().syncDeleteResource(structureId, this);
2096                    }
2097
2098                    @Override
2099                    protected void onResponse(Void result) {
2100
2101                        // nothing to do
2102                    }
2103                };
2104                action.executeSync();
2105            } else {
2106                CmsCoreProvider.get().unlock(structureId);
2107            }
2108        }
2109    }
2110
2111    /**
2112     * Updates the editor values.<p>
2113     *
2114     * @param previous the previous entity state
2115     * @param updated the updated entity state
2116     */
2117    void updateEditorValues(CmsEntity previous, CmsEntity updated) {
2118
2119        if (!m_isDirectEdit && updated.getId().equals(m_entityId)) {
2120            // only apply the changes to the same locale entity
2121            updateEditorValues(previous, updated, getEntity(), Collections.<String> emptyList());
2122        }
2123    }
2124
2125    /**
2126     * Updates the info header.<p>
2127     *
2128     * @param isSettingsTabSelected <code>true</code> if the element settings tab is selected
2129     */
2130    void updateInfoHeader(boolean isSettingsTabSelected) {
2131
2132        m_basePanel.remove(0);
2133        m_basePanel.insert(isSettingsTabSelected ? m_pageInfoHeader : m_contentInfoHeader, 0);
2134    }
2135
2136    /**
2137     * Adds the change listener to the observer.<p>
2138     *
2139     * @param changeListener the change listener
2140     * @param changeScope the change scope
2141     */
2142    private void addChangeListener(JavaScriptObject changeListener, String changeScope) {
2143
2144        try {
2145            System.out.println("Adding native listener for scope " + changeScope);
2146            m_entityObserver.addEntityChangeListener(new CmsEntityChangeListenerWrapper(changeListener), changeScope);
2147        } catch (Exception e) {
2148
2149            CmsDebugLog.getInstance().printLine("Exception occured during listener registration" + e.getMessage());
2150        }
2151    }
2152
2153    /**
2154     * Changes a simple type entity value.<p>
2155     *
2156     * @param attributeName the attribute name
2157     * @param index the value index
2158     * @param value the value
2159     * @param parentPathElements the parent path elements
2160     */
2161    private void changeSimpleValue(String attributeName, int index, String value, List<String> parentPathElements) {
2162
2163        CmsAttributeHandler handler = getAttributeHandler(attributeName, parentPathElements);
2164        handler.changeValue(value, index);
2165    }
2166
2167    /**
2168     * Closes the editor.<p>
2169     */
2170    private native void closeEditorWidow() /*-{
2171        if ($wnd.top.cms_ade_closeEditorDialog) {
2172            $wnd.top.cms_ade_closeEditorDialog();
2173        } else if ($wnd.parent.parent.cms_ade_closeEditorDialog) {
2174            $wnd.parent.parent.cms_ade_closeEditorDialog();
2175        } else {
2176            var backlink = $wnd[@org.opencms.ade.contenteditor.shared.rpc.I_CmsContentService::PARAM_BACKLINK];
2177            if (backlink) {
2178                $wnd.top.location.href = backlink;
2179            }
2180        }
2181    }-*/;
2182
2183    /**
2184     * Creates a push button for the edit tool-bar.<p>
2185     *
2186     * @param title the button title
2187     * @param imageClass the image class
2188     *
2189     * @return the button
2190     */
2191    private CmsPushButton createButton(String title, String imageClass) {
2192
2193        CmsPushButton result = new CmsPushButton();
2194        result.setTitle(title);
2195        result.setImageClass(imageClass);
2196        result.setButtonStyle(ButtonStyle.FONT_ICON, null);
2197        result.setSize(Size.big);
2198        return result;
2199    }
2200
2201    /**
2202     * Enables the save buttons.<p>
2203     */
2204    private void enableSave() {
2205
2206        m_saveButton.enable();
2207        m_saveExitButton.enable();
2208    }
2209
2210    /**
2211     * Exports the add entity change listener method.<p>
2212     */
2213    private native void exportObserver()/*-{
2214        var self = this;
2215        $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::ADD_CHANGE_LISTENER_METHOD] = function(
2216                listener, scope) {
2217            var wrapper = {
2218                onChange : listener.onChange
2219            }
2220            self.@org.opencms.ade.contenteditor.client.CmsContentEditor::addChangeListener(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;)(wrapper, scope);
2221        }
2222        $wnd[@org.opencms.ade.contenteditor.client.CmsContentEditor::GET_CURRENT_ENTITY_METHOD] = function() {
2223            return new $wnd.acacia.CmsEntityWrapper(
2224                    self.@org.opencms.ade.contenteditor.client.CmsContentEditor::getCurrentEntity()());
2225        }
2226    }-*/;
2227
2228    /**
2229     * Returns the attribute handler for the given attribute.<p>
2230     *
2231     * @param attributeName the attribute name
2232     * @param parentPathElements the parent path elements
2233     *
2234     * @return the attribute handler
2235     */
2236    private CmsAttributeHandler getAttributeHandler(String attributeName, List<String> parentPathElements) {
2237
2238        List<String> childPathElements = new ArrayList<String>(parentPathElements);
2239        childPathElements.add(attributeName);
2240        CmsAttributeHandler handler = getRootAttributeHandler().getHandlerByPath(
2241            childPathElements.toArray(new String[childPathElements.size()]));
2242        return handler;
2243    }
2244
2245    /**
2246     * Returns the entity id for the given locale.<p>
2247     *
2248     * @param locale the locale
2249     *
2250     * @return the entity id
2251     */
2252    private String getIdForLocale(String locale) {
2253
2254        return CmsContentDefinition.uuidToEntityId(CmsContentDefinition.entityIdToUuid(m_entityId), locale);
2255    }
2256
2257    /**
2258     * Returns the selected tab index, or null if there are no tabs.
2259     *
2260     * @return the selected tab index or null
2261     */
2262    private Integer getTabIndex() {
2263
2264        if (getFormTabs() != null) {
2265            return Integer.valueOf(getFormTabs().getSelectedIndex());
2266        }
2267        return null;
2268    }
2269
2270    /**
2271     * Initializes the window closing handler to ensure the resource will be unlocked when leaving the editor.<p>
2272     */
2273    private void initClosingHandler() {
2274
2275        m_closingHandlerRegistration = Window.addWindowClosingHandler(new ClosingHandler() {
2276
2277            /**
2278             * @see com.google.gwt.user.client.Window.ClosingHandler#onWindowClosing(com.google.gwt.user.client.Window.ClosingEvent)
2279             */
2280            public void onWindowClosing(ClosingEvent event) {
2281
2282                unlockResource();
2283            }
2284        });
2285    }
2286
2287    /**
2288     * Initializes the event preview handler.<p>
2289     */
2290    private void initEventPreviewHandler() {
2291
2292        m_previewHandlerRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() {
2293
2294            public void onPreviewNativeEvent(NativePreviewEvent event) {
2295
2296                previewNativeEvent(event);
2297            }
2298        });
2299    }
2300
2301    /**
2302     * Initializes the locale selector.<p>
2303     */
2304    private void initLocaleSelect() {
2305
2306        if (m_availableLocales.size() < 2) {
2307            return;
2308        }
2309        Map<String, String> selectOptions = new HashMap<String, String>();
2310        for (Entry<String, String> localeEntry : m_availableLocales.entrySet()) {
2311            if (m_contentLocales.contains(localeEntry.getKey())) {
2312                selectOptions.put(localeEntry.getKey(), localeEntry.getValue());
2313            } else {
2314                selectOptions.put(localeEntry.getKey(), localeEntry.getValue() + " [-]");
2315            }
2316        }
2317        if (m_localeSelect == null) {
2318            m_localeSelect = new CmsSelectBox(selectOptions);
2319            m_toolbar.insertRight(m_localeSelect, 1);
2320            m_localeSelect.addStyleName(I_CmsLayoutBundle.INSTANCE.generalCss().inlineBlock());
2321            m_localeSelect.getElement().getStyle().setWidth(100, Unit.PX);
2322            m_localeSelect.getElement().getStyle().setVerticalAlign(VerticalAlign.MIDDLE);
2323            m_localeSelect.addValueChangeHandler(new ValueChangeHandler<String>() {
2324
2325                public void onValueChange(ValueChangeEvent<String> event) {
2326
2327                    switchLocale(event.getValue());
2328                }
2329            });
2330        } else {
2331            m_localeSelect.setItems(selectOptions);
2332        }
2333        m_localeSelect.setFormValueAsString(m_locale);
2334        if (m_deleteLocaleButton == null) {
2335            m_deleteLocaleButton = createButton(
2336                Messages.get().key(Messages.GUI_TOOLBAR_DELETE_LOCALE_0),
2337                "opencms-icon-remove-locale");
2338            m_deleteLocaleButton.addClickHandler(new ClickHandler() {
2339
2340                public void onClick(ClickEvent event) {
2341
2342                    confirmDeleteLocale();
2343                }
2344            });
2345            m_toolbar.insertRight(m_deleteLocaleButton, 2);
2346        }
2347        if (m_contentLocales.size() > 1) {
2348            m_deleteLocaleButton.enable();
2349        } else {
2350            m_deleteLocaleButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_CANT_DELETE_LAST_LOCALE_0));
2351        }
2352        if (m_copyLocaleButton == null) {
2353            m_copyLocaleButton = createButton(
2354                I_CmsButton.ButtonData.COPY_LOCALE_BUTTON.getTitle(),
2355                I_CmsButton.ButtonData.COPY_LOCALE_BUTTON.getIconClass());
2356            m_copyLocaleButton.addClickHandler(new ClickHandler() {
2357
2358                public void onClick(ClickEvent event) {
2359
2360                    openCopyLocaleDialog();
2361                }
2362            });
2363            m_toolbar.insertRight(m_copyLocaleButton, 3);
2364        }
2365
2366    }
2367
2368    /**
2369     * Generates the button bar displayed beneath the editable fields.<p>
2370     */
2371    private void initToolbar() {
2372
2373        m_toolbar = new CmsToolbar();
2374        m_toolbar.setAppTitle(Messages.get().key(Messages.GUI_CONTENT_EDITOR_TITLE_0));
2375        m_publishButton = createButton(
2376            I_CmsButton.ButtonData.PUBLISH_BUTTON.getTitle(),
2377            I_CmsButton.ButtonData.PUBLISH_BUTTON.getIconClass());
2378        m_toolbar.addLeft(m_publishButton);
2379        m_publishButton.addClickHandler(new ClickHandler() {
2380
2381            public void onClick(ClickEvent event) {
2382
2383                boolean unlock = shouldUnlockAutomatically();
2384
2385                saveAndDeleteEntities(unlock, new I_CmsSimpleCallback<Boolean>() {
2386
2387                    public void execute(final Boolean hasChangedSeetings) {
2388
2389                        setSaved();
2390                        HashMap<String, String> params = new HashMap<String, String>(
2391                            getContext().getPublishParameters());
2392                        CmsUUID structureId = CmsContentDefinition.entityIdToUuid(getEntityId());
2393                        params.put(CmsPublishOptions.PARAM_CONTENT, "" + structureId);
2394                        params.put(CmsPublishOptions.PARAM_START_WITH_CURRENT_PAGE, "");
2395                        CmsPublishDialog.showPublishDialog(params, new CloseHandler<PopupPanel>() {
2396
2397                            public void onClose(CloseEvent<PopupPanel> closeEvent) {
2398
2399                                if (m_onClose != null) {
2400                                    m_onClose.onClose(
2401                                        m_hasChangedSettings || hasChangedSeetings.booleanValue(),
2402                                        /*publishDialog=*/true);
2403                                }
2404                                clearEditor();
2405                            }
2406                        }, new Runnable() {
2407
2408                            public void run() {
2409
2410                                // ignore
2411                            }
2412
2413                        }, null);
2414
2415                    }
2416                });
2417
2418            }
2419        });
2420        m_saveExitButton = createButton(
2421            Messages.get().key(Messages.GUI_TOOLBAR_SAVE_AND_EXIT_0),
2422            "opencms-icon-save-exit");
2423        m_saveExitButton.addClickHandler(new ClickHandler() {
2424
2425            public void onClick(ClickEvent event) {
2426
2427                deferSaveAndExit();
2428            }
2429        });
2430        m_toolbar.addLeft(m_saveExitButton);
2431        m_saveButton = createButton(
2432            Messages.get().key(Messages.GUI_TOOLBAR_SAVE_0),
2433            I_CmsButton.ButtonData.SAVE_BUTTON.getIconClass());
2434        m_saveButton.addClickHandler(new ClickHandler() {
2435
2436            public void onClick(ClickEvent event) {
2437
2438                deferSave();
2439            }
2440        });
2441        m_saveButton.setVisible(false);
2442        m_toolbar.addLeft(m_saveButton);
2443        disableSave(Messages.get().key(Messages.GUI_TOOLBAR_NOTHING_CHANGED_0));
2444        m_undoButton = createButton(Messages.get().key(Messages.GUI_TOOLBAR_UNDO_0), "opencms-icon-undo");
2445        m_undoButton.addClickHandler(new ClickHandler() {
2446
2447            public void onClick(ClickEvent event) {
2448
2449                if (CmsUndoRedoHandler.getInstance().isIntitalized()) {
2450                    CmsUndoRedoHandler.getInstance().undo();
2451                }
2452            }
2453        });
2454        m_undoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_UNDO_DISABLED_0));
2455        m_undoButton.setVisible(false);
2456        m_toolbar.addLeft(m_undoButton);
2457        m_redoButton = createButton(Messages.get().key(Messages.GUI_TOOLBAR_REDO_0), "opencms-icon-redo");
2458        m_redoButton.addClickHandler(new ClickHandler() {
2459
2460            public void onClick(ClickEvent event) {
2461
2462                if (CmsUndoRedoHandler.getInstance().isIntitalized()) {
2463                    CmsUndoRedoHandler.getInstance().redo();
2464                }
2465            }
2466        });
2467        m_redoButton.disable(Messages.get().key(Messages.GUI_TOOLBAR_REDO_DISABLED_0));
2468        m_redoButton.setVisible(false);
2469        m_toolbar.addLeft(m_redoButton);
2470
2471        m_undoRedoHandlerRegistration = CmsUndoRedoHandler.getInstance().addValueChangeHandler(
2472            new ValueChangeHandler<CmsUndoRedoHandler.UndoRedoState>() {
2473
2474                public void onValueChange(ValueChangeEvent<UndoRedoState> event) {
2475
2476                    setUndoRedoState(event.getValue());
2477                }
2478            });
2479        m_openFormButton = createButton(
2480            Messages.get().key(Messages.GUI_TOOLBAR_OPEN_FORM_0),
2481            I_CmsButton.ButtonData.EDIT.getIconClass());
2482        m_openFormButton.addClickHandler(new ClickHandler() {
2483
2484            public void onClick(ClickEvent event) {
2485
2486                initFormPanel();
2487                renderFormContent();
2488            }
2489        });
2490        m_toolbar.addLeft(m_openFormButton);
2491
2492        m_hideHelpBubblesButton = new CmsToggleButton();
2493
2494        m_hideHelpBubblesButton.setImageClass(I_CmsButton.ButtonData.TOGGLE_HELP.getIconClass());
2495        m_hideHelpBubblesButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
2496        m_hideHelpBubblesButton.setSize(Size.big);
2497        m_hideHelpBubblesButton.addClickHandler(new ClickHandler() {
2498
2499            public void onClick(ClickEvent event) {
2500
2501                CmsToggleButton button = (CmsToggleButton)event.getSource();
2502                hideHelpBubbles(!button.isDown());
2503            }
2504        });
2505        m_hideHelpBubblesButton.setDown(CmsCoreProvider.get().isShowEditorHelp());
2506        CmsValueFocusHandler.getInstance().hideHelpBubbles(RootPanel.get(), !CmsCoreProvider.get().isShowEditorHelp());
2507        if (!CmsCoreProvider.get().isShowEditorHelp()) {
2508            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_HIDDEN_0));
2509        } else {
2510            m_hideHelpBubblesButton.setTitle(Messages.get().key(Messages.GUI_TOOLBAR_HELP_BUBBLES_SHOWN_0));
2511        }
2512        m_toolbar.addRight(m_hideHelpBubblesButton);
2513
2514        m_cancelButton = createButton(
2515            Messages.get().key(Messages.GUI_TOOLBAR_RESET_0),
2516            I_CmsButton.ButtonData.RESET_BUTTON.getIconClass());
2517        m_cancelButton.addClickHandler(new ClickHandler() {
2518
2519            public void onClick(ClickEvent event) {
2520
2521                confirmCancel();
2522            }
2523        });
2524        m_toolbar.addRight(m_cancelButton);
2525        RootPanel.get().add(m_toolbar);
2526    }
2527
2528    /**
2529     * Sets the editor state.<p>
2530     *
2531     * @param changed if the content has been changed
2532     */
2533    private native void setEditorState(boolean changed)/*-{
2534        if (typeof $wnd.cmsSetEditorChangedState === 'function') {
2535            $wnd.cmsSetEditorChangedState(changed);
2536        }
2537    }-*/;
2538
2539    /**
2540     * Sets the resource info to native window context variables.<p>
2541     *
2542     * @param sitePath the site path
2543     * @param locale the content locale
2544     */
2545    private native void setNativeResourceInfo(String sitePath, String locale)/*-{
2546        $wnd._editResource = sitePath;
2547        $wnd._editLanguage = locale;
2548    }-*/;
2549
2550    /**
2551     * Updates the editor values according to the given entity.<p>
2552     *
2553     * @param previous the previous entity state
2554     * @param updated the updated entity state
2555     * @param target the target entity
2556     * @param parentPathElements the parent path elements
2557     */
2558    private void updateEditorValues(
2559        CmsEntity previous,
2560        CmsEntity updated,
2561        CmsEntity target,
2562        List<String> parentPathElements) {
2563
2564        for (String attributeName : m_entityBackend.getType(target.getTypeName()).getAttributeNames()) {
2565            CmsAttributeHandler handler = getAttributeHandler(attributeName, parentPathElements);
2566            if (handler == null) {
2567                // non visible attribute, skip it
2568                continue;
2569            }
2570            if (previous.hasAttribute(attributeName)
2571                && updated.hasAttribute(attributeName)
2572                && target.hasAttribute(attributeName)) {
2573                CmsEntityAttribute updatedAttribute = updated.getAttribute(attributeName);
2574                CmsEntityAttribute previousAttribute = previous.getAttribute(attributeName);
2575                CmsEntityAttribute targetAttribute = target.getAttribute(attributeName);
2576                if (updatedAttribute.isSimpleValue()) {
2577                    if ((updatedAttribute.getValueCount() == previousAttribute.getValueCount())
2578                        && (updatedAttribute.getValueCount() == targetAttribute.getValueCount())) {
2579                        for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2580                            if (!updatedAttribute.getSimpleValues().get(i).equals(
2581                                previousAttribute.getSimpleValues().get(i))
2582                                && previousAttribute.getSimpleValues().get(i).equals(
2583                                    targetAttribute.getSimpleValues().get(i))) {
2584
2585                                changeSimpleValue(
2586                                    attributeName,
2587                                    i,
2588                                    updatedAttribute.getSimpleValues().get(i),
2589                                    parentPathElements);
2590                            }
2591                        }
2592                    } else {
2593                        if (targetAttribute.getValueCount() == previousAttribute.getValueCount()) {
2594                            // only act, if the value count has not been altered while executing the server request
2595                            if (updatedAttribute.getValueCount() > previousAttribute.getValueCount()) {
2596                                // new values have been added
2597                                for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2598                                    if (i >= previousAttribute.getSimpleValues().size()) {
2599                                        handler.addNewAttributeValue(updatedAttribute.getSimpleValues().get(i));
2600                                    } else if (!updatedAttribute.getSimpleValues().get(i).equals(
2601                                        previousAttribute.getSimpleValues().get(i))
2602                                        && previousAttribute.getSimpleValues().get(i).equals(
2603                                            targetAttribute.getSimpleValues().get(i))) {
2604                                        changeSimpleValue(
2605                                            attributeName,
2606                                            i,
2607                                            updatedAttribute.getSimpleValues().get(i),
2608                                            parentPathElements);
2609                                    }
2610                                }
2611                            } else {
2612                                // values have been removed
2613                                for (int i = previousAttribute.getValueCount() - 1; i >= 0; i--) {
2614                                    if (i >= updatedAttribute.getSimpleValues().size()) {
2615                                        handler.removeAttributeValue(i);
2616                                    } else if (!updatedAttribute.getSimpleValues().get(i).equals(
2617                                        previousAttribute.getSimpleValues().get(i))
2618                                        && previousAttribute.getSimpleValues().get(i).equals(
2619                                            targetAttribute.getSimpleValues().get(i))) {
2620                                        changeSimpleValue(
2621                                            attributeName,
2622                                            i,
2623                                            updatedAttribute.getSimpleValues().get(i),
2624                                            parentPathElements);
2625                                    }
2626                                }
2627                            }
2628                        }
2629                    }
2630                } else {
2631                    if (targetAttribute.getValueCount() == previousAttribute.getValueCount()) {
2632                        // only act, if the value count has not been altered while executing the server request
2633                        if (updatedAttribute.getValueCount() > previousAttribute.getValueCount()) {
2634                            // new values have been added
2635                            for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2636                                if (i >= previousAttribute.getSimpleValues().size()) {
2637                                    handler.addNewAttributeValue(
2638                                        m_entityBackend.registerEntity(
2639                                            updatedAttribute.getComplexValues().get(i),
2640                                            true));
2641                                } else {
2642                                    List<String> childPathElements = new ArrayList<String>(parentPathElements);
2643                                    childPathElements.add(attributeName + "[" + i + "]");
2644                                    updateEditorValues(
2645                                        previousAttribute.getComplexValues().get(i),
2646                                        updatedAttribute.getComplexValues().get(i),
2647                                        targetAttribute.getComplexValues().get(i),
2648                                        childPathElements);
2649                                }
2650                            }
2651                        } else {
2652                            // values have been removed
2653                            for (int i = previousAttribute.getValueCount() - 1; i >= 0; i--) {
2654                                if (i >= updatedAttribute.getValueCount()) {
2655
2656                                    handler.removeAttributeValue(i);
2657                                } else {
2658                                    List<String> childPathElements = new ArrayList<String>(parentPathElements);
2659                                    childPathElements.add(attributeName + "[" + i + "]");
2660                                    updateEditorValues(
2661                                        previousAttribute.getComplexValues().get(i),
2662                                        updatedAttribute.getComplexValues().get(i),
2663                                        targetAttribute.getComplexValues().get(i),
2664                                        childPathElements);
2665                                }
2666                            }
2667                        }
2668                    }
2669                }
2670            } else if (previous.hasAttribute(attributeName) && target.hasAttribute(attributeName)) {
2671                for (int i = target.getAttribute(attributeName).getValueCount() - 1; i >= 0; i--) {
2672                    handler.removeAttributeValue(i);
2673                }
2674
2675            } else if (!previous.hasAttribute(attributeName)
2676                && !target.hasAttribute(attributeName)
2677                && updated.hasAttribute(attributeName)) //
2678            {
2679                CmsEntityAttribute updatedAttribute = updated.getAttribute(attributeName);
2680                for (int i = 0; i < updatedAttribute.getValueCount(); i++) {
2681                    if (updatedAttribute.isSimpleValue()) {
2682                        handler.addNewAttributeValue(updatedAttribute.getSimpleValues().get(i));
2683                    } else {
2684                        handler.addNewAttributeValue(
2685                            m_entityBackend.registerEntity(updatedAttribute.getComplexValues().get(i), true));
2686                    }
2687                }
2688            }
2689        }
2690    }
2691}