org.jkiss.dbeaver.ui.controls.resultset.ResultSetViewer.java Source code

Java tutorial

Introduction

Here is the source code for org.jkiss.dbeaver.ui.controls.resultset.ResultSetViewer.java

Source

/*
 * DBeaver - Universal Database Manager
 * Copyright (C) 2010-2016 Serge Rieder (serge@jkiss.org)
 *
 * 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.resultset;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.action.*;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.StringConverter;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.*;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.part.MultiPageEditorPart;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.DBeaverPreferences;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.core.CoreCommands;
import org.jkiss.dbeaver.core.CoreMessages;
import org.jkiss.dbeaver.core.DBeaverCore;
import org.jkiss.dbeaver.core.DBeaverUI;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.data.*;
import org.jkiss.dbeaver.model.edit.DBEPersistAction;
import org.jkiss.dbeaver.model.exec.*;
import org.jkiss.dbeaver.model.impl.local.StatResultSet;
import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRRunnableWithProgress;
import org.jkiss.dbeaver.model.runtime.RunnableWithResult;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.model.sql.SQLUtils;
import org.jkiss.dbeaver.model.struct.*;
import org.jkiss.dbeaver.model.virtual.*;
import org.jkiss.dbeaver.tools.transfer.IDataTransferProducer;
import org.jkiss.dbeaver.tools.transfer.database.DatabaseTransferProducer;
import org.jkiss.dbeaver.tools.transfer.wizard.DataTransferWizard;
import org.jkiss.dbeaver.ui.*;
import org.jkiss.dbeaver.ui.actions.navigator.NavigatorHandlerObjectOpen;
import org.jkiss.dbeaver.ui.controls.CImageCombo;
import org.jkiss.dbeaver.ui.controls.resultset.view.EmptyPresentation;
import org.jkiss.dbeaver.ui.controls.resultset.view.StatisticsPresentation;
import org.jkiss.dbeaver.ui.data.IValueController;
import org.jkiss.dbeaver.ui.data.managers.BaseValueManager;
import org.jkiss.dbeaver.ui.dialogs.ActiveWizardDialog;
import org.jkiss.dbeaver.ui.dialogs.ConfirmationDialog;
import org.jkiss.dbeaver.ui.dialogs.EditTextDialog;
import org.jkiss.dbeaver.ui.dialogs.struct.EditConstraintDialog;
import org.jkiss.dbeaver.ui.dialogs.struct.EditDictionaryDialog;
import org.jkiss.dbeaver.ui.editors.data.DatabaseDataEditor;
import org.jkiss.dbeaver.ui.preferences.PrefPageDataFormat;
import org.jkiss.dbeaver.ui.preferences.PrefPageDatabaseGeneral;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;

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

/**
 * ResultSetViewer
 *
 * TODO: fix presentation contributions to toolbar
 *
 * TODO: fix copy multiple cells - tabulation broken
 * TODO: links in both directions, multiple links support (context menu)
 * TODO: not-editable cells (struct owners in record mode)
 * TODO: PROBLEM. Multiple occurrences of the same struct type in a single table.
 * Need to make wrapper over DBSAttributeBase or something. Or maybe it is not a problem
 * because we search for binding by attribute only in constraints and for unique key columns which are unique?
 * But what PK has struct type?
 *
 */
