using System ; using System.Text ; using System.Drawing ; using System.IO ; using System.Threading ; using System.Runtime.InteropServices ; using System.Collections.Generic ; using System.Windows.Forms ; using System.Diagnostics ; using Manina.Windows.Forms ; using Newtonsoft.Json.Linq ; using log4net ; // -------------------------------------------------------------------- public partial class MainForm : Form { private void InitializeComponent() { // initialize the form this.Text = Program.APP_NAME ; this.MinimumSize = new Size( 800, 500 ) ; // initialize the main splitter this.mSplitter.Orientation = Orientation.Horizontal ; this.mSplitter.Location = new Point( 0, 0 ) ; this.mSplitter.Size = new Size( this.Width, this.Height ) ; this.mSplitter.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right ; this.mSplitter.FixedPanel = FixedPanel.Panel1 ; this.mSplitter.Panel1MinSize = 140 ; this.mSplitter.Panel2MinSize = 300 ; this.mSplitter.SplitterDistance = this.mSplitter.Panel1MinSize ; this.mSplitter.Panel1.BackColor = Color.White ; this.mSplitter.Panel2.BackColor = Color.White ; this.mSplitter.BackColor = SystemColors.Control ; this.mSplitter.SplitterWidth = 2 ; this.Controls.Add( mSplitter ) ; // initialize the top pane (search) int margin = 2 ; this.mSearchUserControl.Size = new Size( 100, mSplitter.Panel1.Size.Height ) ; this.mSearchLabel.Location = new Point( margin, margin ) ; this.mSearchLabel.Size = new Size( mSearchUserControl.Width - 2*margin, 15 ) ; this.mSearchLabel.Font = new Font( mSearchLabel.Font, FontStyle.Bold ) ; this.mSearchLabel.Text = "Search for:" ; this.mSearchUserControl.Controls.Add( mSearchLabel ) ; this.mSearchQuery.Location = new Point( margin, mSearchLabel.Location.Y + mSearchLabel.Height ) ; this.mSearchQuery.Width = mSearchLabel.Width ; this.mSearchQuery.BorderStyle = BorderStyle.FixedSingle ; this.mSearchUserControl.Controls.Add( mSearchQuery ) ; LinkLabel showShortcutsLabel = new LinkLabel() ; showShortcutsLabel.Text = "Shortcuts" ; showShortcutsLabel.AutoSize = true ; showShortcutsLabel.Location = new Point( margin, mSearchUserControl.Height-margin-showShortcutsLabel.Height ) ; showShortcutsLabel.LinkClicked += delegate( object sender, LinkLabelLinkClickedEventArgs e ) { showShortcuts() ; } ; this.mSearchUserControl.Controls.Add( showShortcutsLabel ) ; this.mSplitter.Panel1.Controls.Add( mSearchUserControl ) ; this.mSearchResults.View = Manina.Windows.Forms.View.Gallery ; this.mSearchResults.SetRenderer( new AppImageListView.AppImageListViewRenderer() ) ; this.mSearchResults.CacheMode = CacheMode.Continuous ; this.mSearchResults.Location = new Point( mSearchUserControl.Location.X + mSearchUserControl.Width, margin ) ; Size searchResultsSize = new Size( mSplitter.Width - (mSearchQuery.Location.X + mSearchQuery.Width) - margin - 7, mSplitter.Panel1MinSize ) ; this.mSearchResults.Size = searchResultsSize ; this.mSearchResults.BorderStyle = BorderStyle.None ; this.mSearchResults.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom ; this.mSplitter.Panel1.Controls.Add( mSearchResults ) ; this.mSearchResults.Size = searchResultsSize ; // FUDGE! need to do this again :shrug: // initialize the bottom pane (image viewer) // FIXME! We will probably need something better to render the images, but this will do, for now... this.mChartImagePictureBox.SizeMode = PictureBoxSizeMode.Zoom ; // FUDGE! We need to put the PictureBox inside a Panel in order to get the h-scrollbar to appear :-/ mChartImagePanel.Anchor = AnchorStyles.Top | AnchorStyles.Left ; mChartImagePanel.AutoScroll = true ; mChartImagePanel.Controls.Add( mChartImagePictureBox ) ; mSplitter.Panel2.Controls.Add( mChartImagePanel ) ; // initialize the bottom pane (startup messages) this.mWebBrowser.AutoSize = true ; this.mWebBrowser.Dock = DockStyle.Fill ; mSplitter.Panel2.Controls.Add( mWebBrowser ) ; // show the startup page string fname = Path.Combine( Program.resourcesDir, "startup.html" ) ; if ( File.Exists( fname ) ) { string buf = File.ReadAllText( fname ) ; fname = Path.Combine( Program.resourcesDir, "spinner.gif" ) ; mWebBrowser.DocumentText = buf.Replace( "{{SPINNER-URL}}", fname ) ; } // initialize handlers this.Load += new EventHandler( this.MainForm_Load ) ; this.FormClosing += new FormClosingEventHandler( this.MainForm_FormClosing ) ; this.Resize += new EventHandler( this.MainForm_Resize ) ; this.mSearchQuery.TextChanged += new EventHandler( this.SearchQuery_TextChanged ) ; this.mSearchResults.SelectionChanged += new EventHandler( this.SearchResults_SelectionChanged ) ; mChartImagePictureBox.MouseDown += new MouseEventHandler( this.ChartImagePictureBox_MouseDown ) ; mChartImagePictureBox.MouseMove += new MouseEventHandler( this.ChartImagePictureBox_MouseMove ) ; mChartImagePictureBox.MouseUp += new MouseEventHandler( this.ChartImagePictureBox_MouseUp ) ; // NOTE: We also register mouse event handlers for the panel, so that dragging will work if the user clicks // inside the image panel, but not on the image itself (i.e. when the image is smaller than the panel). mChartImagePanel.MouseDown += new MouseEventHandler( this.ChartImagePictureBox_MouseDown ) ; mChartImagePanel.MouseMove += new MouseEventHandler( this.ChartImagePictureBox_MouseMove ) ; mChartImagePanel.MouseUp += new MouseEventHandler( this.ChartImagePictureBox_MouseUp ) ; } public bool preFilterMessage( ref Message msg ) { // FUDGE! On Windows, when we handle Ctrl/Shift-ScrollWheel, Windows still wants to scroll the ChartImage. // To stop this, we disable mouse wheel messages and handle everything ourself :-/ if ( msg.Msg == 0x020A ) { // nb: WM_MOUSEWHEEL ulong wParam = (ulong)msg.WParam & 0xFFFFFFFF ; int nLines = (int)( wParam >> 16 ) ; if ( (nLines & 0x8000) != 0 ) nLines = nLines - 0x10000 ; // nb: negate the 32-bit value nLines = nLines * SystemInformation.MouseWheelScrollLines / 120 ; if ( (wParam & 0xFFFF) == 0 ) nLines = - nLines ; // nb: no virtual keys are down this.scrollMainForm( nLines ) ; return true ; } return false ; } protected override bool ProcessCmdKey( ref Message msg, Keys keyData ) { // prevent recursive calls if ( mDisableProcessCmdKey ) return false ; // initialize Keys modifierKeys = Control.ModifierKeys & (Keys.Control | Keys.Shift | Keys.Alt) ; Keys keyCode = keyData & ~(Keys.Control | Keys.Shift | Keys.Alt) ; // check for special keypresses if ( keyCode == Keys.Left || keyCode == Keys.Right ) { // send the keypress to the search results // NOTE: We could also respond to Up/Down and scroll the ChartImage vertically, // but that would be confusing, given that Left/Right selects a search result. mSearchResults.Focus() ; mDisableProcessCmdKey = true ; SendKeys.SendWait( "{" + keyCode.ToString() + "}" ) ; mDisableProcessCmdKey = false ; return true ; } if ( keyCode == Keys.PageUp || keyCode == Keys.PageDown ) { // scroll the ChartImage up/down if ( mChartImagePictureBox.Image == null ) return false ; double ratio = (double)mChartImagePanel.Height / (double)mChartImagePictureBox.Height ; VScrollProperties vscroll = mChartImagePanel.VerticalScroll ; int scrollRange = vscroll.Maximum - vscroll.Minimum ; int pageSize = (int)( ratio * scrollRange * 0.8 ) ; if ( keyCode == Keys.PageUp ) pageSize = - pageSize ; int scrollY = vscroll.Value + pageSize ; scrollChartImagePanel( null, scrollY ) ; return true ; } if ( keyCode == Keys.Home ) { int? scrollX=null, scrollY=null ; if ( (modifierKeys & Keys.Control) != 0 ) scrollX = scrollY = 0 ; if ( (modifierKeys & Keys.Shift) != 0 ) scrollX = 0 ; else scrollY = 0 ; setChartImagePanelScrollPos( scrollX, scrollY ) ; return true ; } if ( keyCode == Keys.End ) { int? scrollX=null, scrollY=null ; if ( (modifierKeys & Keys.Control) != 0 ) { scrollX = 0 ; scrollY = ChartImagePanel_MaxScrollY() ; } if ( (modifierKeys & Keys.Shift) != 0 ) scrollX = ChartImagePanel_MaxScrollX() ; else scrollY = ChartImagePanel_MaxScrollY() ; setChartImagePanelScrollPos( scrollX, scrollY ) ; return true ; } if ( keyCode == Keys.Escape ) { // clear the search query loadSearchResults( mChartImages.Values, "SHOW-ALL", true ) ; return true ; } if ( keyCode == Keys.Return ) return true ; // nb: stop Windows from beeping :-/ // handle shortcuts if ( Shortcut.handleShortcut( keyData ) ) return true ; // check if we should update the search query // FUDGE! This is incredibly annoying :-/ We get a *key code*, which is not the same as an ASCII character, // so we only recognize a very limited set of characters here. Other characters will still be recognized // and added to the search query *if* the textbox has focus (since the code here won't fire, we end up // flagging the message as "not processed", and the textbox will receive the message normally), so it's // only an issue when the user is typing a search query and something else has focus. We could perhaps // translate things like Keys.OemXXX key codes here, but it's not worth the effort, since search queries // will almost always consist of only letters and/or numbers. int ch = (int) keyCode ; string sendKey = "" ; if ( (ch >= 65 && ch <= 90) || ch == 32 ) sendKey = ((char)ch).ToString().ToLower() ; else if ( ch >= 48 && ch <= 57 ) sendKey = keyCode.ToString().Substring( 1 ) ; else if ( keyCode == Keys.Back ) sendKey = "{BKSP}" ; if ( sendKey != "" ) { // check if it's time to start a new search query int ttl = Program.appConfig.getIntVal( new string[]{"SearchQueryTTL"}, 5 ) ; if ( (DateTime.Now - mLastKeyPressTimeStamp).TotalSeconds > ttl ) mSearchQuery.Text = "" ; mLastKeyPressTimeStamp = DateTime.Now ; // send the keypress to the search query textbox mSearchQuery.Focus() ; mDisableProcessCmdKey = true ; SendKeys.SendWait( sendKey ) ; mDisableProcessCmdKey = false ; return true ; } return false ; } private int ChartImagePanel_MaxScrollX() { return mChartImagePanel.HorizontalScroll.Maximum - mChartImagePanel.Width + 17 ; } private int ChartImagePanel_MaxScrollY() { return mChartImagePanel.VerticalScroll.Maximum - mChartImagePanel.Height + 17 ; } private void MainForm_Load( object sender, EventArgs e ) { // restore the window state string windowStateStr = Program.appConfig.getStringVal( new string[]{"MainWindow","WindowState"} ) ; if ( windowStateStr != "" ) { FormWindowState windowState ; if ( Enum.TryParse( windowStateStr, out windowState ) ) { DesktopBounds = new Rectangle( Program.appConfig.getIntVal( new string[]{"MainWindow","X"}, 50 ), Program.appConfig.getIntVal( new string[]{"MainWindow","Y"}, 50 ), Program.appConfig.getIntVal( new string[]{"MainWindow","Width"}, MinimumSize.Width ), Program.appConfig.getIntVal( new string[]{"MainWindow","Height"}, MinimumSize.Height ) ) ; if ( windowState == FormWindowState.Maximized ) WindowState = windowState ; } } // initialize doMainFormResize( null ) ; // load the chart images // NOTE: This can take some time, so we update the UI as they are loaded. mSearchLabel.Enabled = false ; mSearchQuery.Enabled = false ; Thread thread = new Thread( () => loadChartImages() ) ; thread.Start() ; } private void MainForm_FormClosing( object sender, FormClosingEventArgs e ) { // save the window state // NOTE: RestoreBounds doesn't work properly under Mono :-/ Rectangle rc = (WindowState == FormWindowState.Maximized) ? RestoreBounds : DesktopBounds ; Program.appConfig.setIntVal( new string[]{"MainWindow","X"}, rc.X, false ) ; Program.appConfig.setIntVal( new string[]{"MainWindow","Y"}, rc.Y, false ) ; Program.appConfig.setIntVal( new string[]{"MainWindow","Width"}, rc.Width, false ) ; Program.appConfig.setIntVal( new string[]{"MainWindow","Height"}, rc.Height, false ) ; Program.appConfig.setStringVal( new string[]{"MainWindow","WindowState"}, WindowState.ToString() ) ; } private void MainForm_Resize( object sender, EventArgs e ) { doMainFormResize( null ) ; } private void doMainFormResize( Point? clientMousePos ) { // check if we are showing a ChartImage Image img = mChartImagePictureBox.Image ; if ( img == null ) { mChartImagePanel.Visible = false ; return ; } mChartImagePanel.Visible = true ; // resize the ChartImage panel to fill the available space mChartImagePanel.Size = new Size( mSplitter.Panel2.Width - (Program.isMono?8:17), mSplitter.Panel2.Height - (Program.isMono?28:40) ) ; // figure out how large to show the ChartImage int width, height ; int margin = 5 ; int availableWidth = mChartImagePanel.Width - 2*margin ; double zoom = Math.Max( Math.Min( mCurrZoom, mMaxZoom ), mMinZoom ) ; width = (int)( img.Width * zoom + 0.5 ) ; height = (int)( img.Height * zoom + 0.5 ) ; // check if the v-scrollbar is showing if ( height >= mChartImagePanel.Height ) { // yup - adjust the width int sbWidth = SystemInformation.VerticalScrollBarWidth ; availableWidth -= sbWidth ; width -= sbWidth ; } // remember the current scroll positions HScrollProperties hscroll = mChartImagePanel.HorizontalScroll ; int prevScrollPosX = hscroll.Value ; int prevMaxScrollX = ChartImagePanel_MaxScrollX() ; double prevRelScrollPosX = (double)prevScrollPosX / (double)prevMaxScrollX ; VScrollProperties vscroll = mChartImagePanel.VerticalScroll ; int prevScrollPosY = vscroll.Value ; int prevMaxScrollY = ChartImagePanel_MaxScrollY() ; double prevRelScrollPosY = (double)prevScrollPosY / (double)prevMaxScrollY ; // resize and position the ChartImage setChartImagePanelScrollPos( 0, 0 ) ; Size prevChartImagePictureBoxSize = new Size( mChartImagePictureBox.Size.Width, mChartImagePictureBox.Size.Height ) ; mChartImagePictureBox.Size = new Size( width, height ) ; if ( width < availableWidth ) mChartImagePictureBox.Location = new Point( margin + (availableWidth - width)/2, margin ) ; else mChartImagePictureBox.Location = new Point( margin, margin ) ; // restore the scroll positions int newScrollPosX, newScrollPosY ; int newMaxScrollX = ChartImagePanel_MaxScrollX() ; int newMaxScrollY = ChartImagePanel_MaxScrollY() ; if ( clientMousePos == null ) { // maintain the relative scroll positions newScrollPosX = (int)( prevRelScrollPosX * newMaxScrollX + 0.5 ) ; newScrollPosY = (int)( prevRelScrollPosY * newMaxScrollY + 0.5 ) ; } else { // keep whatever's under the mouse in the same place int absX = prevScrollPosX + clientMousePos.Value.X ; double scaling = (double)mChartImagePictureBox.Size.Width / (double)prevChartImagePictureBoxSize.Width ; newScrollPosX = (int)( absX * scaling + 0.5) - clientMousePos.Value.X ; int absY = prevScrollPosY + clientMousePos.Value.Y ; scaling = (double)mChartImagePictureBox.Size.Height / (double)prevChartImagePictureBoxSize.Height ; newScrollPosY = (int)( absY * scaling + 0.5) - clientMousePos.Value.Y ; } scrollChartImagePanel( newScrollPosX, newScrollPosY ) ; } private void ChartImagePictureBox_MouseDown( object sender, MouseEventArgs e ) { // flag that the user has started to drag the ChartImage // FUDGE! The mouse position as reported by the MouseEventArgs bounces around weirdly during mouse move, // so we read the mouse position directly :shrug: mMouseDragAnchor = Cursor.Position ; mScrollDragAnchor = new Tuple( mChartImagePanel.HorizontalScroll.Value, mChartImagePanel.VerticalScroll.Value ) ; Cursor.Current = Cursors.Hand ; } private void ChartImagePictureBox_MouseMove( object sender, MouseEventArgs e ) { // check if a drag is in progress if ( mMouseDragAnchor == null ) return ; Point pos = Cursor.Position ; int deltaX = pos.X - mMouseDragAnchor.Value.X ; int deltaY = pos.Y - mMouseDragAnchor.Value.Y ; int scrollX = mScrollDragAnchor.Item1 - deltaX ; int scrollY = mScrollDragAnchor.Item2 - deltaY ; scrollChartImagePanel( scrollX, scrollY ) ; } private void ChartImagePictureBox_MouseUp( object sender, MouseEventArgs e ) { // end the drag mMouseDragAnchor = null ; mScrollDragAnchor = null ; Cursor.Current = Cursors.Default ; } private void scrollMainForm( int nLines ) { // check if the mouse is over the ChartImage Panel Point pos = Cursor.Position ; pos = mChartImagePanel.PointToClient( pos ) ; if ( pos.X < 0 || pos.X >= mChartImagePanel.Width || pos.Y < 0 || pos.Y >= mChartImagePanel.Height ) return ; if ( (Control.ModifierKeys & Keys.Shift) != 0 ) { // scroll the ChartImage left/right HScrollProperties hscroll = mChartImagePanel.HorizontalScroll ; int scrollRange = hscroll.Maximum - hscroll.Minimum ; int scrollX = hscroll.Value + (int)( scrollRange * nLines/100 ) ; scrollChartImagePanel( scrollX, null ) ; } else if ( (Control.ModifierKeys & Keys.Control) != 0 ) { double newZoom ; if ( nLines > 0 ) { newZoom = Math.Min( mCurrZoom+0.05, mMaxZoom ) ; } else { newZoom = Math.Max( mCurrZoom-0.05, mMinZoom ) ; } if ( Math.Abs( newZoom - mCurrZoom) > 0.001 ) { ILog logger = LogManager.GetLogger( "zoom" ) ; logger.Info( $"Setting image zoom: {newZoom:0.00}" ) ; mCurrZoom = newZoom ; doMainFormResize( pos ) ; } } else { // scroll the ChartImage up/down VScrollProperties vscroll = mChartImagePanel.VerticalScroll ; int scrollRange = vscroll.Maximum - vscroll.Minimum ; int scrollY = vscroll.Value + (int)( scrollRange * nLines/100 ) ; scrollChartImagePanel( null, scrollY ) ; } } private void scrollChartImagePanel( int? scrollX, int? scrollY ) { // update the scroll position if ( scrollX != null ) { int maxScrollX = ChartImagePanel_MaxScrollX() ; int scrollPos = Math.Max( 0, Math.Min( maxScrollX, scrollX.Value ) ) ; setChartImagePanelScrollPos( scrollPos, null ) ; } if ( scrollY != null ) { int maxScrollY = ChartImagePanel_MaxScrollY() ; int scrollPos = Math.Max( 0, Math.Min( maxScrollY, scrollY.Value ) ) ; setChartImagePanelScrollPos( null, scrollPos ) ; } } private void SearchResults_SelectionChanged( object sender, EventArgs e ) { // figure out the initial zoom Debug.Assert( mSearchResults.SelectedItems.Count == 1 ) ; double userZoom = Program.dataConfig.getDoubleVal( new string[]{"_DefaultZoom"}, 1.0 ) ; double minZoom = Program.dataConfig.getDoubleVal( new string[]{"_MinZoom"}, 0.2*userZoom ) ; double maxZoom = Program.dataConfig.getDoubleVal( new string[]{"_MaxZoom"}, 2*userZoom ) ; if ( mSearchResults.SelectedItems.Count > 0 ) { ChartImage chartImage = (ChartImage) mSearchResults.SelectedItems[0].Tag ; mChartImagePictureBox.Image = chartImage.image ; userZoom = chartImage.jsonConfig.getDoubleVal( new string[]{"defaultZoom"}, userZoom ) ; minZoom = chartImage.jsonConfig.getDoubleVal( new string[]{"minZoom"}, minZoom ) ; maxZoom = chartImage.jsonConfig.getDoubleVal( new string[]{"maxZoom"}, maxZoom ) ; } else { mChartImagePictureBox.Image = null ; } ILog logger = LogManager.GetLogger( "zoom" ) ; logger.Info( $"Setting initial image zoom: {userZoom:0.00} (min={minZoom:0.00}, max={maxZoom:0.00})" ) ; mCurrZoom = userZoom ; mMinZoom = minZoom ; mMaxZoom = maxZoom ; // show the selected chart image mChartImagePanel.BringToFront() ; setChartImagePanelScrollPos( 0, 0 ) ; doMainFormResize( null ) ; } private void SearchQuery_TextChanged( object sender, EventArgs e ) { // update the search results if ( mSearchQueryTextChangedDisabled ) return ; updateSearchResults( mSearchQuery.Text.Trim() ) ; } }