X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fgui%2FSeqPanel.java;h=8a49092db31eaf9e943868b9f0f80a01333df189;hb=00816d26e65bf0c98538773087a8c7519aea672e;hp=a53edb9203a4f55080fb11989713db3c24314fc2;hpb=f484ffbf14ae725e55c80396be1889708988fd8b;p=jalview.git diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index a53edb9..8a49092 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -20,6 +20,29 @@ */ package jalview.gui; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JToolTip; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.ToolTipManager; + import jalview.api.AlignViewportI; import jalview.bin.Cache; import jalview.commands.EditCommand; @@ -29,6 +52,7 @@ import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResults; import jalview.datamodel.SearchResultsI; @@ -49,23 +73,8 @@ import jalview.util.MappingUtils; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.viewmodel.AlignmentViewport; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Point; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.util.Collections; -import java.util.List; - -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import javax.swing.ToolTipManager; +import jalview.viewmodel.ViewportRanges; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; /** * DOCUMENT ME! @@ -164,9 +173,9 @@ public class SeqPanel extends JPanel */ private MousePos lastMousePosition; - protected int lastres; + protected int editLastRes; - protected int startseq; + protected int editStartSeq; protected AlignViewport av; @@ -203,13 +212,21 @@ public class SeqPanel extends JPanel StringBuffer keyboardNo2; - java.net.URL linkImageURL; - private final SequenceAnnotationReport seqARep; - StringBuilder tooltipText = new StringBuilder(); + /* + * the last tooltip on mousing over the alignment (or annotation in wrapped mode) + * - the tooltip is not set again if unchanged + * - this is the tooltip text _before_ formatting as html + */ + private String lastTooltip; - String tmpString; + /* + * the last tooltip on mousing over the alignment (or annotation in wrapped mode) + * - used to decide where to place the tooltip in getTooltipLocation() + * - this is the tooltip text _after_ formatting as html + */ + private String lastFormattedTooltip; EditCommand editCommand; @@ -225,11 +242,12 @@ public class SeqPanel extends JPanel */ public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel) { - linkImageURL = getClass().getResource("/images/link.gif"); - seqARep = new SequenceAnnotationReport(linkImageURL.toString()); + seqARep = new SequenceAnnotationReport(true); ToolTipManager.sharedInstance().registerComponent(this); ToolTipManager.sharedInstance().setInitialDelay(0); ToolTipManager.sharedInstance().setDismissDelay(10000); + + this.av = viewport; setBackground(Color.white); @@ -313,8 +331,10 @@ public class SeqPanel extends JPanel } else { - seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(), + ViewportRanges ranges = av.getRanges(); + seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(), alignmentHeight - 1); + seqIndex = Math.min(seqIndex, ranges.getEndSeq()); } return new MousePos(col, seqIndex, annIndex); @@ -411,8 +431,8 @@ public class SeqPanel extends JPanel /* * Tidy up come what may... */ - startseq = -1; - lastres = -1; + editStartSeq = -1; + editLastRes = -1; editingSeqs = false; groupEditing = false; keyboardNo1 = null; @@ -640,8 +660,8 @@ public class SeqPanel extends JPanel void insertGapAtCursor(boolean group) { groupEditing = group; - startseq = seqCanvas.cursorY; - lastres = seqCanvas.cursorX; + editStartSeq = seqCanvas.cursorY; + editLastRes = seqCanvas.cursorX; editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1()); endEditing(); } @@ -649,8 +669,8 @@ public class SeqPanel extends JPanel void deleteGapAtCursor(boolean group) { groupEditing = group; - startseq = seqCanvas.cursorY; - lastres = seqCanvas.cursorX + getKeyboardNo1(); + editStartSeq = seqCanvas.cursorY; + editLastRes = seqCanvas.cursorX + getKeyboardNo1(); editSequence(false, false, seqCanvas.cursorX); endEditing(); } @@ -659,8 +679,8 @@ public class SeqPanel extends JPanel { // TODO not called - delete? groupEditing = group; - startseq = seqCanvas.cursorY; - lastres = seqCanvas.cursorX; + editStartSeq = seqCanvas.cursorY; + editLastRes = seqCanvas.cursorX; editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1()); endEditing(); } @@ -742,13 +762,14 @@ public class SeqPanel extends JPanel return; } - if (!editingSeqs) + if (editingSeqs) + { + endEditing(); + } + else { doMouseReleasedDefineMode(evt, didDrag); - return; } - - endEditing(); } /** @@ -794,13 +815,13 @@ public class SeqPanel extends JPanel if ((seq < av.getAlignment().getHeight()) && (res < av.getAlignment().getSequenceAt(seq).getLength())) { - startseq = seq; - lastres = res; + editStartSeq = seq; + editLastRes = res; } else { - startseq = -1; - lastres = -1; + editStartSeq = -1; + editLastRes = -1; } return; @@ -828,11 +849,11 @@ public class SeqPanel extends JPanel * the start of the highlighted region. */ @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (results == null || results.equals(lastSearchResults)) { - return; + return null; } lastSearchResults = results; @@ -845,7 +866,7 @@ public class SeqPanel extends JPanel // over residue to change abruptly, causing highlighted residue in panel 2 // to change, causing a scroll in panel 1 etc) ap.setToScrollComplementPanel(false); - wasScrolled = ap.scrollToPosition(results, false); + wasScrolled = ap.scrollToPosition(results); if (wasScrolled) { seqCanvas.revalidate(); @@ -853,11 +874,82 @@ public class SeqPanel extends JPanel ap.setToScrollComplementPanel(true); } - boolean noFastPaint = wasScrolled && av.getWrapAlignment(); - if (seqCanvas.highlightSearchResults(results, noFastPaint)) + boolean fastPaint = !(wasScrolled && av.getWrapAlignment()); + if (seqCanvas.highlightSearchResults(results, fastPaint)) { setStatusMessage(results); } + return results.isEmpty() ? null : getHighlightInfo(results); + } + + /** + * temporary hack: answers a message suitable to show on structure hover + * label. This is normally null. It is a peptide variation description if + * + * in which case the answer is of the format (e.g.) "p.Glu388Asp" + * + * @param results + * @return + */ + private String getHighlightInfo(SearchResultsI results) + { + /* + * ideally, just find mapped CDS (as we don't care about render style here); + * for now, go via split frame complement's FeatureRenderer + */ + AlignViewportI complement = ap.getAlignViewport().getCodingComplement(); + if (complement == null) + { + return null; + } + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + + int j = results.getSize(); + List infos = new ArrayList<>(); + for (int i = 0; i < j; i++) + { + SearchResultMatchI match = results.getResults().get(i); + int pos = match.getStart(); + if (pos == match.getEnd()) + { + SequenceI seq = match.getSequence(); + SequenceI ds = seq.getDatasetSequence() == null ? seq + : seq.getDatasetSequence(); + MappedFeatures mf = fr2 + .findComplementFeaturesAtResidue(ds, pos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + String pv = mf.findProteinVariants(sf); + if (pv.length() > 0 && !infos.contains(pv)) + { + infos.add(pv); + } + } + } + } + } + + if (infos.isEmpty()) + { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String info : infos) + { + if (sb.length() > 0) + { + sb.append("|"); + } + sb.append(info); + } + return sb.toString(); } @Override @@ -897,8 +989,10 @@ public class SeqPanel extends JPanel /* * just a pixel move without change of 'cell' */ + moveTooltip = false; return; } + moveTooltip = true; lastMousePosition = mousePos; if (mousePos.isOverAnnotation()) @@ -914,6 +1008,7 @@ public class SeqPanel extends JPanel lastMousePosition = null; setToolTipText(null); lastTooltip = null; + lastFormattedTooltip = null; ap.alignFrame.setStatus(""); return; } @@ -935,7 +1030,7 @@ public class SeqPanel extends JPanel mouseOverSequence(sequence, column, pos); } - tooltipText.setLength(6); // Cuts the buffer back to + StringBuilder tooltipText = new StringBuilder(64); SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence); if (groups != null) @@ -964,32 +1059,63 @@ public class SeqPanel extends JPanel * add features that straddle the gap (pos may be the residue before or * after the gap) */ + int unshownFeatures = 0; if (av.isShowSequenceFeatures()) { List features = ap.getFeatureRenderer() .findFeaturesAtColumn(sequence, column + 1); - seqARep.appendFeatures(tooltipText, pos, features, - this.ap.getSeqPanel().seqCanvas.fr); + unshownFeatures = seqARep.appendFeatures(tooltipText, pos, + features, this.ap.getSeqPanel().seqCanvas.fr, + MAX_TOOLTIP_LENGTH); + + /* + * add features in CDS/protein complement at the corresponding + * position if configured to do so + */ + if (av.isShowComplementFeatures()) + { + if (!Comparison.isGap(sequence.getCharAt(column))) + { + AlignViewportI complement = ap.getAlignViewport() + .getCodingComplement(); + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence, + pos); + if (mf != null) + { + unshownFeatures += seqARep.appendFeatures(tooltipText, + pos, mf, fr2, MAX_TOOLTIP_LENGTH); + } + } + } } - if (tooltipText.length() == 6) // + if (tooltipText.length() == 0) // nothing added { setToolTipText(null); lastTooltip = null; } else { - if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant + if (tooltipText.length() > MAX_TOOLTIP_LENGTH) { tooltipText.setLength(MAX_TOOLTIP_LENGTH); tooltipText.append("..."); } + if (unshownFeatures > 0) + { + tooltipText.append("
").append("... ").append("") + .append(MessageManager.formatMessage( + "label.features_not_shown", unshownFeatures)) + .append(""); + } String textString = tooltipText.toString(); - if (lastTooltip == null || !lastTooltip.equals(textString)) + if (!textString.equals(lastTooltip)) { - String formattedTooltipText = JvSwingUtils.wrapTooltip(true, - textString); - setToolTipText(formattedTooltipText); lastTooltip = textString; + lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, + textString); + setToolTipText(lastFormattedTooltip); } } } @@ -1015,15 +1141,35 @@ public class SeqPanel extends JPanel String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column, anns); - setToolTipText(tooltip); - lastTooltip = tooltip; + if (!tooltip.equals(lastTooltip)) + { + lastTooltip = tooltip; + lastFormattedTooltip = tooltip == null ? null + : JvSwingUtils.wrapTooltip(true, tooltip); + setToolTipText(lastFormattedTooltip); + } String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column, anns[rowIndex]); ap.alignFrame.setStatus(msg); } - private Point lastp = null; + /* + * if Shift key is held down while moving the mouse, + * the tooltip location is not changed once shown + */ + private Point lastTooltipLocation = null; + + /* + * this flag is false for pixel moves within a residue, + * to reduce tooltip flicker + */ + private boolean moveTooltip = true; + + /* + * a dummy tooltip used to estimate where to position tooltips + */ + private JToolTip tempTip = new JLabel().createToolTip(); /* * (non-Javadoc) @@ -1033,29 +1179,31 @@ public class SeqPanel extends JPanel @Override public Point getToolTipLocation(MouseEvent event) { - if (tooltipText == null || tooltipText.length() <= 6) + // BH 2018 + + if (lastTooltip == null || !moveTooltip) { - lastp = null; return null; } - int x = event.getX(); - int w = getWidth(); - // switch sides when tooltip is too close to edge - int wdth = (w - x < 200) ? -(w / 2) : 5; - Point p = lastp; - if (!event.isShiftDown() || p == null) + if (lastTooltipLocation != null && event.isShiftDown()) { - p = new Point(event.getX() + wdth, event.getY() - 20); - lastp = p; + return lastTooltipLocation; } - /* - * TODO: try to set position so region is not obscured by tooltip - */ - return p; - } - String lastTooltip; + int x = event.getX(); + int y = event.getY(); + int w = getWidth(); + + tempTip.setTipText(lastFormattedTooltip); + int tipWidth = (int) tempTip.getPreferredSize().getWidth(); + + // was x += (w - x < 200) ? -(w / 2) : 5; + x = (x + tipWidth < w ? x + 10 : w - tipWidth); + Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20? + + return lastTooltipLocation = p; + } /** * set when the current UI interaction has resulted in a change that requires @@ -1093,7 +1241,7 @@ public class SeqPanel extends JPanel { char sequenceChar = sequence.getCharAt(column); int pos = sequence.findPosition(column); - setStatusMessage(sequence, seqIndex, sequenceChar, pos); + setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos); return pos; } @@ -1109,7 +1257,7 @@ public class SeqPanel extends JPanel * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2) * * - * @param sequence + * @param seqName * @param seqIndex * sequence position in the alignment (1..) * @param sequenceChar @@ -1117,7 +1265,7 @@ public class SeqPanel extends JPanel * @param residuePos * the sequence residue position (if not over a gap) */ - protected void setStatusMessage(SequenceI sequence, int seqIndex, + protected void setStatusMessage(String seqName, int seqIndex, char sequenceChar, int residuePos) { StringBuilder text = new StringBuilder(32); @@ -1127,7 +1275,7 @@ public class SeqPanel extends JPanel */ String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1); text.append("Sequence").append(seqno).append(" ID: ") - .append(sequence.getName()); + .append(seqName); String residue = null; @@ -1172,7 +1320,8 @@ public class SeqPanel extends JPanel { return; } - SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence(); + SequenceI alignedSeq = al.getSequenceAt(sequenceIndex); + SequenceI ds = alignedSeq.getDatasetSequence(); for (SearchResultMatchI m : results.getResults()) { SequenceI seq = m.getSequence(); @@ -1184,8 +1333,8 @@ public class SeqPanel extends JPanel if (seq == ds) { int start = m.getStart(); - setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1), - start); + setStatusMessage(alignedSeq.getName(), sequenceIndex, + seq.getCharAt(start - 1), start); return; } } @@ -1309,12 +1458,12 @@ public class SeqPanel extends JPanel res = 0; } - if ((lastres == -1) || (lastres == res)) + if ((editLastRes == -1) || (editLastRes == res)) { return; } - if ((res < av.getAlignment().getWidth()) && (res < lastres)) + if ((res < av.getAlignment().getWidth()) && (res < editLastRes)) { // dragLeft, delete gap editSequence(false, false, res); @@ -1325,22 +1474,46 @@ public class SeqPanel extends JPanel } mouseDragging = true; - if ((scrollThread != null) && (scrollThread.isRunning())) + if (scrollThread != null) { - scrollThread.setEvent(evt); + scrollThread.setMousePosition(evt.getPoint()); } } - // TODO: Make it more clever than many booleans + /** + * Edits the sequence to insert or delete one or more gaps, in response to a + * mouse drag or cursor mode command. The number of inserts/deletes may be + * specified with the cursor command, or else depends on the mouse event + * (normally one column, but potentially more for a fast mouse drag). + *

+ * Delete gaps is limited to the number of gaps left of the cursor position + * (mouse drag), or at or right of the cursor position (cursor mode). + *

+ * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in + * the current selection group. + *

+ * In locked editing mode (with a selection group present), inserts/deletions + * within the selection group are limited to its boundaries (and edits outside + * the group stop at its border). + * + * @param insertGap + * true to insert gaps, false to delete gaps + * @param editSeq + * (unused parameter) + * @param startres + * the column at which to perform the action; the number of columns + * affected depends on this.editLastRes (cursor column + * position) + */ synchronized void editSequence(boolean insertGap, boolean editSeq, - int startres) + final int startres) { int fixedLeft = -1; int fixedRight = -1; boolean fixedColumns = false; SequenceGroup sg = av.getSelectionGroup(); - SequenceI seq = av.getAlignment().getSequenceAt(startseq); + final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq); // No group, but the sequence may represent a group if (!groupEditing && av.hasHiddenRows()) @@ -1352,30 +1525,38 @@ public class SeqPanel extends JPanel } } - StringBuilder message = new StringBuilder(64); + StringBuilder message = new StringBuilder(64); // for status bar + + /* + * make a name for the edit action, for + * status bar message and Undo/Redo menu + */ + String label = null; if (groupEditing) { - message.append("Edit group:"); - if (editCommand == null) - { - editCommand = new EditCommand( - MessageManager.getString("action.edit_group")); - } + message.append("Edit group:"); + label = MessageManager.getString("action.edit_group"); } else { - message.append("Edit sequence: " + seq.getName()); - String label = seq.getName(); + message.append("Edit sequence: " + seq.getName()); + label = seq.getName(); if (label.length() > 10) { label = label.substring(0, 10); } - if (editCommand == null) - { - editCommand = new EditCommand(MessageManager - .formatMessage("label.edit_params", new String[] - { label })); - } + label = MessageManager.formatMessage("label.edit_params", + new String[] + { label }); + } + + /* + * initialise the edit command if there is not + * already one being extended + */ + if (editCommand == null) + { + editCommand = new EditCommand(label); } if (insertGap) @@ -1387,12 +1568,17 @@ public class SeqPanel extends JPanel message.append(" delete "); } - message.append(Math.abs(startres - lastres) + " gaps."); + message.append(Math.abs(startres - editLastRes) + " gaps."); ap.alignFrame.setStatus(message.toString()); - // Are we editing within a selection group? - if (groupEditing || (sg != null - && sg.getSequences(av.getHiddenRepSequences()).contains(seq))) + /* + * is there a selection group containing the sequence being edited? + * if so the boundary of the group is the limit of the edit + * (but the edit may be inside or outside the selection group) + */ + boolean inSelectionGroup = sg != null + && sg.getSequences(av.getHiddenRepSequences()).contains(seq); + if (groupEditing || inSelectionGroup) { fixedColumns = true; @@ -1411,10 +1597,10 @@ public class SeqPanel extends JPanel fixedLeft = sg.getStartRes(); fixedRight = sg.getEndRes(); - if ((startres < fixedLeft && lastres >= fixedLeft) - || (startres >= fixedLeft && lastres < fixedLeft) - || (startres > fixedRight && lastres <= fixedRight) - || (startres <= fixedRight && lastres > fixedRight)) + if ((startres < fixedLeft && editLastRes >= fixedLeft) + || (startres >= fixedLeft && editLastRes < fixedLeft) + || (startres > fixedRight && editLastRes <= fixedRight) + || (startres <= fixedRight && editLastRes > fixedRight)) { endEditing(); return; @@ -1440,8 +1626,8 @@ public class SeqPanel extends JPanel int y2 = av.getAlignment().getHiddenColumns() .getNextHiddenBoundary(false, startres); - if ((insertGap && startres > y1 && lastres < y1) - || (!insertGap && startres < y2 && lastres > y2)) + if ((insertGap && startres > y1 && editLastRes < y1) + || (!insertGap && startres < y2 && editLastRes > y2)) { endEditing(); return; @@ -1462,6 +1648,54 @@ public class SeqPanel extends JPanel } } + boolean success = doEditSequence(insertGap, editSeq, startres, + fixedRight, fixedColumns, sg); + + /* + * report what actually happened (might be less than + * what was requested), by inspecting the edit commands added + */ + String msg = getEditStatusMessage(editCommand); + ap.alignFrame.setStatus(msg == null ? " " : msg); + if (!success) + { + endEditing(); + } + + editLastRes = startres; + seqCanvas.repaint(); + } + + /** + * A helper method that performs the requested editing to insert or delete + * gaps (if possible). Answers true if the edit was successful, false if could + * only be performed in part or not at all. Failure may occur in 'locked edit' + * mode, when an insertion requires a matching gapped position (or column) to + * delete, and deletion requires an adjacent gapped position (or column) to + * remove. + * + * @param insertGap + * true if inserting gap(s), false if deleting + * @param editSeq + * (unused parameter, currently always false) + * @param startres + * the column at which to perform the edit + * @param fixedRight + * fixed right boundary column of a locked edit (within or to the + * left of a selection group) + * @param fixedColumns + * true if this is a locked edit + * @param sg + * the sequence group (if group edit is being performed) + * @return + */ + protected boolean doEditSequence(final boolean insertGap, + final boolean editSeq, final int startres, int fixedRight, + final boolean fixedColumns, final SequenceGroup sg) + { + final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq); + SequenceI[] seqs = new SequenceI[] { seq }; + if (groupEditing) { List vseqs = sg.getSequences(av.getHiddenRepSequences()); @@ -1480,7 +1714,8 @@ public class SeqPanel extends JPanel if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight && sg.getEndRes() == av.getAlignment().getWidth() - 1) { - sg.setEndRes(av.getAlignment().getWidth() + startres - lastres); + sg.setEndRes( + av.getAlignment().getWidth() + startres - editLastRes); fixedRight = sg.getEndRes(); } @@ -1488,15 +1723,16 @@ public class SeqPanel extends JPanel // Find the next gap before the end // of the visible region boundary boolean blank = false; - for (; fixedRight > lastres; fixedRight--) + for (; fixedRight > editLastRes; fixedRight--) { blank = true; for (g = 0; g < groupSize; g++) { - for (int j = 0; j < startres - lastres; j++) + for (int j = 0; j < startres - editLastRes; j++) { - if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j))) + if (!Comparison + .isGap(groupSeqs[g].getCharAt(fixedRight - j))) { blank = false; break; @@ -1513,12 +1749,11 @@ public class SeqPanel extends JPanel { if (sg.getSize() == av.getAlignment().getHeight()) { - if ((av.hasHiddenColumns() && startres < av.getAlignment() - .getHiddenColumns() - .getNextHiddenBoundary(false, startres))) + if ((av.hasHiddenColumns() + && startres < av.getAlignment().getHiddenColumns() + .getNextHiddenBoundary(false, startres))) { - endEditing(); - return; + return false; } int alWidth = av.getAlignment().getWidth(); @@ -1533,13 +1768,12 @@ public class SeqPanel extends JPanel } // We can still insert gaps if the selectionGroup // contains all the sequences - sg.setEndRes(sg.getEndRes() + startres - lastres); - fixedRight = alWidth + startres - lastres; + sg.setEndRes(sg.getEndRes() + startres - editLastRes); + fixedRight = alWidth + startres - editLastRes; } else { - endEditing(); - return; + return false; } } } @@ -1552,7 +1786,7 @@ public class SeqPanel extends JPanel for (g = 0; g < groupSize; g++) { - for (int j = startres; j < lastres; j++) + for (int j = startres; j < editLastRes; j++) { if (groupSeqs[g].getLength() <= j) { @@ -1562,8 +1796,7 @@ public class SeqPanel extends JPanel if (!Comparison.isGap(groupSeqs[g].getCharAt(j))) { // Not a gap, block edit not valid - endEditing(); - return; + return false; } } } @@ -1574,15 +1807,15 @@ public class SeqPanel extends JPanel // dragging to the right if (fixedColumns && fixedRight != -1) { - for (int j = lastres; j < startres; j++) + for (int j = editLastRes; j < startres; j++) { - insertChar(j, groupSeqs, fixedRight); + insertGap(j, groupSeqs, fixedRight); } } else { appendEdit(Action.INSERT_GAP, groupSeqs, startres, - startres - lastres); + startres - editLastRes, false); } } else @@ -1590,7 +1823,7 @@ public class SeqPanel extends JPanel // dragging to the left if (fixedColumns && fixedRight != -1) { - for (int j = lastres; j > startres; j--) + for (int j = editLastRes; j > startres; j--) { deleteChar(startres, groupSeqs, fixedRight); } @@ -1598,28 +1831,36 @@ public class SeqPanel extends JPanel else { appendEdit(Action.DELETE_GAP, groupSeqs, startres, - lastres - startres); + editLastRes - startres, false); } - } } else - // ///Editing a single sequence/////////// { + /* + * editing a single sequence + */ if (insertGap) { // dragging to the right if (fixedColumns && fixedRight != -1) { - for (int j = lastres; j < startres; j++) + for (int j = editLastRes; j < startres; j++) { - insertChar(j, new SequenceI[] { seq }, fixedRight); + if (!insertGap(j, seqs, fixedRight)) + { + /* + * e.g. cursor mode command specified + * more inserts than are possible + */ + return false; + } } } else { - appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres, - startres - lastres); + appendEdit(Action.INSERT_GAP, seqs, editLastRes, + startres - editLastRes, false); } } else @@ -1629,21 +1870,20 @@ public class SeqPanel extends JPanel // dragging to the left if (fixedColumns && fixedRight != -1) { - for (int j = lastres; j > startres; j--) + for (int j = editLastRes; j > startres; j--) { if (!Comparison.isGap(seq.getCharAt(startres))) { - endEditing(); - break; + return false; } - deleteChar(startres, new SequenceI[] { seq }, fixedRight); + deleteChar(startres, seqs, fixedRight); } } else { // could be a keyboard edit trying to delete none gaps int max = 0; - for (int m = startres; m < lastres; m++) + for (int m = startres; m < editLastRes; m++) { if (!Comparison.isGap(seq.getCharAt(m))) { @@ -1651,11 +1891,9 @@ public class SeqPanel extends JPanel } max++; } - if (max > 0) { - appendEdit(Action.DELETE_GAP, new SequenceI[] { seq }, - startres, max); + appendEdit(Action.DELETE_GAP, seqs, startres, max, false); } } } @@ -1663,25 +1901,82 @@ public class SeqPanel extends JPanel {// insertGap==false AND editSeq==TRUE; if (fixedColumns && fixedRight != -1) { - for (int j = lastres; j < startres; j++) + for (int j = editLastRes; j < startres; j++) { - insertChar(j, new SequenceI[] { seq }, fixedRight); + insertGap(j, seqs, fixedRight); } } else { - appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres, - startres - lastres); + appendEdit(Action.INSERT_NUC, seqs, editLastRes, + startres - editLastRes, false); } } } } - lastres = startres; - seqCanvas.repaint(); + return true; + } + + /** + * Constructs an informative status bar message while dragging to insert or + * delete gaps. Answers null if inserts and deletes cancel out. + * + * @param editCommand + * a command containing the list of individual edits + * @return + */ + protected static String getEditStatusMessage(EditCommand editCommand) + { + if (editCommand == null) + { + return null; + } + + /* + * add any inserts, and subtract any deletes, + * not counting those auto-inserted when doing a 'locked edit' + * (so only counting edits 'under the cursor') + */ + int count = 0; + for (Edit cmd : editCommand.getEdits()) + { + if (!cmd.isSystemGenerated()) + { + count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber() + : -cmd.getNumber(); + } + } + + if (count == 0) + { + /* + * inserts and deletes cancel out + */ + return null; + } + + String msgKey = count > 1 ? "label.insert_gaps" + : (count == 1 ? "label.insert_gap" + : (count == -1 ? "label.delete_gap" + : "label.delete_gaps")); + count = Math.abs(count); + + return MessageManager.formatMessage(msgKey, String.valueOf(count)); } - void insertChar(int j, SequenceI[] seq, int fixedColumn) + /** + * Inserts one gap at column j, deleting the right-most gapped column up to + * (and including) fixedColumn. Returns true if the edit is successful, false + * if no blank column is available to allow the insertion to be balanced by a + * deletion. + * + * @param j + * @param seq + * @param fixedColumn + * @return + */ + boolean insertGap(int j, SequenceI[] seq, int fixedColumn) { int blankColumn = fixedColumn; for (int s = 0; s < seq.length; s++) @@ -1702,47 +1997,60 @@ public class SeqPanel extends JPanel { blankColumn = fixedColumn; endEditing(); - return; + return false; } } - appendEdit(Action.DELETE_GAP, seq, blankColumn, 1); + appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true); - appendEdit(Action.INSERT_GAP, seq, j, 1); + appendEdit(Action.INSERT_GAP, seq, j, 1, false); + return true; } /** - * Helper method to add and perform one edit action. + * Helper method to add and perform one edit action * * @param action * @param seq * @param pos * @param count + * @param systemGenerated + * true if the edit is a 'balancing' delete (or insert) to match a + * user's insert (or delete) in a locked editing region */ protected void appendEdit(Action action, SequenceI[] seq, int pos, - int count) + int count, boolean systemGenerated) { final Edit edit = new EditCommand().new Edit(action, seq, pos, count, av.getAlignment().getGapCharacter()); + edit.setSystemGenerated(systemGenerated); editCommand.appendEdit(edit, av.getAlignment(), true, null); } - void deleteChar(int j, SequenceI[] seq, int fixedColumn) + /** + * Deletes the character at column j, and inserts a gap at fixedColumn, in + * each of the given sequences. The caller should ensure that all sequences + * are gapped in column j. + * + * @param j + * @param seqs + * @param fixedColumn + */ + void deleteChar(int j, SequenceI[] seqs, int fixedColumn) { + appendEdit(Action.DELETE_GAP, seqs, j, 1, false); - appendEdit(Action.DELETE_GAP, seq, j, 1); - - appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1); + appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true); } /** - * DOCUMENT ME! + * On reentering the panel, stops any scrolling that was started on dragging + * out of the panel * * @param e - * DOCUMENT ME! */ @Override public void mouseEntered(MouseEvent e) @@ -1751,23 +2059,19 @@ public class SeqPanel extends JPanel { oldSeq = 0; } - - if ((scrollThread != null) && (scrollThread.isRunning())) - { - scrollThread.stopScrolling(); - scrollThread = null; - } + stopScrolling(); } /** - * DOCUMENT ME! + * On leaving the panel, if the mouse is being dragged, starts a thread to + * scroll it until the mouse is released (in unwrapped mode only) * * @param e - * DOCUMENT ME! */ @Override public void mouseExited(MouseEvent e) { + lastMousePosition = null; ap.alignFrame.setStatus(" "); if (av.getWrapAlignment()) { @@ -1776,7 +2080,7 @@ public class SeqPanel extends JPanel if (mouseDragging && scrollThread == null) { - scrollThread = new ScrollThread(); + startScrolling(e.getPoint()); } } @@ -1795,7 +2099,7 @@ public class SeqPanel extends JPanel return; } - if (evt.getClickCount() > 1) + if (evt.getClickCount() > 1 && av.isShowSequenceFeatures()) { sg = av.getSelectionGroup(); if (sg != null && sg.getSize() == 1 @@ -1822,17 +2126,13 @@ public class SeqPanel extends JPanel SearchResultsI highlight = new SearchResults(); highlight.addResult(sequence, features.get(0).getBegin(), features .get(0).getEnd()); - seqCanvas.highlightSearchResults(highlight, false); + seqCanvas.highlightSearchResults(highlight, true); /* - * open the Amend Features dialog; clear highlighting afterwards, - * whether changes were made or not + * open the Amend Features dialog */ - List seqs = Collections.singletonList(sequence); - seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false, - ap); - av.setSearchResults(null); // clear highlighting - seqCanvas.repaint(); // draw new/amended features + new FeatureEditor(ap, Collections.singletonList(sequence), features, + false).showDialog(); } } } @@ -1914,19 +2214,19 @@ public class SeqPanel extends JPanel } } - if (evt.isPopupTrigger()) // Mac: mousePressed - { - showPopupMenu(evt, pos); - return; - } - /* * defer right-mouse click handling to mouseReleased on Windows * (where isPopupTrigger() will answer true) * NB isRightMouseButton is also true for Cmd-click on Mac */ - if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac()) + if (Platform.isWinRightButton(evt)) + { + return; + } + + if (evt.isPopupTrigger()) // Mac: mousePressed { + showPopupMenu(evt, pos); return; } @@ -2000,11 +2300,11 @@ public class SeqPanel extends JPanel final int column = pos.column; final int seq = pos.seqIndex; SequenceI sequence = av.getAlignment().getSequenceAt(seq); - List features = ap.getFeatureRenderer() - .findFeaturesAtColumn(sequence, column + 1); - - PopupMenu pop = new PopupMenu(ap, null, features); - pop.show(this, evt.getX(), evt.getY()); + if (sequence != null) + { + PopupMenu pop = new PopupMenu(ap, sequence, column); + pop.show(this, evt.getX(), evt.getY()); + } } /** @@ -2033,8 +2333,11 @@ public class SeqPanel extends JPanel && afterDrag; if (stretchGroup.cs != null) { - stretchGroup.cs.alignmentChanged(stretchGroup, - av.getHiddenRepSequences()); + if (afterDrag) + { + stretchGroup.cs.alignmentChanged(stretchGroup, + av.getHiddenRepSequences()); + } ResidueShaderI groupColourScheme = stretchGroup .getGroupColourScheme(); @@ -2086,10 +2389,7 @@ public class SeqPanel extends JPanel return; } - if (res >= av.getAlignment().getWidth()) - { - res = av.getAlignment().getWidth() - 1; - } + res = Math.min(res, av.getAlignment().getWidth()-1); if (stretchGroup.getEndRes() == res) { @@ -2175,92 +2475,165 @@ public class SeqPanel extends JPanel mouseDragging = true; - if ((scrollThread != null) && (scrollThread.isRunning())) + if (scrollThread != null) { - scrollThread.setEvent(evt); + scrollThread.setMousePosition(evt.getPoint()); } + + /* + * construct a status message showing the range of the selection + */ + StringBuilder status = new StringBuilder(64); + List seqs = stretchGroup.getSequences(); + String name = seqs.get(0).getName(); + if (name.length() > 20) + { + name = name.substring(0, 20); + } + status.append(name).append(" - "); + name = seqs.get(seqs.size() - 1).getName(); + if (name.length() > 20) + { + name = name.substring(0, 20); + } + status.append(name).append(" "); + int startRes = stretchGroup.getStartRes(); + status.append(" cols ").append(String.valueOf(startRes + 1)) + .append("-"); + int endRes = stretchGroup.getEndRes(); + status.append(String.valueOf(endRes + 1)); + status.append(" (").append(String.valueOf(seqs.size())).append(" x ") + .append(String.valueOf(endRes - startRes + 1)).append(")"); + ap.alignFrame.setStatus(status.toString()); } - void scrollCanvas(MouseEvent evt) + /** + * Stops the scroll thread if it is running + */ + void stopScrolling() { - if (evt == null) + if (scrollThread != null) { - if ((scrollThread != null) && (scrollThread.isRunning())) - { - scrollThread.stopScrolling(); - scrollThread = null; - } - mouseDragging = false; + scrollThread.stopScrolling(); + scrollThread = null; } - else + mouseDragging = false; + } + + /** + * Starts a thread to scroll the alignment, towards a given mouse position + * outside the panel bounds, unless the alignment is in wrapped mode + * + * @param mousePos + */ + void startScrolling(Point mousePos) + { + /* + * set this.mouseDragging in case this was called from + * a drag in ScalePanel or AnnotationPanel + */ + mouseDragging = true; + if (!av.getWrapAlignment() && scrollThread == null) { - if (scrollThread == null) + scrollThread = new ScrollThread(); + scrollThread.setMousePosition(mousePos); + if (Platform.isJS()) { - scrollThread = new ScrollThread(); + /* + * Javascript - run every 20ms until scrolling stopped + * or reaches the limit of scrollable alignment + */ + Timer t = new Timer(20, new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + if (scrollThread != null) + { + // if (!scrollOnce() {t.stop();}) gives compiler error :-( + scrollThread.scrollOnce(); + } + } + }); + t.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + if (scrollThread == null) + { + // SeqPanel.stopScrolling called + t.stop(); + } + } + }); + t.start(); + } + else + { + /* + * Java - run in a new thread + */ + scrollThread.start(); } - - mouseDragging = true; - scrollThread.setEvent(evt); } - } - // this class allows scrolling off the bottom of the visible alignment + /** + * Performs scrolling of the visible alignment left, right, up or down, until + * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the + * limit of the alignment is reached + */ class ScrollThread extends Thread { - MouseEvent evt; + private Point mousePos; - private volatile boolean threadRunning = true; + private volatile boolean keepRunning = true; + /** + * Constructor + */ public ScrollThread() { - start(); + setName("SeqPanel$ScrollThread"); } - public void setEvent(MouseEvent e) + /** + * Sets the position of the mouse that determines the direction of the + * scroll to perform. If this is called as the mouse moves, scrolling should + * respond accordingly. For example, if the mouse is dragged right, scroll + * right should start; if the drag continues down, scroll down should also + * happen. + * + * @param p + */ + public void setMousePosition(Point p) { - evt = e; + mousePos = p; } + /** + * Sets a flag that will cause the thread to exit + */ public void stopScrolling() { - threadRunning = false; - } - - public boolean isRunning() - { - return threadRunning; + keepRunning = false; } + /** + * Scrolls the alignment left or right, and/or up or down, depending on the + * last notified mouse position, until the limit of the alignment is + * reached, or a flag is set to stop the scroll + */ @Override public void run() { - while (threadRunning) + while (keepRunning) { - if (evt != null) + if (mousePos != null) { - if (mouseDragging && (evt.getY() < 0) - && (av.getRanges().getStartSeq() > 0)) - { - av.getRanges().scrollUp(true); - } - - if (mouseDragging && (evt.getY() >= getHeight()) && (av - .getAlignment().getHeight() > av.getRanges().getEndSeq())) - { - av.getRanges().scrollUp(false); - } - - if (mouseDragging && (evt.getX() < 0)) - { - av.getRanges().scrollRight(false); - } - else if (mouseDragging && (evt.getX() >= getWidth())) - { - av.getRanges().scrollRight(true); - } + keepRunning = scrollOnce(); } - try { Thread.sleep(20); @@ -2268,6 +2641,60 @@ public class SeqPanel extends JPanel { } } + SeqPanel.this.scrollThread = null; + } + + /** + * Scrolls + *

