diff --git a/.gitignore b/.gitignore index 940794e..36c5bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,64 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ +# Do not use Notepad to edit this. Use VS, vim, Notepad++, etc. Uses UNIX line endings. + +*.cache +*.nuget.props +*.nuget.targets +*.user +*.suo +*.sdf +*.opensdf +*.nupkg +*.opendb +*.VC.db + +project.lock.json +[Oo]bj/ +[Bb]in/ +Debug/ +Release/ +Testing/ +x86/ +x64/ +ARM/ +ipch/ +AppPackages/ +.vs/ +/Loc/Generated/ +LocalizeApp.log +Generated Files/ +/src/packages/ + + +#Localized strings +/src/SoundRecorder/Resources/* +!/src/SoundRecorder/Resources/en-us +/src/SoundRecorder/BundleArtifacts/* +!lib/**/* + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Visual Studio Files +.vs/ + + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -12,17 +69,20 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Debug-only configuration settings +.conf + # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ # Visual Studio 2015 cache/options directory .vs/ @@ -42,11 +102,9 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json -project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -85,7 +143,6 @@ ipch/ *.sdf *.cachefile *.VC.db -*.VC.VC.opendb # Visual Studio profiler *.psess @@ -113,10 +170,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -148,15 +201,12 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ +# TODO: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj # NuGet Packages *.nupkg @@ -166,7 +216,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -178,11 +228,12 @@ csx/ ecf/ rcf/ -# Windows Store app package directories and files +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory AppPackages/ BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored @@ -192,19 +243,16 @@ _pkginfo.txt # Others ClientBin/ +[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - # RIA/Silverlight projects Generated_Code/ @@ -219,7 +267,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +281,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +288,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -256,33 +296,15 @@ typings/ **/*.Server/ModelManifest.xml _Pvt_Extensions +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + # Paket dependency manager .paket/paket.exe -paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +*.StyleCop +/API_Keys diff --git a/Installation.md b/Installation.md new file mode 100644 index 0000000..0f47fde --- /dev/null +++ b/Installation.md @@ -0,0 +1,33 @@ +# Installation + +## Requirements + +* Windows 10 +* .Net Framework +* WPF libraries + +## Installation process + +You can fork the project in the github repository of [SnipAI](https://github.com). + +Then navigate to the code location on your Windows 10 machine and double click the solution file `SnipInsight.sln`. + +From there, select the C# project file `SnipInsight.csproj`. You should now be able to build and run the solution. + +The next step to test the AI features is to generate API keys for each AI service. Read our [guide](https://github.com) to learn how to generate API keys and then paste them in the settings panel. + +Congratulations! You should now have a fully working application to get started. Have fun testing the project and thank you for your contribution! + +## Troubleshooting + +> No project is selected for compilation + +Make sure you have selected the Debug configuration and are building the project correctly. + +> I can't run the project + +You are most likely missing libraries. Right click on the SnipInsight project and select *Install missing packages* if that option is available. + +> I don't see the fields to paste my API keys on the settings panel + +That is because you have not yet enabled AI assistance. Please enable it first and then enter your API keys. \ No newline at end of file diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..26eee3c --- /dev/null +++ b/NuGet.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 72f1506..014e816 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ +#Introduction +TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project. -# Contributing +#Getting Started +TODO: Guide users through getting your code up and running on their own system. In this section you can talk about: +1. Installation process +2. Software dependencies +3. Latest releases +4. API references -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. +#Build and Test +TODO: Describe and show how to build your code and run the tests. -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +#Contribute +TODO: Explain how other users and developers can contribute to make your code better. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +If you want to learn more about creating good readme files then refer the following [guidelines](https://www.visualstudio.com/en-us/docs/git/create-a-readme). You can also seek inspiration from the below readme files: +- [ASP.NET Core](https://github.com/aspnet/Home) +- [Visual Studio Code](https://github.com/Microsoft/vscode) +- [Chakra Core](https://github.com/Microsoft/ChakraCore) \ No newline at end of file diff --git a/SnipInsight/AIServices/AIComponents/AISideNavigation.xaml b/SnipInsight/AIServices/AIComponents/AISideNavigation.xaml new file mode 100644 index 0000000..a56e7c9 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/AISideNavigation.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/AISideNavigation.xaml.cs b/SnipInsight/AIServices/AIComponents/AISideNavigation.xaml.cs new file mode 100644 index 0000000..976590b --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/AISideNavigation.xaml.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; +using CommonServiceLocator; +using SnipInsight.ViewModels; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for AISideNavigation.xaml + /// + public partial class AISideNavigation : UserControl + { + public AISideNavigation() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/CelebrityRecognitionControl.xaml b/SnipInsight/AIServices/AIComponents/CelebrityRecognitionControl.xaml new file mode 100644 index 0000000..f7e792e --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/CelebrityRecognitionControl.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/AIServices/AIComponents/CelebrityRecognitionControl.xaml.cs b/SnipInsight/AIServices/AIComponents/CelebrityRecognitionControl.xaml.cs new file mode 100644 index 0000000..973230e --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/CelebrityRecognitionControl.xaml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; +using SnipInsight.AIServices.AIViewModels; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for CelebrityRecognitionControl.xaml + /// + public partial class CelebrityRecognitionControl : UserControl + { + public CelebrityRecognitionControl() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/AIComponents/EmptyState.xaml b/SnipInsight/AIServices/AIComponents/EmptyState.xaml new file mode 100644 index 0000000..2472d62 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/EmptyState.xaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/EmptyState.xaml.cs b/SnipInsight/AIServices/AIComponents/EmptyState.xaml.cs new file mode 100644 index 0000000..a5bdae6 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/EmptyState.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for EmpyState.xaml + /// + public partial class EmptyState : UserControl + { + public EmptyState() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/ImageSearchControl.xaml b/SnipInsight/AIServices/AIComponents/ImageSearchControl.xaml new file mode 100644 index 0000000..f796739 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/ImageSearchControl.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/AIServices/AIComponents/ImageSearchControl.xaml.cs b/SnipInsight/AIServices/AIComponents/ImageSearchControl.xaml.cs new file mode 100644 index 0000000..48bda29 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/ImageSearchControl.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; +using CommonServiceLocator; +using SnipInsight.AIServices.AIViewModels; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for ImageSearch.xaml + /// + public partial class ImageSearchControl : UserControl + { + /// + /// Reference to image search vm + /// + private ImageSearchViewModel ImageSearchVM = ServiceLocator.Current.GetInstance(); + + /// + /// View for the Image Search + /// + public ImageSearchControl() + { + InitializeComponent(); + } + + /// + /// Passes the width of the panel to the bounded VM + /// + private void ImageGallerySizeChanged(object sender, SizeChangedEventArgs e) + { + ImageSearchVM.CurrentWrapPanelWidth = ImageDisplayPanel.ActualWidth; + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/InsightsPermissions.xaml b/SnipInsight/AIServices/AIComponents/InsightsPermissions.xaml new file mode 100644 index 0000000..2dfe745 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/InsightsPermissions.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + About Cognitive Services + + + + + + + Privacy Policy + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/InsightsPermissions.xaml.cs b/SnipInsight/AIServices/AIComponents/InsightsPermissions.xaml.cs new file mode 100644 index 0000000..5cbaa25 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/InsightsPermissions.xaml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; +using SnipInsight.AIServices.AIViewModels; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for InsightsPermissions.xaml + /// + public partial class InsightsPermissions : UserControl + { + public InsightsPermissions() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/LandmarkRecognitionControl.xaml b/SnipInsight/AIServices/AIComponents/LandmarkRecognitionControl.xaml new file mode 100644 index 0000000..e7e74cf --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/LandmarkRecognitionControl.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/LandmarkRecognitionControl.xaml.cs b/SnipInsight/AIServices/AIComponents/LandmarkRecognitionControl.xaml.cs new file mode 100644 index 0000000..ef4d7d7 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/LandmarkRecognitionControl.xaml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; +using SnipInsight.AIServices.AIViewModels; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for LandmarkRecognitionControl.xaml + /// + public partial class LandmarkRecognitionControl : UserControl + { + public LandmarkRecognitionControl() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/LoadingAnimation.xaml b/SnipInsight/AIServices/AIComponents/LoadingAnimation.xaml new file mode 100644 index 0000000..e6d7077 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/LoadingAnimation.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/LoadingAnimation.xaml.cs b/SnipInsight/AIServices/AIComponents/LoadingAnimation.xaml.cs new file mode 100644 index 0000000..9e44126 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/LoadingAnimation.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for LoadingAnimation.xaml + /// + public partial class LoadingAnimation : UserControl + { + public LoadingAnimation() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/NewsControls.xaml b/SnipInsight/AIServices/AIComponents/NewsControls.xaml new file mode 100644 index 0000000..ec94fcd --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/NewsControls.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/NewsControls.xaml.cs b/SnipInsight/AIServices/AIComponents/NewsControls.xaml.cs new file mode 100644 index 0000000..bde5d01 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/NewsControls.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for NewsControls.xaml + /// + public partial class NewsControls : UserControl + { + public NewsControls() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/AIServices/AIComponents/OCRControl.xaml b/SnipInsight/AIServices/AIComponents/OCRControl.xaml new file mode 100644 index 0000000..4aa50f8 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/OCRControl.xaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/AIServices/AIComponents/OCRControl.xaml.cs b/SnipInsight/AIServices/AIComponents/OCRControl.xaml.cs new file mode 100644 index 0000000..f021f55 --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/OCRControl.xaml.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIViewModels; +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for OCRControl.xaml + /// + public partial class OCRControl : UserControl + { + public OCRControl() + { + InitializeComponent(); + } + + } + + /// + /// Converter class to bind visibility to a list of boolean parameters + /// + public class MultiBooleanToVisibility : IMultiValueConverter + { + /// + /// Converts enabled property of navigation buttons to clip visibility. + /// + /// Current status of nagivation buttons + /// + /// + /// + /// Visible if at least one button is disabled, otherwise hidden + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (object oj in values) + { + if ((Visibility)oj == Visibility.Visible) + { + return Visibility.Visible; + } + } + + return Visibility.Collapsed; + } + + /// + /// Not Implemented, does not need to be implementeds but required to be overriden as part of converter interface + /// + /// + /// + /// + /// + /// + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/AIComponents/ProductSearchControl.xaml b/SnipInsight/AIServices/AIComponents/ProductSearchControl.xaml new file mode 100644 index 0000000..8fb2bbd --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/ProductSearchControl.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/AIServices/AIComponents/ProductSearchControl.xaml.cs b/SnipInsight/AIServices/AIComponents/ProductSearchControl.xaml.cs new file mode 100644 index 0000000..3875efe --- /dev/null +++ b/SnipInsight/AIServices/AIComponents/ProductSearchControl.xaml.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using SnipInsight.AIServices.AIViewModels; +using System.Windows.Controls; + +namespace SnipInsight.AIServices.AIComponents +{ + /// + /// Interaction logic for ProductSearchControl.xaml + /// + public partial class ProductSearchControl : UserControl + { + /// + /// Reference to product search vm + /// + private ProductSearchViewModel ProductSearchVM = ServiceLocator.Current.GetInstance(); + + public ProductSearchControl() + { + InitializeComponent(); + } + + /// + /// Passes the width of the panel to the bounded VM + /// + private void ProductGallerySizeChanged(object sender, System.Windows.SizeChangedEventArgs e) + { + ProductSearchVM.CurrentWrapPanelWidth = ProductDisplayPanel.ActualWidth; + } + } +} diff --git a/SnipInsight/AIServices/AILogic/ContentModerationHandler.cs b/SnipInsight/AIServices/AILogic/ContentModerationHandler.cs new file mode 100644 index 0000000..998b86d --- /dev/null +++ b/SnipInsight/AIServices/AILogic/ContentModerationHandler.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using SnipInsight.Properties; +using SnipInsight.Util; +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// Class to make API request image content moderator cognitive service + /// Parse API response and find the moderation result + /// + /// + internal class ContentModerationHandler + { + private HttpClient contentModerationClient; + private Uri URI { get; set; } + private String key; + + /// + /// Number of total retries in case of request failure + /// + private const int RetryCount = 6; + + /// + /// Delay in ms between each retry + /// + private const int RetryDelay = 500; + + /// + /// Constructor to initialize API key and client for http request + /// + /// API Key + /// HTTP client for making the call + public ContentModerationHandler(string keyFile, HttpClient client=null) + { + RetrieveKey(keyFile); + contentModerationClient = client ?? new HttpClient(); + contentModerationClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", key); + BuildURI(); + } + + /// + /// Returns the result of the API call + /// + /// Captured image used for the call + /// object of httpclient + /// Returns true if content was recognized as inappropriate; false otherwise + public bool GetResult(MemoryStream stream) + { + try + { + var result = Run(stream); + return ExtractResult(result.Content.ReadAsStringAsync().Result); + } + catch (Exception e) + { + Debug.WriteLine(e.Message); + return false; + } + } + + /// + /// Build the URI for the API request + /// + private void BuildURI() + { + URI = new UriBuilder + { + Scheme = "https", + Host = "westus.api.cognitive.microsoft.com", + Path = "contentmoderator/moderate/v1.0/ProcessImage/Evaluate", + Query = "CacheImage=true" + }.Uri; + } + + /// + /// Run the call and get the response message and records telemetry event with time to complete api call and status code + /// + /// Captured image used for the call + /// The HttpResponseMessage containing the Json result + private HttpResponseMessage Run(MemoryStream stream) + { + using (var content = new StreamContent(stream)) + { + HttpResponseMessage result = null; + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + try + { + // Execute the REST API call. + result = RequestAndRetry(() => contentModerationClient.PostAsync(URI, content).Result); + result.EnsureSuccessStatusCode(); + } + finally + { + stopwatch.Stop(); + string responseStatusCode = Telemetry.PropertyValue.NoResponse; + if (result != null) + { + responseStatusCode = result.StatusCode.ToString(); + } + Telemetry.ApplicationLogger.Instance.SubmitApiCallEvent(Telemetry.EventName.CompleteApiCall,Telemetry.EventName.ContentModerationApi, stopwatch.ElapsedMilliseconds, responseStatusCode); + } + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + + return result; + } + } + + /// + /// Run the function and retry if it fails + /// + /// Action to retry in case of failure + /// The response message containing the result + private HttpResponseMessage RequestAndRetry(Func action) + { + int retriesLeft = RetryCount; + int delay = RetryDelay; + HttpResponseMessage response = null; + + while (retriesLeft > 0) + { + response = action(); + if ((int)response.StatusCode != 429) + break; + + Task.Delay(delay); + retriesLeft--; + delay *= 2; + } + + return response; + } + + /// + /// Converts the string response to deserialized object and get data from it + /// + /// Json from the API call + /// Returns true if content was recognized as inappropriate; false otherwise + private bool ExtractResult(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return false; + } + + using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json))) + { + DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(ContentModerationModel)); + ContentModerationModel model = (ContentModerationModel)ser.ReadObject(ms); + return ParseModel(model); + } + } + + /// + /// Gets the image strength from deserialized json response + /// + /// Deserialized ContentModerationModel object of API result + /// Returns true if content was recognized as inappropriate; false otherwise + private bool ParseModel(ContentModerationModel model) + { + if (model.AdultClassificationScore > (1 - (double)UserSettings.ContentModerationStrength / 100)) + return true; + if (model.RacyClassificationScore > (1 - (double)UserSettings.ContentModerationStrength / 100)) + return true; + return false; + } + + private void RetrieveKey(string keyFile) + { + key = UserSettings.GetKey(keyFile); + + if (!string.IsNullOrWhiteSpace(key)) + { + Debug.WriteLine(keyFile + Resources.API_Key_Not_Found); + } + } + } +} diff --git a/SnipInsight/AIServices/AILogic/EntitySearchHandler.cs b/SnipInsight/AIServices/AILogic/EntitySearchHandler.cs new file mode 100644 index 0000000..8e400ec --- /dev/null +++ b/SnipInsight/AIServices/AILogic/EntitySearchHandler.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// Backend logic to access API endpoint for Bing Entity Search service + /// + class EntitySearchHandler : CloudService + { + /// + /// Constructor that get implemented based on CloudService + /// when passing in the api key and http client + /// + /// API key for Bing Entity Search + public EntitySearchHandler(string keyFile): base(keyFile) + { + Host = "api.cognitive.microsoft.com"; + Endpoint = "bing/v7.0/entities"; + } + + /// + /// Returns the result of the API call + /// + /// Captured image used for the call + /// object of httpclient to be used for the request + /// Data extracted from successful API response, default in case of failure + public async Task GetResult(string entityName) + { + try + { + var result = await Run(entityName); + return ExtractResult(await result.Content.ReadAsStringAsync()); + } + catch (Exception e) + { + Debug.WriteLine(e.Message + URI); + return default(EntitySearchModel); + } + } + + /// + /// Run the API call and get the response message and records telemetry event with time to complete api call and status code + /// + /// Name of entity to be used for the call + /// The HttpResponseMessage containing the Json result + protected async Task Run(string entityName) + { + // Construct the uri to perform Bing Entity Search + RequestParams = "mkt=en-us&q=" + System.Net.WebUtility.UrlEncode(entityName); + BuildURI(); + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + HttpResponseMessage response = null; + try + { + // Execute the REST API GET call asynchronously + response = await RequestAndRetry(() => CloudServiceClient.GetAsync(URI)); + response.EnsureSuccessStatusCode(); + } + finally + { + stopwatch.Stop(); + string responseStatusCode = Telemetry.PropertyValue.NoResponse; + if (response != null) + { + responseStatusCode = response.StatusCode.ToString(); + } + Telemetry.ApplicationLogger.Instance.SubmitApiCallEvent(Telemetry.EventName.CompleteApiCall,Telemetry.EventName.EntitySearchApi, stopwatch.ElapsedMilliseconds, responseStatusCode); + } + + return response; + } + } +} diff --git a/SnipInsight/AIServices/AILogic/HandWrittenTextHandler.cs b/SnipInsight/AIServices/AILogic/HandWrittenTextHandler.cs new file mode 100644 index 0000000..eabda1b --- /dev/null +++ b/SnipInsight/AIServices/AILogic/HandWrittenTextHandler.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// Handwritten OCR call + /// + class HandWrittenTextHandler : CloudService + { + /// + /// Initalizes handler with correct endpoint + /// + public HandWrittenTextHandler(string keyFile): base(keyFile) + { + Host = "westcentralus.api.cognitive.microsoft.com"; + Endpoint = "vision/v1.0/recognizeText"; + RequestParams = "handwriting=true"; + } + + /// Run the stream asynchronously, return the HttpResonseMessage and records telemetry event with time to complete api call and status code + /// + /// Captured Image + /// ResponseMessage of the API request/call + protected override async Task Run(MemoryStream stream) + { + var result = await base.Run(stream); + + if (!result.IsSuccessStatusCode) + { + return null; + } + + string operationLocation = result.Headers.GetValues("Operation-Location").FirstOrDefault(); + HttpResponseMessage response = await RequestAndRetry(() => CloudServiceClient.GetAsync(operationLocation)); + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + try + { + response.EnsureSuccessStatusCode(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + // Pass the exception to the next level + throw e; + } + finally{ + stopwatch.Stop(); + string responseStatusCode = Telemetry.PropertyValue.NoResponse; + if (result != null) + { + responseStatusCode = result.StatusCode.ToString(); + } + Telemetry.ApplicationLogger.Instance.SubmitApiCallEvent(Telemetry.EventName.CompleteApiCall, Telemetry.EventName.HandWrittenTextApi, stopwatch.ElapsedMilliseconds, responseStatusCode); + } + + return response; + } + } +} diff --git a/SnipInsight/AIServices/AILogic/ImageAnalysisHandler.cs b/SnipInsight/AIServices/AILogic/ImageAnalysisHandler.cs new file mode 100644 index 0000000..289c74d --- /dev/null +++ b/SnipInsight/AIServices/AILogic/ImageAnalysisHandler.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// API logic for the Image Search service + /// + class ImageAnalysisHandler : CloudService + { + /// + /// Constructor to initialize API and client + /// + /// string of API key + /// Instance of HttpClient to be used for the API call + internal ImageAnalysisHandler(string key): base(key) + { + Host = "westus.api.cognitive.microsoft.com"; + Endpoint = "/vision/v1.0/analyze"; + RequestParams = "visualFeatures=Tags,Description&language=en&details=Celebrities,Landmarks"; + + BuildURI(); + } + + } +} diff --git a/SnipInsight/AIServices/AILogic/ImageSearchHandler.cs b/SnipInsight/AIServices/AILogic/ImageSearchHandler.cs new file mode 100644 index 0000000..3e8ee69 --- /dev/null +++ b/SnipInsight/AIServices/AILogic/ImageSearchHandler.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// API logic for the Image Search service + /// + class ImageSearchHandler : CloudService + { + /// + /// Expected number of results for highest confidence on response + /// + private const double ConfidenceThreshold = 10.0; + + /// + /// Normalize confidence of result to avoid dominance in suggested search + /// + private const double ConfidenceOffset = 0.2; + + /// + /// Constructor to initialize API and client + /// + /// string of API key + /// Instance of HttpClient to be used for the API call + public ImageSearchHandler(string keyFile): base(keyFile) + + { + Host = "api.cognitive.microsoft.com"; + Endpoint = "/bing/v7.0/images/details"; + RequestParams = "modules=SimilarImages"; + + BuildURI(); + } + + /// + /// Make the API call with supplied request contents and records telemetry event with time to complete api call and status code + /// + /// MemoryStream to read the image byte array + /// String response from the API call + protected override async Task Run(MemoryStream stream) + { + var strContent = new StreamContent(stream); + strContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { FileName = "AnyNameWorks" }; + + var content = new MultipartFormDataContent(); + content.Add(strContent); + + HttpResponseMessage result = null; + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + try + { + // Execute the REST API call. + result = await RequestAndRetry(() => CloudServiceClient.PostAsync(URI, content)); + result.EnsureSuccessStatusCode(); + } + finally + { + stopwatch.Stop(); + string responseStatusCode = Telemetry.PropertyValue.NoResponse; + if (result != null) + { + responseStatusCode = result.StatusCode.ToString(); + } + Telemetry.ApplicationLogger.Instance.SubmitApiCallEvent(Telemetry.EventName.CompleteApiCall, Telemetry.EventName.CelebrityRecognitionApi, stopwatch.ElapsedMilliseconds, responseStatusCode); + } + + return result; + } + + } +} diff --git a/SnipInsight/AIServices/AILogic/LUISInsights.cs b/SnipInsight/AIServices/AILogic/LUISInsights.cs new file mode 100644 index 0000000..4bf230c --- /dev/null +++ b/SnipInsight/AIServices/AILogic/LUISInsights.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using SnipInsight.Util; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web; + +namespace SnipInsight.AIServices.AILogic +{ + public class LUISInsights : CloudService + { + /// + /// Backend logic to access LUIS API + /// + /// + public LUISInsights(string keyFile) : base(keyFile) + { + Host = "westus.api.cognitive.microsoft.com"; + Endpoint = "luis/v2.0/apps/" + UserSettings.GetKey("LUISAppId"); + } + + /// + /// Returns the result of the API call + /// + /// OCR text string used for the call + /// Data extracted from successful API response, default in case of failure + public async Task GetResult(string ocrResult) + { + try + { + var result = await Run(ocrResult); + return ExtractResult(await result.Content.ReadAsStringAsync()); + } + catch (WebException e) + { + Debug.WriteLine(e.Message + URI); + return default(LUISModel); + } + } + + /// + /// Run the API call and get the response message and records telemetry event with time to complete api call and status code + /// + /// OCR results to be used for the call + /// The HttpResponseMessage containing the Json result + protected async Task Run(string ocrResult) + { + var queryString = HttpUtility.ParseQueryString(string.Empty); + queryString["q"] = ocrResult; + // These optional request parameters are set to their default values + queryString["timezoneOffset"] = "0"; + queryString["verbose"] = "false"; + queryString["spellCheck"] = "false"; + queryString["staging"] = "false"; + + RequestParams = queryString.ToString(); + BuildURI(); + + HttpResponseMessage response = await RequestAndRetry(() => CloudServiceClient.GetAsync(URI)); + response.EnsureSuccessStatusCode(); + + return response; + } + } +} diff --git a/SnipInsight/AIServices/AILogic/NewsHandler.cs b/SnipInsight/AIServices/AILogic/NewsHandler.cs new file mode 100644 index 0000000..ea9fd74 --- /dev/null +++ b/SnipInsight/AIServices/AILogic/NewsHandler.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices.AILogic +{ + class NewsHandler: CloudService + { + /// + /// Constructor that get implemented based on CloudService + /// when passing in the api key and http client + /// + /// API key for Bing News Search + public NewsHandler(string keyFile): base(keyFile) + { + Host = "api.cognitive.microsoft.com"; + Endpoint = "bing/v7.0/news/search"; + } + + /// + /// Returns the result of the API call + /// + /// Captured name used for the call + /// Data extracted from successful API response, default in case of failure + public async Task GetResult(string entityName) + { + try + { + var result = await Run(entityName); + return ExtractResult(await result.Content.ReadAsStringAsync()); + } + catch (WebException e) + { + Debug.WriteLine(e.Message + URI); + return default(RawNewsModel); + } + } + + /// + /// Run the API call and get the response message and records telemetry event with time to complete api call and status code + /// + /// Name of entity to be used for the call + /// The HttpResponseMessage containing the Json result + protected async Task Run(string entityName) + { + // Construct the uri to perform Bing Entity Search + RequestParams = "q=" + System.Net.WebUtility.UrlEncode(entityName); + BuildURI(); + + HttpResponseMessage response = null; + // Execute the REST API GET call asynchronously and + // await for non-blocking API call to Bing Entity Search + response = await RequestAndRetry(() => CloudServiceClient.GetAsync(URI)); + response.EnsureSuccessStatusCode(); + + return response; + } + } +} diff --git a/SnipInsight/AIServices/AILogic/PrintedTextHandler.cs b/SnipInsight/AIServices/AILogic/PrintedTextHandler.cs new file mode 100644 index 0000000..576982d --- /dev/null +++ b/SnipInsight/AIServices/AILogic/PrintedTextHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +namespace SnipInsight.AIServices.AILogic +{ + /// + /// Printed Text OCR call + /// + class PrintedTextHandler : CloudService + { + /// + /// Initalizes handler with correct endpoint + /// + public PrintedTextHandler(string keyFile) : base(keyFile) + { + Host = "westcentralus.api.cognitive.microsoft.com"; + Endpoint = "vision/v1.0/ocr"; + RequestParams = "language=unk&detectOrientation=true"; + } + } +} diff --git a/SnipInsight/AIServices/AILogic/ProductSearchHandler.cs b/SnipInsight/AIServices/AILogic/ProductSearchHandler.cs new file mode 100644 index 0000000..804576c --- /dev/null +++ b/SnipInsight/AIServices/AILogic/ProductSearchHandler.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.AIServices.AIModels; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// Handler for the product search API + /// Takes a json API model in param and returns a list of products + /// + class ProductSearchHandler : CloudService + { + /// + /// Expected number of results for highest confidence on response + /// + private const double ConfidenceThreshold = 10.0; + + /// + /// Normalize confidence of result to avoid dominance in suggested search + /// + private const double ConfidenceOffset = 0.1; + + /// + /// Initalizes handler with correct endpoint + /// + public ProductSearchHandler(string keyFile) : base(keyFile) + { + Host = "api.cognitive.microsoft.com"; + Endpoint = "/bing/v7.0/images/details"; + RequestParams = "modules=SimilarProducts"; + + BuildURI(); + } + + /// + /// Run the HTTP call to get a list of similar products + /// + /// Captured Image + /// The HTTP response for the call + protected override async Task Run(MemoryStream stream) + { + var strContent = new StreamContent(stream); + strContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { FileName = "AnyNameWorks" }; + + var content = new MultipartFormDataContent(); + content.Add(strContent); + + // Execute the REST API call. + var result = await RequestAndRetry(() => CloudServiceClient.PostAsync(URI, content)); + result.EnsureSuccessStatusCode(); + return result; + } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/AILogic/TranslationHandler.cs b/SnipInsight/AIServices/AILogic/TranslationHandler.cs new file mode 100644 index 0000000..7394d55 --- /dev/null +++ b/SnipInsight/AIServices/AILogic/TranslationHandler.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Xml; + +namespace SnipInsight.AIServices.AILogic +{ + /// + /// Handle the translation service + /// + public class TranslationHandler: CloudService + { + /// + /// Language codes for the translation + /// + private static string[] languageCodes; + private string detectedLanguage = string.Empty; + + public TranslationHandler(string keyFile): base(keyFile) + { + Host = "api.microsofttranslator.com"; + + LanguageCodesAndTitles = new SortedDictionary( + Comparer.Create((a, b) => string.Compare(a, b, true)) + ); + + Task.Run(async () => + { + try + { + await GetLanguagesForTranslate(); + await GetLanguageNames(); + + TranslatorEnable = true; + } + catch (WebException e) + { + Diagnostics.LogException(e); + + TranslatorEnable = false; + } + }); + } + + /// + /// Enable translation if languages loaded + /// + public bool TranslatorEnable { get; set; } + + /// + /// Maps the language codes to their full name + /// + public SortedDictionary LanguageCodesAndTitles { get; set; } + + /// + /// Translate the text and return the translated text and records telemetry + /// + /// Text to translate + /// Translated text + public async Task GetResult(string textToTranslate, string fromLanguage, string toLanguage) + { + //TODO: After Refactoring Log results status and api run time + Telemetry.ApplicationLogger.Instance.SubmitApiCallEvent(Telemetry.EventName.CompleteApiCall, Telemetry.EventName.TranslationApi, -1, "N/A"); + + Endpoint = "/v2/Http.svc/Translate"; + RequestParams = string.Format("text={0}&from={1}&to={2}", + HttpUtility.UrlEncode(textToTranslate), + fromLanguage, + toLanguage); + + BuildURI(); + + var webRequest = WebRequest.Create(URI); + webRequest.Headers.Add("Ocp-Apim-Subscription-Key", Key); + + try + { + WebResponse response = webRequest.GetResponse(); + + using (StreamReader translatedStream = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("utf-8"))) + { + String stringText = translatedStream.ReadToEnd(); + int startPos = stringText.IndexOf(">")+1; + + return stringText.Substring(startPos, stringText.IndexOf("<", startPos) - startPos); + } + } + catch (Exception e) when (e is WebException || e is XmlException ) + { + Diagnostics.LogException(e); + + return textToTranslate; + } + } + + /// + /// Get the languages for the translation + /// + private async Task GetLanguagesForTranslate() + { + Endpoint = "/v2/Http.svc/GetLanguagesForTranslate"; + RequestParams = "scope=text"; + BuildURI(); + + WebRequest request = WebRequest.Create(URI); + request.Headers.Add("Ocp-Apim-Subscription-Key", this.Key); + + WebResponse response = request.GetResponse(); + + using (Stream stream = response.GetResponseStream()) + { + DataContractSerializer serializer = new DataContractSerializer(typeof(string[])); + languageCodes = (string[])serializer.ReadObject(stream); + } + } + + /// + /// Populate the array of string with a list of language codes + /// + private async Task GetLanguageNames() + { + Endpoint = "/v2/Http.svc/GetLanguageNames"; + RequestParams = "locale=en"; + BuildURI(); + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(URI); + request.Headers.Add("Ocp-Apim-Subscription-Key", Key); + request.ContentType = "text/xml"; + request.Method = "POST"; + + DataContractSerializer serializer = new DataContractSerializer(typeof(string[])); + using (Stream stream = request.GetRequestStream()) + { + serializer.WriteObject(stream, languageCodes); + } + + // Read and parse the XML response + var response = request.GetResponse(); + + string[] languageNames; + using (Stream stream = response.GetResponseStream()) + { + languageNames = (string[])serializer.ReadObject(stream); + } + + // Load the dictionary for the combo box + for (int i = 0; i < languageNames.Length; i++) + { + //Sorted by the language name for diaplay + LanguageCodesAndTitles.Add(languageNames[i], languageCodes[i]); + } + } + } +} diff --git a/SnipInsight/AIServices/AIManager.cs b/SnipInsight/AIServices/AIManager.cs new file mode 100644 index 0000000..6118c12 --- /dev/null +++ b/SnipInsight/AIServices/AIManager.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using SnipInsight.AIServices.AIViewModels; +using SnipInsight.ViewModels; +using System; +using System.IO; +using System.Threading.Tasks; +using System.Windows; + +namespace SnipInsight.AIServices +{ + /// + /// Manage the API calls asynchronously + /// Controls what gets displayed into the panel + /// Secure the keys and feed the data to the handlers + /// + class AIManager + { + public Byte[] ImageBytes { get; set; } + + /// + /// Run all the Azure API calls asynchronously. + /// + internal void RunAllAsyncCalls() + { + var AIViewModel = ServiceLocator.Current.GetInstance(); + + AIViewModel.AIControlsVisibility = Visibility.Collapsed; + AIViewModel.EmptyStateVisibility = Visibility.Collapsed; + AIViewModel.LoadingVisibility = Visibility.Visible; + + AppManager.TheBoss.ViewModel.CelebritiesCanvas = null; + + // If we decide to go back to the previously used panel after each snip + // Then removing this line would allow it + AIViewModel.SuggestedCommand.Execute(null); + + MemoryStream imageSearchStream = new MemoryStream(ImageBytes); + var imageSearchVM = ServiceLocator.Current.GetInstance(); + var imageSearchTask = imageSearchVM.LoadImages(imageSearchStream); + + MemoryStream productSearchStream = new MemoryStream(ImageBytes); + var productSearchVM = ServiceLocator.Current.GetInstance(); + var productSearchTask = productSearchVM.LoadProducts(productSearchStream); + + MemoryStream imageAnalysisStream = new MemoryStream(ImageBytes); + var imageAnalysisVM = ServiceLocator.Current.GetInstance(); + var imageAnalysisTask = imageAnalysisVM.LoadAnalysis(imageAnalysisStream); + + MemoryStream writtenStream = new MemoryStream(ImageBytes); + MemoryStream printedStream = new MemoryStream(ImageBytes); + var ocrVM = ServiceLocator.Current.GetInstance(); + var ocrTask = ocrVM.LoadText(writtenStream, printedStream); + + var completionTask = Task.WhenAll(imageSearchTask, productSearchTask, imageAnalysisTask, ocrTask); + + completionTask.ContinueWith(t => + { + AIViewModel.LoadingVisibility = Visibility.Collapsed; + + if (LookForEmptyState()) + { + AIViewModel.EmptyStateVisibility = Visibility.Visible; + } + else + { + AIViewModel.AIControlsVisibility = Visibility.Visible; + } + }); + } + + /// + /// Check if we should display the empty state panel or not + /// + private bool LookForEmptyState() + { + /// Temporary solution to display the empty state in case of no result. + /// Will be changed to work with the Task at a later date but this + /// Workaround does the job without being detrimental for now. + return ServiceLocator.Current.GetInstance().IsVisible == Visibility.Collapsed && + ServiceLocator.Current.GetInstance().IsVisible == Visibility.Collapsed && + ServiceLocator.Current.GetInstance().IsVisible == Visibility.Collapsed && + ServiceLocator.Current.GetInstance().IsPeopleVisible == Visibility.Collapsed && + ServiceLocator.Current.GetInstance().IsPlaceVisible == Visibility.Collapsed; + } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/AIModels/ContentModerationModel.cs b/SnipInsight/AIServices/AIModels/ContentModerationModel.cs new file mode 100644 index 0000000..ae80ff3 --- /dev/null +++ b/SnipInsight/AIServices/AIModels/ContentModerationModel.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SnipInsight.AIServices.AIModels +{ + [DataContract] + public class Status + { + [DataMember(Name = "Code")] + public int Code { get; set; } + [DataMember(Name = "Description")] + public string Description { get; set; } + [DataMember(Name = "Exception")] + public object Exception { get; set; } + } + + [DataContract] + public class ContentModerationModel + { + [DataMember(Name = "AdultClassificationScore")] + public double AdultClassificationScore { get; set; } + [DataMember(Name = "IsImageAdultClassified")] + public bool IsImageAdultClassified { get; set; } + [DataMember(Name = "RacyClassificationScore")] + public double RacyClassificationScore { get; set; } + [DataMember(Name = "IsImageRacyClassified")] + public bool IsImageRacyClassified { get; set; } + [DataMember(Name = "AdvancedInfo")] + public List AdvancedInfo { get; set; } + [DataMember(Name = "Result")] + public bool Result { get; set; } + [DataMember(Name = "Status")] + public Status Status { get; set; } + [DataMember(Name = "TrackingId")] + public string TrackingId { get; set; } + [DataMember(Name = "CacheID")] + public string CacheID { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/EntitySearchModel.cs b/SnipInsight/AIServices/AIModels/EntitySearchModel.cs new file mode 100644 index 0000000..6122be5 --- /dev/null +++ b/SnipInsight/AIServices/AIModels/EntitySearchModel.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.Serialization; + +namespace SnipInsight.AIServices.AIModels +{ + public class CelebrityModel + { + public string Name { get; set; } + + public string Image { get; set; } + + public string URL { get; set; } + + public string Description { get; set; } + + public ObservableCollection News { get; set; } + } + + public class LandmarkModel + { + public string Name { get; set; } + + public string Image { get; set; } + + public string URL { get; set; } + + public string Description { get; set; } + + public string PostalCode { get; set; } + + public string Telephone { get; set; } + + public string Address { get; set; } + } + + [DataContract] + public class NewsModel + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "url")] + public string URL { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "datePublished")] + public string DatePublished { get; set; } + + [DataMember(Name = "image")] + public NewsImage Image { get; set; } + + [DataMember(Name = "provider")] + public List Provider { get; set; } + } + + [DataContract] + public class EntitySearchModel + { + [DataMember(Name = "entities")] + public EntityList Entities { get; set; } + + [DataContract] + public class EntityList + { + [DataMember(Name = "value")] + public List List { get; set; } + } + + [DataContract] + public class Entity + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "webSearchUrl")] + public string URL { get; set; } + + [DataMember(Name = "image")] + public ImageData Image { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + } + + [DataContract] + public class ImageData + { + [DataMember(Name = "hostPageUrl")] + public string URL { get; set; } + } + } + + [DataContract] + public class RawNewsModel + { + [DataMember(Name = "value")] + public List News { get; set; } + } + + [DataContract] + public class NewsImage + { + [DataMember(Name = "thumbnail")] + public NewsThumbnail Thumbnail { get; set; } + } + + [DataContract] + public class NewsThumbnail + { + [DataMember(Name = "contentUrl")] + public string URL { get; set; } + } + + [DataContract] + public class NewsProvider + { + [DataMember(Name = "name")] + public string Name { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/HandWrittenModel.cs b/SnipInsight/AIServices/AIModels/HandWrittenModel.cs new file mode 100644 index 0000000..b12ee7c --- /dev/null +++ b/SnipInsight/AIServices/AIModels/HandWrittenModel.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SnipInsight.AIServices.AIModels +{ + [DataContract] + public class HandWrittenModel + { + [DataMember(Name = "status")] + public string Status { get; set; } + + [DataMember(Name = "recognitionResult")] + public RecognitionResult RecognitionResult { get; set; } + } + + [DataContract] + public class WordWritten + { + [DataMember(Name = "boundingBox")] + public List BoundingBox { get; set; } + + [DataMember(Name = "text")] + public string Text { get; set; } + } + + [DataContract] + public class LineWritten + { + [DataMember(Name = "boundingBox")] + public List BoundingBox { get; set; } + + [DataMember(Name = "text")] + public string Text { get; set; } + + [DataMember(Name = "words")] + public List Words { get; set; } + } + + [DataContract] + public class RecognitionResult + { + [DataMember(Name = "lines")] + public List Lines { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/ImageAnalysisModel.cs b/SnipInsight/AIServices/AIModels/ImageAnalysisModel.cs new file mode 100644 index 0000000..c24d0a2 --- /dev/null +++ b/SnipInsight/AIServices/AIModels/ImageAnalysisModel.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SnipInsight.AIServices.AIModels +{ + public class VisualFeatureModel + { + List Tags { get; set; } + + List Captions { get; set; } + } + + + [DataContract] + public class ImageAnalysisModel + { + [DataMember(Name = "categories")] + public List Categories { get; set; } + + [DataMember(Name = "tags")] + public List Tags { get; set; } + + [DataMember(Name = "description")] + public ImageDescription Description { get; set; } + + [DataMember(Name = "metadata")] + public ImageMetadata Metadata { get; set; } + + [DataContract] + public class Category + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "score")] + public double Score { get; set; } + + [DataMember(Name = "detail")] + public Detail Detail { get; set; } + } + + [DataContract] + public class Tag + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "confidence")] + public double Confidence { get; set; } + } + + [DataContract] + public class Caption + { + [DataMember(Name = "text")] + public string Text { get; set; } + + [DataMember(Name = "confidence")] + public double Confidence { get; set; } + } + + [DataContract] + public class ImageDescription + { + [DataMember(Name = "tags")] + public List Tags { get; set; } + + [DataMember(Name = "captions")] + public List Captions { get; set; } + } + + [DataContract] + public class Celebrity + { + [DataMember(Name = "faceRectangle")] + public FaceRectangle FaceRectangle { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "confidence")] + public double Confidence { get; set; } + } + + [DataContract] + public class Detail + { + [DataMember(Name = "celebrities")] + public List Celebrities { get; set; } + + [DataMember(Name = "landmarks")] + public List Landmarks { get; set; } + } + + [DataContract] + public class Landmark + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "confidence")] + public double Confidence { get; set; } + } + + [DataContract] + public class ImageMetadata + { + [DataMember(Name = "height")] + public int Height { get; set; } + + [DataMember(Name = "width")] + public int Width { get; set; } + } + } + + [DataContract] + public class FaceRectangle + { + [DataMember(Name = "top")] + public int Top { get; set; } + + [DataMember(Name = "left")] + public int Left { get; set; } + + [DataMember(Name = "width")] + public int Width { get; set; } + + [DataMember(Name = "height")] + public int Height { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/ImageAnalysisResult.cs b/SnipInsight/AIServices/AIModels/ImageAnalysisResult.cs new file mode 100644 index 0000000..101590f --- /dev/null +++ b/SnipInsight/AIServices/AIModels/ImageAnalysisResult.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace SnipInsight.AIServices.AIModels +{ + /// + /// Image metadata from the image analysis api response + /// + class ImageAnalysisResult + { + /// + /// Initialize metadata availability to false when creating a new object + /// + internal ImageAnalysisResult() + { + CaptionAvailable = false; + TagsAvailable = false; + Caption = string.Empty; + Tags = new string[0]; + } + + /// + /// True if caption for image is available, false otherwise + /// + internal bool CaptionAvailable { get; set; } + + /// + /// True if tags for image is available, false otherwise + /// + internal bool TagsAvailable { get; set; } + + /// + /// Description of the image content + /// + internal string Caption { get; set; } + + /// + /// Contents of the image identified by the image analysis API + /// + internal string[] Tags { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/ImageSearchModel.cs b/SnipInsight/AIServices/AIModels/ImageSearchModel.cs new file mode 100644 index 0000000..c1f6aa3 --- /dev/null +++ b/SnipInsight/AIServices/AIModels/ImageSearchModel.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; +using GalaSoft.MvvmLight; + +namespace SnipInsight.AIServices.AIModels +{ + [DataContract] + public class ImageSearchModel : ObservableObject + { + /// + /// The width of the returned image + /// + private double width = 0; + + /// + /// The height of the returned image + /// + private double height = 0; + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "webSearchUrl")] + public string WebSearchUrl { get; set; } + + [DataMember(Name = "thumbnailUrl")] + public string Image { get; set; } + + [DataMember(Name = "datePublished")] + public string DatePublished { get; set; } + + [DataMember(Name = "contentUrl")] + public string URL { get; set; } + + [DataMember(Name = "hostPageUrl")] + public string HostPageUrl { get; set; } + + [DataMember(Name = "contentSize")] + public string ContentSize { get; set; } + + [DataMember(Name = "hostPageDisplayUrl")] + public string HostPageDisplayUrl { get; set; } + + [DataMember(Name = "width")] + public double Width + { + get { return width; } + set + { + width = value; + RaisePropertyChanged(); + } + } + + [DataMember(Name = "height")] + public double Height { + get { return height; } + set + { + height = value; + RaisePropertyChanged(); + } + } + } + + [DataContract] + public class ImageSearchModelContainer + { + [DataMember(Name = "visuallySimilarImages")] + public ImageSearchModelList Container { get; set; } + } + + [DataContract] + public class ImageSearchModelList + { + [DataMember(Name = "value")] + public List Images { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/LUISModel.cs b/SnipInsight/AIServices/AIModels/LUISModel.cs new file mode 100644 index 0000000..1de248e --- /dev/null +++ b/SnipInsight/AIServices/AIModels/LUISModel.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SnipInsight.AIServices.AIModels +{ + [DataContract] + public class LUISModel + { + [DataMember(Name = "query")] + public string Query { get; set; } + [DataMember(Name = "topScoringIntent")] + public TopScoringIntent TheTopScoringIntent { get; set; } + [DataMember(Name = "intents")] + public List Intents { get; set; } + [DataMember(Name = "entities")] + public List Entities { get; set; } + + [DataContract] + public class TopScoringIntent + { + [DataMember(Name = "intent")] + public string Intent { get; set; } + [DataMember(Name = "score")] + public double Score { get; set; } + } + + [DataContract] + public class Intent + { + [DataMember(Name = "intent")] + public string IntentValue { get; set; } + [DataMember(Name = "score")] + public double Score { get; set; } + } + + [DataContract] + public class Value + { + [DataMember(Name = "timex")] + public string Timex { get; set; } + [DataMember(Name = "type")] + public string Type { get; set; } + [DataMember(Name = "start")] + public string Start { get; set; } + [DataMember(Name = "end")] + public string End { get; set; } + [DataMember(Name = "value")] + public string TheValue { get; set; } + } + + [DataContract] + public class Resolution + { + [DataMember(Name = "values")] + public List Values { get; set; } + } + + [DataContract] + public class Entity + { + [DataMember(Name = "entity")] + public string TheEntity { get; set; } + [DataMember(Name = "type")] + public string Type { get; set; } + [DataMember(Name = "startIndex")] + public int StartIndex { get; set; } + [DataMember(Name = "endIndex")] + public int EndIndex { get; set; } + [DataMember(Name = "resolution")] + public Resolution Resolution { get; set; } + } + } +} diff --git a/SnipInsight/AIServices/AIModels/PrintedModel.cs b/SnipInsight/AIServices/AIModels/PrintedModel.cs new file mode 100644 index 0000000..d0c5a8b --- /dev/null +++ b/SnipInsight/AIServices/AIModels/PrintedModel.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SnipInsight.AIServices.AIModels +{ + [DataContract] + public class PrintedModel + { + [DataMember(Name = "language")] + public string Language { get; set; } + + [DataMember(Name = "orientation")] + public string Orientation { get; set; } + + [DataMember(Name = "textAngle")] + public double TextAngle { get; set; } + + [DataMember(Name = "regions")] + public List Regions { get; set; } + } + + [DataContract] + public class Word + { + [DataMember(Name = "boundingBox")] + public string BoundingBox { get; set; } + + [DataMember(Name = "text")] + public string Text { get; set; } + } + + [DataContract] + public class Line + { + [DataMember(Name = "boundingBox")] + public string BoundingBox { get; set; } + + [DataMember(Name = "words")] + public List Words { get; set; } + } + + [DataContract] + public class Region + { + [DataMember(Name = "boundingBox")] + public string BoundingBox { get; set; } + + [DataMember(Name = "lines")] + public List Lines { get; set; } + } +} diff --git a/SnipInsight/AIServices/AIModels/ProductSearchModel.cs b/SnipInsight/AIServices/AIModels/ProductSearchModel.cs new file mode 100644 index 0000000..cea09ee --- /dev/null +++ b/SnipInsight/AIServices/AIModels/ProductSearchModel.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Runtime.Serialization; +using GalaSoft.MvvmLight; + +namespace SnipInsight.AIServices.AIModels +{ + + [DataContract] + public class ProductSearchModel : ObservableObject + { + /// + /// The width of the returned image + /// + private double width = 0; + + /// + /// The height of the returned image + /// + private double height = 0; + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "hostPageUrl")] + public string HostPage { get; set; } + + [DataMember(Name = "thumbnailUrl")] + public string Image { get; set; } + + [DataMember(Name = "insightsMetadata")] + public ProductMetadata Metadata { get; set; } + + [DataMember(Name = "webSearchUrl")] + public string WebSearchUrl { get; set; } + + [DataMember(Name = "datePublished")] + public string DatePublished { get; set; } + + [DataMember(Name = "contentUrl")] + public string ContentUrl { get; set; } + + [DataMember(Name = "width")] + public double Width + { + get { return width; } + set + { + width = value; + RaisePropertyChanged(); + } + } + + [DataMember(Name = "height")] + public double Height + { + get { return height; } + set + { + height = value; + RaisePropertyChanged(); + } + } + } + + [DataContract] + public class ProductMetadata + { + [DataMember(Name = "aggregateOffer")] + public ProductOffer Offer { get; set; } + + [DataMember(Name = "shoppingSourcesCount")] + public int ShoppingSourcesCount { get; set; } + + [DataMember(Name = "recipeSourcesCount")] + public int RecipeSourcesCount { get; set; } + } + + [DataContract] + public class ProductOffer + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "priceCurrency")] + public string PriceCurrency { get; set; } + + [DataMember(Name = "aggregateRating")] + public ProductRating Rating { get; set; } + + [DataMember(Name = "lowPrice")] + public double Price { get; set; } + + [DataMember(Name = "offerCount")] + public int OfferCount { get; set; } + } + + [DataContract] + public class ProductRating + { + [DataMember(Name = "ratingValue")] + public double RatingValue { get; set; } + + [DataMember(Name = "bestRating")] + public double BestRating { get; set; } + + [DataMember(Name = "ratingCount")] + public int RatingCount { get; set; } + } + + [DataContract] + public class ProductSearchModelContainer + { + [DataMember(Name = "visuallySimilarProducts")] + public ProductSearchModelList Container { get; set; } + } + + [DataContract] + public class ProductSearchModelList + { + [DataMember(Name = "value")] + public List Products { get; set; } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/AIViewModels/BaseDynamicDisplay.cs b/SnipInsight/AIServices/AIViewModels/BaseDynamicDisplay.cs new file mode 100644 index 0000000..e6162e0 --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/BaseDynamicDisplay.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; + +namespace SnipInsight.AIServices.AIViewModels +{ + public abstract class BaseDynamicDisplay + { + /// + /// The width of the panel which the images resized to + /// + public double CenterBound = 0; + + /// + /// The thresh hold which we resize the display + /// + protected double ResizeDisplayThreshhold = double.MaxValue; + + /// + /// An array storing the aspect ratios of the controls to be analyzed + /// + protected List aspectRatios; + + /// + /// The lower bound of needing another resize + /// + protected double LowerBound = double.MaxValue; + + /// + /// The upper bound of needing another resize + /// + protected double UpperBound = double.MinValue; + + /// + /// Creates a control gallery, similar to the Bing Image Search Website + /// + /// The maximum height for the resized images + /// The panel width for the resized images + public void CreateControlGallery(double maxHeight, double panelWidth) + { + if (IsBetween(panelWidth, LowerBound, UpperBound)) + { + return; + } + + int incrementIndex = 0; + + for (int i = 0; i < aspectRatios.Count; i += incrementIndex) + { + Tuple controlResults = GetNumControls(i, maxHeight, panelWidth); + SizeRow(i, controlResults.Item1, controlResults.Item2); + incrementIndex = controlResults.Item1; + } + + LowerBound = panelWidth - (ResizeDisplayThreshhold); + UpperBound = panelWidth + (ResizeDisplayThreshhold); + CenterBound = panelWidth; + } + + /// + /// Populates the aspect ratio dictionary + /// + public abstract void PopulateAspectRatioDict(); + + /// + /// Sets the controls in the row to the given + /// + /// The start index of image to be resized + /// The number of images to be resized + /// The new height of the images + protected abstract void SizeRow(int startIndex, int numControls, double newHeight); + + /// + /// Gets the aspect ratio of the image + /// + /// The width of the image + /// The height of the image + /// Image aspect ratio + protected double GetAspectRatio(double width, double height) + { + return (height <= 0) ? 0 : width / height; + } + + /// + /// Gets the number of controls to be put into a row + /// + /// The start index of the controls + /// The max height of the row + /// The width to be resized to + /// The number of controls in the row in Item1, and the height of the controls in Item2 + private Tuple GetNumControls(int startIndex, double maxHeight, double resizeWidth) + { + int numImages = 0; + double height = 0; + double sumRatios = 0; + + // Finds the optimal number of images and height for the row + for (int i = startIndex; i < aspectRatios.Count && startIndex + numImages <= aspectRatios.Count; ++i) + { + ++numImages; + sumRatios += aspectRatios[i]; + height = FindHeight(sumRatios, resizeWidth); + + if (height <= maxHeight) + { + break; + } + + if (i == aspectRatios.Count - 1) + { + height = maxHeight; + break; + } + } + + Tuple imageInfo = new Tuple(numImages, height); + return imageInfo; + } + + /// + /// Finds the height of the row based on the sum of the Aspect ratios of the photos + /// + /// The sum of the aspect ratios in the row + /// The width of the row + /// Height of the row + private double FindHeight(double sumRatios, double resizeWidth) + { + return (sumRatios <= 0) ? 0 : resizeWidth / sumRatios; + } + + /// + /// Returns true if num is between lower and upper inclusive, false otherwise + /// + /// num to be evaluated + /// lower bound + /// upper bound + /// + private bool IsBetween(double num, double lower, double upper) + { + return lower <= num && num <= upper; + } + } +} diff --git a/SnipInsight/AIServices/AIViewModels/ImageAnalysisViewModel.cs b/SnipInsight/AIServices/AIViewModels/ImageAnalysisViewModel.cs new file mode 100644 index 0000000..a88c0e9 --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/ImageAnalysisViewModel.cs @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using GalaSoft.MvvmLight; +using GalaSoft.MvvmLight.Command; +using SnipInsight.AIServices.AILogic; +using SnipInsight.AIServices.AIModels; +using SnipInsight.Properties; +using SnipInsight.ViewModels; +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace SnipInsight.AIServices.AIViewModels +{ + public class ImageAnalysisViewModel : ViewModelBase + { + private ImageAnalysisHandler _analysisHandler; + private ObservableCollection _landmark; + + private CelebrityModel _celebrity; + + private Visibility _isPeopleVisible = Visibility.Collapsed; + private Visibility _isPlaceVisible = Visibility.Collapsed; + + /// + /// Command for navigating to url on celebrity panel + /// + public RelayCommand NavigateToCelebrityUrlCommand { get; set; } + + /// + /// Command for navigating to url on landmark panel + /// + public RelayCommand NavigateToLandmarkUrlCommand { get; set; } + + + /// + /// Command for navigating to url on news panel + /// + public RelayCommand NavigateToNewsUrlCommand { get; set; } + + /// + /// Celebrity recognized by the API + /// + public CelebrityModel Celebrity + { + get => _celebrity; + set + { + _celebrity = value; + RaisePropertyChanged(); + } + } + + /// + /// Landmark recognized by the API + /// + public ObservableCollection Landmarks + { + get => _landmark; + set + { + _landmark = value; + RaisePropertyChanged(); + } + } + + /// + /// Whether or not the people panel should be visible + /// + public Visibility IsPeopleVisible + { + get => _isPeopleVisible; + + set + { + _isPeopleVisible = value; + RaisePropertyChanged(); + } + } + + /// + /// Whether or not the place panel should be visible + /// + public Visibility IsPlaceVisible + { + get => _isPlaceVisible; + + set + { + _isPlaceVisible = value; + RaisePropertyChanged(); + } + } + + /// + /// Constructor for the Image Analysis view model + /// + public ImageAnalysisViewModel() + { + _analysisHandler = new ImageAnalysisHandler("ImageAnalysis"); + + NavigateToCelebrityUrlCommand = new RelayCommand(NavigateToCelebrityUrlCommandExecute); + NavigateToLandmarkUrlCommand = new RelayCommand(NavigateToLandmarkUrlCommandExecute); + NavigateToNewsUrlCommand = new RelayCommand (NavigateToNewsUrlCommandExecute); + } + + /// + /// Navigate to the celebrity description detail page + /// + private void NavigateToCelebrityUrlCommandExecute() + { + try + { + Process.Start(Celebrity.URL); + } + catch (Win32Exception e) + { + MessageBox.Show(Resources.No_Browser); + } + } + + /// + /// Navigate to the landmark description detail page + /// + private void NavigateToLandmarkUrlCommandExecute() + { + try + { + Process.Start(Landmarks[0].URL); + } + catch (Win32Exception e) + { + MessageBox.Show(Resources.No_Browser); + } + } + + /// + /// Command for navigating to url on news panel + /// + private void NavigateToNewsUrlCommandExecute(NewsModel newsModel) + { + try + { + Process.Start(newsModel.URL); + } + catch (Win32Exception e) + { + MessageBox.Show(Resources.No_Browser); + } + } + + /// + /// Retrieve the informations about the celebrity from the cloud services + /// + /// Model containing the json image analysis data + /// + private async Task GetCelebrities(ImageAnalysisModel model) + { + EntitySearchHandler entityHandler = new EntitySearchHandler("EntitySearch"); + NewsHandler newsHandler = new NewsHandler("ImageSearch"); + + Celebrity = null; + IsPeopleVisible = Visibility.Collapsed; + Canvas celebrities = new Canvas(); + + try + { + foreach (ImageAnalysisModel.Category category in model.Categories) + { + foreach (ImageAnalysisModel.Celebrity celebrity in category.Detail.Celebrities) + { + var celebrityModel = await entityHandler.GetResult(celebrity.Name); + + if (celebrityModel.Entities != null) + { + var entry = celebrityModel.Entities.List.FirstOrDefault(); + + if (entry != null) + { + Rectangle rect = new Rectangle(); + var celebModel = new CelebrityModel + { + Name = entry.Name, + Image = entry.Image.URL, + URL = entry.URL, + Description = entry.Description + }; + + var result = await newsHandler.GetResult(celebrity.Name); + + celebModel.News = new ObservableCollection(result.News); + + foreach(NewsModel newsModel in celebModel.News) + { + newsModel.DatePublished = newsModel.DatePublished.Substring(0, 10); + newsModel.Description = newsModel.Description.Substring(0, 150) + "..."; + } + + rect.Tag = celebModel; + + rect.Width = celebrity.FaceRectangle.Width; + rect.Height = celebrity.FaceRectangle.Height; + rect.Stroke = Brushes.Transparent; + rect.StrokeThickness = 2; + rect.Fill = Brushes.Transparent; + rect.ToolTip = entry.Name; + rect.MouseDown += CelebritySelected; + rect.MouseEnter += ShowCelebrityRectangle; + rect.MouseLeave += HideCelebrityRectangle; + + celebrities.Children.Add(rect); + Canvas.SetLeft(rect, celebrity.FaceRectangle.Left); + Canvas.SetTop(rect, celebrity.FaceRectangle.Top); + } + } + } + } + } + catch(Exception e) + { + Console.WriteLine(Resources.Exception_at_celebrities + e.Message); + } + + if (celebrities.Children.Count > 0) + { + IsPeopleVisible = Visibility.Visible; + var rect = celebrities.Children[0] as Rectangle; + rect.Stroke = Brushes.Wheat; + Celebrity = rect.Tag as CelebrityModel; + } + + ServiceLocator.Current.GetInstance().PeopleSearchCommand.RaiseCanExecuteChanged(); + + AppManager.TheBoss.ViewModel.CelebritiesCanvas = celebrities; + } + + /// + /// Retrieve the informations about the landmark from the cloud services + /// + /// Model containing the json image analysis data + /// + private async Task GetLandmarks(ImageAnalysisModel model) + { + ObservableCollection landmarkList = new ObservableCollection(); + + EntitySearchHandler entityHandler = new EntitySearchHandler("EntitySearch"); + + try + { + foreach (ImageAnalysisModel.Category category in model.Categories) + { + foreach (ImageAnalysisModel.Landmark landmark in category.Detail.Landmarks) + { + var landmarkModel = await entityHandler.GetResult(landmark.Name); + + var entry = landmarkModel.Entities.List.FirstOrDefault(); + + if (entry != null) + { + landmarkList.Add(new LandmarkModel + { + Name = entry.Name, + Image = entry.Image.URL, + URL = entry.URL, + Description = entry.Description, + PostalCode = null, + Telephone = null, + Address = null + }); + } + } + } + } + catch(Exception e) + { + Console.WriteLine(Resources.Exception_at_landmark + e.Message); + } + + IsPlaceVisible = landmarkList.Count > 0 ? Visibility.Visible : Visibility.Collapsed; + ServiceLocator.Current.GetInstance().PlaceSearchCommand.RaiseCanExecuteChanged(); + + Landmarks = landmarkList; + } + + /// + /// Update the metadata for saving or sharing the image + /// + /// Model containing the json image analysis data + /// + private void UpdateMetadata(ImageAnalysisModel model) + { + ImageAnalysisResult result = new ImageAnalysisResult(); + + if (model == null || model.Description == null) + { + return; + } + + var caption = model.Description.Captions.FirstOrDefault(); + + if (caption != null) + { + result.Caption = caption.Text; + result.CaptionAvailable = true; + } + + if (model.Description.Tags.Count > 0) + { + result.Tags = model.Description.Tags.ToArray(); + result.TagsAvailable = true; + } + + AppManager.TheBoss.ImageMetadata = result; + } + + /// + /// Analyse the image + /// + /// Image used for the analysis + /// A task containing the success or failure of the operation + public async Task LoadAnalysis(MemoryStream imageStream) + { + IsPeopleVisible = Visibility.Collapsed; + IsPlaceVisible = Visibility.Collapsed; + + var model = await _analysisHandler.GetResult(imageStream); + + // Create the celebrities models + await GetCelebrities(model); + + // Create the landmarks models + await GetLandmarks(model); + + UpdateMetadata(model); + } + + /// + /// Change the selected celebrity based on the user's choice + /// + private void CelebritySelected(object sender, MouseButtonEventArgs e) + { + var rect = sender as Rectangle; + + Celebrity = rect.Tag as CelebrityModel; + + var par = rect.Parent as Canvas; + foreach (var obj in par.Children) + { + var otherRect = obj as Rectangle; + otherRect.Stroke = Brushes.Transparent; + } + + rect.Stroke = Brushes.Wheat; + } + + /// + /// Show the hovered celebrity + /// + private void ShowCelebrityRectangle(object sender, MouseEventArgs e) + { + var rect = sender as Rectangle; + + var celeb = rect.Tag as CelebrityModel; + + if (celeb != Celebrity) + { + rect.Stroke = Brushes.White; + } + } + + /// + /// Hide the face rectangle on mouse leave + /// + private void HideCelebrityRectangle(object sender, MouseEventArgs e) + { + var rect = sender as Rectangle; + var celeb = rect.Tag as CelebrityModel; + + if (celeb != Celebrity) + { + rect.Stroke = Brushes.Transparent; + } + } + } +} diff --git a/SnipInsight/AIServices/AIViewModels/ImageDynamicDisplay.cs b/SnipInsight/AIServices/AIViewModels/ImageDynamicDisplay.cs new file mode 100644 index 0000000..b958278 --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/ImageDynamicDisplay.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using SnipInsight.AIServices.AIModels; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace SnipInsight.AIServices.AIViewModels +{ + public class ImageDynamicDisplay : BaseDynamicDisplay + { + /// + /// The image control to be resized + /// + private ObservableCollection renderedImages; + + /// + /// Populating the aspect ratio dictionary + /// + public override void PopulateAspectRatioDict() + { + var imageSearchVM = ServiceLocator.Current.GetInstance(); + + aspectRatios = new List(); + if (imageSearchVM == null || imageSearchVM.Images == null) + { + return; + } + + renderedImages = imageSearchVM.Images; + foreach (ImageSearchModel ic in renderedImages) + { + aspectRatios.Add(GetAspectRatio(ic.Width, ic.Height)); + } + + LowerBound = double.MaxValue; + UpperBound = double.MinValue; + } + + /// + /// Resizes the image row + /// + /// The index of the collection to start resizing + /// The number of images to resize + /// The new height of the image resized + protected override void SizeRow(int startIndex, int numImages, double newHeight) + { + for (int i = startIndex; i < renderedImages.Count && i < startIndex + numImages; ++i) + { + renderedImages[i].Width = newHeight * aspectRatios[i]; + renderedImages[i].Height = newHeight; + + if (renderedImages[i].Width < ResizeDisplayThreshhold) + { + ResizeDisplayThreshhold = renderedImages[i].Width; + } + } + } + } +} diff --git a/SnipInsight/AIServices/AIViewModels/ImageSearchViewModel.cs b/SnipInsight/AIServices/AIViewModels/ImageSearchViewModel.cs new file mode 100644 index 0000000..929bfd7 --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/ImageSearchViewModel.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using GalaSoft.MvvmLight; +using GalaSoft.MvvmLight.Command; +using SnipInsight.AIServices.AILogic; +using SnipInsight.AIServices.AIModels; +using SnipInsight.ImageCapture; +using SnipInsight.Properties; +using SnipInsight.ViewModels; +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using System.Windows; + +namespace SnipInsight.AIServices.AIViewModels +{ + /// + /// ViewModel for the Image Search, simple array string + /// The result is a string containing the text + /// + public class ImageSearchViewModel : ViewModelBase + { + public ImageSearchViewModel() + { + _handler = new ImageSearchHandler("ImageSearch"); + + // Commands initialization + ImageSelected = new RelayCommand(ImageSelectedExecute, ImageSelectedCanExecute); + } + + /// + /// Current wrap panel width + /// + private double currentWrapPanelWidth = 0; + + /// + /// Object used for dynamic image resizing + /// + private readonly ImageDynamicDisplay DynamicImageResizer = new ImageDynamicDisplay(); + + /// + /// Max height of images in the panel + /// + private const double ImageMaxHeight = 175; + + /// + /// The defined width for wrap panel + /// + private double wrapPanelDefinedWidth; + + private ObservableCollection _images; + + /// + /// Image list for the Reverse Search + /// + public ObservableCollection Images + { + get { return _images; } + set + { + _images = value; + RaisePropertyChanged(); + } + } + + /// + /// The current width for wrap panel + /// + public double CurrentWrapPanelWidth + { + get { return currentWrapPanelWidth; } + set + { + currentWrapPanelWidth = value; + if(currentWrapPanelWidth >= DynamicImageResizer.CenterBound) + { + WrapPanelDefinedWidth = currentWrapPanelWidth; + } + DynamicImageResizer.CreateControlGallery(ImageMaxHeight, currentWrapPanelWidth); + } + } + + /// + /// The defined width for wrap panel + /// + public double WrapPanelDefinedWidth + { + get { return currentWrapPanelWidth; } + set + { + wrapPanelDefinedWidth = value; + RaisePropertyChanged(); + } + } + + public async Task LoadImages(MemoryStream imageStream) + { + Images = null; + IsVisible = Visibility.Collapsed; + + var model = await _handler.GetResult(imageStream); + + if (model != null && model.Container != null && model.Container.Images != null) + { + Images = new ObservableCollection(model.Container.Images); + IsVisible = model.Container.Images.Count > 0 ? Visibility.Visible : Visibility.Collapsed; + DynamicImageResizer.PopulateAspectRatioDict(); + DynamicImageResizer.CreateControlGallery(ImageMaxHeight, currentWrapPanelWidth); + } + + ServiceLocator.Current.GetInstance().ImageSearchCommand.RaiseCanExecuteChanged(); + } + + #region Commands + public RelayCommand ImageSelected { get; set; } + #endregion + + #region CommandsCanExecute + + private bool ImageSelectedCanExecute(ImageSearchModel model) + { + return true; + } + #endregion + + #region CommandsExecute + + /// + /// Load the selected image main panel for further opertaion. + /// + /// Details of the image from the AI result to be loaded + private void ImageSelectedExecute(ImageSearchModel model) + { + SnipInsightViewModel viewModel = AppManager.TheBoss.ViewModel; + var aiPanelVM = ServiceLocator.Current.GetInstance(); + + AppManager.TheBoss.OnSaveImage(); + + if (string.IsNullOrEmpty(viewModel.RestoreImageUrl)) + { + viewModel.RestoreImageUrl = viewModel.SavedCaptureImage; + } + + ImageLoader.LoadFromUrl(new Uri(model.URL)).ContinueWith(t => + { + Application.Current.Dispatcher.Invoke(() => + { + aiPanelVM.CapturedImage = t.Result; + viewModel.SelectedImageUrl = model.URL; + AppManager.TheBoss.RunAllInsights(); + }); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + + ImageLoader.LoadFromUrl(new Uri(model.URL)).ContinueWith(t => + { + MessageBox.Show(Resources.Image_Not_Loaded); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + #endregion + + private ImageSearchHandler _handler; + + private Visibility _isVisible = Visibility.Collapsed; + + public Visibility IsVisible { + get => _isVisible; + + set + { + _isVisible = value; + + RaisePropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/AIViewModels/InsightsPermissionsViewModel.cs b/SnipInsight/AIServices/AIViewModels/InsightsPermissionsViewModel.cs new file mode 100644 index 0000000..2020d43 --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/InsightsPermissionsViewModel.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using GalaSoft.MvvmLight; +using GalaSoft.MvvmLight.Command; +using SnipInsight.Properties; +using SnipInsight.Util; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows; + +namespace SnipInsight.AIServices.AIViewModels +{ + public class InsightsPermissionsViewModel : ViewModelBase + { + public InsightsPermissionsViewModel() + { + TurnOnAIAnalysisCommand = new RelayCommand(TurnOnAIAnalysisCommandExecute); + OpenPrivatePolicyInBrowserCommand = new RelayCommand(OpenPrivatePolicyInBrowserCommandExecute); + OpenLearnMoreInBrowserCommand = new RelayCommand(OpenLearnMoreInBrowserCommandExecute); + } + + #region Commands + /// + /// Turns on the setting for using ai analysis, disables visibility for permissions and enables the insights visible + /// + public RelayCommand TurnOnAIAnalysisCommand { get; set; } + + /// + /// Opens the web browser and navigates to the microsoft privacy policy page + /// + public RelayCommand OpenPrivatePolicyInBrowserCommand { get; set; } + + /// + /// Opens the web browser and navigates to the microsoft cognitive services page + /// + public RelayCommand OpenLearnMoreInBrowserCommand { get; set; } + + /// + /// Turns on the setting for using ai analysis, disables visibility for permissions and enables the insights visible + /// + private void TurnOnAIAnalysisCommandExecute() + { + UserSettings.IsAIEnabled = true; + AppManager.TheBoss.ViewModel.InsightsVisible = UserSettings.IsAIEnabled; + AppManager.TheBoss.RunAllInsights(); + } + + /// + /// Opens the web browser and navigates to the microsoft privacy policy page + /// + private void OpenPrivatePolicyInBrowserCommandExecute() + { + GoToUrl("https://go.microsoft.com/fwlink/?LinkId=521839"); + } + + /// + /// Opens the web browser and navigates to the microsoft cognitive services page + /// + private void OpenLearnMoreInBrowserCommandExecute() + { + GoToUrl("https://azure.microsoft.com/en-us/services/cognitive-services/"); + } + #endregion + + #region Helper Methods + + /// + /// Navigates to the given url, throws a Win32 exception if there + /// is no internet browser found on the computer + /// + private void GoToUrl(string url) + { + try + { + Process.Start(url); + } + catch (Win32Exception CaughtExeception) + { + MessageBox.Show(Resources.No_Browser); + } + } + #endregion + } +} diff --git a/SnipInsight/AIServices/AIViewModels/OCRViewModel.cs b/SnipInsight/AIServices/AIViewModels/OCRViewModel.cs new file mode 100644 index 0000000..1be540e --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/OCRViewModel.cs @@ -0,0 +1,677 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using GalaSoft.MvvmLight; +using GalaSoft.MvvmLight.Command; +using SnipInsight.AIServices.AILogic; +using SnipInsight.AIServices.AIModels; +using SnipInsight.ClipboardUtils; +using SnipInsight.EmailController; +using SnipInsight.Properties; +using SnipInsight.Util; +using SnipInsight.ViewModels; +using SnipInsight.Views; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Mail; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; + +namespace SnipInsight.AIServices.AIViewModels +{ + /// + /// ViewModel for the OCR, simple string + /// The result is a string containing the text + /// + public class OCRViewModel : ViewModelBase + { + private HandWrittenTextHandler writtenHandler; + private PrintedTextHandler printedHandler; + private TranslationHandler translationHandler; + private LUISInsights luisInsights; + private string text; + private string printedResult = string.Empty; + private const string unknownLanguage = "unk"; + + public bool DatesAvailable { get; private set; } + public bool EmailAvailable { get; private set; } + + /// + /// List of DateTime object to be used to create calendar event. + /// + private List calendarDates = new List(); + + /// + /// List of email addresses to be used to send new email. + /// + private List toAddress = new List(); + + /// + /// Constructor for the OCR View Model, initialize the commands + /// + public OCRViewModel() + { + writtenHandler = new HandWrittenTextHandler("TextRecognition"); + printedHandler = new PrintedTextHandler("TextRecognition"); + translationHandler = new TranslationHandler("Translator"); + luisInsights = new LUISInsights("LUISKey"); + + ToggleTranslatorCommand = new RelayCommand(ToggleTranslatorCommandExecute); + OpenCalendarEventCommand = new RelayCommand(OpenCalendarEventCommandExecute); + SendNewEmailCommand = new RelayCommand(SendNewEmailCommandExecute); + CopyTextCommand = new RelayCommand(CopyTextCommandExecute); + } + + public async Task LoadText(MemoryStream writtenStream, MemoryStream printedStream) + { + TranslatedText = String.Empty; + ToLanguage = String.Empty; + + IsVisible = Visibility.Collapsed; + + Task writtenTask = writtenHandler.GetResult(writtenStream); + Task printedTask = printedHandler.GetResult(printedStream); + + await Task.WhenAll(writtenTask, printedTask); + CompareResult(writtenTask.Result, printedTask.Result); + + CultureInfo ci = CultureInfo.InstalledUICulture; + ToLanguage = ci.TwoLetterISOLanguageName; + + if (!string.IsNullOrEmpty(printedResult)) + { + await GetLUISInsights(printedResult); + } + + CalendarButtonVisibility = DatesAvailable ? Visibility.Visible : Visibility.Collapsed; + EmailButtonVisibility = EmailAvailable ? Visibility.Visible : Visibility.Collapsed; + } + + /// + /// Detected language marked in the from language dropdown + /// + public string DetectedLanguage { get; set; } + + private string fromLanguage; + + /// + /// selected item in the from language dropdown + /// + public string FromLanguage + { + get + { + return fromLanguage; + } + set + { + fromLanguage = value; + RaisePropertyChanged(); + RunTranslation(); + } + } + + private string toLanguage; + + /// + /// selected item in the to language dropdown + /// + public string ToLanguage + { + get + { + return toLanguage; + } + set + { + toLanguage = value; + RaisePropertyChanged(); + RunTranslation(); + } + } + + private string translatedText; + + /// + /// selected item in the to language dropdown + /// + public string TranslatedText + { + get + { + return translatedText; + } + set + { + translatedText = value; + RaisePropertyChanged(); + } + } + + private bool translatorEnable = false; + + /// + /// Enable/disable toggle translator button + /// + public bool TranslatorEnable + { + get + { + return translatorEnable; + } + set + { + translatorEnable = value; + RaisePropertyChanged(); + } + } + + private Visibility translationVisibility = Visibility.Collapsed; + + /// + /// Make the translation textblock visible + /// + public Visibility TranslationVisibility + { + get { return translationVisibility; } + set + { + translationVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility ocrSingleBoxVisibility = Visibility.Visible; + + /// + /// Visibility for the Single OCR Text Block + /// + public Visibility OCRSingleBoxVisibility + { + get { return ocrSingleBoxVisibility; } + set + { + ocrSingleBoxVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility calendarButtonVisibility = Visibility.Collapsed; + + /// + /// Visibility for the "Create New Event" button + /// + public Visibility CalendarButtonVisibility + { + get { return calendarButtonVisibility; } + set + { + calendarButtonVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility emailButtonVisibility = Visibility.Collapsed; + + /// + /// Visibility for the "Open New Email" button + /// + public Visibility EmailButtonVisibility + { + get { return emailButtonVisibility; } + set + { + emailButtonVisibility = value; + RaisePropertyChanged(); + } + } + + private Dictionary languages; + + /// + /// List of language for the translator dropdown + /// + public Dictionary Languages + { + get { return languages; } + set + { + languages = value; + RaisePropertyChanged(); + } + } + + /// + /// Toggle the visibility of the translate + /// + public RelayCommand ToggleTranslatorCommand { get; set; } + + /// + /// Toggle "Create Calendar Event" button depending on + /// whether date and time is available. + /// + public RelayCommand OpenCalendarEventCommand { get; set; } + + /// + /// Toggle "Send New Email" button depending on + /// whether email address is available. + /// + public RelayCommand SendNewEmailCommand { get; set; } + + /// + /// Toggle the visibility of copy + /// + public RelayCommand CopyTextCommand { get; set; } + + /// + /// Execute the translate command + /// + private void ToggleTranslatorCommandExecute() + { + int change = (int)TranslationVisibility; + TranslationVisibility = OCRSingleBoxVisibility; + OCRSingleBoxVisibility = (Visibility)change; + } + + /// + /// Text to be displayed on screen + /// + public string Text + { + get { return text; } + set + { + text = value; + RaisePropertyChanged(); + } + } + + /// + /// Opens a calendar event by creating and opening an ics file. + /// + private void OpenCalendarEventCommandExecute() + { + CreateCalendarEvent(); + } + + /// + /// Sends new email by creating and opening an eml file. + /// + private void SendNewEmailCommandExecute() + { + OpenNewEmail(); + } + + /// + /// Copy text in OCR result into clipboard + /// + private void CopyTextCommandExecute() + { + bool copiedText = TranslationVisibility == Visibility.Visible ? ClipboardManager.Copy(TranslatedText) : ClipboardManager.Copy(Text); + ToastControl toast = new ToastControl(copiedText ? Resources.Message_CopiedToClipboard : Resources.Message_CopyToClipboardFailed); + toast.ShowInMainWindow(); + } + + /// + /// Compare and set the better result of the text recognition to view model + /// + /// Data extracted from the API call + /// Data extracted from the API call + private void CompareResult(HandWrittenModel writtenModel, PrintedModel printedModel) + { + StringBuilder result = new StringBuilder(); + + try + { + foreach (LineWritten line in writtenModel.RecognitionResult.Lines) + { + result.Append(line.Text + "\n"); + } + } + catch (Exception e) + { } + + string writtenResult = result.ToString() ?? string.Empty; + + result.Clear(); + + try + { + foreach (Region region in printedModel.Regions) + { + foreach (Line line in region.Lines) + { + foreach (Word word in line.Words) + { + result.Append(word.Text + " "); + } + + result.Append("\n"); + } + } + } + catch (Exception e) + { } + + printedResult = result.ToString() ?? string.Empty; + + if (printedModel.Language != unknownLanguage && printedResult.Length > writtenResult.Length) + { + Text = printedResult; + DetectedLanguage = printedModel.Language; + } + else + { + Text = writtenResult; + DetectedLanguage = "en"; + } + + if (string.IsNullOrWhiteSpace(text)) + { + Text = "No Result"; + } + else + { + TranslatorEnable = translationHandler.TranslatorEnable; + + if (TranslatorEnable) + { + Languages = PopulateLanguageMenus(); + } + + IsVisible = Visibility.Visible; + } + + ServiceLocator.Current.GetInstance().OCRCommand.RaiseCanExecuteChanged(); + } + + private Visibility _isVisible = Visibility.Collapsed; + + public Visibility IsVisible + { + get => _isVisible; + set + { + _isVisible = value; + + RaisePropertyChanged(); + } + } + + /// + /// Populate the languages combobox and set the detected language + /// + /// Dictionary of language names and codes + public Dictionary PopulateLanguageMenus() + { + Dictionary languages = new Dictionary(); + + foreach (KeyValuePair language in translationHandler.LanguageCodesAndTitles) + { + //Reversed to fetch detected language code. + languages.Add(language.Value, language.Key); + } + + if (!string.IsNullOrEmpty(DetectedLanguage) && !DetectedLanguage.Equals("unk")) + { + languages[DetectedLanguage] = languages[DetectedLanguage] + " (Detected)"; + } + + FromLanguage = DetectedLanguage; + return languages; + } + + /// + /// Checks if the result from OCR text recognition contains valid entities, + /// such as date/time, email address using LUIS. + /// + /// string of text returned from OCR text recognition + private async Task GetLUISInsights(string ocrText) + { + List datesFound = new List(); + + CalendarButtonVisibility = Visibility.Collapsed; + EmailButtonVisibility = Visibility.Collapsed; + calendarDates.Clear(); + toAddress.Clear(); + + try + { + var luisResult = await luisInsights.GetResult(ocrText); + + for (int i = 0; i < luisResult.Entities.Count; i++) + { + LUISModel.Entity entity = luisResult.Entities[i]; + LUISModel.Entity nextEntity = i + 1 < luisResult.Entities.Count ? luisResult.Entities[i + 1] : null; + var validYear = @"^[1-9]\d*$"; + + switch (entity.Type) + { + case "builtin.email": + toAddress.Add(entity.TheEntity); + EmailAvailable = true; + break; + case "builtin.datetimeV2.datetime": + datesFound.Add(entity.Resolution.Values[0].TheValue); + break; + case "builtin.datetimeV2.date": + if (nextEntity != null && nextEntity.Type.Equals("builtin.datetimeV2.time")) + { + datesFound.Add(entity.Resolution.Values[0].TheValue + " " + nextEntity.Resolution.Values[0].TheValue); + i++; + } + else if (nextEntity != null && nextEntity.Type.Equals("builtin.datetimeV2.timerange")) + { + datesFound.Add(entity.Resolution.Values[0].TheValue + " " + nextEntity.Resolution.Values[0].Start); + datesFound.Add(entity.Resolution.Values[0].TheValue + " " + nextEntity.Resolution.Values[0].End); + i++; + } + else if (nextEntity != null && !nextEntity.Type.Equals("builtin.datetimeV2.daterange")) + { + datesFound.Add(entity.Resolution.Values.Count == 2 ? entity.Resolution.Values[1].TheValue : entity.Resolution.Values[0].TheValue); + } + break; + case "builtin.datetimeV2.daterange": + Match isYear = Regex.Match(entity.Resolution.Values[0].Timex, validYear); + // Should not create calender event for recognized year + if (!isYear.Success) + { + datesFound.Add(entity.Resolution.Values[0].Start); + datesFound.Add(entity.Resolution.Values[0].End); + } + break; + case "builtin.datetimeV2.time": + datesFound.Add(entity.Resolution.Values[0].TheValue); + break; + case "builtin.datetimeV2.timerange": + datesFound.Add(entity.Resolution.Values[0].Start); + datesFound.Add(entity.Resolution.Values[0].End); + break; + case "builtin.datetimeV2.datetimerange": + datesFound.Add(entity.Resolution.Values[0].Start); + datesFound.Add(entity.Resolution.Values[0].End); + break; + } + } + + DatesAvailable = ExtractDateTime(datesFound); + EmailAvailable = (toAddress.Count > 0) ? true : false; + } + catch (WebException e) + { + Debug.WriteLine(e.Message); + DatesAvailable = false; + EmailAvailable = false; + } + } + + /// + /// Temporarily store information in EML file (email in file form). + /// Open the EML file with the default mail client to send out a new email. + /// + /// whether a new email is opened successfully + private void OpenNewEmail() + { + try + { + using (MailMessage message = new MailMessage()) + { + // create a new email message with toAddress. + message.From = new MailAddress("youremail@example.com"); + for (int i = 0; i < toAddress.Count; i++) + { + message.To.Add(new MailAddress(toAddress[i])); + } + message.IsBodyHtml = true; + + // Get the temp EML file. + string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + while (File.Exists(tempFile)) + { + tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + } + + // save the message to disk and open it. + if (EmailManager.SaveMessage(message, tempFile)) + { + Process.Start(tempFile); + } + else if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + + /// + /// Convert collected information - date and time - into a ics file. + /// + public void CreateCalendarEvent() + { + if (calendarDates.Count == 0) + { + return; + } + + DateTime dateStart = calendarDates[0]; + DateTime dateEnd = calendarDates.Count >= 2 ? calendarDates[1] : calendarDates[0]; + + string DateFormat = "yyyyMMddTHHmmssZ"; + string now = DateTime.Now.ToUniversalTime().ToString(DateFormat); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("BEGIN:VCALENDAR"); + sb.AppendLine("PRODID:-//Compnay Inc//Product Application//EN"); + sb.AppendLine("VERSION:2.0"); + sb.AppendLine("METHOD:PUBLISH"); + sb.AppendLine("BEGIN:VEVENT"); + sb.AppendLine("DTSTART:" + dateStart.ToUniversalTime().ToString(DateFormat)); + sb.AppendLine("DTEND:" + dateEnd.ToUniversalTime().ToString(DateFormat)); + sb.AppendLine("DTSTAMP:" + now); + sb.AppendLine("UID:" + Guid.NewGuid()); + sb.AppendLine("CREATED:" + now); + sb.AppendLine("LAST-MODIFIED:" + now); + sb.AppendLine("SEQUENCE:0"); + sb.AppendLine("STATUS:CONFIRMED"); + sb.AppendLine("SUMMARY:" + Resources.Create_New_Event); + sb.AppendLine("TRANSP:OPAQUE"); + sb.AppendLine("END:VEVENT"); + sb.AppendLine("END:VCALENDAR"); + + Task result = CreateAndOpenICSFile(sb); + result.ContinueWith((t) => + { + if (t.Exception != null) + { + Debug.WriteLine(t.Exception.Message); + } + }); + } + + /// + /// Attempts to extract date and time from the given list of string + /// + /// list of date and time recognized using OCR + /// true if at least one date is parsed successfully, false otherwise + private bool ExtractDateTime(List ocrDates) + { + bool extracted = false; + + foreach (string date in ocrDates) + { + if (DateTime.TryParse(date, out DateTime extractedDate)) + { + extracted = true; + calendarDates.Add(extractedDate); + } + } + + return extracted; + } + + /// + /// Create the ics file, save it to user-defined file path, + /// and open the ics file. + /// + /// contains all information needed to create ics file + private async Task CreateAndOpenICSFile(StringBuilder sb) + { + // Generate an unique ics file name using current date and time + string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + string fileName = string.Format( + "event_{0} {1}", + DateTimeOffset.Now.ToString(Resources.Culture.DateTimeFormat.ShortDatePattern), + DateTimeOffset.Now.ToString(Resources.Culture.DateTimeFormat.ShortTimePattern)); + + // Replace invalid characters in filename with "-" + var invalidChars = string.Format(@"[{0}]+", (new string(Path.GetInvalidFileNameChars()))); + fileName = Regex.Replace(fileName, invalidChars, "-"); + string icsFileName = string.Concat(folderPath, "\\", fileName, ".ics"); + + File.WriteAllText(@icsFileName, sb.ToString()); + using (Stream file = File.OpenWrite(icsFileName)) + { + using (StreamWriter ics = new StreamWriter(file)) + { + await ics.WriteAsync(sb.ToString()); + } + } + + Process myProcess = new Process(); + myProcess.StartInfo.FileName = @icsFileName; + myProcess.StartInfo.Arguments = ""; + myProcess.Exited += new EventHandler((sender, e) => { + // TODO: monitor process of exit code (for telemetry) + }); + myProcess.Start(); + } + + /// + /// Run the translation with the update languages + /// + private void RunTranslation() + { + if (!string.IsNullOrEmpty(toLanguage) && !fromLanguage.Equals(toLanguage)) + { + Task.Run(async () => TranslatedText = await translationHandler.GetResult(Text, + FromLanguage, + ToLanguage)); + } + else + { + TranslatedText = Text; + } + } + } +} diff --git a/SnipInsight/AIServices/AIViewModels/ProductDynamicDisplay.cs b/SnipInsight/AIServices/AIViewModels/ProductDynamicDisplay.cs new file mode 100644 index 0000000..25a69ab --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/ProductDynamicDisplay.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Collections.ObjectModel; +using CommonServiceLocator; +using SnipInsight.AIServices.AIModels; + +namespace SnipInsight.AIServices.AIViewModels +{ + public class ProductDynamicDisplay : BaseDynamicDisplay + { + /// + /// The image control to be resized + /// + private ObservableCollection renderedProducts; + + /// + /// The offset height to style the grid + /// + private const double heightOffset = 80; + + /// + /// Populating the aspect ratio dictionary + /// + public override void PopulateAspectRatioDict() + { + var productSearchVM = ServiceLocator.Current.GetInstance(); + + aspectRatios = new List(); + if (productSearchVM.Products == null) + { + return; + } + + renderedProducts = productSearchVM.Products; + foreach (ProductSearchModel ip in renderedProducts) + { + aspectRatios.Add(GetAspectRatio(ip.Width, ip.Height)); + } + + LowerBound = double.MaxValue; + UpperBound = double.MinValue; + } + + /// + /// Resizes the image row + /// + /// The index of the collection to start resizing + /// The number of images to resize + /// The new height of the image resized + protected override void SizeRow(int startIndex, int numImages, double newHeight) + { + for (int i = startIndex; i < renderedProducts.Count && i < startIndex + numImages; ++i) + { + renderedProducts[i].Width = newHeight * aspectRatios[i]; + renderedProducts[i].Height = newHeight + heightOffset; + + if (renderedProducts[i].Width < ResizeDisplayThreshhold) + { + ResizeDisplayThreshhold = renderedProducts[i].Width; + } + } + } + } +} diff --git a/SnipInsight/AIServices/AIViewModels/ProductSearchViewModel.cs b/SnipInsight/AIServices/AIViewModels/ProductSearchViewModel.cs new file mode 100644 index 0000000..75bad1a --- /dev/null +++ b/SnipInsight/AIServices/AIViewModels/ProductSearchViewModel.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using GalaSoft.MvvmLight; +using GalaSoft.MvvmLight.Command; +using SnipInsight.AIServices.AILogic; +using SnipInsight.AIServices.AIModels; +using SnipInsight.ImageCapture; +using SnipInsight.Properties; +using SnipInsight.ViewModels; +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows; + +namespace SnipInsight.AIServices.AIViewModels +{ + /// + /// ViewModel for the Image Search, simple array string + /// The result is a string containing the text + /// + public class ProductSearchViewModel : ViewModelBase + { + public ProductSearchViewModel() + { + _handler = new ProductSearchHandler("ImageSearch"); + + // Commands initialization + ProductSelected = new RelayCommand(ProductSelectedExecute, ProductSelectedCanExecute); + NavigateToUrl = new RelayCommand(NavigateToUrlExecute, NavigateToUrlCanExecute); + } + + /// + /// Current wrap panel width + /// + private double currentWrapPanelWidth = 0; + + /// + /// Object used for dynamic image resizing + /// + private readonly ProductDynamicDisplay DynamicProductResizer = new ProductDynamicDisplay(); + + /// + /// Max height of images in the panel + /// + private const double ImageMaxHeight = 175; + + /// + /// The defined width for wrap panel + /// + private double wrapPanelDefinedWidth; + + private ObservableCollection products; + + /// + /// The current width for wrap panel + /// + public double CurrentWrapPanelWidth + { + get { return currentWrapPanelWidth; } + set + { + currentWrapPanelWidth = value; + if (currentWrapPanelWidth >= DynamicProductResizer.CenterBound) + { + WrapPanelDefinedWidth = currentWrapPanelWidth; + } + DynamicProductResizer.CreateControlGallery(ImageMaxHeight, currentWrapPanelWidth); + } + } + + /// + /// The defined width for wrap panel + /// + public double WrapPanelDefinedWidth + { + get { return currentWrapPanelWidth; } + set + { + wrapPanelDefinedWidth = value; + RaisePropertyChanged(); + } + } + + /// + /// List of the products displayed on screen + /// + public ObservableCollection Products + { + get { return products; } + set + { + products = value; + RaisePropertyChanged(); + } + } + + public async Task LoadProducts(MemoryStream imageStream) + { + Products = null; + + IsVisible = Visibility.Collapsed; + + var model = await _handler.GetResult(imageStream); + + if (model != null && model.Container != null && model.Container.Products != null) + { + Products = new ObservableCollection(model.Container.Products); + IsVisible = Visibility.Visible; + DynamicProductResizer.PopulateAspectRatioDict(); + DynamicProductResizer.CreateControlGallery(ImageMaxHeight, currentWrapPanelWidth); + } + + ServiceLocator.Current.GetInstance().ProductSearchCommand.RaiseCanExecuteChanged(); + } + + #region Commands + public RelayCommand ProductSelected { get; set; } + + public RelayCommand NavigateToUrl { get; set; } + #endregion + + #region CommandsCanExecute + private bool ProductSelectedCanExecute(ProductSearchModel model) + { + return true; + } + + private bool NavigateToUrlCanExecute(ProductSearchModel model) + { + return true; + } + #endregion + + #region CommandsExecute + private void ProductSelectedExecute(ProductSearchModel model) + { + SnipInsightViewModel viewModel = AppManager.TheBoss.ViewModel; + var aiPanelVM = ServiceLocator.Current.GetInstance(); + + AppManager.TheBoss.OnSaveImage(); + + if (string.IsNullOrEmpty(viewModel.RestoreImageUrl)) + { + viewModel.RestoreImageUrl = viewModel.SavedCaptureImage; + } + + ImageLoader.LoadFromUrl(new Uri(model.ContentUrl)).ContinueWith(t => + { + Application.Current.Dispatcher.Invoke(() => + { + aiPanelVM.CapturedImage = t.Result; + viewModel.SelectedImageUrl = model.ContentUrl; + AppManager.TheBoss.RunAllInsights(); + }); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + + ImageLoader.LoadFromUrl(new Uri(model.ContentUrl)).ContinueWith(t => + { + MessageBox.Show(Resources.Image_Not_Loaded); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + + private void NavigateToUrlExecute(ProductSearchModel model) + { + try + { + Process.Start(model.HostPage); + } + catch (Win32Exception CaughtException) + { + MessageBox.Show(Resources.No_Browser); + Console.WriteLine(CaughtException.Message); + } + } + #endregion + + private ProductSearchHandler _handler; + + private Visibility _isVisible = Visibility.Collapsed; + + public Visibility IsVisible + { + get => _isVisible; + + set + { + _isVisible = value; + + RaisePropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/SnipInsight/AIServices/CloudService.cs b/SnipInsight/AIServices/CloudService.cs new file mode 100644 index 0000000..833d771 --- /dev/null +++ b/SnipInsight/AIServices/CloudService.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Util; +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace SnipInsight.AIServices +{ + /// + /// Logic for the API calls of the Azure CloudServices + /// + /// Model used to represent the data from the API response + /// Result type that should be displayed to the screen + public class CloudService + { + /// + /// Client used for the request + /// + protected HttpClient CloudServiceClient; + + protected String Host { get; set; } + + protected String Endpoint { get; set; } + + protected String RequestParams { get; set; } + + /// + /// URI of the request + /// + protected Uri URI { get; set; } + + protected String Key { get; set; } + + /// + /// API Identifier + /// + protected String ServiceName { get; set; } + + /// + /// Indicates the confidence in the result, ranges from 0.0 to 1.0 + /// + public double Confidence { get; set; } + + /// + /// Generic base class for all the AI azure calss + /// + /// API Key + /// HTTP client for making the call + protected CloudService(string keyFile, HttpClient client = null) + { + RetrieveKey(keyFile); + + Host = "westcentralus.api.cognitive.microsoft.com"; + CloudServiceClient = client ?? new HttpClient(); + CloudServiceClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Key); + } + + /// + /// Number of total retries in case of request failure + /// + private const int _retryCount = 6; + + /// + /// Delay in ms between each retry + /// + private const int _retryDelay = 500; + + /// + /// Returns the result of the API call + /// + /// Captured image used for the call + /// object of httpclient to be used for the request + /// Data extracted from successful API response, default in case of failure + public async Task GetResult(MemoryStream stream) + { + Confidence = 0.0; + + try + { + var result = await Run(stream); + return ExtractResult(await result.Content.ReadAsStringAsync()); + } + catch (Exception e) + { + Debug.WriteLine(e.Message); + return default(TModel); + } + } + + /// + /// Run the call and get the response message + /// + /// Captured image used for the call + /// The HttpResponseMessage containing the Json result + protected virtual async Task Run(MemoryStream stream) + { + if (URI == null) + { + this.BuildURI(); + } + + using (var content = new StreamContent(stream)) + { + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + + // Execute the REST API call. + var result = await RequestAndRetry(() => CloudServiceClient.PostAsync(URI, content)); + + return result; + } + } + + /// + /// Run the function and retry if it fails + /// + /// Action to retry in case of failure + /// The response message containing the result + protected async Task RequestAndRetry(Func> action) + { + int retriesLeft = _retryCount; + int delay = _retryDelay; + HttpResponseMessage response = null; + + while (retriesLeft > 0) + { + response = await action(); + if ((int)response.StatusCode != 429) + break; + + await Task.Delay(delay); + retriesLeft--; + delay *= 2; + } + + return response; + } + + /// + /// Extract useful information from the json result in http response message. + /// + /// Json from the API call + /// Result in a format specific to each call + protected virtual TModel ExtractResult(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return default(TModel); + } + + return JsonConvert.DeserializeObject(json); + } + + /// + /// Build the URI for the API request + /// + protected void BuildURI() + { + URI = new UriBuilder + { + Scheme = "https", + Host = this.Host, + Path = this.Endpoint, + Query = this.RequestParams + }.Uri; + } + + private void RetrieveKey(string keyFile) + { + Key = UserSettings.GetKey(keyFile); + + if (!string.IsNullOrWhiteSpace(Key)) + { + Debug.WriteLine(keyFile+" API Key not configured in setting"); ; + } + } + } +} diff --git a/SnipInsight/App.config b/SnipInsight/App.config new file mode 100644 index 0000000..668674d --- /dev/null +++ b/SnipInsight/App.config @@ -0,0 +1,57 @@ + + + + +
+ + +
+ + + + + + + + + https://mix.office-int.com/api/log + + + https://mix.office-int.com/ + + + https://mix.office-int.com/api/tools + + + + + + + + + + + + + + + + + + + + + + + + + + + 0, 0 + + + + + + + diff --git a/SnipInsight/App.xaml b/SnipInsight/App.xaml new file mode 100644 index 0000000..cb1a8d8 --- /dev/null +++ b/SnipInsight/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/SnipInsight/App.xaml.cs b/SnipInsight/App.xaml.cs new file mode 100644 index 0000000..4b59feb --- /dev/null +++ b/SnipInsight/App.xaml.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.ViewModels; +using System.Windows; + +namespace SnipInsight +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + void Application_Startup(object sender, StartupEventArgs e) + { + ViewModelLocator locator = (ViewModelLocator)Application.Current.Resources["Locator"]; + + Telemetry.ApplicationLogger.Instance.Initialize(); + Telemetry.ApplicationLogger.Instance.SubmitEvent(Telemetry.EventName.SnipApplicationInitialized); + AppManager.TheBoss.Run(e.Args); + } + } +} diff --git a/SnipInsight/AppManager.cs b/SnipInsight/AppManager.cs new file mode 100644 index 0000000..713c01f --- /dev/null +++ b/SnipInsight/AppManager.cs @@ -0,0 +1,2147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using Microsoft.Win32; +using Microsoft.WindowsAPICodePack.Shell; +using SnipInsight.AIServices; +using SnipInsight.AIServices.AILogic; +using SnipInsight.AIServices.AIModels; +using SnipInsight.ClipboardUtils; +using SnipInsight.Conversion; +using SnipInsight.EmailController; +using SnipInsight.ImageCapture; +using SnipInsight.Package; +using SnipInsight.Properties; +using SnipInsight.SendTo; +using SnipInsight.StateMachine; +using SnipInsight.Util; +using SnipInsight.ViewModels; +using SnipInsight.Views; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Configuration; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; +using System.Windows.Threading; + +namespace SnipInsight +{ + internal class AppManager : IDisposable + { + #region Enumerations + + /// + /// Enumeration for each of the navbar buttons + /// + private enum NavBarButtons + { + Editor, + AIPanel, + Library, + Settings, + } + + #endregion + + static internal AppManager TheBoss { get; } = new AppManager(); + + #region Member variables + readonly SnipInsightsManager _snipInsightsManager; + IImageCaptureManager _imageCapture; + Mutex _singleInstanceMutex; + readonly string _singleInstanceMutexName = string.Format("{0}_SingleInstanceMutex_2E07A16C-8329-4577-94CA-3318635DBFDD", Assembly.GetExecutingAssembly().GetName().Name); + EventWaitHandle _instanceLaunchedMonitorEvent; + readonly string _instanceLaunchedMonitorEventName = string.Format("{0}_InstanceLaunchedMonitorEvent_B84FC2F6-3AEA-486D-99CC-70886D00941E", Assembly.GetExecutingAssembly().GetName().Name); + ManualResetEvent _backgroundTaskStopEvent; + + // cmd line + string[] _cmdLineArgs; + bool _startShy; + + private bool _loadedSnipInsights; + + /// + /// Stores the currently disabled button + /// + private NavBarButtons? disabledNavButton = null; + private ContentModerationHandler contentModerationHandler = new ContentModerationHandler("ContentModerator"); + #endregion + + #region Properties + + internal SnipInsightViewModel ViewModel + { + get; + private set; + } + + internal AIManager AiCoreManager + { + get; + private set; + } + + internal MainWindow MainWindow + { + get; + private set; + } + + internal ToolWindow ToolWindow + { + get; + private set; + } + + internal FirstRunWindow FirstRunWindow + { + get; + private set; + } + + internal TrayIcon TrayIcon { get; private set; } + + /// + /// Image metadata used by application, obtained from image analysis API + /// + internal ImageAnalysisResult ImageMetadata { get; set; } + + #endregion + + AppManager() + { + EnsureValidUserConfig(); + + Resources.Culture = System.Globalization.CultureInfo.CurrentCulture; + Dictionary actions = new Dictionary + { + { ActionNames.CreateMainWindow, WrapException(CreateMainWindow)}, + { ActionNames.ShowMainWindow, WrapException(ShowMainWindow)}, + { ActionNames.HideMainWindow, WrapException(HideMainWindow)}, + { ActionNames.CloseMainWindow, WrapException(CloseMainWindow)}, + { ActionNames.RestoreImage, WrapException(RestoreImage)}, + { ActionNames.RestoreLibrary, WrapException(RestoreLibrary)}, + { ActionNames.RestoreSettings, WrapException(RestoreSettings)}, + { ActionNames.RestoreMainWindow, WrapException(RestoreMainWindow)}, + { ActionNames.ShowToolWindow, WrapException(ShowToolWindow)}, + { ActionNames.ShowToolWindowShy, WrapException(ShowToolWindowShy)}, + { ActionNames.HideToolWindow, WrapException(HideToolWindow)}, + { ActionNames.CloseFirstRunWindow, WrapException(CloseFirstRunWindow)}, + { ActionNames.InitializeCaptureImage, WrapException(InitializeCaptureImage)}, + { ActionNames.StartCaptureScreen, WrapException(StartCaptureImage)}, + { ActionNames.ShowLibraryPanel, WrapAsyncException(ShowLibraryPanel)}, + { ActionNames.HideLibraryPanel, WrapException(HideLibraryPanel)}, + { ActionNames.ShowSettingsPanel, WrapException(ShowSettingsPanel)}, + { ActionNames.HideSettingsPanel, WrapException(HideSettingsPanel)}, + { ActionNames.StartQuickCapture, WrapException(InitializeQuickCapture)}, + { ActionNames.CloseImageCapture, WrapException(CloseImageCapture)}, + { ActionNames.Exit, WrapException(OnExit)}, + { ActionNames.SaveImage, WrapException(OnSaveImage)}, + { ActionNames.SaveImageWithDialog, WrapException(OnSaveImageWithDialog)}, + { ActionNames.ShareEmailWithImage, WrapException(OnShareEmailWithImage)}, + { ActionNames.ShareSendToOneNoteWithImage, WrapException(OnShareSendToOneNoteWithImage)}, + { ActionNames.CopyWithImage, WrapException(OnCopyWithImage)}, + { ActionNames.ClearOldImageData, WrapException(ClearOldImageData)}, + { ActionNames.Delete, WrapAsyncException(OnDelete)}, + { ActionNames.DeleteLibraryItems, WrapAsyncException(OnDeleteLibraryItems)}, + { ActionNames.CleanFiles, WrapException(OnCleanFiles)}, + { ActionNames.ShowImageCapturedToastMessage, WrapException(ShowImageCapturedToastMessage)}, + { ActionNames.LoadImageFromLibary, WrapException(LoadImageFromLibrary)}, + { ActionNames.SaveMainWindowState, WrapException(SaveMainWindowState)}, + { ActionNames.ShowEditorWindowTour, WrapException(ShowEditorWindowTour)}, + { ActionNames.OpenAIPanel, WrapException(OnShowHideAIPanel)}, + { ActionNames.RunAllInsights, WrapException(RunAllInsights)} + }; + + ViewModel = new SnipInsightViewModel(actions) + { + EraserCommand = new DelegateCommand(OnEraser), + EraseAllCommand = new DelegateCommand(OnEraseAll), + UndoCommand = new DelegateCommand(OnUndo), + RedoCommand = new DelegateCommand(OnRedo), + ToggleLibraryCommand = new DelegateCommand(OnShowHideLibrary), + ToggleSettingsCommand = new DelegateCommand(OnShowHideSettings), + ToggleEditorCommand = new DelegateCommand(OnShowHideEditor), + ToggleAIPanelCommand = new DelegateCommand(OnShowHideAIPanel), + + // Commands for action buttons in ai panel + RestoreImageCommand = new DelegateCommand(OnRestoreImage), + SaveImageCommand = new DelegateCommand(OnSaveImageWithDialog), + CopyImageCommand = new DelegateCommand(OnCopyWithImage), + ShareImageEmailCommand = new DelegateCommand(OnShareEmailWithImage), + ShareImageSendToOneNoteCommand = new DelegateCommand(OnShareSendToOneNoteWithImage), + RefreshAICommand = new DelegateCommand(OnRefreshAI) + }; + + AiCoreManager = new AIManager(); + + _snipInsightsManager = new SnipInsightsManager(); + + _snipInsightsManager.ImageSaved += _snipInsightsManager_ImageSaved; + _snipInsightsManager.ImageDeleted += _snipInsightsManager_ImageDeleted; + + _backgroundTaskStopEvent = new ManualResetEvent(false); + + TrayIcon = new TrayIcon(); + + ImageMetadata = new ImageAnalysisResult(); + } + + private static void EnsureValidUserConfig() + { + // Attempt to open the user's user.config file, or delete it file if we fail to open it. + // This is necessary because it is possible for the user.config file to get corrupted. + try + { + ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal); + } + catch (ConfigurationErrorsException e) + { + if (!string.IsNullOrEmpty(e.Filename) && e.Filename.EndsWith("user.config")) + { + try + { + File.Delete(e.Filename); + } + catch + { } + } + } + } + + void _snipInsightsManager_ImageDeleted(object sender, PackageArgs e) + { + var matchedItem = ViewModel.Packages.FirstOrDefault(x => x.Url == e.PackageUrl); + if (matchedItem != null) + { + ViewModel.Packages.Remove(matchedItem); + matchedItem.Dispose(); + ViewModel.SelectedPackage = null; // This could be pointing to some other content than the currently saved one. Reset it. + } + else + { + e.Thumbnail.Dispose(); + } + } + + void _snipInsightsManager_ImageSaved(object sender, PackageArgs e) + { + // Save should be a new one and NOT an existing one. If new one, consume it + if (ViewModel.Packages.FirstOrDefault(x => x.Url == e.PackageUrl) == null) + { + var link = new SnipInsightLink + { + Url = e.PackageUrl, + ImageStream = e.Thumbnail, + Duration = e.Duration, + HasMedia = e.HasMedia, + LastWriteTime = DateTime.Now, + HasPackage = false + }; + ViewModel.Packages.Insert(0, link); + ViewModel.SelectedPackage = null; // This could be pointing to some other content than the currently saved one. Reset it. + } + else + { + e.Thumbnail.Dispose(); + } + } + + public Action WrapException(Action action) + { + return () => + { + try + { + action(); + } + catch (Exception ex) + { + Debug.Fail("There was an exception when calling an action. Ex Message = ", ex.ToString()); + Diagnostics.LogException(ex); + } + }; + } + + public Action WrapAsyncException(Func asyncAction) + { + return async () => + { + try + { + await asyncAction(); + } + catch (Exception ex) + { + Debug.Fail("There was an exception when calling an action. Ex Message = ", ex.ToString()); + Diagnostics.LogException(ex); + } + }; + } + + /// + /// Used when there is no registry entry for appid. May be set to id from the first appid file from package folder. + /// + internal string _defaultAppIdForRegistry = Guid.NewGuid().ToString(); + + internal string DefaultAppIdForRegisry + { + get + { + return _defaultAppIdForRegistry; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~AppManager() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + IDisposable imageCaptureDispose = _imageCapture as IDisposable; + if (imageCaptureDispose != null) + { + imageCaptureDispose.Dispose(); + _imageCapture = null; + } + if (_singleInstanceMutex != null) + { + _singleInstanceMutex.Close(); + _singleInstanceMutex = null; + } + if (_instanceLaunchedMonitorEvent != null) + { + _instanceLaunchedMonitorEvent.Close(); + _instanceLaunchedMonitorEvent = null; + } + if (_backgroundTaskStopEvent != null) + { + _backgroundTaskStopEvent.Close(); + _backgroundTaskStopEvent = null; + } + if (TrayIcon != null) + { + TrayIcon.Dispose(); + TrayIcon = null; + } + } + + internal void Run(string[] args) + { + if (InstanceAlreadyRunning()) + { + // signal the already running instance that a new instance was launched + using (EventWaitHandle instanceLaunchedMonitorEvent = new EventWaitHandle(false, EventResetMode.AutoReset, _instanceLaunchedMonitorEventName)) + { + instanceLaunchedMonitorEvent.Set(); + } + + this.Dispose(); + Application.Current.Shutdown(); + } + else + { + StartMonitoringForNewInstances(); + var updated = UpdateVersion(); + ProcessCmdLine(args); + ShowToolWindow(!_startShy); + RegisterHotKeys(); + // Feature Out: Uncomment if you want to use the fist time cards feature + //ShowFirstRunWindow(updated); + } + } + + bool InstanceAlreadyRunning() + { + bool acquiredOwnership; + + _singleInstanceMutex = new Mutex(true, _singleInstanceMutexName, out acquiredOwnership); + + return !acquiredOwnership; + } + + void StartMonitoringForNewInstances() + { + new Thread(MonitorForNewInstancesThreadProc) + { + IsBackground = true // allow the process to terminate if this thread is still running + } + .Start(); + } + + void MonitorForNewInstancesThreadProc() + { + try + { + _instanceLaunchedMonitorEvent = new EventWaitHandle(false, EventResetMode.AutoReset, _instanceLaunchedMonitorEventName); + + while (true) + { + WaitHandle[] eventsToWaitOn = new WaitHandle[] { _instanceLaunchedMonitorEvent, _backgroundTaskStopEvent }; + + int signaledEventIndex = WaitHandle.WaitAny(eventsToWaitOn); + + switch (signaledEventIndex) + { + case 0: + // _instanceLaunchedMonitorEvent has been signaled, open the ToolWindow + if (Application.Current != null) + Application.Current.Dispatcher.Invoke(ShowToolWindow); + break; + case 1: + // _backgroundTaskStopEvent has been signaled, exit + return; + } + } + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + } + } + + string GetCmdLineArgsString() + { + StringBuilder cmdLineArgsSb = new StringBuilder(); + + foreach (string arg in _cmdLineArgs) + { + cmdLineArgsSb.Append(string.Format("{0} ", arg)); + } + + return cmdLineArgsSb.ToString().TrimEnd(); + } + + internal void RestartApp(bool killRunningInstance = false) + { + // release the single instance mutex so the new process can acquire ownership + _singleInstanceMutex.Close(); + _singleInstanceMutex = null; + + StopBackgroundTasks(); + + // start the new process with the existing cmd line + // launch from the install dir, not necessarily where the current process is running from + string processPath = UserSettings.AppPath; + string processArgs = GetCmdLineArgsString(); + Process.Start(processPath, processArgs); + + ExitApp(killRunningInstance); + } + + internal void ExitApp(bool killRunningInstance = false) + { + try + { + if (killRunningInstance) + { + Process.GetCurrentProcess().Kill(); + } + else + { + Application.Current.Dispatcher.Invoke(() => ViewModel.StateMachine.Fire(StateMachine.SnipInsightTrigger.Exit)); + } + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + } + } + + void StopBackgroundTasks() + { + if (_backgroundTaskStopEvent != null) + { + _backgroundTaskStopEvent.Set(); + } + } + + private bool UpdateVersion() + { + var appVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + if (String.Equals(appVersion, UserSettings.Version)) + { + return false; + } + + UserSettings.IsAIEnabled = false; + ViewModel.InsightsVisible = UserSettings.IsAIEnabled; + UserSettings.Version = appVersion; + Telemetry.ApplicationLogger.Instance.SubmitEvent(Telemetry.EventName.VersionChange); + return true; + } + + private void ShowFirstRunWindow(bool versionUpdated) + { + bool showFirstRun = versionUpdated && !UserSettings.DisableFirstRun; + + if (showFirstRun) + { + FirstRunWindow firstRunWindow = new FirstRunWindow(); + firstRunWindow.Show(); + FirstRunWindow = firstRunWindow; + firstRunWindow.Closed += (sender, args) => { FirstRunWindow = null; }; + + UserSettings.DisableFirstRun = true; // only show first run on the 'first run' + } + } + + internal void ShowToolWindow() + { + ShowToolWindow(true); + } + + internal void ShowToolWindowShy() + { + ShowToolWindow(false); + } + + private void ShowToolWindow(bool isOpen) + { + if (ToolWindow == null) + { + ToolWindow = new ToolWindow(); + } + + ToolWindow.ShowToolWindow(isOpen); + } + + internal void HideToolWindow() + { + if (ToolWindow != null) + { + ToolWindow.HideToolWindow(); + } + } + + internal void CloseFirstRunWindow() + { + var firstRunWindow = FirstRunWindow; + if (firstRunWindow != null) + { + firstRunWindow.CloseWindow(); + } + } + + internal void CreateMainWindow() + { + if (MainWindow == null) + { + MainWindow = new MainWindow(); + MainWindow.Loaded += OnLoaded; + ViewModel.Mode = Mode.Capturing; + } + } + + internal void ShowMainWindow() + { + var state = WindowState.Normal; + if (MainWindow != null && MainWindow.WindowState != WindowState.Minimized) + { + state = MainWindow.WindowState; + } + ShowMainWindowInternal(state); + } + + internal void ShowMainWindowInternal(WindowState? state = null) + { + CreateMainWindow(); + Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, + new Action(delegate() + { + MainWindow.Show(); + + if (state.HasValue) + { + MainWindow.WindowState = state.Value; + } + + MainWindow.Activate(); + })); + } + + internal void ShowEditorWindowTour() + { + bool showTour = !UserSettings.DisableEditorWindowTour; + + if (showTour && MainWindow != null) + { + UserSettings.DisableEditorWindowTour = true; + + MainWindow.ShowEditorTour(); + } + } + + internal void StopEditorWindowTour() + { + if (MainWindow != null) + { + bool isRunning = MainWindow.StopEditorTour(); + if (isRunning) + { + // They didn't finish to re-enable for next time + UserSettings.DisableEditorWindowTour = false; + } + } + } + + internal void ShowImageCapturedToastMessage() + { + ToastControl toast = new ToastControl(Resources.Message_CopiedToClipboard, 1000); + toast.ShowInMainWindow(); + } + + internal void CloseMainWindow() + { + if (MainWindow != null) + { + SwitchNavButton(null); + ViewModel.AIEnable = false; + ViewModel.EditorEnable = false; + + HideMainWindow(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.EditingWindowClosed); + } + } + + internal void HideMainWindow() + { + if (MainWindow != null) + { + MainWindow.Hide(); + } + } + + void ProcessCmdLine(string[] args) + { + _cmdLineArgs = args; + + foreach (string argRaw in args) + { + string arg = argRaw; + + if (arg.StartsWith("-") || arg.StartsWith("/")) + { + arg = arg.Substring(1); + + if (string.Compare(arg, "startshy", true) == 0) + { + _startShy = true; + } + } + } + } + + async Task ShowLibraryPanel() + { + if (MainWindow != null) + { + ViewModel.AIEnable = true; + SwitchNavButton(NavBarButtons.Library); + + MainWindow.OnShowLibrary(); + MainWindow.SizeToContent = SizeToContent.Manual; + ShowMainWindowInternal(); + + if (!_loadedSnipInsights) + { + _loadedSnipInsights = true; + await StartLoadingSnipInsights(); + } + } + } + + void ShowSettingsPanel() + { + // TODO: Make a shortcut for this panel + if (MainWindow != null) + { + MainWindow.OnShowSettings(); + // To Disable the Setting button when setting button is + //pressed to show enabled overlay + ViewModel.AIEnable = true; + SwitchNavButton(NavBarButtons.Settings); + ShowMainWindowInternal(); + } + } + + void HideSettingsPanel() + { + if (MainWindow != null) + { + MainWindow.OnHideSettings(); + } + } + + private async Task StartLoadingSnipInsights() + { + ViewModel.Packages.Clear(); + List files = _snipInsightsManager.GetAllSnipInsightFileInfos(); + const int batchSize = 25; + List batch; + int count = 0; + do + { + batch = files.GetRange(count, Math.Min(batchSize, files.Count - count)); + if (batch.Count > 0) + { + // Wait for one batch to finish before loading other batches. + await StartLoadingSnipInsightsBatch(batch).ConfigureAwait(false); + } + count += batch.Count; + } while (count < files.Count && batch.Count > 0); + } + + private async Task StartLoadingSnipInsightsBatch(List files) + { + var tasks = new Collection>(); + foreach (var file in files) + { + tasks.Add(LoadPackagesAsync(file)); + } + var packagesData = await Task.WhenAll(tasks).ConfigureAwait(false); + + if (Application.Current != null) + { + Application.Current.Dispatcher.Invoke(() => + { + foreach (var packageData in packagesData.Where(x => x != null)) + { + LoadSnipInsightLinkFromPackageData(packageData); + } + }); + } + } + + private void LoadSnipInsightLinkFromPackageData(PackageData packageData) + { + if (packageData != null) + { + var SnipInsightLink = new SnipInsightLink + { + Url = packageData.Url, + ImageStream = packageData.Thumbnail, + Duration = packageData.Duration, + LastWriteTime = packageData.LastWriteTime, + HasMedia = packageData.HasMedia, + HasPackage = packageData.IsPackage, + MixId = packageData.MixId + }; + ViewModel.Packages.Add(SnipInsightLink); + packageData.Thumbnail = null; + packageData.Dispose(); + } + } + + private async Task LoadPackagesAsync(FileInfo file) + { + try + { + var packageData = await _snipInsightsManager.GetPackageDataAsync(file); + return packageData; + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + return null; + } + } + + void HideLibraryPanel() + { + if (MainWindow != null) + { + MainWindow.OnHideLibrary(); + } + } + + #region Actions + public void OnEraser() + { + ViewModel.InkModeRequested = System.Windows.Controls.InkCanvasEditingMode.EraseByStroke; + ResetEditorButtons(EditorTools.Eraser); + } + + void OnEraseAll() + { + try + { + MainWindow.acetateLayer.InkCanvas.EraseAll(); + ViewModel.HasInk = false; + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + /// + /// Undo most recent edit action performed. + /// + void OnUndo() + { + try + { + MainWindow.acetateLayer.InkCanvas.Undo(); + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + /// + /// Redo the most recent action undone. + /// + void OnRedo() + { + try + { + MainWindow.acetateLayer.InkCanvas.Redo(); + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + internal void OnExit() + { + try + { + StopBackgroundTasks(); + + if (ToolWindow != null) + { + ToolWindow.ToolWindowClosedBySystem = true; + ToolWindow.Close(); + } + ToolWindow = null; + if (MainWindow != null) + { + MainWindow.MainWindowClosedBySystem = true; + MainWindow.Close(); + MainWindow = null; + } + + Dispose(); + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + private async Task OnDelete() + { + await Delete(ViewModel.SavedSnipInsightFile, ViewModel.SavedCaptureImage, ViewModel.SavedInkedImage, true); + } + + private async Task OnDeleteLibraryItems() + { + await DeleteLibraryItemsAsync(); + } + + public Task DeleteLibraryItemsAsync() + { + return DeleteAsync(ViewModel.SelectedLibraryItems, true, false); + } + + async public Task Delete(string SnipInsightFile, string savedCaptureImage, string savedInkedImage, bool raiseOutcomeTrigger) + { + try + { + int deletedIndex = GetCurrentContentIndexInLibrary(SnipInsightFile, savedCaptureImage, savedInkedImage); + Debug.Assert(deletedIndex != -1); + var deletedLink = ViewModel.Packages[deletedIndex]; + + var deleteConfirmed = ShowDeleteConfirmation(1, deletedLink.MixId != null); + + if (!deleteConfirmed) + { + if (raiseOutcomeTrigger) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionCancelled); + } + return; + } + + var success = await DeleteCore(SnipInsightFile, savedCaptureImage, savedInkedImage); + + if (success) + { + SelectPackageAfterDelete(deletedIndex, raiseOutcomeTrigger); + } + else + { + Diagnostics.LogTrace("Deletion failed."); + ToastControl toast = new ToastControl(Resources.Delete_Failed); + toast.ShowInMainWindow(); + if (raiseOutcomeTrigger) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionFailed); + } + } + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + ToastControl toast = new ToastControl(Resources.Delete_Failed); + toast.ShowInMainWindow(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionFailed); + } + } + + public async Task DeleteAsync(IEnumerable items, bool showConfirmation, bool raiseOutcomeTrigger) + { + ProgressControl progressMessage = null; + + // Filter out items that are already in the process of deleting! + items = items.Where(i => i.DeletionPending == false).ToArray(); + + int itemCount = items.Count(); + int failedCount = 0; + + if (itemCount == 0) + { + // There is nothing to do + return; + } + + try + { + // Mark all items as pending deletion + foreach (var item in items) + { + item.DeletionPending = true; + } + + // Confirmation + bool hasMixIds = items.Any(i => i.MixId != null); + + if (showConfirmation) + { + bool deleteConfirmed = ShowDeleteConfirmation(itemCount, hasMixIds); + + if (!deleteConfirmed) // No was clicked. + { + // Restore pending items + foreach (var item in items) + { + item.DeletionPending = false; + } + + if (raiseOutcomeTrigger) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionCancelled); + } + return; + } + } + + // + // Do the actual delete + // + + if (itemCount > 5 || hasMixIds) + { + // Only bother with the Progress Bar if we think it might be slow... + // A large number or we need to call the server... + + progressMessage = new ProgressControl(); + progressMessage.ShowInMainWindow(); + } + + // If the Selected Package was deleted, we'll keep track of it's location + int deletedSelectedPackageIndex = -1; + + int processedCount = 0; + + foreach (var item in items) + { + var deletedIndex = GetCurrentContentIndexInLibrary(item); + + var success = await DeleteCore(item); + + processedCount++; + + if (progressMessage != null) + { + progressMessage.SetProgress(processedCount / itemCount); + } + + if (success == true) + { + if (ViewModel.SelectedPackage != null && ViewModel.SelectedPackage.Url == item.Url) + { + // We just deleted the Selected Package! Remember this so we can choose a replacement + deletedSelectedPackageIndex = deletedIndex; + ViewModel.SelectedPackage = null; + } + } + else + { + failedCount++; + + // Restore the item so the user can try again + item.DeletionPending = false; + } + } + + if (progressMessage != null) + { + progressMessage.Dismiss(); + progressMessage = null; + } + + // + // Restore Selected Package + // + + if (deletedSelectedPackageIndex != -1) + { + // We deleted the SelectedPackage, so we need to restore it to something... + SelectPackageAfterDelete(deletedSelectedPackageIndex, raiseOutcomeTrigger); + } + else + { + if (raiseOutcomeTrigger) + { + // We don't currently have a state for DeletionCompleted, but Cancelled should + // leave us in the same place + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionCancelled); + } + } + + // + // Report any failures + // + + if (failedCount > 0) + { + Diagnostics.LogTrace(string.Format("Deletion failed for {0} item(s).", failedCount)); + ToastControl toast = new ToastControl(string.Format(Resources.Delete_Failed_List, failedCount)); + toast.ShowInMainWindow(); + if (raiseOutcomeTrigger) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionFailed); + } + + } + } + catch (Exception ex) + { + if (progressMessage != null) + { + progressMessage.Dismiss(); + progressMessage = null; + } + + Diagnostics.ReportException(ex); + ToastControl toast = new ToastControl(Resources.Delete_Failed); + toast.ShowInMainWindow(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.DeletionFailed); + } + finally + { + // + // Restore all items. + // This is okay because those that were deleted should already be out of the list by now. + // + + foreach (var item in items) + { + item.DeletionPending = false; + } + + } + } + + private bool ShowDeleteConfirmation(int itemCount, bool containsUploadedMixes) + { + string message; + + if (itemCount == 1) + { + message = Resources.Confirm_Delete; + } + else + { + message = string.Format(Resources.Confirm_Delete_List, itemCount); + } + + var twoButtonDialog = new TwoButtonDialog(message, "Yes", "No"); + twoButtonDialog.Owner = MainWindow; + + twoButtonDialog.ShowDialog(); + + return !twoButtonDialog.Button2Clicked; + } + + private int GetCurrentContentIndexInLibrary(SnipInsightLink link) + { + var packages = ViewModel.Packages; + + for (int i = 0; i < packages.Count; i++) + { + if (link.Url == ViewModel.Packages[i].Url) + { + return i; + } + } + + return -1; + } + + private int GetCurrentContentIndexInLibrary(string SnipInsightFile, string savedCaptureImage, string savedInkedImage) + { + if (string.IsNullOrEmpty(SnipInsightFile) && + string.IsNullOrEmpty(savedInkedImage) && + string.IsNullOrEmpty(savedCaptureImage)) + { + return -1; + } + for (int i = 0; i < ViewModel.Packages.Count; i++) + { + if (!string.IsNullOrEmpty(SnipInsightFile) && ViewModel.Packages[i].Url == SnipInsightFile) + { + return i; + } + if (!string.IsNullOrEmpty(savedInkedImage) && ViewModel.Packages[i].Url == savedInkedImage) + { + return i; + } + if (!string.IsNullOrEmpty(savedCaptureImage) && ViewModel.Packages[i].Url == savedCaptureImage) + { + return i; + } + } + return -1; + } + + + //TODO: Delete packages + private void SelectPackageAfterDelete(int deletedIndex, bool raiseOutcomeTrigger) + { + // Whatever was in deletedIndex was deleted and should now have the next item from the lib or if last item was deleted, it is outside lib. + if (deletedIndex >= ViewModel.Packages.Count) + { + deletedIndex = ViewModel.Packages.Count - 1; + } + if (deletedIndex == -1) + { + if (raiseOutcomeTrigger) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.WhiteboardForCurrentWindow); + } + } + else + { + var currentLink = ViewModel.Packages[deletedIndex]; + ViewModel.SelectedPackage = currentLink; + if (raiseOutcomeTrigger) + { + if (currentLink.HasPackage) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.LoadPackageFromLibrary); + } + else + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.LoadImageFromLibrary); + } + } + } + } + + private void OnAfterHidePanel() + { + if (ViewModel.SelectedPackage == null) + { + if (ViewModel.Packages.Count > 0) + { + ViewModel.SelectedPackage = ViewModel.Packages[0]; + } + else + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.WhiteboardForCurrentWindow); + return; + } + } + + if (Path.GetExtension(ViewModel.SelectedPackage.Url) == ".mixp") + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.LoadPackageFromLibrary); + } + else + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.LoadImageFromLibrary); + } + } + + private void OnBeforeShowPanel() + { + // If current image is whitEimage with no saved file, then do not select anything. Else, select what is currently loaded. + if (ViewModel.IsWhiteboardImage && ViewModel.SavedInkedImage == null && ViewModel.SavedSnipInsightFile == null) + { + ViewModel.SelectedPackage = null; + } + else + { + // The curent image can be one that is currently captured and worked on OR an item from library. Find matching one in packages. + // If user loads a lib item and then capure a new one, the selectedpackage is pointing to lib item. So, we need to select the + // item matching current capture. If item was loaded from library, then SelectedPackage will match but no harm to find match. + var currentUrl = ViewModel.SavedSnipInsightFile ?? + (ViewModel.SavedInkedImage ?? ViewModel.SavedCaptureImage); + ViewModel.SelectedPackage = ViewModel.Packages.FirstOrDefault(x => x.Url == currentUrl); + } + } + + /// + /// Changes the state to show the library panel + /// + void OnShowHideLibrary() + { + OnBeforeShowPanel(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.ShowLibraryPanel); + } + + /// + /// Changes the state to show the settings panel + /// + void OnShowHideSettings() + { + OnBeforeShowPanel(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.ShowSettingsPanel); + } + + /// + /// Changes the state to show the editor + /// + internal void OnShowHideEditor() + { + SwitchNavButton(NavBarButtons.AIPanel); + OnAfterHidePanel(); + } + + /// + /// Changes the state to show the AI panel + /// + internal void OnShowHideAIPanel() + { + OnBeforeShowPanel(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.ShowAIPanel); + } + + /// + /// Replace the highlighted button of the current panel selected + /// + /// NavBarButtons enum indicating the button to be set + void SwitchNavButton(NavBarButtons? button) + { + ReEnableNavBar(disabledNavButton, true); + disabledNavButton = button; + ReEnableNavBar(disabledNavButton, false); + } + + /// + /// Re-enables the previously disabled navbar button + /// + /// NavBarButtons enum indicating the button to be changed + /// True to enable a button, false to disable + void ReEnableNavBar(NavBarButtons? button, bool value) + { + switch(button) + { + case NavBarButtons.AIPanel: + ViewModel.AIEnable = value; + break; + case NavBarButtons.Editor: + ViewModel.EditorEnable = value; + break; + case NavBarButtons.Library: + ViewModel.LibraryEnable = value; + break; + case NavBarButtons.Settings: + ViewModel.SettingsEnable = value; + break; + default: + break; + } + } + + void LoadImageFromLibrary() + { + if (!string.IsNullOrEmpty(ViewModel.RestoreImageUrl)) + { + ViewModel.InkedImage = null; + ViewModel.CapturedImage = new BitmapImage(new Uri(ViewModel.SelectedImageUrl)); + ViewModel.SavedCaptureImage = ViewModel.RestoreImageUrl; + AiCoreManager.ImageBytes = BitmapToStream(ViewModel.CapturedImage).GetBuffer(); + ViewModel.IsWhiteboardImage = false; + } + //TODO: Delete package + else if (ViewModel.SelectedPackage != null) + { + ViewModel.InkedImage = null; + ViewModel.CapturedImage = SnipInsightLink.CreateBitmapSource(ViewModel.SelectedPackage.ImageStream); + ViewModel.SavedCaptureImage = ViewModel.SelectedPackage.Url; + AiCoreManager.ImageBytes = BitmapToStream(ViewModel.CapturedImage).GetBuffer(); + + // Since we don't store whiteboard image itself in file system, the images from lib should never be whiteboard image. + ViewModel.IsWhiteboardImage = false; + ViewModel.RestoreImageUrl = string.Empty; + if (!ViewModel.AIAlreadyRan) + { + RunAllInsights(); + } + ViewModel.AIAlreadyRan = false; + } + ViewModel.Mode = Mode.Captured; + MainWindow.SetInsightVisibility(Visibility.Visible); + } + + void OnCleanFiles() + { + try + { + if (!String.IsNullOrWhiteSpace(ViewModel.SavedSnipInsightFile)) + { + if (!String.IsNullOrWhiteSpace(ViewModel.SavedCaptureImage)) + { + _snipInsightsManager.DeleteImage(ViewModel.SavedCaptureImage); + } + if (!String.IsNullOrWhiteSpace(ViewModel.SavedInkedImage)) + { + _snipInsightsManager.DeleteImage(ViewModel.SavedInkedImage); + } + } + else + { + if (!String.IsNullOrWhiteSpace(ViewModel.SavedInkedImage)) + { + if (!String.IsNullOrWhiteSpace(ViewModel.SavedCaptureImage)) + { + _snipInsightsManager.DeleteImage(ViewModel.SavedCaptureImage); + } + } + } + } + catch (Exception ex) + { + // Deletion is best-effort + Diagnostics.LogLowPriException(ex); + } + } + + + public Task DeleteCore(SnipInsightLink SnipInsight) + { + bool isPackage = Path.GetExtension(SnipInsight.Url) == ".mixp"; + + if (isPackage) + { + return DeleteCore(SnipInsight.Url, null, null); + } + else + { + // I don't know how to detect if we have ink or not. + return DeleteCore(null, SnipInsight.Url, null); + } + } + + + /// + /// Delete files core. + /// + /// + public async Task DeleteCore(string SnipInsightFile, string savedCaptureImage, string savedInkedImage) + { + // delete the captured image. + if (!string.IsNullOrWhiteSpace(savedCaptureImage)) + { + try + { + _snipInsightsManager.DeleteImage(savedCaptureImage); + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + return false; + } + } + // delete the inked image. + if (!string.IsNullOrWhiteSpace(savedInkedImage)) + { + try + { + _snipInsightsManager.DeleteImage(savedInkedImage); + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + return false; + } + } + return true; + } + + internal void OnSaveImage() + { + try + { + if (!string.IsNullOrEmpty(ViewModel.RestoreImageUrl)) + { + ViewModel.SelectedPackage = ViewModel.Packages[0]; + } + // if captured image is already not saved, save it. + if (ViewModel.CapturedImage != null && !ViewModel.IsWhiteboardImage && string.IsNullOrEmpty(ViewModel.SavedCaptureImage)) + { + // Save the image. + using (MemoryStream captured = BitmapToStream(ViewModel.CapturedImage)) + { + ViewModel.SavedCaptureImage = _snipInsightsManager.SaveImage(captured); + } + } + // if acetate layer has ink -> save it. + // else delete the existing inked image. + if (MainWindow.acetateLayer.HasInk()) + { + if (MessageBoxResult.No == MessageBox.Show(Resources.Commit_Changes, + "Confirm", + MessageBoxButton.YesNo, + MessageBoxImage.Question)) + { + DeleteInkedImage(); + } + else + { + SaveInkedImage(); + string path = ViewModel.SavedInkedImage ?? ViewModel.SavedCaptureImage; + ViewModel.CapturedImage = new BitmapImage(new Uri(path)); + ViewModel.SavedInkedImage = null; + ViewModel.SavedCaptureImage = path; + ViewModel.SelectedPackage = ViewModel.Packages[0]; + } + } + else + { + DeleteInkedImage(); + } + MainWindow.AcetateLayer.InkCanvas.Clear(); + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + void OnSaveImageWithDialog() + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.SaveImageButton, Telemetry.ViewName.ActionRibbon); + string[] validPictureExtensions = { ".png", ".jpg", ".jpeg", ".bmp"}; + var invalidChars = string.Format(@"[{0}]+", (new string(Path.GetInvalidFileNameChars()))); + + try + { + if (ViewModel.CapturedImage == null && ViewModel.InkedImage == null) + { + return; + } + + // This is just a static image (possibly with ink) + SaveFileDialog dlg = new SaveFileDialog + { + DefaultExt = ".png", + Filter = "PNG image|*.png|JPEG image|*.jpg;*.jpeg|Bitmap image|*.bmp", + FileName = string.Format( + "snip_{0} {1}", + DateTimeOffset.Now.ToString(Resources.Culture.DateTimeFormat.ShortDatePattern), + DateTimeOffset.Now.ToString(Resources.Culture.DateTimeFormat.ShortTimePattern)) + }; + + if (UserSettings.IsAutoTaggingEnabled && ImageMetadata.CaptionAvailable) + dlg.FileName = string.Concat(dlg.FileName, "_", ImageMetadata.Caption); + + dlg.FileName = Regex.Replace(dlg.FileName, invalidChars, "-"); + + if (dlg.FileName.Length > 255) + { + dlg.FileName = dlg.FileName.Substring(0, 255); + } + + var lastDirectory = Settings.Default.LastSaveImageDirectory; + + if (!string.IsNullOrWhiteSpace(lastDirectory) && Directory.Exists(lastDirectory)) + { + dlg.InitialDirectory = lastDirectory; + } + else + { + dlg.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); + } + + if (dlg.ShowDialog() == true) + { + Settings.Default.LastSaveImageDirectory = Path.GetDirectoryName(dlg.FileName); + Settings.Default.Save(); + + if (!validPictureExtensions.Contains(Path.GetExtension(dlg.FileName).ToLowerInvariant())) + { + MessageBox.Show(string.Format("File saved in unrecognized image type: {0}", Path.GetExtension(dlg.FileName)), "Warning"); + } + + WriteNamedFile(dlg.FileName); + } + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + finally + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.SavingImageWithDialogCompleted); + } + } + + /// + /// Write image into a file as per user choice in save dialog + /// + /// string containing full path, name and extension + private void WriteNamedFile(string filePathName) + { + if (MainWindow.AcetateLayer.HasInk()) + { + ViewModel.InkedImage = PictureConverter.GenerateSnapshot(MainWindow.contentImage, MainWindow.acetateLayer.InkCanvas); + } + + BitmapSource bitmap = ViewModel.InkedImage ?? ViewModel.CapturedImage; + string extension = Path.GetExtension(filePathName).ToLowerInvariant(); + + using (var stream = File.OpenWrite(filePathName)) + { + switch (extension) + { + case ".jpg": + case ".jpeg": + PictureConverter.SaveToJpg(bitmap, stream); + break; + case ".bmp": + PictureConverter.SaveToBmp(bitmap, stream); + break; + case ".png": + default: + PictureConverter.SaveToPng(bitmap, stream); + break; + } + } + + if (UserSettings.IsAutoTaggingEnabled && (extension == ".jpg" || extension == ".jpeg")) + { + WriteMetadata(filePathName); + } + } + + /// + /// Write available metadata to files. + /// + /// string containing full path, name and extension + private void WriteMetadata(string filePathName) + { + using (var file = ShellFile.FromFilePath(filePathName)) + { + // If metadata is available, add it + if (ImageMetadata.TagsAvailable) + file.Properties.System.Keywords.Value = ImageMetadata.Tags; + + if (ImageMetadata.CaptionAvailable) + file.Properties.System.Title.Value = ImageMetadata.Caption; + } + } + + /// + /// Returns the result of user response for content moderation warning + /// + /// true if sharing is to be blocked, false if sharing is allowed + bool IsBlockedByContentModeration() + { + bool warning = false; + if (UserSettings.ContentModerationStrength == 0) + { + return false; + } + else if (UserSettings.ContentModerationStrength == 100) + { + warning = true; + } + + using (MemoryStream stream = BitmapToStream(ViewModel.CapturedImage)) + { + if (warning || contentModerationHandler.GetResult(stream)) + { + var result = MessageBox.Show(Resources.ShareModerateWarning, "Warning", MessageBoxButton.YesNo, + MessageBoxImage.Question); + if (result == MessageBoxResult.No) + { + return true; + } + } + } + return false; + } + + void OnShareEmailWithImage() + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.SaveImageEmailButton, Telemetry.ViewName.ActionRibbon); + try + { + if (IsBlockedByContentModeration()) + { + return; + } + + OnSaveImage(); + + string file = ViewModel.SavedCaptureImage; + if (!String.IsNullOrWhiteSpace(ViewModel.SavedInkedImage)) + { + file = ViewModel.SavedInkedImage; + } + + string subject = Resources.Sharing_Snip; + // TODO: Fix the attachment name + string attachmentName = "Capture.png"; + if (UserSettings.IsAutoTaggingEnabled && ImageMetadata.CaptionAvailable) + { + subject = string.Format(Resources.Sharing_Snip_Name, ImageMetadata.Caption); + attachmentName = string.Format("{0}.png",ImageMetadata.Caption); + if (attachmentName.Length > 255) + { + attachmentName = attachmentName.Substring(0, 255); + } + } + + // Try and open email client with attachment. + if (!EmailManager.OpenEmailClientWithEmbeddedImage(file, subject, attachmentName, "image/png")) + { + File.Delete(file); + bool success; + // Do Work + if (ViewModel.InkedImage != null) + { + success = ClipboardManager.Copy(ViewModel.InkedImage); + } + else if (ViewModel.CapturedImage != null) + { + success = ClipboardManager.Copy(ViewModel.CapturedImage); + } + else + { + throw new InvalidOperationException("no image to share"); + } + + if (success) + { + string mailToImageFormat = "mailto:?subject=" + subject + "&body=Capture copied to clipboard. please paste here"; + Process.Start(mailToImageFormat); + } + else + { + ToastControl toast = new ToastControl(Resources.Message_CopyToClipboardFailed); + toast.ShowInMainWindow(); + } + } + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + finally + { + // Trigger + ViewModel.StateMachine.Fire(SnipInsightTrigger.SharingWithImageCompleted); + } + } + + // TODO: Check which OneNote function is used + void OnShareSendToOneNoteWithImage() + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.ShareImageSendToOneNoteButton, Telemetry.ViewName.ActionRibbon); + try + { + if (IsBlockedByContentModeration()) + { + return; + } + + OnSaveImage(); + + string imageFilePath = ViewModel.SavedCaptureImage; + if (!String.IsNullOrWhiteSpace(ViewModel.SavedInkedImage)) + { + imageFilePath = ViewModel.SavedInkedImage; + } + + using (OneNoteManager oneNoteMgr = new OneNoteManager()) + { + bool? success = oneNoteMgr.InsertSnip(imageFilePath); + + if (success.HasValue) + { + ToastControl toast = new ToastControl(success.Value ? Properties.Resources.Message_SendToOneNote_Succeeded : Properties.Resources.Message_SendToOneNote_Failed, 3000); + toast.ShowInMainWindow(); + } + } + } + finally + { + // Trigger + ViewModel.StateMachine.Fire(SnipInsightTrigger.SharingWithImageCompleted); + } + } + + /// + /// Reload the actual snipped/edited image to the panel content + /// + void OnRestoreImage() + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.RestoreImageButton, Telemetry.ViewName.ActionRibbon); + ImageLoader.LoadFromUrl(new Uri(ViewModel.SavedCaptureImage)).ContinueWith(t => + { + Application.Current.Dispatcher.Invoke(() => + { + ServiceLocator.Current.GetInstance().CapturedImage = t.Result; + ViewModel.SavedCaptureImage = ViewModel.RestoreImageUrl; + ViewModel.RestoreImageUrl = string.Empty; + ViewModel.SelectedImageUrl = string.Empty; + RunAllInsights(); + }); + + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } + + /// + /// Save edits and make the API calls to refresh AI + /// + void OnRefreshAI() + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.RefreshAICommandButton, Telemetry.ViewName.ActionRibbon); + OnSaveImage(); + RunAllInsights(); + } + + void OnCopyWithImage() + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.CopyImageButton, Telemetry.ViewName.ActionRibbon); + try + { + // copy to clipboard. + bool success; + // Do Work + if (MainWindow.acetateLayer.HasInk()) + { + ViewModel.InkedImage = PictureConverter.GenerateSnapshot(MainWindow.contentImage, MainWindow.acetateLayer.InkCanvas); + success = ClipboardManager.Copy(ViewModel.InkedImage); + } + else if (ViewModel.CapturedImage != null) + { + success = ClipboardManager.Copy(ViewModel.CapturedImage); + } + else + { + throw new InvalidOperationException("no image to copy"); + } + ToastControl toast = new ToastControl(success ? Resources.Message_CopiedToClipboard : Resources.Message_CopyToClipboardFailed); + toast.ShowInMainWindow(); + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + finally + { + // Trigger + ViewModel.StateMachine.Fire(SnipInsightTrigger.CopyingWithImageCompleted); + } + } + #endregion + + #region Events + void OnLoaded(object sender, RoutedEventArgs e) + { + try + { + MainWindow.Loaded -= OnLoaded; + MainWindow.Closed += OnClosed; + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + private void OnClosed(object sender, EventArgs e) + { + var mainWindow = MainWindow; + } + #endregion + + #region ImageCapture + + /// + /// Prepare the capture manager for the snip + /// + void SetupImageCaptureManager() + { + var imageCaptureDispose = _imageCapture as IDisposable; + if (imageCaptureDispose != null) + { + imageCaptureDispose.Dispose(); + } + + _imageCapture = new ImageCaptureManager(); + } + + /// + /// Initialize a regular shot with a post-snip editor + /// + void InitializeCaptureImage() + { + SetupImageCaptureManager(); + _imageCapture.CaptureCompleted += ImageCaptureCompleted; + } + + /// + /// Initialize a quick snip with no editor + /// + void InitializeQuickCapture() + { + SetupImageCaptureManager(); + _imageCapture.CaptureCompleted += QuickCaptureCompleted; + } + + void SaveMainWindowState() + { + if (MainWindow != null) + { + _mainWindowVisibilityBeforeCapture = MainWindow.Visibility; + _mainWindowStateBeforeCapture = MainWindow.WindowState; + } + } + + private Visibility _mainWindowVisibilityBeforeCapture; + private WindowState _mainWindowStateBeforeCapture; + + internal void RestoreMainWindow() + { + ShowMainWindowInternal(_mainWindowStateBeforeCapture); + } + + internal void RestoreImage() + { + if (ViewModel.IsWhiteboardImage && ViewModel.InkedImage == null) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.RestoreWhiteboard); + return; + } + if (ViewModel.Packages.Count > 0) + { + if (ViewModel.SelectedPackage == null) // Indicated that current content was not loaded from library + { + ViewModel.SelectedPackage = ViewModel.Packages[0]; // Go back to the latest capture. This matches what the user was manipulating last. + } + LoadImageFromLibrary(); + ViewModel.StateMachine.Fire(SnipInsightTrigger.RestoreImage); + } + } + + internal void RestoreLibrary() + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.RestoreLibrary); + } + + internal void RestoreSettings() + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.RestoreSettings); + } + + void StartCaptureImage() + { + if (_imageCapture != null) + { + int delay = UserSettings.ScreenCaptureDelay; + if (delay > 0) + { + Thread.Sleep(delay * 1000); + } + _imageCapture.StartCapture(); + } + } + + void ClearOldImageData() + { + var viewModel = ViewModel; + if (viewModel != null) + { + viewModel.CapturedImage = null; + viewModel.InkedImage = null; + viewModel.SavedInkedImage = null; + viewModel.SavedCaptureImage = null; + viewModel.SavedSnipInsightFile = null; + viewModel.HasInk = false; + } + if (MainWindow != null) + { + MainWindow.acetateLayer.InkCanvas.Clear(); + } + } + + /// + /// General behaviour no matter the type of screenshot + /// + private bool GeneralCaptureCompleted(ImageCaptureEventArgs e) + { + // Set the image on window.content + if (e.Image == null) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.ImageCaptureCancelled); + return false; + } + + ClearOldImageData(); + ViewModel.CapturedImage = e.Image; + AiCoreManager.ImageBytes = BitmapToStream(e.Image).GetBuffer(); + + ViewModel.IsWhiteboardImage = false; + ViewModel.RestoreImageUrl = string.Empty; + MainWindow.SetInsightVisibility(Visibility.Visible); + + ViewModel.EditorEnable = true; + ViewModel.AIEnable = true; + SwitchNavButton(NavBarButtons.AIPanel); + + return true; + } + + /// + /// Take a screenshot of the screen but do not open the editor + /// + private void QuickCaptureCompleted(object sender, ImageCaptureEventArgs e) + { + if (!GeneralCaptureCompleted(e)) + { + return; + } + + if (UserSettings.IsNotificationToastEnabled) + { + // Post QuickSnip Toast + NotificationWindow toastNotification = new NotificationWindow(); + toastNotification.Show(); + } + + // Fire The QuickSnip trigger + ViewModel.StateMachine.Fire(SnipInsightTrigger.QuickSnip); + } + + /// + /// Take a screenshot of the screen and open the editor if specified + /// + private void ImageCaptureCompleted(object sender, ImageCaptureEventArgs e) + { + if (UserSettings.IsOpenEditorPostSnip && GeneralCaptureCompleted(e)) + { + ViewModel.StateMachine.Fire(SnipInsightTrigger.ImageCaptured); + } + else + { + QuickCaptureCompleted(sender, e); + } + } + + internal void CloseImageCapture() + { + IDisposable imageCaptureDispose = _imageCapture as IDisposable; + if (imageCaptureDispose != null) + { + imageCaptureDispose.Dispose(); + _imageCapture = null; + } + } + + + MemoryStream BitmapToStream(BitmapSource image) + { + MemoryStream capturedImage = PictureConverter.SaveToPng(image, new MemoryStream()); + capturedImage.Position = 0; + return capturedImage; + } + + #endregion + + #region AI Panel + public enum EditorTools + { + Pen = 1, + Highlighter = 2, + Eraser = 3, + } + public void ResetEditorButtons(EditorTools button) + { + if (button != EditorTools.Eraser) + { + ViewModel.EraserChecked = false; + } + if (button != EditorTools.Highlighter) + { + ViewModel.HighlighterChecked = false; + } + if (button != EditorTools.Pen) + { + ViewModel.PenChecked = false; + } + ViewModel.penSelected = false; + } + + /// + /// Runs all the image insights + /// + internal void RunAllInsights() + { + SwitchNavButton(NavBarButtons.AIPanel); + if (UserSettings.IsAIEnabled) + { + AiCoreManager.ImageBytes = BitmapToStream(ViewModel.CapturedImage).GetBuffer(); + AiCoreManager.RunAllAsyncCalls(); + + // Resets the scroll viewer back to the top after insights refresh + AppManager.TheBoss.MainWindow.VerticalScrollViewer.ScrollToTop(); + } + } + #endregion + + /// + /// The return value for operations (especialy service related) to help us give better error information. + /// + public class OperationResult + { + /// + /// Indicates the outcome of the operation. + /// + public bool Succeeded { get; set; } + } + + #region Share + async Task Publish(Action continueWith, string failureMessageForToast, bool embedCodeNeeded = false) + { + if (continueWith == null) + { + throw new ArgumentNullException("continueWith"); + } + if (string.IsNullOrWhiteSpace(ViewModel.SavedSnipInsightFile) || !File.Exists(ViewModel.SavedSnipInsightFile)) + { + ToastControl toast = new ToastControl("File not found!"); + toast.ShowInMainWindow(); + continueWith(new OperationResult { Succeeded = false }); + }; + } + #endregion + + #region Save + /// + /// Save inked image from the canvas. + /// + private void SaveInkedImage() + { + if (MainWindow == null || MainWindow.contentImage == null) + { + return; + } + // Generate the snapshot always. + ViewModel.InkedImage = PictureConverter.GenerateSnapshot(MainWindow.contentImage, MainWindow.acetateLayer.InkCanvas); + // delete the old image + if (!String.IsNullOrWhiteSpace(ViewModel.SavedInkedImage)) + { + _snipInsightsManager.DeleteImage(ViewModel.SavedInkedImage); + } + // save the inked image + using (MemoryStream ms = new MemoryStream()) + { + PictureConverter.SaveToPng(ViewModel.InkedImage, ms); + ms.Position = 0; + ViewModel.SavedInkedImage = _snipInsightsManager.SaveImage(ms); + } + } + #endregion + + #region Delete + private void DeleteInkedImage() + { + // delete the old image. + ViewModel.InkedImage = null; + if (!String.IsNullOrWhiteSpace(ViewModel.SavedInkedImage)) + { + _snipInsightsManager.DeleteImage(ViewModel.SavedInkedImage); + } + ViewModel.SavedInkedImage = null; + } + #endregion + + #region KeyBoard + /// + /// Register the hotkeys at the start of the application + /// + private void RegisterHotKeys() + { + try + { + ToolWindow.RegisterHotKey(SnipHotKey.ScreenCapture, UserSettings.ScreenCaptureShortcut); + ToolWindow.RegisterHotKey(SnipHotKey.QuickCapture, UserSettings.QuickCaptureShortcut); + ToolWindow.HotKeyPressed += ToolWindow_HotKeyPressed; + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + + /// + /// Handler for the hotkey press detection. Fire the correct trigger. + /// + /// + /// + void ToolWindow_HotKeyPressed(object sender, HotKeyPressedEventArgs e) + { + switch (e.KeyPressed) + { + case SnipHotKey.ScreenCapture: + ViewModel.StateMachine.Fire(SnipInsightTrigger.CaptureScreen); + break; + case SnipHotKey.QuickCapture: + ViewModel.StateMachine.Fire(SnipInsightTrigger.QuickSnip); + break; + } + } + #endregion + } +} \ No newline at end of file diff --git a/SnipInsight/ClipboardUtils/ClipboardManager.cs b/SnipInsight/ClipboardUtils/ClipboardManager.cs new file mode 100644 index 0000000..04e6e35 --- /dev/null +++ b/SnipInsight/ClipboardUtils/ClipboardManager.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Text; +using System.Windows; +using System.Windows.Media.Imaging; + +namespace SnipInsight.ClipboardUtils +{ + /// + /// Manager for all clipboard functionality. + /// + public static class ClipboardManager + { + #region Clipboard Html Formats Constants + /// + /// Header for clipboard Html format. + /// + private const string Header = @"Version:0.9 +StartHTML:<<<<<<<<1 +EndHTML:<<<<<<<<2 +StartFragment:<<<<<<<<3 +EndFragment:<<<<<<<<4 +StartSelection:<<<<<<<<3 +EndSelection:<<<<<<<<4 +SourceURL: {0} +"; + + /// + /// Html open tag. + /// + private const string HtmlOpen = "\r\n"; + + /// + /// Html close tag. + /// + private const string HtmlClose = "\r\n"; + + /// + /// Start fragment for the inner html. + /// + private const string StartFragment = ""; + + /// + /// End fragment for the inner html. + /// + private const string EndFragment = ""; + + /// + /// Link only html. + /// + private const string LinkHtml = "{1}"; + + /// + /// Image link html. + /// + private const string ImageLinkHtml = "

{0}"; + #endregion + + /// + /// Copy to clipboard. + /// + /// + public static bool Copy(BitmapSource image) + { + try + { + Clipboard.Clear(); + + IDataObject obj = new DataObject(); + + // Set the image onto clipboard. + if (image != null) + { + obj.SetData(DataFormats.Bitmap, image, false); + } + Clipboard.SetDataObject(obj, true); + return true; + } + catch + { + return false; + } + } + + /// + /// Copy text to clipboard. + /// + public static bool Copy(string text) + { + try + { + Clipboard.Clear(); + Clipboard.SetText(text); + return true; + } + catch + { + return false; + } + } + + #region Helpers + /// + /// Generate the clipboard html + /// + private static string GetClipboardHtml(string header, string html) + { + var sb = new StringBuilder(); + sb.Append(header); + sb.AppendLine(@""); + sb.Append(HtmlOpen); + sb.Append(StartFragment); + // Get the fragmentStart + int fragmentStart = GetByteCount(sb); + sb.Append(html); + int fragmentEnd = GetByteCount(sb); + sb.Append(EndFragment); + sb.Append(HtmlClose); + + // Back-patch offsets (scan only the header part for performance) + sb.Replace("<<<<<<<<1", header.Length.ToString("D9"), 0, header.Length); + sb.Replace("<<<<<<<<2", GetByteCount(sb).ToString("D9"), 0, header.Length); + sb.Replace("<<<<<<<<3", fragmentStart.ToString("D9"), 0, header.Length); + sb.Replace("<<<<<<<<4", fragmentEnd.ToString("D9"), 0, header.Length); + return sb.ToString(); + } + /// + /// Calculates the number of bytes produced by encoding the string in the string builder in UTF-8 and not .NET default string encoding. + /// + /// the string builder to count its string + /// optional: the start index to calculate from (default - start of string) + /// optional: the end index to calculate to (default - end of string) + /// the number of bytes required to encode the string in UTF-8 + private static int GetByteCount(StringBuilder sb, int start = 0, int end = -1) + { + char[] byteArray = new char[1]; + int count = 0; + end = end > -1 ? end : sb.Length; + for (int i = start; i < end; i++) + { + byteArray[0] = sb[i]; + count += Encoding.UTF8.GetByteCount(byteArray); + } + return count; + } + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriBorderedIconLabelButton.cs b/SnipInsight/Controls/Ariadne/AriBorderedIconLabelButton.cs new file mode 100644 index 0000000..58abaeb --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriBorderedIconLabelButton.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriBorderedIconLabelButton : AriIconLabelButton + { + static AriBorderedIconLabelButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriBorderedIconLabelButton), new FrameworkPropertyMetadata(typeof(AriBorderedIconLabelButton))); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriButtonBase.cs b/SnipInsight/Controls/Ariadne/AriButtonBase.cs new file mode 100644 index 0000000..3f7ef78 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriButtonBase.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + [TemplateVisualState(Name = "Shy", GroupName = "ShyStates")] + [TemplateVisualState(Name = "NotShy", GroupName = "ShyStates")] + public abstract class AriButtonBase : Button, IAriControl + { + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + OnIsShyChanged(IsShy, false); + } + + #region IShy + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(AriButtonBase), new PropertyMetadata(false, OnIsShyChangedStatic)); + + protected virtual void OnIsShyChanged(bool value, bool useTransitions = true) + { + VisualStateManager.GoToState(this, value ? "Shy" : "NotShy", useTransitions); + } + + private static void OnIsShyChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriButtonBase; + + if (self != null) + { + self.OnIsShyChanged((bool)e.NewValue); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriCircleIconButton.cs b/SnipInsight/Controls/Ariadne/AriCircleIconButton.cs new file mode 100644 index 0000000..1a4c3ec --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriCircleIconButton.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriCircleIconButton : AriIconLabelButton + { + static AriCircleIconButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriCircleIconButton), new FrameworkPropertyMetadata(typeof(AriCircleIconButton))); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriDialogButton.cs b/SnipInsight/Controls/Ariadne/AriDialogButton.cs new file mode 100644 index 0000000..8ad6263 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriDialogButton.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriDialogButton : AriButtonBase + { + static AriDialogButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriDialogButton), new FrameworkPropertyMetadata(typeof(AriDialogButton))); + } + + } +} diff --git a/SnipInsight/Controls/Ariadne/AriEraseRadiobutton.cs b/SnipInsight/Controls/Ariadne/AriEraseRadiobutton.cs new file mode 100644 index 0000000..93529f0 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriEraseRadiobutton.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriEraseRadioButton : AriRadioButtonBase + { + static AriEraseRadioButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriEraseRadioButton), new FrameworkPropertyMetadata(typeof(AriEraseRadioButton))); + } + + #region Label + + public string Label + { + get { return GetValue(LabelProperty) as string; } + set { SetValue(LabelProperty, value); } + } + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(AriEraseRadioButton), new PropertyMetadata(null, OnLabelChangedStatic)); + + protected virtual void OnLabelChanged(string value) + { + if (ToolTip == null) + { + ToolTip = value; + } + + SetValue(System.Windows.Automation.AutomationProperties.HelpTextProperty, value); + } + + private static void OnLabelChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriEraseRadioButton; + + if (self != null) + { + self.OnLabelChanged(e.NewValue as string); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriFirstRunCard.cs b/SnipInsight/Controls/Ariadne/AriFirstRunCard.cs new file mode 100644 index 0000000..9e108d6 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriFirstRunCard.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriFirstRunCard : Control + { + static AriFirstRunCard() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriFirstRunCard), new FrameworkPropertyMetadata(typeof(AriFirstRunCard))); + } + + #region Image + + public object Image + { + get { return GetValue(ImageProperty); } + set { SetValue(ImageProperty, value); } + } + + public static readonly DependencyProperty ImageProperty = + DependencyProperty.Register("Image", typeof(object), typeof(AriFirstRunCard), new PropertyMetadata(null)); + + #endregion + + #region Heading + + public string Heading + { + get { return GetValue(HeadingProperty) as string; } + set { SetValue(HeadingProperty, value); } + } + + public static readonly DependencyProperty HeadingProperty = + DependencyProperty.Register("Heading", typeof(string), typeof(AriFirstRunCard), new PropertyMetadata(null)); + + #endregion + + #region Message + + public string Message + { + get { return GetValue(MessageProperty) as string; } + set { SetValue(MessageProperty, value); } + } + + public static readonly DependencyProperty MessageProperty = + DependencyProperty.Register("Message", typeof(string), typeof(AriFirstRunCard), new PropertyMetadata(null)); + + #endregion + + public void FlyOn() + { + Visibility = Visibility.Visible; + IsEnabled = true; + } + + public void FlyOff() + { + IsEnabled = false; + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriIcon.cs b/SnipInsight/Controls/Ariadne/AriIcon.cs new file mode 100644 index 0000000..10fe803 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriIcon.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriIcon : Control + { + static AriIcon() + { + FocusableProperty.OverrideMetadata(typeof(AriIcon), new FrameworkPropertyMetadata(false)); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriIconLabel.cs b/SnipInsight/Controls/Ariadne/AriIconLabel.cs new file mode 100644 index 0000000..73541d5 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriIconLabel.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriIconLabel : Control + { + static AriIconLabel() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriIconLabel), new FrameworkPropertyMetadata(typeof(AriIconLabel))); + } + + #region Label + + public string Label + { + get { return GetValue(LabelProperty) as string; } + set { SetValue(LabelProperty, value); } + } + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(AriIconLabel), new PropertyMetadata(null)); + + #endregion + + #region LabelPadding + + public Thickness LabelPadding + { + get { return (Thickness)GetValue(LabelPaddingProperty); } + set { SetValue(LabelPaddingProperty, value); } + } + + public static readonly DependencyProperty LabelPaddingProperty = + DependencyProperty.Register("LabelPadding", typeof(Thickness), typeof(AriIconLabel), new PropertyMetadata(new Thickness())); + + #endregion + + #region TextWrapping + + public TextWrapping TextWrapping + { + get { return (TextWrapping)GetValue(TextWrappingProperty); } + set { SetValue(TextWrappingProperty, value); } + } + + public static readonly DependencyProperty TextWrappingProperty = + DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(AriIconLabel), new PropertyMetadata(TextWrapping.NoWrap)); + + #endregion + + #region ShowLabel + + public bool ShowLabel + { + get { return (bool)GetValue(ShowLabelProperty); } + set { SetValue(ShowLabelProperty, value); } + } + + public static readonly DependencyProperty ShowLabelProperty = + DependencyProperty.Register("ShowLabel", typeof(bool), typeof(AriIconLabel), new PropertyMetadata(true)); + + #endregion + + #region Icon + + [BindableAttribute(true)] + public object Icon + { + get { return GetValue(IconProperty) as string; } + set { SetValue(IconProperty, value); } + } + + public static readonly DependencyProperty IconProperty = + DependencyProperty.Register("Icon", typeof(object), typeof(AriIconLabel), new PropertyMetadata(null)); + + #endregion + + #region IconPadding + + public Thickness IconPadding + { + get { return (Thickness)GetValue(IconPaddingProperty); } + set { SetValue(IconPaddingProperty, value); } + } + + public static readonly DependencyProperty IconPaddingProperty = + DependencyProperty.Register("IconPadding", typeof(Thickness), typeof(AriIconLabel), new PropertyMetadata(new Thickness())); + + #endregion + + #region Orientation + + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register("Orientation", typeof(Orientation), typeof(AriIconLabel), new PropertyMetadata(Orientation.Vertical)); + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriIconLabelButton.cs b/SnipInsight/Controls/Ariadne/AriIconLabelButton.cs new file mode 100644 index 0000000..4baf503 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriIconLabelButton.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriIconLabelButton : AriButtonBase + { + static AriIconLabelButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriIconLabelButton), new FrameworkPropertyMetadata(typeof(AriIconLabelButton))); + } + + #region Label + + public string Label + { + get { return GetValue(LabelProperty) as string; } + set { SetValue(LabelProperty, value); } + } + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(AriIconLabelButton), new PropertyMetadata(null, OnLabelChangedStatic)); + + protected virtual void OnLabelChanged(string value) + { + if (ToolTip == null) + { + ToolTip = value; + } + + SetValue(System.Windows.Automation.AutomationProperties.HelpTextProperty, value); + } + + private static void OnLabelChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriIconLabelButton; + + if (self != null) + { + self.OnLabelChanged(e.NewValue as string); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriIconLabelMenuItem.cs b/SnipInsight/Controls/Ariadne/AriIconLabelMenuItem.cs new file mode 100644 index 0000000..e1a69ac --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriIconLabelMenuItem.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriIconLabelMenuItem : AriMenuItemBase + { + public AriIconLabelMenuItem() + { + ToolTipService.SetPlacement(this, System.Windows.Controls.Primitives.PlacementMode.Bottom); + ToolTipService.SetInitialShowDelay(this, 1000); + } + + static AriIconLabelMenuItem() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriIconLabelMenuItem), new FrameworkPropertyMetadata(typeof(AriIconLabelMenuItem))); + } + + #region Label + + public string Label + { + get { return GetValue(LabelProperty) as string; } + set { SetValue(LabelProperty, value); } + } + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(AriIconLabelMenuItem), new PropertyMetadata(null, OnLabelChangedStatic)); + + protected virtual void OnLabelChanged(string value) + { + Header = value; + RecomputeToolTip(); + SetValue(System.Windows.Automation.AutomationProperties.HelpTextProperty, value); + } + + private static void OnLabelChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriIconLabelMenuItem; + + if (self != null) + { + self.OnLabelChanged(e.NewValue as string); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriIconLabelToggleButton.cs b/SnipInsight/Controls/Ariadne/AriIconLabelToggleButton.cs new file mode 100644 index 0000000..c643d5e --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriIconLabelToggleButton.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriIconLabelToggleButton : AriToggleButtonBase + { + static AriIconLabelToggleButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriIconLabelToggleButton), new FrameworkPropertyMetadata(typeof(AriIconLabelToggleButton))); + } + + #region Label + + public string Label + { + get { return GetValue(LabelProperty) as string; } + set { SetValue(LabelProperty, value); } + } + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(AriIconLabelToggleButton), new PropertyMetadata(null, OnLabelChangedStatic)); + + protected virtual void OnLabelChanged(string value) + { + if (ToolTip == null) + { + ToolTip = value; + } + + SetValue(System.Windows.Automation.AutomationProperties.HelpTextProperty, value); + } + + private static void OnLabelChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriIconLabelToggleButton; + + if (self != null) + { + self.OnLabelChanged(e.NewValue as string); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriIconSmallButton.cs b/SnipInsight/Controls/Ariadne/AriIconSmallButton.cs new file mode 100644 index 0000000..8a73b36 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriIconSmallButton.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Controls.Ariadne +{ + class AriIconSmallButton : AriButtonBase + { + + } +} diff --git a/SnipInsight/Controls/Ariadne/AriInkRadioButton.cs b/SnipInsight/Controls/Ariadne/AriInkRadioButton.cs new file mode 100644 index 0000000..97aa860 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriInkRadioButton.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Media; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriInkRadioButton : AriRadioButtonBase + { + static AriInkRadioButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriInkRadioButton), new FrameworkPropertyMetadata(typeof(AriInkRadioButton))); + } + + #region Ink + + public Brush Ink + { + get { return GetValue(InkProperty) as Brush; } + set { SetValue(InkProperty, value); } + } + + public static readonly DependencyProperty InkProperty = + DependencyProperty.Register("Ink", typeof(Brush), typeof(AriInkRadioButton), new PropertyMetadata(Brushes.Black)); + + #endregion + + #region Label + + public string Label + { + get { return GetValue(LabelProperty) as string; } + set { SetValue(LabelProperty, value); } + } + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(AriInkRadioButton), new PropertyMetadata(null, OnLabelChangedStatic)); + + protected virtual void OnLabelChanged(string value) + { + if (ToolTip == null) + { + ToolTip = value; + } + + SetValue(System.Windows.Automation.AutomationProperties.HelpTextProperty, value); + } + + private static void OnLabelChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriInkRadioButton; + + if (self != null) + { + self.OnLabelChanged(e.NewValue as string); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriLinkButton.cs b/SnipInsight/Controls/Ariadne/AriLinkButton.cs new file mode 100644 index 0000000..cc9f548 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriLinkButton.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriLinkButton : Button + { + static AriLinkButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriLinkButton), new FrameworkPropertyMetadata(typeof(AriLinkButton))); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriMenuItemBase.cs b/SnipInsight/Controls/Ariadne/AriMenuItemBase.cs new file mode 100644 index 0000000..fe97fa3 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriMenuItemBase.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + [TemplateVisualState(Name = "Shy", GroupName = "ShyStates")] + [TemplateVisualState(Name = "NotShy", GroupName = "ShyStates")] + public class AriMenuItemBase : MenuItem, IAriControl + { + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + OnIsShyChanged(IsShy, false); + } + + #region IShy + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(AriMenuItemBase), new PropertyMetadata(false, OnIsShyChangedStatic)); + + protected virtual void OnIsShyChanged(bool value, bool useTransitions = true) + { + RecomputeToolTip(); + + VisualStateManager.GoToState(this, value ? "Shy" : "NotShy", useTransitions); + } + + private static void OnIsShyChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriMenuItemBase; + + if (self != null) + { + self.OnIsShyChanged((bool)e.NewValue); + } + } + + protected void RecomputeToolTip() + { + if (ToolTip == null) + { + ToolTip = Header; + } + + //if (IsShy && ToolTip == null) + //{ + // ToolTip = Header; + //} + //else if (!IsShy) + //{ + // ToolTip = null; + //} + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriModernWindow.cs b/SnipInsight/Controls/Ariadne/AriModernWindow.cs new file mode 100644 index 0000000..d21f5a4 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriModernWindow.cs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using WinInterop = System.Windows.Interop; +using SnipInsight.Util; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriModernWindow : DpiAwareWindow + { + ContentPresenter contentArea; + TextBlock captionArea; + + public AriModernWindow() + { + SourceInitialized += ModernWindow_SourceInitialized; + } + + static AriModernWindow() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriModernWindow), new FrameworkPropertyMetadata(typeof(AriModernWindow))); + } + + #region Apply Template + + public override void OnApplyTemplate() + { + bool canMinimize = ResizeMode != System.Windows.ResizeMode.NoResize; + bool canRestore = ResizeMode != System.Windows.ResizeMode.NoResize && ResizeMode != System.Windows.ResizeMode.CanMinimize; + + Button closeButton = GetTemplateChild("CloseButton") as Button; + + if (closeButton != null) + closeButton.Click += closeButton_Click; + + Button restoreButton = GetTemplateChild("RestoreButton") as Button; + + if (restoreButton != null) + { + restoreButton.Click += restoreButton_Click; + + if (!canMinimize && !canRestore) + restoreButton.Visibility = Visibility.Collapsed; + else if (!canRestore) + restoreButton.IsEnabled = false; + } + + Button maxButton = GetTemplateChild("MaximizeButton") as Button; + + if (maxButton != null) + { + maxButton.Click += restoreButton_Click; + + if (!canMinimize && !canRestore) + maxButton.Visibility = Visibility.Collapsed; + else if (!canRestore) + maxButton.IsEnabled = false; + } + + Button minButton = GetTemplateChild("MinimizeButton") as Button; + + if (minButton != null) + { + minButton.Click += minButton_Click; + + if (!canMinimize) + minButton.Visibility = Visibility.Collapsed; + } + + contentArea = GetTemplateChild("ContentArea") as ContentPresenter; + captionArea = GetTemplateChild("CaptionArea") as TextBlock; + + if (ShowWindowCaption == true) + { + if (captionArea != null) + captionArea.Visibility = Visibility.Visible; + } + } + + private void closeButton_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + private void restoreButton_Click(object sender, RoutedEventArgs e) + { + if (WindowState == System.Windows.WindowState.Maximized) + WindowState = System.Windows.WindowState.Normal; + else + WindowState = System.Windows.WindowState.Maximized; + } + + private void minButton_Click(object sender, RoutedEventArgs e) + { + WindowState = System.Windows.WindowState.Minimized; + } + + #endregion + + #region Virtual MinHeight + MinWidth + + /// + /// Gets or sets the minimum width of the window. Setting MinWidth causes problems with DPI-Awareness, + /// so we use this value instead. + /// + /// + /// The minimum width. + /// + public double VirtualMinWidth + { + get { return (double)GetValue(VirtualMinWidthProperty); } + set { SetValue(VirtualMinWidthProperty, value); } + } + + public static readonly DependencyProperty VirtualMinWidthProperty = + DependencyProperty.Register("VirtualMinWidth", typeof(double), typeof(AriModernWindow), new PropertyMetadata(double.NaN)); + + /// + /// Gets or sets the minimum height of the window. Setting MinHeight causes problems with DPI-Awareness, + /// so we use this value instead. + /// + /// + /// The minimum height. + /// + public double VirtualMinHeight + { + get { return (double)GetValue(VirtualMinHeightProperty); } + set { SetValue(VirtualMinHeightProperty, value); } + } + + public static readonly DependencyProperty VirtualMinHeightProperty = + DependencyProperty.Register("VirtualMinHeight", typeof(double), typeof(AriModernWindow), new PropertyMetadata(double.NaN)); + + #endregion + + #region Helper Properties + + public static readonly DependencyProperty ModernBorderThicknessProperty = DependencyProperty.Register("ModernBorderThickness", typeof(Thickness), typeof(AriModernWindow)); + + public Thickness ModernBorderThickness + { + get { return (Thickness)GetValue(ModernBorderThicknessProperty); } + set { SetValue(ModernBorderThicknessProperty, value); } + } + + public static readonly DependencyProperty ModernBorderBrushProperty = DependencyProperty.Register("ModernBorderBrush", typeof(Brush), typeof(AriModernWindow), new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 255, 0, 0)))); + + public Brush ModernBorderBrush + { + get { return (Brush)GetValue(ModernBorderBrushProperty); } + set { SetValue(ModernBorderBrushProperty, value); } + } + + public static readonly DependencyProperty ModernCaptionBrushProperty = DependencyProperty.Register("ModernCaptionBrush", typeof(Brush), typeof(AriModernWindow), new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 105, 105, 105)))); + + public Brush ModernCaptionBrush + { + get { return (Brush)GetValue(ModernCaptionBrushProperty); } + set { SetValue(ModernCaptionBrushProperty, value); } + } + + public static readonly DependencyProperty ModernCaptionButtonBrushProperty = DependencyProperty.Register("ModernCaptionButtonBrush", typeof(Brush), typeof(AriModernWindow), new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 68, 68, 68)))); + + public Brush ModernCaptionButtonBrush + { + get { return (Brush)GetValue(ModernCaptionButtonBrushProperty); } + set { SetValue(ModernCaptionButtonBrushProperty, value); } + } + + public static readonly DependencyProperty ShowWindowCaptionProperty = DependencyProperty.Register("ShowWindowCaption", typeof(bool), typeof(AriModernWindow), new PropertyMetadata(true)); + + public bool ShowWindowCaption + { + get { return (bool)GetValue(ShowWindowCaptionProperty); } + set { SetValue(ShowWindowCaptionProperty, value); } + } + + public static readonly DependencyProperty CaptionBarHeightProperty = DependencyProperty.Register("CaptionBarHeight", typeof(double), typeof(AriModernWindow), new PropertyMetadata(32.0)); + + public double CaptionBarHeight + { + get { return (double)GetValue(CaptionBarHeightProperty); } + set { SetValue(CaptionBarHeightProperty, value); } + } + + #endregion + + #region Modern Window Resize + + void ModernWindow_SourceInitialized(object sender, EventArgs e) + { + System.IntPtr handle = (new WinInterop.WindowInteropHelper(this)).Handle; + WinInterop.HwndSource.FromHwnd(handle).AddHook(new WinInterop.HwndSourceHook(WindowProc)); + } + + private System.IntPtr WindowProc( + System.IntPtr hwnd, + int msg, + System.IntPtr wParam, + System.IntPtr lParam, + ref bool handled) + { + switch (msg) + { + case 0x0024: + WmGetMinMaxInfo(hwnd, lParam); + + // This may seem odd, but setting handled = false allows some extra + // system code to fire that is necessary for correct behavior when + // the DPI of the screen changes. There is a glitch with WPF and + // folks on the web have discovered that this is the fix. + handled = false; + break; + } + + return (System.IntPtr)0; + } + + private int GetMinWidthInScreenPixels(DpiScale dpiScale) + { + double value = double.IsNaN(VirtualMinWidth) ? SystemParameters.MinimumWindowWidth : VirtualMinWidth; + + return DoubleToInt32(value * dpiScale.X); + } + + private int GetMinHeightInScreenPixels(DpiScale dpiScale) + { + double value = double.IsNaN(VirtualMinHeight) ? SystemParameters.MinimumWindowHeight : VirtualMinHeight; + + return DoubleToInt32(value * dpiScale.Y); + } + + private void WmGetMinMaxInfo(System.IntPtr hwnd, System.IntPtr lParam) + { + NativeMethods.MINMAXINFO mmi = (NativeMethods.MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(NativeMethods.MINMAXINFO)); + + // Adjust the maximized size and position to fit the work area of the correct monitor + System.IntPtr monitor = NativeMethods.GetMonitorFromWindow(hwnd); + + // MinHeight and MinWidth need to be scaled from Virtual Pixels back to Logical Pixels + DpiScale dpiScale = VirtualPixelScale; + + if (monitor != System.IntPtr.Zero) + { + NativeMethods.MONITORINFO monitorInfo = new NativeMethods.MONITORINFO(); + NativeMethods.GetMonitorInfo(monitor, monitorInfo); + NativeMethods.RECT rcWorkArea = monitorInfo.rcWork; + NativeMethods.RECT rcMonitorArea = monitorInfo.rcMonitor; + mmi.ptMaxPosition.x = Math.Abs(rcWorkArea.left - rcMonitorArea.left); + mmi.ptMaxPosition.y = Math.Abs(rcWorkArea.top - rcMonitorArea.top); + mmi.ptMaxSize.x = DoubleToInt32(Math.Abs(rcWorkArea.right - rcWorkArea.left)); + mmi.ptMaxSize.y = DoubleToInt32(Math.Abs(rcWorkArea.bottom - rcWorkArea.top)); + // After much research, it appears that the MaxTrackSize is used for secondary monitors + // while MaxSize is used for the primary monitor. + mmi.ptMaxTrackSize.x = DoubleToInt32(Math.Abs(rcWorkArea.right - rcWorkArea.left)); + mmi.ptMaxTrackSize.y = DoubleToInt32(Math.Abs(rcWorkArea.bottom - rcWorkArea.top)); + mmi.ptMinTrackSize.x = GetMinWidthInScreenPixels(dpiScale); + mmi.ptMinTrackSize.y = GetMinHeightInScreenPixels(dpiScale); + } + + Marshal.StructureToPtr(mmi, lParam, true); + } + + [SecurityCritical] + public static Object PtrToStructure(IntPtr lparam, Type clrType) + { + return Marshal.PtrToStructure(lparam, clrType); + } + + private Int32 DoubleToInt32(Double value) + { + return (Int32)Math.Ceiling(value); + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriProgressBar.cs b/SnipInsight/Controls/Ariadne/AriProgressBar.cs new file mode 100644 index 0000000..2306811 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriProgressBar.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriProgressBar : ProgressBar + { + static AriProgressBar() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriProgressBar), new FrameworkPropertyMetadata(typeof(AriProgressBar))); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriRadioButtonBase.cs b/SnipInsight/Controls/Ariadne/AriRadioButtonBase.cs new file mode 100644 index 0000000..80c7d31 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriRadioButtonBase.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls.Ariadne +{ + [TemplateVisualState(Name = "Shy", GroupName = "ShyStates")] + [TemplateVisualState(Name = "NotShy", GroupName = "ShyStates")] + public abstract class AriRadioButtonBase : RadioButton, IAriControl + { + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + OnIsShyChanged(IsShy, false); + } + + #region IShy + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(AriRadioButtonBase), new PropertyMetadata(false, OnIsShyChangedStatic)); + + protected virtual void OnIsShyChanged(bool value, bool useTransitions = true) + { + VisualStateManager.GoToState(this, value ? "Shy" : "NotShy", useTransitions); + } + + private static void OnIsShyChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriRadioButtonBase; + + if (self != null) + { + self.OnIsShyChanged((bool)e.NewValue); + } + } + + #endregion + + } +} diff --git a/SnipInsight/Controls/Ariadne/AriRectangleIconButton.cs b/SnipInsight/Controls/Ariadne/AriRectangleIconButton.cs new file mode 100644 index 0000000..7cca52b --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriRectangleIconButton.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriRectangleIconButton : AriIconLabelButton + { + static AriRectangleIconButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriRectangleIconButton), new FrameworkPropertyMetadata(typeof(AriRectangleIconButton))); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriSuperTip.cs b/SnipInsight/Controls/Ariadne/AriSuperTip.cs new file mode 100644 index 0000000..4ce1644 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriSuperTip.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; +using System.Windows.Controls; +using SnipInsight.Util; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriSuperTip : ContentControl + { + public event EventHandler AfterFadeIn; + + static AriSuperTip() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriSuperTip), new FrameworkPropertyMetadata(typeof(AriSuperTip))); + } + + public void FadeIn() + { + Opacity = 0; + Visibility = Visibility.Visible; + + var s = AnimationUtilities.CreateFadeInStoryboard(this, 500); + + // Delay by half a second to decrease the overlap of two tooltips + s.BeginTime = TimeSpan.FromSeconds(1); + + s.Completed += (sender, evt) => + { + if (AfterFadeIn != null) + { + AfterFadeIn(this, EventArgs.Empty); + } + }; + + s.Begin(); + } + + public void FadeOut() + { + AnimationUtilities.FadeOutAndRemove(this, 500); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/AriToggleButtonBase.cs b/SnipInsight/Controls/Ariadne/AriToggleButtonBase.cs new file mode 100644 index 0000000..4cb55a6 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriToggleButtonBase.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls.Primitives; + +namespace SnipInsight.Controls.Ariadne +{ + [TemplateVisualState(Name = "Shy", GroupName = "ShyStates")] + [TemplateVisualState(Name = "NotShy", GroupName = "ShyStates")] + public abstract class AriToggleButtonBase : ToggleButton, IAriControl + { + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + OnIsShyChanged(IsShy, false); + } + + #region IShy + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(AriToggleButtonBase), new PropertyMetadata(false, OnIsShyChangedStatic)); + + protected virtual void OnIsShyChanged(bool value, bool useTransitions = true) + { + VisualStateManager.GoToState(this, value ? "Shy" : "NotShy", useTransitions); + } + + private static void OnIsShyChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as AriToggleButtonBase; + + if (self != null) + { + self.OnIsShyChanged((bool)e.NewValue); + } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/Ariadne/AriToggleSwitch.cs b/SnipInsight/Controls/Ariadne/AriToggleSwitch.cs new file mode 100644 index 0000000..12b69e6 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/AriToggleSwitch.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls.Primitives; + +namespace SnipInsight.Controls.Ariadne +{ + public class AriToggleSwitch : ToggleButton + { + static AriToggleSwitch() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AriToggleSwitch), new FrameworkPropertyMetadata(typeof(AriToggleSwitch))); + } + } +} diff --git a/SnipInsight/Controls/Ariadne/IAriControl.cs b/SnipInsight/Controls/Ariadne/IAriControl.cs new file mode 100644 index 0000000..aeb4634 --- /dev/null +++ b/SnipInsight/Controls/Ariadne/IAriControl.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Controls.Ariadne +{ + public interface IAriControl : IShyControl + { + + } +} diff --git a/SnipInsight/Controls/DpiAwareWindow.cs b/SnipInsight/Controls/DpiAwareWindow.cs new file mode 100644 index 0000000..c682819 --- /dev/null +++ b/SnipInsight/Controls/DpiAwareWindow.cs @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Media; +using WinInterop = System.Windows.Interop; +using SnipInsight.Util; +using System.Security; + +namespace SnipInsight.Controls +{ + public class DpiAwareWindow : Window + { + private IntPtr _hwnd; + + public DpiAwareWindow() + { + RecalculateSystemScale(); + + Loaded += DpiAwareWindow_Loaded; + } + + private void DpiAwareWindow_Loaded(object sender, RoutedEventArgs e) + { + // Save our Hwnd for future use + _hwnd = NativeMethods.GetWindowHwnd(this); + + WinInterop.HwndSource.FromHwnd(_hwnd).AddHook(new WinInterop.HwndSourceHook(WindowProc)); + + RecalculateSystemScale(); + RecalculateMonitorScale(); + + // Just to be extra sure that this fires for the first time! + // There seems to be an occassional race condition where the Visual + // Child doesn't exist, so this extra call should prevent that. + // + // I believe that the root cause is that occassionally the DpiChanged + // method happens before our visual tree is loaded. This ensures that + // we don't fail to set the ScaleTransform. + ApplyScaleTransformToWindow(); + } + + private System.IntPtr WindowProc( + System.IntPtr hwnd, + int msg, + System.IntPtr wParam, + System.IntPtr lParam, + ref bool handled) + { + switch (msg) + { + case (int)NativeMethods.WindowMsg.WM_DPICHANGED: // DPI Changed + NativeMethods.RECT rect = (NativeMethods.RECT)PtrToStructure(lParam, typeof(NativeMethods.RECT)); + + // It's important to call these methods to set the Scale + // variables before attempting to move the window. The location of + // these methods were critical to creating the desired effect. + // + // There is a subclass called AriModernWindow that handles Min/Max + // events. To function correctly, it needs the Scale variables to be set. + // The SetWindowPos caused the Mix/Max events to fire; therefore, we + // must call these methods first, before positioning the window. + RecalculateSystemScale(); + RecalculateMonitorScale(); + + // SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE = 0x214 + NativeMethods.SetWindowPos(hwnd, IntPtr.Zero, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, (uint)(NativeMethods.SetWindowPosFlags.SWP_NOZORDER | NativeMethods.SetWindowPosFlags.SWP_NOOWNERZORDER | NativeMethods.SetWindowPosFlags.SWP_NOACTIVATE)); + + handled = false; + break; + } + + return (System.IntPtr)0; + } + + #region DpiScalors + + private uint _systemDpiX = 96; + private uint _systemDpiY = 96; + + private DpiScale _monitorScale = new DpiScale(1, 1); + private DpiScale _virtualPixelScale = new DpiScale(1, 1); + private DpiScale _systemScale = new DpiScale(1, 1); + + /// + /// Gets the monitor scale, which is the monitor DPI versus System DPI. + /// + /// + /// The monitor scale. + /// + /// + /// This is the ratio of the Effective DPI of a specific Monitor, versus it's + /// physical DPI. + /// When developing a DPI-aware application, this is useful for applying a ScaleTransform + /// to your window so all fonts and graphics are scaled and rendered beautifully based on the + /// physical capabilities of the display and the accessibility settings of the user. + /// + protected DpiScale MonitorScale + { + get { return _monitorScale; } + private set + { + ThrowIfNullArgument(value); + + if (!value.Equals(_monitorScale)) + { + _monitorScale = value; + OnMonitorScaleChanged(); + } + } + } + + /// + /// Gets the virtual pixel scale, which is the monitor's effective DPI versus 96 DPI. + /// + /// + /// The virtual pixel scale. + /// + /// + /// This is the ratio of the Effective DPI of a specific Monitor, versus 96 DPI. + /// When developing a DPI-aware application, this is useful for responding to + /// Min/Max Height/Width requests. The values that you return must be scaled against + /// the VirtualPixelScale of the Window. + /// + protected DpiScale VirtualPixelScale + { + get { return _virtualPixelScale; } + private set + { + ThrowIfNullArgument(value); + + if (!value.Equals(_virtualPixelScale)) + { + _virtualPixelScale = value; + } + } + } + + /// + /// Gets the virtual pixel scale, which is the system's effective DPI versus 96 DPI. + /// + /// + /// The virtual pixel scale. + /// + /// + /// The System Effective DPI is derived by the operating system + /// by looking across all monitors and determining an "Effective DPI" that works + /// well across all the screens for applications that do not support + /// per-monitor DPI. + /// When developing a DPI-aware application, this is useful for translating + /// window geometry (Left, Top, Width, Height) against the screen coordinates and + /// across monitors. + /// + protected DpiScale SystemScale + { + get { return _systemScale; } + private set + { + ThrowIfNullArgument(value); + + if (!value.Equals(_systemScale)) + { + _systemScale = value; + } + } + } + + /// + /// Called when the MonitorScale has changed. This method handles local changes + /// to ensure they occur before the OnMonitorScaleChanged event happens. + /// + private void OnMonitorScaleChanged() + { + ApplyScaleTransformToWindow(); + } + + private void ApplyScaleTransformToWindow() + { +#if (DEBUG) + System.Diagnostics.Debug.WriteLine("Scale X=" + MonitorScale.X.ToString() + ", Y=" + MonitorScale.Y.ToString()); +#endif + + if (VisualChildrenCount == 0) + return; + + var child = GetVisualChild(0); + + if (MonitorScale.X == 1 && MonitorScale.Y == 1) + { + child.SetValue(LayoutTransformProperty, null); + } + else + { + child.SetValue(LayoutTransformProperty, new ScaleTransform(MonitorScale.X, MonitorScale.Y)); + } + } + + + private void ThrowIfNullArgument(object obj) + { + if (obj == null) + { + throw new ArgumentNullException(); + } + } + + private uint lastDpiX = 0; + private uint lastDpiY = 0; + + private void RecalculateMonitorScale() + { + uint dpiX; + uint dpiY; + + DpiUtilities.GetWindowEffectiveDpi(_hwnd, out dpiX, out dpiY); + + if (dpiX != lastDpiX && dpiY != lastDpiY) + { + VirtualPixelScale = DpiUtilities.CalculateScale(dpiX, dpiY, 96, 96); + + lastDpiX = dpiX; + lastDpiY = dpiY; + } + } + + private void RecalculateSystemScale() + { + DpiUtilities.GetSystemEffectiveDpi(out _systemDpiX, out _systemDpiY); + + SystemScale = DpiUtilities.CalculateScale(_systemDpiX, _systemDpiY, 96, 96); + +#if (DEBUG) + System.Diagnostics.Debug.WriteLine("System DPI = " + _systemDpiX.ToString()); +#endif + } + + #endregion + + [SecurityCritical] + private static Object PtrToStructure(IntPtr lparam, Type clrType) + { + return Marshal.PtrToStructure(lparam, clrType); + } + } +} diff --git a/SnipInsight/Controls/HighContrastMode.cs b/SnipInsight/Controls/HighContrastMode.cs new file mode 100644 index 0000000..86afea0 --- /dev/null +++ b/SnipInsight/Controls/HighContrastMode.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Diagnostics; +using System.Windows; + +namespace SnipInsight.Controls +{ + public class HighContrastMode : DependencyObject + { + public HighContrastMode() + { + SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged; + } + + public static HighContrastMode Instance + { + get + { + if (_instance == null) + { + _instance = new HighContrastMode(); + _instance.IsHighContrast = SystemParameters.HighContrast; + } + return _instance; + } + } + + private static HighContrastMode _instance; + + private void SystemParameters_StaticPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == "HighContrast") + { + HighContrastMode.Instance.IsHighContrast = SystemParameters.HighContrast; + } + } + + #region DynamicProperty IsHighContrast + + public static readonly DependencyProperty IsHighContrastProperty = DependencyProperty.Register( + "IsHighContrast", typeof(bool), typeof(HighContrastMode), new PropertyMetadata(false)); + + public bool IsHighContrast + { + get { return (bool)GetValue(IsHighContrastProperty); } + private set { SetValue(IsHighContrastProperty, value); } + } + + #endregion + } +} diff --git a/SnipInsight/Controls/IIconLabelControl.cs b/SnipInsight/Controls/IIconLabelControl.cs new file mode 100644 index 0000000..9f3e70e --- /dev/null +++ b/SnipInsight/Controls/IIconLabelControl.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.ComponentModel; +using System.Windows; + +namespace SnipInsight.Controls +{ + public class IIconLabelControl + { + [TypeConverterAttribute(typeof(LengthConverter))] + public double IconHeight { get; set; } + + [TypeConverterAttribute(typeof(LengthConverter))] + public double IconWidth { get; set; } + + string Label { get; set; } + } +} diff --git a/SnipInsight/Controls/IShyControl.cs b/SnipInsight/Controls/IShyControl.cs new file mode 100644 index 0000000..23c7909 --- /dev/null +++ b/SnipInsight/Controls/IShyControl.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace SnipInsight.Controls +{ + public interface IShyControl + { + bool IsShy { get; set; } + } +} diff --git a/SnipInsight/Controls/PaneButton.cs b/SnipInsight/Controls/PaneButton.cs new file mode 100644 index 0000000..0df631c --- /dev/null +++ b/SnipInsight/Controls/PaneButton.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace SnipInsight.Controls +{ + /// + /// Custom button for panes to allow two images for the Default and MouseOver states, + /// and background highlight for Pressed and Disabled states. + /// + public class PaneButton : Button + { + /// + /// Ctor + /// + static PaneButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(PaneButton), new FrameworkPropertyMetadata(typeof(PaneButton))); + } + + /// + /// ///////////////////////////////////////////////////////////// + /// + public ImageSource DefaultImage + { + get { return (ImageSource)GetValue(DefaultImageProperty); } + set { SetValue(DefaultImageProperty, value); } + } + public static readonly DependencyProperty DefaultImageProperty = + DependencyProperty.Register("DefaultImage", typeof(ImageSource), typeof(PaneButton), new PropertyMetadata(null)); + + /// + /// ///////////////////////////////////////////////////////////// + /// + public ImageSource MouseOverImage + { + get { return (ImageSource)GetValue(MouseOverImageProperty); } + set { SetValue(MouseOverImageProperty, value); } + } + public static readonly DependencyProperty MouseOverImageProperty = + DependencyProperty.Register("MouseOverImage", typeof(ImageSource), typeof(PaneButton), new PropertyMetadata(null)); + + /// + /// ///////////////////////////////////////////////////////////// + /// + public Color BackgroundPressed + { + get { return (Color)GetValue(BackgroundPressedProperty); } + set { SetValue(BackgroundPressedProperty, value); } + } + public static readonly DependencyProperty BackgroundPressedProperty = + DependencyProperty.Register("BackgroundPressed", typeof(Color), typeof(PaneButton), new PropertyMetadata(Color.FromArgb(0x70, 0xFF, 0xFF, 0xFF))); + + /// + /// ///////////////////////////////////////////////////////////// + /// + public Color BackgroundDisabled + { + get { return (Color)GetValue(BackgroundDisabledProperty); } + set { SetValue(BackgroundDisabledProperty, value); } + } + public static readonly DependencyProperty BackgroundDisabledProperty = + DependencyProperty.Register("BackgroundDisabled", typeof(Color), typeof(PaneButton), new PropertyMetadata(Color.FromArgb(0x70, 0xFF, 0xFF, 0xFF))); + + } +} diff --git a/SnipInsight/Controls/RibbonButton.cs b/SnipInsight/Controls/RibbonButton.cs new file mode 100644 index 0000000..b566243 --- /dev/null +++ b/SnipInsight/Controls/RibbonButton.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace SnipInsight.Controls +{ + /// + /// Custom button for ribbon to allow different images for different states + /// + public class RibbonButton : Button, IShyControl + { + static RibbonButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(RibbonButton), new FrameworkPropertyMetadata(typeof(RibbonButton))); + } + + public ImageSource DefaultImage + { + get { return (ImageSource)GetValue(DefaultImageProperty); } + set { SetValue(DefaultImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DefaultImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DefaultImageProperty = + DependencyProperty.Register("DefaultImage", typeof(ImageSource), typeof(RibbonButton), new PropertyMetadata(null)); + + public ImageSource DisabledImage + { + get { return (ImageSource)GetValue(DisabledImageProperty); } + set { SetValue(DisabledImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DisabledImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DisabledImageProperty = + DependencyProperty.Register("DisabledImage", typeof(ImageSource), typeof(RibbonButton), new PropertyMetadata(null)); + + public ImageSource PressedImage + { + get { return (ImageSource)GetValue(PressedImageProperty); } + set { SetValue(PressedImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for PressedImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PressedImageProperty = + DependencyProperty.Register("PressedImage", typeof(ImageSource), typeof(RibbonButton), new PropertyMetadata(null)); + + public ImageSource MouseOverImage + { + get { return (ImageSource)GetValue(MouseOverImageProperty); } + set { SetValue(MouseOverImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for MouseOverImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty MouseOverImageProperty = + DependencyProperty.Register("MouseOverImage", typeof(ImageSource), typeof(RibbonButton), new PropertyMetadata(null)); + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(RibbonButton), new PropertyMetadata(false)); + } +} diff --git a/SnipInsight/Controls/RibbonComboBox.cs b/SnipInsight/Controls/RibbonComboBox.cs new file mode 100644 index 0000000..359c0cd --- /dev/null +++ b/SnipInsight/Controls/RibbonComboBox.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls +{ + public class RibbonComboBox : ComboBox, IShyControl + { + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(RibbonComboBox), new PropertyMetadata(false)); + + } +} diff --git a/SnipInsight/Controls/RibbonComboBoxItem.cs b/SnipInsight/Controls/RibbonComboBoxItem.cs new file mode 100644 index 0000000..c4594c0 --- /dev/null +++ b/SnipInsight/Controls/RibbonComboBoxItem.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace SnipInsight.Controls +{ + /// + /// Custom ComboBoxItem for ribbon to allow different images for different states + /// + public class RibbonComboBoxItem : ComboBoxItem, ICommandSource + { + static RibbonComboBoxItem() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(RibbonComboBoxItem), new FrameworkPropertyMetadata(typeof(RibbonComboBoxItem))); + } + + public ImageSource DefaultImage + { + get { return (ImageSource)GetValue(DefaultImageProperty); } + set { SetValue(DefaultImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DefaultImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DefaultImageProperty = + DependencyProperty.Register("DefaultImage", typeof(ImageSource), typeof(RibbonComboBoxItem), new PropertyMetadata(null)); + + public ImageSource DisabledImage + { + get { return (ImageSource)GetValue(DisabledImageProperty); } + set { SetValue(DisabledImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DisabledImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DisabledImageProperty = + DependencyProperty.Register("DisabledImage", typeof(ImageSource), typeof(RibbonComboBoxItem), new PropertyMetadata(null)); + + public ImageSource PressedImage + { + get { return (ImageSource)GetValue(PressedImageProperty); } + set { SetValue(PressedImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for PressedImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PressedImageProperty = + DependencyProperty.Register("PressedImage", typeof(ImageSource), typeof(RibbonComboBoxItem), new PropertyMetadata(null)); + + public ImageSource MouseOverImage + { + get { return (ImageSource)GetValue(MouseOverImageProperty); } + set { SetValue(MouseOverImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for MouseOverImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty MouseOverImageProperty = + DependencyProperty.Register("MouseOverImage", typeof(ImageSource), typeof(RibbonComboBoxItem), new PropertyMetadata(null)); + + public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", + typeof(ICommand), + typeof(RibbonComboBoxItem), + new PropertyMetadata((ICommand)null, + new PropertyChangedCallback(CommandChanged))); + + public ICommand Command + { + get { return (ICommand)GetValue(CommandProperty); } + set { SetValue(CommandProperty, value); } + + } + + public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register("CommandTarget", + typeof(IInputElement), + typeof(RibbonComboBoxItem), + new PropertyMetadata((IInputElement)null)); + + public IInputElement CommandTarget + { + get { return (IInputElement)GetValue(CommandTargetProperty); } + set { SetValue(CommandTargetProperty, value); } + } + + public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", + typeof(object), + typeof(RibbonComboBoxItem), + new PropertyMetadata((object)null)); + + public object CommandParameter + { + get { return (object)GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + private static void CommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + RibbonComboBoxItem cb = (RibbonComboBoxItem)d; + } + + protected override void OnSelected(RoutedEventArgs e) + { + base.OnSelected(e); + + if (this.Command != null) + { + RoutedCommand command = this.Command as RoutedCommand; + + if (command != null) + { + command.Execute(this.CommandParameter, this.CommandTarget); + } + else + { + ((ICommand)Command).Execute(CommandParameter); + } + } + } + + } +} diff --git a/SnipInsight/Controls/RibbonGroupPanel.cs b/SnipInsight/Controls/RibbonGroupPanel.cs new file mode 100644 index 0000000..5100730 --- /dev/null +++ b/SnipInsight/Controls/RibbonGroupPanel.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Views; +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls +{ + public class RibbonGroupPanel : ContentControl, IShyControl + { + static RibbonGroupPanel() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(RibbonGroupPanel), new FrameworkPropertyMetadata(typeof(RibbonGroupPanel))); + } + + #region IsShy + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyPropertyUtilities.Register("IsShy", false, (o, p) => { o.OnShyChanged(p); }); + + private void OnShyChanged(bool value) + { + ShowLabel = !value; + } + + #endregion + + public bool ShowLabel + { + get { return (bool)GetValue(ShowLabelProperty); } + set { SetValue(ShowLabelProperty, value); } + } + + // Using a DependencyProperty as the backing store for ShowLabel. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ShowLabelProperty = + DependencyProperty.Register("ShowLabel", typeof(bool), typeof(RibbonGroupPanel), new PropertyMetadata(true)); + + public string Label + { + get { return (string)GetValue(LabelProperty); } + set { SetValue(LabelProperty, value); } + } + + // Using a DependencyProperty as the backing store for Label. This enables animation, styling, binding, etc... + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(RibbonGroupPanel), new PropertyMetadata(null)); + + } +} diff --git a/SnipInsight/Controls/RibbonSeparator.cs b/SnipInsight/Controls/RibbonSeparator.cs new file mode 100644 index 0000000..0c806b4 --- /dev/null +++ b/SnipInsight/Controls/RibbonSeparator.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Controls +{ + public class RibbonSeparator : Separator + { + static RibbonSeparator() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(RibbonSeparator), new FrameworkPropertyMetadata(typeof(RibbonSeparator))); + } + + } +} diff --git a/SnipInsight/Controls/RibbonToggleButton.cs b/SnipInsight/Controls/RibbonToggleButton.cs new file mode 100644 index 0000000..d9bffaf --- /dev/null +++ b/SnipInsight/Controls/RibbonToggleButton.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Media; + +namespace SnipInsight.Controls +{ + public class RibbonToggleButton : ToggleButton, IShyControl + { + ///////////////////////////////////////////// + /// State images. + ///////////////////////////////////////////// + static RibbonToggleButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(RibbonToggleButton), new FrameworkPropertyMetadata(typeof(RibbonToggleButton))); + } + + public ImageSource DefaultToggleImage + { + get { return (ImageSource)GetValue(DefaultToggleImageProperty); } + set { SetValue(DefaultToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DefaultToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DefaultToggleImageProperty = + DependencyProperty.Register("DefaultToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public ImageSource DisabledToggleImage + { + get { return (ImageSource)GetValue(DisabledToggleImageProperty); } + set { SetValue(DisabledToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DisabledToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DisabledToggleImageProperty = + DependencyProperty.Register("DisabledToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public ImageSource PressedToggleImage + { + get { return (ImageSource)GetValue(PressedToggleImageProperty); } + set { SetValue(PressedToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for PressedToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PressedToggleImageProperty = + DependencyProperty.Register("PressedToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public ImageSource MouseOverToggleImage + { + get { return (ImageSource)GetValue(MouseOverToggleImageProperty); } + set { SetValue(MouseOverToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for MouseOverToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty MouseOverToggleImageProperty = + DependencyProperty.Register("MouseOverToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + ///////////////////////////////////////////// + // Checked state content (text or anything else styled appropriately) + ///////////////////////////////////////////// + public object CheckedContent + { + get { return GetValue(CheckedContentProperty); } + set { SetValue(CheckedContentProperty, value); } + } + public static readonly DependencyProperty CheckedContentProperty = + DependencyProperty.Register("CheckedContent", typeof(object), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public ImageSource CheckedDefaultToggleImage + { + get { return (ImageSource)GetValue(CheckedDefaultToggleImageProperty); } + set { SetValue(CheckedDefaultToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for DefaultToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CheckedDefaultToggleImageProperty = + DependencyProperty.Register("CheckedDefaultToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public ImageSource CheckedPressedToggleImage + { + get { return (ImageSource)GetValue(CheckedPressedToggleImageProperty); } + set { SetValue(CheckedPressedToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for PressedToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CheckedPressedToggleImageProperty = + DependencyProperty.Register("CheckedPressedToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public ImageSource CheckedMouseOverToggleImage + { + get { return (ImageSource)GetValue(CheckedMouseOverToggleImageProperty); } + set { SetValue(CheckedMouseOverToggleImageProperty, value); } + } + + // Using a DependencyProperty as the backing store for MouseOverToggleImage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CheckedMouseOverToggleImageProperty = + DependencyProperty.Register("CheckedMouseOverToggleImage", typeof(ImageSource), typeof(RibbonToggleButton), new PropertyMetadata(null)); + + public bool IsShy + { + get { return (bool)GetValue(IsShyProperty); } + set { SetValue(IsShyProperty, value); } + } + + // Using a DependencyProperty as the backing store for IsShy. This enables animation, styling, binding, etc... + public static readonly DependencyProperty IsShyProperty = + DependencyProperty.Register("IsShy", typeof(bool), typeof(RibbonToggleButton), new PropertyMetadata(false)); + } +} diff --git a/SnipInsight/Conversion/PictureConverter.cs b/SnipInsight/Conversion/PictureConverter.cs new file mode 100644 index 0000000..b950fc4 --- /dev/null +++ b/SnipInsight/Conversion/PictureConverter.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.IO; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace SnipInsight.Conversion +{ + internal static class PictureConverter + { + internal const int ThumbnailWidth = 320; + internal const int ThumbnailHeight = 240; + + /// + /// Generate snapshot of WPF UI elements + /// which may be used to get image+ink as currently shown on the screen + /// This function can be used independenly of any recording + /// + /// + /// + /// + /// + public static BitmapSource GenerateSnapshot(UIElement baseElement, UIElement overlayElement, double scale=1) + { + Size requestedSize = new Size(baseElement.RenderSize.Width * scale, baseElement.RenderSize.Height * scale); + + // Render the content + RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap((int)requestedSize.Width, (int)requestedSize.Height, 96d, 96d, PixelFormats.Pbgra32); + VisualBrush baseElementBrush = new VisualBrush(baseElement); + VisualBrush overlayElementBrush = new VisualBrush(overlayElement); + + DrawingVisual drawingVisual = new DrawingVisual(); + using (DrawingContext drawingContext = drawingVisual.RenderOpen()) + { + drawingContext.PushTransform(new ScaleTransform(scale, scale)); + drawingContext.DrawRectangle(baseElementBrush, null, new Rect(new Point(0, 0), new Point(baseElement.RenderSize.Width, baseElement.RenderSize.Height))); + drawingContext.DrawRectangle(overlayElementBrush, null, new Rect(new Point(0, 0), new Point(baseElement.RenderSize.Width, baseElement.RenderSize.Height))); + } + + renderTargetBitmap.Render(drawingVisual); + return renderTargetBitmap; + } + + + public static T SaveToPng(BitmapSource bitmap, T outputStream) where T:Stream + { + PngBitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(bitmap)); + encoder.Save(outputStream); + return outputStream; + } + + public static T SaveToJpg(BitmapSource bitmap, T outputStream, int quality = 95) where T:Stream + { + JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder(); + jpgEncoder.QualityLevel = quality; + jpgEncoder.Frames.Add(BitmapFrame.Create(bitmap)); + jpgEncoder.Save(outputStream); + return outputStream; + } + + public static T SaveToBmp(BitmapSource bitmap, T outputStream) where T : Stream + { + BmpBitmapEncoder bmpEncoder = new BmpBitmapEncoder(); + bmpEncoder.Frames.Add(BitmapFrame.Create(bitmap)); + bmpEncoder.Save(outputStream); + return outputStream; + } + } +} diff --git a/SnipInsight/Email/EmailManager.cs b/SnipInsight/Email/EmailManager.cs new file mode 100644 index 0000000..b48392e --- /dev/null +++ b/SnipInsight/Email/EmailManager.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace SnipInsight.EmailController +{ + using SnipInsight.Util; + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net.Mail; + + public static class EmailManager + { + /// + /// Open email client with attachment. + /// + public static bool OpenEmailClientWithAttachment(string file, string subject, string attachmentName, string attachmentMimeType, string toAddress = null, string body = null) + { + if (string.IsNullOrWhiteSpace(file) || !File.Exists(file)) + { + return false; + } + + try + { + using (MemoryStream ms = new MemoryStream()) + using (MailMessage message = new MailMessage()) + { + // copy the file to memory. + using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read)) + { + fs.CopyTo(ms); + fs.Flush(); + ms.Position = 0; + } + + // create a new email message with attachment. + message.From = new MailAddress("youremail@domain.com"); + message.To.Add(new MailAddress(toAddress ?? "toemail@domain.com")); + message.Subject = subject; + message.Attachments.Add(new Attachment(ms, attachmentName, attachmentMimeType)); + if (!string.IsNullOrWhiteSpace(body)) + { + message.Body = body; + } + string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + while (File.Exists(tempFile)) + { + tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + } + + // save the message to disk and open it. + if (SaveMessage(message, tempFile)) + { + Process.Start(tempFile); + return true; + } + else + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return false; + } + } + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + return false; + } + } + + /// + /// Open email client with image embedded in the email and also attached. + /// + public static bool OpenEmailClientWithEmbeddedImage(string file, string subject, string attachmentName, string imageContentType) + { + if (string.IsNullOrWhiteSpace(file) || !File.Exists(file)) + { + return false; + } + + try + { + using (MemoryStream ms = new MemoryStream()) + using (MailMessage message = new MailMessage()) + { + // copy the file to memory. + using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read)) + { + fs.CopyTo(ms); + fs.Flush(); + ms.Position = 0; + } + + // create a new email message with embedded image and also attach the image as alternate. + message.From = new MailAddress("youremail@domain.com"); + message.To.Add(new MailAddress("toemail@domain.com")); + message.Subject = subject; + message.Attachments.Add(new Attachment(ms, attachmentName, imageContentType)); + message.IsBodyHtml = true; + + // setup the linked image. + using (MemoryStream imageSource = new MemoryStream(ms.GetBuffer())) + { + LinkedResource resource = new LinkedResource(imageSource); + resource.ContentId = "snipimage"; + resource.ContentType = new System.Net.Mime.ContentType(imageContentType); + + // Create a plain view for clients not supporting html. + var plainView = AlternateView.CreateAlternateViewFromString("Please find my snip attached with this email.", null, "text/plain"); + + // Create a html view for clients supporting html + const string format = @"
"; + var htmlView = AlternateView.CreateAlternateViewFromString(format, null, "text/html"); + htmlView.LinkedResources.Add(resource); + + // Add both the views to the mail message. + message.AlternateViews.Add(htmlView); + message.AlternateViews.Add(plainView); + + // Get the temp file. + string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + while (File.Exists(tempFile)) + { + tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + } + + // save the message to disk and open it. + if (SaveMessage(message, tempFile)) + { + Process.Start(tempFile); + return true; + } + else + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return false; + } + } + } + } + catch(Exception ex) + { + Diagnostics.LogException(ex); + return false; + } + } + + + /// + /// Open email client with html body. + /// + public static bool OpenEmailClientWithHyperlinkedImage(string file, string url, int width, int height) + { + if (String.IsNullOrWhiteSpace(file) || !File.Exists(file) || String.IsNullOrWhiteSpace(url)) + { + return false; + } + + // create a new email message with embedded image + using (MailMessage message = new MailMessage()) + { + message.From = new MailAddress("youremail@domain.com"); + message.To.Add(new MailAddress("toemail@domain.com")); + message.Subject = "Sharing a snip with you"; + message.IsBodyHtml = true; + + // setup the linked image. + LinkedResource resource = new LinkedResource(file); + resource.ContentId = "playImage"; + resource.ContentType = new System.Net.Mime.ContentType("image/png"); + + // Create a plain view for clients not supporting html. + var plainView = AlternateView.CreateAlternateViewFromString("Here is the link: " + url, null, + "text/plain"); + + // Create a html view for clients supporting html + const string format = + @"

{0}

"; + var htmlView = AlternateView.CreateAlternateViewFromString(String.Format(format, url, width, height), + null, "text/html"); + htmlView.LinkedResources.Add(resource); + + // Add both the views to the mail message. + message.AlternateViews.Add(htmlView); + message.AlternateViews.Add(plainView); + + // Get the temp file. + string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + while (File.Exists(tempFile)) + { + tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".eml"); + } + + // save the message to disk and open it. + if (SaveMessage(message, tempFile)) + { + Process.Start(tempFile); + return true; + } + else + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return false; + } + } + } + + #region Helpers + /// + /// Save message as an EML format. + /// + /// + /// + /// + public static bool SaveMessage(MailMessage message, string fileName) + { + try + { + // get a temp folder for the email. + string tempMailFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + while (Directory.Exists(tempMailFolder)) + { + tempMailFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + } + Directory.CreateDirectory(tempMailFolder); + + // Write to the temporary folder. + using (SmtpClient client = new SmtpClient()) + { + client.UseDefaultCredentials = true; + client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; + client.PickupDirectoryLocation = tempMailFolder; + client.Send(message); + } + + // verify the folder. + string[] files = Directory.GetFiles(tempMailFolder); + if (files.Count() != 1 || !files[0].EndsWith(".eml")) + { + return false; + } + + // Setup the output email file. + using (var binaryWriter = new BinaryWriter(new FileStream(fileName, FileMode.Create))) + { + //Write the Unsent header to the file so the mail client knows this mail must be presented in "New message" mode + binaryWriter.Write(System.Text.Encoding.UTF8.GetBytes("X-Unsent: 1" + Environment.NewLine)); + binaryWriter.Flush(); + } + + // Copy to the given output file. + using (StreamReader sr = new StreamReader(new FileStream(files[0], FileMode.Open, FileAccess.Read))) + using (StreamWriter sw = new StreamWriter(new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.None))) + { + string line; + while ((line = sr.ReadLine()) != null) + { + if (line.ToLower().Contains("youremail@domain.com") || line.ToLower().Contains("toemail@domain.com")) + { + continue; + } + else + { + sw.WriteLine(line); + } + } + sw.Flush(); + } + + // Clean up the directory. + Directory.Delete(tempMailFolder, true); + return true; + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + return false; + } + } + + #endregion + } +} diff --git a/SnipInsight/ImageCapture/AreaSelection.cs b/SnipInsight/ImageCapture/AreaSelection.cs new file mode 100644 index 0000000..7776f34 --- /dev/null +++ b/SnipInsight/ImageCapture/AreaSelection.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using SnipInsight.Util; +using Point = System.Drawing.Point; + +namespace SnipInsight.ImageCapture +{ + public class AreaSelection + { + /// + /// detect the UI element for the point cursor is + /// + private SmartBoundaryDetection _boundaryDetection = null; + + /// + /// Cache of multiple monitor properties + /// + private ScreenProperties _screenProps = null; + + /// + /// the UI element where the cursor point is + /// + private IntPtr _detectedElementHnd = IntPtr.Zero; + + /// + /// When user have left mouse down, we will start to select area + /// + private bool _selectingArea = false; + + /// + /// The point left mouse clicks + /// + private Point _clickPoint = new Point(); + + /// + /// The point mouse position + /// + private Point _mousePoint = new Point(); + + /// + /// the selectedArea coordinators + /// + private NativeMethods.RECT _selectedArea = new NativeMethods.RECT(); + + private const string LogId = "AreaSelection:"; + + private const int MinSelectedAreaSide = 10; + + private static int _borderWidth; + private static int _borderHeight; + + /// + /// Get system metrics (default window border width) at application startup + /// Changes to system metrics require machine reboot, so it is OK to cache the values + /// + static AreaSelection() + { + // Default border thickness includes resize frame + padding. High DPI scaling is not needed here. + // See https://connect.microsoft.com/VisualStudio/feedback/details/763767 + var borderPadding = NativeMethods.GetSystemMetrics((int)NativeMethods.SystemMetrixIndex.SM_CXPADDEDBORDER); + _borderWidth = NativeMethods.GetSystemMetrics((int)NativeMethods.SystemMetrixIndex.SM_CXSIZEFRAME) + borderPadding; + _borderHeight = NativeMethods.GetSystemMetrics((int)NativeMethods.SystemMetrixIndex.SM_CYSIZEFRAME) + borderPadding; + } + + public bool SelectingArea + { + get { return _selectingArea; } + } + + public ScreenProperties.MonitorInformation GetMonitorInformation(IntPtr hMonitor) + { + return _screenProps.GetMonitorInformation(hMonitor); + } + + public bool DraggingRight + { + get { return _mousePoint.X >= _clickPoint.X; } + } + + public bool DraggingDown + { + get { return _mousePoint.Y >= _clickPoint.Y; } + } + + public ScreenProperties ScreenProps + { + get { return _screenProps; } + } + + public AreaSelection() + { + _screenProps = new ScreenProperties(); + _boundaryDetection = new SmartBoundaryDetection(_screenProps); + } + + /// + /// When mouse up we end dragging + /// + public void EndDragging() + { + _selectingArea = false; + } + + /// + /// When mouse clicks, we start to drag + /// + /// + /// + public void StartDragging(int x, int y) + { + _selectingArea = true; + _clickPoint = new Point(x, y); + } + + /// + /// When mouse moves you might be choosing the area + /// + /// + /// + public void Dragging(int x, int y) + { + NativeMethods.RECT rect = new NativeMethods.RECT(); + _mousePoint.X = x; + _mousePoint.Y = y; + + if (!_selectingArea) + { + rect = AutoDetectWindow(x, y); + } + else + { + rect = SelectArea(x, y); + if (!EmptyRect(rect)) + { + _detectedElementHnd = IntPtr.Zero; + _selectedArea = rect; + } + } + } + + internal NativeMethods.RECT GetSelectedArea() + { + if (EmptyRect(_selectedArea)) + { + if (_detectedElementHnd != IntPtr.Zero) + { + // Bounding rectangle defines the outer limits of the window area + var rect = _boundaryDetection.GetBoundaryRect(_detectedElementHnd); + System.Windows.Thickness borderThickness; + double monitorScaling = 1.0d; + + NativeMethods.WINDOWPLACEMENT wndPlacement; + NativeMethods.GetWindowPlacement(_detectedElementHnd, out wndPlacement); + bool fullScreen = wndPlacement.showCmd == NativeMethods.ShowWindowCommands.SW_SHOWMAXIMIZED; + + var monitor = _boundaryDetection.GetMonitor(_detectedElementHnd); + if (monitor != IntPtr.Zero) + { + var monitorInfo = _screenProps.GetMonitorInformation(monitor); + NativeMethods.RECT rcWorkArea = monitorInfo.rcWork; + + // Calculate monitor scaling relative to primary monitor + var monitorScalingFactor = monitorInfo.scalingFactor; + var defaultScalingFactor = _screenProps.GetMonitorInformation(IntPtr.Zero).scalingFactor; + monitorScaling = monitorScalingFactor / defaultScalingFactor; + + // Window may be maximized even if SW_SHOWMAXIMIZED is not set - check the monitor work area + if (fullScreen || + rect.left == rcWorkArea.left && + rect.top == rcWorkArea.top && + rect.width == (rcWorkArea.right - rcWorkArea.left) && + rect.height == (rcWorkArea.bottom - rcWorkArea.top)) + { + return new NativeMethods.RECT + { + left = (int)(rcWorkArea.left * monitorScaling), + right = (int)(rcWorkArea.right * monitorScaling), + top = (int)(rcWorkArea.top * monitorScaling), + bottom = (int)(rcWorkArea.bottom * monitorScaling), + }; + } + // fall through + } + + // Get client rectangle to check if it uses custom or default frame + NativeMethods.RECT clientRect; + NativeMethods.GetClientRect(_detectedElementHnd, out clientRect); + + // For normal frames retain 1px border around sides and bottom and leave the window title bar on the top + // Owner drawn frames may not have the border - make sure clientRect is left intact + borderThickness = new System.Windows.Thickness( + Math.Min(Math.Max(_borderWidth - 1, 0), (rect.width - (clientRect.right - clientRect.left)) / 2), + 0, + Math.Min(Math.Max(_borderWidth - 1, 0), (rect.width - (clientRect.right - clientRect.left)) / 2), + Math.Min(Math.Max(_borderHeight - 1, 0), rect.height - (clientRect.bottom - clientRect.top))); + + return new NativeMethods.RECT + { + left = (int)((rect.left + borderThickness.Left) * monitorScaling), + right = (int)((rect.right - borderThickness.Right) * monitorScaling), + top = (int)((rect.top + borderThickness.Top) * monitorScaling), + bottom = (int)((rect.bottom - borderThickness.Bottom) * monitorScaling), + }; + } + } + return _selectedArea; + } + + private NativeMethods.RECT AutoDetectWindow(int x, int y) + { + _detectedElementHnd = _boundaryDetection.GetTopElement(x, y); + + if (_detectedElementHnd != IntPtr.Zero) + { + var rect = _boundaryDetection.GetBoundaryRect(_detectedElementHnd); + return rect; + } + return new NativeMethods.RECT(); + } + + private NativeMethods.RECT SelectArea(int x, int y) + { + NativeMethods.RECT selectedArea = new NativeMethods.RECT(); + + //Calculate X Coordinates + if (x < _clickPoint.X) + { + selectedArea.left = x; + selectedArea.right = _clickPoint.X; + } + else + { + selectedArea.left = _clickPoint.X; + selectedArea.right = x; + } + + //Calculate Y Coordinates + if (y < _clickPoint.Y) + { + selectedArea.top = y; + selectedArea.bottom = _clickPoint.Y; + } + else + { + selectedArea.top = _clickPoint.Y; + selectedArea.bottom = y; + } + return selectedArea; + } + + private bool EmptyRect(NativeMethods.RECT rect) + { + var width = rect.right - rect.left; + var height = rect.bottom - rect.top; + if (width > MinSelectedAreaSide && height > MinSelectedAreaSide) + { + return false; + } + return true; + } + } +} diff --git a/SnipInsight/ImageCapture/DpiScalor.cs b/SnipInsight/ImageCapture/DpiScalor.cs new file mode 100644 index 0000000..2d1939f --- /dev/null +++ b/SnipInsight/ImageCapture/DpiScalor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using SnipInsight.Util; + +namespace SnipInsight.ImageCapture +{ + public class DpiScalors + { + public double X; + public double Y; + } + + public static class DpiScalor + { + /// + /// Gets scaling factor for a monitor + /// This is defined as current pixel size relative to effective DPI + /// + /// Monitor device name or null for the primary monitor + /// + public static double GetScreenScalingFactor(string deviceName = null) + { + var dc = NativeMethods.CreateDC("DISPLAY", deviceName, null, IntPtr.Zero); + if (dc == IntPtr.Zero) + return 1.0d; + + int LogicalScreenHeight = NativeMethods.GetDeviceCaps(dc, (int)NativeMethods.DeviceCap.VERTRES); + int PhysicalScreenHeight = NativeMethods.GetDeviceCaps(dc, (int)NativeMethods.DeviceCap.DESKTOPVERTRES); + + double ScreenScalingFactor = (double)PhysicalScreenHeight / (double)LogicalScreenHeight; + System.Diagnostics.Trace.WriteLine("Monitor:\"" + deviceName + "\" Scaling factor:" + ScreenScalingFactor + + " Logical height:" + LogicalScreenHeight + " Physical height:" + PhysicalScreenHeight); + + NativeMethods.DeleteDC(dc); + return ScreenScalingFactor; + } + + public static DpiScalors GetScalor() + { + return GetScalor(AppManager.TheBoss.MainWindow); + } + + public static DpiScalors GetScalor(Window window) + { + DpiScalors scalor = null; + try + { + PresentationSource MainWindowPresentationSource = PresentationSource.FromVisual(window); + + Matrix m; + if (MainWindowPresentationSource != null) + { + m = MainWindowPresentationSource.CompositionTarget.TransformToDevice; + } + else + { + using (var src = new HwndSource(new HwndSourceParameters())) + { + m = src.CompositionTarget.TransformToDevice; + } + } + + scalor = new DpiScalors(); + scalor.X = m.M11; + scalor.Y = m.M22; + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + return scalor; + } + } + +} diff --git a/SnipInsight/ImageCapture/IImageCaptureManager.cs b/SnipInsight/ImageCapture/IImageCaptureManager.cs new file mode 100644 index 0000000..9c05d43 --- /dev/null +++ b/SnipInsight/ImageCapture/IImageCaptureManager.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows.Media.Imaging; + +namespace SnipInsight.ImageCapture +{ + internal delegate void CapturingDoneNotify(); + + internal delegate void DrawSelectedAreaNotify(System.Drawing.Point ptCursor); + + public class ImageCaptureEventArgs : EventArgs + { + public ImageCaptureEventArgs(BitmapSource image) + { + Image = image; + } + + public BitmapSource Image { get; private set; } + } + + public interface IImageCaptureManager + { + void StartCapture(); + void CapturingDone(); + void CapturingCancel(); + event EventHandler CaptureCompleted; + } +} diff --git a/SnipInsight/ImageCapture/ImageCaptureCursor.cs b/SnipInsight/ImageCapture/ImageCaptureCursor.cs new file mode 100644 index 0000000..395d80f --- /dev/null +++ b/SnipInsight/ImageCapture/ImageCaptureCursor.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Input; +using System.Windows.Interop; +using Microsoft.Win32.SafeHandles; +using SnipInsight.Util; + +namespace SnipInsight.ImageCapture +{ + /// + /// See http://stackoverflow.com/questions/9218029/safefilehandle-close-throws-an-exception-but-the-handle-is-valid-and-works + /// + class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeIconHandle() + : base(true) + { + } + + public SafeIconHandle(IntPtr hIcon) + : this() + { + SetHandle(hIcon); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.DestroyIcon(this.handle); + } + } + + public class ImageCaptureCursor : IDisposable + { + private Cursor _cursor; + private SafeIconHandle _safeHandle; + + public Cursor GetCursor() + { + if (_cursor == null) + { + _cursor = CrossHairCursor(64, 64); + } + return _cursor; + } + + private Cursor CrossHairCursor(int w, int h) + { + Pen pen = new Pen(Color.Red, 5); + Pen thinPen = new Pen(Color.Red, 1); + var pic = new Bitmap(w, h); + var gr = Graphics.FromImage(pic); + + var pathXL = new GraphicsPath(); + var pathXR = new GraphicsPath(); + var pathX = new GraphicsPath(); + + var pathYT = new GraphicsPath(); + var pathYB = new GraphicsPath(); + var pathY = new GraphicsPath(); + + pathY.AddLine(new Point(w / 2, 0), new Point(w / 2, h)); + pathYT.AddLine(new Point(w / 2, h / 2 - 2 * 16), new Point(w / 2, h / 2 - 16)); + pathYB.AddLine(new Point(w / 2, h / 2 + 2 * 16), new Point(w / 2, h / 2 + 16)); + + pathX.AddLine(new Point(0, h / 2), new Point(w, h / 2)); + pathXL.AddLine(new Point(w / 2 - 2 * 16, h / 2), new Point(w / 2 - 16, h / 2)); + pathXR.AddLine(new Point(w / 2 + 16, h / 2), new Point(w / 2 + 2 * 16, h / 2)); + + gr.DrawPath(pen, pathXL); + gr.DrawPath(pen, pathXR); + gr.DrawPath(pen, pathYT); + gr.DrawPath(pen, pathYB); + + gr.DrawPath(thinPen, pathX); + gr.DrawPath(thinPen, pathY); + var icon = Icon.FromHandle(pic.GetHicon()); + SafeIconHandle safeHandle = _safeHandle; + if (safeHandle != null) + { + safeHandle.Dispose(); + } + _safeHandle = new SafeIconHandle(icon.Handle); + safeHandle = _safeHandle; + return CursorInteropHelper.Create(safeHandle); + } + + ~ImageCaptureCursor() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (_cursor != null) + { + _cursor.Dispose(); + _cursor = null; + } + if (_safeHandle != null) + { + _safeHandle.Dispose(); + _safeHandle = null; + } + } + } + } +} diff --git a/SnipInsight/ImageCapture/ImageCaptureManager.cs b/SnipInsight/ImageCapture/ImageCaptureManager.cs new file mode 100644 index 0000000..ce033b3 --- /dev/null +++ b/SnipInsight/ImageCapture/ImageCaptureManager.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Drawing; + +namespace SnipInsight.ImageCapture +{ + /// + /// capture the image + /// + public class ImageCaptureManager : IImageCaptureManager, IDisposable + { + private ImageCaptureWindow _wCapture; + + private ImageCaptureCursor _imageCaptureCursor; + + private readonly AreaSelection _areaSelection; + + public ImageCaptureManager() + { + // SetDpiAwareness(); + _areaSelection = new AreaSelection(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_wCapture != null) + { + _wCapture.Close(); + _wCapture = null; + } + if (_imageCaptureCursor != null) + { + _imageCaptureCursor.Dispose(); + _imageCaptureCursor = null; + } + } + } + + ~ImageCaptureManager() + { + Dispose(false); + } + + /// + /// start capturing + /// + public void StartCapture() + { + Rectangle rctDeskTop = SmartBoundaryDetection.GetDesktopBounds(); + _imageCaptureCursor = new ImageCaptureCursor(); + var cursor = _imageCaptureCursor.GetCursor(); + + _wCapture = new ImageCaptureWindow(rctDeskTop, _areaSelection); + + _wCapture.Cursor = cursor; + _wCapture.NotifyCapturingDone += OnScreenCapturingDoneNotify; + _wCapture.NotifyCapturingCancel += OnScreenCapturingCancelNotify; + _wCapture.Show(); + } + + public void CapturingDone() + { + _wCapture.CapturingDone(_wCapture, null); + } + + public void CapturingCancel() + { + _wCapture.CapturingCancel(_wCapture, null); + } + + public event EventHandler CaptureCompleted; + + private void OnScreenCapturingDoneNotify(object sender, EventArgs args) + { + if (_wCapture != null) + { + _wCapture.Close(); + } + + var captureCompleted = CaptureCompleted; + if (captureCompleted != null) + { + captureCompleted(this, (ImageCaptureEventArgs)args); + } + } + + private void OnScreenCapturingCancelNotify(object sender, EventArgs args) + { + if (_wCapture != null) + { + _wCapture.Close(); + } + + var captureCompleted = CaptureCompleted; + if (captureCompleted != null) + { + captureCompleted(this, new ImageCaptureEventArgs(null)); + } + } + } +} diff --git a/SnipInsight/ImageCapture/ImageCaptureWindow.xaml b/SnipInsight/ImageCapture/ImageCaptureWindow.xaml new file mode 100644 index 0000000..5d37d66 --- /dev/null +++ b/SnipInsight/ImageCapture/ImageCaptureWindow.xaml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/ImageCapture/ImageCaptureWindow.xaml.cs b/SnipInsight/ImageCapture/ImageCaptureWindow.xaml.cs new file mode 100644 index 0000000..7c930ed --- /dev/null +++ b/SnipInsight/ImageCapture/ImageCaptureWindow.xaml.cs @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Media.Imaging; +using SnipInsight.Util; +using MouseEventArgs = System.Windows.Input.MouseEventArgs; + + +namespace SnipInsight.ImageCapture +{ + /// + /// Interaction logic for ImageCaptureWindow.xaml + /// + public partial class ImageCaptureWindow + { + enum MagnifierPosition + { + // magnifier stays at the cursor upper left quadrant + UpperLeft, + // magnifier stays at the cursor upper right quadrant + UpperRight, + // magnifier stays at the cursor lower left quadrant + LowerRight, + // magnifier stays at the cursor lower left quadrant + LowerLeft, + // magnifier overlays the cursor + Center, + } + + public static RoutedCommand CaptureFullScreenCommand = new RoutedCommand(); + public static RoutedCommand CaptureDoneCommand = new RoutedCommand(); + public static RoutedCommand CaptureCancelCommand = new RoutedCommand(); + public static RoutedCommand CaptureKeySpaceCommand = new RoutedCommand(); + public static RoutedCommand CaptureKeyRightCommand = new RoutedCommand(); + public static RoutedCommand CaptureKeyLeftCommand = new RoutedCommand(); + public static RoutedCommand CaptureKeyUpCommand = new RoutedCommand(); + public static RoutedCommand CaptureKeyDownCommand = new RoutedCommand(); + + public event EventHandler NotifyCapturingDone; + public event EventHandler NotifyCapturingCancel; + + private readonly AreaSelection _areaSelection; + private readonly DpiScale _scalor; + private readonly double _screenScalor; + + private ScreenshotImage _screenShotImage = null; + + private const int MagnifierRadius = 41; + private const int MagnifierDiameter = MagnifierRadius*2; + private const int MagnifierHorizonSpace = 8; + private const int MagnifierVerticalSpace = 10; + private const int MagnifierTextHeight = 10; + private const int MagnifierHeight = MagnifierVerticalSpace + MagnifierDiameter + MagnifierTextHeight; //100 + private const int MagnifierWidth = MagnifierHorizonSpace + MagnifierDiameter; + private const double MagnifierZoomFactor = 2; + private bool ClickSpace = false; + + public ImageCaptureWindow(System.Drawing.Rectangle rect, AreaSelection areaSelection) + { + InitializeComponent(); + + MouseUp += OnMouseUp; + MouseDown += OnMouseDown; + MouseMove += OnMouseMove; + Loaded += OnMainWindowLoaded; + _scalor = DpiUtilities.GetVirtualPixelScale(this); + + _areaSelection = areaSelection; + _screenShotImage = new ScreenshotImage(); + + _screenScalor = DpiUtilities.GetScreenScalingFactor(); + _screenShotImage.SnapShot(rect, _screenScalor); + + WindowStartupLocation = WindowStartupLocation.Manual; + + SourceInitialized += (sender, e) => + { + IntPtr hWnd = new WindowInteropHelper(this).Handle; + NativeMethods.SetWindowPos(hWnd, (IntPtr)NativeMethods.SetWindowPosInsertAfter.HWND_TOP, rect.Left, rect.Top, rect.Width, rect.Height, 0); + }; + + var bmDesktopSource = ScreenCapture.GetBitmapSource(_screenShotImage.ScreenSnapshotImage); + + BackgroundImage.Fill = new ImageBrush(bmDesktopSource); + MagnifierBackgroundImage.Source = bmDesktopSource; + } + + void OnMainWindowLoaded(object sender, RoutedEventArgs e) + { + var animateAnts = TryFindResource("animateAnts") as Storyboard; + if (animateAnts != null) + { + animateAnts.Begin(); + } + } + + private BitmapSource GetCapturedImage() + { + var rect = _areaSelection.GetSelectedArea(); + // decide which monitor the selected area belongs to by checking the mid point of the selected area + var x = (rect.left + rect.width/2); + var y = (rect.top + rect.height/2); + + var hMonitor = NativeMethods.MonitorFromPoint(new NativeMethods.POINT() { x = x, y = y }, 0); + var curMon = _areaSelection.ScreenProps.GetMonitorInformation(hMonitor); + var monitorScalingX = curMon.dpiX / 96 / _screenScalor; + var monitorScalingY = curMon.dpiY / 96 / _screenScalor; + var dpiScaler = new DpiScale(monitorScalingX, monitorScalingY); + var capturedImage = _screenShotImage.GetCaptureImage(rect, dpiScaler); + + // Copies to clipboard based on user setting + if (UserSettings.CopyToClipboardAfterSnip) + { + System.Windows.Clipboard.SetImage(capturedImage); + } + + return capturedImage; + } + + private void OnMouseUp(object sender, MouseEventArgs e) + { + _areaSelection.EndDragging(); + var capturedImage = GetCapturedImage(); + + if (NotifyCapturingDone != null) + { + NotifyCapturingDone(this, new ImageCaptureEventArgs(capturedImage)); + } + } + + private void OnMouseDown(object sender, MouseEventArgs e) + { + var p = Mouse.GetPosition(this); + var pos = PointToScreen(p); + _areaSelection.StartDragging((int)pos.X, (int)pos.Y); + } + + private void OnMouseMove(object sender, MouseEventArgs e) + { + var p = Mouse.GetPosition(this); + var cursorPos = PointToScreen(p); + + _areaSelection.Dragging((int)cursorPos.X, (int)cursorPos.Y); + var selectedArea = _areaSelection.GetSelectedArea(); + + UpdateSelectedArea(selectedArea); + + double length = MagnifierCircle.ActualWidth / MagnifierZoomFactor; + double radius = (length - 1) / 2; + Rect viewboxRect = new Rect(p.X * _screenScalor - radius, (p.Y * _screenScalor - radius), length, length); + MagnifierBrush.Viewbox = viewboxRect; + + var width = selectedArea.right - selectedArea.left; + var height = selectedArea.bottom - selectedArea.top; + MagnifierText.Text = string.Format("{0} x {1}", width, height); + + UpdateMagnifierPosition(p); + MagnifierPanel.Visibility = Visibility.Visible; + } + + internal void CapturingFullScreen(object sender, ExecutedRoutedEventArgs e) + { + var capturedImage = ScreenCapture.GetBitmapSource(_screenShotImage.ScreenSnapshotImage); + + if (NotifyCapturingDone != null) + { + NotifyCapturingDone(this, new ImageCaptureEventArgs(capturedImage)); + } + } + + internal void CapturingDone(object sender, ExecutedRoutedEventArgs e) + { + var capturedImage = GetCapturedImage(); + + if (NotifyCapturingDone != null) + { + NotifyCapturingDone(this, new ImageCaptureEventArgs(capturedImage)); + } + } + + internal void CapturingCancel(object sender, ExecutedRoutedEventArgs e) + { + if (NotifyCapturingCancel != null) + { + NotifyCapturingCancel(this, null); + } + } + + [DllImport("User32.dll")] + private static extern bool SetCursorPos(int X, int Y); + + /// + /// Move pointer using mouse + /// + internal void CapturingUp(object sender, ExecutedRoutedEventArgs e) + { + var p = Mouse.GetPosition(this); + var cursorPos = PointToScreen(p); + + SetCursorPos((int)cursorPos.X, (int)cursorPos.Y-10); + + } + + /// + /// Move pointer using mouse + /// + internal void CapturingDown(object sender, ExecutedRoutedEventArgs e) + { + var p = Mouse.GetPosition(this); + var cursorPos = PointToScreen(p); + + SetCursorPos((int)cursorPos.X, (int)cursorPos.Y + 10); + + } + + /// + /// Move pointer using mouse + /// + internal void CapturingRight(object sender, ExecutedRoutedEventArgs e) + { + var p = Mouse.GetPosition(this); + var cursorPos = PointToScreen(p); + + SetCursorPos((int)cursorPos.X + 10, (int)cursorPos.Y); + + } + + /// + /// Move pointer using mouse + /// + internal void CapturingLeft(object sender, ExecutedRoutedEventArgs e) + { + var p = Mouse.GetPosition(this); + var cursorPos = PointToScreen(p); + + SetCursorPos((int)cursorPos.X - 10, (int)cursorPos.Y); + + } + + /// + /// Start and stop snip cropping + /// + internal void CapturingSpace(object sender, ExecutedRoutedEventArgs e) + { + if (!ClickSpace) + { + var p = Mouse.GetPosition(this); + var pos = PointToScreen(p); + _areaSelection.StartDragging((int)pos.X, (int)pos.Y); + } + else + { + _areaSelection.EndDragging(); + var capturedImage = GetCapturedImage(); + + if (NotifyCapturingDone != null) + { + NotifyCapturingDone(this, new ImageCaptureEventArgs(capturedImage)); + } + } + ClickSpace = !ClickSpace; + } + + private void UpdateSelectedArea(NativeMethods.RECT area) + { + var topLeft = PointFromScreen(new System.Windows.Point(area.left, area.top)); + var bottomRight = PointFromScreen(new System.Windows.Point(area.right, area.bottom)); + + ForegroundClip.Rect = new Rect(topLeft, bottomRight); + ForegroundAnts.Width = bottomRight.X - topLeft.X; + ForegroundAnts.Height = bottomRight.Y - topLeft.Y; + ForegroundAntsTransform.X = topLeft.X; + ForegroundAntsTransform.Y = topLeft.Y; + } + +#region Magnifier + + private MagnifierPosition GetMagnifierPosition(out double monitorScalingX, out double monitorScalingY) + { + var pos = System.Windows.Forms.Control.MousePosition; + var screen = System.Windows.Forms.Screen.FromPoint(pos); + + var hMonitor = NativeMethods.MonitorFromPoint(new NativeMethods.POINT() { x = pos.X, y = pos.Y }, 0); + var curMon = _areaSelection.ScreenProps.GetMonitorInformation(hMonitor); + monitorScalingX = curMon.dpiX / 96 /_screenScalor; + monitorScalingY = curMon.dpiY / 96 / _screenScalor; +//#if (TRACE) +// System.Diagnostics.Trace.WriteLine("Screen :" + screen); +// System.Diagnostics.Trace.WriteLine("_screenScalor :" + _screenScalor); +// System.Diagnostics.Trace.WriteLine("pos x:" + pos.X + " y:" + pos.Y); +// System.Diagnostics.Trace.WriteLine("monitorScalingX:" + monitorScalingX + " monitorScalingY:" + monitorScalingY); +//#endif + + var x = Math.Abs(pos.X - screen.Bounds.Left); + var y = Math.Abs(pos.Y - screen.Bounds.Top); + + // the cursor is at the monitor's right edge + bool isRight = x + MagnifierWidth * _scalor.X * monitorScalingX >= screen.Bounds.Width; + // the cursor is at the monitor's left edge + bool isLeft = x <= MagnifierWidth * _scalor.X * monitorScalingX; + // the cursor is at the monitor's bottom edge + bool isLower = y + MagnifierHeight * _scalor.Y * monitorScalingY >= screen.Bounds.Height; + // the cursor is at the monitor's top edge + bool isUpper = y <= MagnifierHeight * _scalor.Y * monitorScalingY; + if (isRight && isLower) + { + return MagnifierPosition.UpperLeft; + } + else if (isLeft && isLower) + { + return MagnifierPosition.UpperRight; + } + else if (isLeft && isUpper) + { + return MagnifierPosition.LowerRight; + } + else if (isRight && isUpper) + { + return MagnifierPosition.LowerLeft; + } + else if (isLower) + { + if (_areaSelection.DraggingRight) + { + return MagnifierPosition.UpperRight; + } + else + { + return MagnifierPosition.UpperLeft; + } + } + else if (isUpper) + { + if (_areaSelection.DraggingRight) + { + return MagnifierPosition.LowerRight; + } + else + { + return MagnifierPosition.LowerLeft; + } + } + else if (isLeft) + { + if (_areaSelection.DraggingDown) + { + return MagnifierPosition.LowerRight; + } + else + { + return MagnifierPosition.UpperRight; + } + } + else if (isRight) + { + if (_areaSelection.DraggingDown) + { + return MagnifierPosition.LowerLeft; + } + else + { + return MagnifierPosition.UpperLeft; + } + } + else if (!_areaSelection.DraggingDown && _areaSelection.DraggingRight) + { + return MagnifierPosition.LowerRight; + } + + return MagnifierPosition.LowerLeft; + } + + private void UpdateMagnifierPosition(System.Windows.Point p) + { + double left; + double top; + double monitorScalingX, monitorScalingY; +#if CHANGE_MAGNIFIER_POSITION + var pos = MagnifierPosition.Center; +#else + var pos = GetMagnifierPosition(out monitorScalingX, out monitorScalingY); +#endif + + switch (pos) + { + case MagnifierPosition.LowerLeft: + left = p.X - MagnifierWidth; + top = p.Y + MagnifierVerticalSpace; + break; + case MagnifierPosition.UpperLeft: + left = p.X - MagnifierWidth; + top = p.Y - MagnifierHeight; + break; + case MagnifierPosition.UpperRight: + left = p.X + MagnifierHorizonSpace; + top = p.Y - MagnifierHeight; + break; + case MagnifierPosition.LowerRight: + left = p.X + MagnifierHorizonSpace; + top = p.Y + MagnifierVerticalSpace; + break; + case MagnifierPosition.Center: + left = p.X - MagnifierRadius; + top = p.Y - MagnifierRadius; + break; + default: + throw new InvalidOperationException("invalid MagnifierPosition"); + } + + MagnifierTransform.X = left; + MagnifierTransform.Y = top; + MagnifierScale.CenterX = p.X; + MagnifierScale.CenterY = p.Y; + MagnifierScale.ScaleX = monitorScalingX; + MagnifierScale.ScaleY = monitorScalingY; + } +#endregion + } +} diff --git a/SnipInsight/ImageCapture/ImageLoader.cs b/SnipInsight/ImageCapture/ImageLoader.cs new file mode 100644 index 0000000..48d9b89 --- /dev/null +++ b/SnipInsight/ImageCapture/ImageLoader.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace SnipInsight.ImageCapture +{ + /// + /// Loading a bitmap image. + /// + class ImageLoader + { + /// + /// Loading a bitmap image from URL handles async and sync loading + /// + /// url as a string + /// task object with result of BitmapImage if successful, else exception + public static Task LoadFromUrl(Uri uri) + { + var tcs = new TaskCompletionSource(); + + var bitmap = new BitmapImage(); + + bitmap.DownloadCompleted += (object sender, EventArgs e) => + { + tcs.TrySetResult(bitmap); + }; + + bitmap.DownloadFailed += (object sender, System.Windows.Media.ExceptionEventArgs e) => + { + tcs.TrySetException(e.ErrorException); + }; + + bitmap.BeginInit(); + bitmap.UriSource=uri; + bitmap.EndInit(); + + if (!bitmap.IsDownloading) + { + tcs.TrySetResult(bitmap); + } + + return tcs.Task; + } + } +} \ No newline at end of file diff --git a/SnipInsight/ImageCapture/RectangleConverter.cs b/SnipInsight/ImageCapture/RectangleConverter.cs new file mode 100644 index 0000000..485ca60 --- /dev/null +++ b/SnipInsight/ImageCapture/RectangleConverter.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows; +using System.Windows.Markup; + +namespace SnipInsight.ImageCapture +{ + public class RectangleConverter : MarkupExtension, IMultiValueConverter + { + private static RectangleConverter _instance; + + #region IValueConverter Members + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length == 2) + { + var width = System.Convert.ToDouble(values[0]); + var height = System.Convert.ToDouble(values[1]); + return new Rect(0, 0, width, height); + } + return null; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + #endregion + + public RectangleConverter() + { + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return _instance ?? (_instance = new RectangleConverter()); + } + } +} diff --git a/SnipInsight/ImageCapture/ScreenCapture.cs b/SnipInsight/ImageCapture/ScreenCapture.cs new file mode 100644 index 0000000..d2e734d --- /dev/null +++ b/SnipInsight/ImageCapture/ScreenCapture.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using SnipInsight.Util; + +namespace SnipInsight.ImageCapture +{ + internal static class ScreenCapture + { + /// + /// Creates an Image object containing a screen shot of a specific window + /// + /// + /// + /// + /// + /// + /// + public static Bitmap CaptureWindowRegion(IntPtr hWnd, int x, int y, int width, int height) + { + Bitmap bmCapture = null; + var dpiScale = DpiUtilities.GetSystemScale(); + if (width > 0 && height > 0) + { + IntPtr ptrWindowDc = NativeMethods.GetWindowDC(hWnd); + bmCapture = new Bitmap(width, height, + System.Drawing.Imaging.PixelFormat.Format32bppRgb); + bmCapture.MakeTransparent(); + Graphics g = Graphics.FromImage(bmCapture); + IntPtr hdcPtr = g.GetHdc(); + + NativeMethods.BitBlt(hdcPtr, 0, 0, width, height, ptrWindowDc, x, + y, NativeMethods.TernaryRasterOperations.SRCCOPY | NativeMethods.TernaryRasterOperations.CAPTUREBLT); + bmCapture.SetResolution((float)(96.0 * dpiScale.X), (float)(96.0 * dpiScale.Y)); + + g.ReleaseHdc(); + NativeMethods.ReleaseDC(IntPtr.Zero, ptrWindowDc); + g.Dispose(); + } + return bmCapture; + } + + public static BitmapSource GetBitmapSource(System.Drawing.Bitmap source) + { + if (source == null) + { + return null; + } + + // + // The following conversion preserves the DPI of the original image. + // + + BitmapSource bs = null; + + BitmapData data = source.LockBits(new System.Drawing.Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, source.PixelFormat); + + try + { + bs = BitmapSource.Create(source.Width, source.Height, source.HorizontalResolution, source.VerticalResolution, PixelFormats.Bgra32, null, + data.Scan0, data.Stride * source.Height, data.Stride); + } + finally + { + source.UnlockBits(data); + } + + return bs; + } + + public static BitmapSource CaptureBmpFromImage(Bitmap bmpImage, Rectangle r, DpiScale dpiScale = null) + { + Bitmap srcImage = CropImage(bmpImage, r); + if (dpiScale != null && dpiScale.X > 0.0 && dpiScale.Y > 0.0) + { + // This preserves the DPI from the original screen where the image + // was captured. + srcImage.SetResolution((float)(96.0 * dpiScale.X), (float)(96.0 * dpiScale.Y)); + } + return GetBitmapSource(srcImage); + } + + public static Bitmap CropImage(Bitmap bmpImage, Rectangle r) + { + Bitmap nb = new Bitmap(r.Width, r.Height); + using (Graphics g = Graphics.FromImage(nb)) + { + g.DrawImage(bmpImage, -r.X, -r.Y); + } + return nb; + } + + public static Bitmap ResizeImage(Bitmap srcImage, int newWidth, int newHeight) + { + Bitmap newImage = new Bitmap(newWidth, newHeight); + using (Graphics gr = Graphics.FromImage(newImage)) + { + gr.SmoothingMode = SmoothingMode.HighQuality; + gr.InterpolationMode = InterpolationMode.HighQualityBicubic; + gr.PixelOffsetMode = PixelOffsetMode.HighQuality; + gr.DrawImage(srcImage, new Rectangle(0, 0, newWidth, newHeight)); + } + return newImage; + } + } +} diff --git a/SnipInsight/ImageCapture/ScreenProperties.cs b/SnipInsight/ImageCapture/ScreenProperties.cs new file mode 100644 index 0000000..6e53b9b --- /dev/null +++ b/SnipInsight/ImageCapture/ScreenProperties.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Windows; +using SnipInsight.Util; + +namespace SnipInsight.ImageCapture +{ + /// + /// Implements a cache for getting monitor properties + /// Note: Cache must get invalidated if user changes screen resolution + /// + public class ScreenProperties + { + public class MonitorInformation + { + public string deviceName; + public bool isPrimary; + internal NativeMethods.RECT rcMonitor; + internal NativeMethods.RECT rcWork; + public double scalingFactor; + public double dpiX; + public double dpiY; + } + + Dictionary _monitorInfoCache = new Dictionary(); + + public ScreenProperties() + { + GetMonitorsInformation(); + } + + public MonitorInformation GetMonitorInformation(IntPtr hMonitor) + { + if (_monitorInfoCache.ContainsKey(hMonitor)) + return _monitorInfoCache[hMonitor]; + + if (hMonitor == IntPtr.Zero) + { + // Get handle to the default monitor + const int MONITOR_DEFAULTTOPRIMARY = 0x00000001; + hMonitor = NativeMethods.MonitorFromWindow(IntPtr.Zero, MONITOR_DEFAULTTOPRIMARY); + if (_monitorInfoCache.ContainsKey(hMonitor)) + { + _monitorInfoCache[IntPtr.Zero] = _monitorInfoCache[hMonitor]; + return _monitorInfoCache[hMonitor]; + } + } + + var monitorInfo = NativeMethods.MONITORINFOEX.New(); + if (!NativeMethods.GetMonitorInfoEx(hMonitor, ref monitorInfo)) + { + _monitorInfoCache[hMonitor] = null; + return null; + } + + System.Diagnostics.Trace.WriteLine("Monitor:\"" + monitorInfo.deviceName + "\" Handle:" + hMonitor + + " Left:" + monitorInfo.rcMonitor.left + " Top:" + monitorInfo.rcMonitor.top + + " Right:" + monitorInfo.rcMonitor.right + " Bottom:" + monitorInfo.rcMonitor.bottom); + + UInt32 effectiveDPIx, effectiveDPIy; + //GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_Effective_DPI, out effectiveDPIx, out effectiveDPIy); + DpiUtilities.GetMonitorEffectiveDpi(hMonitor, out effectiveDPIx, out effectiveDPIy); + System.Diagnostics.Trace.WriteLine("Effective DPI:" + effectiveDPIx + " " + effectiveDPIy); + + //UInt32 rawDPIx, rawDPIy; + //GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_Raw_DPI, out rawDPIx, out rawDPIy); + //System.Diagnostics.Trace.WriteLine("Raw DPI:" + rawDPIx + " " + rawDPIy); + + var monitorInformation = new MonitorInformation() + { + deviceName = monitorInfo.deviceName, + isPrimary = (monitorInfo.dwFlags & NativeMethods.MONITORINFOF_PRIMARY) != 0, + rcMonitor = monitorInfo.rcMonitor, + rcWork = monitorInfo.rcWork, + scalingFactor = DpiUtilities.GetScreenScalingFactor(monitorInfo.deviceName), + dpiX = effectiveDPIx, + dpiY = effectiveDPIy, + }; + + _monitorInfoCache[hMonitor] = monitorInformation; + if (monitorInformation.isPrimary && !_monitorInfoCache.ContainsKey(IntPtr.Zero)) + _monitorInfoCache[IntPtr.Zero] = monitorInformation; + return monitorInformation; + } + + private void GetMonitorsInformation() + { + NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, + delegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData) + { + GetMonitorInformation(hMonitor); + return true; + }, IntPtr.Zero); + } + } +} diff --git a/SnipInsight/ImageCapture/ScreenshotImage.cs b/SnipInsight/ImageCapture/ScreenshotImage.cs new file mode 100644 index 0000000..b887b2f --- /dev/null +++ b/SnipInsight/ImageCapture/ScreenshotImage.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Drawing; +using System.Windows.Media.Imaging; +using SnipInsight.Util; +using Rectangle = System.Drawing.Rectangle; + +namespace SnipInsight.ImageCapture +{ + /// + /// This class is to manage the screen snapshot of the time that user clicks image capture button + /// + internal class ScreenshotImage : IDisposable + { + private Rectangle _rectangleCaptureImage; + private Bitmap _screenSnapshot; + private double _screenScalor; + + public Bitmap ScreenSnapshotImage + { + get { return _screenSnapshot; } + } + + public ScreenshotImage() + { + } + + public void Dispose() + { + if (_screenSnapshot != null) + { + _screenSnapshot.Dispose(); + } + } + + ~ScreenshotImage() + { + if (_screenSnapshot != null) + { + _screenSnapshot.Dispose(); + } + } + + public void SnapShot(Rectangle rectangle, double screenScalor) + { + _screenScalor = screenScalor; + _rectangleCaptureImage = new Rectangle( + (int)(rectangle.Left * _screenScalor), + (int)(rectangle.Top * _screenScalor), + (int)(rectangle.Width * _screenScalor), + (int)(rectangle.Height * _screenScalor) + ); + + var bmDesktop = ScreenCapture.CaptureWindowRegion(IntPtr.Zero, _rectangleCaptureImage.Left, _rectangleCaptureImage.Top, + _rectangleCaptureImage.Width, _rectangleCaptureImage.Height); + _screenSnapshot = bmDesktop; + } + + public BitmapSource GetCaptureImage(NativeMethods.RECT rect, DpiScale dpiScaleOfSourceWindow) + { + if (_screenScalor != 1) + { + DpiScale adjustedScale = new DpiScale(dpiScaleOfSourceWindow.X * _screenScalor, dpiScaleOfSourceWindow.Y * _screenScalor); + dpiScaleOfSourceWindow = adjustedScale; + } + + // crop the invisible area + var left = rect.left * _screenScalor < _rectangleCaptureImage.Left ? _rectangleCaptureImage.Left : rect.left * _screenScalor; + var top = rect.top * _screenScalor < _rectangleCaptureImage.Top ? _rectangleCaptureImage.Top : rect.top * _screenScalor; + var right = rect.right * _screenScalor > _rectangleCaptureImage.Right ? _rectangleCaptureImage.Right : rect.right * _screenScalor; + var bottom = rect.bottom * _screenScalor > _rectangleCaptureImage.Bottom ? _rectangleCaptureImage.Bottom : rect.bottom * _screenScalor; + + var height = bottom - top; + var width = right - left; + + left = left - _rectangleCaptureImage.Left; + top = top - _rectangleCaptureImage.Top; + + + if (left < 0) + { + width = width + left; + left = 0; + } + + if (top < 0) + { + height = height + top; + top = 0; + } + + Rectangle rectangle = new Rectangle( + (int)left, + (int)top, + (int)width, + (int)height + ); + + return ScreenCapture.CaptureBmpFromImage(_screenSnapshot, rectangle, dpiScaleOfSourceWindow); + } + } +} diff --git a/SnipInsight/ImageCapture/SmartBoundaryDetection.cs b/SnipInsight/ImageCapture/SmartBoundaryDetection.cs new file mode 100644 index 0000000..eb40533 --- /dev/null +++ b/SnipInsight/ImageCapture/SmartBoundaryDetection.cs @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Windows.Forms; +using SnipInsight.Util; + +namespace SnipInsight.ImageCapture +{ + /// + /// Algorithm to detect the UI Element where the mouse cursor is + /// + public class SmartBoundaryDetection + { + /// + /// Cache of multiple monitor properties + /// + private ScreenProperties _screenProps; + + /// + /// It has all the top level UI element handles + /// + private List _topElementHnds = new List(); + + /// + /// the window handle corresponding zorder + /// + private Dictionary _windowZOrderMapping = new Dictionary(); + + /// + /// the window handle corresponding Bounding rectangle + /// + private Dictionary _windowRectMapping = new Dictionary(); + + /// + /// the window handle corresponding monitor handle + /// + private Dictionary _windowMonitorMapping = new Dictionary(); + + private const string LogId = "SmartBoundaryDetection:"; + + /// + /// constructor + /// As the capture will put two forms on the UI, SmartBoundaryDetection has + /// to take a snapshot of the UI elements before the two forms show up + /// + public SmartBoundaryDetection(ScreenProperties screenProps) + { + _screenProps = screenProps; + GetTopLevelWindows(); + } + + /// + /// Get desktop outer boundaries in screen coordinates + /// + /// + public static Rectangle GetDesktopBounds() + { + // GetShellWindow returns Windows Explorer window which covers all monitors. + // Notes: + // GetDesktopWindow returns a window for the primary monitor only. + // SystemInformation.VirtualScreen may be incorrect if the primary monitor has low DPI + // + Rectangle rctDeskTop; + IntPtr hWndProgMan = NativeMethods.GetShellWindow(); + if (hWndProgMan != IntPtr.Zero) + { + NativeMethods.RECT rect; + NativeMethods.GetWindowRect(hWndProgMan, out rect); + rctDeskTop = new Rectangle(rect.left, rect.top, rect.width, rect.height); + } + else + { + // In the unlikely case shell window doesn't exist... + rctDeskTop = new Rectangle( + SystemInformation.VirtualScreen.Left, + SystemInformation.VirtualScreen.Top, + SystemInformation.VirtualScreen.Right - SystemInformation.VirtualScreen.Left, + SystemInformation.VirtualScreen.Bottom - SystemInformation.VirtualScreen.Top + ); + } + return rctDeskTop; + } + + /// + /// get the top UI elements + /// + /// + /// + /// + public IntPtr GetTopElement(int x, int y) + { + List elementHnds = GetElementHndsInRange(_topElementHnds, x, y); + if (elementHnds == null || elementHnds.Count == 0) + { + return IntPtr.Zero; + } + + var minElementHandle = elementHnds[0]; + int minZOrder = int.MaxValue; + foreach (var ohWnd in elementHnds) + { + int zOrder = 0; + + if (_windowZOrderMapping.TryGetValue(ohWnd, out zOrder)) + { + if (zOrder < minZOrder) + { + minZOrder = zOrder; + minElementHandle = ohWnd; + } + } + } + + // Check if the window is fully visible before looking for child windows + if (minElementHandle == IntPtr.Zero) + return IntPtr.Zero; + + var hdc = NativeMethods.GetDC(minElementHandle); + if (hdc != IntPtr.Zero) + { + NativeMethods.RECT rcClip, rcClient; + var ret = NativeMethods.GetClipBox(hdc, out rcClip); + NativeMethods.GetClientRect(minElementHandle, out rcClient); + NativeMethods.ReleaseDC(minElementHandle, hdc); + if (ret == (int)NativeMethods.GetClipBoxReturn.SimpleRegion && + rcClip.left == rcClient.left && + rcClip.right == rcClient.right && + rcClip.bottom == rcClient.bottom && + rcClip.top == rcClient.top) + { + IntPtr element = GetTopElement(minElementHandle, x, y); + if (element != IntPtr.Zero) + { + return element; + } + } + + } + return minElementHandle; + } + + /// + /// Return the UI windows with the smallest zorder, i.e. the top window + /// + /// + /// + /// + /// + public IntPtr GetTopElement(IntPtr eHnd, int x, int y) + { + List childrenWHnds = new List(); + try + { + NativeMethods.EnumChildWindows( + eHnd, (hWnd, lParam) => + { + try + { + if (IsVisibleWindow(hWnd)) + { + childrenWHnds.Add(hWnd); + } + return true; + } + catch (Exception ex) + { + Diagnostics.LogTrace("EnumChildrenWindowsProc Exception hWnd:" + hWnd); + Diagnostics.LogException(ex); + return false; + } + + }, IntPtr.Zero); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + + if (childrenWHnds.Count > 0) + { + List eleChildren = GetElementHndsInRange(childrenWHnds, x, y); + if (eleChildren != null && eleChildren.Count > 0) + { + if (eleChildren.Count > 1) + { + //TODO(qiazhang) should we return the smallest window? + return eleChildren[0]; + } + } + } + return eHnd; + } + + /// + /// Return the window boundary rectangle + /// + /// + /// + internal NativeMethods.RECT GetBoundaryRect(IntPtr wHnd) + { + NativeMethods.RECT rect; + if (_windowRectMapping.ContainsKey(wHnd)) + { + if (_windowRectMapping.TryGetValue(wHnd, out rect)) + { + return rect; + } + } + + // go get it + NativeMethods.GetWindowRect(wHnd, out rect); + _windowRectMapping.Add(wHnd, rect); + return rect; + } + + /// + /// Return the window monitor + /// + /// + /// + internal IntPtr GetMonitor(IntPtr wHnd) + { + IntPtr monitor = IntPtr.Zero; + if (_windowMonitorMapping.ContainsKey(wHnd)) + { + if (_windowMonitorMapping.TryGetValue(wHnd, out monitor)) + { + return monitor; + } + } + + // go get it + const int MONITOR_DEFAULTTONEAREST = 0x00000002; + monitor = NativeMethods.MonitorFromWindow(wHnd, MONITOR_DEFAULTTONEAREST); + _windowMonitorMapping.Add(wHnd, monitor); + return monitor; + } + + /// + /// get the UI elements at point(x,y) + /// + /// + /// + /// + /// + private List GetElementHndsInRange(List elementCollection, double x, double y) + { + List elements = new List(); + if (elementCollection != null && elementCollection.Count > 0) + { + foreach (IntPtr elementHnd in elementCollection) + { + if (InRange((IntPtr) elementHnd, x, y)) + { + elements.Add(elementHnd); + } + } + } + return elements; + } + + /// + /// whether the UI element contains point(x,y) + /// + /// + /// + /// + /// + public bool InRange(IntPtr hWnd, double x, double y) + { + try + { + NativeMethods.RECT clientRect = GetBoundaryRect(hWnd); + + if (clientRect.width == 0 || clientRect.height == 0) + { + return false; + } + + double monitorScaling = 1.0d; + var monitor = GetMonitor(hWnd); + if (monitor != IntPtr.Zero) + { + // Calculate monitor scaling relative to primary monitor + var monitorInfo = _screenProps.GetMonitorInformation(monitor); + var monitorScalingFactor = monitorInfo.scalingFactor; + var defaultScalingFactor = _screenProps.GetMonitorInformation(IntPtr.Zero).scalingFactor; + monitorScaling = monitorScalingFactor / defaultScalingFactor; + } + + if ((clientRect.left * monitorScaling <= x && x <= clientRect.right * monitorScaling) && + (clientRect.top * monitorScaling <= y && y < clientRect.bottom * monitorScaling)) + { + return true; + } + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + return false; + } + + protected bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam) + { + try + { + var processId = Process.GetCurrentProcess().Id; + uint wndProcessId; + NativeMethods.GetWindowThreadProcessId(hWnd, out wndProcessId); + if (wndProcessId != processId) + { + if (IsVisibleWindow(hWnd)) + { + _topElementHnds.Add(hWnd); + + var zOrder = GetZOrder(hWnd); + if (!_windowZOrderMapping.ContainsKey(hWnd)) + { + _windowZOrderMapping.Add(hWnd, zOrder); + } + } + } + return true; + } + catch (Exception ex) + { + Diagnostics.LogTrace("EnumWindowsProc Exception hWnd:" + hWnd); + Diagnostics.LogException(ex); + return false; + } + } + + /// + /// Enumerates all top-level windows on the screen by passing the handle to each window, + /// If EnumWindows meets some exception the callback function returns FALSE so that will stop enumwindow. + /// + private void GetTopLevelWindows() + { + try + { + NativeMethods.EnumWindows(new NativeMethods.EnumWindowsProc(EnumWindowsProc), IntPtr.Zero); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + + /// + /// get z order of the UI element + /// + /// + /// + private int GetZOrder(IntPtr hWnd) + { + var z = 0; + for (IntPtr h = hWnd; h != IntPtr.Zero; h = NativeMethods.GetWindow(h, NativeMethods.GW_HWNDPREV)) z++; + return z; + } + + /// + /// The window meets our conditions: + /// 1. visible + /// 2. transparent + /// + /// + /// + private bool IsVisibleWindow(IntPtr hWnd) + { + if (NativeMethods.IsWindowVisible(hWnd)) + { + // only add non transparent window + uint style = NativeMethods.GetWindowLong(hWnd, NativeMethods.GWL_EXSTYLE); + if ((style & NativeMethods.WS_EX_TRANSPARENT) != NativeMethods.WS_EX_TRANSPARENT) + { + return true; + } + } + return false; + } + } +} diff --git a/SnipInsight/Ink/AcetateLayerInkCanvas.cs b/SnipInsight/Ink/AcetateLayerInkCanvas.cs new file mode 100644 index 0000000..19a4f2f --- /dev/null +++ b/SnipInsight/Ink/AcetateLayerInkCanvas.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Ink; +using System.Windows.Input; + +namespace SnipInsight.Ink +{ + public class AcetateLayerInkCanvas : InkCanvas + { + List undoStrokes; + List redoStrokes; + List undoActions; + List redoActions; + + public AcetateLayerInkCanvas() + { + // TODO: Fix Ink Cursor + //using (Stream inkCursor = new MemoryStream(Properties.Resources.InkCursor)) + //{ + // Cursor = new Cursor(inkCursor); + //} + + Cursor = Cursors.Pen; + EditingMode = InkCanvasEditingMode.None; + EditingModeInverted = InkCanvasEditingMode.None; + undoStrokes = new List(); + redoStrokes = new List(); + undoActions = new List(); + redoActions = new List(); + } + + internal void Clear() + { + Strokes.Clear(); + redoActions.Clear(); + redoStrokes.Clear(); + undoActions.Clear(); + undoStrokes.Clear(); + } + + internal void EraseAll() + { + Strokes.Clear(); + undoActions.Add(InkCanvasEditingMode.None); + undoStrokes.Add(null); + redoActions.Clear(); + redoStrokes.Clear(); + } + + internal bool HasInk() + { + return Strokes.Count > 0; + } + + protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e) + { + base.OnStrokeCollected(e); + undoActions.Add(ActiveEditingMode); + undoStrokes.Add(e.Stroke); + redoActions.Clear(); + redoStrokes.Clear(); + } + + protected override void OnStrokeErasing(InkCanvasStrokeErasingEventArgs e) + { + base.OnStrokeErasing(e); + undoActions.Add(ActiveEditingMode); + undoStrokes.Add(e.Stroke); + redoActions.Clear(); + redoStrokes.Clear(); + } + + protected override void OnActiveEditingModeChanged(RoutedEventArgs e) + { + base.OnActiveEditingModeChanged(e); + + if (ActiveEditingMode == InkCanvasEditingMode.Ink) + { + UseCustomCursor = true; + } + else + { + UseCustomCursor = false; + } + } + + /// + /// Undo edit actions performed + /// + internal void Undo () + { + int index = undoActions.Count - 1; + if (index < 0) + return; + if (undoActions[index] == InkCanvasEditingMode.Ink) + Strokes.RemoveAt(Strokes.Count - 1); + else if (undoActions[index] == InkCanvasEditingMode.EraseByStroke) + Strokes.Add(undoStrokes[index]); + else if (undoActions[index] == InkCanvasEditingMode.None) + for (int i = 0 ; i + /// Redo image edits that were undone + /// + internal void Redo() + { + int index = redoActions.Count - 1; + if (index < 0) + return; + if (redoActions[index] == InkCanvasEditingMode.Ink) + Strokes.Add(redoStrokes[index]); + else if (redoActions[index] == InkCanvasEditingMode.EraseByStroke) + Strokes.RemoveAt(Strokes.Count - 1); + else if (redoActions[index] == InkCanvasEditingMode.None) + Strokes.Clear(); + undoActions.Add(redoActions[index]); + undoStrokes.Add(redoStrokes[index]); + redoActions.RemoveAt(index); + redoStrokes.RemoveAt(index); + } + } +} diff --git a/SnipInsight/Package/PackageData.cs b/SnipInsight/Package/PackageData.cs new file mode 100644 index 0000000..876ecb4 --- /dev/null +++ b/SnipInsight/Package/PackageData.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.IO; + +namespace SnipInsight.Package +{ + public class PackageData : IDisposable + { + public PackageData() + { + + } + + public PackageData(string url, MemoryStream thumbnail, ulong duration, bool hasMedia, bool isPackage) + { + Url = url; + Thumbnail = thumbnail; + Duration = duration; + HasMedia = hasMedia; + IsPackage = isPackage; + } + + /// + /// Url to the image or package. + /// + public string Url { get; set; } + + public MemoryStream Thumbnail { get; set; } + + /// + /// Indicates if there is media (audio/video) in the package. + /// + public bool HasMedia { get; set; } + + /// + /// Indicates if the URL is for mixp package or just png file. + /// + public bool IsPackage { get; set; } // Can be removed in future if everything is a mixp package. + + public ulong Duration { get; set; } + + public DateTime LastWriteTime { get; set; } + + public string MixId { get; set; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (Thumbnail != null) + { + Thumbnail.Dispose(); + Thumbnail = null; + } + } + } + } +} diff --git a/SnipInsight/Package/SnipInsightLink.cs b/SnipInsight/Package/SnipInsightLink.cs new file mode 100644 index 0000000..765894f --- /dev/null +++ b/SnipInsight/Package/SnipInsightLink.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace SnipInsight.Package +{ + using System; + using System.IO; + using System.Windows.Media.Imaging; + + /// + /// Represents a link to a SnipInsight. + /// + public class SnipInsightLink : IDisposable, INotifyPropertyChanged + { + private MemoryStream _imageStream; + + ~SnipInsightLink() + { + Dispose(true); + } + + public BitmapSource ThumbnailImage { get; private set; } + + /// + /// SnipInsight path. + /// + public string Url { get; set; } + + public MemoryStream ImageStream + { + get + { + return _imageStream; + } + set + { + if (_imageStream != null) + { + _imageStream.Dispose(); + _imageStream = null; + } + _imageStream = value; + if (value != null) + { + ThumbnailImage = CreateBitmapSource(value, 112); + } + } + } + + /// + /// Indicates if there is media (audio/video) in the package. + /// + public bool HasMedia { get; set; } + + /// + /// Indicates if the URL is for mixp package or just png file. + /// + public bool HasPackage { get; set; } // Can be removed in future if everything is a mixp package. + + public ulong Duration { get; set; } + + private string _mixId; + + public string MixId + { + get { return _mixId; } + set + { + if (_mixId == value) return; + _mixId = value; + OnPropertyChanged("MixId"); + } + } + + /// + /// True if there is a pending deletion for this item. Helps to avoid multiple deletion without complexity of disabling and enabling close controls. + /// + public bool DeletionPending + { + get { return _deletionPending; } + set + { + if (_deletionPending != value) + { + _deletionPending = value; + OnPropertyChanged("DeletionPending"); + } + } + } + private bool _deletionPending; + + public string LeftCaption + { + get + { + var dateString = LastWriteTime.ToString("MMMM dd, yyyy hh:mm"); //TODO: Need to add to resoure since date format can be different in other countries. + return dateString; + } + } + + public string RightCaption + { + get + { + string durationString = string.Empty; + if (Duration > 0) + { + durationString = " " + TimeSpan.FromMilliseconds(Duration).ToString(@"mm\:ss", null); // TODO: Need to add to resource. + } + return durationString; + } + } + + public DateTime LastWriteTime { get; set; } + + #region Time Grouping + + /// + /// Gets the text label used for grouping items by date. + /// + /// + /// A value that can be used for grouping items by date. + /// + public string TimeGroupingLabel + { + get + { + DateTime now = DateTime.Now; + + DateTime time = LastWriteTime; + + if (time.Year == now.Year) + { + if (time.Month == now.Month && time.Day == now.Day) + { + return "Today"; + } + else if (time >= now.AddDays(-7).Date) + { + return "Last 7 days"; + } + else + { + // If it's the current year, just return month + return LastWriteTime.ToString("MMMM"); + } + } + else + { + return LastWriteTime.ToString("MMMM yyyy"); + } + } + } + + #endregion + + public static BitmapSource CreateBitmapSource(Stream stream, int? maxHeight = null) + { + BitmapImage bitmapImage = null; + if (stream != null) + { + stream.Position = 0; + bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + if (maxHeight.HasValue) + bitmapImage.DecodePixelHeight = maxHeight.Value; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.StreamSource = stream; + bitmapImage.StreamSource.Position = 0; + bitmapImage.EndInit(); + if (bitmapImage.CanFreeze) + { + bitmapImage.Freeze(); + } + } + return bitmapImage; + } + + public override bool Equals(object obj) + { + var other = obj as SnipInsightLink; + if (other != null && other.Url == Url) + { + return true; + } + return false; + } + + public override int GetHashCode() + { + return Url.GetHashCode(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + ThumbnailImage = null; + if (_imageStream != null) + { + _imageStream.Dispose(); + _imageStream = null; + } + } + } + + #region INotifyPropertyChanged implementation + + public event PropertyChangedEventHandler PropertyChanged; + + void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChangedEventHandler handler = PropertyChanged; + if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } +} diff --git a/SnipInsight/Package/SnipInsightLinkTimeSorter.cs b/SnipInsight/Package/SnipInsightLinkTimeSorter.cs new file mode 100644 index 0000000..81c813d --- /dev/null +++ b/SnipInsight/Package/SnipInsightLinkTimeSorter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections; +using System.Collections.Generic; + +namespace SnipInsight.Package +{ + /// + /// Represents a Sorter for SnipInsightLink. This dramatically improves sorting speed in WPF. + /// + public class SnipInsightLinkTimeSorter : IComparer, IComparer + { + public int Compare(object x, object y) + { + return Compare(x as SnipInsightLink, y as SnipInsightLink); + } + + public int Compare(SnipInsightLink x, SnipInsightLink y) + { + if (x == null) + { + if (y == null) + return 0; + else + return 1; + } + else if (y == null) + { + return -1; + } + else + { + return y.LastWriteTime.CompareTo(x.LastWriteTime); + } + } + } +} diff --git a/SnipInsight/Package/SnipInsightsManager.cs b/SnipInsight/Package/SnipInsightsManager.cs new file mode 100644 index 0000000..4abed29 --- /dev/null +++ b/SnipInsight/Package/SnipInsightsManager.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace SnipInsight.Package +{ + /// + /// Manager to maintain and handle snipInsights. + /// + internal class SnipInsightsManager + { + public event EventHandler ImageSaved; + public event EventHandler ImageDeleted; + + private const string SnipInsightsFolder = "My Snips"; + + private readonly string _snipInsightsDirectory; + + public SnipInsightsManager() + { + _snipInsightsDirectory = GetSnipInsightsDirectory(); + } + + public List GetAllSnipInsightFileInfos() + { + DirectoryInfo directoryInfo = new DirectoryInfo(_snipInsightsDirectory); + return directoryInfo.GetFiles().OrderByDescending(p => p.LastWriteTimeUtc).ToList(); + } + + public async Task GetPackageDataAsync(FileInfo file) + { + PackageData data = null; + switch (file.Extension) + { + case ".png": + MemoryStream thumbnail = new MemoryStream(); + using (var fileStream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read)) + { + await fileStream.CopyToAsync(thumbnail); + } + thumbnail.Position = 0; + data = new PackageData + { + Duration = 0, + HasMedia = false, + IsPackage = false, + Url = file.FullName, + Thumbnail = thumbnail + }; + break; + } + if (data != null) + { + data.LastWriteTime = file.LastAccessTime; + } + return data; + } + + /// + /// Deletes the given image file. + /// + public void DeleteImage(string imageFile) + { + if (File.Exists(imageFile)) + { + File.Delete(imageFile); + if (ImageDeleted != null) + { + ImageDeleted(this, new PackageArgs { PackageUrl = imageFile }); + } + } + } + + /// + /// Saves an image to the snipInsights. + /// + public string SaveImage(MemoryStream image) + { + // Create the mix file path. + string file; + do + { + file = Path.Combine(_snipInsightsDirectory, String.Format("capture{0}.{1}", DateTime.Now.ToString("yyyyMMddHHmmssfff"), "png")); + } while (File.Exists(file)); + + using (FileStream fs = new FileStream(file, FileMode.CreateNew)) + { + image.CopyTo(fs); + } + + SaveInCustomFolder(image); + + image.Position = 0; + MemoryStream cloned = new MemoryStream(); + image.CopyTo(cloned); + image.Position = 0; + cloned.Position = 0; + if (ImageSaved != null) + { + ImageSaved(this, new PackageArgs { PackageUrl = file, Thumbnail = cloned, Duration = 0, HasMedia = false }); + } + return file; + } + + /// + /// Save the screenshot in the user's location of choice + /// + /// The stream containing the screenshot + public void SaveInCustomFolder(MemoryStream image) + { + EnsureCustomFolderExists(); + + if (UserSettings.CustomDirectory != null && UserSettings.CustomDirectory != _snipInsightsDirectory) + { + string file; + do + { + file = Path.Combine(UserSettings.CustomDirectory, + String.Format("capture{0}.{1}", + DateTime.Now.ToString("yyyyMMddHHmmssfff"), "png")); + } while (File.Exists(file)); + + using (FileStream fs = new FileStream(file, FileMode.CreateNew)) + { + if (image != null) + { + // Reset the pointer position to the start of stream + image.Position = 0; + // Write the memory in a new file + image.CopyTo(fs); + } + } + } + } + + /// + /// Ensure the user's custom directory still exists when saving + /// In case of deletion pre-screenshot + /// + public void EnsureCustomFolderExists() + { + if (!Directory.Exists(UserSettings.CustomDirectory)) + { + try + { + Directory.CreateDirectory(UserSettings.CustomDirectory); + } + catch (Exception ex) + { + // If it couldn't be created, we redirect to the default value + UserSettings.CustomDirectory = _snipInsightsDirectory; + Diagnostics.LogException(ex); + } + } + } + + #region Helpers + /// + /// Get the snipInsights directory. + /// + /// + private string GetSnipInsightsDirectory() + { + string snipInsightsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), SnipInsightsFolder); + if (!Directory.Exists(snipInsightsDirectory)) + { + Directory.CreateDirectory(snipInsightsDirectory); + } + return snipInsightsDirectory; + } + #endregion + } + public class PackageArgs : EventArgs + { + public string PackageUrl { get; set; } + public MemoryStream Thumbnail { get; set; } + + public ulong Duration { get; set; } + + public bool HasMedia { get; set; } + } +} diff --git a/SnipInsight/Play_360x200.png b/SnipInsight/Play_360x200.png new file mode 100644 index 0000000..5eda53f Binary files /dev/null and b/SnipInsight/Play_360x200.png differ diff --git a/SnipInsight/Properties/AssemblyInfo.cs b/SnipInsight/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..fb0be7b --- /dev/null +++ b/SnipInsight/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Snip")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8990e84f-d1d4-4081-b0fa-a6cccda50f5a")] + +// All other assembly attributes are defined in AthenaVersion.cs + +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] +[assembly: AssemblyVersion("1.0.*")] + diff --git a/SnipInsight/Properties/Resources.Designer.cs b/SnipInsight/Properties/Resources.Designer.cs new file mode 100644 index 0000000..2a596a7 --- /dev/null +++ b/SnipInsight/Properties/Resources.Designer.cs @@ -0,0 +1,1407 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SnipInsight.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SnipInsight.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to API Key not configured in setting. + /// + public static string API_Key_Not_Found { + get { + return ResourceManager.GetString("API_Key_Not_Found", resourceCulture); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + public static System.Drawing.Icon AppIcon { + get { + object obj = ResourceManager.GetObject("AppIcon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized string similar to There was a problem with your microphone. Please try restarting your device.. + /// + public static string AudioVideo_ErrorReport { + get { + return ResourceManager.GetString("AudioVideo_ErrorReport", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The microphone is not recording. Please check your device, and try again.. + /// + public static string AudioVideo_NoAudioReceived { + get { + return ResourceManager.GetString("AudioVideo_NoAudioReceived", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nothing was recorded. Please try a longer recording.. + /// + public static string AudioVideo_NoSamplesProcessed { + get { + return ResourceManager.GetString("AudioVideo_NoSamplesProcessed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The microphone is not supported. Please try a different audio device.. + /// + public static string AudioVideo_NoSuitableAudioStream { + get { + return ResourceManager.GetString("AudioVideo_NoSuitableAudioStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to We are unable to record using the microphone. Please try a different audio device.. + /// + public static string AudioVideo_NoTranscodeAudioType { + get { + return ResourceManager.GetString("AudioVideo_NoTranscodeAudioType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No microphone. + /// + public static string AudioVideoSource_NoAudio { + get { + return ResourceManager.GetString("AudioVideoSource_NoAudio", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No camera. + /// + public static string AudioVideoSource_NoVideo { + get { + return ResourceManager.GetString("AudioVideoSource_NoVideo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically open editor on snip. + /// + public static string Auto_open_editor { + get { + return ResourceManager.GetString("Auto_open_editor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string Camera_Cancel { + get { + return ResourceManager.GetString("Camera_Cancel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select. + /// + public static string Camera_Select { + get { + return ResourceManager.GetString("Camera_Select", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select a camera. + /// + public static string Camera_SelectToolTip { + get { + return ResourceManager.GetString("Camera_SelectToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Take. + /// + public static string Camera_TakePicture { + get { + return ResourceManager.GetString("Camera_TakePicture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Take a photo. + /// + public static string Camera_TakeToolTip { + get { + return ResourceManager.GetString("Camera_TakeToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip camera. + /// + public static string CameraView_title { + get { + return ResourceManager.GetString("CameraView_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Capture. + /// + public static string Capture_Content { + get { + return ResourceManager.GetString("Capture_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Capture your screen. + /// + public static string Capture_ToolTip { + get { + return ResourceManager.GetString("Capture_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Celebrity Information. + /// + public static string Celebrity_Information { + get { + return ResourceManager.GetString("Celebrity_Information", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close. + /// + public static string Close_Content { + get { + return ResourceManager.GetString("Close_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close the Snip Insights application. + /// + public static string Close_ToolTip { + get { + return ResourceManager.GetString("Close_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do you want to save the changes?. + /// + public static string Commit_Changes { + get { + return ResourceManager.GetString("Commit_Changes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Are you sure you want to delete this snip?. + /// + public static string Confirm_Delete { + get { + return ResourceManager.GetString("Confirm_Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Are you sure you want to delete {0} snips?. + /// + public static string Confirm_Delete_List { + get { + return ResourceManager.GetString("Confirm_Delete_List", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ContentModerator. + /// + public static string ContentModerator { + get { + return ResourceManager.GetString("ContentModerator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy to clipboard after a snip. + /// + public static string Copy_to_clipboard { + get { + return ResourceManager.GetString("Copy_to_clipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create New Event. + /// + public static string Create_New_Event { + get { + return ResourceManager.GetString("Create_New_Event", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + public static string Delete_Content { + get { + return ResourceManager.GetString("Delete_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The deletion failed.. + /// + public static string Delete_Failed { + get { + return ResourceManager.GetString("Delete_Failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The deletion failed for {0} item(s). + /// + public static string Delete_Failed_List { + get { + return ResourceManager.GetString("Delete_Failed_List", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Entity Search. + /// + public static string Entity_Search { + get { + return ResourceManager.GetString("Entity_Search", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Erase All. + /// + public static string Erase_All { + get { + return ResourceManager.GetString("Erase_All", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Eraser. + /// + public static string Eraser { + get { + return ResourceManager.GetString("Eraser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exception at celebrities: . + /// + public static string Exception_at_celebrities { + get { + return ResourceManager.GetString("Exception_at_celebrities", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exception at landmark: . + /// + public static string Exception_at_landmark { + get { + return ResourceManager.GetString("Exception_at_landmark", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sorry! It looks like something has gone wrong. A software problem has occurred in Snip Insights . Not to worry, we've reported it to the team. You may continue to use the application, but some features may not work as expected.. + /// + public static string Exception_Dialog_Text { + get { + return ResourceManager.GetString("Exception_Dialog_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights error. + /// + public static string Exception_Dialog_Title { + get { + return ResourceManager.GetString("Exception_Dialog_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Insights. + /// + public static string FirstRun_Annotate_Heading { + get { + return ResourceManager.GetString("FirstRun_Annotate_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Turn on insights to see similar images, auto-naming and more.. + /// + public static string FirstRun_Annotate_Message { + get { + return ResourceManager.GetString("FirstRun_Annotate_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights. + /// + public static string FirstRun_Grab_Heading { + get { + return ResourceManager.GetString("FirstRun_Grab_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Click and drag to capture any window or area of your screen.. + /// + public static string FirstRun_Grab_Message { + get { + return ResourceManager.GetString("FirstRun_Grab_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights AI. + /// + public static string FirstRun_Intro_Heading { + get { + return ResourceManager.GetString("FirstRun_Intro_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the tool window or assign shorcuts to access features.. + /// + public static string FirstRun_Intro_Message { + get { + return ResourceManager.GetString("FirstRun_Intro_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show-and-tell. + /// + public static string FirstRun_Share_Heading { + get { + return ResourceManager.GetString("FirstRun_Share_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit, share and save your snips, all in the same window.. + /// + public static string FirstRun_Share_Message { + get { + return ResourceManager.GetString("FirstRun_Share_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Highlighter. + /// + public static string Highlighter { + get { + return ResourceManager.GetString("Highlighter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image Analysis. + /// + public static string Image_Analysis { + get { + return ResourceManager.GetString("Image_Analysis", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sorry, Image cannot be loaded. + /// + public static string Image_Not_Loaded { + get { + return ResourceManager.GetString("Image_Not_Loaded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image Search. + /// + public static string Image_Search { + get { + return ResourceManager.GetString("Image_Search", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image to Text. + /// + public static string Image_to_Text { + get { + return ResourceManager.GetString("Image_to_Text", resourceCulture); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] InkCursor { + get { + object obj = ResourceManager.GetObject("InkCursor", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized string similar to Could not find insights!. + /// + public static string Insight_Not_Found { + get { + return ResourceManager.GetString("Insight_Not_Found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keys have been updated. + ///Please restart the application to apply changes. . + /// + public static string Key_Restart { + get { + return ResourceManager.GetString("Key_Restart", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not a supported combination. + /// + public static string KeyComboPicker_NotSupportCombination { + get { + return ResourceManager.GetString("KeyComboPicker_NotSupportCombination", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated. + /// + public static string KeyComboPicker_Updated { + get { + return ResourceManager.GetString("KeyComboPicker_Updated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Landmark Information. + /// + public static string Landmark_Information { + get { + return ResourceManager.GetString("Landmark_Information", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to https://go.microsoft.com/fwlink/?LinkId=521839. + /// + public static string Link_Privacy { + get { + return ResourceManager.GetString("Link_Privacy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to https://www.microsoft.com/en-us/servicesagreement/. + /// + public static string Link_Terms { + get { + return ResourceManager.GetString("Link_Terms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LUIS App ID. + /// + public static string LUIS_App { + get { + return ResourceManager.GetString("LUIS_App", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 2891e896-28de-4a0e-818e-504de767d08f. + /// + public static string LUIS_App_id { + get { + return ResourceManager.GetString("LUIS_App_id", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LUIS Key. + /// + public static string LUIS_Key { + get { + return ResourceManager.GetString("LUIS_Key", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string Menu_Settings { + get { + return ResourceManager.GetString("Menu_Settings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Share. + /// + public static string Menu_Share { + get { + return ResourceManager.GetString("Menu_Share", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email. + /// + public static string Menu_Share_Email { + get { + return ResourceManager.GetString("Menu_Share_Email", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Embed. + /// + public static string Menu_Share_Embed { + get { + return ResourceManager.GetString("Menu_Share_Embed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Link. + /// + public static string Menu_Share_Link { + get { + return ResourceManager.GetString("Menu_Share_Link", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OneNote. + /// + public static string Menu_Share_OneNote { + get { + return ResourceManager.GetString("Menu_Share_OneNote", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copied to clipboard. + /// + public static string Message_CopiedToClipboard { + get { + return ResourceManager.GetString("Message_CopiedToClipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to copy to clipboard! Please try again later.. + /// + public static string Message_CopyToClipboardFailed { + get { + return ResourceManager.GetString("Message_CopyToClipboardFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Link copied to clipboard. + /// + public static string Message_LinkCopiedToClipboard { + get { + return ResourceManager.GetString("Message_LinkCopiedToClipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send to OneNote failed. + /// + public static string Message_SendToOneNote_Failed { + get { + return ResourceManager.GetString("Message_SendToOneNote_Failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip sent to OneNote. + /// + public static string Message_SendToOneNote_Succeeded { + get { + return ResourceManager.GetString("Message_SendToOneNote_Succeeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to More info on Bing. + /// + public static string More_information { + get { + return ResourceManager.GetString("More_information", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to My Snips. + /// + public static string My_Snips { + get { + return ResourceManager.GetString("My_Snips", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No Internet Browser Found. Please install a browser and try again.. + /// + public static string No_Browser { + get { + return ResourceManager.GetString("No_Browser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pause. + /// + public static string Pause_Content { + get { + return ResourceManager.GetString("Pause_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pause your recording. + /// + public static string Pause_ToolTip { + get { + return ResourceManager.GetString("Pause_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pen Toggle. + /// + public static string Pen_Toggle { + get { + return ResourceManager.GetString("Pen_Toggle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Photo. + /// + public static string Photo_Content { + get { + return ResourceManager.GetString("Photo_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Take a photo. + /// + public static string Photo_ToolTip { + get { + return ResourceManager.GetString("Photo_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please try again. + /// + public static string Please_try_again { + get { + return ResourceManager.GetString("Please_try_again", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Popular landmark. + /// + public static string Popular_landmark { + get { + return ResourceManager.GetString("Popular_landmark", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play. + /// + public static string PreviewStart_Content { + get { + return ResourceManager.GetString("PreviewStart_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Watch a preview of your recording. + /// + public static string PreviewStart_ToolTip { + get { + return ResourceManager.GetString("PreviewStart_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop. + /// + public static string PreviewStop_Content { + get { + return ResourceManager.GetString("PreviewStop_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uploading your snip so it'll be easy to share.... + /// + public static string Progress_Upload { + get { + return ResourceManager.GetString("Progress_Upload", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Processing video.... + /// + public static string Progress_Video { + get { + return ResourceManager.GetString("Progress_Video", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish failed!. + /// + public static string Publish_Failed { + get { + return ResourceManager.GetString("Publish_Failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish succeed!. + /// + public static string Publish_Succeed { + get { + return ResourceManager.GetString("Publish_Succeed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Click to View. + /// + public static string Publish_WatchLink { + get { + return ResourceManager.GetString("Publish_WatchLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Record. + /// + public static string Record_Content { + get { + return ResourceManager.GetString("Record_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Record audio and write on your picture. + /// + public static string Record_ToolTip { + get { + return ResourceManager.GetString("Record_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop. + /// + public static string RecordingStop_Text { + get { + return ResourceManager.GetString("RecordingStop_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop. + /// + public static string RecordingStop_ToolTip { + get { + return ResourceManager.GetString("RecordingStop_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Redo. + /// + public static string Redo { + get { + return ResourceManager.GetString("Redo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send Email. + /// + public static string Send_Email { + get { + return ResourceManager.GetString("Send_Email", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pick a section or page to insert the Snip:. + /// + public static string SendToOneNoteDialogDescription { + get { + return ResourceManager.GetString("SendToOneNoteDialogDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send to selected location. + /// + public static string SendToOneNoteDialogSendToButton { + get { + return ResourceManager.GetString("SendToOneNoteDialogSendToButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send Snip to OneNote. + /// + public static string SendToOneNoteDialogTitle { + get { + return ResourceManager.GetString("SendToOneNoteDialogTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OneNote has not been configured. Launch and configure OneNote first and then try again.. + /// + public static string SendToOneNoteFirstRunNotCompleteDialogMessage { + get { + return ResourceManager.GetString("SendToOneNoteFirstRunNotCompleteDialogMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OneNote not configured. + /// + public static string SendToOneNoteFirstRunNotCompleteDialogTitle { + get { + return ResourceManager.GetString("SendToOneNoteFirstRunNotCompleteDialogTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights is unable to open OneNote because {0} is running as Administrator and {1} is not. Close OneNote and try again.. + /// + public static string SendToOneNoteProcessElevationMismatchDiaglogMessage { + get { + return ResourceManager.GetString("SendToOneNoteProcessElevationMismatchDiaglogMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to open OneNote. + /// + public static string SendToOneNoteProcessElevationMismatchDiaglogTitle { + get { + return ResourceManager.GetString("SendToOneNoteProcessElevationMismatchDiaglogTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights needs to be restarted. Click OK to restart Snip Insights.. + /// + public static string SendToOneNoteRestartSnipDialogMessage { + get { + return ResourceManager.GetString("SendToOneNoteRestartSnipDialogMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restart Snip Insights. + /// + public static string SendToOneNoteRestartSnipDialogTitle { + get { + return ResourceManager.GetString("SendToOneNoteRestartSnipDialogTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to About this app. + /// + public static string Settings_AboutThisApp_Heading { + get { + return ResourceManager.GetString("Settings_AboutThisApp_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Capture preferences. + /// + public static string Settings_Capture_Heading { + get { + return ResourceManager.GetString("Settings_Capture_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cognitive Services. + /// + public static string Settings_Cognitive_Services_Heading { + get { + return ResourceManager.GetString("Settings_Cognitive_Services_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Share image moderator strength: {0} %. + /// + public static string Settings_ContentModerationStrength_FormatString { + get { + return ResourceManager.GetString("Settings_ContentModerationStrength_FormatString", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to © 2018 Microsoft Corporation. + /// + public static string Settings_Copyright_Text { + get { + return ResourceManager.GetString("Settings_Copyright_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delay screen capture: {0} second(s). + /// + public static string Settings_Delay_FormatString { + get { + return ResourceManager.GetString("Settings_Delay_FormatString", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable AI assistance. + /// + public static string Settings_EnableAI { + get { + return ResourceManager.GetString("Settings_EnableAI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to General. + /// + public static string Settings_General_Heading { + get { + return ResourceManager.GetString("Settings_General_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hotkeys - (Ctrl + Alt + AnyKey). + /// + public static string Settings_Hotkeys_Heading { + get { + return ResourceManager.GetString("Settings_Hotkeys_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Privacy Statement. + /// + public static string Settings_PrivacyStatement_Button { + get { + return ResourceManager.GetString("Settings_PrivacyStatement_Button", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run Snip Insights when Windows starts. + /// + public static string Settings_RunWhenWindowsStarts_Label { + get { + return ResourceManager.GetString("Settings_RunWhenWindowsStarts_Label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft Services Agreement. + /// + public static string Settings_ServiceAgreement_Button { + get { + return ResourceManager.GetString("Settings_ServiceAgreement_Button", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show toolbar on the desktop. + /// + public static string Settings_ShowToolbarOnDesktop { + get { + return ResourceManager.GetString("Settings_ShowToolbarOnDesktop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snipping. + /// + public static string Settings_Snipping_Heading { + get { + return ResourceManager.GetString("Settings_Snipping_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This image may contain inappropriate content, would you like to proceed anyway?. + /// + public static string ShareModerateWarning { + get { + return ResourceManager.GetString("ShareModerateWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sharing a snip with you. + /// + public static string Sharing_Snip { + get { + return ResourceManager.GetString("Sharing_Snip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sharing a snip of '{0}' with you. + /// + public static string Sharing_Snip_Name { + get { + return ResourceManager.GetString("Sharing_Snip_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the notification toast after a snip. + /// + public static string Show_toast { + get { + return ResourceManager.GetString("Show_toast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Similar Images. + /// + public static string Similar_Images { + get { + return ResourceManager.GetString("Similar_Images", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to From the web. + /// + public static string Similar_Images_Web { + get { + return ResourceManager.GetString("Similar_Images_Web", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Similar Products. + /// + public static string Similar_Products { + get { + return ResourceManager.GetString("Similar_Products", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Suggested Insights. + /// + public static string Suggested_Insights { + get { + return ResourceManager.GetString("Suggested_Insights", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Text Recognition. + /// + public static string Text_Recognition { + get { + return ResourceManager.GetString("Text_Recognition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Translator. + /// + public static string Translator { + get { + return ResourceManager.GetString("Translator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights is still running in the system tray, listening for hotkey commands to create new snips. Click here to stop showing this message.. + /// + public static string TrayIcon_BalloonTip_AppIsStillRunning_Text { + get { + return ResourceManager.GetString("TrayIcon_BalloonTip_AppIsStillRunning_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights is still running. + /// + public static string TrayIcon_BalloonTip_AppIsStillRunning_Title { + get { + return ResourceManager.GetString("TrayIcon_BalloonTip_AppIsStillRunning_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exit. + /// + public static string TrayIcon_ContextMenuItem_Exit { + get { + return ResourceManager.GetString("TrayIcon_ContextMenuItem_Exit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Library. + /// + public static string TrayIcon_ContextMenuItem_Library { + get { + return ResourceManager.GetString("TrayIcon_ContextMenuItem_Library", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New Capture. + /// + public static string TrayIcon_ContextMenuItem_NewCapture { + get { + return ResourceManager.GetString("TrayIcon_ContextMenuItem_NewCapture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New Photo. + /// + public static string TrayIcon_ContextMenuItem_NewPhoto { + get { + return ResourceManager.GetString("TrayIcon_ContextMenuItem_NewPhoto", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New Whiteboard. + /// + public static string TrayIcon_ContextMenuItem_NewWhiteboard { + get { + return ResourceManager.GetString("TrayIcon_ContextMenuItem_NewWhiteboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string TrayIcon_ContextMenuItem_Settings { + get { + return ResourceManager.GetString("TrayIcon_ContextMenuItem_Settings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights. + /// + public static string TrayIcon_ToolTip { + get { + return ResourceManager.GetString("TrayIcon_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Undo. + /// + public static string Undo { + get { + return ResourceManager.GetString("Undo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UpdateKeys. + /// + public static string UpdateKeys { + get { + return ResourceManager.GetString("UpdateKeys", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Who is this?. + /// + public static string Who_is_this { + get { + return ResourceManager.GetString("Who_is_this", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close. + /// + public static string WindowsChrome_Close { + get { + return ResourceManager.GetString("WindowsChrome_Close", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximize. + /// + public static string WindowsChrome_Maximize { + get { + return ResourceManager.GetString("WindowsChrome_Maximize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimize. + /// + public static string WindowsChrome_Minimize { + get { + return ResourceManager.GetString("WindowsChrome_Minimize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restore Down. + /// + public static string WindowsChrome_Restore { + get { + return ResourceManager.GetString("WindowsChrome_Restore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tutorial. + /// + public static string WindowTitle_FirstRun { + get { + return ResourceManager.GetString("WindowTitle_FirstRun", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights Editor. + /// + public static string WindowTitle_Main { + get { + return ResourceManager.GetString("WindowTitle_Main", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alert. + /// + public static string WindowTitle_Toast { + get { + return ResourceManager.GetString("WindowTitle_Toast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snip Insights Toolbar. + /// + public static string WindowTitle_Tool { + get { + return ResourceManager.GetString("WindowTitle_Tool", resourceCulture); + } + } + } +} diff --git a/SnipInsight/Properties/Resources.resx b/SnipInsight/Properties/Resources.resx new file mode 100644 index 0000000..5a8bd6e --- /dev/null +++ b/SnipInsight/Properties/Resources.resx @@ -0,0 +1,569 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No microphone + + + Close + + + Close the Snip Insights application + + + Delete + + + Sorry! It looks like something has gone wrong. A software problem has occurred in Snip Insights . Not to worry, we've reported it to the team. You may continue to use the application, but some features may not work as expected. + + + Snip Insights error + + + Pause + + + Pause your recording + + + Play + + + Watch a preview of your recording + + + Stop + + + Stop + + + Stop + + + Record + + + Record audio and write on your picture + + + Capture + + + Capture your screen + + + There was a problem with your microphone. Please try restarting your device. + + + The microphone is not recording. Please check your device, and try again. + + + Nothing was recorded. Please try a longer recording. + + + The microphone is not supported. Please try a different audio device. + + + We are unable to record using the microphone. Please try a different audio device. + + + No camera + + + Snip camera + + + Cancel + + + Select + + + Select a camera + + + Take + + + Take a photo + + + Photo + + + Take a photo + + + Publish failed! + + + Publish succeed! + + + Click to View + + + Share + + + Email + + + Embed + + + Link + + + Insights + + + Snip Insights + + + Show-and-tell + + + Snip Insights AI + + + Tutorial + + + Snip Insights Editor + + + Alert + + + Snip Insights Toolbar + + + Turn on insights to see similar images, auto-naming and more. + + + Use the tool window or assign shorcuts to access features. + + + Edit, share and save your snips, all in the same window. + + + Click and drag to capture any window or area of your screen. + + + Close + + + Maximize + + + Minimize + + + Restore Down + + + Uploading your snip so it'll be easy to share... + + + Processing video... + + + https://go.microsoft.com/fwlink/?LinkId=521839 + + + Settings + + + Copied to clipboard + + + Link copied to clipboard + + + https://www.microsoft.com/en-us/servicesagreement/ + + + Failed to copy to clipboard! Please try again later. + + + Not a supported combination + + + Updated + + + About this app + + + Capture preferences + + + © 2018 Microsoft Corporation + + + Hotkeys - (Ctrl + Alt + AnyKey) + + + Privacy Statement + + + General + + + Snipping + + + Cognitive Services + + + Run Snip Insights when Windows starts + + + Microsoft Services Agreement + + + New Capture + + + New Photo + + + New Whiteboard + + + Library + + + Settings + + + Exit + + + Snip Insights + + + Snip Insights is still running in the system tray, listening for hotkey commands to create new snips. Click here to stop showing this message. + + + Snip Insights is still running + + + Show toolbar on the desktop + + + Delay screen capture: {0} second(s) + + + OneNote + + + Pick a section or page to insert the Snip: + + + Send to selected location + + + Send Snip to OneNote + + + Snip Insights is unable to open OneNote because {0} is running as Administrator and {1} is not. Close OneNote and try again. + + + Unable to open OneNote + + + Send to OneNote failed + + + Snip sent to OneNote + + + OneNote has not been configured. Launch and configure OneNote first and then try again. + + + OneNote not configured + + + Snip Insights needs to be restarted. Click OK to restart Snip Insights. + + + Restart Snip Insights + + + Share image moderator strength: {0} % + + + This image may contain inappropriate content, would you like to proceed anyway? + + + Enable AI assistance + + + Do you want to save the changes? + + + 2891e896-28de-4a0e-818e-504de767d08f + + + API Key not configured in setting + + + Automatically open editor on snip + + + Celebrity Information + + + Are you sure you want to delete this snip? + + + Are you sure you want to delete {0} snips? + + + ContentModerator + + + Copy to clipboard after a snip + + + Create New Event + + + The deletion failed. + + + The deletion failed for {0} item(s) + + + Entity Search + + + Eraser + + + Erase All + + + Exception at celebrities: + + + Exception at landmark: + + + Highlighter + + + Image Analysis + + + Sorry, Image cannot be loaded + + + Image Search + + + Image to Text + + + Could not find insights! + + + Keys have been updated. +Please restart the application to apply changes. + + + Landmark Information + + + LUIS App ID + + + LUIS Key + + + More info on Bing + + + My Snips + + + No Internet Browser Found. Please install a browser and try again. + + + Pen Toggle + + + Please try again + + + Popular landmark + + + Redo + + + Send Email + + + Sharing a snip with you + + + Sharing a snip of '{0}' with you + + + Show the notification toast after a snip + + + Similar Images + + + From the web + + + Similar Products + + + Suggested Insights + + + Text Recognition + + + Translator + + + Undo + + + UpdateKeys + + + Who is this? + + + + ..\Resources\SnipInsights.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\InkCursor.cur;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/SnipInsight/Properties/Settings.Designer.cs b/SnipInsight/Properties/Settings.Designer.cs new file mode 100644 index 0000000..57b09d4 --- /dev/null +++ b/SnipInsight/Properties/Settings.Designer.cs @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SnipInsight.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.5.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://mix.office-int.com/api/log")] + public string DiagsReportUri { + get { + return ((string)(this["DiagsReportUri"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://mix.office-int.com/")] + public string MixApiEndpoint { + get { + return ((string)(this["MixApiEndpoint"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://mix.office-int.com/api/tools")] + public string AutoUpdateUri { + get { + return ((string)(this["AutoUpdateUri"])); + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string LastSaveVideoDirectory { + get { + return ((string)(this["LastSaveVideoDirectory"])); + } + set { + this["LastSaveVideoDirectory"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string LastSaveImageDirectory { + get { + return ((string)(this["LastSaveImageDirectory"])); + } + set { + this["LastSaveImageDirectory"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0, 0")] + public global::System.Drawing.Point ToolPosition { + get { + return ((global::System.Drawing.Point)(this["ToolPosition"])); + } + set { + this["ToolPosition"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string ToolDocking { + get { + return ((string)(this["ToolDocking"])); + } + set { + this["ToolDocking"] = value; + } + } + } +} diff --git a/SnipInsight/Properties/Settings.settings b/SnipInsight/Properties/Settings.settings new file mode 100644 index 0000000..598bb55 --- /dev/null +++ b/SnipInsight/Properties/Settings.settings @@ -0,0 +1,27 @@ + + + + + + https://mix.office-int.com/api/log + + + https://mix.office-int.com/ + + + https://mix.office-int.com/api/tools + + + + + + + + + 0, 0 + + + + + + \ No newline at end of file diff --git a/SnipInsight/ResourceDictionaries/AriadneStyles.xaml b/SnipInsight/ResourceDictionaries/AriadneStyles.xaml new file mode 100644 index 0000000..abfba86 --- /dev/null +++ b/SnipInsight/ResourceDictionaries/AriadneStyles.xaml @@ -0,0 +1,1763 @@ + + + + + + + + + #00FFFFFF + White + #D9D9E6 + #D9D9E6 + #3C3C3C + #6B69D6 + White + #00FFFFFF + White + #22FFFFFF + White + #343434 + White + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/ResourceDictionaries/ComboBoxStyles.xaml b/SnipInsight/ResourceDictionaries/ComboBoxStyles.xaml new file mode 100644 index 0000000..1c2bfd3 --- /dev/null +++ b/SnipInsight/ResourceDictionaries/ComboBoxStyles.xaml @@ -0,0 +1,384 @@ + + + + + + + + + + + Transparent + White + Transparent + #FFF0623E + Transparent + #FF58595B + #FFB83B1D + #FF212121 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/ResourceDictionaries/FirstRunImages.xaml b/SnipInsight/ResourceDictionaries/FirstRunImages.xaml new file mode 100644 index 0000000..3a0971e --- /dev/null +++ b/SnipInsight/ResourceDictionaries/FirstRunImages.xaml @@ -0,0 +1,1174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/ResourceDictionaries/Icons.xaml b/SnipInsight/ResourceDictionaries/Icons.xaml new file mode 100644 index 0000000..c11a37b --- /dev/null +++ b/SnipInsight/ResourceDictionaries/Icons.xaml @@ -0,0 +1,2222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/ResourceDictionaries/SnipStyles.xaml b/SnipInsight/ResourceDictionaries/SnipStyles.xaml new file mode 100644 index 0000000..e7e96c1 --- /dev/null +++ b/SnipInsight/ResourceDictionaries/SnipStyles.xaml @@ -0,0 +1,1148 @@ + + + + + + + + 34 + 35 + 20 + 130 + 14 + 500 + 150 + 350 + 450 + 27 + 20 + 12 + 20 + 18 + 30 + 12 + 16 + 24 + 32 + 45 + 60 + 60 + 32 + 56 + 55 + 140 + 55 + + + #6B69D6 + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/ResourceDictionaries/SnipTemplates.xaml b/SnipInsight/ResourceDictionaries/SnipTemplates.xaml new file mode 100644 index 0000000..d815a0c --- /dev/null +++ b/SnipInsight/ResourceDictionaries/SnipTemplates.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/Resources/ImageBlank.jpg b/SnipInsight/Resources/ImageBlank.jpg new file mode 100644 index 0000000..8730c11 Binary files /dev/null and b/SnipInsight/Resources/ImageBlank.jpg differ diff --git a/SnipInsight/Resources/InkCursor.cur b/SnipInsight/Resources/InkCursor.cur new file mode 100644 index 0000000..eb1df53 Binary files /dev/null and b/SnipInsight/Resources/InkCursor.cur differ diff --git a/SnipInsight/Resources/SnipInsights.ico b/SnipInsight/Resources/SnipInsights.ico new file mode 100644 index 0000000..7a4b2df Binary files /dev/null and b/SnipInsight/Resources/SnipInsights.ico differ diff --git a/SnipInsight/SendTo/OneNoteManager.cs b/SnipInsight/SendTo/OneNoteManager.cs new file mode 100644 index 0000000..b540b6b --- /dev/null +++ b/SnipInsight/SendTo/OneNoteManager.cs @@ -0,0 +1,509 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Windows; +using System.Xml; +using OneNote = Microsoft.Office.Interop.OneNote; +using SnipInsight.Util; +using System.Windows.Interop; +using System.Diagnostics; +using System.Threading; +using Microsoft.Win32; + +namespace SnipInsight.SendTo +{ + internal class OneNoteEntity + { + internal string Id; + internal string Name; + + internal OneNoteEntity(string id, string name) + { + Id = id; + Name = name; + } + } + + internal class OneNoteSection : OneNoteEntity + { + internal OneNoteSection(string id, string name) + : base(id, name) + { + } + } + + internal class OneNotePage : OneNoteEntity + { + internal OneNotePage(string id, string name) + : base(id, name) + { + } + } + + internal class OneNoteManager : OneNote.IQuickFilingDialogCallback, IDisposable + { + // Application interface (OneNote 2013) --> https://msdn.microsoft.com/en-us/library/office/jj680120.aspx + // Note + // We recommend specifying a version of OneNote (such as xs2013) instead of using xsCurrent or leaving it blank, because this will allow your add-in to work with future versions of OneNote. + + OneNote.Application _oneNoteApp; + const OneNote.XMLSchema _oneNoteXmlSchema = OneNote.XMLSchema.xs2013; + OneNoteXmlDoc _oneNoteXml; + string _imageFilePath; + string _publishUrl; + ManualResetEvent _insertCompleteEvent; + Exception _insertException; + bool _cancelled; + + ~OneNoteManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _oneNoteApp = null; // there is no Dispose or Close + + if (_insertCompleteEvent != null) + { + _insertCompleteEvent.Dispose(); + _insertCompleteEvent = null; + } + } + } + + private bool EnsureSnipOneNoteElevationParity() + { + Process oneNoteProcess = GetRunningOneNoteProcess(); + if (oneNoteProcess != null) + { + bool snipElevated = Utils.IsProcessRunningElevated(Process.GetCurrentProcess()); + bool oneNoteElevated = Utils.IsProcessRunningElevated(oneNoteProcess); + + if (snipElevated != oneNoteElevated) + { + string elevatedApp = snipElevated ? "Snip" : "OneNote"; + string unelevatedApp = snipElevated ? "OneNote" : "Snip"; + + string message = string.Format(Properties.Resources.SendToOneNoteProcessElevationMismatchDiaglogMessage, elevatedApp, unelevatedApp); + + MessageBox.Show(message, Properties.Resources.SendToOneNoteProcessElevationMismatchDiaglogTitle, MessageBoxButton.OK); + + return false; + } + } + + return true; + } + + private bool EnsureOneNoteFirstRunComplete() + { + bool firstRunComplete = false; + try + { + string[] officeMajorVersions = { "16", "15" }; // search order preference is newer Office versions first + const string firstBootRegkeyTemplate = @"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Office\{0}.0\OneNote"; + + foreach (string officeMajorVersion in officeMajorVersions) + { + object firstBootStatus = Registry.GetValue(string.Format(firstBootRegkeyTemplate, officeMajorVersion), "FirstBootStatus", null); + + if (firstBootStatus != null && (int)firstBootStatus >= 0x01000101) // regkey will not exist if OneNote has never been used. Regvalue is set to 0x01000101 (Office 15) or higher (i.e. 0x02000202 (Office 16)) once OneNote has been configured + { + firstRunComplete = true; + break; + } + } + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(new ApplicationException("Error querying registry for OneNote FirstRun", ex)); + } + + if (!firstRunComplete) + { + MessageBox.Show(Properties.Resources.SendToOneNoteFirstRunNotCompleteDialogMessage, Properties.Resources.SendToOneNoteFirstRunNotCompleteDialogTitle, MessageBoxButton.OK); + } + + return firstRunComplete; + } + + private void InitializeOneNoteApplication() + { + // create the OneNote Application with retry since starting the Application\OneNote.exe process can fail if a running OneNote process is in the midst of shutting down just as we try to initialize + uint attemptCount = 0; + uint attemptCountMax = 3; + + do + { + try + { + attemptCount++; + + _oneNoteApp = new OneNote.Application(); + + break; + } + catch (Exception) + { + if (attemptCount == attemptCountMax) + { + throw; + } + + System.Threading.Thread.Sleep(1000); // give a second to let OneNote exit completely + } + } while (attemptCount < attemptCountMax); + } + + // Initialize outside the constructor so that we can return bool 'success' to the caller of the class + private bool Initialize(string imageFilePath, string publishUrl) + { + if (EnsureSnipOneNoteElevationParity() && EnsureOneNoteFirstRunComplete()) + { + InitializeOneNoteApplication(); + _oneNoteXml = new OneNoteXmlDoc(_oneNoteXmlSchema); + + _imageFilePath = imageFilePath; + _publishUrl = publishUrl; + _insertCompleteEvent = new ManualResetEvent(false); + _insertException = null; + _cancelled = false; + + return true; + } + + return false; + } + + /// + /// Inserts the snip into OneNote + /// + /// + /// + /// bool on success or failure. Null if the user cancelled + internal bool? InsertSnip(string imageFilePath, string publishUrl = null) + { + // Quick Filing dialog box interfaces (OneNote 2013) --> https://msdn.microsoft.com/EN-US/library/office/jj680122.aspx + + try + { + if (!Initialize(imageFilePath, publishUrl)) + { + return false; + } + + OneNote.IQuickFilingDialog qfDialog = _oneNoteApp.QuickFiling(); + + qfDialog.Title = Properties.Resources.SendToOneNoteDialogTitle; + qfDialog.Description = Properties.Resources.SendToOneNoteDialogDescription; + qfDialog.TreeDepth = OneNote.HierarchyElement.hePages; + qfDialog.ParentWindowHandle = (ulong)new WindowInteropHelper(AppManager.TheBoss.MainWindow).Handle.ToInt64(); // sets the dialog modal to the editor window + + qfDialog.SetRecentResults(OneNote.RecentResultType.rrtFiling, true, true, false); + + // add a 'Send To' button, it will have index 0 since its the first button we added + qfDialog.AddButton(Properties.Resources.SendToOneNoteDialogSendToButton, OneNote.HierarchyElement.heSections | OneNote.HierarchyElement.hePages, OneNote.HierarchyElement.heNone, true); + + // watch for the OneNote process to exit since the QuickFilingDialog is modal to the MainWindow, which will cause Snip to hang if OneNote is killed while the QuickFilingDialog is being displayed + WatchForOneNoteProcessExit(); + + qfDialog.Run(this); // OnDialogClosed() callback is called just after the user makes a selction and finishes running the dialog + + // wait for the OnDialogClosed() callback thread to complete the insert + _insertCompleteEvent.WaitOne(); + + if (_cancelled) + { + return null; + } + + // check for an exception raised from the Insert code ran in the OnDialogClosed() callback thread + if (_insertException != null) + { + Diagnostics.LogException(_insertException); + return false; + } + + return true; + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + return false; + } + } + + private void WatchForOneNoteProcessExit() + { + new Thread(WatchForOneNoteProcessExitThreadProc) + { + IsBackground = true // allow the process to terminate if this thread is still running + } + .Start(); + } + + /// + /// Watches for the OneNote process to exit and prompts the user to restart Snip Insights if the OneNote process is killed while the QuickFilingDialog is being displayed. + /// This fcn is hang recovery code + /// + void WatchForOneNoteProcessExitThreadProc() + { + try + { + Process oneNoteProcess = GetRunningOneNoteProcess(); + + if (oneNoteProcess != null) + { + while (_insertCompleteEvent != null && !_insertCompleteEvent.WaitOne(0) && !oneNoteProcess.HasExited) // wait until the OnDialogClosed() callback finishes the insert and signals or the OneNote process exits + { + Thread.Sleep(500); + } + + if (oneNoteProcess.HasExited && _insertCompleteEvent != null && !_insertCompleteEvent.WaitOne(0)) + { + // OneNote QuickFilingDialog is modal to the MainWindow and if OneNote gets killed the Snip Insights MainWindow will be hung + // prompt the user to restart Snip Insights + MessageBox.Show(Properties.Resources.SendToOneNoteRestartSnipDialogMessage, Properties.Resources.SendToOneNoteRestartSnipDialogTitle, MessageBoxButton.OK); + + AppManager.TheBoss.RestartApp(true); // 'true' to kill the app since it will not gracefully close if the MainWindow is hung + } + } + } + catch + {} + } + + private Process GetRunningOneNoteProcess() + { + // get the OneNote process that is running in the same Windows session as the Snip Insights. OneNote is a single instance app per session. + return Process.GetProcessesByName("OneNote").Where(p => p.SessionId == Process.GetCurrentProcess().SessionId).FirstOrDefault(); + } + + public void OnDialogClosed(OneNote.IQuickFilingDialog qfDialog) + { + try + { + if (qfDialog.PressedButton == 0) // 0 is the index for the 'Send To' button we added to the dialog + { + InsertSnipIntoSelectedObject(qfDialog.SelectedItem); + } + else + { + _cancelled = true; + } + } + finally + { + _insertCompleteEvent.Set(); + } + } + + private void InsertSnipIntoSelectedObject(string oneNoteObjectId) + { + try + { + string hierarchyXml; + _oneNoteApp.GetHierarchy(oneNoteObjectId, OneNote.HierarchyScope.hsSelf, out hierarchyXml, _oneNoteXmlSchema); + + OneNoteEntity entity = _oneNoteXml.GetEntity(hierarchyXml); + + if (entity is OneNoteSection) + { + InsertImageIntoSection((OneNoteSection)entity, _imageFilePath, _publishUrl); + } + else if (entity is OneNotePage) + { + InsertImageIntoPage((OneNotePage)entity, _imageFilePath, _publishUrl); + } + } + catch (Exception ex) + { + _insertException = ex; // will be reported in InsertSnip() after this fcn returns + } + } + + internal void InsertImageIntoSection(OneNoteSection section, string imageFilePath, string publishUrl) + { + string newPageId; + _oneNoteApp.CreateNewPage(section.Id, out newPageId, OneNote.NewPageStyle.npsDefault); + + OneNotePage newPage = GetPageById(newPageId); + + InsertImageIntoPage(newPage, imageFilePath, publishUrl, true); + } + + internal void InsertImageIntoPage(OneNotePage page, string imageFilePath, string publishUrl, bool addTitle = false) + { + string xmlToInsert = _oneNoteXml.GetInsertSnipXml(page, GetBase64ImageString(imageFilePath), publishUrl, addTitle); + + _oneNoteApp.UpdatePageContent(xmlToInsert, System.DateTime.MinValue, _oneNoteXmlSchema); + + // navigate to inserted Snip + string pageContentXmlAfterInsert; + _oneNoteApp.GetPageContent(page.Id, out pageContentXmlAfterInsert, OneNote.PageInfo.piBasic, _oneNoteXmlSchema); + + string insertedSnipObjectId = _oneNoteXml.GetInsertedSnipObjectId(pageContentXmlAfterInsert); + + _oneNoteApp.NavigateTo(page.Id, insertedSnipObjectId); + } + + private string GetBase64ImageString(string imageFilePath) + { + using (Bitmap image = new Bitmap(imageFilePath)) + { + using (MemoryStream ms = new MemoryStream()) + { + image.Save(ms, ImageFormat.Png); + return Convert.ToBase64String(ms.ToArray()); + } + } + } + + internal OneNotePage GetPageById(string pageId) + { + string hierarchyXml; + _oneNoteApp.GetHierarchy(pageId, OneNote.HierarchyScope.hsSelf, out hierarchyXml, _oneNoteXmlSchema); + + return (OneNotePage)_oneNoteXml.GetEntity(hierarchyXml); + } + } + + internal class OneNoteXmlDoc + { + const OneNote.XMLSchema _oneNoteXmlSchema = OneNote.XMLSchema.xs2013; + XmlDocument _xmlDoc; + XmlNamespaceManager _namespaceMgr; + static readonly string NamespaceUri = "http://schemas.microsoft.com/office/onenote/2013/onenote"; + static readonly string NamespacePrefix = "one"; + static readonly string SectionNodeName = string.Format("{0}:Section", NamespacePrefix); + static readonly string PageNodeName = string.Format("{0}:Page", NamespacePrefix); + static readonly string OutlineNodeName = string.Format("{0}:Outline", NamespacePrefix); + static readonly string SectionNodeXPath = string.Format("//{0}", SectionNodeName); + static readonly string PageNodeXPath = string.Format("//{0}", PageNodeName); + static readonly string OutlineNodeXPath = string.Format("//{0}/{1}", PageNodeName, OutlineNodeName); + + internal OneNoteXmlDoc(OneNote.XMLSchema xmlSchema) + { + if (xmlSchema != _oneNoteXmlSchema) + { + throw new NotSupportedException(string.Format("The only supported xmlSchema is {0}", _oneNoteXmlSchema)); + } + + _xmlDoc = new XmlDocument(); + + _namespaceMgr = new XmlNamespaceManager(_xmlDoc.NameTable); + _namespaceMgr.AddNamespace(NamespacePrefix, NamespaceUri); + } + + internal OneNoteEntity GetEntity(string hierarchyXml) + { + _xmlDoc.LoadXml(hierarchyXml); + + XmlElement rootNode = _xmlDoc.DocumentElement; + + OneNoteEntity entity = GetEntity(rootNode); + + if (string.Compare(rootNode.Name, SectionNodeName, true) == 0) + { + return new OneNoteSection(entity.Id, entity.Name); + } + else if (string.Compare(rootNode.Name, PageNodeName, true) == 0) + { + return new OneNotePage(entity.Id, entity.Name); + } + else + { + throw new ApplicationException(string.Format("Invalid OneNote xml. Unable to create OneNoteEntity. XML: {0}", hierarchyXml)); + } + } + + private OneNoteEntity GetEntity(XmlNode node) + { + string id = node.Attributes["ID"].Value; + string name = node.Attributes["name"].Value; + + return new OneNoteEntity(id, name); + } + + internal string GetInsertSnipXml(OneNotePage page, string base64ImageString, string publishUrl, bool addTitle) + { + const string xmlFormat = + "" + + "" + // format {0} - page.ID + "{1}" + // format {1} - titleXml + "" + + "" + + "" + + "" + // format {2} - imageHyperlinkAttribure + "{3}" + // format {3} - base64ImageString + "" + + "" + + "{4}" + // format {4} - hyperlinkOutlineXml + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + // format {5} - caption + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + string caption = string.Format("{0} {1} - Snip", DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString()); + string titleXmlFormat = + "" + + "" + + "" + + "" + // format {0} - caption + "" + + "" + + ""; + string titleXml = addTitle ? string.Format(titleXmlFormat, caption) : ""; + string imageHyperlinkAttribute = !string.IsNullOrWhiteSpace(publishUrl) ? string.Format("hyperlink=\"{0}\"", publishUrl) : ""; + const string hyperlinkOutlineXmlFormat = + "" + + "" + + "" + + "" + + "" + + "" + // format {0} - publishUrl + "" + + ""; + string hyperlinkOutlineXml = !string.IsNullOrWhiteSpace(publishUrl) ? string.Format(hyperlinkOutlineXmlFormat, publishUrl) : ""; + + return string.Format(xmlFormat, page.Id, titleXml, imageHyperlinkAttribute, base64ImageString, hyperlinkOutlineXml, caption); + } + + internal string GetInsertedSnipObjectId(string pageContentXmlAfterInsert) + { + _xmlDoc.LoadXml(pageContentXmlAfterInsert); + + // return the objectID of the last Outline element + + XmlNodeList outlineElements = _xmlDoc.SelectNodes(OutlineNodeXPath, _namespaceMgr); + + return outlineElements[outlineElements.Count - 1].Attributes["objectID"].Value; + } + } +} diff --git a/SnipInsight/SnipInsight.csproj b/SnipInsight/SnipInsight.csproj new file mode 100644 index 0000000..c764fd0 --- /dev/null +++ b/SnipInsight/SnipInsight.csproj @@ -0,0 +1,465 @@ + + + + + Debug + AnyCPU + {2C90CA1B-BD65-41C9-B542-5F1F6E472863} + WinExe + SnipInsight + Snip + v4.6 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + + true + ..\bin\x64\Debug\ + TRACE;DEBUG;SNIP + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + ..\bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + + + + + + + + + + + + + + + + + + + + + 4.0 + + + + + + + + + + + + MSBuild:Compile + Designer + + + AISideNavigation.xaml + + + CelebrityRecognitionControl.xaml + + + EmptyState.xaml + + + ImageSearchControl.xaml + + + InsightsPermissions.xaml + + + LandmarkRecognitionControl.xaml + + + LoadingAnimation.xaml + + + NewsControls.xaml + + + OCRControl.xaml + + + ProductSearchControl.xaml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UserControl + + + TrayIconContextMenu.cs + + + + + + + + + + + + + + + + + + + + + + + + + AcetateLayer.xaml + + + ActionRibbon.xaml + + + CapturedImage.xaml + + + + + EditorSideNavigation.xaml + + + EditorWindowTourPanel.xaml + + + FirstRunWindow.xaml + + + + KeyComboPicker.xaml + + + + LibraryPanel.xaml + + + MainWindow.xaml + + + NotificationWindow.xaml + + + ProgressControl.xaml + + + SettingsPanel.xaml + + + ToastControl.xaml + + + ToolWindow.xaml + + + TopRibbon.xaml + + + TwoButtonDialog.xaml + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + + + + + + + + + ImageCaptureWindow.xaml + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + PublicResXFileCodeGenerator + Resources.Designer.cs + Designer + + + TrayIconContextMenu.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + + + + + taskkill /IM Snip.exe /FI "STATUS eq RUNNING" /F + + \ No newline at end of file diff --git a/SnipInsight/StateMachine/ActionNames.cs b/SnipInsight/StateMachine/ActionNames.cs new file mode 100644 index 0000000..9d4b5ff --- /dev/null +++ b/SnipInsight/StateMachine/ActionNames.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace SnipInsight.StateMachine +{ + public static class ActionNames + { + public const string CreateMainWindow = "CreateMainWindow"; + public const string ShowMainWindow = "ShowMainWindow"; + public const string HideMainWindow = "HideMainWindow"; + public const string CloseMainWindow = "CloseMainWindow"; // Hide + EditingWindowsClosedTrigger + public const string ShowToolWindow = "ShowToolWindow"; + public const string ShowToolWindowShy = "ShowToolWindowShy"; + public const string HideToolWindow = "HideToolWindow"; + public const string ShowSharePanel = "ShowSharePanel"; + public const string HideSharePanel = "HideSharePanel"; + public const string CloseFirstRunWindow = "CloseFirstRunWindow"; + public const string InitializeCaptureImage = "InitializeCaptureImage"; + public const string ShowLibraryPanel = "ShowLibraryPanel"; + public const string HideLibraryPanel = "HideLibraryPanel"; + public const string ShowSettingsPanel = "ShowSettingsPanel"; + public const string HideSettingsPanel = "HideSettingsPanel"; + public const string StartCaptureScreen = "StartCaptureScreen"; + public const string StartCaptureCamera = "StartCaptureCamera"; + public const string StartQuickCapture = "StartQuickCapture"; // To save a picture without opening the editor + public const string StartWhiteboard = "StartWhiteboard"; + public const string CloseImageCapture = "CloseImageCapture"; + public const string StartWhiteboardForCurrentWindow = "StartWhiteboardForCurrentWindow"; + public const string PrepareRecording = "PrepareRecording"; + public const string Record = "Record"; + public const string Pause = "Pause"; + public const string Stop = "Stop"; + public const string StartPlay = "StartPlay"; + public const string StopPlay = "StopPlay"; + public const string Exit = "Exit"; + public const string SaveImage = "SaveImage"; + public const string SaveImageWithDialog = "SaveImageWithDialog"; + public const string SaveVideoWithDialog = "SaveVideoWithDialog"; + public const string ShareLinkWithPublish= "ShareLinkWithPublish"; + public const string ShareEmbedWithPublish = "ShareEmbedWithPublish"; + public const string ShareEmailWithPublish = "ShareEmailWithPublish"; + public const string ShareSendToOneNoteWithPublish = "ShareSendToOneNoteWithPublish"; + public const string ShareEmailWithImage = "ShareEmailWithImage"; + public const string ShareSendToOneNoteWithImage = "ShareSendToOneNoteWithImage"; + public const string CopyWithImage = "CopyWithImage"; + public const string CopyWithPublish = "CopyWithPublish"; + public const string ClearOldImageData = "ClearOldImageData"; + public const string Redo = "Redo"; + public const string Delete = "Delete"; + public const string DeleteLibraryItems = "DeleteLibraryItems"; + public const string CleanFiles = "CleanFiles"; + public const string OpenMediaCapture = "OpenMediaCapture"; + public const string CloseMediaCapture = "CloseMediaCapture"; + public const string ShowImageCapturedToastMessage = "ShowImageCapturedToastMessage"; + public const string ShowMicrophoneOptions = "ShowMicrophoneOptions"; + public const string LoadImageFromLibary = "LoadImageFromLibary"; + public const string LoadPackageFromLibary = "LoadPackageFromLibary"; + public const string SelectLatestLibItem = "SelectLatestLibItem"; + public const string RestoreImage = "RestoreImage"; // To set trigger to go back to Editing state after screen capture cancel. + public const string RestorePackage = "RestorePackage"; // To set trigger to go back to EditingCompleted state after screen capture cancel. + public const string RestoreLibrary = "RestoreLibrary"; // To set trigger to go back to Library state after screen capture cancel. + public const string RestoreSettings = "RestoreSettings"; // To set trigger to go back to Settings state after screen capture cancel. + public const string RestoreMainWindow = "RestoreMainWindow"; + public const string SaveMainWindowState = "SaveMainWindowState"; + public const string ShowEditorWindowTour = "ShowEditorWindowTour"; + public const string StopEditorWindowTour = "StopEditorWindowTour"; + public const string DoImageInsights = "DoImageInsights"; + public const string ShowImageResultsWindow = "ShowImageResultsWindow"; + public const string CreateImageResultsWindow = "CreateImageResultsWindow"; + public const string DoCompVisionAnalysis = "DoCompVisionAnalysis"; + public const string ShowAIPanel = "ShowAIPanel"; + public const string HideAIPanel = "HideAIPanel"; + public const string OpenAIPanel = "OpenAIPanel"; + public const string RunAllInsights = "RunAllInsights"; + } +} diff --git a/SnipInsight/StateMachine/RelayCommand.cs b/SnipInsight/StateMachine/RelayCommand.cs new file mode 100644 index 0000000..6b22fb6 --- /dev/null +++ b/SnipInsight/StateMachine/RelayCommand.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Diagnostics; +using System.Windows.Input; + +namespace SnipInsight.StateMachine +{ + /// + /// A command whose sole purpose is to relay its functionality to other objects by invoking delegates. The default return value for the CanExecute method is 'true'. + /// + public class RelayCommand : ICommand + { + + #region Declarations + + readonly Predicate _canExecute; + readonly Action _execute; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class and the command can always be executed. + /// + /// The execution logic. + public RelayCommand(Action execute) + : this(execute, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The execution logic. + /// The execution status logic. + public RelayCommand(Action execute, Predicate canExecute) + { + + if (execute == null) + throw new ArgumentNullException("execute"); + _execute = execute; + _canExecute = canExecute; + } + + #endregion + + #region ICommand Members + + public event EventHandler CanExecuteChanged + { + add + { + + if (_canExecute != null) + CommandManager.RequerySuggested += value; + } + remove + { + + if (_canExecute != null) + CommandManager.RequerySuggested -= value; + } + } + + [DebuggerStepThrough] + public Boolean CanExecute(Object parameter) + { + return _canExecute == null || _canExecute((T)parameter); + } + + public void Execute(Object parameter) + { + _execute((T)parameter); + } + + #endregion + } + + /// + /// A command whose sole purpose is to relay its functionality to other objects by invoking delegates. The default return value for the CanExecute method is 'true'. + /// + public class RelayCommand : ICommand + { + + #region Declarations + + readonly Func _canExecute; + readonly Action _execute; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class and the command can always be executed. + /// + /// The execution logic. + public RelayCommand(Action execute) + : this(execute, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The execution logic. + /// The execution status logic. + public RelayCommand(Action execute, Func canExecute) + { + + if (execute == null) + throw new ArgumentNullException("execute"); + _execute = execute; + _canExecute = canExecute; + } + + #endregion + + #region ICommand Members + + public event EventHandler CanExecuteChanged + { + add + { + + if (_canExecute != null) + CommandManager.RequerySuggested += value; + } + remove + { + + if (_canExecute != null) + CommandManager.RequerySuggested -= value; + } + } + + [DebuggerStepThrough] + public Boolean CanExecute(Object parameter) + { + return _canExecute == null ? true : _canExecute(); + } + + public void Execute(Object parameter) + { + _execute(); + } + + #endregion + } +} diff --git a/SnipInsight/StateMachine/SnipInsightState.cs b/SnipInsight/StateMachine/SnipInsightState.cs new file mode 100644 index 0000000..9a8a785 --- /dev/null +++ b/SnipInsight/StateMachine/SnipInsightState.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace SnipInsight.StateMachine +{ + public enum SnipInsightState + { + Ready, + CapturingScreen, // User is capturing the screen + CapturingCamera, // User is capturing the camera + QuickCapture, // Capture but the picture is auto-saved and no editing option is presented to the user + Editing, // User captured something and is ready for inking or recording. Editor is shown at this time. + Recording, // The recording is happening. + Paused, // Recording is still in progress with pause + EditingCompleted, // A recording has been completed with Stop. + LibraryPanelOpened, + SettingsPanelOpened, + Playing, + SavingImage, + SavingVideo, + Deleting, + Sharing, + Copying, + ImageInsights, + AIPanelOpened, + Exiting + } +} diff --git a/SnipInsight/StateMachine/SnipInsightTrigger.cs b/SnipInsight/StateMachine/SnipInsightTrigger.cs new file mode 100644 index 0000000..b161e04 --- /dev/null +++ b/SnipInsight/StateMachine/SnipInsightTrigger.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; + +namespace SnipInsight.StateMachine +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + public enum SnipInsightTrigger + { + CaptureScreen, + QuickSnip, + CaptureCamera, + Whiteboard, + Editor, + + WhiteboardForCurrentWindow, // whiteboard to fit current window size rather than full screen size. + + Record, + Pause, + Stop, + TogglePlayStop, + Redo, + + EditingWindowClosed, + ToolWindowClosed, + Exit, + + ShareLink, + ShareEmbed, + ShareEmail, + ShareSendToOneNote, + + Copy, + Save, + Delete, + + ImageCaptured, + ImageCaptureCancelled, + + SavingImageWithDialogCompleted, + SavingVideoWithDialogCompleted, + + DeletionFailed, + DeletionCancelled, + + SharingWithPublishCompleted, + SharingWithImageCompleted, + + CopyingWithImageCompleted, + CopyingWithPublishCompleted, + + ShowMainWindow, + HideMainWindow, + + ShowLibraryPanel, + LoadImageFromLibrary, + LoadPackageFromLibrary, + + ShowSettingsPanel, + + ShowMicrophoneOptions, + + RestoreImage, + RestorePackage, + RestoreLibrary, + RestoreSettings, + RestoreWhiteboard, + + DoImageInsights, + ShowImageResultsWindow, + + ShowAIPanel, + HideAIPanel + } +} diff --git a/SnipInsight/StateMachine/StateMachine.cs b/SnipInsight/StateMachine/StateMachine.cs new file mode 100644 index 0000000..975ddb4 --- /dev/null +++ b/SnipInsight/StateMachine/StateMachine.cs @@ -0,0 +1,476 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Input; +using SnipInsight.Util; +using Stateless; + +namespace SnipInsight.StateMachine +{ + public class StateMachine : StateMachine, INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + private readonly Dictionary _actions; + + private SnipInsightState _preCaptureState = SnipInsightState.Ready; // State that was there before entering capture state. We are interested in Editing, EditingCompleted, and LibraryPanelOpened states only. + + public StateMachine(Dictionary actions) + : base(SnipInsightState.Ready) + { + _actions = actions; +#if DEBUG + _actions = new Dictionary(); + foreach (var entry in actions) + { + _actions.Add(entry.Key, WrapAction(entry.Key, entry.Value)); + } +#endif + + ConfigureUnhandledTriggers(); + + ConfigureReadyState(); + ConfigureQuickCapture(); + ConfigureCapturingScreenState(); + ConfigureEditingState(); + ConfigureEditingCompletedState(); + ConfigureLibraryPanelOpenedState(); + ConfigureSettingsPanelOpenedState(); + ConfigureSavingImageState(); + ConfigureDeletingState(); + ConfigureSharingState(); + ConfigureCopyingState(); + ConfigureExitingState(); + + OnTransitioned + ( + t => + { + Telemetry.ApplicationLogger.Instance.SubmitStateTransitionEvent(Telemetry.EventName.StateTransition, t.Source, t.Trigger, t.Destination); + OnPropertyChanged("State"); + CommandManager.InvalidateRequerySuggested(); + } + ); + + //used to debug commands and UI components +#if DEBUG + OnTransitioned + ( + // ReSharper disable once LocalizableElement + (t) => Console.WriteLine("StateMachine: {0} -> Trig({2}) -> {1}", t.Source, t.Destination, t.Trigger) + ); +#endif + } + + public Action WrapAction(string actionKey, Action action) + { + return () => + { + Console.WriteLine("ExecutingAction: " + actionKey); + action(); + }; + } + + private void ConfigureUnhandledTriggers() + { + OnUnhandledTrigger((state, trigger) => + { +#if DEBUG + // ReSharper disable once LocalizableElement + Console.WriteLine("StateMachine: Unhandled Trigger Encountered. s:{0} t:{1}]", state, trigger); +#endif + }); + } + + private StateConfiguration ConfigureReadyState() + { + return Configure(SnipInsightState.Ready) + .OnEntry(tr => + { + // Clean and Clear old data if any. + _actions[ActionNames.CleanFiles](); + _actions[ActionNames.ClearOldImageData](); + _actions[ActionNames.ShowToolWindowShy](); + if (tr.Trigger == SnipInsightTrigger.ImageCaptureCancelled && _preCaptureState == SnipInsightState.Editing) + { + _actions[ActionNames.RestoreImage](); + } + else if (tr.Trigger == SnipInsightTrigger.ImageCaptureCancelled && _preCaptureState == SnipInsightState.LibraryPanelOpened) + { + _actions[ActionNames.RestoreLibrary](); + } + else if (tr.Trigger == SnipInsightTrigger.ImageCaptureCancelled && _preCaptureState == SnipInsightState.SettingsPanelOpened) + { + _actions[ActionNames.RestoreSettings](); + } + else + { + _actions[ActionNames.HideMainWindow](); + } + }) + .OnExit(_actions[ActionNames.CloseFirstRunWindow]) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.RestoreImage, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.RestoreLibrary, SnipInsightState.LibraryPanelOpened) + .Permit(SnipInsightTrigger.RestoreSettings, SnipInsightState.SettingsPanelOpened) + .Permit(SnipInsightTrigger.CaptureScreen, SnipInsightState.CapturingScreen) + .Permit(SnipInsightTrigger.ShowLibraryPanel, SnipInsightState.LibraryPanelOpened) + .Permit(SnipInsightTrigger.ShowSettingsPanel, SnipInsightState.SettingsPanelOpened) + .Permit(SnipInsightTrigger.LoadImageFromLibrary, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.QuickSnip, SnipInsightState.QuickCapture); + } + + private StateConfiguration ConfigureCapturingScreenState() + { + return Configure(SnipInsightState.CapturingScreen) + .OnEntry(tr => + { + _preCaptureState = tr.Source; + _actions[ActionNames.SaveMainWindowState](); + _actions[ActionNames.CreateMainWindow](); // Image stored needs to adjust other dependent sizes. So, create it. + _actions[ActionNames.InitializeCaptureImage](); + _actions[ActionNames.HideMainWindow](); + _actions[ActionNames.HideToolWindow](); + _actions[ActionNames.StartCaptureScreen](); + }) + .OnExit(_actions[ActionNames.CloseImageCapture]) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.ImageCaptureCancelled, SnipInsightState.Ready) + .Permit(SnipInsightTrigger.ImageCaptured, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.QuickSnip, SnipInsightState.Editing); + } + + /// + /// Add the initial configuration and permissions for the QuickSnip + /// Describe the next possible steps and setup the environnement + /// + /// + /// The configuration for the current state + /// + private StateConfiguration ConfigureQuickCapture() + { + return Configure(SnipInsightState.QuickCapture) + .OnEntry(tr => + { + _preCaptureState = tr.Source; + _actions[ActionNames.SaveMainWindowState](); + _actions[ActionNames.CreateMainWindow](); // Image stored needs to adjust other dependent sizes. So, create it. + _actions[ActionNames.StartQuickCapture](); + _actions[ActionNames.HideMainWindow](); + _actions[ActionNames.HideToolWindow](); + _actions[ActionNames.StartCaptureScreen](); + }) + .OnExit(_actions[ActionNames.CloseImageCapture]) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.ImageCaptureCancelled, SnipInsightState.Ready) + .Permit(SnipInsightTrigger.QuickSnip, SnipInsightState.Editing); + } + + private StateConfiguration ConfigureEditingState() + { + return Configure(SnipInsightState.Editing) + .OnEntry(tr => + { + if (tr.Trigger == SnipInsightTrigger.QuickSnip) + { + _actions[ActionNames.SaveImage](); // User might have inked. So, save it again. + _actions[ActionNames.CleanFiles](); // Clean so that orig image is cleanedup. + Fire(SnipInsightTrigger.EditingWindowClosed); + return; + } + + _actions[ActionNames.CreateMainWindow](); + _actions[ActionNames.ShowToolWindowShy](); + switch (tr.Trigger) + { + case SnipInsightTrigger.Redo: + _actions[ActionNames.Redo](); + break; + case SnipInsightTrigger.LoadImageFromLibrary: + _actions[ActionNames.CleanFiles](); + _actions[ActionNames.ClearOldImageData](); // First clear old image if any. + _actions[ActionNames.LoadImageFromLibary](); + break; + } + _actions[ActionNames.HideLibraryPanel](); // When we come to this state, it is always to show content and not the library. + if (tr.Trigger == SnipInsightTrigger.RestoreImage) + { + _actions[ActionNames.RestoreMainWindow](); + } + else + { + _actions[ActionNames.ShowMainWindow](); + } + if (tr.Trigger != SnipInsightTrigger.LoadImageFromLibrary) // If we load from lib, no need to save. Avoids loop. + { + _actions[ActionNames.SaveImage](); + } + + // Feature Out: Uncomment if you want to use the Editor Tour feature + //_actions[ActionNames.ShowEditorWindowTour](); + + if (tr.Trigger == SnipInsightTrigger.ImageCaptured && UserSettings.CopyToClipboardAfterSnip) + { + _actions[ActionNames.ShowImageCapturedToastMessage](); + } + + if (tr.Trigger == SnipInsightTrigger.ImageCaptured) + { + _actions[ActionNames.RunAllInsights](); + } + }) + .OnExit(tr => + { + _actions[ActionNames.SaveImage](); // User might have inked. So, save it again. + _actions[ActionNames.CleanFiles](); // Clean so that orig image is cleanedup. + }) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.CaptureScreen, SnipInsightState.CapturingScreen) + .Permit(SnipInsightTrigger.Save, SnipInsightState.SavingImage) + .Permit(SnipInsightTrigger.Copy, SnipInsightState.Copying) + .Permit(SnipInsightTrigger.ShareEmail, SnipInsightState.Sharing) + .Permit(SnipInsightTrigger.ShareSendToOneNote, SnipInsightState.Sharing) + .Permit(SnipInsightTrigger.EditingWindowClosed, SnipInsightState.Ready) + .Permit(SnipInsightTrigger.ShowLibraryPanel, SnipInsightState.LibraryPanelOpened) + .Permit(SnipInsightTrigger.ShowSettingsPanel, SnipInsightState.SettingsPanelOpened); + } + + private StateConfiguration ConfigureLibraryPanelOpenedState() + { + return Configure(SnipInsightState.LibraryPanelOpened) + .OnEntry(tr => + { + _actions[ActionNames.ShowToolWindowShy](); + if (tr.Trigger == SnipInsightTrigger.RestoreLibrary) + { + _actions[ActionNames.RestoreMainWindow](); + } + else + { + _actions[ActionNames.ShowMainWindow](); + } + _actions[ActionNames.ShowLibraryPanel](); + + }) + .OnExit(tr => + { + _actions[ActionNames.HideLibraryPanel](); + }) + .Permit(SnipInsightTrigger.CaptureScreen, SnipInsightState.CapturingScreen) + .Permit(SnipInsightTrigger.LoadImageFromLibrary, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.LoadPackageFromLibrary, SnipInsightState.EditingCompleted) + .PermitReentry(SnipInsightTrigger.ShowLibraryPanel) + .Permit(SnipInsightTrigger.ShowSettingsPanel, SnipInsightState.SettingsPanelOpened) + .Permit(SnipInsightTrigger.EditingWindowClosed, SnipInsightState.Ready) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting); + } + + private StateConfiguration ConfigureSettingsPanelOpenedState() + { + return Configure(SnipInsightState.SettingsPanelOpened) + .OnEntry(tr => + { + _actions[ActionNames.ShowToolWindowShy](); + if (tr.Trigger == SnipInsightTrigger.RestoreSettings) + { + _actions[ActionNames.RestoreMainWindow](); + } + else + { + _actions[ActionNames.ShowMainWindow](); + } + _actions[ActionNames.ShowSettingsPanel](); + + }) + .OnExit(tr => + { + _actions[ActionNames.HideSettingsPanel](); + }) + .Permit(SnipInsightTrigger.CaptureScreen, SnipInsightState.CapturingScreen) + .Permit(SnipInsightTrigger.LoadImageFromLibrary, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.LoadPackageFromLibrary, SnipInsightState.EditingCompleted) + .Permit(SnipInsightTrigger.ShowLibraryPanel, SnipInsightState.LibraryPanelOpened) + .PermitReentry(SnipInsightTrigger.ShowSettingsPanel) + .Permit(SnipInsightTrigger.EditingWindowClosed, SnipInsightState.Ready) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting); + } + + private StateConfiguration ConfigureEditingCompletedState() + { + return Configure(SnipInsightState.EditingCompleted) + .OnEntry(tr => + { + if (tr.Trigger != SnipInsightTrigger.DeletionFailed) // If not failed, we keep the old image/recording as it is. Otherwise, clean old image. + { + _actions[ActionNames.CleanFiles](); + } + + if (tr.Trigger == SnipInsightTrigger.RestorePackage) + { + _actions[ActionNames.RestoreMainWindow](); + } + _actions[ActionNames.ShowSharePanel](); + }) + .OnExit(tr => + { + if (tr.Destination != SnipInsightState.Sharing) // To avoid glitch. We will come back to this state after sharing is done. + { + _actions[ActionNames.HideSharePanel](); + } + }) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.CaptureScreen, SnipInsightState.CapturingScreen) + .Permit(SnipInsightTrigger.Save, SnipInsightState.SavingVideo) + .Permit(SnipInsightTrigger.Copy, SnipInsightState.Copying) + .Permit(SnipInsightTrigger.Redo, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.Delete, SnipInsightState.Deleting) + .Permit(SnipInsightTrigger.EditingWindowClosed, SnipInsightState.Ready) + .Permit(SnipInsightTrigger.ShowLibraryPanel, SnipInsightState.LibraryPanelOpened) + .Permit(SnipInsightTrigger.ShowSettingsPanel, SnipInsightState.SettingsPanelOpened); + } + + private StateConfiguration ConfigureSavingImageState() + { + return Configure(SnipInsightState.SavingImage) + .OnEntry(tr => + { + if (tr.IsReentry) return; + switch (tr.Source) + { + case SnipInsightState.Editing: + _actions[ActionNames.SaveImageWithDialog](); + break; + } + }) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.SavingImageWithDialogCompleted, SnipInsightState.Editing) + .PermitReentry(SnipInsightTrigger.EditingWindowClosed); // Save should not allow close of main window. + } + + private StateConfiguration ConfigureDeletingState() + { + return Configure(SnipInsightState.Deleting) + .OnEntry(tr => + { + if (tr.IsReentry) return; + _actions[ActionNames.CleanFiles](); // Clear before performing delete operation to ensure that images are deleted if it has package. + _actions[ActionNames.Delete](); + }) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.LoadImageFromLibrary, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.DeletionFailed, SnipInsightState.EditingCompleted) + .Permit(SnipInsightTrigger.DeletionCancelled, SnipInsightState.EditingCompleted) + .PermitReentry(SnipInsightTrigger.EditingWindowClosed); // Delete should not allow close of main window. + } + + private StateConfiguration ConfigureCopyingState() + { + return Configure(SnipInsightState.Copying) + .OnEntry(tr => + { + if (tr.IsReentry) return; + _actions[ActionNames.CopyWithImage](); + }) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.CopyingWithImageCompleted, SnipInsightState.Editing) + .PermitReentry(SnipInsightTrigger.EditingWindowClosed); // Copy should finish before closing main window. + } + + private StateConfiguration ConfigureSharingState() + { + return Configure(SnipInsightState.Sharing) + .OnEntry(tr => + { + if (tr.IsReentry) return; + ProcessShareAction(tr); + }) + .Permit(SnipInsightTrigger.Exit, SnipInsightState.Exiting) + .Permit(SnipInsightTrigger.SharingWithImageCompleted, SnipInsightState.Editing) + .Permit(SnipInsightTrigger.SharingWithPublishCompleted, SnipInsightState.EditingCompleted) + .PermitReentry(SnipInsightTrigger.EditingWindowClosed); // Share should finish before letting main window to close. + } + + private void ProcessShareAction(Transition tr) + { + switch (tr.Trigger) + { + case SnipInsightTrigger.ShareLink: + { + switch (tr.Source) + { + case SnipInsightState.EditingCompleted: + { + _actions[ActionNames.ShareLinkWithPublish](); + break; + } + } + break; + } + case SnipInsightTrigger.ShareEmbed: + { + switch (tr.Source) + { + case SnipInsightState.EditingCompleted: + { + _actions[ActionNames.ShareEmbedWithPublish](); + break; + } + } + break; + } + case SnipInsightTrigger.ShareEmail: + { + switch (tr.Source) + { + case SnipInsightState.Editing: + { + _actions[ActionNames.ShareEmailWithImage](); + break; + } + case SnipInsightState.EditingCompleted: + { + _actions[ActionNames.ShareEmailWithPublish](); + break; + } + } + break; + } + case SnipInsightTrigger.ShareSendToOneNote: + { + switch (tr.Source) + { + case SnipInsightState.Editing: + { + _actions[ActionNames.ShareSendToOneNoteWithImage](); + break; + } + case SnipInsightState.EditingCompleted: + { + _actions[ActionNames.ShareSendToOneNoteWithPublish](); + break; + } + } + break; + } + } + } + + private StateConfiguration ConfigureExitingState() + { + return Configure(SnipInsightState.Exiting) + .OnEntry(tr => + { + _actions[ActionNames.CleanFiles](); + _actions[ActionNames.ClearOldImageData](); + _actions[ActionNames.Exit](); + }); + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/SnipInsight/StateMachine/StateMachineEnableConverter.cs b/SnipInsight/StateMachine/StateMachineEnableConverter.cs new file mode 100644 index 0000000..f9b0254 --- /dev/null +++ b/SnipInsight/StateMachine/StateMachineEnableConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Linq; +using System.Windows.Data; + +namespace SnipInsight.StateMachine +{ + public class StateMachineEnableConverter : IValueConverter + { + /// + /// Converts state machine's state property to boolean for a UI component. + /// + /// The current state + /// + /// SnipInsightState enum value or a comma seperated string of state enum values. e..g SnipInsightState.Recording or 'Recording, Recorded' + /// Ignored. + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var state = value != null ? value.ToString() : string.Empty; + var targetStates = parameter.ToString().Split(','); + + return targetStates.Contains(state); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/SnipInsight/StateMachine/StateMachineExtensions.cs b/SnipInsight/StateMachine/StateMachineExtensions.cs new file mode 100644 index 0000000..8ddb417 --- /dev/null +++ b/SnipInsight/StateMachine/StateMachineExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Input; +using Stateless; + +namespace SnipInsight.StateMachine +{ + public static class StateMachineExtensions + { + public static ICommand CreateCommand(this StateMachine stateMachine, + TTrigger trigger) + { + return new RelayCommand + ( + () => stateMachine.Fire(trigger), + () => stateMachine.CanFire(trigger) + ); + } + } +} diff --git a/SnipInsight/StateMachine/StateMachineVisibilityConverter.cs b/SnipInsight/StateMachine/StateMachineVisibilityConverter.cs new file mode 100644 index 0000000..8fe8bcc --- /dev/null +++ b/SnipInsight/StateMachine/StateMachineVisibilityConverter.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Data; + +namespace SnipInsight.StateMachine +{ + public class StateMachineVisibilityConverter : IValueConverter + { + /// + /// Converts state machine's state property to visibility for a UI component. + /// + /// The current state + /// + /// SnipInsightState enum value or a comma seperated string of state enum values. e..g SnipInsightState.Recording or 'Recording, Recorded' + /// Ignored. + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var state = value != null ? value.ToString() : string.Empty; + var targetStates = parameter.ToString().Split(','); + + return targetStates.Contains(state) ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/SnipInsight/Telemetry/ApplicationLogger.cs b/SnipInsight/Telemetry/ApplicationLogger.cs new file mode 100644 index 0000000..0a49255 --- /dev/null +++ b/SnipInsight/Telemetry/ApplicationLogger.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Net.NetworkInformation; + +namespace SnipInsight.Telemetry +{ + /// + /// Singleton Class for ILogger class for logging events using ARIA + /// + class ApplicationLogger + { + private static ApplicationLogger applicationLogger = new ApplicationLogger(); + private string versionNumber, macAddress; + + /// + /// Get function for the instance of Singleton ApplicationLogger Class + /// + public static ApplicationLogger Instance + { + get + { + return applicationLogger; + } + } + + /// + /// Constructor for Application Logger private to follow singleton design pattern + /// + private ApplicationLogger() + { + } + + /// + /// Generates initialization event to submit to ARIA + /// + public void SubmitEvent(string eventName) + { + // TODO: Log generic user events + } + + public void SubmitButtonClickEvent(string eventName, string viewName) + { + // TODO : Log clicks on UI elements by users + } + + public void SubmitApiCallEvent(string eventName, string apiCalled, long timeToCompleteInMilliSeconds, string apiResponseStatusCode) + { + // TODO : Log web request to AI services + } + + public void SubmitStateTransitionEvent(string eventName, + StateMachine.SnipInsightState source, + StateMachine.SnipInsightTrigger trigger, + StateMachine.SnipInsightState destination) + { + // TODO : Log transition of states in application. + } + + /// + /// Initializes Application Logger + /// + public void Initialize() + { + //TODO: Initialize the or set the logging module to be used to gather user telemetry + versionNumber = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(); + macAddress = GetMacAddress(); + } + + /// + /// Finds the MAC address of the NIC with maximum speed. + /// + /// The MAC address. + private string GetMacAddress() + { + const int MIN_MAC_ADDR_LENGTH = 12; + string macAddress = string.Empty; + long maxSpeed = -1; + + foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) + { + string tempMac = nic.GetPhysicalAddress().ToString(); + if (nic.Speed > maxSpeed && + !string.IsNullOrEmpty(tempMac) && + tempMac.Length >= MIN_MAC_ADDR_LENGTH) + { + maxSpeed = nic.Speed; + macAddress = tempMac; + } + } + + return macAddress; + } + } +} diff --git a/SnipInsight/Telemetry/EventName.cs b/SnipInsight/Telemetry/EventName.cs new file mode 100644 index 0000000..14f6925 --- /dev/null +++ b/SnipInsight/Telemetry/EventName.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Telemetry +{ + public static class EventName + { + public const string SnipApplicationInitialized = "SnipApplicationInitialized"; + public const string StateTransition = "StateTransition"; + public const string SingleApiCall = "SingleApiCall"; + public const string CompleteApiCall = "CompleteApiCall"; + public const string ButtonClick = "ButtonClick"; + public const string VersionChange = "VersionChange"; + + //Cognitive Services Api Call Event types + public const string CelebrityRecognitionApi = "CelebrityRecognitionApi"; + public const string ContentModerationApi = "ContentModerationApi"; + public const string EntitySearchApi = "EntitySearchApi"; + public const string HandWrittenTextApi = "HandWrittenTextApi"; + public const string ImageAnalysisApi = "ImageAnalysisApi"; + public const string ImageSearchApi = "ImageSearchApi"; + public const string LandmarkRecognitionApi = "LandmarkRecognitionApi"; + public const string PrintTextApi= "PrintTextApi"; + public const string ProductSearchApi = "ProductSearchApi"; + public const string TranslationApi = "TranslationApi"; + + //Api Panel Button Clicks + public const string SuggestedInsightsButton = "SuggestedInsightsButton"; + public const string ProductSearchButton = "ProductSearchButton"; + public const string PeopleSearchButton = "PeopleSearchButton"; + public const string PlaceSearchButton = "PlaceSearchButton"; + public const string OCRButton = "OCRButton"; + public const string ImageSearchButton = "ImageSearchButton"; + + //Action Ribbon Button Clicks + public const string RestoreImageButton = "RestoreImageButton"; + public const string CopyImageButton = "CopyImageButton"; + public const string SaveImageButton = "SaveImageButton"; + public const string SaveImageEmailButton = "SaveImageEmailButton"; + public const string ShareImageSendToOneNoteButton = "ShareImageSendToOneNoteButton"; + public const string RefreshAICommandButton = "RefrestAICommandButton"; + + //Clip Ribbon Button Clicks + public const string PauseButton = "PauseButton"; + public const string StopButton = "StopButton"; + public const string PlayButton = "PlayButton"; + + public const string PenSize1Button = "PenSize1Button"; + public const string PenSize3Button = "PenSize3Button"; + public const string PenSize5Button = "PenSize5Button"; + public const string PenSize7Button = "PenSize7Button"; + public const string PenSize9Button = "PenSize9Button"; + + public const string BlackColorToggle = "BlackColorToggle"; + public const string RedColorToggle = "RedColorToggle"; + public const string YellowColorToggle = "YellowColorToggle"; + public const string GreenColorToggle = "GreenColorToggle"; + public const string BlueColorToggle = "BlueColorToggle"; + } +} diff --git a/SnipInsight/Telemetry/PanelName.cs b/SnipInsight/Telemetry/PanelName.cs new file mode 100644 index 0000000..041ff4d --- /dev/null +++ b/SnipInsight/Telemetry/PanelName.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Telemetry +{ + public static class ViewName + { + public const string AiPanel = "AiPanel"; + public const string ActionRibbon = "ActionRibbon"; + public const string EditorSideNavigation = "EditorSideNavigation"; + } +} diff --git a/SnipInsight/Telemetry/PropertyName.cs b/SnipInsight/Telemetry/PropertyName.cs new file mode 100644 index 0000000..94f7f5a --- /dev/null +++ b/SnipInsight/Telemetry/PropertyName.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Telemetry +{ + public static class PropertyName + { + //Common Property Names for All Events + public const string VersionNumber = "VersionNumber"; + public const string MACAddress = "MACAddress"; + + //StateTransition Event Property Names + public const string StateTransitionSource = "StateTransitionSource"; + public const string StateTransitionTrigger = "StateTransitionTrigger"; + public const string StateTransitionDestination = "StateTransitionDestination"; + + //Api Call Event Properties + public const string ApiCalled = "ApiCalled"; + public const string ApiResponseStatus = "ApiResponseStatus"; + public const string TimeToComplete = "TimeToComplete(ms)"; + + //Button Click Event Properties + public const string ButtonName = "ButtonName"; + public const string ViewName = "ViewName"; + } +} diff --git a/SnipInsight/Telemetry/PropertyValue.cs b/SnipInsight/Telemetry/PropertyValue.cs new file mode 100644 index 0000000..9fa07d7 --- /dev/null +++ b/SnipInsight/Telemetry/PropertyValue.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Telemetry +{ + public static class PropertyValue + { + public const string NoResponse = "NoResponse"; + } +} diff --git a/SnipInsight/ThirdPartyNotices.txt b/SnipInsight/ThirdPartyNotices.txt new file mode 100644 index 0000000..b53e5d7 --- /dev/null +++ b/SnipInsight/ThirdPartyNotices.txt @@ -0,0 +1,48 @@ +This file is based on or incorporates material from the projects listed below (collectively "Third Party Code"). Microsoft is not the original author of the Third Party Code. The original copyright notice and the license, under which Microsoft received such Third Party Code, are set forth below. Such licenses and notices are provided for informational purposes only. Microsoft, not the third party, licenses the Third Party Code to you under the terms set forth in the EULA for the Microsoft Product. Microsoft reserves all other rights not expressly granted under this agreement, whether by implication, estoppel or otherwise. + +JSON.NET + +Copyright (c) 2007 James Newton-King +Provided for Informational Purposes Only + +stateless + +Copyright 2015 Nicholas Blumhardt + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Facebook-csharp-sdk + +Copyright (c) 2011, The Outercurve Foundation +Provided for Informational Purposes Only + +Apache 2.0 License + +This software is released under the Apache License 2.0 (the "License"); +you may not use the software except in compliance with the License. You +can find a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, +FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + diff --git a/SnipInsight/TrayIcon.cs b/SnipInsight/TrayIcon.cs new file mode 100644 index 0000000..837ec2b --- /dev/null +++ b/SnipInsight/TrayIcon.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows.Forms; +using SnipInsight.Util; + +namespace SnipInsight +{ + internal class TrayIcon : IDisposable + { + NotifyIcon _icon; + TrayIconContextMenu _contextMenu; + + internal TrayIcon() + { + _contextMenu = new TrayIconContextMenu(); + + _icon = new System.Windows.Forms.NotifyIcon(); + _icon.Icon = new System.Drawing.Icon(Properties.Resources.AppIcon, new System.Drawing.Size(16, 16)); + // TODO: Update the tray icon + _icon.Text = Properties.Resources.TrayIcon_ToolTip; + _icon.Visible = true; + _icon.ContextMenuStrip = _contextMenu.contextMenu; + _icon.Click += + delegate(object sender, EventArgs args) + { + if (((MouseEventArgs)args).Button == MouseButtons.Left) + { + AppManager.TheBoss.ToolWindow.ShowToolWindow(true, true); + } + }; + _icon.BalloonTipClicked += + delegate(object sender, EventArgs args) + { + UserSettings.DisableSysTrayBalloonAppStillRunning = true; + }; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~TrayIcon() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_icon != null) + { + _icon.Dispose(); + _icon = null; + } + + if (_contextMenu != null) + { + _contextMenu.Dispose(); + _contextMenu = null; + } + } + } + + internal void Activate() + { + ShowBalloonAppIsStillRunning(); + } + + private void ShowBalloonAppIsStillRunning() + { + if (!UserSettings.DisableSysTrayBalloonAppStillRunning) + { + _icon.BalloonTipTitle = Properties.Resources.TrayIcon_BalloonTip_AppIsStillRunning_Title; + _icon.BalloonTipText = Properties.Resources.TrayIcon_BalloonTip_AppIsStillRunning_Text; + _icon.ShowBalloonTip(5000); + } + } + + } +} diff --git a/SnipInsight/TrayIconContextMenu.Designer.cs b/SnipInsight/TrayIconContextMenu.Designer.cs new file mode 100644 index 0000000..1c7e65e --- /dev/null +++ b/SnipInsight/TrayIconContextMenu.Designer.cs @@ -0,0 +1,123 @@ +using SnipInsight.ViewModels; + +namespace SnipInsight +{ + partial class TrayIconContextMenu + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.contextMenu = new System.Windows.Forms.ContextMenuStrip(this.components); + this.menuItemNewCapture = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); + this.menuItemLibrary = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); + this.menuItemSettings = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator(); + this.menuItemExit = new System.Windows.Forms.ToolStripMenuItem(); + this.contextMenu.SuspendLayout(); + this.SuspendLayout(); + // + // contextMenu + // + this.contextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.menuItemNewCapture, + this.toolStripSeparator1, + this.menuItemLibrary, + this.toolStripSeparator2, + this.menuItemSettings, + this.toolStripSeparator3, + this.menuItemExit}); + this.contextMenu.Name = "contextMenuStrip1"; + this.contextMenu.Size = new System.Drawing.Size(174, 176); + // + // menuItemNewCapture + // + this.menuItemNewCapture.Name = "menuItemNewCapture"; + this.menuItemNewCapture.Size = new System.Drawing.Size(173, 22); + this.menuItemNewCapture.Text = "New Capture #"; + this.menuItemNewCapture.Click += new System.EventHandler(this.menuItemNewCapture_Click); + // toolStripSeparator1 + // + this.toolStripSeparator1.Name = "toolStripSeparator1"; + this.toolStripSeparator1.Size = new System.Drawing.Size(170, 6); + // + // menuItemLibrary + // + this.menuItemLibrary.Name = "menuItemLibrary"; + this.menuItemLibrary.Size = new System.Drawing.Size(173, 22); + this.menuItemLibrary.Text = "Library #"; + this.menuItemLibrary.Click += new System.EventHandler(this.menuItemLibrary_Click); + // + // toolStripSeparator2 + // + this.toolStripSeparator2.Name = "toolStripSeparator2"; + this.toolStripSeparator2.Size = new System.Drawing.Size(170, 6); + // + // menuItemSettings + // + this.menuItemSettings.Name = "menuItemSettings"; + this.menuItemSettings.Size = new System.Drawing.Size(173, 22); + this.menuItemSettings.Text = "Settings #"; + this.menuItemSettings.Click += new System.EventHandler(this.menuItemSettings_Click); + // + // toolStripSeparator3 + // + this.toolStripSeparator3.Name = "toolStripSeparator3"; + this.toolStripSeparator3.Size = new System.Drawing.Size(170, 6); + // + // menuItemExit + // + this.menuItemExit.Name = "menuItemExit"; + this.menuItemExit.Size = new System.Drawing.Size(173, 22); + this.menuItemExit.Text = "Exit #"; + this.menuItemExit.Click += new System.EventHandler(this.menuItemExit_Click); + // + // TrayIconContextMenu + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSize = true; + this.Name = "TrayIconContextMenu"; + this.Size = new System.Drawing.Size(321, 236); + this.contextMenu.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + internal System.Windows.Forms.ContextMenuStrip contextMenu; + private System.Windows.Forms.ToolStripMenuItem menuItemSettings; + private System.Windows.Forms.ToolStripMenuItem menuItemExit; + internal System.Windows.Forms.ToolStripMenuItem menuItemNewCapture; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; + private System.Windows.Forms.ToolStripMenuItem menuItemLibrary; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator3; + } +} diff --git a/SnipInsight/TrayIconContextMenu.cs b/SnipInsight/TrayIconContextMenu.cs new file mode 100644 index 0000000..93cde28 --- /dev/null +++ b/SnipInsight/TrayIconContextMenu.cs @@ -0,0 +1,53 @@ +using System; +using System.Windows.Forms; + +namespace SnipInsight +{ + public partial class TrayIconContextMenu : UserControl + { + SnipInsight.StateMachine.StateMachine AppStateMachine + { + get { return AppManager.TheBoss.ViewModel.StateMachine; } + } + + public TrayIconContextMenu() + { + InitializeComponent(); + + menuItemNewCapture.Text = Properties.Resources.TrayIcon_ContextMenuItem_NewCapture; + menuItemLibrary.Text = Properties.Resources.TrayIcon_ContextMenuItem_Library; + menuItemSettings.Text = Properties.Resources.TrayIcon_ContextMenuItem_Settings; + menuItemExit.Text = Properties.Resources.TrayIcon_ContextMenuItem_Exit; + } + + private void menuItemNewCapture_Click(object sender, EventArgs e) + { + AppStateMachine.Fire(StateMachine.SnipInsightTrigger.CaptureScreen); + } + + private void menuItemNewWhiteboard_Click(object sender, EventArgs e) + { + AppStateMachine.Fire(StateMachine.SnipInsightTrigger.Whiteboard); + } + + private void menuItemNewPhoto_Click(object sender, EventArgs e) + { + AppStateMachine.Fire(StateMachine.SnipInsightTrigger.CaptureCamera); + } + + private void menuItemLibrary_Click(object sender, EventArgs e) + { + AppStateMachine.Fire(StateMachine.SnipInsightTrigger.ShowLibraryPanel); + } + + private void menuItemSettings_Click(object sender, EventArgs e) + { + AppStateMachine.Fire(StateMachine.SnipInsightTrigger.ShowSettingsPanel); + } + + private void menuItemExit_Click(object sender, EventArgs e) + { + AppStateMachine.Fire(StateMachine.SnipInsightTrigger.Exit); + } + } +} diff --git a/SnipInsight/TrayIconContextMenu.resx b/SnipInsight/TrayIconContextMenu.resx new file mode 100644 index 0000000..da22e46 --- /dev/null +++ b/SnipInsight/TrayIconContextMenu.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 69 + + \ No newline at end of file diff --git a/SnipInsight/Util/AnimationUtilities.cs b/SnipInsight/Util/AnimationUtilities.cs new file mode 100644 index 0000000..ed94630 --- /dev/null +++ b/SnipInsight/Util/AnimationUtilities.cs @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace SnipInsight.Util +{ + public static class AnimationUtilities + { + #region Property Paths + + public static PropertyPath CanvasLeftPropertyPath = new PropertyPath(Canvas.LeftProperty); + public static PropertyPath CanvasTopPropertyPath = new PropertyPath(Canvas.TopProperty); + public static PropertyPath GradientStopColorPropertyPath = new PropertyPath(GradientStop.ColorProperty); + public static PropertyPath HeightPropertyPath = new PropertyPath(FrameworkElement.HeightProperty); + public static PropertyPath WidthPropertyPath = new PropertyPath(FrameworkElement.WidthProperty); + public static PropertyPath OpacityPropertyPath = new PropertyPath(UIElement.OpacityProperty); + public static PropertyPath SolidColorBrushColorPropertyPath = new PropertyPath(SolidColorBrush.ColorProperty); + public static PropertyPath ScaleXPropertyPath = new PropertyPath(ScaleTransform.ScaleXProperty); + public static PropertyPath ScaleYPropertyPath = new PropertyPath(ScaleTransform.ScaleYProperty); + public static PropertyPath TranslateTransformXPropertyPath = new PropertyPath(TranslateTransform.XProperty); + public static PropertyPath TranslateTransformYPropertyPath = new PropertyPath(TranslateTransform.YProperty); + public static PropertyPath VisibilityPropertyPath = new PropertyPath(UIElement.VisibilityProperty); + + #endregion + + #region Color Animation + + public static ColorAnimation CreateColorAnimation(Nullable from, Nullable to) + { + ColorAnimation animation = new ColorAnimation(); + + animation.From = from; + animation.To = to; + + return animation; + } + + public static ColorAnimation CreateColorAnimation(int durationInMilliseconds, Nullable from, Nullable to) + { + ColorAnimation animation = CreateColorAnimation(from, to); + + animation.Duration = CreateDuration(durationInMilliseconds); + + return animation; + } + + public static ColorAnimation CreateColorAnimation(PropertyPath path, int durationInMilliseconds, Nullable from, Nullable to) + { + ColorAnimation animation = CreateColorAnimation(durationInMilliseconds, from, to); + + SetTargetProperty(animation, path); + + return animation; + } + + public static ColorAnimation CreateColorAnimation(DependencyObject element, PropertyPath path, int durationInMilliseconds, Nullable from, Nullable to) + { + ColorAnimation animation = CreateColorAnimation(durationInMilliseconds, from, to); + + SetTargetProperty(animation, element, path); + + return animation; + } + + #endregion + + #region Double Animation + + public static DoubleAnimation CreateDoubleAnimation(Nullable from, Nullable to) + { + DoubleAnimation animation = new DoubleAnimation(); + + animation.From = from; + animation.To = to; + + return animation; + } + + public static DoubleAnimation CreateDoubleAnimation(int durationInMilliseconds, Nullable from, Nullable to) + { + DoubleAnimation animation = CreateDoubleAnimation(from, to); + + animation.Duration = CreateDuration(durationInMilliseconds); + + return animation; + } + + public static DoubleAnimation CreateDoubleAnimation(PropertyPath path, int durationInMilliseconds, Nullable from, Nullable to) + { + DoubleAnimation animation = CreateDoubleAnimation(durationInMilliseconds, from, to); + + SetTargetProperty(animation, path); + + return animation; + } + + public static DoubleAnimation CreateDoubleAnimation(DependencyObject element, PropertyPath path, int durationInMilliseconds, Nullable from, Nullable to) + { + DoubleAnimation animation = CreateDoubleAnimation(durationInMilliseconds, from, to); + + SetTargetProperty(animation, element, path); + + return animation; + } + + #endregion + + #region Opacity Animation + + public static DoubleAnimation CreateOpacityAnimation(int durationInMilliseconds, Nullable from, Nullable to) + { + return CreateDoubleAnimation(OpacityPropertyPath, durationInMilliseconds, from, to); + } + + public static DoubleAnimation CreateOpacityAnimation(DependencyObject element, int durationInMilliseconds, Nullable from, Nullable to) + { + return CreateDoubleAnimation(element, OpacityPropertyPath, durationInMilliseconds, from, to); + } + + #endregion + + #region Fade In/Out Animation + + public static DoubleAnimation CreateFadeInAnimation(int durationInMilliseconds) + { + return CreateOpacityAnimation(durationInMilliseconds, null, 1); + } + + public static DoubleAnimation CreateFadeInAnimation(DependencyObject element, int durationInMilliseconds) + { + return CreateOpacityAnimation(element, durationInMilliseconds, null, 1); + } + + public static Storyboard CreateFadeInStoryboard(DependencyObject element, int durationInMilliseconds) + { + return CreateStoryboard(CreateFadeInAnimation(element, durationInMilliseconds)); + } + + public static DoubleAnimation CreateFadeOutAnimation(int durationInMilliseconds) + { + return CreateOpacityAnimation(durationInMilliseconds, null, 0); + } + + public static DoubleAnimation CreateFadeOutAnimation(DependencyObject element, int durationInMilliseconds) + { + return CreateOpacityAnimation(element, durationInMilliseconds, null, 0); + } + + private static Storyboard CreateFadeOutStoryboard(UIElement element, int durationInMilliseconds) + { + return CreateFadeOutStoryboard(element, durationInMilliseconds, false, false); + } + + private static Storyboard CreateFadeOutAndHideStoryboard(UIElement element, int durationInMilliseconds) + { + return CreateFadeOutStoryboard(element, durationInMilliseconds, true, false); + } + + private static Storyboard CreateFadeOutAndRemoveStoryboard(UIElement element, int durationInMilliseconds) + { + return CreateFadeOutStoryboard(element, durationInMilliseconds, false, true); + } + + private static Storyboard CreateFadeOutStoryboard(UIElement element, int durationInMilliseconds, bool hideOnComplete, bool removeOnComplete) + { + Storyboard storyboard = CreateStoryboard(CreateFadeOutAnimation(element, durationInMilliseconds)); + + if (removeOnComplete) + { + AddRemoveOnComplete(storyboard, element); + } + else if (hideOnComplete) + { + AddHideOnComplete(storyboard, element); + } + + return storyboard; + } + + #endregion + + #region Fade In/Out + + public static Storyboard FadeIn(this UIElement element, int durationInMilliseconds) + { + Storyboard storyboard = CreateStoryboard(CreateFadeInAnimation(element, durationInMilliseconds)); + + element.Visibility = Visibility.Visible; + + storyboard.Begin(); + + return storyboard; + } + + public static Storyboard FadeOut(this UIElement element, int durationInMilliseconds) + { + return FadeOut(element, durationInMilliseconds, false, false); + } + + public static Storyboard FadeOutAndHide(this UIElement element, int durationInMilliseconds) + { + return FadeOut(element, durationInMilliseconds, true, false); + } + + public static Storyboard FadeOutAndRemove(this UIElement element, int durationInMilliseconds) + { + return FadeOut(element, durationInMilliseconds, false, true); + } + + private static Storyboard FadeOut(UIElement element, int durationInMilliseconds, bool hideOnComplete, bool removeOnComplete) + { + Storyboard storyboard = CreateFadeOutStoryboard(element, durationInMilliseconds, hideOnComplete, removeOnComplete); + + storyboard.Begin(); + + return storyboard; + } + + #endregion + + #region Storyboard + + public static Storyboard CreateStoryboard(AnimationTimeline timeline) + { + Storyboard storyboard = new Storyboard(); + + storyboard.Children.Add(timeline); + + return storyboard; + } + + #endregion + + #region Storyboard Targets + + public static void SetTargetProperty(AnimationTimeline timeline, PropertyPath path) + { + Storyboard.SetTargetProperty(timeline, path); + } + + public static void SetTarget(AnimationTimeline timeline, DependencyObject element) + { + Storyboard.SetTarget(timeline, element); + } + + public static void SetTargetProperty(AnimationTimeline timeline, DependencyObject element, PropertyPath path) + { + SetTarget(timeline, element); + SetTargetProperty(timeline, path); + } + + #endregion + + #region Hide and Remove + + public static void AddHideOnComplete(Storyboard storyboard, UIElement element) + { + storyboard.Completed += (s, e) => + { + Hide(element); + }; + } + + public static void AddRemoveOnComplete(Storyboard storyboard, UIElement element) + { + storyboard.Completed += (s, e) => + { + Hide(element); + RemoveFromParent(element); + }; + } + + private static void Hide(UIElement element) + { + element.Visibility = Visibility.Collapsed; + } + + private static void RemoveFromParent(UIElement element) + { + FrameworkElement frameworkElement = element as FrameworkElement; + + if (frameworkElement != null) + { + if (frameworkElement.Parent != null && frameworkElement.Parent is Panel) + { + ((Panel)frameworkElement.Parent).Children.Remove(element); + } + } + } + + #endregion + + #region Duration + + public static Duration CreateDuration(int milliseconds) + { + return new Duration(TimeSpan.FromMilliseconds(milliseconds)); + } + + public static KeyTime CreateKeyTime(int milliseconds) + { + return KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(milliseconds)); + } + + #endregion + } +} diff --git a/SnipInsight/Util/AppDiagnosticsLogger.cs b/SnipInsight/Util/AppDiagnosticsLogger.cs new file mode 100644 index 0000000..a611647 --- /dev/null +++ b/SnipInsight/Util/AppDiagnosticsLogger.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Properties; +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace SnipInsight.Util +{ + internal class AppDiagnosticsLogger : IDisposable + { + const string LogType = "diagnostics"; + internal AppDiagnosticsLogger(string langName, string appName, string appVersion, string applicationOS, string envOS, string osBitness, string processBitness, bool itInstall) + : base() + { + _lock = new object(); + _lastLogged = DateTime.UtcNow; + + _logFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Snip.txt"; + _logHeaderWritten = false; + _logHeader = string.Format("Language Locale: {0} Name: {1} Version: {2} Started: {3:s} OS: {4} ITInstall: {5}\r\nVersion2: OS: {6} ({7}), Process: ({8})\r\n\r\n", + langName, + appName, + appVersion, + _lastLogged, + applicationOS, + itInstall, + envOS, + osBitness, + processBitness); + + const int UploadDelay = 301303; + const int IdleThreshold = 4973; + LogUploader.RetrieveContentDelegate retrieveContentDelegate = new LogUploader.RetrieveContentDelegate(this.GetUploadLogContent); + _logUploader = new LogUploader(Settings.Default.DiagsReportUri, UserSettings.RequestId, LogType, IdleThreshold, UploadDelay, + this.GetUploadLogContent, null, null); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_logUploader != null) + { + _logUploader.Dispose(); + _logUploader = null; + } + } + } + + /// + /// Log info into log + /// + /// The string representation of the info to be logged + /// + internal void Info(string description) + { + bool success = true; + DateTime requestTime = DateTime.MinValue; + + lock (_lock) + { + try + { + _lastLogged = requestTime = DateTime.UtcNow; + string logInfo = String.Format("{0:s}: {1}\r\n", requestTime, description); + + using (StreamWriter writer = File.AppendText(_logFile)) + { + if (_logHeaderWritten) + { + Debug.WriteLineIf(System.Diagnostics.Debugger.IsLogging(), logInfo); + writer.Write(logInfo); + } + else + { + // Include the header information the first time + writer.Write(_logHeader); + writer.Write(logInfo); + _logHeaderWritten = true; + } + } + } + // ReSharper disable EmptyGeneralCatchClause + catch + // ReSharper restore EmptyGeneralCatchClause + { + success = false; + } + } + + if (success) + { + _logUploader.Queue(requestTime); + } + } + + internal string Exception(Exception ex, int severity) + { + string text = MessageFromException(ex, severity); + Info(text); + return text; + } + + private string GetUploadLogContent(ref DateTime contentTime) + { + lock (_lock) + { + try + { + contentTime = _lastLogged; + // Trim the file to keep only the last lines + string[] allLines = File.ReadAllLines(_logFile); + if (allLines.Length > _sMaxLines) + { + string[] trimmedLines = new string[_sMaxLines]; + Array.Copy(allLines, allLines.Length - _sMaxLines, trimmedLines, 0, _sMaxLines); + File.WriteAllLines(_logFile, trimmedLines); + return string.Join("\r\n", trimmedLines); + } + else + { + return string.Join("\r\n", allLines); + } + } + // ReSharper disable EmptyGeneralCatchClause + catch + // ReSharper restore EmptyGeneralCatchClause + { + return null; + } + } + } + + internal string GetLogFile() + { + return _logFile; + } + + static string MessageFromException(Exception ex, int severity) + { + // Create human decipherable message + StringBuilder text = new StringBuilder(); + + // header + text.AppendLine(string.Format("[exception]({0})-({1})-({2})", Diagnostics.Version, ex.HResult, severity)); + + text.AppendLine(ex.Message); + text.AppendLine(ex.StackTrace); + if (ex.InnerException != null) + { + text.AppendLine("-----"); + text.AppendLine(ex.InnerException.Message); + text.AppendLine(ex.InnerException.StackTrace); + } + + // footer + text.Append("[/exception]"); + + return text.ToString(); + } + + private static int _sMaxLines = 5000; + + private object _lock; + private DateTime _lastLogged; + private LogUploader _logUploader; + private string _logFile; + private bool _logHeaderWritten; + private string _logHeader; + + } +} diff --git a/SnipInsight/Util/AppUsageLogger.cs b/SnipInsight/Util/AppUsageLogger.cs new file mode 100644 index 0000000..7dcde1e --- /dev/null +++ b/SnipInsight/Util/AppUsageLogger.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Properties; +using System; +using System.IO; + +namespace SnipInsight.Util +{ + internal class AppUsageLogger : IDisposable + { + const string LogType = "usage"; + internal AppUsageLogger(string langName, string appName, string appVersion, string applicationOS, string envOS, string osBitness, string processBitness, bool itInstall) + : base() + { + _lock = new object(); + _lastLogged = DateTime.UtcNow; + + _logFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\SnipUsages.txt"; + _uploadLogFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\SnipUsagesUpload.txt"; + + try + { + MergeUploadLogToLog(); + } + catch (Exception) + { + + } + _langName = langName; + _sessionId = Guid.NewGuid().ToString(); + _appName = appName; + _appVersion = appVersion; + _os = string.Format("{0} ({1})", envOS, osBitness); + _process = processBitness; + _itInstall = itInstall; + _entryId = 0; + + const int UploadDelay = 29567; + const int IdleThreshold = 5107; + _logUploader = new LogUploader(Settings.Default.DiagsReportUri, UserSettings.RequestId, LogType, IdleThreshold, UploadDelay, + this.GetUploadLogContent, this.UploadSuccessCallback, this.UploadFailureCallback); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_logUploader != null) + { + _logUploader.Dispose(); + _logUploader = null; + } + } + } + + internal void Exception(Exception ex, int severity) + { + + bool success = true; + DateTime requestTime = DateTime.MinValue; + + lock (_lock) + { + try + { + _lastLogged = requestTime = DateTime.UtcNow; + } + // ReSharper disable EmptyGeneralCatchClause + catch + // ReSharper restore EmptyGeneralCatchClause + { + success = false; + } + } + + if (success) + { + _logUploader.Queue(requestTime); + } + } + + private string GetUploadLogContent(ref DateTime contentTime) + { + lock(_lock) + { + try + { + if (File.Exists(_logFile)) + { + File.Move(_logFile, _uploadLogFile); + contentTime = _lastLogged; + // Trim the file to keep only the last lines + string[] allLines = File.ReadAllLines(_uploadLogFile); + if (allLines.Length > _sMaxLines) + { + string[] trimmedLines = new string[_sMaxLines]; + Array.Copy(allLines, allLines.Length - _sMaxLines, trimmedLines, 0, _sMaxLines); + File.WriteAllLines(_logFile, trimmedLines); + return string.Join("\r\n", trimmedLines); + } + else + { + return string.Join("\r\n", allLines); + } + } + else + { + return null; + } + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + return null; + } + } + } + + private void UploadFailureCallback() + { + // merge upload log with + lock (_lock) + { + try + { + Diagnostics.LogTrace("Usage log upload failed."); + MergeUploadLogToLog(); + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + } + } + } + + private void MergeUploadLogToLog() + { + if (!File.Exists(_logFile)) + { + if (File.Exists(_uploadLogFile)) + { + File.Move(_uploadLogFile, _logFile); + } + } + else + { + using (Stream inputStream = File.OpenRead(_logFile)) + using (Stream outputStream = new FileStream(_uploadLogFile, FileMode.Append, FileAccess.Write, FileShare.None)) + { + inputStream.CopyTo(outputStream); + } + File.Delete(_logFile); + File.Move(_uploadLogFile, _logFile); + } + } + + private void UploadSuccessCallback() + { + // delete upload log + lock(_lock) + { + try + { + if (File.Exists(_uploadLogFile)) + { + File.Delete(_uploadLogFile); + } + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + } + } + } + + private static int _sMaxLines = 512; + + private string _langName; + private string _uploadLogFile; + private string _sessionId; + private int _entryId; + private object _lock; + private DateTime _lastLogged; + private LogUploader _logUploader; + private string _logFile; + private string _appName; + private string _appVersion; + private string _os; + private string _process; + private bool _itInstall; + } +} diff --git a/SnipInsight/Util/Diagnostics.cs b/SnipInsight/Util/Diagnostics.cs new file mode 100644 index 0000000..16ec2e5 --- /dev/null +++ b/SnipInsight/Util/Diagnostics.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Properties; +using System; +using System.Globalization; +using System.Windows; + +namespace SnipInsight.Util +{ + internal static class Diagnostics + { + + const string AppName = "SnipInsight"; + static Diagnostics() + { + string langName = CultureInfo.CurrentCulture.Name; + string applicationOS = Environment.OSVersion.VersionString; + string envOS = Environment.OSVersion.ToString(); + string osBitness = GetBitnessString(Environment.Is64BitOperatingSystem); + string processBitness = GetBitnessString(Environment.Is64BitProcess); + bool itInstall = false; + + _diagnosticsLogger = new AppDiagnosticsLogger(langName, AppName, Version, applicationOS, envOS, osBitness, processBitness, itInstall); + _usageLogger = new AppUsageLogger(langName, AppName, Version, applicationOS, envOS, osBitness, processBitness, itInstall); + } + + internal static string Version + { + get { return _sVersion; } + } + + internal static bool IsSilent { get; set; } + + /// + /// Helper method to log trace information + /// + /// The string representation of the info to be logged + /// Whether to upload log immediately + internal static void LogTrace(string description) + { + _diagnosticsLogger.Info(description); + } + + /// + /// Helper method to report a severe exception + /// + /// The exception that occurred + /// + internal static void LogException(Exception ex) + { + try + { + const int Severity = 2; + _diagnosticsLogger.Exception(ex, Severity); + _usageLogger.Exception(ex, Severity); + } + // ReSharper disable EmptyGeneralCatchClause + catch + // ReSharper restore EmptyGeneralCatchClause + { + } + } + + /// + /// Helper method to report a non-severe exception + /// + /// The exception that occurred + /// + internal static void LogLowPriException(Exception ex) + { + try + { + const int Severity = 3; + _diagnosticsLogger.Exception(ex, Severity); + _usageLogger.Exception(ex, Severity); + } + // ReSharper disable EmptyGeneralCatchClause + catch + // ReSharper restore EmptyGeneralCatchClause + { + } + } + + internal static void ReportException(Exception ex) + { + const int Severity = 1; + try + { + _usageLogger.Exception(ex, Severity); + string text = _diagnosticsLogger.Exception(ex, 1); + + if (!IsSilent) + { +#if DEBUG + MessageBox.Show(AppManager.TheBoss.MainWindow, text, Resources.Exception_Dialog_Title, MessageBoxButton.OK); +#else + MessageBox.Show(AppManager.TheBoss.MainWindow, Resources.Exception_Dialog_Text, Resources.Exception_Dialog_Title, MessageBoxButton.OK); +#endif + } + } + // ReSharper disable EmptyGeneralCatchClause + catch + // ReSharper restore EmptyGeneralCatchClause + { + } + } + + internal static string GetDiagnosticsLog() + { + return _diagnosticsLogger.GetLogFile(); + } + + private static string GetBitnessString(bool is64bit) + { + return is64bit ? "64-bit" : "32-bit"; + } + + static string _sVersion = "1.0";/*((AssemblyFileVersionAttribute)(Assembly.GetExecutingAssembly().GetCustomAttribute(typeof(AssemblyFileVersionAttribute)))).Version;*/ + static AppDiagnosticsLogger _diagnosticsLogger; + static AppUsageLogger _usageLogger; + } +} diff --git a/SnipInsight/Util/DpiScale.cs b/SnipInsight/Util/DpiScale.cs new file mode 100644 index 0000000..fedc1f0 --- /dev/null +++ b/SnipInsight/Util/DpiScale.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; + +namespace SnipInsight.Util +{ + public class DpiScale : IEquatable + { + private readonly double _x; + private readonly double _y; + + public DpiScale() + : this(1, 1) + { + + } + + public DpiScale(double value) + : this(value, value) + { + + } + + public DpiScale(double x, double y) + { + if (double.IsNaN(x) || x <= 0) + throw new ArgumentOutOfRangeException("x"); + + if (double.IsNaN(y) || y <= 0) + throw new ArgumentOutOfRangeException("y"); + + this._x = x; + this._y = y; + } + + public double X + { + get { return _x; } + } + + public double Y + { + get { return _y; } + } + + public override string ToString() + { + return "X=" + X.ToString("0.###") + ", Y=" + Y.ToString("0.###"); + } + + #region Equatable + + public sealed override bool Equals(object obj) + { + if (obj is DpiScale) + { + return Equals((DpiScale)obj); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return X.GetHashCode() + Y.GetHashCode() << 2; + } + + public bool Equals(DpiScale other) + { + return X.Equals(other.X) && Y.Equals(other.Y); + } + + #endregion + } +} diff --git a/SnipInsight/Util/DpiUtilities.cs b/SnipInsight/Util/DpiUtilities.cs new file mode 100644 index 0000000..af965c9 --- /dev/null +++ b/SnipInsight/Util/DpiUtilities.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; + +namespace SnipInsight.Util +{ + public static class DpiUtilities + { + #region DpiAwareness + + private static ProcessDpiAwareness _dpiAwareness = (ProcessDpiAwareness)(-1); + + public static ProcessDpiAwareness DpiAwareness + { + get + { + if (_dpiAwareness == (ProcessDpiAwareness)(-1)) + { + _dpiAwareness = GetDpiAwareness(); + } + + return _dpiAwareness; + } + } + + private static ProcessDpiAwareness GetDpiAwareness() + { + ProcessDpiAwareness result = ProcessDpiAwareness.DpiUnaware; + + try + { + // If at least Windows 8.1 + if (IsOsGreaterOrEqualTo(6, 3)) + { + int value = 0; + + if (NativeMethods.GetProcessDpiAwareness(IntPtr.Zero, ref value) == 0) + { + result = (ProcessDpiAwareness)value; + } + } + } + catch + { + result = ProcessDpiAwareness.DpiUnaware; + } + + return result; + } + + private static bool IsOsGreaterOrEqualTo(int major, int minor) + { + int currentMajor = System.Environment.OSVersion.Version.Major; + int currentMinor = System.Environment.OSVersion.Version.Minor; + + if (currentMajor == major && currentMinor >= minor) + { + return true; + } + else if (currentMajor > major) + { + return true; + } + else + { + return false; + } + } + + #endregion + + #region Scale + + public static DpiScale CalculateScale(uint dpi1X, uint dpi1Y, uint dpi2X, uint dpi2Y) + { + return new DpiScale((double)dpi1X / (double)dpi2X, (double)dpi1Y / (double)dpi2Y); + } + + public static DpiScale GetSystemScale() + { + uint systemDpiX; + uint systemDpiY; + + GetSystemEffectiveDpi(out systemDpiX, out systemDpiY); + + return CalculateScale(systemDpiX, systemDpiY, 96, 96); + } + + /// + /// Gets the window (monitor) scale versus System DPI. + /// + /// The window. + /// + /// + /// When developing a DPI-aware application, this is useful for applying a ScaleTransform + /// to your window so all fonts and graphics are scaled and rendered beautifully based on the + /// physical capabilities of the display and the accessibility settings of the user. + /// + public static DpiScale GetWindowScale(Window window) + { + return GetWindowScale(GetWindowHwnd(window)); + } + + /// + /// Gets the window (monitor) scale versus System DPI. + /// + /// The window. + /// + /// + /// When developing a DPI-aware application, this is useful for applying a ScaleTransform + /// to your window so all fonts and graphics are scaled and rendered beautifully based on the + /// physical capabilities of the display and the accessibility settings of the user. + /// + public static DpiScale GetWindowScale(IntPtr hwnd) + { + uint dpiX; + uint dpiY; + + GetWindowEffectiveDpi(hwnd, out dpiX, out dpiY); + + uint systemDpiX; + uint systemDpiY; + + GetSystemEffectiveDpi(out systemDpiX, out systemDpiY); + + return CalculateScale(dpiX, dpiY, systemDpiX, systemDpiY); + } + + /// + /// Gets the virtual pixel scale for a window. This is essentially + /// the Effective DPI of the monitor versus a standard 96 DPI. + /// + /// The window. + /// + public static DpiScale GetVirtualPixelScale(Window window) + { + return GetVirtualPixelScale(GetWindowHwnd(window)); + } + + /// + /// Gets the virtual pixel scale for a window. This is essentially + /// the Effective DPI of the monitor versus a standard 96 DPI. + /// + /// The HWND. + /// + public static DpiScale GetVirtualPixelScale(IntPtr hwnd) + { + uint dpiX; + uint dpiY; + + GetWindowEffectiveDpi(hwnd, out dpiX, out dpiY); + + return CalculateScale(dpiX, dpiY, 96, 96); + } + + /// + /// Gets the virtual pixel scale for a monitor. This is essentially + /// the Effective DPI of the monitor versus a standard 96 DPI. + /// + /// The h monitor. + /// + public static DpiScale GetVirtualPixelScaleByMonitor(IntPtr hMonitor) + { + uint dpiX; + uint dpiY; + + GetMonitorEffectiveDpi(hMonitor, out dpiX, out dpiY); + + return CalculateScale(dpiX, dpiY, 96, 96); + } + + #endregion + + #region Effective DPI + + /// + /// Get the Effective DPI of a monitor after a user's accessibility + /// preferences have been applied. + /// + /// The monitor. + /// The dpi x. + /// The dpi y. + public static void GetMonitorEffectiveDpi(IntPtr hMonitor, out uint dpiX, out uint dpiY) + { + dpiX = 96; + dpiY = 96; + + ProcessDpiAwareness awareness = DpiAwareness; + + if (awareness >= ProcessDpiAwareness.PerMonitorDpiAware) + { + int hresult = NativeMethods.GetDpiForMonitor(hMonitor, NativeMethods.Monitor_DPI_Type.MDT_Effective_DPI, ref dpiX, ref dpiY); + + if (hresult != 0) + { + // If anything goes wrong, return a reasonable default + + dpiX = 96; + dpiY = 96; + } + } + else if (awareness == ProcessDpiAwareness.SystemDpiAware) + { + GetSystemEffectiveDpi(out dpiX, out dpiY); + } + else + { + // Use the default of 96 + return; + } + } + + /// + /// Get the System Effective DPI. + /// + /// + /// The System Effective DPI is derived by the operating system + /// by looking across all monitors and determining an "Effective DPI" that works + /// well across all the screens for applications that do not support + /// per-monitor DPI. + /// + /// The dpi x. + /// The dpi y. + public static void GetSystemEffectiveDpi(out uint dpiX, out uint dpiY) + { + IntPtr handle = NativeMethods.GetDC(IntPtr.Zero); + + int x = NativeMethods.GetDeviceCaps(handle, (int)NativeMethods.DeviceCap.LOGPIXELSX); + int y = NativeMethods.GetDeviceCaps(handle, (int)NativeMethods.DeviceCap.LOGPIXELSY); + + // If anything goes wrong, return a reasonable DPI + + dpiX = x > 0 ? (uint)x : 96; + dpiY = y > 0 ? (uint)y : 96; + } + + /// + /// Get the Effective DPI of a windows (monitor) after a user's accessibility + /// preferences have been applied. + /// + /// The window. + /// The dpi x. + /// The dpi y. + public static void GetWindowEffectiveDpi(Window window, out uint dpiX, out uint dpiY) + { + GetWindowEffectiveDpi(GetWindowHwnd(window), out dpiX, out dpiY); + } + + /// + /// Get the Effective DPI of a windows (monitor) after a user's accessibility + /// preferences have been applied. + /// + /// The window. + /// The dpi x. + /// The dpi y. + public static void GetWindowEffectiveDpi(IntPtr hwnd, out uint dpiX, out uint dpiY) + { + IntPtr hMonitor = GetMonitorFromWindow(hwnd); + + GetMonitorEffectiveDpi(hMonitor, out dpiX, out dpiY); + } + + #endregion + + #region Miscellaneous + + /// + /// Gets scaling factor for a monitor + /// This is defined as current pixel size relative to effective DPI + /// + /// Monitor device name or null for the primary monitor + /// + public static double GetScreenScalingFactor(string deviceName = null) + { + // WIFRY: I tried to replace this method with one of the other methods in this class, + // but for some reason, it generates a result that I can't quite match. Therefore, + // I'm leaving this method in place as it seems to work and service a specific need. + // Ultimately, it might be nice to find a way to consolidate it later. + + var dc = NativeMethods.CreateDC("DISPLAY", deviceName, null, IntPtr.Zero); + if (dc == IntPtr.Zero) + return 1.0d; + + int LogicalScreenHeight = NativeMethods.GetDeviceCaps(dc, (int)NativeMethods.DeviceCap.VERTRES); + int PhysicalScreenHeight = NativeMethods.GetDeviceCaps(dc, (int)NativeMethods.DeviceCap.DESKTOPVERTRES); + + double ScreenScalingFactor = (double)PhysicalScreenHeight / (double)LogicalScreenHeight; + + System.Diagnostics.Trace.WriteLine("Monitor:\"" + deviceName + "\" Scaling factor:" + ScreenScalingFactor + + " Logical height:" + LogicalScreenHeight + " Physical height:" + PhysicalScreenHeight); + + NativeMethods.DeleteDC(dc); + + return ScreenScalingFactor; + } + + #endregion + + #region Helpers + + private static IntPtr GetWindowHwnd(Window window) + { + return NativeMethods.GetWindowHwnd(window); + } + + private static IntPtr GetMonitorFromWindow(IntPtr hwnd) + { + return NativeMethods.GetMonitorFromWindow(hwnd); + } + + #endregion + } +} diff --git a/SnipInsight/Util/FormatStringConverter.cs b/SnipInsight/Util/FormatStringConverter.cs new file mode 100644 index 0000000..a6a9282 --- /dev/null +++ b/SnipInsight/Util/FormatStringConverter.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Diagnostics; +using System.Globalization; +using System.Windows.Data; + +namespace SnipInsight.Util +{ + public class FormatStringConverter : IValueConverter + { + public string FormatString { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var formatString = this.FormatString ?? parameter as String; + Debug.Assert( + !string.IsNullOrEmpty(formatString), + "FormatStringConverter requires providing a format string as either the FormatString property or the converter parameter"); + return string.Format(formatString, value); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/SnipInsight/Util/HotKey.cs b/SnipInsight/Util/HotKey.cs new file mode 100644 index 0000000..a26e355 --- /dev/null +++ b/SnipInsight/Util/HotKey.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; + +namespace SnipInsight.Util +{ + public enum SnipHotKey + { + QuickCapture = 0, + ScreenCapture = 1, + Library = 2, + } + + internal class HotKeyPressedEventArgs : EventArgs + { + internal SnipHotKey KeyPressed { get; set; } + } +} diff --git a/SnipInsight/Util/ImageUtils.cs b/SnipInsight/Util/ImageUtils.cs new file mode 100644 index 0000000..d4b686e --- /dev/null +++ b/SnipInsight/Util/ImageUtils.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Conversion; +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Reflection; +using System.Windows.Media.Imaging; + +namespace SnipInsight.Util +{ + public static class ImageUtils + { + private const int OverlayHeight = 128; + private const int OverlayWidth = 128; + + /// + /// Overlay the captured image with play button. + /// + public static string OverlayImageWithPlayButton(BitmapSource source, out int outputWidth, out int outputHeight) + { + try + { + string result = Path.Combine(Path.GetTempPath(), string.Format("cNImage{0}.png", DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss"))); + string overlayUrl = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Play_360x200.png"); + + using (MemoryStream ms = new MemoryStream()) + using (Image image = new Bitmap(PictureConverter.SaveToPng(source, ms))) + { + // Use aspect ratio to determine size of output image. If source image is wider than it is tall, + // then we set max width to 320. If it is taller than it is wide, we set max height to 240. + double aspectRatio = (double)image.Width / image.Height; + if (aspectRatio > 1) + { + outputWidth = 320; + outputHeight = (int)(320 / aspectRatio); + } + else + { + outputHeight = 240; + outputWidth = (int)(240 * aspectRatio); + } + + int sourceHeight = outputHeight; + int sourceWidth = outputWidth; + + // If source image is too narrow, ensure the output image is large enough to fit the play button + // (which is 128x128) and some buffer space. + if (outputWidth < 150) + { + outputWidth = 150; + } + if (outputHeight < 150) + { + outputHeight = 150; + } + + using (var bitmap = new Bitmap(outputWidth, outputHeight)) + using (var canvas = Graphics.FromImage(bitmap)) + { + canvas.InterpolationMode = InterpolationMode.HighQualityBicubic; + + canvas.DrawImage(image, new Rectangle((outputWidth - sourceWidth) / 2, (outputHeight - sourceHeight) / 2, sourceWidth, sourceHeight), new Rectangle(0, 0, image.Width, image.Height), GraphicsUnit.Pixel); + + // Try and combine both the images together. + if (File.Exists(overlayUrl)) + { + int overlayX = (int)((double)(outputWidth - OverlayWidth) / 2); + int overlayY = (int)((double)(outputHeight - OverlayHeight) / 2); + + using (Image overlay = Image.FromFile(overlayUrl)) + { + canvas.DrawImage(overlay, new Rectangle(overlayX, overlayY, OverlayWidth, OverlayHeight)); + } + } + + canvas.Save(); + bitmap.Save(result, ImageFormat.Png); + } + } + return result; + } + catch (Exception ex) + { + outputWidth = 340; + outputHeight = 240; + Diagnostics.LogException(ex); + return null; + } + } + } +} diff --git a/SnipInsight/Util/KeyCombo.cs b/SnipInsight/Util/KeyCombo.cs new file mode 100644 index 0000000..364fe7d --- /dev/null +++ b/SnipInsight/Util/KeyCombo.cs @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Linq; +using System.Text; +using System.Windows.Input; + +namespace SnipInsight.Util +{ + public class KeyCombo : IEquatable + { + public bool Alt { get; set; } + public bool Shift { get; set; } + public bool Ctrl { get; set; } + + private Key _key; + + public Key Key + { + get { return _key; } + set + { + if (IsValidPrimaryKey(value)) + { + _key = value; + } + else + { + _key = Key.None; + } + } + } + + public bool HasKey + { + get { return Key != Key.None; } + } + + public bool IsValid + { + get + { + return HasKey + && (!IsSimpleKey(Key) || (Alt && Ctrl)) + && (!IsDependentKey(Key) || (Alt || Ctrl || Shift)); + } + } + + public bool IsEmpty + { + get { return !(HasKey || Alt || Ctrl || Shift); } + } + + public int VirtualKeyCode + { + get + { + return KeyInterop.VirtualKeyFromKey(Key); + } + } + + public int KeyModifier + { + get + { + int keyModifier = 0; + if (Alt) + keyModifier = keyModifier | (int)NativeMethods.HotKeyModifiers.MOD_ALT; + if (Ctrl) + keyModifier = keyModifier | (int)NativeMethods.HotKeyModifiers.MOD_CONTROL; + if (Shift) + keyModifier = keyModifier | (int)NativeMethods.HotKeyModifiers.MOD_SHIFT; + + return keyModifier | (int)NativeMethods.HotKeyModifiers.MOD_NOREPEAT; + } + } + + public void Reset() + { + Key = Key.None; + Ctrl = false; + Alt = false; + Shift = false; + } + + public string ToDescriptiveString() + { + StringBuilder sb = new StringBuilder(32); + + if (Ctrl) + { + sb.Append("Ctrl"); + } + + if (Alt) + { + AppendIfNotZeroLength(sb, " + "); + sb.Append("Alt"); + } + + if (Shift) + { + AppendIfNotZeroLength(sb, " + "); + sb.Append("Shift"); + } + + if (HasKey) + { + AppendIfNotZeroLength(sb, " + "); + if (Key == Key.Snapshot) + { + sb.Append("PrintScreen"); + } + else + { + sb.Append(Key.ToString()); + } + } + + return sb.ToString(); + } + + private static void AppendIfNotZeroLength(StringBuilder sb, string text) + { + if (sb.Length > 0) + { + sb.Append(text); + } + } + + #region Storage String + + public override string ToString() + { + if (!IsValid) + { + return ""; + } + else + { + StringBuilder sb = new StringBuilder(32); + + if (Ctrl) + { + sb.Append("Ctrl"); + } + + if (Alt) + { + AppendIfNotZeroLength(sb, "+"); + sb.Append("Alt"); + } + + if (Shift) + { + AppendIfNotZeroLength(sb, "+"); + sb.Append("Shift"); + } + + if (HasKey) + { + AppendIfNotZeroLength(sb, "+"); + sb.Append(Key.ToString()); + } + + return sb.ToString(); + } + } + + public static KeyCombo ParseOrDefault(string s) + { + KeyCombo combo = new KeyCombo(); + + if (!string.IsNullOrEmpty(s)) + { + string[] parts = s.ToLowerInvariant().Split('+'); + + Key key; + + if (Enum.TryParse(parts[parts.Length - 1].Trim(), true, out key)) + { + combo.Key = key; + + combo.Ctrl = parts.Contains("ctrl"); + combo.Alt = parts.Contains("alt"); + combo.Shift = parts.Contains("shift"); + + if (!combo.IsValid) + { + combo.Reset(); + } + } + } + + return combo; + } + + #endregion + + #region Key Categories + public static bool IsValidPrimaryKey(Key key) + { + switch (key) + { + case Key.Apps: // Properties + case Key.System: + case Key.Sleep: + case Key.None: + case Key.NoName: + case Key.JunjaMode: + case Key.KanaMode: + case Key.KanjiMode: + case Key.ImeAccept: + case Key.ImeConvert: + case Key.ImeModeChange: + case Key.ImeNonConvert: + case Key.ImeProcessed: + case Key.DeadCharProcessed: + case Key.NumLock: + case Key.OemSemicolon: + case Key.OemOpenBrackets: + case Key.CapsLock: + return false; + } + + if (IsDbeKey(key)) + return false; + + return true; + } + + /// + /// Returns a value indicating that they required both CTRL + ALT + /// + /// The key. + /// + public bool IsSimpleKey(Key key) + { + return key >= Key.A && key <= Key.Z + || key >= Key.D0 && key <= Key.D9 + || IsSimpleKeyExtended(key); + + } + + public bool IsDependentKey(Key key) + { + switch (key) + { + case Key.Up: + case Key.Down: + case Key.Left: + case Key.Right: + case Key.PageUp: + case Key.PageDown: + return true; + default: + return false; + } + } + + private bool IsSimpleKeyExtended(Key key) + { + switch (key) + { + case Key.Enter: + case Key.Space: + return true; + } + + if (IsOemKey(key)) + return true; + + return false; + } + + public static bool IsShiftKey(Key key) + { + return key == Key.LeftShift || key == Key.RightShift; + } + + public static bool IsCtrlKey(Key key) + { + return key == Key.LeftCtrl || key == Key.RightCtrl; + } + + public static bool IsAltKey(Key key) + { + return key == Key.LeftAlt || key == Key.RightAlt; + } + + public static bool IsWindowsKey(Key key) + { + return key == Key.LWin || key == Key.RWin; + } + + private static bool IsDbeKey(Key key) + { + switch (key) + { + + case Key.DbeAlphanumeric: + case Key.DbeCodeInput: + case Key.DbeDbcsChar: + case Key.DbeEnterDialogConversionMode: + case Key.DbeEnterImeConfigureMode: + case Key.DbeEnterWordRegisterMode: + case Key.DbeFlushString: + case Key.DbeHiragana: + case Key.DbeKatakana: + case Key.DbeNoCodeInput: + case Key.DbeNoRoman: + case Key.DbeRoman: + case Key.DbeSbcsChar: + return true; + } + + return false; + } + + private static bool IsOemKey(Key key) + { + // Note: OEM Keys appear more than once in the enum, + // so you won't see all values listed below intentionally. + + switch (key) + { + case Key.OemAttn: + case Key.OemAuto: + case Key.OemBackslash: + case Key.OemBackTab: + case Key.OemClear: + case Key.OemCloseBrackets: + case Key.OemComma: + case Key.OemCopy: + case Key.OemEnlw: + case Key.OemFinish: + case Key.OemMinus: + case Key.OemOpenBrackets: + case Key.OemPeriod: + case Key.OemPipe: + case Key.OemPlus: + case Key.OemQuestion: + case Key.OemQuotes: + case Key.OemSemicolon: + case Key.OemTilde: + return true; + } + + return false; + } + + #endregion + + #region Equals + + public override bool Equals(object obj) + { + if (obj is KeyCombo) + { + return Equals((KeyCombo)obj); + } + else + { + return false; + } + } + + public bool Equals(KeyCombo other) + { + return this.Key == other.Key + && this.Alt == other.Alt + && this.Ctrl == other.Ctrl + && this.Shift == other.Shift; + } + + public override int GetHashCode() + { + // This is good enough. We don't current plan + // to hash this object. + return (int)Key; + } + + #endregion + + #region Clone + + public KeyCombo Clone() + { + return new KeyCombo() + { + Key = this.Key, + Ctrl = this.Ctrl, + Alt = this.Alt, + Shift = this.Shift + }; + } + + #endregion + } +} diff --git a/SnipInsight/Util/LogUploader.cs b/SnipInsight/Util/LogUploader.cs new file mode 100644 index 0000000..2d89283 --- /dev/null +++ b/SnipInsight/Util/LogUploader.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Text; + +namespace SnipInsight.Util +{ + internal class LogUploader : IDisposable + { + internal delegate string RetrieveContentDelegate(ref DateTime contentTime); + internal delegate void SuccessCallbackDelegate(); + internal delegate void FailureCallbackDelegate(); + + const int cPollingPeriod = 521; + + Uri _url; + RetrieveContentDelegate _retrieveContent; + SuccessCallbackDelegate _successCallback; + FailureCallbackDelegate _failureCallback; + object _lock; + DateTime _lastRequest; + DateTime _lastSent; + bool _sending; + System.Threading.Timer _timer; + int _idleThreshold; + int _uploadPeriod; + + internal LogUploader(string url, Guid id, string logType, int idleThreshold, int uploadDelay, + RetrieveContentDelegate retrieveContent, SuccessCallbackDelegate successCallback, FailureCallbackDelegate failureCallback) + { + if (retrieveContent == null) + { + throw new ArgumentNullException("retrieveContent"); + } + _url = CreateReportUrl(url, id, logType); + _idleThreshold = idleThreshold; + _uploadPeriod = uploadDelay; + _retrieveContent = retrieveContent; + _successCallback = successCallback; + _failureCallback = failureCallback; + _lock = new object(); + _lastRequest = _lastSent = DateTime.MinValue; + _timer = new System.Threading.Timer(Callback, null, cPollingPeriod, cPollingPeriod); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_timer != null) + { + _timer.Dispose(); + } + } + } + + Uri CreateReportUrl(string reportString, Guid id, string logType) + { + UriBuilder url = new UriBuilder(reportString); + + url.Query = string.Format("source=SnipInsight&type={0}&requestId={1}", logType, id.ToString()); + + return url.Uri; + } + + internal void Queue(DateTime requestTime) + { + lock (_lock) + { + if (requestTime > _lastRequest) + { + _lastRequest = requestTime; + } + } + } + + void Callback(Object state) + { + bool send = false; + DateTime now = DateTime.UtcNow; + + // evaluate whether we need to initiate a send + lock (_lock) + { + if (!_sending) + { + if (_lastRequest > _lastSent) + { + // process lazy request after the delay has expired + if ((now - _lastRequest).TotalMilliseconds > _idleThreshold && (now - _lastSent).TotalMilliseconds > _uploadPeriod) + { + send = _sending = true; + } + } + } + } + + if (send) + { + Send(now); + } + } + + async void Send(DateTime sendTime) + { + try + { + if (!System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) + { + // Queue for retry + Queue(DateTime.UtcNow + TimeSpan.FromMilliseconds(_uploadPeriod)); + if (_failureCallback != null) + { + _failureCallback(); + } + return; + } + + DateTime contentTime = DateTime.MinValue; + string uploadContent = _retrieveContent(ref contentTime); + if (!string.IsNullOrEmpty(uploadContent)) + { + uploadContent = Compress(uploadContent); + using (StringContent payload = new StringContent(uploadContent)) + { + using (HttpClient client = new HttpClient()) + { + // Build the POST request + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _url); + request.Headers.Add("X-MS-SnipInsight", "WqCK64ev30QAPKSoLUIEig4s9QJoJqYwzGDMXOrmS7CU3iHrHqZhosksf34QAu=="); + request.Content = payload; + request.Content.Headers.ContentEncoding.Add("gzip"); + HttpResponseMessage response = await client.SendAsync(request); + if (response.IsSuccessStatusCode) + { + lock (_lock) + { + _lastSent = sendTime > contentTime ? sendTime : contentTime; + if (_successCallback != null) + { + _successCallback(); + } + } + } + else + { + // Queue for retry + Queue(DateTime.UtcNow + TimeSpan.FromMilliseconds(_uploadPeriod)); + if (_failureCallback != null) + { + _failureCallback(); + } + } + } + } + } + } + catch (Exception ex) + { + if (ex is HttpRequestException) + { + // SendAsync may have thrown, queue for retry + Queue(DateTime.UtcNow + TimeSpan.FromMilliseconds(_uploadPeriod)); + } + Diagnostics.LogException(ex); + if (_failureCallback != null) + { + _failureCallback(); + } + } + finally + { + lock (_lock) + { + _sending = false; + } + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + static string Compress(string s) + { + var bytes = Encoding.Unicode.GetBytes(s); + using (var msi = new MemoryStream(bytes)) + { + using (var mso = new MemoryStream()) + { + using (var gs = new GZipStream(mso, CompressionMode.Compress, true)) + { + msi.CopyTo(gs); + } + return Convert.ToBase64String(mso.ToArray()); + } + } + } + } +} diff --git a/SnipInsight/Util/NativeMethods.cs b/SnipInsight/Util/NativeMethods.cs new file mode 100644 index 0000000..8e98b9e --- /dev/null +++ b/SnipInsight/Util/NativeMethods.cs @@ -0,0 +1,1152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; + +namespace SnipInsight.Util +{ + internal class NativeMethods + { + + #region User32 + internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + [DllImport("user32.dll")] + internal static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern IntPtr GetDC(IntPtr hWnd); + [DllImport("user32.dll")] + internal static extern bool GetClientRect(IntPtr hWnd, out NativeMethods.RECT lpRect); + + [DllImport("user32")] + internal static extern bool GetMonitorInfo(IntPtr hMonitor, MONITORINFO lpmi); + [DllImport("user32", EntryPoint = "GetMonitorInfo", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool GetMonitorInfoEx(IntPtr hMonitor, ref MONITORINFOEX lpmi); + + [DllImport("user32")] + internal static extern IntPtr MonitorFromWindow(IntPtr handle, int flags); + [DllImport("user32")] + internal static extern IntPtr MonitorFromPoint(POINT pt, int flags); + + [DllImport("user32.dll")] + internal static extern int GetSystemMetrics(int nIndex); + + internal enum GetClipBoxReturn : int + { + Error = 0, + NullRegion = 1, + SimpleRegion = 2, + ComplexRegion = 3 + } + + [Flags] + internal enum WindowStyles : uint + { + // ReSharper disable InconsistentNaming + WS_BORDER = 0x800000, + WS_CAPTION = 0xc00000, + WS_CHILD = 0x40000000, + WS_CLIPCHILDREN = 0x2000000, + WS_CLIPSIBLINGS = 0x4000000, + WS_DISABLED = 0x8000000, + WS_DLGFRAME = 0x400000, + WS_GROUP = 0x20000, + WS_HSCROLL = 0x100000, + WS_MAXIMIZE = 0x1000000, + WS_MAXIMIZEBOX = 0x10000, + WS_MINIMIZE = 0x20000000, + WS_MINIMIZEBOX = 0x20000, + WS_OVERLAPPED = 0x0, + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_SIZEFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_POPUP = 0x80000000u, + WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, + WS_SIZEFRAME = 0x40000, + WS_SYSMENU = 0x80000, + WS_THICKFRAME = 0x40000, + WS_TABSTOP = 0x10000, + WS_VISIBLE = 0x10000000, + WS_VSCROLL = 0x200000, + // ReSharper restore InconsistentNaming + } + + [Flags] + internal enum WindowStylesEx : uint + { + // ReSharper disable InconsistentNaming + WS_EX_TRANSPARENT = 0x00000020, + WS_EX_TOOLWINDOW = 0x00000080, + // ReSharper restore InconsistentNaming + } + + internal enum ShowWindowCommands : int + { + // ReSharper disable InconsistentNaming + SW_FORCEMINIMIZE = 11, + SW_HIDE = 0, + SW_MAXIMIZE = 3, + SW_MINIMIZE = 6, + SW_RESTORE = 9, + SW_SHOW = 5, + SW_SHOWDEFAULT = 10, + SW_SHOWMAXIMIZED = 3, + SW_SHOWMINIMIZED = 2, + SW_SHOWMINNOACTIVE = 7, + SW_SHOWNA = 8, + SW_SHOWNOACTIVATE = 4, + SW_SHOWNORMAL = 1, + // ReSharper restore InconsistentNaming + } + + [StructLayout(LayoutKind.Sequential)] + internal struct MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMinTrackSize; + public POINT ptMaxTrackSize; + }; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class MONITORINFO + { + public int cbSize = Marshal.SizeOf(typeof(MONITORINFO)); + public RECT rcMonitor = new RECT(); + public RECT rcWork = new RECT(); + public int dwFlags = 0; + } + + internal const int MONITORINFOF_PRIMARY = 1; + internal const int CCHDEVICENAME = 32; + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal struct MONITORINFOEX + { + public int cbSize; + public RECT rcMonitor; + public RECT rcWork; + public int dwFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)] + public string deviceName; + static public MONITORINFOEX New() + { + return new MONITORINFOEX + { + cbSize = Marshal.SizeOf(typeof(MONITORINFOEX)), + deviceName = string.Empty, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct POINT + { + // ReSharper disable InconsistentNaming + public int x; + public int y; + // ReSharper restore InconsistentNaming + + /// + /// Construct a point of coordinates (x,y). + /// + public POINT(int x, int y) + { + this.x = x; + this.y = y; + } + + public override string ToString() + { + return "(" + x.ToString() + "," + y.ToString() + ")"; + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 0)] + internal struct RECT + { + // ReSharper disable InconsistentNaming + internal int left; + internal int top; + internal int right; + internal int bottom; + + internal int width + { + get { return right - left; } + } + + internal int height + { + get { return bottom - top; } + } + // ReSharper restore InconsistentNaming + + + /// Win32 + public static readonly RECT Empty = new RECT(); + + /// Win32 + public RECT(int left, int top, int right, int bottom) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + + /// Win32 + public RECT(RECT rcSrc) + { + this.left = rcSrc.left; + this.top = rcSrc.top; + this.right = rcSrc.right; + this.bottom = rcSrc.bottom; + } + + /// Win32 + public bool IsEmpty + { + get + { + // BUGBUG : On Bidi OS (hebrew arabic) left > right + return left >= right || top >= bottom; + } + } + /// Return a user friendly representation of this struct + public override string ToString() + { + if (this == RECT.Empty) { return "RECT {Empty}"; } + return "RECT { left : " + left + " / top : " + top + " / right : " + right + " / bottom : " + bottom + " }"; + } + + /// Determine if 2 RECT are equal (deep compare) + public override bool Equals(object obj) + { + if (!(obj is Rect)) { return false; } + return (this == (RECT)obj); + } + + /// Return the HashCode for this struct (not garanteed to be unique) + public override int GetHashCode() + { + return left.GetHashCode() + top.GetHashCode() + right.GetHashCode() + bottom.GetHashCode(); + } + + + /// Determine if 2 RECT are equal (deep compare) + public static bool operator ==(RECT rect1, RECT rect2) + { + return (rect1.left == rect2.left && rect1.top == rect2.top && rect1.right == rect2.right && rect1.bottom == rect2.bottom); + } + + /// Determine if 2 RECT are different(deep compare) + public static bool operator !=(RECT rect1, RECT rect2) + { + return !(rect1 == rect2); + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WINDOWPLACEMENT + { + // ReSharper disable InconsistentNaming + internal int length; + internal int flags; + internal ShowWindowCommands showCmd; + internal POINT ptMinPosition; + internal POINT ptMaxPosition; + internal RECT rcNormalPosition; + internal static WINDOWPLACEMENT Empty + { + get + { + WINDOWPLACEMENT result = new WINDOWPLACEMENT(); + result.length = Marshal.SizeOf(result); + return result; + } + } + // ReSharper restore InconsistentNaming + } + + internal enum WindowMsg : uint + { + // ReSharper disable InconsistentNaming + WM_SIZE = 0x0005, + WM_CLOSE = 0x0010, + WM_MOUSEACTIVATE = 0x0021, + WM_MOUSEMOVE = 0x0200, + WM_LBUTTONDOWN = 0x0201, + WM_LBUTTONUP = 0x0202, + WM_LBUTTONDBLCLK = 0x0203, + WM_DPICHANGED = 0x02E0, + + WM_NCCREATE = 0x0081, + WM_NCDESTROY = 0x0082, + WM_NCCALCSIZE = 0x0083, + WM_NCHITTEST = 0x0084, + WM_NCPAINT = 0x0085, + WM_NCACTIVATE = 0x0086, + + WM_HOTKEY = 0x0312, + // ReSharper restore InconsistentNaming + } + + [Flags] + internal enum SetWindowPosFlags : uint + { + SWP_NOSIZE = 0x0001, + SWP_NOMOVE = 0x0002, + SWP_NOZORDER = 0x0004, + SWP_NOREDRAW = 0x0008, + SWP_NOACTIVATE = 0x0010, + SWP_FRAMECHANGED = 0x0020, /* The frame changed: send WM_NCCALCSIZE */ + SWP_SHOWWINDOW = 0x0040, + SWP_HIDEWINDOW = 0x0080, + SWP_NOCOPYBITS = 0x0100, + SWP_NOOWNERZORDER = 0x0200, /* Don't do owner Z ordering */ + SWP_NOSENDCHANGING = 0x0400, /* Don't send WM_WINDOWPOSCHANGING */ + + SWP_DRAWFRAME = SWP_FRAMECHANGED, + SWP_NOREPOSITION = SWP_NOOWNERZORDER, + } + + internal enum HResults : int + { + // ReSharper disable InconsistentNaming + S_OK = 0, + S_FALSE = 1, + S_ENDOFSTREAM = 513183767, + E_NOTIMPL = -2147467263, + E_OUTOFMEMORY = -2147024882, + E_INVALIDARG = -2147024809, + E_NOINTERFACE = -2147467262, + E_POINTER = -2147467261, + E_HANDLE = -2147024890, + E_ABORT = -2147467260, + E_FAIL = -2147467259, + E_ACCESSDENIED = -2147024891, + E_NOSUITABLEAUDIOSTREAM = -1634299904, + E_NOSUITABLEVIDEOSTREAM = -1634299903, + E_NOTRANSCODEAUDIOTYPE = -1634299902, + MF_E_SINK_NO_SAMPLES_PROCESSED = -1072870844, + E_NOAUDIORECEIVED = -1634299880, + // ReSharper restore InconsistentNaming + } + + internal enum GaFlags + { + /// + /// Retrieves the parent window. This does not include the owner, as it does with the GetParent function. + /// + GA_PARENT = 1, + /// + /// Retrieves the root window by walking the chain of parent windows. + /// + GA_ROOT = 2, + /// + /// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent. + /// + GA_ROOTOWNER = 3 + } + + internal enum GwlIndex + { + // ReSharper disable InconsistentNaming + + /// + /// Gets/sets the window styles. + /// + GWL_STYLE = -16, + + /// + /// Gets/sets the extended window styles. + /// + GWL_EXSTYLE = -20, + + // ReSharper restore InconsistentNaming + } + + internal enum MouseActivate + { + // ReSharper disable InconsistentNaming + MA_ACTIVATE = 1, + MA_ACTIVATEANDEAT = 2, + MA_NOACTIVATE = 3, + MA_NOACTIVATEANDEAT = 4, + // ReSharper restore InconsistentNaming + } + + internal enum HookType : int + { + WH_JOURNALRECORD = 0, + WH_JOURNALPLAYBACK = 1, + WH_KEYBOARD = 2, + WH_GETMESSAGE = 3, + WH_CALLWNDPROC = 4, + WH_CBT = 5, + WH_SYSMSGFILTER = 6, + WH_MOUSE = 7, + WH_HARDWARE = 8, + WH_DEBUG = 9, + WH_SHELL = 10, + WH_FOREGROUNDIDLE = 11, + WH_CALLWNDPROCRET = 12, + WH_KEYBOARD_LL = 13, + WH_MOUSE_LL = 14 + } + + internal const int WM_KEYDOWN = 0x0100; + internal const int WM_KEYUP= 0x0101; + + [Flags] + public enum HotKeyModifiers : uint + { + MOD_ALT = 0x0001, // Either ALT key must be held down + MOD_CONTROL = 0x0002, // Either CTRL key must be held down. + MOD_NOREPEAT = 0x4000, // Changes the hotkey behavior so that the keyboard auto-repeat does not yield multiple hotkey notifications. + MOD_SHIFT = 0x0004, // Either SHIFT key must be held down. + MOD_WIN = 0x0008, // Either WINDOWS key was held down. These keys are labeled with the Windows logo. Keyboard shortcuts that involve the WINDOWS key are reserved for use by the operating system. + } + + internal enum SystemMetrixIndex : int + { + // ReSharper disable InconsistentNaming + SM_CXSIZEFRAME = 32, + SM_CYSIZEFRAME = 33, + SM_CXPADDEDBORDER = 92, + // ReSharper restore InconsistentNaming + } + + [DllImport("user32.dll")] + internal static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + internal static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll", ExactSpelling = true)] + internal static extern IntPtr GetAncestor(IntPtr hwnd, GaFlags flags); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern Int32 GetWindowLongPtr32(IntPtr hWnd, int nIndex); + + // supress CA1400 for 64-bit systems + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1400:PInvokeEntryPointsShouldExist"), DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + /// + /// Handles 32/64 bit differences + /// + public static IntPtr GetWindowLongPtr(IntPtr hWnd, GwlIndex nIndex) + { + if (Is32BitProcess()) + { + return (IntPtr)GetWindowLongPtr32(hWnd, (int)nIndex); + } + + return GetWindowLongPtr64(hWnd, (int)nIndex); + } + + [StructLayout(LayoutKind.Sequential)] + internal struct LASTINPUTINFO + { + internal uint cbSize; + internal uint dwTime; + } + + [DllImport("User32.dll")] + internal static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DestroyIcon([In] IntPtr hIcon); + + #endregion + + #region Shcore + + [DllImport("shcore")] + internal static extern int GetDpiForMonitor(IntPtr hMonitor, Monitor_DPI_Type dpiType, ref uint dpiX, ref uint dpiY); + + [DllImport("shcore")] + internal static extern int SetProcessDpiAwareness(int value); + + [DllImport("shcore")] + internal static extern int GetProcessDpiAwareness(IntPtr handle, ref int value); + + internal enum Monitor_DPI_Type : int + { + MDT_Effective_DPI = 0, + MDT_Angular_DPI = 1, + MDT_Raw_DPI = 2, + MDT_Default = MDT_Effective_DPI + }; + + #endregion + + #region Kernel32 + + [DllImport("kernel32.dll")] + internal static extern uint GetCurrentThreadId(); + + [DllImport("kernel32.dll")] + internal static extern ulong GetTickCount64(); + + [DllImport("kernel32.dll")] + internal static extern bool CloseHandle(IntPtr handle); + + #endregion + + #region Advapi32 + + /// + /// Passed to to specify what information about the token to return. + /// + internal enum TokenInformationClass + { + TokenUser = 1, + TokenGroups, + TokenPrivileges, + TokenOwner, + TokenPrimaryGroup, + TokenDefaultDacl, + TokenSource, + TokenType, + TokenImpersonationLevel, + TokenStatistics, + TokenRestrictedSids, + TokenSessionId, + TokenGroupsAndPrivileges, + TokenSessionReference, + TokenSandBoxInert, + TokenAuditPolicy, + TokenOrigin, + TokenElevationType, + TokenLinkedToken, + TokenElevation, + TokenHasRestrictions, + TokenAccessInformation, + TokenVirtualizationAllowed, + TokenVirtualizationEnabled, + TokenIntegrityLevel, + TokenUiAccess, + TokenMandatoryPolicy, + TokenLogonSid, + MaxTokenInfoClass + } + + [StructLayout(LayoutKind.Sequential)] + internal struct TokenElevation + { + internal uint TokenIsElevated; + } + + [DllImport("advapi32.dll", SetLastError = true)] + internal static extern bool GetTokenInformation(IntPtr tokenHandle, TokenInformationClass tokenInformationClass, IntPtr tokenInformation, uint tokenInformationLength, out uint returnLength); + + + internal static uint STANDARD_RIGHTS_READ = 0x00020000; + internal static uint TOKEN_QUERY = 0x0008; + internal static uint TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY); + + [DllImport("advapi32.dll", SetLastError = true)] + internal static extern bool OpenProcessToken(IntPtr processHandle, UInt32 desiredAccess, out IntPtr tokenHandle); + + + #endregion + + #region Utility functions + + internal static void ThrowLastError() + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + #endregion + + #region ImageCapture + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr MonitorFromPoint(System.Drawing.Point pt, MonitorOptions dwFlags); + + public enum MonitorOptions : uint + { + MONITOR_DEFAULTTONULL = 0x00000000, + MONITOR_DEFAULTTOPRIMARY = 0x00000001, + MONITOR_DEFAULTTONEAREST = 0x00000002 + } + + public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); + + [DllImport("user32.dll")] + public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, + MonitorEnumDelegate lpfnEnum, IntPtr dwData); + + [DllImport("user32.dll", SetLastError = true)] + public static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex); + + public const int GWL_EXSTYLE = -20; + + public const int WS_EX_TRANSPARENT = 0x20; + + [DllImport("user32.dll")] + internal extern static IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + internal static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll")] + internal static extern IntPtr GetWindowDC(IntPtr hwnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern IntPtr GetWindow(IntPtr hWnd, int uCmd); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + + internal enum SetWindowPosInsertAfter : int + { + HWND_BOTTOM = 1, + HWND_TOP = 0, + HWND_TOPMOST = -1, + HWND_NOTOPMOST = -2, + } + + [DllImport("user32")] + internal static extern bool SetWindowPos( + IntPtr hWnd, + IntPtr hWndInsertAfter, + int x, + int y, + int cx, + int cy, + uint uFlags); + + public const int GW_HWNDNEXT = 2; + public const int GW_HWNDPREV = 3; + + [DllImport("user32.dll")] + internal static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); + + [StructLayout(LayoutKind.Sequential)] + internal struct IconInfo + { + public bool fIcon; // Specifies whether this structure defines an icon or a cursor. A value of TRUE specifies + // an icon; FALSE specifies a cursor. + public Int32 xHotspot; // Specifies the x-coordinate of a cursor's hot spot. If this structure defines an icon, the hot + // spot is always in the center of the icon, and this member is ignored. + public Int32 yHotspot; // Specifies the y-coordinate of the cursor's hot spot. If this structure defines an icon, the hot + // spot is always in the center of the icon, and this member is ignored. + public IntPtr hbmMask; // (HBITMAP) Specifies the icon bitmask bitmap. If this structure defines a black and white icon, + // this bitmask is formatted so that the upper half is the icon AND bitmask and the lower half is + // the icon XOR bitmask. Under this condition, the height should be an even multiple of two. If + // this structure defines a color icon, this mask only defines the AND bitmask of the icon. + public IntPtr hbmColor; // (HBITMAP) Handle to the icon color bitmap. This member can be optional if this + // structure defines a black and white icon. The AND bitmask of hbmMask is applied with the SRCAND + // flag to the destination; subsequently, the color bitmap is applied (using XOR) to the + // destination by using the SRCINVERT flag. + } + + #endregion + + #region AuthorEx + [DllImport("authorex.dll")] + internal static extern int CreateDeviceDiscoveryClass(out AuthorEx.IDeviceEnumerator enumerator, AuthorEx.IDeviceEnumeratorCallback callback); + + [DllImport("authorex.dll")] + internal static extern int CreateCaptureControlClass(out AuthorEx.ICaptureControl control, AuthorEx.ICaptureCallback callback); + + [DllImport("authorex.dll")] + internal static extern int CreateMP4MediaTransferClass(out AuthorEx.IMP4MediaTransfer transfer); + + [DllImport("authorex.dll")] + internal static extern int VerifyEmbeddedSignature([MarshalAs(UnmanagedType.BStr)] string filename, bool mayBeOffline); + + [DllImport("authorex.dll")] + internal static extern int CheckD3D(uint d3dVersion); + + internal static bool Is32BitProcess() + { + return IntPtr.Size == 4 /*bytes*/; + } + + #endregion + + #region gdi32 + [DllImport("gdi32.dll")] + internal static extern int GetClipBox(IntPtr hdc, out NativeMethods.RECT lprc); + + [DllImport("gdi32.dll", EntryPoint = "CreateCompatibleBitmap")] + public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, + int nWidth, int nHeight); + + [DllImport("gdi32.dll", EntryPoint = "CreateCompatibleDC")] + public static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport("gdi32.dll", EntryPoint = "SelectObject")] + public static extern IntPtr SelectObject(IntPtr hdc, IntPtr bmp); + + [DllImport("gdi32")] + internal static extern bool DeleteObject(IntPtr o); + + [DllImport("gdi32.dll", EntryPoint = "CreateDC", CharSet = CharSet.Unicode)] + internal static extern IntPtr CreateDC(string driver, string device, string output, IntPtr initData); + + [DllImport("gdi32.dll", EntryPoint = "DeleteDC")] + internal static extern bool DeleteDC(IntPtr hDc); + + internal enum DeviceCap + { + /// + /// Device driver version + /// + DRIVERVERSION = 0, + /// + /// Device classification + /// + TECHNOLOGY = 2, + /// + /// Horizontal size in millimeters + /// + HORZSIZE = 4, + /// + /// Vertical size in millimeters + /// + VERTSIZE = 6, + /// + /// Horizontal width in pixels + /// + HORZRES = 8, + /// + /// Vertical height in pixels + /// + VERTRES = 10, + /// + /// Number of bits per pixel + /// + BITSPIXEL = 12, + /// + /// Number of planes + /// + PLANES = 14, + /// + /// Number of brushes the device has + /// + NUMBRUSHES = 16, + /// + /// Number of pens the device has + /// + NUMPENS = 18, + /// + /// Number of markers the device has + /// + NUMMARKERS = 20, + /// + /// Number of fonts the device has + /// + NUMFONTS = 22, + /// + /// Number of colors the device supports + /// + NUMCOLORS = 24, + /// + /// Size required for device descriptor + /// + PDEVICESIZE = 26, + /// + /// Curve capabilities + /// + CURVECAPS = 28, + /// + /// Line capabilities + /// + LINECAPS = 30, + /// + /// Polygonal capabilities + /// + POLYGONALCAPS = 32, + /// + /// Text capabilities + /// + TEXTCAPS = 34, + /// + /// Clipping capabilities + /// + CLIPCAPS = 36, + /// + /// Bitblt capabilities + /// + RASTERCAPS = 38, + /// + /// Length of the X leg + /// + ASPECTX = 40, + /// + /// Length of the Y leg + /// + ASPECTY = 42, + /// + /// Length of the hypotenuse + /// + ASPECTXY = 44, + /// + /// Shading and Blending caps + /// + SHADEBLENDCAPS = 45, + + /// + /// Logical pixels inch in X + /// + LOGPIXELSX = 88, + /// + /// Logical pixels inch in Y + /// + LOGPIXELSY = 90, + + /// + /// Number of entries in physical palette + /// + SIZEPALETTE = 104, + /// + /// Number of reserved entries in palette + /// + NUMRESERVED = 106, + /// + /// Actual color resolution + /// + COLORRES = 108, + + // Printing related DeviceCaps. These replace the appropriate Escapes + /// + /// Physical Width in device units + /// + PHYSICALWIDTH = 110, + /// + /// Physical Height in device units + /// + PHYSICALHEIGHT = 111, + /// + /// Physical Printable Area x margin + /// + PHYSICALOFFSETX = 112, + /// + /// Physical Printable Area y margin + /// + PHYSICALOFFSETY = 113, + /// + /// Scaling factor x + /// + SCALINGFACTORX = 114, + /// + /// Scaling factor y + /// + SCALINGFACTORY = 115, + + /// + /// Current vertical refresh rate of the display device (for displays only) in Hz + /// + VREFRESH = 116, + /// + /// Vertical height of entire desktop in pixels + /// + DESKTOPVERTRES = 117, + /// + /// Horizontal width of entire desktop in pixels + /// + DESKTOPHORZRES = 118, + /// + /// Preferred blt alignment + /// + BLTALIGNMENT = 119 + } + + [DllImport("gdi32.dll")] + internal static extern int GetDeviceCaps(IntPtr hdc, int nIndex); + + internal enum TernaryRasterOperations : uint + { + SRCCOPY = 0x00CC0020, + SRCPAINT = 0x00EE0086, + SRCAND = 0x008800C6, + SRCINVERT = 0x00660046, + SRCERASE = 0x00440328, + NOTSRCCOPY = 0x00330008, + NOTSRCERASE = 0x001100A6, + MERGECOPY = 0x00C000CA, + MERGEPAINT = 0x00BB0226, + PATCOPY = 0x00F00021, + PATPAINT = 0x00FB0A09, + PATINVERT = 0x005A0049, + DSTINVERT = 0x00550009, + BLACKNESS = 0x00000042, + WHITENESS = 0x00FF0062, + CAPTUREBLT = 0x40000000 //only if WinVer >= 5.0.0 (see wingdi.h) + } + + [DllImport("gdi32.dll")] + internal static extern bool BitBlt(IntPtr hDestDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, TernaryRasterOperations dwRop); + + #endregion + + #region Helpers + + public static IntPtr GetWindowHwnd(Window window) + { + WindowInteropHelper windowHwnd = new WindowInteropHelper(window); + return windowHwnd.Handle; + } + + public static IntPtr GetMonitorFromWindow(IntPtr hwnd) + { + const int MONITOR_DEFAULTTONEAREST = 0x00000002; + + return NativeMethods.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + } + + #endregion + } + + internal class AuthorEx + { + internal enum CaptureDeviceType + { + InvalidCaptureDevice = 0x00000000, + VideoCaptureDevice = 0x00000001, + AudioCaptureDevice = 0x00000002, + AudioRenderDevice = 0x00000004, + AnyCaptureDevice = VideoCaptureDevice | AudioCaptureDevice, + AnyDevice = AnyCaptureDevice | AudioRenderDevice + } + + internal enum VideoFormat + { + UnknownVideoFormat = -1, + VideoFormatYuy2 = 0, + VideoFormatNv12 = 1, + VideoFormatRgb24 = 2, + VideoFormatRgb32 = 3, + }; + + [StructLayout(LayoutKind.Sequential)] + internal struct StreamRecordingStats + { + // Encoder stats + internal long llLastReceived; + internal long llLastEncoded; + internal long llLastProcessed; + internal ulong cReceived; + internal ulong cEncoded; + internal ulong cProcessed; + internal ulong cbProcessed; + internal uint cDiscontinuities; + // Pipeline stats + internal ulong cCaptured; + internal ulong cRecorded; + }; + + [Guid("BA1164FC-5183-45AF-AC16-05413E12972C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IDeviceCollection + { + [PreserveSig] + uint GetVersion(); + [PreserveSig] + uint GetCount(); + [PreserveSig] + int GetId(uint index, [MarshalAs(UnmanagedType.BStr)] out string id); + [PreserveSig] + int GetFriendlyName(uint index, [MarshalAs(UnmanagedType.BStr)] out string name); + [PreserveSig] + int GetScore(uint index, ref float score); + }; + + [Guid("14508A70-EAA4-429D-B7E9-1A20BA55544F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IDeviceEnumerator + { + [PreserveSig] + int Enumerate(CaptureDeviceType type); + [PreserveSig] + int GetDevices(CaptureDeviceType type, out IDeviceCollection devices); + } + + [Guid("6E1BDC8B-4569-4E02-B870-BA7FAFA5DCC7"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IDeviceEnumeratorCallback + { + void OnDeviceChange(); + } + + [Guid("CE1ABBC9-79D3-4CB1-B86E-C7C8FE26CF55"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ICaptureControl + { + [PreserveSig] + int StartCapture( + [MarshalAs(UnmanagedType.BStr)] string audioDeviceId, + bool voiceOptimized, + [MarshalAs(UnmanagedType.BStr)] string videoDeviceId, + int pixelCount, + bool preferHighElseLow, + float aspectRatio, + float frameRate + ); + [PreserveSig] + int StartRecording([MarshalAs(UnmanagedType.BStr)] string filename); + [PreserveSig] + int StartTranscodingFromUrl( + [MarshalAs(UnmanagedType.BStr)] string source, + [MarshalAs(UnmanagedType.BStr)] string filename, + bool removeVideo); + [PreserveSig] + int PauseRecording(); + [PreserveSig] + int ResumeRecording(); + [PreserveSig] + int StopRecording(); + [PreserveSig] + int StopCapture(); + [PreserveSig] + int GetRecordingStats( + out StreamRecordingStats audioStats, + out StreamRecordingStats videoStats + ); + }; + + [Guid("37FDFE04-DF59-4555-ABF0-616764D5835C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ICaptureVideoControl + { + [PreserveSig] + int SetPreviewLocation(int x, int y); + [PreserveSig] + int GetPreviewBackBufferNoRef(out IntPtr pSurface); + [PreserveSig] + int RenderPreview(); + }; + + [Guid("795B1614-71DD-464B-9B2C-809C978542E1"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ICaptureAudioControl + { + [PreserveSig] + int GetPeak(ref float peak); + [PreserveSig] + int GetVolume(ref bool mute, ref float level); + [PreserveSig] + int SetVolume(bool mute, float level); + }; + + [Guid("87524C71-CD9B-4327-96D2-302751A11F91"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ICaptureCallback + { + void OnInitialized + ( + uint uVideoFrameWidth, + uint uVideoFrameHeight, + int iStride, + VideoFormat format, + [MarshalAs(UnmanagedType.BStr)] string formatsInfo, + int voiceDspResult, + bool blankVideo + ); + void OnError(int hr); + void OnSampleTime(Int64 llRecordedFrameTime); + + } + + [Guid("E1D3A706-F753-44EC-BE3A-2559D969AE2F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IErrorInjection + { + [PreserveSig] + int ErrorCondition(int error); + } + + [Guid("B9260913-0335-4E66-8C70-0568293EAD50"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IMP4MediaTransfer + { + [PreserveSig] + int SetOutputFile([MarshalAs(UnmanagedType.BStr)] string outputFile); + [PreserveSig] + int SetSampleTimeOffset(long sampleTimeOffset); + [PreserveSig] + int LoadSourceMediaFile([MarshalAs(UnmanagedType.BStr)] string inputFile); + [PreserveSig] + int ReadSample(out long sampleTime, out long duration, out bool isVideoSample, out bool isVideoStreamEnded, out bool isAudioStreamEnded); + [PreserveSig] + int ProcessSample(IntPtr pImageBytes, int length); + [PreserveSig] + int EndWriting(); + [PreserveSig] + uint GetVideoWidth(); + [PreserveSig] + uint GetVideoHeight(); + [PreserveSig] + int ConfigureOutputMedia(uint videoWidth, uint videoHeight, uint frameRateHeight, uint frameRateLow); + [PreserveSig] + int AppendFrame(IntPtr pImageBytes, int length, long timeStamp, long duration, out bool isAudioStreamEnded); + }; + } + + internal class AuthorScreenCap + { + internal enum ScreenCaptureRecordingErrorType + { + NoError, + InputAudioDeviceDisconnected, // microphone disconnected + PlaybackAudioDeviceDisconnected, // speaker disconnected, + AudioServiceNotRunning, // Audio service not running + GenericAudioDeviceError, // Any errors with audio device + GenericVideoDeviceError, // Any errors with video device + EncodingError, //media encoding error + UnknownError, + + LastError // Please add any new errors before this + }; + + // Add ScreenCaptureRecordingErrorHResultBase + ScreenCaptureRecordingErrorType to compute the ScreenCapture recording error HRESULT + internal const int ScreenCaptureRecordingErrorHResultBase = -1634271232; // = (int)0x9e970000 = MAKE_HRESULT(0x01, 0x1e97, 0) + + [ComImport, Guid("EFEBD47E-056E-401D-AE28-803E7755DFD0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IScreenCaptureCallback + { + void OnLog([In, MarshalAs(UnmanagedType.BStr)] string logString); + void OnSampleTime(Int64 llRecordedFrameTime); + void OnComplete(int hr); + void OnRecordingError(ScreenCaptureRecordingErrorType recordingErrorType); + } + + [ComImport, Guid("58037FDA-69A8-47DD-84B0-95EC3AF7B7D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IScreenCaptureControl + { + [PreserveSig] + int Initialize + ( + [In]IScreenCaptureCallback callback, + [In, MarshalAs(UnmanagedType.BStr)]string filename, + int left, int right, + int top, int bottom, + bool showCursor, + [In, MarshalAs(UnmanagedType.BStr)]string audioInputDevice + ); + [PreserveSig] + int Start(); + [PreserveSig] + int Stop(); + } + + [ComImport, Guid("99205731-EE27-4ADA-A614-4A113AF8DB09")] + internal class ScreenCaptureClass + { + } + + } +} diff --git a/SnipInsight/Util/ProcessDpiAwareness.cs b/SnipInsight/Util/ProcessDpiAwareness.cs new file mode 100644 index 0000000..29c5eea --- /dev/null +++ b/SnipInsight/Util/ProcessDpiAwareness.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Util +{ + public enum ProcessDpiAwareness + { + DpiUnaware = 0, + SystemDpiAware = 1, + PerMonitorDpiAware = 2 + } +} diff --git a/SnipInsight/Util/RegistrySettings.cs b/SnipInsight/Util/RegistrySettings.cs new file mode 100644 index 0000000..76ee3d9 --- /dev/null +++ b/SnipInsight/Util/RegistrySettings.cs @@ -0,0 +1,889 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Microsoft.Win32; +using System; +using System.Text; +using System.Security.Cryptography; + +namespace SnipInsight.Util +{ + internal static class UserSettings + { + #region Properties + static readonly byte[] SAditionalEntropy = { 0,1,11,111,2,22,222}; + + const string _oldSettingsSubkey = @"Software\Microsoft\SnipInsight"; + + const RegistryHive _settingsHive = RegistryHive.CurrentUser; + const string _settingsSubkey = @"Software\Microsoft\Snip"; + + const string _requestIdName = @"RequestId"; + static Guid? _requestId = null; + + const string _appIdName = @"AppId"; + static string _appId = null; + + const string _fallbackEndpointsName = @"FallbackEndpoints"; + + // app version that showed a first run window. + const string _versionName = @"Version"; + static string _version= null; + + // set true if a user explicitly requests. + const string _disableFirstRunName = @"DisableFirstRun"; + static bool? _disableFirstRun = null; + + // set true if a user explicitly requests. + const string _disableEditorWindowTourName = @"DisableEditorWindowTour"; + static bool? _disableEditorWindowTour = null; + + static string _setupDownloadPath = null; + const string _setupDownloadPathName = @"SetupDownloadPath"; + + static string _appPath = null; + const string _appPathName = @"AppPath"; + + static bool? _allowProxyForAutoUpdate = null; + const string _allowProxyForAutoUpdateName = @"AllowProxyForAutoUpdate"; + + static string _mainWindowLocation = null; + const string _mainWindowLocationName = @"MainWindowLocation"; + + static string _screenCaptureShortcut = null; + const string _screenCaptureShortcutName = @"ScreenCaptureShortcut"; + + static string _quickCaptureShortcut = null; + const string _quickCaptureShortcutName = @"QuickCaptureShortcut"; + + static string _libraryShortcut = null; + const string _libraryShortcutName = @"LibraryShortcut"; + + static int? _screenCaptureDelay = 0; + const string _screenCaptureDelayName = @"ScreenCaptureDelay"; + + static bool? _disableRunWithWindows = null; + const string _disableRunWithWindowsName = @"DisableRunWithWindows"; + + static bool? _disableSysTrayBalloonAppStillRunning = null; + const string _disableSysTrayBalloonAppStillRunningName = @"DisableSysTrayBalloonAppStillRunning"; + + static bool? _disableToolWindow = null; + const string _disableToolWindowName = @"DisableToolWindow"; + + const string _disableKeyCombo = "None"; + + static bool? _autoUpdate = null; + const string _autoUpdateName = @"AutoUpdate"; + + static string _autoUpdateLogPath = null; + const string _autoUpdateLogPathName = @"AutoUpdateLogPath"; + + static bool? _isNotificationToastEnabled = null; + const string _isNotificationToastEnabledName = @"NotificationToastEnabled"; + + static string _customDirectory = null; + const string _customDirectoryName = @"CustomDirectory"; + + static bool? _isOpenEditorPostSnip = null; + const string _isOpenEditorPostSnipName = @"OpenEditorPostSnip"; + + static bool? _copyToClipboardAfterSnip = null; + const string _copyToClipboardAfterSnipName = @"CopyToClipboard"; + + static int? _contentModerationStrength = 0; + const string _contentModerationStrengthName = @"ContentModeration"; + + static bool? _isAIEnabled = null; + const string _isAIEnabledName = @"IsAIEnabled"; + + static bool? _isAutoTaggingEnabled = true; + const string _isAutoTaggingEnabledName = @"IsAutoTaggingEnabled"; + + static string _key = null; + #endregion + + #region Methods + static SettingsRegKey _settingsKey = null; + + static SettingsRegKey SettingsKey + { + get + { + if (_settingsKey == null) + { + try + { + _settingsKey = new SettingsRegKey(_settingsHive, _settingsSubkey); + } + catch (Exception e) + { + Diagnostics.LogTrace(string.Format("Unable to create SettingsRegKey object for {0}\\{1}", _settingsHive, _settingsSubkey)); + Diagnostics.LogLowPriException(e); + } + } + + return _settingsKey; + } + } + + internal static Guid RequestId + { + get + { + if ( _requestId != null) + { + return _requestId.Value; + } + + try + { + object regValue = SettingsRegKey.GetValue(SettingsKey, _requestIdName, null); + + Guid requestId; + if (regValue is string && Guid.TryParse((string)regValue, out requestId) && requestId != Guid.Empty) + { + // Request Id was read from registry + _requestId = requestId; + } + else + { + _requestId = Guid.NewGuid(); + SettingsRegKey.SetValue(SettingsKey, _requestIdName, _requestId.Value); + } + return _requestId.Value; + } + catch + { + if (_requestId != null) + { + return _requestId.Value; + } + else + { + return Guid.Empty; + } + } + } + } + + internal static string FallbackEndpoints + { + get + { + object regValue = SettingsRegKey.GetValue(SettingsKey, _fallbackEndpointsName, null); + return regValue as string; + } + } + + internal static string AppId + { + get + { + if (_appId != null) + { + return _appId; // Cache. + } + + try + { + object regValue = SettingsRegKey.GetValue(SettingsKey, _appIdName, null); + + var regString = regValue as string; + if (regString != null) + { + _appId = DecryptAppId(regString); + return _appId; + } + return null; // No Registry entry. + } + catch + { + return null; + } + } + set + { + _appId = value; + var protectedAppId = EncryptAppId(value); + SettingsRegKey.SetValue(SettingsKey, _appIdName, protectedAppId); + } + } + + internal static string OldAppId + { + get + { + try + { + var settings = new SettingsRegKey(_settingsHive, _oldSettingsSubkey); + object regValue = SettingsRegKey.GetValue(settings, _appIdName, null); + + var regString = regValue as string; + if (regString != null) + { + return DecryptAppId(regString); + } + return null; // No Registry entry. + } + catch + { + return null; + } + } + } + + /// + /// Convert protected string to clear string. Null if it cannot decrypt successfully. + /// + public static string DecryptAppId(string protectedString) + { + try + { + var protectedBytes = Convert.FromBase64String(protectedString); + var clearBytes = Unprotect(protectedBytes); + if (clearBytes != null) + { + var appId = Encoding.UTF8.GetString(clearBytes); + return appId; + } + } + catch (FormatException) + { + return null; // Treat as if protected string is not usable. + } + return null; + } + + public static string EncryptAppId(string appId) + { + var clearBytes = Encoding.UTF8.GetBytes(appId); + var protectedBytes = Protect(clearBytes); + if (protectedBytes != null) + { + var encryptedString = Convert.ToBase64String(protectedBytes); + return encryptedString; + } + return null; // Should not happen. + } + + internal static string Version + { + get + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, _versionName, ref _version); + } + set + { + SettingsRegKey.UpdateStringValue(SettingsKey, _versionName, ref _version, value); + } + } + + internal static bool DisableFirstRun + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _disableFirstRunName, ref _disableFirstRun); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _disableFirstRunName, ref _disableFirstRun, value); + } + } + + internal static bool DisableEditorWindowTour + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _disableEditorWindowTourName, ref _disableEditorWindowTour); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _disableEditorWindowTourName, ref _disableEditorWindowTour, value); + } + } + + internal static string SetupDownloadPath + { + get + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, _setupDownloadPathName, ref _setupDownloadPath); + } + set + { + SettingsRegKey.UpdateStringValue(SettingsKey, _setupDownloadPathName, ref _setupDownloadPath, value); + } + } + + internal static string AppPath + { + get + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, _appPathName, ref _appPath); + } + set + { + SettingsRegKey.UpdateStringValue(SettingsKey, _appPathName, ref _appPath, value); + } + } + + internal static bool AllowProxyForAutoUpdate + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _allowProxyForAutoUpdateName, ref _allowProxyForAutoUpdate); + } + } + + internal static string MainWindowLocation + { + get + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, _mainWindowLocationName, ref _mainWindowLocation); + } + set + { + SettingsRegKey.UpdateStringValue(SettingsKey, _mainWindowLocationName, ref _mainWindowLocation, value); + } + } + + internal static bool DisableRunWithWindows + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _disableRunWithWindowsName, ref _disableRunWithWindows); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _disableRunWithWindowsName, ref _disableRunWithWindows, value); + } + } + + internal static bool DisableSysTrayBalloonAppStillRunning + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _disableSysTrayBalloonAppStillRunningName, ref _disableSysTrayBalloonAppStillRunning); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _disableSysTrayBalloonAppStillRunningName, ref _disableSysTrayBalloonAppStillRunning, value); + } + } + + internal static bool DisableToolWindow + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _disableToolWindowName, ref _disableToolWindow); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _disableToolWindowName, ref _disableToolWindow, value); + } + } + + internal static int ScreenCaptureDelay + { + get + { + return SettingsRegKey.RetrieveNullableIntValue(SettingsKey, _screenCaptureDelayName, ref _screenCaptureDelay); + } + set + { + SettingsRegKey.UpdateNullableIntValue(SettingsKey, _screenCaptureDelayName, ref _screenCaptureDelay, value); + } + } + + internal static bool AutoUpdate + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _autoUpdateName, ref _autoUpdate, true); + } + } + + internal static string AutoUpdateLogPath + { + get + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, _autoUpdateLogPathName, ref _autoUpdateLogPath); + } + set + { + SettingsRegKey.UpdateStringValue(SettingsKey, _autoUpdateLogPathName, ref _autoUpdateLogPath, value); + } + } + + internal static bool IsNotificationToastEnabled + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, + _isNotificationToastEnabledName, ref _isNotificationToastEnabled, true); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _isNotificationToastEnabledName, ref _isNotificationToastEnabled, value); + } + } + + /// + /// Custom directory for the auto-save functionnality + /// + internal static string CustomDirectory + { + get + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, _customDirectoryName, ref _customDirectory); + } + set + { + SettingsRegKey.UpdateStringValue(SettingsKey, _customDirectoryName, ref _customDirectory, value); + } + } + + /// + /// Whether to open the editor after a snip or not + /// + internal static bool IsOpenEditorPostSnip + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, + _isOpenEditorPostSnipName, ref _isOpenEditorPostSnip, true); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _isOpenEditorPostSnipName, ref _isOpenEditorPostSnip, value); + } + } + + /// + /// Gets or sets the nullable bool value whether to copy to clipboad or not + /// + internal static bool CopyToClipboardAfterSnip + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _copyToClipboardAfterSnipName, ref _copyToClipboardAfterSnip); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _copyToClipboardAfterSnipName, ref _copyToClipboardAfterSnip, value); + } + } + + /// + /// Strength of moderation applied to prompt warning before sharing + /// + internal static int ContentModerationStrength + { + get + { + return SettingsRegKey.RetrieveNullableIntValue(SettingsKey, _contentModerationStrengthName, ref _contentModerationStrength); + } + set + { + SettingsRegKey.UpdateNullableIntValue(SettingsKey, _contentModerationStrengthName, ref _contentModerationStrength, value); + } + } + + /// + /// Enable/disable intelligent naming and add meta data to file save. + /// + /// True if enabled, false if disabled + internal static bool IsAutoTaggingEnabled + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _isAutoTaggingEnabledName, ref _isAutoTaggingEnabled); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _isAutoTaggingEnabledName, ref _isAutoTaggingEnabled, value); + } + } + + /// + /// Enable/disable all the features using AI services services. + /// + /// True if enabled, false if disabled + internal static bool IsAIEnabled + { + get + { + return SettingsRegKey.RetrieveNullableBoolValue(SettingsKey, _isAIEnabledName, ref _isAIEnabled, false); + } + set + { + SettingsRegKey.UpdateNullableBoolValue(SettingsKey, _isAIEnabledName, ref _isAIEnabled, value); + } + } + + /// + /// Fetch API keys for cognitive services. + /// + /// Name of the service, whose key is fetched + /// Key + internal static string GetKey(string keyName) + { + return SettingsRegKey.RetrieveStringValue(SettingsKey, keyName, ref _key); + } + + /// + /// Store the user entered API key. + /// + /// Name of the service, whose key is to be updated + /// Updated key to be stored and used + internal static void SetKey(string keyName, string keyValue) + { + SettingsRegKey.UpdateStringValue(SettingsKey, keyName, ref _key, keyValue); + } + #endregion + + #region Shortcuts + + #region ShortcutsHandler + public static KeyCombo ScreenCaptureShortcut + { + get + { + return RetrieveKeyComboValue(SettingsKey, _screenCaptureShortcutName, ref _screenCaptureShortcut); + } + set + { + UpdateKeyComboValue(SettingsKey, _screenCaptureShortcutName, ref _screenCaptureShortcut, value); + } + } + + /// + /// Property for the quick capture shortcut + /// + public static KeyCombo QuickCaptureShortcut + { + get + { + return RetrieveKeyComboValue(SettingsKey, _quickCaptureShortcutName, ref _quickCaptureShortcut); + } + + set + { + UpdateKeyComboValue(SettingsKey, _quickCaptureShortcutName, ref _quickCaptureShortcut, value); + } + } + + /// + /// Property for accessing the library panel + /// + public static KeyCombo LibraryShortcut + { + get + { + return RetrieveKeyComboValue(SettingsKey, _libraryShortcutName, ref _libraryShortcut); + } + + set + { + UpdateKeyComboValue(SettingsKey, _libraryShortcutName, ref _libraryShortcut, value); + } + } + #endregion + + private static KeyCombo RetrieveKeyComboValue(SettingsRegKey key, string valueName, ref string member) + { + string text = SettingsRegKey.RetrieveStringValue(key, valueName, ref member); + KeyCombo returnKey = null; + + // set default KeyCombo + if (string.IsNullOrWhiteSpace(text)) + { + if (valueName == _screenCaptureShortcutName) + { + returnKey = KeyCombo.ParseOrDefault("Snapshot"); + ScreenCaptureShortcut = returnKey; + } + else if (valueName == _quickCaptureShortcutName) + { + returnKey = KeyCombo.ParseOrDefault("None"); + QuickCaptureShortcut = returnKey; + } + else if (valueName == _libraryShortcutName) + { + returnKey = KeyCombo.ParseOrDefault("None"); + LibraryShortcut = returnKey; + } + } + else + { + returnKey = KeyCombo.ParseOrDefault(text); + } + + return returnKey; + } + + private static void UpdateKeyComboValue(SettingsRegKey key, string valueName, ref string member, KeyCombo value) + { + string text = _disableKeyCombo; + + if (value != null && value.IsValid) + { + text = value.ToString(); + } + + SettingsRegKey.UpdateStringValue(key, valueName, ref member, text); + } + + #endregion + + /// + /// Protect data for user. + /// + /// + public static byte[] Protect(byte[] data) + { + try + { + // Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted + // only by the same current user. + return ProtectedData.Protect(data, SAditionalEntropy, DataProtectionScope.CurrentUser); + } + catch (CryptographicException e) + { + Diagnostics.LogTrace("Unable to protect appid data"); + Diagnostics.LogLowPriException(e); + return null; + } + } + + /// + /// UnProtect data for user. + /// + /// + public static byte[] Unprotect(byte[] data) + { + try + { + //Decrypt the data using DataProtectionScope.CurrentUser. + return ProtectedData.Unprotect(data, SAditionalEntropy, DataProtectionScope.CurrentUser); + } + catch (CryptographicException e) + { + Diagnostics.LogTrace("Unable to unprotect appid data"); + Diagnostics.LogLowPriException(e); + return null; + } + } + } + + #region SettingReg + internal class SettingsRegKey + { + readonly RegistryHive _settingsHive = RegistryHive.CurrentUser; + readonly string _settingsSubkey = null; + + internal SettingsRegKey(RegistryHive settingsHive, string settingsSubkey) + { + _settingsHive = settingsHive; + _settingsSubkey = settingsSubkey; + } + + RegistryHive SettingsHive + { + get + { + return _settingsHive; + } + } + + RegistryKey SettingsKey + { + get + { + RegistryKey settingsKey = null; + + try + { + // Use the 64-bit view of the registry. This will open the 32-bit view on 32-bit machines + using (RegistryKey regRoot = RegistryKey.OpenBaseKey(_settingsHive, RegistryView.Registry64)) + { + if (_settingsHive == RegistryHive.LocalMachine) + { + settingsKey = regRoot.OpenSubKey(_settingsSubkey); + } + else // CurrentUser + { + settingsKey = regRoot.CreateSubKey(_settingsSubkey); // create key or open existing for write + } + } + } + catch (Exception e) + { + Diagnostics.LogTrace(string.Format("Unable to open {0} settings regkey", _settingsHive)); + Diagnostics.LogLowPriException(e); + } + + return settingsKey; + } + } + + internal static object GetValue(SettingsRegKey key, string valueName, object defaultValue) + { + if (key == null) + { + return defaultValue; + } + + using (RegistryKey regKey = key.SettingsKey) + { + return regKey == null ? defaultValue : regKey.GetValue(valueName, defaultValue); // HKLM key may not exist + } + } + + internal static void SetValue(SettingsRegKey key, string valueName, object value) + { + if (key == null) + { + return; + } + + if (key.SettingsHive == RegistryHive.LocalMachine) // prevent writes to HKLM registry + { + return; + } + + using (RegistryKey regKey = key.SettingsKey) + { + regKey.SetValue(valueName, value); + } + } + + internal static string RetrieveStringValue(SettingsRegKey key, string valueName, ref string member) + { + try + { + member = GetValue(key, valueName, null) as string; + if (member == null) + { + member = string.Empty; + SetValue(key, valueName, member); + } + } + catch + { + } + + if (member != null) + { + return member; + } + else + { + return string.Empty; + } + } + + internal static void UpdateStringValue(SettingsRegKey key, string valueName, ref string member, string value) + { + try + { + if (!string.Equals(member, value)) + { + member = value; + SetValue(key, valueName, value); + } + } + catch + { + } + } + + internal static bool RetrieveNullableBoolValue(SettingsRegKey key, string valueName, ref bool? member, bool defaultValue = false) + { + try + { + int? value = GetValue(key, valueName, null) as int?; + if (value == null) + { + member = defaultValue; + SetValue(key, valueName, Convert.ToUInt32(member)); + } + else + { + member = value.Value != 0; + } + } + catch + { + } + + if (member != null) + { + return member.Value; + } + else + { + return false; + } + } + + internal static void UpdateNullableBoolValue(SettingsRegKey key, string valueName, ref bool? member, bool value) + { + try + { + if (member == null || member.Value != value) + { + member = value; + SetValue(key, valueName, value ? 1 : 0); + } + } + catch + { + } + } + + internal static int RetrieveNullableIntValue(SettingsRegKey key, string valueName, ref int? member) + { + try + { + int? value = GetValue(key, valueName, null) as int?; + if (value == null) + { + member = 0; + SetValue(key, valueName, 0); + } + else + { + member = value.Value; + } + } + catch + { + } + + if (member != null) + { + return member.Value; + } + else + { + return 0; + } + } + + internal static void UpdateNullableIntValue(SettingsRegKey key, string valueName, ref int? member, int value) + { + try + { + if (member == null || member.Value != value) + { + member = value; + SetValue(key, valueName, value); + } + } + catch + { + } + } + } + #endregion +} diff --git a/SnipInsight/Util/StringToVisibility.cs b/SnipInsight/Util/StringToVisibility.cs new file mode 100644 index 0000000..4ea9cd0 --- /dev/null +++ b/SnipInsight/Util/StringToVisibility.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace SnipInsight.Util +{ + /// + /// Bind a string value to visibility attribute of UI elements + /// + class StringToVisibility : IValueConverter + { + /// + /// Convert string value to visibility + /// + /// String to be converted + /// + /// + /// + /// Visible if string contains value, collapsed otherwise + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is string && !string.IsNullOrEmpty((string)value) ? + Visibility.Visible : + Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/SnipInsight/Util/UnitConversion.cs b/SnipInsight/Util/UnitConversion.cs new file mode 100644 index 0000000..6fea27c --- /dev/null +++ b/SnipInsight/Util/UnitConversion.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +namespace SnipInsight.Util +{ + using System.Windows; + + public static class UnitConversion + { + /// + /// Convert to EMUs from pixels. + /// + static public Size ConvertToEMU(Size input) + { + double dx = 96; + PresentationSource source = PresentationSource.FromVisual(AppManager.TheBoss.MainWindow); + if (source != null) + { + dx = 96.0 * source.CompositionTarget.TransformToDevice.M11; + } + double width = (input.Width) * 12700; + double height = (input.Height) * 12700; + return new Size(width, height); + } + + /// + /// Convert to EMUs from pixels. + /// + static public Rect ConvertToEMU(Rect input) + { + double dx = 96; + PresentationSource source = PresentationSource.FromVisual(AppManager.TheBoss.MainWindow); + if (source != null) + { + dx = 96.0 * source.CompositionTarget.TransformToDevice.M11; + } + double x = (input.X) * 12700; + double y = (input.Y) * 12700; + double width = (input.Width) * 12700; + double height = (input.Height) * 12700; + return new Rect(x, y, width, height); + } + + /// + /// Convert to pixels from EMU's. + /// + static public Size ConvertToPixels(Size input) + { + double dx = 96; + PresentationSource source = PresentationSource.FromVisual(AppManager.TheBoss.MainWindow); + if (source != null) + { + dx = 96.0 * source.CompositionTarget.TransformToDevice.M11; + } + double width = (input.Width / 12700); + double height = (input.Height / 12700); + return new Size(width, height); + } + + /// + /// Convert to pixels from EMU's. + /// + static public Rect ConvertToPixels(Rect input) + { + double dx = 96; + PresentationSource source = PresentationSource.FromVisual(AppManager.TheBoss.MainWindow); + if (source != null) + { + dx = 96.0 * source.CompositionTarget.TransformToDevice.M11; + } + double x = (input.X / 12700); + double y = (input.Y / 12700); + double width = (input.Width / 12700); + double height = (input.Height / 12700); + return new Rect(x, y, width, height); + } + } +} diff --git a/SnipInsight/Util/Utils.cs b/SnipInsight/Util/Utils.cs new file mode 100644 index 0000000..7b6e76f --- /dev/null +++ b/SnipInsight/Util/Utils.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace SnipInsight.Util +{ + internal class Utils + { + internal static TimeSpan GetUserIdleTime() + { + NativeMethods.LASTINPUTINFO lii = new NativeMethods.LASTINPUTINFO(); + lii.cbSize = (uint)Marshal.SizeOf(lii); + + NativeMethods.GetLastInputInfo(ref lii); + ulong tickCount = NativeMethods.GetTickCount64(); + + return TimeSpan.FromMilliseconds(tickCount - lii.dwTime); + } + + // if UAC is enabled (default) and the process being queried is running as Built-in Administrator, this function will return 'false'. + // The BI Admin account is not technically elevated since it is always Admin, so technically the return value of 'false' is correct. + // Should not be a problem if current Windows user account is BI Admin since (likely) all processes then will be running as BI Admin. + // Could be a problem if the current Windows user account is not the BI Admin and we're checking a process that was 'Run-As' the BI Admin. + // In that case the current process elevation might be 'false' and the one running as BI Admin will also be 'false', + // but indeed there will be an elevation mismatch between the two. This fcn does not account for that specifically as it does not + // check the current Windows Identity + // But in practice, the unelevated process querying an elevated one should return the correct result of 'true' with the 'Access Denied' + // error handling in this fcn + internal static bool IsProcessRunningElevated(Process process) + { + IntPtr processToken = IntPtr.Zero; + IntPtr tokenInformationElevation = IntPtr.Zero; + + try + { + try + { + if (!NativeMethods.OpenProcessToken(process.Handle, NativeMethods.TOKEN_READ, out processToken)) + { + throw new ApplicationException("OpenProcessToken() failed", Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())); + } + } + catch (Exception ex) + { + if (ex is System.ComponentModel.Win32Exception && (uint)ex.HResult == 0x80004005) + { + // may get Access Denied if current process is not running elevated and tries to open an elevated process + Process currentProcess = Process.GetCurrentProcess(); + if (process != currentProcess) + { + if (!IsProcessRunningElevated(currentProcess)) // recursion should not re-enter this block when calling with the current process + { + return true; + } + } + } + + throw; + } + + uint tokenInfoLength = (uint)Marshal.SizeOf(typeof(NativeMethods.TokenElevation)); + tokenInformationElevation = Marshal.AllocHGlobal((int)tokenInfoLength); + + if (!NativeMethods.GetTokenInformation(processToken, NativeMethods.TokenInformationClass.TokenElevation, tokenInformationElevation, tokenInfoLength, out tokenInfoLength)) + { + throw new InvalidOperationException("GetTokenInformation() failed", Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())); + } + + NativeMethods.TokenElevation elevationStatus = (NativeMethods.TokenElevation)Marshal.PtrToStructure(tokenInformationElevation, typeof(NativeMethods.TokenElevation)); + + return elevationStatus.TokenIsElevated > 0; + } + finally + { + if (processToken != IntPtr.Zero) + { + NativeMethods.CloseHandle(processToken); + } + + if (tokenInformationElevation != IntPtr.Zero) + { + Marshal.FreeHGlobal(tokenInformationElevation); + } + } + } + } +} diff --git a/SnipInsight/Util/WinMessage.cs b/SnipInsight/Util/WinMessage.cs new file mode 100644 index 0000000..7477551 --- /dev/null +++ b/SnipInsight/Util/WinMessage.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Runtime.InteropServices; + +namespace SnipInsight.Util +{ + internal class WinMessage + { + const int MAINWINDOW_MIN_WIDTH = 1024; + const int MAINWINDOW_MIN_HEIGHT = 640; + + internal static System.IntPtr WindowProc( + System.IntPtr hwnd, + int msg, + System.IntPtr wParam, + System.IntPtr lParam, + ref bool handled) + { + switch (msg) + { + case 0x0024: + WmGetMinMaxInfo(hwnd, lParam); + handled = true; + break; + } + + return (System.IntPtr)0; + } + + private static void WmGetMinMaxInfo(System.IntPtr hwnd, System.IntPtr lParam) + { + + NativeMethods.MINMAXINFO mmi = (NativeMethods.MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(NativeMethods.MINMAXINFO)); + + // Adjust the maximized size and position to fit the work area of the correct monitor + int MONITOR_DEFAULTTONEAREST = 0x00000002; + System.IntPtr monitor = NativeMethods.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + + if (monitor != System.IntPtr.Zero) + { + NativeMethods.MONITORINFO monitorInfo = new NativeMethods.MONITORINFO(); + NativeMethods.GetMonitorInfo(monitor, monitorInfo); + NativeMethods.RECT rcWorkArea = monitorInfo.rcWork; + NativeMethods.RECT rcMonitorArea = monitorInfo.rcMonitor; + mmi.ptMaxPosition.x = Math.Abs(rcWorkArea.left - rcMonitorArea.left); + mmi.ptMaxPosition.y = Math.Abs(rcWorkArea.top - rcMonitorArea.top); + mmi.ptMaxSize.x = Math.Abs(rcWorkArea.right - rcWorkArea.left); + mmi.ptMaxSize.y = Math.Abs(rcWorkArea.bottom - rcWorkArea.top); + mmi.ptMinTrackSize.x = MAINWINDOW_MIN_WIDTH; + mmi.ptMinTrackSize.y = MAINWINDOW_MIN_HEIGHT; + } + + Marshal.StructureToPtr(mmi, lParam, true); + } + } +} diff --git a/SnipInsight/ViewModels/AIPanelViewModel.cs b/SnipInsight/ViewModels/AIPanelViewModel.cs new file mode 100644 index 0000000..9989593 --- /dev/null +++ b/SnipInsight/ViewModels/AIPanelViewModel.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using GalaSoft.MvvmLight; +using GalaSoft.MvvmLight.Command; +using SnipInsight.AIServices.AIViewModels; +using System.Windows; +using System.Windows.Media.Imaging; + +namespace SnipInsight.ViewModels +{ + public class AIPanelViewModel : ViewModelBase + { + public enum AiSelected + { + Suggested = 0, + ImageSearch, + ProductSearch, + PeopleSearch, + PlaceSearch, + Ocr + } + + /// + /// Current selected AI + /// + private AiSelected _currentAI = AiSelected.Suggested; + + public void ActivateButtons(AiSelected newSelection) + { + _currentAI = newSelection; + OCRCommand.RaiseCanExecuteChanged(); + SuggestedCommand.RaiseCanExecuteChanged(); + ImageSearchCommand.RaiseCanExecuteChanged(); + ProductSearchCommand.RaiseCanExecuteChanged(); + PeopleSearchCommand.RaiseCanExecuteChanged(); + PlaceSearchCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged("SuggestedInsightsVisible"); + RaisePropertyChanged("ImageToTextVisible"); + RaisePropertyChanged("SimilarImagesVisible"); + RaisePropertyChanged("ProductSearchVisible"); + RaisePropertyChanged("PeopleSearchVisible"); + RaisePropertyChanged("PlaceSearchVisible"); + ImageSearchVisibility = Visibility.Collapsed; + ProductSearchVisibility = Visibility.Collapsed; + PeopleSearchVisibility = Visibility.Collapsed; + OCRVisibility = Visibility.Collapsed; + PlaceSearchVisibility = Visibility.Collapsed; + AppManager.TheBoss.MainWindow.VerticalScrollViewer.ScrollToTop(); + } + + public AIPanelViewModel() + { + SuggestedCommand = new RelayCommand(SuggestedCommandExecute, SuggestedCommandCanExecute); + ImageSearchCommand = new RelayCommand(ImageSearchCommandExecute, ImageSearchCommandCanExecute); + ProductSearchCommand = new RelayCommand(ProductSearchCommandExecute, ProductSearchCommandCanExecute); + PeopleSearchCommand = new RelayCommand(PeopleSearchCommandExecute, PeopleSearchCommandCanExecute); + PlaceSearchCommand = new RelayCommand(PlaceSearchCommandExecute, PlaceSearchCommandCanExecute); + OCRCommand = new RelayCommand(OCRCommandExecute, OCRCommandCanExecute); + } + + #region Properties + public BitmapSource CapturedImage + { + get + { + return AppManager.TheBoss.ViewModel.CapturedImage; + } + set + { + AppManager.TheBoss.ViewModel.CapturedImage = value; + RaisePropertyChanged(); + } + } + + public bool SuggestedInsightsVisible + { + get { return _currentAI == AiSelected.Suggested; } + set + { + if(!SuggestedInsightsVisible) + { + _currentAI = AiSelected.Suggested; + SuggestedCommandExecute(); + } + } + } + + public bool ImageToTextVisible + { + get { return _currentAI == AiSelected.Ocr; } + set + { + if (!ImageToTextVisible) + { + _currentAI = AiSelected.Ocr; + OCRCommandExecute(); + } + } + } + public bool SimilarImagesVisible + { + get { return _currentAI == AiSelected.ImageSearch; } + set + { + if (!SimilarImagesVisible) + { + _currentAI = AiSelected.ImageSearch; + ImageSearchCommandExecute(); + } + } + } + public bool ProductSearchVisible + { + get { return _currentAI == AiSelected.ProductSearch; } + set + { + if (!ProductSearchVisible) + { + _currentAI = AiSelected.ImageSearch; + ProductSearchCommandExecute(); + } + } + } + public bool PeopleSearchVisible + { + get { return _currentAI == AiSelected.PeopleSearch; } + set + { + if (!PeopleSearchVisible) + { + _currentAI = AiSelected.ImageSearch; + PeopleSearchCommandExecute(); + } + } + } + public bool PlaceSearchVisible + { + get { return _currentAI == AiSelected.PlaceSearch; } + set + { + if (!PlaceSearchVisible) + { + _currentAI = AiSelected.ImageSearch; + PlaceSearchCommandExecute(); + } + } + } + + private Visibility _imageSearchVisibility = Visibility.Visible; + + public Visibility ImageSearchVisibility + { + get { return _imageSearchVisibility; } + set + { + _imageSearchVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _productSearchVisibility = Visibility.Visible; + + public Visibility ProductSearchVisibility + { + get { return _productSearchVisibility; } + set + { + _productSearchVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _oCRVisibility = Visibility.Visible; + + public Visibility OCRVisibility + { + get { return _oCRVisibility; } + set + { + _oCRVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _peopleSearchVisibility = Visibility.Visible; + + public Visibility PeopleSearchVisibility + { + get { return _peopleSearchVisibility; } + set + { + _peopleSearchVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _placeSearchVisibility = Visibility.Visible; + + public Visibility PlaceSearchVisibility + { + get { return _placeSearchVisibility; } + set + { + _placeSearchVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _loadingVisibility = Visibility.Visible; + + public Visibility LoadingVisibility + { + get => _loadingVisibility; + + set + { + _loadingVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _emptyStateVisibility = Visibility.Collapsed; + + public Visibility EmptyStateVisibility + { + get => _emptyStateVisibility; + + set + { + _emptyStateVisibility = value; + RaisePropertyChanged(); + } + } + + private Visibility _aiControlsVisibility = Visibility.Collapsed; + + public Visibility AIControlsVisibility + { + get => _aiControlsVisibility; + + set + { + _aiControlsVisibility = value; + RaisePropertyChanged(); + } + } + + public RelayCommand SuggestedCommand { get; set; } + + public RelayCommand ImageSearchCommand { get; set; } + + public RelayCommand ProductSearchCommand { get; set; } + + public RelayCommand PeopleSearchCommand { get; set; } + + public RelayCommand PlaceSearchCommand { get; set; } + #endregion + + #region Commands + + private bool SuggestedCommandCanExecute() + { + return _currentAI != AiSelected.Suggested; + } + + public void SuggestedCommandExecute() + { + ActivateButtons(AiSelected.Suggested); + ImageSearchVisibility = Visibility.Visible; + ProductSearchVisibility = Visibility.Visible; + PeopleSearchVisibility = Visibility.Visible; + OCRVisibility = Visibility.Visible; + PlaceSearchVisibility = Visibility.Visible; + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.SuggestedInsightsButton, Telemetry.ViewName.AiPanel); + } + + private bool ImageSearchCommandCanExecute() + { + return (_currentAI != AiSelected.ImageSearch) && + ServiceLocator.Current.GetInstance().IsVisible == Visibility.Visible; + + } + + public void ImageSearchCommandExecute() + { + ActivateButtons(AiSelected.ImageSearch); + ImageSearchVisibility = Visibility.Visible; + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.ImageSearchButton, Telemetry.ViewName.AiPanel); + } + + private bool ProductSearchCommandCanExecute() + { + return _currentAI != AiSelected.ProductSearch && + ServiceLocator.Current.GetInstance().IsVisible == Visibility.Visible; + } + + public void ProductSearchCommandExecute() + { + ActivateButtons(AiSelected.ProductSearch); + ProductSearchVisibility = Visibility.Visible; + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.ProductSearchButton, Telemetry.ViewName.AiPanel); + } + + private bool PeopleSearchCommandCanExecute() + { + return _currentAI != AiSelected.PeopleSearch && + ServiceLocator.Current.GetInstance().IsPeopleVisible == Visibility.Visible; + } + + public void PeopleSearchCommandExecute() + { + ActivateButtons(AiSelected.PeopleSearch); + PeopleSearchVisibility = Visibility.Visible; + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.PeopleSearchButton,Telemetry.ViewName.AiPanel); + } + + private bool PlaceSearchCommandCanExecute() + { + return _currentAI != AiSelected.PlaceSearch && + ServiceLocator.Current.GetInstance().IsPlaceVisible == Visibility.Visible; + } + + public void PlaceSearchCommandExecute() + { + ActivateButtons(AiSelected.PlaceSearch); + PlaceSearchVisibility = Visibility.Visible; + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.PlaceSearchButton, Telemetry.ViewName.AiPanel); + } + + public RelayCommand OCRCommand { get; set; } + + private bool OCRCommandCanExecute() + { + return _currentAI != AiSelected.Ocr && + ServiceLocator.Current.GetInstance().IsVisible == Visibility.Visible; + } + + public void OCRCommandExecute() + { + ActivateButtons(AiSelected.Ocr); + OCRVisibility = Visibility.Visible; + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.OCRButton, Telemetry.ViewName.AiPanel); + } + + #endregion + } +} diff --git a/SnipInsight/ViewModels/DelegateCommand.cs b/SnipInsight/ViewModels/DelegateCommand.cs new file mode 100644 index 0000000..d52c911 --- /dev/null +++ b/SnipInsight/ViewModels/DelegateCommand.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Windows.Input; + +namespace SnipInsight.ViewModels +{ + /// + /// This class allows delegating the commanding logic to methods passed as parameters, + /// and enables a View to bind commands to objects that are not part of the element tree. + /// + public class DelegateCommand : ICommand + { + #region Constructors + + /// + /// Constructor + /// + public DelegateCommand(Action executeMethod) + : this(executeMethod, null, false) + { + } + + /// + /// Constructor + /// + public DelegateCommand(Action executeMethod, Func canExecuteMethod) + : this(executeMethod, canExecuteMethod, false) + { + } + + /// + /// Constructor + /// + public DelegateCommand(Action executeMethod, Func canExecuteMethod, bool isAutomaticRequeryDisabled) + { + if (executeMethod == null) + { + throw new ArgumentNullException("executeMethod"); + } + + _executeMethod = executeMethod; + _canExecuteMethod = canExecuteMethod; + _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled; + } + + #endregion + + #region Public Methods + + /// + /// Method to determine if the command can be executed + /// + public bool CanExecute() + { + if (_canExecuteMethod != null) + { + return _canExecuteMethod(); + } + return true; + } + + /// + /// Execution of the command + /// + public void Execute() + { + if (_executeMethod != null) + { + _executeMethod(); + } + } + + /// + /// Property to enable or disable CommandManager's automatic requery on this command + /// + public bool IsAutomaticRequeryDisabled + { + get + { + return _isAutomaticRequeryDisabled; + } + set + { + if (_isAutomaticRequeryDisabled != value) + { + if (value) + { + CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers); + } + else + { + CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers); + } + _isAutomaticRequeryDisabled = value; + } + } + } + + /// + /// Raises the CanExecuteChaged event + /// + public void RaiseCanExecuteChanged() + { + OnCanExecuteChanged(); + } + + /// + /// Protected virtual method to raise CanExecuteChanged event + /// + protected virtual void OnCanExecuteChanged() + { + CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers); + } + + #endregion + + #region ICommand Members + + /// + /// ICommand.CanExecuteChanged implementation + /// + public event EventHandler CanExecuteChanged + { + add + { + if (!_isAutomaticRequeryDisabled) + { + CommandManager.RequerySuggested += value; + } + CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2); + } + remove + { + if (!_isAutomaticRequeryDisabled) + { + CommandManager.RequerySuggested -= value; + } + CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value); + } + } + + bool ICommand.CanExecute(object parameter) + { + return CanExecute(); + } + + void ICommand.Execute(object parameter) + { + Execute(); + } + + #endregion + + #region Data + + private readonly Action _executeMethod; + private readonly Func _canExecuteMethod; + private bool _isAutomaticRequeryDisabled; + private List _canExecuteChangedHandlers; + + #endregion + } + + /// + /// This class contains methods for the CommandManager that help avoid memory leaks by + /// using weak references. + /// + internal class CommandManagerHelper + { + internal static void CallWeakReferenceHandlers(List handlers) + { + if (handlers != null) + { + // Take a snapshot of the handlers before we call out to them since the handlers + // could cause the array to me modified while we are reading it. + + EventHandler[] callees = new EventHandler[handlers.Count]; + int count = 0; + + for (int i = handlers.Count - 1; i >= 0; i--) + { + WeakReference reference = handlers[i]; + EventHandler handler = reference.Target as EventHandler; + if (handler == null) + { + // Clean up old handlers that have been collected + handlers.RemoveAt(i); + } + else + { + callees[count] = handler; + count++; + } + } + + // Call the handlers that we snapshotted + for (int i = 0; i < count; i++) + { + EventHandler handler = callees[i]; + handler(null, EventArgs.Empty); + } + } + } + + internal static void AddHandlersToRequerySuggested(List handlers) + { + if (handlers != null) + { + foreach (WeakReference handlerRef in handlers) + { + EventHandler handler = handlerRef.Target as EventHandler; + if (handler != null) + { + CommandManager.RequerySuggested += handler; + } + } + } + } + + internal static void RemoveHandlersFromRequerySuggested(List handlers) + { + if (handlers != null) + { + foreach (WeakReference handlerRef in handlers) + { + EventHandler handler = handlerRef.Target as EventHandler; + if (handler != null) + { + CommandManager.RequerySuggested -= handler; + } + } + } + } + + internal static void AddWeakReferenceHandler(ref List handlers, EventHandler handler) + { + AddWeakReferenceHandler(ref handlers, handler, -1); + } + + internal static void AddWeakReferenceHandler(ref List handlers, EventHandler handler, int defaultListSize) + { + if (handlers == null) + { + handlers = (defaultListSize > 0 ? new List(defaultListSize) : new List()); + } + + handlers.Add(new WeakReference(handler)); + } + + internal static void RemoveWeakReferenceHandler(List handlers, EventHandler handler) + { + if (handlers != null) + { + for (int i = handlers.Count - 1; i >= 0; i--) + { + WeakReference reference = handlers[i]; + EventHandler existingHandler = reference.Target as EventHandler; + if ((existingHandler == null) || (existingHandler == handler)) + { + // Clean up old handlers that have been collected + // in addition to the handler that is to be removed. + handlers.RemoveAt(i); + } + } + } + } + } +} + diff --git a/SnipInsight/ViewModels/SnipInsightViewModel.cs b/SnipInsight/ViewModels/SnipInsightViewModel.cs new file mode 100644 index 0000000..a61319a --- /dev/null +++ b/SnipInsight/ViewModels/SnipInsightViewModel.cs @@ -0,0 +1,792 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Package; +using SnipInsight.Properties; +using SnipInsight.StateMachine; +using SnipInsight.Util; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace SnipInsight.ViewModels +{ + public enum Mode + { + Initializing, + Capturing, + Captured, + Recording, + PausedRecording, + Saving, + Stopped, + Playing, + Exporting + } + + // + // Since UI properties have dependencies on state properties, make + // sure that any changes keep these relationship consistent + // + + public sealed class SnipInsightViewModel : INotifyPropertyChanged + { + static SnipInsightViewModel() + { + // Ensure all static member variables are initialized at once + } + + internal SnipInsightViewModel(Dictionary actions) + { + StateMachine = new StateMachine.StateMachine(actions); + + CaptureCommand = StateMachine.CreateCommand(SnipInsightTrigger.CaptureScreen); + QuickCaptureCommand = StateMachine.CreateCommand(SnipInsightTrigger.QuickSnip); + PhotoCommand = StateMachine.CreateCommand(SnipInsightTrigger.CaptureCamera); + WhiteboardCommand = StateMachine.CreateCommand(SnipInsightTrigger.Whiteboard); + WhiteboardForCurrentWindowCommand = StateMachine.CreateCommand(SnipInsightTrigger.WhiteboardForCurrentWindow); + RecordCommand = StateMachine.CreateCommand(SnipInsightTrigger.Record); + PauseCommand = StateMachine.CreateCommand(SnipInsightTrigger.Pause); + StopCommand = StateMachine.CreateCommand(SnipInsightTrigger.Stop); + PlayCommand = StateMachine.CreateCommand(SnipInsightTrigger.TogglePlayStop); + ExitCommand = StateMachine.CreateCommand(SnipInsightTrigger.Exit); + ShareLinkCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShareLink); + ShareEmbedCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShareEmbed); + ShareEmailCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShareEmail); + ShareSendToOneNoteCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShareSendToOneNote); + SaveCommand = StateMachine.CreateCommand(SnipInsightTrigger.Save); + CopyCommand = StateMachine.CreateCommand(SnipInsightTrigger.Copy); + DeleteCommand = StateMachine.CreateCommand(SnipInsightTrigger.Delete); + ShowMainWindowCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShowMainWindow); // Tool Window used to show main window for lib. + ShowLibraryCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShowLibraryPanel); + ShowSettingsCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShowSettingsPanel); + MicrophoneOptionsCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShowMicrophoneOptions); + DeleteLibraryItemsCommand = new DelegateCommand(actions[ActionNames.DeleteLibraryItems]); + DoImageInsightsCommand = StateMachine.CreateCommand(SnipInsightTrigger.DoImageInsights); + ShowImageResultsWindowCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShowImageResultsWindow); + ShowAIPanelCommand = StateMachine.CreateCommand(SnipInsightTrigger.ShowAIPanel); + HideAIPanelCommand = StateMachine.CreateCommand(SnipInsightTrigger.HideAIPanel); + + CloseMainWindowCommand = new DelegateCommand(actions[ActionNames.CloseMainWindow]); // Hide + EditingWindowClosed Trigger + } + + public StateMachine.StateMachine StateMachine { get; private set; } + + #region INotifyPropertyChanged implementation + + public event PropertyChangedEventHandler PropertyChanged; + + void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChangedEventHandler handler = PropertyChanged; + if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + + #region Commands + public ICommand ShowMainWindowCommand { get; set; } + + public ICommand CloseMainWindowCommand { get; set; } + + public ICommand CaptureCommand { get; set; } + public ICommand PhotoCommand { get; set; } + public ICommand WhiteboardCommand { get; set; } + public ICommand WhiteboardForCurrentWindowCommand { get; set; } + public ICommand RecordCommand { get; set; } + public ICommand PauseCommand { get; set; } + public ICommand StopCommand { get; set; } + public ICommand PlayCommand { get; set; } + public ICommand ExitCommand { get; set; } + public ICommand EraserCommand { get; set; } + public ICommand EraseAllCommand { get; set; } + public ICommand UndoCommand { get; set; } + public ICommand SaveCommand { get; set; } + public ICommand ShareLinkCommand { get; set; } + public ICommand ShareEmbedCommand { get; set; } + public ICommand ShareEmailCommand { get; set; } + public ICommand ShareSendToOneNoteCommand { get; set; } + public ICommand CopyCommand { get; set; } + public ICommand RedoCommand { get; set; } + public ICommand DeleteCommand { get; set; } + public ICommand DeleteLibraryItemsCommand { get; set; } + + public ICommand ToggleLibraryCommand { get; set; } + + + + public ICommand ShowLibraryCommand { get; set; } + public ICommand MicrophoneOptionsCommand { get; set; } + + public ICommand ToggleSettingsCommand { get; set; } + public ICommand ShowSettingsCommand { get; set; } + + public ICommand QuickCaptureCommand { get; set; } + + public ICommand ShowImageResultsWindowCommand { get; set; } + + public ICommand DoImageInsightsCommand { get; set; } + + public ICommand ShowAIPanelCommand { get; set; } + public ICommand HideAIPanelCommand { get; set; } + public ICommand ToggleEditorCommand { get; set; } + public ICommand ToggleAIPanelCommand { get; set; } + public ICommand SaveImageCommand { get; set; } + public ICommand RestoreImageCommand { get; set; } + public ICommand CopyImageCommand { get; set; } + public ICommand ShareImageEmailCommand { get; set; } + public ICommand ShareImageSendToOneNoteCommand { get; set; } + public ICommand RefreshAICommand { get; set; } + #endregion + + #region Ink properties + + public DrawingAttributes InkDrawingAttributes + { + get + { + return _inkAttributes; + } + } + readonly DrawingAttributes _inkAttributes = new DrawingAttributes() + { + Color = Colors.Black, + IsHighlighter = false, + Width = 5, + Height = 5 + }; + + public InkCanvasEditingMode InkModeRequested + { + get + { + return _inkModeRequested; + } + set + { + // Always notify of a mode prop set + _inkModeRequested = value; + OnPropertyChanged(); + } + } + InkCanvasEditingMode _inkModeRequested = InkCanvasEditingMode.Ink; + + #endregion + + #region State properties + + public Mode Mode + { + get { return _mode; } + set + { + if (_mode != value) + { + _mode = value; + OnPropertyChanged(); + OnPropertyChanged("CaptureEnabled"); + OnPropertyChanged("EraserEnabled"); + OnPropertyChanged("RecordEnabled"); + OnPropertyChanged("PlayEnabled"); + OnPropertyChanged("RecordingInProgress"); + OnPropertyChanged("RecordingNotInProgress"); + OnPropertyChanged("RecordingOrPaused"); + OnPropertyChanged("DeviceSelectionEnabled"); + OnPropertyChanged("MicrophoneLevelEnabled"); + OnPropertyChanged("AcceptingInk"); + OnPropertyChanged("MainWindowTitle"); + } + } + } + Mode _mode = Mode.Initializing; + + public void SetRecordingTime(ulong timer) + { + // we only care about seconds granularity + timer /= 1000; + TimeSpan value = new TimeSpan((Int64)timer * TimeSpan.TicksPerSecond); + if (_recordingTime != value) + { + _recordingTime = value; + OnPropertyChanged("MainWindowTitle"); + } + } + TimeSpan _recordingTime; + + #endregion + + #region UI elements properties + BitmapSource _capturedImage; + BitmapSource _similarImage1; + BitmapSource _similarImage2; + BitmapSource _similarImage3; + BitmapSource _inkedImage; + String _ocrTextResults; + TextBox _ocrTextBox; + Canvas _celebritiesCanvas; + + Size _canvasSize = new Size(800, 480); + + public Size CanvasSize + { + get { return _canvasSize; } + private set + { + if (_canvasSize != value) + { + _canvasSize = value; + OnPropertyChanged(); + } + } + } + + /// + /// Captured image. + /// + public BitmapSource CapturedImage + { + get { return _capturedImage; } + set + { + if (!Equals(_capturedImage, value)) + { + _capturedImage = value; + if (_capturedImage != null) + { + var virtualPixelWidth = _capturedImage.PixelWidth / _capturedImage.DpiX * 96.0; + var virtualPixelHeight = _capturedImage.PixelHeight / _capturedImage.DpiY * 96.0; + CanvasSize = new Size(virtualPixelWidth, virtualPixelHeight); + } + + OnPropertyChanged(); + } + } + } + /// + /// Gets and sets first similar image + /// + public BitmapSource SimilarImage1 + { + get { return _similarImage1; } + set + { + if (!Equals(_similarImage1, value)) + { + _similarImage1 = value; + if (_similarImage1 != null) + { + var virtualPixelWidth = _similarImage1.PixelWidth / _similarImage1.DpiX * 96.0; + var virtualPixelHeight = _similarImage1.PixelHeight / _similarImage1.DpiY * 96.0; + CanvasSize = new Size(virtualPixelWidth, virtualPixelHeight); + } + + OnPropertyChanged(); + } + } + } + + /// + /// Gets and sets second similar image + /// + public BitmapSource SimilarImage2 + { + get { return _similarImage2; } + set + { + if (!Equals(_similarImage2, value)) + { + _similarImage2 = value; + if (_similarImage2 != null) + { + var virtualPixelWidth = _similarImage2.PixelWidth / _similarImage2.DpiX * 96.0; + var virtualPixelHeight = _similarImage2.PixelHeight / _similarImage2.DpiY * 96.0; + CanvasSize = new Size(virtualPixelWidth, virtualPixelHeight); + } + + OnPropertyChanged(); + } + } + } + + /// + /// Gets and sets third similar image + /// + public BitmapSource SimilarImage3 + { + get { return _similarImage3; } + set + { + if (!Equals(_similarImage3, value)) + { + _similarImage3 = value; + if (_similarImage3 != null) + { + var virtualPixelWidth = _similarImage3.PixelWidth / _similarImage3.DpiX * 96.0; + var virtualPixelHeight = _similarImage3.PixelHeight / _similarImage3.DpiY * 96.0; + CanvasSize = new Size(virtualPixelWidth, virtualPixelHeight); + } + + OnPropertyChanged(); + } + } + } + + /// + /// Gets and sets OCR text string + /// + public String OCRTextResults + { + get { return _ocrTextResults; } + set + { + if (!Equals(_ocrTextResults, value)) + { + _ocrTextResults = value; + OnPropertyChanged(); + } + } + } + + /// + /// Gets and sets OCR text box + /// + public TextBox OCRTextBox + { + get { return _ocrTextBox; } + set + { + if (!Equals(_ocrTextBox, value)) + { + _ocrTextBox = value; + OnPropertyChanged(); + } + } + } + + + public Canvas CelebritiesCanvas + { + get => _celebritiesCanvas; + + set + { + _celebritiesCanvas = value; + OnPropertyChanged(); + } + } + + + /// + /// Image capture with static ink overlayed. + /// + public BitmapSource InkedImage + { + get { return _inkedImage; } + set + { + _inkedImage = value; + } + } + + public string MainWindowTitle + { + get + { + switch (_mode) + { + case Mode.Recording: + case Mode.PausedRecording: + case Mode.Saving: + return _recordingTime.ToString("g"); + default: + return Resources.WindowTitle_Main; + } + } + } + + public bool CaptureEnabled + { + get { return _mode == Mode.Capturing; } + } + + + private bool _hasInk; + public bool HasInk + { + get { return _hasInk; } + set + { + if (_hasInk != value) + { + _hasInk = value; + OnPropertyChanged(); + OnPropertyChanged("CanShare"); + } + } + } + + public bool CanShare + { + get { return !IsWhiteboardImage || HasInk; } + } + + /// + /// Defines whether the editor button on the navbar is enabled + /// + private bool _editorEnable = false; + + public bool EditorEnable + { + get { return _editorEnable; } + set + { + _editorEnable = value; + OnPropertyChanged("EditorEnable"); + } + } + #endregion + + #region Saved Data Properties + // These need to be reset for a new capture. + public string SavedCaptureImage { get; set; } + + public string SavedInkedImage { get; set; } + + public string SavedSnipInsightFile { get; set; } + + private bool _isWhiteboardImage; + + public bool IsWhiteboardImage + { + get { return _isWhiteboardImage; } + set + { + if (_isWhiteboardImage != value) + { + _isWhiteboardImage = value; + OnPropertyChanged(); + OnPropertyChanged("CanShare"); + } + } + } + + #endregion + + #region Library Panel Properties + + private IList _selectedLibraryItemsList; + private int _selectedLibraryItemsCount; + private SnipInsightLink _selectedPackage; + + private ObservableCollection _packages = new ObservableCollection(); + + public ObservableCollection Packages + { + get { return _packages; } + } + + public SnipInsightLink SelectedPackage + { + get { return _selectedPackage; } + set + { + if (!Equals(_selectedPackage, value)) + { + _selectedPackage = value; + OnPropertyChanged("SelectedPackage"); + } + } + } + + internal IList SelectedLibraryItemsList + { + get { return _selectedLibraryItemsList; } + set + { + if (_selectedLibraryItemsList != value) + { + _selectedLibraryItemsList = value; + + OnPropertyChanged("SelectedLibraryItems"); + } + + SelectedLibraryItemsCount = value != null ? value.Count : 0; + } + } + + public IEnumerable SelectedLibraryItems + { + get + { + if (_selectedLibraryItemsList != null) + { + return _selectedLibraryItemsList.OfType(); + } + else + { + return Enumerable.Empty(); + } + } + } + + public int SelectedLibraryItemsCount + { + get { return _selectedLibraryItemsCount; } + private set + { + if (value != _selectedLibraryItemsCount) + { + bool hasSelectedItemsChanged = (value == 0 || _selectedLibraryItemsCount == 0); + + _selectedLibraryItemsCount = value; + + OnPropertyChanged("SelectedLibraryItemsCount"); + + if (hasSelectedItemsChanged) + { + OnPropertyChanged("HasSelectedLibraryItems"); + } + } + } + } + + public bool HasSelectedLibraryItems + { + get { return _selectedLibraryItemsCount != 0; } + } + + /// + /// Defines whether the library button on the navbar is enabled + /// + private bool _libraryEnable = true; + public bool LibraryEnable + { + get { return _libraryEnable; } + set + { + _libraryEnable = value; + if (value == true) + { + // Clear the selected items + SelectedLibraryItemsList = null; + } + + OnPropertyChanged("LibraryEnable"); + } + } + + #endregion + + #region Settings Panel Properties + + /// + /// Defines whether the settings button on the navbar is enabled + /// + private bool _settingsEnable = true; + + /// + /// Defines whether we open editor post snip + /// + private bool isOpenEditorPostSnip = UserSettings.IsOpenEditorPostSnip; + + /// + /// Defines wheter we copy to clipboard post snip + /// + private bool copyClipboardPostSnip = UserSettings.CopyToClipboardAfterSnip; + + /// + /// String for the app version + /// + private string appVersion = "Snip Insights " + Assembly.GetExecutingAssembly().GetName().Version.ToString(); + + public bool SettingsEnable + { + get { return _settingsEnable; } + set + { + _settingsEnable = value; + OnPropertyChanged("SettingsEnable"); + } + } + + /// + /// Defines whether we open editor post snip + /// + public bool IsOpenEditorPostSnip + { + get { return isOpenEditorPostSnip; } + set + { + isOpenEditorPostSnip = value; + OnPropertyChanged(); + } + } + + /// + /// Defines wheter we copy to clipboard post snip + /// + public bool CopyClipboardPostSnip + { + get { return copyClipboardPostSnip; } + set + { + copyClipboardPostSnip = value; + OnPropertyChanged(); + } + } + + public string AppVersion + { + get { return appVersion; } + set + { + appVersion = value; + OnPropertyChanged(); + } + } + + #endregion + + #region AI Panel Properties + + /// + /// Defines whether AI panel button is enabled + /// + private bool _aiEnable = false; + + /// + /// Defines if insights are visible + /// + private bool insightsVisible = UserSettings.IsAIEnabled; + + /// + /// Defines if the eraser button is checked + /// + private bool eraserChecked = false; + + /// + /// Defines if the highlighter button is checked + /// + private bool highlighterChecked = false; + + /// + /// Defines if the pen button is checked + /// + private bool penChecked = true; + + /// + /// Defines if the pen button is checked + /// + public bool penSelected = true; + + public bool AIEnable + { + get { return _aiEnable; } + set + { + _aiEnable = value; + OnPropertyChanged("AIEnable"); + } + } + + /// + /// Defines if insights are visible + /// + public bool InsightsVisible + { + get { return insightsVisible; } + set + { + insightsVisible = value; + OnPropertyChanged(); + } + } + + /// + /// Defines if the eraser button is checked + /// + public bool EraserChecked + { + get { return eraserChecked; } + set + { + eraserChecked = value; + penSelected = !eraserChecked; + if(eraserChecked) + { + AppManager.TheBoss.OnEraser(); + } + OnPropertyChanged(); + } + } + + /// + /// Defines if the highlighter button is checked + /// + public bool HighlighterChecked + { + get { return highlighterChecked; } + set + { + highlighterChecked = value; + penSelected = !highlighterChecked; + OnPropertyChanged(); + } + } + + /// + /// Defines if the pen button is checked + /// + public bool PenChecked + { + get { return penChecked; } + set + { + penChecked = value; + OnPropertyChanged(); + } + } + #endregion + + #region AI Properties + public List ImageTags { get; set; } + public string ImageCaption { get; set; } + public string SelectedImageUrl { get; set; } + + private string _restoreImageUrl = ""; + + /// + /// URL of image selected from AI + /// + public string RestoreImageUrl + { + get + { + return _restoreImageUrl; + } + set + { + // Always notify of a mode prop set + _restoreImageUrl = value; + OnPropertyChanged(); + } + } + + public bool AIAlreadyRan + { + get; + set; + } + #endregion + } +} diff --git a/SnipInsight/ViewModels/ViewModelLocator.cs b/SnipInsight/ViewModels/ViewModelLocator.cs new file mode 100644 index 0000000..63c9cba --- /dev/null +++ b/SnipInsight/ViewModels/ViewModelLocator.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CommonServiceLocator; +using GalaSoft.MvvmLight.Ioc; +using SnipInsight.AIServices.AIViewModels; + +namespace SnipInsight.ViewModels +{ + /// + /// This class contains static references to all the view models in the + /// application and provides an entry point for the bindings. + /// + public class ViewModelLocator + { + /// + /// Initializes a new instance of the ViewModelLocator class. + /// + public ViewModelLocator() + { + ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); + + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + } + + public ProductSearchViewModel ProductSearchLoc + { + get => ServiceLocator.Current.GetInstance(); + } + + public ImageSearchViewModel ImageSearchLoc + { + get => ServiceLocator.Current.GetInstance(); + } + + public AIPanelViewModel AIPanelLoc + { + get => ServiceLocator.Current.GetInstance(); + } + + public ImageAnalysisViewModel ImageAnalysisLoc + { + get => ServiceLocator.Current.GetInstance(); + } + + public OCRViewModel OCRLoc + { + get => ServiceLocator.Current.GetInstance(); + } + + public InsightsPermissionsViewModel InsightsPermissionsLoc + { + get => ServiceLocator.Current.GetInstance(); + } + + public static void Cleanup() + { + // TODO Clear the ViewModels + } + } +} + + +/* + In App.xaml: + + + + + In the View: + DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}" + + You can also use Blend to do all this with the tool's support. + See http://www.galasoft.ch/mvvm +*/ diff --git a/SnipInsight/Views/AcetateLayer.xaml b/SnipInsight/Views/AcetateLayer.xaml new file mode 100644 index 0000000..89c9c73 --- /dev/null +++ b/SnipInsight/Views/AcetateLayer.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/AcetateLayer.xaml.cs b/SnipInsight/Views/AcetateLayer.xaml.cs new file mode 100644 index 0000000..8e9b027 --- /dev/null +++ b/SnipInsight/Views/AcetateLayer.xaml.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Util; +using SnipInsight.ViewModels; +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media.Animation; + +namespace SnipInsight.Views +{ + // Interaction logic for Acetate Layer + public partial class AcetateLayer + { + Storyboard _animateAnts; + + public AcetateLayer() + { + InitializeComponent(); + Loaded += OnLoaded; + InkCanvas.StrokeCollected += InkCanvasOnStrokeCollected; + InkCanvas.StrokeErased += InkCanvasOnStrokeErased; + } + + private void InkCanvasOnStrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs inkCanvasStrokeCollectedEventArgs) + { + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model != null) + { + model.HasInk = true; + } + } + + private void InkCanvasOnStrokeErased(object sender, RoutedEventArgs routedEventArgs) + { + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model != null) + { + model.HasInk = HasInk(); + } + } + + void OnLoaded(object sender, RoutedEventArgs e) + { + _animateAnts = (Storyboard)this.TryFindResource("animateAnts"); + if (_animateAnts != null) + { + _animateAnts.Begin(); + _animateAnts.Pause(); + } + + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model != null) + { + model.PropertyChanged += ViewModelOnPropertyChanged; + } + } + + void ViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + try + { + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model == null) + { + return; + } + + switch (e.PropertyName) + { + case "Mode": + { + if (_animateAnts != null) + { + switch (model.Mode) + { + case Mode.Recording: + _animateAnts.Resume(); + break; + default: + _animateAnts.Pause(); + break; + } + } + } + break; + case "InkModeRequested": + InkCanvas.EditingMode = model.InkModeRequested; + break; + } + } + catch (Exception ex) + { + Diagnostics.ReportException(ex); + } + } + + internal bool HasInk() + { + return InkCanvas.HasInk(); + } + } +} diff --git a/SnipInsight/Views/ActionRibbon.xaml b/SnipInsight/Views/ActionRibbon.xaml new file mode 100644 index 0000000..e0871d5 --- /dev/null +++ b/SnipInsight/Views/ActionRibbon.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/ActionRibbon.xaml.cs b/SnipInsight/Views/ActionRibbon.xaml.cs new file mode 100644 index 0000000..cacf2be --- /dev/null +++ b/SnipInsight/Views/ActionRibbon.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for ActionRibbon.xaml + /// + public partial class ActionRibbon : UserControl + { + public ActionRibbon() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/Views/CapturedImage.xaml b/SnipInsight/Views/CapturedImage.xaml new file mode 100644 index 0000000..fb0ea49 --- /dev/null +++ b/SnipInsight/Views/CapturedImage.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/SnipInsight/Views/CapturedImage.xaml.cs b/SnipInsight/Views/CapturedImage.xaml.cs new file mode 100644 index 0000000..fd49a69 --- /dev/null +++ b/SnipInsight/Views/CapturedImage.xaml.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for CapturedImage.xaml + /// + public partial class CapturedImage : UserControl + { + public CapturedImage() + { + InitializeComponent(); + } + } +} diff --git a/SnipInsight/Views/ControlTreeEnumerator.cs b/SnipInsight/Views/ControlTreeEnumerator.cs new file mode 100644 index 0000000..9b76227 --- /dev/null +++ b/SnipInsight/Views/ControlTreeEnumerator.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; + +namespace SnipInsight.Views +{ + public static class ControlTreeIterator + { + //private static readonly MethodInfo UserControlContentPropertyGetMethod = typeof(UserControl).GetProperty("Content", BindingFlags.Instance | BindingFlags.NonPublic).GetGetMethod(true); + + public static T GetAncestorOfType(FrameworkElement control) + where T : FrameworkElement + { + FrameworkElement parent = control.Parent as FrameworkElement; + + while (parent != null) + { + if (parent is T) + return (T)parent; + + parent = parent.Parent as FrameworkElement; + } + + return null; + } + + public static UIElementCollection GetChildren(Panel control) + { + return control.Children; + } + + public static object GetContent(ContentControl control) + { + return control.Content; + } + + public static object GetContent(ContentPresenter control) + { + return control.Content; + } + + public static object GetContent(Popup control) + { + return control.Child; + } + + public static object GetContent(UserControl control) + { + //return UserControlContentPropertyGetMethod.Invoke(control, BindingFlags.Instance | BindingFlags.NonPublic, null, null, CultureInfo.CurrentCulture); + + // TODO: Can't find a way to get to the children of a UserControl... + return null; + } + + public static IEnumerable IterateChildren(object control) + { + if (!(control == null)) + { + if (control is Panel) + return IterateChildren((Panel)control); + else if (control is Canvas) + return IterateChildren((Canvas)control); + else if (control is ContentControl) + return IterateChildren((ContentControl)control); + else if (control is ContentPresenter) + return IterateChildren((ContentPresenter)control); + else if (control is Border) + return IterateChildren(((Border)control).Child); + else if (control is Popup) + { + IEnumerable children = IterateChildren((Popup)control); + return children; + } + } + + return null; + } + + public static IEnumerable IterateChildren(ContentControl control) + { + object content = GetContent(control); + + if (content != null) + yield return content; + } + + public static IEnumerable IterateChildren(ContentPresenter control) + { + object content = GetContent(control); + + if (content != null) + yield return content; + } + + public static IEnumerable IterateChildren(Panel control) + { + foreach (UIElement element in GetChildren(control)) + { + yield return element; + } + } + + public static IEnumerable IterateChildren(Popup control) + { + object content = GetContent(control); + + if (content != null) + yield return content; + } + + public static IEnumerable IterateChildren(UserControl control) + { + object content = GetContent(control); + + if (content != null) + yield return content; + } + + public static IEnumerable IterateChildrenOfType(object control) + { + foreach (object item in IterateChildren(control)) + { + if (item is T) + yield return (T)item; + } + } + + public static IEnumerable IterateSelfAndDescendants(object self) + { + return IterateSelfAndDescendants(self, false); + } + + public static IEnumerable IterateSelfAndDescendants(object self, bool depthFirst) + { + if (self != null) + { + if (!depthFirst) + yield return self; + + foreach (object item in IterateSelfAndDescendants(IterateChildren(self), depthFirst)) + yield return item; + + if (depthFirst) + yield return self; + } + } + + public static IEnumerable IterateSelfAndDescendants(IEnumerable items, bool depthFirst) + { + if (items != null) + { + foreach (object item in items) + { + if (!depthFirst) + yield return item; + + foreach (object child in IterateSelfAndDescendants(IterateChildren(item), depthFirst)) + yield return child; + + if (depthFirst) + yield return item; + } + } + } + + public static IEnumerable IterateSelfAndDescendantsOfType(object self) + { + return IterateSelfAndDescendantsOfType(self, false); + } + + public static IEnumerable IterateSelfAndDescendantsOfType(object self, bool depthFirst) + { + foreach (object item in IterateSelfAndDescendants(self, depthFirst)) + { + if (item is T) + yield return (T)item; + } + } + + public static bool SetFocusToFirstChild(object self) + { + foreach (object item in IterateSelfAndDescendants(self)) + { + if (item is Control && ((Control)item).IsTabStop == true) + { + ((Control)item).Focus(); + + return true; + } + } + + return false; + } + + public static void Visit(IEnumerable items, Action action) + { + foreach (T item in items) + { + action(item); + } + } + + public static void VisitSelfAndDescendants(object self, Action action) + { + Visit(IterateSelfAndDescendants(self), action); + } + + public static void VisitSelfAndDescendants(object self, bool depthFirst, Action action) + { + Visit(IterateSelfAndDescendants(self, depthFirst), action); + } + + public static void VisitSelfAndDescendants(IEnumerable items, bool depthFirst, Action action) + { + Visit(IterateSelfAndDescendants(items, depthFirst), action); + } + + public static void VisitSelfAndDescendantsOfType(object self, Action action) + { + Visit(IterateSelfAndDescendantsOfType(self), action); + } + + public static void VisitSelfAndDescendantsOfType(object self, bool depthFirst, Action action) + { + Visit(IterateSelfAndDescendantsOfType(self, depthFirst), action); + } + } +} diff --git a/SnipInsight/Views/DependencyPropertyUtilities.cs b/SnipInsight/Views/DependencyPropertyUtilities.cs new file mode 100644 index 0000000..413a009 --- /dev/null +++ b/SnipInsight/Views/DependencyPropertyUtilities.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; + +namespace SnipInsight.Views +{ + public static class DependencyPropertyUtilities + { + #region Register + + // + // Standard Register + // + + public static DependencyProperty Register(string name) + where TOBJ : DependencyObject + { + return DependencyProperty.Register(name, typeof(TPROP), typeof(TOBJ)); + } + + public static DependencyProperty Register(string name, TPROP defaultValue) + where TOBJ : DependencyObject + { + return DependencyProperty.Register(name, typeof(TPROP), typeof(TOBJ), CreatePropertyMetadata(defaultValue)); + } + + public static DependencyProperty Register(string name, TPROP defaultValue, PropertyChangedCallback propertyChangedCallback) + where TOBJ : DependencyObject + { + return DependencyProperty.Register(name, typeof(TPROP), typeof(TOBJ), CreatePropertyMetadata(defaultValue, propertyChangedCallback)); + } + + public static DependencyProperty Register(string name, TPROP defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback) + where TOBJ : DependencyObject + { + return DependencyProperty.Register(name, typeof(TPROP), typeof(TOBJ), CreatePropertyMetadata(defaultValue, propertyChangedCallback, coerceValueCallback)); + } + + // + // On Change with New Value Only + // + + public static DependencyProperty Register(string name, TPROP defaultValue, Action onChange) + where TOBJ : DependencyObject + { + return Register(name, defaultValue, CreatePropertyChangedCallback(onChange)); + } + + public static DependencyProperty Register(string name, TPROP defaultValue, Action onChange, Func coerceValue) + where TOBJ : DependencyObject + { + return Register(name, defaultValue, CreatePropertyChangedCallback(onChange), CreateCoerceValueCallback(coerceValue)); + } + + // + // On Change with New/Old Values + // + + public static DependencyProperty Register(string name, TPROP defaultValue, Action onChange) + where TOBJ : DependencyObject + { + return Register(name, defaultValue, CreatePropertyChangedCallback(onChange)); + } + + public static DependencyProperty Register(string name, TPROP defaultValue, Action onChange, Func coerceValue) + where TOBJ : DependencyObject + { + return Register(name, defaultValue, CreatePropertyChangedCallback(onChange), CreateCoerceValueCallback(coerceValue)); + } + + #endregion + + #region PropertyMetadata + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue) + { + return new PropertyMetadata(defaultValue); + } + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue, PropertyChangedCallback propertyChangedCallback) + { + return new PropertyMetadata(defaultValue, propertyChangedCallback); + } + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback) + { + return new PropertyMetadata(defaultValue, propertyChangedCallback, coerceValueCallback); + } + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue, Action onChange) + where TOBJ : DependencyObject + { + return new PropertyMetadata(defaultValue, CreatePropertyChangedCallback(onChange)); + } + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue, Action onChange) + where TOBJ : DependencyObject + { + return new PropertyMetadata(defaultValue, CreatePropertyChangedCallback(onChange)); + } + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue, Action onChange, Func coerceValue) + where TOBJ : DependencyObject + { + return new PropertyMetadata(defaultValue, CreatePropertyChangedCallback(onChange), CreateCoerceValueCallback(coerceValue)); + } + + public static PropertyMetadata CreatePropertyMetadata(TPROP defaultValue, Action onChange, Func coerceValue) + where TOBJ : DependencyObject + { + return new PropertyMetadata(defaultValue, CreatePropertyChangedCallback(onChange), CreateCoerceValueCallback(coerceValue)); + } + + #endregion + + #region PropertyChangedCallback + + public static PropertyChangedCallback CreatePropertyChangedCallback(Action onChange) + where TOBJ : DependencyObject + { + PropertyChangedCallback callback = null; + + if (onChange != null) + { + Action staticOnChange = (d, e) => + { + TOBJ obj = d as TOBJ; + + if (obj != null) + onChange(obj, (TPROP)e.NewValue); + }; + + callback = new PropertyChangedCallback(staticOnChange); + } + + return callback; + } + + public static PropertyChangedCallback CreatePropertyChangedCallback(Action onChange) + where TOBJ : DependencyObject + { + PropertyChangedCallback callback = null; + + if (onChange != null) + { + Action staticOnChange = (d, e) => + { + TOBJ obj = d as TOBJ; + + if (obj != null) + onChange(obj, (TPROP)e.NewValue, (TPROP)e.OldValue); + }; + + callback = new PropertyChangedCallback(staticOnChange); + } + + return callback; + } + + #endregion + + #region CoerceValueCallback + + public static CoerceValueCallback CreateCoerceValueCallback(Func coerceValue) + { + CoerceValueCallback callback = null; + + if (coerceValue != null) + { + callback = new CoerceValueCallback((o, p) => { return coerceValue((TPROP)p); }); + } + + return callback; + } + + public static CoerceValueCallback CreateCoerceValueCallback(Func coerceValue) + { + CoerceValueCallback callback = null; + + if (coerceValue != null) + { + callback = new CoerceValueCallback((o, p) => { return coerceValue(p); }); + } + + return callback; + } + + #endregion + } +} diff --git a/SnipInsight/Views/EditorSideNavigation.xaml b/SnipInsight/Views/EditorSideNavigation.xaml new file mode 100644 index 0000000..2b4e119 --- /dev/null +++ b/SnipInsight/Views/EditorSideNavigation.xaml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/EditorSideNavigation.xaml.cs b/SnipInsight/Views/EditorSideNavigation.xaml.cs new file mode 100644 index 0000000..e5c61b3 --- /dev/null +++ b/SnipInsight/Views/EditorSideNavigation.xaml.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using SnipInsight.Controls.Ariadne; +using SnipInsight.ViewModels; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for EditorSideNavigation.xaml + /// + public partial class EditorSideNavigation : UserControl + { + + private AriInkRadioButton lastCheckedInkButton = null; + private int lastCheckedPenSize; + + /// + /// Defines if the pen was already checked + /// + private bool isPenChecked = false; + + public EditorSideNavigation() + { + InitializeComponent(); + + lastCheckedPenSize = (int)AppManager.TheBoss.ViewModel.InkDrawingAttributes.Width; + HighlightPenSize(lastCheckedPenSize); + } + + private void ColorButton_Checked(object sender, RoutedEventArgs e) + { + AriInkRadioButton button = sender as AriInkRadioButton; + + if (button == null) + { + return; + } + + switch (button.Ink.ToString()) + { + case "Black": + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.BlackColorToggle, Telemetry.ViewName.EditorSideNavigation); + break; + case "Red": + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.RedColorToggle, Telemetry.ViewName.EditorSideNavigation); + break; + case "Orange": + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.YellowColorToggle, Telemetry.ViewName.EditorSideNavigation); + break; + case "Green": + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.GreenColorToggle, Telemetry.ViewName.EditorSideNavigation); + break; + case "Blue": + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.BlueColorToggle, Telemetry.ViewName.EditorSideNavigation); + break; + } + + lastCheckedInkButton = button; + + SetInkColor(button); + } + + private void SetInkColor(AriInkRadioButton button) + { + var brush = button.Ink as SolidColorBrush; + var model = DataContext as SnipInsightViewModel; + + if (brush == null) + { + //Diagnostics.LogTrace("Fail to set ink color. brush is null"); + return; + } + + if (model == null) + { + return; + } + + model.InkDrawingAttributes.Color = brush.Color; + model.InkDrawingAttributes.Width = lastCheckedPenSize; + model.InkDrawingAttributes.Height = lastCheckedPenSize; + model.InkDrawingAttributes.IsHighlighter = false; + model.InkDrawingAttributes.StylusTip = System.Windows.Ink.StylusTip.Ellipse; + model.InkModeRequested = InkCanvasEditingMode.Ink; + } + + private void PenSizeButton_Click(object sender, RoutedEventArgs e) + { + int ptSize = 3; + + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model == null) + { + return; + } + + if (sender == PenSize1Button) + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.PenSize1Button, Telemetry.ViewName.EditorSideNavigation); + ptSize = 1; + } + else if (sender == PenSize3Button) + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.PenSize3Button, Telemetry.ViewName.EditorSideNavigation); + ptSize = 3; + } + else if (sender == PenSize5Button) + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.PenSize5Button, Telemetry.ViewName.EditorSideNavigation); + ptSize = 5; + } + else if (sender == PenSize9Button) + { + Telemetry.ApplicationLogger.Instance.SubmitButtonClickEvent(Telemetry.EventName.PenSize9Button, Telemetry.ViewName.EditorSideNavigation); + ptSize = 9; + } + + if (ptSize != 0) + { + model.InkDrawingAttributes.Width = ptSize; + model.InkDrawingAttributes.Height = ptSize; + model.InkDrawingAttributes.IsHighlighter = false; + model.InkDrawingAttributes.StylusTip = System.Windows.Ink.StylusTip.Ellipse; + model.InkModeRequested = InkCanvasEditingMode.Ink; + lastCheckedPenSize = ptSize; + + HighlightPenSize(ptSize); + + //var currentInkButton = LeftBar.Children.OfType().FirstOrDefault(t => t.IsChecked.HasValue && t.IsChecked.Value); + //if (currentInkButton == null) + //{ + // lastCheckedInkButton.IsChecked = true; + //} + } + } + + private void HighlightPenSize(int ptSize) + { + HighlightPenSize(PenSize1Shape, ptSize == 1); + HighlightPenSize(PenSize3Shape, ptSize == 3); + HighlightPenSize(PenSize5Shape, ptSize == 5); + HighlightPenSize(PenSize9Shape, ptSize == 9); + } + + private readonly SolidColorBrush HighlightedPenSizeBrush = new SolidColorBrush(Color.FromArgb(255, 0, 0, 0)); + private readonly SolidColorBrush NormalPenSizeBrush = new SolidColorBrush(Color.FromArgb(255, 204, 204, 204)); + + private void HighlightPenSize(Shape shapeObject, bool isSelected) + { + shapeObject.Fill = isSelected ? HighlightedPenSizeBrush : NormalPenSizeBrush; + } + + private readonly Color[] highlighterColors = { new Color() { R = 255, G = 255, B = 0, A = 127 }, new Color() { R = 128, G = 255, B = 0, A = 127 }, new Color() { R = 255, G = 0, B = 255, A = 127 } }; + + private void PenButton_Click(object sender, RoutedEventArgs e) + { + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model == null) + { + return; + } + + if (model.penSelected) + { + PenPalettePopup.IsOpen = true; + } + else + { + model.penSelected = true; + SetInkColor(lastCheckedInkButton); + } + } + + private void Eraser_Click(object sender, RoutedEventArgs e) + { + AppManager.TheBoss.OnEraser(); + } + + /// + /// Event trigger to move the next element to the black color button if pen palette is open + /// + private void PenToggleLostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e) + { + if (PenPalettePopup.IsOpen) + { + BlackColorButton.Focus(); + } + } + + /// + /// Event trigger to move to the highlighter once we move past the pen sizes + /// + private void PenSizeLostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e) + { + HighlighterButton.Focus(); + } + + private void Highlighter_Checked(object sender, RoutedEventArgs e) + { + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model == null) + { + return; + } + + model.InkDrawingAttributes.Width = 6; + model.InkDrawingAttributes.Height = 20; + model.InkDrawingAttributes.IsHighlighter = true; + model.InkDrawingAttributes.Color = highlighterColors[0]; + model.InkDrawingAttributes.StylusTip = System.Windows.Ink.StylusTip.Rectangle; + model.InkModeRequested = InkCanvasEditingMode.Ink; + + AppManager.TheBoss.ResetEditorButtons(AppManager.EditorTools.Highlighter); + } + + private void PenButton_Check(object sender, RoutedEventArgs e) + { + SnipInsightViewModel model = DataContext as SnipInsightViewModel; + if (model == null) + { + return; + } + + SetInkColor(lastCheckedInkButton); + + AppManager.TheBoss.ResetEditorButtons(AppManager.EditorTools.Pen); + } + } +} diff --git a/SnipInsight/Views/EditorWindowTourPanel.xaml b/SnipInsight/Views/EditorWindowTourPanel.xaml new file mode 100644 index 0000000..5753985 --- /dev/null +++ b/SnipInsight/Views/EditorWindowTourPanel.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + Edit, learn and explore! + + + Interesting! + + + + + + + + You can always find your old snips here! + + + Got it! + + + + + + + + Input your keys here! + + + Let's begin... + + + + + + diff --git a/SnipInsight/Views/EditorWindowTourPanel.xaml.cs b/SnipInsight/Views/EditorWindowTourPanel.xaml.cs new file mode 100644 index 0000000..7e54247 --- /dev/null +++ b/SnipInsight/Views/EditorWindowTourPanel.xaml.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media.Animation; +using SnipInsight.Util; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for EditorWindowTourPanel.xaml + /// + public partial class EditorWindowTourPanel : UserControl + { + public event EventHandler Completed; + + public EditorWindowTourPanel() + { + InitializeComponent(); + } + + public void Start() + { + try + { + var s = (Storyboard)TryFindResource("ShowStoryboard") as Storyboard; + + if (s != null) + { + s.Begin(); + } + + Tip1.FadeIn(); + Tip1.AfterFadeIn += (sender, evt) => { Button1.Focus(); }; + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + OnError(); + } + } + + public void Stop() + { + BeginEndAnimation(); + } + + private void Button1_Click(object sender, RoutedEventArgs e) + { + try + { + Tip1.FadeOut(); + Tip2.AfterFadeIn += (s, evt) => { Button2.Focus(); }; + Tip2.FadeIn(); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + OnError(); + } + } + + private void Button2_Click(object sender, RoutedEventArgs e) + { + try + { + Tip2.FadeOut(); + Tip3.AfterFadeIn += (s, evt) => { Button3.Focus(); }; + Tip3.FadeIn(); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + OnError(); + } + } + + private void Button3_Click(object sender, RoutedEventArgs e) + { + try + { + Tip3.FadeOut(); + BeginEndAnimation(); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + OnError(); + } + } + + public void BeginEndAnimation() + { + var s = (Storyboard)TryFindResource("HideStoryboard") as Storyboard; + + if (s != null) + { + s.Completed += EndAnimationCompleted; + s.Begin(); + } + else + { + EndAnimationCompleted(null, null); + } + } + + private void EndAnimationCompleted(object sender, EventArgs e) + { + Finish(); + } + + private void Finish() + { + // Become invisible + Visibility = Visibility.Collapsed; + + RaiseCompleted(); + + RemoveSelfFromTheVisualTree(); + } + + private void RaiseCompleted() + { + if (Completed != null) + { + Completed(this, EventArgs.Empty); + } + } + + private void RemoveSelfFromTheVisualTree() + { + // Remove from the parent Panel (if possible) + if (Parent != null && Parent is Panel) + { + ((Panel)Parent).Children.Remove(this); + } + } + + private void OnError() + { + // In case of error, it's essential that we exit properly (even if it's not graceful)!!! + Finish(); + } + } +} diff --git a/SnipInsight/Views/FirstRunWindow.xaml b/SnipInsight/Views/FirstRunWindow.xaml new file mode 100644 index 0000000..b525db4 --- /dev/null +++ b/SnipInsight/Views/FirstRunWindow.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/FirstRunWindow.xaml.cs b/SnipInsight/Views/FirstRunWindow.xaml.cs new file mode 100644 index 0000000..442aa2b --- /dev/null +++ b/SnipInsight/Views/FirstRunWindow.xaml.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows.Threading; +using SnipInsight.Controls; +using SnipInsight.Controls.Ariadne; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for FirstRunWindow.xaml + /// + public partial class FirstRunWindow : DpiAwareWindow + { + readonly TimeSpan timerInterval = TimeSpan.FromSeconds(6); + DispatcherTimer timer; + + public FirstRunWindow() + { + InitializeComponent(); + } + + public void CloseWindow() + { + Close(); + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + timer = new DispatcherTimer(); + timer.Interval = timerInterval; + timer.Tick += timer_Tick; + + // MoveNext starts the timer + MoveNext(); + LayoutUtilities.PositionWindowOnPrimaryWorkingArea(this, HorizontalAlignment.Center, VerticalAlignment.Center); + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + // Prevent double close + this.Deactivated -= Window_Deactivated; + + Close(); + } + + void Window_Deactivated(object sender, EventArgs e) + { + // Removing the Soft Dismiss for now. We don't think this is necessarily helpful. + //Close(); + } + + private void Window_MouseDown(object sender, MouseButtonEventArgs e) + { + MoveNext(); + } + + void Window_MouseEnter(object sender, MouseEventArgs e) + { + timer.Stop(); + } + + void Window_MouseLeave(object sender, MouseEventArgs e) + { + timer.Start(); + } + + void timer_Tick(object sender, EventArgs e) + { + MoveNext(); + } + + #region Cards + + private int currentCardIndex = -1; + AriFirstRunCard currentCard = null; + Ellipse currentBall = null; + + private void MoveNext() + { + MoveTo((currentCardIndex + 1) % 4); + } + + private void MovePrevious() + { + MoveTo((currentCardIndex - 1) % 4); + } + + private void MoveTo(int index) + { + if (index == currentCardIndex) + { + return; + } + + switch (index) + { + case 0: + MoveTo(Card0, Ball0); + break; + case 1: + MoveTo(Card1, Ball1); + break; + case 2: + MoveTo(Card2, Ball2); + break; + case 3: + MoveTo(Card3, Ball3); + break; + default: + // if we've moved off the ends, we don't + // want to save the new position + return; + } + + currentCardIndex = index; + } + + private void MoveTo(AriFirstRunCard newCard, Ellipse newBall) + { + if (currentBall != null) + { + currentBall.Fill = new SolidColorBrush(Color.FromRgb(201, 201, 201)); + } + + if (currentCard != null) + { + currentCard.IsEnabled = false; + } + + newBall.Fill = new SolidColorBrush(Color.FromRgb(104, 98, 209)); + newCard.IsEnabled = true; + + currentCard = newCard; + currentBall = newBall; + timer.Stop(); + timer.Start(); + } + + private void Ball0_MouseDown(object sender, MouseButtonEventArgs e) + { + MoveTo(0); + e.Handled = true; + } + + private void Ball1_MouseDown(object sender, MouseButtonEventArgs e) + { + MoveTo(1); + e.Handled = true; + } + + private void Ball2_MouseDown(object sender, MouseButtonEventArgs e) + { + MoveTo(2); + e.Handled = true; + } + + private void Ball3_MouseDown(object sender, MouseButtonEventArgs e) + { + MoveTo(3); + e.Handled = true; + } + + #endregion + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + + // The Tool Window should never be null, but just to be extra safe... + if (AppManager.TheBoss.ToolWindow != null) + { + AppManager.TheBoss.ToolWindow.RestartOpenByTimer(); + } + } + + private void Window_KeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Space: + case Key.Enter: + case Key.Right: + case Key.Down: + case Key.PageDown: + MoveNext(); + e.Handled = true; + break; + case Key.Left: + case Key.Up: + case Key.PageUp: + MovePrevious(); + e.Handled = true; + break; + case Key.Escape: + Close(); + e.Handled = true; + break; + } + } + } +} diff --git a/SnipInsight/Views/HighContrastHelper.cs b/SnipInsight/Views/HighContrastHelper.cs new file mode 100644 index 0000000..7081bf4 --- /dev/null +++ b/SnipInsight/Views/HighContrastHelper.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.ComponentModel; +using System.Windows; + +namespace SnipInsight.Views +{ + public class HighContrastHelper : DependencyObject + { + #region Singleton pattern + + private HighContrastHelper() + { + SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged; + } + + private static HighContrastHelper _instance; + + public static HighContrastHelper Instance + { + get + { + if (_instance != null) return _instance; + _instance = new HighContrastHelper(); + return _instance; + } + } + + #endregion + + public void ApplyCurrentTheme() + { + if (!SystemParameters.HighContrast) return; + var windowbrush = SystemColors.WindowBrush; + + if (windowbrush.Color.R == 255 && windowbrush.Color.G == 255 && windowbrush.Color.B == 255) + { + HighContrastHelper.Instance.IsHighContrastLight = true; + } + else + { + HighContrastHelper.Instance.IsHighContrastLight = false; + } + } + + void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != "HighContrast") return; + HighContrastHelper.Instance.IsHighContrast = SystemParameters.HighContrast; + ApplyCurrentTheme(); + } + + #region DP IsHighContrast, IsHighContrastLight + + public static readonly DependencyProperty IsHighContrastProperty = DependencyProperty.Register( + "IsHighContrast", + typeof(bool), + typeof(HighContrastHelper), + new PropertyMetadata( + false + )); + + public bool IsHighContrast + { + get { return (bool)GetValue(IsHighContrastProperty); } + private set { SetValue(IsHighContrastProperty, value); } + } + + public static readonly DependencyProperty IsHighContrastLightProperty = DependencyProperty.Register( + "IsHighContrastLight", + typeof(bool), + typeof(HighContrastHelper), + new PropertyMetadata( + false + )); + + public bool IsHighContrastLight + { + get { return (bool)GetValue(IsHighContrastLightProperty); } + private set { SetValue(IsHighContrastLightProperty, value); } + } + + #endregion + + } +} diff --git a/SnipInsight/Views/KeyComboPicker.xaml b/SnipInsight/Views/KeyComboPicker.xaml new file mode 100644 index 0000000..31e4e49 --- /dev/null +++ b/SnipInsight/Views/KeyComboPicker.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/KeyComboPicker.xaml.cs b/SnipInsight/Views/KeyComboPicker.xaml.cs new file mode 100644 index 0000000..06436d7 --- /dev/null +++ b/SnipInsight/Views/KeyComboPicker.xaml.cs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using SnipInsight.Util; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for KeyCombinationPicker.xaml + /// + public partial class KeyComboPicker : UserControl + { + public KeyComboPicker() + { + InitializeComponent(); + } + + private void UserControl_Loaded(object sender, RoutedEventArgs e) + { + SetKeyComboText(); + } + + #region KeyCombo + + public event EventHandler KeyComboChanged; + + public KeyCombo KeyCombo + { + get { return GetValue(KeyComboProperty) as KeyCombo; } + set { SetValue(KeyComboProperty, value); } + } + + public static readonly DependencyProperty KeyComboProperty = + DependencyProperty.Register("KeyCombo", typeof(KeyCombo), typeof(KeyComboPicker), new PropertyMetadata(null, OnKeyComboChangedStatic)); + + protected virtual void OnKeyComboChanged(KeyCombo value) + { + SetKeyComboText(); + + if (KeyComboChanged != null) + { + KeyComboChanged(this, EventArgs.Empty); + } + } + + private static void OnKeyComboChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as KeyComboPicker; + + if (self != null) + { + self.OnKeyComboChanged(e.NewValue as KeyCombo); + } + } + + #endregion + + private KeyCombo _workingKeyCombo; + + private static readonly SolidColorBrush WorkingBrush = new SolidColorBrush(Color.FromArgb(255, 153, 153, 153)); + private static readonly SolidColorBrush ActiveBrush = new SolidColorBrush(SystemColors.MenuTextColor); + + private void UserControl_PreviewKeyDown(object sender, KeyEventArgs e) + { + Key key = e.Key; + e.Handled = true; + // If the System tried to capture the key and handle it, find out + // what the key originally was... + if (key == Key.System) + { + key = e.SystemKey; + } + + if (IsKeyRequiredForNavigation(key)) + { + e.Handled = false; + return; + } + + if (e.IsRepeat || !e.IsDown) + { + return; + } + +#if (DEBUG) + System.Diagnostics.Debug.WriteLine("PreviewKeyDown: " + e.Key.ToString() + ", " + e.SystemKey.ToString() + ", " + e.KeyStates.ToString()); +#endif + + MessageTextBlock.Text = ""; + + if (_workingKeyCombo == null) + { + _workingKeyCombo = new KeyCombo(); + } + + if (KeyCombo.IsCtrlKey(key)) + { + _workingKeyCombo.Ctrl = true; + SetKeyComboText(); + } + else if (KeyCombo.IsAltKey(key)) + { + _workingKeyCombo.Alt = true; + SetKeyComboText(); + } + else if (KeyCombo.IsShiftKey(key)) + { + _workingKeyCombo.Shift = true; + SetKeyComboText(); + } + else if (KeyCombo.IsWindowsKey(key)) + { + + } + else if (key == Key.Delete && !_workingKeyCombo.Shift && !_workingKeyCombo.Alt && !_workingKeyCombo.Ctrl) + { + // Allow Delete (by itself) to clear the existing combo + KeyCombo = null; + return; + } + else + { + // Try to set it and see if it works! + _workingKeyCombo.Key = key; + } + + HandleAfterKeyPressed(); + } + + private void UserControl_PreviewKeyUp(object sender, KeyEventArgs e) + { + var key = e.Key; + + e.Handled = true; + + // If the System tried to capture the key and handle it, find out + // what the key originally was... + if (key == Key.System) + { + key = e.SystemKey; + } + + if (IsKeyRequiredForNavigation(key)) + { + return; + } + + if (e.IsRepeat) + { + return; + } + +#if (DEBUG) + System.Diagnostics.Debug.WriteLine("PreviewKeyUp: " + e.Key.ToString() + ", " + e.SystemKey.ToString()); +#endif + + MessageTextBlock.Text = ""; + + if (_workingKeyCombo == null) + { + if (key == Key.PrintScreen) + { + // The Print Key is special. We need to watch + // KeyUp instead of just KeyDown + _workingKeyCombo = new KeyCombo(); + } + else + { + return; + } + } + + if (KeyCombo.IsCtrlKey(key)) + { + _workingKeyCombo.Ctrl = false; + SetKeyComboText(); + } + else if (KeyCombo.IsAltKey(key)) + { + _workingKeyCombo.Alt = false; + SetKeyComboText(); + } + else if (KeyCombo.IsShiftKey(key)) + { + _workingKeyCombo.Shift = false; + SetKeyComboText(); + } + else if (key == Key.PrintScreen) + { + // The KeyDown for PrintScreen seems to be consumed by + // Windows and we don't see it. Therefore, we need to + // treat PrintScreen as a special key that we handle on + // KeyUp. + _workingKeyCombo.Key = key; + HandleAfterKeyPressed(); + } + else if (_workingKeyCombo.Key == key) + { + _workingKeyCombo.Key = Key.None; + SetKeyComboText(); + } + } + + private void HandleAfterKeyPressed() + { + if (_workingKeyCombo.HasKey) + { + SetKeyComboText(); + + if (_workingKeyCombo.IsValid == false) + { + MessageTextBlock.Text = SnipInsight.Properties.Resources.KeyComboPicker_NotSupportCombination; + } + else + { + MessageTextBlock.Text = SnipInsight.Properties.Resources.KeyComboPicker_Updated; + KeyCombo = _workingKeyCombo.Clone(); + _workingKeyCombo.Key = Key.None; + } + } + } + + private bool IsKeyRequiredForNavigation(Key key) + { + switch (key) + { + case Key.Tab: + case Key.OemBackTab: + return true; + } + + return false; + } + + private void SetKeyComboText() + { + KeyCombo workingCombo = _workingKeyCombo; + KeyCombo activeCombo = KeyCombo; + + if (workingCombo != null && !workingCombo.IsEmpty) + { + if (activeCombo != null && activeCombo.Equals(workingCombo)) + { + KeyComboTextBox.Foreground = ActiveBrush; + } + else + { + KeyComboTextBox.Foreground = WorkingBrush; + } + KeyComboTextBox.Text = workingCombo.ToDescriptiveString(); + } + else + { + KeyComboTextBox.Foreground = ActiveBrush; + + string text = null; + + if (activeCombo != null) + { + text = activeCombo.ToDescriptiveString(); + } + + if (string.IsNullOrEmpty(text)) + { + text = "None"; + } + + KeyComboTextBox.Text = text; + } + } + + private void UserControl_LostFocus(object sender, RoutedEventArgs e) + { + MessageTextBlock.Text = ""; + _workingKeyCombo = null; + SetKeyComboText(); + } + + private void ClearButton_MouseDown(object sender, MouseButtonEventArgs e) + { + KeyCombo = null; + _workingKeyCombo = null; + SetKeyComboText(); + } + } +} diff --git a/SnipInsight/Views/LayoutUtilities.cs b/SnipInsight/Views/LayoutUtilities.cs new file mode 100644 index 0000000..5d2bfed --- /dev/null +++ b/SnipInsight/Views/LayoutUtilities.cs @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Windows; +using SnipInsight.Util; + +namespace SnipInsight.Views +{ + internal static class LayoutUtilities + { + #region Position Window + + public static void PositionWindowOnPrimaryWorkingArea(Window window, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) + { + PositionWindowOnScreen(window, System.Windows.Forms.Screen.PrimaryScreen, horizontalAlignment, verticalAlignment); + } + + public static void PositionWindowOnScreen(Window window, System.Windows.Forms.Screen screen, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) + { + var workingArea = screen.WorkingArea; + var dpiScale = DpiUtilities.GetSystemScale(); + + double workingLeft = workingArea.Left / dpiScale.X; + double workingWidth = workingArea.Width / dpiScale.X; + double workingTop = workingArea.Top / dpiScale.Y; + double workingHeight = workingArea.Height / dpiScale.Y; + + PositionWindow(window, workingLeft, workingTop, workingWidth, workingHeight, horizontalAlignment, verticalAlignment); + } + + public static void PositionWindow(Window window, double canvasLeft, double canvasTop, double canvasWidth, double canvasHeight, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) + { + window.Left = AlignHorizontally(canvasLeft, canvasWidth, window.Width, horizontalAlignment); + window.Top = AlignVertically(canvasTop, canvasHeight, window.Height, verticalAlignment); + } + + /// + /// Positions a window such that it is located entirely inside a specific canvas area. + /// + /// The window. + /// The canvas left. + /// The canvas top. + /// Width of the canvas. + /// Height of the canvas. + public static void PositionWindowInsideCanvas(Window window, double canvasLeft, double canvasTop, double canvasWidth, double canvasHeight) + { + double winLeft = window.Left; + double winTop = window.Top; + double winWidth = window.ActualWidth; + double winHeight = window.ActualHeight; + + if (winLeft + winWidth > canvasLeft + canvasWidth) + { + window.Left = AlignHorizontally(canvasLeft, canvasWidth, winWidth, HorizontalAlignment.Right); + } + + if (winTop + winHeight > canvasTop + canvasHeight) + { + window.Top = AlignVertically(canvasTop, canvasHeight, winHeight, VerticalAlignment.Bottom); + } + + if (winLeft < canvasLeft) + { + window.Left = AlignHorizontally(canvasLeft, canvasWidth, winWidth, HorizontalAlignment.Left); + } + + if (winTop < canvasTop) + { + window.Top = AlignVertically(canvasTop, canvasHeight, winHeight, VerticalAlignment.Top); + } + } + + public static void PositionWindowInsideWorkingArea(Window window) + { + var workingArea = GetWorkingAreaInSystemScale(window); + + PositionWindowInsideCanvas(window, workingArea.Item1, workingArea.Item2, workingArea.Item3, workingArea.Item4); + } + + #endregion + + #region Alignment + + public static double AlignVertically(double canvasTop, double canvasHeight, double objectHeight, VerticalAlignment alignment) + { + return AlignEdge(canvasTop, canvasHeight, objectHeight, (int)alignment); + } + + public static double AlignHorizontally(double canvasLeft, double canvasWidth, double objectWidth, HorizontalAlignment alignment) + { + return AlignEdge(canvasLeft, canvasWidth, objectWidth, (int)alignment); + } + + private static double AlignEdge(double canvasStartPosition, double canvasLength, double objectLength, int alignment) + { + switch (alignment) + { + case 0: // Left or Top + return canvasStartPosition; + case 2: // Right or Bottom; + return canvasStartPosition + canvasLength - objectLength; + default: // Center + return canvasStartPosition + ((canvasLength - objectLength)) / 2; + } + } + + #endregion + + #region Window Location Storage + + public static string SaveWindowLocationToString(Window window) + { + string result = null; + + if (window != null) + { + // We are rounding doubles to ints to simplify storage + + int left = (int)window.Left; + int top = (int)window.Top; + int width = (int)window.Width; + int height = (int)window.Height; + bool isMaximized = window.WindowState == WindowState.Maximized; + + result = left.ToString(CultureInfo.InvariantCulture) + + "," + top.ToString(CultureInfo.InvariantCulture) + + "," + width.ToString(CultureInfo.InvariantCulture) + + "," + height.ToString(CultureInfo.InvariantCulture) + + "," + (isMaximized ? "1" : "0"); + } + + return result; + } + + public static void RestoreWindowLocation(Window window, string location, bool includeSize = true) + { + if (string.IsNullOrWhiteSpace(location)) + return; + + string[] parts = location.Split(new char[] { ',' }); + + int? value; + + // Left + value = ParseStringPartAsInt(parts, 0); + if (value.HasValue) + { + window.Left = value.Value; + } + + // Top + value = ParseStringPartAsInt(parts, 1); + if (value.HasValue) + { + window.Top = value.Value; + } + + bool isMaximized = false; + + // IsMaximized + if (includeSize) + { + value = ParseStringPartAsInt(parts, 4); + if (value.HasValue) + { + isMaximized = value.Value != 0; + } + } + + // Width + if (includeSize && !isMaximized) + { + value = ParseStringPartAsInt(parts, 2); + if (value.HasValue) + { + window.Width = value.Value; + } + } + + // Height + if (includeSize && !isMaximized) + { + value = ParseStringPartAsInt(parts, 3); + if (value.HasValue) + { + window.Height = value.Value; + } + } + + // Ensure that we are really on a screen and that we aren't offscreen somewhere! + + SnipInsight.Views.LayoutUtilities.PositionWindowInsideWorkingArea(window); + + if (isMaximized) + { + window.WindowState = WindowState.Maximized; + } + else + { + if (window.WindowState == WindowState.Maximized) + { + window.WindowState = WindowState.Normal; + } + } + } + + private static int? ParseStringPartAsInt(string[] parts, int index) + { + if (index <= parts.Length - 1) + { + string part = parts[index]; + + int value; + + if (int.TryParse(part, out value)) + { + return value; + } + } + + return null; + } + + #endregion + + #region Screen + WorkingAreas + + /// + /// Gets the working area in virtual pixels (Left, Top, Width, Height). + /// + /// The window. + /// + public static Tuple GetWorkingAreaInVirtualPixels(Window window) + { + return GetWorkingAreaInVirtualPixels(window, GetDpiScale(window)); + } + + /// + /// Gets the working area in virtual pixels (Left, Top, Width, Height). + /// + /// The window. + /// + public static Tuple GetWorkingAreaInVirtualPixels(Window window, DpiScale dpiScale) + { + var workingArea = GetWorkingArea(window); + + return new Tuple(workingArea.Item1 / dpiScale.X, + workingArea.Item2 / dpiScale.Y, + workingArea.Item3 / dpiScale.X, + workingArea.Item4 / dpiScale.Y); + } + + public static Tuple GetWorkingAreaInSystemScale(Window window) + { + var dpiScale = DpiUtilities.GetSystemScale(); + var workingArea = GetWorkingArea(window); + + return new Tuple(workingArea.Item1 / dpiScale.X, + workingArea.Item2 / dpiScale.Y, + workingArea.Item3 / dpiScale.X, + workingArea.Item4 / dpiScale.Y); + } + + public static Tuple GetWorkingArea(Window window) + { + return GetWorkingArea(NativeMethods.GetWindowHwnd(window)); + } + + public static Tuple GetWorkingArea(IntPtr hwnd) + { + return GetWorkingAreaForMonitor(NativeMethods.GetMonitorFromWindow(hwnd)); + } + + public static Tuple GetWorkingAreaForMonitor(IntPtr monitor) + { + NativeMethods.MONITORINFO monitorInfo = new NativeMethods.MONITORINFO(); + NativeMethods.GetMonitorInfo(monitor, monitorInfo); + NativeMethods.RECT rcWorkArea = monitorInfo.rcWork; + NativeMethods.RECT rcMonitorArea = monitorInfo.rcMonitor; + + return new Tuple(rcWorkArea.left, rcWorkArea.top, rcWorkArea.width, rcWorkArea.height); + } + + private static System.Windows.Forms.Screen GetScreen(Window window) + { + IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle; + + return System.Windows.Forms.Screen.FromHandle(hwnd); + } + + #endregion + + #region DPI + + /// + /// Returns DPI (X, Y) + /// + /// + /// + public static DpiScale GetDpiScale(Window window) + { + return DpiUtilities.GetVirtualPixelScale(window); + } + + #endregion + + #region Helpers + + private static int DoubleToInt32(double value) + { + return (int)Math.Ceiling(value); + } + + #endregion + } +} diff --git a/SnipInsight/Views/LibraryPanel.xaml b/SnipInsight/Views/LibraryPanel.xaml new file mode 100644 index 0000000..014237a --- /dev/null +++ b/SnipInsight/Views/LibraryPanel.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/LibraryPanel.xaml.cs b/SnipInsight/Views/LibraryPanel.xaml.cs new file mode 100644 index 0000000..ed77351 --- /dev/null +++ b/SnipInsight/Views/LibraryPanel.xaml.cs @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Package; +using SnipInsight.Util; +using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for LibraryPanel.xaml + /// + public partial class LibraryPanel : UserControl + { + public LibraryPanel() + { + InitializeComponent(); + } + + #region PackagesSource + + public static readonly DependencyProperty PackagesSourceProperty = + DependencyProperty.Register("PackagesSource", typeof(ObservableCollection), typeof(LibraryPanel), new PropertyMetadata(null, OnPackagesSourceChangedStatic)); + + public ObservableCollection PackagesSource + { + get { return GetValue(PackagesSourceProperty) as ObservableCollection; } + set { SetValue(PackagesSourceProperty, value); } + } + + protected virtual void OnPackagesSourceChanged(ObservableCollection packagesSource) + { + BuildPackagesCollectionView(packagesSource); + } + + private static void OnPackagesSourceChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as LibraryPanel; + + if (self != null) + { + self.OnPackagesSourceChanged(e.NewValue as ObservableCollection); + } + } + + #endregion + + #region Packages View + + private ListCollectionView _packagesView; + + private void BuildPackagesCollectionView(ObservableCollection packagesSource) + { + if (packagesSource == null) + { + LibraryListView.ItemsSource = null; + return; + } + + LibraryListView.ItemsSource = packagesSource; + + ListCollectionView view = CollectionViewSource.GetDefaultView(LibraryListView.ItemsSource) as ListCollectionView; + + // Sorting + view.CustomSort = new SnipInsightLinkTimeSorter(); + + // Filtering + ApplyFilters(view, FilterSinceDate); + + // Grouping + view.GroupDescriptions.Clear(); + view.GroupDescriptions.Add(new PropertyGroupDescription("TimeGroupingLabel")); + + _packagesView = view; + + //LibraryListView.ItemsSource = _packagesView; + } + + #endregion + + #region Filters + + public static readonly DependencyProperty FilterSinceDateProperty = + DependencyProperty.Register("FilterSinceDate", typeof(Nullable), typeof(LibraryPanel), new PropertyMetadata(null, OnFilterSinceDateChangedStatic)); + + public Nullable FilterSinceDate + { + get { return (Nullable)GetValue(FilterSinceDateProperty); } + set { SetValue(FilterSinceDateProperty, value); } + } + + protected virtual void OnFilterSinceDateChanged(Nullable sinceDate) + { + ApplyFilters(_packagesView, sinceDate); + } + + private static void OnFilterSinceDateChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as LibraryPanel; + + if (self != null) + { + self.OnFilterSinceDateChanged((Nullable)e.NewValue); + } + } + + private void ApplyFilters(ListCollectionView view, Nullable dateTime) + { + if (view != null) + { + if (dateTime.HasValue) + { + DateTime filterValue = dateTime.Value; + + view.Filter = (x) => (x as SnipInsightLink).LastWriteTime >= filterValue; + } + else + { + view.Filter = null; + } + } + } + + #endregion + + #region Delete + + private bool _isDeleting; + + private async void DeleteButton_OnLeftMouseDown(object sender, MouseButtonEventArgs e) + { + try + { + var lb = sender as FrameworkElement; + + if (lb == null) + { + return; + } + + var model = lb.DataContext as SnipInsightLink; + + if (model == null || model.DeletionPending) + { + return; + } + + await HandleDelete(model); + } + catch (Exception ex) + { + System.Diagnostics.Debug.Fail("There was an exception when calling DeleteButton_OnLeftMouseDown. Ex Message = ", ex.ToString()); + Diagnostics.LogException(ex); + } + } + + private async Task HandleDelete(SnipInsightLink item) + { + if (item != null) + { + await HandleDelete(new SnipInsightLink[] { item }); + } + } + + private async Task HandleDelete(IEnumerable items) + { + try + { + if (_isDeleting) + { + // Prevent re-entry + return; + } + + _isDeleting = true; + await AppManager.TheBoss.DeleteAsync(items, true, true); + } + finally + { + _isDeleting = false; + } + } + + #endregion + + private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left && e.ClickCount == 2) + { + OpenSelectedPackage(); + e.Handled = true; + } + } + + private void OpenSelectedPackage() + { + var item = LibraryListView.SelectedItem as SnipInsightLink; + + if (item != null) + { + AppManager.TheBoss.ViewModel.SelectedPackage = item; + AppManager.TheBoss.ViewModel.RestoreImageUrl = string.Empty; + } + + AppManager.TheBoss.ViewModel.ToggleEditorCommand.Execute(null); + } + + private void LibraryListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + AppManager.TheBoss.ViewModel.SelectedLibraryItemsList = LibraryListView.SelectedItems; + } + + private void LibraryListView_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter || e.Key == Key.Return) + { + OpenSelectedPackage(); + e.Handled = true; + } + } + + internal void SetInitialFocus() + { + LibraryListView.Focus(); + } + } +} diff --git a/SnipInsight/Views/MainWindow.xaml b/SnipInsight/Views/MainWindow.xaml new file mode 100644 index 0000000..272a3d0 --- /dev/null +++ b/SnipInsight/Views/MainWindow.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/Views/MainWindow.xaml.cs b/SnipInsight/Views/MainWindow.xaml.cs new file mode 100644 index 0000000..08ca95b --- /dev/null +++ b/SnipInsight/Views/MainWindow.xaml.cs @@ -0,0 +1,436 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.Controls.Ariadne; +using SnipInsight.Util; +using System; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : AriModernWindow + { + readonly AppManager _manager; + TimeSpan _lastCompositionTargetRender; + TimeSpan _lastCompositionTargetSlowFreq; + const double c_compositionTargetSlowPeriod = 193; // slow frequency updates, in milliseconds + const int destructionTimerTimeinMins = 2; + + public MainWindow() + { + _manager = AppManager.TheBoss; + InitializeComponent(); + DataContext = _manager.ViewModel; + + this.Loaded += OnLoaded; + + InputBindings.Add(new KeyBinding( + AppManager.TheBoss.ViewModel.UndoCommand, + new KeyGesture(Key.Z, ModifierKeys.Control))); + InputBindings.Add(new KeyBinding( + AppManager.TheBoss.ViewModel.RedoCommand, + new KeyGesture(Key.Y, ModifierKeys.Control))); + } + + internal void OnLoaded(object sender, RoutedEventArgs e) + { + LayoutUtilities.RestoreWindowLocation(this, UserSettings.MainWindowLocation); + + // Both acetate layer and media capture should use the same Timer instance. The timer instancce is global in app manager and never destroyed. + CompositionTarget.Rendering += OnRendering; + this.Loaded -= OnLoaded; + } + + internal AcetateLayer AcetateLayer { get { return acetateLayer; } } + + public bool MainWindowClosedBySystem { get; set; } + + void OnRendering(object sender, EventArgs e) + { + try + { + if (!IsLoaded) // We register only in OnLoaded. No harm to check as well. + { + return; + } + + RenderingEventArgs args = (RenderingEventArgs)e; + // We may be called back twice for the same frame + if (_lastCompositionTargetRender == args.RenderingTime) + { + return; + } + _lastCompositionTargetRender = args.RenderingTime; + + // Low frequency updates + if ((_lastCompositionTargetRender - _lastCompositionTargetSlowFreq).TotalMilliseconds > c_compositionTargetSlowPeriod) + { + _lastCompositionTargetSlowFreq = _lastCompositionTargetRender; + } + } + catch (Exception ex) + { + Diagnostics.LogLowPriException(ex); + } + } + + private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) + { + System.Diagnostics.Process.Start(e.Uri.ToString()); + } + + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + UserSettings.MainWindowLocation = LayoutUtilities.SaveWindowLocationToString(this); + + if (!MainWindowClosedBySystem) + { + e.Cancel = true; + AppManager.TheBoss.CloseMainWindow(); + } + } + private void MainWindow_OnClosed(object sender, EventArgs e) + { + CompositionTarget.Rendering -= OnRendering; // No harm if we never registered (loaded) + } + + #region Window Resizing + + private readonly double ThresholdToMaximize = .9; // If we are filling more than this percentage, just maximize. + + private void OptimizeSizeAndPosition(double contentWidth, double contentHeight) + { + if (WindowState == WindowState.Maximized) + { + // If Maximized, don't change... + return; + } + + // + // Estimate Chrome requirements + // + + double estimatedChromeHeight = CaptionBarHeight + + contentGrid.Margin.Top + contentGrid.Margin.Bottom + + 12.0; // A little buffer for padding and to be safe + + double estimatedChromeWidth = contentGrid.Margin.Left + + contentGrid.Margin.Right + + 8.0; // A little padding for the sides and to be safe + + // + // Estimate Window dimensions required for unscaled content + // + + double estimatedWindowWidth = contentWidth + estimatedChromeWidth; + double estimatedWindowHeight = contentHeight + estimatedChromeHeight; + + // + // Decide the best window size given the content and available working area + // + + var workingArea = LayoutUtilities.GetWorkingAreaInVirtualPixels(this); + + if ((estimatedWindowWidth > workingArea.Item3 * ThresholdToMaximize) + || (estimatedWindowHeight > workingArea.Item4 * ThresholdToMaximize)) + { + WindowState = WindowState.Maximized; + } + else + { + if (estimatedWindowHeight <= this.Height && estimatedWindowWidth <= this.Width) + { + // Just quit if the window size isn't increasing... + return; + } + + if (Application.Current != null) + { + Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, + new Action(delegate () + { + WindowState = WindowState.Normal; + + if (estimatedWindowHeight > this.Height) + { + this.Height = estimatedWindowHeight; + } + + if (estimatedWindowWidth > this.Width) + { + this.Width = estimatedWindowWidth; + } + + LayoutUtilities.PositionWindowInsideCanvas(this, workingArea.Item1, workingArea.Item2, workingArea.Item3, workingArea.Item4); + }) + ); + } + } + } + + #endregion + + #region Key Handler + + private void AriModernWindow_KeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Delete: + if (!AppManager.TheBoss.ViewModel.LibraryEnable && + AppManager.TheBoss.ViewModel.DeleteLibraryItemsCommand.CanExecute(null)) + { + AppManager.TheBoss.ViewModel.DeleteLibraryItemsCommand.Execute(null); + + e.Handled = true; + } + break; + } + } + + #endregion + + #region Library Panel + + private LibraryPanel _libraryPanel; + private DispatcherTimer _libraryPanelSelfDestructTimer; + + public void OnShowLibrary() + { + // Kill off the timer immediately so it doesn't fire + // while we are loading. + DestroyLibrarySelfDestructTimer(); + + if (_libraryPanel == null) + { + LibraryPanel newPanel = new LibraryPanel(); + + Binding binding = new Binding("Packages"); + binding.Mode = BindingMode.OneWay; + newPanel.SetBinding(LibraryPanel.PackagesSourceProperty, binding); + + LibraryPanelContainer.Children.Add(newPanel); + + _libraryPanel = newPanel; + } + + _libraryPanel.SetInitialFocus(); + } + + public void OnHideLibrary() + { + CreateLibrarySelfDestructTimer(); + } + + private void CreateLibrarySelfDestructTimer() + { + TimeSpan duration = TimeSpan.FromMinutes(destructionTimerTimeinMins); + + DestroyLibrarySelfDestructTimer(); + + _libraryPanelSelfDestructTimer = new DispatcherTimer(duration, DispatcherPriority.Normal, OnLibrarySelfDestruct, Dispatcher); + _libraryPanelSelfDestructTimer.Start(); + } + + private void DestroyLibrarySelfDestructTimer() + { + if (_libraryPanelSelfDestructTimer != null) + { + _libraryPanelSelfDestructTimer.IsEnabled = false; + _libraryPanelSelfDestructTimer.Stop(); + _libraryPanelSelfDestructTimer = null; + } + } + + private void OnLibrarySelfDestruct(object sender, EventArgs e) + { + try + { + if (_libraryPanel != null) + { + var panel = _libraryPanel; + + _libraryPanel = null; + + LibraryPanelContainer.Children.Remove(panel); + + Diagnostics.LogTrace("LibraryPanel: Removed from the visual tree."); + } + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + + #endregion + + #region Settings Panel + + private SettingsPanel _settingsPanel; + + internal void OnShowSettings() + { + if (_settingsPanel == null) + { + SettingsPanel newPanel = new SettingsPanel(); + + SettingsPanelContainer.Children.Add(newPanel); + + _settingsPanel = newPanel; + } + } + + internal void OnHideSettings() + { + if (_settingsPanel != null) + { + var panel = _settingsPanel; + + _settingsPanel = null; + + SettingsPanelContainer.Children.Remove(panel); + } + } + + #endregion + + #region AIPanel Panel + public void SetInsightVisibility (Visibility visibility) + { + TopRib.InsightsToggle.Visibility = visibility; + } + #endregion + + #region Tour + + private EditorWindowTourPanel _activeEditorTour; + + public void ShowEditorTour() + { + try + { + if (_activeEditorTour != null) + { + // We are already running, so leave + } + + EditorWindowTourPanel tour = new EditorWindowTourPanel(); + + tour.SetValue(Grid.RowProperty, 1); + tour.SetValue(Grid.RowSpanProperty, 2); + + tour.Completed += Tour_Completed; + + rootGrid.Children.Add(tour); + + _activeEditorTour = tour; + + tour.Start(); + } + catch (Exception ex) + { + // If anything goes wrong, restore the Ribbon + //clipRibbon.IsHitTestVisible = true; + + Diagnostics.LogException(ex); + } + } + + public bool StopEditorTour() + { + if (_activeEditorTour != null) + { + _activeEditorTour.Stop(); + + _activeEditorTour = null; + + return true; + } + + return false; + } + + private void Tour_Completed(object sender, EventArgs e) + { + _activeEditorTour = null; + } + + #endregion + + #region FaceRectangles + /// + /// Show the face rectangles on the celebrities + /// + private void ShowFaceRectangles(object sender, RoutedEventArgs e) + { + if (AppManager.TheBoss.ViewModel.CelebritiesCanvas != null) + { + AppManager.TheBoss.ViewModel.CelebritiesCanvas.Visibility = Visibility.Visible; + } + } + + /// + /// Hide the face rectangles on the celebrities + /// + private void HideFaceRectangles(object sender, RoutedEventArgs e) + { + if (AppManager.TheBoss.ViewModel.CelebritiesCanvas != null) + { + AppManager.TheBoss.ViewModel.CelebritiesCanvas.Visibility = Visibility.Collapsed; + } + } + #endregion + } + + /// + /// Converter class to bind visibility to a list of boolean parameters + /// + public class InvertMultiBooleanToVisibility : IMultiValueConverter + { + /// + /// Converts enabled property of navigation buttons to clip visibility. + /// + /// Current status of nagivation buttons + /// + /// + /// + /// Visible if at least one button is disabled, otherwise hidden + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + return values.OfType().Any(b => b == false) ? Visibility.Visible : Visibility.Collapsed; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class ViewBoxConstantFontSizeConverter : IValueConverter + { + /// + /// Preserves the size of the object based on + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(value is double)) return null; + double d = (double)value; + return 32*(d/250); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/SnipInsight/Views/NotificationWindow.xaml b/SnipInsight/Views/NotificationWindow.xaml new file mode 100644 index 0000000..88ac8a8 --- /dev/null +++ b/SnipInsight/Views/NotificationWindow.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + Screenshot Saved! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + V + + + + V + + + + Open Editor + + + + + + + + + + + + + + + + + + + F + + + + Open Folder + + + + + + + + + + + + + + + + + X + + + + Dismiss + + + + + + + + + + + + + diff --git a/SnipInsight/Views/NotificationWindow.xaml.cs b/SnipInsight/Views/NotificationWindow.xaml.cs new file mode 100644 index 0000000..a1644ed --- /dev/null +++ b/SnipInsight/Views/NotificationWindow.xaml.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using SnipInsight.StateMachine; +using SnipInsight.Util; +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Input; +using System.Windows.Threading; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for NotificationWindow.xaml + /// Show up a toast to notify that the image was captured and saved + /// Allow for a quick editor access + /// + public partial class NotificationWindow : Window + { + + const int offsetTop = 220; + const int offsetLeft = 10; + + public NotificationWindow() + { + InitializeComponent(); + + var desktopWorkingArea = System.Windows.SystemParameters.WorkArea; + + // Toast window position, top right of the screen. + this.Left = desktopWorkingArea.Right - this.Width - offsetLeft; + this.Top = desktopWorkingArea.Bottom - offsetTop; + + Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => + { + var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea; + var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice; + var corner = transform.Transform(new Point(workingArea.Right, workingArea.Top)); + })); + } + + /// + /// Open the editor when the window is clicked + /// + protected void OpenInEditor(object sender, EventArgs e) + { + AppManager.TheBoss.ViewModel.SelectedPackage = AppManager.TheBoss.ViewModel.Packages[0]; + AppManager.TheBoss.ViewModel.AIAlreadyRan = true; + AppManager.TheBoss.ViewModel.StateMachine.Fire(SnipInsightTrigger.LoadImageFromLibrary); + this.Close(); + } + + /// + /// Close the window without handling + /// + private void CloseWindow(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + this.Close(); + } + + /// + /// Open save folder location + /// + private void OpenFolder(object sender, MouseButtonEventArgs e) + { + Process.Start(UserSettings.CustomDirectory); + this.Close(); + } + } +} diff --git a/SnipInsight/Views/ProgressControl.xaml b/SnipInsight/Views/ProgressControl.xaml new file mode 100644 index 0000000..2379b45 --- /dev/null +++ b/SnipInsight/Views/ProgressControl.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/ProgressControl.xaml.cs b/SnipInsight/Views/ProgressControl.xaml.cs new file mode 100644 index 0000000..d38b167 --- /dev/null +++ b/SnipInsight/Views/ProgressControl.xaml.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows.Controls; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for ProgressControl.xaml + /// + public partial class ProgressControl : UserControl + { + public ProgressControl(string message = null) + { + InitializeComponent(); + if (message != null) + Notification_Message.Text = message; + } + + public void ShowInMainWindow() + { + var mainWindow = AppManager.TheBoss.MainWindow; + if (mainWindow != null) + { + mainWindow.rootGrid.Children.Add(this); + Grid.SetRow(this, 1); + Grid.SetZIndex(this, 1); + } + } + + public void SetProgress(double percentCompleted) + { + this.Progress_Bar.Value = percentCompleted; + } + + public void Dismiss() + { + if (this.Parent as Grid != null) + { + ((Grid)this.Parent).Children.Remove(this); + } + } + } +} diff --git a/SnipInsight/Views/SettingsPanel.xaml b/SnipInsight/Views/SettingsPanel.xaml new file mode 100644 index 0000000..e33d5e4 --- /dev/null +++ b/SnipInsight/Views/SettingsPanel.xaml @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Current Location + + + + + Save Location Here + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/SettingsPanel.xaml.cs b/SnipInsight/Views/SettingsPanel.xaml.cs new file mode 100644 index 0000000..c4805c5 --- /dev/null +++ b/SnipInsight/Views/SettingsPanel.xaml.cs @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Microsoft.Win32; +using Microsoft.WindowsAPICodePack.Dialogs; +using SnipInsight.Controls.Ariadne; +using SnipInsight.Util; +using System; +using System.IO; +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for Settings_Panel.xaml + /// + public partial class SettingsPanel : UserControl + { + // Max Display Length for path preview + const int maxDisplayLength = 17; + + // Location of the default folder for the library view + private readonly string mySnipLoc; + + public SettingsPanel() + { + InitializeComponent(); + + mySnipLoc = GetDefaultPath(); + + RunWhenWindowsStartsCheckbox.IsChecked = !UserSettings.DisableRunWithWindows; + ShowToolbarOnDesktopCheckbox.IsChecked = !UserSettings.DisableToolWindow; + ScreenCaptureKeyCombo.KeyCombo = UserSettings.ScreenCaptureShortcut; + QuickCaptureKeyCombo.KeyCombo = UserSettings.QuickCaptureShortcut; + ScreenCaptureDelaySlider.Value = UserSettings.ScreenCaptureDelay; + ShowNotificationPostSnip.IsChecked = UserSettings.IsNotificationToastEnabled; + OpenEditorPostSnip.IsChecked = UserSettings.IsOpenEditorPostSnip; + ContentModerationStrengthSlider.Value = UserSettings.ContentModerationStrength; + AutoTagging.IsChecked = UserSettings.IsAutoTaggingEnabled; + EnableAI.IsChecked = UserSettings.IsAIEnabled; + EntitySearch.Text = UserSettings.GetKey(EntitySearch.Name); + ImageAnalysis.Text = UserSettings.GetKey(ImageAnalysis.Name); + ImageSearch.Text = UserSettings.GetKey(ImageSearch.Name); + TextRecognition.Text = UserSettings.GetKey(TextRecognition.Name); + Translator.Text = UserSettings.GetKey(Translator.Name); + ContentModerator.Text = UserSettings.GetKey(ContentModerator.Name); + LUISAppId.Text = UserSettings.GetKey(LUISAppId.Name); + LUISKey.Text = UserSettings.GetKey(LUISKey.Name); + + UpdateLocationPreview(); + + this.DataContext = AppManager.TheBoss.ViewModel; + AppManager.TheBoss.ViewModel.InsightsVisible = UserSettings.IsAIEnabled; + } + + private void OpenLink(object sender, RoutedEventArgs e) + { + AriLinkButton menu = (AriLinkButton)sender; + if (menu == null || menu.Tag == null) + return; + + string link = (string)menu.Tag; + System.Diagnostics.Process.Start(link); + } + + private void ScreenCaptureKeyCombo_KeyComboChanged(object sender, EventArgs e) + { + UserSettings.ScreenCaptureShortcut = ScreenCaptureKeyCombo.KeyCombo; + AppManager.TheBoss.ToolWindow.RegisterHotKey(SnipHotKey.ScreenCapture, ScreenCaptureKeyCombo.KeyCombo); + } + + /// + /// Detector for a change in the hotkey to access the quick capture feature + /// + private void QuickCaptureKeyCombo_KeyComboChanged(object sender, EventArgs e) + { + UserSettings.QuickCaptureShortcut = QuickCaptureKeyCombo.KeyCombo; + AppManager.TheBoss.ToolWindow.RegisterHotKey(SnipHotKey.QuickCapture, QuickCaptureKeyCombo.KeyCombo); + } + + private void RunWhenWindowsStartsCheckbox_Clicked(object sender, RoutedEventArgs e) + { + try + { + bool run = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + + using (RegistryKey runKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true)) + { + string regValueName = "Snip"; + + if (run) + { + string appPath = UserSettings.AppPath; + if (string.IsNullOrEmpty(appPath)) + { + throw new ApplicationException("AppPath reg setting was not found"); + } + + string regValueData = string.Format("{0} -startshy", appPath); + + runKey.SetValue(regValueName, regValueData); + } + else + { + runKey.DeleteValue(regValueName, false); + } + } + + // save the setting once the operation completes successfully so, in case of error, we show + // the correct button state the next time the settings dialog is opened + UserSettings.DisableRunWithWindows = !run; + } + catch (Exception ex) + { + Diagnostics.LogException(new ApplicationException("Error configuring RunWhenWindowsStarts", ex)); + } + } + + private void ShowToolbarOnDesktopCheckbox_Clicked(object sender, RoutedEventArgs e) + { + try + { + bool show = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + + if (show) + { + UserSettings.DisableToolWindow = false; + AppManager.TheBoss.ToolWindow.ShowToolWindow(true); + } + else + { + UserSettings.DisableToolWindow = true; + AppManager.TheBoss.ToolWindow.HideToolWindow(); + AppManager.TheBoss.TrayIcon.Activate(); + } + } + catch (Exception ex) + { + Diagnostics.LogException(new ApplicationException("Error configuring ShowToolbarOnDesktop", ex)); + } + } + + /// + /// Toggle the open in editor automatically option after a snip + /// + private void OpenEditorCheckbox_Clicked(object sender, RoutedEventArgs e) + { + try + { + UserSettings.IsOpenEditorPostSnip = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + } + catch (Exception ex) + { + Diagnostics.LogException(new ApplicationException("Error configuring OpenEditorPostSnip", ex)); + } + } + + /// + /// Whether the user wants to see a notification popping up after a snip + /// + private void NotificationCheckbox_Clicked(object sender, RoutedEventArgs e) + { + try + { + UserSettings.IsNotificationToastEnabled = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + } + catch (Exception ex) + { + Diagnostics.LogException(new ApplicationException("Error configuring ShowNotification", ex)); + } + } + + /// + /// Whether the user wants the AI to help him with auto-tagging the screenshot captured + /// + private void AutotaggingCheckbox_Clicked(object sender, RoutedEventArgs e) + { + try + { + UserSettings.IsAutoTaggingEnabled = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + } + catch (Exception ex) + { + Diagnostics.LogException(new ApplicationException("Error configuring auto tag", ex)); + } + } + + /// + /// Whether the user wants enable AI services + /// + private void EnableAI_Clicked(object sender, RoutedEventArgs e) + { + UserSettings.IsAIEnabled = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + AppManager.TheBoss.ViewModel.InsightsVisible = UserSettings.IsAIEnabled; + } + + /// + /// Allow the user to choose where to save the pictures automatically + /// + private void ChooseFolder_Clicked(object sender, RoutedEventArgs e) + { + var dlg = new CommonOpenFileDialog { + Title = "Choose your folder location", + IsFolderPicker = true, + EnsureFileExists = true, + EnsureValidNames = true + }; + + if (dlg.ShowDialog() == CommonFileDialogResult.Ok) + { + UserSettings.CustomDirectory = dlg.FileName; + } + + UpdateLocationPreview(); + } + + private void ScreenCaptureDelaySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + UserSettings.ScreenCaptureDelay = (int)e.NewValue; + } + + /// + /// Reset the CustomDirectory to be the default directory + /// + private void ResetSaveLocation(object sender, EventArgs e) + { + UserSettings.CustomDirectory = mySnipLoc; + + UpdateLocationPreview(); + } + + /// + /// Update the path preview on the settings panel + /// Also update the tooltip (full path) when hovered + /// + private void UpdateLocationPreview() + { + string path = UserSettings.CustomDirectory; + + if (path.Length > maxDisplayLength) + { + path = "..." + path.Substring(path.Length - maxDisplayLength, maxDisplayLength); + } + + CurrentSaveLocation.ToolTip = UserSettings.CustomDirectory; + CurrentSaveLocation.Text = path; + } + + /// + /// Default path for the save location + /// Used by the library to view/edit the screenshots + /// By default, the path is: C:\Users\user\Documents\My Snips + /// + /// The default saving path as a string + private string GetDefaultPath() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "My Snips"); + } + + /// + /// Strength of content moderator for sharing + /// By default, the value is 0 + /// + private void ContentModerationStrengthSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + UserSettings.ContentModerationStrength = (int)e.NewValue; + } + + /// + /// Set the API keys used to authenticate cognnitive services. + /// + private void UpdateKey(TextBox t) + { + string oldKey = UserSettings.GetKey(t.Name); + if (t.Text.Equals(oldKey)) + { + return; + } + UserSettings.SetKey(t.Name,t.Text); + } + + /// + /// Update all API keys used by application. + /// + private void UpdateAllKeys(object sender, RoutedEventArgs e) + { + UpdateKey(EntitySearch); + UpdateKey(ImageAnalysis); + UpdateKey(ImageSearch); + UpdateKey(TextRecognition); + UpdateKey(Translator); + UpdateKey(ContentModerator); + UpdateKey(LUISKey); + UpdateKey(LUISAppId); + MessageBox.Show(SnipInsight.Properties.Resources.Key_Restart, + "Info", + MessageBoxButton.OK, + MessageBoxImage.Information); + } + + /// + /// Changes the settings for copy to clipboard + /// + private void CopyToClipboardCheckbox_Clicked(object sender, RoutedEventArgs e) + { + UserSettings.CopyToClipboardAfterSnip = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + } + + /// + /// Opens the hyperlink for the corresponding key + /// + private void Open_HyperLink(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) + { + System.Diagnostics.Process.Start(e.Uri.ToString()); + } + } +} diff --git a/SnipInsight/Views/ToastControl.xaml b/SnipInsight/Views/ToastControl.xaml new file mode 100644 index 0000000..7d752af --- /dev/null +++ b/SnipInsight/Views/ToastControl.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/ToastControl.xaml.cs b/SnipInsight/Views/ToastControl.xaml.cs new file mode 100644 index 0000000..fb596a9 --- /dev/null +++ b/SnipInsight/Views/ToastControl.xaml.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Timers; +using System.Windows; +using System.Windows.Controls; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for ToastControl.xaml + /// + public partial class ToastControl : UserControl + { + private int _interval; + + public ToastControl(string message) + { + InitializeComponent(); + this.Notification_Message.Text = message; + this.Loaded += new RoutedEventHandler(Control_Loaded); + _interval = 5000; + } + + public ToastControl(string message, int interval) + { + InitializeComponent(); + this.Notification_Message.Text = message; + this.Loaded += new RoutedEventHandler(Control_Loaded); + _interval = interval; + } + + public void ShowInMainWindow() + { + var mainWindow = AppManager.TheBoss.MainWindow; + if (mainWindow != null) + { + mainWindow.rootGrid.Children.Add(this); + Grid.SetRow(this, 1); + Grid.SetZIndex(this, 1); + } + } + + public void Control_Loaded(object sender, RoutedEventArgs e) + { + Timer t = new Timer(); + t.Interval = _interval; + t.Elapsed += new ElapsedEventHandler(t_Elapsed); + t.AutoReset = false; + t.Start(); + } + + void t_Elapsed(object sender, ElapsedEventArgs e) + { + this.Dispatcher.Invoke(new Action(() => + { + if (this.Parent as Grid != null) + { + ((Grid)this.Parent).Children.Remove(this); + } + }), null); + } + } +} diff --git a/SnipInsight/Views/ToolWindow.xaml b/SnipInsight/Views/ToolWindow.xaml new file mode 100644 index 0000000..e468301 --- /dev/null +++ b/SnipInsight/Views/ToolWindow.xaml @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnipInsight/Views/ToolWindow.xaml.cs b/SnipInsight/Views/ToolWindow.xaml.cs new file mode 100644 index 0000000..df29831 --- /dev/null +++ b/SnipInsight/Views/ToolWindow.xaml.cs @@ -0,0 +1,788 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Timers; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media.Animation; +using SnipInsight.Properties; +using SnipInsight.Util; +using SnipInsight.Controls; +using SnipInsight.Controls.Ariadne; + +namespace SnipInsight.Views +{ + internal delegate void HotKeyPressedEventHandler(object sender, HotKeyPressedEventArgs e); + + public partial class ToolWindow : DpiAwareWindow + { + enum DockState + { + Top, + Left, + Right, + Bottom, + Middle, + } + DockState _currentDockState = DockState.Top; + + private Storyboard _activeStoryboard; + private bool _isAnimating; // Forbid dragging the tool window while it is growing/shrinking + private bool _isRepositioning; // Suppress processing of LocationChanged events when changing window position programmatically + private HwndSource _source; + private IntPtr _handle; + internal event HotKeyPressedEventHandler HotKeyPressed; + private Dictionary _monitoringHotKeys = new Dictionary(); + + public bool ToolWindowClosedBySystem { get; set; } + + public ToolWindow() + { + InitializeComponent(); + CopyClipboardToggle.IsChecked = UserSettings.CopyToClipboardAfterSnip; + AIEnableToggle.IsChecked = UserSettings.IsAIEnabled; + OpenEditorToggle.IsChecked = UserSettings.IsOpenEditorPostSnip; + + DataContext = AppManager.TheBoss.ViewModel; + } + + private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + handled = false; + + switch (msg) + { + case (int)SnipInsight.Util.NativeMethods.WindowMsg.WM_HOTKEY: + if (HotKeyPressed != null) + { + HotKeyPressed(this, new HotKeyPressedEventArgs { KeyPressed = (SnipHotKey)(wParam.ToInt32()) }); + handled = true; + } + break; + } + + return (IntPtr)0; + } + + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + try + { + _handle = new WindowInteropHelper(this).Handle; + _source = HwndSource.FromHwnd(_handle); + _source.AddHook(WindowProc); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + + protected override void OnClosed(EventArgs e) + { + if (_source != null) + { + _source.RemoveHook(WindowProc); + _source = null; + } + + foreach (SnipHotKey key in _monitoringHotKeys.Keys.ToList()) + { + UnregisterHotKey(key); + } + + if (!ToolWindowClosedBySystem) + { + AppManager.TheBoss.ViewModel.StateMachine.Fire(StateMachine.SnipInsightTrigger.Exit); + } + base.OnClosed(e); + } + + #region HotKeys + internal void RegisterHotKey(SnipHotKey key, KeyCombo keyCombo) + { + try + { + // Register Key.None means remove it + if (keyCombo == null || keyCombo.Key == Key.None) + { + UnregisterHotKey(key); + return; + } + + if (_monitoringHotKeys.ContainsKey(key)) + { + UnregisterHotKey(key); + } + NativeMethods.RegisterHotKey(_handle, (int)key, keyCombo.KeyModifier, keyCombo.VirtualKeyCode); + _monitoringHotKeys.Add(key, keyCombo); + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + + internal void UnregisterHotKey(SnipHotKey key) + { + try + { + if (_monitoringHotKeys.ContainsKey(key)) + { + NativeMethods.UnregisterHotKey(_handle, (int)key); + _monitoringHotKeys.Remove(key); + } + } + catch (Exception ex) + { + Diagnostics.LogException(ex); + } + } + #endregion + + #region IsOpen + + public bool IsOpen + { + get { return (bool)GetValue(IsOpenProperty); } + private set { SetValue(IsOpenProperty, value); } + } + + public static readonly DependencyProperty IsOpenProperty = + DependencyProperty.Register("IsOpen", typeof(bool), typeof(ToolWindow), new PropertyMetadata(true, OnIsOpenChangedStatic)); + + protected virtual void OnIsOpenChanged(bool value, bool useTransitions = true) + { + Storyboard s = null; + switch (_currentDockState) + { + case DockState.Top: + if (value) + s = (Storyboard)TryFindResource("GrowStoryboardTop") as Storyboard; + else + s = (Storyboard)TryFindResource("ShrinkStoryboardTop") as Storyboard; + break; + case DockState.Left: + if (value) + s = (Storyboard)TryFindResource("GrowStoryboardLeft") as Storyboard; + else + s = (Storyboard)TryFindResource("ShrinkStoryboardLeft") as Storyboard; + break; + case DockState.Right: + if (value) + s = (Storyboard)TryFindResource("GrowStoryboardRight") as Storyboard; + else + s = (Storyboard)TryFindResource("ShrinkStoryboardRight") as Storyboard; + break; + } + + if (s != null) + { + if (!value) + { + IsFullyOpen = false; + } + + if (_activeStoryboard != null) + { + // Stop the active storyboard so that its Completed event doesn't fire. This ensures + // that our _isAnimating bool accurately tracks whether an animation is on-going. + _activeStoryboard.Stop(); + } + _isAnimating = true; + _activeStoryboard = s; + + s.Begin(); + } + else + { + // We are floating, always allow clicks + IsFullyOpen = true; + } + } + + private static void OnIsOpenChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = d as ToolWindow; + + if (self != null) + { + self.OnIsOpenChanged((bool)e.NewValue); + } + } + + private void AfterGrowStoryboard(object sender, EventArgs e) + { + IsFullyOpen = true; + _isAnimating = false; + _activeStoryboard = null; + } + + private void AfterShrinkStoryboard(object sender, EventArgs e) + { + _isAnimating = false; + _activeStoryboard = null; + } + + private void RecalculateIsOpen() + { + IsOpen = IsOpenByMouseOver + || IsOpenByTimer + || IsOpenByPenOrTouch; + } + + private bool _isOpenByTimer; + + public bool IsOpenByTimer + { + get { return _isOpenByTimer; } + set + { + if (value != _isOpenByTimer) + { + _isOpenByTimer = value; + RecalculateIsOpen(); + + if (value) + { + _autoCloseTimer.Start(); + } + else + { + _autoCloseTimer.Stop(); + } + } + } + } + + public void RestartOpenByTimer() + { + IsOpenByTimer = false; + IsOpenByTimer = true; + } + + private bool _isOpenByMouseOver; + + private bool IsOpenByMouseOver + { + get { return _isOpenByMouseOver; } + set + { + if (value != _isOpenByMouseOver) + { + _isOpenByMouseOver = value; + RecalculateIsOpen(); + } + } + } + + private bool _isOpenByPenOrTouch; + + private bool IsOpenByPenOrTouch + { + get { return _isOpenByPenOrTouch; } + set + { + if (value != _isOpenByPenOrTouch) + { + _isOpenByPenOrTouch = value; + RecalculateIsOpen(); + } + } + } + + readonly System.Timers.Timer _autoCloseTimer = new System.Timers.Timer(5000) { AutoReset = false }; // 5 sec. + + public void AutoCloseTimerElapsed(object sender, ElapsedEventArgs e) + { + if (Application.Current != null) + Application.Current.Dispatcher.Invoke(() => { IsOpenByTimer = false; }); + } + + #endregion + + #region IsFullyOpen + + private bool _isFullyOpen; + + + public bool IsFullyOpen + { + get + { + return _isFullyOpen; + } + + set + { + if (value != _isFullyOpen) + { + _isFullyOpen = value; + OnIsFullyOpenChanged(); + } + } + } + + private void OnIsFullyOpenChanged() + { + RecalculateIfClickIsAvailable(); + } + + private void RecalculateIfClickIsAvailable() + { + Root.IsHitTestVisible = IsFullyOpen + && !IsWaitingForStylusSwipeToFinish; + + //Debug.WriteLine(" IsFullyOpen = " + IsFullyOpen.ToString()); + //Debug.WriteLine(" IsWaitingForStylusSwipeToFinish = " + IsWaitingForStylusSwipeToFinish.ToString()); + //Debug.WriteLine(" IsHitTestVisible = " + Root.IsHitTestVisible.ToString()); + } + + #endregion + + #region IsWaitingForStylusSwipeToFinish + + private bool _isWaitingForStylusSwipeToFinish; + + public bool IsWaitingForStylusSwipeToFinish + { + get + { + return _isWaitingForStylusSwipeToFinish; + } + + set + { + if (value != _isWaitingForStylusSwipeToFinish) + { + _isWaitingForStylusSwipeToFinish = value; + OnIsWaitingForStylusSwipeToFinish(); + } + } + } + + private void OnIsWaitingForStylusSwipeToFinish() + { + RecalculateIfClickIsAvailable(); + } + + #endregion + + #region Show/Hide + + internal void ShowToolWindow(bool isOpen, bool force = false) + { + if (force || !UserSettings.DisableToolWindow) + { + IsOpen = isOpen; + + Storyboard s = (Storyboard)TryFindResource("ShowStoryboard") as Storyboard; + + if (s != null) + { + Opacity = 0; + s.Begin(); + } + else + { + Opacity = 1; + } + IsOpenByTimer = isOpen; + Show(); + } + + // a hack to get window handle created so hotkey hook can be initialized when user disabled the tool window + if (_source == null && UserSettings.DisableToolWindow) + { + Opacity = 0; + Show(); + Hide(); + Opacity = 1; + } + } + + internal void HideToolWindow() + { + IsFullyOpen = false; + + // Make sure we are Collapsed + IsOpenByMouseOver = false; + IsOpenByPenOrTouch = false; + IsOpenByTimer = false; + + Hide(); + Opacity = 0; + + // Close toggle panel and uncheck the toggle panel button when tool window is closed + TogglePanel.IsChecked = false; + } + + #endregion + + private void Window_MouseEnter(object sender, MouseEventArgs e) + { + IsOpenByMouseOver = true; + + // Disable the timer, if it's running + IsOpenByTimer = false; + } + + private void Window_MouseLeave(object sender, MouseEventArgs e) + { + IsOpenByMouseOver = false; + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + _autoCloseTimer.Elapsed += AutoCloseTimerElapsed; + PositionWindow(); + Closed += (o, args) => + { + _autoCloseTimer.Elapsed -= AutoCloseTimerElapsed; + }; + + // Added functionality where toggle panel window will move with the tool window + Window ToolWindow = Window.GetWindow(TogglePanel); + + if (null != ToolWindow) + { + ToolWindow.LocationChanged += delegate (object sender2, EventArgs args) + { + var offset = MyPopup.HorizontalOffset; + + // "bump" the offset to cause the popup to reposition itself + // on its own + MyPopup.HorizontalOffset = offset + 1; + MyPopup.HorizontalOffset = offset; + }; + } + } + + private void Window_SizeChanged(object sender, SizeChangedEventArgs e) + { + //PositionWindow(); + } + + private void PositionWindow() + { + _isRepositioning = true; // suppress processing of LocationChanged events + if (Enum.TryParse(Settings.Default.ToolDocking, out _currentDockState)) + { + // Restore last tool position + var upperLeft = Settings.Default.ToolPosition; + Left = upperLeft.X; + Top = upperLeft.Y; + AdjustOrientation(_currentDockState); + } + else + { + // Default tool position + LayoutUtilities.PositionWindowOnPrimaryWorkingArea(this, HorizontalAlignment.Center, VerticalAlignment.Top); + _currentDockState = DockState.Top; + } + _isRepositioning = false; + + // Now, raise LocationChanged event to do final repositioning + this.OnLocationChanged(new EventArgs()); + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + this.HideToolWindow(); // keep running in the system tray + AppManager.TheBoss.TrayIcon.Activate(); + } + + + bool _dragging; + Point _startPoint; + private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + // Store the mouse position + _startPoint = e.GetPosition(null); + } + + private void Window_PreviewMouseMove(object sender, MouseEventArgs e) + { + if (IsWaitingForStylusSwipeToFinish) + { + return; + } + + if (!_dragging && e.LeftButton == MouseButtonState.Pressed && !_isAnimating) + { + // Get the current mouse position + Point mousePos = e.GetPosition(null); + Vector diff = mousePos - _startPoint; + + // If the current dock state is Top, the minimum drag distance in the Y direction is greater because + // the user may be intending to swipe down. We don't want to drag when the user is just swiping. + if ((_currentDockState == DockState.Top && (Math.Abs(diff.X) > 30 || Math.Abs(diff.Y) > 60)) || + (_currentDockState != DockState.Top && (Math.Abs(diff.X) > 30 || Math.Abs(diff.Y) > 30))) + { + _dragging = true; + CaptureMouse(); + Debug.WriteLine("Mouse captured - start dragging"); + } + } + } + + private void Window_MouseMove(object sender, MouseEventArgs e) + { + if (_dragging && e.LeftButton == MouseButtonState.Pressed) + { + // Get the current mouse position + Point mousePos = e.GetPosition(null); + Vector diff = mousePos - _startPoint; + + // Snap to screen boundaries + _isRepositioning = true; + var scale = LayoutUtilities.GetDpiScale(this); + scale = DpiUtilities.GetSystemScale(); + scale = SystemScale; + //scale = new DpiScale(); + var screen = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Control.MousePosition); + System.Diagnostics.Debug.WriteLine("Screen.Left = " + screen.WorkingArea.Left.ToString()); + Left = Math.Min(Math.Max(Left + diff.X, screen.WorkingArea.Left / scale.X), screen.WorkingArea.Right / scale.X - ActualWidth); + Top = Math.Min(Math.Max(Top + diff.Y, screen.WorkingArea.Top / scale.Y), screen.WorkingArea.Bottom / scale.Y - ActualHeight); + + var dockState = ToolsDockState(scale, screen); + if (dockState == DockState.Left || dockState == DockState.Right) + { + if (dockState == DockState.Right && StackContainer.Orientation != Orientation.Vertical) + { + // Adjust for snapping to the right + Left += ActualWidth - ActualHeight; + _startPoint.X -= ActualWidth - ActualHeight; + } + } + else + { + if (_currentDockState == DockState.Right && StackContainer.Orientation != Orientation.Horizontal) + { + // Adjust for snapping from the right + Left += ActualWidth - ActualHeight; + _startPoint.X -= ActualWidth - ActualHeight; + } + } + _isRepositioning = false; + AdjustOrientation(dockState); + _currentDockState = dockState; + } + } + + private void Window_LocationChanged(object sender, EventArgs e) + { + if (!_isRepositioning) + { + // Position was changed by Windows, e.g. due to screen resizing or app reloading + // Snap to screen boundaries + _isRepositioning = true; + var scale = LayoutUtilities.GetDpiScale(this); + scale = DpiUtilities.GetSystemScale(); + scale = SystemScale; + var screen = System.Windows.Forms.Screen.FromPoint(new System.Drawing.Point((int)Left, (int)Top)); + Left = Math.Min(Math.Max(Left, screen.WorkingArea.Left / scale.X), screen.WorkingArea.Right / scale.X - ActualWidth); + Top = Math.Min(Math.Max(Top, screen.WorkingArea.Top / scale.Y), screen.WorkingArea.Bottom / scale.Y - ActualHeight); + + // Make sure docking state is correct + var dockState = ToolsDockState(scale, screen); + Trace.WriteLine("Window position changed, new position: (" + Left + ", " + Top + "), Current dock state:" + _currentDockState + ", Dock state for position:" + dockState); + if (dockState != _currentDockState) + { + // Adjust tool window position to match the old dock state + switch (_currentDockState) + { + case DockState.Top: + Top = screen.WorkingArea.Top / scale.Y; + break; + case DockState.Left: + Left = screen.WorkingArea.Left / scale.X; + break; + case DockState.Right: + Left = screen.WorkingArea.Right / scale.X - ActualWidth; + break; + case DockState.Bottom: + Top = screen.WorkingArea.Bottom / scale.Y - ActualHeight; + break; + case DockState.Middle: + Left = (screen.WorkingArea.Left + screen.WorkingArea.Right) / 2 / scale.X - ActualWidth / 2; + Top = (screen.WorkingArea.Top + screen.WorkingArea.Bottom) / 2 / scale.Y - ActualHeight / 2; + break; + } + Trace.WriteLine("Adjusted dockState:" + _currentDockState + ", adjusted position: (" + Left + ", " + Top + ")"); + } + _isRepositioning = false; + + Settings.Default.ToolDocking = _currentDockState.ToString(); + Settings.Default.ToolPosition = new System.Drawing.Point((int)Left, (int)Top); + Settings.Default.Save(); + } + } + + private void Window_MouseUp(object sender, MouseButtonEventArgs e) + { + if (_dragging) + { + _dragging = false; + Mouse.Capture(null); + Debug.WriteLine("Mouse released - stop dragging (X=" + Left + ", Y=" + Top + ", CurrentDockState=" + _currentDockState + ")"); + + Settings.Default.ToolDocking = _currentDockState.ToString(); + Settings.Default.ToolPosition = new System.Drawing.Point((int)Left, (int)Top); + Settings.Default.Save(); + } + } + + /// + /// Detect if the tools are docked at the edge of the screen + /// + /// + /// + /// + private DockState ToolsDockState(DpiScale scale, System.Windows.Forms.Screen screen) + { + if ((int)Math.Round(Top * scale.Y) <= screen.WorkingArea.Top) + return DockState.Top; + else if ((int)Math.Round(Left * scale.X) <= screen.WorkingArea.Left) + return DockState.Left; + else if ((int)Math.Round((Left + ActualWidth) * scale.X) >= screen.WorkingArea.Right) + return DockState.Right; + else if ((int)Math.Round((Top + ActualHeight) * scale.Y) >= screen.WorkingArea.Bottom) + return DockState.Bottom; + return DockState.Middle; + } + + private void AdjustOrientation(DockState dockState) + { + if (dockState == DockState.Left || dockState == DockState.Right) + { + // If docked along left or right edge, switch to vertical orientation + if (StackContainer.Orientation != Orientation.Vertical) + { + StackContainer.Orientation = Orientation.Vertical; + StackControls.Orientation = Orientation.Horizontal; + ControlContainer.Width = ControlContainer.ActualHeight; + ControlContainer.Height = ControlContainer.ActualWidth; + StackControls.Width = StackControls.ActualHeight; + StackControls.Height = StackControls.ActualWidth; + } + } + else + { + // Ensure horizontal orientation + if (StackContainer.Orientation != Orientation.Horizontal) + { + StackContainer.Orientation = Orientation.Horizontal; + StackControls.Orientation = Orientation.Vertical; + ControlContainer.Width = ControlContainer.ActualHeight; + ControlContainer.Height = ControlContainer.ActualWidth; + StackControls.Width = StackControls.ActualHeight; + StackControls.Height = StackControls.ActualWidth; + } + } + + // Adjust border thickness + ToolBorder.BorderThickness = new Thickness( + dockState == DockState.Left ? 0 : 1, + dockState == DockState.Top ? 0 : 1, + dockState == DockState.Right ? 0 : 1, + dockState == DockState.Bottom ? 0 : 1); + + // Apply changes + UpdateLayout(); + } + + private void Self_StylusDown(object sender, StylusDownEventArgs e) + { + if (!IsFullyOpen) + { + // Enable an open by pen or touch when the style + // clicks down while we are closed + IsOpenByPenOrTouch = true; + IsWaitingForStylusSwipeToFinish = true; + CaptureStylus(); + e.Handled = true; + } + } + + private void Self_StylusUp(object sender, StylusEventArgs e) + { + if (IsWaitingForStylusSwipeToFinish) + { + IsWaitingForStylusSwipeToFinish = false; + ReleaseStylusCapture(); + } + } + + private void Self_Deactivated(object sender, EventArgs e) + { + // Enable a soft dismiss for touch when the window is + // deactivated + + IsOpenByPenOrTouch = false; + } + + /// + /// Show tool window when user click/double click on taskbar + /// + private void Window_Activated(object sender, EventArgs e) + { + if (IsOpen) + return; + + ShowToolWindow(true); + } + + #region Toggle Panel + /// + /// Sets the settings for whether to copy to clipboard automatically when captured or annotated/modified + /// + private void CopyClipboard_Clicked(object sender, RoutedEventArgs e) + { + UserSettings.CopyToClipboardAfterSnip = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + } + #endregion + + /// + /// Sets the open editor setting in the tool window + /// + private void OpenEditor_Clicked(object sender, RoutedEventArgs e) + { + UserSettings.IsOpenEditorPostSnip = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + } + + /// + /// Sets the enable ai setting in the tool window + /// + private void EnableAI_Clicked(object sender, RoutedEventArgs e) + { + UserSettings.IsAIEnabled = ((AriToggleSwitch)sender).IsChecked.GetValueOrDefault(false); + AppManager.TheBoss.ViewModel.InsightsVisible = UserSettings.IsAIEnabled; + } + + /// + /// Event trigger to move onto the ai toggle if popup is open + /// + private void LostKeyboardFocus_Event(object sender, KeyboardFocusChangedEventArgs e) + { + if (MyPopup.IsOpen) + { + AIEnableToggle.Focus(); + } + } + + /// + /// Event trigger to move to the settings button once we move past the toggles + /// + private void LostKeyboardFocus_Event_Popup(object sender, KeyboardFocusChangedEventArgs e) + { + SettingsButton.Focus(); + } + } +} diff --git a/SnipInsight/Views/TopRibbon.xaml b/SnipInsight/Views/TopRibbon.xaml new file mode 100644 index 0000000..423f090 --- /dev/null +++ b/SnipInsight/Views/TopRibbon.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SnipInsight/Views/TopRibbon.xaml.cs b/SnipInsight/Views/TopRibbon.xaml.cs new file mode 100644 index 0000000..037aa09 --- /dev/null +++ b/SnipInsight/Views/TopRibbon.xaml.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for TopRibbon.xaml + /// + public partial class TopRibbon : UserControl + { + public TopRibbon() + { + InitializeComponent(); + } + } + + /// + /// Inverts booleans for xaml + /// + public class InvertBooleanToVisibility : IValueConverter + { + /// + /// Converts booleans to visibilty, for single parameter + /// + /// Current status of nagivation buttons + /// + /// + /// + /// Returns the visible for true, collapsed otherwise + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (value is bool) ? ((bool)value ? Visibility.Collapsed : Visibility.Visible) : throw new ArgumentException(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/SnipInsight/Views/TwoButtonDialog.xaml b/SnipInsight/Views/TwoButtonDialog.xaml new file mode 100644 index 0000000..c468867 --- /dev/null +++ b/SnipInsight/Views/TwoButtonDialog.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/SnipInsight/Views/TwoButtonDialog.xaml.cs b/SnipInsight/Views/TwoButtonDialog.xaml.cs new file mode 100644 index 0000000..34dc803 --- /dev/null +++ b/SnipInsight/Views/TwoButtonDialog.xaml.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Windows; +using SnipInsight.Controls; + +namespace SnipInsight.Views +{ + /// + /// Interaction logic for TwoButtonDialog.xaml + /// + public partial class TwoButtonDialog : DpiAwareWindow + { + public TwoButtonDialog(string message, string button1Text, string button2Text) + { + InitializeComponent(); + DialogMessageText.Text = message; + Button1.Content = button1Text; + Button2.Content = button2Text; + } + + public void Reset() // useful if the same dialog is reused. + { + Button1Clicked = false; + Button2Clicked = false; + } + + public bool Button1Clicked { get; set; } + + public bool Button2Clicked { get; set; } + + private void Button1_OnClick(object sender, RoutedEventArgs e) + { + Button1Clicked = true; + Close(); + } + + private void Button2_OnClick(object sender, RoutedEventArgs e) + { + Button2Clicked = true; + Close(); + } + } +} diff --git a/SnipInsight/app.manifest b/SnipInsight/app.manifest new file mode 100644 index 0000000..de5a7ed --- /dev/null +++ b/SnipInsight/app.manifest @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True/PM + + + + diff --git a/SnipInsight/project.json b/SnipInsight/project.json new file mode 100644 index 0000000..6b65205 --- /dev/null +++ b/SnipInsight/project.json @@ -0,0 +1,24 @@ +{ + "dependencies": { + "Interop.Microsoft.Office.Interop.OneNote": "1.1.0.0", + "Microsoft.Data.Edm": "5.6.2", + "Microsoft.Data.OData": "5.6.2", + "Microsoft.Data.Services.Client": "5.6.2", + "Microsoft.Logging.Stubs": "3.3.0", + "Microsoft.Office.Interop.Outlook": "15.0.4797.1003", + "MvvmLightLibs": "5.4.1", + "Microsoft.WindowsAPICodePack.Shell": "1.1.0", + "Newtonsoft.Json": "10.0.3", + "Stateless": "2.4.0", + "System.Diagnostics.DiagnosticSource": "4.0.0", + "System.Net.Http.Formatting.Extension": "5.2.3" + }, + "frameworks": { + "net46": {} + }, + "runtimes": { + "win-x86": {}, + "win-x64": {}, + "win": {} + } +} \ No newline at end of file diff --git a/SnipInsights.sln b/SnipInsights.sln new file mode 100644 index 0000000..897e426 --- /dev/null +++ b/SnipInsights.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2027 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnipInsight", "SnipInsight\SnipInsight.csproj", "{2C90CA1B-BD65-41C9-B542-5F1F6E472863}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C71B59A9-1837-4B1D-A353-5EFA1B782452}" + ProjectSection(SolutionItems) = preProject + NuGet.config = NuGet.config + ..\README.md = ..\README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Debug|Any CPU.ActiveCfg = Debug|x64 + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Debug|Any CPU.Build.0 = Debug|x64 + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Debug|x64.ActiveCfg = Debug|x64 + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Debug|x64.Build.0 = Debug|x64 + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Release|Any CPU.ActiveCfg = Release|x64 + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Release|x64.ActiveCfg = Release|x64 + {2C90CA1B-BD65-41C9-B542-5F1F6E472863}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AF3CD1AC-CD06-4762-8D31-84339ABA9283} + EndGlobalSection +EndGlobal