Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
SeqPanel | 85 | 910 | 413 | ||
SeqPanel.MousePos | 98 | 11 | 7 | ||
SeqPanel.ScrollThread | 2729 | 22 | 13 |
1 | /* | |
2 | * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) | |
3 | * Copyright (C) $$Year-Rel$$ 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 java.awt.BorderLayout; | |
24 | import java.awt.Color; | |
25 | import java.awt.Font; | |
26 | import java.awt.FontMetrics; | |
27 | import java.awt.Point; | |
28 | import java.awt.event.ActionEvent; | |
29 | import java.awt.event.ActionListener; | |
30 | import java.awt.event.MouseEvent; | |
31 | import java.awt.event.MouseListener; | |
32 | import java.awt.event.MouseMotionListener; | |
33 | import java.awt.event.MouseWheelEvent; | |
34 | import java.awt.event.MouseWheelListener; | |
35 | import java.util.ArrayList; | |
36 | import java.util.Collections; | |
37 | import java.util.List; | |
38 | ||
39 | import javax.swing.JLabel; | |
40 | import javax.swing.JPanel; | |
41 | import javax.swing.JToolTip; | |
42 | import javax.swing.SwingUtilities; | |
43 | import javax.swing.Timer; | |
44 | import javax.swing.ToolTipManager; | |
45 | ||
46 | import jalview.api.AlignViewportI; | |
47 | import jalview.bin.Console; | |
48 | import jalview.commands.EditCommand; | |
49 | import jalview.commands.EditCommand.Action; | |
50 | import jalview.commands.EditCommand.Edit; | |
51 | import jalview.datamodel.AlignmentAnnotation; | |
52 | import jalview.datamodel.AlignmentI; | |
53 | import jalview.datamodel.ColumnSelection; | |
54 | import jalview.datamodel.HiddenColumns; | |
55 | import jalview.datamodel.MappedFeatures; | |
56 | import jalview.datamodel.SearchResultMatchI; | |
57 | import jalview.datamodel.SearchResults; | |
58 | import jalview.datamodel.SearchResultsI; | |
59 | import jalview.datamodel.Sequence; | |
60 | import jalview.datamodel.SequenceFeature; | |
61 | import jalview.datamodel.SequenceGroup; | |
62 | import jalview.datamodel.SequenceI; | |
63 | import jalview.io.SequenceAnnotationReport; | |
64 | import jalview.renderer.ResidueShaderI; | |
65 | import jalview.schemes.ResidueProperties; | |
66 | import jalview.structure.SelectionListener; | |
67 | import jalview.structure.SelectionSource; | |
68 | import jalview.structure.SequenceListener; | |
69 | import jalview.structure.StructureSelectionManager; | |
70 | import jalview.structure.VamsasSource; | |
71 | import jalview.util.Comparison; | |
72 | import jalview.util.MappingUtils; | |
73 | import jalview.util.MessageManager; | |
74 | import jalview.util.Platform; | |
75 | import jalview.viewmodel.AlignmentViewport; | |
76 | import jalview.viewmodel.ViewportRanges; | |
77 | import jalview.viewmodel.seqfeatures.FeatureRendererModel; | |
78 | ||
79 | /** | |
80 | * DOCUMENT ME! | |
81 | * | |
82 | * @author $author$ | |
83 | * @version $Revision: 1.130 $ | |
84 | */ | |
85 | public class SeqPanel extends JPanel | |
86 | implements MouseListener, MouseMotionListener, MouseWheelListener, | |
87 | SequenceListener, SelectionListener | |
88 | { | |
89 | /* | |
90 | * a class that holds computed mouse position | |
91 | * - column of the alignment (0...) | |
92 | * - sequence offset (0...) | |
93 | * - annotation row offset (0...) | |
94 | * where annotation offset is -1 unless the alignment is shown | |
95 | * in wrapped mode, annotations are shown, and the mouse is | |
96 | * over an annnotation row | |
97 | */ | |
98 | static class MousePos | |
99 | { | |
100 | /* | |
101 | * alignment column position of cursor (0...) | |
102 | */ | |
103 | final int column; | |
104 | ||
105 | /* | |
106 | * index in alignment of sequence under cursor, | |
107 | * or nearest above if cursor is not over a sequence | |
108 | */ | |
109 | final int seqIndex; | |
110 | ||
111 | /* | |
112 | * index in annotations array of annotation under the cursor | |
113 | * (only possible in wrapped mode with annotations shown), | |
114 | * or -1 if cursor is not over an annotation row | |
115 | */ | |
116 | final int annotationIndex; | |
117 | ||
118 | 57 | MousePos(int col, int seq, int ann) |
119 | { | |
120 | 57 | column = col; |
121 | 57 | seqIndex = seq; |
122 | 57 | annotationIndex = ann; |
123 | } | |
124 | ||
125 | 0 | boolean isOverAnnotation() |
126 | { | |
127 | 0 | return annotationIndex != -1; |
128 | } | |
129 | ||
130 | 0 | @Override |
131 | public boolean equals(Object obj) | |
132 | { | |
133 | 0 | if (obj == null || !(obj instanceof MousePos)) |
134 | { | |
135 | 0 | return false; |
136 | } | |
137 | 0 | MousePos o = (MousePos) obj; |
138 | 0 | boolean b = (column == o.column && seqIndex == o.seqIndex |
139 | && annotationIndex == o.annotationIndex); | |
140 | // jalview.bin.Console.outPrintln(obj + (b ? "= " : "!= ") + this); | |
141 | 0 | return b; |
142 | } | |
143 | ||
144 | /** | |
145 | * A simple hashCode that ensures that instances that satisfy equals() have | |
146 | * the same hashCode | |
147 | */ | |
148 | 0 | @Override |
149 | public int hashCode() | |
150 | { | |
151 | 0 | return column + seqIndex + annotationIndex; |
152 | } | |
153 | ||
154 | /** | |
155 | * toString method for debug output purposes only | |
156 | */ | |
157 | 0 | @Override |
158 | public String toString() | |
159 | { | |
160 | 0 | return String.format("c%d:s%d:a%d", column, seqIndex, |
161 | annotationIndex); | |
162 | } | |
163 | } | |
164 | ||
165 | private static final int MAX_TOOLTIP_LENGTH = 300; | |
166 | ||
167 | public SeqCanvas seqCanvas; | |
168 | ||
169 | public AlignmentPanel ap; | |
170 | ||
171 | /* | |
172 | * last position for mouseMoved event | |
173 | */ | |
174 | private MousePos lastMousePosition; | |
175 | ||
176 | protected int editLastRes; | |
177 | ||
178 | protected int editStartSeq; | |
179 | ||
180 | protected AlignViewport av; | |
181 | ||
182 | ScrollThread scrollThread = null; | |
183 | ||
184 | boolean mouseDragging = false; | |
185 | ||
186 | boolean editingSeqs = false; | |
187 | ||
188 | boolean groupEditing = false; | |
189 | ||
190 | // //////////////////////////////////////// | |
191 | // ///Everything below this is for defining the boundary of the rubberband | |
192 | // //////////////////////////////////////// | |
193 | int oldSeq = -1; | |
194 | ||
195 | boolean changeEndSeq = false; | |
196 | ||
197 | boolean changeStartSeq = false; | |
198 | ||
199 | boolean changeEndRes = false; | |
200 | ||
201 | boolean changeStartRes = false; | |
202 | ||
203 | SequenceGroup stretchGroup = null; | |
204 | ||
205 | boolean remove = false; | |
206 | ||
207 | Point lastMousePress; | |
208 | ||
209 | boolean mouseWheelPressed = false; | |
210 | ||
211 | StringBuffer keyboardNo1; | |
212 | ||
213 | StringBuffer keyboardNo2; | |
214 | ||
215 | private final SequenceAnnotationReport seqARep; | |
216 | ||
217 | /* | |
218 | * the last tooltip on mousing over the alignment (or annotation in wrapped mode) | |
219 | * - the tooltip is not set again if unchanged | |
220 | * - this is the tooltip text _before_ formatting as html | |
221 | */ | |
222 | private String lastTooltip; | |
223 | ||
224 | /* | |
225 | * the last tooltip on mousing over the alignment (or annotation in wrapped mode) | |
226 | * - used to decide where to place the tooltip in getTooltipLocation() | |
227 | * - this is the tooltip text _after_ formatting as html | |
228 | */ | |
229 | private String lastFormattedTooltip; | |
230 | ||
231 | EditCommand editCommand; | |
232 | ||
233 | StructureSelectionManager ssm; | |
234 | ||
235 | SearchResultsI lastSearchResults; | |
236 | ||
237 | /** | |
238 | * Creates a new SeqPanel object | |
239 | * | |
240 | * @param viewport | |
241 | * @param alignPanel | |
242 | */ | |
243 | 478 | public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel) |
244 | { | |
245 | 478 | seqARep = new SequenceAnnotationReport(true); |
246 | 478 | ToolTipManager.sharedInstance().registerComponent(this); |
247 | 478 | ToolTipManager.sharedInstance().setInitialDelay(0); |
248 | 478 | ToolTipManager.sharedInstance().setDismissDelay(10000); |
249 | ||
250 | 478 | this.av = viewport; |
251 | 478 | setBackground(Color.white); |
252 | ||
253 | 478 | seqCanvas = new SeqCanvas(alignPanel); |
254 | 478 | setLayout(new BorderLayout()); |
255 | 478 | add(seqCanvas, BorderLayout.CENTER); |
256 | ||
257 | 478 | this.ap = alignPanel; |
258 | ||
259 | 478 | if (!viewport.isDataset()) |
260 | { | |
261 | 478 | addMouseMotionListener(this); |
262 | 478 | addMouseListener(this); |
263 | 478 | addMouseWheelListener(this); |
264 | 478 | ssm = viewport.getStructureSelectionManager(); |
265 | 478 | ssm.addStructureViewerListener(this); |
266 | 478 | ssm.addSelectionListener(this); |
267 | } | |
268 | } | |
269 | ||
270 | int startWrapBlock = -1; | |
271 | ||
272 | int wrappedBlock = -1; | |
273 | ||
274 | /** | |
275 | * Computes the column and sequence row (and possibly annotation row when in | |
276 | * wrapped mode) for the given mouse position | |
277 | * <p> | |
278 | * Mouse position is not set if in wrapped mode with the cursor either between | |
279 | * sequences, or over the left or right vertical scale. | |
280 | * | |
281 | * @param evt | |
282 | * @return | |
283 | */ | |
284 | 57 | MousePos findMousePosition(MouseEvent evt) |
285 | { | |
286 | 57 | int col = findColumn(evt); |
287 | 57 | int seqIndex = -1; |
288 | 57 | int annIndex = -1; |
289 | 57 | int y = evt.getY(); |
290 | ||
291 | 57 | int charHeight = av.getCharHeight(); |
292 | 57 | int alignmentHeight = av.getAlignment().getHeight(); |
293 | 57 | if (av.getWrapAlignment()) |
294 | { | |
295 | 57 | seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(), |
296 | seqCanvas.getHeight()); | |
297 | ||
298 | /* | |
299 | * yPos modulo height of repeating width | |
300 | */ | |
301 | 57 | int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx; |
302 | ||
303 | /* | |
304 | * height of sequences plus space / scale above, | |
305 | * plus gap between sequences and annotations | |
306 | */ | |
307 | 57 | int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment |
308 | + alignmentHeight * charHeight | |
309 | + SeqCanvas.SEQS_ANNOTATION_GAP; | |
310 | 57 | if (yOffsetPx >= alignmentHeightPixels) |
311 | { | |
312 | /* | |
313 | * mouse is over annotations; find annotation index, also set | |
314 | * last sequence above (for backwards compatible behaviour) | |
315 | */ | |
316 | 16 | AlignmentAnnotation[] anns = av.getAlignment() |
317 | .getAlignmentAnnotation(); | |
318 | 16 | int rowOffsetPx = yOffsetPx - alignmentHeightPixels; |
319 | 16 | annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns); |
320 | 16 | seqIndex = alignmentHeight - 1; |
321 | } | |
322 | else | |
323 | { | |
324 | /* | |
325 | * mouse is over sequence (or the space above sequences) | |
326 | */ | |
327 | 41 | yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment; |
328 | 41 | if (yOffsetPx >= 0) |
329 | { | |
330 | 24 | seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1); |
331 | } | |
332 | } | |
333 | } | |
334 | else | |
335 | { | |
336 | 0 | ViewportRanges ranges = av.getRanges(); |
337 | 0 | seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(), |
338 | alignmentHeight - 1); | |
339 | 0 | seqIndex = Math.min(seqIndex, ranges.getEndSeq()); |
340 | } | |
341 | ||
342 | 57 | return new MousePos(col, seqIndex, annIndex); |
343 | } | |
344 | ||
345 | /** | |
346 | * @param evt | |
347 | * @return absolute column in alignment nearest to the mouse pointer | |
348 | */ | |
349 | 25 | int findAlignmentColumn(MouseEvent evt) |
350 | { | |
351 | 25 | return findNearestColumn(evt, true); |
352 | } | |
353 | ||
354 | /** | |
355 | * Returns the aligned sequence position (base 0) at the mouse position, or | |
356 | * the closest visible one | |
357 | * <p> | |
358 | * Returns -1 if in wrapped mode with the mouse over either left or right | |
359 | * vertical scale. | |
360 | * | |
361 | * @param evt | |
362 | * @return | |
363 | */ | |
364 | 70 | int findColumn(MouseEvent evt) |
365 | { | |
366 | 70 | return findNearestColumn(evt, false); |
367 | } | |
368 | ||
369 | /** | |
370 | * @param nearestColumn | |
371 | * when false returns negative values for out of bound positions - -1 | |
372 | * for scale left/right, <-1 if far to right | |
373 | * @return nearest absolute column to mouse pointer | |
374 | */ | |
375 | 95 | private int findNearestColumn(MouseEvent evt, boolean nearestColumn) |
376 | { | |
377 | 95 | int res = 0; |
378 | 95 | int x = evt.getX(); |
379 | ||
380 | 95 | final int startRes = av.getRanges().getStartRes(); |
381 | 95 | final int charWidth = av.getCharWidth(); |
382 | ||
383 | 95 | if (av.getWrapAlignment()) |
384 | { | |
385 | 71 | int hgap = av.getCharHeight(); |
386 | 71 | if (av.getScaleAboveWrapped()) |
387 | { | |
388 | 31 | hgap += av.getCharHeight(); |
389 | } | |
390 | ||
391 | 71 | int cHeight = av.getAlignment().getHeight() * av.getCharHeight() |
392 | + hgap + seqCanvas.getAnnotationHeight(); | |
393 | ||
394 | 71 | int y = evt.getY(); |
395 | 71 | y = Math.max(0, y - hgap); |
396 | 71 | x -= seqCanvas.getLabelWidthWest(); |
397 | 71 | if (x < 0) |
398 | { | |
399 | // mouse is over left scale | |
400 | 4 | if (!nearestColumn) |
401 | { | |
402 | 3 | return -1; |
403 | } | |
404 | else | |
405 | { | |
406 | 1 | x = 0; |
407 | } | |
408 | } | |
409 | ||
410 | 68 | int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth()); |
411 | 68 | if (cwidth < 1) |
412 | { | |
413 | 0 | return 0; |
414 | } | |
415 | 68 | if (x >= cwidth * charWidth) |
416 | { | |
417 | 2 | if (!nearestColumn) |
418 | { | |
419 | // mouse is over right scale | |
420 | 1 | return -1; |
421 | } | |
422 | else | |
423 | { | |
424 | 1 | x = cwidth * charWidth - 1; |
425 | } | |
426 | } | |
427 | ||
428 | 67 | wrappedBlock = y / cHeight; |
429 | 67 | wrappedBlock += startRes / cwidth; |
430 | // allow for wrapped view scrolled right (possible from Overview) | |
431 | 67 | int startOffset = startRes % cwidth; |
432 | 67 | res = wrappedBlock * cwidth + startOffset |
433 | + Math.min(cwidth - 1, x / charWidth); | |
434 | } | |
435 | else | |
436 | { | |
437 | /* | |
438 | * make sure we calculate relative to visible alignment, | |
439 | * rather than right-hand gutter | |
440 | */ | |
441 | 24 | x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth()); |
442 | 24 | if (nearestColumn) |
443 | { | |
444 | 18 | x = Math.max(x, 0); |
445 | } | |
446 | ||
447 | 24 | res = (x / charWidth) + startRes; |
448 | 24 | res = Math.min(res, av.getRanges().getEndRes()); |
449 | ||
450 | } | |
451 | ||
452 | 91 | if (av.hasHiddenColumns()) |
453 | { | |
454 | 15 | res = av.getAlignment().getHiddenColumns() |
455 | .visibleToAbsoluteColumn(res); | |
456 | } | |
457 | ||
458 | 91 | return res; |
459 | } | |
460 | ||
461 | /** | |
462 | * When all of a sequence of edits are complete, put the resulting edit list | |
463 | * on the history stack (undo list), and reset flags for editing in progress. | |
464 | */ | |
465 | 0 | void endEditing() |
466 | { | |
467 | 0 | try |
468 | { | |
469 | 0 | if (editCommand != null && editCommand.getSize() > 0) |
470 | { | |
471 | 0 | ap.alignFrame.addHistoryItem(editCommand); |
472 | 0 | av.firePropertyChange("alignment", null, |
473 | av.getAlignment().getSequences()); | |
474 | } | |
475 | } finally | |
476 | { | |
477 | /* | |
478 | * Tidy up come what may... | |
479 | */ | |
480 | 0 | editStartSeq = -1; |
481 | 0 | editLastRes = -1; |
482 | 0 | editingSeqs = false; |
483 | 0 | groupEditing = false; |
484 | 0 | keyboardNo1 = null; |
485 | 0 | keyboardNo2 = null; |
486 | 0 | editCommand = null; |
487 | } | |
488 | } | |
489 | ||
490 | 0 | void setCursorRow() |
491 | { | |
492 | 0 | seqCanvas.cursorY = getKeyboardNo1() - 1; |
493 | 0 | scrollToVisible(true); |
494 | } | |
495 | ||
496 | 0 | void setCursorColumn() |
497 | { | |
498 | 0 | seqCanvas.cursorX = getKeyboardNo1() - 1; |
499 | 0 | scrollToVisible(true); |
500 | } | |
501 | ||
502 | 0 | void setCursorRowAndColumn() |
503 | { | |
504 | 0 | if (keyboardNo2 == null) |
505 | { | |
506 | 0 | keyboardNo2 = new StringBuffer(); |
507 | } | |
508 | else | |
509 | { | |
510 | 0 | seqCanvas.cursorX = getKeyboardNo1() - 1; |
511 | 0 | seqCanvas.cursorY = getKeyboardNo2() - 1; |
512 | 0 | scrollToVisible(true); |
513 | } | |
514 | } | |
515 | ||
516 | 0 | void setCursorPosition() |
517 | { | |
518 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY); |
519 | ||
520 | 0 | seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1; |
521 | 0 | scrollToVisible(true); |
522 | } | |
523 | ||
524 | 0 | void moveCursor(int dx, int dy) |
525 | { | |
526 | 0 | moveCursor(dx, dy, false); |
527 | } | |
528 | ||
529 | 0 | void moveCursor(int dx, int dy, boolean nextWord) |
530 | { | |
531 | 0 | HiddenColumns hidden = av.getAlignment().getHiddenColumns(); |
532 | ||
533 | 0 | if (nextWord) |
534 | { | |
535 | 0 | int maxWidth = av.getAlignment().getWidth(); |
536 | 0 | int maxHeight = av.getAlignment().getHeight(); |
537 | 0 | SequenceI seqAtRow = av.getAlignment() |
538 | .getSequenceAt(seqCanvas.cursorY); | |
539 | // look for next gap or residue | |
540 | 0 | boolean isGap = Comparison |
541 | .isGap(seqAtRow.getCharAt(seqCanvas.cursorX)); | |
542 | 0 | int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR; |
543 | 0 | do |
544 | { | |
545 | 0 | lastP = p; |
546 | 0 | lastR = r; |
547 | 0 | if (dy != 0) |
548 | { | |
549 | 0 | r += dy; |
550 | 0 | if (r < 0) |
551 | { | |
552 | 0 | r = 0; |
553 | } | |
554 | 0 | if (r >= maxHeight) |
555 | { | |
556 | 0 | r = maxHeight - 1; |
557 | } | |
558 | 0 | seqAtRow = av.getAlignment().getSequenceAt(r); |
559 | } | |
560 | 0 | p = nextVisible(hidden, maxWidth, p, dx); |
561 | 0 | } while ((dx != 0 ? p != lastP : r != lastR) |
562 | && isGap == Comparison.isGap(seqAtRow.getCharAt(p))); | |
563 | 0 | seqCanvas.cursorX = p; |
564 | 0 | seqCanvas.cursorY = r; |
565 | } | |
566 | else | |
567 | { | |
568 | 0 | int maxWidth = av.getAlignment().getWidth(); |
569 | 0 | seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, |
570 | dx); | |
571 | 0 | seqCanvas.cursorY += dy; |
572 | } | |
573 | 0 | scrollToVisible(false); |
574 | } | |
575 | ||
576 | 0 | private int nextVisible(HiddenColumns hidden, int maxWidth, int original, |
577 | int dx) | |
578 | { | |
579 | 0 | int newCursorX = original + dx; |
580 | 0 | if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX)) |
581 | { | |
582 | 0 | int visx = hidden.absoluteToVisibleColumn(newCursorX - dx); |
583 | 0 | int[] region = hidden.getRegionWithEdgeAtRes(visx); |
584 | ||
585 | 0 | if (region != null) // just in case |
586 | { | |
587 | 0 | if (dx == 1) |
588 | { | |
589 | // moving right | |
590 | 0 | newCursorX = region[1] + 1; |
591 | } | |
592 | 0 | else if (dx == -1) |
593 | { | |
594 | // moving left | |
595 | 0 | newCursorX = region[0] - 1; |
596 | } | |
597 | } | |
598 | } | |
599 | 0 | newCursorX = (newCursorX < 0) ? 0 : newCursorX; |
600 | 0 | if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX)) |
601 | { | |
602 | 0 | newCursorX = original; |
603 | } | |
604 | 0 | return newCursorX; |
605 | } | |
606 | ||
607 | /** | |
608 | * Scroll to make the cursor visible in the viewport. | |
609 | * | |
610 | * @param jump | |
611 | * just jump to the location rather than scrolling | |
612 | */ | |
613 | 0 | void scrollToVisible(boolean jump) |
614 | { | |
615 | 0 | if (seqCanvas.cursorX < 0) |
616 | { | |
617 | 0 | seqCanvas.cursorX = 0; |
618 | } | |
619 | 0 | else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1) |
620 | { | |
621 | 0 | seqCanvas.cursorX = av.getAlignment().getWidth() - 1; |
622 | } | |
623 | ||
624 | 0 | if (seqCanvas.cursorY < 0) |
625 | { | |
626 | 0 | seqCanvas.cursorY = 0; |
627 | } | |
628 | 0 | else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1) |
629 | { | |
630 | 0 | seqCanvas.cursorY = av.getAlignment().getHeight() - 1; |
631 | } | |
632 | ||
633 | 0 | endEditing(); |
634 | ||
635 | 0 | boolean repaintNeeded = true; |
636 | 0 | if (jump) |
637 | { | |
638 | // only need to repaint if the viewport did not move, as otherwise it will | |
639 | // get a repaint | |
640 | 0 | repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX, |
641 | seqCanvas.cursorY); | |
642 | } | |
643 | else | |
644 | { | |
645 | 0 | if (av.getWrapAlignment()) |
646 | { | |
647 | // scrollToWrappedVisible expects x-value to have hidden cols subtracted | |
648 | 0 | int x = av.getAlignment().getHiddenColumns() |
649 | .absoluteToVisibleColumn(seqCanvas.cursorX); | |
650 | 0 | av.getRanges().scrollToWrappedVisible(x); |
651 | } | |
652 | else | |
653 | { | |
654 | 0 | av.getRanges().scrollToVisible(seqCanvas.cursorX, |
655 | seqCanvas.cursorY); | |
656 | } | |
657 | } | |
658 | ||
659 | 0 | if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX)) |
660 | { | |
661 | 0 | setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY), |
662 | seqCanvas.cursorX, seqCanvas.cursorY); | |
663 | } | |
664 | ||
665 | 0 | if (repaintNeeded) |
666 | { | |
667 | 0 | seqCanvas.repaint(); |
668 | } | |
669 | } | |
670 | ||
671 | 0 | void setSelectionAreaAtCursor(boolean topLeft) |
672 | { | |
673 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY); |
674 | ||
675 | 0 | if (av.getSelectionGroup() != null) |
676 | { | |
677 | 0 | SequenceGroup sg = av.getSelectionGroup(); |
678 | // Find the top and bottom of this group | |
679 | 0 | int min = av.getAlignment().getHeight(), max = 0; |
680 | 0 | for (int i = 0; i < sg.getSize(); i++) |
681 | { | |
682 | 0 | int index = av.getAlignment().findIndex(sg.getSequenceAt(i)); |
683 | 0 | if (index > max) |
684 | { | |
685 | 0 | max = index; |
686 | } | |
687 | 0 | if (index < min) |
688 | { | |
689 | 0 | min = index; |
690 | } | |
691 | } | |
692 | ||
693 | 0 | max++; |
694 | ||
695 | 0 | if (topLeft) |
696 | { | |
697 | 0 | sg.setStartRes(seqCanvas.cursorX); |
698 | 0 | if (sg.getEndRes() < seqCanvas.cursorX) |
699 | { | |
700 | 0 | sg.setEndRes(seqCanvas.cursorX); |
701 | } | |
702 | ||
703 | 0 | min = seqCanvas.cursorY; |
704 | } | |
705 | else | |
706 | { | |
707 | 0 | sg.setEndRes(seqCanvas.cursorX); |
708 | 0 | if (sg.getStartRes() > seqCanvas.cursorX) |
709 | { | |
710 | 0 | sg.setStartRes(seqCanvas.cursorX); |
711 | } | |
712 | ||
713 | 0 | max = seqCanvas.cursorY + 1; |
714 | } | |
715 | ||
716 | 0 | if (min > max) |
717 | { | |
718 | // Only the user can do this | |
719 | 0 | av.setSelectionGroup(null); |
720 | } | |
721 | else | |
722 | { | |
723 | // Now add any sequences between min and max | |
724 | 0 | sg.getSequences(null).clear(); |
725 | 0 | for (int i = min; i < max; i++) |
726 | { | |
727 | 0 | sg.addSequence(av.getAlignment().getSequenceAt(i), false); |
728 | } | |
729 | } | |
730 | } | |
731 | ||
732 | 0 | if (av.getSelectionGroup() == null) |
733 | { | |
734 | 0 | SequenceGroup sg = new SequenceGroup(); |
735 | 0 | sg.setStartRes(seqCanvas.cursorX); |
736 | 0 | sg.setEndRes(seqCanvas.cursorX); |
737 | 0 | sg.addSequence(sequence, false); |
738 | 0 | av.setSelectionGroup(sg); |
739 | } | |
740 | ||
741 | 0 | ap.paintAlignment(false, false); |
742 | 0 | av.sendSelection(); |
743 | } | |
744 | ||
745 | 0 | void insertGapAtCursor(boolean group) |
746 | { | |
747 | 0 | groupEditing = group; |
748 | 0 | editStartSeq = seqCanvas.cursorY; |
749 | 0 | editLastRes = seqCanvas.cursorX; |
750 | 0 | editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1()); |
751 | 0 | endEditing(); |
752 | } | |
753 | ||
754 | 0 | void deleteGapAtCursor(boolean group) |
755 | { | |
756 | 0 | groupEditing = group; |
757 | 0 | editStartSeq = seqCanvas.cursorY; |
758 | 0 | editLastRes = seqCanvas.cursorX + getKeyboardNo1(); |
759 | 0 | editSequence(false, false, seqCanvas.cursorX); |
760 | 0 | endEditing(); |
761 | } | |
762 | ||
763 | 0 | void insertNucAtCursor(boolean group, String nuc) |
764 | { | |
765 | // TODO not called - delete? | |
766 | 0 | groupEditing = group; |
767 | 0 | editStartSeq = seqCanvas.cursorY; |
768 | 0 | editLastRes = seqCanvas.cursorX; |
769 | 0 | editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1()); |
770 | 0 | endEditing(); |
771 | } | |
772 | ||
773 | 0 | void numberPressed(char value) |
774 | { | |
775 | 0 | if (keyboardNo1 == null) |
776 | { | |
777 | 0 | keyboardNo1 = new StringBuffer(); |
778 | } | |
779 | ||
780 | 0 | if (keyboardNo2 != null) |
781 | { | |
782 | 0 | keyboardNo2.append(value); |
783 | } | |
784 | else | |
785 | { | |
786 | 0 | keyboardNo1.append(value); |
787 | } | |
788 | } | |
789 | ||
790 | 0 | int getKeyboardNo1() |
791 | { | |
792 | 0 | try |
793 | { | |
794 | 0 | if (keyboardNo1 != null) |
795 | { | |
796 | 0 | int value = Integer.parseInt(keyboardNo1.toString()); |
797 | 0 | keyboardNo1 = null; |
798 | 0 | return value; |
799 | } | |
800 | } catch (Exception x) | |
801 | { | |
802 | } | |
803 | 0 | keyboardNo1 = null; |
804 | 0 | return 1; |
805 | } | |
806 | ||
807 | 0 | int getKeyboardNo2() |
808 | { | |
809 | 0 | try |
810 | { | |
811 | 0 | if (keyboardNo2 != null) |
812 | { | |
813 | 0 | int value = Integer.parseInt(keyboardNo2.toString()); |
814 | 0 | keyboardNo2 = null; |
815 | 0 | return value; |
816 | } | |
817 | } catch (Exception x) | |
818 | { | |
819 | } | |
820 | 0 | keyboardNo2 = null; |
821 | 0 | return 1; |
822 | } | |
823 | ||
824 | /** | |
825 | * DOCUMENT ME! | |
826 | * | |
827 | * @param evt | |
828 | * DOCUMENT ME! | |
829 | */ | |
830 | 0 | @Override |
831 | public void mouseReleased(MouseEvent evt) | |
832 | { | |
833 | 0 | MousePos pos = findMousePosition(evt); |
834 | 0 | if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1) |
835 | { | |
836 | 0 | return; |
837 | } | |
838 | ||
839 | 0 | boolean didDrag = mouseDragging; // did we come here after a drag |
840 | 0 | mouseDragging = false; |
841 | 0 | mouseWheelPressed = false; |
842 | ||
843 | 0 | if (evt.isPopupTrigger()) // Windows: mouseReleased |
844 | { | |
845 | 0 | showPopupMenu(evt, pos); |
846 | 0 | evt.consume(); |
847 | 0 | return; |
848 | } | |
849 | ||
850 | 0 | if (editingSeqs) |
851 | { | |
852 | 0 | endEditing(); |
853 | } | |
854 | else | |
855 | { | |
856 | 0 | doMouseReleasedDefineMode(evt, didDrag); |
857 | } | |
858 | } | |
859 | ||
860 | /** | |
861 | * DOCUMENT ME! | |
862 | * | |
863 | * @param evt | |
864 | * DOCUMENT ME! | |
865 | */ | |
866 | 0 | @Override |
867 | public void mousePressed(MouseEvent evt) | |
868 | { | |
869 | 0 | lastMousePress = evt.getPoint(); |
870 | 0 | MousePos pos = findMousePosition(evt); |
871 | 0 | if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1) |
872 | { | |
873 | 0 | return; |
874 | } | |
875 | ||
876 | 0 | if (SwingUtilities.isMiddleMouseButton(evt)) |
877 | { | |
878 | 0 | mouseWheelPressed = true; |
879 | 0 | return; |
880 | } | |
881 | ||
882 | 0 | boolean isControlDown = Platform.isControlDown(evt); |
883 | 0 | if (evt.isShiftDown() || isControlDown) |
884 | { | |
885 | 0 | editingSeqs = true; |
886 | 0 | if (isControlDown) |
887 | { | |
888 | 0 | groupEditing = true; |
889 | } | |
890 | } | |
891 | else | |
892 | { | |
893 | 0 | doMousePressedDefineMode(evt, pos); |
894 | 0 | return; |
895 | } | |
896 | ||
897 | 0 | int seq = pos.seqIndex; |
898 | 0 | int res = pos.column; |
899 | ||
900 | 0 | if ((seq < av.getAlignment().getHeight()) |
901 | && (res < av.getAlignment().getSequenceAt(seq).getLength())) | |
902 | { | |
903 | 0 | editStartSeq = seq; |
904 | 0 | editLastRes = res; |
905 | } | |
906 | else | |
907 | { | |
908 | 0 | editStartSeq = -1; |
909 | 0 | editLastRes = -1; |
910 | } | |
911 | ||
912 | 0 | return; |
913 | } | |
914 | ||
915 | String lastMessage; | |
916 | ||
917 | 0 | @Override |
918 | public void mouseOverSequence(SequenceI sequence, int index, int pos) | |
919 | { | |
920 | 0 | String tmp = sequence.hashCode() + " " + index + " " + pos; |
921 | ||
922 | 0 | if (lastMessage == null || !lastMessage.equals(tmp)) |
923 | { | |
924 | // jalview.bin.Console.errPrintln("mouseOver Sequence: "+tmp); | |
925 | 0 | ssm.mouseOverSequence(sequence, index, pos, av); |
926 | } | |
927 | 0 | lastMessage = tmp; |
928 | } | |
929 | ||
930 | /** | |
931 | * Highlight the mapped region described by the search results object (unless | |
932 | * unchanged). This supports highlight of protein while mousing over linked | |
933 | * cDNA and vice versa. The status bar is also updated to show the location of | |
934 | * the start of the highlighted region. | |
935 | */ | |
936 | 1 | @Override |
937 | public String highlightSequence(SearchResultsI results) | |
938 | { | |
939 | 1 | if (results == null || results.equals(lastSearchResults)) |
940 | { | |
941 | 0 | return null; |
942 | } | |
943 | 1 | lastSearchResults = results; |
944 | ||
945 | 1 | boolean wasScrolled = false; |
946 | ||
947 | 1 | if (av.isFollowHighlight()) |
948 | { | |
949 | // don't allow highlight of protein/cDNA to also scroll a complementary | |
950 | // panel,as this sets up a feedback loop (scrolling panel 1 causes moused | |
951 | // over residue to change abruptly, causing highlighted residue in panel 2 | |
952 | // to change, causing a scroll in panel 1 etc) | |
953 | 1 | ap.setToScrollComplementPanel(false); |
954 | 1 | wasScrolled = ap.scrollToPosition(results); |
955 | 1 | if (wasScrolled) |
956 | { | |
957 | 0 | seqCanvas.revalidate(); |
958 | } | |
959 | 1 | ap.setToScrollComplementPanel(true); |
960 | } | |
961 | ||
962 | 1 | boolean fastPaint = !(wasScrolled && av.getWrapAlignment()); |
963 | 1 | if (seqCanvas.highlightSearchResults(results, fastPaint)) |
964 | { | |
965 | 1 | setStatusMessage(results); |
966 | } | |
967 | 1 | return results.isEmpty() ? null : getHighlightInfo(results); |
968 | } | |
969 | ||
970 | /** | |
971 | * temporary hack: answers a message suitable to show on structure hover | |
972 | * label. This is normally null. It is a peptide variation description if | |
973 | * <ul> | |
974 | * <li>results are a single residue in a protein alignment</li> | |
975 | * <li>there is a mapping to a coding sequence (codon)</li> | |
976 | * <li>there are one or more SNP variant features on the codon</li> | |
977 | * </ul> | |
978 | * in which case the answer is of the format (e.g.) "p.Glu388Asp" | |
979 | * | |
980 | * @param results | |
981 | * @return | |
982 | */ | |
983 | 1 | private String getHighlightInfo(SearchResultsI results) |
984 | { | |
985 | /* | |
986 | * ideally, just find mapped CDS (as we don't care about render style here); | |
987 | * for now, go via split frame complement's FeatureRenderer | |
988 | */ | |
989 | 1 | AlignViewportI complement = ap.getAlignViewport().getCodingComplement(); |
990 | 1 | if (complement == null) |
991 | { | |
992 | 1 | return null; |
993 | } | |
994 | 0 | AlignFrame af = Desktop.getAlignFrameFor(complement); |
995 | 0 | FeatureRendererModel fr2 = af.getFeatureRenderer(); |
996 | ||
997 | 0 | List<SearchResultMatchI> matches = results.getResults(); |
998 | 0 | int j = matches.size(); |
999 | 0 | List<String> infos = new ArrayList<>(); |
1000 | 0 | for (int i = 0; i < j; i++) |
1001 | { | |
1002 | 0 | SearchResultMatchI match = matches.get(i); |
1003 | 0 | int pos = match.getStart(); |
1004 | 0 | if (pos == match.getEnd()) |
1005 | { | |
1006 | 0 | SequenceI seq = match.getSequence(); |
1007 | 0 | SequenceI ds = seq.getDatasetSequence() == null ? seq |
1008 | : seq.getDatasetSequence(); | |
1009 | 0 | MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos); |
1010 | 0 | if (mf != null) |
1011 | { | |
1012 | 0 | for (SequenceFeature sf : mf.features) |
1013 | { | |
1014 | 0 | String pv = mf.findProteinVariants(sf); |
1015 | 0 | if (pv.length() > 0 && !infos.contains(pv)) |
1016 | { | |
1017 | 0 | infos.add(pv); |
1018 | } | |
1019 | } | |
1020 | } | |
1021 | } | |
1022 | } | |
1023 | ||
1024 | 0 | if (infos.isEmpty()) |
1025 | { | |
1026 | 0 | return null; |
1027 | } | |
1028 | 0 | StringBuilder sb = new StringBuilder(); |
1029 | 0 | for (String info : infos) |
1030 | { | |
1031 | 0 | if (sb.length() > 0) |
1032 | { | |
1033 | 0 | sb.append("|"); |
1034 | } | |
1035 | 0 | sb.append(info); |
1036 | } | |
1037 | 0 | return sb.toString(); |
1038 | } | |
1039 | ||
1040 | 0 | @Override |
1041 | public VamsasSource getVamsasSource() | |
1042 | { | |
1043 | 0 | return this.ap == null ? null : this.ap.av; |
1044 | } | |
1045 | ||
1046 | 0 | @Override |
1047 | public void updateColours(SequenceI seq, int index) | |
1048 | { | |
1049 | 0 | jalview.bin.Console.outPrintln("update the seqPanel colours"); |
1050 | // repaint(); | |
1051 | } | |
1052 | ||
1053 | /** | |
1054 | * Action on mouse movement is to update the status bar to show the current | |
1055 | * sequence position, and (if features are shown) to show any features at the | |
1056 | * position in a tooltip. Does nothing if the mouse move does not change | |
1057 | * residue position. | |
1058 | * | |
1059 | * @param evt | |
1060 | */ | |
1061 | 0 | @Override |
1062 | public void mouseMoved(MouseEvent evt) | |
1063 | { | |
1064 | 0 | if (editingSeqs) |
1065 | { | |
1066 | // This is because MacOSX creates a mouseMoved | |
1067 | // If control is down, other platforms will not. | |
1068 | 0 | mouseDragged(evt); |
1069 | } | |
1070 | ||
1071 | 0 | final MousePos mousePos = findMousePosition(evt); |
1072 | 0 | if (mousePos.equals(lastMousePosition)) |
1073 | { | |
1074 | /* | |
1075 | * just a pixel move without change of 'cell' | |
1076 | */ | |
1077 | 0 | moveTooltip = false; |
1078 | 0 | return; |
1079 | } | |
1080 | 0 | moveTooltip = true; |
1081 | 0 | lastMousePosition = mousePos; |
1082 | ||
1083 | 0 | if (mousePos.isOverAnnotation()) |
1084 | { | |
1085 | 0 | mouseMovedOverAnnotation(mousePos); |
1086 | 0 | return; |
1087 | } | |
1088 | 0 | final int seq = mousePos.seqIndex; |
1089 | ||
1090 | 0 | final int column = mousePos.column; |
1091 | 0 | if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight()) |
1092 | { | |
1093 | 0 | lastMousePosition = null; |
1094 | 0 | setToolTipText(null); |
1095 | 0 | lastTooltip = null; |
1096 | 0 | lastFormattedTooltip = null; |
1097 | 0 | ap.alignFrame.setStatus(""); |
1098 | 0 | return; |
1099 | } | |
1100 | ||
1101 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(seq); |
1102 | ||
1103 | 0 | if (column >= sequence.getLength()) |
1104 | { | |
1105 | 0 | return; |
1106 | } | |
1107 | ||
1108 | /* | |
1109 | * set status bar message, returning residue position in sequence | |
1110 | */ | |
1111 | 0 | boolean isGapped = Comparison.isGap(sequence.getCharAt(column)); |
1112 | 0 | final int pos = setStatusMessage(sequence, column, seq); |
1113 | 0 | if (ssm != null && !isGapped) |
1114 | { | |
1115 | 0 | mouseOverSequence(sequence, column, pos); |
1116 | } | |
1117 | ||
1118 | 0 | StringBuilder tooltipText = new StringBuilder(64); |
1119 | ||
1120 | 0 | SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence); |
1121 | 0 | if (groups != null) |
1122 | { | |
1123 | 0 | for (int g = 0; g < groups.length; g++) |
1124 | { | |
1125 | 0 | if (groups[g].getStartRes() <= column |
1126 | && groups[g].getEndRes() >= column) | |
1127 | { | |
1128 | 0 | if (!groups[g].getName().startsWith("JTreeGroup") |
1129 | && !groups[g].getName().startsWith("JGroup")) | |
1130 | { | |
1131 | 0 | tooltipText.append(groups[g].getName()); |
1132 | } | |
1133 | ||
1134 | 0 | if (groups[g].getDescription() != null) |
1135 | { | |
1136 | 0 | tooltipText.append(": " + groups[g].getDescription()); |
1137 | } | |
1138 | } | |
1139 | } | |
1140 | } | |
1141 | ||
1142 | /* | |
1143 | * add any features at the position to the tooltip; if over a gap, only | |
1144 | * add features that straddle the gap (pos may be the residue before or | |
1145 | * after the gap) | |
1146 | */ | |
1147 | 0 | int unshownFeatures = 0; |
1148 | 0 | if (av.isShowSequenceFeatures()) |
1149 | { | |
1150 | 0 | List<SequenceFeature> features = ap.getFeatureRenderer() |
1151 | .findFeaturesAtColumn(sequence, column + 1); | |
1152 | 0 | unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features, |
1153 | this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH); | |
1154 | ||
1155 | /* | |
1156 | * add features in CDS/protein complement at the corresponding | |
1157 | * position if configured to do so | |
1158 | */ | |
1159 | 0 | if (av.isShowComplementFeatures()) |
1160 | { | |
1161 | 0 | if (!Comparison.isGap(sequence.getCharAt(column))) |
1162 | { | |
1163 | 0 | AlignViewportI complement = ap.getAlignViewport() |
1164 | .getCodingComplement(); | |
1165 | 0 | AlignFrame af = Desktop.getAlignFrameFor(complement); |
1166 | 0 | FeatureRendererModel fr2 = af.getFeatureRenderer(); |
1167 | 0 | MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence, |
1168 | pos); | |
1169 | 0 | if (mf != null) |
1170 | { | |
1171 | 0 | unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf, |
1172 | fr2, MAX_TOOLTIP_LENGTH); | |
1173 | } | |
1174 | } | |
1175 | } | |
1176 | } | |
1177 | 0 | if (tooltipText.length() == 0) // nothing added |
1178 | { | |
1179 | 0 | setToolTipText(null); |
1180 | 0 | lastTooltip = null; |
1181 | } | |
1182 | else | |
1183 | { | |
1184 | 0 | if (tooltipText.length() > MAX_TOOLTIP_LENGTH) |
1185 | { | |
1186 | 0 | tooltipText.setLength(MAX_TOOLTIP_LENGTH); |
1187 | 0 | tooltipText.append("..."); |
1188 | } | |
1189 | 0 | if (unshownFeatures > 0) |
1190 | { | |
1191 | 0 | tooltipText.append("<br/>").append("... ").append("<i>") |
1192 | .append(MessageManager.formatMessage( | |
1193 | "label.features_not_shown", unshownFeatures)) | |
1194 | .append("</i>"); | |
1195 | } | |
1196 | 0 | String textString = tooltipText.toString(); |
1197 | 0 | if (!textString.equals(lastTooltip)) |
1198 | { | |
1199 | 0 | lastTooltip = textString; |
1200 | 0 | lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString); |
1201 | 0 | setToolTipText(lastFormattedTooltip); |
1202 | } | |
1203 | } | |
1204 | } | |
1205 | ||
1206 | /** | |
1207 | * When the view is in wrapped mode, and the mouse is over an annotation row, | |
1208 | * shows the corresponding tooltip and status message (if any) | |
1209 | * | |
1210 | * @param pos | |
1211 | * @param column | |
1212 | */ | |
1213 | 0 | protected void mouseMovedOverAnnotation(MousePos pos) |
1214 | { | |
1215 | 0 | final int column = pos.column; |
1216 | 0 | final int rowIndex = pos.annotationIndex; |
1217 | ||
1218 | // TODO - get yOffset for annotation, too | |
1219 | 0 | if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation() |
1220 | || rowIndex < 0) | |
1221 | { | |
1222 | 0 | return; |
1223 | } | |
1224 | 0 | AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation(); |
1225 | ||
1226 | 0 | String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column, |
1227 | anns, 0, av, ap); | |
1228 | 0 | if (tooltip == null ? tooltip != lastTooltip |
1229 | : !tooltip.equals(lastTooltip)) | |
1230 | { | |
1231 | 0 | lastTooltip = tooltip; |
1232 | 0 | lastFormattedTooltip = tooltip == null ? null |
1233 | : JvSwingUtils.wrapTooltip(true, tooltip); | |
1234 | 0 | setToolTipText(lastFormattedTooltip); |
1235 | } | |
1236 | ||
1237 | 0 | String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column, |
1238 | anns[rowIndex], 0, av); | |
1239 | 0 | ap.alignFrame.setStatus(msg); |
1240 | } | |
1241 | ||
1242 | /* | |
1243 | * if Shift key is held down while moving the mouse, | |
1244 | * the tooltip location is not changed once shown | |
1245 | */ | |
1246 | private Point lastTooltipLocation = null; | |
1247 | ||
1248 | /* | |
1249 | * this flag is false for pixel moves within a residue, | |
1250 | * to reduce tooltip flicker | |
1251 | */ | |
1252 | private boolean moveTooltip = true; | |
1253 | ||
1254 | /* | |
1255 | * a dummy tooltip used to estimate where to position tooltips | |
1256 | */ | |
1257 | private JToolTip tempTip = new JLabel().createToolTip(); | |
1258 | ||
1259 | /* | |
1260 | * (non-Javadoc) | |
1261 | * | |
1262 | * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent) | |
1263 | */ | |
1264 | 0 | @Override |
1265 | public Point getToolTipLocation(MouseEvent event) | |
1266 | { | |
1267 | // BH 2018 | |
1268 | ||
1269 | 0 | if (lastTooltip == null || !moveTooltip) |
1270 | { | |
1271 | 0 | return null; |
1272 | } | |
1273 | ||
1274 | 0 | if (lastTooltipLocation != null && event.isShiftDown()) |
1275 | { | |
1276 | 0 | return lastTooltipLocation; |
1277 | } | |
1278 | ||
1279 | 0 | int x = event.getX(); |
1280 | 0 | int y = event.getY(); |
1281 | 0 | int w = getWidth(); |
1282 | ||
1283 | 0 | tempTip.setTipText(lastFormattedTooltip); |
1284 | 0 | int tipWidth = (int) tempTip.getPreferredSize().getWidth(); |
1285 | ||
1286 | // was x += (w - x < 200) ? -(w / 2) : 5; | |
1287 | 0 | x = (x + tipWidth < w ? x + 10 : w - tipWidth); |
1288 | 0 | Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20? |
1289 | ||
1290 | 0 | return lastTooltipLocation = p; |
1291 | } | |
1292 | ||
1293 | /** | |
1294 | * set when the current UI interaction has resulted in a change that requires | |
1295 | * shading in overviews and structures to be recalculated. this could be | |
1296 | * changed to a something more expressive that indicates what actually has | |
1297 | * changed, so selective redraws can be applied (ie. only structures, only | |
1298 | * overview, etc) | |
1299 | */ | |
1300 | private boolean updateOverviewAndStructs = false; // TODO: refactor to | |
1301 | // avcontroller | |
1302 | ||
1303 | /** | |
1304 | * set if av.getSelectionGroup() refers to a group that is defined on the | |
1305 | * alignment view, rather than a transient selection | |
1306 | */ | |
1307 | // private boolean editingDefinedGroup = false; // TODO: refactor to | |
1308 | // avcontroller or viewModel | |
1309 | ||
1310 | /** | |
1311 | * Sets the status message in alignment panel, showing the sequence number | |
1312 | * (index) and id, and residue and residue position if not at a gap, for the | |
1313 | * given sequence and column position. Returns the residue position returned | |
1314 | * by Sequence.findPosition. Note this may be for the nearest adjacent residue | |
1315 | * if at a gapped position. | |
1316 | * | |
1317 | * @param sequence | |
1318 | * aligned sequence object | |
1319 | * @param column | |
1320 | * alignment column | |
1321 | * @param seqIndex | |
1322 | * index of sequence in alignment | |
1323 | * @return sequence position of residue at column, or adjacent residue if at a | |
1324 | * gap | |
1325 | */ | |
1326 | 5 | int setStatusMessage(SequenceI sequence, final int column, int seqIndex) |
1327 | { | |
1328 | 5 | char sequenceChar = sequence.getCharAt(column); |
1329 | 5 | int pos = sequence.findPosition(column); |
1330 | 5 | setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos); |
1331 | ||
1332 | 5 | return pos; |
1333 | } | |
1334 | ||
1335 | /** | |
1336 | * Builds the status message for the current cursor location and writes it to | |
1337 | * the status bar, for example | |
1338 | * | |
1339 | * <pre> | |
1340 | * Sequence 3 ID: FER1_SOLLC | |
1341 | * Sequence 5 ID: FER1_PEA Residue: THR (4) | |
1342 | * Sequence 5 ID: FER1_PEA Residue: B (3) | |
1343 | * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2) | |
1344 | * </pre> | |
1345 | * | |
1346 | * @param seqName | |
1347 | * @param seqIndex | |
1348 | * sequence position in the alignment (1..) | |
1349 | * @param sequenceChar | |
1350 | * the character under the cursor | |
1351 | * @param residuePos | |
1352 | * the sequence residue position (if not over a gap) | |
1353 | */ | |
1354 | 6 | protected void setStatusMessage(String seqName, int seqIndex, |
1355 | char sequenceChar, int residuePos) | |
1356 | { | |
1357 | 6 | StringBuilder text = new StringBuilder(32); |
1358 | ||
1359 | /* | |
1360 | * Sequence number (if known), and sequence name. | |
1361 | */ | |
1362 | 6 | String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1); |
1363 | 6 | text.append("Sequence").append(seqno).append(" ID: ").append(seqName); |
1364 | ||
1365 | 6 | String residue = null; |
1366 | ||
1367 | /* | |
1368 | * Try to translate the display character to residue name (null for gap). | |
1369 | */ | |
1370 | 6 | boolean isGapped = Comparison.isGap(sequenceChar); |
1371 | ||
1372 | 6 | if (!isGapped) |
1373 | { | |
1374 | 4 | boolean nucleotide = av.getAlignment().isNucleotide(); |
1375 | 4 | String displayChar = String.valueOf(sequenceChar); |
1376 | 4 | if (nucleotide) |
1377 | { | |
1378 | 0 | residue = ResidueProperties.nucleotideName.get(displayChar); |
1379 | } | |
1380 | else | |
1381 | { | |
1382 | 4 | residue = "X".equalsIgnoreCase(displayChar) ? "X" |
1383 | 4 | : ("*".equals(displayChar) ? "STOP" |
1384 | : ResidueProperties.aa2Triplet.get(displayChar)); | |
1385 | } | |
1386 | 4 | text.append(" ").append(nucleotide ? "Nucleotide" : "Residue") |
1387 | 4 | .append(": ").append(residue == null ? displayChar : residue); |
1388 | ||
1389 | 4 | text.append(" (").append(Integer.toString(residuePos)).append(")"); |
1390 | } | |
1391 | 6 | ap.alignFrame.setStatus(text.toString()); |
1392 | } | |
1393 | ||
1394 | /** | |
1395 | * Set the status bar message to highlight the first matched position in | |
1396 | * search results. | |
1397 | * | |
1398 | * @param results | |
1399 | */ | |
1400 | 1 | private void setStatusMessage(SearchResultsI results) |
1401 | { | |
1402 | 1 | AlignmentI al = this.av.getAlignment(); |
1403 | 1 | int sequenceIndex = al.findIndex(results); |
1404 | 1 | if (sequenceIndex == -1) |
1405 | { | |
1406 | 0 | return; |
1407 | } | |
1408 | 1 | SequenceI alignedSeq = al.getSequenceAt(sequenceIndex); |
1409 | 1 | SequenceI ds = alignedSeq.getDatasetSequence(); |
1410 | 1 | for (SearchResultMatchI m : results.getResults()) |
1411 | { | |
1412 | 1 | SequenceI seq = m.getSequence(); |
1413 | 1 | if (seq.getDatasetSequence() != null) |
1414 | { | |
1415 | 1 | seq = seq.getDatasetSequence(); |
1416 | } | |
1417 | ||
1418 | 1 | if (seq == ds) |
1419 | { | |
1420 | 1 | int start = m.getStart(); |
1421 | 1 | setStatusMessage(alignedSeq.getName(), sequenceIndex, |
1422 | seq.getCharAt(start - 1), start); | |
1423 | 1 | return; |
1424 | } | |
1425 | } | |
1426 | } | |
1427 | ||
1428 | /** | |
1429 | * {@inheritDoc} | |
1430 | */ | |
1431 | 0 | @Override |
1432 | public void mouseDragged(MouseEvent evt) | |
1433 | { | |
1434 | 0 | MousePos pos = findMousePosition(evt); |
1435 | 0 | if (pos.isOverAnnotation() || pos.column == -1) |
1436 | { | |
1437 | 0 | return; |
1438 | } | |
1439 | ||
1440 | 0 | if (mouseWheelPressed) |
1441 | { | |
1442 | 0 | boolean inSplitFrame = ap.av.getCodingComplement() != null; |
1443 | 0 | boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna(); |
1444 | ||
1445 | 0 | int oldWidth = av.getCharWidth(); |
1446 | ||
1447 | // Which is bigger, left-right or up-down? | |
1448 | 0 | if (Math.abs(evt.getY() - lastMousePress.getY()) > Math |
1449 | .abs(evt.getX() - lastMousePress.getX())) | |
1450 | { | |
1451 | /* | |
1452 | * on drag up or down, decrement or increment font size | |
1453 | */ | |
1454 | 0 | int fontSize = av.font.getSize(); |
1455 | 0 | boolean fontChanged = false; |
1456 | ||
1457 | 0 | if (evt.getY() < lastMousePress.getY()) |
1458 | { | |
1459 | 0 | fontChanged = true; |
1460 | 0 | fontSize--; |
1461 | } | |
1462 | 0 | else if (evt.getY() > lastMousePress.getY()) |
1463 | { | |
1464 | 0 | fontChanged = true; |
1465 | 0 | fontSize++; |
1466 | } | |
1467 | ||
1468 | 0 | if (fontSize < 1) |
1469 | { | |
1470 | 0 | fontSize = 1; |
1471 | } | |
1472 | ||
1473 | 0 | if (fontChanged) |
1474 | { | |
1475 | 0 | Font newFont = new Font(av.font.getName(), av.font.getStyle(), |
1476 | fontSize); | |
1477 | 0 | av.setFont(newFont, true); |
1478 | 0 | av.setCharWidth(oldWidth); |
1479 | 0 | ap.fontChanged(); |
1480 | 0 | if (copyChanges) |
1481 | { | |
1482 | 0 | ap.av.getCodingComplement().setFont(newFont, true); |
1483 | 0 | SplitFrame splitFrame = (SplitFrame) ap.alignFrame |
1484 | .getSplitViewContainer(); | |
1485 | 0 | splitFrame.adjustLayout(); |
1486 | 0 | splitFrame.repaint(); |
1487 | } | |
1488 | } | |
1489 | } | |
1490 | else | |
1491 | { | |
1492 | /* | |
1493 | * on drag left or right, decrement or increment character width | |
1494 | */ | |
1495 | 0 | int newWidth = 0; |
1496 | 0 | if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1) |
1497 | { | |
1498 | 0 | newWidth = av.getCharWidth() - 1; |
1499 | 0 | av.setCharWidth(newWidth); |
1500 | } | |
1501 | 0 | else if (evt.getX() > lastMousePress.getX()) |
1502 | { | |
1503 | 0 | newWidth = av.getCharWidth() + 1; |
1504 | 0 | av.setCharWidth(newWidth); |
1505 | } | |
1506 | 0 | if (newWidth > 0) |
1507 | { | |
1508 | 0 | ap.paintAlignment(false, false); |
1509 | 0 | if (copyChanges) |
1510 | { | |
1511 | /* | |
1512 | * need to ensure newWidth is set on cdna, regardless of which | |
1513 | * panel the mouse drag happened in; protein will compute its | |
1514 | * character width as 1:1 or 3:1 | |
1515 | */ | |
1516 | 0 | av.getCodingComplement().setCharWidth(newWidth); |
1517 | 0 | SplitFrame splitFrame = (SplitFrame) ap.alignFrame |
1518 | .getSplitViewContainer(); | |
1519 | 0 | splitFrame.adjustLayout(); |
1520 | 0 | splitFrame.repaint(); |
1521 | } | |
1522 | } | |
1523 | } | |
1524 | ||
1525 | 0 | FontMetrics fm = getFontMetrics(av.getFont()); |
1526 | 0 | av.validCharWidth = fm.charWidth('M') <= av.getCharWidth(); |
1527 | ||
1528 | 0 | lastMousePress = evt.getPoint(); |
1529 | ||
1530 | 0 | return; |
1531 | } | |
1532 | ||
1533 | 0 | if (!editingSeqs) |
1534 | { | |
1535 | 0 | dragStretchGroup(evt); |
1536 | 0 | return; |
1537 | } | |
1538 | ||
1539 | 0 | int res = pos.column; |
1540 | ||
1541 | 0 | if (res < 0) |
1542 | { | |
1543 | 0 | res = 0; |
1544 | } | |
1545 | ||
1546 | 0 | if ((editLastRes == -1) || (editLastRes == res)) |
1547 | { | |
1548 | 0 | return; |
1549 | } | |
1550 | ||
1551 | 0 | if ((res < av.getAlignment().getWidth()) && (res < editLastRes)) |
1552 | { | |
1553 | // dragLeft, delete gap | |
1554 | 0 | editSequence(false, false, res); |
1555 | } | |
1556 | else | |
1557 | { | |
1558 | 0 | editSequence(true, false, res); |
1559 | } | |
1560 | ||
1561 | 0 | mouseDragging = true; |
1562 | 0 | if (scrollThread != null) |
1563 | { | |
1564 | 0 | scrollThread.setMousePosition(evt.getPoint()); |
1565 | } | |
1566 | } | |
1567 | ||
1568 | /** | |
1569 | * Edits the sequence to insert or delete one or more gaps, in response to a | |
1570 | * mouse drag or cursor mode command. The number of inserts/deletes may be | |
1571 | * specified with the cursor command, or else depends on the mouse event | |
1572 | * (normally one column, but potentially more for a fast mouse drag). | |
1573 | * <p> | |
1574 | * Delete gaps is limited to the number of gaps left of the cursor position | |
1575 | * (mouse drag), or at or right of the cursor position (cursor mode). | |
1576 | * <p> | |
1577 | * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in | |
1578 | * the current selection group. | |
1579 | * <p> | |
1580 | * In locked editing mode (with a selection group present), inserts/deletions | |
1581 | * within the selection group are limited to its boundaries (and edits outside | |
1582 | * the group stop at its border). | |
1583 | * | |
1584 | * @param insertGap | |
1585 | * true to insert gaps, false to delete gaps | |
1586 | * @param editSeq | |
1587 | * (unused parameter) | |
1588 | * @param startres | |
1589 | * the column at which to perform the action; the number of columns | |
1590 | * affected depends on <code>this.editLastRes</code> (cursor column | |
1591 | * position) | |
1592 | */ | |
1593 | 0 | synchronized void editSequence(boolean insertGap, boolean editSeq, |
1594 | final int startres) | |
1595 | { | |
1596 | 0 | int fixedLeft = -1; |
1597 | 0 | int fixedRight = -1; |
1598 | 0 | boolean fixedColumns = false; |
1599 | 0 | SequenceGroup sg = av.getSelectionGroup(); |
1600 | ||
1601 | 0 | final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq); |
1602 | ||
1603 | // No group, but the sequence may represent a group | |
1604 | 0 | if (!groupEditing && av.hasHiddenRows()) |
1605 | { | |
1606 | 0 | if (av.isHiddenRepSequence(seq)) |
1607 | { | |
1608 | 0 | sg = av.getRepresentedSequences(seq); |
1609 | 0 | groupEditing = true; |
1610 | } | |
1611 | } | |
1612 | ||
1613 | 0 | StringBuilder message = new StringBuilder(64); // for status bar |
1614 | ||
1615 | /* | |
1616 | * make a name for the edit action, for | |
1617 | * status bar message and Undo/Redo menu | |
1618 | */ | |
1619 | 0 | String label = null; |
1620 | 0 | if (groupEditing) |
1621 | { | |
1622 | 0 | message.append("Edit group:"); |
1623 | 0 | label = MessageManager.getString("action.edit_group"); |
1624 | } | |
1625 | else | |
1626 | { | |
1627 | 0 | message.append("Edit sequence: " + seq.getName()); |
1628 | 0 | label = seq.getName(); |
1629 | 0 | if (label.length() > 10) |
1630 | { | |
1631 | 0 | label = label.substring(0, 10); |
1632 | } | |
1633 | 0 | label = MessageManager.formatMessage("label.edit_params", |
1634 | new String[] | |
1635 | { label }); | |
1636 | } | |
1637 | ||
1638 | /* | |
1639 | * initialise the edit command if there is not | |
1640 | * already one being extended | |
1641 | */ | |
1642 | 0 | if (editCommand == null) |
1643 | { | |
1644 | 0 | editCommand = new EditCommand(label); |
1645 | } | |
1646 | ||
1647 | 0 | if (insertGap) |
1648 | { | |
1649 | 0 | message.append(" insert "); |
1650 | } | |
1651 | else | |
1652 | { | |
1653 | 0 | message.append(" delete "); |
1654 | } | |
1655 | ||
1656 | 0 | message.append(Math.abs(startres - editLastRes) + " gaps."); |
1657 | 0 | ap.alignFrame.setStatus(message.toString()); |
1658 | ||
1659 | /* | |
1660 | * is there a selection group containing the sequence being edited? | |
1661 | * if so the boundary of the group is the limit of the edit | |
1662 | * (but the edit may be inside or outside the selection group) | |
1663 | */ | |
1664 | 0 | boolean inSelectionGroup = sg != null |
1665 | && sg.getSequences(av.getHiddenRepSequences()).contains(seq); | |
1666 | 0 | if (groupEditing || inSelectionGroup) |
1667 | { | |
1668 | 0 | fixedColumns = true; |
1669 | ||
1670 | // sg might be null as the user may only see 1 sequence, | |
1671 | // but the sequence represents a group | |
1672 | 0 | if (sg == null) |
1673 | { | |
1674 | 0 | if (!av.isHiddenRepSequence(seq)) |
1675 | { | |
1676 | 0 | endEditing(); |
1677 | 0 | return; |
1678 | } | |
1679 | 0 | sg = av.getRepresentedSequences(seq); |
1680 | } | |
1681 | ||
1682 | 0 | fixedLeft = sg.getStartRes(); |
1683 | 0 | fixedRight = sg.getEndRes(); |
1684 | ||
1685 | 0 | if ((startres < fixedLeft && editLastRes >= fixedLeft) |
1686 | || (startres >= fixedLeft && editLastRes < fixedLeft) | |
1687 | || (startres > fixedRight && editLastRes <= fixedRight) | |
1688 | || (startres <= fixedRight && editLastRes > fixedRight)) | |
1689 | { | |
1690 | 0 | endEditing(); |
1691 | 0 | return; |
1692 | } | |
1693 | ||
1694 | 0 | if (fixedLeft > startres) |
1695 | { | |
1696 | 0 | fixedRight = fixedLeft - 1; |
1697 | 0 | fixedLeft = 0; |
1698 | } | |
1699 | 0 | else if (fixedRight < startres) |
1700 | { | |
1701 | 0 | fixedLeft = fixedRight; |
1702 | 0 | fixedRight = -1; |
1703 | } | |
1704 | } | |
1705 | ||
1706 | 0 | if (av.hasHiddenColumns()) |
1707 | { | |
1708 | 0 | fixedColumns = true; |
1709 | 0 | int y1 = av.getAlignment().getHiddenColumns() |
1710 | .getNextHiddenBoundary(true, startres); | |
1711 | 0 | int y2 = av.getAlignment().getHiddenColumns() |
1712 | .getNextHiddenBoundary(false, startres); | |
1713 | ||
1714 | 0 | if ((insertGap && startres > y1 && editLastRes < y1) |
1715 | || (!insertGap && startres < y2 && editLastRes > y2)) | |
1716 | { | |
1717 | 0 | endEditing(); |
1718 | 0 | return; |
1719 | } | |
1720 | ||
1721 | // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~"); | |
1722 | // Selection spans a hidden region | |
1723 | 0 | if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1)) |
1724 | { | |
1725 | 0 | if (startres >= y2) |
1726 | { | |
1727 | 0 | fixedLeft = y2; |
1728 | } | |
1729 | else | |
1730 | { | |
1731 | 0 | fixedRight = y2 - 1; |
1732 | } | |
1733 | } | |
1734 | } | |
1735 | ||
1736 | 0 | boolean success = doEditSequence(insertGap, editSeq, startres, |
1737 | fixedRight, fixedColumns, sg); | |
1738 | ||
1739 | /* | |
1740 | * report what actually happened (might be less than | |
1741 | * what was requested), by inspecting the edit commands added | |
1742 | */ | |
1743 | 0 | String msg = getEditStatusMessage(editCommand); |
1744 | 0 | ap.alignFrame.setStatus(msg == null ? " " : msg); |
1745 | 0 | if (!success) |
1746 | { | |
1747 | 0 | endEditing(); |
1748 | } | |
1749 | ||
1750 | 0 | editLastRes = startres; |
1751 | 0 | seqCanvas.repaint(); |
1752 | } | |
1753 | ||
1754 | /** | |
1755 | * A helper method that performs the requested editing to insert or delete | |
1756 | * gaps (if possible). Answers true if the edit was successful, false if could | |
1757 | * only be performed in part or not at all. Failure may occur in 'locked edit' | |
1758 | * mode, when an insertion requires a matching gapped position (or column) to | |
1759 | * delete, and deletion requires an adjacent gapped position (or column) to | |
1760 | * remove. | |
1761 | * | |
1762 | * @param insertGap | |
1763 | * true if inserting gap(s), false if deleting | |
1764 | * @param editSeq | |
1765 | * (unused parameter, currently always false) | |
1766 | * @param startres | |
1767 | * the column at which to perform the edit | |
1768 | * @param fixedRight | |
1769 | * fixed right boundary column of a locked edit (within or to the | |
1770 | * left of a selection group) | |
1771 | * @param fixedColumns | |
1772 | * true if this is a locked edit | |
1773 | * @param sg | |
1774 | * the sequence group (if group edit is being performed) | |
1775 | * @return | |
1776 | */ | |
1777 | 0 | protected boolean doEditSequence(final boolean insertGap, |
1778 | final boolean editSeq, final int startres, int fixedRight, | |
1779 | final boolean fixedColumns, final SequenceGroup sg) | |
1780 | { | |
1781 | 0 | final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq); |
1782 | 0 | SequenceI[] seqs = new SequenceI[] { seq }; |
1783 | ||
1784 | 0 | if (groupEditing) |
1785 | { | |
1786 | 0 | List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences()); |
1787 | 0 | int g, groupSize = vseqs.size(); |
1788 | 0 | SequenceI[] groupSeqs = new SequenceI[groupSize]; |
1789 | 0 | for (g = 0; g < groupSeqs.length; g++) |
1790 | { | |
1791 | 0 | groupSeqs[g] = vseqs.get(g); |
1792 | } | |
1793 | ||
1794 | // drag to right | |
1795 | 0 | if (insertGap) |
1796 | { | |
1797 | // If the user has selected the whole sequence, and is dragging to | |
1798 | // the right, we can still extend the alignment and selectionGroup | |
1799 | 0 | if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight |
1800 | && sg.getEndRes() == av.getAlignment().getWidth() - 1) | |
1801 | { | |
1802 | 0 | sg.setEndRes( |
1803 | av.getAlignment().getWidth() + startres - editLastRes); | |
1804 | 0 | fixedRight = sg.getEndRes(); |
1805 | } | |
1806 | ||
1807 | // Is it valid with fixed columns?? | |
1808 | // Find the next gap before the end | |
1809 | // of the visible region boundary | |
1810 | 0 | boolean blank = false; |
1811 | 0 | for (; fixedRight > editLastRes; fixedRight--) |
1812 | { | |
1813 | 0 | blank = true; |
1814 | ||
1815 | 0 | for (g = 0; g < groupSize; g++) |
1816 | { | |
1817 | 0 | for (int j = 0; j < startres - editLastRes; j++) |
1818 | { | |
1819 | 0 | if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j))) |
1820 | { | |
1821 | 0 | blank = false; |
1822 | 0 | break; |
1823 | } | |
1824 | } | |
1825 | } | |
1826 | 0 | if (blank) |
1827 | { | |
1828 | 0 | break; |
1829 | } | |
1830 | } | |
1831 | ||
1832 | 0 | if (!blank) |
1833 | { | |
1834 | 0 | if (sg.getSize() == av.getAlignment().getHeight()) |
1835 | { | |
1836 | 0 | if ((av.hasHiddenColumns() |
1837 | && startres < av.getAlignment().getHiddenColumns() | |
1838 | .getNextHiddenBoundary(false, startres))) | |
1839 | { | |
1840 | 0 | return false; |
1841 | } | |
1842 | ||
1843 | 0 | int alWidth = av.getAlignment().getWidth(); |
1844 | 0 | if (av.hasHiddenRows()) |
1845 | { | |
1846 | 0 | int hwidth = av.getAlignment().getHiddenSequences() |
1847 | .getWidth(); | |
1848 | 0 | if (hwidth > alWidth) |
1849 | { | |
1850 | 0 | alWidth = hwidth; |
1851 | } | |
1852 | } | |
1853 | // We can still insert gaps if the selectionGroup | |
1854 | // contains all the sequences | |
1855 | 0 | sg.setEndRes(sg.getEndRes() + startres - editLastRes); |
1856 | 0 | fixedRight = alWidth + startres - editLastRes; |
1857 | } | |
1858 | else | |
1859 | { | |
1860 | 0 | return false; |
1861 | } | |
1862 | } | |
1863 | } | |
1864 | ||
1865 | // drag to left | |
1866 | 0 | else if (!insertGap) |
1867 | { | |
1868 | // / Are we able to delete? | |
1869 | // ie are all columns blank? | |
1870 | ||
1871 | 0 | for (g = 0; g < groupSize; g++) |
1872 | { | |
1873 | 0 | for (int j = startres; j < editLastRes; j++) |
1874 | { | |
1875 | 0 | if (groupSeqs[g].getLength() <= j) |
1876 | { | |
1877 | 0 | continue; |
1878 | } | |
1879 | ||
1880 | 0 | if (!Comparison.isGap(groupSeqs[g].getCharAt(j))) |
1881 | { | |
1882 | // Not a gap, block edit not valid | |
1883 | 0 | return false; |
1884 | } | |
1885 | } | |
1886 | } | |
1887 | } | |
1888 | ||
1889 | 0 | if (insertGap) |
1890 | { | |
1891 | // dragging to the right | |
1892 | 0 | if (fixedColumns && fixedRight != -1) |
1893 | { | |
1894 | 0 | for (int j = editLastRes; j < startres; j++) |
1895 | { | |
1896 | 0 | insertGap(j, groupSeqs, fixedRight); |
1897 | } | |
1898 | } | |
1899 | else | |
1900 | { | |
1901 | 0 | appendEdit(Action.INSERT_GAP, groupSeqs, startres, |
1902 | startres - editLastRes, false); | |
1903 | } | |
1904 | } | |
1905 | else | |
1906 | { | |
1907 | // dragging to the left | |
1908 | 0 | if (fixedColumns && fixedRight != -1) |
1909 | { | |
1910 | 0 | for (int j = editLastRes; j > startres; j--) |
1911 | { | |
1912 | 0 | deleteChar(startres, groupSeqs, fixedRight); |
1913 | } | |
1914 | } | |
1915 | else | |
1916 | { | |
1917 | 0 | appendEdit(Action.DELETE_GAP, groupSeqs, startres, |
1918 | editLastRes - startres, false); | |
1919 | } | |
1920 | } | |
1921 | } | |
1922 | else | |
1923 | { | |
1924 | /* | |
1925 | * editing a single sequence | |
1926 | */ | |
1927 | 0 | if (insertGap) |
1928 | { | |
1929 | // dragging to the right | |
1930 | 0 | if (fixedColumns && fixedRight != -1) |
1931 | { | |
1932 | 0 | for (int j = editLastRes; j < startres; j++) |
1933 | { | |
1934 | 0 | if (!insertGap(j, seqs, fixedRight)) |
1935 | { | |
1936 | /* | |
1937 | * e.g. cursor mode command specified | |
1938 | * more inserts than are possible | |
1939 | */ | |
1940 | 0 | return false; |
1941 | } | |
1942 | } | |
1943 | } | |
1944 | else | |
1945 | { | |
1946 | 0 | appendEdit(Action.INSERT_GAP, seqs, editLastRes, |
1947 | startres - editLastRes, false); | |
1948 | } | |
1949 | } | |
1950 | else | |
1951 | { | |
1952 | 0 | if (!editSeq) |
1953 | { | |
1954 | // dragging to the left | |
1955 | 0 | if (fixedColumns && fixedRight != -1) |
1956 | { | |
1957 | 0 | for (int j = editLastRes; j > startres; j--) |
1958 | { | |
1959 | 0 | if (!Comparison.isGap(seq.getCharAt(startres))) |
1960 | { | |
1961 | 0 | return false; |
1962 | } | |
1963 | 0 | deleteChar(startres, seqs, fixedRight); |
1964 | } | |
1965 | } | |
1966 | else | |
1967 | { | |
1968 | // could be a keyboard edit trying to delete none gaps | |
1969 | 0 | int max = 0; |
1970 | 0 | for (int m = startres; m < editLastRes; m++) |
1971 | { | |
1972 | 0 | if (!Comparison.isGap(seq.getCharAt(m))) |
1973 | { | |
1974 | 0 | break; |
1975 | } | |
1976 | 0 | max++; |
1977 | } | |
1978 | 0 | if (max > 0) |
1979 | { | |
1980 | 0 | appendEdit(Action.DELETE_GAP, seqs, startres, max, false); |
1981 | } | |
1982 | } | |
1983 | } | |
1984 | else | |
1985 | {// insertGap==false AND editSeq==TRUE; | |
1986 | 0 | if (fixedColumns && fixedRight != -1) |
1987 | { | |
1988 | 0 | for (int j = editLastRes; j < startres; j++) |
1989 | { | |
1990 | 0 | insertGap(j, seqs, fixedRight); |
1991 | } | |
1992 | } | |
1993 | else | |
1994 | { | |
1995 | 0 | appendEdit(Action.INSERT_NUC, seqs, editLastRes, |
1996 | startres - editLastRes, false); | |
1997 | } | |
1998 | } | |
1999 | } | |
2000 | } | |
2001 | ||
2002 | 0 | return true; |
2003 | } | |
2004 | ||
2005 | /** | |
2006 | * Constructs an informative status bar message while dragging to insert or | |
2007 | * delete gaps. Answers null if inserts and deletes cancel out. | |
2008 | * | |
2009 | * @param editCommand | |
2010 | * a command containing the list of individual edits | |
2011 | * @return | |
2012 | */ | |
2013 | 14 | protected static String getEditStatusMessage(EditCommand editCommand) |
2014 | { | |
2015 | 14 | if (editCommand == null) |
2016 | { | |
2017 | 1 | return null; |
2018 | } | |
2019 | ||
2020 | /* | |
2021 | * add any inserts, and subtract any deletes, | |
2022 | * not counting those auto-inserted when doing a 'locked edit' | |
2023 | * (so only counting edits 'under the cursor') | |
2024 | */ | |
2025 | 13 | int count = 0; |
2026 | 13 | for (Edit cmd : editCommand.getEdits()) |
2027 | { | |
2028 | 63 | if (!cmd.isSystemGenerated()) |
2029 | { | |
2030 | 42 | count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber() |
2031 | : -cmd.getNumber(); | |
2032 | } | |
2033 | } | |
2034 | ||
2035 | 13 | if (count == 0) |
2036 | { | |
2037 | /* | |
2038 | * inserts and deletes cancel out | |
2039 | */ | |
2040 | 3 | return null; |
2041 | } | |
2042 | ||
2043 | 10 | String msgKey = count > 1 ? "label.insert_gaps" |
2044 | 7 | : (count == 1 ? "label.insert_gap" |
2045 | 4 | : (count == -1 ? "label.delete_gap" |
2046 | : "label.delete_gaps")); | |
2047 | 10 | count = Math.abs(count); |
2048 | ||
2049 | 10 | return MessageManager.formatMessage(msgKey, String.valueOf(count)); |
2050 | } | |
2051 | ||
2052 | /** | |
2053 | * Inserts one gap at column j, deleting the right-most gapped column up to | |
2054 | * (and including) fixedColumn. Returns true if the edit is successful, false | |
2055 | * if no blank column is available to allow the insertion to be balanced by a | |
2056 | * deletion. | |
2057 | * | |
2058 | * @param j | |
2059 | * @param seq | |
2060 | * @param fixedColumn | |
2061 | * @return | |
2062 | */ | |
2063 | 0 | boolean insertGap(int j, SequenceI[] seq, int fixedColumn) |
2064 | { | |
2065 | 0 | int blankColumn = fixedColumn; |
2066 | 0 | for (int s = 0; s < seq.length; s++) |
2067 | { | |
2068 | // Find the next gap before the end of the visible region boundary | |
2069 | // If lastCol > j, theres a boundary after the gap insertion | |
2070 | ||
2071 | 0 | for (blankColumn = fixedColumn; blankColumn > j; blankColumn--) |
2072 | { | |
2073 | 0 | if (Comparison.isGap(seq[s].getCharAt(blankColumn))) |
2074 | { | |
2075 | // Theres a space, so break and insert the gap | |
2076 | 0 | break; |
2077 | } | |
2078 | } | |
2079 | ||
2080 | 0 | if (blankColumn <= j) |
2081 | { | |
2082 | 0 | blankColumn = fixedColumn; |
2083 | 0 | endEditing(); |
2084 | 0 | return false; |
2085 | } | |
2086 | } | |
2087 | ||
2088 | 0 | appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true); |
2089 | ||
2090 | 0 | appendEdit(Action.INSERT_GAP, seq, j, 1, false); |
2091 | ||
2092 | 0 | return true; |
2093 | } | |
2094 | ||
2095 | /** | |
2096 | * Helper method to add and perform one edit action | |
2097 | * | |
2098 | * @param action | |
2099 | * @param seq | |
2100 | * @param pos | |
2101 | * @param count | |
2102 | * @param systemGenerated | |
2103 | * true if the edit is a 'balancing' delete (or insert) to match a | |
2104 | * user's insert (or delete) in a locked editing region | |
2105 | */ | |
2106 | 0 | protected void appendEdit(Action action, SequenceI[] seq, int pos, |
2107 | int count, boolean systemGenerated) | |
2108 | { | |
2109 | ||
2110 | 0 | final Edit edit = new EditCommand().new Edit(action, seq, pos, count, |
2111 | av.getAlignment().getGapCharacter()); | |
2112 | 0 | edit.setSystemGenerated(systemGenerated); |
2113 | ||
2114 | 0 | editCommand.appendEdit(edit, av.getAlignment(), true, null); |
2115 | } | |
2116 | ||
2117 | /** | |
2118 | * Deletes the character at column j, and inserts a gap at fixedColumn, in | |
2119 | * each of the given sequences. The caller should ensure that all sequences | |
2120 | * are gapped in column j. | |
2121 | * | |
2122 | * @param j | |
2123 | * @param seqs | |
2124 | * @param fixedColumn | |
2125 | */ | |
2126 | 0 | void deleteChar(int j, SequenceI[] seqs, int fixedColumn) |
2127 | { | |
2128 | 0 | appendEdit(Action.DELETE_GAP, seqs, j, 1, false); |
2129 | ||
2130 | 0 | appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true); |
2131 | } | |
2132 | ||
2133 | /** | |
2134 | * On reentering the panel, stops any scrolling that was started on dragging | |
2135 | * out of the panel | |
2136 | * | |
2137 | * @param e | |
2138 | */ | |
2139 | 0 | @Override |
2140 | public void mouseEntered(MouseEvent e) | |
2141 | { | |
2142 | 0 | if (oldSeq < 0) |
2143 | { | |
2144 | 0 | oldSeq = 0; |
2145 | } | |
2146 | 0 | stopScrolling(); |
2147 | } | |
2148 | ||
2149 | /** | |
2150 | * On leaving the panel, if the mouse is being dragged, starts a thread to | |
2151 | * scroll it until the mouse is released (in unwrapped mode only) | |
2152 | * | |
2153 | * @param e | |
2154 | */ | |
2155 | 0 | @Override |
2156 | public void mouseExited(MouseEvent e) | |
2157 | { | |
2158 | 0 | lastMousePosition = null; |
2159 | 0 | ap.alignFrame.setStatus(" "); |
2160 | 0 | if (av.getWrapAlignment()) |
2161 | { | |
2162 | 0 | return; |
2163 | } | |
2164 | ||
2165 | 0 | if (mouseDragging && scrollThread == null) |
2166 | { | |
2167 | 0 | startScrolling(e.getPoint()); |
2168 | } | |
2169 | } | |
2170 | ||
2171 | /** | |
2172 | * Handler for double-click on a position with one or more sequence features. | |
2173 | * Opens the Amend Features dialog to allow feature details to be amended, or | |
2174 | * the feature deleted. | |
2175 | */ | |
2176 | 0 | @Override |
2177 | public void mouseClicked(MouseEvent evt) | |
2178 | { | |
2179 | 0 | SequenceGroup sg = null; |
2180 | 0 | MousePos pos = findMousePosition(evt); |
2181 | 0 | if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1) |
2182 | { | |
2183 | 0 | return; |
2184 | } | |
2185 | ||
2186 | 0 | if (evt.getClickCount() > 1 && av.isShowSequenceFeatures()) |
2187 | { | |
2188 | 0 | sg = av.getSelectionGroup(); |
2189 | 0 | if (sg != null && sg.getSize() == 1 |
2190 | && sg.getEndRes() - sg.getStartRes() < 2) | |
2191 | { | |
2192 | 0 | av.setSelectionGroup(null); |
2193 | } | |
2194 | ||
2195 | 0 | int column = pos.column; |
2196 | ||
2197 | /* | |
2198 | * find features at the position (if not gapped), or straddling | |
2199 | * the position (if at a gap) | |
2200 | */ | |
2201 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex); |
2202 | 0 | List<SequenceFeature> features = seqCanvas.getFeatureRenderer() |
2203 | .findFeaturesAtColumn(sequence, column + 1); | |
2204 | ||
2205 | 0 | if (!features.isEmpty()) |
2206 | { | |
2207 | /* | |
2208 | * highlight the first feature at the position on the alignment | |
2209 | */ | |
2210 | 0 | SearchResultsI highlight = new SearchResults(); |
2211 | 0 | highlight.addResult(sequence, features.get(0).getBegin(), |
2212 | features.get(0).getEnd()); | |
2213 | 0 | seqCanvas.highlightSearchResults(highlight, true); |
2214 | ||
2215 | /* | |
2216 | * open the Amend Features dialog | |
2217 | */ | |
2218 | 0 | new FeatureEditor(ap, Collections.singletonList(sequence), features, |
2219 | false).showDialog(); | |
2220 | } | |
2221 | } | |
2222 | } | |
2223 | ||
2224 | /** | |
2225 | * Responds to a mouse wheel movement by scrolling the alignment | |
2226 | * <ul> | |
2227 | * <li>left or right, if the shift key is down, else up or down</li> | |
2228 | * <li>right (or down) if the reported mouse movement is positive</li> | |
2229 | * <li>left (or up) if the reported mouse movement is negative</li> | |
2230 | * </ul> | |
2231 | * Note that this method may also be fired by scrolling with a gesture on a | |
2232 | * trackpad. | |
2233 | */ | |
2234 | 0 | @Override |
2235 | public void mouseWheelMoved(MouseWheelEvent e) | |
2236 | { | |
2237 | 0 | e.consume(); |
2238 | 0 | double wheelRotation = e.getPreciseWheelRotation(); |
2239 | ||
2240 | /* | |
2241 | * scroll more for large (fast) mouse movements | |
2242 | */ | |
2243 | 0 | int size = 1 + (int) Math.abs(wheelRotation); |
2244 | ||
2245 | 0 | if (wheelRotation > 0) |
2246 | { | |
2247 | 0 | if (e.isShiftDown()) |
2248 | { | |
2249 | /* | |
2250 | * scroll right | |
2251 | * stop trying to scroll right when limit is reached (saves | |
2252 | * expensive calls to Alignment.getWidth()) | |
2253 | */ | |
2254 | 0 | while (size-- > 0 && !ap.isScrolledFullyRight()) |
2255 | { | |
2256 | 0 | if (!av.getRanges().scrollRight(true)) |
2257 | { | |
2258 | 0 | break; |
2259 | } | |
2260 | } | |
2261 | } | |
2262 | else | |
2263 | { | |
2264 | /* | |
2265 | * scroll down | |
2266 | */ | |
2267 | 0 | while (size-- > 0) |
2268 | { | |
2269 | 0 | if (!av.getRanges().scrollUp(false)) |
2270 | { | |
2271 | 0 | break; |
2272 | } | |
2273 | } | |
2274 | } | |
2275 | } | |
2276 | 0 | else if (wheelRotation < 0) |
2277 | { | |
2278 | 0 | if (e.isShiftDown()) |
2279 | { | |
2280 | /* | |
2281 | * scroll left | |
2282 | */ | |
2283 | 0 | while (size-- > 0) |
2284 | { | |
2285 | 0 | if (!av.getRanges().scrollRight(false)) |
2286 | { | |
2287 | 0 | break; |
2288 | } | |
2289 | } | |
2290 | } | |
2291 | else | |
2292 | { | |
2293 | /* | |
2294 | * scroll up | |
2295 | */ | |
2296 | 0 | while (size-- > 0) |
2297 | { | |
2298 | 0 | if (!av.getRanges().scrollUp(true)) |
2299 | { | |
2300 | 0 | break; |
2301 | } | |
2302 | } | |
2303 | } | |
2304 | } | |
2305 | ||
2306 | /* | |
2307 | * update status bar and tooltip for new position | |
2308 | * (need to synthesize a mouse movement to refresh tooltip) | |
2309 | */ | |
2310 | 0 | mouseMoved(e); |
2311 | 0 | ToolTipManager.sharedInstance().mouseMoved(e); |
2312 | } | |
2313 | ||
2314 | /** | |
2315 | * DOCUMENT ME! | |
2316 | * | |
2317 | * @param pos | |
2318 | * DOCUMENT ME! | |
2319 | */ | |
2320 | 0 | protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos) |
2321 | { | |
2322 | 0 | if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1) |
2323 | { | |
2324 | 0 | return; |
2325 | } | |
2326 | ||
2327 | 0 | final int res = pos.column; |
2328 | 0 | final int seq = pos.seqIndex; |
2329 | 0 | oldSeq = seq; |
2330 | 0 | updateOverviewAndStructs = false; |
2331 | ||
2332 | 0 | startWrapBlock = wrappedBlock; |
2333 | ||
2334 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(seq); |
2335 | ||
2336 | 0 | if ((sequence == null) || (res > sequence.getLength())) |
2337 | { | |
2338 | 0 | return; |
2339 | } | |
2340 | ||
2341 | 0 | stretchGroup = av.getSelectionGroup(); |
2342 | ||
2343 | 0 | if (stretchGroup == null || !stretchGroup.contains(sequence, res)) |
2344 | { | |
2345 | 0 | stretchGroup = av.getAlignment().findGroup(sequence, res); |
2346 | 0 | if (stretchGroup != null) |
2347 | { | |
2348 | // only update the current selection if the popup menu has a group to | |
2349 | // focus on | |
2350 | 0 | av.setSelectionGroup(stretchGroup); |
2351 | } | |
2352 | } | |
2353 | ||
2354 | /* | |
2355 | * defer right-mouse click handling to mouseReleased on Windows | |
2356 | * (where isPopupTrigger() will answer true) | |
2357 | * NB isRightMouseButton is also true for Cmd-click on Mac | |
2358 | */ | |
2359 | 0 | if (Platform.isWinRightButton(evt)) |
2360 | { | |
2361 | 0 | return; |
2362 | } | |
2363 | ||
2364 | 0 | if (evt.isPopupTrigger()) // Mac: mousePressed |
2365 | { | |
2366 | 0 | showPopupMenu(evt, pos); |
2367 | 0 | return; |
2368 | } | |
2369 | ||
2370 | 0 | if (av.cursorMode) |
2371 | { | |
2372 | 0 | seqCanvas.cursorX = res; |
2373 | 0 | seqCanvas.cursorY = seq; |
2374 | 0 | seqCanvas.repaint(); |
2375 | 0 | return; |
2376 | } | |
2377 | ||
2378 | 0 | if (stretchGroup == null) |
2379 | { | |
2380 | 0 | createStretchGroup(res, sequence); |
2381 | } | |
2382 | ||
2383 | 0 | if (stretchGroup != null) |
2384 | { | |
2385 | 0 | stretchGroup.addPropertyChangeListener(seqCanvas); |
2386 | } | |
2387 | ||
2388 | 0 | seqCanvas.repaint(); |
2389 | } | |
2390 | ||
2391 | 0 | private void createStretchGroup(int res, SequenceI sequence) |
2392 | { | |
2393 | // Only if left mouse button do we want to change group sizes | |
2394 | // define a new group here | |
2395 | 0 | SequenceGroup sg = new SequenceGroup(); |
2396 | 0 | sg.setStartRes(res); |
2397 | 0 | sg.setEndRes(res); |
2398 | 0 | sg.addSequence(sequence, false); |
2399 | 0 | av.setSelectionGroup(sg); |
2400 | 0 | stretchGroup = sg; |
2401 | ||
2402 | 0 | if (av.getConservationSelected()) |
2403 | { | |
2404 | 0 | SliderPanel.setConservationSlider(ap, av.getResidueShading(), |
2405 | ap.getViewName()); | |
2406 | } | |
2407 | ||
2408 | 0 | if (av.getAbovePIDThreshold()) |
2409 | { | |
2410 | 0 | SliderPanel.setPIDSliderSource(ap, av.getResidueShading(), |
2411 | ap.getViewName()); | |
2412 | } | |
2413 | // TODO: stretchGroup will always be not null. Is this a merge error ? | |
2414 | // or is there a threading issue here? | |
2415 | 0 | if ((stretchGroup != null) && (stretchGroup.getEndRes() == res)) |
2416 | { | |
2417 | // Edit end res position of selected group | |
2418 | 0 | changeEndRes = true; |
2419 | } | |
2420 | 0 | else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res)) |
2421 | { | |
2422 | // Edit end res position of selected group | |
2423 | 0 | changeStartRes = true; |
2424 | } | |
2425 | 0 | stretchGroup.getWidth(); |
2426 | ||
2427 | } | |
2428 | ||
2429 | /** | |
2430 | * Build and show a pop-up menu at the right-click mouse position | |
2431 | * | |
2432 | * @param evt | |
2433 | * @param pos | |
2434 | */ | |
2435 | 0 | void showPopupMenu(MouseEvent evt, MousePos pos) |
2436 | { | |
2437 | 0 | final int column = pos.column; |
2438 | 0 | final int seq = pos.seqIndex; |
2439 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(seq); |
2440 | 0 | if (sequence != null) |
2441 | { | |
2442 | 0 | PopupMenu pop = new PopupMenu(ap, sequence, column); |
2443 | 0 | pop.show(this, evt.getX(), evt.getY()); |
2444 | } | |
2445 | } | |
2446 | ||
2447 | /** | |
2448 | * Update the display after mouse up on a selection or group | |
2449 | * | |
2450 | * @param evt | |
2451 | * mouse released event details | |
2452 | * @param afterDrag | |
2453 | * true if this event is happening after a mouse drag (rather than a | |
2454 | * mouse down) | |
2455 | */ | |
2456 | 0 | protected void doMouseReleasedDefineMode(MouseEvent evt, |
2457 | boolean afterDrag) | |
2458 | { | |
2459 | 0 | if (stretchGroup == null) |
2460 | { | |
2461 | 0 | return; |
2462 | } | |
2463 | ||
2464 | 0 | stretchGroup.removePropertyChangeListener(seqCanvas); |
2465 | ||
2466 | // always do this - annotation has own state | |
2467 | // but defer colourscheme update until hidden sequences are passed in | |
2468 | 0 | boolean vischange = stretchGroup.recalcConservation(true); |
2469 | 0 | updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup() |
2470 | && afterDrag; | |
2471 | 0 | if (stretchGroup.cs != null) |
2472 | { | |
2473 | 0 | if (afterDrag) |
2474 | { | |
2475 | 0 | stretchGroup.cs.alignmentChanged(stretchGroup, |
2476 | av.getHiddenRepSequences()); | |
2477 | } | |
2478 | ||
2479 | 0 | ResidueShaderI groupColourScheme = stretchGroup |
2480 | .getGroupColourScheme(); | |
2481 | 0 | String name = stretchGroup.getName(); |
2482 | 0 | if (stretchGroup.cs.conservationApplied()) |
2483 | { | |
2484 | 0 | SliderPanel.setConservationSlider(ap, groupColourScheme, name); |
2485 | } | |
2486 | 0 | if (stretchGroup.cs.getThreshold() > 0) |
2487 | { | |
2488 | 0 | SliderPanel.setPIDSliderSource(ap, groupColourScheme, name); |
2489 | } | |
2490 | 0 | if (stretchGroup.cs.isConsensusSecondaryStructureColouring()) |
2491 | { | |
2492 | 0 | SliderPanel.setConsensusSecondaryStructureSlider(ap, |
2493 | groupColourScheme, name); | |
2494 | } | |
2495 | } | |
2496 | 0 | PaintRefresher.Refresh(this, av.getSequenceSetId()); |
2497 | // TODO: structure colours only need updating if stretchGroup used to or now | |
2498 | // does contain sequences with structure views | |
2499 | 0 | ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs); |
2500 | 0 | updateOverviewAndStructs = false; |
2501 | 0 | changeEndRes = false; |
2502 | 0 | changeStartRes = false; |
2503 | 0 | stretchGroup = null; |
2504 | 0 | av.sendSelection(); |
2505 | } | |
2506 | ||
2507 | /** | |
2508 | * Resizes the borders of a selection group depending on the direction of | |
2509 | * mouse drag | |
2510 | * | |
2511 | * @param evt | |
2512 | */ | |
2513 | 0 | protected void dragStretchGroup(MouseEvent evt) |
2514 | { | |
2515 | 0 | if (stretchGroup == null) |
2516 | { | |
2517 | 0 | return; |
2518 | } | |
2519 | ||
2520 | 0 | MousePos pos = findMousePosition(evt); |
2521 | 0 | if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1) |
2522 | { | |
2523 | 0 | return; |
2524 | } | |
2525 | ||
2526 | 0 | int res = pos.column; |
2527 | 0 | int y = pos.seqIndex; |
2528 | ||
2529 | 0 | if (wrappedBlock != startWrapBlock) |
2530 | { | |
2531 | 0 | return; |
2532 | } | |
2533 | ||
2534 | 0 | res = Math.min(res, av.getAlignment().getWidth() - 1); |
2535 | ||
2536 | 0 | if (stretchGroup.getEndRes() == res) |
2537 | { | |
2538 | // Edit end res position of selected group | |
2539 | 0 | changeEndRes = true; |
2540 | } | |
2541 | 0 | else if (stretchGroup.getStartRes() == res) |
2542 | { | |
2543 | // Edit start res position of selected group | |
2544 | 0 | changeStartRes = true; |
2545 | } | |
2546 | ||
2547 | 0 | if (res < av.getRanges().getStartRes()) |
2548 | { | |
2549 | 0 | res = av.getRanges().getStartRes(); |
2550 | } | |
2551 | ||
2552 | 0 | if (changeEndRes) |
2553 | { | |
2554 | 0 | if (res > (stretchGroup.getStartRes() - 1)) |
2555 | { | |
2556 | 0 | stretchGroup.setEndRes(res); |
2557 | 0 | updateOverviewAndStructs |= av.isSelectionDefinedGroup(); |
2558 | } | |
2559 | } | |
2560 | 0 | else if (changeStartRes) |
2561 | { | |
2562 | 0 | if (res < (stretchGroup.getEndRes() + 1)) |
2563 | { | |
2564 | 0 | stretchGroup.setStartRes(res); |
2565 | 0 | updateOverviewAndStructs |= av.isSelectionDefinedGroup(); |
2566 | } | |
2567 | } | |
2568 | ||
2569 | 0 | int dragDirection = 0; |
2570 | ||
2571 | 0 | if (y > oldSeq) |
2572 | { | |
2573 | 0 | dragDirection = 1; |
2574 | } | |
2575 | 0 | else if (y < oldSeq) |
2576 | { | |
2577 | 0 | dragDirection = -1; |
2578 | } | |
2579 | ||
2580 | 0 | while ((y != oldSeq) && (oldSeq > -1) |
2581 | && (y < av.getAlignment().getHeight())) | |
2582 | { | |
2583 | // This routine ensures we don't skip any sequences, as the | |
2584 | // selection is quite slow. | |
2585 | 0 | Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq); |
2586 | ||
2587 | 0 | oldSeq += dragDirection; |
2588 | ||
2589 | 0 | if (oldSeq < 0) |
2590 | { | |
2591 | 0 | break; |
2592 | } | |
2593 | ||
2594 | 0 | Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq); |
2595 | ||
2596 | 0 | if (stretchGroup.getSequences(null).contains(nextSeq)) |
2597 | { | |
2598 | 0 | stretchGroup.deleteSequence(seq, false); |
2599 | 0 | updateOverviewAndStructs |= av.isSelectionDefinedGroup(); |
2600 | } | |
2601 | else | |
2602 | { | |
2603 | 0 | if (seq != null) |
2604 | { | |
2605 | 0 | stretchGroup.addSequence(seq, false); |
2606 | } | |
2607 | ||
2608 | 0 | stretchGroup.addSequence(nextSeq, false); |
2609 | 0 | updateOverviewAndStructs |= av.isSelectionDefinedGroup(); |
2610 | } | |
2611 | } | |
2612 | ||
2613 | 0 | if (oldSeq < 0) |
2614 | { | |
2615 | 0 | oldSeq = -1; |
2616 | } | |
2617 | ||
2618 | 0 | mouseDragging = true; |
2619 | ||
2620 | 0 | if (scrollThread != null) |
2621 | { | |
2622 | 0 | scrollThread.setMousePosition(evt.getPoint()); |
2623 | } | |
2624 | ||
2625 | /* | |
2626 | * construct a status message showing the range of the selection | |
2627 | */ | |
2628 | 0 | StringBuilder status = new StringBuilder(64); |
2629 | 0 | List<SequenceI> seqs = stretchGroup.getSequences(); |
2630 | 0 | String name = seqs.get(0).getName(); |
2631 | 0 | if (name.length() > 20) |
2632 | { | |
2633 | 0 | name = name.substring(0, 20); |
2634 | } | |
2635 | 0 | status.append(name).append(" - "); |
2636 | 0 | name = seqs.get(seqs.size() - 1).getName(); |
2637 | 0 | if (name.length() > 20) |
2638 | { | |
2639 | 0 | name = name.substring(0, 20); |
2640 | } | |
2641 | 0 | status.append(name).append(" "); |
2642 | 0 | int startRes = stretchGroup.getStartRes(); |
2643 | 0 | status.append(" cols ").append(String.valueOf(startRes + 1)) |
2644 | .append("-"); | |
2645 | 0 | int endRes = stretchGroup.getEndRes(); |
2646 | 0 | status.append(String.valueOf(endRes + 1)); |
2647 | 0 | status.append(" (").append(String.valueOf(seqs.size())).append(" x ") |
2648 | .append(String.valueOf(endRes - startRes + 1)).append(")"); | |
2649 | 0 | ap.alignFrame.setStatus(status.toString()); |
2650 | } | |
2651 | ||
2652 | /** | |
2653 | * Stops the scroll thread if it is running | |
2654 | */ | |
2655 | 3 | void stopScrolling() |
2656 | { | |
2657 | 3 | if (scrollThread != null) |
2658 | { | |
2659 | 0 | scrollThread.stopScrolling(); |
2660 | 0 | scrollThread = null; |
2661 | } | |
2662 | 3 | mouseDragging = false; |
2663 | } | |
2664 | ||
2665 | /** | |
2666 | * Starts a thread to scroll the alignment, towards a given mouse position | |
2667 | * outside the panel bounds, unless the alignment is in wrapped mode | |
2668 | * | |
2669 | * @param mousePos | |
2670 | */ | |
2671 | 0 | void startScrolling(Point mousePos) |
2672 | { | |
2673 | /* | |
2674 | * set this.mouseDragging in case this was called from | |
2675 | * a drag in ScalePanel or AnnotationPanel | |
2676 | */ | |
2677 | 0 | mouseDragging = true; |
2678 | 0 | if (!av.getWrapAlignment() && scrollThread == null) |
2679 | { | |
2680 | 0 | scrollThread = new ScrollThread(); |
2681 | 0 | scrollThread.setMousePosition(mousePos); |
2682 | 0 | if (Platform.isJS()) |
2683 | { | |
2684 | /* | |
2685 | * Javascript - run every 20ms until scrolling stopped | |
2686 | * or reaches the limit of scrollable alignment | |
2687 | */ | |
2688 | 0 | Timer t = new Timer(20, new ActionListener() |
2689 | { | |
2690 | 0 | @Override |
2691 | public void actionPerformed(ActionEvent e) | |
2692 | { | |
2693 | 0 | if (scrollThread != null) |
2694 | { | |
2695 | // if (!scrollOnce() {t.stop();}) gives compiler error :-( | |
2696 | 0 | scrollThread.scrollOnce(); |
2697 | } | |
2698 | } | |
2699 | }); | |
2700 | 0 | t.addActionListener(new ActionListener() |
2701 | { | |
2702 | 0 | @Override |
2703 | public void actionPerformed(ActionEvent e) | |
2704 | { | |
2705 | 0 | if (scrollThread == null) |
2706 | { | |
2707 | // SeqPanel.stopScrolling called | |
2708 | 0 | t.stop(); |
2709 | } | |
2710 | } | |
2711 | }); | |
2712 | 0 | t.start(); |
2713 | } | |
2714 | else | |
2715 | { | |
2716 | /* | |
2717 | * Java - run in a new thread | |
2718 | */ | |
2719 | 0 | scrollThread.start(); |
2720 | } | |
2721 | } | |
2722 | } | |
2723 | ||
2724 | /** | |
2725 | * Performs scrolling of the visible alignment left, right, up or down, until | |
2726 | * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the | |
2727 | * limit of the alignment is reached | |
2728 | */ | |
2729 | class ScrollThread extends Thread | |
2730 | { | |
2731 | private Point mousePos; | |
2732 | ||
2733 | private volatile boolean keepRunning = true; | |
2734 | ||
2735 | /** | |
2736 | * Constructor | |
2737 | */ | |
2738 | 0 | public ScrollThread() |
2739 | { | |
2740 | 0 | setName("SeqPanel$ScrollThread"); |
2741 | } | |
2742 | ||
2743 | /** | |
2744 | * Sets the position of the mouse that determines the direction of the | |
2745 | * scroll to perform. If this is called as the mouse moves, scrolling should | |
2746 | * respond accordingly. For example, if the mouse is dragged right, scroll | |
2747 | * right should start; if the drag continues down, scroll down should also | |
2748 | * happen. | |
2749 | * | |
2750 | * @param p | |
2751 | */ | |
2752 | 0 | public void setMousePosition(Point p) |
2753 | { | |
2754 | 0 | mousePos = p; |
2755 | } | |
2756 | ||
2757 | /** | |
2758 | * Sets a flag that will cause the thread to exit | |
2759 | */ | |
2760 | 0 | public void stopScrolling() |
2761 | { | |
2762 | 0 | keepRunning = false; |
2763 | } | |
2764 | ||
2765 | /** | |
2766 | * Scrolls the alignment left or right, and/or up or down, depending on the | |
2767 | * last notified mouse position, until the limit of the alignment is | |
2768 | * reached, or a flag is set to stop the scroll | |
2769 | */ | |
2770 | 0 | @Override |
2771 | public void run() | |
2772 | { | |
2773 | 0 | while (keepRunning) |
2774 | { | |
2775 | 0 | if (mousePos != null) |
2776 | { | |
2777 | 0 | keepRunning = scrollOnce(); |
2778 | } | |
2779 | 0 | try |
2780 | { | |
2781 | 0 | Thread.sleep(20); |
2782 | } catch (Exception ex) | |
2783 | { | |
2784 | } | |
2785 | } | |
2786 | 0 | SeqPanel.this.scrollThread = null; |
2787 | } | |
2788 | ||
2789 | /** | |
2790 | * Scrolls | |
2791 | * <ul> | |
2792 | * <li>one row up, if the mouse is above the panel</li> | |
2793 | * <li>one row down, if the mouse is below the panel</li> | |
2794 | * <li>one column left, if the mouse is left of the panel</li> | |
2795 | * <li>one column right, if the mouse is right of the panel</li> | |
2796 | * </ul> | |
2797 | * Answers true if a scroll was performed, false if not - meaning either | |
2798 | * that the mouse position is within the panel, or the edge of the alignment | |
2799 | * has been reached. | |
2800 | */ | |
2801 | 0 | boolean scrollOnce() |
2802 | { | |
2803 | /* | |
2804 | * quit after mouseUp ensures interrupt in JalviewJS | |
2805 | */ | |
2806 | 0 | if (!mouseDragging) |
2807 | { | |
2808 | 0 | return false; |
2809 | } | |
2810 | ||
2811 | 0 | boolean scrolled = false; |
2812 | 0 | ViewportRanges ranges = SeqPanel.this.av.getRanges(); |
2813 | ||
2814 | /* | |
2815 | * scroll up or down | |
2816 | */ | |
2817 | 0 | if (mousePos.y < 0) |
2818 | { | |
2819 | // mouse is above this panel - try scroll up | |
2820 | 0 | scrolled = ranges.scrollUp(true); |
2821 | } | |
2822 | 0 | else if (mousePos.y >= getHeight()) |
2823 | { | |
2824 | // mouse is below this panel - try scroll down | |
2825 | 0 | scrolled = ranges.scrollUp(false); |
2826 | } | |
2827 | ||
2828 | /* | |
2829 | * scroll left or right | |
2830 | */ | |
2831 | 0 | if (mousePos.x < 0) |
2832 | { | |
2833 | 0 | scrolled |= ranges.scrollRight(false); |
2834 | } | |
2835 | 0 | else if (mousePos.x >= getWidth()) |
2836 | { | |
2837 | 0 | scrolled |= ranges.scrollRight(true); |
2838 | } | |
2839 | 0 | return scrolled; |
2840 | } | |
2841 | } | |
2842 | ||
2843 | /** | |
2844 | * modify current selection according to a received message. | |
2845 | */ | |
2846 | 100 | @Override |
2847 | public void selection(SequenceGroup seqsel, ColumnSelection colsel, | |
2848 | HiddenColumns hidden, SelectionSource source) | |
2849 | { | |
2850 | // TODO: fix this hack - source of messages is align viewport, but SeqPanel | |
2851 | // handles selection messages... | |
2852 | // TODO: extend config options to allow user to control if selections may be | |
2853 | // shared between viewports. | |
2854 | 100 | boolean iSentTheSelection = (av == source |
2855 | || (source instanceof AlignViewport | |
2856 | && ((AlignmentViewport) source).getSequenceSetId() | |
2857 | .equals(av.getSequenceSetId()))); | |
2858 | ||
2859 | 100 | if (iSentTheSelection) |
2860 | { | |
2861 | // respond to our own event by updating dependent dialogs | |
2862 | 24 | if (ap.getCalculationDialog() != null) |
2863 | { | |
2864 | 0 | ap.getCalculationDialog().validateCalcTypes(); |
2865 | } | |
2866 | ||
2867 | 24 | return; |
2868 | } | |
2869 | ||
2870 | // process further ? | |
2871 | 76 | if (!av.followSelection) |
2872 | { | |
2873 | 0 | return; |
2874 | } | |
2875 | ||
2876 | /* | |
2877 | * Ignore the selection if there is one of our own pending. | |
2878 | */ | |
2879 | 76 | if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false)) |
2880 | { | |
2881 | 0 | return; |
2882 | } | |
2883 | ||
2884 | /* | |
2885 | * Check for selection in a view of which this one is a dna/protein | |
2886 | * complement. | |
2887 | */ | |
2888 | 76 | if (selectionFromTranslation(seqsel, colsel, hidden, source)) |
2889 | { | |
2890 | 0 | return; |
2891 | } | |
2892 | ||
2893 | // do we want to thread this ? (contention with seqsel and colsel locks, I | |
2894 | // suspect) | |
2895 | /* | |
2896 | * only copy colsel if there is a real intersection between | |
2897 | * sequence selection and this panel's alignment | |
2898 | */ | |
2899 | 76 | boolean repaint = false; |
2900 | 76 | boolean copycolsel = false; |
2901 | ||
2902 | 76 | SequenceGroup sgroup = null; |
2903 | 76 | if (seqsel != null && seqsel.getSize() > 0) |
2904 | { | |
2905 | 57 | if (av.getAlignment() == null) |
2906 | { | |
2907 | 0 | Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId() |
2908 | + " ViewId=" + av.getViewId() | |
2909 | + " 's alignment is NULL! returning immediately."); | |
2910 | 0 | return; |
2911 | } | |
2912 | 57 | sgroup = seqsel.intersect(av.getAlignment(), |
2913 | 57 | (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null); |
2914 | 57 | if ((sgroup != null && sgroup.getSize() > 0)) |
2915 | { | |
2916 | 0 | copycolsel = true; |
2917 | } | |
2918 | } | |
2919 | 76 | if (sgroup != null && sgroup.getSize() > 0) |
2920 | { | |
2921 | 0 | av.setSelectionGroup(sgroup); |
2922 | } | |
2923 | else | |
2924 | { | |
2925 | 76 | av.setSelectionGroup(null); |
2926 | } | |
2927 | 76 | av.isSelectionGroupChanged(true); |
2928 | 76 | repaint = true; |
2929 | ||
2930 | 76 | if (copycolsel) |
2931 | { | |
2932 | // the current selection is unset or from a previous message | |
2933 | // so import the new colsel. | |
2934 | 0 | if (colsel == null || colsel.isEmpty()) |
2935 | { | |
2936 | 0 | if (av.getColumnSelection() != null) |
2937 | { | |
2938 | 0 | av.getColumnSelection().clear(); |
2939 | 0 | repaint = true; |
2940 | } | |
2941 | } | |
2942 | else | |
2943 | { | |
2944 | // TODO: shift colSel according to the intersecting sequences | |
2945 | 0 | if (av.getColumnSelection() == null) |
2946 | { | |
2947 | 0 | av.setColumnSelection(new ColumnSelection(colsel)); |
2948 | } | |
2949 | else | |
2950 | { | |
2951 | 0 | av.getColumnSelection().setElementsFrom(colsel, |
2952 | av.getAlignment().getHiddenColumns()); | |
2953 | } | |
2954 | } | |
2955 | 0 | av.isColSelChanged(true); |
2956 | 0 | repaint = true; |
2957 | } | |
2958 | ||
2959 | 76 | if (copycolsel && av.hasHiddenColumns() |
2960 | && (av.getAlignment().getHiddenColumns() == null)) | |
2961 | { | |
2962 | 0 | jalview.bin.Console.errPrintln("Bad things"); |
2963 | } | |
2964 | 76 | if (repaint) // always true! |
2965 | { | |
2966 | // probably finessing with multiple redraws here | |
2967 | 76 | PaintRefresher.Refresh(this, av.getSequenceSetId()); |
2968 | // ap.paintAlignment(false); | |
2969 | } | |
2970 | ||
2971 | // lastly, update dependent dialogs | |
2972 | 76 | if (ap.getCalculationDialog() != null) |
2973 | { | |
2974 | 0 | ap.getCalculationDialog().validateCalcTypes(); |
2975 | } | |
2976 | ||
2977 | } | |
2978 | ||
2979 | /** | |
2980 | * If this panel is a cdna/protein translation view of the selection source, | |
2981 | * tries to map the source selection to a local one, and returns true. Else | |
2982 | * returns false. | |
2983 | * | |
2984 | * @param seqsel | |
2985 | * @param colsel | |
2986 | * @param source | |
2987 | */ | |
2988 | 76 | protected boolean selectionFromTranslation(SequenceGroup seqsel, |
2989 | ColumnSelection colsel, HiddenColumns hidden, | |
2990 | SelectionSource source) | |
2991 | { | |
2992 | 76 | if (!(source instanceof AlignViewportI)) |
2993 | { | |
2994 | 0 | return false; |
2995 | } | |
2996 | 76 | final AlignViewportI sourceAv = (AlignViewportI) source; |
2997 | 76 | if (sourceAv.getCodingComplement() != av |
2998 | && av.getCodingComplement() != sourceAv) | |
2999 | { | |
3000 | 76 | return false; |
3001 | } | |
3002 | ||
3003 | /* | |
3004 | * Map sequence selection | |
3005 | */ | |
3006 | 0 | SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av); |
3007 | 0 | av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null); |
3008 | 0 | av.isSelectionGroupChanged(true); |
3009 | ||
3010 | /* | |
3011 | * Map column selection | |
3012 | */ | |
3013 | // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv, | |
3014 | // av); | |
3015 | 0 | ColumnSelection cs = new ColumnSelection(); |
3016 | 0 | HiddenColumns hs = new HiddenColumns(); |
3017 | 0 | MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs); |
3018 | 0 | av.setColumnSelection(cs); |
3019 | 0 | boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs); |
3020 | ||
3021 | // lastly, update any dependent dialogs | |
3022 | 0 | if (ap.getCalculationDialog() != null) |
3023 | { | |
3024 | 0 | ap.getCalculationDialog().validateCalcTypes(); |
3025 | } | |
3026 | ||
3027 | /* | |
3028 | * repaint alignment, and also Overview or Structure | |
3029 | * if hidden column selection has changed | |
3030 | */ | |
3031 | 0 | ap.paintAlignment(hiddenChanged, hiddenChanged); |
3032 | // propagate any selection changes | |
3033 | 0 | PaintRefresher.Refresh(ap, av.getSequenceSetId()); |
3034 | ||
3035 | 0 | return true; |
3036 | } | |
3037 | ||
3038 | /** | |
3039 | * | |
3040 | * @return null or last search results handled by this panel | |
3041 | */ | |
3042 | 1 | public SearchResultsI getLastSearchResults() |
3043 | { | |
3044 | 1 | return lastSearchResults; |
3045 | } | |
3046 | } |