/** * 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()); }