+ * Answers true if a scroll was performed, false if not - meaning either + * that the mouse position is within the panel, or the edge of the alignment + * has been reached. + */ + boolean scrollOnce() + { + /* + * quit after mouseUp ensures interrupt in JalviewJS + */ + if (!mouseDragging) + { + return false; + } + + boolean scrolled = false; + ViewportRanges ranges = SeqPanel.this.av.getRanges(); + + /* + * scroll up or down + */ + if (mousePos.y < 0) + { + // mouse is above this panel - try scroll up + scrolled = ranges.scrollUp(true); + } + else if (mousePos.y >= getHeight()) + { + // mouse is below this panel - try scroll down + scrolled = ranges.scrollUp(false); + } + + /* + * scroll left or right + */ + if (mousePos.x < 0) + { + scrolled |= ranges.scrollRight(false); + } + else if (mousePos.x >= getWidth()) + { + scrolled |= ranges.scrollRight(true); + } + return scrolled; } } @@ -2435,7 +2862,7 @@ public class SeqPanel extends JPanel * Map sequence selection */ SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av); - av.setSelectionGroup(sg); + av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null); av.isSelectionGroupChanged(true); /* @@ -2447,7 +2874,7 @@ public class SeqPanel extends JPanel HiddenColumns hs = new HiddenColumns(); MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs); av.setColumnSelection(cs); - av.getAlignment().setHiddenColumns(hs); + boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs); // lastly, update any dependent dialogs if (ap.getCalculationDialog() != null) @@ -2455,7 +2882,11 @@ public class SeqPanel extends JPanel ap.getCalculationDialog().validateCalcTypes(); } - PaintRefresher.Refresh(this, av.getSequenceSetId()); + /* + * repaint alignment, and also Overview or Structure + * if hidden column selection has changed + */ + ap.paintAlignment(hiddenChanged, hiddenChanged); return true; }