/**
 ***************************************************************************
 *                 RegionList.js for Sony Vegas 5                          *
 *         A script by Robert Nolty, www.otpproductions.com                *
 *                                                                         *
 * You may use, modify or distribute this script freely.                   *
 * If you would like to hire me to adapt it or do other Vegas scripting,   *
 * contact me through my website.                                          *
 * 16 Nov 04                                                               *
 ***************************************************************************
 *
 * Functionality:
 *    View a list of all regions
 *    Click column headings to sort on that column
 *    Double click a region to select it in the timeline
 *    Click within a selected name to change the name of the region
 *    Right-click a region and select "Delete" from pop-up menu to delete a region
 *    Select a region and hit the "Del" key to delete a region
 *
 * You may move back and forth between the Region List window and Vegas without closing the Region List.
 * If you make changes to the regions in Vegas, hit the Refresh button in the Region List window.
 * The Region List window will remain visible on top of the Vegas window as long as the On Top box is checked.
 *
 * Bugs/issues:
 *    Requires Vegas 5 (could probably be adapted to Vegas 4 trivially -- search for "Vegas 5" below).
 *    Scrolling Vegas display to selected region will break when Vegas 6 is released.
 *    Delete item appears on context menu even if no item is selected for deletion.
 *    RETURN and ESCAPE keys do not work for dismissing the form.
 *    Multiple instances of the Region List may be opened, from within one instance of Vegas, or from
 *       multiple instances of Vegas.  After selecting a region in a Region List, the wrong Region List
 *       may receive focus.
 *    If multiple instances of Vegas are open to the same project, after selecting a region the wrong
 *       instance of Vegas may be scrolled to the selected region.
 *    Cannot undo changes made by Region List; and Vegas does not realize project is modified.  (NOTE: if
 *       you need undo functionality, change theForm.Show to theForm.ShowDialog.  Then you will not be able
 *       to work in the Vegas window until the Region List is closed, but you will get undo and Vegas will
 *       mark the project as modified if you modify a region name or delete a region in the dialog.)
 **/

import System.Text;
import System.Collections;
import System.Windows.Forms;
import System.Globalization;
import Sony.Vegas;

import Microsoft.Win32; 	// for WSH scripting

try {
    var theForm = new RegionListForm(); // RegionListForm defined below
    theForm.Show();  // Make it non-modal, so user can select a region and keep RegionList open
} catch (e) {
    if (!e.skipMessageBox)
        MessageBox.Show(e);
}

class RegionListView extends ListView {
    // weird -- when form is non-modal (i.e. when I open it with theForm.Show, rather than theForm.ShowDialog)
    //    then Vegas.Project exists during initialization of the form; but after that if the user interacts in
    //    a way that calls an event handler, referring to Vegas.Project generates a runtime error, "Object reference
    //    not set to an instance of an object".  When form is modal I don't have the problem.  Since I like the
    //    non-modal functionality, I work around the problem by storing the Vegas.Project in a variable during
    //    initialization; then I can use the stored variable in my event handlers.
    var m_Project : Sony.Vegas.Project;

    var m_mnuDelete : MenuItem;
    var m_mnuContext : System.Windows.Forms.ContextMenu;

    function RegionListView() {	// constructor
	this.Size = new System.Drawing.Size(400, 400);
	// by anchoring to all 4 sides, ListView resizes when its parent form resizes
	this.Anchor = AnchorStyles.Left + AnchorStyles.Right + AnchorStyles.Top + AnchorStyles.Bottom;
	this.Columns.Add("Region Name",200,HorizontalAlignment.Left);
	this.Columns.Add("Start Time",-2,HorizontalAlignment.Right);
	this.Columns.Add("End Time",-2,HorizontalAlignment.Right);
	this.FullRowSelect = true; // clicking anywhere in row selects row
	this.AllowColumnReorder = true;
	this.LabelEdit = true;	// users can rename regions in the RegionList control (requires event handler)
	this.Sorting = SortOrder.None; // will be changed if user clicks a column heading
	this.View = System.Windows.Forms.View.Details;
	this.m_Project = Vegas.Project;

	// register the event handlers
	this.add_DoubleClick(RegionListDoubleClickHandler);
	this.add_AfterLabelEdit(RegionListAfterLabelEditHandler);
	this.add_ColumnClick(RegionListColumnClickHandler);
	this.add_KeyUp(RegionListKeyUpHandler)

	// make the context menu
	m_mnuDelete = new MenuItem();
	m_mnuDelete.Text = "&Delete";
	m_mnuDelete.add_Click(RegionListCMenuDeleteHandler);
	m_mnuContext = new System.Windows.Forms.ContextMenu();
	m_mnuContext.MenuItems.Add(m_mnuDelete);
	this.ContextMenu = m_mnuContext;

	this.Populate();	// gets list of regions from Vegas and puts in ListView as Items
    } // end of constructor

