Link Here
|
|
|
1 |
/* |
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
3 |
* |
4 |
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. |
5 |
* |
6 |
* Oracle and Java are registered trademarks of Oracle and/or its affiliates. |
7 |
* Other names may be trademarks of their respective owners. |
8 |
* |
9 |
* The contents of this file are subject to the terms of either the GNU |
10 |
* General Public License Version 2 only ("GPL") or the Common |
11 |
* Development and Distribution License("CDDL") (collectively, the |
12 |
* "License"). You may not use this file except in compliance with the |
13 |
* License. You can obtain a copy of the License at |
14 |
* http://www.netbeans.org/cddl-gplv2.html |
15 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the |
16 |
* specific language governing permissions and limitations under the |
17 |
* License. When distributing the software, include this License Header |
18 |
* Notice in each file and include the License file at |
19 |
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this |
20 |
* particular file as subject to the "Classpath" exception as provided |
21 |
* by Oracle in the GPL Version 2 section of the License file that |
22 |
* accompanied this code. If applicable, add the following below the |
23 |
* License Header, with the fields enclosed by brackets [] replaced by |
24 |
* your own identifying information: |
25 |
* "Portions Copyrighted [year] [name of copyright owner]" |
26 |
* |
27 |
* Contributor(s): |
28 |
* |
29 |
* The Original Software is NetBeans. The Initial Developer of the Original |
30 |
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun |
31 |
* Microsystems, Inc. All Rights Reserved. |
32 |
* |
33 |
* If you wish your version of this file to be governed by only the CDDL |
34 |
* or only the GPL Version 2, indicate your decision by adding |
35 |
* "[Contributor] elects to include this software in this distribution |
36 |
* under the [CDDL or GPL Version 2] license." If you do not indicate a |
37 |
* single choice of license, a recipient has the option to distribute |
38 |
* your version of this file under either the CDDL, the GPL Version 2 or |
39 |
* to extend the choice of license to its licensees as provided above. |
40 |
* However, if you add GPL Version 2 code and therefore, elected the GPL |
41 |
* Version 2 license, then the option applies only if the new code is |
42 |
* made subject to such option by the copyright holder. |
43 |
*/ |
44 |
|
45 |
package org.netbeans.modules.editor.fold.ui; |
46 |
|
47 |
import java.awt.BasicStroke; |
48 |
import java.awt.Color; |
49 |
import java.awt.Dimension; |
50 |
import java.awt.Font; |
51 |
import java.awt.FontMetrics; |
52 |
import java.awt.Graphics; |
53 |
import java.awt.Graphics2D; |
54 |
import java.awt.Point; |
55 |
import java.awt.Rectangle; |
56 |
import java.awt.Stroke; |
57 |
import java.awt.event.MouseAdapter; |
58 |
import java.awt.event.MouseEvent; |
59 |
import java.util.ArrayList; |
60 |
import java.util.Collections; |
61 |
import java.util.List; |
62 |
import java.util.Map; |
63 |
import java.util.NavigableMap; |
64 |
import java.util.TreeMap; |
65 |
import java.util.logging.Level; |
66 |
import java.util.logging.Logger; |
67 |
import java.util.prefs.PreferenceChangeEvent; |
68 |
import java.util.prefs.PreferenceChangeListener; |
69 |
import java.util.prefs.Preferences; |
70 |
import javax.accessibility.Accessible; |
71 |
import javax.accessibility.AccessibleContext; |
72 |
import javax.accessibility.AccessibleRole; |
73 |
import javax.swing.JComponent; |
74 |
import javax.swing.SwingUtilities; |
75 |
import javax.swing.event.DocumentEvent; |
76 |
import javax.swing.event.DocumentListener; |
77 |
import javax.swing.text.AbstractDocument; |
78 |
import javax.swing.text.AttributeSet; |
79 |
import javax.swing.text.BadLocationException; |
80 |
import javax.swing.text.Document; |
81 |
import javax.swing.text.JTextComponent; |
82 |
import javax.swing.text.View; |
83 |
import org.netbeans.api.editor.fold.Fold; |
84 |
import org.netbeans.api.editor.fold.FoldHierarchy; |
85 |
import org.netbeans.api.editor.fold.FoldHierarchyEvent; |
86 |
import org.netbeans.api.editor.fold.FoldHierarchyListener; |
87 |
import org.netbeans.api.editor.fold.FoldUtilities; |
88 |
import org.netbeans.api.editor.mimelookup.MimeLookup; |
89 |
import org.netbeans.api.editor.settings.AttributesUtilities; |
90 |
import org.netbeans.api.editor.settings.FontColorNames; |
91 |
import org.netbeans.api.editor.settings.FontColorSettings; |
92 |
import org.netbeans.api.editor.settings.SimpleValueNames; |
93 |
import org.netbeans.editor.BaseDocument; |
94 |
import org.netbeans.editor.BaseDocumentEvent; |
95 |
import org.netbeans.editor.BaseTextUI; |
96 |
import org.netbeans.editor.Coloring; |
97 |
import org.netbeans.editor.EditorUI; |
98 |
import org.netbeans.editor.SideBarFactory; |
99 |
import org.netbeans.editor.Utilities; |
100 |
import org.netbeans.modules.editor.fold.ApiPackageAccessor; |
101 |
import org.netbeans.modules.editor.fold.FoldHierarchyExecution; |
102 |
import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; |
103 |
import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; |
104 |
import org.netbeans.modules.editor.lib2.view.ParagraphViewDescriptor; |
105 |
import org.netbeans.modules.editor.lib2.view.ViewHierarchy; |
106 |
import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; |
107 |
import org.netbeans.modules.editor.lib2.view.ViewHierarchyListener; |
108 |
import org.openide.util.Lookup; |
109 |
import org.openide.util.LookupEvent; |
110 |
import org.openide.util.LookupListener; |
111 |
import org.openide.util.NbBundle; |
112 |
import org.openide.util.WeakListeners; |
113 |
|
114 |
/** |
115 |
* Code Folding Side Bar. Component responsible for drawing folding signs and responding |
116 |
* on user fold/unfold action. |
117 |
* <p/> |
118 |
* The class was copied/hidden from org.netbeans.editor.CodeFoldingSidebar. If any error is fixed here, |
119 |
* please fix it as well in {@link org.netbeans.editor.CodeFoldingSidebar} in this module for backward |
120 |
* compatibility with potential users. |
121 |
* |
122 |
* @author Martin Roskanin |
123 |
*/ |
124 |
public final class CodeFoldingSideBar extends JComponent implements Accessible { |
125 |
public static final String PROP_SIDEBAR_MARK = "org.netbeans.editor.CodeFoldingSidebar"; // NOI18N |
126 |
|
127 |
private static final Logger LOG = Logger.getLogger(CodeFoldingSideBar.class.getName()); |
128 |
|
129 |
/** This field should be treated as final. Subclasses are forbidden to change it. |
130 |
* @deprecated Without any replacement. |
131 |
*/ |
132 |
protected Color backColor; |
133 |
/** This field should be treated as final. Subclasses are forbidden to change it. |
134 |
* @deprecated Without any replacement. |
135 |
*/ |
136 |
protected Color foreColor; |
137 |
/** This field should be treated as final. Subclasses are forbidden to change it. |
138 |
* @deprecated Without any replacement. |
139 |
*/ |
140 |
protected Font font; |
141 |
|
142 |
/** This field should be treated as final. Subclasses are forbidden to change it. */ |
143 |
protected /*final*/ JTextComponent component; |
144 |
private volatile AttributeSet attribs; |
145 |
private Lookup.Result<? extends FontColorSettings> fcsLookupResult; |
146 |
private final LookupListener fcsTracker = new LookupListener() { |
147 |
public void resultChanged(LookupEvent ev) { |
148 |
attribs = null; |
149 |
SwingUtilities.invokeLater(new Runnable() { |
150 |
public void run() { |
151 |
//EMI: This is needed as maybe the DEFAULT_COLORING is changed, the font is different |
152 |
// and while getMarkSize() is used in paint() and will make the artifacts bigger, |
153 |
// the component itself will be the same size and it must be changed. |
154 |
// See http://www.netbeans.org/issues/show_bug.cgi?id=153316 |
155 |
updatePreferredSize(); |
156 |
CodeFoldingSideBar.this.repaint(); |
157 |
} |
158 |
}); |
159 |
} |
160 |
}; |
161 |
private final Listener listener = new Listener(); |
162 |
|
163 |
private boolean enabled = false; |
164 |
|
165 |
protected List<Mark> visibleMarks = new ArrayList<Mark>(); |
166 |
|
167 |
/** |
168 |
* Mouse moved point, possibly {@code null}. Set from mouse-moved, mouse-entered |
169 |
* handlers, so that painting will paint this fold in bold. -1, if mouse is not |
170 |
* in the sidebar region. The value is used to compute highlighted portions of the |
171 |
* folding outline. |
172 |
*/ |
173 |
private int mousePoint = -1; |
174 |
|
175 |
/** |
176 |
* if true, the {@link #mousePoint} has been already used to make a PaintInfo active. |
177 |
* The flag is tested by {@link #traverseForward} and {@link #traverseBackward} after children |
178 |
* of the current fold are processed and cleared if the {@link #mousePoint} falls to the fold area - |
179 |
* fields of PaintInfo are set accordingly. |
180 |
* It's also used to compute (current) mouseBoundary, so mouse movement does not trigger |
181 |
* refreshes eagerly |
182 |
*/ |
183 |
private boolean mousePointConsumed; |
184 |
|
185 |
/** |
186 |
* Boundaries of the current area under the mouse. Can be eiher the span of the |
187 |
* current fold (or part of it), or the span not occupied by any fold. Serves as an optimization |
188 |
* for mouse handler, which does not trigger painting (refresh) unless mouse |
189 |
* leaves this region. |
190 |
*/ |
191 |
private Rectangle mouseBoundary; |
192 |
|
193 |
/** |
194 |
* Y-end of the nearest fold that ends above the {@link #mousePoint}. Undefined if mousePoint is null. |
195 |
* These two variables are initialized at each level of folds, and help to compute {@link #mouseBoundary} for |
196 |
* the case the mousePointer is OUTSIDE all children (or outside all folds). |
197 |
*/ |
198 |
private int lowestAboveMouse = -1; |
199 |
|
200 |
/** |
201 |
* Y-begin of the nearest fold, which starts below the {@link #mousePoint}. Undefined if mousePoint is null |
202 |
*/ |
203 |
private int topmostBelowMouse = Integer.MAX_VALUE; |
204 |
|
205 |
private boolean alreadyPresent; |
206 |
|
207 |
/** Paint operations */ |
208 |
public static final int PAINT_NOOP = 0; |
209 |
/** |
210 |
* Normal opening +- marker |
211 |
*/ |
212 |
public static final int PAINT_MARK = 1; |
213 |
|
214 |
/** |
215 |
* Vertical line - typically at the end of the screen |
216 |
*/ |
217 |
public static final int PAINT_LINE = 2; |
218 |
|
219 |
/** |
220 |
* End angled line, without a sign |
221 |
*/ |
222 |
public static final int PAINT_END_MARK = 3; |
223 |
|
224 |
/** |
225 |
* Single-line marker, both start and end |
226 |
*/ |
227 |
public static final int SINGLE_PAINT_MARK = 4; |
228 |
|
229 |
/** |
230 |
* Marker value for {@link #mousePoint} indicating that mouse is outside the Component. |
231 |
*/ |
232 |
private static final int NO_MOUSE_POINT = -1; |
233 |
|
234 |
/** |
235 |
* Stroke used to draw inactive (regular) fold outlines. |
236 |
*/ |
237 |
private static Stroke LINE_DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, |
238 |
1f, new float[] { 1f, 1f }, 0f); |
239 |
|
240 |
/** |
241 |
* Stroke used to draw outlines for 'active' fold |
242 |
*/ |
243 |
private static final Stroke LINE_BOLD = new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER); |
244 |
|
245 |
private final Preferences prefs; |
246 |
private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() { |
247 |
public void preferenceChange(PreferenceChangeEvent evt) { |
248 |
String key = evt == null ? null : evt.getKey(); |
249 |
if (key == null || SimpleValueNames.CODE_FOLDING_ENABLE.equals(key)) { |
250 |
updateColors(); |
251 |
|
252 |
boolean newEnabled = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, EditorPreferencesDefaults.defaultCodeFoldingEnable); |
253 |
if (enabled != newEnabled) { |
254 |
enabled = newEnabled; |
255 |
updatePreferredSize(); |
256 |
} |
257 |
} |
258 |
} |
259 |
}; |
260 |
|
261 |
private void checkRepaint(ViewHierarchyEvent vhe) { |
262 |
if (!vhe.isChangeY()) { |
263 |
// does not obscur sidebar graphics |
264 |
return; |
265 |
} |
266 |
|
267 |
SwingUtilities.invokeLater(new Runnable() { |
268 |
public void run() { |
269 |
updatePreferredSize(); |
270 |
CodeFoldingSideBar.this.repaint(); |
271 |
} |
272 |
}); |
273 |
} |
274 |
|
275 |
/** |
276 |
* @deprecated Don't use this constructor, it does nothing! |
277 |
*/ |
278 |
public CodeFoldingSideBar() { |
279 |
component = null; |
280 |
prefs = null; |
281 |
throw new IllegalStateException("Do not use this constructor!"); //NOI18N |
282 |
} |
283 |
|
284 |
public CodeFoldingSideBar(final JTextComponent component){ |
285 |
super(); |
286 |
this.component = component; |
287 |
|
288 |
// prevent from display CF sidebar twice |
289 |
if (component.getClientProperty(PROP_SIDEBAR_MARK) == null) { |
290 |
component.putClientProperty("org.netbeans.editor.CodeFoldingSidebar", Boolean.TRUE); |
291 |
} else { |
292 |
alreadyPresent = true; |
293 |
} |
294 |
|
295 |
addMouseListener(listener); |
296 |
addMouseMotionListener(listener); |
297 |
|
298 |
final FoldHierarchy foldHierarchy = FoldHierarchy.get(component); |
299 |
foldHierarchy.addFoldHierarchyListener(WeakListeners.create(FoldHierarchyListener.class, listener, foldHierarchy)); |
300 |
|
301 |
final Document doc = getDocument(); |
302 |
doc.addDocumentListener(WeakListeners.document(listener, doc)); |
303 |
setOpaque(true); |
304 |
|
305 |
prefs = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)).lookup(Preferences.class); |
306 |
prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs)); |
307 |
prefsListener.preferenceChange(null); |
308 |
|
309 |
ViewHierarchy.get(component).addViewHierarchyListener(new ViewHierarchyListener() { |
310 |
|
311 |
@Override |
312 |
public void viewHierarchyChanged(ViewHierarchyEvent evt) { |
313 |
checkRepaint(evt); |
314 |
} |
315 |
|
316 |
}); |
317 |
} |
318 |
|
319 |
private boolean hasProviders() { |
320 |
final FoldHierarchy foldHierarchy = FoldHierarchy.get(component); |
321 |
FoldHierarchyExecution exec = ApiPackageAccessor.get().foldGetExecution(foldHierarchy); |
322 |
exec.lock(); |
323 |
try { |
324 |
return exec.hasProviders(); |
325 |
} finally { |
326 |
exec.unlock(); |
327 |
} |
328 |
|
329 |
} |
330 |
|
331 |
private void updatePreferredSize() { |
332 |
// do not show at all, if there are no providers registered. |
333 |
if (enabled && hasProviders() && !alreadyPresent) { |
334 |
setPreferredSize(new Dimension(getColoring().getFont().getSize(), component.getHeight())); |
335 |
setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); |
336 |
}else{ |
337 |
setPreferredSize(new Dimension(0,0)); |
338 |
setMaximumSize(new Dimension(0,0)); |
339 |
} |
340 |
revalidate(); |
341 |
} |
342 |
|
343 |
private void updateColors() { |
344 |
Coloring c = getColoring(); |
345 |
this.backColor = c.getBackColor(); |
346 |
this.foreColor = c.getForeColor(); |
347 |
this.font = c.getFont(); |
348 |
} |
349 |
|
350 |
/** |
351 |
* This method should be treated as final. Subclasses are forbidden to override it. |
352 |
* @return The background color used for painting this component. |
353 |
* @deprecated Without any replacement. |
354 |
*/ |
355 |
protected Color getBackColor() { |
356 |
if (backColor == null) { |
357 |
updateColors(); |
358 |
} |
359 |
return backColor; |
360 |
} |
361 |
|
362 |
/** |
363 |
* This method should be treated as final. Subclasses are forbidden to override it. |
364 |
* @return The foreground color used for painting this component. |
365 |
* @deprecated Without any replacement. |
366 |
*/ |
367 |
protected Color getForeColor() { |
368 |
if (foreColor == null) { |
369 |
updateColors(); |
370 |
} |
371 |
return foreColor; |
372 |
} |
373 |
|
374 |
/** |
375 |
* This method should be treated as final. Subclasses are forbidden to override it. |
376 |
* @return The font used for painting this component. |
377 |
* @deprecated Without any replacement. |
378 |
*/ |
379 |
protected Font getColoringFont() { |
380 |
if (font == null) { |
381 |
updateColors(); |
382 |
} |
383 |
return font; |
384 |
} |
385 |
|
386 |
// overriding due to issue #60304 |
387 |
public @Override void update(Graphics g) { |
388 |
} |
389 |
|
390 |
protected void collectPaintInfos( |
391 |
View rootView, Fold fold, Map<Integer, PaintInfo> map, int level, int startIndex, int endIndex |
392 |
) throws BadLocationException { |
393 |
//never called |
394 |
} |
395 |
|
396 |
/** |
397 |
* Adjust lowest/topmost boundaries from the Fold range y1-y2. |
398 |
* @param y1 |
399 |
* @param y2 |
400 |
* @param level |
401 |
*/ |
402 |
private void setMouseBoundaries(int y1, int y2, int level) { |
403 |
if (!hasMousePoint() || mousePointConsumed) { |
404 |
return; |
405 |
} |
406 |
int y = mousePoint; |
407 |
if (y2 < y && lowestAboveMouse < y2) { |
408 |
LOG.log(Level.FINEST, "lowestAbove at {1}: {0}", new Object[] { y2, level }); |
409 |
lowestAboveMouse = y2; |
410 |
} |
411 |
if (y1 > y && topmostBelowMouse > y1) { |
412 |
LOG.log(Level.FINEST, "topmostBelow at {1}: {0}", new Object[] { y1, level }); |
413 |
topmostBelowMouse = y1; |
414 |
} |
415 |
} |
416 |
|
417 |
/* |
418 |
* Even collapsed fold MAY contain a continuation line, IF one of folds on the same line is NOT collapsed. Such a fold should |
419 |
* then visually span multiple lines && be marked as collapsed. |
420 |
*/ |
421 |
|
422 |
protected List<? extends PaintInfo> getPaintInfo(Rectangle clip) throws BadLocationException { |
423 |
javax.swing.plaf.TextUI textUI = component.getUI(); |
424 |
if (!(textUI instanceof BaseTextUI)) { |
425 |
return Collections.<PaintInfo>emptyList(); |
426 |
} |
427 |
BaseTextUI baseTextUI = (BaseTextUI)textUI; |
428 |
BaseDocument bdoc = Utilities.getDocument(component); |
429 |
if (bdoc == null) { |
430 |
return Collections.<PaintInfo>emptyList(); |
431 |
} |
432 |
mousePointConsumed = false; |
433 |
mouseBoundary = null; |
434 |
topmostBelowMouse = Integer.MAX_VALUE; |
435 |
lowestAboveMouse = -1; |
436 |
bdoc.readLock(); |
437 |
try { |
438 |
int startPos = baseTextUI.getPosFromY(clip.y); |
439 |
int endPos = baseTextUI.viewToModel(component, Short.MAX_VALUE / 2, clip.y + clip.height); |
440 |
|
441 |
if (startPos < 0 || endPos < 0) { |
442 |
// editor window is not properly sized yet; return no infos |
443 |
return Collections.<PaintInfo>emptyList(); |
444 |
} |
445 |
|
446 |
// #218282: if the view hierarchy is not yet updated, the Y coordinate may map to an incorrect offset outside |
447 |
// the document. |
448 |
int docLen = bdoc.getLength(); |
449 |
if (startPos >= docLen || endPos > docLen) { |
450 |
return Collections.<PaintInfo>emptyList(); |
451 |
} |
452 |
|
453 |
startPos = Utilities.getRowStart(bdoc, startPos); |
454 |
endPos = Utilities.getRowEnd(bdoc, endPos); |
455 |
|
456 |
FoldHierarchy hierarchy = FoldHierarchy.get(component); |
457 |
hierarchy.lock(); |
458 |
try { |
459 |
View rootView = Utilities.getDocumentView(component); |
460 |
if (rootView != null) { |
461 |
Object [] arr = getFoldList(hierarchy.getRootFold(), startPos, endPos); |
462 |
@SuppressWarnings("unchecked") |
463 |
List<? extends Fold> foldList = (List<? extends Fold>) arr[0]; |
464 |
int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; |
465 |
|
466 |
/* |
467 |
* Note: |
468 |
* |
469 |
* The Map is keyed by Y-VISUAL position of the fold mark, not the textual offset of line start. |
470 |
* This is because several folds may occupy the same line, while only one + sign is displayed, |
471 |
* and affect the last fold in the row. |
472 |
*/ |
473 |
NavigableMap<Integer, PaintInfo> map = new TreeMap<Integer, PaintInfo>(); |
474 |
// search backwards |
475 |
for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { |
476 |
Fold fold = foldList.get(i); |
477 |
if (!traverseBackwards(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { |
478 |
break; |
479 |
} |
480 |
} |
481 |
|
482 |
// search forward |
483 |
for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { |
484 |
Fold fold = foldList.get(i); |
485 |
if (!traverseForward(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { |
486 |
break; |
487 |
} |
488 |
} |
489 |
|
490 |
if (map.isEmpty() && foldList.size() > 0) { |
491 |
assert foldList.size() == 1; |
492 |
PaintInfo pi = new PaintInfo(PAINT_LINE, 0, clip.y, clip.height, -1, -1); |
493 |
mouseBoundary = new Rectangle(0, 0, 0, clip.height); |
494 |
LOG.log(Level.FINEST, "Mouse boundary for full side line set to: {0}", mouseBoundary); |
495 |
if (hasMousePoint()) { |
496 |
pi.markActive(true, true, true); |
497 |
} |
498 |
return Collections.singletonList(pi); |
499 |
} else { |
500 |
if (mouseBoundary == null) { |
501 |
mouseBoundary = makeMouseBoundary(clip.y, clip.y + clip.height); |
502 |
LOG.log(Level.FINEST, "Mouse boundary not set, defaulting to: {0}", mouseBoundary); |
503 |
} |
504 |
return new ArrayList<PaintInfo>(map.values()); |
505 |
} |
506 |
} else { |
507 |
return Collections.<PaintInfo>emptyList(); |
508 |
} |
509 |
} finally { |
510 |
hierarchy.unlock(); |
511 |
} |
512 |
} finally { |
513 |
bdoc.readUnlock(); |
514 |
} |
515 |
} |
516 |
|
517 |
/** |
518 |
* Adds a paint info to the map. If a paintinfo already exists, it merges |
519 |
* the structures, so the painting process can just follow the instructions. |
520 |
* |
521 |
* @param infos |
522 |
* @param yOffset |
523 |
* @param nextInfo |
524 |
*/ |
525 |
private void addPaintInfo(Map<Integer, PaintInfo> infos, int yOffset, PaintInfo nextInfo) { |
526 |
PaintInfo prevInfo = infos.get(yOffset); |
527 |
nextInfo.mergeWith(prevInfo); |
528 |
infos.put(yOffset, nextInfo); |
529 |
} |
530 |
|
531 |
private boolean traverseForward(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary,int level, NavigableMap<Integer, PaintInfo> infos) throws BadLocationException { |
532 |
// System.out.println("~~~ traverseForward<" + lowerBoundary + ", " + upperBoundary |
533 |
// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " |
534 |
// + (f.getStartOffset() > upperBoundary ? ", f.gSO > uB" : "") |
535 |
// + ", level=" + level); |
536 |
|
537 |
if (f.getStartOffset() > upperBoundary) { |
538 |
return false; |
539 |
} |
540 |
|
541 |
int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); |
542 |
int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); |
543 |
int y1 = btui.getYFromPos(lineStartOffset1); |
544 |
int h = btui.getEditorUI().getLineHeight(); |
545 |
int y2 = btui.getYFromPos(lineStartOffset2); |
546 |
|
547 |
// the 'active' flags can be set only after children are processed; highlights |
548 |
// correspond to the innermost expanded child. |
549 |
boolean activeMark = false; |
550 |
boolean activeIn = false; |
551 |
boolean activeOut = false; |
552 |
PaintInfo spi; |
553 |
boolean activated; |
554 |
|
555 |
if (y1 == y2) { |
556 |
// whole fold is on a single line |
557 |
spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); |
558 |
if (activated = isActivated(y1, y1 + h)) { |
559 |
activeMark = true; |
560 |
} |
561 |
addPaintInfo(infos, y1, spi); |
562 |
} else { |
563 |
// fold spans multiple lines |
564 |
spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); |
565 |
if (activated = isActivated(y1, y2 + h / 2)) { |
566 |
activeMark = true; |
567 |
activeOut = true; |
568 |
} |
569 |
addPaintInfo(infos, y1, spi); |
570 |
} |
571 |
|
572 |
setMouseBoundaries(y1, y2 + h / 2, level); |
573 |
|
574 |
// Handle end mark after possible inner folds were processed because |
575 |
// otherwise if there would be two nested folds both ending at the same line |
576 |
// then the end mark for outer one would be replaced by an end mark for inner one |
577 |
// (same key in infos map) and the painting code would continue to paint line marking a fold |
578 |
// until next fold is reached (or end of doc). |
579 |
PaintInfo epi = null; |
580 |
if (y1 != y2 && !f.isCollapsed() && f.getEndOffset() <= upperBoundary) { |
581 |
epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); |
582 |
addPaintInfo(infos, y2, epi); |
583 |
} |
584 |
|
585 |
// save the topmost/lowest information, reset for child processing |
586 |
int topmost = topmostBelowMouse; |
587 |
int lowest = lowestAboveMouse; |
588 |
topmostBelowMouse = y2 + h / 2; |
589 |
lowestAboveMouse = y1; |
590 |
|
591 |
try { |
592 |
if (!f.isCollapsed()) { |
593 |
Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); |
594 |
@SuppressWarnings("unchecked") |
595 |
List<? extends Fold> foldList = (List<? extends Fold>) arr[0]; |
596 |
int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; |
597 |
|
598 |
// search backwards |
599 |
for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { |
600 |
Fold fold = foldList.get(i); |
601 |
if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { |
602 |
break; |
603 |
} |
604 |
} |
605 |
|
606 |
// search forward |
607 |
for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { |
608 |
Fold fold = foldList.get(i); |
609 |
if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { |
610 |
return false; |
611 |
} |
612 |
} |
613 |
} |
614 |
if (!mousePointConsumed && activated) { |
615 |
mousePointConsumed = true; |
616 |
mouseBoundary = makeMouseBoundary(y1, y2 + h); |
617 |
LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); |
618 |
spi.markActive(activeMark, activeIn, activeOut); |
619 |
if (epi != null) { |
620 |
epi.markActive(true, true, false); |
621 |
} |
622 |
markDeepChildrenActive(infos, y1, y2, level); |
623 |
} |
624 |
} finally { |
625 |
topmostBelowMouse = topmost; |
626 |
lowestAboveMouse = lowest; |
627 |
} |
628 |
return true; |
629 |
} |
630 |
|
631 |
/** |
632 |
* Sets outlines of all children to 'active'. Assuming yFrom and yTo are from-to Y-coordinates of the parent |
633 |
* fold, it finds all nested folds (folds, which are in between yFrom and yTo) and changes their in/out lines |
634 |
* as active. |
635 |
* The method returns Y start coordinate of the 1st child found. |
636 |
* |
637 |
* @param infos fold infos collected so far |
638 |
* @param yFrom upper Y-coordinate of the parent fold |
639 |
* @param yTo lower Y-coordinate of the parent fold |
640 |
* @param level level of the parent fold |
641 |
* @return Y-coordinate of the 1st child. |
642 |
*/ |
643 |
private int markDeepChildrenActive(NavigableMap<Integer, PaintInfo> infos, int yFrom, int yTo, int level) { |
644 |
int result = Integer.MAX_VALUE; |
645 |
Map<Integer, PaintInfo> m = infos.subMap(yFrom, yTo); |
646 |
for (Map.Entry<Integer, PaintInfo> me : m.entrySet()) { |
647 |
PaintInfo pi = me.getValue(); |
648 |
int y = pi.getPaintY(); |
649 |
if (y > yFrom && y < yTo) { |
650 |
if (LOG.isLoggable(Level.FINEST)) { |
651 |
LOG.log(Level.FINEST, "Marking chind as active: {0}", pi); |
652 |
} |
653 |
pi.markActive(false, true, true); |
654 |
if (y < result) { |
655 |
y = result; |
656 |
} |
657 |
} |
658 |
} |
659 |
return result; |
660 |
} |
661 |
|
662 |
/** |
663 |
* Returns stroke appropriate for painting (in)active outlines |
664 |
* @param s the default stroke |
665 |
* @param active true for active outlines |
666 |
* @return value of 's' or a Stroke which should be used to paint the outline. |
667 |
*/ |
668 |
private static Stroke getStroke(Stroke s, boolean active) { |
669 |
if (active) { |
670 |
return LINE_BOLD; |
671 |
} else { |
672 |
return s; |
673 |
} |
674 |
} |
675 |
|
676 |
private boolean traverseBackwards(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary, int level, NavigableMap<Integer, PaintInfo> infos) throws BadLocationException { |
677 |
// System.out.println("~~~ traverseBackwards<" + lowerBoundary + ", " + upperBoundary |
678 |
// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " |
679 |
// + (f.getEndOffset() < lowerBoundary ? ", f.gEO < lB" : "") |
680 |
// + ", level=" + level); |
681 |
|
682 |
if (f.getEndOffset() < lowerBoundary) { |
683 |
return false; |
684 |
} |
685 |
|
686 |
int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); |
687 |
int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); |
688 |
int h = btui.getEditorUI().getLineHeight(); |
689 |
|
690 |
boolean activeMark = false; |
691 |
boolean activeIn = false; |
692 |
boolean activeOut = false; |
693 |
PaintInfo spi = null; |
694 |
PaintInfo epi = null; |
695 |
boolean activated = false; |
696 |
int y1 = 0; |
697 |
int y2 = 0; |
698 |
|
699 |
if (lineStartOffset1 == lineStartOffset2) { |
700 |
// whole fold is on a single line |
701 |
y2 = y1 = btui.getYFromPos(lineStartOffset1); |
702 |
spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset1); |
703 |
if (activated = isActivated(y1, y1 + h)) { |
704 |
activeMark = true; |
705 |
} |
706 |
addPaintInfo(infos, y1, spi); |
707 |
} else { |
708 |
y2 = btui.getYFromPos(lineStartOffset2); |
709 |
// fold spans multiple lines |
710 |
y1 = btui.getYFromPos(lineStartOffset1); |
711 |
activated = isActivated(y1, y2 + h / 2); |
712 |
if (f.getStartOffset() >= upperBoundary) { |
713 |
spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); |
714 |
if (activated) { |
715 |
activeMark = true; |
716 |
activeOut = true; |
717 |
} |
718 |
addPaintInfo(infos, y1, spi); |
719 |
} |
720 |
|
721 |
if (!f.isCollapsed() && f.getEndOffset() <= upperBoundary) { |
722 |
activated |= isActivated(y1, y2 + h / 2); |
723 |
epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); |
724 |
addPaintInfo(infos, y2, epi); |
725 |
} |
726 |
} |
727 |
|
728 |
setMouseBoundaries(y1, y2 + h / 2, level); |
729 |
|
730 |
// save the topmost/lowest information, reset for child processing |
731 |
int topmost = topmostBelowMouse; |
732 |
int lowest = lowestAboveMouse; |
733 |
topmostBelowMouse = y2 + h /2; |
734 |
lowestAboveMouse = y1; |
735 |
|
736 |
try { |
737 |
if (!f.isCollapsed()) { |
738 |
Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); |
739 |
@SuppressWarnings("unchecked") |
740 |
List<? extends Fold> foldList = (List<? extends Fold>) arr[0]; |
741 |
int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; |
742 |
|
743 |
// search backwards |
744 |
for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { |
745 |
Fold fold = foldList.get(i); |
746 |
if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { |
747 |
return false; |
748 |
} |
749 |
} |
750 |
|
751 |
// search forward |
752 |
for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { |
753 |
Fold fold = foldList.get(i); |
754 |
if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { |
755 |
break; |
756 |
} |
757 |
} |
758 |
} |
759 |
if (!mousePointConsumed && activated) { |
760 |
mousePointConsumed = true; |
761 |
mouseBoundary = makeMouseBoundary(y1, y2 + h); |
762 |
LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); |
763 |
if (spi != null) { |
764 |
spi.markActive(activeMark, activeIn, activeOut); |
765 |
} |
766 |
if (epi != null) { |
767 |
epi.markActive(true, true, false); |
768 |
} |
769 |
int lowestChild = markDeepChildrenActive(infos, y1, y2, level); |
770 |
if (lowestChild < Integer.MAX_VALUE && lineStartOffset1 < upperBoundary) { |
771 |
// the fold starts above the screen clip region, and is 'activated'. We need to setup instructions to draw activated line up to the |
772 |
// 1st child marker. |
773 |
epi = new PaintInfo(PAINT_LINE, level, y1, y2 - y1, false, lineStartOffset1, lineStartOffset2); |
774 |
epi.markActive(true, true, false); |
775 |
addPaintInfo(infos, y1, epi); |
776 |
} |
777 |
} |
778 |
} finally { |
779 |
topmostBelowMouse = topmost; |
780 |
lowestAboveMouse = lowest; |
781 |
} |
782 |
return true; |
783 |
} |
784 |
|
785 |
private Rectangle makeMouseBoundary(int y1, int y2) { |
786 |
if (!hasMousePoint()) { |
787 |
return null; |
788 |
} |
789 |
if (topmostBelowMouse < Integer.MAX_VALUE) { |
790 |
y2 = topmostBelowMouse; |
791 |
} |
792 |
if (lowestAboveMouse > -1) { |
793 |
y1 = lowestAboveMouse; |
794 |
} |
795 |
return new Rectangle(0, y1, 0, y2 - y1); |
796 |
} |
797 |
|
798 |
protected EditorUI getEditorUI(){ |
799 |
return Utilities.getEditorUI(component); |
800 |
} |
801 |
|
802 |
protected Document getDocument(){ |
803 |
return component.getDocument(); |
804 |
} |
805 |
|
806 |
|
807 |
private Fold getLastLineFold(FoldHierarchy hierarchy, int rowStart, int rowEnd, boolean shift){ |
808 |
Fold fold = FoldUtilities.findNearestFold(hierarchy, rowStart); |
809 |
Fold prevFold = fold; |
810 |
while (fold != null && fold.getStartOffset()<rowEnd){ |
811 |
Fold nextFold = FoldUtilities.findNearestFold(hierarchy, (fold.isCollapsed()) ? fold.getEndOffset() : fold.getStartOffset()+1); |
812 |
if (nextFold == fold) return fold; |
813 |
if (nextFold!=null && nextFold.getStartOffset() < rowEnd){ |
814 |
prevFold = shift ? fold : nextFold; |
815 |
fold = nextFold; |
816 |
}else{ |
817 |
return prevFold; |
818 |
} |
819 |
} |
820 |
return prevFold; |
821 |
} |
822 |
|
823 |
protected void performAction(Mark mark) { |
824 |
performAction(mark, false); |
825 |
} |
826 |
|
827 |
private void performActionAt(Mark mark, int mouseY) throws BadLocationException { |
828 |
if (mark != null) { |
829 |
return; |
830 |
} |
831 |
BaseDocument bdoc = Utilities.getDocument(component); |
832 |
BaseTextUI textUI = (BaseTextUI)component.getUI(); |
833 |
|
834 |
View rootView = Utilities.getDocumentView(component); |
835 |
if (rootView == null) return; |
836 |
|
837 |
bdoc.readLock(); |
838 |
try { |
839 |
int yOffset = textUI.getPosFromY(mouseY); |
840 |
FoldHierarchy hierarchy = FoldHierarchy.get(component); |
841 |
hierarchy.lock(); |
842 |
try { |
843 |
Fold f = FoldUtilities.findOffsetFold(hierarchy, yOffset); |
844 |
if (f == null) { |
845 |
return; |
846 |
} |
847 |
if (f.isCollapsed()) { |
848 |
LOG.log(Level.WARNING, "Clicked on a collapsed fold {0} at {1}", new Object[] { f, mouseY }); |
849 |
return; |
850 |
} |
851 |
int startOffset = f.getStartOffset(); |
852 |
int endOffset = f.getEndOffset(); |
853 |
|
854 |
int startY = textUI.getYFromPos(startOffset); |
855 |
int nextLineOffset = Utilities.getRowStart(bdoc, startOffset, 1); |
856 |
int nextY = textUI.getYFromPos(nextLineOffset); |
857 |
|
858 |
if (mouseY >= startY && mouseY <= nextY) { |
859 |
LOG.log(Level.FINEST, "Starting line clicked, ignoring. MouseY={0}, startY={1}, nextY={2}", |
860 |
new Object[] { mouseY, startY, nextY }); |
861 |
return; |
862 |
} |
863 |
|
864 |
startY = textUI.getYFromPos(endOffset); |
865 |
nextLineOffset = Utilities.getRowStart(bdoc, endOffset, 1); |
866 |
nextY = textUI.getYFromPos(nextLineOffset); |
867 |
|
868 |
if (mouseY >= startY && mouseY <= nextY) { |
869 |
// the mouse can be positioned above the marker (the fold found above), or |
870 |
// below it; in that case, the immediate enclosing fold should be used - should be the fold |
871 |
// that corresponds to the nextLineOffset, if any |
872 |
int h2 = (startY + nextY) / 2; |
873 |
if (mouseY >= h2) { |
874 |
Fold f2 = f; |
875 |
|
876 |
f = FoldUtilities.findOffsetFold(hierarchy, nextLineOffset); |
877 |
if (f == null) { |
878 |
// fold does not exist for the position below end-of-fold indicator |
879 |
return; |
880 |
} |
881 |
} |
882 |
|
883 |
} |
884 |
|
885 |
LOG.log(Level.FINEST, "Collapsing fold: {0}", f); |
886 |
hierarchy.collapse(f); |
887 |
} finally { |
888 |
hierarchy.unlock(); |
889 |
} |
890 |
} finally { |
891 |
bdoc.readUnlock(); |
892 |
} |
893 |
} |
894 |
|
895 |
private void performAction(final Mark mark, final boolean shiftFold) { |
896 |
Document doc = component.getDocument(); |
897 |
doc.render(new Runnable() { |
898 |
@Override |
899 |
public void run() { |
900 |
ViewHierarchy vh = ViewHierarchy.get(component); |
901 |
LockedViewHierarchy lockedVH = vh.lock(); |
902 |
try { |
903 |
int pViewIndex = lockedVH.yToParagraphViewIndex(mark.y + mark.size / 2); |
904 |
if (pViewIndex >= 0) { |
905 |
ParagraphViewDescriptor pViewDesc = lockedVH.getParagraphViewDescriptor(pViewIndex); |
906 |
int pViewStartOffset = pViewDesc.getStartOffset(); |
907 |
int pViewEndOffset = pViewStartOffset + pViewDesc.getLength(); |
908 |
// Find corresponding fold |
909 |
FoldHierarchy foldHierarchy = FoldHierarchy.get(component); |
910 |
foldHierarchy.lock(); |
911 |
try { |
912 |
int rowStart = javax.swing.text.Utilities.getRowStart(component, pViewStartOffset); |
913 |
int rowEnd = javax.swing.text.Utilities.getRowEnd(component, pViewStartOffset); |
914 |
Fold clickedFold = getLastLineFold(foldHierarchy, rowStart, rowEnd, shiftFold);//FoldUtilities.findNearestFold(foldHierarchy, viewStartOffset); |
915 |
if (clickedFold != null && clickedFold.getStartOffset() < pViewEndOffset) { |
916 |
foldHierarchy.toggle(clickedFold); |
917 |
} |
918 |
} catch (BadLocationException ble) { |
919 |
LOG.log(Level.WARNING, null, ble); |
920 |
} finally { |
921 |
foldHierarchy.unlock(); |
922 |
} |
923 |
} |
924 |
} finally { |
925 |
lockedVH.unlock(); |
926 |
} |
927 |
} |
928 |
}); |
929 |
} |
930 |
|
931 |
protected int getMarkSize(Graphics g){ |
932 |
if (g != null){ |
933 |
FontMetrics fm = g.getFontMetrics(getColoring().getFont()); |
934 |
if (fm != null){ |
935 |
int ret = fm.getAscent() - fm.getDescent(); |
936 |
return ret - ret%2; |
937 |
} |
938 |
} |
939 |
return -1; |
940 |
} |
941 |
|
942 |
private boolean hasMousePoint() { |
943 |
return mousePoint >= 0; |
944 |
} |
945 |
|
946 |
private boolean isActivated(int y1, int y2) { |
947 |
return hasMousePoint() && |
948 |
(mousePoint >= y1 && mousePoint < y2); |
949 |
} |
950 |
|
951 |
private void drawFoldLine(Graphics2D g2d, boolean active, int x1, int y1, int x2, int y2) { |
952 |
Stroke origStroke = g2d.getStroke(); |
953 |
g2d.setStroke(getStroke(origStroke, active)); |
954 |
g2d.drawLine(x1, y1, x2, y2); |
955 |
g2d.setStroke(origStroke); |
956 |
} |
957 |
|
958 |
protected @Override void paintComponent(Graphics g) { |
959 |
if (!enabled) { |
960 |
return; |
961 |
} |
962 |
|
963 |
Rectangle clip = getVisibleRect();//g.getClipBounds(); |
964 |
visibleMarks.clear(); |
965 |
|
966 |
Coloring coloring = getColoring(); |
967 |
g.setColor(coloring.getBackColor()); |
968 |
g.fillRect(clip.x, clip.y, clip.width, clip.height); |
969 |
g.setColor(coloring.getForeColor()); |
970 |
|
971 |
AbstractDocument adoc = (AbstractDocument)component.getDocument(); |
972 |
adoc.readLock(); |
973 |
try { |
974 |
List<? extends PaintInfo> ps = getPaintInfo(clip); |
975 |
Font defFont = coloring.getFont(); |
976 |
int markSize = getMarkSize(g); |
977 |
int halfMarkSize = markSize / 2; |
978 |
int markX = (defFont.getSize() - markSize) / 2; // x position of mark rectangle |
979 |
int plusGap = (int)Math.round(markSize / 3.8); // distance between mark rectangle vertical side and start/end of minus sign |
980 |
int lineX = markX + halfMarkSize; // x position of the centre of mark |
981 |
|
982 |
LOG.fine("CFSBar: PAINT START ------\n"); |
983 |
int descent = g.getFontMetrics(defFont).getDescent(); |
984 |
PaintInfo previousInfo = null; |
985 |
Graphics2D g2d = (Graphics2D)g; |
986 |
LOG.log(Level.FINEST, "MousePoint: {0}", mousePoint); |
987 |
|
988 |
for(PaintInfo paintInfo : ps) { |
989 |
boolean isFolded = paintInfo.isCollapsed(); |
990 |
int y = paintInfo.getPaintY(); |
991 |
int height = paintInfo.getPaintHeight(); |
992 |
int markY = y + descent; // y position of mark rectangle |
993 |
int paintOperation = paintInfo.getPaintOperation(); |
994 |
|
995 |
if (previousInfo == null) { |
996 |
if (paintInfo.hasLineIn()) { |
997 |
if (LOG.isLoggable(Level.FINE)) { |
998 |
LOG.fine("prevInfo=NULL; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N |
999 |
} |
1000 |
drawFoldLine(g2d, paintInfo.lineInActive, lineX, clip.y, lineX, y); |
1001 |
} |
1002 |
} else { |
1003 |
if (previousInfo.hasLineOut() || paintInfo.hasLineIn()) { |
1004 |
// Draw middle vertical line |
1005 |
int prevY = previousInfo.getPaintY(); |
1006 |
if (LOG.isLoggable(Level.FINE)) { |
1007 |
LOG.log(Level.FINE, "prevInfo={0}; y=" + y + ", PI:" + paintInfo + "\n", previousInfo); // NOI18N |
1008 |
} |
1009 |
drawFoldLine(g2d, previousInfo.lineOutActive || paintInfo.lineInActive, lineX, prevY + previousInfo.getPaintHeight(), lineX, y); |
1010 |
} |
1011 |
} |
1012 |
|
1013 |
if (paintInfo.hasSign()) { |
1014 |
g.drawRect(markX, markY, markSize, markSize); |
1015 |
g.drawLine(plusGap + markX, markY + halfMarkSize, markSize + markX - plusGap, markY + halfMarkSize); |
1016 |
String opStr = (paintOperation == PAINT_MARK) ? "PAINT_MARK" : "SINGLE_PAINT_MARK"; // NOI18N |
1017 |
if (isFolded) { |
1018 |
if (LOG.isLoggable(Level.FINE)) { |
1019 |
LOG.fine(opStr + ": folded; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N |
1020 |
} |
1021 |
g.drawLine(lineX, markY + plusGap, lineX, markY + markSize - plusGap); |
1022 |
} |
1023 |
if (paintOperation != SINGLE_PAINT_MARK) { |
1024 |
if (LOG.isLoggable(Level.FINE)) { |
1025 |
LOG.fine(opStr + ": non-single; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N |
1026 |
} |
1027 |
} |
1028 |
if (paintInfo.hasLineIn()) { //[PENDING] |
1029 |
drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, markY); |
1030 |
} |
1031 |
if (paintInfo.hasLineOut()) { |
1032 |
// This is an error in case there's a next paint info at the same y which is an end mark |
1033 |
// for this mark (it must be cleared explicitly). |
1034 |
drawFoldLine(g2d, paintInfo.lineOutActive, lineX, markY + markSize, lineX, y + height); |
1035 |
} |
1036 |
visibleMarks.add(new Mark(markX, markY, markSize, isFolded)); |
1037 |
|
1038 |
} else if (paintOperation == PAINT_LINE) { |
1039 |
if (LOG.isLoggable(Level.FINE)) { |
1040 |
LOG.fine("PAINT_LINE: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N |
1041 |
} |
1042 |
// FIXME !! |
1043 |
drawFoldLine(g2d, paintInfo.signActive, lineX, y, lineX, y + height ); |
1044 |
} else if (paintOperation == PAINT_END_MARK) { |
1045 |
if (LOG.isLoggable(Level.FINE)) { |
1046 |
LOG.fine("PAINT_END_MARK: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N |
1047 |
} |
1048 |
if (previousInfo == null || y != previousInfo.getPaintY()) { |
1049 |
drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, y + height / 2); |
1050 |
drawFoldLine(g2d, paintInfo.signActive, lineX, y + height / 2, lineX + halfMarkSize, y + height / 2); |
1051 |
if (paintInfo.getInnerLevel() > 0) {//[PENDING] |
1052 |
if (LOG.isLoggable(Level.FINE)) { |
1053 |
LOG.fine(" PAINT middle-line\n"); // NOI18N |
1054 |
} |
1055 |
drawFoldLine(g2d, paintInfo.lineOutActive, lineX, y + height / 2, lineX, y + height); |
1056 |
} |
1057 |
} |
1058 |
} |
1059 |
|
1060 |
previousInfo = paintInfo; |
1061 |
} |
1062 |
|
1063 |
if (previousInfo != null && |
1064 |
(previousInfo.getInnerLevel() > 0 || |
1065 |
(previousInfo.getPaintOperation() == PAINT_MARK && !previousInfo.isCollapsed())) |
1066 |
) { |
1067 |
drawFoldLine(g2d, previousInfo.lineOutActive, |
1068 |
lineX, previousInfo.getPaintY() + previousInfo.getPaintHeight(), lineX, clip.y + clip.height); |
1069 |
} |
1070 |
|
1071 |
} catch (BadLocationException ble) { |
1072 |
LOG.log(Level.WARNING, null, ble); |
1073 |
} finally { |
1074 |
LOG.fine("CFSBar: PAINT END ------\n\n"); |
1075 |
adoc.readUnlock(); |
1076 |
} |
1077 |
} |
1078 |
|
1079 |
private static Object [] getFoldList(Fold parentFold, int start, int end) { |
1080 |
List<Fold> ret = new ArrayList<Fold>(); |
1081 |
|
1082 |
int index = FoldUtilities.findFoldEndIndex(parentFold, start); |
1083 |
int foldCount = parentFold.getFoldCount(); |
1084 |
int idxOfFirstFoldStartingInside = -1; |
1085 |
while (index < foldCount) { |
1086 |
Fold f = parentFold.getFold(index); |
1087 |
if (f.getStartOffset() <= end) { |
1088 |
ret.add(f); |
1089 |
} else { |
1090 |
break; // no more relevant folds |
1091 |
} |
1092 |
if (idxOfFirstFoldStartingInside == -1 && f.getStartOffset() >= start) { |
1093 |
idxOfFirstFoldStartingInside = ret.size() - 1; |
1094 |
} |
1095 |
index++; |
1096 |
} |
1097 |
|
1098 |
return new Object [] { ret, idxOfFirstFoldStartingInside != -1 ? idxOfFirstFoldStartingInside : ret.size() }; |
1099 |
} |
1100 |
|
1101 |
/** |
1102 |
* This class should be never used by other code; will be made private |
1103 |
*/ |
1104 |
public class PaintInfo { |
1105 |
|
1106 |
int paintOperation; |
1107 |
/** |
1108 |
* level of the 1st marker on the line |
1109 |
*/ |
1110 |
int innerLevel; |
1111 |
|
1112 |
/** |
1113 |
* Y-coordinate of the cell |
1114 |
*/ |
1115 |
int paintY; |
1116 |
|
1117 |
/** |
1118 |
* Height of the paint cell |
1119 |
*/ |
1120 |
int paintHeight; |
1121 |
|
1122 |
/** |
1123 |
* State of the marker (+/-) |
1124 |
*/ |
1125 |
boolean isCollapsed; |
1126 |
|
1127 |
/** |
1128 |
* all markers on the line are collapsed |
1129 |
*/ |
1130 |
boolean allCollapsed; |
1131 |
int startOffset; |
1132 |
int endOffset; |
1133 |
/** |
1134 |
* nesting level of the last marker on the line |
1135 |
*/ |
1136 |
int outgoingLevel; |
1137 |
|
1138 |
/** |
1139 |
* Force incoming line (from above) to be present |
1140 |
*/ |
1141 |
boolean lineIn; |
1142 |
|
1143 |
/** |
1144 |
* Force outgoing line (down from marker) to be present |
1145 |
*/ |
1146 |
boolean lineOut; |
1147 |
|
1148 |
/** |
1149 |
* The 'incoming' (upper) line should be painted as active |
1150 |
*/ |
1151 |
boolean lineInActive; |
1152 |
|
1153 |
/** |
1154 |
* The 'outgoing' (down) line should be painted as active |
1155 |
*/ |
1156 |
boolean lineOutActive; |
1157 |
|
1158 |
/** |
1159 |
* The sign/marker itself should be painted as active |
1160 |
*/ |
1161 |
boolean signActive; |
1162 |
|
1163 |
public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, boolean isCollapsed, int startOffset, int endOffset){ |
1164 |
this.paintOperation = paintOperation; |
1165 |
this.innerLevel = this.outgoingLevel = innerLevel; |
1166 |
this.paintY = paintY; |
1167 |
this.paintHeight = paintHeight; |
1168 |
this.isCollapsed = this.allCollapsed = isCollapsed; |
1169 |
this.startOffset = startOffset; |
1170 |
this.endOffset = endOffset; |
1171 |
|
1172 |
switch (paintOperation) { |
1173 |
case PAINT_MARK: |
1174 |
lineIn = false; |
1175 |
lineOut = true; |
1176 |
outgoingLevel++; |
1177 |
break; |
1178 |
case SINGLE_PAINT_MARK: |
1179 |
lineIn = false; |
1180 |
lineOut = false; |
1181 |
break; |
1182 |
case PAINT_END_MARK: |
1183 |
lineIn = true; |
1184 |
lineOut = false; |
1185 |
isCollapsed = true; |
1186 |
allCollapsed = true; |
1187 |
break; |
1188 |
case PAINT_LINE: |
1189 |
lineIn = lineOut = true; |
1190 |
break; |
1191 |
} |
1192 |
} |
1193 |
|
1194 |
/** |
1195 |
* Sets active flags on inidivual parts of the mark |
1196 |
* @param mark |
1197 |
* @param lineIn |
1198 |
* @param lineOut S |
1199 |
*/ |
1200 |
void markActive(boolean mark, boolean lineIn, boolean lineOut) { |
1201 |
this.signActive |= mark; |
1202 |
this.lineInActive |= lineIn; |
1203 |
this.lineOutActive |= lineOut; |
1204 |
} |
1205 |
|
1206 |
boolean hasLineIn() { |
1207 |
return lineIn || innerLevel > 0; |
1208 |
} |
1209 |
|
1210 |
boolean hasLineOut() { |
1211 |
return lineOut || outgoingLevel > 0 || (paintOperation != SINGLE_PAINT_MARK && !isAllCollapsed()); |
1212 |
} |
1213 |
|
1214 |
public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, int startOffset, int endOffset){ |
1215 |
this(paintOperation, innerLevel, paintY, paintHeight, false, startOffset, endOffset); |
1216 |
} |
1217 |
|
1218 |
public int getPaintOperation(){ |
1219 |
return paintOperation; |
1220 |
} |
1221 |
|
1222 |
public int getInnerLevel(){ |
1223 |
return innerLevel; |
1224 |
} |
1225 |
|
1226 |
public int getPaintY(){ |
1227 |
return paintY; |
1228 |
} |
1229 |
|
1230 |
public int getPaintHeight(){ |
1231 |
return paintHeight; |
1232 |
} |
1233 |
|
1234 |
public boolean isCollapsed(){ |
1235 |
return isCollapsed; |
1236 |
} |
1237 |
|
1238 |
boolean isAllCollapsed() { |
1239 |
return allCollapsed; |
1240 |
} |
1241 |
|
1242 |
public void setPaintOperation(int paintOperation){ |
1243 |
this.paintOperation = paintOperation; |
1244 |
} |
1245 |
|
1246 |
public void setInnerLevel(int innerLevel){ |
1247 |
this.innerLevel = innerLevel; |
1248 |
} |
1249 |
|
1250 |
public @Override String toString(){ |
1251 |
StringBuffer sb = new StringBuffer(""); |
1252 |
if (paintOperation == PAINT_MARK){ |
1253 |
sb.append("PAINT_MARK"); // NOI18N |
1254 |
}else if (paintOperation == PAINT_LINE){ |
1255 |
sb.append("PAINT_LINE"); // NOI18N |
1256 |
}else if (paintOperation == PAINT_END_MARK) { |
1257 |
sb.append("PAINT_END_MARK"); // NOI18N |
1258 |
}else if (paintOperation == SINGLE_PAINT_MARK) { |
1259 |
sb.append("SINGLE_PAINT_MARK"); |
1260 |
} |
1261 |
sb.append(",L:").append(innerLevel).append("/").append(outgoingLevel); // NOI18N |
1262 |
sb.append(',').append(isCollapsed ? "C" : "E"); // NOI18N |
1263 |
sb.append(", start=").append(startOffset).append(", end=").append(endOffset); |
1264 |
sb.append(", lineIn=").append(lineIn).append(", lineOut=").append(lineOut); |
1265 |
return sb.toString(); |
1266 |
} |
1267 |
|
1268 |
boolean hasSign() { |
1269 |
return paintOperation == PAINT_MARK || paintOperation == SINGLE_PAINT_MARK; |
1270 |
} |
1271 |
|
1272 |
|
1273 |
void mergeWith(PaintInfo prevInfo) { |
1274 |
if (prevInfo == null) { |
1275 |
return; |
1276 |
} |
1277 |
|
1278 |
int operation = this.paintOperation; |
1279 |
boolean lineIn = prevInfo.lineIn; |
1280 |
boolean lineOut = prevInfo.lineOut; |
1281 |
|
1282 |
LOG.log(Level.FINE, "Merging {0} with {1}: ", new Object[] { this, prevInfo }); |
1283 |
if (prevInfo.getPaintOperation() == PAINT_END_MARK) { |
1284 |
// merge with start|single -> start mark + line-in |
1285 |
lineIn = true; |
1286 |
} else { |
1287 |
operation = PAINT_MARK; |
1288 |
} |
1289 |
|
1290 |
int level1 = Math.min(prevInfo.innerLevel, innerLevel); |
1291 |
int level2 = prevInfo.outgoingLevel; |
1292 |
|
1293 |
if (getPaintOperation() == PAINT_END_MARK |
1294 |
&& innerLevel == prevInfo.outgoingLevel) { |
1295 |
// if merging end marker at the last level, update to the new outgoing level |
1296 |
level2 = outgoingLevel; |
1297 |
} else if (!isCollapsed) { |
1298 |
level2 = Math.max(prevInfo.outgoingLevel, outgoingLevel); |
1299 |
} |
1300 |
|
1301 |
if (prevInfo.getInnerLevel() < getInnerLevel()) { |
1302 |
int paintFrom = Math.min(prevInfo.paintY, paintY); |
1303 |
int paintTo = Math.max(prevInfo.paintY + prevInfo.paintHeight, paintY + paintHeight); |
1304 |
// at least one collapsed -> paint plus sign |
1305 |
boolean collapsed = prevInfo.isCollapsed() || isCollapsed(); |
1306 |
int offsetFrom = Math.min(prevInfo.startOffset, startOffset); |
1307 |
int offsetTo = Math.max(prevInfo.endOffset, endOffset); |
1308 |
|
1309 |
this.paintY = paintFrom; |
1310 |
this.paintHeight = paintTo - paintFrom; |
1311 |
this.isCollapsed = collapsed; |
1312 |
this.startOffset = offsetFrom; |
1313 |
this.endOffset = offsetTo; |
1314 |
} |
1315 |
this.paintOperation = operation; |
1316 |
this.allCollapsed = prevInfo.allCollapsed && allCollapsed; |
1317 |
this.innerLevel = level1; |
1318 |
this.outgoingLevel = level2; |
1319 |
this.lineIn |= lineIn; |
1320 |
this.lineOut |= lineOut; |
1321 |
|
1322 |
this.signActive |= prevInfo.signActive; |
1323 |
this.lineInActive |= prevInfo.lineInActive; |
1324 |
this.lineOutActive |= prevInfo.lineOutActive; |
1325 |
|
1326 |
LOG.log(Level.FINE, "Merged result: {0}", this); |
1327 |
} |
1328 |
} |
1329 |
|
1330 |
/** Keeps info of visible folding mark */ |
1331 |
public class Mark{ |
1332 |
public int x; |
1333 |
public int y; |
1334 |
public int size; |
1335 |
public boolean isFolded; |
1336 |
|
1337 |
public Mark(int x, int y, int size, boolean isFolded){ |
1338 |
this.x = x; |
1339 |
this.y = y; |
1340 |
this.size = size; |
1341 |
this.isFolded = isFolded; |
1342 |
} |
1343 |
} |
1344 |
|
1345 |
private final class Listener extends MouseAdapter implements FoldHierarchyListener, DocumentListener, Runnable { |
1346 |
|
1347 |
public Listener(){ |
1348 |
} |
1349 |
|
1350 |
// -------------------------------------------------------------------- |
1351 |
// FoldHierarchyListener implementation |
1352 |
// -------------------------------------------------------------------- |
1353 |
|
1354 |
public void foldHierarchyChanged(FoldHierarchyEvent evt) { |
1355 |
refresh(); |
1356 |
} |
1357 |
|
1358 |
// -------------------------------------------------------------------- |
1359 |
// DocumentListener implementation |
1360 |
// -------------------------------------------------------------------- |
1361 |
|
1362 |
public void insertUpdate(DocumentEvent evt) { |
1363 |
if (!(evt instanceof BaseDocumentEvent)) return; |
1364 |
|
1365 |
BaseDocumentEvent bevt = (BaseDocumentEvent)evt; |
1366 |
if (bevt.getLFCount() > 0) { // one or more lines inserted |
1367 |
refresh(); |
1368 |
} |
1369 |
} |
1370 |
|
1371 |
public void removeUpdate(DocumentEvent evt) { |
1372 |
if (!(evt instanceof BaseDocumentEvent)) return; |
1373 |
|
1374 |
BaseDocumentEvent bevt = (BaseDocumentEvent)evt; |
1375 |
if (bevt.getLFCount() > 0) { // one or more lines removed |
1376 |
refresh(); |
1377 |
} |
1378 |
} |
1379 |
|
1380 |
public void changedUpdate(DocumentEvent evt) { |
1381 |
} |
1382 |
|
1383 |
// -------------------------------------------------------------------- |
1384 |
// MouseListener implementation |
1385 |
// -------------------------------------------------------------------- |
1386 |
|
1387 |
@Override |
1388 |
public void mousePressed (MouseEvent e) { |
1389 |
Mark mark = getClickedMark(e); |
1390 |
if (mark!=null){ |
1391 |
e.consume(); |
1392 |
performAction(mark, (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0); |
1393 |
} |
1394 |
} |
1395 |
|
1396 |
@Override |
1397 |
public void mouseClicked(MouseEvent e) { |
1398 |
// #102288 - missing event consuming caused quick doubleclicks to break |
1399 |
// fold expanding/collapsing and move caret to the particular line |
1400 |
if (e.getClickCount() > 1) { |
1401 |
LOG.log(Level.FINEST, "Mouse {0}click at {1}", new Object[] { e.getClickCount(), e.getY()}); |
1402 |
Mark mark = getClickedMark(e); |
1403 |
try { |
1404 |
performActionAt(mark, e.getY()); |
1405 |
} catch (BadLocationException ex) { |
1406 |
LOG.log(Level.WARNING, "Error during fold expansion using sideline", ex); |
1407 |
} |
1408 |
} else { |
1409 |
e.consume(); |
1410 |
} |
1411 |
} |
1412 |
|
1413 |
private void refreshIfMouseOutside(Point pt) { |
1414 |
mousePoint = (int)pt.getY(); |
1415 |
if (LOG.isLoggable(Level.FINEST)) { |
1416 |
if (mouseBoundary == null) { |
1417 |
LOG.log(Level.FINEST, "Mouse boundary not set, refreshing: {0}", mousePoint); |
1418 |
} else { |
1419 |
LOG.log(Level.FINEST, "Mouse {0} inside known mouse boundary: {1}-{2}", |
1420 |
new Object[] { mousePoint, mouseBoundary.y, mouseBoundary.getMaxY() }); |
1421 |
} |
1422 |
} |
1423 |
if (mouseBoundary == null || mousePoint < mouseBoundary.y || mousePoint > mouseBoundary.getMaxY()) { |
1424 |
refresh(); |
1425 |
} |
1426 |
} |
1427 |
|
1428 |
@Override |
1429 |
public void mouseMoved(MouseEvent e) { |
1430 |
refreshIfMouseOutside(e.getPoint()); |
1431 |
} |
1432 |
|
1433 |
public void mouseEntered(MouseEvent e) { |
1434 |
refreshIfMouseOutside(e.getPoint()); |
1435 |
} |
1436 |
|
1437 |
public void mouseExited(MouseEvent e) { |
1438 |
mousePoint = NO_MOUSE_POINT; |
1439 |
refresh(); |
1440 |
} |
1441 |
|
1442 |
|
1443 |
// -------------------------------------------------------------------- |
1444 |
// private implementation |
1445 |
// -------------------------------------------------------------------- |
1446 |
|
1447 |
private Mark getClickedMark(MouseEvent e){ |
1448 |
if (e == null || !SwingUtilities.isLeftMouseButton(e)) { |
1449 |
return null; |
1450 |
} |
1451 |
|
1452 |
int x = e.getX(); |
1453 |
int y = e.getY(); |
1454 |
for (Mark mark : visibleMarks) { |
1455 |
if (x >= mark.x && x <= (mark.x + mark.size) && y >= mark.y && y <= (mark.y + mark.size)) { |
1456 |
return mark; |
1457 |
} |
1458 |
} |
1459 |
return null; |
1460 |
} |
1461 |
|
1462 |
private void refresh() { |
1463 |
SwingUtilities.invokeLater(this); |
1464 |
} |
1465 |
|
1466 |
@Override |
1467 |
public void run() { |
1468 |
if (getPreferredSize().width == 0 && enabled) { |
1469 |
updatePreferredSize(); |
1470 |
} |
1471 |
repaint(); |
1472 |
} |
1473 |
} // End of Listener class |
1474 |
|
1475 |
@Override |
1476 |
public AccessibleContext getAccessibleContext() { |
1477 |
if (accessibleContext == null) { |
1478 |
accessibleContext = new AccessibleJComponent() { |
1479 |
public @Override AccessibleRole getAccessibleRole() { |
1480 |
return AccessibleRole.PANEL; |
1481 |
} |
1482 |
}; |
1483 |
accessibleContext.setAccessibleName(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSN_CodeFoldingSideBar")); //NOI18N |
1484 |
accessibleContext.setAccessibleDescription(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSD_CodeFoldingSideBar")); //NOI18N |
1485 |
} |
1486 |
return accessibleContext; |
1487 |
} |
1488 |
|
1489 |
private Coloring getColoring() { |
1490 |
if (attribs == null) { |
1491 |
if (fcsLookupResult == null) { |
1492 |
fcsLookupResult = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)) |
1493 |
.lookupResult(FontColorSettings.class); |
1494 |
fcsLookupResult.addLookupListener(WeakListeners.create(LookupListener.class, fcsTracker, fcsLookupResult)); |
1495 |
} |
1496 |
|
1497 |
FontColorSettings fcs = fcsLookupResult.allInstances().iterator().next(); |
1498 |
AttributeSet attr = fcs.getFontColors(FontColorNames.CODE_FOLDING_BAR_COLORING); |
1499 |
if (attr == null) { |
1500 |
attr = fcs.getFontColors(FontColorNames.DEFAULT_COLORING); |
1501 |
} else { |
1502 |
attr = AttributesUtilities.createComposite(attr, fcs.getFontColors(FontColorNames.DEFAULT_COLORING)); |
1503 |
} |
1504 |
attribs = attr; |
1505 |
} |
1506 |
return Coloring.fromAttributeSet(attribs); |
1507 |
} |
1508 |
|
1509 |
/** |
1510 |
* Factory for the sidebars. Use {@link FoldUtilities#getFoldingSidebarFactory()} to |
1511 |
* obtain an instance. |
1512 |
*/ |
1513 |
public static class Factory implements SideBarFactory { |
1514 |
@Override |
1515 |
public JComponent createSideBar(JTextComponent target) { |
1516 |
return new CodeFoldingSideBar(target); |
1517 |
} |
1518 |
} |
1519 |
} |