/** Draws the background annoations. */
        private void draw(
            final ExecutionUnit process,
            final Graphics2D g2,
            final ProcessRendererModel rendererModel,
            final boolean printing) {
          if (!visualizer.isActive()) {
            return;
          }

          // background annotations
          WorkflowAnnotations annotations = rendererModel.getProcessAnnotations(process);
          if (annotations != null) {
            for (WorkflowAnnotation anno : annotations.getAnnotationsDrawOrder()) {
              // selected is drawn by highlight decorator
              if (anno.equals(model.getSelected())) {
                continue;
              }

              // paint the annotation itself
              Graphics2D g2P = (Graphics2D) g2.create();
              drawer.drawAnnotation(anno, g2P, printing);
              g2P.dispose();
            }
          }
        }
 /**
  * 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);
   }
 }
  /**
   * Calculates the preferred height of the editor pane with the given fixed width.
   *
   * @param width the width of the pane
   * @return the preferred height given the current editor pane content or {@code -1} if there was a
   *     problem. Value will never exceed {@link WorkflowAnnotation#MAX_HEIGHT}
   */
  private int getContentHeightOfEditor(final int width) {
    HTMLDocument document = (HTMLDocument) editPane.getDocument();
    StringWriter writer = new StringWriter();
    try {
      editPane.getEditorKit().write(writer, document, 0, document.getLength());
    } catch (IndexOutOfBoundsException | IOException | BadLocationException e1) {
      // should not happen
      return -1;
    }
    String comment = writer.toString();
    comment = AnnotationDrawUtils.removeStyleFromComment(comment);

    int maxHeight =
        model.getSelected() instanceof ProcessAnnotation
            ? ProcessAnnotation.MAX_HEIGHT
            : OperatorAnnotation.MAX_HEIGHT;
    return Math.min(
        AnnotationDrawUtils.getContentHeight(
            AnnotationDrawUtils.createStyledCommentString(comment, model.getSelected().getStyle()),
            width),
        maxHeight);
  }
  /**
   * 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();
  }
        /** Draws the selected annotation. */
        private void draw(
            final ExecutionUnit process,
            final Graphics2D g2,
            final ProcessRendererModel rendererModel,
            final boolean printing) {
          if (!visualizer.isActive()) {
            return;
          }

          // paint the selected annotation
          WorkflowAnnotation selected = model.getSelected();
          if (selected != null) {
            // only draw in correct execution unit
            if (selected.getProcess().equals(process)) {
              // only paint annotation if not editing
              if (editPane == null) {
                // paint the annotation itself
                Graphics2D g2P = (Graphics2D) g2.create();
                drawer.drawAnnotation(selected, g2P, printing);
                g2P.dispose();
              } else {
                // only paint shadow
                Rectangle2D loc = selected.getLocation();
                g2.draw(
                    new Rectangle2D.Double(
                        loc.getX() - 1,
                        loc.getY() - 1,
                        editPane.getBounds().getWidth() + 1,
                        editPane.getBounds().getHeight() + 1));
                Rectangle2D shadowFrameEditor =
                    new Rectangle2D.Double(
                        loc.getX(),
                        loc.getY(),
                        editPane.getBounds().getWidth() + 1,
                        editPane.getBounds().getHeight() + 1);
                ProcessDrawUtils.drawShadow(shadowFrameEditor, g2);
                if (editPanel != null) {
                  Point absolute = new Point(editPanel.getX(), editPanel.getY());
                  Point relative =
                      ProcessDrawUtils.convertToRelativePoint(
                          absolute, rendererModel.getProcessIndex(process), rendererModel);
                  Rectangle2D shadowFramePanel =
                      new Rectangle2D.Double(
                          relative.getX(), relative.getY(), EDIT_PANEL_WIDTH, EDIT_PANEL_HEIGHT);
                  ProcessDrawUtils.drawShadow(shadowFramePanel, g2);
                }
              }
            }
          }
        }
        /** Draws the annotation for the given operator (if he has one). */
        private void drawOpAnno(
            final Operator operator,
            final Graphics2D g2,
            final ProcessRendererModel rendererModel,
            final boolean printing) {
          WorkflowAnnotations annotations = rendererModel.getOperatorAnnotations(operator);
          if (annotations == null) {
            return;
          }
          for (WorkflowAnnotation anno : annotations.getAnnotationsDrawOrder()) {
            // selected is drawn by highlight decorator
            if (anno.equals(model.getSelected())) {
              continue;
            }

            // paint the annotation itself
            Graphics2D g2P = (Graphics2D) g2.create();
            drawer.drawAnnotation(anno, g2P, printing);
            g2P.dispose();
          }
        }
  /**
   * Saves the content of the comment editor as the new comment for the given {@link
   * WorkflowAnnotation}.
   *
   * @param selected the annotation for which the content of the editor pane should be saved as new
   *     comment
   */
  private void saveEdit(final WorkflowAnnotation selected) {
    if (editPane == null) {
      return;
    }
    HTMLDocument document = (HTMLDocument) editPane.getDocument();
    StringWriter writer = new StringWriter();
    try {
      editPane.getEditorKit().write(writer, document, 0, document.getLength());
    } catch (IndexOutOfBoundsException | IOException | BadLocationException e1) {
      // should not happen
      LogService.getRoot()
          .log(
              Level.WARNING,
              "com.rapidminer.gui.flow.processrendering.annotations.AnnotationsDecorator.cannot_save");
    }
    String comment = writer.toString();
    comment = AnnotationDrawUtils.removeStyleFromComment(comment);
    Rectangle2D loc = selected.getLocation();
    Rectangle2D newLoc =
        new Rectangle2D.Double(
            loc.getX(),
            loc.getY(),
            editPane.getBounds().getWidth(),
            editPane.getBounds().getHeight());
    selected.setLocation(newLoc);

    boolean overflowing = false;
    int prefHeight =
        AnnotationDrawUtils.getContentHeight(
            AnnotationDrawUtils.createStyledCommentString(comment, selected.getStyle()),
            (int) newLoc.getWidth());
    if (prefHeight > newLoc.getHeight()) {
      overflowing = true;
    }
    selected.setOverflowing(overflowing);

    model.setAnnotationComment(selected, comment);
  }
  /**
   * 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();
  }