    public function Populate() {
	// charge through the Vegas project regions, adding them to ListView as Items
	for (var region : Sony.Vegas.Region in this.m_Project.Regions) {
	    var newItem : ListViewItem = new ListViewItem(region.Label);
	    newItem.SubItems.Add(TimeToString(region.Position));
	    newItem.SubItems.Add(TimeToString(region.End));
	    // setting the Tag attribute attaches this region variable to this row in the ListView
	    newItem.Tag = region;
	    this.Items.Add(newItem);
	}
    }

    protected function deleteSelectedRegion() {
	// called by 2 handlers -- the contextMenu delete item, and the Delete key KeyPress
	if (this.SelectedItems.Count == 1) { // if nothing selected, do nothing
	    // remove the corresponding region from Vegas
	    var region : Sony.Vegas.Region = this.SelectedItems[0].Tag;
	    m_Project.Regions.Remove(region);
	    // remove the item from the ListView
	    this.Items.Remove(this.SelectedItems[0]);
	}
    }

    protected function RegionListCMenuDeleteHandler(o : Object, e : System.EventArgs) {
	// called when Delete selected from Context Menu
	this.deleteSelectedRegion();
    }

    protected function RegionListKeyUpHandler(o : Object, e : KeyEventArgs) {
	// I wanted to use the KeyPress event, but the Del key doesn't appear to raise that event
	// respond only to Delete key
	if (e.KeyCode == Keys.Delete) {
	    this.deleteSelectedRegion();
	}
    }

    protected function RegionListDoubleClickHandler(o : Object, e : System.EventArgs) {
	// Select the region in timeline
	var region : Sony.Vegas.Region = this.SelectedItems[0].Tag;
	Vegas.Cursor = region.Position;
	Vegas.SelectionStart = region.Position;
	Vegas.SelectionLength = region.Length;

	// Vegas interface does not allow to scroll the timeline display.  Activate the Vegas
	//    app and send the Up-arrow and Down-arrow keystrokes to ensure cursor is visible.
	// Figure out the appName -- in my version of Vegas, it is "project.veg - Sony Vegas 5.0" or
	//    "project.veg * - Sony Vegas 5.0" (if project has been modified)
	var projectName : String = this.m_Project.FilePath;
	projectName = projectName.substring(projectName.lastIndexOf("\\") + 1);
	var appName : String = projectName + " - Sony Vegas 5.0";

	var wshShell = new ActiveXObject("WScript.Shell");
	var activateWasSuccessful : Boolean = wshShell.AppActivate(appName);
	if (! activateWasSuccessful) {
	    appName = projectName + " * - Sony Vegas 5.0";
	    wshShell.AppActivate(appName);
	}
	System.Threading.Thread.Sleep(100); // puts current thread to sleep for 100 milliseconds
	SendKeys.SendWait("{Up}");
	SendKeys.SendWait("{Down}");
	wshShell.AppActivate(this.FindForm().Text);
    }

    protected function RegionListAfterLabelEditHandler(o:Object,e:LabelEditEventArgs) {
	// Take the text just entered by user and make it the name of the selected region
	var region : Sony.Vegas.Region = this.SelectedItems[0].Tag;
	region.Label = e.Label;
    }

    protected function RegionListColumnClickHandler(o:Object,e:ColumnClickEventArgs) {
	// sort along clicked column
	// just register a new item comparer, then sort happens automatically
	// first, check if this was already the column of sort; in that case reverse the sort
	if (this.ListViewItemSorter instanceof RegionListItemComparer &&
	    (RegionListItemComparer)(this.ListViewItemSorter).m_sortColumn == e.Column) {
	    if (this.Sorting == SortOrder.Ascending) {this.Sorting = SortOrder.Descending;}
	    else {this.Sorting = SortOrder.Ascending;}
	}
	else {
	    // first time a column is clicked, or a new column has been clicked
	    this.Sorting = SortOrder.Ascending;
	}
	this.ListViewItemSorter = new RegionListItemComparer(e.Column,this.Sorting); // defined below
    }
}

class RegionListItemComparer implements IComparer {
    var m_sortColumn : int;
    var m_sortOrder : SortOrder;

    function RegionListItemComparer(iCol : int, so : SortOrder) { // constructor
	m_sortColumn = iCol;
	m_sortOrder = so;
    }

    // to implment IComparer interface, must provide a Compare function
    public function Compare(x : Object, y : Object) : int {
	// Implements IComparer.Compare
	// at run time, x and y will be of type ListViewItem, and so have SubItems member
	if (m_sortOrder == SortOrder.Ascending) {
	    return String.Compare(x.SubItems[m_sortColumn].Text,y.SubItems[m_sortColumn].Text);
	}
	else {
	    return -1*String.Compare(x.SubItems[m_sortColumn].Text,y.SubItems[m_sortColumn].Text);
	}
    }
}

class RegionListForm extends Form {
    // save the controls in the form as members of the class -- I don't actually use these
    //    variables for anything, but they're there if I need them....
    var m_lblForm : Label;
    var m_pnlOnTop : Panel;
    var m_ckbxOnTop : CheckBox;
    var m_lblOnTop : Label;
    var m_listView : RegionListView;
    var m_btnRefresh : Button;
    var m_btnDismiss : Button;

