org.jkiss.dbeaver.ui.controls.itemlist.ObjectListControl.java Source code

Java tutorial

Introduction

Here is the source code for org.jkiss.dbeaver.ui.controls.itemlist.ObjectListControl.java

Source

/*
 * DBeaver - Universal Database Manager
 * Copyright (C) 2010-2016 Serge Rieder (serge@jkiss.org)
 * Copyright (C) 2011-2012 Eugene Fradkin (eugene.fradkin@gmail.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (version 2)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.jkiss.dbeaver.ui.controls.itemlist;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.viewers.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.*;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.core.CoreMessages;
import org.jkiss.dbeaver.model.DBPImage;
import org.jkiss.dbeaver.model.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.IDataSourceContainerProvider;
import org.jkiss.dbeaver.model.runtime.AbstractJob;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.runtime.properties.*;
import org.jkiss.dbeaver.ui.DBeaverIcons;
import org.jkiss.dbeaver.ui.LoadingJob;
import org.jkiss.dbeaver.ui.UIUtils;
import org.jkiss.dbeaver.ui.controls.ObjectViewerRenderer;
import org.jkiss.dbeaver.ui.controls.ProgressPageControl;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.List;

/**
 * ObjectListControl
 */
public abstract class ObjectListControl<OBJECT_TYPE> extends ProgressPageControl {
    private static final Log log = Log.getLog(ObjectListControl.class);

    private final static LazyValue DEF_LAZY_VALUE = new LazyValue("..."); //$NON-NLS-1$
    private final static String DATA_OBJECT_COLUMN = "objectColumn"; //$NON-NLS-1$
    private final static int LAZY_LOAD_DELAY = 100;
    private final static Object NULL_VALUE = new Object();

    private boolean isFitWidth;

    private ColumnViewer itemsViewer;
    //private ColumnViewerEditor itemsEditor;
    @NotNull
    private final List<ObjectColumn> columns = new ArrayList<>();
    private IDoubleClickListener doubleClickHandler;
    private PropertySourceAbstract listPropertySource;

    private ObjectViewerRenderer renderer;

    // Sample flag. True only when initial content is packed. Used to provide actual cell data to Tree/Table pack() methods
    // After content is loaded is always false (and all hyperlink cells have empty text)
    private transient boolean sampleItems = false;

    private volatile OBJECT_TYPE curListObject;
    private volatile LoadingJob<Collection<OBJECT_TYPE>> loadingJob;

    private Job lazyLoadingJob = null;
    private Map<OBJECT_TYPE, List<ObjectColumn>> lazyObjects;
    private final Map<OBJECT_TYPE, Map<String, Object>> lazyCache = new IdentityHashMap<>();
    private volatile boolean lazyLoadCanceled;
    private List<OBJECT_TYPE> objectList = null;

