/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * Copyright (C) 2011-2012 Eugene Fradkin (eugene.fradkin@gmail.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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.dnd.TextTransfer; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Color; 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.core.DBeaverUI; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.data.DBDDisplayFormat; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.runtime.properties.*; import org.jkiss.dbeaver.ui.*; import org.jkiss.dbeaver.ui.controls.ObjectViewerRenderer; import org.jkiss.dbeaver.ui.controls.ProgressPageControl; import org.jkiss.dbeaver.ui.controls.ViewerColumnController; 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 implements IClipboardSource { private static final Log log = Log.getLog(ObjectListControl.class); private final static LazyValue DEF_LAZY_VALUE = new LazyValue("..."); //$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; private IDoubleClickListener doubleClickHandler; private PropertySourceAbstract listPropertySource; private ObjectViewerRenderer renderer; protected ViewerColumnController columnController; // 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; private Object focusObject; private ObjectColumn focusColumn; 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 TraverseListener traverseListener = new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { if (e.detail == SWT.TRAVERSE_RETURN && doubleClickHandler != null) { doubleClickHandler.doubleClick(new DoubleClickEvent(itemsViewer, itemsViewer.getSelection())); e.doit = false; } } }; 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.addTraverseListener(traverseListener); } 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_VERTICAL | ColumnViewerEditor.TABBING_HORIZONTAL); table.addTraverseListener(traverseListener); } //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); } }); } /** * Used to save/load columns configuration. * Must depend on object types * * @param classList classes of objects in the list */ @NotNull protected abstract String getListConfigId(List<Class<?>> classList); 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(ObjectColumn objectColumn) { return new ObjectColumnLabelProvider(objectColumn); } @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 getColumnByIndex(int index) { return (ObjectColumn) columnController.getColumnData(index); } public void setFitWidth(boolean fitWidth) { isFitWidth = fitWidth; } @Override public void disposeControl() { 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) { int dataLoadUpdatePeriod = 200; int dataLoadTimes = getDataLoadTimeout() / dataLoadUpdatePeriod; try { for (int i = 0; i < dataLoadTimes; i++) { Thread.sleep(dataLoadUpdatePeriod); 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(); } } } protected int getDataLoadTimeout() { return 4000; } protected 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) || (columnController == null); { // 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); if (reload) { clearListData(); columnController = new ViewerColumnController(getListConfigId(classList), getItemsViewer()); } // Create columns from classes' annotations for (ObjectPropertyDescriptor prop : allProps) { if (!getListPropertySource().hasProperty(prop)) { getListPropertySource().addProperty(prop); createColumn(prop); } } } if (itemsControl.isDisposed()) { return; } if (reload) { columnController.createColumns(false); } 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); } if (reload) { columnController.repackColumns(); } } 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() { if (columnController != null) { columnController.dispose(); columnController = null; } 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 (objectValue != null) { 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 protected final Object getCellValue(Object element, int columnIndex) { final ObjectColumn columnInfo = getColumnByIndex(columnIndex); return getCellValue(element, columnInfo); } @Nullable protected Object getCellValue(Object element, ObjectColumn objectColumn) { OBJECT_TYPE object = (OBJECT_TYPE) element; Object objectValue = getObjectValue(object); if (objectValue == null) { return null; } ObjectPropertyDescriptor prop = getPropertyByObject(objectColumn, 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(objectColumn.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); } /** * Gets property descriptor by column and object value (NB! not OBJECT_TYPE but real object value). */ @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 Color getObjectBackground(OBJECT_TYPE item) { return null; } protected Color getObjectForeground(OBJECT_TYPE item) { return null; } protected boolean isNewObject(OBJECT_TYPE objectValue) { return false; } @NotNull protected Set<DBPPropertyDescriptor> getAllProperties() { ObjectColumn[] columns = columnController.getColumnsData(ObjectColumn.class); 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 : columnController.getColumnsData(ObjectColumn.class)) { if (CommonUtils.equalObjects(col.id, prop.getId()) || CommonUtils.equalObjects(col.displayName, prop.getDisplayName())) { objectColumn = col; break; } } // Use prop class from top parent Class<?> propClass = prop.getParent() == null ? prop.getDeclaringClass() : prop.getParent().getDeclaringClass(); if (objectColumn == null) { if (prop.isHidden()) { return; } objectColumn = new ObjectColumn(prop.getId(), prop.getDisplayName()); objectColumn.addProperty(propClass, prop); final CellLabelProvider labelProvider = getColumnLabelProvider(objectColumn); final EditingSupport editingSupport = makeEditingSupport(objectColumn); // Add column in controller columnController.addColumn( prop.getDisplayName(), prop.getDescription(), prop.isNumeric() ? SWT.RIGHT : SWT.NONE, prop.isViewable(), prop.isNameProperty(), prop.isNumeric(), objectColumn, labelProvider, editingSupport); } 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(ObjectColumn objectColumn) { return null; } protected void setFocusCell(Object object, ObjectColumn objectColumn) { this.focusObject = object; this.focusColumn = objectColumn; } ////////////////////////////////////////////////////// // Clipboard @Override public void addClipboardData(CopyMode mode, ClipboardData clipboardData) { String selectedText; if (mode == CopyMode.ADVANCED) { // Multiple rows selected // Copy all of them in tsv format selectedText = copyGridToText(); if (!CommonUtils.isEmpty(selectedText)) { clipboardData.addTransfer(TextTransfer.getInstance(), selectedText); } } else { IStructuredSelection selection = itemsViewer.getStructuredSelection(); if (selection.size() > 1) { StringBuilder buf = new StringBuilder(); for (Iterator iter = selection.iterator(); iter.hasNext(); ) { Object object = getObjectValue((OBJECT_TYPE) iter.next()); ObjectColumn nameColumn = null; int columnsCount = columnController.getColumnsCount(); for (int i = 0 ; i < columnsCount; i++) { ObjectColumn column = getColumnByIndex(i); if (column.isNameColumn(object)) { nameColumn = column; break; } } String objectName = null; if (nameColumn != null) { try { ObjectPropertyDescriptor nameProperty = nameColumn.getProperty(object); if (nameProperty != null) { objectName = CommonUtils.toString(nameProperty.readValue(object, null)); } } catch (Throwable e) { log.debug(e); } } if (objectName == null) { if (object instanceof DBPNamedObject) { objectName = ((DBPNamedObject) object).getName(); } else { objectName = DBValueFormatting.getDefaultValueDisplayString(object, DBDDisplayFormat.UI); } } if (buf.length() > 0) buf.append("\n"); buf.append(objectName); } selectedText = buf.toString(); } else { selectedText = getRenderer().getSelectedText(); } } if (!CommonUtils.isEmpty(selectedText)) { clipboardData.addTransfer(TextTransfer.getInstance(), selectedText); } } ////////////////////////////////////////////////////// // 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; String displayName; Map<Class<?>, ObjectPropertyDescriptor> propMap = new IdentityHashMap<>(); private ObjectColumn(String id, String displayName) { this.id = id; this.displayName = displayName; } void addProperty(Class<?> objectClass, ObjectPropertyDescriptor prop) { this.propMap.put(objectClass, prop); } public boolean isNameColumn(Object object) { final ObjectPropertyDescriptor property = getProperty(object); return property != null && property.isNameProperty(); } @Nullable public ObjectPropertyDescriptor getProperty(Object object) { return object == null ? null : getPropertyByObject(this, object); } } ////////////////////////////////////////////////////// // List sorter protected class ObjectColumnLabelProvider extends ColumnLabelProvider implements ILabelProviderEx { protected final ObjectColumn objectColumn; ObjectColumnLabelProvider(ObjectColumn objectColumn) { this.objectColumn = objectColumn; } @Nullable @Override public Image getImage(Object element) { OBJECT_TYPE object = (OBJECT_TYPE) element; final Object objectValue = getObjectValue(object); if (objectValue == null) { // This may happen if list redraw happens during node dispose return null; } final ObjectPropertyDescriptor prop = getPropertyByObject(objectColumn, objectValue); if (prop != null && prop.isNameProperty()) { DBPImage objectImage = getObjectImage(object); return objectImage == null ? null : DBeaverIcons.getImage(objectImage); } return null; } @Override public String getText(Object element) { return getText(element, true); } @Override public Color getBackground(Object element) { final Color background = getObjectBackground((OBJECT_TYPE) element); if (background == null) { } return background; } @Override public Color getForeground(Object element) { return getObjectForeground((OBJECT_TYPE) element); } @Override public String getText(Object element, boolean forUI) { Object cellValue = getCellValue(element, objectColumn); if (cellValue instanceof LazyValue) { cellValue = ((LazyValue) cellValue).value; } if (forUI && !sampleItems && renderer.isHyperlink(cellValue)) { return ""; //$NON-NLS-1$ } final Object objectValue = getObjectValue((OBJECT_TYPE) element); if (objectValue == null) { // This may happen if list redraw happens during node dispose return ""; } final ObjectPropertyDescriptor prop = getPropertyByObject(objectColumn, objectValue); if (prop != null) { if (forUI && cellValue instanceof Boolean) { return ""; } return ObjectViewerRenderer.getCellString(cellValue, prop.isNameProperty()); } else { return ""; } } } 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 e) { if (isDisposed()) { return; } switch (e.type) { case SWT.PaintItem: if (e.index < columnController.getColumnsCount()) { final ObjectColumn objectColumn = getColumnByIndex(e.index); final OBJECT_TYPE object = (OBJECT_TYPE) e.item.getData(); final boolean isFocusCell = focusObject == object && focusColumn == objectColumn; final Object objectValue = getObjectValue(object); Object cellValue = getCellValue(object, objectColumn); if (cellValue instanceof LazyValue) { if (!lazyLoadCanceled) { addLazyObject(object, objectColumn); } } else if (cellValue != null) { ObjectPropertyDescriptor prop = getPropertyByObject(objectColumn, objectValue); if (prop != null) { if (itemsViewer.isCellEditorActive() && isFocusCell) { // Do not paint over active editor return; } renderer.paintCell(e, object, e.item, e.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 DBeaverUI.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); } } private String copyGridToText() { StringBuilder buf = new StringBuilder(); int columnsCount = columnController.getColumnsCount(); { // Header for (int i = 0; i < columnsCount; i++) { ObjectColumn column = getColumnByIndex(i); if (i > 0) buf.append("\t"); buf.append(column.displayName); } buf.append("\n"); } List<OBJECT_TYPE> elementList = itemsViewer.getStructuredSelection().toList(); for (OBJECT_TYPE element : elementList) { Object object = getObjectValue(element); for (int i = 0; i < columnsCount; i++) { ObjectPropertyDescriptor property = getColumnByIndex(i).getProperty(object); try { Object cellValue = property == null ? null : property.readValue(object, new VoidProgressMonitor()); if (i > 0) buf.append("\t"); String strValue = DBValueFormatting.getDefaultValueDisplayString(cellValue, DBDDisplayFormat.UI); if (strValue.contains("\n") || strValue.contains("\t")) { strValue = '"' + strValue + '"'; } buf.append(strValue); } catch (Throwable e) { // ignore } } buf.append("\n"); } return buf.toString(); } }