//----------------------------------------------------------------------------
// COMPONENT NAME: LPEX Editor
//
// © Copyright IBM Corporation 2006, 2007
// All Rights Reserved.
//
// DESCRIPTION:
// SyncCommand - sample user-defined command (sync)
//----------------------------------------------------------------------------
package com.ibm.lpex.samples;
import java.util.ArrayList;
import java.util.HashMap;
import com.ibm.lpex.core.LpexCommand;
import com.ibm.lpex.core.LpexMessageConstants;
import com.ibm.lpex.core.LpexResources;
import com.ibm.lpex.core.LpexStringTokenizer;
import com.ibm.lpex.core.LpexView;
import com.ibm.lpex.core.LpexViewAdapter;
import com.ibm.lpex.core.LpexWindow;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;
/**
* Sample command <b>sync</b> - synchronize the vertical scrolling of two views.
* Vertical scrolling in the current document view will trigger a similar
* scrolling of the selected view. Scrolling in the other view does not reciprocate,
* allowing you to adjust the synchronization when there is a difference in the number
* of lines between the two views.
*
* <p>For best results ensure the views have the same size, and are either not
* filtered or filtered on the same terms.</p>
*
* <p>Here is the SyncCommand
* <a href="doc-files/SyncCommand.java.html">source code</a>.</p>
*
* <p>To run this sample:
* <ul>
* <li>Define this user command via an editor preference page, where available,
* or from the editor command line:
* <pre>set commandClass.sync com.ibm.lpex.samples.SyncCommand</pre></li>
* <li>Run it from the editor command line:
* <pre>sync [on | off]</pre></li>
* </ul></p>
*
* @see com.ibm.lpex.samples All the samples
*/
public class SyncCommand implements LpexCommand
{
static LpexView[] _lpexViews;
static String[] _lpexViewNames;
/**
* Runs this command.
* Prompts the user for the view to synchronize to current view's vertical
* scrolling, and starts the synchronized scrolling.
*
* @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))
{
Syncher.uninstall(lpexView);
return true;
}
if ("?".equals(parameters)) // command help
{
lpexView.doCommand("set messageText Syntax: sync [on | off]");
return true;
}
if (!"on".equals(parameters))
{
lpexView.doCommand("set messageText " + parameters +
" is not a valid parameter for the \"sync\" command.");
return false;
}
}
// set up the arrays of active windowed document views & their names
if (!findLpexViews())
{
lpexView.doCommand("set messageText There are no views to sync.");
return true;
}
// ensure our sync command is defined in the view in which this action runs
if (lpexView.command("doSync") != doSyncCommand)
{
lpexView.defineCommand("doSync", doSyncCommand);
}
// prompt the user to select the view to sync, process the selection
prompt(lpexView);
}
return true;
}
/**
* Prompts the user for the document view to sync to the current view.
* When user selects a view and presses Enter, the "doSync" command is run.
*/
static void prompt(LpexView currentLpexView)
{
// create a string for the "input" command prompt
StringBuilder selections = new StringBuilder(512);
for (int i = 0; i < _lpexViews.length; i++)
{
if (_lpexViews[i] != currentLpexView)
{
if (selections.length() != 0)
{
selections.append('\0'); // separate items in "input" command's prompt list
}
selections.append(_lpexViewNames[i]);
}
}
// issue the command (it returns immediately, before user presses Enter / Esc to cancel)
StringBuilder cmd = new StringBuilder(512);
cmd.append("input ")
.append(LpexStringTokenizer.addQuotes("Select (up/down) a view to sync:")).append(' ')
.append(LpexStringTokenizer.addQuotes(selections.toString())).append(' ')
.append(LpexStringTokenizer.addQuotes("doSync "));
LpexView.doGlobalCommand("set status");
currentLpexView.doCommand(cmd.toString());
}
/**
* Sets up the arrays of all windowed document views currently active and their
* names. Returns false if there are no views to sync.
*/
static boolean findLpexViews()
{
Display display = Display.getCurrent();
if (display == null)
{
return false; // called from a non-UI thread.
}
ArrayList<LpexView> views = new ArrayList<LpexView>();
addLpexViews(display.getShells(), views);
int viewsCount = views.size();
if (viewsCount <= 1)
{
return false; // nothing beyond the current view.
}
_lpexViews = views.toArray(new LpexView[viewsCount]);
_lpexViewNames = new String[viewsCount];
for (int i = 0; i < viewsCount; i++)
{
_lpexViewNames[i] = getViewName(_lpexViews[i]);
}
return true;
}
/**
* Adds all windowed document views currently active in the given hierarchy
* of controls to the given ArrayList. Calls itself recursively in order to
* traverse all the Composites.
*/
static void addLpexViews(Control[] controls, ArrayList<LpexView> views)
{
for (int i = 0; i < controls.length; i++)
{
if (controls[i] instanceof LpexWindow)
{
LpexView lpexView = ((LpexWindow)controls[i]).getLpexView();
if (lpexView != null)
{
views.add(lpexView);
}
}
else if (controls[i] instanceof Composite)
{
addLpexViews(((Composite)controls[i]).getChildren(), views);
}
}
}
/**
* Returns the name of the given view.
*/
static String getViewName(LpexView lpexView)
{
String name = lpexView.query("sourceName"); // source name
if (name == null)
{
name = lpexView.query("name"); // document name
if (name == null)
{ // untitled
name = LpexResources.message(LpexMessageConstants.MSG_UNTITLED_DOCUMENT,
lpexView.query("documentId"));
}
}
if (lpexView.queryInt("documentViews") > 1) // : <view id>
{
name += " : " + lpexView.query("viewId");
}
return name;
}
/*----------------------------------------------------------*/
/* doSync helper command for the prompt ("input" command) */
/*----------------------------------------------------------*/
/**
* Helper command, issued by our prompt's <b>input</b> command, to start synchronized
* scrolling in the selected document view (specified in <code>parameters</code>).
* This command must be registered in a view under the name <b>doSync</b>.
*/
static LpexCommand doSyncCommand = new LpexCommand() {
public boolean doCommand(LpexView lpexView, String parameters) {
LpexView selectedView = getSelectedView(parameters);
if (selectedView == null)
{
lpexView.doCommand("set messageText Selected view not found.");
}
else if (selectedView == lpexView)
{
lpexView.doCommand("set messageText Current view is already sync-ed to itself.");
}
else if (selectedView.isDisposed())
{
lpexView.doCommand("set messageText Selected view has been meanwhile disposed.");
// retry prompting user (if still enough views, else just give up)
if (findLpexViews())
{
prompt(lpexView); // prompt user - "Enter" will call us again, with the new selection
return false; // return false here, in order to keep the (nested) "input" mode on
}
}
else
{
Syncher.install(lpexView, selectedView);
}
// clear the arrays (to release memory, reference to the LpexViews)
_lpexViews = null;
_lpexViewNames = null;
return true;
}
};
/**
* Matches the user selection (a view name) to its LpexView.
* @param selection the user prompt selection
* @return selected LpexView, or null if not found
*/
static LpexView getSelectedView(String selection)
{
// if we've cleared the arrays (e.g., when several sync commands are
// issued from different views simultaneously), set them up again
if (_lpexViews == null)
{
findLpexViews();
}
for (int i = 0; i < _lpexViewNames.length; i++)
{
if (selection.equals(_lpexViewNames[i]))
{
return _lpexViews[i];
}
}
return null;
}
}
/*-------------------------------------*/
/* synchronized scrolling management */
/*-------------------------------------*/
/**
* This class manages the synchronized scrolling or two document views.
*/
class Syncher extends LpexViewAdapter
implements DisposeListener
{
private static HashMap<LpexView,Syncher> _synchers = new HashMap<LpexView,Syncher>();
private LpexView _mainView;
private LpexView _syncView;
private int _scrollBarValue;
/**
* Constructs a scroll syncher for the specified views.
*/
private Syncher(LpexView mainView, LpexView syncView)
{
_mainView = mainView;
_syncView = syncView;
_synchers.put(_mainView, this);
_mainView.addLpexViewListener(this);
_syncView.addLpexViewListener(this);
_mainView.window().textWindow().addDisposeListener(this);
// cannot use scrollbar's selection listener, as it only generates events when activated
// directly by the user and not, e.g., when the editor modifies it on a pageDown, etc.
// _mainView.window().textWindow().getVerticalBar().addSelectionListener(this);
_scrollBarValue = scrollBarValue();
}
/**
* Request to start echoing the vertical scrolling of mainView to syncView.
* Assumes that the specified main view will only ever be shown in its
* current window.
*/
static void install(LpexView mainView, LpexView syncView)
{
// check for valid parameters
if (mainView == null || syncView == null ||
mainView.window() == null)
{
return;
}
// currently allow any view to only participate in one sync pair
// (and also avoid dangerous case of a -> b plus b -> a)
if (_synchers.get(mainView) != null)
{
uninstall(mainView);
}
if (_synchers.get(syncView) != null)
{
uninstall(syncView);
}
new Syncher(mainView, syncView);
}
/**
* Request to stop echoing the vertical scrolling of the current view to another.
*/
static void uninstall(LpexView mainView)
{
Syncher syncher = _synchers.get(mainView);
if (syncher != null)
{
syncher.uninstall();
}
}
/**
* Removes this syncher.
*/
private void uninstall()
{
if (_mainView != null)
{
_mainView.removeLpexViewListener(this);
LpexWindow mainWindow = _mainView.window();
if (mainWindow != null)
{
mainWindow.textWindow().removeDisposeListener(this);
}
if (_syncView != null)
{
_syncView.removeLpexViewListener(this);
_syncView = null;
}
_synchers.remove(_mainView);
_mainView = null;
}
}
/**
* View listener - the main or sync view is being disposed.
* Uninstalls this syncher.
*/
public void disposed(LpexView lpexView)
{ uninstall(); }
/**
* Text window dispose listener - the main window is being disposed.
* Uninstalls the syncher from this view.
*/
public void widgetDisposed(DisposeEvent e)
{ uninstall(); }
/**
* View listener - the view screen has been refreshed.
* Checks the scrollbar value in the main view, and echoes any scroll actions
* in the sync view.
*/
public void shown(LpexView lpexView)
{
if (lpexView == _mainView)
{
int scrollBarValue = scrollBarValue();
if (scrollBarValue != _scrollBarValue)
{
int actionId = _syncView.actionId((scrollBarValue > _scrollBarValue)?
"scrollDown" : "scrollUp");
for (int i = Math.abs(scrollBarValue - _scrollBarValue); i > 0; i--)
{
// TODO a "scroll" editor action that uses the ± "actionRepeat"
// parameter?! (may speed up filtered view(s) scrolling)
_syncView.doAction(actionId);
}
_syncView.doCommand("screenShow view");
// typematic keys, for example, may tie up the event queue and cause sluggish
// repaints in one or both views: Control#update() forces all the outstanding
// paint requests for the widget to be processed before the method returns
_mainView.window().textWindow().update();
LpexWindow syncWindow = _syncView.window();
if (syncWindow != null)
{
syncWindow.textWindow().update();
}
_scrollBarValue = scrollBarValue;
}
}
}
/**
* Returns the current value of the vertical scrollbar.
*/
private int scrollBarValue()
{
ScrollBar scrollBar = _mainView.window().textWindow().getVerticalBar();
return scrollBar.isVisible()? scrollBar.getSelection() : 1;
}
}