org.csstudio.alarm.beast.ui.alarmtable.GUI.java Source code

Java tutorial

Introduction

Here is the source code for org.csstudio.alarm.beast.ui.alarmtable.GUI.java

Source

/*******************************************************************************
 * Copyright (c) 2010 Oak Ridge National Laboratory.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 ******************************************************************************/
package org.csstudio.alarm.beast.ui.alarmtable;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;

import org.csstudio.alarm.beast.client.AlarmTreeItem;
import org.csstudio.alarm.beast.client.AlarmTreePV;
import org.csstudio.alarm.beast.client.AlarmTreeRoot;
import org.csstudio.alarm.beast.client.GUIUpdateThrottle;
import org.csstudio.alarm.beast.ui.AuthIDs;
import org.csstudio.alarm.beast.ui.ContextMenuHelper;
import org.csstudio.alarm.beast.ui.SelectionHelper;
import org.csstudio.alarm.beast.ui.SeverityColorProvider;
import org.csstudio.alarm.beast.ui.actions.AlarmPerspectiveAction;
import org.csstudio.alarm.beast.ui.actions.ConfigureItemAction;
import org.csstudio.alarm.beast.ui.actions.DisableComponentAction;
import org.csstudio.alarm.beast.ui.clientmodel.AlarmClientModel;
import org.csstudio.alarm.beast.ui.clientmodel.AlarmClientModelListener;
import org.csstudio.apputil.text.RegExHelper;
import org.csstudio.security.SecuritySupport;
import org.csstudio.ui.util.MinSizeTableColumnLayout;
import org.csstudio.ui.util.dnd.ControlSystemDragSource;
import org.csstudio.ui.util.helpers.ComboHistoryHelper;
import org.csstudio.utility.singlesource.SingleSourcePlugin;
import org.csstudio.utility.singlesource.UIHelper.UI;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.CheckboxCellEditor;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchPartSite;

/** Alarm table GUI
 *
 *  @author Kay Kasemir
 *  @author Jaka Bobnar - Combined/split alarm tables, configurable columns, filtering, blinking
 */
public class GUI extends Composite implements AlarmClientModelListener {
    /**
     * <code>Blinker</code> is a singleton responsible for synchronous blinking of icons
     * on all existing alarm tables.
     *
     * @author <a href="mailto:jaka.bobnar@cosylab.com">Jaka Bobnar</a>
     *
     */
    private static enum Blinker {
        INSTANCE;

        private final int period = Preferences.getBlinkingPeriod();
        private final Set<GUI> guis = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
        private final Display display = Display.getDefault();
        private boolean active = false;

        void blink() {
            if (!display.isDisposed()) {
                display.timerExec(period, () -> {
                    if (active) {
                        for (GUI g : guis) {
                            //if the GUI thread is this thread, blink on this thread,
                            //otherwise delegate to the GUI's thread and wait for it to finish
                            if (display == g.display)
                                g.blink();
                            else if (!g.display.isDisposed())
                                g.display.syncExec(() -> g.blink());
                        }
                        blink();
                    }
                });
            }
        }

        void reset() {
            if (!display.isDisposed()) {
                display.syncExec(() -> {
                    for (GUI g : guis)
                        g.icon_provider.reset();
                });
            }
        }

        void add(GUI gui) {
            if (!display.isDisposed()) {
                display.syncExec(() -> {
                    guis.add(gui);
                    //when a new gui is added, always reset everything so that all tables are synchronised
                    reset();
                    if (!active) {
                        active = true;
                        blink();
                    }
                });
            }
        }

        void remove(GUI gui) {
            if (!display.isDisposed()) {
                display.syncExec(() -> {
                    guis.remove(gui);
                    if (guis.isEmpty())
                        active = false;
                });
            }
        }
    }

    /**
     * Initial place holder for display of alarm counts to allocate enough screen space
     */
    final private static String ALARM_COUNT_PLACEHOLDER = "999999"; //$NON-NLS-1$

    final private Display display;

    /** Labels to show alarm counts */
    private Label current_alarms, acknowledged_alarms;

    /** TableViewer for active alarms */
    private TableViewer active_table_viewer;

    /** TableViewer for acknowledged alarms */
    private TableViewer acknowledged_table_viewer;

