//----------------------------------------------------------------------------
// 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;
}
}
}