public class ResultSetViewer extends Viewer
        implements DBPContextProvider, IResultSetController, ISaveablePart2, IAdaptable {
    private static final Log log = Log.getLog(ResultSetViewer.class);

    @NotNull
    private static IResultSetFilterManager filterManager = new SimpleFilterManager();

    public static void registerFilterManager(@Nullable IResultSetFilterManager filterManager) {
        if (filterManager == null) {
            filterManager = new SimpleFilterManager();
        }
        ResultSetViewer.filterManager = filterManager;
    }

    @NotNull
    private final IWorkbenchPartSite site;
    private final Composite viewerPanel;
    private ResultSetFilterPanel filtersPanel;
    private final Composite presentationPanel;
    private Text statusLabel;

    private final DynamicFindReplaceTarget findReplaceTarget;

    // Presentation
    private IResultSetPresentation activePresentation;
    private ResultSetPresentationDescriptor activePresentationDescriptor;
    private List<ResultSetPresentationDescriptor> availablePresentations;
    private PresentationSwitchCombo presentationSwitchCombo;

    @NotNull
    private final IResultSetContainer container;
    @NotNull
    private final ResultSetDataReceiver dataReceiver;

    private ToolBarManager toolBarManager;

    // Current row/col number
    @Nullable
    private ResultSetRow curRow;
    // Mode
    private boolean recordMode;

    private final List<IResultSetListener> listeners = new ArrayList<>();

    private volatile ResultSetDataPumpJob dataPumpJob;

    private final ResultSetModel model = new ResultSetModel();
    private StateItem curState = null;
    private final List<StateItem> stateHistory = new ArrayList<>();
    private int historyPosition = -1;

    private final Color colorRed;

    private boolean actionsDisabled;

    public ResultSetViewer(@NotNull Composite parent, @NotNull IWorkbenchPartSite site,
            @NotNull IResultSetContainer container) {
        super();

        this.site = site;
        this.recordMode = false;
        this.container = container;
        this.dataReceiver = new ResultSetDataReceiver(this);

        this.colorRed = Display.getDefault().getSystemColor(SWT.COLOR_RED);

        this.viewerPanel = UIUtils.createPlaceholder(parent, 1);
        UIUtils.setHelp(this.viewerPanel, IHelpContextIds.CTX_RESULT_SET_VIEWER);

        this.filtersPanel = new ResultSetFilterPanel(this);
        this.findReplaceTarget = new DynamicFindReplaceTarget();
        this.presentationPanel = UIUtils.createPlaceholder(viewerPanel, 1);
        this.presentationPanel.setLayoutData(new GridData(GridData.FILL_BOTH));

        setActivePresentation(new EmptyPresentation());

        createStatusBar();

        this.viewerPanel.addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                dispose();
            }
        });

        changeMode(false);
    }

    @Override
    @NotNull
    public IResultSetContainer getContainer() {
        return container;
    }

    ////////////////////////////////////////////////////////////
    // Filters

    boolean supportsDataFilter() {
        DBSDataContainer dataContainer = getDataContainer();
        return dataContainer != null && (dataContainer.getSupportedFeatures()
                & DBSDataContainer.DATA_FILTER) == DBSDataContainer.DATA_FILTER;
    }

    public void resetDataFilter(boolean refresh) {
        setDataFilter(model.createDataFilter(), refresh);
    }

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

    private void updateFiltersText(boolean resetFilterValue) {
        boolean enableFilters = false;
        DBCExecutionContext context = getExecutionContext();
        if (context != null) {
            if (activePresentation instanceof StatisticsPresentation) {
                enableFilters = false;
            } else {
                StringBuilder where = new StringBuilder();
                SQLUtils.appendConditionString(model.getDataFilter(), context.getDataSource(), null, where, true);
                String whereCondition = where.toString().trim();
                if (resetFilterValue) {
                    filtersPanel.setFilterValue(whereCondition);
                    if (!whereCondition.isEmpty()) {
                        filtersPanel.addFiltersHistory(whereCondition);
                    }
                }

                if (container.isReadyToRun() && !model.isUpdateInProgress()
                        && model.getVisibleAttributeCount() > 0) {
                    enableFilters = true;
                }
            }
        }
        filtersPanel.enableFilters(enableFilters);
        presentationSwitchCombo.combo.setEnabled(enableFilters);
    }

    public void setDataFilter(final DBDDataFilter dataFilter, boolean refreshData) {
        if (!model.getDataFilter().equals(dataFilter)) {
            //model.setDataFilter(dataFilter);
            if (refreshData) {
                refreshWithFilter(dataFilter);
            } else {
                model.setDataFilter(dataFilter);
                activePresentation.refreshData(true, false);
                updateFiltersText();
            }
        }
    }

    ////////////////////////////////////////////////////////////
    // Misc

    @NotNull
    public DBPPreferenceStore getPreferenceStore() {
        DBCExecutionContext context = getExecutionContext();
        if (context != null) {
            return context.getDataSource().getContainer().getPreferenceStore();
        }
        return DBeaverCore.getGlobalPreferenceStore();
    }

    private void persistConfig() {
        DBCExecutionContext context = getExecutionContext();
        if (context != null) {
            context.getDataSource().getContainer().persistConfiguration();
        }
    }

    @Override
    public Color getDefaultBackground() {
        return filtersPanel.getEditControl().getBackground();
    }

    @Override
    public Color getDefaultForeground() {
        return filtersPanel.getEditControl().getForeground();
    }

    public List<ResultSetPresentationDescriptor> getAvailablePresentations() {
        return availablePresentations;
    }

    @Override
    public IResultSetPresentation getActivePresentation() {
        return activePresentation;
    }

    public boolean isActionsDisabled() {
        return actionsDisabled;
    }

    @Override
    public void lockActionsByControl(Control lockedBy) {
        if (checkDoubleLock(lockedBy)) {
            return;
        }
        actionsDisabled = true;
        lockedBy.addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                actionsDisabled = false;
            }
        });
    }

    @Override
    public void lockActionsByFocus(final Control lockedBy) {
        lockedBy.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                checkDoubleLock(lockedBy);
                actionsDisabled = true;
            }

            @Override
            public void focusLost(FocusEvent e) {
                actionsDisabled = false;
            }
        });
        lockedBy.addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                actionsDisabled = false;
            }
        });
    }

    private boolean checkDoubleLock(Control lockedBy) {
        if (actionsDisabled) {
            log.error("Internal error: actions double-lock by [" + lockedBy + "]");
            return true;
        }
        return false;
    }

    void updatePresentation(final DBCResultSet resultSet) {
        try {
            if (resultSet instanceof StatResultSet) {
                // Statistics - let's use special presentation for it
                availablePresentations = Collections.emptyList();
                setActivePresentation(new StatisticsPresentation());
                activePresentationDescriptor = null;
            } else {
                // Regular results
                IResultSetContext context = new IResultSetContext() {
                    @Override
                    public boolean supportsAttributes() {
                        DBDAttributeBinding[] attrs = model.getAttributes();
                        return attrs.length > 0 && (attrs[0].getDataKind() != DBPDataKind.DOCUMENT
                                || !CommonUtils.isEmpty(attrs[0].getNestedBindings()));
                    }

                    @Override
                    public boolean supportsDocument() {
                        return model.getDocumentAttribute() != null;
                    }

                    @Override
                    public String getDocumentContentType() {
                        DBDAttributeBinding docAttr = model.getDocumentAttribute();
                        return docAttr == null ? null : docAttr.getValueHandler().getValueContentType(docAttr);
                    }
                };
                availablePresentations = ResultSetPresentationRegistry.getInstance()
                        .getAvailablePresentations(resultSet, context);
                if (!availablePresentations.isEmpty()) {
                    for (ResultSetPresentationDescriptor pd : availablePresentations) {
                        if (pd == activePresentationDescriptor) {
                            // Keep the same presentation
                            return;
                        }
                    }
                    String defaultPresentationId = getPreferenceStore()
                            .getString(DBeaverPreferences.RESULT_SET_PRESENTATION);
                    ResultSetPresentationDescriptor newPresentation = null;
                    if (!CommonUtils.isEmpty(defaultPresentationId)) {
                        for (ResultSetPresentationDescriptor pd : availablePresentations) {
                            if (pd.getId().equals(defaultPresentationId)) {
                                newPresentation = pd;
                                break;
                            }
                        }
                    }
                    if (newPresentation == null) {
                        newPresentation = availablePresentations.get(0);
                    }
                    try {
                        IResultSetPresentation instance = newPresentation.createInstance();
                        activePresentationDescriptor = newPresentation;
                        setActivePresentation(instance);
                    } catch (DBException e) {
                        log.error(e);
                    }
                }
            }
        } finally {
            // Update combo
            CImageCombo combo = presentationSwitchCombo.combo;
            combo.setRedraw(false);
            try {
                if (activePresentationDescriptor == null) {
                    combo.setEnabled(false);
                } else {
                    combo.setEnabled(true);
                    combo.removeAll();
                    for (ResultSetPresentationDescriptor pd : availablePresentations) {
                        combo.add(DBeaverIcons.getImage(pd.getIcon()), pd.getLabel(), null, pd);
                    }
                    combo.select(activePresentationDescriptor);
                }
            } finally {
                // Enable redraw
                combo.setRedraw(true);
            }
        }

    }

    private void setActivePresentation(IResultSetPresentation presentation) {
        // Dispose previous presentation
        for (Control child : presentationPanel.getChildren()) {
            child.dispose();
        }
        // Set new one
        activePresentation = presentation;
        activePresentation.createPresentation(this, presentationPanel);
        presentationPanel.layout();

        // Update dynamic find/replace target
        {
            IFindReplaceTarget nested = null;
            if (presentation instanceof IAdaptable) {
                nested = ((IAdaptable) presentation).getAdapter(IFindReplaceTarget.class);
            }
            findReplaceTarget.setTarget(nested);
        }

        // Update toolbar
        if (toolBarManager != null) {
            // Cleanup previous presentation contributions
            boolean presContributions = false;
            for (IContributionItem item : toolBarManager.getItems()) {
                if (presContributions) {
                    if (IResultSetPresentation.PRES_TOOLS_END.equals(item.getId())) {
                        break;
                    }
                    toolBarManager.remove(item);
                } else if (IResultSetPresentation.PRES_TOOLS_BEGIN.equals(item.getId())) {
                    presContributions = true;
                }
            }

            // fill presentation contributions
            activePresentation.fillToolbar(toolBarManager);
            toolBarManager.update(true);
        }

        // Set focus in presentation control
        // Use async exec to avoid focus switch after user UI interaction (e.g. combo)
        Display display = getControl().getDisplay();
        if (UIUtils.isParent(viewerPanel, display.getFocusControl())) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    activePresentation.getControl().setFocus();
                }
            });
        }
    }

    /**
     * Switch to the next presentation
     */
    void switchPresentation() {
        if (availablePresentations.size() < 2) {
            return;
        }
        int index = availablePresentations.indexOf(activePresentationDescriptor);
        if (index < availablePresentations.size() - 1) {
            index++;
        } else {
            index = 0;
        }
        switchPresentation(availablePresentations.get(index));
    }

    private void switchPresentation(ResultSetPresentationDescriptor selectedPresentation) {
        try {
            IResultSetPresentation instance = selectedPresentation.createInstance();
            activePresentationDescriptor = selectedPresentation;
            setActivePresentation(instance);
            instance.refreshData(true, false);

            presentationSwitchCombo.combo.select(activePresentationDescriptor);
            // Save in global preferences
            DBeaverCore.getGlobalPreferenceStore().setValue(DBeaverPreferences.RESULT_SET_PRESENTATION,
                    activePresentationDescriptor.getId());
        } catch (Throwable e1) {
            UIUtils.showErrorDialog(viewerPanel.getShell(), "Presentation switch", "Can't switch presentation", e1);
        }
    }

    @Nullable
    @Override
    public <T> T getAdapter(Class<T> adapter) {
        if (adapter == IFindReplaceTarget.class) {
            return adapter.cast(findReplaceTarget);
        }
        if (adapter.isAssignableFrom(activePresentation.getClass())) {
            return adapter.cast(activePresentation);
        }
        // Try to get it from adapter
        if (activePresentation instanceof IAdaptable) {
            return ((IAdaptable) activePresentation).getAdapter(adapter);
        }
        return null;
    }

    public void addListener(IResultSetListener listener) {
        synchronized (listeners) {
            listeners.add(listener);
        }
    }

    public void removeListener(IResultSetListener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }

    private void updateRecordMode() {
        //Object state = savePresentationState();
        //this.redrawData(false);
        activePresentation.refreshData(true, false);
        this.updateStatusMessage();
        //restorePresentationState(state);
    }

    public void updateEditControls() {
        ResultSetPropertyTester.firePropertyChange(ResultSetPropertyTester.PROP_EDITABLE);
        ResultSetPropertyTester.firePropertyChange(ResultSetPropertyTester.PROP_CHANGED);
        updateToolbar();
    }

    /**
     * It is a hack function. Generally all command associated widgets should be updated automatically by framework.
     * Freaking E4 do not do it. I've spent a couple of days fighting it. Guys, you owe me.
     */
    private void updateToolbar() {
        if (toolBarManager.isEmpty()) {
            return;
        }
        for (IContributionItem item : toolBarManager.getItems()) {
            item.update();
        }
    }

    public void redrawData(boolean rowsChanged) {
        if (viewerPanel.isDisposed()) {
            return;
        }
        if (rowsChanged) {
            int rowCount = model.getRowCount();
            if (curRow == null || curRow.getVisualNumber() >= rowCount) {
                curRow = rowCount == 0 ? null : model.getRow(rowCount - 1);
            }

            // Set cursor on new row
            if (!recordMode) {
                activePresentation.refreshData(false, false);
                this.updateFiltersText();
                this.updateStatusMessage();
            } else {
                this.updateRecordMode();
            }
        } else {
            activePresentation.refreshData(false, false);
        }
    }

    private void createStatusBar() {
        UIUtils.createHorizontalLine(viewerPanel);

        Composite statusBar = new Composite(viewerPanel, SWT.NONE);
        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
        statusBar.setLayoutData(gd);
        GridLayout gl = new GridLayout(4, false);
        gl.marginWidth = 0;
        gl.marginHeight = 0;
        //gl.marginBottom = 5;
        statusBar.setLayout(gl);

        statusLabel = new Text(statusBar, SWT.READ_ONLY);
        gd = new GridData(GridData.FILL_HORIZONTAL);
        statusLabel.setLayoutData(gd);
        statusLabel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDoubleClick(MouseEvent e) {
                EditTextDialog.showText(site.getShell(), CoreMessages.controls_resultset_viewer_dialog_status_title,
                        statusLabel.getText());
            }
        });

        toolBarManager = new ToolBarManager(SWT.FLAT | SWT.HORIZONTAL);

        // Add presentation switcher
        presentationSwitchCombo = new PresentationSwitchCombo();
        presentationSwitchCombo.createControl(statusBar);
        //toolBarManager.add(presentationSwitchCombo);
        //toolBarManager.add(new Separator());

        // handle own commands
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_APPLY_CHANGES));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_REJECT_CHANGES));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_GENERATE_SCRIPT));
        toolBarManager.add(new Separator());
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_EDIT));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_ADD));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_COPY));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_DELETE));
        toolBarManager.add(new Separator());
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_FIRST));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_PREVIOUS));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_NEXT));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_LAST));
        toolBarManager.add(new Separator());
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_FETCH_PAGE));
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_FETCH_ALL));
        // Use simple action for refresh to avoid ambiguous behaviour of F5 shortcut
        toolBarManager.add(new Separator());
        //        // FIXME: Link to standard Find/Replace action - it has to be handled by owner site
        //        toolBarManager.add(ActionUtils.makeCommandContribution(
        //            site,
        //            IWorkbenchCommandConstants.EDIT_FIND_AND_REPLACE,
        //            CommandContributionItem.STYLE_PUSH,
        //            UIIcon.FIND_TEXT));

        toolBarManager.add(new Separator());
        toolBarManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_TOGGLE_MODE,
                CommandContributionItem.STYLE_CHECK));
        toolBarManager.add(new GroupMarker(IResultSetPresentation.PRES_TOOLS_BEGIN));
        toolBarManager.add(new GroupMarker(IResultSetPresentation.PRES_TOOLS_END));

        toolBarManager.add(new ConfigAction());
        toolBarManager.createControl(statusBar);

        //updateEditControls();
    }

    @Nullable
    public DBSDataContainer getDataContainer() {
        return curState != null ? curState.dataContainer : container.getDataContainer();
    }

    ////////////////////////////////////////////////////////////
    // Grid/Record mode

    @Override
    public boolean isRecordMode() {
        return recordMode;
    }

    public void toggleMode() {
        changeMode(!recordMode);

        // Refresh elements
        ICommandService commandService = site.getService(ICommandService.class);
        if (commandService != null) {
            commandService.refreshElements(ResultSetCommandHandler.CMD_TOGGLE_MODE, null);
        }
    }

    private void changeMode(boolean recordMode) {
        //Object state = savePresentationState();
        this.recordMode = recordMode;
        //redrawData(false);
        activePresentation.refreshData(true, false);
        activePresentation.changeMode(recordMode);
        updateStatusMessage();
        //restorePresentationState(state);
    }

    ////////////////////////////////////////////////////////////
    // Misc

    private void dispose() {
        clearData();

        if (toolBarManager != null) {
            try {
                toolBarManager.dispose();
            } catch (Throwable e) {
                // ignore
                log.debug("Error disposing toolbar", e);
            }
        }
    }

    public boolean isAttributeReadOnly(DBDAttributeBinding attribute) {
        if (isReadOnly()) {
            return true;
        }
        if (!model.isAttributeReadOnly(attribute)) {
            return false;
        }
        boolean newRow = (curRow != null && curRow.getState() == ResultSetRow.STATE_ADDED);
        return !newRow;
    }

    private Object savePresentationState() {
        if (activePresentation instanceof IStatefulControl) {
            return ((IStatefulControl) activePresentation).saveState();
        } else {
            return null;
        }
    }

    private void restorePresentationState(Object state) {
        if (activePresentation instanceof IStatefulControl) {
            ((IStatefulControl) activePresentation).restoreState(state);
        }
    }

    ///////////////////////////////////////
    // History

    List<StateItem> getStateHistory() {
        return stateHistory;
    }

    private void setNewState(DBSDataContainer dataContainer, @Nullable DBDDataFilter dataFilter) {
        // Create filter copy to avoid modifications
        dataFilter = new DBDDataFilter(dataFilter);
        // Search in history
        for (int i = 0; i < stateHistory.size(); i++) {
            StateItem item = stateHistory.get(i);
            if (item.dataContainer == dataContainer && CommonUtils.equalObjects(item.filter, dataFilter)) {
                curState = item;
                historyPosition = i;
                return;
            }
        }
        // Save current state in history
        while (historyPosition < stateHistory.size() - 1) {
            stateHistory.remove(stateHistory.size() - 1);
        }
        curState = new StateItem(dataContainer, dataFilter, curRow == null ? -1 : curRow.getVisualNumber());
        stateHistory.add(curState);
        historyPosition = stateHistory.size() - 1;
    }

    public void resetHistory() {
        curState = null;
        stateHistory.clear();
        historyPosition = -1;
    }

    ///////////////////////////////////////
    // Misc

    @Nullable
    public ResultSetRow getCurrentRow() {
        return curRow;
    }

    @Override
    public void setCurrentRow(@Nullable ResultSetRow curRow) {
        this.curRow = curRow;
        if (curState != null && curRow != null) {
            curState.rowNumber = curRow.getVisualNumber();
        }
        //        if (recordMode) {
        //            updateRecordMode();
        //        }
    }

    ///////////////////////////////////////
    // Status

    public void setStatus(String status) {
        setStatus(status, false);
    }

    public void setStatus(String status, boolean error) {
        if (statusLabel.isDisposed()) {
            return;
        }
        if (error) {
            statusLabel.setForeground(colorRed);
        } else if (colorRed.equals(statusLabel.getForeground())) {
            statusLabel.setForeground(getDefaultForeground());
        }
        if (status == null) {
            status = "???"; //$NON-NLS-1$
        }
        statusLabel.setText(status);
    }

    public void updateStatusMessage() {
        if (model.getRowCount() == 0) {
            if (model.getVisibleAttributeCount() == 0) {
                setStatus(CoreMessages.controls_resultset_viewer_status_empty + getExecutionTimeMessage());
            } else {
                setStatus(CoreMessages.controls_resultset_viewer_status_no_data + getExecutionTimeMessage());
            }
        } else {
            if (recordMode) {
                setStatus(CoreMessages.controls_resultset_viewer_status_row
                        + (curRow == null ? 0 : curRow.getVisualNumber() + 1) + "/" + model.getRowCount()
                        + getExecutionTimeMessage());
            } else {
                setStatus(String.valueOf(model.getRowCount())
                        + CoreMessages.controls_resultset_viewer_status_rows_fetched + getExecutionTimeMessage());
            }
        }
    }

    private String getExecutionTimeMessage() {
        DBCStatistics statistics = model.getStatistics();
        if (statistics == null || statistics.isEmpty()) {
            return "";
        }
        return " - " + RuntimeUtils.formatExecutionTime(statistics.getTotalTime());
    }

    /**
     * Sets new metadata of result set
     * @param attributes attributes metadata
     */
    void setMetaData(DBDAttributeBinding[] attributes) {
        model.setMetaData(attributes);
        activePresentation.clearMetaData();
    }

    void setData(List<Object[]> rows) {
        if (viewerPanel.isDisposed()) {
            return;
        }
        this.curRow = null;
        this.model.setData(rows);
        this.curRow = (this.model.getRowCount() > 0 ? this.model.getRow(0) : null);

        {

            if (getPreferenceStore().getBoolean(DBeaverPreferences.RESULT_SET_AUTO_SWITCH_MODE)) {
                boolean newRecordMode = (rows.size() == 1);
                if (newRecordMode != recordMode) {
                    toggleMode();
                    //                    ResultSetPropertyTester.firePropertyChange(ResultSetPropertyTester.PROP_CAN_TOGGLE);
                }
            }
        }

        this.activePresentation.refreshData(true, false);
        if (recordMode) {
            this.updateRecordMode();
        }
        this.updateFiltersText();
        this.updateStatusMessage();
        this.updateEditControls();
    }

    void appendData(List<Object[]> rows) {
        model.appendData(rows);
        //redrawData(true);
        activePresentation.refreshData(false, true);

        setStatus(
                NLS.bind(CoreMessages.controls_resultset_viewer_status_rows_size, model.getRowCount(), rows.size())
                        + getExecutionTimeMessage());

        updateEditControls();
    }

    @Override
    public int promptToSaveOnClose() {
        if (!isDirty()) {
            return ISaveablePart2.YES;
        }
        int result = ConfirmationDialog.showConfirmDialog(viewerPanel.getShell(),
                DBeaverPreferences.CONFIRM_RS_EDIT_CLOSE, ConfirmationDialog.QUESTION_WITH_CANCEL);
        if (result == IDialogConstants.YES_ID) {
            return ISaveablePart2.YES;
        } else if (result == IDialogConstants.NO_ID) {
            rejectChanges();
            return ISaveablePart2.NO;
        } else {
            return ISaveablePart2.CANCEL;
        }
    }

    @Override
    public void doSave(IProgressMonitor monitor) {
        applyChanges(RuntimeUtils.makeMonitor(monitor));
    }

    public void doSave(DBRProgressMonitor monitor) {
        applyChanges(monitor);
    }

    @Override
    public void doSaveAs() {
    }

    @Override
    public boolean isDirty() {
        return model.isDirty();
    }

    @Override
    public boolean isSaveAsAllowed() {
        return false;
    }

    @Override
    public boolean isSaveOnCloseNeeded() {
        return true;
    }

    @Override
    public boolean hasData() {
        return model.hasData();
    }

    @Override
    public boolean isHasMoreData() {
        return getExecutionContext() != null && dataReceiver.isHasMoreData();
    }

    @Override
    public boolean isReadOnly() {
        if (model.isUpdateInProgress() || !(activePresentation instanceof IResultSetEditor)) {
            return true;
        }
        DBCExecutionContext executionContext = getExecutionContext();
        return executionContext == null || !executionContext.isConnected()
                || executionContext.getDataSource().getContainer().isConnectionReadOnly()
                || executionContext.getDataSource().getInfo().isReadOnlyData();
    }

    /**
     * Checks that current state of result set allows to insert new rows
     * @return true if new rows insert is allowed
     */
    public boolean isInsertable() {
        return !isReadOnly() && model.isSingleSource() && model.getVisibleAttributeCount() > 0;
    }

    public boolean isRefreshInProgress() {
        return dataPumpJob != null;
    }

    ///////////////////////////////////////////////////////
    // Context menu & filters

    public void showFiltersMenu() {
        DBDAttributeBinding curAttribute = getActivePresentation().getCurrentAttribute();
        if (curAttribute == null) {
            return;
        }
        Control control = getActivePresentation().getControl();
        Point cursorLocation = getActivePresentation().getCursorLocation();
        Point location = control.getDisplay().map(control, null, cursorLocation);

        MenuManager menuManager = new MenuManager();
        fillFiltersMenu(curAttribute, menuManager);

        final Menu contextMenu = menuManager.createContextMenu(control);
        contextMenu.setLocation(location);
        contextMenu.setVisible(true);
    }

    @Override
    public void fillContextMenu(@NotNull IMenuManager manager, @Nullable final DBDAttributeBinding attr,
            @Nullable final ResultSetRow row) {
        final DBPDataSource dataSource = getDataContainer() == null ? null : getDataContainer().getDataSource();

        // Custom oldValue items
        final ResultSetValueController valueController;
        if (attr != null && row != null) {
            valueController = new ResultSetValueController(this, attr, row, IValueController.EditType.NONE, null);

            final Object value = valueController.getValue();
            {
                // Standard items
                manager.add(ActionUtils.makeCommandContribution(site, IWorkbenchCommandConstants.EDIT_CUT));
                manager.add(ActionUtils.makeCommandContribution(site, IWorkbenchCommandConstants.EDIT_COPY));

                MenuManager extCopyMenu = new MenuManager(
                        ActionUtils.findCommandName(CoreCommands.CMD_COPY_SPECIAL));
                extCopyMenu.add(ActionUtils.makeCommandContribution(site, CoreCommands.CMD_COPY_SPECIAL));
                extCopyMenu.add(new Action("Copy column name(s)") {
                    @Override
                    public void run() {
                        StringBuilder buffer = new StringBuilder();
                        for (DBDAttributeBinding attr : getSelection().getSelectedAttributes()) {
                            if (buffer.length() > 0) {
                                buffer.append("\t");
                            }
                            buffer.append(attr.getName());
                        }
                        ResultSetUtils.copyToClipboard(buffer.toString());
                    }
                });
                extCopyMenu.add(new Action("Copy row number(s)") {
                    @Override
                    public void run() {
                        StringBuilder buffer = new StringBuilder();
                        for (ResultSetRow row : getSelection().getSelectedRows()) {
                            if (buffer.length() > 0) {
                                buffer.append("\n");
                            }
                            buffer.append(row.getVisualNumber());
                        }
                        ResultSetUtils.copyToClipboard(buffer.toString());
                    }
                });
                manager.add(extCopyMenu);

                manager.add(ActionUtils.makeCommandContribution(site, IWorkbenchCommandConstants.EDIT_PASTE));
                manager.add(ActionUtils.makeCommandContribution(site, CoreCommands.CMD_PASTE_SPECIAL));
                manager.add(ActionUtils.makeCommandContribution(site, IWorkbenchCommandConstants.EDIT_DELETE));
                // Edit items
                manager.add(new Separator());
                manager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_EDIT));
                manager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_EDIT_INLINE));
                if (!valueController.isReadOnly() && !DBUtils.isNullValue(value)/* && !attr.isRequired()*/) {
                    manager.add(new Action(CoreMessages.controls_resultset_viewer_action_set_to_null) {
                        @Override
                        public void run() {
                            valueController.updateValue(BaseValueManager.makeNullValue(valueController));
                        }
                    });
                }
                manager.add(new GroupMarker(MENU_GROUP_EDIT));
            }

            // Menus from value handler
            try {
                manager.add(new Separator());
                valueController.getValueManager().contributeActions(manager, valueController, null);
            } catch (Exception e) {
                log.error(e);
            }

            if (row.isChanged()) {
                Action resetValueAction = new Action(CoreMessages.controls_resultset_viewer_action_reset_value) {
                    @Override
                    public void run() {
                        model.resetCellValue(attr, row);
                        updateValueView();
                    }
                };
                resetValueAction.setAccelerator(SWT.ESC);
                manager.insertAfter(IResultSetController.MENU_GROUP_EDIT, resetValueAction);
            }
        } else {
            valueController = null;
        }

        if (dataSource != null && attr != null && model.getVisibleAttributeCount() > 0
                && !model.isUpdateInProgress()) {
            // Filters and View
            manager.add(new Separator());
            {
                MenuManager filtersMenu = new MenuManager(
                        CoreMessages.controls_resultset_viewer_action_order_filter,
                        DBeaverIcons.getImageDescriptor(UIIcon.FILTER), "filters"); //$NON-NLS-1$
                filtersMenu.setRemoveAllWhenShown(true);
                filtersMenu.addMenuListener(new IMenuListener() {
                    @Override
                    public void menuAboutToShow(IMenuManager manager) {
                        fillFiltersMenu(attr, manager);
                    }
                });
                manager.add(filtersMenu);
            }
            {
                MenuManager viewMenu = new MenuManager("View", null, "view"); //$NON-NLS-2$

                List<? extends DBDAttributeTransformerDescriptor> transformers = dataSource.getContainer()
                        .getApplication().getValueHandlerRegistry().findTransformers(dataSource, attr, null);
                if (!CommonUtils.isEmpty(transformers)) {
                    MenuManager transformersMenu = new MenuManager("View as");
                    transformersMenu.setRemoveAllWhenShown(true);
                    transformersMenu.addMenuListener(new IMenuListener() {
                        @Override
                        public void menuAboutToShow(IMenuManager manager) {
                            fillAttributeTransformersMenu(manager, attr);
                        }
                    });
                    viewMenu.add(transformersMenu);
                } else {
                    final Action customizeAction = new Action("View as") {
                    };
                    customizeAction.setEnabled(false);
                    viewMenu.add(customizeAction);
                }
                if (getModel().isSingleSource()) {
                    if (valueController != null) {
                        viewMenu.add(new SetRowColorAction(attr, valueController.getValue()));
                        if (getModel().hasColorMapping(attr)) {
                            viewMenu.add(new ResetRowColorAction(attr, valueController.getValue()));
                        }
                    }
                    viewMenu.add(new CustomizeColorsAction(attr, row));
                    viewMenu.add(new Separator());
                }
                viewMenu.add(new Action("Data formats ...") {
                    @Override
                    public void run() {
                        UIUtils.showPreferencesFor(getControl().getShell(), null, PrefPageDataFormat.PAGE_ID);
                    }
                });
                manager.add(viewMenu);
            }

            {
                // Navigate
                MenuManager navigateMenu = new MenuManager("Navigate", null, "navigate"); //$NON-NLS-2$
                if (ActionUtils.isCommandEnabled(ResultSetCommandHandler.CMD_NAVIGATE_LINK, site)) {
                    navigateMenu.add(
                            ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_NAVIGATE_LINK));
                    navigateMenu.add(new Separator());
                }
                navigateMenu
                        .add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_TOGGLE_MODE));
                navigateMenu.add(
                        ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_SWITCH_PRESENTATION));
                navigateMenu.add(new Separator());
                navigateMenu
                        .add(ActionUtils.makeCommandContribution(site, ITextEditorActionDefinitionIds.LINE_GOTO));
                navigateMenu.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_FIRST));
                navigateMenu.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_NEXT));
                navigateMenu
                        .add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_PREVIOUS));
                navigateMenu.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_ROW_LAST));

                manager.add(navigateMenu);
            }
        }

        // Fill general menu
        final DBSDataContainer dataContainer = getDataContainer();
        if (dataContainer != null && model.hasData()) {
            manager.add(new Action(CoreMessages.controls_resultset_viewer_action_export,
                    DBeaverIcons.getImageDescriptor(UIIcon.EXPORT)) {
                @Override
                public void run() {
                    ActiveWizardDialog dialog = new ActiveWizardDialog(site.getWorkbenchWindow(),
                            new DataTransferWizard(
                                    new IDataTransferProducer[] {
                                            new DatabaseTransferProducer(dataContainer, model.getDataFilter()) },
                                    null),
                            getSelection());
                    dialog.open();
                }
            });
        }
        manager.add(new GroupMarker(CoreCommands.GROUP_TOOLS));
        if (dataContainer != null && model.hasData()) {
            manager.add(new Separator());
            manager.add(ActionUtils.makeCommandContribution(site, IWorkbenchCommandConstants.FILE_REFRESH));
        }

        manager.add(new Separator());
        manager.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS));
    }

    private class TransformerAction extends Action {
        private final DBDAttributeBinding attrribute;

        public TransformerAction(DBDAttributeBinding attr, String text, int style, boolean checked) {
            super(text, style);
            this.attrribute = attr;
            setChecked(checked);
        }

        @NotNull
        DBVTransformSettings getTransformSettings() {
            final DBVTransformSettings settings = DBVUtils.getTransformSettings(attrribute, true);
            if (settings == null) {
                throw new IllegalStateException(
                        "Can't get/create transformer settings for '" + attrribute.getFullQualifiedName() + "'");
            }
            return settings;
        }

        protected void saveTransformerSettings() {
            attrribute.getDataSource().getContainer().persistConfiguration();
            refreshData(null);
        }
    }

    private void fillAttributeTransformersMenu(IMenuManager manager, final DBDAttributeBinding attr) {
        final DBSDataContainer dataContainer = getDataContainer();
        if (dataContainer == null) {
            return;
        }
        final DBPDataSource dataSource = dataContainer.getDataSource();
        final DBDRegistry registry = dataSource.getContainer().getApplication().getValueHandlerRegistry();
        final DBVTransformSettings transformSettings = DBVUtils.getTransformSettings(attr, false);
        DBDAttributeTransformerDescriptor customTransformer = null;
        if (transformSettings != null && transformSettings.getCustomTransformer() != null) {
            customTransformer = registry.getTransformer(transformSettings.getCustomTransformer());
        }
        List<? extends DBDAttributeTransformerDescriptor> customTransformers = registry.findTransformers(dataSource,
                attr, true);
        if (customTransformers != null && !customTransformers.isEmpty()) {
            manager.add(new TransformerAction(attr, "Default", IAction.AS_RADIO_BUTTON,
                    transformSettings == null || CommonUtils.isEmpty(transformSettings.getCustomTransformer())) {
                @Override
                public void run() {
                    if (isChecked()) {
                        getTransformSettings().setCustomTransformer(null);
                        saveTransformerSettings();
                    }
                }
            });
            for (final DBDAttributeTransformerDescriptor descriptor : customTransformers) {
                final TransformerAction action = new TransformerAction(attr, descriptor.getName(),
                        IAction.AS_RADIO_BUTTON, transformSettings != null
                                && descriptor.getId().equals(transformSettings.getCustomTransformer())) {
                    @Override
                    public void run() {
                        if (isChecked()) {
                            final DBVTransformSettings settings = getTransformSettings();
                            final String oldCustomTransformer = settings.getCustomTransformer();
                            settings.setCustomTransformer(descriptor.getId());
                            TransformerSettingsDialog settingsDialog = new TransformerSettingsDialog(
                                    ResultSetViewer.this, attr, settings);
                            if (settingsDialog.open() == IDialogConstants.OK_ID) {
                                saveTransformerSettings();
                            } else {
                                settings.setCustomTransformer(oldCustomTransformer);
                            }
                        }
                    }
                };
                manager.add(action);
            }
        }
        if (customTransformer != null && !CommonUtils.isEmpty(customTransformer.getProperties())) {
            manager.add(new TransformerAction(attr, "Settings ...", IAction.AS_UNSPECIFIED, false) {
                @Override
                public void run() {
                    TransformerSettingsDialog settingsDialog = new TransformerSettingsDialog(ResultSetViewer.this,
                            attr, transformSettings);
                    if (settingsDialog.open() == IDialogConstants.OK_ID) {
                        saveTransformerSettings();
                    }
                }
            });
        }

        List<? extends DBDAttributeTransformerDescriptor> applicableTransformers = registry
                .findTransformers(dataSource, attr, false);
        if (applicableTransformers != null) {
            manager.add(new Separator());

            for (final DBDAttributeTransformerDescriptor descriptor : applicableTransformers) {
                boolean checked;
                if (transformSettings != null) {
                    if (descriptor.isApplicableByDefault()) {
                        checked = !transformSettings.isExcluded(descriptor.getId());
                    } else {
                        checked = transformSettings.isIncluded(descriptor.getId());
                    }
                } else {
                    checked = descriptor.isApplicableByDefault();
                }
                manager.add(new TransformerAction(attr, descriptor.getName(), IAction.AS_CHECK_BOX, checked) {
                    @Override
                    public void run() {
                        getTransformSettings().enableTransformer(descriptor, !isChecked());
                        saveTransformerSettings();
                    }
                });
            }
        }
    }

    private void fillFiltersMenu(@NotNull DBDAttributeBinding attribute, @NotNull IMenuManager filtersMenu) {
        if (supportsDataFilter()) {
            //filtersMenu.add(new FilterByListAction(operator, type, attribute));
            DBCLogicalOperator[] operators = attribute.getValueHandler().getSupportedOperators(attribute);
            // Operators with multiple inputs
            for (DBCLogicalOperator operator : operators) {
                if (operator.getArgumentCount() < 0) {
                    filtersMenu.add(new FilterByAttributeAction(operator, FilterByAttributeType.INPUT, attribute));
                }
            }
            filtersMenu.add(new Separator());
            // Operators with no inputs
            for (DBCLogicalOperator operator : operators) {
                if (operator.getArgumentCount() == 0) {
                    filtersMenu.add(new FilterByAttributeAction(operator, FilterByAttributeType.NONE, attribute));
                }
            }
            for (FilterByAttributeType type : FilterByAttributeType.values()) {
                if (type == FilterByAttributeType.NONE) {
                    // Value filters are available only if certain cell is selected
                    continue;
                }
                filtersMenu.add(new Separator());
                if (type.getValue(this, attribute, DBCLogicalOperator.EQUALS, true) == null) {
                    // Null cell value - no operators can be applied
                    continue;
                }
                for (DBCLogicalOperator operator : operators) {
                    if (operator.getArgumentCount() > 0) {
                        filtersMenu.add(new FilterByAttributeAction(operator, type, attribute));
                    }
                }
            }
            filtersMenu.add(new Separator());
            DBDAttributeConstraint constraint = model.getDataFilter().getConstraint(attribute);
            if (constraint != null && constraint.hasCondition()) {
                filtersMenu.add(new FilterResetAttributeAction(attribute));
            }
        }
        filtersMenu.add(new Separator());
        filtersMenu.add(new ToggleServerSideOrderingAction());
        filtersMenu.add(new ShowFiltersAction(true));
    }

    @Override
    public void navigateAssociation(@NotNull DBRProgressMonitor monitor, @NotNull DBDAttributeBinding attr,
            @NotNull ResultSetRow row, boolean newWindow) throws DBException {
        if (getExecutionContext() == null) {
            throw new DBException("Not connected");
        }
        DBSEntityAssociation association = null;
        List<DBSEntityReferrer> referrers = attr.getReferrers();
        if (referrers != null) {
            for (final DBSEntityReferrer referrer : referrers) {
                if (referrer instanceof DBSEntityAssociation) {
                    association = (DBSEntityAssociation) referrer;
                    break;
                }
            }
        }
        if (association == null) {
            throw new DBException("Association not found in attribute [" + attr.getName() + "]");
        }

        DBSEntityConstraint refConstraint = association.getReferencedConstraint();
        if (refConstraint == null) {
            throw new DBException("Broken association (referenced constraint missing)");
        }
        if (!(refConstraint instanceof DBSEntityReferrer)) {
            throw new DBException("Referenced constraint [" + refConstraint + "] is not a referrer");
        }
        DBSEntity targetEntity = refConstraint.getParentObject();
        if (targetEntity == null) {
            throw new DBException("Null constraint parent");
        }
        if (!(targetEntity instanceof DBSDataContainer)) {
            throw new DBException(
                    "Entity [" + DBUtils.getObjectFullName(targetEntity) + "] is not a data container");
        }

        // make constraints
        List<DBDAttributeConstraint> constraints = new ArrayList<>();
        int visualPosition = 0;
        // Set conditions
        List<? extends DBSEntityAttributeRef> ownAttrs = CommonUtils
                .safeList(((DBSEntityReferrer) association).getAttributeReferences(monitor));
        List<? extends DBSEntityAttributeRef> refAttrs = CommonUtils
                .safeList(((DBSEntityReferrer) refConstraint).getAttributeReferences(monitor));
        if (ownAttrs.size() != refAttrs.size()) {
            throw new DBException("Entity [" + DBUtils.getObjectFullName(targetEntity) + "] association ["
                    + association.getName() + "] columns differs from referenced constraint ["
                    + refConstraint.getName() + "] (" + ownAttrs.size() + "<>" + refAttrs.size() + ")");
        }
        // Add association constraints
        for (int i = 0; i < ownAttrs.size(); i++) {
            DBSEntityAttributeRef ownAttr = ownAttrs.get(i);
            DBSEntityAttributeRef refAttr = refAttrs.get(i);
            DBDAttributeBinding ownBinding = model.getAttributeBinding(ownAttr.getAttribute());
            assert ownBinding != null;

            DBDAttributeConstraint constraint = new DBDAttributeConstraint(refAttr.getAttribute(),
                    visualPosition++);
            constraint.setVisible(true);
            constraints.add(constraint);

            Object keyValue = model.getCellValue(ownBinding, row);
            constraint.setOperator(DBCLogicalOperator.EQUALS);
            constraint.setValue(keyValue);
        }
        DBDDataFilter newFilter = new DBDDataFilter(constraints);

        if (newWindow) {
            openResultsInNewWindow(monitor, targetEntity, newFilter);
        } else {
            runDataPump((DBSDataContainer) targetEntity, newFilter, 0, getSegmentMaxRows(), -1, null);
        }
    }

    private void openResultsInNewWindow(DBRProgressMonitor monitor, DBSEntity targetEntity,
            final DBDDataFilter newFilter) {
        final DBNDatabaseNode targetNode = getExecutionContext().getDataSource().getContainer().getApplication()
                .getNavigatorModel().getNodeByObject(monitor, targetEntity, false);
        if (targetNode == null) {
            UIUtils.showMessageBox(null, "Open link", "Can't navigate to '"
                    + DBUtils.getObjectFullName(targetEntity) + "' - navigator node not found", SWT.ICON_ERROR);
            return;
        }
        UIUtils.runInDetachedUI(null, new Runnable() {
            @Override
            public void run() {
                openNewDataEditor(targetNode, newFilter);
            }
        });
    }

    @Override
    public int getHistoryPosition() {
        return historyPosition;
    }

    @Override
    public int getHistorySize() {
        return stateHistory.size();
    }

    @Override
    public void navigateHistory(int position) {
        if (position < 0 || position >= stateHistory.size()) {
            // out of range
            log.debug("Wrong history position: " + position);
            return;
        }
        StateItem state = stateHistory.get(position);
        int segmentSize = getSegmentMaxRows();
        if (state.rowNumber >= 0 && state.rowNumber >= segmentSize && segmentSize > 0) {
            segmentSize = (state.rowNumber / segmentSize + 1) * segmentSize;
        }

        runDataPump(state.dataContainer, state.filter, 0, segmentSize, state.rowNumber, null);
    }

    @Override
    public void updateValueView() {
        activePresentation.updateValueView();
        updateEditControls();
    }

    @Override
    public Composite getControl() {
        return this.viewerPanel;
    }

    @NotNull
    @Override
    public IWorkbenchPartSite getSite() {
        return site;
    }

    @Override
    @NotNull
    public ResultSetModel getModel() {
        return model;
    }

    @Override
    public ResultSetModel getInput() {
        return model;
    }

    @Override
    public void setInput(Object input) {
        throw new IllegalArgumentException("ResultSet model can't be changed");
    }

    @Override
    @NotNull
    public IResultSetSelection getSelection() {
        if (activePresentation instanceof ISelectionProvider) {
            ISelection selection = ((ISelectionProvider) activePresentation).getSelection();
            if (selection instanceof IResultSetSelection) {
                return (IResultSetSelection) selection;
            }
        }
        return new EmptySelection();
    }

    @Override
    public void setSelection(ISelection selection, boolean reveal) {
        if (activePresentation instanceof ISelectionProvider) {
            ((ISelectionProvider) activePresentation).setSelection(selection);
        }
    }

    @NotNull
    @Override
    public DBDDataReceiver getDataReceiver() {
        return dataReceiver;
    }

    @NotNull
    @Override
    public IResultSetFilterManager getFilterManager() {
        return filterManager;
    }

    @Nullable
    @Override
    public DBCExecutionContext getExecutionContext() {
        return container.getExecutionContext();
    }

    @Override
    public void refresh() {
        // Check if we are dirty
        if (isDirty()) {
            switch (promptToSaveOnClose()) {
            case ISaveablePart2.CANCEL:
                return;
            case ISaveablePart2.YES:
                // Apply changes
                applyChanges(null, new ResultSetPersister.DataUpdateListener() {
                    @Override
                    public void onUpdate(boolean success) {
                        if (success) {
                            getControl().getDisplay().asyncExec(new Runnable() {
                                @Override
                                public void run() {
                                    refresh();
                                }
                            });
                        }
                    }
                });
                return;
            default:
                // Just ignore previous RS values
                break;
            }
        }

        // Pump data
        ResultSetRow oldRow = curRow;

        DBSDataContainer dataContainer = getDataContainer();
        if (container.isReadyToRun() && dataContainer != null && dataPumpJob == null) {
            int segmentSize = getSegmentMaxRows();
            if (oldRow != null && oldRow.getVisualNumber() >= segmentSize && segmentSize > 0) {
                segmentSize = (oldRow.getVisualNumber() / segmentSize + 1) * segmentSize;
            }
            runDataPump(dataContainer, null, 0, segmentSize, oldRow == null ? -1 : oldRow.getVisualNumber(),
                    new Runnable() {
                        @Override
                        public void run() {
                            activePresentation.formatData(true);
                        }
                    });
        } else {
            UIUtils.showErrorDialog(null, "Error executing query",
                    dataContainer == null ? "Viewer detached from data source"
                            : dataPumpJob == null ? "Can't refresh after reconnect. Re-execute query."
                                    : "Previous query is still running");
        }
    }

    public void refreshWithFilter(DBDDataFilter filter) {
        DBSDataContainer dataContainer = getDataContainer();
        if (dataContainer != null) {
            runDataPump(dataContainer, filter, 0, getSegmentMaxRows(), -1, null);
        }
    }

    @Override
    public boolean refreshData(@Nullable Runnable onSuccess) {
        DBSDataContainer dataContainer = getDataContainer();
        if (container.isReadyToRun() && dataContainer != null && dataPumpJob == null) {
            int segmentSize = getSegmentMaxRows();
            if (curRow != null && curRow.getVisualNumber() >= segmentSize && segmentSize > 0) {
                segmentSize = (curRow.getVisualNumber() / segmentSize + 1) * segmentSize;
            }
            return runDataPump(dataContainer, null, 0, segmentSize, -1, onSuccess);
        } else {
            return false;
        }
    }

    public synchronized void readNextSegment() {
        if (!dataReceiver.isHasMoreData()) {
            return;
        }
        DBSDataContainer dataContainer = getDataContainer();
        if (dataContainer != null && !model.isUpdateInProgress() && dataPumpJob == null) {
            dataReceiver.setHasMoreData(false);
            dataReceiver.setNextSegmentRead(true);

            runDataPump(dataContainer, null, model.getRowCount(), getSegmentMaxRows(), -1, //curRow == null ? -1 : curRow.getRowNumber(), // Do not reposition cursor after next segment read!
                    null);
        }
    }

    @Override
    public void readAllData() {
        if (!dataReceiver.isHasMoreData()) {
            return;
        }
        if (ConfirmationDialog.showConfirmDialogEx(viewerPanel.getShell(), DBeaverPreferences.CONFIRM_RS_FETCH_ALL,
                ConfirmationDialog.QUESTION, ConfirmationDialog.WARNING) != IDialogConstants.YES_ID) {
            return;
        }

        DBSDataContainer dataContainer = getDataContainer();
        if (dataContainer != null && !model.isUpdateInProgress() && dataPumpJob == null) {
            dataReceiver.setHasMoreData(false);
            dataReceiver.setNextSegmentRead(true);

            runDataPump(dataContainer, null, model.getRowCount(), -1, curRow == null ? -1 : curRow.getRowNumber(),
                    null);
        }
    }

    int getSegmentMaxRows() {
        if (getDataContainer() == null) {
            return 0;
        }
        return getPreferenceStore().getInt(DBeaverPreferences.RESULT_SET_MAX_ROWS);
    }

    synchronized boolean runDataPump(@NotNull final DBSDataContainer dataContainer,
            @Nullable final DBDDataFilter dataFilter, final int offset, final int maxRows, final int focusRow,
            @Nullable final Runnable finalizer) {
        if (dataPumpJob != null) {
            UIUtils.showMessageBox(viewerPanel.getShell(), "Data read",
                    "Data read is in progress - can't run another", SWT.ICON_WARNING);
            return false;
        }
        // Read data
        final DBDDataFilter useDataFilter = dataFilter != null ? dataFilter
                : (dataContainer == getDataContainer() ? model.getDataFilter() : null);
        Composite progressControl = viewerPanel;
        if (activePresentation.getControl() instanceof Composite) {
            progressControl = (Composite) activePresentation.getControl();
        }
        final Object presentationState = savePresentationState();
        dataPumpJob = new ResultSetDataPumpJob(dataContainer, useDataFilter, this, getExecutionContext(),
                progressControl);
        dataPumpJob.addJobChangeListener(new JobChangeAdapter() {
            @Override
            public void aboutToRun(IJobChangeEvent event) {
                model.setUpdateInProgress(true);
                getControl().getDisplay().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        filtersPanel.enableFilters(false);
                    }
                });
            }

            @Override
            public void done(IJobChangeEvent event) {
                ResultSetDataPumpJob job = (ResultSetDataPumpJob) event.getJob();
                final Throwable error = job.getError();
                if (job.getStatistics() != null) {
                    model.setStatistics(job.getStatistics());
                }
                final Control control = getControl();
                if (control.isDisposed()) {
                    return;
                }
                control.getDisplay().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            if (control.isDisposed()) {
                                return;
                            }
                            final Shell shell = control.getShell();
                            if (error != null) {
                                //setStatus(error.getMessage(), true);
                                UIUtils.showErrorDialog(shell, "Error executing query", "Query execution failed",
                                        error);
                            } else if (focusRow >= 0 && focusRow < model.getRowCount()
                                    && model.getVisibleAttributeCount() > 0) {
                                // Seems to be refresh
                                // Restore original position
                                curRow = model.getRow(focusRow);
                                //curAttribute = model.getVisibleAttribute(0);
                                if (recordMode) {
                                    updateRecordMode();
                                } else {
                                    updateStatusMessage();
                                }
                                restorePresentationState(presentationState);
                            }
                            activePresentation.updateValueView();

                            if (error == null) {
                                setNewState(dataContainer, dataFilter != null ? dataFilter
                                        : (dataContainer == getDataContainer() ? model.getDataFilter() : null));
                            }

                            model.setUpdateInProgress(false);
                            if (error == null && dataFilter != null) {
                                model.updateDataFilter(dataFilter);
                                activePresentation.refreshData(true, false);
                            }
                            updateFiltersText(error == null);
                            updateToolbar();
                            fireResultSetLoad();
                        } finally {
                            if (finalizer != null) {
                                try {
                                    finalizer.run();
                                } catch (Throwable e) {
                                    log.error(e);
                                }
                            }

                            dataPumpJob = null;
                        }
                    }
                });
            }
        });
        dataPumpJob.setOffset(offset);
        dataPumpJob.setMaxRows(maxRows);
        dataPumpJob.schedule();

        return true;
    }

    private void clearData() {
        this.model.clearData();
        this.curRow = null;
        this.activePresentation.clearMetaData();
    }

    @Override
    public boolean applyChanges(@Nullable DBRProgressMonitor monitor) {
        return applyChanges(monitor, null);
    }

    /**
     * Saves changes to database
     * @param monitor monitor. If null then save will be executed in async job
     * @param listener finish listener (may be null)
     */
    public boolean applyChanges(@Nullable DBRProgressMonitor monitor,
            @Nullable ResultSetPersister.DataUpdateListener listener) {
        try {
            ResultSetPersister persister = createDataPersister(false);
            return persister.applyChanges(monitor, false, listener);
        } catch (DBException e) {
            UIUtils.showErrorDialog(null, "Apply changes error", "Error saving changes in database", e);
            return false;
        }
    }

    @Override
    public void rejectChanges() {
        if (!isDirty()) {
            return;
        }
        try {
            createDataPersister(true).rejectChanges();
        } catch (DBException e) {
            log.debug(e);
        }
    }

    @Override
    public List<DBEPersistAction> generateChangesScript(@NotNull DBRProgressMonitor monitor) {
        try {
            ResultSetPersister persister = createDataPersister(false);
            persister.applyChanges(monitor, true, null);
            return persister.getScript();
        } catch (DBException e) {
            UIUtils.showErrorDialog(null, "SQL script generate error", "Error saving changes in database", e);
            return Collections.emptyList();
        }
    }

    @NotNull
    private ResultSetPersister createDataPersister(boolean skipKeySearch) throws DBException {
        if (!skipKeySearch && !model.isSingleSource()) {
            throw new DBException("Can't save data for result set from multiple sources");
        }
        boolean needPK = false;
        if (!skipKeySearch) {
            for (ResultSetRow row : model.getAllRows()) {
                if (row.getState() == ResultSetRow.STATE_REMOVED
                        || (row.getState() == ResultSetRow.STATE_NORMAL && row.isChanged())) {
                    needPK = true;
                    break;
                }
            }
        }
        if (needPK) {
            // If we have deleted or updated rows then check for unique identifier
            if (!checkEntityIdentifier()) {
                throw new DBException("No unique identifier defined");
            }
        }
        return new ResultSetPersister(this);
    }

    void addNewRow(final boolean copyCurrent, boolean afterCurrent) {
        int rowNum = curRow == null ? 0 : curRow.getVisualNumber();
        if (rowNum >= model.getRowCount()) {
            rowNum = model.getRowCount() - 1;
        }
        if (rowNum < 0) {
            rowNum = 0;
        }

        final DBCExecutionContext executionContext = getExecutionContext();
        if (executionContext == null) {
            return;
        }

        // Add new row
        final DBDAttributeBinding docAttribute = model.getDocumentAttribute();
        final DBDAttributeBinding[] attributes = model.getAttributes();
        final Object[] cells;
        final int currentRowNumber = rowNum;
        // Copy cell values in new context
        try (DBCSession session = executionContext.openSession(VoidProgressMonitor.INSTANCE,
                DBCExecutionPurpose.UTIL, CoreMessages.controls_resultset_viewer_add_new_row_context_name)) {
            if (docAttribute != null) {
                cells = new Object[1];
                if (copyCurrent && currentRowNumber >= 0 && currentRowNumber < model.getRowCount()) {
                    Object[] origRow = model.getRowData(currentRowNumber);
                    try {
                        cells[0] = docAttribute.getValueHandler().getValueFromObject(session, docAttribute,
                                origRow[0], true);
                    } catch (DBCException e) {
                        log.warn(e);
                    }
                }
                if (cells[0] == null) {
                    try {
                        cells[0] = DBUtils.makeNullValue(session, docAttribute.getValueHandler(),
                                docAttribute.getAttribute());
                    } catch (DBCException e) {
                        log.warn(e);
                    }
                }
            } else {
                cells = new Object[attributes.length];
                if (copyCurrent && currentRowNumber >= 0 && currentRowNumber < model.getRowCount()) {
                    Object[] origRow = model.getRowData(currentRowNumber);
                    for (int i = 0; i < attributes.length; i++) {
                        DBDAttributeBinding metaAttr = attributes[i];
                        DBSAttributeBase attribute = metaAttr.getAttribute();
                        if (attribute.isAutoGenerated() || attribute.isPseudoAttribute()) {
                            // set pseudo and autoincrement attributes to null
                            cells[i] = null;
                        } else {
                            try {
                                cells[i] = metaAttr.getValueHandler().getValueFromObject(session, attribute,
                                        origRow[i], true);
                            } catch (DBCException e) {
                                log.warn(e);
                                try {
                                    cells[i] = DBUtils.makeNullValue(session, metaAttr.getValueHandler(),
                                            attribute);
                                } catch (DBCException e1) {
                                    log.warn(e1);
                                }
                            }
                        }
                    }
                } else {
                    // Initialize new values
                    for (int i = 0; i < attributes.length; i++) {
                        DBDAttributeBinding metaAttr = attributes[i];
                        try {
                            cells[i] = DBUtils.makeNullValue(session, metaAttr.getValueHandler(),
                                    metaAttr.getAttribute());
                        } catch (DBCException e) {
                            log.warn(e);
                        }
                    }
                }
            }
        }
        curRow = model.addNewRow(afterCurrent ? rowNum + 1 : rowNum, cells);
        redrawData(true);
        updateEditControls();
        fireResultSetChange();
    }

    void deleteSelectedRows() {
        Set<ResultSetRow> rowsToDelete = new LinkedHashSet<>();
        if (recordMode) {
            rowsToDelete.add(curRow);
        } else {
            IResultSetSelection selection = getSelection();
            if (!selection.isEmpty()) {
                rowsToDelete.addAll(selection.getSelectedRows());
            }
        }
        if (rowsToDelete.isEmpty()) {
            return;
        }

        int rowsRemoved = 0;
        int lastRowNum = -1;
        for (ResultSetRow row : rowsToDelete) {
            if (model.deleteRow(row)) {
                rowsRemoved++;
            }
            lastRowNum = row.getVisualNumber();
        }
        redrawData(rowsRemoved > 0);
        // Move one row down (if we are in grid mode)
        if (!recordMode && lastRowNum < model.getRowCount() - 1) {
            activePresentation.scrollToRow(IResultSetPresentation.RowPosition.NEXT);
        }

        updateEditControls();
        fireResultSetChange();
    }

    //////////////////////////////////
    // Virtual identifier management

    @Nullable
    DBDRowIdentifier getVirtualEntityIdentifier() {
        if (!model.isSingleSource() || model.getVisibleAttributeCount() == 0) {
            return null;
        }
        DBDRowIdentifier rowIdentifier = model.getVisibleAttribute(0).getRowIdentifier();
        DBSEntityReferrer identifier = rowIdentifier == null ? null : rowIdentifier.getUniqueKey();
        if (identifier != null && identifier instanceof DBVEntityConstraint) {
            return rowIdentifier;
        } else {
            return null;
        }
    }

    boolean checkEntityIdentifier() throws DBException {
        DBSEntity entity = model.getSingleSource();
        if (entity == null) {
            UIUtils.showErrorDialog(null, "Unrecognized entity", "Can't detect source entity");
            return false;
        }
        final DBCExecutionContext executionContext = getExecutionContext();
        if (executionContext == null) {
            return false;
        }
        // Check for value locators
        // Probably we have only virtual one with empty attribute set
        final DBDRowIdentifier identifier = getVirtualEntityIdentifier();
        if (identifier != null) {
            if (CommonUtils.isEmpty(identifier.getAttributes())) {
                // Empty identifier. We have to define it
                RunnableWithResult<Boolean> confirmer = new RunnableWithResult<Boolean>() {
                    @Override
                    public void run() {
                        result = ValidateUniqueKeyUsageDialog.validateUniqueKey(ResultSetViewer.this,
                                executionContext);
                    }
                };
                UIUtils.runInUI(null, confirmer);
                return confirmer.getResult();
            }
        }
        {
            // Check attributes of non-virtual identifier
            DBDRowIdentifier rowIdentifier = model.getVisibleAttribute(0).getRowIdentifier();
            if (rowIdentifier == null) {
                // We shouldn't be here ever!
                // Virtual id should be created if we missing natural one
                UIUtils.showErrorDialog(null, "No entity identifier",
                        "Entity " + entity.getName() + " has no unique key");
                return false;
            } else if (CommonUtils.isEmpty(rowIdentifier.getAttributes())) {
                UIUtils.showErrorDialog(null, "No entity identifier", "Attributes of '"
                        + DBUtils.getObjectFullName(rowIdentifier.getUniqueKey()) + "' are missing in result set");
                return false;
            }
        }
        return true;
    }

    boolean editEntityIdentifier(DBRProgressMonitor monitor) throws DBException {
        DBDRowIdentifier virtualEntityIdentifier = getVirtualEntityIdentifier();
        if (virtualEntityIdentifier == null) {
            log.warn("No virtual identifier");
            return false;
        }
        DBVEntityConstraint constraint = (DBVEntityConstraint) virtualEntityIdentifier.getUniqueKey();

        EditConstraintDialog dialog = new EditConstraintDialog(getControl().getShell(),
                "Define virtual unique identifier", constraint);
        if (dialog.open() != IDialogConstants.OK_ID) {
            return false;
        }

        Collection<DBSEntityAttribute> uniqueAttrs = dialog.getSelectedAttributes();
        constraint.setAttributes(uniqueAttrs);
        virtualEntityIdentifier = getVirtualEntityIdentifier();
        if (virtualEntityIdentifier == null) {
            log.warn("No virtual identifier defined");
            return false;
        }
        virtualEntityIdentifier.reloadAttributes(monitor, model.getAttributes());
        persistConfig();

        return true;
    }

    void clearEntityIdentifier(DBRProgressMonitor monitor) throws DBException {
        DBDAttributeBinding firstAttribute = model.getVisibleAttribute(0);
        DBDRowIdentifier rowIdentifier = firstAttribute.getRowIdentifier();
        if (rowIdentifier != null) {
            DBVEntityConstraint virtualKey = (DBVEntityConstraint) rowIdentifier.getUniqueKey();
            virtualKey.setAttributes(Collections.<DBSEntityAttribute>emptyList());
            rowIdentifier.reloadAttributes(monitor, model.getAttributes());
            virtualKey.getParentObject().setProperty(DBVConstants.PROPERTY_USE_VIRTUAL_KEY_QUIET, null);
        }

        persistConfig();
    }

    public void fireResultSetChange() {
        synchronized (listeners) {
            if (!listeners.isEmpty()) {
                for (IResultSetListener listener : listeners) {
                    listener.handleResultSetChange();
                }
            }
        }
    }

    public void fireResultSetLoad() {
        synchronized (listeners) {
            if (!listeners.isEmpty()) {
                for (IResultSetListener listener : listeners) {
                    listener.handleResultSetLoad();
                }
            }
        }
    }

    private static class SimpleFilterManager implements IResultSetFilterManager {
        private final Map<String, List<String>> filterHistory = new HashMap<>();

        @NotNull
        @Override
        public List<String> getQueryFilterHistory(@NotNull String query) throws DBException {
            final List<String> filters = filterHistory.get(query);
            if (filters != null) {
                return filters;
            }
            return Collections.emptyList();
        }

        @Override
        public void saveQueryFilterValue(@NotNull String query, @NotNull String filterValue) throws DBException {
            List<String> filters = filterHistory.get(query);
            if (filters == null) {
                filters = new ArrayList<>();
                filterHistory.put(query, filters);
            }
            filters.add(filterValue);
        }

        @Override
        public void deleteQueryFilterValue(@NotNull String query, String filterValue) throws DBException {
            List<String> filters = filterHistory.get(query);
            if (filters != null) {
                filters.add(filterValue);
            }
        }
    }

    private class EmptySelection extends StructuredSelection implements IResultSetSelection {
        @NotNull
        @Override
        public IResultSetController getController() {
            return ResultSetViewer.this;
        }

        @NotNull
        @Override
        public Collection<DBDAttributeBinding> getSelectedAttributes() {
            return Collections.emptyList();
        }

        @NotNull
        @Override
        public Collection<ResultSetRow> getSelectedRows() {
            return Collections.emptyList();
        }
    }

    private class ConfigAction extends Action implements IMenuCreator {
        public ConfigAction() {
            super(CoreMessages.controls_resultset_viewer_action_options, IAction.AS_DROP_DOWN_MENU);
            setImageDescriptor(DBeaverIcons.getImageDescriptor(UIIcon.CONFIGURATION));
        }

        @Override
        public IMenuCreator getMenuCreator() {
            return this;
        }

        @Override
        public void runWithEvent(Event event) {
            Menu menu = getMenu(activePresentation.getControl());
            if (menu != null && event.widget instanceof ToolItem) {
                Rectangle bounds = ((ToolItem) event.widget).getBounds();
                Point point = ((ToolItem) event.widget).getParent().toDisplay(bounds.x, bounds.y + bounds.height);
                menu.setLocation(point.x, point.y);
                menu.setVisible(true);
            }
        }

        @Override
        public void dispose() {

        }

        @Override
        public Menu getMenu(Control parent) {
            MenuManager menuManager = new MenuManager();
            menuManager.add(new ShowFiltersAction(false));
            menuManager.add(new CustomizeColorsAction());
            menuManager.add(new Separator());
            menuManager.add(new VirtualKeyEditAction(true));
            menuManager.add(new VirtualKeyEditAction(false));
            menuManager.add(new DictionaryEditAction());
            menuManager.add(new Separator());
            menuManager.add(ActionUtils.makeCommandContribution(site, ResultSetCommandHandler.CMD_TOGGLE_MODE,
                    CommandContributionItem.STYLE_CHECK));
            activePresentation.fillMenu(menuManager);
            if (!CommonUtils.isEmpty(availablePresentations) && availablePresentations.size() > 1) {
                menuManager.add(new Separator());
                for (final ResultSetPresentationDescriptor pd : availablePresentations) {
                    Action action = new Action(pd.getLabel(), IAction.AS_RADIO_BUTTON) {
                        @Override
                        public boolean isEnabled() {
                            return !isRefreshInProgress();
                        }

                        @Override
                        public boolean isChecked() {
                            return pd == activePresentationDescriptor;
                        }

                        @Override
                        public void run() {
                            switchPresentation(pd);
                        }
                    };
                    if (pd.getIcon() != null) {
                        //action.setImageDescriptor(ImageDescriptor.createFromImage(pd.getIcon()));
                    }
                    menuManager.add(action);
                }
            }
            menuManager.add(new Separator());
            menuManager.add(new Action("Preferences") {
                @Override
                public void run() {
                    UIUtils.showPreferencesFor(getControl().getShell(), ResultSetViewer.this,
                            PrefPageDatabaseGeneral.PAGE_ID);
                }
            });
            return menuManager.createContextMenu(parent);
        }

        @Nullable
        @Override
        public Menu getMenu(Menu parent) {
            return null;
        }

    }

    private class ShowFiltersAction extends Action {
        public ShowFiltersAction(boolean context) {
            super(context ? "Customize ..." : "Order/Filter ...", DBeaverIcons.getImageDescriptor(UIIcon.FILTER));
        }

        @Override
        public void run() {
            new FilterSettingsDialog(ResultSetViewer.this).open();
        }
    }

    private class ToggleServerSideOrderingAction extends Action {
        public ToggleServerSideOrderingAction() {
            super(CoreMessages.pref_page_database_resultsets_label_server_side_order);
        }

        @Override
        public int getStyle() {
            return AS_CHECK_BOX;
        }

        @Override
        public boolean isChecked() {
            return getPreferenceStore().getBoolean(DBeaverPreferences.RESULT_SET_ORDER_SERVER_SIDE);
        }

        @Override
        public void run() {
            DBPPreferenceStore preferenceStore = getPreferenceStore();
            preferenceStore.setValue(DBeaverPreferences.RESULT_SET_ORDER_SERVER_SIDE,
                    !preferenceStore.getBoolean(DBeaverPreferences.RESULT_SET_ORDER_SERVER_SIDE));
        }
    }

    private enum FilterByAttributeType {
        VALUE(UIIcon.FILTER_VALUE) {
            @Override
            Object getValue(@NotNull ResultSetViewer viewer, @NotNull DBDAttributeBinding attribute,
                    @NotNull DBCLogicalOperator operator, boolean useDefault) {
                final ResultSetRow row = viewer.getCurrentRow();
                if (attribute == null || row == null) {
                    return null;
                }
                Object cellValue = viewer.model.getCellValue(attribute, row);
                if (operator == DBCLogicalOperator.LIKE && cellValue != null) {
                    cellValue = "%" + cellValue + "%";
                }
                return cellValue;
            }
        },
        INPUT(UIIcon.FILTER_INPUT) {
            @Override
            Object getValue(@NotNull ResultSetViewer viewer, @NotNull DBDAttributeBinding attribute,
                    @NotNull DBCLogicalOperator operator, boolean useDefault) {
                if (useDefault) {
                    return "..";
                } else {
                    ResultSetRow[] rows = null;
                    if (operator.getArgumentCount() < 0) {
                        Collection<ResultSetRow> selectedRows = viewer.getSelection().getSelectedRows();
                        rows = selectedRows.toArray(new ResultSetRow[selectedRows.size()]);
                    } else {
                        ResultSetRow focusRow = viewer.getCurrentRow();
                        if (focusRow != null) {
                            rows = new ResultSetRow[] { focusRow };
                        }
                    }
                    if (rows == null || rows.length == 0) {
                        return null;
                    }
                    FilterValueEditDialog dialog = new FilterValueEditDialog(viewer, attribute, rows, operator);
                    if (dialog.open() == IDialogConstants.OK_ID) {
                        return dialog.getValue();
                    } else {
                        return null;
                    }
                }
            }
        },
        /*
                CLIPBOARD(UIIcon.FILTER_CLIPBOARD) {
        @Override
        Object getValue(ResultSetViewer viewer, DBDAttributeBinding attribute, DBCLogicalOperator operator, boolean useDefault)
        {
            try {
                return ResultSetUtils.getAttributeValueFromClipboard(attribute);
            } catch (DBCException e) {
                log.debug("Error copying from clipboard", e);
                return null;
            }
        }
                },
        */
        NONE(UIIcon.FILTER_VALUE) {
            @Override
            Object getValue(@NotNull ResultSetViewer viewer, @NotNull DBDAttributeBinding attribute,
                    @NotNull DBCLogicalOperator operator, boolean useDefault) {
                return null;
            }
        };

        final ImageDescriptor icon;

        FilterByAttributeType(DBPImage icon) {
            this.icon = DBeaverIcons.getImageDescriptor(icon);
        }

        @Nullable
        abstract Object getValue(@NotNull ResultSetViewer viewer, @NotNull DBDAttributeBinding attribute,
                @NotNull DBCLogicalOperator operator, boolean useDefault);
    }

    private String translateFilterPattern(DBCLogicalOperator operator, FilterByAttributeType type,
            DBDAttributeBinding attribute) {
        Object value = type.getValue(this, attribute, operator, true);
        DBCExecutionContext executionContext = getExecutionContext();
        String strValue = executionContext == null ? String.valueOf(value)
                : attribute.getValueHandler().getValueDisplayString(attribute, value, DBDDisplayFormat.UI);
        if (operator.getArgumentCount() == 0) {
            return operator.getStringValue();
        } else {
            return operator.getStringValue() + " " + CommonUtils.truncateString(strValue, 64);
        }
    }

    private class FilterByAttributeAction extends Action {
        private final DBCLogicalOperator operator;
        private final FilterByAttributeType type;
        private final DBDAttributeBinding attribute;

        public FilterByAttributeAction(DBCLogicalOperator operator, FilterByAttributeType type,
                DBDAttributeBinding attribute) {
            super(attribute.getName() + " " + translateFilterPattern(operator, type, attribute), type.icon);
            this.operator = operator;
            this.type = type;
            this.attribute = attribute;
        }

        @Override
        public void run() {
            Object value = type.getValue(ResultSetViewer.this, attribute, operator, false);
            if (operator.getArgumentCount() != 0 && value == null) {
                return;
            }
            DBDDataFilter filter = new DBDDataFilter(model.getDataFilter());
            DBDAttributeConstraint constraint = filter.getConstraint(attribute);
            if (constraint != null) {
                constraint.setOperator(operator);
                constraint.setValue(value);
                setDataFilter(filter, true);
            }
        }
    }

    private class FilterResetAttributeAction extends Action {
        private final DBDAttributeBinding attribute;

        public FilterResetAttributeAction(DBDAttributeBinding attribute) {
            super("Remove filter for '" + attribute.getName() + "'",
                    DBeaverIcons.getImageDescriptor(UIIcon.REVERT));
            this.attribute = attribute;
        }

        @Override
        public void run() {
            DBDDataFilter dataFilter = new DBDDataFilter(model.getDataFilter());
            DBDAttributeConstraint constraint = dataFilter.getConstraint(attribute);
            if (constraint != null) {
                constraint.setCriteria(null);
                setDataFilter(dataFilter, true);
            }
        }
    }

    private abstract class ColorAction extends Action {
        protected ColorAction(String name) {
            super(name);
        }

        @NotNull
        protected DBVEntity getVirtualEntity(DBDAttributeBinding binding) {
            final DBSEntity entity = getModel().getSingleSource();
            if (entity == null) {
                throw new IllegalStateException("No virtual entity for multi-source query");
            }
            final DBVEntity vEntity = DBVUtils.findVirtualEntity(entity, true);
            assert vEntity != null;
            return vEntity;
        }

        protected void updateColors(DBVEntity entity) {
            model.updateColorMapping();
            redrawData(false);
            entity.getDataSource().getContainer().persistConfiguration();
        }
    }

    private class SetRowColorAction extends ColorAction {
        private final DBDAttributeBinding attribute;
        private final Object value;

        public SetRowColorAction(DBDAttributeBinding attr, Object value) {
            super("Color by " + attr.getName());
            this.attribute = attr;
            this.value = value;
        }

        @Override
        public void run() {
            RGB color;
            final Shell shell = UIUtils.createCenteredShell(getControl().getShell());
            try {
                ColorDialog cd = new ColorDialog(shell);
                color = cd.open();
                if (color == null) {
                    return;
                }
            } finally {
                shell.dispose();
            }
            final DBVEntity vEntity = getVirtualEntity(attribute);
            vEntity.setColorOverride(attribute, value, null, StringConverter.asString(color));
            updateColors(vEntity);
        }
    }

    private class ResetRowColorAction extends ColorAction {
        private final DBDAttributeBinding attribute;

        public ResetRowColorAction(DBDAttributeBinding attr, Object value) {
            super("Reset color by " + attr.getName());
            this.attribute = attr;
        }

        @Override
        public void run() {
            final DBVEntity vEntity = getVirtualEntity(attribute);
            vEntity.removeColorOverride(attribute);
            updateColors(vEntity);
        }
    }

    private class CustomizeColorsAction extends ColorAction {
        private final DBDAttributeBinding curAttribute;
        private final ResultSetRow row;

        public CustomizeColorsAction() {
            this(null, null);
        }

        public CustomizeColorsAction(DBDAttributeBinding curAttribute, ResultSetRow row) {
            super("Row colors ...");
            this.curAttribute = curAttribute;
            this.row = row;
        }

        @Override
        public void run() {
            ColorSettingsDialog dialog = new ColorSettingsDialog(ResultSetViewer.this, curAttribute, row);
            if (dialog.open() != IDialogConstants.OK_ID) {
                return;
            }
            final DBVEntity vEntity = getVirtualEntity(curAttribute);
            //vEntity.removeColorOverride(attribute);
            updateColors(vEntity);
        }

        @Override
        public boolean isEnabled() {
            return false;
        }
    }

    private class VirtualKeyEditAction extends Action {
        private boolean define;

        public VirtualKeyEditAction(boolean define) {
            super(define ? "Define virtual unique key" : "Clear virtual unique key");
            this.define = define;
        }

        @Override
        public boolean isEnabled() {
            DBDRowIdentifier identifier = getVirtualEntityIdentifier();
            return identifier != null && (define || !CommonUtils.isEmpty(identifier.getAttributes()));
        }

        @Override
        public void run() {
            DBeaverUI.runUIJob("Edit virtual key", new DBRRunnableWithProgress() {
                @Override
                public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
                    try {
                        if (define) {
                            editEntityIdentifier(monitor);
                        } else {
                            clearEntityIdentifier(monitor);
                        }
                    } catch (DBException e) {
                        throw new InvocationTargetException(e);
                    }
                }
            });
        }
    }

    private class DictionaryEditAction extends Action {
        public DictionaryEditAction() {
            super("Define dictionary");
        }

        @Override
        public void run() {
            EditDictionaryDialog dialog = new EditDictionaryDialog(getSite().getShell(), "Edit dictionary",
                    model.getSingleSource());
            dialog.open();
        }

        @Override
        public boolean isEnabled() {
            final DBSEntity singleSource = model.getSingleSource();
            return singleSource != null;
        }
    }

    private class PresentationSwitchCombo extends ContributionItem implements SelectionListener {
        private ToolItem toolitem;
        private CImageCombo combo;

        @Override
        public void fill(ToolBar parent, int index) {
            toolitem = new ToolItem(parent, SWT.SEPARATOR, index);
            Control control = createControl(parent);
            toolitem.setControl(control);
        }

        @Override
        public void fill(Composite parent) {
            createControl(parent);
        }

        protected Control createControl(Composite parent) {
            combo = new CImageCombo(parent, SWT.BORDER | SWT.DROP_DOWN | SWT.READ_ONLY);
            combo.add(DBeaverIcons.getImage(DBIcon.TYPE_UNKNOWN), "", null, null);
            final int textWidth = parent.getFont().getFontData()[0].getHeight() * 10;
            combo.setWidthHint(textWidth);
            if (toolitem != null) {
                toolitem.setWidth(textWidth);
            }
            combo.addSelectionListener(this);
            combo.setToolTipText(ActionUtils.findCommandDescription(ResultSetCommandHandler.CMD_SWITCH_PRESENTATION,
                    getSite(), false));
            return combo;
        }

        @Override
        public void widgetSelected(SelectionEvent e) {
            ResultSetPresentationDescriptor selectedPresentation = (ResultSetPresentationDescriptor) combo
                    .getData(combo.getSelectionIndex());
            if (activePresentationDescriptor == selectedPresentation) {
                return;
            }
            switchPresentation(selectedPresentation);
        }

        @Override
        public void widgetDefaultSelected(SelectionEvent e) {

        }
    }

    class StateItem {
        DBSDataContainer dataContainer;
        DBDDataFilter filter;
        int rowNumber;

        public StateItem(DBSDataContainer dataContainer, @Nullable DBDDataFilter filter, int rowNumber) {
            this.dataContainer = dataContainer;
            this.filter = filter;
            this.rowNumber = rowNumber;
        }

        public String describeState() {
            DBCExecutionContext context = getExecutionContext();
            String desc = dataContainer.getName();
            if (context != null && filter != null && filter.hasConditions()) {
                StringBuilder condBuffer = new StringBuilder();
                SQLUtils.appendConditionString(filter, context.getDataSource(), null, condBuffer, true);
                desc += " [" + condBuffer + "]";
            }
            return desc;
        }
    }

    public static void openNewDataEditor(DBNDatabaseNode targetNode, DBDDataFilter newFilter) {
        IEditorPart entityEditor = NavigatorHandlerObjectOpen.openEntityEditor(targetNode,
                DatabaseDataEditor.class.getName(),
                Collections.<String, Object>singletonMap(DatabaseDataEditor.ATTR_DATA_FILTER, newFilter),
                DBeaverUI.getActiveWorkbenchWindow());

        if (entityEditor instanceof MultiPageEditorPart) {
            Object selectedPage = ((MultiPageEditorPart) entityEditor).getSelectedPage();
            if (selectedPage instanceof IResultSetContainer) {
                ResultSetViewer rsv = ((IResultSetContainer) selectedPage).getResultSetViewer();
                if (rsv != null && !rsv.isRefreshInProgress()
                        && !newFilter.equals(rsv.getModel().getDataFilter())) {
                    // Set filter directly
                    rsv.refreshWithFilter(newFilter);
                }
            }
        }
    }

}