    function RegionListForm() { // constructor
	this.Text = "Region List";
        this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
        this.StartPosition = FormStartPosition.CenterScreen;
        this.Width = 440;
	this.Height = 510;

	m_lblForm = new Label();
	m_lblForm.Text = "A free script from www.otpproductions.com";
	// make the label in italics, and a bit larger than default font
	m_lblForm.Font = new System.Drawing.Font(m_lblForm.Font.FontFamily, 1.25*m_lblForm.Font.Size);
	var newFontStyle : System.Drawing.FontStyle = m_lblForm.Font.Style;
	newFontStyle |= System.Drawing.FontStyle.Italic;
	m_lblForm.Font = new System.Drawing.Font(m_lblForm.Font,newFontStyle);
	m_lblForm.Location = new System.Drawing.Point(10,10);
	m_lblForm.Size = new System.Drawing.Size(m_lblForm.PreferredWidth+5, m_lblForm.PreferredHeight);
	m_lblForm.BorderStyle = BorderStyle.FixedSingle;

	m_ckbxOnTop = new CheckBox();
	m_ckbxOnTop.Size = new System.Drawing.Size(10,15);
	m_ckbxOnTop.Location = new System.Drawing.Point(2,4); // relative to panel it will go into
	m_ckbxOnTop.Checked = true;
	m_ckbxOnTop.add_Click(ckbxOnTopClickHandler);
	this.TopMost = true;	// TopMost will be toggled whenever user toggles checkbox (requires handler)

	m_lblOnTop = new Label();
	m_lblOnTop.Text = "On Top";
	m_lblOnTop.Size = new System.Drawing.Size(m_lblOnTop.PreferredWidth+5, m_lblOnTop.PreferredHeight);
	m_lblOnTop.Location = new System.Drawing.Point(m_ckbxOnTop.Right+10,4);

	m_pnlOnTop = new Panel();
	m_pnlOnTop.Size = new System.Drawing.Size(m_ckbxOnTop.Size.Width+m_lblOnTop.Size.Width+10,
						  m_lblOnTop.Size.Height+10);
	m_pnlOnTop.BorderStyle = BorderStyle.FixedSingle;
	m_pnlOnTop.Anchor = AnchorStyles.Right + AnchorStyles.Top;
	m_pnlOnTop.Controls.Add(m_ckbxOnTop);
	m_pnlOnTop.Controls.Add(m_lblOnTop);
	// Delay adding until we can compute location relative to ListView control

	m_listView = new RegionListView();
	m_listView.Location = new System.Drawing.Point(10,m_lblForm.Bottom+10);

	// Make pnlOnTop flush with listView on the right
	m_pnlOnTop.Location = new System.Drawing.Point(m_listView.Right - m_pnlOnTop.Size.Width,10);

	m_btnRefresh = new Button();
	m_btnRefresh.Text = "Refresh";
	m_btnRefresh.Location = new System.Drawing.Point(130,m_listView.Bottom+10);
	m_btnRefresh.Anchor = AnchorStyles.Bottom;
	m_btnRefresh.add_Click(btnRefreshClickHandler);

	m_btnDismiss = new Button();
	m_btnDismiss.Text = "Dismiss";
	m_btnDismiss.Location = new System.Drawing.Point(210,m_listView.Bottom+10);
	m_btnDismiss.Anchor = AnchorStyles.Bottom;
	m_btnDismiss.add_Click(btnDismissClickHandler);

	this.Controls.Add(m_pnlOnTop);
	this.Controls.Add(m_lblForm);
	this.Controls.Add(m_listView);
	this.Controls.Add(m_btnRefresh);
	this.Controls.Add(m_btnDismiss);

	// following two lines are ineffectual, I don't know why
	this.AcceptButton = m_btnDismiss; // hitting RETURN is same as pressing Dismiss button
	this.CancelButton = m_btnDismiss; // hitting ESC is same as pressing Dismiss button
	this.ActiveControl = m_btnDismiss;
    }  // end of RegionListForm constructor

    protected function btnRefreshClickHandler(o: Object, e: System.EventArgs) {
	m_listView.Items.Clear();
	m_listView.Populate();
    }

    protected function btnDismissClickHandler(o: Object, e: System.EventArgs) {
	this.Close();
    }

    protected function ckbxOnTopClickHandler(o: Object, e: System.EventArgs) {
	// toggle TopMost property of form (checked state of check box toggles automatically)
	this.TopMost = ! this.TopMost;
    }
}

function TimeToString(time) : String 
{
    // from System.Globalization import.  Copied from Sony's "Export Regions as Subtitles" script.
    var decimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;

    var rgTime = time.ToString( RulerFormat.Time ).split(decimalSeparator); // {"hh:mm:ss", "ddd"}
    var sbRes : StringBuilder = new StringBuilder();
	
    sbRes.Append( rgTime[0]);
    sbRes.Append( ':' );
	
    var iCentiseconds = Math.round( rgTime[1]/ 10);
	
    sbRes.Append((( iCentiseconds / 10 ) >> 0 )% 10 );
    sbRes.Append((( iCentiseconds /  1 ) >> 0 )% 10 );
	
    return sbRes;	
}
