You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
415 lines
20 KiB
415 lines
20 KiB
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 ) ;
|
|
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.mStartupWebBrowser.AutoSize = true ;
|
|
this.mStartupWebBrowser.Dock = DockStyle.Fill ;
|
|
mSplitter.Panel2.Controls.Add( mStartupWebBrowser ) ;
|
|
|
|
// 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" ) ;
|
|
mStartupWebBrowser.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.mSearchQuery.KeyPress += new KeyPressEventHandler( this.SearchQuery_KeyPress ) ;
|
|
this.mSearchQuery.KeyDown += new KeyEventHandler( this.SearchQuery_KeyDown ) ;
|
|
this.mSearchResults.KeyPress += new KeyPressEventHandler( this.SearchResults_KeyPress ) ;
|
|
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 ;
|
|
}
|
|
|
|
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 ;
|
|
mSearchResults.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<int,int>( 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 )
|
|
{
|
|
// remove the startup messages
|
|
if ( mStartupWebBrowser != null ) {
|
|
mSplitter.Panel2.Controls.Remove( mStartupWebBrowser ) ;
|
|
mStartupWebBrowser = null ;
|
|
}
|
|
|
|
// 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
|
|
setChartImagePanelScrollPos( 0, 0 ) ;
|
|
doMainFormResize( null ) ;
|
|
}
|
|
|
|
private void SearchQuery_TextChanged( object sender, EventArgs e )
|
|
{
|
|
// update the search results
|
|
updateSearchResults( mSearchQuery.Text.Trim() ) ;
|
|
}
|
|
|
|
private void SearchQuery_KeyPress( object sender, KeyPressEventArgs e )
|
|
{
|
|
// check how much time has passed since the last keypress
|
|
int ch = (int) e.KeyChar ;
|
|
if ( ch >= 32 && ch < 127 ) {
|
|
int ttl = Program.appConfig.getIntVal( new string[]{"SearchQueryTTL"}, 5 ) ;
|
|
if ( (DateTime.Now - mLastKeyPressTimeStamp).TotalSeconds > ttl ) {
|
|
// it's been a while - start a new search query
|
|
mSearchQuery.Text = "" ;
|
|
}
|
|
mLastKeyPressTimeStamp = DateTime.Now ;
|
|
}
|
|
}
|
|
|
|
private void SearchQuery_KeyDown( object sender, KeyEventArgs e )
|
|
{
|
|
// check if there are any ChartImage's associated with the keypress
|
|
List<ChartImage> chartImages = ChartImage.checkShortcut( e.Modifiers, e.KeyCode ) ;
|
|
if ( chartImages != null ) {
|
|
// yup - show them as search results
|
|
ILog logger = LogManager.GetLogger( "shortcuts" ) ;
|
|
logger.Info( $"Found ChartImage's for shortcut: {ChartImage.shortcutString(e.Modifiers,e.KeyCode)}" ) ;
|
|
foreach ( ChartImage chartImage in chartImages )
|
|
logger.Info( $"- {chartImage.caption()}" ) ;
|
|
mSearchQuery.Text = "" ;
|
|
loadSearchResults( chartImages, "HOTKEY:"+e.Modifiers+":"+e.KeyCode ) ;
|
|
e.Handled = true ;
|
|
return ;
|
|
}
|
|
|
|
// check if we should apply the keypress to the search results
|
|
if ( e.KeyCode == Keys.Escape ) {
|
|
mSearchQuery.Text = "" ;
|
|
e.Handled = true ;
|
|
} else if ( e.KeyCode == Keys.Left || e.KeyCode == Keys.Right ) {
|
|
mSearchResults.Focus() ;
|
|
SendKeys.SendWait( "{" + e.KeyCode.ToString() + "}" ) ;
|
|
mSearchQuery.Focus() ;
|
|
e.Handled = true ;
|
|
}
|
|
}
|
|
|
|
private void SearchResults_KeyPress( object sender, KeyPressEventArgs e )
|
|
{
|
|
// check if we should apply the keypress to the search query
|
|
int ch = (int) e.KeyChar ;
|
|
if ( ch == 27 )
|
|
mSearchQuery.Text = "" ;
|
|
else if ( ch >= 32 && ch < 127 ) {
|
|
mSearchQuery.Focus() ;
|
|
SendKeys.Send( e.KeyChar.ToString() ) ;
|
|
}
|
|
}
|
|
}
|
|
|