1 /* 2 * Jalview - A Sequence Alignment Editor and Viewer (2.11.1.4) 3 * Copyright (C) 2021 The Jalview Authors 4 * 5 * This file is part of Jalview. 6 * 7 * Jalview is free software: you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License 9 * as published by the Free Software Foundation, either version 3 10 * of the License, or (at your option) any later version. 11 * 12 * Jalview is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty 14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 15 * PURPOSE. See the GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>. 19 * The Jalview Authors are detailed in the 'AUTHORS' file. 20 */ 21 package jalview.gui; 22 23 import jalview.analysis.AlignSeq; 24 import jalview.analysis.AlignmentUtils; 25 import jalview.datamodel.Alignment; 26 import jalview.datamodel.AlignmentAnnotation; 27 import jalview.datamodel.Annotation; 28 import jalview.datamodel.HiddenColumns; 29 import jalview.datamodel.Sequence; 30 import jalview.datamodel.SequenceGroup; 31 import jalview.datamodel.SequenceI; 32 import jalview.io.FileFormat; 33 import jalview.io.FormatAdapter; 34 import jalview.util.Comparison; 35 import jalview.util.MessageManager; 36 import jalview.util.Platform; 37 38 import java.awt.Color; 39 import java.awt.Cursor; 40 import java.awt.Dimension; 41 import java.awt.Font; 42 import java.awt.FontMetrics; 43 import java.awt.Graphics; 44 import java.awt.Graphics2D; 45 import java.awt.RenderingHints; 46 import java.awt.Toolkit; 47 import java.awt.datatransfer.StringSelection; 48 import java.awt.event.ActionEvent; 49 import java.awt.event.ActionListener; 50 import java.awt.event.MouseEvent; 51 import java.awt.event.MouseListener; 52 import java.awt.event.MouseMotionListener; 53 import java.awt.geom.AffineTransform; 54 import java.util.Arrays; 55 import java.util.Collections; 56 import java.util.Iterator; 57 58 import javax.swing.JCheckBoxMenuItem; 59 import javax.swing.JMenuItem; 60 import javax.swing.JPanel; 61 import javax.swing.JPopupMenu; 62 import javax.swing.SwingUtilities; 63 import javax.swing.ToolTipManager; 64 65 /** 66 * The panel that holds the labels for alignment annotations, providing 67 * tooltips, context menus, drag to reorder rows, and drag to adjust panel 68 * height 69 */ 70 public class AnnotationLabels extends JPanel 71 implements MouseListener, MouseMotionListener, ActionListener 72 { 73 private static final String HTML_END_TAG = "</html>"; 74 75 private static final String HTML_START_TAG = "<html>"; 76 77 /** 78 * width in pixels within which height adjuster arrows are shown and active 79 */ 80 private static final int HEIGHT_ADJUSTER_WIDTH = 50; 81 82 /** 83 * height in pixels for allowing height adjuster to be active 84 */ 85 private static int HEIGHT_ADJUSTER_HEIGHT = 10; 86 87 private static final Font font = new Font("Arial", Font.PLAIN, 11); 88 89 private static final String TOGGLE_LABELSCALE = MessageManager 90 .getString("label.scale_label_to_column"); 91 92 private static final String ADDNEW = MessageManager 93 .getString("label.add_new_row"); 94 95 private static final String EDITNAME = MessageManager 96 .getString("label.edit_label_description"); 97 98 private static final String HIDE = MessageManager 99 .getString("label.hide_row"); 100 101 private static final String DELETE = MessageManager 102 .getString("label.delete_row"); 103 104 private static final String SHOWALL = MessageManager 105 .getString("label.show_all_hidden_rows"); 106 107 private static final String OUTPUT_TEXT = MessageManager 108 .getString("label.export_annotation"); 109 110 private static final String COPYCONS_SEQ = MessageManager 111 .getString("label.copy_consensus_sequence"); 112 113 private final boolean debugRedraw = false; 114 115 private AlignmentPanel ap; 116 117 AlignViewport av; 118 119 private MouseEvent dragEvent; 120 121 private int oldY; 122 123 private int selectedRow; 124 125 private int scrollOffset = 0; 126 127 private boolean hasHiddenRows; 128 129 private boolean resizePanel = false; 130 131 /** 132 * Creates a new AnnotationLabels object 133 * 134 * @param ap 135 */ AnnotationLabels(AlignmentPanel ap)136 public AnnotationLabels(AlignmentPanel ap) 137 { 138 this.ap = ap; 139 av = ap.av; 140 ToolTipManager.sharedInstance().registerComponent(this); 141 142 addMouseListener(this); 143 addMouseMotionListener(this); 144 addMouseWheelListener(ap.getAnnotationPanel()); 145 } 146 AnnotationLabels(AlignViewport av)147 public AnnotationLabels(AlignViewport av) 148 { 149 this.av = av; 150 } 151 152 /** 153 * DOCUMENT ME! 154 * 155 * @param y 156 * DOCUMENT ME! 157 */ setScrollOffset(int y)158 public void setScrollOffset(int y) 159 { 160 scrollOffset = y; 161 repaint(); 162 } 163 164 /** 165 * sets selectedRow to -2 if no annotation preset, -1 if no visible row is at 166 * y 167 * 168 * @param y 169 * coordinate position to search for a row 170 */ getSelectedRow(int y)171 void getSelectedRow(int y) 172 { 173 int height = 0; 174 AlignmentAnnotation[] aa = ap.av.getAlignment() 175 .getAlignmentAnnotation(); 176 selectedRow = -2; 177 if (aa != null) 178 { 179 for (int i = 0; i < aa.length; i++) 180 { 181 selectedRow = -1; 182 if (!aa[i].visible) 183 { 184 continue; 185 } 186 187 height += aa[i].height; 188 189 if (y < height) 190 { 191 selectedRow = i; 192 193 break; 194 } 195 } 196 } 197 } 198 199 /** 200 * DOCUMENT ME! 201 * 202 * @param evt 203 * DOCUMENT ME! 204 */ 205 @Override actionPerformed(ActionEvent evt)206 public void actionPerformed(ActionEvent evt) 207 { 208 AlignmentAnnotation[] aa = ap.av.getAlignment() 209 .getAlignmentAnnotation(); 210 211 boolean fullRepaint = false; 212 if (evt.getActionCommand().equals(ADDNEW)) 213 { 214 AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null, 215 null, new Annotation[ap.av.getAlignment().getWidth()]); 216 217 if (!editLabelDescription(newAnnotation)) 218 { 219 return; 220 } 221 222 ap.av.getAlignment().addAnnotation(newAnnotation); 223 ap.av.getAlignment().setAnnotationIndex(newAnnotation, 0); 224 fullRepaint = true; 225 } 226 else if (evt.getActionCommand().equals(EDITNAME)) 227 { 228 String name = aa[selectedRow].label; 229 editLabelDescription(aa[selectedRow]); 230 if (!name.equalsIgnoreCase(aa[selectedRow].label)) 231 { 232 fullRepaint = true; 233 } 234 } 235 else if (evt.getActionCommand().equals(HIDE)) 236 { 237 aa[selectedRow].visible = false; 238 } 239 else if (evt.getActionCommand().equals(DELETE)) 240 { 241 ap.av.getAlignment().deleteAnnotation(aa[selectedRow]); 242 ap.av.getCalcManager().removeWorkerForAnnotation(aa[selectedRow]); 243 fullRepaint = true; 244 } 245 else if (evt.getActionCommand().equals(SHOWALL)) 246 { 247 for (int i = 0; i < aa.length; i++) 248 { 249 if (!aa[i].visible && aa[i].annotations != null) 250 { 251 aa[i].visible = true; 252 } 253 } 254 fullRepaint = true; 255 } 256 else if (evt.getActionCommand().equals(OUTPUT_TEXT)) 257 { 258 new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]); 259 } 260 else if (evt.getActionCommand().equals(COPYCONS_SEQ)) 261 { 262 SequenceI cons = null; 263 if (aa[selectedRow].groupRef != null) 264 { 265 cons = aa[selectedRow].groupRef.getConsensusSeq(); 266 } 267 else 268 { 269 cons = av.getConsensusSeq(); 270 } 271 if (cons != null) 272 { 273 copy_annotseqtoclipboard(cons); 274 } 275 276 } 277 else if (evt.getActionCommand().equals(TOGGLE_LABELSCALE)) 278 { 279 aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel; 280 } 281 282 ap.refresh(fullRepaint); 283 284 } 285 286 /** 287 * DOCUMENT ME! 288 * 289 * @param e 290 * DOCUMENT ME! 291 */ editLabelDescription(AlignmentAnnotation annotation)292 boolean editLabelDescription(AlignmentAnnotation annotation) 293 { 294 // TODO i18n 295 EditNameDialog dialog = new EditNameDialog(annotation.label, 296 annotation.description, " Annotation Name ", 297 "Annotation Description ", "Edit Annotation Name/Description", 298 ap.alignFrame); 299 300 if (!dialog.accept) 301 { 302 return false; 303 } 304 305 annotation.label = dialog.getName(); 306 307 String text = dialog.getDescription(); 308 if (text != null && text.length() == 0) 309 { 310 text = null; 311 } 312 annotation.description = text; 313 314 return true; 315 } 316 317 @Override mousePressed(MouseEvent evt)318 public void mousePressed(MouseEvent evt) 319 { 320 getSelectedRow(evt.getY() - getScrollOffset()); 321 oldY = evt.getY(); 322 if (evt.isPopupTrigger()) 323 { 324 showPopupMenu(evt); 325 } 326 } 327 328 /** 329 * Build and show the Pop-up menu at the right-click mouse position 330 * 331 * @param evt 332 */ showPopupMenu(MouseEvent evt)333 void showPopupMenu(MouseEvent evt) 334 { 335 evt.consume(); 336 final AlignmentAnnotation[] aa = ap.av.getAlignment() 337 .getAlignmentAnnotation(); 338 339 JPopupMenu pop = new JPopupMenu( 340 MessageManager.getString("label.annotations")); 341 JMenuItem item = new JMenuItem(ADDNEW); 342 item.addActionListener(this); 343 pop.add(item); 344 if (selectedRow < 0) 345 { 346 if (hasHiddenRows) 347 { // let the user make everything visible again 348 item = new JMenuItem(SHOWALL); 349 item.addActionListener(this); 350 pop.add(item); 351 } 352 pop.show(this, evt.getX(), evt.getY()); 353 return; 354 } 355 item = new JMenuItem(EDITNAME); 356 item.addActionListener(this); 357 pop.add(item); 358 item = new JMenuItem(HIDE); 359 item.addActionListener(this); 360 pop.add(item); 361 // JAL-1264 hide all sequence-specific annotations of this type 362 if (selectedRow < aa.length) 363 { 364 if (aa[selectedRow].sequenceRef != null) 365 { 366 final String label = aa[selectedRow].label; 367 JMenuItem hideType = new JMenuItem(); 368 String text = MessageManager.getString("label.hide_all") + " " 369 + label; 370 hideType.setText(text); 371 hideType.addActionListener(new ActionListener() 372 { 373 @Override 374 public void actionPerformed(ActionEvent e) 375 { 376 AlignmentUtils.showOrHideSequenceAnnotations( 377 ap.av.getAlignment(), Collections.singleton(label), 378 null, false, false); 379 ap.refresh(true); 380 } 381 }); 382 pop.add(hideType); 383 } 384 } 385 item = new JMenuItem(DELETE); 386 item.addActionListener(this); 387 pop.add(item); 388 if (hasHiddenRows) 389 { 390 item = new JMenuItem(SHOWALL); 391 item.addActionListener(this); 392 pop.add(item); 393 } 394 item = new JMenuItem(OUTPUT_TEXT); 395 item.addActionListener(this); 396 pop.add(item); 397 // TODO: annotation object should be typed for autocalculated/derived 398 // property methods 399 if (selectedRow < aa.length) 400 { 401 final String label = aa[selectedRow].label; 402 if (!aa[selectedRow].autoCalculated) 403 { 404 if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH) 405 { 406 // display formatting settings for this row. 407 pop.addSeparator(); 408 // av and sequencegroup need to implement same interface for 409 item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE, 410 aa[selectedRow].scaleColLabel); 411 item.addActionListener(this); 412 pop.add(item); 413 } 414 } 415 else if (label.indexOf("Consensus") > -1) 416 { 417 addConsensusMenuOptions(ap, aa[selectedRow], pop); 418 419 final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ); 420 consclipbrd.addActionListener(this); 421 pop.add(consclipbrd); 422 } 423 } 424 pop.show(this, evt.getX(), evt.getY()); 425 } 426 427 /** 428 * A helper method that adds menu options for calculation and visualisation of 429 * group and/or alignment consensus annotation to a popup menu. This is 430 * designed to be reusable for either unwrapped mode (popup menu is shown on 431 * component AnnotationLabels), or wrapped mode (popup menu is shown on 432 * IdPanel when the mouse is over an annotation label). 433 * 434 * @param ap 435 * @param ann 436 * @param pop 437 */ addConsensusMenuOptions(AlignmentPanel ap, AlignmentAnnotation ann, JPopupMenu pop)438 static void addConsensusMenuOptions(AlignmentPanel ap, 439 AlignmentAnnotation ann, 440 JPopupMenu pop) 441 { 442 pop.addSeparator(); 443 444 final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem( 445 MessageManager.getString("label.ignore_gaps_consensus"), 446 (ann.groupRef != null) ? ann.groupRef.getIgnoreGapsConsensus() 447 : ap.av.isIgnoreGapsConsensus()); 448 final AlignmentAnnotation aaa = ann; 449 cbmi.addActionListener(new ActionListener() 450 { 451 @Override 452 public void actionPerformed(ActionEvent e) 453 { 454 if (aaa.groupRef != null) 455 { 456 aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState()); 457 ap.getAnnotationPanel() 458 .paint(ap.getAnnotationPanel().getGraphics()); 459 } 460 else 461 { 462 ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap); 463 } 464 ap.alignmentChanged(); 465 } 466 }); 467 pop.add(cbmi); 468 469 if (aaa.groupRef != null) 470 { 471 /* 472 * group consensus options 473 */ 474 final JCheckBoxMenuItem chist = new JCheckBoxMenuItem( 475 MessageManager.getString("label.show_group_histogram"), 476 ann.groupRef.isShowConsensusHistogram()); 477 chist.addActionListener(new ActionListener() 478 { 479 @Override 480 public void actionPerformed(ActionEvent e) 481 { 482 aaa.groupRef.setShowConsensusHistogram(chist.getState()); 483 ap.repaint(); 484 } 485 }); 486 pop.add(chist); 487 final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem( 488 MessageManager.getString("label.show_group_logo"), 489 ann.groupRef.isShowSequenceLogo()); 490 cprofl.addActionListener(new ActionListener() 491 { 492 @Override 493 public void actionPerformed(ActionEvent e) 494 { 495 aaa.groupRef.setshowSequenceLogo(cprofl.getState()); 496 ap.repaint(); 497 } 498 }); 499 pop.add(cprofl); 500 final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem( 501 MessageManager.getString("label.normalise_group_logo"), 502 ann.groupRef.isNormaliseSequenceLogo()); 503 cproflnorm.addActionListener(new ActionListener() 504 { 505 @Override 506 public void actionPerformed(ActionEvent e) 507 { 508 aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState()); 509 // automatically enable logo display if we're clicked 510 aaa.groupRef.setshowSequenceLogo(true); 511 ap.repaint(); 512 } 513 }); 514 pop.add(cproflnorm); 515 } 516 else 517 { 518 /* 519 * alignment consensus options 520 */ 521 final JCheckBoxMenuItem chist = new JCheckBoxMenuItem( 522 MessageManager.getString("label.show_histogram"), 523 ap.av.isShowConsensusHistogram()); 524 chist.addActionListener(new ActionListener() 525 { 526 @Override 527 public void actionPerformed(ActionEvent e) 528 { 529 ap.av.setShowConsensusHistogram(chist.getState()); 530 ap.alignFrame.setMenusForViewport(); 531 ap.repaint(); 532 } 533 }); 534 pop.add(chist); 535 final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem( 536 MessageManager.getString("label.show_logo"), 537 ap.av.isShowSequenceLogo()); 538 cprof.addActionListener(new ActionListener() 539 { 540 @Override 541 public void actionPerformed(ActionEvent e) 542 { 543 ap.av.setShowSequenceLogo(cprof.getState()); 544 ap.alignFrame.setMenusForViewport(); 545 ap.repaint(); 546 } 547 }); 548 pop.add(cprof); 549 final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem( 550 MessageManager.getString("label.normalise_logo"), 551 ap.av.isNormaliseSequenceLogo()); 552 cprofnorm.addActionListener(new ActionListener() 553 { 554 @Override 555 public void actionPerformed(ActionEvent e) 556 { 557 ap.av.setShowSequenceLogo(true); 558 ap.av.setNormaliseSequenceLogo(cprofnorm.getState()); 559 ap.alignFrame.setMenusForViewport(); 560 ap.repaint(); 561 } 562 }); 563 pop.add(cprofnorm); 564 } 565 } 566 567 /** 568 * Reorders annotation rows after a drag of a label 569 * 570 * @param evt 571 */ 572 @Override mouseReleased(MouseEvent evt)573 public void mouseReleased(MouseEvent evt) 574 { 575 if (evt.isPopupTrigger()) 576 { 577 showPopupMenu(evt); 578 return; 579 } 580 581 int start = selectedRow; 582 getSelectedRow(evt.getY() - getScrollOffset()); 583 int end = selectedRow; 584 585 /* 586 * if dragging to resize instead, start == end 587 */ 588 if (start != end) 589 { 590 // Swap these annotations 591 AlignmentAnnotation startAA = ap.av.getAlignment() 592 .getAlignmentAnnotation()[start]; 593 if (end == -1) 594 { 595 end = ap.av.getAlignment().getAlignmentAnnotation().length - 1; 596 } 597 AlignmentAnnotation endAA = ap.av.getAlignment() 598 .getAlignmentAnnotation()[end]; 599 600 ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA; 601 ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA; 602 } 603 604 resizePanel = false; 605 dragEvent = null; 606 repaint(); 607 ap.getAnnotationPanel().repaint(); 608 } 609 610 /** 611 * Removes the height adjuster image on leaving the panel, unless currently 612 * dragging it 613 */ 614 @Override mouseExited(MouseEvent evt)615 public void mouseExited(MouseEvent evt) 616 { 617 if (resizePanel && dragEvent == null) 618 { 619 resizePanel = false; 620 repaint(); 621 } 622 } 623 624 /** 625 * A mouse drag may be either an adjustment of the panel height (if flag 626 * resizePanel is set on), or a reordering of the annotation rows. The former 627 * is dealt with by this method, the latter in mouseReleased. 628 * 629 * @param evt 630 */ 631 @Override mouseDragged(MouseEvent evt)632 public void mouseDragged(MouseEvent evt) 633 { 634 dragEvent = evt; 635 636 if (resizePanel) 637 { 638 Dimension d = ap.annotationScroller.getPreferredSize(); 639 int dif = evt.getY() - oldY; 640 641 dif /= ap.av.getCharHeight(); 642 dif *= ap.av.getCharHeight(); 643 644 if ((d.height - dif) > 20) 645 { 646 ap.annotationScroller 647 .setPreferredSize(new Dimension(d.width, d.height - dif)); 648 d = ap.annotationSpaceFillerHolder.getPreferredSize(); 649 ap.annotationSpaceFillerHolder 650 .setPreferredSize(new Dimension(d.width, d.height - dif)); 651 ap.paintAlignment(true, false); 652 } 653 654 ap.addNotify(); 655 } 656 else 657 { 658 repaint(); 659 } 660 } 661 662 /** 663 * Updates the tooltip as the mouse moves over the labels 664 * 665 * @param evt 666 */ 667 @Override mouseMoved(MouseEvent evt)668 public void mouseMoved(MouseEvent evt) 669 { 670 showOrHideAdjuster(evt); 671 672 getSelectedRow(evt.getY() - getScrollOffset()); 673 674 if (selectedRow > -1 && ap.av.getAlignment() 675 .getAlignmentAnnotation().length > selectedRow) 676 { 677 AlignmentAnnotation[] anns = ap.av.getAlignment() 678 .getAlignmentAnnotation(); 679 AlignmentAnnotation aa = anns[selectedRow]; 680 681 String desc = getTooltip(aa); 682 this.setToolTipText(desc); 683 String msg = getStatusMessage(aa, anns); 684 ap.alignFrame.setStatus(msg); 685 } 686 } 687 688 /** 689 * Constructs suitable text to show in the status bar when over an annotation 690 * label, containing the associated sequence name (if any), and the annotation 691 * labels (or all labels for a graph group annotation) 692 * 693 * @param aa 694 * @param anns 695 * @return 696 */ getStatusMessage(AlignmentAnnotation aa, AlignmentAnnotation[] anns)697 static String getStatusMessage(AlignmentAnnotation aa, 698 AlignmentAnnotation[] anns) 699 { 700 if (aa == null) 701 { 702 return null; 703 } 704 705 StringBuilder msg = new StringBuilder(32); 706 if (aa.sequenceRef != null) 707 { 708 msg.append(aa.sequenceRef.getName()).append(" : "); 709 } 710 711 if (aa.graphGroup == -1) 712 { 713 msg.append(aa.label); 714 } 715 else if (anns != null) 716 { 717 boolean first = true; 718 for (int i = anns.length - 1; i >= 0; i--) 719 { 720 if (anns[i].graphGroup == aa.graphGroup) 721 { 722 if (!first) 723 { 724 msg.append(", "); 725 } 726 msg.append(anns[i].label); 727 first = false; 728 } 729 } 730 } 731 732 return msg.toString(); 733 } 734 735 /** 736 * Answers a tooltip, formatted as html, containing the annotation description 737 * (prefixed by associated sequence id if applicable), and the annotation 738 * (non-positional) score if it has one. Answers null if neither description 739 * nor score is found. 740 * 741 * @param aa 742 * @return 743 */ getTooltip(AlignmentAnnotation aa)744 static String getTooltip(AlignmentAnnotation aa) 745 { 746 if (aa == null) 747 { 748 return null; 749 } 750 StringBuilder tooltip = new StringBuilder(); 751 if (aa.description != null && !aa.description.equals("New description")) 752 { 753 // TODO: we could refactor and merge this code with the code in 754 // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature 755 // tooltips 756 String desc = aa.getDescription(true).trim(); 757 if (!desc.toLowerCase().startsWith(HTML_START_TAG)) 758 { 759 tooltip.append(HTML_START_TAG); 760 desc = desc.replace("<", "<"); 761 } 762 else if (desc.toLowerCase().endsWith(HTML_END_TAG)) 763 { 764 desc = desc.substring(0, desc.length() - HTML_END_TAG.length()); 765 } 766 tooltip.append(desc); 767 } 768 else 769 { 770 // begin the tooltip's html fragment 771 tooltip.append(HTML_START_TAG); 772 } 773 if (aa.hasScore()) 774 { 775 if (tooltip.length() > HTML_START_TAG.length()) 776 { 777 tooltip.append("<br/>"); 778 } 779 // TODO: limit precision of score to avoid noise from imprecise 780 // doubles 781 // (64.7 becomes 64.7+/some tiny value). 782 tooltip.append(" Score: ").append(String.valueOf(aa.score)); 783 } 784 785 if (tooltip.length() > HTML_START_TAG.length()) 786 { 787 return tooltip.append(HTML_END_TAG).toString(); 788 } 789 790 /* 791 * nothing in the tooltip (except "<html>") 792 */ 793 return null; 794 } 795 796 /** 797 * Shows the height adjuster image if the mouse moves into the top left 798 * region, or hides it if the mouse leaves the regio 799 * 800 * @param evt 801 */ showOrHideAdjuster(MouseEvent evt)802 protected void showOrHideAdjuster(MouseEvent evt) 803 { 804 boolean was = resizePanel; 805 resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH; 806 807 if (resizePanel != was) 808 { 809 setCursor(Cursor.getPredefinedCursor( 810 resizePanel ? Cursor.S_RESIZE_CURSOR 811 : Cursor.DEFAULT_CURSOR)); 812 repaint(); 813 } 814 } 815 816 @Override 817 public void mouseClicked(MouseEvent evt) 818 { 819 final AlignmentAnnotation[] aa = ap.av.getAlignment() 820 .getAlignmentAnnotation(); 821 if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt)) 822 { 823 if (selectedRow > -1 && selectedRow < aa.length) 824 { 825 if (aa[selectedRow].groupRef != null) 826 { 827 if (evt.getClickCount() >= 2) 828 { 829 // todo: make the ap scroll to the selection - not necessary, first 830 // click highlights/scrolls, second selects 831 ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null); 832 // process modifiers 833 SequenceGroup sg = ap.av.getSelectionGroup(); 834 if (sg == null || sg == aa[selectedRow].groupRef 835 || !(Platform.isControlDown(evt) || evt.isShiftDown())) 836 { 837 if (Platform.isControlDown(evt) || evt.isShiftDown()) 838 { 839 // clone a new selection group from the associated group 840 ap.av.setSelectionGroup( 841 new SequenceGroup(aa[selectedRow].groupRef)); 842 } 843 else 844 { 845 // set selection to the associated group so it can be edited 846 ap.av.setSelectionGroup(aa[selectedRow].groupRef); 847 } 848 } 849 else 850 { 851 // modify current selection with associated group 852 int remainToAdd = aa[selectedRow].groupRef.getSize(); 853 for (SequenceI sgs : aa[selectedRow].groupRef.getSequences()) 854 { 855 if (jalview.util.Platform.isControlDown(evt)) 856 { 857 sg.addOrRemove(sgs, --remainToAdd == 0); 858 } 859 else 860 { 861 // notionally, we should also add intermediate sequences from 862 // last added sequence ? 863 sg.addSequence(sgs, --remainToAdd == 0); 864 } 865 } 866 } 867 868 ap.paintAlignment(false, false); 869 PaintRefresher.Refresh(ap, ap.av.getSequenceSetId()); 870 ap.av.sendSelection(); 871 } 872 else 873 { 874 ap.getSeqPanel().ap.getIdPanel().highlightSearchResults( 875 aa[selectedRow].groupRef.getSequences(null)); 876 } 877 return; 878 } 879 else if (aa[selectedRow].sequenceRef != null) 880 { 881 if (evt.getClickCount() == 1) 882 { 883 ap.getSeqPanel().ap.getIdPanel() 884 .highlightSearchResults(Arrays.asList(new SequenceI[] 885 { aa[selectedRow].sequenceRef })); 886 } 887 else if (evt.getClickCount() >= 2) 888 { 889 ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null); 890 SequenceGroup sg = ap.av.getSelectionGroup(); 891 if (sg != null) 892 { 893 // we make a copy rather than edit the current selection if no 894 // modifiers pressed 895 // see Enhancement JAL-1557 896 if (!(Platform.isControlDown(evt) || evt.isShiftDown())) 897 { 898 sg = new SequenceGroup(sg); 899 sg.clear(); 900 sg.addSequence(aa[selectedRow].sequenceRef, false); 901 } 902 else 903 { 904 if (Platform.isControlDown(evt)) 905 { 906 sg.addOrRemove(aa[selectedRow].sequenceRef, true); 907 } 908 else 909 { 910 // notionally, we should also add intermediate sequences from 911 // last added sequence ? 912 sg.addSequence(aa[selectedRow].sequenceRef, true); 913 } 914 } 915 } 916 else 917 { 918 sg = new SequenceGroup(); 919 sg.setStartRes(0); 920 sg.setEndRes(ap.av.getAlignment().getWidth() - 1); 921 sg.addSequence(aa[selectedRow].sequenceRef, false); 922 } 923 ap.av.setSelectionGroup(sg); 924 ap.paintAlignment(false, false); 925 PaintRefresher.Refresh(ap, ap.av.getSequenceSetId()); 926 ap.av.sendSelection(); 927 } 928 929 } 930 } 931 return; 932 } 933 } 934 935 /** 936 * do a single sequence copy to jalview and the system clipboard 937 * 938 * @param sq 939 * sequence to be copied to clipboard 940 */ copy_annotseqtoclipboard(SequenceI sq)941 protected void copy_annotseqtoclipboard(SequenceI sq) 942 { 943 SequenceI[] seqs = new SequenceI[] { sq }; 944 String[] omitHidden = null; 945 SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() }; 946 if (dseqs[0] == null) 947 { 948 dseqs[0] = new Sequence(sq); 949 dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars, 950 sq.getSequenceAsString())); 951 952 sq.setDatasetSequence(dseqs[0]); 953 } 954 Alignment ds = new Alignment(dseqs); 955 if (av.hasHiddenColumns()) 956 { 957 Iterator<int[]> it = av.getAlignment().getHiddenColumns() 958 .getVisContigsIterator(0, sq.getLength(), false); 959 omitHidden = new String[] { sq.getSequenceStringFromIterator(it) }; 960 } 961 962 int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 }; 963 if (av.hasHiddenColumns()) 964 { 965 alignmentStartEnd = av.getAlignment().getHiddenColumns() 966 .getVisibleStartAndEndIndex(av.getAlignment().getWidth()); 967 } 968 969 String output = new FormatAdapter().formatSequences(FileFormat.Fasta, 970 seqs, omitHidden, alignmentStartEnd); 971 972 Toolkit.getDefaultToolkit().getSystemClipboard() 973 .setContents(new StringSelection(output), Desktop.instance); 974 975 HiddenColumns hiddenColumns = null; 976 977 if (av.hasHiddenColumns()) 978 { 979 hiddenColumns = new HiddenColumns( 980 av.getAlignment().getHiddenColumns()); 981 } 982 983 Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset 984 // of a consensus 985 // sequence ? need to 986 // flag 987 // sequence as special. 988 hiddenColumns }; 989 } 990 991 /** 992 * DOCUMENT ME! 993 * 994 * @param g1 995 * DOCUMENT ME! 996 */ 997 @Override paintComponent(Graphics g)998 public void paintComponent(Graphics g) 999 { 1000 1001 int width = getWidth(); 1002 if (width == 0) 1003 { 1004 width = ap.calculateIdWidth().width; 1005 } 1006 1007 Graphics2D g2 = (Graphics2D) g; 1008 if (av.antiAlias) 1009 { 1010 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1011 RenderingHints.VALUE_ANTIALIAS_ON); 1012 } 1013 1014 drawComponent(g2, true, width); 1015 1016 } 1017 1018 /** 1019 * Draw the full set of annotation Labels for the alignment at the given 1020 * cursor 1021 * 1022 * @param g 1023 * Graphics2D instance (needed for font scaling) 1024 * @param width 1025 * Width for scaling labels 1026 * 1027 */ drawComponent(Graphics g, int width)1028 public void drawComponent(Graphics g, int width) 1029 { 1030 drawComponent(g, false, width); 1031 } 1032 1033 /** 1034 * Draw the full set of annotation Labels for the alignment at the given 1035 * cursor 1036 * 1037 * @param g 1038 * Graphics2D instance (needed for font scaling) 1039 * @param clip 1040 * - true indicates that only current visible area needs to be 1041 * rendered 1042 * @param width 1043 * Width for scaling labels 1044 */ drawComponent(Graphics g, boolean clip, int width)1045 public void drawComponent(Graphics g, boolean clip, int width) 1046 { 1047 if (av.getFont().getSize() < 10) 1048 { 1049 g.setFont(font); 1050 } 1051 else 1052 { 1053 g.setFont(av.getFont()); 1054 } 1055 1056 FontMetrics fm = g.getFontMetrics(g.getFont()); 1057 g.setColor(Color.white); 1058 g.fillRect(0, 0, getWidth(), getHeight()); 1059 1060 g.translate(0, getScrollOffset()); 1061 g.setColor(Color.black); 1062 1063 AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation(); 1064 int fontHeight = g.getFont().getSize(); 1065 int y = 0; 1066 int x = 0; 1067 int graphExtras = 0; 1068 int offset = 0; 1069 Font baseFont = g.getFont(); 1070 FontMetrics baseMetrics = fm; 1071 int ofontH = fontHeight; 1072 int sOffset = 0; 1073 int visHeight = 0; 1074 int[] visr = (ap != null && ap.getAnnotationPanel() != null) 1075 ? ap.getAnnotationPanel().getVisibleVRange() 1076 : null; 1077 if (clip && visr != null) 1078 { 1079 sOffset = visr[0]; 1080 visHeight = visr[1]; 1081 } 1082 boolean visible = true, before = false, after = false; 1083 if (aa != null) 1084 { 1085 hasHiddenRows = false; 1086 int olY = 0; 1087 for (int i = 0; i < aa.length; i++) 1088 { 1089 visible = true; 1090 if (!aa[i].visible) 1091 { 1092 hasHiddenRows = true; 1093 continue; 1094 } 1095 olY = y; 1096 y += aa[i].height; 1097 if (clip) 1098 { 1099 if (y < sOffset) 1100 { 1101 if (!before) 1102 { 1103 if (debugRedraw) 1104 { 1105 System.out.println("before vis: " + i); 1106 } 1107 before = true; 1108 } 1109 // don't draw what isn't visible 1110 continue; 1111 } 1112 if (olY > visHeight) 1113 { 1114 1115 if (!after) 1116 { 1117 if (debugRedraw) 1118 { 1119 System.out.println( 1120 "Scroll offset: " + sOffset + " after vis: " + i); 1121 } 1122 after = true; 1123 } 1124 // don't draw what isn't visible 1125 continue; 1126 } 1127 } 1128 g.setColor(Color.black); 1129 1130 offset = -aa[i].height / 2; 1131 1132 if (aa[i].hasText) 1133 { 1134 offset += fm.getHeight() / 2; 1135 offset -= fm.getDescent(); 1136 } 1137 else 1138 { 1139 offset += fm.getDescent(); 1140 } 1141 1142 x = width - fm.stringWidth(aa[i].label) - 3; 1143 1144 if (aa[i].graphGroup > -1) 1145 { 1146 int groupSize = 0; 1147 // TODO: JAL-1291 revise rendering model so the graphGroup map is 1148 // computed efficiently for all visible labels 1149 for (int gg = 0; gg < aa.length; gg++) 1150 { 1151 if (aa[gg].graphGroup == aa[i].graphGroup) 1152 { 1153 groupSize++; 1154 } 1155 } 1156 if (groupSize * (fontHeight + 8) < aa[i].height) 1157 { 1158 graphExtras = (aa[i].height - (groupSize * (fontHeight + 8))) 1159 / 2; 1160 } 1161 else 1162 { 1163 // scale font to fit 1164 float h = aa[i].height / (float) groupSize, s; 1165 if (h < 9) 1166 { 1167 visible = false; 1168 } 1169 else 1170 { 1171 fontHeight = -8 + (int) h; 1172 s = ((float) fontHeight) / (float) ofontH; 1173 Font f = baseFont 1174 .deriveFont(AffineTransform.getScaleInstance(s, s)); 1175 g.setFont(f); 1176 fm = g.getFontMetrics(); 1177 graphExtras = (aa[i].height - (groupSize * (fontHeight + 8))) 1178 / 2; 1179 } 1180 } 1181 if (visible) 1182 { 1183 for (int gg = 0; gg < aa.length; gg++) 1184 { 1185 if (aa[gg].graphGroup == aa[i].graphGroup) 1186 { 1187 x = width - fm.stringWidth(aa[gg].label) - 3; 1188 g.drawString(aa[gg].label, x, y - graphExtras); 1189 1190 if (aa[gg]._linecolour != null) 1191 { 1192 1193 g.setColor(aa[gg]._linecolour); 1194 g.drawLine(x, y - graphExtras + 3, 1195 x + fm.stringWidth(aa[gg].label), 1196 y - graphExtras + 3); 1197 } 1198 1199 g.setColor(Color.black); 1200 graphExtras += fontHeight + 8; 1201 } 1202 } 1203 } 1204 g.setFont(baseFont); 1205 fm = baseMetrics; 1206 fontHeight = ofontH; 1207 } 1208 else 1209 { 1210 g.drawString(aa[i].label, x, y + offset); 1211 } 1212 } 1213 } 1214 1215 if (!resizePanel && dragEvent != null && aa != null) 1216 { 1217 g.setColor(Color.lightGray); 1218 g.drawString(aa[selectedRow].label, dragEvent.getX(), 1219 dragEvent.getY() - getScrollOffset()); 1220 } 1221 1222 if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1))) 1223 { 1224 g.drawString(MessageManager.getString("label.right_click"), 2, 8); 1225 g.drawString(MessageManager.getString("label.to_add_annotation"), 2, 1226 18); 1227 } 1228 } 1229 getScrollOffset()1230 public int getScrollOffset() 1231 { 1232 return scrollOffset; 1233 } 1234 1235 @Override mouseEntered(MouseEvent e)1236 public void mouseEntered(MouseEvent e) 1237 { 1238 } 1239 } 1240