    /** PV selection filter text box */
    private Combo filter;

    /** PV un-select button */
    private Button unselect;

    private SeverityColorProvider color_provider;
    private SeverityColorPairProvider color_pair_provider;
    private SeverityIconProvider icon_provider;
    private AlarmTableLabelProvider table_label_provider;

    /** Is something displayed in <code>error_message</code>? */
    private volatile boolean have_error_message = false;

    /** Error message (no server...) */
    private Label error_message;

    private Composite base_composite;

    private final boolean separate_tables;

    private final ColumnWrapper[] columns;

    /** The item used for filtering the alarms */
    private AlarmTreeItem filter_item_parent;
    /** Model with all the alarm information */
    private AlarmClientModel model;
    /** Flag indicating if unacknowledged alarms should blink */
    private boolean blink_unacknowledged;
    /** Are the models writable or not */
    private final boolean writable;
    /** Should the labels with the number of alarms be displayed or not */
    private final boolean show_header;

    /** GUI updates are throttled to reduce flicker */
    final private GUIUpdateThrottle gui_update = new GUIUpdateThrottle() {
        @Override
        protected void fire() {
            if (display.isDisposed() || model == null)
                return;

            AlarmTreePV[] rawAlarms = model.getActiveAlarms();
            final AlarmTreePV[] filteredAlarms = filter(rawAlarms);
            final int rawAlarmsLength = rawAlarms.length;
            rawAlarms = model.getAcknowledgedAlarms();
            final AlarmTreePV[] filteredAcknowledged = filter(rawAlarms);
            final int rawAcknowledgedAlarmsLength = rawAlarms.length;
            final AlarmTreePV[] combinedAlarms;
            if (separate_tables)
                combinedAlarms = null;
            else {
                combinedAlarms = new AlarmTreePV[filteredAlarms.length + filteredAcknowledged.length];
                System.arraycopy(filteredAlarms, 0, combinedAlarms, 0, filteredAlarms.length);
                System.arraycopy(filteredAcknowledged, 0, combinedAlarms, filteredAlarms.length,
                        filteredAcknowledged.length);
            }
            display.syncExec(() -> {
                // Display counts, update tables

                // Don't use TableViewer.setInput(), it causes flicker on Linux!
                // active_table_viewer.setInput(model.getActiveAlarms());
                // acknowledged_table_viewer.setInput(model.getAcknowledgedAlarms());
                //
                // Instead, tell ModelInstanceProvider about the data,
                // which then updates the table with setItemCount(), refresh(),
                // as that happens to not flicker.
                updateGUI(rawAlarmsLength, filteredAlarms, rawAcknowledgedAlarmsLength, filteredAcknowledged,
                        combinedAlarms);
            });
        }
    };

    /** Column editor for the 'ACK' column that acknowledges or un-ack's
     *  alarm in that row
     */
    private static class AcknowledgeEditingSupport extends EditingSupport {
        public AcknowledgeEditingSupport(ColumnViewer viewer) {
            super(viewer);
        }

        @Override
        protected CellEditor getCellEditor(final Object element) {
            return new CheckboxCellEditor(((TableViewer) getViewer()).getTable());
        }

        @Override
        protected Object getValue(final Object element) {
            return ((AlarmTreePV) element).getSeverity().isActive();
        }

        @Override
        protected void setValue(final Object element, final Object value) {
            if (getViewer().isBusy())
                return;
            if (value instanceof Boolean) {
                if (SecuritySupport.havePermission(AuthIDs.ACKNOWLEDGE))
                    ((AlarmTreePV) element).acknowledge(!(Boolean) value);
            }
        }

        @Override
        protected boolean canEdit(Object element) {
            return element instanceof AlarmTreePV;
        }
    }

