Source code: com/virtuosotechnologies/asaph/standardgui/SongLineEditor.java
1 /*
2 ================================================================================
3
4 FILE: SongLineEditor.java
5
6 PROJECT:
7
8 Asaph
9
10 CONTENTS:
11
12 The song line editor
13
14 PROGRAMMERS:
15
16 Daniel Azuma (DA) <dazuma@kagi.com>
17
18 COPYRIGHT:
19
20 Copyright (C) 2003 Daniel Azuma (dazuma@kagi.com)
21
22 This program is free software; you can redistribute it and/or
23 modify it under the terms of the GNU General Public License as
24 published by the Free Software Foundation; either version 2
25 of the License, or (at your option) any later version.
26
27 This program is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 GNU General Public License for more details.
31
32 You should have received a copy of the GNU General Public
33 License along with this program; if not, write to
34 Free Software Foundation, Inc.
35 59 Temple Place, Suite 330
36 Boston, MA 02111-1307 USA
37
38 ================================================================================
39 */
40
41
42 package com.virtuosotechnologies.asaph.standardgui;
43
44
45 import java.awt.Graphics2D;
46 import java.awt.Rectangle;
47 import java.awt.Point;
48 import java.awt.Toolkit;
49 import java.awt.datatransfer.Transferable;
50 import java.awt.datatransfer.DataFlavor;
51 import java.awt.datatransfer.StringSelection;
52 import java.awt.datatransfer.UnsupportedFlavorException;
53 import java.awt.font.FontRenderContext;
54 import java.awt.geom.Rectangle2D;
55 import java.awt.geom.Line2D;
56 import java.awt.geom.GeneralPath;
57 import java.awt.event.MouseEvent;
58 import java.awt.event.KeyEvent;
59 import java.io.IOException;
60 import java.util.List;
61 import java.util.ArrayList;
62 import java.util.Map;
63 import java.util.HashMap;
64 import java.util.Iterator;
65 import java.util.logging.Logger;
66 import java.util.logging.Level;
67
68 import com.virtuosotechnologies.lib.platform.PlatformUtils;
69
70 import com.virtuosotechnologies.asaph.model.Song;
71 import com.virtuosotechnologies.asaph.model.Variation;
72 import com.virtuosotechnologies.asaph.model.ChordSet;
73 import com.virtuosotechnologies.asaph.model.SongBlock;
74 import com.virtuosotechnologies.asaph.model.SongLine;
75 import com.virtuosotechnologies.asaph.model.SongLineMember;
76 import com.virtuosotechnologies.asaph.model.StringSongLineMember;
77 import com.virtuosotechnologies.asaph.model.ChordAnnotation;
78 import com.virtuosotechnologies.asaph.model.TextString;
79 import com.virtuosotechnologies.asaph.model.CommentString;
80 import com.virtuosotechnologies.asaph.model.notation.Chord;
81 import com.virtuosotechnologies.asaph.model.notation.NotationFactory;
82 import virtuoso.asaph.util.render.LineLayout;
83 import virtuoso.asaph.util.render.TextFragment;
84 import virtuoso.asaph.util.render.ChordFragment;
85 import virtuoso.asaph.util.render.ChordFragmentGroup;
86 import com.virtuosotechnologies.asaph.modelutils.DataTransferUtils;
87 import com.virtuosotechnologies.asaph.modelutils.SongUtils;
88 import com.virtuosotechnologies.asaph.modelutils.CannotPasteException;
89
90
91 /**
92 * The song line editor
93 */
94 /*package*/ class SongLineEditor
95 {
96 private static final char SMARTQUOTE_OPENSINGLE = (char)Integer.parseInt(
97 ResourceAccess.Strings.buildString("SmartQuote_OpenSingle_Hex"), 16);
98 private static final char SMARTQUOTE_CLOSESINGLE = (char)Integer.parseInt(
99 ResourceAccess.Strings.buildString("SmartQuote_CloseSingle_Hex"), 16);
100 private static final char SMARTQUOTE_OPENDOUBLE = (char)Integer.parseInt(
101 ResourceAccess.Strings.buildString("SmartQuote_OpenDouble_Hex"), 16);
102 private static final char SMARTQUOTE_CLOSEDOUBLE = (char)Integer.parseInt(
103 ResourceAccess.Strings.buildString("SmartQuote_CloseDouble_Hex"), 16);
104
105
106 private SongBlock block_;
107 private SongLine line_;
108 private SongBodyEditorPane pane_;
109 private SongBlockEditor blockEditor_;
110
111 private BodyEditorSize curSize_;
112 private List lineViews_;
113 private List chordGroupViews_;
114
115 private float chordsTop_;
116 private float chordsBottom_;
117 private float textTop_;
118 private float textBottom_;
119
120 private int draggingChordNum_;
121 private int chordDragIndex_;
122 private int chordDragOffset_;
123
124
125 /**
126 * Constructor
127 */
128 /*package*/ SongLineEditor(
129 SongBodyEditorPane pane,
130 SongBlockEditor blockEditor,
131 SongBlock block,
132 SongLine line)
133 {
134 pane_ = pane;
135 blockEditor_ = blockEditor;
136 block_ = block;
137 line_ = line;
138 lineViews_ = new ArrayList();
139 chordGroupViews_ = new ArrayList();
140 draggingChordNum_ = 0;
141 chordDragIndex_ = -1;
142 chordDragOffset_ = -1;
143 }
144
145
146 /**
147 * Get the size of the editor
148 */
149 /*package*/ BodyEditorSize getSize()
150 {
151 return curSize_;
152 }
153
154
155 /**
156 * Rebuild according to the current model and settings
157 */
158 /*package*/ void rebuild(
159 boolean refresh)
160 {
161 lineViews_.clear();
162 chordGroupViews_.clear();
163
164 FontRenderContext frc = pane_.getFontRenderContext();
165 LineLayout layout = new EditorLineLayout(line_, pane_.getCurChordSet(),
166 block_.getIndentLevel(), frc);
167 chordsTop_ = layout.getLineTop();
168 chordsBottom_ = layout.getChordBottom()+2;
169
170 float handleLeft = (line_.getSongBlock().getIndentLevel()+line_.getIndentLevel())*EditorConstants.INDENT_WIDTH+
171 (-EditorConstants.LEFT_LINE_HANDLE_WIDTH-EditorConstants.LINE_HANDLE_SPACING);
172 curSize_ = new BodyEditorSize(
173 Math.min(handleLeft, layout.getLineLeft()-EditorConstants.LINE_HANDLE_SPACING),
174 EditorConstants.RIGHT_LINE_HANDLE_WIDTH+EditorConstants.LINE_HANDLE_SPACING+layout.getLineRight(),
175 layout.getLineBottom()+EditorConstants.LINE_BOTTOM_MARGIN);
176
177 // Create views of chord groups
178 Map annotationToGroupView = new HashMap();
179 int numChordGroups = layout.getChordFragmentGroupCount();
180 for (int i=0; i<numChordGroups; ++i)
181 {
182 ChordFragmentGroup group = layout.getNthChordFragmentGroup(i);
183 ChordAnnotation ann = group.getChordAnnotation();
184 ChordGroupView groupView = new ChordGroupView(ann, group.getPosition(), group.getWidth(),
185 chordsTop_, chordsBottom_);
186 chordGroupViews_.add(groupView);
187 annotationToGroupView.put(ann, groupView);
188
189 // Views of chord fragments
190 ChordFragmentView lastFragmentView = null;
191 int numChordFragments = group.getChordFragmentCount();
192 for (int j=0; j<numChordFragments; ++j)
193 {
194 ChordFragment frag = group.getNthChordFragment(j);
195 ChordFragmentView view = new ChordFragmentView(ann, j-ann.getPrecedingChords().length,
196 frag.getString(), frag.getPosition(), frag.getWidth(), layout.getChordBaseline(), frc);
197 groupView.addChordFragment(view);
198 if (lastFragmentView != null)
199 {
200 float div = (lastFragmentView.getXPos()+lastFragmentView.getWidth()+view.getXPos())/2;
201 lastFragmentView.setRightLimit(div);
202 view.setLeftLimit(div);
203 }
204 lastFragmentView = view;
205 }
206 }
207 for (int i=1; i<numChordGroups; ++i)
208 {
209 ChordGroupView group1 = (ChordGroupView)chordGroupViews_.get(i-1);
210 ChordGroupView group2 = (ChordGroupView)chordGroupViews_.get(i);
211 float limit = (group1.getRightLimit()+group2.getLeftLimit())*0.5f;
212 group1.setRightLimit(limit);
213 group2.setLeftLimit(limit);
214 }
215 if (numChordGroups > 0)
216 {
217 ((ChordGroupView)chordGroupViews_.get(0)).setLeftLimit(Float.NEGATIVE_INFINITY);
218 ((ChordGroupView)chordGroupViews_.get(numChordGroups-1)).setRightLimit(Float.POSITIVE_INFINITY);
219 }
220
221 // Create views of text fragments
222 textTop_ = Float.POSITIVE_INFINITY;
223 textBottom_ = Float.NEGATIVE_INFINITY;
224 LineTextView lastTextView = null;
225 int numTextFragments = layout.getTextFragmentCount();
226 for (int i=0; i<numTextFragments; ++i)
227 {
228 TextFragment frag = layout.getNthTextFragment(i);
229 TextFragment.Type type = frag.getType();
230 LineMemberView view = null;
231 if (type == TextFragment.TEXT_TYPE)
232 {
233 view = new LineTextView((TextString)frag.getSongLineMember(), frag.getString(),
234 frag.getPosition(), frag.getWidth(), layout.getTextBaseline(), frc);
235 }
236 else if (type == TextFragment.COMMENT_TYPE)
237 {
238 view = new LineTextView((CommentString)frag.getSongLineMember(), frag.getString(),
239 frag.getPosition(), frag.getWidth(), layout.getTextBaseline(), frc);
240 }
241 else if (type == TextFragment.CHORD_PLACEHOLDER_TYPE)
242 {
243 ChordAnnotation ann = (ChordAnnotation)frag.getSongLineMember();
244 ChordGroupView groupView = (ChordGroupView)annotationToGroupView.get(ann);
245 view = new LineChordPlaceholderView(ann, frag.getPosition(), frag.getWidth(),
246 layout.getChordBottom()+1, layout.getLineBottom(), groupView);
247 groupView.setPlaceholderView((LineChordPlaceholderView)view);
248 }
249 if (view != null)
250 {
251 if (view instanceof LineTextView)
252 {
253 LineTextView textView = (LineTextView)view;
254 textTop_ = Math.min(textTop_, textView.getTop());
255 textBottom_ = Math.max(textBottom_, textView.getTop()+textView.getHeight());
256 if (lastTextView != null)
257 {
258 float div = (lastTextView.getXPos()+lastTextView.getWidth()+textView.getXPos())/2;
259 lastTextView.setRightLimit(div);
260 textView.setLeftLimit(div);
261 }
262 lastTextView = textView;
263 }
264 lineViews_.add(view);
265 }
266 }
267
268 blockEditor_.setLineSize(this, curSize_, refresh);
269 }
270
271
272 /**
273 * Draw
274 */
275 /*package*/ void paint(
276 Graphics2D g2d,
277 float x,
278 float y,
279 Rectangle clipBounds,
280 boolean select,
281 boolean containsSelect)
282 {
283 EditorSelection selection = pane_.getSelection();
284 boolean focused = pane_.isFocused();
285
286 // Draw structure
287 float handleLeft = (line_.getSongBlock().getIndentLevel()+line_.getIndentLevel())*EditorConstants.INDENT_WIDTH+
288 (-EditorConstants.LEFT_LINE_HANDLE_WIDTH-EditorConstants.LINE_HANDLE_SPACING);
289 g2d.setColor(EditorConstants.BACKGROUND_COLOR);
290 g2d.fill(new Rectangle2D.Float(x+handleLeft, y,
291 curSize_.getRight()-handleLeft, curSize_.getBottom()));
292
293 if (select && focused)
294 {
295 g2d.setColor(EditorConstants.SELECTED_LINE_HANDLE_COLOR);
296 }
297 else
298 {
299 g2d.setColor(EditorConstants.LINE_HANDLE_COLOR);
300 }
301 g2d.fill(new Rectangle2D.Float(
302 x+handleLeft, y,
303 EditorConstants.LEFT_LINE_HANDLE_WIDTH, curSize_.getBottom()));
304 g2d.fill(new Rectangle2D.Float(
305 x+curSize_.getRight()-EditorConstants.RIGHT_LINE_HANDLE_WIDTH,
306 y, EditorConstants.RIGHT_LINE_HANDLE_WIDTH, curSize_.getBottom()));
307 g2d.fill(new Rectangle2D.Float(
308 x+handleLeft, y,
309 curSize_.getRight()-handleLeft, EditorConstants.LINE_BORDER_WIDTH));
310 g2d.fill(new Rectangle2D.Float(
311 x+handleLeft, y+curSize_.getBottom()-EditorConstants.LINE_BORDER_WIDTH,
312 curSize_.getRight()-handleLeft, EditorConstants.LINE_BORDER_WIDTH));
313
314 // Draw backgrounds behind chord groups
315 for (Iterator iter = chordGroupViews_.iterator(); iter.hasNext(); )
316 {
317 ChordGroupView groupView = (ChordGroupView)iter.next();
318 groupView.paintBackground(g2d, x, y);
319 }
320
321 // Draw hilite
322 if (containsSelect && focused)
323 {
324 Line2D leftSeg;
325 Line2D rightSeg;
326 if (selection.isChordSelection())
327 {
328 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
329 ChordFragmentView leftView = groupView.getNthChordFragment(selection.getLeftChordView());
330 ChordFragmentView rightView = groupView.getNthChordFragment(selection.getRightChordView());
331 leftSeg = leftView.getCaretSegment(selection.getLeftChordPos(), x, y);
332 rightSeg = rightView.getCaretSegment(selection.getRightChordPos(), x, y);
333 }
334 else
335 {
336 assert selection.isTextSelection();
337 LineTextView leftView = (LineTextView)lineViews_.get(selection.getLeftTextView());
338 LineTextView rightView = (LineTextView)lineViews_.get(selection.getRightTextView());
339 leftSeg = leftView.getCaretSegment(selection.getLeftTextPos(), x, y);
340 rightSeg = rightView.getCaretSegment(selection.getRightTextPos(), x, y);
341 }
342 if (!selection.isSinglePointSelection())
343 {
344 GeneralPath path = new GeneralPath();
345 path.moveTo((float)leftSeg.getX1(), (float)leftSeg.getY1());
346 path.lineTo((float)rightSeg.getX1(), (float)rightSeg.getY1());
347 path.lineTo((float)rightSeg.getX2(), (float)rightSeg.getY2());
348 path.lineTo((float)leftSeg.getX2(), (float)leftSeg.getY2());
349 path.closePath();
350 if (draggingChordNum_ != 0)
351 {
352 g2d.setColor(EditorConstants.DRAGGING_TEXT_HILITE_COLOR);
353 }
354 else
355 {
356 g2d.setColor(EditorConstants.TEXT_HILITE_COLOR);
357 }
358 g2d.fill(path);
359 }
360 }
361
362 // Draw line views
363 for (Iterator iter = lineViews_.iterator(); iter.hasNext(); )
364 {
365 LineMemberView view = (LineMemberView)iter.next();
366 view.paint(g2d, x, y);
367 }
368 for (Iterator iter = chordGroupViews_.iterator(); iter.hasNext(); )
369 {
370 ChordGroupView groupView = (ChordGroupView)iter.next();
371 groupView.paint(g2d, x, y);
372 }
373
374 // Draw caret
375 if (pane_.isCaretVisible() && containsSelect && focused && draggingChordNum_ == 0)
376 {
377 Line2D endSeg;
378 if (selection.isChordSelection())
379 {
380 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
381 ChordFragmentView endView = groupView.getNthChordFragment(selection.getEndChordView());
382 endSeg = endView.getCaretSegment(selection.getEndChordPos(), x, y);
383 }
384 else
385 {
386 assert selection.isTextSelection();
387 LineTextView endView = (LineTextView)lineViews_.get(selection.getEndTextView());
388 endSeg = endView.getCaretSegment(selection.getEndTextPos(), x, y);
389 }
390 g2d.setColor(EditorConstants.TEXT_CARET_COLOR);
391 g2d.draw(endSeg);
392 }
393
394 // Drag chord drag position
395 if (draggingChordNum_ != 0 && chordDragIndex_ != -1)
396 {
397 LineTextView dragView = (LineTextView)lineViews_.get(chordDragIndex_);
398 Line2D dragSeg = dragView.getCaretSegment(chordDragOffset_, x, y);
399 g2d.setColor(EditorConstants.CHORD_DRAG_COLOR);
400 g2d.draw(dragSeg);
401 }
402 }
403
404
405 /**
406 * Get the update rectangle for the current selection.
407 * Returns a Rectangle2D, or null for no selection.
408 */
409 /*package*/ Rectangle2D getSelectionBounds(
410 EditorSelection selection,
411 boolean blink,
412 float x,
413 float y)
414 {
415 if (selection.isChordSelection())
416 {
417 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
418 if (blink || selection.isSinglePointSelection())
419 {
420 ChordFragmentView view = groupView.getNthChordFragment(selection.getEndChordView());
421 Line2D seg = view.getCaretSegment(selection.getEndChordPos(), x, y);
422 return new Rectangle2D.Float(((float)seg.getX2())-1, ((float)seg.getY1())-1,
423 (float)(seg.getX1()-seg.getX2())+2, (float)(seg.getY2()-seg.getY1())+2);
424 }
425 else
426 {
427 ChordFragmentView leftView = groupView.getNthChordFragment(selection.getLeftChordView());
428 ChordFragmentView rightView = groupView.getNthChordFragment(selection.getRightChordView());
429 Line2D leftSeg = leftView.getCaretSegment(selection.getLeftChordPos(), x, y);
430 Line2D rightSeg = rightView.getCaretSegment(selection.getRightChordPos(), x, y);
431 float top = Math.min((float)leftSeg.getY1(), (float)rightSeg.getY1());
432 float bottom = Math.max((float)leftSeg.getY2(), (float)rightSeg.getY2());
433 return new Rectangle2D.Float(((float)leftSeg.getX2())-1, top-1,
434 (float)(rightSeg.getX1()-leftSeg.getX2())+2, bottom-top+2);
435 }
436 }
437 else if (selection.isTextSelection())
438 {
439 if (blink || selection.isSinglePointSelection())
440 {
441 LineTextView view = (LineTextView)lineViews_.get(selection.getEndTextView());
442 Line2D seg = view.getCaretSegment(selection.getEndTextPos(), x, y);
443 return new Rectangle2D.Float(((float)seg.getX2())-1, ((float)seg.getY1())-1,
444 (float)(seg.getX1()-seg.getX2())+2, (float)(seg.getY2()-seg.getY1())+2);
445 }
446 else
447 {
448 LineTextView leftView = (LineTextView)lineViews_.get(selection.getLeftTextView());
449 LineTextView rightView = (LineTextView)lineViews_.get(selection.getRightTextView());
450 Line2D leftSeg = leftView.getCaretSegment(selection.getLeftTextPos(), x, y);
451 Line2D rightSeg = rightView.getCaretSegment(selection.getRightTextPos(), x, y);
452 float top = Math.min((float)leftSeg.getY1(), (float)rightSeg.getY1());
453 float bottom = Math.max((float)leftSeg.getY2(), (float)rightSeg.getY2());
454 return new Rectangle2D.Float(((float)leftSeg.getX2())-1, top-1,
455 (float)(rightSeg.getX1()-leftSeg.getX2())+2, bottom-top+2);
456 }
457 }
458 return null;
459 }
460
461
462 /**
463 * Select default. Should be called only when the user drills in from
464 * the block level using the keyboard.
465 */
466 /*package*/ void selectDefault()
467 {
468 pane_.getSelection().setTextSelection(0, 0, 0, 0, true);
469 }
470
471
472 /**
473 * Select in the line, given an x in line-relative coordinates.
474 */
475 /*package*/ void selectInLineForX(
476 float x)
477 {
478 int numLineViews = lineViews_.size();
479 for (int i=0; i<numLineViews; ++i)
480 {
481 LineMemberView view = (LineMemberView)lineViews_.get(i);
482 if (view instanceof LineTextView)
483 {
484 LineTextView textView = (LineTextView)view;
485 int offset = textView.getOffsetForPosition(x, textTop_);
486 if (offset != -1)
487 {
488 pane_.getSelection().setTextSelection(i, offset, i, offset, true);
489 return;
490 }
491 }
492 }
493 assert false;
494 }
495
496
497 /**
498 * Select in the chords, given an x in line-relative coordinates.
499 */
500 /*package*/ boolean selectInChordsForX(
501 float x)
502 {
503 int numChordGroups = chordGroupViews_.size();
504 for (int groupi=0; groupi<numChordGroups; ++groupi)
505 {
506 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(groupi);
507 if (groupView.isInXLimits(x))
508 {
509 int numChordViews = groupView.getFragmentCount();
510 for (int viewi=0; viewi<numChordViews; ++viewi)
511 {
512 ChordFragmentView chordView = groupView.getNthChordFragment(viewi);
513 int offset = chordView.getOffsetForPosition(x, textTop_);
514 if (offset != -1)
515 {
516 pane_.getSelection().setChordSelection(groupi, viewi, offset, viewi, offset, true);
517 return true;
518 }
519 }
520 }
521 }
522 return false;
523 }
524
525
526 /**
527 * Handle mouse-down events
528 */
529 /*package*/ boolean handleMousePressed(
530 MouseEvent ev,
531 float x,
532 float y,
533 int containerBlock,
534 int containerLine)
535 {
536 EditorSelection selection = pane_.getSelection();
537 float xorigin = (line_.getSongBlock().getIndentLevel()+line_.getIndentLevel())*EditorConstants.INDENT_WIDTH;
538 float handleLeft = xorigin-EditorConstants.LEFT_LINE_HANDLE_WIDTH-EditorConstants.LINE_HANDLE_SPACING;
539
540 Point p = ev.getPoint();
541 float px = p.x-x;
542 float py = p.y-y;
543
544 if (px > xorigin-EditorConstants.LINE_HANDLE_SPACING &&
545 px < curSize_.getRight()-EditorConstants.RIGHT_LINE_HANDLE_WIDTH &&
546 py >= textTop_ && py <= textBottom_)
547 {
548 int numLineViews = lineViews_.size();
549
550 // Check draggable chords
551 for (int i=0; i<numLineViews; ++i)
552 {
553 LineMemberView view = (LineMemberView)lineViews_.get(i);
554 if (view instanceof LineChordPlaceholderView)
555 {
556 LineChordPlaceholderView chordView = (LineChordPlaceholderView)view;
557 if (chordView.isInSelectionArea(px))
558 {
559 draggingChordNum_ = i;
560 LineTextView prevView = (LineTextView)lineViews_.get(i-1);
561 chordDragIndex_ = -1;
562 chordDragOffset_ = -1;
563 selection.prepareSelectionInsideBlock(containerBlock);
564 selection.prepareSelectionInsideLine(containerLine);
565 selection.setTextSelection(i-1, prevView.getCharacterCount(), i+1, 0, false);
566 return true;
567 }
568 }
569 }
570
571 // Check text fragments
572 for (int i=0; i<numLineViews; ++i)
573 {
574 LineMemberView view = (LineMemberView)lineViews_.get(i);
575 if (view instanceof LineTextView)
576 {
577 LineTextView textView = (LineTextView)view;
578 int offset = textView.getOffsetForPosition(px, py);
579 if (offset != -1)
580 {
581 if (ev.isShiftDown() && selection.isTextSelection() &&
582 selection.getContainingBlock() == containerBlock &&
583 selection.getContainingLine() == containerLine)
584 {
585 selection.extendTextSelection(i, offset, false);
586 }
587 else
588 {
589 selection.prepareSelectionInsideBlock(containerBlock);
590 selection.prepareSelectionInsideLine(containerLine);
591 if (ev.getClickCount() == 1)
592 {
593 selection.setTextSelection(i, offset, i, offset, false);
594 }
595 else if (ev.getClickCount() == 2)
596 {
597 int[] offsets = textView.getDoubleClickOffsetsForPosition(px, py);
598 selection.startTextSelection(i, offsets[0], i, offsets[1], false);
599 }
600 else if (ev.getClickCount() == 3)
601 {
602 selection.startTextSelection(i, 0, i, textView.getCharacterCount(), false);
603 }
604 else
605 {
606 selection.startTextSelection(0, 0, numLineViews-1,
607 ((LineTextView)lineViews_.get(numLineViews-1)).getCharacterCount(), false);
608 }
609 }
610 return true;
611 }
612 }
613 }
614 }
615
616 // Check chord fragments
617 float leftChordPos = 0;
618 if (!chordGroupViews_.isEmpty())
619 {
620 leftChordPos = Math.min(0, ((ChordGroupView)chordGroupViews_.get(0)).getXPos());
621 }
622 if (px > xorigin+leftChordPos-EditorConstants.LINE_HANDLE_SPACING &&
623 px < curSize_.getRight()-EditorConstants.RIGHT_LINE_HANDLE_WIDTH &&
624 py >= chordsTop_ && py <= chordsBottom_)
625 {
626 int numChordGroups = chordGroupViews_.size();
627 for (int groupi=0; groupi<numChordGroups; ++groupi)
628 {
629 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(groupi);
630 if (groupView.isInXLimits(px))
631 {
632 int numChordViews = groupView.getFragmentCount();
633 for (int viewi=0; viewi<numChordViews; ++viewi)
634 {
635 ChordFragmentView chordView = groupView.getNthChordFragment(viewi);
636 int offset = chordView.getOffsetForPosition(px, py);
637 if (offset != -1)
638 {
639 if (ev.isShiftDown() && selection.isChordSelection() &&
640 selection.getContainingBlock() == containerBlock &&
641 selection.getContainingLine() == containerLine &&
642 selection.getChordGroup() == groupi)
643 {
644 selection.extendChordSelection(viewi, offset, false);
645 }
646 else
647 {
648 selection.prepareSelectionInsideBlock(containerBlock);
649 selection.prepareSelectionInsideLine(containerLine);
650 if (ev.getClickCount() == 1)
651 {
652 selection.setChordSelection(groupi, viewi, offset, viewi, offset, false);
653 }
654 else if (ev.getClickCount() == 2)
655 {
656 selection.startChordSelection(groupi, viewi, 0, viewi,
657 chordView.getCharacterCount(), false);
658 }
659 else
660 {
661 selection.startChordSelection(groupi, 0, 0, numChordViews-1,
662 groupView.getNthChordFragment(numChordViews-1).getCharacterCount(), false);
663 }
664 }
665 return true;
666 }
667 }
668 }
669 }
670 }
671
672 if (px < handleLeft || px > curSize_.getRight())
673 {
674 return false;
675 }
676
677 if (ev.isShiftDown() && selection.isLineSelection() &&
678 selection.getContainingBlock() == containerBlock)
679 {
680 if (containerLine >= selection.getStartLinePos())
681 {
682 ++containerLine;
683 }
684 selection.extendLineSelection(containerLine, false);
685 }
686 else
687 {
688 selection.prepareSelectionInsideBlock(containerBlock);
689 selection.startLineSelection(containerLine, containerLine+1, false);
690 }
691 return true;
692 }
693
694
695 /**
696 * Handle mouse-drag events
697 */
698 /*package*/ void handleMouseDragged(
699 MouseEvent ev,
700 float x,
701 float y)
702 {
703 EditorSelection selection = pane_.getSelection();
704
705 Point p = ev.getPoint();
706 float px = p.x-x;
707 float py = p.y-y;
708
709 if (draggingChordNum_ != 0)
710 {
711 chordDragIndex_ = -1;
712 chordDragOffset_ = -1;
713 int numLineViews = lineViews_.size();
714 for (int i=0; i<numLineViews; ++i)
715 {
716 LineMemberView view = (LineMemberView)lineViews_.get(i);
717 if (view instanceof LineTextView)
718 {
719 LineTextView textView = (LineTextView)view;
720 int offset = textView.getOffsetForPosition(px, py);
721 if (offset != -1)
722 {
723 chordDragIndex_ = i;
724 chordDragOffset_ = offset;
725 break;
726 }
727 }
728 }
729 pane_.repaintFilledWidthSelection();
730 }
731 else if (selection.isChordSelection())
732 {
733 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
734 int numChordViews = groupView.getFragmentCount();
735 for (int i=0; i<numChordViews; ++i)
736 {
737 ChordFragmentView chordView = groupView.getNthChordFragment(i);
738 int offset = chordView.getOffsetForPosition(px, py);
739 if (offset != -1)
740 {
741 selection.extendChordSelection(i, offset, false);
742 break;
743 }
744 }
745 }
746 else if (selection.isTextSelection())
747 {
748 int numLineViews = lineViews_.size();
749 for (int i=0; i<numLineViews; ++i)
750 {
751 LineMemberView view = (LineMemberView)lineViews_.get(i);
752 if (view instanceof LineTextView)
753 {
754 LineTextView textView = (LineTextView)view;
755 int offset = textView.getOffsetForPosition(px, py);
756 if (offset != -1)
757 {
758 selection.extendTextSelection(i, offset, false);
759 break;
760 }
761 }
762 }
763 }
764 }
765
766
767 /**
768 * Handle mouse-up events
769 */
770 /*package*/ void handleMouseReleased(
771 MouseEvent ev,
772 float x,
773 float y)
774 {
775 EditorSelection selection = pane_.getSelection();
776
777 Point p = ev.getPoint();
778 float px = p.x-x;
779 float py = p.y-y;
780
781 if (draggingChordNum_ != 0)
782 {
783 if (chordDragIndex_ == -1)
784 {
785 draggingChordNum_ = 0;
786 pane_.repaintFilledWidthSelection();
787 }
788 else
789 {
790 SongUtils songUtils = pane_.getSongUtils();
791 LineChordPlaceholderView fromView = (LineChordPlaceholderView)lineViews_.get(draggingChordNum_);
792 ChordAnnotation fromChord = (ChordAnnotation)fromView.getModel();
793 LineTextView toView = (LineTextView)lineViews_.get(chordDragIndex_);
794 StringSongLineMember toModel = (StringSongLineMember)toView.getModel();
795
796 UndoHelper helper = new LineUndoHelper();
797 ChordAnnotation nChord = songUtils.moveChordAnnotation(
798 fromChord, toModel, chordDragOffset_, null, helper);
799
800 draggingChordNum_ = 0;
801 chordDragIndex_ = -1;
802 chordDragOffset_ = -1;
803
804 if (nChord != fromChord)
805 {
806 ChordSet chordSet = pane_.getCurChordSet();
807 EditorSelection newSelection = selection.createCopy();
808 int index = songUtils.getMemberPosition(nChord, chordSet);
809 StringSongLineMember prevMember = (StringSongLineMember)line_.getPreviousMember(nChord, chordSet);
810 newSelection.setTextSelection(index-1, prevMember.getString().length(), index+1, 0, true);
811 helper.finish(newSelection);
812 }
813 else
814 {
815 pane_.repaintFilledWidthSelection();
816 }
817 }
818 }
819 }
820
821
822 /**
823 * Handle key typed events
824 */
825 /*package*/ void handleKey(
826 KeyEvent ev,
827 float x)
828 {
829 EditorSelection selection = pane_.getSelection();
830 int metaMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
831 boolean metaDown = ((ev.getModifiers() & metaMask) == metaMask);
832
833 if (ev.getID() == KeyEvent.KEY_TYPED)
834 {
835 if (!metaDown)
836 {
837 char ch = ev.getKeyChar();
838 if (!Character.isISOControl(ch))
839 {
840 if (pane_.isEducatingQuotes() && selection.isTextSelection())
841 {
842 ch = educateQuote(selection, ch);
843 }
844 doReplaceSelection(new String(new char[]{ch}));
845 ev.consume();
846 }
847 }
848 }
849 else if (ev.getID() == KeyEvent.KEY_PRESSED)
850 {
851 if (selection.isChordSelection())
852 {
853 // Selection is in the chords
854 switch (ev.getKeyCode())
855 {
856 case KeyEvent.VK_ENTER:
857 case KeyEvent.VK_TAB:
858 {
859 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
860 LineChordPlaceholderView placeholderView = groupView.getPlaceholderView();
861 boolean foundPlaceholder = false;
862 for (int i=0; i<lineViews_.size(); ++i)
863 {
864 LineMemberView view = (LineMemberView)lineViews_.get(i);
865 if (view == placeholderView)
866 {
867 foundPlaceholder = true;
868 }
869 else if (foundPlaceholder && (view instanceof LineTextView))
870 {
871 selection.setTextSelection(i, 0, i, 0, true);
872 break;
873 }
874 }
875 ev.consume();
876 break;
877 }
878
879 case KeyEvent.VK_LEFT:
880 case KeyEvent.VK_KP_LEFT:
881 {
882 if (metaDown)
883 {
884 int line = selection.getContainingLine();
885 selection.setLineSelection(line, line+1, true);
886 }
887 else
888 {
889 moveChordSelectionLeft(selection, ev.isShiftDown());
890 }
891 ev.consume();
892 break;
893 }
894
895 case KeyEvent.VK_RIGHT:
896 case KeyEvent.VK_KP_RIGHT:
897 {
898 moveChordSelectionRight(selection, ev.isShiftDown());
899 ev.consume();
900 break;
901 }
902
903 case KeyEvent.VK_UP:
904 case KeyEvent.VK_KP_UP:
905 {
906 float localx;
907 Float lastx = selection.getLastX();
908 if (lastx == null)
909 {
910 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
911 ChordFragmentView chordView = groupView.getNthChordFragment(selection.getEndChordView());
912 localx = chordView.getXForOffset(selection.getEndChordPos());
913 }
914 else
915 {
916 localx = lastx.floatValue();
917 }
918 if (metaDown)
919 {
920 int line = selection.getContainingLine();
921 selection.setLineSelection(line, line, true);
922 }
923 else
924 {
925 blockEditor_.selectInPreviousLine(localx);
926 selection.setLastX(localx);
927 }
928 ev.consume();
929 break;
930 }
931
932 case KeyEvent.VK_DOWN:
933 case KeyEvent.VK_KP_DOWN:
934 {
935 float localx;
936 Float lastx = selection.getLastX();
937 if (lastx == null)
938 {
939 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(selection.getChordGroup());
940 ChordFragmentView chordView = groupView.getNthChordFragment(selection.getEndChordView());
941 localx = chordView.getXForOffset(selection.getEndChordPos());
942 }
943 else
944 {
945 localx = lastx.floatValue();
946 }
947 selectInLineForX(localx);
948 selection.setLastX(localx);
949 ev.consume();
950 break;
951 }
952
953 case KeyEvent.VK_BACK_SPACE:
954 {
955 doBackspace();
956 ev.consume();
957 break;
958 }
959 }
960 }
961 else if (selection.isTextSelection())
962 {
963 // Selection is in the line text
964 switch (ev.getKeyCode())
965 {
966 case KeyEvent.VK_LEFT:
967 case KeyEvent.VK_KP_LEFT:
968 {
969 if (metaDown)
970 {
971 int line = selection.getContainingLine();
972 selection.setLineSelection(line, line+1, true);
973 }
974 else
975 {
976 moveLineSelectionLeft(selection, ev.isShiftDown());
977 }
978 ev.consume();
979 break;
980 }
981
982 case KeyEvent.VK_RIGHT:
983 case KeyEvent.VK_KP_RIGHT:
984 {
985 moveLineSelectionRight(selection, ev.isShiftDown());
986 ev.consume();
987 break;
988 }
989
990 case KeyEvent.VK_UP:
991 case KeyEvent.VK_KP_UP:
992 {
993 float localx;
994 Float lastx = selection.getLastX();
995 if (lastx == null)
996 {
997 LineTextView lineView = (LineTextView)lineViews_.get(selection.getEndTextView());
998 localx = lineView.getXForOffset(selection.getEndTextPos());
999 }
1000 else
1001 {
1002 localx = lastx.floatValue();
1003 }
1004 if (metaDown)
1005 {
1006 if (selectInChordsForX(localx))
1007 {
1008 selection.setLastX(localx);
1009 }
1010 else
1011 {
1012 int line = selection.getContainingLine();
1013 selection.setLineSelection(line, line, true);
1014 }
1015 }
1016 else
1017 {
1018 blockEditor_.selectInPreviousLine(localx);
1019 selection.setLastX(localx);
1020 }
1021 ev.consume();
1022 break;
1023 }
1024
1025 case KeyEvent.VK_DOWN:
1026 case KeyEvent.VK_KP_DOWN:
1027 {
1028 float localx;
1029 Float lastx = selection.getLastX();
1030 if (lastx == null)
1031 {
1032 LineTextView lineView = (LineTextView)lineViews_.get(selection.getEndTextView());
1033 localx = lineView.getXForOffset(selection.getEndTextPos());
1034 }
1035 else
1036 {
1037 localx = lastx.floatValue();
1038 }
1039 if (metaDown)
1040 {
1041 int line = selection.getContainingLine()+1;
1042 selection.setLineSelection(line, line, true);
1043 }
1044 else
1045 {
1046 blockEditor_.selectInNextLine(localx);
1047 selection.setLastX(localx);
1048 }
1049 ev.consume();
1050 break;
1051 }
1052
1053 case KeyEvent.VK_BACK_SPACE:
1054 {
1055 doBackspace();
1056 ev.consume();
1057 break;
1058 }
1059
1060 case KeyEvent.VK_ENTER:
1061 {
1062 doEnter();
1063 ev.consume();
1064 break;
1065 }
1066
1067 case KeyEvent.VK_HOME:
1068 case KeyEvent.VK_PAGE_UP:
1069 {
1070 if (ev.isShiftDown())
1071 {
1072 selection.extendTextSelection(0, 0, true);
1073 }
1074 else
1075 {
1076 selection.setTextSelection(0, 0, 0, 0, true);
1077 }
1078 ev.consume();
1079 break;
1080 }
1081
1082 case KeyEvent.VK_END:
1083 case KeyEvent.VK_PAGE_DOWN:
1084 {
1085 int viewNum = lineViews_.size()-1;
1086 LineTextView lineView = (LineTextView)lineViews_.get(viewNum);
1087 int pos = lineView.getCharacterCount();
1088 if (ev.isShiftDown())
1089 {
1090 selection.extendTextSelection(viewNum, pos, true);
1091 }
1092 else
1093 {
1094 selection.setTextSelection(viewNum, pos, viewNum, pos, true);
1095 }
1096 ev.consume();
1097 break;
1098 }
1099 }
1100 }
1101 }
1102 }
1103
1104
1105 private char educateQuote(
1106 EditorSelection selection,
1107 char ch)
1108 {
1109 if (ch == '\"' || ch == '\'')
1110 {
1111 char prev = ' ';
1112 int view = selection.getLeftTextView();
1113 int pos = selection.getLeftTextPos()-1;
1114 if (pos < 0)
1115 {
1116 --view;
1117 }
1118 while (view >= 0)
1119 {
1120 Object obj = lineViews_.get(view);
1121 if (obj instanceof LineTextView)
1122 {
1123 LineTextView ltv = (LineTextView)obj;
1124 if (pos >= 0)
1125 {
1126 prev = ltv.getText().charAt(pos);
1127 break;
1128 }
1129 String text = ltv.getText();
1130 if (text.length() > 0)
1131 {
1132 prev = text.charAt(text.length()-1);
1133 break;
1134 }
1135 }
1136 --view;
1137 }
1138 if (Character.isWhitespace(prev))
1139 {
1140 if (ch == '\'')
1141 {
1142 return SMARTQUOTE_OPENSINGLE;
1143 }
1144 else
1145 {
1146 return SMARTQUOTE_OPENDOUBLE;
1147 }
1148 }
1149 else
1150 {
1151 if (ch == '\'')
1152 {
1153 return SMARTQUOTE_CLOSESINGLE;
1154 }
1155 else
1156 {
1157 return SMARTQUOTE_CLOSEDOUBLE;
1158 }
1159 }
1160 }
1161 return ch;
1162 }
1163
1164
1165 /*package*/ class LineUndoHelper
1166 extends UndoHelper
1167 {
1168 private int blockNum_;
1169 private int lineNum_;
1170
1171 /*package*/ LineUndoHelper()
1172 {
1173 super(pane_);
1174 lineNum_ = blockEditor_.getLineIndex(SongLineEditor.this);
1175 blockNum_ = pane_.getSongBodyEditor().getBlockIndex(blockEditor_);
1176 }
1177
1178 protected void rebuildView()
1179 {
1180 pane_.getSongBodyEditor().getNthBlock(blockNum_).getNthLine(lineNum_).rebuild(true);
1181 }
1182 }
1183
1184
1185 /*package*/ void doCopy(
1186 boolean remove)
1187 {
1188 EditorSelection selection = pane_.getSelection();
1189 DataTransferUtils dataTransferUtils = pane_.getDataTransferUtils();
1190
1191 if (selection.isChordSelection())
1192 {
1193 int chordGroup = selection.getChordGroup();
1194 ChordGroupView groupView = (ChordGroupView)chordGroupViews_.get(chordGroup);
1195 int leftView = selection.getLeftChordView();
1196 int leftPos = selection.getLeftChordPos();
1197 int rightView = selection.getRightChordView();
1198 int rightPos = selection.getRightChordPos();
1199
1200 String str = groupView.getRangeAsString(leftView, leftPos, rightView, rightPos);
1201 dataTransferUtils.setClipboardContents(new StringSelection(str));
1202 if (remove)
1203 {
1204 doDeleteSelection();
1205 }
1206 }
1207 else
1208 {
1209 assert selection.isTextSelection();
1210
1211 int leftView = selection.getLeftTextView();
1212 int leftPos = selection.getLeftTextPos();
1213 int rightView = selection.getRightTextView();
1214 int rightPos = selection.getRightTextPos();
1215
1216 EditorSelection newSelection = selection.createCopy();
1217 newSelection.setTextSelection(leftView, leftPos, leftView, leftPos, false);
1218
1219 StringSongLineMember leftModel = (StringSongLineMember)
1220 ((LineMemberView)lineViews_.get(leftView)).getModel();
1221 StringSongLineMember rightModel = (StringSongLineMember)
1222 ((LineMemberView)lineViews_.get(rightView)).getModel();
1223
1224 UndoHelper helper = null;
1225 if (remove)
1226 {
1227 helper = new LineUndoHelper();
1228 }
1229 Transferable transfer = dataTransferUtils.createLineFragmentTransferable(
1230 line_, leftModel, leftPos, rightModel, rightPos,
1231 pane_.getCurChordSet(), remove, helper);
1232 dataTransferUtils.setClipboardContents(transfer);
1233 if (helper != null)
1234 {
1235 helper.finish(newSelection);
1236 }
1237 }
1238 }
1239
1240
1241 /*package*/ void doPaste()
1242 {
1243 EditorSelection selection = pane_.getSelection();
1244 DataTransferUtils dataTransferUtils = pane_.getDataTransferUtils();
1245
1246 if (selection.isChordSelection())
1247 {
1248 Transferable transfer = dataTransferUtils.getClipboardContents();
1249 if (transfer.isDataFlavorSupported(DataFlavor.stringFlavor))
1250 {
1251 try
1252 {
1253 String str = (String)transfer.getTransferData(DataFlavor.stringFlavor);
1254 char[] chars = str.toCharArray();
1255 StringBuffer buf = new StringBuffer();
1256 for (int i=0; i<chars.length; )
1257 {
1258 char ch = chars[i++];
1259 if (ch >= 32 && ch != 127)
1260 {
1261 buf.append(ch);
1262 }
1263 }
1264 doReplaceSelection(new String(buf));
1265 }
1266 catch (UnsupportedFlavorException ex)
1267 {
1268 // Unexpected
1269 Logger.getLogger("com.virtuosotechnologies.asaph.standardgui").log(Level.WARNING,
1270 "Unexpected exception", ex);
1271 }
1272 catch (IOException ex)
1273 {
1274 Logger.getLogger("com.virtuosotechnologies.asaph.standardgui").log(Level.WARNING,
1275 "Unable to paste", ex);
1276 }
1277 }
1278 }
1279 else
1280 {
1281 assert selection.isTextSelection();
1282 int leftView = selection.getLeftTextView();
1283 int leftPos = selection.getLeftTextPos();
1284 int rightView = selection.getRightTextView();
1285 int rightPos = selection.getRightTextPos();
1286
1287 EditorSelection newSelection = selection.createCopy();
1288 newSelection.setTextSelection(leftView, leftPos, leftView, leftPos, false);
1289
1290 StringSongLineMember leftModel = (StringSongLineMember)
1291 ((LineMemberView)lineViews_.get(leftView)).getModel();
1292 StringSongLineMember rightModel = (StringSongLineMember)
1293 ((LineMemberView)lineViews_.get(rightView)).getModel();
1294
1295 try
1296 {
1297 Transferable transfer = dataTransferUtils.getClipboardContents();
1298 DataFlavor pasteType = dataTransferUtils.getPasteType(
1299 transfer, DataTransferUtils.LINEFRAGMENT_FLAVOR);
1300 Variation var = pane_.getCurVariation();
1301 ChordSet chordSet = pane_.getCurChordSet();
1302
1303 UndoHelper helper;
1304 int ocount;
1305 if (pasteType.equals(DataTransferUtils.BLOCKLIST_FLAVOR))
1306 {
1307 helper = blockEditor_.getBodyEditor().new BodyUndoHelper();
1308 ocount = line_.getSongBlock().getSo