/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */

/**
 * This examples shows illustrates how to use various
 * accessibility features inside a Perforce JViews Diagrammer.
 */
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;

import javax.swing.Box;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JToolBar;

import ilog.views.IlvAccelerator;
import ilog.views.IlvGrapher;
import ilog.views.IlvGraphic;
import ilog.views.IlvManager;
import ilog.views.IlvManagerView;
import ilog.views.IlvRect;
import ilog.views.accelerator.IlvCycleSelectAccelerator;
import ilog.views.accelerator.IlvDeleteSelectionAccelerator;
import ilog.views.accelerator.IlvExpandSelectionAccelerator;
import ilog.views.accelerator.IlvFitToSizeAccelerator;
import ilog.views.accelerator.IlvIdentityAccelerator;
import ilog.views.accelerator.IlvMoveSelectionAccelerator;
import ilog.views.accelerator.IlvPopupMenuAccelerator;
import ilog.views.accelerator.IlvRotateAccelerator;
import ilog.views.accelerator.IlvScrollDownAccelerator;
import ilog.views.accelerator.IlvScrollLeftAccelerator;
import ilog.views.accelerator.IlvScrollRightAccelerator;
import ilog.views.accelerator.IlvScrollUpAccelerator;
import ilog.views.accelerator.IlvSelectAllAccelerator;
import ilog.views.accelerator.IlvZoomInAccelerator;
import ilog.views.accelerator.IlvZoomOutAccelerator;
import ilog.views.diagrammer.IlvDiagrammer;
import ilog.views.diagrammer.IlvDiagrammerProduct;
import ilog.views.diagrammer.application.IlvDiagrammerAction;
import ilog.views.diagrammer.application.IlvDiagrammerFrame;
import ilog.views.diagrammer.application.IlvDiagrammerMenu;
import ilog.views.diagrammer.application.IlvDiagrammerMenuBar;
import ilog.views.diagrammer.project.IlvDiagrammerProject;
import ilog.views.sdm.IlvSDMEngine;
import ilog.views.sdm.IlvSDMView;
import ilog.views.sdm.renderer.IlvRendererUtil;
import ilog.views.swing.IlvPopupMenuContext;
import ilog.views.swing.IlvPopupMenuManager;
import ilog.views.swing.IlvSimplePopupMenu;
import ilog.views.util.IlvProductUtil;
import ilog.views.util.internal.IlvSplash;
import ilog.views.util.swing.IlvSwingUtil;

/**
 * This is a very simple application that shows how to use various
 * accessibility features inside an IlvDiagrammer.
 */
public class AccessibleDiagram extends IlvDiagrammerFrame {
  
  private ResourceBundle bundle;
  private URL backgroundStyleSheet;
  private URL normalBackgroundStyleSheet;
  private URL highContrastBackgroundStyleSheet;
  private URL varyShapesStyleSheet;
  private URL varyShapesOnStyleSheet;
  private URL varyShapesOffStyleSheet;

  /**
   * The color modes.
   */
  private static int NORMAL = 0;
  private static int GRAYSCALE = 1;
  private static int DEUTAN = 2;

  private int colorMode = NORMAL;
  private boolean blockColorModeChange = false;

  private JMenu colorModeMenu;

  /**
   * Varying shapes modes.
   */
  private boolean varyingShapes = false;

  /**
   * High contrast mode.
   */
  private boolean highContrastMode = false;

  /**
   * High contrast colors, for the GUI only.
   */
  private Color lightColor = new Color(238, 238, 238);
  private Color darkColor = new Color(51, 51, 51);

  /**
   * Fonts for normal contrast.
   */
  private Map<Object, Font> normalContrastFonts = new HashMap<Object, Font>();

  /**
   * Fonts for high contrast.
   */
  private Map<Object, Font> highContrastFonts = new HashMap<Object, Font>();

  /**
   * The popup menu.
   */
  private JPopupMenu popupMenu = null;

  /**
   * The about dialog.
   */
  private JDialog aboutDialog = null;

  /**
   * The label showing the keypress.
   */
  private JLabel statusLabel = new JLabel("No focus on view");

