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