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