  /**
   * Comparator for the traversal order: nesting has highest, then x has
   * priority
   */
  Comparator<IlvGraphic> nestedThenXThenY = new Comparator<IlvGraphic>() {
    Override
    public int compare(IlvGraphic a, IlvGraphic b) {
      IlvGrapher commonBag = IlvGrapher.getLowestCommonGrapher(a, b);
      if (commonBag != null) {
        while (a.getGraphicBag() != commonBag)
          a = (IlvGraphic) a.getGraphicBag();
        while (b.getGraphicBag() != commonBag)
          b = (IlvGraphic) b.getGraphicBag();
      }
      // use model coordinates, not graphic coordinates to have a
      // predictive behavior
      IlvSDMEngine engine = IlvSDMEngine.getSDMEngine(a);
      if (IlvSDMEngine.getSDMEngine(b) != engine)
        return 0;
      Object ma = engine.getObject(a);
      Object mb = engine.getObject(b);
      if (ma == mb)
        return 0;
      if (ma == null)
        return -1;
      if (mb == null)
        return 1;

      // does not work when coordinates are not stored in the model
      // or when coordinates are stored as latitude/logitude
      float ax = IlvRendererUtil.getGraphicPropertyAsFloat(engine, ma, "x", null, -1);
      float bx = IlvRendererUtil.getGraphicPropertyAsFloat(engine, mb, "x", null, -1);

      if (ax < bx)
        return -1;
      if (ax > bx)
        return 1;

      float ay = IlvRendererUtil.getGraphicPropertyAsFloat(engine, ma, "y", null, -1);
      float by = IlvRendererUtil.getGraphicPropertyAsFloat(engine, mb, "y", null, -1);

      if (ay < by)
        return -1;
      if (ay > by)
        return 1;
      return 0;
    }
  };

