//---------------------------------------------------------------------------- // COMPONENT NAME: LPEX Editor // // © Copyright IBM Corporation 2006, 2007 // All Rights Reserved. // // DESCRIPTION: // NestingCommand - sample user-defined command (nesting) //---------------------------------------------------------------------------- package com.ibm.lpex.samples; import java.util.ArrayList; import java.util.HashMap; import com.ibm.lpex.core.LpexCommand; import com.ibm.lpex.core.LpexDocumentLocation; import com.ibm.lpex.core.LpexMatch; import com.ibm.lpex.core.LpexMarkListener; import com.ibm.lpex.core.LpexView; import com.ibm.lpex.core.LpexViewAdapter; import com.ibm.lpex.core.LpexWindow; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; /** * Sample command <b>nesting</b> - display block limits in the source. * This is an example of custom drawing in the prefix area margin. Run this * command with the cursor positioned on a block-delimiter bracket (for example, * '{' or '}' in a C/C++ source document) to display the limits of the included * blocks (up to a given level) as defined by the selected bracket. * * <p>Here is the NestingCommand <a href="doc-files/NestingCommand.java.html">source * code</a>.</p> * * <p>To run this sample: * <ul> * <li>Define the command via an editor preference page, where available, or * from the editor command line: * <pre>set commandClass.nesting com.ibm.lpex.samples.NestingCommand</pre></li> * <li>Run it from the editor command line: * <pre>nesting [on | off]</pre></li> * </ul></p> * * @see com.ibm.lpex.samples All the samples */ public class NestingCommand implements LpexCommand { /** * Runs this command. * Establishes the bounds of the blocks to display, displays the nestings * in the prefix-area margin. * * @param lpexView the document view in which the command was issued * @param parameters optional parameter "on" / "off" / "?" */ public boolean doCommand(LpexView lpexView, String parameters) { if (lpexView != null) { parameters = parameters.trim(); if (parameters.length() != 0) { if ("off".equals(parameters)) { Nester.uninstall(lpexView); return true; } if ("?".equals(parameters)) // command help { lpexView.doCommand("set messageText Syntax: nesting [on | off]"); return true; } if (!"on".equals(parameters)) { lpexView.doCommand("set messageText " + parameters + " is not a valid parameter for the \"nesting\" command."); return false; } } // try to display the nestings Nester.install(lpexView); } return true; } } /*------------------------------*/ /* nesting-display management */ /*------------------------------*/ /** * This class manages the display of the requested nesting. */ class Nester extends LpexViewAdapter implements DisposeListener, LpexMarkListener, PaintListener { // maximum nesting and the margin area needed for it static final int MAX_LEVEL = 5; static final int INDENT = 3; static final int MARGIN = INDENT + MAX_LEVEL * 2 + 4; // global list of active nesters static HashMap<LpexView,Nester> _nesters = new HashMap<LpexView,Nester>(); // info for one nester instance LpexView _lpexView; String _initPrefixArea, _initPrefixAreaText, _initPrefixAreaMargin; LpexCommand _initParseCommand; int _markId, _markId1; char _bracket, _bracketStyle; Color _extentColor, _badExtentColor, _backgroundColor; Image _image; ArrayList<Extent> _extents = new ArrayList<Extent>(); boolean _validExtents; /** * Constructs a nester for the specified view. */ private Nester(LpexView lpexView) { _lpexView = lpexView; _nesters.put(_lpexView, this); _lpexView.addLpexViewListener(this); _lpexView.window().textWindow().addDisposeListener(this); _lpexView.window().textWindow().addPaintListener(this); _lpexView.setOwnerDrawMargin(true); _extentColor = new Color(_lpexView.window().getDisplay(), 0, 0, 128); // RGB dark blue _badExtentColor = new Color(_lpexView.window().getDisplay(), 255, 0, 0); // RGB red // set up the view if ("off".equals(_lpexView.query("current.prefixArea"))) { _initPrefixArea = _lpexView.query("prefixArea"); _initPrefixAreaText = _lpexView.query("prefixAreaText"); _lpexView.doCommand("set prefixAreaText none"); } if (_lpexView.queryInt("current.prefixAreaMargin") < MARGIN) { _initPrefixAreaMargin = _lpexView.query("prefixAreaMargin"); _lpexView.doCommand("set prefixAreaMargin " + MARGIN); } initTransients(); _markId = _lpexView.queryInt("markId.@NestingCommand"); _markId1 = _lpexView.queryInt("markId.@NestingCommand1"); _lpexView.addLpexMarkListener(_markId, this); _lpexView.addLpexMarkListener(_markId1, this); } /** * Nester initializations that must be redone after an "updateProfile". */ final void initTransients() { if (_backgroundColor != null) { _backgroundColor.dispose(); } String[] ma = _lpexView.query("styleAttributes.prefixArea").split(" "); _backgroundColor = new Color(_lpexView.window().getDisplay(), // prefix area margin's Integer.parseInt(ma[3]), Integer.parseInt(ma[4]), Integer.parseInt(ma[5])); _lpexView.doCommand("set prefixArea on"); _initParseCommand = _lpexView.defineCommand("parse", new LpexCommand() { public boolean doCommand(LpexView lpexView, String parameters) { return doParseCommand(lpexView, parameters); } }); } /** * Request to start displaying the nesting in a block in the given document view. * Assumes that the specified view will only ever be shown in its current window. */ static void install(LpexView lpexView) { if (lpexView == null || lpexView.window() == null) { return; } LpexDocumentLocation loc = lpexView.documentLocation(); if (loc.element == 0) { lpexView.doCommand("set messageText No visible elements."); return; } if (lpexView.show(loc.element)) { lpexView.doCommand("set messageText Current position on a show line."); return; } lpexView.doCommand("parse"); // parse any pending changes LpexDocumentLocation match = LpexMatch.match(lpexView, loc); if (match == null) { lpexView.doCommand("set messageText No match for the current position."); return; } // allow the display of only one nesting per view if (_nesters.get(lpexView) != null) { uninstall(lpexView); } // mark main block (one Nester per view, one mark pair is sufficient) int markElement, markPosition, markElement1, markPosition1; if (match.element >= loc.element) { markElement = loc.element; markPosition = (match.position > loc.position)? loc.position : match.position; markElement1 = match.element; markPosition1 = (match.position > loc.position)? match.position : loc.position; } else { markElement = match.element; markPosition = match.position; markElement1 = loc.element; markPosition1 = loc.position; } lpexView.doCommand("set mark.@NestingCommand " + markElement + " " + markPosition); lpexView.doCommand("set mark.@NestingCommand1 " + markElement1 + " " + markPosition1); Nester n = new Nester(lpexView); n._bracket = lpexView.elementText(markElement).charAt(markPosition - 1); String style = lpexView.elementStyle(markElement); n._bracketStyle = (style.length() >= markPosition)? style.charAt(markPosition - 1) : '!'; } /** * Request to stop the nesting display in the given view. */ static void uninstall(LpexView lpexView) { Nester nester = _nesters.get(lpexView); if (nester != null) { nester.uninstall(); } } /** * Removes this nester. Removes its mark and listeners, * restores the original view settings, cleans up. */ private void uninstall() { if (_lpexView != null) { _lpexView.setOwnerDrawMargin(false); _lpexView.defineCommand("parse", _initParseCommand); _lpexView.removeLpexViewListener(this); _lpexView.removeLpexMarkListener(_markId, this); _lpexView.doCommand("set mark.@NestingCommand clear"); _lpexView.doCommand("set mark.@NestingCommand1 clear"); LpexWindow lpexWindow = _lpexView.window(); if (lpexWindow != null) { Composite textWindow = lpexWindow.textWindow(); if (!textWindow.isDisposed()) { textWindow.removePaintListener(this); textWindow.removeDisposeListener(this); textWindow.redraw(); } } if (_initPrefixAreaMargin != null) _lpexView.doCommand("set prefixAreaMargin " + _initPrefixAreaMargin); if (_initPrefixAreaText != null) _lpexView.doCommand("set prefixAreaText " + _initPrefixAreaText); if (_initPrefixArea != null) _lpexView.doCommand("set prefixArea " + _initPrefixArea); _extentColor.dispose(); _badExtentColor.dispose(); _backgroundColor.dispose(); if (_image != null) { _image.dispose(); } _nesters.remove(_lpexView); _lpexView = null; } } // Text window dispose listener - view's window is being disposed, uninstall. public void widgetDisposed(DisposeEvent e) { uninstall(); } // Mark listener - main block's start/end bracket has been modified, uninstall. public void markChanged(LpexView lpexView, int markId) { uninstall(); } // Mark listener - main block's start/end bracket has been deleted, uninstall. public void markDeleted(LpexView lpexView, int markId) { uninstall(); } // View listener - view is being disposed, uninstall. public void disposed(LpexView lpexView) { uninstall(); } // View listener - the updateProfile command has completed. public void updateProfile(LpexView lpexView) { initTransients(); // ensure our transient settings stick _validExtents = false; // must now update extents (doc may have changed) } // View listener - view's screen has been refreshed, update our display. public void shown(LpexView lpexView) { // ensure the extents are up-to-date getExtents(); // display them - should optimize: only call it from here if extents have been // updated or the viewport / font (row height) / view filtering has changed doubleBufferPaint(null); } // Text window paint listener - paint event notification. public void paintControl(PaintEvent e) { if (e.x > _lpexView.queryInt("prefixAreaWidth")) { return; // invalidated area is beyond the prefix-area margin. } doubleBufferPaint(e); } // Runs our extended "parse" command - do original command, invalidate _extents. boolean doParseCommand(LpexView lpexView, String parameters) { if (_initParseCommand != null) { _initParseCommand.doCommand(lpexView, parameters); } else { _lpexView.doDefaultCommand("parse " + parameters); } _validExtents = false; return true; } // Draws the nestings. Uses an Image that covers the prefix-area margin. void doubleBufferPaint(PaintEvent e) { LpexWindow lpexWindow = _lpexView.window(); if (lpexWindow == null) { return; } int prefixAreaWidth = _lpexView.queryInt("prefixAreaWidth"); if (prefixAreaWidth == 0) { return; } // get height, adjust for non-element rows below text area & top expand header int rowHeight = _lpexView.queryInt("rowHeight"); int rows = _lpexView.queryInt("rows"); int y = 0; for (int i = rows; i > 0 && _lpexView.elementOfRow(i) == 0; i--) { rows--; } if (rows > 0 && _lpexView.elementOfRow(1) == 0) { y += rowHeight; rows--; } if (rows == 0) { return; // no text elements visible at all. } int height = rows * rowHeight; int prefixAreaMargin = _lpexView.queryInt("current.prefixAreaMargin"); if (_image != null) { Rectangle r = _image.getBounds(); if (r.width != prefixAreaMargin || r.height != height) { _image.dispose(); _image = null; } } if (_image == null) { _image = new Image(lpexWindow.getDisplay(), prefixAreaMargin, height); } /**/ GC imageGC = new GC(_image); imageGC.setBackground(_backgroundColor); imageGC.fillRectangle(0, 0, prefixAreaMargin, height); imageGC.setLineStyle(SWT.LINE_SOLID); imageGC.setLineWidth(1); doPaint(imageGC, y); GC g = (e != null)? e.gc : new GC(lpexWindow.textWindow()); g.drawImage(_image, _lpexView.queryInt("expandHideAreaWidth") + prefixAreaWidth - prefixAreaMargin /*x*/, y); /**/ imageGC.dispose(); if (e == null) { g.dispose(); } } // Determines all the extents for the initially selected block. void getExtents() { // only done 1.- very first time around, 2.- after an "updateProfile" (the document // may have been reloaded), 3.- after a text change was committed (we extend the // "parse" command to listen: this also ensures text is parsed, as we need // up-to-date styles) or on every shown() notification if there is no parser if (_validExtents && _lpexView.parser() != null) { return; } _extents.clear(); String start = _lpexView.query("mark.@NestingCommand"); String end = _lpexView.query("mark.@NestingCommand1"); if (start != null && end != null) { String[] loc = start.split(" "); LpexDocumentLocation top = new LpexDocumentLocation(Integer.parseInt(loc[0]), Integer.parseInt(loc[1])); loc = end.split(" "); LpexDocumentLocation match = new LpexDocumentLocation(Integer.parseInt(loc[0]), Integer.parseInt(loc[1])); if (top.element != match.element) { _extents.add(new Extent(top.element, match.element, 0)); // main block scope getExtents(top, match, 1); // add any inner blocks } } _validExtents = true; } // Adds the extents found inside the given non-inclusive start & end locations. void getExtents(LpexDocumentLocation start, LpexDocumentLocation end, int level) { if (level >= MAX_LEVEL) { return; } boolean skippingFoundBlock = false; int startOffset = start.position; for (int e = start.element; e <= end.element;) { if (!_lpexView.show(e)) { String text = _lpexView.elementText(e); int len = (e != end.element)? text.length() : end.position; for (int i = startOffset; i < len; i++) { if (text.charAt(i) == _bracket) { String style = _lpexView.elementStyle(e); char s = (i < style.length())? style.charAt(i) : '!'; if (s == _bracketStyle) { LpexDocumentLocation top = new LpexDocumentLocation(e, i+1); LpexDocumentLocation match = LpexMatch.match(_lpexView, top); // check for one level of unmatched start bracket boolean mismatch = match == null || match.element > end.element || match.element == end.element && match.position == end.position; int endElement = mismatch? _lpexView.elements() + 1 : match.element; // new extent found if (top.element != endElement) { _extents.add(new Extent(top.element, endElement, level)); // check (recursively) for its inner blocks getExtents(top, (match != null)? match : end, level + 1); } // continue at this level after the found block if (match == null) { return; } e = endElement; startOffset = match.position; skippingFoundBlock = true; break; // out of inner "for" } } }//end "for" positions inside an element } if (!skippingFoundBlock) { e++; startOffset = 0; // move to a new element } skippingFoundBlock = false; }//end "for" elements } /** * Paints the block extents to the given Image graphic context. * @param imageShift Image may be shifted down one row for top expand header */ void doPaint(GC gc, int imageShift) { if (_extents.size() == 0) { return; } /*==========================================================================*/ /* establish lowest/highest lines inside the currently-loaded doc section */ /*==========================================================================*/ int rows = _lpexView.queryInt("rows"); // 1..rows visible in the edit area int lowestShowing = _lpexView.queryInt("lines") + 1 - (_lpexView.queryInt("lines.beforeStart") + _lpexView.queryInt("lines.afterEnd")); int highestShowing = 0; for (int i = 1; i <= rows; i++) { int element = _lpexView.elementOfRow(i); if (element != 0 && !_lpexView.show(element)) { int line = _lpexView.lineOfElement(element); if (line < lowestShowing) { lowestShowing = line; } if (line > highestShowing) { highestShowing = line; } } } if (highestShowing == 0) { return; } /*=================*/ /* draw nestings */ /*=================*/ int rowHeight = _lpexView.queryInt("rowHeight"); int currentMargin = _lpexView.queryInt("current.prefixAreaMargin"); int windowHeight = _lpexView.window().textWindow().getSize().y; // iterate through the extents... for (int e = 0; e < _extents.size(); e++) { Extent extent = _extents.get(e); int startLine = extent.startLine; int endLine = extent.endLine; if (startLine > highestShowing || endLine < lowestShowing) { continue; } /*------------------------------------*/ /* determine its rows on the screen */ /*------------------------------------*/ int startRow = -1, endRow = -1; // ZERO-based rows for paint calculations boolean realStart = false, realEnd = false; for (int i = 1; i <= rows; i++) { int element = _lpexView.elementOfRow(i); if (element != 0 && !_lpexView.show(element)) { int line = _lpexView.lineOfElement(element); if (line == startLine) { startRow = i - 1; realStart = true; } else if (line > startLine && startRow == -1 && // start has not yet been set line <= endLine) { startRow = i - 1; endRow = startRow; realEnd = line == endLine; } if (line > endLine) { break; } else if (line == endLine) { endRow = i - 1; realEnd = true; break; } else if (line < endLine && startRow != -1) { endRow = i - 1; } } }//end "for rows" /*------------*/ /* paint it */ /*------------*/ if (startRow >= 0) // if visible... { int y = (startRow * rowHeight) - imageShift; if (y < windowHeight) // ...and within the text window height { int height = (endRow - startRow + 1) * rowHeight; if (realStart) { y += rowHeight / 2; height -= rowHeight / 2; } if (realEnd) { height -= rowHeight / 2; } // paint this extent gc.setForeground(extent.bad? _badExtentColor : _extentColor); int x = (currentMargin - MARGIN) + INDENT + extent.nestingLevel * 2; gc.drawLine(x, y, x, y + height); int arrowWidth = currentMargin - x - 3; if (realStart) { gc.drawLine(x, y, x+arrowWidth, y); gc.drawLine(x+arrowWidth-2, y-2, x+arrowWidth+1, y); gc.drawLine(x+arrowWidth-2, y+2, x+arrowWidth+1, y); } if (realEnd) { gc.drawLine(x, y+height, x+arrowWidth, y+height); gc.drawLine(x+arrowWidth-2, y+height-2, x+arrowWidth+1, y+height); gc.drawLine(x+arrowWidth-2, y+height+2, x+arrowWidth+1, y+height); } } } } } // An extent. Kept in terms of text lines, rather than elements, so it's // not affected by added/removed SHOW lines (e.g., parser error messages). class Extent { int startLine, endLine, nestingLevel; boolean bad; // no matching end Extent(int startElement, int endElement, int nestingLevel) { startLine = _lpexView.lineOfElement(startElement); endLine = _lpexView.lineOfElement(endElement); if (endLine == 0) { endLine = _lpexView.queryInt("lines") + 1; bad = true; } this.nestingLevel = nestingLevel; } } }