org.eclipse.sapphire.ui.swt.renderer.HyperlinkTable.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.sapphire.ui.swt.renderer.HyperlinkTable.java

Source

/******************************************************************************
 * Copyright (c) 2013 Oracle
 * 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
 *
 * Contributors:
 *    Konstantin Komissarchik - initial implementation and ongoing maintenance
 ******************************************************************************/

package org.eclipse.sapphire.ui.swt.renderer;

import static org.eclipse.sapphire.ui.swt.renderer.GridLayoutUtil.gd;
import static org.eclipse.sapphire.ui.swt.renderer.GridLayoutUtil.gdhfill;
import static org.eclipse.sapphire.ui.swt.renderer.GridLayoutUtil.gdhindent;
import static org.eclipse.sapphire.ui.swt.renderer.GridLayoutUtil.gdwhint;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.sapphire.modeling.util.NLS;
import org.eclipse.sapphire.ui.SapphireAction;
import org.eclipse.sapphire.ui.SapphireActionGroup;
import org.eclipse.sapphire.ui.SapphireActionHandler;
import org.eclipse.sapphire.ui.SapphireActionSystem;
import org.eclipse.sapphire.ui.SapphireRenderingContext;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
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.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;

/**
 * Implements Ctrl+Click navigation in a standard SWT table widget. 
 * 
 * @author <a href="konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
 */

public final class HyperlinkTable {
    public static abstract class Controller {
        public boolean isHyperlinkEnabled(final TableItem item, final int column) {
            return true;
        }

        public abstract void handleHyperlinkEvent(final TableItem item, final int column);
    }

    private static final Point IMAGE_OFFSET_PRIMARY_COLUMN;
    private static final Point IMAGE_OFFSET_SECONDARY_COLUMN;
    private static final Point TEXT_OFFSET_PRIMARY_COLUMN;
    private static final Point TEXT_OFFSET_SECONDARY_COLUMN;

    static {
        final String os = Platform.getOS();
        final String osName = System.getProperties().getProperty("os.name");

        if (os.equals(Platform.OS_WIN32)) {
            if (osName.equals("Windows XP")) {
                IMAGE_OFFSET_PRIMARY_COLUMN = new Point(0, 0);
                IMAGE_OFFSET_SECONDARY_COLUMN = new Point(0, 0);
                TEXT_OFFSET_PRIMARY_COLUMN = new Point(0, 2);
                TEXT_OFFSET_SECONDARY_COLUMN = new Point(-1, 2);
            } else {
                IMAGE_OFFSET_PRIMARY_COLUMN = new Point(0, 1);
                IMAGE_OFFSET_SECONDARY_COLUMN = new Point(0, 1);
                TEXT_OFFSET_PRIMARY_COLUMN = new Point(0, 2);
                TEXT_OFFSET_SECONDARY_COLUMN = new Point(0, 2);
            }
        } else {
            // This number has been derived on openSUSE 11.0, but we will use it
            // for all non-windows systems for now.

            IMAGE_OFFSET_PRIMARY_COLUMN = new Point(1, 3);
            IMAGE_OFFSET_SECONDARY_COLUMN = new Point(1, 3);
            TEXT_OFFSET_PRIMARY_COLUMN = new Point(1, 3);
            TEXT_OFFSET_SECONDARY_COLUMN = new Point(1, 3);
        }
    }

    private boolean controlKeyActive;
    private final Table table;
    private TableItem mouseOverTableItem;
    private int mouseOverColumn;
    private Controller controller;