    public ObjectListControl(Composite parent, int style, IContentProvider contentProvider) {
        super(parent, style);
        this.isFitWidth = false;

        int viewerStyle = getDefaultListStyle();
        if ((style & SWT.SHEET) == 0) {
            viewerStyle |= SWT.BORDER;
        }

        EditorActivationStrategy editorActivationStrategy;
        final SelectionAdapter enterListener = new SelectionAdapter() {
            @Override
            public void widgetDefaultSelected(SelectionEvent e) {
                if (doubleClickHandler != null) {
                    doubleClickHandler.doubleClick(new DoubleClickEvent(itemsViewer, itemsViewer.getSelection()));
                }
            }
        };
        if (contentProvider instanceof ITreeContentProvider) {
            TreeViewer treeViewer = new TreeViewer(this, viewerStyle);
            final Tree tree = treeViewer.getTree();
            tree.setLinesVisible(true);
            tree.setHeaderVisible(true);
            itemsViewer = treeViewer;
            editorActivationStrategy = new EditorActivationStrategy(treeViewer);
            TreeViewerEditor.create(treeViewer, editorActivationStrategy, ColumnViewerEditor.TABBING_CYCLE_IN_ROW);
            // We need measure item listener to prevent collapse/expand on double click
            // Looks like a bug in SWT: http://www.eclipse.org/forums/index.php/t/257325/
            treeViewer.getControl().addListener(SWT.MeasureItem, new Listener() {
                @Override
                public void handleEvent(Event event) {
                    // Just do nothing
                }
            });
            tree.addSelectionListener(enterListener);
        } else {
            TableViewer tableViewer = new TableViewer(this, viewerStyle);
            final Table table = tableViewer.getTable();
            table.setLinesVisible(true);
            table.setHeaderVisible(true);
            itemsViewer = tableViewer;
            //UIUtils.applyCustomTolTips(table);
            //itemsEditor = new TableEditor(table);
            editorActivationStrategy = new EditorActivationStrategy(tableViewer);
            TableViewerEditor.create(tableViewer, editorActivationStrategy,
                    ColumnViewerEditor.TABBING_CYCLE_IN_ROW);
            table.addSelectionListener(enterListener);
        }
        //editorActivationStrategy.setEnableEditorActivationWithKeyboard(true);
        renderer = createRenderer();
        itemsViewer.getColumnViewerEditor().addEditorActivationListener(new EditorActivationListener());

        itemsViewer.setContentProvider(contentProvider);
        //itemsViewer.setLabelProvider(new ItemLabelProvider());
        itemsViewer.getControl().addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDoubleClick(MouseEvent e) {
                if (doubleClickHandler != null) {
                    // Uee provided double click
                    doubleClickHandler.doubleClick(new DoubleClickEvent(itemsViewer, itemsViewer.getSelection()));
                }
            }
        });
        itemsViewer.getControl().addListener(SWT.PaintItem, new PaintListener());
        GridData gd = new GridData(GridData.FILL_BOTH);
        itemsViewer.getControl().setLayoutData(gd);
        //PropertiesContributor.getInstance().addLazyListener(this);

        // Add selection listener
        itemsViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                IStructuredSelection selection = (IStructuredSelection) event.getSelection();
                if (selection.isEmpty()) {
                    setCurListObject(null);
                } else {
                    setCurListObject((OBJECT_TYPE) selection.getFirstElement());
                }

                String status;
                if (selection.isEmpty()) {
                    status = ""; //$NON-NLS-1$
                } else if (selection.size() == 1) {
                    Object selectedNode = selection.getFirstElement();
                    status = ObjectViewerRenderer.getCellString(selectedNode, false);
                } else {
                    status = NLS.bind(CoreMessages.controls_object_list_status_objects, selection.size());
                }
                setInfo(status);
            }
        });
    }

    protected int getDefaultListStyle() {
        return SWT.MULTI | SWT.FULL_SELECTION;
    }

    public ObjectViewerRenderer getRenderer() {
        return renderer;
    }

    public PropertySourceAbstract getListPropertySource() {
        if (this.listPropertySource == null) {
            this.listPropertySource = createListPropertySource();
        }
        return listPropertySource;
    }

    protected PropertySourceAbstract createListPropertySource() {
        return new DefaultListPropertySource();
    }

    protected CellLabelProvider getColumnLabelProvider(int columnIndex) {
        return new ObjectColumnLabelProvider(columnIndex);
    }

    @Override
    protected boolean cancelProgress() {
        synchronized (this) {
            if (loadingJob != null) {
                loadingJob.cancel();
                return true;
            }
        }
        return false;
    }

    public OBJECT_TYPE getCurrentListObject() {
        return curListObject;
    }

    protected void setCurListObject(@Nullable OBJECT_TYPE curListObject) {
        this.curListObject = curListObject;
    }

    public ColumnViewer getItemsViewer() {
        return itemsViewer;
    }

    public Composite getControl() {
        // Both table and tree are composites so its ok
        return (Composite) itemsViewer.getControl();
    }

    public ISelectionProvider getSelectionProvider() {
        return itemsViewer;
    }

    protected ObjectColumn getColumn(int index) {
        return columns.get(index);
    }

    @Nullable
    protected ObjectPropertyDescriptor getObjectProperty(OBJECT_TYPE object, int columnIndex) {
        return columns.get(columnIndex).getProperty(getObjectValue(object));
    }

    public void setFitWidth(boolean fitWidth) {
        isFitWidth = fitWidth;
    }

    @Override
    public void disposeControl() {
        //PropertiesContributor.getInstance().removeLazyListener(this);
        synchronized (this) {
            if (loadingJob != null) {
                // Cancel running job
                loadingJob.cancel();
                loadingJob = null;
            }
        }
        renderer.dispose();
        super.disposeControl();
    }

    public synchronized boolean isLoading() {
        return loadingJob != null;
    }

    public void loadData() {
        loadData(true);
    }

    public void loadData(boolean lazy) {
        if (this.loadingJob != null) {
            try {
                for (int i = 0; i < 4; i++) {
                    Thread.sleep(500);
                    if (this.loadingJob == null) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                // interrupted
            }
            if (loadingJob != null) {
                UIUtils.showMessageBox(getShell(), "Load", "Service is busy", SWT.ICON_WARNING);
                return;
            }
            return;
        }
        getListPropertySource();

        clearLazyCache();
        this.lazyLoadCanceled = false;

        if (lazy) {
            // start loading service
            synchronized (this) {
                this.loadingJob = createLoadService();
                if (this.loadingJob != null) {
                    this.loadingJob.addJobChangeListener(new JobChangeAdapter() {
                        @Override
                        public void done(IJobChangeEvent event) {
                            loadingJob = null;
                        }
                    });
                    this.loadingJob.schedule(LAZY_LOAD_DELAY);
                }
            }
        } else {
            // Load data synchronously
            final LoadingJob<Collection<OBJECT_TYPE>> loadService = createLoadService();
            if (loadService != null) {
                loadService.syncRun();
            }
        }
    }

    private void setListData(Collection<OBJECT_TYPE> items, boolean append) {
        final Control itemsControl = itemsViewer.getControl();
        if (itemsControl.isDisposed()) {
            return;
        }
        itemsControl.setRedraw(false);
        try {
            final boolean reload = !append && (objectList == null) || (columns.isEmpty());

            if (reload) {
                clearListData();
            }

            {
                // Collect list of items' classes
                final List<Class<?>> classList = new ArrayList<>();
                Class<?>[] baseTypes = getListBaseTypes(items);
                if (!ArrayUtils.isEmpty(baseTypes)) {
                    Collections.addAll(classList, baseTypes);
                }
                if (!CommonUtils.isEmpty(items)) {
                    for (OBJECT_TYPE item : items) {
                        Object object = getObjectValue(item);
                        if (object != null && !classList.contains(object.getClass())) {
                            classList.add(object.getClass());
                        }
                        if (renderer.isTree()) {
                            Map<OBJECT_TYPE, Boolean> collectedSet = new IdentityHashMap<>();
                            collectItemClasses(item, classList, collectedSet);
                        }
                    }
                }

                IPropertyFilter propertyFilter = new DataSourcePropertyFilter(
                        ObjectListControl.this instanceof IDataSourceContainerProvider
                                ? ((IDataSourceContainerProvider) ObjectListControl.this).getDataSourceContainer()
                                : null);

                // Collect all properties
                List<ObjectPropertyDescriptor> allProps = ObjectAttributeDescriptor
                        .extractAnnotations(getListPropertySource(), classList, propertyFilter);

                // Create columns from classes' annotations
                for (ObjectPropertyDescriptor prop : allProps) {
                    if (!prop.isViewable() || prop.isHidden()) {
                        continue;
                    }
                    if (!getListPropertySource().hasProperty(prop)) {
                        getListPropertySource().addProperty(prop);
                        createColumn(prop);
                    }
                }
            }

            if (!itemsControl.isDisposed()) {
                if (reload || objectList.isEmpty()) {
                    // Set viewer content
                    objectList = CommonUtils.isEmpty(items) ? new ArrayList<OBJECT_TYPE>() : new ArrayList<>(items);

                    // Pack columns
                    sampleItems = true;
                    try {
                        List<OBJECT_TYPE> sampleList;
                        if (objectList.size() > 200) {
                            sampleList = objectList.subList(0, 100);
                        } else {
                            sampleList = objectList;
                        }
                        itemsViewer.setInput(sampleList);

                        if (renderer.isTree()) {
                            ((TreeViewer) itemsViewer).expandToLevel(4);
                            UIUtils.packColumns(getTree(), isFitWidth, null);
                        } else {
                            UIUtils.packColumns(getTable(), isFitWidth);
                        }
                    } finally {
                        sampleItems = false;
                    }
                    // Set real content
                    itemsViewer.setInput(objectList);
                } else if (items != null) {
                    if (append) {
                        // Simply append new list to the tail
                        for (OBJECT_TYPE newObject : items) {
                            if (!objectList.contains(newObject)) {
                                objectList.add(newObject);
                            }
                        }
                    } else {
                        // Update object list
                        if (!objectList.equals(items)) {
                            int newListSize = items.size();
                            int itemIndex = 0;
                            for (OBJECT_TYPE newObject : items) {
                                if (itemIndex >= objectList.size()) {
                                    // Add to tail
                                    objectList.add(itemIndex, newObject);
                                } else {
                                    OBJECT_TYPE oldObject = objectList.get(itemIndex);
                                    if (!CommonUtils.equalObjects(oldObject, newObject)) {
                                        // Replace old object
                                        objectList.set(itemIndex, newObject);
                                    }
                                }
                                itemIndex++;
                            }
                            while (objectList.size() > newListSize) {
                                objectList.remove(objectList.size() - 1);
                            }
                        }
                    }

                    itemsViewer.refresh();
                }
            }
        } finally {
            itemsControl.setRedraw(true);
        }
        setInfo(getItemsLoadMessage(objectList.size()));
    }

    public void appendListData(Collection<OBJECT_TYPE> items) {
        setListData(items, true);
    }

    public Collection<OBJECT_TYPE> getListData() {
        return objectList;
    }

    public void clearListData() {
        for (ObjectColumn column : columns) {
            column.item.dispose();
        }
        columns.clear();
        if (!itemsViewer.getControl().isDisposed()) {
            itemsViewer.setInput(Collections.emptyList());
        }
        if (listPropertySource != null) {
            listPropertySource.clearProperties();
        }
        clearLazyCache();
    }

    private void collectItemClasses(OBJECT_TYPE item, List<Class<?>> classList,
            Map<OBJECT_TYPE, Boolean> collectedSet) {
        if (collectedSet.containsKey(item)) {
            log.warn("Cycled object tree: " + item);
            return;
        }
        collectedSet.put(item, Boolean.TRUE);
        ITreeContentProvider contentProvider = (ITreeContentProvider) itemsViewer.getContentProvider();
        if (!contentProvider.hasChildren(item)) {
            return;
        }
        Object[] children = contentProvider.getChildren(item);
        if (!ArrayUtils.isEmpty(children)) {
            for (Object child : children) {
                OBJECT_TYPE childItem = (OBJECT_TYPE) child;
                Object objectValue = getObjectValue(childItem);
                if (!classList.contains(objectValue.getClass())) {
                    classList.add(objectValue.getClass());
                }
                collectItemClasses(childItem, classList, collectedSet);
            }
        }
    }

    private void clearLazyCache() {
        synchronized (lazyCache) {
            lazyCache.clear();
        }
    }

    protected String getItemsLoadMessage(int count) {
        if (count == 0) {
            return CoreMessages.controls_object_list_message_no_items;
        } else {
            return NLS.bind(CoreMessages.controls_object_list_message_items, count);
        }
    }

    public void setDoubleClickHandler(IDoubleClickListener doubleClickHandler) {
        this.doubleClickHandler = doubleClickHandler;
    }

    private Tree getTree() {
        return ((TreeViewer) itemsViewer).getTree();
    }

    private Table getTable() {
        return ((TableViewer) itemsViewer).getTable();
    }

    private synchronized void addLazyObject(OBJECT_TYPE object, ObjectColumn column) {
        if (lazyObjects == null) {
            lazyObjects = new LinkedHashMap<>();
        }
        List<ObjectColumn> objectColumns = lazyObjects.get(object);
        if (objectColumns == null) {
            objectColumns = new ArrayList<>();
            lazyObjects.put(object, objectColumns);
        }
        if (!objectColumns.contains(column)) {
            objectColumns.add(column);
        }
        if (lazyLoadingJob == null) {
            lazyLoadingJob = new LazyLoaderJob();
            lazyLoadingJob.addJobChangeListener(new JobChangeAdapter() {
                @Override
                public void done(IJobChangeEvent event) {
                    synchronized (ObjectListControl.this) {
                        if (lazyObjects == null || lazyObjects.isEmpty()) {
                            lazyLoadingJob = null;
                        } else {
                            lazyLoadingJob.schedule(LAZY_LOAD_DELAY);
                        }
                    }
                }
            });
            lazyLoadingJob.schedule(LAZY_LOAD_DELAY);
        }
    }

    @Nullable
    private synchronized Map<OBJECT_TYPE, List<ObjectColumn>> obtainLazyObjects() {
        synchronized (lazyCache) {
            if (lazyObjects == null) {
                return null;
            }
            Map<OBJECT_TYPE, List<ObjectColumn>> tmp = lazyObjects;
            lazyObjects = null;
            return tmp;
        }
    }

    @Nullable
    private Object getCellValue(Object element, int columnIndex) {
        OBJECT_TYPE object = (OBJECT_TYPE) element;

        if (columnIndex >= columns.size()) {
            return null;
        }
        ObjectColumn column = columns.get(columnIndex);
        if (column.item.isDisposed()) {
            return null;
        }
        Object objectValue = getObjectValue(object);
        if (objectValue == null) {
            return null;
        }
        ObjectPropertyDescriptor prop = getPropertyByObject(column, objectValue);
        if (prop == null) {
            return null;
        }
        //if (!prop.isReadOnly(objectValue) && isNewObject(object)) {
        // Non-editable properties are empty for new objects
        //return null;
        //}
        if (prop.isLazy(objectValue, true)) {
            synchronized (lazyCache) {
                final Map<String, Object> cache = lazyCache.get(object);
                if (cache != null) {
                    final Object value = cache.get(column.id);
                    if (value != null) {
                        if (value == NULL_VALUE) {
                            return null;
                        } else {
                            return value;
                        }
                    }
                }
            }
            if (prop.supportsPreview()) {
                final Object previewValue = getListPropertySource().getPropertyValue(null, objectValue, prop);
                if (previewValue != null) {
                    return new LazyValue(previewValue);
                }
            }
            return DEF_LAZY_VALUE;
        }
        return getListPropertySource().getPropertyValue(null, objectValue, prop);
    }

    @Nullable
    private static ObjectPropertyDescriptor getPropertyByObject(ObjectColumn column, Object objectValue) {
        ObjectPropertyDescriptor prop = null;
        for (Class valueClass = objectValue.getClass(); prop == null
                && valueClass != Object.class; valueClass = valueClass.getSuperclass()) {
            prop = column.propMap.get(valueClass);
        }
        if (prop == null) {
            for (Map.Entry<Class<?>, ObjectPropertyDescriptor> entry : column.propMap.entrySet()) {
                if (entry.getKey().isInstance(objectValue)) {
                    prop = entry.getValue();
                    break;
                }
            }
        }
        return prop;
    }

    @Nullable
    protected Class<?>[] getListBaseTypes(Collection<OBJECT_TYPE> items) {
        return null;
    }

    /**
     * Returns object with properties
     * @param item list item
     * @return object which will be examined for properties
     */
    protected Object getObjectValue(OBJECT_TYPE item) {
        return item;
    }

    /**
     * Returns object's image
     * @param item object
     * @return image or null
     */
    @Nullable
    protected DBPImage getObjectImage(OBJECT_TYPE item) {
        return null;
    }

    protected boolean isNewObject(OBJECT_TYPE objectValue) {
        return false;
    }

    @NotNull
    protected Set<DBPPropertyDescriptor> getAllProperties() {
        Set<DBPPropertyDescriptor> props = new LinkedHashSet<>();
        for (ObjectColumn column : columns) {
            props.addAll(column.propMap.values());
        }
        return props;
    }

    protected void createColumn(ObjectPropertyDescriptor prop) {
        ObjectColumn objectColumn = null;
        for (ObjectColumn col : columns) {
            if (col.id.equals(prop.getId())) {
                objectColumn = col;
                break;
            }
        }
        // Use prop class from top parent
        Class<?> propClass = prop.getParent() == null ? prop.getDeclaringClass()
                : prop.getParent().getDeclaringClass();
        if (objectColumn == null) {
            Item columnItem;
            ViewerColumn newColumn;
            boolean numeric = prop.isNumeric();
            if (renderer.isTree()) {
                TreeViewerColumn viewerColumn = new TreeViewerColumn((TreeViewer) itemsViewer,
                        numeric ? SWT.RIGHT : SWT.NONE);
                viewerColumn.getColumn().setText(prop.getDisplayName());
                viewerColumn.getColumn().setToolTipText(prop.getDescription());
                viewerColumn.getColumn().addListener(SWT.Selection, renderer.getSortListener());
                newColumn = viewerColumn;
                columnItem = viewerColumn.getColumn();
            } else {
                TableViewerColumn viewerColumn = new TableViewerColumn((TableViewer) itemsViewer,
                        numeric ? SWT.RIGHT : SWT.NONE);
                viewerColumn.getColumn().setText(prop.getDisplayName());
                viewerColumn.getColumn().setToolTipText(prop.getDescription());
                //column.setData(prop);
                viewerColumn.getColumn().addListener(SWT.Selection, renderer.getSortListener());
                newColumn = viewerColumn;
                columnItem = viewerColumn.getColumn();
            }
            newColumn.setLabelProvider(getColumnLabelProvider(columns.size()));
            final EditingSupport editingSupport = makeEditingSupport(newColumn, columns.size());
            if (editingSupport != null) {
                newColumn.setEditingSupport(editingSupport);
            }
            objectColumn = new ObjectColumn(newColumn, columnItem, CommonUtils.toString(prop.getId()));
            objectColumn.addProperty(propClass, prop);
            this.columns.add(objectColumn);
            columnItem.setData(DATA_OBJECT_COLUMN, objectColumn);
        } else {
            objectColumn.addProperty(propClass, prop);
            String oldTitle = objectColumn.item.getText();
            if (!oldTitle.contains(prop.getDisplayName())) {
                objectColumn.item.setText(CommonUtils.capitalizeWord(objectColumn.id));
            }
        }
    }

    //////////////////////////////////////////////////////
    // Overridable functions

    protected abstract LoadingJob<Collection<OBJECT_TYPE>> createLoadService();

    protected ObjectViewerRenderer createRenderer() {
        return new ViewerRenderer();
    }

    //////////////////////////////////////////////////////
    // Edit

    @Nullable
    protected EditingSupport makeEditingSupport(ViewerColumn viewerColumn, int columnIndex) {
        return null;
    }

    //////////////////////////////////////////////////////
    // Editor activation

    private class EditorActivationStrategy extends ColumnViewerEditorActivationStrategy {

        public EditorActivationStrategy(ColumnViewer viewer) {
            super(viewer);
        }

        @Override
        protected boolean isEditorActivationEvent(ColumnViewerEditorActivationEvent event) {
            ViewerCell cell = (ViewerCell) event.getSource();
            if (renderer.isHyperlink(getCellValue(cell.getElement(), cell.getColumnIndex()))
                    && getItemsViewer().getControl().getCursor() == getItemsViewer().getControl().getDisplay()
                            .getSystemCursor(SWT.CURSOR_HAND)) {
                return false;
            }
            return super.isEditorActivationEvent(event);
        }
    }

    private static class EditorActivationListener extends ColumnViewerEditorActivationListener {
        @Override
        public void beforeEditorActivated(ColumnViewerEditorActivationEvent event) {
        }

        @Override
        public void afterEditorActivated(ColumnViewerEditorActivationEvent event) {
        }

        @Override
        public void beforeEditorDeactivated(ColumnViewerEditorDeactivationEvent event) {
        }

        @Override
        public void afterEditorDeactivated(ColumnViewerEditorDeactivationEvent event) {
        }
    }

    //////////////////////////////////////////////////////
    // Property source implementation

    private class DefaultListPropertySource extends PropertySourceAbstract {

        public DefaultListPropertySource() {
            super(ObjectListControl.this, ObjectListControl.this, true);
        }

        @Override
        public Object getSourceObject() {
            return getCurrentListObject();
        }

        @Override
        public Object getEditableValue() {
            return getObjectValue(getCurrentListObject());
        }

        @Override
        public DBPPropertyDescriptor[] getPropertyDescriptors2() {
            Set<DBPPropertyDescriptor> props = getAllProperties();
            return props.toArray(new DBPPropertyDescriptor[props.size()]);
        }

    }

    //////////////////////////////////////////////////////
    // Column descriptor

    protected static class ObjectColumn {
        String id;
        Item item;
        ViewerColumn column;
        Map<Class<?>, ObjectPropertyDescriptor> propMap = new IdentityHashMap<>();

        private ObjectColumn(ViewerColumn column, Item item, String id) {
            this.id = id;
            this.column = column;
            this.item = item;
        }

        void addProperty(Class<?> objectClass, ObjectPropertyDescriptor prop) {
            this.propMap.put(objectClass, prop);
        }

        @Nullable
        public ObjectPropertyDescriptor getProperty(Object element) {
            return element == null ? null : getPropertyByObject(this, element);
        }
    }

    //////////////////////////////////////////////////////
    // List sorter

    protected class ObjectColumnLabelProvider extends ColumnLabelProvider {
        protected final int columnIndex;

        ObjectColumnLabelProvider(int columnIndex) {
            this.columnIndex = columnIndex;
        }

        @Nullable
        @Override
        public Image getImage(Object element) {
            if (columnIndex == 0) {
                DBPImage objectImage = getObjectImage((OBJECT_TYPE) element);
                return objectImage == null ? null : DBeaverIcons.getImage(objectImage);
            }
            return null;
        }

        @Override
        public String getText(Object element) {
            Object cellValue = getCellValue(element, columnIndex);
            if (cellValue instanceof LazyValue) {
                cellValue = ((LazyValue) cellValue).value;
            }
            if (!sampleItems && renderer.isHyperlink(cellValue)) {
                return ""; //$NON-NLS-1$
            }
            return ObjectViewerRenderer.getCellString(cellValue, columnIndex == 0);
        }

    }

    public class ObjectsLoadVisualizer extends ProgressVisualizer<Collection<OBJECT_TYPE>> {

        public ObjectsLoadVisualizer() {
        }

        @Override
        public void completeLoading(Collection<OBJECT_TYPE> items) {
            super.completeLoading(items);
            setListData(items, false);
        }

    }

    public class ObjectActionVisualizer extends ProgressVisualizer<Void> {

        public ObjectActionVisualizer() {
        }

        @Override
        public void completeLoading(Void v) {
            super.completeLoading(v);
        }
    }

    private static class LazyValue {
        private final Object value;

        private LazyValue(Object value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value.toString();
        }
    }

    class PaintListener implements Listener {

        @Override
        public void handleEvent(Event event) {
            if (isDisposed()) {
                return;
            }
            switch (event.type) {
            case SWT.PaintItem:
                if (event.index < columns.size()) {
                    final OBJECT_TYPE object = (OBJECT_TYPE) event.item.getData();
                    final Object objectValue = getObjectValue(object);
                    Object cellValue = getCellValue(object, event.index);
                    final ObjectColumn objectColumn = columns.get(event.index);
                    if (cellValue instanceof LazyValue) {
                        if (!lazyLoadCanceled) {
                            addLazyObject(object, objectColumn);
                        }
                    } else if (cellValue != null) {
                        ObjectPropertyDescriptor prop = getPropertyByObject(objectColumn, objectValue);
                        if (prop != null) {
                            renderer.paintCell(event, object, event.index, prop.isEditable(objectValue));
                        }
                    }
                    break;
                }
            }
        }
    }

    private class LazyLoaderJob extends AbstractJob {
        public LazyLoaderJob() {
            super(CoreMessages.controls_object_list_job_props_read);
        }

        @Override
        protected IStatus run(final DBRProgressMonitor monitor) {
            final Map<OBJECT_TYPE, List<ObjectColumn>> objectMap = obtainLazyObjects();
            if (isDisposed()) {
                return Status.OK_STATUS;
            }
            monitor.beginTask(CoreMessages.controls_object_list_monitor_load_lazy_props, objectMap.size());
            for (Map.Entry<OBJECT_TYPE, List<ObjectColumn>> entry : objectMap.entrySet()) {
                if (monitor.isCanceled() || isDisposed()) {
                    break;
                }
                final OBJECT_TYPE element = entry.getKey();
                Object object = getObjectValue(element);
                if (object == null) {
                    continue;
                }
                Map<String, Object> objectCache;
                synchronized (lazyCache) {
                    objectCache = lazyCache.get(element);
                    if (objectCache == null) {
                        objectCache = new HashMap<>();
                        lazyCache.put(element, objectCache);
                    }
                }
                String objectName = GeneralUtils.makeDisplayString(getObjectValue(element)).toString();
                monitor.subTask(NLS.bind(CoreMessages.controls_object_list_monitor_load_props, objectName));
                for (ObjectColumn column : entry.getValue()) {
                    if (monitor.isCanceled() || isDisposed()) {
                        break;
                    }
                    ObjectPropertyDescriptor prop = getPropertyByObject(column, object);
                    if (prop != null) {
                        try {
                            synchronized (lazyCache) {
                                if (objectCache.containsKey(prop.getId())) {
                                    // This property already cached
                                    continue;
                                }
                            }
                            Object lazyValue = prop.readValue(object, monitor);
                            if (lazyValue == null) {
                                lazyValue = NULL_VALUE;
                            }
                            synchronized (lazyCache) {
                                objectCache.put(prop.getId(), lazyValue);
                            }
                        } catch (Throwable e) {
                            if (e instanceof InvocationTargetException) {
                                e = ((InvocationTargetException) e).getTargetException();
                            }
                            log.error("Error reading property '" + prop.getId() + "' from " + object, e); //$NON-NLS-1$ //$NON-NLS-2$
                            // do not return error - it causes a lot of error boxes
                            //return RuntimeUtils.makeExceptionStatus(e);
                        }
                    }
                }
                monitor.worked(1);
            }
            monitor.done();
            if (!isDisposed()) {
                // Make refresh of whole table
                // Some other objects could also be updated implicitly with our lazy loader
                getDisplay().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        if (!isDisposed()) {
                            itemsViewer.refresh();
                        }
                    }
                });
            }

            /*
                        // Update viewer
                        if (!isDisposed()) {
            getDisplay().asyncExec(new Runnable() {
                public void run()
                {
                    itemsViewer.getControl().setRedraw(false);
                    try {
                        itemsViewer.update(objectMap.keySet().toArray(), null);
                    } finally {
                        itemsViewer.getControl().setRedraw(true);
                    }
                }
            });
                        }
            */
            if (monitor.isCanceled()) {
                lazyLoadCanceled = true;
                obtainLazyObjects();
            }
            return Status.OK_STATUS;
        }
    }

    protected class ViewerRenderer extends ObjectViewerRenderer {
        protected ViewerRenderer() {
            super(itemsViewer);
        }

        @Nullable
        @Override
        protected Object getCellValue(Object element, int columnIndex) {
            return ObjectListControl.this.getCellValue(element, columnIndex);
        }
    }

}