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