001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.acacia.client.ui;
029
030import org.opencms.acacia.client.CmsAttributeHandler;
031import org.opencms.acacia.client.CmsButtonBarHandler;
032import org.opencms.acacia.client.CmsChoiceMenuEntryBean;
033import org.opencms.acacia.client.CmsEditorBase;
034import org.opencms.acacia.client.CmsValueFocusHandler;
035import org.opencms.acacia.client.I_CmsEntityRenderer;
036import org.opencms.acacia.client.I_CmsWidgetService;
037import org.opencms.acacia.client.css.I_CmsLayoutBundle;
038import org.opencms.acacia.client.widgets.CmsFormWidgetWrapper;
039import org.opencms.acacia.client.widgets.I_CmsEditWidget;
040import org.opencms.acacia.client.widgets.I_CmsFormEditWidget;
041import org.opencms.acacia.client.widgets.I_CmsHasDisplayDirection;
042import org.opencms.acacia.client.widgets.I_CmsHasDisplayDirection.Direction;
043import org.opencms.acacia.shared.CmsEntity;
044import org.opencms.gwt.client.CmsCoreProvider;
045import org.opencms.gwt.client.I_CmsDescendantResizeHandler;
046import org.opencms.gwt.client.I_CmsHasResizeOnShow;
047import org.opencms.gwt.client.dnd.I_CmsDragHandle;
048import org.opencms.gwt.client.dnd.I_CmsDraggable;
049import org.opencms.gwt.client.dnd.I_CmsDropTarget;
050import org.opencms.gwt.client.ui.CmsPushButton;
051import org.opencms.gwt.client.ui.I_CmsButton;
052import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
053import org.opencms.gwt.client.util.CmsDomUtil;
054import org.opencms.gwt.client.util.CmsStyleVariable;
055import org.opencms.util.CmsStringUtil;
056
057import java.util.List;
058
059import com.google.common.base.Optional;
060import com.google.gwt.animation.client.Animation;
061import com.google.gwt.core.client.GWT;
062import com.google.gwt.core.client.Scheduler;
063import com.google.gwt.core.client.Scheduler.ScheduledCommand;
064import com.google.gwt.dom.client.DivElement;
065import com.google.gwt.dom.client.Document;
066import com.google.gwt.dom.client.Element;
067import com.google.gwt.dom.client.Node;
068import com.google.gwt.dom.client.SpanElement;
069import com.google.gwt.dom.client.Style;
070import com.google.gwt.dom.client.Style.Display;
071import com.google.gwt.dom.client.Style.Unit;
072import com.google.gwt.event.dom.client.ClickEvent;
073import com.google.gwt.event.dom.client.ClickHandler;
074import com.google.gwt.event.dom.client.FocusEvent;
075import com.google.gwt.event.dom.client.FocusHandler;
076import com.google.gwt.event.dom.client.HasMouseDownHandlers;
077import com.google.gwt.event.dom.client.HasMouseOutHandlers;
078import com.google.gwt.event.dom.client.HasMouseOverHandlers;
079import com.google.gwt.event.dom.client.MouseDownEvent;
080import com.google.gwt.event.dom.client.MouseDownHandler;
081import com.google.gwt.event.dom.client.MouseOutEvent;
082import com.google.gwt.event.dom.client.MouseOutHandler;
083import com.google.gwt.event.dom.client.MouseOverEvent;
084import com.google.gwt.event.dom.client.MouseOverHandler;
085import com.google.gwt.event.logical.shared.HasResizeHandlers;
086import com.google.gwt.event.logical.shared.ValueChangeEvent;
087import com.google.gwt.event.logical.shared.ValueChangeHandler;
088import com.google.gwt.event.shared.HandlerRegistration;
089import com.google.gwt.uibinder.client.UiBinder;
090import com.google.gwt.uibinder.client.UiField;
091import com.google.gwt.uibinder.client.UiHandler;
092import com.google.gwt.user.client.Timer;
093import com.google.gwt.user.client.Window;
094import com.google.gwt.user.client.rpc.AsyncCallback;
095import com.google.gwt.user.client.ui.Composite;
096import com.google.gwt.user.client.ui.FlowPanel;
097import com.google.gwt.user.client.ui.HTML;
098import com.google.gwt.user.client.ui.HTMLPanel;
099import com.google.gwt.user.client.ui.HasWidgets;
100import com.google.gwt.user.client.ui.Label;
101import com.google.gwt.user.client.ui.RootPanel;
102import com.google.gwt.user.client.ui.Widget;
103
104/**
105 * UI object holding an attribute value.<p>
106 */
107public class CmsAttributeValueView extends Composite
108implements I_CmsDraggable, I_CmsHasResizeOnShow, HasMouseOverHandlers, HasMouseOutHandlers, HasMouseDownHandlers {
109
110    /**
111     * The widget value change handler.<p>
112     */
113    protected class ChangeHandler implements ValueChangeHandler<String> {
114
115        /**
116         * @see com.google.gwt.event.logical.shared.ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)
117         */
118        public void onValueChange(ValueChangeEvent<String> event) {
119
120            getHandler().handleValueChange(CmsAttributeValueView.this, event.getValue());
121            removeValidationMessage();
122        }
123    }
124
125    /**
126     * Handles showing and hiding the help bubble on mouse over the title label.<p>
127     */
128    protected class LabelHoverHandler implements MouseOverHandler, MouseOutHandler {
129
130        /** Mouse in timer. */
131        Timer m_inTimer;
132
133        /** Mouse out timer. */
134        Timer m_outTimer;
135
136        /**
137         * @see com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google.gwt.event.dom.client.MouseOutEvent)
138         */
139        public void onMouseOut(MouseOutEvent event) {
140
141            if (m_inTimer != null) {
142                m_inTimer.cancel();
143                m_inTimer = null;
144            }
145            if (m_outTimer == null) {
146                m_outTimer = new Timer() {
147
148                    @Override
149                    public void run() {
150
151                        toggleLabelHover(false);
152                        m_outTimer = null;
153                    }
154                };
155                m_outTimer.schedule(200);
156            }
157        }
158
159        /**
160         * @see com.google.gwt.event.dom.client.MouseOverHandler#onMouseOver(com.google.gwt.event.dom.client.MouseOverEvent)
161         */
162        public void onMouseOver(MouseOverEvent event) {
163
164            if (m_outTimer != null) {
165                m_outTimer.cancel();
166                m_outTimer = null;
167            }
168            if (m_inTimer == null) {
169                m_inTimer = new Timer() {
170
171                    @Override
172                    public void run() {
173
174                        toggleLabelHover(true);
175                        m_inTimer = null;
176                    }
177                };
178                m_inTimer.schedule(400);
179            }
180        }
181    }
182
183    /** The move handle. */
184    protected class MoveHandle extends CmsPushButton implements I_CmsDragHandle {
185
186        /** The draggable. */
187        private CmsAttributeValueView m_draggable;
188
189        /**
190         * Constructor.<p>
191         *
192         * @param draggable the draggable
193         */
194        MoveHandle(CmsAttributeValueView draggable) {
195
196            setImageClass(I_CmsButton.MOVE_SMALL);
197            setButtonStyle(ButtonStyle.FONT_ICON, null);
198            if (CmsEditorBase.hasDictionary()) {
199                setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_MOVE_1, getLabel()));
200            }
201            m_draggable = draggable;
202        }
203
204        /**
205         * @see org.opencms.gwt.client.dnd.I_CmsDragHandle#getDraggable()
206         */
207        public I_CmsDraggable getDraggable() {
208
209            return m_draggable;
210        }
211
212    }
213
214    /**
215     * The UI binder interface.<p>
216     */
217    interface I_AttributeValueUiBinder extends UiBinder<HTMLPanel, CmsAttributeValueView> {
218        // nothing to do
219    }
220
221    public static final String ATTR_KEY = "data-key";
222
223    /** The first column compact view mode. */
224    public static final int COMPACT_MODE_FIRST_COLUMN = 1;
225
226    /** The nested compact view mode. */
227    public static final int COMPACT_MODE_NESTED = 3;
228
229    /** The second column compact view mode. */
230    public static final int COMPACT_MODE_SECOND_COLUMN = 2;
231
232    /** The single line compact view mode. */
233    public static final int COMPACT_MODE_SINGLE_LINE = 4;
234
235    /** The wide compact view mode. */
236    public static final int COMPACT_MODE_WIDE = 0;
237
238    /** The toolbar height. */
239    private static final int TOOLBAR_HEIGHT = 52;
240
241    /** Attribute for marking menu bars that are actually menu bars which should open on hovering (rather than just a single button). */
242    public static final String ATTR_HAS_HOVER = "data-has-hover-menu";
243
244    /** The UI binder instance. */
245    private static I_AttributeValueUiBinder uiBinder = GWT.create(I_AttributeValueUiBinder.class);
246
247    /** The add button. */
248    @UiField
249    protected CmsAttributeChoiceWidget m_addButton;
250
251    /** The attribute choice button. */
252    @UiField
253    protected CmsAttributeChoiceWidget m_attributeChoice;
254
255    /** The button bar. */
256    @UiField
257    protected FlowPanel m_buttonBar;
258
259    /** The down button. */
260    @UiField
261    protected CmsPushButton m_downButton;
262
263    /** The help bubble element. */
264    @UiField
265    protected DivElement m_helpBubble;
266
267    /** The help bubble close button. */
268    @UiField
269    protected CmsPushButton m_helpBubbleClose;
270
271    /** The help bubble text element. */
272    @UiField
273    protected DivElement m_helpBubbleText;
274
275    /** The message text element. */
276    @UiField
277    protected SpanElement m_messageText;
278
279    /** The move button. */
280    @UiField(provided = true)
281    protected MoveHandle m_moveButton;
282
283    /** The remove button. */
284    @UiField
285    protected CmsPushButton m_removeButton;
286
287    /** The up button. */
288    @UiField
289    protected CmsPushButton m_upButton;
290
291    /** The widget holder elemenet. */
292    @UiField
293    protected FlowPanel m_widgetHolder;
294
295    /** The currently running animation. */
296    Animation m_currentAnimation;
297
298    /** The activation mouse down handler registration. */
299    private HandlerRegistration m_activationHandlerRegistration;
300
301    /** Style variable to enable/disable 'collapsed' style. */
302    private CmsStyleVariable m_collapsedStyle = new CmsStyleVariable(this);
303
304    /** The compact view style variable. */
305    private CmsStyleVariable m_compacteModeStyle;
306
307    /** The default widget value. */
308    private String m_defaultValue;
309
310    /** Flag indicating if drag and drop is enabled for this attribute. */
311    private boolean m_dragEnabled;
312
313    /** Drag and drop helper element. */
314    private Element m_dragHelper;
315
316    /** The attribute handler. */
317    private CmsAttributeHandler m_handler;
318
319    /** Flag indicating a validation error. */
320    private boolean m_hasError;
321
322    /** Flag indicating if there is a value set for this UI object. */
323    private boolean m_hasValue;
324
325    /** The help text. */
326    private String m_help;
327
328    /** Flag indicating this is a representing an attribute choice value. */
329    private boolean m_isChoice;
330
331    /** Flag indicating that this view represents a simple value. */
332    private boolean m_isSimpleValue;
333
334    /** The label text. */
335    private String m_label;
336
337    /** The label mouse in handler registration. */
338    private HandlerRegistration m_labelOutRegistration;
339
340    /** The label mouse over handler registration. */
341    private HandlerRegistration m_labelOverRegistration;
342
343    /** The drag and drop place holder element. */
344    private Element m_placeHolder;
345
346    /** The editing widget. */
347    private I_CmsFormEditWidget m_widget;
348
349    /**
350     * Constructor.<p>
351     *
352     * @param handler the attribute handler
353     * @param label the attribute label
354     * @param help the attribute help information
355     */
356    public CmsAttributeValueView(CmsAttributeHandler handler, String label, String help) {
357
358        // important: provide the move button before binding the widget UI
359        m_moveButton = new MoveHandle(this);
360        initWidget(uiBinder.createAndBindUi(this));
361        m_handler = handler;
362        m_handler.registerAttributeValue(this);
363        if (!CmsCoreProvider.isTouchOnly()) {
364            m_moveButton.addMouseDownHandler(m_handler.getDNDHandler());
365        }
366        m_label = label;
367        m_help = help;
368        if (m_help == null) {
369            m_helpBubble.getStyle().setDisplay(Display.NONE);
370            m_help = "";
371        }
372        generateLabel();
373        m_helpBubbleText.setInnerHTML(m_help);
374        addStyleName(formCss().emptyValue());
375        m_compacteModeStyle = new CmsStyleVariable(this);
376        m_compacteModeStyle.setValue(formCss().defaultView());
377        initHighlightingHandler();
378        initButtons();
379        m_buttonBar.addStyleName(CmsButtonBarHandler.HOVERABLE_MARKER);
380
381        m_buttonBar.addDomHandler(CmsButtonBarHandler.INSTANCE, MouseOverEvent.getType());
382        m_buttonBar.addDomHandler(CmsButtonBarHandler.INSTANCE, MouseOutEvent.getType());
383        m_collapsedStyle.setValue(formCss().uncollapsed());
384
385    }
386
387    /**
388     * Adds a new choice  choice selection menu.<p>
389     *
390     * @param widgetService the widget service to use for labels
391     * @param menuEntry the menu entry bean for the choice
392     */
393    public void addChoice(I_CmsWidgetService widgetService, final CmsChoiceMenuEntryBean menuEntry) {
394
395        AsyncCallback<CmsChoiceMenuEntryBean> selectHandler = new AsyncCallback<CmsChoiceMenuEntryBean>() {
396
397            public void onFailure(Throwable caught) {
398
399                // will not be called
400
401            }
402
403            public void onSuccess(CmsChoiceMenuEntryBean selectedEntry) {
404
405                m_attributeChoice.hide();
406                selectChoice(selectedEntry.getPath());
407            }
408        };
409
410        m_attributeChoice.addChoice(widgetService, menuEntry, selectHandler);
411        m_isChoice = true;
412    }
413
414    /**
415     * @see com.google.gwt.event.dom.client.HasMouseDownHandlers#addMouseDownHandler(com.google.gwt.event.dom.client.MouseDownHandler)
416     */
417    public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
418
419        return addDomHandler(handler, MouseDownEvent.getType());
420    }
421
422    /**
423     * @see com.google.gwt.event.dom.client.HasMouseOutHandlers#addMouseOutHandler(com.google.gwt.event.dom.client.MouseOutHandler)
424     */
425    public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
426
427        return addDomHandler(handler, MouseOutEvent.getType());
428    }
429
430    /**
431     * @see com.google.gwt.event.dom.client.HasMouseOverHandlers#addMouseOverHandler(com.google.gwt.event.dom.client.MouseOverHandler)
432     */
433    public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
434
435        return addDomHandler(handler, MouseOverEvent.getType());
436    }
437
438    public FlowPanel getButtonBar() {
439
440        return m_buttonBar;
441    }
442
443    /**
444     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getCursorOffsetDelta()
445     */
446    public Optional<int[]> getCursorOffsetDelta() {
447
448        return Optional.absent();
449    }
450
451    /**
452     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getDragHelper(org.opencms.gwt.client.dnd.I_CmsDropTarget)
453     */
454    public Element getDragHelper(I_CmsDropTarget target) {
455
456        closeHelpBubble(null);
457        // using the widget element as the drag helper also to avoid cloning issues on input fields
458        m_dragHelper = getElement();
459        Element parentElement = getElement().getParentElement();
460        if (parentElement == null) {
461            parentElement = target.getElement();
462        }
463        int elementTop = getElement().getAbsoluteTop();
464        int parentTop = parentElement.getAbsoluteTop();
465        Style style = m_dragHelper.getStyle();
466        style.setWidth(m_dragHelper.getOffsetWidth(), Unit.PX);
467        // the dragging class will set position absolute
468        style.setTop(elementTop - parentTop, Unit.PX);
469        m_dragHelper.addClassName(formCss().dragHelper());
470        return m_dragHelper;
471    }
472
473    /**
474     * Returns the attribute handler.<p>
475     *
476     * @return the attribute handler
477     */
478    public CmsAttributeHandler getHandler() {
479
480        return m_handler;
481    }
482
483    /**
484     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getId()
485     */
486    public String getId() {
487
488        String id = getElement().getId();
489        if ((id == null) || "".equals(id)) {
490            id = Document.get().createUniqueId();
491            getElement().setId(id);
492        }
493        return id;
494    }
495
496    /**
497     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getParentTarget()
498     */
499    public I_CmsDropTarget getParentTarget() {
500
501        return (I_CmsDropTarget)getParent();
502    }
503
504    /**
505     * Gets the parent attribute value view, or null if none exists.<p>
506     *
507     * @return the parent attribute value view
508     */
509    public CmsAttributeValueView getParentView() {
510
511        Widget ancestor = getParent();
512        while ((ancestor != null) && !(ancestor instanceof CmsAttributeValueView)) {
513            ancestor = ancestor.getParent();
514        }
515        return (CmsAttributeValueView)ancestor;
516    }
517
518    /**
519     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getPlaceholder(org.opencms.gwt.client.dnd.I_CmsDropTarget)
520     */
521    public Element getPlaceholder(I_CmsDropTarget target) {
522
523        m_placeHolder = CmsDomUtil.clone(getElement());
524        removeDragHelperStyles(m_placeHolder);
525        m_placeHolder.addClassName(formCss().dragPlaceholder());
526        return m_placeHolder;
527    }
528
529    /**
530     * Returns the attribute value index.<p>
531     *
532     * @return the attribute value index
533     */
534    public int getValueIndex() {
535
536        int result = 0;
537        Node previousSibling = getElement().getPreviousSibling();
538        while (previousSibling != null) {
539            result++;
540            previousSibling = previousSibling.getPreviousSibling();
541        }
542        return result;
543    }
544
545    /**
546     * Returns the editing widget.<p>
547     *
548     * @return the editing widget or <code>null</code> if not available
549     */
550    public I_CmsEditWidget getValueWidget() {
551
552        return m_widget;
553    }
554
555    /**
556     * Returns if there is a value set for this attribute.<p>
557     *
558     * @return <code>true</code> if there is a value set for this attribute
559     */
560    public boolean hasValue() {
561
562        return m_hasValue && ((m_widget == null) || m_widget.isActive());
563    }
564
565    /**
566     * Hides the button bar.<p>
567     */
568    public void hideAllButtons() {
569
570        m_buttonBar.getElement().getStyle().setDisplay(Display.NONE);
571    }
572
573    /**
574     * Returns if drag and drop is enabled for this attribute.<p>
575     *
576     * @return <code>true</code> if drag and drop is enabled for this attribute
577     */
578    public boolean isDragEnabled() {
579
580        return m_dragEnabled;
581    }
582
583    /**
584     * Returns if this view represents a simple value.<p>
585     *
586     * @return <code>true</code> if this view represents a simple value
587     */
588    public boolean isSimpleValue() {
589
590        return m_isSimpleValue;
591    }
592
593    /**
594     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDragCancel()
595     */
596    public void onDragCancel() {
597
598        clearDrag();
599    }
600
601    /**
602     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDrop(org.opencms.gwt.client.dnd.I_CmsDropTarget)
603     */
604    public void onDrop(I_CmsDropTarget target) {
605
606        clearDrag();
607    }
608
609    /**
610     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onStartDrag(org.opencms.gwt.client.dnd.I_CmsDropTarget)
611     */
612    public void onStartDrag(I_CmsDropTarget target) {
613
614        // nothing to do
615    }
616
617    /**
618     * Checks if the attribute value view's widget "owns" the given element.<p>
619     *
620     * @param element the element to check
621     * @return true if the widget owns the element
622     */
623    public boolean owns(Element element) {
624
625        return (m_widget != null) && m_widget.owns(element);
626    }
627
628    /**
629     * Removes any present error message.<p>
630     */
631    public void removeValidationMessage() {
632
633        if (m_hasError) {
634            m_messageText.setInnerText("");
635            removeStyleName(formCss().hasError());
636            removeStyleName(formCss().hasWarning());
637            m_hasError = false;
638        }
639
640    }
641
642    /**
643     * Removes the value.<p>
644     */
645    public void removeValue() {
646
647        if (!isSimpleValue()) {
648            m_hasValue = false;
649            m_widgetHolder.clear();
650            generateLabel();
651        } else {
652            // only deactivate the widget and restore the default value
653            m_widget.setActive(false);
654            if (m_widget.shouldSetDefaultWhenDisabled() && (m_defaultValue != null)) {
655                m_widget.setValue(m_defaultValue);
656            } else {
657                m_widget.setValue("");
658            }
659            addActivationHandler();
660        }
661        addStyleName(formCss().emptyValue());
662        removeValidationMessage();
663    }
664
665    /**
666     * @see org.opencms.gwt.client.I_CmsHasResizeOnShow#resizeOnShow()
667     */
668    public void resizeOnShow() {
669
670        // call resize on all children implementing org.opencms.acacia.client.ui.I_HasResizeOnShow
671        if (hasValue()) {
672            if (isSimpleValue()) {
673                if (m_widget instanceof I_CmsHasResizeOnShow) {
674                    ((I_CmsHasResizeOnShow)m_widget).resizeOnShow();
675                }
676            } else {
677                for (Widget panel : m_widgetHolder) {
678                    if (panel instanceof HasWidgets.ForIsWidget) {
679                        for (Widget w : (HasWidgets.ForIsWidget)panel) {
680                            if (w instanceof I_CmsHasResizeOnShow) {
681                                ((I_CmsHasResizeOnShow)w).resizeOnShow();
682                            }
683                        }
684                    }
685                }
686            }
687        }
688    }
689
690    /**
691     * Sets the value widget active and removes the inactive view styling.<p>
692     */
693    public void setActive() {
694
695        if (m_widget != null) {
696            m_widget.setActive(true);
697            removeStyleName(formCss().emptyValue());
698        }
699    }
700
701    /**
702     * Enables or disables the "collapsed" style, which is used for choice elements to reduce the nesting level visually.<p>
703     *
704     * @param collapsed true if the view should be set to 'collapsed'
705     */
706    public void setCollapsed(boolean collapsed) {
707
708        m_collapsedStyle.setValue(collapsed ? formCss().collapsed() : formCss().uncollapsed());
709    }
710
711    /**
712     * Sets the compact view mode.<p>
713     *
714     * @param mode the mode to set
715     */
716    public void setCompactMode(int mode) {
717
718        switch (mode) {
719            case COMPACT_MODE_FIRST_COLUMN:
720                m_compacteModeStyle.setValue(formCss().firstColumn());
721                break;
722            case COMPACT_MODE_SECOND_COLUMN:
723                m_compacteModeStyle.setValue(formCss().secondColumn());
724                break;
725            case COMPACT_MODE_NESTED:
726                m_compacteModeStyle.setValue(formCss().compactView());
727                break;
728            case COMPACT_MODE_SINGLE_LINE:
729                m_compacteModeStyle.setValue(formCss().singleLine());
730                break;
731            default:
732
733        }
734        updateWidth();
735    }
736
737    /**
738     * Shows a validation error message.<p>
739     *
740     * @param message the error message
741     */
742    public void setErrorMessage(String message) {
743
744        m_messageText.setInnerHTML(message);
745        addStyleName(formCss().hasError());
746        m_hasError = true;
747    }
748
749    /**
750     * Sets the value entity.<p>
751     *
752     * @param renderer the entity renderer
753     * @param value the value entity
754     */
755    public void setValueEntity(I_CmsEntityRenderer renderer, CmsEntity value) {
756
757        if (m_hasValue) {
758            throw new RuntimeException("Value has already been set");
759        }
760        m_hasValue = true;
761        m_isSimpleValue = false;
762        FlowPanel entityPanel = new FlowPanel();
763        m_widgetHolder.add(entityPanel);
764        int index = getValueIndex();
765        m_handler.ensureHandlers(index);
766        renderer.renderForm(value, entityPanel, m_handler, index);
767        removeStyleName(formCss().emptyValue());
768    }
769
770    /**
771     * Sets the value widget.<p>
772     *
773     * @param widget the widget
774     * @param value the value
775     * @param defaultValue the default attribute value
776     * @param active <code>true</code> if the widget should be activated
777     */
778    public void setValueWidget(I_CmsFormEditWidget widget, String value, String defaultValue, boolean active) {
779
780        if (m_hasValue) {
781            throw new RuntimeException("Value has already been set");
782        }
783        m_defaultValue = defaultValue;
784        m_hasValue = true;
785        m_isSimpleValue = true;
786        m_widget = widget;
787        if (CmsAttributeHandler.hasResizeHandler() && (m_widget instanceof HasResizeHandlers)) {
788            ((HasResizeHandlers)m_widget).addResizeHandler(CmsAttributeHandler.getResizeHandler());
789        }
790        m_widgetHolder.clear();
791        m_widget.setWidgetInfo(m_label, m_help);
792        if (active) {
793            m_widget.setValue(value, false);
794        } else if (m_widget.shouldSetDefaultWhenDisabled()) {
795            m_widget.setValue(defaultValue, false);
796        } else {
797            m_widget.setValue("");
798        }
799        m_widgetHolder.add(m_widget);
800        m_widget.setName(getHandler().getAttributeName());
801        m_widget.addValueChangeHandler(new ChangeHandler());
802        m_widget.addFocusHandler(new FocusHandler() {
803
804            public void onFocus(FocusEvent event) {
805
806                CmsValueFocusHandler.getInstance().setFocus(CmsAttributeValueView.this);
807                activateWidget();
808            }
809        });
810        m_widget.setActive(active);
811        if (!active) {
812            addActivationHandler();
813        } else {
814            removeStyleName(formCss().emptyValue());
815        }
816        addStyleName(formCss().simpleValue());
817
818        if (m_widget instanceof CmsFormWidgetWrapper) {
819            addLabelHoverHandler(((CmsFormWidgetWrapper)m_widget).getLabel());
820        } else {
821            removeLabelHoverHandler();
822        }
823    }
824
825    /**
826     * Shows a validation warning message.<p>
827     *
828     * @param message the warning message
829     */
830    public void setWarningMessage(String message) {
831
832        m_messageText.setInnerText(message);
833        addStyleName(formCss().hasWarning());
834        m_hasError = true;
835    }
836
837    /**
838     * Shows the button bar.<p>
839     */
840    public void showButtons() {
841
842        m_buttonBar.getElement().getStyle().clearDisplay();
843    }
844
845    /**
846     * Tells the attribute value view to change its display state between focused/unfocused (this doesn't actually change the focus).<p>
847     *
848     * @param focusOn <code>true</code> to change the display state to 'focused'
849     */
850    public void toggleFocus(boolean focusOn) {
851
852        if (!isAttached()) {
853            // can happen if values get actively deleted
854            return;
855        }
856        if (focusOn) {
857            addStyleName(formCss().focused());
858            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_help) || m_hasError) {
859                m_helpBubble.getStyle().clearDisplay();
860                if (shouldDisplayTooltipAbove()) {
861                    addStyleName(formCss().displayAbove());
862                } else {
863                    removeStyleName(formCss().displayAbove());
864                }
865            } else {
866                m_helpBubble.getStyle().setDisplay(Display.NONE);
867            }
868        } else {
869            removeStyleName(formCss().focused());
870            if (m_widget != null) {
871                if (m_handler.hasSingleOptionalValue()) {
872                    if (m_handler.getWidgetService().shouldRemoveLastValueAfterUnfocus(m_widget)) {
873                        m_handler.removeAttributeValue(this);
874                    }
875                }
876            }
877        }
878    }
879
880    /**
881     * Updates the visibility of the add, remove, up and down buttons.<p>
882     *
883     * @param hasAddButton <code>true</code> if the add button should be visible
884     * @param hasRemoveButton <code>true</code> if the remove button should be visible
885     * @param hasSortButtons <code>true</code> if the sort buttons should be visible
886     */
887    public void updateButtonVisibility(boolean hasAddButton, boolean hasRemoveButton, boolean hasSortButtons) {
888
889        boolean hasOnlyDelete = !hasAddButton && hasRemoveButton && !hasSortButtons;
890        boolean hasHoverMenu = !hasOnlyDelete;
891
892        m_buttonBar.getElement().setAttribute(ATTR_HAS_HOVER, "" + hasHoverMenu);
893
894        if (hasAddButton && m_isChoice) {
895            m_attributeChoice.getElement().getStyle().clearDisplay();
896        } else {
897            m_attributeChoice.getElement().getStyle().setDisplay(Display.NONE);
898        }
899        if (hasAddButton && !m_isChoice) {
900            m_addButton.getElement().getStyle().clearDisplay();
901        } else {
902            m_addButton.getElement().getStyle().setDisplay(Display.NONE);
903        }
904
905        if (hasRemoveButton) {
906            m_removeButton.getElement().getStyle().clearDisplay();
907        } else {
908            m_removeButton.getElement().getStyle().setDisplay(Display.NONE);
909        }
910        if (hasSortButtons && (getValueIndex() != 0)) {
911            m_upButton.getElement().getStyle().clearDisplay();
912        } else {
913            m_upButton.getElement().getStyle().setDisplay(Display.NONE);
914        }
915        if (hasSortButtons && (getElement().getNextSibling() != null)) {
916            m_downButton.getElement().getStyle().clearDisplay();
917        } else {
918            m_downButton.getElement().getStyle().setDisplay(Display.NONE);
919        }
920        if (hasSortButtons) {
921            m_moveButton.addStyleName(I_CmsLayoutBundle.INSTANCE.form().moveHandle());
922            if (CmsEditorBase.hasDictionary()) {
923                m_moveButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_MOVE_1, m_label));
924            }
925        } else {
926            m_moveButton.setTitle("");
927            m_moveButton.removeStyleName(I_CmsLayoutBundle.INSTANCE.form().moveHandle());
928        }
929        m_dragEnabled = hasSortButtons;
930        if (!hasAddButton && !hasRemoveButton && !hasSortButtons) {
931            // hide the button bar if no button is visible
932            m_buttonBar.getElement().getStyle().setDisplay(Display.NONE);
933        } else {
934            // show the button bar
935            m_buttonBar.getElement().getStyle().clearDisplay();
936            if (hasSortButtons || (hasAddButton && hasRemoveButton)) {
937                // set multi button mode
938                m_buttonBar.addStyleName(formCss().multiButtonBar());
939                m_moveButton.getElement().getStyle().clearDisplay();
940            } else {
941                m_moveButton.getElement().getStyle().setDisplay(Display.NONE);
942                m_buttonBar.removeStyleName(formCss().multiButtonBar());
943            }
944        }
945    }
946
947    /**
948     * Adds a new attribute value.<p>
949     */
950    protected void addNewAttributeValue() {
951
952        if ((m_widget != null) && !m_widget.isActive()) {
953            activateWidget();
954        } else {
955            m_handler.addNewAttributeValue(this);
956        }
957        onResize();
958    }
959
960    /**
961     * Handles the click event to close the help bubble.<p>
962     *
963     * @param event the click event
964     */
965    @UiHandler("m_helpBubbleClose")
966    protected void closeHelpBubble(ClickEvent event) {
967
968        addStyleName(formCss().closedBubble());
969    }
970
971    /**
972     * Returns the attribute label.<p>
973     *
974     * @return the attribute label
975     */
976    protected String getLabel() {
977
978        return m_label;
979    }
980
981    /**
982     * Handles the click event to move the attribute value down.<p>
983     *
984     * @param event the click event
985     */
986    @UiHandler("m_downButton")
987    protected void moveAttributeValueDown(ClickEvent event) {
988
989        m_handler.moveAttributeValueDown(this);
990    }
991
992    /**
993     * Handles the click event to move the attribute value up.<p>
994     *
995     * @param event the click event
996     */
997    @UiHandler("m_upButton")
998    protected void moveAttributeValueUp(ClickEvent event) {
999
1000        m_handler.moveAttributeValueUp(this);
1001    }
1002
1003    /**
1004     * @see com.google.gwt.user.client.ui.Composite#onAttach()
1005     */
1006    @Override
1007    protected void onAttach() {
1008
1009        super.onAttach();
1010        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
1011
1012            public void execute() {
1013
1014                updateWidth();
1015            }
1016        });
1017    }
1018
1019    /**
1020     * Call when content changes.<p>
1021     */
1022    protected void onResize() {
1023
1024        Widget parent = getParent();
1025        while (parent != null) {
1026            if (parent instanceof I_CmsDescendantResizeHandler) {
1027                ((I_CmsDescendantResizeHandler)parent).onResizeDescendant();
1028                break;
1029            }
1030            parent = parent.getParent();
1031        }
1032    }
1033
1034    /**
1035     * Handles the click event to remove the attribute value.<p>
1036     *
1037     * @param event the click event
1038     */
1039    @UiHandler("m_removeButton")
1040
1041    protected void removeAttributeValue(ClickEvent event) {
1042
1043        m_handler.removeAttributeValue(this);
1044        onResize();
1045    }
1046
1047    /**
1048     * Selects the attribute choice.<p>
1049     *
1050     * @param choicePath the choice attribute path
1051     */
1052    protected void selectChoice(List<String> choicePath) {
1053
1054        m_handler.addNewChoiceAttributeValue(this, choicePath);
1055    }
1056
1057    /**
1058     * Activates the value widget if prNamet.<p>
1059     */
1060    void activateWidget() {
1061
1062        if (m_activationHandlerRegistration != null) {
1063            m_activationHandlerRegistration.removeHandler();
1064            m_activationHandlerRegistration = null;
1065        }
1066        if ((m_widget != null) && !m_widget.isActive()) {
1067            m_widget.setActive(true);
1068            if ((m_defaultValue != null) && (m_defaultValue.trim().length() > 0)) {
1069                m_widget.setValue(m_defaultValue, true);
1070            }
1071            m_handler.updateButtonVisisbility();
1072            removeStyleName(formCss().emptyValue());
1073        }
1074    }
1075
1076    /**
1077     * Toggles the label hover CSS class.<p>
1078     *
1079     * @param hovered <code>true</code> in case the
1080     */
1081    void toggleLabelHover(boolean hovered) {
1082
1083        boolean hover = hovered && (m_hasError || CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_help));
1084        if (hover) {
1085            addStyleName(formCss().labelHover());
1086            if (shouldDisplayTooltipAbove()) {
1087                addStyleName(formCss().displayAbove());
1088            } else {
1089                removeStyleName(formCss().displayAbove());
1090            }
1091        } else {
1092            removeStyleName(formCss().labelHover());
1093        }
1094        CmsValueFocusHandler.getInstance().hideHelpBubbles(
1095            RootPanel.get(),
1096            hover || !CmsCoreProvider.get().isShowEditorHelp());
1097    }
1098
1099    /**
1100     * Updates the widget width according to the compact mode setting.<p>
1101     */
1102    void updateWidth() {
1103
1104        if (formCss().firstColumn().equals(m_compacteModeStyle.getValue())) {
1105            int width = getElement().getParentElement().getOffsetWidth() - formCss().SECOND_COLUMN_WIDTH();
1106            // if width could not be evaluated, fall back to a 'save' value
1107            if (width < 0) {
1108                width = 400;
1109            }
1110            getElement().getStyle().setWidth(width, Unit.PX);
1111        } else {
1112            getElement().getStyle().clearWidth();
1113        }
1114    }
1115
1116    /**
1117     * Adds a mouse down handler to activate the editing widget.<p>
1118     */
1119    private void addActivationHandler() {
1120
1121        if (m_activationHandlerRegistration == null) {
1122            m_activationHandlerRegistration = addMouseDownHandler(new MouseDownHandler() {
1123
1124                public void onMouseDown(MouseDownEvent event) {
1125
1126                    // only act on click if not inside the button bar
1127                    if (!m_buttonBar.getElement().isOrHasChild((Node)event.getNativeEvent().getEventTarget().cast())) {
1128                        activateWidget();
1129                    }
1130                }
1131            });
1132        }
1133    }
1134
1135    /**
1136     * Adds the label hover mouse handler.<p>
1137     *
1138     * @param label the label widget
1139     */
1140    private void addLabelHoverHandler(Label label) {
1141
1142        removeLabelHoverHandler();
1143        LabelHoverHandler hoverHandler = new LabelHoverHandler();
1144        m_labelOutRegistration = label.addMouseOutHandler(hoverHandler);
1145        m_labelOverRegistration = label.addMouseOverHandler(hoverHandler);
1146    }
1147
1148    /**
1149     * Called when a drag operation for this widget is stopped.<p>
1150     */
1151    private void clearDrag() {
1152
1153        if (m_dragHelper != null) {
1154            removeDragHelperStyles(m_dragHelper);
1155            m_dragHelper = null;
1156        }
1157        // preventing issue where mouse out was never triggered after drag and drop
1158        m_moveButton.clearHoverState();
1159        CmsButtonBarHandler.INSTANCE.closeAll();
1160    }
1161
1162    /**
1163     * Returns the CSS bundle for the form editor.<p>
1164     *
1165     * @return the form CSS bundle
1166     */
1167    private I_CmsLayoutBundle.I_Style formCss() {
1168
1169        return I_CmsLayoutBundle.INSTANCE.form();
1170    }
1171
1172    /**
1173     * Generates the attribute label.<p>
1174     */
1175    private void generateLabel() {
1176
1177        HTML labelWidget = new HTML("<div class=\"" + formCss().label() + "\">" + m_label + "</div>");
1178        addLabelHoverHandler(labelWidget);
1179        m_widgetHolder.add(labelWidget);
1180    }
1181
1182    /**
1183     * Initializes the button styling.<p>
1184     */
1185    private void initButtons() {
1186
1187        m_addButton.addChoice(
1188            m_handler.getWidgetService(),
1189            new CmsChoiceMenuEntryBean(m_handler.getAttributeName()),
1190            new AsyncCallback<CmsChoiceMenuEntryBean>() {
1191
1192                public void onFailure(Throwable caught) {
1193
1194                    // will not be called
1195
1196                }
1197
1198                public void onSuccess(CmsChoiceMenuEntryBean selectedEntry) {
1199
1200                    // nothing to do
1201                }
1202            });
1203        m_addButton.addDomHandler(new ClickHandler() {
1204
1205            public void onClick(ClickEvent event) {
1206
1207                m_addButton.hide();
1208                addNewAttributeValue();
1209                event.preventDefault();
1210                event.stopPropagation();
1211
1212            }
1213        }, ClickEvent.getType());
1214
1215        m_removeButton.setImageClass(I_CmsButton.CUT_SMALL);
1216        m_removeButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
1217        m_removeButton.setHideFromTabNav(true);
1218
1219        m_upButton.setImageClass(I_CmsButton.EDIT_UP_SMALL);
1220        m_upButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
1221        m_upButton.setHideFromTabNav(true);
1222
1223        m_downButton.setImageClass(I_CmsButton.EDIT_DOWN_SMALL);
1224        m_downButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
1225        m_downButton.setHideFromTabNav(true);
1226
1227        m_helpBubbleClose.setImageClass(I_CmsButton.DELETE_SMALL);
1228        m_helpBubbleClose.setButtonStyle(ButtonStyle.FONT_ICON, null);
1229        m_helpBubbleClose.setHideFromTabNav(true);
1230        if (CmsEditorBase.hasDictionary()) {
1231            m_addButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_ADD_1, m_label));
1232            m_attributeChoice.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_CHOICE_ADD_CHOICE_1, m_label));
1233            m_removeButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_DELETE_1, m_label));
1234            m_helpBubbleClose.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_CLOSE_0));
1235            m_upButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_MOVE_UP_0, m_label));
1236            m_downButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_MOVE_DOWN_0, m_label));
1237        }
1238    }
1239
1240    /**
1241     * Initializes the highlighting handler.<p>
1242     */
1243    private void initHighlightingHandler() {
1244
1245        addMouseOverHandler(CmsValueFocusHandler.getInstance());
1246        addMouseOutHandler(CmsValueFocusHandler.getInstance());
1247        addMouseDownHandler(CmsValueFocusHandler.getInstance());
1248    }
1249
1250    /**
1251     * Removes the drag helper styles from the given element.<p>
1252     *
1253     * @param helper the helper element
1254     */
1255    private void removeDragHelperStyles(Element helper) {
1256
1257        Style style = helper.getStyle();
1258        style.clearTop();
1259        style.clearLeft();
1260        style.clearPosition();
1261        style.clearWidth();
1262        helper.removeClassName(formCss().dragHelper());
1263    }
1264
1265    /**
1266     * Removes the label hover mouse handler.<p>
1267     */
1268    private void removeLabelHoverHandler() {
1269
1270        if (m_labelOverRegistration != null) {
1271            m_labelOverRegistration.removeHandler();
1272            m_labelOverRegistration = null;
1273        }
1274        if (m_labelOutRegistration != null) {
1275            m_labelOutRegistration.removeHandler();
1276            m_labelOutRegistration = null;
1277        }
1278    }
1279
1280    /**
1281     * Returns if the help bubble should be displayed above the value field.<p>
1282     *
1283     * @return <code>true</code> if the help bubble should be displayed above
1284     */
1285    private boolean shouldDisplayTooltipAbove() {
1286
1287        boolean displayAbove;
1288        if (isSimpleValue()) {
1289            Direction direction = Direction.none;
1290            if (m_widget instanceof I_CmsHasDisplayDirection) {
1291                direction = ((I_CmsHasDisplayDirection)m_widget).getDisplayingDirection();
1292            }
1293            switch (direction) {
1294                case above:
1295                    displayAbove = false;
1296                    break;
1297                case below:
1298                case none:
1299                default:
1300                    displayAbove = true;
1301                    break;
1302            }
1303        } else {
1304            displayAbove = true;
1305        }
1306
1307        m_helpBubble.getStyle().setDisplay(Display.BLOCK);
1308        int bubbleHeight = m_helpBubble.getOffsetHeight();
1309        m_helpBubble.getStyle().clearDisplay();
1310
1311        Element widgetElement;
1312        if (m_widget != null) {
1313            widgetElement = m_widget.asWidget().getElement();
1314        } else {
1315            return true;
1316        }
1317
1318        // Calculate top position for the popup
1319        int top = widgetElement.getAbsoluteTop();
1320
1321        int windowTop = Window.getScrollTop();
1322        int windowBottom = Window.getScrollTop() + Window.getClientHeight();
1323        int distanceFromWindowTop = top - windowTop - TOOLBAR_HEIGHT;
1324
1325        int distanceToWindowBottom = windowBottom - (top + widgetElement.getOffsetHeight());
1326        if (displayAbove
1327            && ((distanceFromWindowTop < bubbleHeight) && (distanceToWindowBottom > distanceFromWindowTop))) {
1328            // in case there is too little space above, and there is more below, change direction
1329            displayAbove = false;
1330        } else if (!displayAbove
1331            && ((distanceToWindowBottom < bubbleHeight) && (distanceFromWindowTop > distanceToWindowBottom))
1332            && !m_hasError) {
1333            // in case there is too little space below, and there is more above, change direction
1334            // (exception for m_hasError: when displaying a validation error for a widget that displays a popup above it, disregard the
1335            // available space under it, because if we display the message below, the page expands so the user can scroll down to read it, which is less
1336            // bad than the message covering a popup required to use the widget).
1337            displayAbove = true;
1338        }
1339        return displayAbove;
1340    }
1341}