001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.gwt.client.ui.input.category;
029
030import org.opencms.gwt.client.CmsCoreProvider;
031import org.opencms.gwt.client.Messages;
032import org.opencms.gwt.client.rpc.CmsRpcAction;
033import org.opencms.gwt.client.ui.CmsList;
034import org.opencms.gwt.client.ui.CmsPushButton;
035import org.opencms.gwt.client.ui.CmsScrollPanel;
036import org.opencms.gwt.client.ui.CmsSimpleListItem;
037import org.opencms.gwt.client.ui.I_CmsButton;
038import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
039import org.opencms.gwt.client.ui.I_CmsButton.Size;
040import org.opencms.gwt.client.ui.I_CmsListItem;
041import org.opencms.gwt.client.ui.I_CmsTruncable;
042import org.opencms.gwt.client.ui.css.I_CmsInputLayoutBundle;
043import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
044import org.opencms.gwt.client.ui.input.CmsCategoryField;
045import org.opencms.gwt.client.ui.input.CmsCheckBox;
046import org.opencms.gwt.client.ui.input.CmsSelectBox;
047import org.opencms.gwt.client.ui.input.CmsTextBox;
048import org.opencms.gwt.client.ui.tree.CmsTreeItem;
049import org.opencms.gwt.shared.CmsCategoryBean;
050import org.opencms.gwt.shared.CmsCategoryTreeEntry;
051import org.opencms.gwt.shared.CmsGwtLog;
052import org.opencms.util.CmsStringUtil;
053
054import java.util.ArrayList;
055import java.util.Collection;
056import java.util.Collections;
057import java.util.HashMap;
058import java.util.HashSet;
059import java.util.Iterator;
060import java.util.LinkedHashMap;
061import java.util.List;
062import java.util.Map;
063import java.util.Set;
064
065import com.google.gwt.core.client.GWT;
066import com.google.gwt.core.client.Scheduler;
067import com.google.gwt.core.client.Scheduler.ScheduledCommand;
068import com.google.gwt.dom.client.Style;
069import com.google.gwt.dom.client.Style.Float;
070import com.google.gwt.dom.client.Style.Unit;
071import com.google.gwt.event.dom.client.ClickEvent;
072import com.google.gwt.event.dom.client.ClickHandler;
073import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
074import com.google.gwt.event.logical.shared.ValueChangeEvent;
075import com.google.gwt.event.logical.shared.ValueChangeHandler;
076import com.google.gwt.event.shared.HandlerRegistration;
077import com.google.gwt.uibinder.client.UiBinder;
078import com.google.gwt.uibinder.client.UiField;
079import com.google.gwt.user.client.Timer;
080import com.google.gwt.user.client.rpc.IsSerializable;
081import com.google.gwt.user.client.ui.Composite;
082import com.google.gwt.user.client.ui.FlowPanel;
083import com.google.gwt.user.client.ui.HasText;
084import com.google.gwt.user.client.ui.Label;
085import com.google.gwt.user.client.ui.Widget;
086
087/**
088 * Builds the category tree.<p>
089 * */
090public class CmsCategoryTree extends Composite implements I_CmsTruncable, HasValueChangeHandlers<List<String>> {
091
092    /** Sorting parameters. */
093    public enum SortParams implements IsSerializable {
094
095        /** Date last modified ascending. */
096        dateLastModified_asc,
097
098        /** Date last modified descending. */
099        dateLastModified_desc,
100
101        /** Resource path ascending sorting. */
102        path_asc,
103
104        /** Resource path descending sorting.*/
105        path_desc,
106
107        /** Title ascending sorting. */
108        title_asc,
109
110        /** Title descending sorting. */
111        title_desc,
112
113        /** Tree.*/
114        tree,
115
116        /** Resource type ascending sorting. */
117        type_asc,
118
119        /** Resource type descending sorting. */
120        type_desc,
121
122        /** Recently used. */
123        used;
124    }
125
126    /**
127     * @see com.google.gwt.uibinder.client.UiBinder
128     */
129    interface I_CmsCategoryTreeUiBinder extends UiBinder<Widget, CmsCategoryTree> {
130        // GWT interface, nothing to do here
131    }
132
133    /**
134     * Inner class for select box handler.<p>
135     */
136    private class CategoryValueChangeHandler implements ValueChangeHandler<String> {
137
138        /**
139         * Default Constructor.<p>
140         */
141        public CategoryValueChangeHandler() {
142
143            // nothing to do
144        }
145
146        /**
147         * Will be triggered if the value in the select box changes.<p>
148         *
149         * @see com.google.gwt.event.logical.shared.ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)
150         */
151        public void onValueChange(ValueChangeEvent<String> event) {
152
153            cancelQuickFilterTimer();
154            if (event.getSource() == m_sortSelectBox) {
155
156                List<CmsTreeItem> categories = new ArrayList<CmsTreeItem>(m_categories.values());
157                SortParams sort = SortParams.valueOf(event.getValue());
158                sort(categories, sort);
159            }
160            if ((event.getSource() == m_quickSearch)) {
161                if (!m_listView) {
162                    m_listView = true;
163                    m_sortSelectBox.setFormValueAsString(SortParams.title_asc.name());
164                }
165                if (hasQuickFilter()) {
166
167                    if ((CmsStringUtil.isEmptyOrWhitespaceOnly(event.getValue()) || (event.getValue().length() >= 2))) {
168                        // only act if filter length is at least 3 characters or empty
169                        scheduleQuickFilterTimer();
170                    }
171                } else {
172                    checkQuickSearchStatus();
173                }
174            }
175        }
176
177    }
178
179    /**
180     * Inner class for check box handler.<p>
181     */
182    private class CheckBoxValueChangeHandler implements ValueChangeHandler<Boolean> {
183
184        /** Path of the TreeItem. */
185        private CmsTreeItem m_item;
186
187        /**
188         * Default constructor.<p>
189         * @param item The CmsTreeItem of this check box
190         */
191        public CheckBoxValueChangeHandler(CmsTreeItem item) {
192
193            m_item = item;
194
195        }
196
197        /**
198         * Is triggered if an check box is selected or deselected.<p>
199         *
200         * @param event The event that is triggered
201         */
202        public void onValueChange(ValueChangeEvent<Boolean> event) {
203
204            toggleSelection(m_item, false);
205        }
206
207    }
208
209    /**
210     * Inner class for check box handler.<p>
211     */
212    private class DataValueClickHander implements ClickHandler {
213
214        /** The TreeItem. */
215        private CmsTreeItem m_item;
216
217        /** Constructor to set the right CmsTreeItem for this handler.<p>
218         *
219         * @param item the CmsTreeItem of this Handler
220         */
221        public DataValueClickHander(CmsTreeItem item) {
222
223            m_item = item;
224        }
225
226        /**
227         * Is triggered if the DataValue widget is clicked.<p>
228         * If its check box was selected the click will deselect this box otherwise it will select it.
229         *
230         * @param event The event that is triggered
231         * */
232        public void onClick(ClickEvent event) {
233
234            if (isEnabled()) {
235                toggleSelection(m_item, true);
236            }
237        }
238
239    }
240
241    /** The filtering delay. */
242    private static final int FILTER_DELAY = 100;
243
244    /** Text metrics key. */
245    private static final String TM_GALLERY_SORT = "gallerySort";
246
247    /** The ui-binder instance for this class. */
248    private static I_CmsCategoryTreeUiBinder uiBinder = GWT.create(I_CmsCategoryTreeUiBinder.class);
249
250    /** Map of categories. */
251    protected Map<String, CmsTreeItem> m_categories;
252
253    /** All category tree items, including duplicates with the same category path. */
254    protected List<CmsTreeItem> m_categoriesAsList = new ArrayList<>();
255
256    /** List of categories selected from the server. */
257    protected List<CmsCategoryTreeEntry> m_categoryBeans;
258
259    /** Map from category paths to the paths of their children. */
260    protected Map<String, List<String>> m_childrens;
261
262    /** A label for displaying additional information about the tab. */
263    protected HasText m_infoLabel;
264
265    /** Vale to store the widget mode. True means the single selection. */
266    protected boolean m_isSingleSelection;
267
268    /** Vale to store the view mode. True means the list view. */
269    protected boolean m_listView;
270
271    /** The option panel. */
272    @UiField
273    protected FlowPanel m_options;
274
275    /** The quick search box. */
276    protected CmsTextBox m_quickSearch;
277
278    /** List of categories. */
279    protected CmsList<CmsTreeItem> m_scrollList;
280
281    /** The quick search button. */
282    protected CmsPushButton m_searchButton;
283
284    /**
285     * List of all selected categories.
286     *
287     * <p>IMPORTANT: This may unfortunately contain either category paths or category site paths.
288     *  */
289    protected Collection<String> m_selectedCategories;
290
291    /** Result string for single selection. */
292    protected String m_singleResult = "";
293
294    /** The select box to change the sort order. */
295    protected CmsSelectBox m_sortSelectBox;
296
297    /** The scroll panel. */
298    @UiField
299    CmsScrollPanel m_list;
300
301    /** The main panel. */
302    @UiField
303    FlowPanel m_tab;
304
305    /** Map of categories from the server, with their category path (not site path) as key. */
306    private Map<String, CmsCategoryTreeEntry> m_categoriesById = new HashMap<>();
307
308    /** The disable reason, will be displayed as check box title. */
309    private String m_disabledReason;
310
311    /** The quick filter timer. */
312    private Timer m_filterTimer;
313
314    /** The category selection enabled flag. */
315    private boolean m_isEnabled;
316
317    /** Flag, indicating if the category tree should be collapsed when shown first. */
318    private boolean m_showCollapsed;
319
320    /** Set of used categories (either reported as used from the server, or used locally in this widget instance). */
321    private Set<String> m_used = new HashSet<>();
322
323    /**
324     * Default Constructor.<p>
325     */
326    public CmsCategoryTree() {
327
328        uiBinder.createAndBindUi(this);
329        initWidget(uiBinder.createAndBindUi(this));
330        m_isEnabled = true;
331    }
332
333    /**
334     * Constructor to collect all categories and build a view tree.<p>
335     *
336     * @param selectedCategories A list of all selected categories
337     * @param height The height of this widget
338     * @param isSingleValue Sets the modes of this widget
339     * @param categories the categories
340     **/
341    public CmsCategoryTree(
342        Collection<String> selectedCategories,
343        int height,
344        boolean isSingleValue,
345        List<CmsCategoryTreeEntry> categories) {
346
347        this(selectedCategories, height, isSingleValue, categories, false);
348    }
349
350    /**
351     * Constructor to collect all categories and build a view tree.<p>
352     *
353     * @param selectedCategories A list of all selected categories
354     * @param height The height of this widget
355     * @param isSingleValue Sets the modes of this widget
356     * @param categories the categories
357     * @param showCollapsed if true, the category tree will be collapsed when opened.
358     **/
359    public CmsCategoryTree(
360        Collection<String> selectedCategories,
361        int height,
362        boolean isSingleValue,
363        List<CmsCategoryTreeEntry> categories,
364        boolean showCollapsed) {
365
366        this();
367        m_isSingleSelection = isSingleValue;
368        addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().categoryItem());
369        m_list.addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().categoryScrollPanel());
370        m_selectedCategories = selectedCategories;
371        Iterator<String> it = selectedCategories.iterator();
372        while (it.hasNext()) {
373            m_singleResult = it.next();
374        }
375        m_scrollList = createScrollList();
376        m_list.setHeight(height + "px");
377        m_categoryBeans = categories;
378        processCategories(m_categoryBeans);
379        m_list.add(m_scrollList);
380        m_showCollapsed = showCollapsed;
381        m_childrens = new HashMap<>();
382        m_categories = new HashMap<>();
383        updateContentTree(false);
384        normalizeSelectedCategories();
385        init();
386    }
387
388    /**
389     * Adds children item to the category tree and select the categories.<p>
390     *
391     * @param parent the parent item
392     * @param children the list of children
393     * @param selectedCategories the list of categories to select
394     */
395    public void addChildren(
396        CmsTreeItem parent,
397        List<CmsCategoryTreeEntry> children,
398        Collection<String> selectedCategories) {
399
400        if (children != null) {
401            for (CmsCategoryTreeEntry child : children) {
402                // set the category tree item and add to parent tree item
403                CmsTreeItem treeItem = buildTreeItem(child, selectedCategories);
404                m_childrens.get(parent.getId()).add(treeItem.getId());
405                m_childrens.put(treeItem.getId(), new ArrayList<>(child.getChildren().size()));
406                if ((selectedCategories != null)
407                    && CmsCategoryField.isParentCategoryOfSelected(child.getPath(), selectedCategories)) {
408                    openWithParents(parent);
409
410                }
411                if (m_isSingleSelection) {
412                    if (treeItem.getCheckBox().isChecked()) {
413                        parent.getCheckBox().setChecked(false);
414                    }
415                }
416                parent.addChild(treeItem);
417                addChildren(treeItem, child.getChildren(), selectedCategories);
418            }
419        }
420    }
421
422    /**
423     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
424     */
425    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<String>> handler) {
426
427        return addHandler(handler, ValueChangeEvent.getType());
428    }
429
430    /**
431     * Disabled the category selection.<p>
432     *
433     * @param disabledReason the disable reason, will be displayed as check box title
434     */
435    public void disable(String disabledReason) {
436
437        if (m_isEnabled
438            || (CmsStringUtil.isNotEmptyOrWhitespaceOnly(disabledReason) && !disabledReason.equals(m_disabledReason))) {
439            m_isEnabled = false;
440            m_disabledReason = disabledReason;
441            m_scrollList.addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().disabled());
442            setListEnabled(m_scrollList, false, disabledReason);
443        }
444    }
445
446    /**
447     * Enables the category selection.<p>
448     */
449    public void enable() {
450
451        if (!m_isEnabled) {
452            m_isEnabled = true;
453            m_disabledReason = null;
454            m_scrollList.removeStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().disabled());
455            setListEnabled(m_scrollList, true, null);
456        }
457    }
458
459    /**
460     * Represents a value change event.<p>
461     */
462    public void fireValueChange() {
463
464        ValueChangeEvent.fire(this, getAllSelected());
465    }
466
467    /**
468     * Returns a list of all selected values.<p>
469     *
470     * @return a list of selected values
471     */
472    public List<String> getAllSelected() {
473
474        List<String> result = new ArrayList<String>();
475        for (String cat : m_selectedCategories) {
476            result.add(m_categories.get(cat).getId());
477        }
478        return result;
479    }
480
481    /**
482     * Returns a list of all selected values as Sidepath.<p>
483     *
484     * @return a list of selected values
485     */
486    public List<String> getAllSelectedSitePath() {
487
488        List<String> result = new ArrayList<String>();
489        for (String cat : m_selectedCategories) {
490            result.add(((CmsDataValue)m_categories.get(cat).getMainWidget()).getParameter(2));
491        }
492        return result;
493    }
494
495    /**
496     * Returns the scrollpanel of this widget.<p>
497     *
498     * @return CmsScrollPanel the scrollpanel of this widget
499     * */
500    public CmsScrollPanel getScrollPanel() {
501
502        return m_list;
503    }
504
505    /**
506     * Returns the last selected value.<p>
507     *
508     * @return the last selected value
509     */
510    public List<String> getSelected() {
511
512        List<String> result = new ArrayList<String>();
513        result.add(
514            m_singleResult.isEmpty()
515            ? ""
516            : ((CmsDataValue)m_categories.get(m_singleResult).getMainWidget()).getParameter(2));
517        return result;
518    }
519
520    /**
521     * Returns if the category selection is enabled.<p>
522     *
523     * @return <code>true</code> if the category selection is enabled
524     */
525    public boolean isEnabled() {
526
527        return m_isEnabled;
528    }
529
530    /**
531     * Goes up the tree and opens the parents of the item.<p>
532     *
533     * @param item the child item to start from
534     */
535    public void openWithParents(CmsTreeItem item) {
536
537        if (item != null) {
538            item.setOpen(true);
539            openWithParents(item.getParentItem());
540        }
541    }
542
543    /**
544     * Shows the tab list is empty label.<p>
545     */
546    public void showIsEmptyLabel() {
547
548        CmsSimpleListItem item = new CmsSimpleListItem();
549        Label isEmptyLabel = new Label(Messages.get().key(Messages.GUI_CATEGORIES_IS_EMPTY_0));
550        isEmptyLabel.addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().categoryEmptyLabel());
551        item.add(isEmptyLabel);
552        m_scrollList.add(item);
553    }
554
555    /**
556     * @see org.opencms.gwt.client.ui.I_CmsTruncable#truncate(java.lang.String, int)
557     */
558    public void truncate(String textMetricsKey, int clientWidth) {
559
560        m_scrollList.truncate(textMetricsKey, clientWidth);
561    }
562
563    /**
564     * Updates the content of the categories list.<p>
565     *
566     * @param treeItemsToShow the updates list of categories tree item beans
567     */
568    public void updateContentList(List<CmsTreeItem> treeItemsToShow) {
569
570        m_scrollList.clearList();
571        if ((treeItemsToShow != null) && !treeItemsToShow.isEmpty()) {
572            for (CmsTreeItem dataValue : treeItemsToShow) {
573                dataValue.removeOpener();
574                m_scrollList.add(dataValue);
575                CmsScrollPanel scrollparent = (CmsScrollPanel)m_scrollList.getParent();
576                scrollparent.onResizeDescendant();
577            }
578        } else {
579            showIsEmptyLabel();
580        }
581        scheduleResize();
582    }
583
584    /**
585     * Updates the content of the categories tree.<p>
586     *
587     * @param removeUnused if true, only show used categories, with all levels opened
588     */
589    public void updateContentTree(boolean removeUnused) {
590
591        m_scrollList.clearList();
592        m_childrens.clear();
593        m_categories.clear();
594        m_categoriesAsList.clear();
595        if ((m_categoryBeans != null) && !m_categoryBeans.isEmpty()) {
596            // add the first level and children
597            for (CmsCategoryTreeEntry category : m_categoryBeans) {
598                // set the category tree item and add to list
599                CmsTreeItem treeItem = buildTreeItem(category, m_selectedCategories);
600                m_childrens.put(treeItem.getId(), new ArrayList<>(category.getChildren().size()));
601
602                // We set the 'open' state of the item*before* processing the children, so that even if a top-level item
603                // is set to 'closed' here, it can still be opened when encountering a descendant whose category
604                // is among the selected categories.
605                treeItem.setOpen(!m_showCollapsed);
606                addChildren(treeItem, category.getChildren(), m_selectedCategories);
607
608                if (!category.getPath().isEmpty() || (treeItem.getChildCount() > 0)) {
609                    m_scrollList.add(treeItem);
610                }
611            }
612            if (removeUnused) {
613                for (CmsTreeItem item : m_categoriesAsList) {
614                    String categoryOfCurrentTreeItem = item.getId();
615                    if (!showInUsedView(categoryOfCurrentTreeItem)) {
616                        item.removeFromParent();
617                    }
618                }
619
620                for (CmsTreeItem item : m_categoriesAsList) {
621                    if (item.getChildren().getWidgetCount() == 0) {
622                        if ("".equals(item.getId())) {
623                            // It's an empty 'global categories / site categories' entry, we don't need it
624                            item.removeFromParent();
625                        } else {
626                            // empty normal top-level category
627                            item.setLeafStyle(true);
628                        }
629                    } else {
630                        item.setOpen(true);
631                    }
632                }
633            }
634        }
635        if (m_scrollList.getWidgetCount() == 0) {
636            showIsEmptyLabel();
637        }
638        scheduleResize();
639    }
640
641    /**
642     * Cancels the quick filter timer.<p>
643     */
644    protected void cancelQuickFilterTimer() {
645
646        if (m_filterTimer != null) {
647            m_filterTimer.cancel();
648        }
649    }
650
651    /**
652     * Checks the quick search input and enables/disables the search button accordingly.<p>
653     */
654    protected void checkQuickSearchStatus() {
655
656        if ((m_quickSearch != null) && (m_searchButton != null)) {
657            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_quickSearch.getFormValueAsString())) {
658                m_searchButton.enable();
659            } else {
660                m_searchButton.disable("Enter a search query");
661            }
662        }
663    }
664
665    /**
666     * Creates the quick search/finder box.<p>
667     */
668    protected void createQuickBox() {
669
670        m_quickSearch = new CmsTextBox();
671        // m_quickFilter.setVisible(hasQuickFilter());
672        m_quickSearch.getElement().getStyle().setFloat(Float.RIGHT);
673        m_quickSearch.setTriggerChangeOnKeyPress(true);
674        m_quickSearch.setGhostValue(Messages.get().key(Messages.GUI_QUICK_FINDER_SEARCH_0), true);
675        m_quickSearch.setGhostModeClear(true);
676        m_options.insert(m_quickSearch, 0);
677        m_searchButton = new CmsPushButton();
678        m_searchButton.setImageClass(I_CmsButton.SEARCH);
679        m_searchButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
680        m_searchButton.setSize(Size.small);
681        m_searchButton.getElement().getStyle().setFloat(Style.Float.RIGHT);
682        m_searchButton.getElement().getStyle().setMarginTop(4, Unit.PX);
683        m_searchButton.getElement().getStyle().setMarginLeft(4, Unit.PX);
684        m_options.insert(m_searchButton, 0);
685        m_quickSearch.addValueChangeHandler(new CategoryValueChangeHandler());
686
687        m_filterTimer = new Timer() {
688
689            @Override
690            public void run() {
691
692                quickSearch();
693
694            }
695        };
696        m_searchButton.setTitle(Messages.get().key(Messages.GUI_QUICK_FINDER_SEARCH_0));
697
698        m_searchButton.addClickHandler(new ClickHandler() {
699
700            public void onClick(ClickEvent arg0) {
701
702                quickSearch();
703            }
704        });
705    }
706
707    /**
708     * Creates the list which should contain the list items of the tab.<p>
709     *
710     * @return the newly created list widget
711     */
712    protected CmsList<CmsTreeItem> createScrollList() {
713
714        return new CmsList<CmsTreeItem>();
715    }
716
717    /**
718     * Gets the filtered list of categories.<p>
719     *
720     * @param filter the search string to use for filtering
721     *
722     * @return the filtered category beans
723     */
724    protected List<CmsTreeItem> getFilteredCategories(String filter) {
725
726        List<CmsTreeItem> result = new ArrayList<CmsTreeItem>();
727
728        result = new ArrayList<CmsTreeItem>();
729        for (CmsTreeItem category : m_categories.values()) {
730            CmsDataValue dataWidget = (CmsDataValue)category.getMainWidget();
731            if (CmsStringUtil.isEmptyOrWhitespaceOnly(filter) || dataWidget.matchesFilter(filter, 0, 1)) {
732                result.add(category);
733            }
734        }
735        return result;
736    }
737
738    /**
739     * List of all sort parameters.<p>
740     *
741     * @return List of all sort parameters
742     */
743    protected LinkedHashMap<String, String> getSortList() {
744
745        LinkedHashMap<String, String> list = new LinkedHashMap<String, String>();
746        list.put(SortParams.tree.name(), Messages.get().key(Messages.GUI_SORT_LABEL_HIERARCHIC_0));
747        list.put(SortParams.used.name(), Messages.get().key(Messages.GUI_SORT_LABEL_USED_0));
748        list.put(SortParams.title_asc.name(), Messages.get().key(Messages.GUI_SORT_LABEL_TITLE_ASC_0));
749        list.put(SortParams.title_desc.name(), Messages.get().key(Messages.GUI_SORT_LABEL_TITLE_DECS_0));
750        list.put(SortParams.path_asc.name(), Messages.get().key(Messages.GUI_SORT_LABEL_PATH_ASC_0));
751        list.put(SortParams.path_desc.name(), Messages.get().key(Messages.GUI_SORT_LABEL_PATH_DESC_0));
752
753        return list;
754    }
755
756    /**
757     * Returns true if this widget hat an QuickFilter.<p>
758     *
759     * @return true if this widget hat an QuickFilter
760     */
761    protected boolean hasQuickFilter() {
762
763        // allow filter if not in tree mode
764        return SortParams.tree != SortParams.valueOf(m_sortSelectBox.getFormValueAsString());
765    }
766
767    /**
768     * Call after all handlers have been set.<p>
769     */
770    protected void init() {
771
772        LinkedHashMap<String, String> sortList = getSortList();
773        if (sortList != null) {
774            // generate the sort select box
775            m_sortSelectBox = new CmsSelectBox(sortList);
776            // add the right handler
777            m_sortSelectBox.addValueChangeHandler(new CategoryValueChangeHandler());
778            // style the select box
779            m_sortSelectBox.getElement().getStyle().setWidth(200, Unit.PX);
780            m_sortSelectBox.truncate(TM_GALLERY_SORT, 200);
781            // add it to the right panel
782            m_options.add(m_sortSelectBox);
783            // create the box label
784            Label infoLabel = new Label();
785            infoLabel.setStyleName(I_CmsLayoutBundle.INSTANCE.categoryDialogCss().infoLabel());
786            m_infoLabel = infoLabel;
787            // add it to the right panel
788            m_options.insert(infoLabel, 0);
789            // create quick search box
790            createQuickBox();
791        }
792
793    }
794
795    /**
796     * Sets the search query an selects the result tab.<p>
797     */
798    protected void quickSearch() {
799
800        List<CmsTreeItem> categories = new ArrayList<CmsTreeItem>();
801        if ((m_quickSearch != null)) {
802            categories = getFilteredCategories(hasQuickFilter() ? m_quickSearch.getFormValueAsString() : null);
803            sort(categories, SortParams.valueOf(m_sortSelectBox.getFormValueAsString()));
804        }
805    }
806
807    /**
808     * Removes the quick search/finder box.<p>
809     */
810    protected void removeQuickBox() {
811
812        if (m_quickSearch != null) {
813            m_quickSearch.removeFromParent();
814            m_quickSearch = null;
815        }
816        if (m_searchButton != null) {
817            m_searchButton.removeFromParent();
818            m_searchButton = null;
819        }
820    }
821
822    /**
823     * Schedules the quick filter action.<p>
824     */
825    protected void scheduleQuickFilterTimer() {
826
827        m_filterTimer.schedule(FILTER_DELAY);
828    }
829
830    /**
831     * Sorts a list of tree items according to the sort parameter.<p>
832     *
833     * @param items the items to sort
834     * @param sort the sort parameter
835     */
836    protected void sort(List<CmsTreeItem> items, SortParams sort) {
837
838        int sortParam = -1;
839        boolean ascending = true;
840        m_quickSearch.setVisible(true);
841        m_searchButton.setVisible(true);
842        switch (sort) {
843            case tree:
844                m_quickSearch.setFormValueAsString("");
845                m_listView = false;
846                updateContentTree(false);
847                break;
848            case title_asc:
849                sortParam = 0;
850                break;
851            case title_desc:
852                sortParam = 0;
853                ascending = false;
854                break;
855            case path_asc:
856                sortParam = 1;
857                break;
858            case path_desc:
859                sortParam = 1;
860                ascending = false;
861                break;
862            case used:
863                m_quickSearch.setFormValueAsString("");
864                m_quickSearch.setVisible(false);
865                m_searchButton.setVisible(false);
866                m_listView = false;
867                updateContentTree(true);
868                break;
869            default:
870                break;
871        }
872        if (sortParam != -1) {
873            m_listView = true;
874            items = getFilteredCategories(hasQuickFilter() ? m_quickSearch.getFormValueAsString() : null);
875            Collections.sort(items, new CmsListItemDataComparator(sortParam, ascending));
876            updateContentList(items);
877        }
878    }
879
880    /**
881     * Called if a category is selected/deselected.
882     *
883     * The checkbox state of the selected/deselected item has to be the state BEFORE toggling.
884     *
885     * @param item the tree item that should be selected/deselected.
886     * @param changeState flag, indicating if the checkbox state should be changed.
887     */
888    protected void toggleSelection(CmsTreeItem item, boolean changeState) {
889
890        boolean select = item.getCheckBox().isChecked();
891        select = changeState ? !select : select;
892        String saveId = item.getId();
893        saveUsed(saveId);
894
895        if (m_isSingleSelection) {
896            m_selectedCategories.clear();
897            uncheckAll(m_scrollList);
898        }
899        if (select) {
900            if (m_isSingleSelection) {
901                m_singleResult = item.getId();
902            }
903            CmsTreeItem currentItem = item;
904            do {
905                currentItem.getCheckBox().setChecked(true);
906                String id = currentItem.getId();
907                if (!m_selectedCategories.contains(id)) {
908                    m_selectedCategories.add(id);
909                }
910                currentItem = currentItem.getParentItem();
911            } while (currentItem != null);
912        } else {
913            if (m_isSingleSelection) {
914                m_singleResult = "";
915            }
916            deselectChildren(item);
917            CmsTreeItem currentItem = item;
918            do {
919                currentItem.getCheckBox().setChecked(false);
920                String id = currentItem.getId();
921                if (m_selectedCategories.contains(id)) {
922                    m_selectedCategories.remove(id);
923                }
924                currentItem = currentItem.getParentItem();
925            } while ((currentItem != null) && !hasSelectedChildren(currentItem));
926        }
927        fireValueChange();
928    }
929
930    /**
931     * Builds a tree item for the given category.<p>
932     *
933     * @param category the category
934     * @param selectedCategories the selected categories
935     *
936     * @return the tree item widget
937     */
938    private CmsTreeItem buildTreeItem(CmsCategoryTreeEntry category, Collection<String> selectedCategories) {
939
940        // generate the widget that should be shown in the list
941        CmsDataValue dataValue = new CmsDataValue(
942            600,
943            3,
944            CmsCategoryBean.SMALL_ICON_CLASSES,
945            true,
946            category.getTitle(),
947            "rtl:" + category.getPath(),
948            "hide:" + category.getSitePath());
949
950        // create the check box for this item
951        CmsCheckBox checkBox = new CmsCheckBox();
952        // if it has to be selected, select it
953        boolean isPartofPath = false;
954        isPartofPath = CmsCategoryField.isParentCategoryOfSelected(category.getPath(), selectedCategories);
955        if (isPartofPath) {
956            checkBox.setChecked(true);
957        }
958        if (!isEnabled()) {
959            checkBox.disable(m_disabledReason);
960        }
961        if (category.getPath().isEmpty()) {
962            checkBox.setVisible(false);
963        }
964        // bild the CmsTreeItem out of the widget and the check box
965        CmsTreeItem treeItem = new CmsTreeItem(true, checkBox, dataValue);
966        // abb the handler to the check box
967        dataValue.addClickHandler(new DataValueClickHander(treeItem));
968
969        checkBox.addValueChangeHandler(new CheckBoxValueChangeHandler(treeItem));
970
971        // set the right style for the small view
972        treeItem.setSmallView(true);
973        treeItem.setId(category.getPath());
974        // add it to the list of all categories
975        m_categories.put(treeItem.getId(), treeItem);
976        m_categoriesAsList.add(treeItem);
977        return treeItem;
978    }
979
980    /**
981     * Deselects all child items of the provided item.
982     * @param item the item for which all childs should be deselected.d
983     */
984    private void deselectChildren(CmsTreeItem item) {
985
986        for (String childId : m_childrens.get(item.getId())) {
987            CmsTreeItem child = m_categories.get(childId);
988            deselectChildren(child);
989            child.getCheckBox().setChecked(false);
990            if (m_selectedCategories.contains(childId)) {
991                m_selectedCategories.remove(childId);
992            }
993        }
994    }
995
996    /**
997     * Return true if at least one child of the given tree item is selected.<p>
998     * @param item The CmsTreeItem to start the check
999     * @return true if the given CmsTreeItem or its children is selected
1000     */
1001    private boolean hasSelectedChildren(CmsTreeItem item) {
1002
1003        for (String childId : m_childrens.get(item.getId())) {
1004            CmsTreeItem child = m_categories.get(childId);
1005            if (child.getCheckBox().isChecked() || hasSelectedChildren(child)) {
1006                return true;
1007            }
1008        }
1009        return false;
1010    }
1011
1012    /**
1013     * Normalize the list of selected categories to fit for the ids of the tree items.
1014     */
1015    private void normalizeSelectedCategories() {
1016
1017        Collection<String> normalizedCategories = new ArrayList<String>(m_selectedCategories.size());
1018        for (CmsTreeItem item : m_categories.values()) {
1019            if (item.getCheckBox().isChecked()) {
1020                normalizedCategories.add(item.getId());
1021            }
1022        }
1023        m_selectedCategories = normalizedCategories;
1024
1025    }
1026
1027    /**
1028     * Traverses the tree of categories and stores all of them by category path.
1029     *
1030     * @param categoryBeans the top-level category entries
1031     */
1032    private void processCategories(List<CmsCategoryTreeEntry> categoryBeans) {
1033
1034        for (CmsCategoryTreeEntry category : categoryBeans) {
1035            m_categoriesById.put(category.getPath(), category);
1036            if (category.isUsed()) {
1037                m_used.add(category.getPath());
1038            }
1039            processCategories(category.getChildren());
1040        }
1041    }
1042
1043    /**
1044     * Marks the category with the given path as 'used', both on the server and in the local 'used categories' cache.
1045     *
1046     * @param category the path of the category to mark as used
1047     */
1048    private void saveUsed(String category) {
1049
1050        m_used.add(category);
1051        CmsRpcAction<Void> saveUsed = new CmsRpcAction<Void>() {
1052
1053            @Override
1054            public void execute() {
1055
1056                start(0, false);
1057                CmsCoreProvider.getService().saveUsedCategory(category, this);
1058            }
1059
1060            @Override
1061            protected void onResponse(Void result) {
1062
1063                stop(false);
1064            }
1065        };
1066        saveUsed.execute();
1067    }
1068
1069    /**
1070     * Schedules the execution of onResize deferred.<p>
1071     */
1072    private void scheduleResize() {
1073
1074        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
1075
1076            public void execute() {
1077
1078                m_list.onResizeDescendant();
1079            }
1080        });
1081    }
1082
1083    /**
1084     * Sets the given tree list enabled/disabled.<p>
1085     *
1086     * @param list the list of tree items
1087     * @param enabled <code>true</code> to enable
1088     * @param disabledReason the disable reason, will be displayed as check box title
1089     */
1090    private void setListEnabled(CmsList<? extends I_CmsListItem> list, boolean enabled, String disabledReason) {
1091
1092        for (Widget child : list) {
1093            CmsTreeItem treeItem = (CmsTreeItem)child;
1094            if (enabled) {
1095                treeItem.getCheckBox().enable();
1096            } else {
1097                treeItem.getCheckBox().disable(disabledReason);
1098            }
1099            setListEnabled(treeItem.getChildren(), enabled, disabledReason);
1100        }
1101    }
1102
1103    /**
1104     * Checks if the given category path has any used sub-categories.
1105     *
1106     * @param category the category to check
1107     *
1108     * @return true if the category has used sub-categories
1109     */
1110    private boolean showInUsedView(String category) {
1111
1112        if (CmsCategoryField.isParentCategoryOfSelected(category, m_selectedCategories)) {
1113            return true;
1114        }
1115        if ("".equals(category)) {
1116            return m_used.size() > 0;
1117        } else {
1118            return m_used.stream().anyMatch(used -> CmsStringUtil.isPrefixPath(category, used));
1119        }
1120    }
1121
1122    /**
1123     * Uncheck all items in the list including all sub-items.
1124     * @param list list of CmsTreeItem entries.
1125     */
1126    private void uncheckAll(CmsList<? extends I_CmsListItem> list) {
1127
1128        for (Widget it : list) {
1129            CmsTreeItem treeItem = (CmsTreeItem)it;
1130            treeItem.getCheckBox().setChecked(false);
1131            uncheckAll(treeItem.getChildren());
1132        }
1133    }
1134
1135}