  public static void main(final String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      Override
      public void run() {
        AccessibleDiagram app = new AccessibleDiagram();
        app.init(args);
      }
    });
  }

  public AccessibleDiagram() {
    super(new String[] { "-simple" });
  }

  /**
   * This method is overridden to change the size of the frame.
   */
  Override
  public void initFrame() {
    super.initFrame();
    setSize(800, 400);
  }

  /**
   * Initializes the GUI.
   * 
   * @param contentPane
   *          The container of the application.
   */
  Override
  public void init(Container contentPane) {
    // This sample uses JViews Diagrammer features. When deploying an
    // application that includes this code, you need to be in possession
    // of a Perforce JViews Diagrammer Deployment license.
    IlvProductUtil.DeploymentLicenseRequired(IlvProductUtil.JViews_Diagrammer_Deployment);

    bundle = ResourceBundle.getBundle("accessibledemo");

    try {
      URL documentBase = new URL("file:./");
      

      normalBackgroundStyleSheet = new URL(documentBase, "data/background.css");
      highContrastBackgroundStyleSheet = new URL(documentBase, "data/backgroundHC.css");
      varyShapesOnStyleSheet = new URL(documentBase, "data/varyshapeon.css");
      varyShapesOffStyleSheet = new URL(documentBase, "data/varyshapeoff.css");
    } catch (Exception ex) {
      ex.printStackTrace();
    }

    super.init(contentPane);

    // remove the items that we don't need
    IlvDiagrammerMenuBar menuBar = (IlvDiagrammerMenuBar) getJMenuBar();
    int idx = menuBar.getFileMenu().indexOf(IlvDiagrammerAction.printWithDialog);
    if (idx > 0) {
      try {
        idx--;
        // separator
        menuBar.getFileMenu().remove(idx);
        // the printWithDiaglog
        menuBar.getFileMenu().remove(idx);
      } catch (Exception ex) {
      }
    }

    // change the about dialog
    IlvDiagrammerMenu helpMenu = menuBar.getDiagrammerHelpMenu();

    final IlvDiagrammerAction aboutAction = new IlvDiagrammerAction("Diagrammer.Action.About") {

      Override
      protected boolean isEnabled(IlvDiagrammer diagrammer) throws Exception {
        return true;
      }

      Override
      public void perform(ActionEvent e, IlvDiagrammer diagrammer) throws Exception {
        if (aboutDialog == null)
          return;
        aboutDialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
        aboutDialog.setVisible(true);
      }

    };
    helpMenu.insertAction(aboutAction, 0);
    try {
      helpMenu.remove(1);
    } catch (Exception ex) {
    }

    // remove the properties and tree view
    JToolBar viewToolBar = getViewToolBar();
    try {
      viewToolBar.remove(8);
      viewToolBar.remove(7);
    } catch (Exception ex) {
    }

    // remove the properties and tree view
    IlvDiagrammerMenu viewMenu = menuBar.getViewMenu();
    try {
      viewMenu.remove(6);
      viewMenu.remove(5);
    } catch (Exception ex) {
    }

    // enhance the view menu with the high contrast mode
    highContrastMode = IlvSwingUtil.isHighContrastMode();
    final IlvDiagrammerAction.ToggleAction hcAction = new IlvDiagrammerAction.ToggleAction(
        "AccessibleDiagram.HighContrast", bundle) {

      Override
      protected boolean isEnabled(IlvDiagrammer diagrammer) throws Exception {
        return true;
      }

      Override
      protected boolean isSelected(IlvDiagrammer diagrammer) throws Exception {
        return highContrastMode;
      }

      Override
      protected void setSelected(IlvDiagrammer diagrammer, boolean selected) throws Exception {
        if (selected) {
          setHighContrast(true);
        } else {
          setHighContrast(false);
        }
      }
    };
    viewMenu.insertAction(hcAction, 0);
    viewMenu.insertSeparator(1);

    final IlvDiagrammerAction.ToggleAction vsAction = new IlvDiagrammerAction.ToggleAction(
        "AccessibleDiagram.VaryShapes", bundle) {

      Override
      protected boolean isEnabled(IlvDiagrammer diagrammer) throws Exception {
        return true;
      }

      Override
      protected boolean isSelected(IlvDiagrammer diagrammer) throws Exception {
        return varyingShapes;
      }

      Override
      protected void setSelected(IlvDiagrammer diagrammer, boolean selected) throws Exception {
        setVaryShapesCSS(selected);
        varyingShapes = selected;
      }
    };
    viewMenu.insertAction(vsAction, 2);

    // submenu for the drawing mode
    colorModeMenu = new JMenu(getString("AccessibleDiagram.ColorMode.Name"));
    colorModeMenu.setOpaque(true);
    final JCheckBoxMenuItem[] colorModeChoices = new JCheckBoxMenuItem[3];

    colorModeChoices[0] = new JCheckBoxMenuItem(getString("AccessibleDiagram.ColorMode.Normal"));
    colorModeChoices[1] = new JCheckBoxMenuItem(getString("AccessibleDiagram.ColorMode.Grayscale"));
    colorModeChoices[2] = new JCheckBoxMenuItem(getString("AccessibleDiagram.ColorMode.Deutan"));

    ActionListener action = new ActionListener() {
      Override
      public void actionPerformed(ActionEvent evt) {
        if (blockColorModeChange)
          return;
        blockColorModeChange = true;
        try {
          Object src = evt.getSource();
          for (int i = 0; i < colorModeChoices.length; i++) {
            if (src == colorModeChoices[i])
              colorMode = i;
            colorModeChoices[i].setSelected(false);
          }
          colorModeChoices[colorMode].setSelected(true);
        } finally {
          blockColorModeChange = false;
        }
        getCurrentDiagrammer().getView().repaint();
      }
    };

    colorModeChoices[0].setSelected(true);
    for (int i = 0; i < colorModeChoices.length; i++) {
      colorModeChoices[i].addActionListener(action);
      colorModeMenu.add(colorModeChoices[i]);
    }
    viewMenu.add(colorModeMenu, 3);
    viewMenu.insertSeparator(4);

    // add the status label at the right side of the menu bar
    menuBar.add(Box.createHorizontalGlue());
    menuBar.add(statusLabel);

    // exchange the grapher with one that notifies the status label
    IlvDiagrammer diagrammer = getCurrentDiagrammer();
    diagrammer.getEngine().setGrapher(new IlvGrapher() {
      Override
      protected boolean handleAccelerators(AWTEvent evt, IlvManagerView view) {
        if (evt instanceof KeyEvent) {
          KeyEvent kev = (KeyEvent) evt;
          if (kev.getID() == KeyEvent.KEY_PRESSED) {
            statusLabel.setText(getKeyEventLabel(kev));
          }
        }
        return super.handleAccelerators(evt, view);
      }
    });
    diagrammer.getView().setManager(diagrammer.getEngine().getGrapher());

    // set the manager view to revieve TAB events
    IlvManagerView mgrview = diagrammer.getView();
    mgrview.setFocusable(true);
    mgrview.setFocusCycleRoot(true);
    mgrview.setFocusTraversalKeysEnabled(false);
    mgrview.addFocusListener(new FocusListener() {
      Override
      public void focusGained(FocusEvent e) {
        statusLabel.setText("Key:0x----(-)---------");
      }

      Override
      public void focusLost(FocusEvent e) {
        statusLabel.setText("No focus on view");
      }
    });

    // install the accelerators
    installAccelerators(diagrammer.getEngine().getGrapher());

    // install the popup menu
    // register view view to the popup manager
    IlvPopupMenuManager.registerView(diagrammer.getView());

    // register the menu
    IlvPopupMenuManager.registerMenu("MENU", popupMenu = createPopupMenu());

    // allocate the about dialog
    aboutDialog = IlvSplash.createSplashDialog(diagrammer, null, null, null, "Perforce JViews Diagrammer Demo", "",
        IlvDiagrammerProduct.getVersion(), IlvDiagrammerProduct.getMinorVersion(),
        IlvDiagrammerProduct.getSubMinorVersion(), IlvDiagrammerProduct.getPatchLevel(),
        IlvDiagrammerProduct.getBuildNumber(), IlvDiagrammerProduct.getReleaseDate());

    // check whether we started with high contrast node
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    Boolean highContrast = (Boolean) toolkit.getDesktopProperty("win.highContrast.on");
    if (highContrast != null && highContrast.booleanValue()) {
      setHighContrast(true);
    }
  }

  /**
   * Loads the first diagram.
   */
  Override
  protected void ready() {
    super.ready();
    loadDiagram();
    // fit to the initial part
    IlvSDMView view = getCurrentDiagrammer().getView();
    view.fitTransformerToArea(null, new IlvManagerView.FitAreaCalculator() {
      Override
      public IlvRect getAreaToFit(IlvManagerView view) {
        return new IlvRect(-420, 100, 200, 200);
      }
    }, 1, true);
  }

  /**
   * Loads a diagram.
   * 
   * @param project
   *          The diagram project to load.
   */
  private void loadDiagram() {
    try {
      URL documentBase = new URL("file:./");
      URL url = new URL(documentBase, "data/accsample.idpr");
      IlvDiagrammerProject project = new IlvDiagrammerProject(url);
      getCurrentDiagrammer().setProject(project);

      // add the style sheet for baclground
      backgroundStyleSheet = normalBackgroundStyleSheet;
      varyShapesStyleSheet = varyShapesOffStyleSheet;
      getCurrentDiagrammer().addStyleSheet(backgroundStyleSheet);
      getCurrentDiagrammer().addStyleSheet(varyShapesStyleSheet);
    } catch (Exception e1) {
      e1.printStackTrace();
    }

    setHighContrastCSS(getCurrentDiagrammer(), highContrastMode);
    setVaryShapesCSS(varyingShapes);
  }

  /**
   * Sets the high contrast code.
   */
  private void setHighContrast(boolean on) {
    if (on == highContrastMode)
      return;
    highContrastMode = on;
    setHighContrastCSS(getCurrentDiagrammer(), on);
    Container c = getCurrentDiagrammer();
    while (c.getParent() != null)
      c = c.getParent();
    setHighContrast(c, on);
    if (popupMenu != null) {
      setHighContrast(popupMenu, on);
    }
    if (aboutDialog != null) {
      setHighContrast(aboutDialog, on);
    }
    setHighContrast(getOverview(), on);
  }

  /**
   * Sets high contrast on a hierarchy of Swing components. This is usually not
   * needed, if the high contrast mode was set at the operating level already
   * when starting this demo. We do this code in this demo only to illustrate
   * and to be able to switch the high contrast mode dynamically on the fly.
   */
  void setHighContrast(Container c, boolean on) {
    c.setForeground(on ? lightColor : darkColor);
    c.setBackground(on ? darkColor : lightColor);
    adaptFont(c, on);

    if (c instanceof JMenu) {
      Component[] cs = ((JMenu) c).getMenuComponents();
      if (cs == null)
        return;
      for (int i = 0; i < cs.length; i++) {
        if (cs[i] instanceof Container)
          setHighContrast((Container) cs[i], on);
      }
    } else {
      Component[] cs = c.getComponents();
      if (cs == null)
        return;
      for (int i = 0; i < cs.length; i++) {
        if (cs[i] instanceof Container)
          setHighContrast((Container) cs[i], on);
      }
    }
  }

  /**
   * Adapt the font to high contrast on a component.
   */
  void adaptFont(Component c, boolean highContrast) {
    if (c.isFontSet()) {
      Font oldFont = c.getFont();
      Font newFont = getAdapted(oldFont, c, highContrast);
      c.setFont(newFont);
    }
  }

  /**
   * Returns the high or low contrast font.
   */
  Font getAdapted(Font oldFont, Object key, boolean highContrast) {
    Font newFont = null;
    if (highContrast) {
      if (normalContrastFonts.get(key) == null)
        normalContrastFonts.put(key, oldFont);
      newFont = (Font) highContrastFonts.get(key);
      if (newFont == null) {
        newFont = new Font(oldFont.getName(), oldFont.getStyle(), (int) (oldFont.getSize() * 1.2));
        highContrastFonts.put(key, newFont);
      }
    } else {
      if (highContrastFonts.get(key) == null)
        highContrastFonts.put(key, oldFont);
      newFont = (Font) normalContrastFonts.get(key);
      if (newFont == null) {
        newFont = new Font(oldFont.getName(), oldFont.getStyle(), (int) (oldFont.getSize() / 1.2));
        normalContrastFonts.put(key, newFont);
      }
    }
    return newFont;
  }

  /**
   * Sets the CSS for high contrast.
   */
  private void setHighContrastCSS(IlvDiagrammer diagrammer, boolean on) {
    String theme = on ? IlvSDMEngine.HIGH_CONTRAST : IlvSDMEngine.NORMAL_CONTRAST;
    diagrammer.getEngine().setContrastAccessibilityTheme(theme);
    try {
      diagrammer.removeStyleSheet(backgroundStyleSheet);
      backgroundStyleSheet = on ? highContrastBackgroundStyleSheet : normalBackgroundStyleSheet;
      diagrammer.addStyleSheet(backgroundStyleSheet);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Sets the CSS for varying shapes.
   */
  private void setVaryShapesCSS(boolean on) {
    IlvDiagrammer diagrammer = getCurrentDiagrammer();
    try {
      diagrammer.removeStyleSheet(varyShapesStyleSheet);
      varyShapesStyleSheet = on ? varyShapesOnStyleSheet : varyShapesOffStyleSheet;
      diagrammer.addStyleSheet(varyShapesStyleSheet);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Install all accelerators.
   */
  private void installAccelerators(IlvGrapher grapher) {
    // F1 -> popup menu
    // P -> popup menu
    // TAB -> select forward
    // Shift TAB -> select backward
    // F -> select forward
    // B -> select backward
    // A -> select all
    // Ctrl A -> deselect all
    // Ctrl 0 -> Identity zoom
    // Ctrl F -> Fit zoom
    // Ctrl - -> Zoom out
    // Ctrl + -> Zoom in
    // Ctrl R -> Rotate view
    // Arrow up -> Scroll up
    // Arrow down -> Scroll down
    // Arrow left -> Scroll left
    // Arrow right -> Scroll right
    // Shift Arrow up -> Move selection up
    // Shift Arrow down -> Move selection down
    // Shift Arrow left -> Move selection left
    // Shift Arrow right -> Move selection right
    // Delete key -> Delete selection
    // E -> Expand selection

    IlvPopupMenuAccelerator popupAcc;
    IlvCycleSelectAccelerator selAcc;
    IlvMoveSelectionAccelerator movAcc;
    IlvSelectAllAccelerator selAllAcc;
    IlvAccelerator acc;

    // just for shorter writing
    int keypress = KeyEvent.KEY_PRESSED;
    // int keyrelease = KeyEvent.KEY_RELEASED;
    int shift = KeyEvent.SHIFT_DOWN_MASK;
    int ctrl = KeyEvent.CTRL_DOWN_MASK;
    // int ctsht = KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK;
    char unspecCode = KeyEvent.VK_UNDEFINED;

    // popup menu
    popupAcc = new IlvPopupMenuAccelerator(keypress, KeyEvent.VK_F1, 0, true);
    grapher.addAccelerator(popupAcc);
    popupAcc = new IlvPopupMenuAccelerator(keypress, unspecCode, 'p', 0, true);
    grapher.addAccelerator(popupAcc);
    popupAcc = new IlvPopupMenuAccelerator(keypress, unspecCode, 'P', shift, true);
    grapher.addAccelerator(popupAcc);

    // select all
    selAllAcc = new IlvSelectAllAccelerator(keypress, unspecCode, 'a', 0, true);
    selAllAcc.setTraverse(true);
    grapher.addAccelerator(selAllAcc);
    selAllAcc = new IlvSelectAllAccelerator(keypress, unspecCode, 'A', shift, true);
    selAllAcc.setTraverse(true);
    grapher.addAccelerator(selAllAcc);
    selAllAcc = new IlvSelectAllAccelerator(keypress, KeyEvent.VK_A, ctrl, true);
    selAllAcc.setTraverse(true);
    selAllAcc.setSelectAll(false);
    grapher.addAccelerator(selAllAcc);

    // select
    selAcc = new IlvCycleSelectAccelerator(keypress, KeyEvent.VK_TAB, 0, true);
    selAcc.setForward(true);
    selAcc.setComparator(nestedThenXThenY);
    grapher.addAccelerator(selAcc);
    selAcc = new IlvCycleSelectAccelerator(keypress, unspecCode, 'f', 0, true);
    selAcc.setForward(true);
    selAcc.setComparator(nestedThenXThenY);
    grapher.addAccelerator(selAcc);
    selAcc = new IlvCycleSelectAccelerator(keypress, unspecCode, 'F', shift, true);
    selAcc.setForward(true);
    selAcc.setComparator(nestedThenXThenY);
    grapher.addAccelerator(selAcc);
    selAcc = new IlvCycleSelectAccelerator(keypress, KeyEvent.VK_TAB, shift, true);
    selAcc.setForward(false);
    selAcc.setComparator(nestedThenXThenY);
    grapher.addAccelerator(selAcc);
    selAcc = new IlvCycleSelectAccelerator(keypress, unspecCode, 'b', 0, true);
    selAcc.setForward(false);
    selAcc.setComparator(nestedThenXThenY);
    grapher.addAccelerator(selAcc);
    selAcc = new IlvCycleSelectAccelerator(keypress, unspecCode, 'B', shift, true);
    selAcc.setForward(false);
    selAcc.setComparator(nestedThenXThenY);
    grapher.addAccelerator(selAcc);

    acc = new IlvDeleteSelectionAccelerator(keypress, KeyEvent.VK_DELETE, 0, true) {
      Override
      protected boolean handleEvent(IlvManagerView v) {
        // super.handleEvents deals only with the graphic objects, but
        // we also need to delete the model objects
        if (v instanceof IlvSDMView) {
          IlvSDMEngine engine = ((IlvSDMView) v).getSDMEngine();
          // delete the selected objects
          engine.delete();
        }
        // in case there are graphics that don't correspond to model objects
        return super.handleEvent(v);
      }
    };
    grapher.addAccelerator(acc);

    acc = new IlvIdentityAccelerator(keypress, KeyEvent.VK_HOME, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvIdentityAccelerator(keypress, '0', ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvIdentityAccelerator(keypress, KeyEvent.VK_NUMPAD0, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvIdentityAccelerator(keypress, KeyEvent.VK_INSERT, ctrl, true);
    grapher.addAccelerator(acc);

    acc = new IlvZoomOutAccelerator(keypress, KeyEvent.VK_SUBTRACT, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvZoomOutAccelerator(keypress, unspecCode, '-', ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvZoomInAccelerator(keypress, KeyEvent.VK_ADD, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvZoomInAccelerator(keypress, unspecCode, '+', ctrl, true);
    grapher.addAccelerator(acc);

    acc = new IlvFitToSizeAccelerator(keypress, KeyEvent.VK_NUMPAD5, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvFitToSizeAccelerator(keypress, KeyEvent.VK_BEGIN, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvFitToSizeAccelerator(keypress, KeyEvent.VK_F, ctrl, true);
    grapher.addAccelerator(acc);

    acc = new IlvRotateAccelerator(keypress, KeyEvent.VK_R, ctrl, true);
    grapher.addAccelerator(acc);

    // scroll with normal arrows
    acc = new IlvScrollUpAccelerator(keypress, KeyEvent.VK_UP, 0, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollDownAccelerator(keypress, KeyEvent.VK_DOWN, 0, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollRightAccelerator(keypress, KeyEvent.VK_RIGHT, 0, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollLeftAccelerator(keypress, KeyEvent.VK_LEFT, 0, true);
    grapher.addAccelerator(acc);
    // scroll with numpad
    acc = new IlvScrollUpAccelerator(keypress, KeyEvent.VK_NUMPAD8, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollDownAccelerator(keypress, KeyEvent.VK_NUMPAD2, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollLeftAccelerator(keypress, KeyEvent.VK_NUMPAD4, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollRightAccelerator(keypress, KeyEvent.VK_NUMPAD6, ctrl, true);
    grapher.addAccelerator(acc);
    // scroll with arrows on numpad
    acc = new IlvScrollUpAccelerator(keypress, KeyEvent.VK_KP_UP, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollDownAccelerator(keypress, KeyEvent.VK_KP_DOWN, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollLeftAccelerator(keypress, KeyEvent.VK_KP_LEFT, ctrl, true);
    grapher.addAccelerator(acc);
    acc = new IlvScrollRightAccelerator(keypress, KeyEvent.VK_KP_RIGHT, ctrl, true);
    grapher.addAccelerator(acc);

    // move with normal arrows
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_UP, shift, true);
    movAcc.setMoveVector(0, -10);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_DOWN, shift, true);
    movAcc.setMoveVector(0, 10);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_LEFT, shift, true);
    movAcc.setMoveVector(-10, 0);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_RIGHT, shift, true);
    movAcc.setMoveVector(10, 0);
    grapher.addAccelerator(movAcc);
    // move with numpad
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_NUMPAD8, shift, true);
    movAcc.setMoveVector(0, -5);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_NUMPAD2, shift, true);
    movAcc.setMoveVector(0, 5);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_NUMPAD4, shift, true);
    movAcc.setMoveVector(-5, 0);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_NUMPAD6, shift, true);
    movAcc.setMoveVector(5, 0);
    grapher.addAccelerator(movAcc);
    // move with arrows on numpad
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_KP_UP, shift, true);
    movAcc.setMoveVector(0, -3);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_KP_DOWN, shift, true);
    movAcc.setMoveVector(0, 3);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_KP_LEFT, shift, true);
    movAcc.setMoveVector(-3, 0);
    grapher.addAccelerator(movAcc);
    movAcc = new IlvMoveSelectionAccelerator(keypress, KeyEvent.VK_KP_RIGHT, shift, true);
    movAcc.setMoveVector(3, 0);
    grapher.addAccelerator(movAcc);

    // expand selection
    acc = new IlvExpandSelectionAccelerator(keypress, KeyEvent.VK_E, 0, true);
    grapher.addAccelerator(acc);
    acc = new IlvExpandSelectionAccelerator(keypress, KeyEvent.VK_E, shift, true);
    grapher.addAccelerator(acc);
  }

  /**
   * Returns the indicator of a key event.
   */
  private String getKeyEventLabel(KeyEvent ev) {
    String digits = "0123456789ABCDEF";
    StringBuffer buf = new StringBuffer("Key:0x");
    int code = ev.getKeyCode();

    code = code & 0xffff;
    int d = code / 4096;
    buf.append(digits.charAt(d));
    code -= d * 4096;
    d = code / 256;
    buf.append(digits.charAt(d));
    code -= d * 256;
    d = code / 16;
    buf.append(digits.charAt(d));
    code -= d * 16;
    buf.append(digits.charAt(code));
    buf.append('(');
    if (ev.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
      // nothing
    } else if (Character.isISOControl(ev.getKeyChar())) {
      code = (int) ev.getKeyChar();
      code = code & 0xff;
      d = code / 16;
      buf.append(digits.charAt(d));
      code -= d * 16;
      buf.append(digits.charAt(code));
    } else {
      buf.append(ev.getKeyChar());
    }
    buf.append(')');

    int mod = ev.getModifiersEx();
    boolean shift = ((mod & InputEvent.SHIFT_DOWN_MASK) != 0);
    boolean ctrl = ((mod & InputEvent.CTRL_DOWN_MASK) != 0);
    boolean alt = ((mod & InputEvent.ALT_DOWN_MASK) != 0);
    boolean meta = ((mod & InputEvent.META_DOWN_MASK) != 0);
    boolean altgr = ((mod & InputEvent.ALT_GRAPH_DOWN_MASK) != 0);

    int count = 0;
    if (shift)
      count++;
    if (ctrl)
      count++;
    if (alt)
      count++;
    if (meta)
      count++;
    if (altgr)
      count++;

    if (shift) {
      if (count > 4)
        buf.append("S");
      else if (count >= 3)
        buf.append("Sh");
      else if (count == 2)
        buf.append("Shft");
      else
        buf.append("Shift");
    }
    if (ctrl) {
      if (count > 4)
        buf.append("C");
      else if (count >= 3)
        buf.append("Ct");
      else if (count == 2)
        buf.append("Ctrl");
      else
        buf.append("Control");
    }
    if (alt) {
      if (count > 4)
        buf.append("A");
      else if (count >= 3)
        buf.append("Al");
      else
        buf.append("Alt");
    }
    if (meta) {
      if (count > 4)
        buf.append("M");
      else if (count >= 3)
        buf.append("Me");
      else
        buf.append("Meta");
    }
    if (altgr) {
      if (count > 4)
        buf.append("a");
      else if (count >= 3)
        buf.append("Ag");
      else if (count == 2)
        buf.append("Algr");
      else
        buf.append("Altgraph");
    }

    if (ev.isConsumed())
      buf.append("*");
    else
      buf.append(" ");

    return buf.toString();
  }

  /**
   * Creates the popup menu.
   */
  private JPopupMenu createPopupMenu() {
    // create the action listener for the popup menu
    ActionListener actionListener = new ActionListener() {
      Override
      public void actionPerformed(ActionEvent e) {

        // retrieve the selected menu item
        JMenuItem m = (JMenuItem) e.getSource();

        // retrieve the graphic that has this popup menu
        IlvPopupMenuContext context = IlvPopupMenuManager.getPopupMenuContext(m);
        IlvGraphic graphic = context.getGraphic();

        // do the action
        if (m.getText().equals("Shift left")) {
          IlvRect bbox = graphic.boundingBox();
          bbox.x -= 20;
          IlvManager manager = (IlvManager) graphic.getGraphicBag();
          // this will also notify the model
          manager.moveObject(graphic, bbox.x, bbox.y, true);
        } else if (m.getText().equals("Shift right")) {
          IlvRect bbox = graphic.boundingBox();
          bbox.x += 20;
          IlvManager manager = (IlvManager) graphic.getGraphicBag();
          // this will also notify the model
          manager.moveObject(graphic, bbox.x, bbox.y, true);
        }
      }
    };

    // create a simple popup menu with the following items
    // - Make larger: makes the graphic larger
    // - Make smaller: makes the graphic smaller
    IlvSimplePopupMenu menu = new IlvSimplePopupMenu("Menu1", "Shift left | Shift right", null, actionListener);

    return menu;
  }

  /**
   * Creates a new diagrammer.
   */
  Override
  protected IlvDiagrammer createDiagrammer() {
    IlvSDMView view = new IlvSDMView() {
      Override
      protected Image createDoubleBufferImage(int width, int height) {
        return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
      }

      Override
      protected void doubleBufferedImageUpToDate(Image doubleBufferImage) {
        super.doubleBufferedImageUpToDate(doubleBufferImage);
        adaptBuffer(doubleBufferImage);
      }
    };
    // enable double buffering to allow the colorMode GRAYSCALE...
    view.setDoubleBuffering(true);
    // keep aspect ratio
    view.setKeepingAspectRatio(true);
    return new IlvDiagrammer(view);
  }

  /**
   * Adapts the double buffer to the color mode.
   */
  private void adaptBuffer(Image image) {
    if (colorMode == GRAYSCALE) {
      BufferedImage buffer = (BufferedImage) image;
      WritableRaster raster = buffer.copyData(null);
      int mx = raster.getMinX();
      int my = raster.getMinY();
      int w = raster.getWidth();
      int h = raster.getHeight();
      int[] data = null;
      for (int x = mx; x < mx + w; x++) {
        for (int y = my; y < my + h; y++) {
          data = raster.getPixel(x, y, data);
          if (data[0] == data[1] && data[1] == data[2])
            continue;
          int g = (int) (0.299 * data[0] + 0.587 * data[1] + 0.114 * data[2]);
          if (g < 0)
            g = 0;
          if (g > 255)
            g = 255;
          data[0] = data[1] = data[2] = g;
          raster.setPixel(x, y, data);
        }
      }
      buffer.setData(raster);
    } else if (colorMode == DEUTAN) {
      // There are many variants of red green blind. This is just one of them.
      BufferedImage buffer = (BufferedImage) image;
      WritableRaster raster = buffer.copyData(null);
      int mx = raster.getMinX();
      int my = raster.getMinY();
      int w = raster.getWidth();
      int h = raster.getHeight();
      int[] data = null;
      for (int x = mx; x < mx + w; x++) {
        for (int y = my; y < my + h; y++) {
          data = raster.getPixel(x, y, data);
          if (data[0] == data[1])
            continue;
          int g = (int) (0.337 * data[0] + 0.663 * data[1]);
          if (g < 0)
            g = 0;
          if (g > 255)
            g = 255;
          data[0] = data[1] = g;
          raster.setPixel(x, y, data);
        }
      }
      buffer.setData(raster);
    }
  }

  /**
   * Returns a string from the resource bundle.
   */
  private String getString(String key) {
    try {
      return bundle.getString(key);
    } catch (Exception ex) {
      return key;
    }
  }
}