/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * 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.navigator.database; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.viewers.*; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.TreeEditor; import org.eclipse.swt.events.*; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; import org.eclipse.ui.IWorkbenchCommandConstants; import org.eclipse.ui.dialogs.FilteredTree; import org.eclipse.ui.dialogs.PatternFilter; import org.eclipse.ui.progress.WorkbenchJob; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.DBeaverPreferences; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.core.DBeaverCore; import org.jkiss.dbeaver.core.DBeaverUI; import org.jkiss.dbeaver.model.navigator.*; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.DBRRunnableWithResult; import org.jkiss.dbeaver.ui.AbstractUIJob; import org.jkiss.dbeaver.ui.ActionUtils; import org.jkiss.dbeaver.ui.actions.navigator.NavigatorHandlerObjectRename; import org.jkiss.dbeaver.ui.navigator.NavigatorUtils; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; public class DatabaseNavigatorTree extends Composite implements INavigatorListener { private static final Log log = Log.getLog(DatabaseNavigatorTree.class); private TreeViewer treeViewer; private DBNModel model; private TreeEditor treeEditor; private boolean checkEnabled; private ISelection defaultSelection; private IFilter navigatorFilter; public DatabaseNavigatorTree(Composite parent, DBNNode rootNode, int style) { this(parent, rootNode, style, false); } public DatabaseNavigatorTree(Composite parent, DBNNode rootNode, int style, boolean showRoot) { this(parent, rootNode, style, showRoot, null); } public DatabaseNavigatorTree(Composite parent, DBNNode rootNode, int style, boolean showRoot, IFilter navigatorFilter) { super(parent, SWT.NONE); this.setLayout(new FillLayout()); this.navigatorFilter = navigatorFilter; this.defaultSelection = new StructuredSelection(rootNode); this.model = DBeaverCore.getInstance().getNavigatorModel(); this.model.addListener(this); addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { if (model != null) { model.removeListener(DatabaseNavigatorTree.this); model = null; } } }); treeViewer = doCreateTreeViewer(this, style); treeViewer.getTree().setCursor(getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); treeViewer.setUseHashlookup(true); if (rootNode.getParentNode() == null) { //this.treeViewer.setAutoExpandLevel(2); } treeViewer.setLabelProvider(new DatabaseNavigatorLabelProvider(treeViewer)); treeViewer.setContentProvider(new DatabaseNavigatorContentProvider(this, showRoot)); treeViewer.setInput(new DatabaseNavigatorContent(rootNode)); ColumnViewerToolTipSupport.enableFor(treeViewer); initEditor(); } private TreeViewer doCreateTreeViewer(Composite parent, int style) { checkEnabled = (style & SWT.CHECK) != 0; // Create tree int treeStyle = SWT.H_SCROLL | SWT.V_SCROLL | style; if (checkEnabled) { return new CheckboxTreeViewer(parent, treeStyle); } else { if (navigatorFilter != null) { CustomFilteredTree filteredTree = new CustomFilteredTree(this, treeStyle); return filteredTree.getViewer(); } else { return doCreateNavigatorTreeViewer(parent, style); } } } private TreeViewer doCreateNavigatorTreeViewer(Composite parent, int style) { return new TreeViewer(parent, style) { @Override public ISelection getSelection() { ISelection selection = super.getSelection(); return selection.isEmpty() && defaultSelection != null ? defaultSelection : selection; } protected void handleTreeExpand(TreeEvent event) { // Disable redraw during expand (its blinking) getTree().setRedraw(false); try { super.handleTreeExpand(event); } finally { getTree().setRedraw(true); } } protected void handleTreeCollapse(TreeEvent event) { getTree().setRedraw(false); try { super.handleTreeCollapse(event); } finally { getTree().setRedraw(true); } } }; } public DBNNode getModel() { DatabaseNavigatorContent content = (DatabaseNavigatorContent) this.treeViewer.getInput(); return content.getRootNode(); } private void initEditor() { Tree treeControl = this.treeViewer.getTree(); treeEditor = new TreeEditor(treeControl); treeEditor.horizontalAlignment = SWT.LEFT; treeEditor.verticalAlignment = SWT.TOP; treeEditor.grabHorizontal = false; treeEditor.minimumWidth = 50; //treeControl.addSelectionListener(new TreeSelectionAdapter()); if (!checkEnabled) { // Add rename listener only for non CHECK trees treeControl.addMouseListener(new TreeSelectionAdapter()); } } @NotNull public TreeViewer getViewer() { return treeViewer; } @Override public void nodeChanged(final DBNEvent event) { switch (event.getAction()) { case ADD: case REMOVE: { final DBNNode node = event.getNode(); final DBNNode parentNode = node.getParentNode(); if (parentNode != null) { DBeaverUI.syncExec(new Runnable() { @Override public void run() { if (!treeViewer.getControl().isDisposed()) { if (!parentNode.isDisposed()) { treeViewer.refresh(getViewerObject(parentNode)); if (event.getNodeChange() == DBNEvent.NodeChange.SELECT) { treeViewer.reveal(node); treeViewer.setSelection(new StructuredSelection(node)); } } } } }); } break; } case UPDATE: DBeaverUI.syncExec(new NodeUpdater(event)); break; default: break; } } private void expandNodeOnLoad(final DBNNode node) { if (node instanceof DBNDataSource && DBeaverCore.getGlobalPreferenceStore().getBoolean(DBeaverPreferences.NAVIGATOR_EXPAND_ON_CONNECT)) { try { DBRRunnableWithResult<DBNNode> runnable = new DBRRunnableWithResult<DBNNode>() { @Override public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { try { result = findActiveNode(monitor, node); } catch (DBException e) { throw new InvocationTargetException(e); } } }; DBeaverUI.runInProgressService(runnable); if (runnable.getResult() != null) { showNode(runnable.getResult()); treeViewer.expandToLevel(runnable.getResult(), 1); } } catch (InvocationTargetException e) { log.error("Can't expand node", e.getTargetException()); } catch (InterruptedException e) { // skip it } } } private DBNNode findActiveNode(DBRProgressMonitor monitor, DBNNode node) throws DBException { DBNNode[] children = node.getChildren(monitor); if (!ArrayUtils.isEmpty(children)) { if (children[0] instanceof DBNContainer) { // Use only first folder to search return findActiveNode(monitor, children[0]); } for (DBNNode child : children) { if (NavigatorUtils.isDefaultElement(child)) { return child; } } } return node; } Object getViewerObject(DBNNode node) { if (((DatabaseNavigatorContent) treeViewer.getInput()).getRootNode() == node) { return treeViewer.getInput(); } else { return node; } } public void showNode(DBNNode node) { treeViewer.reveal(node); treeViewer.setSelection(new StructuredSelection(node)); } public void reloadTree(final DBNNode rootNode) { DatabaseNavigatorTree.this.treeViewer.setInput(new DatabaseNavigatorContent(rootNode)); } private class TreeSelectionAdapter implements MouseListener { private volatile TreeItem curSelection; private volatile RenameJob renameJob = new RenameJob(); private volatile boolean doubleClick = false; @Override public synchronized void mouseDoubleClick(MouseEvent e) { curSelection = null; renameJob.canceled = true; } @Override public void mouseDown(MouseEvent e) { } @Override public void mouseUp(MouseEvent e) { if ((e.stateMask & SWT.BUTTON1) == 0) { curSelection = null; return; } changeSelection(e); } public void changeSelection(MouseEvent e) { disposeOldEditor(); final TreeItem newSelection = treeViewer.getTree().getItem(new Point(e.x, e.y)); if (newSelection == null) { return; } if (!(newSelection.getData() instanceof DBNNode) || !(ActionUtils.isCommandEnabled(IWorkbenchCommandConstants.FILE_RENAME, DBeaverUI.getActiveWorkbenchWindow().getActivePage().getActivePart().getSite()))) { curSelection = null; return; } if (curSelection != null && curSelection == newSelection && renameJob.selection == null) { renameJob.selection = curSelection; renameJob.schedule(1000); } curSelection = newSelection; } private class RenameJob extends AbstractUIJob { private volatile boolean canceled = false; public TreeItem selection; public RenameJob() { super("Rename "); } @Override protected IStatus runInUIThread(DBRProgressMonitor monitor) { try { if (!treeViewer.getTree().isDisposed() && treeViewer.getTree().isFocusControl() && curSelection == selection && !canceled) { final TreeItem itemToRename = selection; DBeaverUI.asyncExec(new Runnable() { @Override public void run() { renameItem(itemToRename); } }); } } finally { canceled = false; selection = null; } return Status.OK_STATUS; } } } private void renameItem(final TreeItem item) { // Clean up any previous editor control disposeOldEditor(); if (item.isDisposed()) { return; } final DBNNode node = (DBNNode) item.getData(); Text text = new Text(treeViewer.getTree(), SWT.BORDER); text.setText(node.getNodeName()); text.selectAll(); text.setFocus(); text.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { disposeOldEditor(); } }); text.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.keyCode == SWT.CR) { Text text = (Text) treeEditor.getEditor(); final String newName = text.getText(); disposeOldEditor(); treeViewer.getTree().setFocus(); if (!CommonUtils.isEmpty(newName) && !newName.equals(node.getNodeName())) { NavigatorHandlerObjectRename.renameNode(DBeaverUI.getActiveWorkbenchWindow(), node, newName); } } else if (e.keyCode == SWT.ESC) { disposeOldEditor(); treeViewer.getTree().setFocus(); } } }); final Rectangle itemBounds = item.getBounds(0); final Rectangle treeBounds = treeViewer.getTree().getBounds(); treeEditor.minimumWidth = Math.max(itemBounds.width, 50); treeEditor.minimumWidth = Math.min(treeEditor.minimumWidth, treeBounds.width - (itemBounds.x - treeBounds.x) - item.getImageBounds(0).width - 4); treeEditor.setEditor(text, item, 0); } private void disposeOldEditor() { Control oldEditor = treeEditor.getEditor(); if (oldEditor != null) oldEditor.dispose(); } //////////////////////////////////////////////////////////////////////////// // Filtered tree private static class TreeFilter extends PatternFilter { private final IFilter filter; TreeFilter(IFilter filter) { setIncludeLeadingWildcard(true); this.filter = filter; } public Object[] filter(Viewer viewer, Object parent, Object[] elements) { int size = elements.length; ArrayList<Object> out = new ArrayList<>(size); for (int i = 0; i < size; ++i) { Object element = elements[i]; if (select(viewer, parent, element)) { out.add(element); } } return out.toArray(); } public boolean isElementVisible(Viewer viewer, Object element){ if (filter.select(element)) { return true; } return super.isLeafMatch(viewer, element); } } private static class CustomFilteredTree extends FilteredTree { CustomFilteredTree(DatabaseNavigatorTree navigatorTree, int treeStyle) { super(navigatorTree, treeStyle, new TreeFilter(navigatorTree.navigatorFilter), true); setInitialText("Type table/view name to filter"); ((GridLayout)getLayout()).verticalSpacing = 0; } @Override protected TreeViewer doCreateTreeViewer(Composite parent, int style) { return ((DatabaseNavigatorTree)getParent()).doCreateNavigatorTreeViewer(parent, style); } protected WorkbenchJob doCreateRefreshJob() { return new WorkbenchJob("Refresh Filter") {//$NON-NLS-1$ @Override public IStatus runInUIThread(IProgressMonitor monitor) { if (treeViewer.getControl().isDisposed()) { return Status.CANCEL_STATUS; } String text = getFilterString(); if (text == null) { return Status.OK_STATUS; } boolean initial = initialText != null && initialText.equals(text); if (initial) { getPatternFilter().setPattern(null); } else { getPatternFilter().setPattern(text); } final Control redrawFalseControl = treeComposite != null ? treeComposite : treeViewer.getControl(); try { // don't want the user to see updates that will be made to // the tree // we are setting redraw(false) on the composite to avoid // dancing scrollbar redrawFalseControl.setRedraw(false); treeViewer.refresh(true); if (text.length() > 0 && !initial) { // enabled toolbar - there is text to clear // and the list is currently being filtered updateToolbar(true); } else { // disabled toolbar - there is no text to clear // and the list is currently not filtered updateToolbar(false); } } finally { // done updating the tree - set redraw back to true redrawFalseControl.setRedraw(true); } return Status.OK_STATUS; } }; } } private class NodeUpdater implements Runnable { private final DBNEvent event; NodeUpdater(DBNEvent event) { this.event = event; } @Override public void run() { if (!treeViewer.getControl().isDisposed() && !treeViewer.isBusy()) { if (event.getNode() != null) { switch (event.getNodeChange()) { case LOAD: treeViewer.refresh(getViewerObject(event.getNode())); expandNodeOnLoad(event.getNode()); break; case UNLOAD: treeViewer.collapseToLevel(event.getNode(), -1); treeViewer.refresh(getViewerObject(event.getNode())); break; case REFRESH: treeViewer.refresh(getViewerObject(event.getNode()), true); break; case LOCK: case UNLOCK: case STRUCT_REFRESH: treeViewer.refresh(getViewerObject(event.getNode())); break; } } else { log.warn("Null node object"); } } } } }