    /**
     * Initialize GUI
     *
     * @param parent Parent widget
     * @param site Workbench site or <code>null</code>
     * @param writable indicates if this GUI is used in a writable or read only environment
     * @param separate_tables true if two tables should be created (one for acked and one for unacked alarms) or false
     *          if only one table should be created
     * @param columns column configuration for the tables
     * @param sorting_column default sorting column
     * @param sort_up default sorting direction
     * @param show_header true if the header showing the number of all alarms and select field should be displayed or
     *          false otherwise
     */
    public GUI(final Composite parent, final IWorkbenchPartSite site, final boolean writable,
            final boolean separate_tables, final ColumnWrapper[] columns, final ColumnInfo sorting_column,
            boolean sort_up, boolean show_header) {
        super(parent, SWT.NONE);
        this.display = parent.getDisplay();
        this.separate_tables = separate_tables;
        this.columns = columns;
        this.writable = writable;
        this.show_header = show_header;
        createComponents(sorting_column, sort_up);

        base_composite.addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                if (model != null)
                    model.removeListener(GUI.this);
                gui_update.dispose();
            }
        });
        connectContextMenu(active_table_viewer, site);
        if (separate_tables)
            connectContextMenu(acknowledged_table_viewer, site);

        // Allow 'drag' of alarm info as text
        new ControlSystemDragSource(active_table_viewer.getTable()) {
            @Override
            public Object getSelection() {
                return SelectionHelper
                        .getAlarmTreePVsForDragging((IStructuredSelection) active_table_viewer.getSelection());
            }
        };

        if (separate_tables) {
            new ControlSystemDragSource(acknowledged_table_viewer.getTable()) {
                @Override
                public Object getSelection() {
                    return SelectionHelper.getAlarmTreePVsForDragging(
                            (IStructuredSelection) acknowledged_table_viewer.getSelection());
                }
            };
        }
        gui_update.trigger();
    }

    @Override
    public void dispose() {
        Blinker.INSTANCE.remove(this);
        super.dispose();
    }

    private void setUpModel() {
        if (model == null)
            return;
        if (model.isServerAlive())
            setErrorMessage(null);
        else
            setErrorMessage(org.csstudio.alarm.beast.ui.Messages.WaitingForServer);
        // Subscribe to model updates, arrange to un-subscribe
        model.addListener(this);
    }

    /** @return Table of active alarms */
    public TableViewer getActiveAlarmTable() {
        return active_table_viewer;
    }

    /** @return Table of acknowledged alarms */
    public TableViewer getAcknowledgedAlarmTable() {
        return separate_tables ? acknowledged_table_viewer : active_table_viewer;
    }

    /**
     * Create GUI elements
     *
     * @param parent Parent widget
     * @param memento the memento that provides the sorting information
     */
    private void createComponents(ColumnInfo sorting_column, boolean sort_up) {
        setLayout(new FillLayout());

        if (separate_tables) {
            base_composite = new SashForm(this, SWT.VERTICAL | SWT.SMOOTH);
            base_composite.setLayout(new FillLayout());
            color_provider = new SeverityColorProvider(base_composite);
            color_pair_provider = new SeverityColorPairProvider(base_composite);
            icon_provider = new SeverityIconProvider(base_composite);
            table_label_provider = new AlarmTableLabelProvider(icon_provider, color_provider, color_pair_provider,
                    ColumnInfo.TIME);
            addActiveAlarmSashElement(base_composite);
            addAcknowledgedAlarmSashElement(base_composite);
            ((SashForm) base_composite).setWeights(new int[] { 80, 20 });
        } else {
            base_composite = new Composite(this, SWT.NONE);
            base_composite.setLayout(new FillLayout());
            color_provider = new SeverityColorProvider(base_composite);
            color_pair_provider = new SeverityColorPairProvider(base_composite);
            icon_provider = new SeverityIconProvider(base_composite);
            table_label_provider = new AlarmTableLabelProvider(icon_provider, color_provider, color_pair_provider,
                    ColumnInfo.TIME);
            addActiveAlarmSashElement(base_composite);
        }
        syncTables(active_table_viewer, acknowledged_table_viewer, sorting_column, sort_up);
        syncTables(acknowledged_table_viewer, active_table_viewer, sorting_column, sort_up);

        if (show_header) {
            // Update selection in active & ack'ed alarm table
            // in response to filter changes
            final ComboHistoryHelper filter_history = new ComboHistoryHelper(
                    Activator.getDefault().getDialogSettings(), "FILTER", filter) //$NON-NLS-1$
            {
                @Override
                public void itemSelected(final String selection) {
                    final String filter_text = filter.getText().trim();
                    // Turn glob-type filter into regex, then pattern
                    final Pattern pattern = Pattern.compile(RegExHelper.fullRegexFromGlob(filter_text),
                            Pattern.CASE_INSENSITIVE);
                    selectFilteredPVs(pattern, active_table_viewer);
                    if (separate_tables) {
                        selectFilteredPVs(pattern, acknowledged_table_viewer);
                    }
                }
            };
            filter_history.loadSettings();

            // Clear filter, un-select all items
            unselect.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    filter.setText(""); //$NON-NLS-1$
                    active_table_viewer.setSelection(null, true);
                    if (separate_tables) {
                        acknowledged_table_viewer.setSelection(null, true);
                    }
                }
            });
        }
        gui_update.start();
        blink();
    }

    /**
     * Synchronise the UI actions between the two tables. When a sorting column is selected in the primary table the
     * same column should be selected in the secondary table. Similarly, the moving and resizing of columns is also
     * synchronised.
     *
     * @param primary the table, which is the source of the actions
     * @param secondary the table that is synchronised with the primary table
     * @param sortColumn the column that is being sorted by default
     * @param sortUp default sorting direction
     */
    private void syncTables(final TableViewer primary, final TableViewer secondary, ColumnInfo sortColumn,
            boolean sortUp) {
        if (primary == null)
            return;
        TableColumn[] priColumns = primary.getTable().getColumns();
        TableColumn[] secColumns = secondary == null ? priColumns : secondary.getTable().getColumns();
        boolean applied = false;
        AlarmColumnSortingSelector defaultListener = null;
        for (int i = 0; i < priColumns.length; i++) {
            final TableColumn c = priColumns[i];
            final TableColumn s = secColumns[i];
            final ColumnWrapper w = getColumnWrapper(i);
            AlarmColumnSortingSelector listener = new AlarmColumnSortingSelector(primary, secondary, c, s,
                    w.getColumnInfo());
            c.addSelectionListener(listener);
            if (secondary != null) {
                c.addControlListener(new ControlAdapter() {
                    @Override
                    public void controlResized(ControlEvent e) {
                        if (Math.abs(s.getWidth() - c.getWidth()) > 1) {
                            s.setWidth(c.getWidth());
                            w.setMinWidth(c.getWidth());
                        }
                    }

                    @Override
                    public void controlMoved(ControlEvent e) {
                        secondary.getTable().setColumnOrder(primary.getTable().getColumnOrder());
                    }
                });
            }
            if (w.getColumnInfo() == sortColumn) {
                listener.setSortDirection(sortUp);
                applied = true;
            } else if (defaultListener == null && w.getColumnInfo() != ColumnInfo.ACK) {
                defaultListener = listener;
            }
        }
        if (!applied && defaultListener != null) {
            defaultListener.setSortDirection(sortUp);
        }
    }

    /**
     * Return the column wrapper that represents the table column at the given index.
     *
     * @param tableColumnIndex the table column index
     * @return the column wrapper that matches the table column (the n-th visible wrapper)
     */
    private ColumnWrapper getColumnWrapper(int tableColumnIndex) {
        int idx = -1;
        for (ColumnWrapper cw : columns) {
            if (cw.isVisible())
                idx++;
            if (tableColumnIndex == idx)
                return cw;
        }
        return null;
    }

    /**
     * Updates the given columns with the order that is currently applied to the tables. Most of the time the orders are
     * the same, unless user moved the columns around by dragging their headers to a different position.
     *
     * @param columns the columns to update with the order
     */
    public void updateColumnOrder(ColumnWrapper[] columns) {
        if (active_table_viewer.getTable().isDisposed())
            return;
        int[] order = active_table_viewer.getTable().getColumnOrder();
        ColumnWrapper[] ret = new ColumnWrapper[columns.length];
        for (int i = 0; i < order.length; i++)
            ret[i] = columns[order[i]];
        int j = 0;
        for (int i = 0; i < columns.length; i++) {
            if (!columns[i].isVisible()) {
                for (; j < ret.length; j++) {
                    if (ret[j] == null) {
                        ret[j] = columns[i];
                        break;
                    }
                }
            }
        }
        for (int i = 0; i < columns.length; i++)
            columns[i] = ret[i];
    }

    private void blink() {
        if (blink_unacknowledged) {
            icon_provider.toggle();
            if (!active_table_viewer.getTable().isDisposed()) {
                // because of lazy label provider refresh is faster than updating individual cells
                if (!active_table_viewer.isBusy())
                    active_table_viewer.refresh();
                if (acknowledged_table_viewer != null && !acknowledged_table_viewer.isBusy())
                    acknowledged_table_viewer.refresh();
            }
        }
    }

    /**
     * @return the column info of the column that is currently used as the sorting key in the table
     */
    public ColumnInfo getSortingColumn() {
        final Table table = active_table_viewer.getTable();
        final TableColumn sort_column = table.getSortColumn();
        if (sort_column == null)
            return null;

        final int col_count = table.getColumnCount();
        for (int column = 0; column < col_count; ++column) {
            if (table.getColumn(column) == sort_column) {
                int count = 0;
                for (int i = 0; i < columns.length; i++) {
                    if (columns[i].isVisible()) {
                        if (count == column) {
                            return columns[i].getColumnInfo();
                        }
                        count++;
                    }
                }
            }
        }
        return null;
    }

    /**
     * @return true if sorting direction is equals to {@link SWT#UP} or false otherwise
     */
    public boolean isSortingUp() {
        final Table table = active_table_viewer.getTable();
        return table.getSortDirection() == SWT.UP;
    }

    /**
     * Add the sash element for active alarms
     *
     * @param parent the parent composite
     */
    private void addActiveAlarmSashElement(final Composite parent) {
        final Composite box = new Composite(parent, SWT.BORDER);
        box.setLayout(new GridLayout(5, false));

        if (show_header) {
            current_alarms = new Label(box, SWT.NONE);
            current_alarms.setText(NLS.bind(Messages.CurrentAlarmsFmt,
                    new Object[] { ALARM_COUNT_PLACEHOLDER, ALARM_COUNT_PLACEHOLDER, ALARM_COUNT_PLACEHOLDER }));
            current_alarms.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

            error_message = new Label(box, SWT.NONE);
            error_message.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false));

            Label l = new Label(box, SWT.NONE);
            l.setText(Messages.Filter);
            l.setLayoutData(new GridData());

            filter = new Combo(box, SWT.BORDER);
            filter.setToolTipText(Messages.FilterTT);
            filter.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

            unselect = new Button(box, SWT.PUSH);
            unselect.setText(Messages.Unselect);
            unselect.setToolTipText(Messages.UnselectTT);
            unselect.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false));

            if (!separate_tables) {
                acknowledged_alarms = new Label(box, SWT.NONE);
                acknowledged_alarms.setText(NLS.bind(Messages.AcknowledgedAlarmsFmt, new Object[] {
                        ALARM_COUNT_PLACEHOLDER, ALARM_COUNT_PLACEHOLDER, ALARM_COUNT_PLACEHOLDER }));
                acknowledged_alarms.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 5, 1));
            }
        }

        // Table w/ active alarms
        active_table_viewer = createAlarmTable(box, true);
        active_table_viewer.setInput(null);
    }

    /**
     * Add the sash element for acknowledged alarms
     *
     * @param parent the parent composite
     */
    private void addAcknowledgedAlarmSashElement(final Composite parent) {
        final Composite box = new Composite(parent, SWT.BORDER);
        box.setLayout(new GridLayout(1, false));

        if (show_header) {
            acknowledged_alarms = new Label(box, SWT.NONE);
            acknowledged_alarms.setText(NLS.bind(Messages.AcknowledgedAlarmsFmt, ALARM_COUNT_PLACEHOLDER));
            acknowledged_alarms.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
        }

        // Table w/ ack'ed alarms
        acknowledged_table_viewer = createAlarmTable(box, false);
        acknowledged_table_viewer.setInput(null);
    }

    /**
     * Select PVs in table that match filter expression
     *
     * @param pattern
     *            PV name pattern ('vac', 'amp*trip')
     * @param table_viewer
     *            Table in which to select PVs
     */
    private void selectFilteredPVs(final Pattern pattern, final TableViewer table_viewer) {
        final AlarmTreePV pvs[] = ((AlarmTableContentProvider) table_viewer.getContentProvider()).getAlarms();
        final ArrayList<AlarmTreePV> selected = new ArrayList<AlarmTreePV>();
        for (AlarmTreePV pv : pvs) {
            if (pattern.matcher(pv.getName()).matches() || pattern.matcher(pv.getDescription()).matches())
                selected.add(pv);
        }
        table_viewer.setSelection(new StructuredSelection(selected), true);
    }

    /**
     * Create a table viewer for displaying alarms
     *
     * @param parent Parent widget, uses GridLayout
     * @param is_active_alarm_table true if the table is for the active alarms or false if for acknowledged alarms
     *
     * @return TableViewer, still needs input
     */
    private TableViewer createAlarmTable(final Composite parent, final boolean is_active_alarm_table) {
        // TableColumnLayout requires the TableViewer to be in its own Composite
        final GridLayout parent_layout = (GridLayout) parent.getLayout();
        final Composite table_parent = new Composite(parent, 0);
        table_parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, parent_layout.numColumns, 1));

        // Auto-size table columns
        final TableColumnLayout table_layout = new MinSizeTableColumnLayout(10);
        table_parent.setLayout(table_layout);

        final TableViewer table_viewer = new TableViewer(table_parent,
                SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);

        // Some tweaks to the underlying table widget
        final Table table = table_viewer.getTable();
        table.setHeaderVisible(true);
        table.setLinesVisible(true);

        ColumnViewerToolTipSupport.enableFor(table_viewer);

        // Connect TableViewer to the Model: Provide content from model...
        table_viewer.setContentProvider(new AlarmTableContentProvider());

        // Create the columns of the table, using a fixed initial width.
        for (ColumnWrapper cw : columns) {
            if (!cw.isVisible())
                continue;

            // Create auto-size column
            final TableViewerColumn view_col = new TableViewerColumn(table_viewer, 0);
            final TableColumn table_col = view_col.getColumn();
            table_layout.setColumnData(table_col, new ColumnWeightData(cw.getWeight(), cw.getMinWidth()));
            table_col.setText(cw.getName());
            table_col.setMoveable(true);

            ColumnInfo col_info = cw.getColumnInfo();
            // Tell column how to display the model elements

            if (col_info == ColumnInfo.TIME)
                view_col.setLabelProvider(table_label_provider);
            else
                view_col.setLabelProvider(
                        new AlarmTableLabelProvider(icon_provider, color_provider, color_pair_provider, col_info));

            if (col_info == ColumnInfo.ACK) {
                if (writable)
                    view_col.setEditingSupport(new AcknowledgeEditingSupport(table_viewer));
            }
            table_col.setToolTipText(col_info.getTooltip());
        }

        return table_viewer;
    }

    /**
     * Add context menu to tree
     *
     * @param table_viewer
     *            TableViewer to which to add the menu
     * @param site
     *            Workbench site or <code>null</code>
     */
    private void connectContextMenu(final TableViewer table_viewer, final IWorkbenchPartSite site) {
        final Table table = table_viewer.getTable();
        final boolean isRcp = UI.RCP.equals(SingleSourcePlugin.getUIHelper().getUI());

        final MenuManager manager = new MenuManager();
        manager.setRemoveAllWhenShown(true);
        manager.addMenuListener(new IMenuListener() {
            // Dynamically build menu based on current selection
            @Override
            @SuppressWarnings("unchecked")
            public void menuAboutToShow(IMenuManager manager) {
                final Shell shell = table.getShell();
                final List<AlarmTreeItem> items = ((IStructuredSelection) table_viewer.getSelection()).toList();

                new ContextMenuHelper(table_viewer, manager, shell, items, writable);
                manager.add(new Separator());
                // Add edit items
                if (items.size() == 1 && writable && model != null) {
                    final AlarmTreeItem item = items.get(0);
                    manager.add(new ConfigureItemAction(shell, model, item));
                }
                if (items.size() >= 1 && writable && model != null)
                    manager.add(new DisableComponentAction(shell, model, items));
                manager.add(new Separator());
                if (isRcp) {
                    manager.add(new AlarmPerspectiveAction());
                    manager.add(new Separator());
                }
                // Placeholder for CSS PV contributions
                manager.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS));
            }
        });
        table.setMenu(manager.createContextMenu(table));

        // Allow extensions to add to the context menu
        if (site != null)
            site.registerContextMenu(manager, table_viewer);
    }

    /**
     * Set or clear error message. Setting an error message also disables the GUI.
     * <p>
     * OK to call multiple times or after disposal.
     *
     * @param error
     *            Error message or <code>null</code> to clear error
     */
    private void setErrorMessage(final String error) {
        final Table act_table = active_table_viewer.getTable();
        if (act_table.isDisposed())
            return;
        if (error == null) {
            if (!have_error_message)
                return; // msg already cleared, GUI already enabled
            if (show_header) {
                error_message.setText(""); //$NON-NLS-1$
                error_message.setBackground(null);
                error_message.setVisible(false);
                error_message.getParent().layout();
            }
            act_table.setEnabled(true);
            if (separate_tables)
                acknowledged_table_viewer.getTable().setEnabled(true);
            have_error_message = false;
        } else {
            // Update the message
            if (show_header) {
                error_message.setText(error);
                error_message.setBackground(display.getSystemColor(SWT.COLOR_MAGENTA));
                error_message.setVisible(true);
                error_message.getParent().layout();
            }
            if (have_error_message)
                return; // GUI already disabled
            act_table.setEnabled(false);
            if (separate_tables)
                acknowledged_table_viewer.getTable().setEnabled(false);
            have_error_message = true;
        }
    }

    // @see AlarmClientModelListener
    @Override
    public void serverModeUpdate(AlarmClientModel model, boolean maintenanceMode) {
        // Ignored
    }

    // @see AlarmClientModelListener
    @Override
    public void serverTimeout(final AlarmClientModel model) {
        display.asyncExec(() -> setErrorMessage(org.csstudio.alarm.beast.ui.Messages.ServerTimeout));
    }

    // For now, the table responds to any changes with a full update
    // @see AlarmClientModelListener
    @Override
    public void newAlarmConfiguration(final AlarmClientModel model) {
        gui_update.trigger();
        display.asyncExec(() -> {
            if (model.isServerAlive())
                setErrorMessage(null);
            else
                setErrorMessage(org.csstudio.alarm.beast.ui.Messages.WaitingForServer);
        });
    }

    // @see AlarmClientModelListener
    @Override
    public void newAlarmState(final AlarmClientModel model, final AlarmTreePV pv, final boolean parent_changed) {
        gui_update.trigger();

        if (model.isServerAlive() && have_error_message) {
            // Clear error message now that we have info from the alarm server
            display.asyncExec(() -> setErrorMessage(null));
        }
    }

    private AlarmTreePV[] filter(AlarmTreePV[] alarms) {
        if (filter_item_parent == null || filter_item_parent instanceof AlarmTreeRoot || alarms.length == 0)
            return alarms;
        else {
            List<AlarmTreePV> items = new ArrayList<>(alarms.length);
            AlarmTreeItem item;
            for (AlarmTreePV pv : alarms) {
                item = pv;
                do {
                    if (item == filter_item_parent) {
                        items.add(pv);
                        break;
                    }
                    item = item.getParent();
                } while (item != null && !(item instanceof AlarmTreeRoot));

            }
            return items.toArray(new AlarmTreePV[items.size()]);
        }
    }

    private void updateGUI(int numberOfRawAlarms, AlarmTreePV[] filteredAlarms, int numberOfRawAcknowledgedAlarms,
            AlarmTreePV[] filteredAcknowledgedAlarms, AlarmTreePV[] combinedAlarms) {
        if (model == null || active_table_viewer.getTable().isDisposed())
            return;
        //if GUI is currently busy, do not update anything, just trigger another update
        if (active_table_viewer.isBusy()) {
            gui_update.trigger();
            return;
        }

        if (show_header) {
            if (current_alarms.isDisposed())
                return;

            String text = null;
            if (filter_item_parent == null || filter_item_parent instanceof AlarmTreeRoot)
                text = NLS.bind(Messages.CurrentAlarmsFmtAll, filteredAlarms.length);
            else
                text = NLS.bind(Messages.CurrentAlarmsFmt, new Object[] { filteredAlarms.length, numberOfRawAlarms,
                        filter_item_parent.getPathName() });
            current_alarms.setText(text);
            current_alarms.setToolTipText(text);

            if (filteredAlarms.length != numberOfRawAlarms)
                current_alarms.setForeground(current_alarms.getDisplay().getSystemColor(SWT.COLOR_RED));
            else
                current_alarms.setForeground(null);
            current_alarms.pack();

            if (filter_item_parent == null || filter_item_parent instanceof AlarmTreeRoot)
                text = NLS.bind(Messages.AcknowledgedAlarmsFmtAll, filteredAcknowledgedAlarms.length);
            else
                text = NLS.bind(Messages.AcknowledgedAlarmsFmt, new Object[] { filteredAcknowledgedAlarms.length,
                        numberOfRawAcknowledgedAlarms, filter_item_parent.getPathName() });
            acknowledged_alarms.setText(text);
            acknowledged_alarms.setToolTipText(text);

            if (filteredAcknowledgedAlarms.length != numberOfRawAcknowledgedAlarms)
                acknowledged_alarms.setForeground(acknowledged_alarms.getDisplay().getSystemColor(SWT.COLOR_RED));
            else
                acknowledged_alarms.setForeground(null);
            acknowledged_alarms.pack();
        }
        if (separate_tables) {
            ((AlarmTableContentProvider) active_table_viewer.getContentProvider()).setAlarms(filteredAlarms);
            ((AlarmTableContentProvider) acknowledged_table_viewer.getContentProvider())
                    .setAlarms(filteredAcknowledgedAlarms);
        } else {
            ((AlarmTableContentProvider) active_table_viewer.getContentProvider()).setAlarms(combinedAlarms);
        }
    }

    /**
     * Sets the model and the filter item to use. Only the alarms that are blow the filter
     * item parent will be displayed, providing that the filter item parent is from the
     * given model.
     *
     * @param filterItemParent the item used for filtering alarms
     * @param model the model to show events for
     */
    public void setFilterItem(AlarmTreeItem filterItemParent, AlarmClientModel model) {
        this.filter_item_parent = filterItemParent;
        if (this.model != null)
            this.model.removeListener(this);
        this.model = model;
        setUpModel();
        gui_update.trigger();
    }

    /**
     * Enables or disables the blinking of unacknowledged alarms icons.
     *
     * @param blinking true if icons should blink or stop otherwise
     */
    public void setBlinking(boolean blinking) {
        if (this.blink_unacknowledged == blinking)
            return;
        this.blink_unacknowledged = blinking;
        if (this.blink_unacknowledged)
            Blinker.INSTANCE.add(this);
        else {
            Blinker.INSTANCE.remove(this);
            icon_provider.reset();
            active_table_viewer.refresh();
            if (acknowledged_table_viewer != null)
                acknowledged_table_viewer.refresh();
        }
    }

    /**
     * Sets the format used for formatting the date and time column. The format has
     * to be acceptable by the {@link DateTimeFormatter}.
     *
     * @param timeFormat the format string
     */
    public void setTimeFormat(String timeFormat) {
        table_label_provider.setTimeFormat(timeFormat);
    }

    /**
     * Set the maximum number of alarms displayed in the table.
     *
     * @param limit the maximum number of alarms
     */
    public void setNumberOfAlarmsLimit(int limit) {
        ((AlarmTableContentProvider) active_table_viewer.getContentProvider()).setNumberOfAlarmsLimit(limit);
        if (acknowledged_table_viewer != null)
            ((AlarmTableContentProvider) acknowledged_table_viewer.getContentProvider())
                    .setNumberOfAlarmsLimit(limit);
    }

    /**
     * Set the weights of the sash form used for displaying acknowledged and unacknowledged tables. If the
     * GUI only shows one table, this method as no effect.
     *
     * @param ack the weight for the acknowledged table
     * @param unack the weight for the unacknowledged table
     */
    public void setSashWeights(int ack, int unack) {
        if (base_composite instanceof SashForm)
            ((SashForm) base_composite).setWeights(new int[] { ack, unack });
    }

    /**
     * Toggles the visibility of both table column headers.
     *
     * @param visible true if headers should be visible or false if hidden
     */
    public void setTableColumnsHeadersVisible(boolean visible) {
        Table table = active_table_viewer.getTable();
        if (table.isDisposed())
            return;
        table.setHeaderVisible(visible);
        if (separate_tables)
            acknowledged_table_viewer.getTable().setHeaderVisible(visible);
    }
}