    public HyperlinkTable(final Table table, final SapphireActionGroup actions) {
        this.table = table;
        this.controlKeyActive = false;
        this.mouseOverTableItem = null;
        this.mouseOverColumn = -1;

        final Listener keyListener = new Listener() {
            public void handleEvent(final Event event) {
                handleKeyEvent(event);
            }
        };

        final Display display = this.table.getDisplay();

        display.addFilter(SWT.KeyDown, keyListener);
        display.addFilter(SWT.KeyUp, keyListener);

        this.table.addListener(SWT.EraseItem, new Listener() {
            public void handleEvent(final Event event) {
                handleEraseItem(event);
            }
        });

        this.table.addListener(SWT.PaintItem, new Listener() {
            public void handleEvent(final Event event) {
                handlePaintItem(event);
            }
        });

        this.table.addListener(SWT.MouseMove, new Listener() {
            public void handleEvent(final Event event) {
                handleMouseMove(event);
            }
        });

        this.table.addListener(SWT.MouseExit, new Listener() {
            public void handleEvent(final Event event) {
                handleMouseExit(event);
            }
        });

        this.table.addListener(SWT.MouseDown, new Listener() {
            public void handleEvent(final Event event) {
                handleMouseDown(event);
            }
        });

        final SapphireActionHandler jumpActionHandler = new SapphireActionHandler() {
            @Override
            protected Object run(final SapphireRenderingContext context) {
                handleJumpCommand();
                return null;
            }
        };

        final SapphireAction jumpAction = actions.getAction(SapphireActionSystem.ACTION_JUMP);
        jumpActionHandler.init(jumpAction, null);
        jumpAction.addHandler(jumpActionHandler);

        this.table.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(final DisposeEvent event) {
                display.removeFilter(SWT.KeyDown, keyListener);
                display.removeFilter(SWT.KeyUp, keyListener);
                jumpAction.removeHandler(jumpActionHandler);
            }
        });
    }

    public void setController(final Controller controller) {
        this.controller = controller;
    }

    public Table getTable() {
        return this.table;
    }

    private void handleKeyEvent(final Event event) {
        if (event.keyCode == SWT.CONTROL) {
            this.controlKeyActive = (event.type == SWT.KeyDown);

            // Only force update when user releases the control key. We want the hyperlink
            // to show only after user starts moving the mouse after holding down the
            // control key.

            if (!this.controlKeyActive) {
                update();
            }
        }
    }

    private void handleMouseMove(final Event event) {
        this.mouseOverTableItem = null;
        this.mouseOverColumn = -1;

        for (int i = this.table.getTopIndex(), n = this.table.getItemCount(); i < n; i++) {
            final TableItem item = this.table.getItem(i);

            for (int j = 0, m = getColumnCount(this.table); j < m; j++) {
                final Rectangle bounds = item.getTextBounds(j);

                if (bounds.contains(event.x, event.y)) {
                    final GC gc = new GC(this.table);
                    final Point textExtent = gc.textExtent(item.getText(j));
                    gc.dispose();

                    bounds.width = textExtent.x;
                    bounds.height = textExtent.y;

                    if (bounds.contains(event.x, event.y) && this.controller.isHyperlinkEnabled(item, j)) {
                        this.mouseOverTableItem = item;
                        this.mouseOverColumn = j;
                    }

                    break;
                }
            }
        }

        update();
    }

    private void handleMouseExit(final Event event) {
        this.mouseOverTableItem = null;
        this.mouseOverColumn = -1;

        update();
    }

    private void handleMouseDown(final Event event) {
        if (this.controlKeyActive && this.mouseOverTableItem != null) {
            final TableItem item = this.mouseOverTableItem;

            this.table.setCursor(null);

            // Ideally, it would be best to prevent table selection from taking place. Haven't found
            // a way to do that yet. At the very least, the following makes sure that Ctrl+Click hyperlink
            // action doesn't also have a multi-select behavior.

            this.table.setSelection(item);

            handleJumpCommand(this.mouseOverTableItem, this.mouseOverColumn);
        }
    }

    private void handleJumpCommand() {
        final TableItem[] items = HyperlinkTable.this.table.getSelection();

        if (items.length == 1) {
            final TableItem item = items[0];
            final List<Integer> columnsWithHyperlinks = new ArrayList<Integer>();

            for (int i = 0, n = getColumnCount(HyperlinkTable.this.table); i < n; i++) {
                if (this.controller.isHyperlinkEnabled(item, i)) {
                    columnsWithHyperlinks.add(i);
                }
            }

            if (columnsWithHyperlinks.size() == 1) {
                handleJumpCommand(item, columnsWithHyperlinks.get(0));
            } else if (!columnsWithHyperlinks.isEmpty()) {
                final Dialog dialog = new Dialog(this.table.getShell()) {
                    private int choice = columnsWithHyperlinks.get(0);

                    @Override
                    protected Control createDialogArea(final Composite parent) {
                        getShell().setText(Resources.jumpDialogTitle);

                        final Composite composite = (Composite) super.createDialogArea(parent);

                        final Label prompt = new Label(composite, SWT.WRAP);
                        prompt.setLayoutData(gdwhint(gdhfill(), 300));
                        prompt.setText(Resources.jumpDialogPrompt);

                        final SelectionListener listener = new SelectionAdapter() {
                            public void widgetSelected(final SelectionEvent event) {
                                setChoice((Integer) event.widget.getData());
                            }
                        };

                        boolean first = true;

                        for (Integer col : columnsWithHyperlinks) {
                            final Button button = new Button(composite, SWT.RADIO | SWT.WRAP);
                            button.setLayoutData(gdhindent(gd(), 10));
                            button.setText(item.getText(col));
                            button.setData(col);

                            if (first) {
                                button.setSelection(true);
                                first = false;
                            }

                            button.addSelectionListener(listener);
                        }

                        return composite;
                    }

                    @Override
                    protected void okPressed() {
                        super.okPressed();
                        handleJumpCommand(item, this.choice);
                    }

                    private void setChoice(final int choice) {
                        this.choice = choice;
                    }
                };

                dialog.open();
            }
        }
    }

    private void handleJumpCommand(final TableItem item, final int column) {
        final Runnable op = new Runnable() {
            public void run() {
                HyperlinkTable.this.controller.handleHyperlinkEvent(item, column);
            }
        };

        BusyIndicator.showWhile(this.table.getDisplay(), op);
    }

    private void handleEraseItem(final Event event) {
        final TableItem item = (TableItem) event.item;

        if (this.controlKeyActive && this.mouseOverTableItem == item) {
            event.detail &= ~SWT.FOREGROUND;
        }
    }

    private void handlePaintItem(final Event event) {
        final TableItem item = (TableItem) event.item;

        if (this.controlKeyActive && this.mouseOverTableItem == item) {
            for (int i = 0, n = getColumnCount(this.table); i < n; i++) {
                final Display display = this.table.getDisplay();
                final String text = item.getText(i);

                final Font font = item.getFont(i);
                final TextStyle style = new TextStyle(font, null, null);

                if (this.mouseOverColumn == i) {
                    final Color hyperlinkColor = JFaceColors.getActiveHyperlinkText(display);
                    style.underline = true;
                    style.foreground = hyperlinkColor;
                    style.underlineColor = hyperlinkColor;
                }

                final Image image = item.getImage(i);

                if (image != null) {
                    final Rectangle bounds = item.getBounds(i);
                    final Point offset = (i == 0 ? IMAGE_OFFSET_PRIMARY_COLUMN : IMAGE_OFFSET_SECONDARY_COLUMN);
                    event.gc.drawImage(image, bounds.x + offset.x, bounds.y + offset.y);
                }

                final TextLayout layout = new TextLayout(display);
                layout.setText(text);
                layout.setStyle(style, 0, text.length() - 1);

                final Point offset = (i == 0 ? TEXT_OFFSET_PRIMARY_COLUMN : TEXT_OFFSET_SECONDARY_COLUMN);

                final Rectangle clientArea = item.getTextBounds(i);
                layout.setWidth(clientArea.width);
                layout.draw(event.gc, clientArea.x + offset.x, clientArea.y + offset.y);
            }
        }
    }

    private void update() {
        if (this.controlKeyActive && this.mouseOverTableItem != null) {
            this.table.setCursor(this.table.getDisplay().getSystemCursor(SWT.CURSOR_HAND));
        } else {
            this.table.setCursor(null);
        }

        this.table.redraw();
    }

    private static int getColumnCount(final Table table) {
        final int count = table.getColumnCount();
        return (count == 0 ? 1 : count);
    }

    private static final class Resources extends NLS {
        public static String jumpDialogTitle;
        public static String jumpDialogPrompt;

        static {
            initializeMessages(HyperlinkTable.class.getName(), Resources.class);
        }
    }

}