/**
  * Updates the position and size of the edit panel relative to the given location.
  *
  * @param loc the location the edit panel should be relative to
  * @param absolute if {@code true} the loc is treated as absolute position on the process
  *     renderer; if {@code false} it is treated as relative to the current process
  */
 private void updateEditPanelPosition(final Rectangle2D loc, final boolean absolute) {
   int panelX = (int) loc.getCenterX() - EDIT_PANEL_WIDTH / 2;
   int panelY = (int) loc.getY() - EDIT_PANEL_HEIGHT - ProcessDrawer.PADDING;
   // if panel would be outside process renderer, fix it
   if (panelX < WorkflowAnnotation.MIN_X) {
     panelX = WorkflowAnnotation.MIN_X;
   }
   if (panelY < 0) {
     panelY = (int) loc.getMaxY() + ProcessDrawer.PADDING;
   }
   // last fallback is cramped to the bottom. If that does not fit either, don't care
   if (panelY + EDIT_PANEL_HEIGHT > view.getSize().getHeight() - ProcessDrawer.PADDING * 2) {
     panelY = (int) loc.getMaxY();
   }
   int index = view.getModel().getProcessIndex(model.getSelected().getProcess());
   if (absolute) {
     editPanel.setBounds(panelX, panelY, EDIT_PANEL_WIDTH, EDIT_PANEL_HEIGHT);
   } else {
     Point absoluteP =
         ProcessDrawUtils.convertToAbsoluteProcessPoint(
             new Point(panelX, panelY), index, rendererModel);
     editPanel.setBounds(
         (int) absoluteP.getX(), (int) absoluteP.getY(), EDIT_PANEL_WIDTH, EDIT_PANEL_HEIGHT);
   }
 }
  /** Removes the annotation editor from the process renderer. */
  private void removeEditor() {
    if (editPane != null) {
      view.remove(editPane);
      editPane = null;
    }
    if (editPanel != null) {
      view.remove(editPanel);
      editPanel = null;
    }
    removeColorPanel();

    // this makes sure that pressing F2 afterwards works
    // otherwise nothing is focused until the next click
    view.requestFocusInWindow();
    view.repaint();
  }
  /**
   * Makes sure the current editor height matches its content if the annotation was never resized.
   * If the annotation has been manually resized before, does nothing.
   *
   * @param anno the annotation currently in the editor
   */
  private void updateEditorHeight(final WorkflowAnnotation anno) {
    if (anno.wasResized()) {
      return;
    }

    Rectangle bounds = editPane.getBounds();
    // height is either the pref height or the current height, depending on what is bigger
    int prefHeight;
    if (anno instanceof ProcessAnnotation) {
      prefHeight =
          (int) Math.max(getContentHeightOfEditor((int) bounds.getWidth()), bounds.getHeight());
    } else {
      prefHeight =
          Math.max(
              getContentHeightOfEditor((int) bounds.getWidth()), OperatorAnnotation.MIN_HEIGHT);
    }
    Rectangle newBounds =
        new Rectangle(
            (int) bounds.getX(), (int) bounds.getY(), (int) bounds.getWidth(), prefHeight);
    if (!bounds.equals(newBounds)) {
      editPane.setBounds(newBounds);
      updateEditPanelPosition(newBounds, true);
      view.getModel().fireAnnotationMiscChanged(anno);
    }
  }
 /**
  * Creates a new workflow annotation decorator
  *
  * @param view the process renderer instance
  * @param visualizer the annotation visualizer instance
  * @param model the model backing this instance
  */
 public AnnotationsDecorator(
     final ProcessRendererView view,
     final AnnotationsVisualizer visualizer,
     final AnnotationsModel model) {
   this.view = view;
   this.model = model;
   this.rendererModel = view.getModel();
   this.drawer = new AnnotationDrawer(model, rendererModel);
   this.hook = new AnnotationEventHook(this, model, visualizer, drawer, view, rendererModel);
   this.visualizer = visualizer;
 }
  /**
   * Start inline editing of the selected annotation. If no annotation is selected, does nothing.
   */
  public void editSelected() {
    if (model.getSelected() == null) {
      return;
    }
    // editor to actually edit comment string
    removeEditor();
    createEditor();

    // panel to edit alignment and color
    createEditPanel();

    editPane.requestFocusInWindow();
    view.repaint();
  }
  /** Removes the event hooks and draw decorators from the process renderer. */
  void unregisterDecorators() {
    view.removeDrawDecorator(processAnnotationDrawer, RenderPhase.ANNOTATIONS);
    view.removeDrawDecorator(operatorAnnotationDrawer, RenderPhase.OPERATOR_ANNOTATIONS);
    view.removeDrawDecorator(workflowAnnotationDrawerHighlight, RenderPhase.OVERLAY);
    view.removeDrawDecorator(opAnnotationIconDrawer);

    view.getOverviewPanelDrawer().removeDecorator(processAnnotationDrawer, RenderPhase.ANNOTATIONS);
    view.getOverviewPanelDrawer()
        .removeDecorator(operatorAnnotationDrawer, RenderPhase.OPERATOR_ANNOTATIONS);

    hook.unregisterEventHooks();

    view.removeComponentListener(colorPanelMover);
    ApplicationFrame.getApplicationFrame().removeComponentListener(colorPanelMover);
  }
  /** Registers the event hooks and draw decorators to the process renderer. */
  void registerEventHooks() {
    view.addDrawDecorator(processAnnotationDrawer, RenderPhase.ANNOTATIONS);
    view.addDrawDecorator(operatorAnnotationDrawer, RenderPhase.OPERATOR_ANNOTATIONS);
    view.addDrawDecorator(workflowAnnotationDrawerHighlight, RenderPhase.OVERLAY);
    view.addDrawDecorator(opAnnotationIconDrawer);

    view.getOverviewPanelDrawer().addDecorator(processAnnotationDrawer, RenderPhase.ANNOTATIONS);
    view.getOverviewPanelDrawer()
        .addDecorator(operatorAnnotationDrawer, RenderPhase.OPERATOR_ANNOTATIONS);

    hook.registerDecorators();

    // this listener makes the color edit panel move when required
    view.addComponentListener(colorPanelMover);
    ApplicationFrame.getApplicationFrame().addComponentListener(colorPanelMover);
  }
  /**
   * Creates and adds the edit panel for the currently selected annotation to the process renderer.
   */
  private void createEditPanel() {
    final WorkflowAnnotation selected = model.getSelected();
    Rectangle2D loc = selected.getLocation();

    // panel containing buttons
    editPanel = new JPanel();
    editPanel.setCursor(Cursor.getDefaultCursor());
    editPanel.setLayout(new BoxLayout(editPanel, BoxLayout.LINE_AXIS));
    updateEditPanelPosition(loc, false);
    editPanel.setOpaque(true);
    editPanel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
    // consume mouse events so focus is not lost
    editPanel.addMouseListener(
        new MouseAdapter() {

          @Override
          public void mouseReleased(MouseEvent e) {
            e.consume();
          }

          @Override
          public void mousePressed(MouseEvent e) {
            e.consume();
          }

          @Override
          public void mouseClicked(MouseEvent e) {
            e.consume();
          }
        });

    // add alignment controls
    final List<JButton> alignmentButtonList = new LinkedList<JButton>();
    for (AnnotationAlignment align : AnnotationAlignment.values()) {
      final Action action = align.makeAlignmentChangeAction(model, model.getSelected());
      final JButton alignButton = new JButton();
      alignButton.setIcon((Icon) action.getValue(Action.SMALL_ICON));
      alignButton.setBorderPainted(false);
      alignButton.setBorder(null);
      alignButton.setFocusable(false);
      if (align == selected.getStyle().getAnnotationAlignment()) {
        alignButton.setBackground(Color.LIGHT_GRAY);
      }
      alignButton.addActionListener(
          new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
              removeColorPanel();
              colorButton.setBackground(null);
              for (JButton button : alignmentButtonList) {
                button.setBackground(null);
              }
              alignButton.setBackground(Color.LIGHT_GRAY);

              int caretPos = editPane.getCaretPosition();
              // remember if we were at last position because doc length can change after 1st
              // save
              boolean lastPos = caretPos == editPane.getDocument().getLength();
              int selStart = editPane.getSelectionStart();
              int selEnd = editPane.getSelectionEnd();
              // change alignment and save current comment
              action.actionPerformed(e);
              saveEdit(selected);
              // reload edit pane with changes
              editPane.setText(AnnotationDrawUtils.createStyledCommentString(selected));
              // special handling for documents of length 1 to avoid not being able to type
              if (editPane.getDocument().getLength() == 1) {
                caretPos = 1;
              } else if (lastPos) {
                caretPos = editPane.getDocument().getLength();
              } else {
                caretPos = Math.min(editPane.getDocument().getLength(), caretPos);
              }
              editPane.setCaretPosition(caretPos);
              if (selEnd - selStart > 0) {
                editPane.setSelectionStart(selStart);
                editPane.setSelectionEnd(selEnd);
              }
              editPane.requestFocusInWindow();
            }
          });
      editPanel.add(alignButton);
      alignmentButtonList.add(alignButton);
    }

    // add small empty space
    editPanel.add(Box.createHorizontalStrut(2));

    // add color controls
    colorOverlay = new JDialog(ApplicationFrame.getApplicationFrame());
    colorOverlay.setCursor(Cursor.getDefaultCursor());
    colorOverlay
        .getRootPane()
        .setLayout(new BoxLayout(colorOverlay.getRootPane(), BoxLayout.LINE_AXIS));
    colorOverlay.setUndecorated(true);
    colorOverlay.getRootPane().setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
    colorOverlay.setFocusable(false);
    colorOverlay.setAutoRequestFocus(false);
    // consume mouse events so focus is not lost
    colorOverlay.addMouseListener(
        new MouseAdapter() {

          @Override
          public void mouseReleased(MouseEvent e) {
            e.consume();
          }

          @Override
          public void mousePressed(MouseEvent e) {
            e.consume();
          }

          @Override
          public void mouseClicked(MouseEvent e) {
            e.consume();
          }
        });

    for (final AnnotationColor color : AnnotationColor.values()) {
      final Action action = color.makeColorChangeAction(model, selected);
      JButton colChangeButton = new JButton();
      colChangeButton.setText(null);
      colChangeButton.setBorderPainted(false);
      colChangeButton.setBorder(null);
      colChangeButton.setFocusable(false);
      final Icon icon =
          SwingTools.createIconFromColor(
              color.getColor(), Color.BLACK, 16, 16, new Rectangle2D.Double(1, 1, 14, 14));
      colChangeButton.setIcon(icon);
      colChangeButton.addActionListener(
          new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
              // change color and save current comment
              action.actionPerformed(e);
              saveEdit(selected);
              // set edit pane bg color
              editPane.requestFocusInWindow();
              if (color == AnnotationColor.TRANSPARENT) {
                editPane.setBackground(Color.WHITE);
              } else {
                editPane.setBackground(color.getColorHighlight());
              }

              // adapt color of main button, remove color panel
              colorButton.setIcon(icon);
              if (removeColorPanel()) {
                colorButton.setBackground(null);
                view.repaint();
              }
            }
          });

      colorOverlay.getRootPane().add(colChangeButton);
    }

    colorButton = new JButton("\u25BE");
    colorButton.setBorderPainted(false);
    colorButton.setFocusable(false);
    AnnotationColor color = selected.getStyle().getAnnotationColor();
    colorButton.setIcon(
        SwingTools.createIconFromColor(
            color.getColor(), Color.BLACK, 16, 16, new Rectangle2D.Double(1, 1, 14, 14)));
    colorButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            if (removeColorPanel()) {
              colorButton.setBackground(null);
              view.repaint();
              return;
            }

            updateColorPanelPosition();
            colorOverlay.setVisible(true);
            colorButton.setBackground(Color.LIGHT_GRAY);
            editPane.requestFocusInWindow();
            view.repaint();
          }
        });
    editPanel.add(colorButton);

    // add separator
    JLabel separator =
        new JLabel() {

          private static final long serialVersionUID = 1L;

          @Override
          public void paintComponent(Graphics g) {
            Graphics2D g2 = (Graphics2D) g;

            g2.setColor(Color.LIGHT_GRAY);
            g2.drawLine(2, 0, 2, 20);
          }
        };
    separator.setText(" "); // dummy text to show label
    editPanel.add(separator);

    // add delete button
    final JButton deleteButton =
        new JButton(
            I18N.getMessage(I18N.getGUIBundle(), "gui.action.workflow.annotation.delete.label"));
    deleteButton.setForeground(Color.RED);
    deleteButton.setContentAreaFilled(false);
    deleteButton.setFocusable(false);
    deleteButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            model.deleteAnnotation(selected);
            removeEditor();
          }
        });
    deleteButton.addMouseListener(
        new MouseAdapter() {

          @Override
          @SuppressWarnings({"unchecked", "rawtypes"})
          public void mouseExited(MouseEvent e) {
            Font font = deleteButton.getFont();
            Map attributes = font.getAttributes();
            attributes.put(TextAttribute.UNDERLINE, -1);
            deleteButton.setFont(font.deriveFont(attributes));
          }

          @SuppressWarnings({"unchecked", "rawtypes"})
          @Override
          public void mouseEntered(MouseEvent e) {
            Font font = deleteButton.getFont();
            Map attributes = font.getAttributes();
            attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
            deleteButton.setFont(font.deriveFont(attributes));
          }
        });
    editPanel.add(deleteButton);

    // add panel to view
    view.add(editPanel);
  }
  /**
   * Creates and adds the JEditorPane for the currently selected annotation to the process renderer.
   */
  private void createEditor() {
    final WorkflowAnnotation selected = model.getSelected();
    Rectangle2D loc = selected.getLocation();

    // JEditorPane to edit the comment string
    editPane = new JEditorPane("text/html", "");
    editPane.setBorder(null);
    int paneX = (int) loc.getX();
    int paneY = (int) loc.getY();
    int index = view.getModel().getProcessIndex(selected.getProcess());
    Point absolute =
        ProcessDrawUtils.convertToAbsoluteProcessPoint(
            new Point(paneX, paneY), index, rendererModel);
    editPane.setBounds(
        (int) absolute.getX(), (int) absolute.getY(), (int) loc.getWidth(), (int) loc.getHeight());
    editPane.setText(AnnotationDrawUtils.createStyledCommentString(selected));
    // use proxy for paste actions to trigger reload of editor after paste
    Action pasteFromClipboard = editPane.getActionMap().get(PASTE_FROM_CLIPBOARD_ACTION_NAME);
    Action paste = editPane.getActionMap().get(PASTE_ACTION_NAME);
    if (pasteFromClipboard != null) {
      editPane
          .getActionMap()
          .put(
              PASTE_FROM_CLIPBOARD_ACTION_NAME,
              new PasteAnnotationProxyAction(pasteFromClipboard, this));
    }
    if (paste != null) {
      editPane.getActionMap().put(PASTE_ACTION_NAME, new PasteAnnotationProxyAction(paste, this));
    }
    // use proxy for transfer actions to convert e.g. HTML paste to plaintext paste
    editPane.setTransferHandler(new TransferHandlerAnnotationPlaintext(editPane));
    // IMPORTANT: Linebreaks do not work without the following!
    // this filter inserts a \r every time the user enters a newline
    // this signal is later used to convert newline to <br/>
    ((HTMLDocument) editPane.getDocument())
        .setDocumentFilter(
            new DocumentFilter() {

              @Override
              public void insertString(
                  DocumentFilter.FilterBypass fb, int offs, String str, AttributeSet a)
                  throws BadLocationException {
                // this is never called..
                super.insertString(
                    fb,
                    offs,
                    str.replaceAll("\n", "\n" + AnnotationDrawUtils.ANNOTATION_HTML_NEWLINE_SIGNAL),
                    a);
              }

              @Override
              public void replace(FilterBypass fb, int offs, int length, String str, AttributeSet a)
                  throws BadLocationException {
                if (selected instanceof OperatorAnnotation) {
                  // operator annotations have a character limit, enforce here
                  try {
                    int existingLength =
                        AnnotationDrawUtils.getPlaintextFromEditor(editPane, false).length()
                            - length;
                    if (existingLength + str.length() > OperatorAnnotation.MAX_CHARACTERS) {
                      // insert at beginning or end is fine, cut off excess characters
                      if (existingLength <= 0 || offs >= existingLength) {
                        int acceptableLength = OperatorAnnotation.MAX_CHARACTERS - existingLength;
                        int newLength = Math.max(acceptableLength, 0);
                        str = str.substring(0, newLength);
                      } else {
                        // inserting into middle, do NOT paste at all
                        return;
                      }
                    }
                  } catch (IOException e) {
                    // should not happen, if it does this is our smallest problem -> ignore
                  }
                }
                super.replace(
                    fb,
                    offs,
                    length,
                    str.replaceAll("\n", "\n" + AnnotationDrawUtils.ANNOTATION_HTML_NEWLINE_SIGNAL),
                    a);
              }
            });
    // set background color
    if (selected.getStyle().getAnnotationColor() == AnnotationColor.TRANSPARENT) {
      editPane.setBackground(Color.WHITE);
    } else {
      editPane.setBackground(selected.getStyle().getAnnotationColor().getColorHighlight());
    }
    editPane.addFocusListener(
        new FocusAdapter() {

          @Override
          public void focusLost(final FocusEvent e) {
            // right-click menu
            if (e.isTemporary()) {
              return;
            }
            if (editPane != null && e.getOppositeComponent() != null) {
              // style edit menu, no real focus loss
              if (SwingUtilities.isDescendingFrom(e.getOppositeComponent(), editPanel)) {
                return;
              }
              if (SwingUtilities.isDescendingFrom(e.getOppositeComponent(), colorOverlay)) {
                return;
              }
              if (colorOverlay.getParent() == e.getOppositeComponent()) {
                return;
              }

              saveEdit(selected);
              removeEditor();
            }
          }
        });
    editPane.addKeyListener(
        new KeyAdapter() {

          /** keep track of control down so Ctrl+Enter works but Enter+Ctrl not */
          private boolean controlDown;

          @Override
          public void keyPressed(final KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
              controlDown = true;
            }
            // consume so undo/redo etc are not passed to the process
            if (SwingTools.isControlOrMetaDown(e) && e.getKeyCode() == KeyEvent.VK_Z
                || e.getKeyCode() == KeyEvent.VK_Y) {
              e.consume();
            }
          }

          @Override
          public void keyReleased(final KeyEvent e) {
            switch (e.getKeyCode()) {
              case KeyEvent.VK_CONTROL:
                controlDown = false;
                break;
              case KeyEvent.VK_ENTER:
                if (!controlDown) {
                  updateEditorHeight(selected);
                } else {
                  // if control was down before Enter was pressed, save & exit
                  saveEdit(selected);
                  removeEditor();
                  model.setSelected(null);
                }
                break;
              case KeyEvent.VK_ESCAPE:
                // ignore changes on escape
                removeEditor();
                model.setSelected(null);
                break;
              default:
                break;
            }
          }
        });
    editPane
        .getDocument()
        .addDocumentListener(
            new DocumentListener() {

              @Override
              public void removeUpdate(DocumentEvent e) {
                updateEditorHeight(selected);
              }

              @Override
              public void insertUpdate(DocumentEvent e) {
                updateEditorHeight(selected);
              }

              @Override
              public void changedUpdate(DocumentEvent e) {
                updateEditorHeight(selected);
              }
            });
    view.add(editPane);
    editPane.selectAll();
  }
 @Override
 public void actionPerformed(final ActionEvent e) {
   controller.autoArrange(view.getModel().getProcesses());
 }