diff --git a/ClientApp/App.xaml b/ClientApp/App.xaml index 1400927..61cadcc 100644 --- a/ClientApp/App.xaml +++ b/ClientApp/App.xaml @@ -6,13 +6,25 @@ xmlns:views="clr-namespace:ClientApp.Views" StartupUri="Views/MainWindow.xaml"> - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/ClientApp/ClientApp.csproj b/ClientApp/ClientApp.csproj index 7b0bcaf..637f86c 100644 --- a/ClientApp/ClientApp.csproj +++ b/ClientApp/ClientApp.csproj @@ -7,6 +7,36 @@ Images\Logo\icon1.ico + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + @@ -19,6 +49,10 @@ + + + + \ No newline at end of file diff --git a/ClientApp/Images/gradient-geometric-shape-background_78532-374.jpg b/ClientApp/Images/CoolBackground.jpg similarity index 100% rename from ClientApp/Images/gradient-geometric-shape-background_78532-374.jpg rename to ClientApp/Images/CoolBackground.jpg diff --git a/ClientApp/Images/Icons/CheckMark.png b/ClientApp/Images/Icons/CheckMark.png new file mode 100644 index 0000000..6bb2ed3 Binary files /dev/null and b/ClientApp/Images/Icons/CheckMark.png differ diff --git a/ClientApp/Images/Icons/CrossMark.png b/ClientApp/Images/Icons/CrossMark.png new file mode 100644 index 0000000..4a30bac Binary files /dev/null and b/ClientApp/Images/Icons/CrossMark.png differ diff --git a/ClientApp/Images/Logo/icon1.ico b/ClientApp/Images/Logo/icon1.ico index 95760d6..faa56b4 100644 Binary files a/ClientApp/Images/Logo/icon1.ico and b/ClientApp/Images/Logo/icon1.ico differ diff --git a/ClientApp/Images/Logo/icon1_small.ico b/ClientApp/Images/Logo/icon1_small.ico new file mode 100644 index 0000000..95760d6 Binary files /dev/null and b/ClientApp/Images/Logo/icon1_small.ico differ diff --git a/ClientApp/MagicCode/WindowResizer.cs b/ClientApp/MagicCode/WindowResizer.cs new file mode 100644 index 0000000..c997d89 --- /dev/null +++ b/ClientApp/MagicCode/WindowResizer.cs @@ -0,0 +1,212 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; + +namespace ClientApp.MagicCode +{ + /// + /// Fixes the issue with Windows of Style covering the taskbar + /// + public class WindowResizer + { + #region Private Members + + /// + /// The window to handle the resizing for + /// + private Window mWindow; + + #endregion + + #region Dll Imports + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool GetCursorPos(out POINT lpPoint); + + [DllImport("user32.dll")] + static extern bool GetMonitorInfo(IntPtr hMonitor, MONITORINFO lpmi); + + [DllImport("user32.dll", SetLastError = true)] + static extern IntPtr MonitorFromPoint(POINT pt, MonitorOptions dwFlags); + + #endregion + + #region Constructor + + /// + /// Default constructor + /// + /// The window to monitor and correctly maximize + /// The callback for the host to adjust the maximum available size if needed + public WindowResizer(Window window) + { + mWindow = window; + + // Listen out for source initialized to setup + mWindow.SourceInitialized += Window_SourceInitialized; + } + + #endregion + + #region Initialize + + /// + /// Initialize and hook into the windows message pump + /// + /// + /// + private void Window_SourceInitialized(object sender, System.EventArgs e) + { + // Get the handle of this window + var handle = (new WindowInteropHelper(mWindow)).Handle; + var handleSource = HwndSource.FromHwnd(handle); + + // If not found, end + if (handleSource == null) + return; + + // Hook into it's Windows messages + handleSource.AddHook(WindowProc); + } + + #endregion + + #region Windows Proc + + /// + /// Listens out for all windows messages for this window + /// + /// + /// + /// + /// + /// + /// + private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + switch (msg) + { + // Handle the GetMinMaxInfo of the Window + case 0x0024:/* WM_GETMINMAXINFO */ + WmGetMinMaxInfo(hwnd, lParam); + handled = true; + break; + } + + return (IntPtr)0; + } + + #endregion + + /// + /// Get the min/max window size for this window + /// Correctly accounting for the taskbar size and position + /// + /// + /// + private void WmGetMinMaxInfo(System.IntPtr hwnd, System.IntPtr lParam) + { + POINT lMousePosition; + GetCursorPos(out lMousePosition); + + IntPtr lPrimaryScreen = MonitorFromPoint(new POINT(0, 0), MonitorOptions.MONITOR_DEFAULTTOPRIMARY); + MONITORINFO lPrimaryScreenInfo = new MONITORINFO(); + if (GetMonitorInfo(lPrimaryScreen, lPrimaryScreenInfo) == false) + { + return; + } + + IntPtr lCurrentScreen = MonitorFromPoint(lMousePosition, MonitorOptions.MONITOR_DEFAULTTONEAREST); + + MINMAXINFO lMmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO)); + + if (lPrimaryScreen.Equals(lCurrentScreen) == true) + { + lMmi.ptMaxPosition.X = lPrimaryScreenInfo.rcWork.Left; + lMmi.ptMaxPosition.Y = lPrimaryScreenInfo.rcWork.Top; + lMmi.ptMaxSize.X = lPrimaryScreenInfo.rcWork.Right - lPrimaryScreenInfo.rcWork.Left; + lMmi.ptMaxSize.Y = lPrimaryScreenInfo.rcWork.Bottom - lPrimaryScreenInfo.rcWork.Top; + } + else + { + lMmi.ptMaxPosition.X = lPrimaryScreenInfo.rcMonitor.Left; + lMmi.ptMaxPosition.Y = lPrimaryScreenInfo.rcMonitor.Top; + lMmi.ptMaxSize.X = lPrimaryScreenInfo.rcMonitor.Right - lPrimaryScreenInfo.rcMonitor.Left; + lMmi.ptMaxSize.Y = lPrimaryScreenInfo.rcMonitor.Bottom - lPrimaryScreenInfo.rcMonitor.Top; + } + + // Now we have the max size, allow the host to tweak as needed + Marshal.StructureToPtr(lMmi, lParam, true); + } + } + + #region Dll Helper Structures + + enum MonitorOptions : uint + { + MONITOR_DEFAULTTONULL = 0x00000000, + MONITOR_DEFAULTTOPRIMARY = 0x00000001, + MONITOR_DEFAULTTONEAREST = 0x00000002 + } + + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class MONITORINFO + { + public int cbSize = Marshal.SizeOf(typeof(MONITORINFO)); + public Rectangle rcMonitor = new Rectangle(); + public Rectangle rcWork = new Rectangle(); + public int dwFlags = 0; + } + + + [StructLayout(LayoutKind.Sequential)] + public struct Rectangle + { + public int Left, Top, Right, Bottom; + + public Rectangle(int left, int top, int right, int bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMinTrackSize; + public POINT ptMaxTrackSize; + }; + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + /// + /// x coordinate of point. + /// + public int X; + /// + /// y coordinate of point. + /// + public int Y; + + /// + /// Construct a point of coordinates (x,y). + /// + public POINT(int x, int y) + { + this.X = x; + this.Y = y; + } + } + + #endregion +} \ No newline at end of file diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index 58b0177..963c1d3 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -10,7 +10,7 @@ using Util; namespace ClientApp.Utils { public delegate void EngineCallback(); - public class Client : IDataReceiver + public class Client : IDataReceiver, IDisposable { public EngineCallback engineConnectFailed; public EngineCallback engineConnectSuccess; @@ -46,6 +46,7 @@ namespace ClientApp.Utils engineConnection = EngineConnection.INSTANCE; engineConnection.OnNoTunnelId = RetryEngineConnection; engineConnection.OnSuccessFullConnection = engineConnected; + engineConnection.OnEngineDisconnect = RetryEngineConnection; if (!engineConnection.Connected) engineConnection.Connect(); } @@ -87,6 +88,10 @@ namespace ClientApp.Utils /// the result of the async read private void OnRead(IAsyncResult ar) { + if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead)) + return; + + int receivedBytes = this.stream.EndRead(ar); if (totalBufferReceived + receivedBytes > 1024) @@ -122,7 +127,7 @@ namespace ClientApp.Utils this.LoginViewModel.setLoginStatus(true); this.connected = true; initEngine(); - + } else { @@ -224,7 +229,7 @@ namespace ClientApp.Utils /// the message public void Bike(byte[] bytes) { - + if (!sessionRunning) { return; @@ -266,7 +271,7 @@ namespace ClientApp.Utils /// public void tryLogin(string username, string password) { - + string hashPassword = Util.Hasher.HashString(password); byte[] message = DataParser.getJsonMessage(DataParser.GetLoginJson(username, hashPassword)); @@ -288,5 +293,13 @@ namespace ClientApp.Utils { this.LoginViewModel = loginViewModel; } + + public void Dispose() + { + Debug.WriteLine("client dispose called"); + this.stream.Dispose(); + this.client.Dispose(); + this.handler.stop(); + } } } diff --git a/ClientApp/Utils/EngineConnection.cs b/ClientApp/Utils/EngineConnection.cs index ea699a3..3706e87 100644 --- a/ClientApp/Utils/EngineConnection.cs +++ b/ClientApp/Utils/EngineConnection.cs @@ -10,16 +10,17 @@ using LibNoise.Primitive; namespace ClientApp.Utils { public delegate void HandleSerial(string message); - public delegate void HandleNoTunnelId(); - public delegate void OnSuccessfullConnection(); + public delegate void EngineDelegate(); public sealed class EngineConnection { private static EngineConnection instance = null; private static readonly object padlock = new object(); private static System.Timers.Timer updateTimer; - public HandleNoTunnelId OnNoTunnelId; - public OnSuccessfullConnection OnSuccessFullConnection; + private static System.Timers.Timer noVRResponseTimer; + public EngineDelegate OnNoTunnelId; + public EngineDelegate OnSuccessFullConnection; + public EngineDelegate OnEngineDisconnect; private static PC[] PCs = { @@ -42,6 +43,7 @@ namespace ClientApp.Utils private static string headId = string.Empty; private static string groundPlaneId = string.Empty; private static string terrainId = string.Empty; + private static string lastMessage = "No message received yet"; public float BikeSpeed { get; set; } public float BikePower { get; set; } @@ -67,6 +69,12 @@ namespace ClientApp.Utils updateTimer.Elapsed += UpdateTimer_Elapsed; updateTimer.AutoReset = true; updateTimer.Enabled = false; + + noVRResponseTimer = new System.Timers.Timer(15000); + noVRResponseTimer.Elapsed += noVRResponseTimeout; + noVRResponseTimer.AutoReset = false; + noVRResponseTimer.Enabled = false; + } private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) @@ -74,6 +82,21 @@ namespace ClientApp.Utils UpdateInfoPanel(); } + private void noVRResponseTimeout(object sender, System.Timers.ElapsedEventArgs e) + { + Write("VR RESPONSE TIMEOUT"); + noVRResponseTimer.Stop(); + sessionId = string.Empty; + tunnelId = string.Empty; + cameraId = string.Empty; + routeId = string.Empty; + panelId = string.Empty; + bikeId = string.Empty; + groundPlaneId = string.Empty; + terrainId = string.Empty; + OnEngineDisconnect?.Invoke(); + } + /// /// Singleton constructor /// @@ -165,6 +188,7 @@ namespace ClientApp.Utils { Write("got tunnel id! " + tunnelId); Connected = true; + noVRResponseTimer.Enabled = true; OnSuccessFullConnection?.Invoke(); } } @@ -176,6 +200,9 @@ namespace ClientApp.Utils //Console.WriteLine("Got serial " + serial); if (serialResponses.ContainsKey(serial)) serialResponses[serial].Invoke(message); } + + noVRResponseTimer.Stop(); + noVRResponseTimer.Start(); } public void initScene() @@ -297,6 +324,11 @@ namespace ClientApp.Utils { // TODO check if is drawn }); + SendMessageAndOnResponse(mainCommand.showMessage(panelId, "message", lastMessage), "message", + (message) => + { + // TODO check if is drawn + }); // Check if every text is drawn! diff --git a/ClientApp/ValueConverters/BoolToMarkConverter.cs b/ClientApp/ValueConverters/BoolToMarkConverter.cs new file mode 100644 index 0000000..ea03634 --- /dev/null +++ b/ClientApp/ValueConverters/BoolToMarkConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace ClientApp.ValueConverters +{ + //[ValueConversion(typeof(bool), typeof(BitmapImage))] + + class BoolToMarkConverter : IValueConverter + { + public BitmapImage TrueImage { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Images/Icons/CheckMark.png")); + public BitmapImage FalseImage { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Images/Icons/CrossMark.png")); + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (!(value is bool)) + { + return null; + } + + bool b = (bool)value; + if (b) + { + return this.TrueImage; + } + else + { + return this.FalseImage; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/ClientApp/ViewModels/LoginViewModel.cs b/ClientApp/ViewModels/LoginViewModel.cs index 1179ec2..7b9bfcf 100644 --- a/ClientApp/ViewModels/LoginViewModel.cs +++ b/ClientApp/ViewModels/LoginViewModel.cs @@ -20,14 +20,14 @@ namespace ClientApp.ViewModels public bool InvertedLoginStatus { get; set; } - private MainWindowViewModel mainWindowViewModel; + private MainWindowViewModel MainWindowViewModel; public LoginViewModel(MainWindowViewModel mainWindowViewModel) { - this.mainWindowViewModel = mainWindowViewModel; + this.MainWindowViewModel = mainWindowViewModel; LoginCommand = new RelayCommand((parameter) => { //TODO send username and password to server - this.mainWindowViewModel.client.tryLogin(Username, ((PasswordBox)parameter).Password); + this.MainWindowViewModel.client.tryLogin(Username, ((PasswordBox)parameter).Password); }); } @@ -35,11 +35,11 @@ namespace ClientApp.ViewModels internal void setLoginStatus(bool status) { - this.mainWindowViewModel.InfoModel.ConnectedToServer = status; + this.MainWindowViewModel.InfoModel.ConnectedToServer = status; this.InvertedLoginStatus = !status; if (status) { - this.mainWindowViewModel.SelectedViewModel = new MainViewModel(mainWindowViewModel); + this.MainWindowViewModel.SelectedViewModel = new MainViewModel(MainWindowViewModel); } } } diff --git a/ClientApp/ViewModels/MainWindowViewModel.cs b/ClientApp/ViewModels/MainWindowViewModel.cs index 2bfe0fd..cdb15a7 100644 --- a/ClientApp/ViewModels/MainWindowViewModel.cs +++ b/ClientApp/ViewModels/MainWindowViewModel.cs @@ -1,37 +1,137 @@ -using ClientApp.Models; +using ClientApp.MagicCode; +using ClientApp.Models; using ClientApp.Utils; +using GalaSoft.MvvmLight.Command; using System; using System.Collections.Generic; -using System.ComponentModel; +using System.Diagnostics; using System.Text; using Util; +using System.Windows; +using System.Windows.Input; namespace ClientApp.ViewModels { class MainWindowViewModel : ObservableObject { + #region private members + + private Window mWindow; + + private int mOuterMarginSize = 10; + private int mWindowRadius = 10; + + #endregion + + #region commands + + public ICommand MinimizeCommand { get; set; } + + public ICommand MaximizeCommand { get; set; } + + public ICommand CloseCommand { get; set; } + + public ICommand MenuCommand { get; set; } + + #endregion + + #region public properties public Info InfoModel { get; set; } public ObservableObject SelectedViewModel { get; set; } + public Client client { get; } - LoginViewModel loginViewModel; + /// + /// size of the resize border around the window + /// - public MainWindowViewModel(Client client) + public double MinimumWidth { get; set; } = 250; + + public double MinimumHeight { get; set; } = 250; + + + + public int ResizeBorder { get; set; } = 6; + + public Thickness ResizeBorderThickness { get { return new Thickness(ResizeBorder + OuterMarginSize); } } + + public Thickness InnerContentPadding { get { return new Thickness(ResizeBorder); } } + + + public Thickness OuterMarginThickness { get { return new Thickness(OuterMarginSize); } } + + public CornerRadius WindowCornerRadius { get { return new CornerRadius(WindowRadius); } } + + public int OuterMarginSize { + get + { + return mWindow.WindowState == WindowState.Maximized ? 0 : mOuterMarginSize; + } + set + { + mOuterMarginSize = value; + } + } + + public int WindowRadius + { + get + { + return mWindow.WindowState == WindowState.Maximized ? 0 : mWindowRadius; + } + set + { + mWindowRadius = value; + } + } + + public int TitleHeight { get; set; } = 42; + + public GridLength TitleHeightGridLength { get { return new GridLength(TitleHeight + ResizeBorder); } } + + #endregion + + public MainWindowViewModel(Window window, Client client) + { + this.mWindow = window; + + this.mWindow.StateChanged += (sender, e) => + { + OnPropertyChanged(nameof(ResizeBorderThickness)); + OnPropertyChanged(nameof(OuterMarginThickness)); + OnPropertyChanged(nameof(WindowCornerRadius)); + OnPropertyChanged(nameof(OuterMarginSize)); + OnPropertyChanged(nameof(WindowRadius)); + }; + this.InfoModel = new Info(); this.client = client; loginViewModel = new LoginViewModel(this); SelectedViewModel = loginViewModel; this.client.SetLoginViewModel(loginViewModel); - App.Current.MainWindow.Closing += new CancelEventHandler(MainWindow_Closing); + + this.MinimizeCommand = new RelayCommand(() => this.mWindow.WindowState = WindowState.Minimized); + this.MaximizeCommand = new RelayCommand(() => this.mWindow.WindowState ^= WindowState.Maximized); + this.CloseCommand = new RelayCommand(() => this.mWindow.Close()); + this.MenuCommand = new RelayCommand(() => SystemCommands.ShowSystemMenu(this.mWindow, GetMousePosition())); + + var resizer = new WindowResizer(this.mWindow); + + this.mWindow.Closed += (sender, e) => this.client.Dispose(); } - void MainWindow_Closing(object sender, CancelEventArgs e) + + #region helper + + private Point GetMousePosition() { - this.client.sendMessage(DataParser.getDisconnectJson(loginViewModel.Username)); - + Debug.WriteLine("getmousePosition called"); + var p = Mouse.GetPosition(this.mWindow); + return new Point(p.X + this.mWindow.Left, p.Y + this.mWindow.Top); } + #endregion } } diff --git a/ClientApp/Views/LoginView.xaml b/ClientApp/Views/LoginView.xaml index 472674d..9266c85 100644 --- a/ClientApp/Views/LoginView.xaml +++ b/ClientApp/Views/LoginView.xaml @@ -6,11 +6,12 @@ xmlns:local="clr-namespace:ClientApp.Views" xmlns:viewModels="clr-namespace:ClientApp.ViewModels" mc:Ignorable="d" + ShowsNavigationUI="False" d:DesignHeight="450" d:DesignWidth="800"> - - + + + +