diff --git a/.gitignore b/.gitignore index 3faee1d..fbd02d6 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,7 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json +#**/Properties/launchSettings.json # StyleCop StyleCopReport.xml @@ -341,4 +341,6 @@ tests/*_.html #Wix *.g.wxs -install/ \ No newline at end of file +install/ +Deploy/ +*.CA.dll \ No newline at end of file diff --git a/Build.ps1 b/Build.ps1 new file mode 100644 index 0000000..97adb69 --- /dev/null +++ b/Build.ps1 @@ -0,0 +1,16 @@ +echo "If build fails, do this:" +echo "cmd.exe ""/K"" '""C:\Program Files\Microsoft Visual Studio\2022\Preview\Common7\Tools\VsDevCmd.bat"" && pwsh -noexit'" + +echo "Cleaning Deploy folder" +rm -Recurse -Path setup/Deploy -Include *.* + +echo "Building" +$env:winprint_telemetryId="put Kindel Systems key here" +texttransform .\src\WinPrint.Core\Services\TelemetryService.tt +msbuild /p:Configuration=Debug /p:Platform=x64 src/WinPrint.sln + +echo "Testing Out-winprint" +pwsh.exe -NoLogo -Command "Import-Module '.\setup\Deploy\winprint.dll'; Out-WinPrint .\testfiles\Program.cs -WhatIf -Verbose;" + +echo "Testing winprintgui" +.\setup\Deploy\winprintgui.exe .\testfiles\Program.cs diff --git a/README.md b/README.md index 0137ece..a3bdf15 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![.NET Core](https://github.com/tig/winprint/workflows/.NET%20Core/badge.svg) [![CodeFactor](https://www.codefactor.io/repository/github/tig/winprint/badge?s=93affda0738af869187afe5296914b814511f529)](https://www.codefactor.io/repository/github/tig/winprint) -# winprint 2.0 +# winprint 2 *winprint - A modern take on the the classic source code printing app from [1988](https://tig.github.io/winprint/about.html).* @@ -13,12 +13,11 @@ Advanced source code and text file printing for PowerShell. The perfect tool for * Complete control over page formatting options, including headers and footers, margins, fonts, page orientation, etc... * Headers and Footers support detailed file and print information macros with rich date/time formatting. * Simple and elegant graphical user interface with accurate print preview. -* The most capable PowerShell printing tool enabling printing from the command line. +* The most capable PowerShell printing tool for printing from the command line. * Complete control of printing features with dozens of parameters, including *Intellicode* parameter completion (using `tab` key). - * Allows **winprint** to be used from other applications or solutions. The **winpprint** PowerShell `out-winprint` CmdLet is a drop-in replacement for `out-printer`. + * Allows **winprint** to be used from other applications or solutions. The **winpprint** PowerShell `Out-WinPrint` CmdLet is a drop-in replacement for `out-printer`. * Sheet Definitions make it easy to save settings for frequent print jobs. * Comprehensive logging. -* Cross platform. Even though it's named **win**print, it works on Windows, Linux (command line only; some assembly required), and (not yet tested) Mac OS. ## Documentation @@ -30,15 +29,15 @@ Advanced source code and text file printing for PowerShell. The perfect tool for ## Graphical Interface -![winprint 2.0](https://tig.github.io/winprint/winprint2.png) +![winprint 2](https://tig.github.io/winprint/winprint2.png) ## PowerShell Command Line Interface See what version is installed: ```powershell -PS > out-winprint -verbose -VERBOSE: Out-WinPrint 2.0.5.0 - Copyright Kindel Systems, LLC - https://tig.github.io/winprint +PS > Out-WinPrint -verbose +VERBOSE: Out-WinPrint 2.0.5.0 - Copyright Kindel, LLC - https://tig.github.io/winprint ``` Print a Powershell profile using the default sheet definition and default printer: @@ -48,9 +47,9 @@ Get-Content $profile.CurrentUserAllHosts | winprint -Language powershell ``` ```powershell -PS > cat Program.cs | wp -PrinterName PDF -Orientation Portrait -Verbose -Title Program.cs -VERBOSE: Out-WinPrint 2.0.5.0 - Copyright Kindel Systems, LLC - https://tig.github.io/winprint -VERBOSE: Printer: PDF +PS > cat Program.cs | wp -PrinterName "Epson MX-80" -Orientation Portrait -Verbose -Title Program.cs +VERBOSE: Out-WinPrint 2.0.5.0 - Copyright Kindel, LLC - https://tig.github.io/winprint +VERBOSE: Printer: Epson MX-80 VERBOSE: Paper Size: Letter VERBOSE: Orientation: Portrait VERBOSE: Sheet Definition: Default 2-Up (0002a500-0000-0000-c000-000000000046) @@ -63,7 +62,7 @@ PS > The following all do the same thing: ```powershell -out-winprint -FileName program.cs +Out-WinPrint -FileName program.cs wp program.cs winprint program.cs cat program.cs | wp -Title "program.cs" @@ -72,7 +71,7 @@ cat program.cs | wp -Title "program.cs" Print all `.c` and `.h` files in the current directory to the "HP LaserJet" printer, ensuring the `{Title`} in the header/footers shows the filename. Present verbose output along the way: ```powershell -ls .\* -include ('*.c', '*.h') | foreach { cat $_.FullName | out-winPrint -p "HP LaserJet" -title $_.FullName -verbose} +ls .\* -include ('*.c', '*.h') | foreach { cat $_.FullName | Out-WinPrint -p "Epson MX-80" -title $_.FullName -verbose} ``` ## Contributing diff --git a/docs/_config.yml b/docs/_config.yml index 209d72e..f04c303 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: winprint 2.0 +title: WinPrint 2 description: winprint - Advanced source code and text file printing for PowerShell. logo: https://tig.github.io/winprint/winprint-icon.png theme: jekyll-theme-dinky diff --git a/docs/about.md b/docs/about.md index 17f421b..6c6b22a 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,28 +1,28 @@ # About & History -Ever since I started programming on an Apple ][+ in 1981, I've had a thing for printing. My earliest apps focused on printing and my first money-making endeavor was "Tapes", which printed casette tape '[J-cards](https://en.wikipedia.org/wiki/J-card)' for all the mix-tapes of great '80s music we made for the girls. Whenever I learned a new programming language or OS, the first app I'd write was **[Spit](https://github.com/tig/Tigger/blob/a2013af12fef6d9946adea7fe510f8c649766160/College/C_SRC/SPIT153.C)**, an app for printing my source code all pretty (it "spits" source code out of a printer). Over the years, I wrote Spit for AppleDOS (Apple BASIC), UCSD-Pascal, CP/M (Turbo Pascal), DOS (8086 assembly and C), VAX/VMS (Pascal and FORTRAN-77), and Mac (Pascal). +I've had a thing for printing since I started programming on an Apple ][+ in 1981. My first money-making endeavor was "Tapes", which printed casette tape '[J-cards](https://en.wikipedia.org/wiki/J-card)' for all the mix-tapes of great '80s music we made for the girls. Whenever I learned a new programming language or OS, the first app I'd write was **[Spit](https://github.com/tig/Tigger/blob/a2013af12fef6d9946adea7fe510f8c649766160/College/C_SRC/SPIT153.C)**, an app for printing my source code all pretty (it "spits" source code out of a printer). Over the years, I wrote Spit for AppleDOS (Apple BASIC), UCSD-Pascal, CP/M (Turbo Pascal), DOS (8086 assembly and C), VAX/VMS (Pascal and FORTRAN-77), and Mac (Pascal). -In 1988, as a college junior at the University of Arizona (Go Cats!), I decided Windows was going to win over OS/2 and I was going to work for Microsoft. I bought Charles Petzold's [Programming Windows](https://www.amazon.com/Programming-Windows-Writing-Developer-Reference/dp/0735671761?tag=ceklog-20) and conned my dad into buying me a copy of the Windows 2.0 SDK (which was like $300 back then!). On my amazeballs ALR 386/33 PC I set about becoming a Windows programmer. The first useful) app I wrote was **WinSpit**. In a rare moment of adulting, I renamed the app [WinPrint and listed it on CompuServe](http://www.kindel.com/products/winprint/) as shareware ($25). For over ten years I received $25 checks the mail from folks all over the world. Even better, WinPrint demonstrated to Microsoft I could actually, really write code. So they hired me. +In 1988, as a college junior at the University of Arizona (Go Cats!), I decided Windows was going to win over OS/2 and I was going to work for Microsoft. I bought Charles Petzold's [Programming Windows](https://www.amazon.com/Programming-Windows-Writing-Developer-Reference/dp/0735671761?tag=ceklog-20) and conned my dad into buying me a copy of the Windows 2.0 SDK (which was like $300 back then!). On my amazeballs ALR 386/33 PC I set about becoming a Windows programmer. The first useful app I wrote was **WinSpit**. In a rare moment of adulting, I renamed the app [WinPrint and listed it on CompuServe](http://www.kindel.com/products/winprint/) as shareware ($25). For over ten years I received $25 checks the mail from folks all over the world. Even better, WinPrint demonstrated to Microsoft I could actually, really write code and they hired me. -![Winrpint 1.54](winprint-154.gif) +![WinPrint 1.54](winprint-154.gif) Several times in the early 1990s I started writing WinPrint 2.0. Each time I had the basics working and realized three things: 1) Nobody cares about printing source code, 3) I'd over-engineered things, and 2) the technology I choose was already dated (e.g. MFC). Two of those abandoned efforts can be found in my GitHub archive [here (1992)](https://github.com/tig/Tigger/tree/master/Shareware/WINPRT2/WINPRINT) and [here (1994)](https://github.com/tig/Tigger/tree/master/Shareware/WINPRT2/WINPRT20). -Last year (2019) I got a wild-hair to write some code as a way of blowing off steam, and proving to myself I was still cool. It all started with Microsoft releasing the [Cascadia Code](https://devblogs.microsoft.com/commandline/cascadia-code/) font. I have a thing for fixed-pitch fonts. It's weird. Anyway, I installed the font in Terminal and VScode but just looking at stuff didn't satisfy me. I needed to *use the font in anger*! So I fixed some long-standing issues in [MCE Controller](https://tig.github.io/mcec/) (another app I wrote that nobody uses anymore). +In 2019 got a wild-hair to write some code as a way of blowing off steam, and proving to myself I was still cool. It all started with Microsoft releasing the [Cascadia Code](https://devblogs.microsoft.com/commandline/cascadia-code/) font. I have a thing for fixed-pitch fonts. It's weird. Anyway, I installed the font in Terminal and VScode but just looking at stuff didn't satisfy me. I needed to *use the font in anger*! So I fixed some long-standing issues in [MCE Controller](https://tig.github.io/mcec/) (another app I wrote that nobody uses anymore). This all led to me re-discovering my old WinPrint 2.0 source code. Reminiscing on how much time I wasted back then, and how effective it was as a procrastination tool, I just had to try again. So I did. And, just to be clear, here's what I did: 1) I wrote a *printing* app in 2019-2020. Nobody prints these days. *I* don't even print anymore. -2) I over-engineered it. It has a full GUI with print preview. Headers and Footers with *Macros*. A full command-line interface. It can syntax-highlight over 200 different programming languages. It's cross-platfom. It's written in C# using the very latest .NET Core. It uses NodeJS and C++ under the covers. And more. +2) I over-engineered it. It has a full GUI with print preview. Headers and Footers with *Macros*. A full command-line interface. It can syntax-highlight over 200 different programming languages. It's cross-platform. It's written in C# using the very latest .NET Core. It uses NodeJS and C++ under the covers. And more. 3) I used .NET and C#. Ok, this part I can defend (assuming you get past point #1 and #2): First, I know C# well and it is awesome. Second, no other modern language/app-framework can even SPELL "print". I tried both Electron and Flutter and both suck when it comes to printing. -So, there you are: I present to you winprint 2.0. I hope you enjoy it. +So, there you are: I present to you WinPrint 2.0. I hope you enjoy it. -tig ([@ckindel](twitter.com/ckindel) on twitter; see my [blog](https://ceklog.kindel.com)) ## License (MIT) -Copyright (c) Kindel Systems, LLC +Copyright (c) Kindel, LLC 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: diff --git a/docs/index.md b/docs/index.md index cf0e930..19bbfb5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,15 @@ -# winprint 2.0 +# WinPrint *A modern take on the the classic source code printing app from [1988](about.md).* Advanced source code and text file printing for PowerShell. The perfect tool for printing source code, web pages, reports generated by legacy systems, documentation, or any text or HTML file. It works interactively or from the command line making it great for single users or whole enterprises. -![winprint 2.0](winprint2.png) +![WinPrint 2.0](winprint2.png) ```powershell winprint -verbose -VERBOSE: Out-WinPrint 2.0.3.0 - Copyright Kindel Systems, LLC - https://tig.github.io/winprint +VERBOSE: Out-WinPrint 2.3.0 - Copyright Kindel Systems, LLC - https://tig.github.io/winprint ``` ```powershell @@ -21,7 +21,7 @@ cat $profile.CurrentUserAllHosts | wp -Language powershell ``` ```powershell -ls .\* -include ('*.c', '*.h') | foreach { cat $_.FullName | out-winprint -p "LaserJet" -title $_.FullName -verbose} +ls .\* -include ('*.c', '*.h') | foreach { cat $_.FullName | out-winprint -p "Epson MX-80" -title $_.FullName -verbose} ``` ## Features @@ -34,21 +34,29 @@ ls .\* -include ('*.c', '*.h') | foreach { cat $_.FullName | out-winprint -p "La * Simple and elegant graphical user interface with accurate print preview. * The most capable PowerShell printing tool enabling printing from the command line. * Complete control of printing features with dozens of parameters, including *Intellicode* parameter completion (using `tab` key). - * Allows **winprint** to be used from other applications or solutions. The **winpprint** PowerShell `out-winprint` CmdLet is a drop-in replacement for `out-printer`. + * Allows **WinPrint** to be used from other applications or solutions. The **WinPrint** PowerShell `Out-WinPrint` CmdLet is a drop-in replacement for `Out-Printer`. * Sheet Definitions make it easy to save settings for frequent print jobs. * Comprehensive logging. -* Cross platform. Even though it's named **win**print, it works on Windows, Linux (command line only; some assembly required), and (not yet tested) Mac OS. +* Theoretically Cross platform. Even though it's named **Win**pPrint, it works on Windows, Linux (command line only; some assembly required), and (not yet tested) Mac OS. See [User's Guide](users-guide.md) for more details. ## History -See [About](about.md) for the history prior to *winprint 2.0*. +See [About](about.md) for the history prior to **WinPrint 2,x**. -* 03-May-2020 - 2.0.5 (RC2) - +* 09-Sept-2022 - 2.1.0 - Significant bug and usability fixes. + * Fixes #35 - Detects if pre-reqs (Python and Pygments) are installed when run. Installs Pygments if needed. + * Fixes #37 - Rendering is stupidly slow. + * Fixes #30 - Switch to launch GUI (-gui) not working + * Fixes #33 - Pygments not loading + * Fixes #24 - Line Numbers greater than 999 are too wide + +* 03-May-2020 - 2.0.5 (RC2) - First public release * Fixed content type / file extension detection. * Changed Header/Footer macros to support `{Language}`, `{ContentType}`, and `{CteName}` instead of just `{FileType}`. * Installer detects if .NET Core 3.1 is installed. Makes it easy to install it if not installed. + * 30-Apr-2020 - 2.0.4 (RC1) - Totally new rendering engine * Replaced the nodejs `Prism.js`-based syntax highlighter with the Python `Pygments`-based system. This: * Long lines now wrap correctly. Issue #12 @@ -59,6 +67,7 @@ See [About](about.md) for the history prior to *winprint 2.0*. * Enabled printing of ANSI Escape sequence encoded files (because `winprint` uses Pygments `terminal` formatter as input). * Removed dependency on nodejs. Issue #17 * Dozens of small bug fixes and improvements. + * 12-Apr-2020 - 2.0.0.6100 (Beta3) - Updated PowerShell cmdlet to run as a standalone app (e.g. wp foo.cs or out-winprint foo.cs. * Added ability to choose fonts from GUI * fixed print preview status bugs @@ -71,4 +80,4 @@ See [About](about.md) for the history prior to *winprint 2.0*. * 20-Feb-2020 - 2.0.0.1034 (Alpha) - Minor bug fixes. Added version number to GUI. * 18-Feb-2020 - 2.0.0.1003 (Alpha) - First public build (alpha). -I'm embarrased by a few bugs and performance issues that I want to fix before I declare beta; see [Issues](https://github.com/tig/winprint/issues). +See [Issues](https://github.com/tig/winprint/issues). diff --git a/docs/install.md b/docs/install.md index c09c3b2..f8f5317 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,6 +1,6 @@ # Installation Instructions -***winprint*** 2.0 is almost not beta. It works well (on my machine), and I've had a dozen or so users verify it works well for them; see [Issues](https://github.com/tig/winprint/issues).* +***WinPrint*** works well (on my machine), and I've had a dozen or so users verify it works well for them; see [Issues](https://github.com/tig/winprint/issues).* *Please report any problems or feature requests [here](https://github.com/tig/winprint/issues).* @@ -18,4 +18,4 @@ Good luck. Start by cloning the *winprint* repo, installing .NET Core 3, and bui ### On Mac -I haven't even tried as my old Macbook Air died and Apple wont fix it. I refuse to buy anything from Apple (except for for my family who are all all-in with Apple...sigh). I'll buy beer for someone who contributes to getting the Mac version working. It should not be hard given I've proven the stupid things works on Linux already. +I haven't even tried as my old Macbook Air died and Apple wont fix it. I'll buy beer for someone who contributes to getting the Mac version working. It should not be hard given I've proven the stupid things works on Linux already. diff --git a/proto/netpygments/Program.cs b/proto/netpygments/Program.cs new file mode 100644 index 0000000..2c1fcf4 --- /dev/null +++ b/proto/netpygments/Program.cs @@ -0,0 +1,23 @@ +static void Main(string[] args) +{ + using (Py.GIL()) + { + dynamic np = Py.Import("numpy"); + Console.WriteLine(np.cos(np.pi * 2)); + + dynamic sin = np.sin; + Console.WriteLine(sin(5)); + + double c = (double)(np.cos(5) + sin(5)); + Console.WriteLine(c); + + dynamic a = np.array(new List { 1, 2, 3 }); + Console.WriteLine(a.dtype); + + dynamic b = np.array(new List { 6, 5, 4 }, dtype: np.int32); + Console.WriteLine(b.dtype); + + Console.WriteLine(a * b); + Console.ReadKey(); + } +} \ No newline at end of file diff --git a/proto/netpygments/netpygments.csproj b/proto/netpygments/netpygments.csproj new file mode 100644 index 0000000..74abf5c --- /dev/null +++ b/proto/netpygments/netpygments.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/proto/winforms/WinPrint.Core/Models/Uuid.cs b/proto/winforms/WinPrint.Core/Models/Uuid.cs index c6983eb..87caef6 100644 --- a/proto/winforms/WinPrint.Core/Models/Uuid.cs +++ b/proto/winforms/WinPrint.Core/Models/Uuid.cs @@ -4,7 +4,7 @@ namespace WinPrint.Core { - // WinPrint 2.0 has been assigned the following GUIDs by Microsoft + // WinPrint 2 has been assigned the following GUIDs by Microsoft // // Available range: 0002A5xx-0000-0000-C000-000000000046 // diff --git a/setup/WinPrint.Installer/App.config b/setup/WinPrint.Installer/App.config index 11b88dc..df8242c 100644 --- a/setup/WinPrint.Installer/App.config +++ b/setup/WinPrint.Installer/App.config @@ -1,6 +1,6 @@ - + diff --git a/setup/WinPrint.Installer/EmbeddedUI.config b/setup/WinPrint.Installer/EmbeddedUI.config index 28c8b3c..9fcba55 100644 --- a/setup/WinPrint.Installer/EmbeddedUI.config +++ b/setup/WinPrint.Installer/EmbeddedUI.config @@ -2,7 +2,8 @@ - + + \ No newline at end of file diff --git a/setup/WinPrint.Installer/EmbeddedUI_WPF.CA.dll b/setup/WinPrint.Installer/EmbeddedUI_WPF.CA.dll deleted file mode 100644 index 40316af..0000000 Binary files a/setup/WinPrint.Installer/EmbeddedUI_WPF.CA.dll and /dev/null differ diff --git a/setup/WinPrint.Installer/Properties/AssemblyInfo.cs b/setup/WinPrint.Installer/Properties/AssemblyInfo.cs deleted file mode 100644 index 12434a6..0000000 --- a/setup/WinPrint.Installer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; -using System.Windows; - -// 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("winprint installer")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("winprint installer")] -[assembly: AssemblyCopyright("Copyright © Kindel Systems, LLC")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// 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)] - -//In order to begin building localizable applications, set -//CultureYouAreCodingWith in your .csproj file -//inside a . For example, if you are using US english -//in your source files, set the to en-US. Then uncomment -//the NeutralResourceLanguage attribute below. Update the "en-US" in -//the line below to match the UICulture setting in the project file. - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] - - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.3.0")] -[assembly: AssemblyFileVersion("2.0.3.0")] diff --git a/setup/WinPrint.Installer/Properties/Resources.Designer.cs b/setup/WinPrint.Installer/Properties/Resources.Designer.cs index 9b67d21..b0ff5ea 100644 --- a/setup/WinPrint.Installer/Properties/Resources.Designer.cs +++ b/setup/WinPrint.Installer/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace WinPrintInstaller.Properties { // 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", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -61,11 +61,11 @@ internal Resources() { } /// - /// Looks up a localized string similar to Copyright (c) Kindel Systems, LLC + /// Looks up a localized string similar to Copyright (c) Kindel, LLC /// ///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 pe [rest of string was truncated]";. + ///The above copyright notice and this permission [rest of string was truncated]";. /// internal static string License { get { diff --git a/setup/WinPrint.Installer/Properties/Resources.resx b/setup/WinPrint.Installer/Properties/Resources.resx index e7539e6..9af5edb 100644 --- a/setup/WinPrint.Installer/Properties/Resources.resx +++ b/setup/WinPrint.Installer/Properties/Resources.resx @@ -112,13 +112,13 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Copyright (c) Kindel Systems, LLC + Copyright (c) Kindel, LLC 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: diff --git a/setup/WinPrint.Installer/Properties/Settings.Designer.cs b/setup/WinPrint.Installer/Properties/Settings.Designer.cs index b19e72b..ab98576 100644 --- a/setup/WinPrint.Installer/Properties/Settings.Designer.cs +++ b/setup/WinPrint.Installer/Properties/Settings.Designer.cs @@ -8,11 +8,11 @@ // //------------------------------------------------------------------------------ -namespace WinPrintInstaller.Properties { +namespace WinPrint.Installer.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.5.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.3.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/setup/WinPrint.Installer/WinPrint.Installer.csproj b/setup/WinPrint.Installer/WinPrint.Installer.csproj index e6e4ecf..ac35f0b 100644 --- a/setup/WinPrint.Installer/WinPrint.Installer.csproj +++ b/setup/WinPrint.Installer/WinPrint.Installer.csproj @@ -1,166 +1,50 @@  - - + - Debug - AnyCPU - {29DF07CE-4648-47DE-8440-3DB54D49D5D2} - Exe - Properties - WinPrintInstaller - WinPrintInstaller - v4.7.2 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - - - - - false - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true - - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - true - - - + WinExe + net472 + $(MSBuildProjectName.Replace(" ", "_").Replace("-", "_")) - - ..\packages\WixSharp.bin.1.14.3\lib\BootstrapperCore.dll - - - ..\packages\WixSharp.bin.1.14.3\lib\Microsoft.Deployment.WindowsInstaller.dll - - - - - - - - - + + + + + + + + + + + + + + + + + + - - ..\packages\WixSharp.bin.1.14.3\lib\WixSharp.dll - - - ..\packages\WixSharp.bin.1.14.3\lib\WixSharp.Msi.dll - - - ..\packages\WixSharp.bin.1.14.3\lib\WixSharp.UI.dll - + + + - - - - - SetupWizard.xaml + + True + True + Settings.settings - - MSBuild:Compile - Designer - - - - Code - - - True - True - Resources.resx - - - True - Settings.settings - True - - - ResXFileCodeGenerator - Resources.Designer.cs - - - PreserveNewest - - + SettingsSingleFileGenerator Settings.Designer.cs - - - - - - - - - - False - Microsoft .NET Framework 4.7.2 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - - copy $(ProjectDir)CustomAction.config ..\..\..\..\release\ - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - \ No newline at end of file diff --git a/setup/WinPrint.Installer/WixSharp.CA.dll b/setup/WinPrint.Installer/WixSharp.CA.dll deleted file mode 100644 index 9475789..0000000 Binary files a/setup/WinPrint.Installer/WixSharp.CA.dll and /dev/null differ diff --git a/setup/WinPrint.Installer/installer/EmbeddedUI.config b/setup/WinPrint.Installer/installer/EmbeddedUI.config new file mode 100644 index 0000000..2210120 --- /dev/null +++ b/setup/WinPrint.Installer/installer/EmbeddedUI.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.Installer/installer/WinPrint.Installer.exe b/setup/WinPrint.Installer/installer/WinPrint.Installer.exe new file mode 100644 index 0000000..f1521b6 Binary files /dev/null and b/setup/WinPrint.Installer/installer/WinPrint.Installer.exe differ diff --git a/setup/WinPrint.Installer/installer/winprint.msi b/setup/WinPrint.Installer/installer/winprint.msi new file mode 100644 index 0000000..97a98fc Binary files /dev/null and b/setup/WinPrint.Installer/installer/winprint.msi differ diff --git a/setup/WinPrint.Installer/installer/winprint.wxs b/setup/WinPrint.Installer/installer/winprint.wxs new file mode 100644 index 0000000..e073a51 --- /dev/null +++ b/setup/WinPrint.Installer/installer/winprint.wxs @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (NOT Installed) + (NOT Installed) + + + + + diff --git a/setup/WinPrint.Installer/packages.config b/setup/WinPrint.Installer/packages.config deleted file mode 100644 index 4213831..0000000 --- a/setup/WinPrint.Installer/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/setup/WinPrint.Installer/setup.cs b/setup/WinPrint.Installer/setup.cs index e76f0e7..ab73c8e 100644 --- a/setup/WinPrint.Installer/setup.cs +++ b/setup/WinPrint.Installer/setup.cs @@ -8,9 +8,9 @@ internal class Install { public static readonly Guid ProductCode = new Guid("{0002A501-0000-0000-C000-000000000046}"); private static void Main() { - const string sourceBaseDir = @"..\..\release"; - const string outDir = @"..\..\install"; - var versionFile = $"{sourceBaseDir}\\WinPrint.Core.dll"; + const string sourceBaseDir = @"..\..\setup\release"; + const string outDir = @"installer"; + var versionFile = $@"{sourceBaseDir}\WinPrint.Core.dll"; Debug.WriteLine($"version path: {versionFile}"); var info = FileVersionInfo.GetVersionInfo(versionFile); var feature = new Feature(new Id("winprint")); diff --git a/setup/WinPrint.SetupBootstrapper/Code/Bundle.wxs b/setup/WinPrint.SetupBootstrapper/Code/Bundle.wxs new file mode 100644 index 0000000..868f937 --- /dev/null +++ b/setup/WinPrint.SetupBootstrapper/Code/Bundle.wxs @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupBootstrapper/Resources/EULA.rtf b/setup/WinPrint.SetupBootstrapper/Resources/EULA.rtf new file mode 100644 index 0000000..2ea5003 --- /dev/null +++ b/setup/WinPrint.SetupBootstrapper/Resources/EULA.rtf @@ -0,0 +1,202 @@ +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi31507\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;} +{\f1\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;} +{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;} +{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f39\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f40\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\f42\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f43\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f44\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f45\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\f46\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f47\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f49\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\f50\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;} +{\f52\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\f53\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\f54\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f55\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);} +{\f56\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\f57\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\f49\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\f50\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;} +{\f52\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\f53\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\f54\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f55\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);} +{\f56\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\f57\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;} +{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;} +{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;} +{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} +{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0; +\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\*\defchp +\fs22\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\sa160\sl259\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;} +{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext11 \ssemihidden \sunhideused +Normal Table;}}{\*\rsidtbl \rsid1733461\rsid7679086}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\operator chrpai}{\creatim\yr2013\mo11\dy30\hr12\min23} +{\revtim\yr2013\mo11\dy30\hr12\min23}{\version2}{\edmins0}{\nofpages1}{\nofwords4}{\nofchars24}{\nofcharsws27}{\vern57435}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}} +\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect +\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701 +\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale100\rsidroot7679086 \nouicompat \fet0{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1 +\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5 +\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang +{\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 +\fs22\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af1\afs20 \ltrch\fcs0 \f1\fs20\insrsid7679086 \hich\af1\dbch\af31505\loch\f1 TODO: Place EULA Text Here.}{\rtlch\fcs1 \af1\afs20 \ltrch\fcs0 +\f1\fs20\insrsid1733461 +\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a +9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad +5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 +b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 +0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 +a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f +c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 +0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 +a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b +4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b +4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100aa5225dfc60600008b1a0000160000007468656d652f7468656d652f +7468656d65312e786d6cec595d8bdb46147d2ff43f08bd3bfe92fcb1c41b6cd9ceb6d94d42eca4e4716c8fadc98e344633de8d0981923c160aa569e943037deb +43691b48a02fe9afd936a54d217fa17746b63c638fbb9b2585a5640d8b343af7ce997bafce1d4997afdc8fa87384134e58dc708b970aae83e3211b9178d2706f +f7bbb99aeb7081e211a22cc60d778eb97b65f7c30f2ea31d11e2083b601ff31dd4704321a63bf93c1fc230e297d814c7706dcc920809384d26f951828ec16f44 +f3a542a1928f10895d274611b8bd311e932176fad2a5bbbb74dea1701a0b2e078634e949d7d8b050d8d1615122f89c0734718e106db830cf881df7f17de13a14 +7101171a6e41fdb9f9ddcb79b4b330a2628bad66d7557f0bbb85c1e8b0a4e64c26836c52cff3bd4a33f3af00546ce23ad54ea553c9fc29001a0e61a52917dda7 +dfaab7dafe02ab81d2438bef76b55d2e1a78cd7f798373d3973f03af40a97f6f03dfed06104503af4029dedfc07b5eb51478065e81527c65035f2d34db5ed5c0 +2b5048497cb8812ef89572b05c6d061933ba6785d77daf5b2d2d9caf50500d5975c929c62c16db6a2d42f758d2058004522448ec88f9148fd110aa3840940c12 +e2ec93490885374531e3305c2815ba8532fc973f4f1da988a01d8c346bc90b98f08d21c9c7e1c3844c45c3fd18bcba1ae4cdcb1fdfbc7cee9c3c7a71f2e89793 +c78f4f1efd9c3a32acf6503cd1ad5e7fffc5df4f3f75fe7afeddeb275fd9f15cc7fffed367bffdfaa51d082b5d85e0d5d7cffe78f1ecd5379ffff9c3130bbc99 +a0810eef930873e73a3e766eb10816a6426032c783e4ed2cfa2122ba45339e701423398bc57f478406fafa1c5164c1b5b019c13b09488c0d787576cf20dc0b93 +9920168fd7c2c8001e30465b2cb146e19a9c4b0b737f164fec9327331d770ba123dbdc018a8dfc766653d05662731984d8a07993a258a0098eb170e4357688b1 +6575770931e27a408609e36c2c9cbbc46921620d499f0c8c6a5a19ed9108f232b711847c1bb139b8e3b418b5adba8d8f4c24dc15885ac8f73135c27815cd048a +6c2efb28a27ac0f791086d247bf364a8e33a5c40a6279832a733c29cdb6c6e24b05e2de9d7405eec693fa0f3c84426821cda7cee23c674649b1d06218aa6366c +8fc4a18efd881f428922e7261336f80133ef10790e7940f1d674df21d848f7e96a701b9455a7b42a107965965872791533a37e7b733a4658490d08bfa1e71189 +4f15f73559f7ff5b5907217df5ed53cbaa2eaaa0371362bda3f6d6647c1b6e5dbc03968cc8c5d7ee369ac53731dc2e9b0decbd74bf976ef77f2fdddbeee7772f +d82b8d06f9965bc574abae36eed1d67dfb9850da13738af7b9daba73e84ca32e0c4a3bf5cc8ab3e7b8690887f24e86090cdc2441cac64998f88488b017a229ec +ef8bae7432e10bd713ee4c19876dbf1ab6fa96783a8b0ed8287d5c2d16e5a3692a1e1c89d578c1cfc6e15143a4e84a75f50896b9576c27ea51794940dabe0d09 +6d329344d942a2ba1c9441520fe610340b09b5b277c2a26e615193ee97a9da6001d4b2acc0d6c9810d57c3f53d30012378a242148f649ed2542fb3ab92f92e33 +bd2d984605c03e625901ab4cd725d7adcb93ab4b4bed0c99364868e566925091513d8c87688417d52947cf42e36d735d5fa5d4a02743a1e683d25ad1a8d6fe8d +c579730d76ebda40635d2968ec1c37dc4ad9879219a269c31dc3633f1c4653a81d2eb7bc884ee0ddd95024e90d7f1e6599265cb4110fd3802bd149d520220227 +0e2551c395cbcfd24063a5218a5bb104827061c9d541562e1a3948ba99643c1ee3a1d0d3ae8dc848a7a7a0f0a95658af2af3f383a5259b41ba7be1e8d819d059 +720b4189f9d5a20ce0887078fb534ca33922f03a3313b255fdad35a685eceaef13550da5e3884e43b4e828ba98a77025e5191d7596c5403b5bac1902aa8564d1 +080713d960f5a01add34eb1a2987ad5df7742319394d34573dd35015d935ed2a66ccb06c036bb13c5f93d7582d430c9aa677f854bad725b7bed4bab57d42d625 +20e059fc2c5df70c0d41a3b69acca026196fcab0d4ecc5a8d93b960b3c85da599a84a6fa95a5dbb5b8653dc23a1d0c9eabf383dd7ad5c2d078b9af549156df3d +f44f136c700fc4a30d2f81675470954af8f09020d810f5d49e24950db845ee8bc5ad0147ce2c210df741c16f7a41c90f72859adfc97965af90abf9cd72aee9fb +e562c72f16daadd243682c228c8a7efacda50bafa2e87cf1e5458d6f7c7d89966fdb2e0d599467eaeb4a5e11575f5f8aa5ed5f5f1c02a2f3a052ead6cbf55625 +572f37bb39afddaae5ea41a5956b57826abbdb0efc5abdfbd0758e14d86b9603afd2a9e52ac520c8799582a45fabe7aa5ea9d4f4aacd5ac76b3e5c6c6360e5a9 +7c2c6201e155bc76ff010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f +7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be +9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980 +ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5b +babac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f6e74656e +745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f +2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f7468656d652f74 +68656d654d616e616765722e786d6c504b01022d0014000600080000002100aa5225dfc60600008b1a00001600000000000000000000000000d6020000746865 +6d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b0100002700000000000000000000000000d00900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000cb0a00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax371\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; +\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text; +\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2; +\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List; +\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1; +\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision; +\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1; +\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1; +\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2; +\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2; +\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2; +\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3; +\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3; +\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4; +\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4; +\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4; +\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5; +\lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; +\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; +\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; +\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; +\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; +\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; +\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; +\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; +\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; +\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; +\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;}}{\*\datastore 010500000200000018000000 +4d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000e0c0 +7045f9edce01feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/setup/WinPrint.SetupBootstrapper/Resources/Icon.png b/setup/WinPrint.SetupBootstrapper/Resources/Icon.png new file mode 100644 index 0000000..995d691 Binary files /dev/null and b/setup/WinPrint.SetupBootstrapper/Resources/Icon.png differ diff --git a/setup/WinPrint.SetupBootstrapper/WinPrint.SetupBootstrapper.wixproj b/setup/WinPrint.SetupBootstrapper/WinPrint.SetupBootstrapper.wixproj new file mode 100644 index 0000000..cba7870 --- /dev/null +++ b/setup/WinPrint.SetupBootstrapper/WinPrint.SetupBootstrapper.wixproj @@ -0,0 +1,68 @@ + + + + Debug + x86 + 3.8 + {3e532735-b85a-4a2d-856b-20ec9d1c95a8} + 2.0 + WinPrint.Setup + Bundle + + $([System.Text.RegularExpressions.Regex]::Match($(TF_BUILD_BUILDNUMBER), "\d+.\d+.\d+.\d+")) + + $([System.Text.RegularExpressions.Regex]::Match($(BUILD_BUILDNUMBER), "\d+.\d+.\d+.\d+")) + + 0.0.1 + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug;MSIProductVersion=$(MSIProductVersion) + + + bin\$(Configuration)\ + obj\$(Configuration)\ + MSIProductVersion=$(MSIProductVersion) + + + + $(WixExtDir)\WixBalExtension.dll + WixBalExtension + + + + + WinPrint.SetupSetup + {4974b9fd-017c-47bf-8d09-ea3407a4148a} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupMM/WinPrint.SetupMM.wixproj b/setup/WinPrint.SetupMM/WinPrint.SetupMM.wixproj new file mode 100644 index 0000000..4ba8483 --- /dev/null +++ b/setup/WinPrint.SetupMM/WinPrint.SetupMM.wixproj @@ -0,0 +1,37 @@ + + + Debug + x86 + 3.8 + {e53c3cb2-dfb8-4ae0-b93d-6583f054fc91} + 2.0 + WinPrint.SetupMM + Module + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupMM/WinPrint.SetupMM.wxs b/setup/WinPrint.SetupMM/WinPrint.SetupMM.wxs new file mode 100644 index 0000000..422f0da --- /dev/null +++ b/setup/WinPrint.SetupMM/WinPrint.SetupMM.wxs @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupMM/WinPrint.SetupMMcustom.wxs b/setup/WinPrint.SetupMM/WinPrint.SetupMMcustom.wxs new file mode 100644 index 0000000..90dcac4 --- /dev/null +++ b/setup/WinPrint.SetupMM/WinPrint.SetupMMcustom.wxs @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupSetup/Code/Features.wxs b/setup/WinPrint.SetupSetup/Code/Features.wxs new file mode 100644 index 0000000..c3bbd7f --- /dev/null +++ b/setup/WinPrint.SetupSetup/Code/Features.wxs @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupSetup/Code/Product.wxs b/setup/WinPrint.SetupSetup/Code/Product.wxs new file mode 100644 index 0000000..25a303f --- /dev/null +++ b/setup/WinPrint.SetupSetup/Code/Product.wxs @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + Installed OR NETFRAMEWORK40FULL + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupSetup/Code/UI-CustomDialog.wxs b/setup/WinPrint.SetupSetup/Code/UI-CustomDialog.wxs new file mode 100644 index 0000000..1a9cfc3 --- /dev/null +++ b/setup/WinPrint.SetupSetup/Code/UI-CustomDialog.wxs @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + NOT Installed + + + + + + + + + + + + + + + Not FINDCUSTOM + + + Not CUSTOM + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupSetup/Code/UI.wxs b/setup/WinPrint.SetupSetup/Code/UI.wxs new file mode 100644 index 0000000..6bafab2 --- /dev/null +++ b/setup/WinPrint.SetupSetup/Code/UI.wxs @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.SetupSetup/Resources/Banner.jpg b/setup/WinPrint.SetupSetup/Resources/Banner.jpg new file mode 100644 index 0000000..6178904 Binary files /dev/null and b/setup/WinPrint.SetupSetup/Resources/Banner.jpg differ diff --git a/setup/WinPrint.SetupSetup/Resources/Dialog.jpg b/setup/WinPrint.SetupSetup/Resources/Dialog.jpg new file mode 100644 index 0000000..74c6abd Binary files /dev/null and b/setup/WinPrint.SetupSetup/Resources/Dialog.jpg differ diff --git a/setup/WinPrint.SetupSetup/Resources/EULA.rtf b/setup/WinPrint.SetupSetup/Resources/EULA.rtf new file mode 100644 index 0000000..ba2a79f Binary files /dev/null and b/setup/WinPrint.SetupSetup/Resources/EULA.rtf differ diff --git a/setup/WinPrint.SetupSetup/Resources/Icon.ico b/setup/WinPrint.SetupSetup/Resources/Icon.ico new file mode 100644 index 0000000..0576997 Binary files /dev/null and b/setup/WinPrint.SetupSetup/Resources/Icon.ico differ diff --git a/setup/WinPrint.SetupSetup/WinPrint.SetupSetup.wixproj b/setup/WinPrint.SetupSetup/WinPrint.SetupSetup.wixproj new file mode 100644 index 0000000..c03bdc9 --- /dev/null +++ b/setup/WinPrint.SetupSetup/WinPrint.SetupSetup.wixproj @@ -0,0 +1,70 @@ + + + Debug + x86 + 3.8 + {4974b9fd-017c-47bf-8d09-ea3407a4148a} + 2.0 + WinPrint.Setup + Package + + $([System.Text.RegularExpressions.Regex]::Match($(TF_BUILD_BUILDNUMBER), "\d+.\d+.\d+.\d+")) + + $([System.Text.RegularExpressions.Regex]::Match($(BUILD_BUILDNUMBER), "\d+.\d+.\d+.\d+")) + + 0.0.1 + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug;MSIProductVersion=$(MSIProductVersion) + WixUIBannerBmp=Resources\Banner.jpg;WixUIDialogBmp=Resources\Dialog.jpg;WixUILicenseRtf=Resources\EULA.rtf + + + bin\$(Configuration)\ + obj\$(Configuration)\ + MSIProductVersion=$(MSIProductVersion) + WixUIBannerBmp=Resources\Banner.jpg;WixUIDialogBmp=Resources\Dialog.jpg;WixUILicenseRtf=Resources\EULA.rtf + + + + + + + + + + + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + $(WixExtDir)\WixNetFxExtension.dll + WixNetFxExtension + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.Wix.Setup/HeatGeneratedFileList.wxs b/setup/WinPrint.Wix.Setup/HeatGeneratedFileList.wxs new file mode 100644 index 0000000..84cad8e --- /dev/null +++ b/setup/WinPrint.Wix.Setup/HeatGeneratedFileList.wxs @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.Wix.Setup/Product.wxs b/setup/WinPrint.Wix.Setup/Product.wxs new file mode 100644 index 0000000..b039bc9 --- /dev/null +++ b/setup/WinPrint.Wix.Setup/Product.wxs @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/WinPrint.Wix.Setup/Variables.wxi b/setup/WinPrint.Wix.Setup/Variables.wxi new file mode 100644 index 0000000..023b4b2 --- /dev/null +++ b/setup/WinPrint.Wix.Setup/Variables.wxi @@ -0,0 +1,5 @@ + + + + + diff --git a/setup/WinPrint.Wix.Setup/WinPrint.Wix.Setup.wixproj b/setup/WinPrint.Wix.Setup/WinPrint.Wix.Setup.wixproj new file mode 100644 index 0000000..30cada1 --- /dev/null +++ b/setup/WinPrint.Wix.Setup/WinPrint.Wix.Setup.wixproj @@ -0,0 +1,83 @@ + + + + Debug + x64 + 3.10 + 26d38fa9-72ec-4eac-9de2-37cd5e761d09 + 2.0 + WinPrint.Wix.Setup + Package + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + true + + + + + + + + + WinPrint.Console + {a82f3b2b-5e3a-49b8-8552-983d06081186} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + WinPrint.Core + {78b6744f-df3e-4d86-ace5-18854a0af54e} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + WinPrint.WinForms + {6dea2a6f-7eca-411c-b25a-2b7293afd336} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + + + + $(WixExtDir)\WixUtilExtension.dll + WixUtilExtension + + + + + + + + + HarvestPath=..\release + + + + + + \ No newline at end of file diff --git a/setup/WinPrint.Wix/Bundle.wxs b/setup/WinPrint.Wix/Bundle.wxs new file mode 100644 index 0000000..1ca15d3 --- /dev/null +++ b/setup/WinPrint.Wix/Bundle.wxs @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/setup/WinPrint.Wix/WinPrint.Wix.Bundle.wixproj b/setup/WinPrint.Wix/WinPrint.Wix.Bundle.wixproj new file mode 100644 index 0000000..5b7980b --- /dev/null +++ b/setup/WinPrint.Wix/WinPrint.Wix.Bundle.wixproj @@ -0,0 +1,44 @@ + + + + Debug + x86 + 3.10 + eaa1e7ca-efa6-49e5-8435-bd952e6b8a7d + 2.0 + WinPrint.Wix + Bundle + WinPrint.Wix.Bundle + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + $(WixExtDir)\WixBalExtension.dll + WixBalExtension + + + + + + + + + \ No newline at end of file diff --git a/specs/headerfooter.md b/specs/headerfooter.md index 642b6e5..1ae7aaf 100644 --- a/specs/headerfooter.md +++ b/specs/headerfooter.md @@ -22,7 +22,7 @@ MachineName E.g. $"{} -## WinPrint 2.0 (original) header/footer format macros +## WinPrint 2 (original) header/footer format macros * &p - Page numberstucture * &P - Total number of pages * &d - Current date (short format). diff --git a/src/LiteHtmlSharp b/src/LiteHtmlSharp index fceb511..c7888cf 160000 --- a/src/LiteHtmlSharp +++ b/src/LiteHtmlSharp @@ -1 +1 @@ -Subproject commit fceb511f1916bc8ae2ae26d1de9e03b134a9324b +Subproject commit c7888cff415dfd53757ec82cecf2a4c12782224c diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..04daf08 --- /dev/null +++ b/src/README.md @@ -0,0 +1,23 @@ +# Build and Deploy + +## Getting source ready + +* `git config --global status.submoduleSummary true` +* `git clone --recurse-submodules -j8 tig:tig/winprint` + +# Pre-reqs + +* VS 2022+ +* Python: `winget install python` +* Pygments: `pip install Pygments` + +# Enable telemtry/logging key + +* Right click on `winprint\src\WinPrint.Core\Services\TelemetryService.tt` and "Run Custom Tool" to run T4 compiler +* MS ApplicationInsights used to be in the Kindel account, but is no longer there! + +# Versions + +* Used to be managed by `msbump` https://github.com/BalassaMarton/MSBump + * Prime version is stored in `Winprint.Core.dll` via `src\WinPrint.Core\WinPrint.Core.csproj`: `2.1.0.0` + * Must manually bump `major.minor.rel` in `WinPrint.WinForms.csproj`, `WinPrint.Console.csproj`, and `Winprint.LiteHtml.csproj` before release diff --git a/src/WinPrint.Console/OutWinPrint.cs b/src/WinPrint.Console/OutWinPrint.cs index 90563f9..06781c9 100644 --- a/src/WinPrint.Console/OutWinPrint.cs +++ b/src/WinPrint.Console/OutWinPrint.cs @@ -240,9 +240,9 @@ private async Task DoUpdateAsync() { } if (_force || - //await Task.Run(() => ShouldContinue("The winprint installer requires any Powershell instances that have used out-winprint be closed.", + //await Task.Run(() => ShouldContinue("The winprint installer requires any Powershell instances that have used Out-WinPrint be closed.", //"Exit this Powershell instance?"))) { - ShouldContinue("The winprint installer requires any Powershell instances that have used out-winprint be closed.", + ShouldContinue("The winprint installer requires any Powershell instances that have used Out-WinPrint be closed.", "Exit this Powershell instance?")) { // Kill process? System.Environment.Exit(0); @@ -267,14 +267,15 @@ private void UpdateService_GotLatestVersion(object sender, Version version) { if (ServiceLocator.Current.UpdateService.CompareVersions() < 0) { _updateMsg = $"An update to winprint is available at {ServiceLocator.Current.UpdateService.ReleasePageUri}. " + $"Run '{MyInvocation.InvocationName} -InstallUpdate' to upgrade"; + Log.Warning("Update: {msg}", _updateMsg); } else if (ServiceLocator.Current.UpdateService.CompareVersions() > 0) { - _updateMsg = $"This is a MORE recent version than can be found at github.com/tig/winprint ({version})"; + _updateMsg = $"This is a more recent version than can be found at github.com/tig/winprint ({version})"; } else { _updateMsg = "This is lastest version of winprint"; } - Log.Debug("UpdateService_GotLatestVersion" + _updateMsg); + Log.Debug("Update: {msg}", _updateMsg); } #endregion @@ -289,11 +290,11 @@ protected override async Task BeginProcessingAsync() { // If this is the first invoke since loading start telemetry and logging if (ServiceLocator.Current.TelemetryService.GetTelemetryClient() == null) { - ServiceLocator.Current.TelemetryService.Start("out-winprint"); + ServiceLocator.Current.TelemetryService.Start("Out-WinPrint"); // AsyncCmdlet base adds each cmdlet instance to PowerShellSink.Instance; this call configures // the Debug and File LogEventLevel's only - ServiceLocator.Current.LogService.Start("out-winprint", PowerShellSink.Instance, debug: _debug, verbose: _verbose); + ServiceLocator.Current.LogService.Start("Out-WinPrint", PowerShellSink.Instance, debug: _debug, verbose: _verbose); } else { // Change Console logging as specified by paramters (e.g. -verbose and/or -debug) @@ -303,7 +304,7 @@ protected override async Task BeginProcessingAsync() { } var ver = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(UpdateService)).Location); - Log.Information("out-winprint v{version} - {copyright} - {link}", ver.ProductVersion, ver.LegalCopyright, @"https://tig.github.io/winprint"); + Log.Information("Out-WinPrint v{version} - {copyright} - {link}", ver.ProductVersion, ver.LegalCopyright, @"https://tig.github.io/winprint"); Log.Debug("PowerShell Invoked: command: {appname}, module: {modulename}", MyInvocation.MyCommand.Name, MyInvocation.MyCommand.ModuleName); var dict = MyInvocation.BoundParameters.ToDictionary(item => item.Key, item => $"{item.Value}"); @@ -362,7 +363,7 @@ protected override async Task EndProcessingAsync() { Process proc = null; try { var psi = new ProcessStartInfo { - UseShellExecute = true, // This is important + UseShellExecute = true, // This is important - don't wait for exit FileName = ServiceLocator.Current.SettingsService.SettingsFileName }; proc = Process.Start(psi); @@ -379,11 +380,42 @@ protected override async Task EndProcessingAsync() { return; } + if (Gui) { + Process proc = null; + try { + var path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(SettingsService)).Location); + + var psi = new ProcessStartInfo { + UseShellExecute = true, // This is important - don't wait for exit + Arguments = FileName, + + FileName = path + Path.DirectorySeparatorChar + "winprintgui.exe" + }; + proc = Process.Start(psi); + } + catch (Win32Exception e) { + // TODO: Better error message (output of stderr?) + ServiceLocator.Current.TelemetryService.TrackException(e, false); + + Log.Error(e, $"Couldn't launch winprint GUI"); + } + finally { + proc?.Dispose(); + } + return; + } + if (WinPrint.Core.Models.ModelLocator.Current.Settings == null) { Log.Fatal(new Exception($"Settings are invalid. See {ServiceLocator.Current.LogService.LogPath} for more information."), ""); return; } + // Check to ensure Python/Pygments is working + (bool installed, string message) = ServiceLocator.Current.PygmentsConverterService.CheckInstall(); + if (!installed) { + Log.Warning(message); + } + SetupUpdateHandler(); // Check for new version @@ -409,12 +441,14 @@ protected override async Task EndProcessingAsync() { await Task.Run(() => ServiceLocator.Current.UpdateService.GetLatestVersionAsync(_getVersionCancellationToken.Token).ConfigureAwait(true), _getVersionCancellationToken.Token).ConfigureAwait(true); + // Start progress meter var rec = new ProgressRecord(1, "Printing", "Printing...") { PercentComplete = 0, StatusDescription = "Initializing winprint" }; WriteProgress(rec); + Debug.Assert(_print != null); if (!string.IsNullOrEmpty(PrinterName)) { try { @@ -608,7 +642,7 @@ await Task.Run(() => ServiceLocator.Current.UpdateService.GetLatestVersionAsync( // End by sharing update info, if any if (!string.IsNullOrEmpty(_updateMsg)) { - Log.Information(_updateMsg); + Log.Information("Update: {msg}", _updateMsg); } CleanUpUpdateHandler(); diff --git a/src/WinPrint.Console/PowerShellSink.cs b/src/WinPrint.Console/PowerShellSink.cs index 762101a..a992468 100644 --- a/src/WinPrint.Console/PowerShellSink.cs +++ b/src/WinPrint.Console/PowerShellSink.cs @@ -79,7 +79,7 @@ private void EmitToCmdLet(AsyncCmdlet cmdlet, LogEvent logEvent) { // The Write-Error cmdlet declares a non-terminating error. case LogEventLevel.Error: - //_cmdlet.WriteDebug("error: " + strWriter.ToString()); + //cmdlet.WriteDebug("error: " + strWriter.ToString()); var ex = logEvent.Exception; if (logEvent.Exception == null) { ex = new Exception(); diff --git a/src/WinPrint.Console/Program.cs b/src/WinPrint.Console/Program.cs index 15c6456..f46cc13 100644 --- a/src/WinPrint.Console/Program.cs +++ b/src/WinPrint.Console/Program.cs @@ -1,4 +1,4 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint /// diff --git a/src/WinPrint.Console/Properties/Resources.Designer.cs b/src/WinPrint.Console/Properties/Resources.Designer.cs index f81d9c3..f0f7352 100644 --- a/src/WinPrint.Console/Properties/Resources.Designer.cs +++ b/src/WinPrint.Console/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace WinPrint.Console.Properties { // 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", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/src/WinPrint.Console/Properties/launchSettings.json b/src/WinPrint.Console/Properties/launchSettings.json new file mode 100644 index 0000000..7a6198d --- /dev/null +++ b/src/WinPrint.Console/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + }, + "Out-WinPrint": { + "commandName": "Executable", + "executablePath": "pwsh.exe", + "commandLineArgs": "-NoLogo -Command \"Import-Module '.\\winprint.dll'; Out-WinPrint -Verbose -WhatIf -FileName ..\\..\\..\\..\\..\\..\\testfiles\\Program.cs;\"" + } + } +} \ No newline at end of file diff --git a/src/WinPrint.Console/WinPrint.Console.csproj b/src/WinPrint.Console/WinPrint.Console.csproj index 55b1148..3e17e19 100644 --- a/src/WinPrint.Console/WinPrint.Console.csproj +++ b/src/WinPrint.Console/WinPrint.Console.csproj @@ -1,60 +1,65 @@  - - Exe - netcoreapp3.1 - WinPrint.Console - winprint Console App - winprint - - - 2.0.5.103 - Kindel Systems - winprint - Charlie Kindel - winprint Console App - Copyright Kindel Systems, LLC - https://github.com/tig/winprint - true - en-US - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - + + Exe + net8.0-windows + WinPrint.Console + winprint Console App + winprint + WinPrint.Console.Program + 2.1.0.400 + Kindel + winprint + Tig Kindel + winprint Console App + Copyright Kindel, LLC + https://github.com/tig/winprint + true + en-US + AnyCPU;x64 + winprint.ico + + + + + + + + + + + + + + + + + + + True + True + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + \ No newline at end of file diff --git a/src/WinPrint.Console/WinPrint.Console.msbump b/src/WinPrint.Console/WinPrint.Console.msbump deleted file mode 100644 index fee6de8..0000000 --- a/src/WinPrint.Console/WinPrint.Console.msbump +++ /dev/null @@ -1,11 +0,0 @@ -{ - Configurations: { - "Debug": { - BumpRevision: true - }, - - "Release": { - BumpRevision: false - } - } -} \ No newline at end of file diff --git a/src/WinPrint.Console/winprint.ico b/src/WinPrint.Console/winprint.ico new file mode 100644 index 0000000..c54ce49 Binary files /dev/null and b/src/WinPrint.Console/winprint.ico differ diff --git a/src/WinPrint.Core/ContentTypeEngines/AnsiCte.cs b/src/WinPrint.Core/ContentTypeEngines/AnsiCte.cs index cf3e601..322573c 100644 --- a/src/WinPrint.Core/ContentTypeEngines/AnsiCte.cs +++ b/src/WinPrint.Core/ContentTypeEngines/AnsiCte.cs @@ -1,270 +1,288 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint -/// -/// Define this to use the DyanmicScreen class. Otherwise, use Screen -/// - using System; using System.Drawing; -using System.IO; -using System.Linq; +using System.Drawing.Printing; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using libvt100; using Serilog; using WinPrint.Core.Models; using WinPrint.Core.Services; -using static libvt100.Screen; +using Font = System.Drawing.Font; -namespace WinPrint.Core.ContentTypeEngines { +namespace WinPrint.Core.ContentTypeEngines; - /// - /// Implements text/plain file type support. - /// - public class AnsiCte : ContentTypeEngineBase, IDisposable { - private static readonly string[] _supportedContentTypes = { "text/plain", "text/ansi" }; - /// - /// ContentType identifier (shorthand for class name). - /// - public override string[] SupportedContentTypes => _supportedContentTypes; - - public static AnsiCte Create() { - var engine = new AnsiCte(); - // Populate it with the common settings - engine.CopyPropertiesFrom(ModelLocator.Current.Settings.AnsiContentTypeEngineSettings); - return engine; - } +/// +/// Implements text/plain and text/ansi file type support. Uses libvt100 to parse/render ANSI formatted +/// text. +/// Relies on libvt100 for word/line wrapping (whereas TextCte implements our own wrapping logic for +/// text files). +/// NOTE: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +/// +public class AnsiCte : ContentTypeEngineBase, IDisposable { + private static readonly string?[]? _supportedContentTypes = ["text/plain", "text/ansi"]; + private Font? _cachedFont; - // All of the lines of the text file, after reflow/line-wrap - private DynamicScreen _screen; - public IAnsiDecoderClient DecoderClient { get => (IAnsiDecoderClient)_screen; } + private SizeF _charSize; - private SizeF _charSize; - private int _linesPerPage; + // Protected implementation of Dispose pattern. + // Flag: Has Dispose already been called? + private bool _disposed; - private float lineNumberWidth; - private int _minLineLen; - private System.Drawing.Font _cachedFont; + private float _lineNumberWidth; + private int _linesPerPage; + private int _minLineLen; - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } + // All the lines of the text file, after reflow/line-wrap + private DynamicScreen? _screen; - // Protected implementation of Dispose pattern. - // Flag: Has Dispose already been called? - private bool _disposed = false; + /// + /// ContentType identifier (shorthand for class name). + /// + public override string?[]? SupportedContentTypes => _supportedContentTypes; - private void Dispose(bool disposing) { - LogService.TraceMessage($"disposing: {disposing}"); + public IAnsiDecoderClient? DecoderClient => _screen!; - if (_disposed) { - return; - } + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } - if (disposing) { - if (_cachedFont != null) { - _cachedFont.Dispose(); - } + public static AnsiCte Create() { + var engine = new AnsiCte(); + // Populate it with the common settings + engine.CopyPropertiesFrom(ModelLocator.Current.Settings.AnsiContentTypeEngineSettings); + return engine; + } - _screen = null; - } - _disposed = true; - } + private void Dispose(bool disposing) { + LogService.TraceMessage($"disposing: {disposing}"); - // TODO: Pass doc around by ref to save copies - public override async Task SetDocumentAsync(string doc) { - Document = doc; - return await Task.FromResult(true); + if (_disposed) { + return; } - /// - /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage). - /// - /// - /// - public override async Task RenderAsync(System.Drawing.Printing.PrinterResolution printerResolution, EventHandler reflowProgress) { - LogService.TraceMessage(); - - if (Document == null) { - throw new ArgumentNullException("document can't be null for Render"); + if (disposing) { + if (_cachedFont != null) { + _cachedFont.Dispose(); } - var dpiX = printerResolution.X; - var dpiY = printerResolution.Y; + _screen = null; + } - // BUGBUG: On Windows we can use the printer's resolution to be more accurate. But on Linux we - // have to use 96dpi. See https://github.com/mono/libgdiplus/issues/623, etc... - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || dpiX < 0 || dpiY < 0) { - dpiX = dpiY = 96; - } + _disposed = true; + } - // Create a representative Graphcis used for determining glyph metrics. - using var bitmap = new Bitmap(1, 1); - bitmap.SetResolution(dpiX, dpiY); - var g = Graphics.FromImage(bitmap); - g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" + // TODO: Pass doc around by ref to save copies + public override async Task SetDocumentAsync(string doc) { + Document = doc; + return await Task.FromResult(true); + } - // Calculate the number of lines per page; first we need our font. Keep it around. - _cachedFont = new System.Drawing.Font(ContentSettings.Font.Family, ContentSettings.Font.Size / 72F * 96, ContentSettings.Font.Style, GraphicsUnit.Pixel); // World? - Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, _cachedFont.Style); + /// + /// Renders source file to a libvt100 canvas. + /// Returns total count of pages. Sets any local page-size related values (e.g. linesPerPage). + /// + /// + /// + /// + public override async Task RenderAsync(PrinterResolution? printerResolution, EventHandler? reflowProgress) { + LogService.TraceMessage(); + + if (Document == null) { + throw new InvalidOperationException("Document can't be null for RenderAsync"); + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - _cachedFont.Dispose(); - _cachedFont = new System.Drawing.Font(ContentSettings.Font.Family, ContentSettings.Font.Size, ContentSettings.Font.Style, GraphicsUnit.Point); - Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, _cachedFont.Style); - g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" - } + var dpiX = printerResolution!.X; + var dpiY = printerResolution.Y; - _charSize = MeasureString(g, _cachedFont, "W"); + // BUGBUG: On Windows we can use the printer's resolution to be more accurate. But on Linux we + // have to use 96dpi. See https://github.com/mono/libgdiplus/issues/623, etc... + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || dpiX < 0 || dpiY < 0) { + dpiX = dpiY = 96; + } - if (PageSize.Height < (int)Math.Floor(_charSize.Height)) { - throw new InvalidOperationException("The line height is greater than page height."); - } + // Create a representative Graphics used for determining glyph metrics. + using var bitmap = new Bitmap(1, 1); + bitmap.SetResolution(dpiX, dpiY); + var g = Graphics.FromImage(bitmap); + g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" + + // Calculate the number of lines per page; first we need our font. Keep it around. + _cachedFont = new Font(ContentSettings!.Font.Family, ContentSettings.Font.Size / 72F * 96, + ContentSettings.Font.Style, GraphicsUnit.Pixel); // World? + Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, + _cachedFont.Style); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + _cachedFont.Dispose(); + _cachedFont = new Font(ContentSettings.Font.Family, ContentSettings.Font.Size, ContentSettings.Font.Style, + GraphicsUnit.Point); + Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, + _cachedFont.Style); + g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" + } - // Round down # of lines per page to ensure lines don't clip on bottom - _linesPerPage = (int)Math.Floor(PageSize.Height / (int)Math.Floor(_charSize.Height)); + _charSize = MeasureString(g, _cachedFont, "i"); - // 3 digits + 1 wide - Will support 999 lines before line numbers start to not fit - // TODO: Make line number width dynamic - // Note, MeasureString is actually dependent on lineNumberWidth! - lineNumberWidth = ContentSettings.LineNumbers ? _charSize.Width * 4 : 0; + if (PageSize.Height < (int)Math.Floor(_charSize.Height)) { + throw new InvalidOperationException("The line height is greater than page height."); + } - // This is the shortest line length (in chars) that we think we'll see. - // This is used as a performance optimization (probably premature) and - // could be 0 with no functional change. - _minLineLen = (int)((PageSize.Width - lineNumberWidth) / (int)Math.Floor(_charSize.Width)); + // Round down # of lines per page to ensure lines don't clip on bottom + _linesPerPage = (int)Math.Floor(PageSize.Height / (int)Math.Floor(_charSize.Height)); - // Note, MeasureLines may increment numPages due to form feeds and line wrapping - _screen = new DynamicScreen(_minLineLen); - IAnsiDecoder vt100 = new AnsiDecoder(); - vt100.Encoding = Encoding; - vt100.Subscribe(_screen); + // 3 digits + 1 wide - Will support 999 lines before line numbers start to not fit + // TODO: Make line number width dynamic + // Note, MeasureString is actually dependent on lineNumberWidth! + _lineNumberWidth = ContentSettings.LineNumbers ? _charSize.Width * 4 : 0; - var bytes = vt100.Encoding.GetBytes(Document); - if (bytes != null && bytes.Length > 0) { - vt100.Input(bytes); - } + // This is the shortest line length (in chars) that we think we'll see. + _minLineLen = (int)((PageSize.Width - _lineNumberWidth) / _charSize.Width); + + _screen = new DynamicScreen(_minLineLen); + IAnsiDecoder vt100 = new AnsiDecoder(); + vt100.Encoding = Encoding; + vt100.Subscribe(_screen); - var n = (int)Math.Ceiling(_screen.Lines.Count / (double)_linesPerPage); - Log.Debug("Rendered {pages} pages of {linesperpage} lines per page, for a total of {lines} lines.", n, _linesPerPage, _screen.Lines.Count); - return await Task.FromResult(n); + var bytes = vt100.Encoding.GetBytes(Document); + if (bytes is { Length: > 0 }) { + vt100.Input(bytes); } - private SizeF MeasureString(Graphics g, System.Drawing.Font font, string text) { - return MeasureString(g, text, font, out var charsFitted, out var linesFilled); + var n = (int)Math.Ceiling(_screen.Lines.Count / (double)_linesPerPage); + Log.Debug("Rendered {pages} pages of {linesperpage} lines per page, for a total of {lines} lines.", n, + _linesPerPage, _screen.Lines.Count); + return await Task.FromResult(n); + } + + private SizeF MeasureString(Graphics g, Font? font, string text) { + return MeasureString(g, text, font, out var charsFitted, out var linesFilled); + } + + /// + /// Measures how much width a string will take, given current page settings + /// + /// + /// + /// + /// + /// + private SizeF MeasureString(Graphics? g, string text, Font? font, out int charsFitted, out int linesFilled) { + if (g is null) { + // define context used for determining glyph metrics. + using var bitmap = new Bitmap(1, 1); + g = Graphics.FromImage(bitmap); + g.PageUnit = GraphicsUnit.Display; } - /// - /// Measures how much width a string will take, given current page settings - /// - /// - /// - /// - /// - /// - private SizeF MeasureString(Graphics g, string text, System.Drawing.Font font, out int charsFitted, out int linesFilled) { - if (g is null) { - // define context used for determining glyph metrics. - using var bitmap = new Bitmap(1, 1); - g = Graphics.FromImage(bitmap); - //g = Graphics.FromHwnd(PrintPreview.Instance.Handle); - g.PageUnit = GraphicsUnit.Display; - } + g.TextRenderingHint = TextRenderingHint; - g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; + // determine width + var fontHeight = (int)Math.Floor(_charSize.Height); + // Use page settings including lineNumberWidth + var proposedSize = new SizeF(PageSize.Width, + (int)Math.Floor(_charSize.Height) + ((int)Math.Floor(_charSize.Height) / 2)); + var size = g.MeasureString(text, font!, proposedSize, StringFormat, out charsFitted, out linesFilled); - // determine width - var fontHeight = (int)Math.Floor(_charSize.Height); - // Use page settings including lineNumberWidth - var proposedSize = new SizeF(PageSize.Width, (int)Math.Floor(_charSize.Height) + ((int)Math.Floor(_charSize.Height) / 2)); - var size = g.MeasureString(text, font, proposedSize, ContentTypeEngineBase.StringFormat, out charsFitted, out linesFilled); + // TODO: HACK to work around MeasureString not working right on Linux + //if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + // linesFilled = 1; + return size; + } - // TODO: HACK to work around MeasureString not working right on Linux - //if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - // linesFilled = 1; - return size; + /// + /// Paints a single page by iterating through the relevant part of the libvt100 screen (_screen), + /// formatting each as needed. + /// + /// Graphics with 0,0 being the origin of the Page + /// Page number to print + public override void PaintPage(Graphics? g, int pageNum) { + LogService.TraceMessage($"{pageNum}"); + if (_screen is null) { + Log.Debug("_screen must not be null"); + return; } - /// - /// Paints a single page. - /// - /// Graphics with 0,0 being the origin of the Page - /// Page number to print - public override void PaintPage(Graphics g, int pageNum) { - LogService.TraceMessage($"{pageNum}"); - if (_screen == null) { - Log.Debug("_ansiDocument must not be null"); - return; - } + g.TextRenderingHint = TextRenderingHint; - g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; + // Paint each line of the file + var firstLineOnPage = _linesPerPage * (pageNum - 1); + int i; + for (i = firstLineOnPage; i < firstLineOnPage + _linesPerPage && i < _screen.Lines.Count; i++) { + var yPos = (i - (_linesPerPage * (pageNum - 1))) * (int)Math.Floor(_charSize.Height); - // Paint each line of the file - var firstLineOnPage = _linesPerPage * (pageNum - 1); - int i; - for (i = firstLineOnPage; i < firstLineOnPage + _linesPerPage && i < _screen.Lines.Count; i++) { - var yPos = (i - (_linesPerPage * (pageNum - 1))) * (int)Math.Floor(_charSize.Height); - var x = ContentSettings.LineNumberSeparator ? (int)(lineNumberWidth - 6 - MeasureString(g, _cachedFont, $"{_screen.Lines[i].LineNumber}").Width) : 0; - // Line #s + // Right align line number + // TODO: Why "6". Make it a setting? + var x = ContentSettings!.LineNumberSeparator + ? (int)(_lineNumberWidth - 6 - MeasureString(g, _cachedFont, $"{_screen.Lines[i].LineNumber}").Width) + : 0; + + // Line #s + if (ContentSettings.LineNumbers && _lineNumberWidth != 0) { if (_screen.Lines[i].LineNumber > 0) { - if (ContentSettings.LineNumbers && lineNumberWidth != 0) { - // TOOD: Figure out how to make the spacig around separator more dynamic - // TODO: Allow a different (non-monospace) font for line numbers - g.DrawString($"{_screen.Lines[i].LineNumber}", _cachedFont, Brushes.Gray, x, yPos, ContentTypeEngineBase.StringFormat); - } + // TODO: Figure out how to make the spacing around separator more dynamic + // TODO: Allow a different (non-monospace) font for line numbers + g.DrawString($"{_screen.Lines[i].LineNumber}", _cachedFont!, Brushes.Gray, x, yPos, StringFormat); } // Line # separator (draw even if there's no line number, but stop at end of doc) // TODO: Support setting color of line #s and separator - if (ContentSettings.LineNumbers && ContentSettings.LineNumberSeparator && lineNumberWidth != 0) { - g.DrawLine(Pens.Gray, lineNumberWidth - 2, yPos, lineNumberWidth - 2, yPos + (int)Math.Floor(_charSize.Height)); + if (ContentSettings.LineNumberSeparator) { + g.DrawLine(Pens.Gray, _lineNumberWidth - 4, yPos, _lineNumberWidth - 4, + yPos + (int)Math.Floor(_charSize.Height)); } + } - // Text - float xPos = lineNumberWidth; - foreach (var run in _screen.Lines[i].Runs) { - System.Drawing.Font font = _cachedFont; - if (!ContentSettings.DisableFontStyles && run.Attributes.Bold) { - if (run.Attributes.Italic) { - font = new System.Drawing.Font(_cachedFont.FontFamily, _cachedFont.SizeInPoints, FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Point); - } - else { - font = new System.Drawing.Font(_cachedFont.FontFamily, _cachedFont.SizeInPoints, FontStyle.Bold, GraphicsUnit.Point); - } - } - else if (!ContentSettings.DisableFontStyles && run.Attributes.Italic) { - font = new System.Drawing.Font(_cachedFont.FontFamily, _cachedFont.SizeInPoints, FontStyle.Italic, GraphicsUnit.Point); - } - var fg = Color.Black; - if (run.Attributes.ForegroundColor != Color.White) - fg = run.Attributes.ForegroundColor; - - var text = _screen.Lines[i].Text[run.Start..(run.Start + run.Length)]; + // Text + var xPos = _lineNumberWidth; + foreach (var run in _screen.Lines[i].Runs) { + if (ContentSettings.Diagnostics) { + g.DrawRectangle(Pens.Red, _lineNumberWidth, yPos, PageSize.Width - _lineNumberWidth, + (int)Math.Floor(_charSize.Height)); + } - for (var c = 0; c < text.Length; c++) { - g.DrawString($"{text[c]}", font, new SolidBrush(fg), xPos + (c * (int)Math.Floor(_charSize.Width)), yPos, ContentTypeEngineBase.StringFormat); + var font = _cachedFont; + if (!ContentSettings.DisableFontStyles && run.Attributes.Bold) { + if (run.Attributes.Italic) { + font = new Font(_cachedFont!.FontFamily, _cachedFont.SizeInPoints, + FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Point); } - - if (ContentSettings.Diagnostics && run.HasTab) { - var pen = new Pen(Color.Red, 1); - g.DrawRectangle(pen, xPos, yPos, text.Length * (int)Math.Floor(_charSize.Width), (int)Math.Floor(_charSize.Height)); - g.DrawString($"→", font, new SolidBrush(Color.DarkGray), xPos, yPos, ContentTypeEngineBase.StringFormat); + else { + font = new Font(_cachedFont!.FontFamily, _cachedFont.SizeInPoints, FontStyle.Bold, + GraphicsUnit.Point); } - xPos += (int)Math.Floor(_charSize.Width) * text.Length; - - //var proposedSize = new SizeF(PageSize.Width, _lineHeight); - //var size = g.MeasureString(text, font, proposedSize, ContentTypeEngineBase.StringFormat, out int charsFitted, out int linesFilled); - //g.DrawString(text, font, new SolidBrush(fg), xPos, yPos, ContentTypeEngineBase.StringFormat); + } + else if (!ContentSettings.DisableFontStyles && run.Attributes.Italic) { + font = new Font(_cachedFont!.FontFamily, _cachedFont.SizeInPoints, FontStyle.Italic, + GraphicsUnit.Point); + } - //xPos += size.Width; + var fg = Color.Black; + if (run.Attributes.ForegroundColor != Color.White) { + fg = run.Attributes.ForegroundColor; } + + var text = _screen.Lines[i].Text[run.Start..(run.Start + run.Length)]; + g.DrawString(text, font, new SolidBrush(fg), xPos, yPos, StringFormat); + var size = MeasureString(g, font, text); + if (ContentSettings.Diagnostics) { - g.DrawRectangle(Pens.Red, lineNumberWidth, yPos, PageSize.Width - lineNumberWidth, (int)Math.Floor(_charSize.Height)); + g.DrawRectangle(new Pen(Color.Orange, 1), xPos, yPos, size.Width, size.Height); + } + + if (ContentSettings.Diagnostics && run.HasTab) { + var pen = new Pen(Color.Red, 1); + g.DrawRectangle(pen, xPos, yPos, text.Length * (int)Math.Floor(_charSize.Width), + (int)Math.Floor(_charSize.Height)); + g.DrawString("\u2192", font, new SolidBrush(Color.DarkGray), xPos, yPos, StringFormat); } + + xPos += size.Width; } + } #if CURSOR if (_screen.CursorPosition.Y >= firstLineOnPage && _screen.CursorPosition.Y < firstLineOnPage + _linesPerPage) { @@ -272,12 +290,12 @@ public override void PaintPage(Graphics g, int pageNum) { var x = ContentSettings.LineNumberSeparator ? (int)(lineNumberWidth) : 0; var width = MeasureString(g, _cachedFont, text).Width; - RectangleF rect = new RectangleF(x + _screen.CursorPosition.X * width, _screen.CursorPosition.Y * _lineHeight, width, _lineHeight); + RectangleF rect = + new RectangleF(x + _screen.CursorPosition.X * width, _screen.CursorPosition.Y * _lineHeight, width, _lineHeight); //g.DrawString(text, _cachedFont, new SolidBrush(Color.Blue), rect, StringFormat); g.DrawRectangle(Pens.Black, x + _screen.CursorPosition.X * width, _screen.CursorPosition.Y * _lineHeight, width, _lineHeight); } #endif - Log.Debug("Painted {lineOnPage} lines.", i - 1); - } + Log.Debug("Painted {lineOnPage} lines.", i - 1); } } diff --git a/src/WinPrint.Core/ContentTypeEngines/ContentTypeEngineBase.cs b/src/WinPrint.Core/ContentTypeEngines/ContentTypeEngineBase.cs index 6297165..d8991bf 100644 --- a/src/WinPrint.Core/ContentTypeEngines/ContentTypeEngineBase.cs +++ b/src/WinPrint.Core/ContentTypeEngines/ContentTypeEngineBase.cs @@ -1,4 +1,4 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint using System; @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Drawing; +using System.Drawing.Printing; using System.Drawing.Text; using System.Globalization; using System.IO; @@ -14,292 +15,324 @@ using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; -using Serilog; using WinPrint.Core.Models; using WinPrint.Core.Services; -namespace WinPrint.Core.ContentTypeEngines { +namespace WinPrint.Core.ContentTypeEngines; + +/// +/// Base class for Content/File Type Engines (CTEs) +/// +public abstract class ContentTypeEngineBase : ModelBase, INotifyPropertyChanged { + // These can be overidden in Settings + //public static string DefaultContentType = "text/plain"; + public static string DefaultCteClassName = "AnsiCte"; + public static string DefaultSyntaxHighlighterCteNameClassName = "AnsiCte"; + private static readonly string?[]? _supportedContentTypes = null; + /// - /// Base class for Content/File Type Engines (CTEs) + /// These are the global StringFormat settings; set here to ensure all rendering and measuring uses same settings /// - public abstract class ContentTypeEngineBase : ModelBase, INotifyPropertyChanged { - public static string DefaultContentType = "text/plain"; - public static string DefaultCteClassName = "AnsiCte"; - public static string DefaultSyntaxHighlighterCteNameClassName = "AnsiCte"; - - public new event PropertyChangedEventHandler PropertyChanged; - protected new void OnPropertyChanged([CallerMemberName] string propertyName = null) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + public static readonly StringFormat StringFormat = new(StringFormat.GenericTypographic) { + FormatFlags = StringFormatFlags.NoClip | + StringFormatFlags.LineLimit | + //StringFormatFlags.FitBlackBox | + StringFormatFlags.MeasureTrailingSpaces | + StringFormatFlags.DisplayFormatControl, + Alignment = StringAlignment.Near, + LineAlignment = StringAlignment.Near, + Trimming = StringTrimming.None + }; + + public static readonly TextRenderingHint TextRenderingHint = TextRenderingHint.ClearTypeGridFit; + private ContentSettings? _contentSettings; + internal string? _document; + private Encoding? _encoding = Encoding.Default; - protected new bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) { - if (EqualityComparer.Default.Equals(field, value)) { - return false; - } + /// + /// Calculated page size. Set by Sheet view model. + /// + public SizeF PageSize { get; set; } + + /// + /// ContentType identifier (shorthand for class name). + /// + public virtual string?[]? SupportedContentTypes => _supportedContentTypes; + + /// + /// Holds content settings for the CTE. These are used as defaults when a Sheet does not + /// specify any. + /// + public ContentSettings? ContentSettings { get => _contentSettings; set => SetField(ref _contentSettings, value); } + + /// + /// The contents of the file to be printed. + /// + [JsonIgnore] + public string? Document { + get => _document; + set => + //LogService.TraceMessage($"Document is {document.Length} chars."); + SetField(ref _document, value); + } + + /// + /// The contents encoding of the file to be printed. + /// + [JsonIgnore] + public Encoding? Encoding { get => _encoding; set => SetField(ref _encoding, value); } + + public new event PropertyChangedEventHandler? PropertyChanged; - field = value; - OnPropertyChanged(propertyName); - OnSettingsChanged(true); - return true; + protected new void OnPropertyChanged([CallerMemberName] string propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected new bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) { + if (EqualityComparer.Default.Equals(field, value)) { + return false; } - // if bool is true, reflow. Otherwise just paint - public event EventHandler SettingsChanged; - protected void OnSettingsChanged(bool reflow) { - SettingsChanged?.Invoke(this, reflow); + field = value; + OnPropertyChanged(propertyName); + OnSettingsChanged(true); + return true; + } + + // if bool is true, reflow. Otherwise, just paint + public event EventHandler? SettingsChanged; + + protected void OnSettingsChanged(bool reflow) { + SettingsChanged?.Invoke(this, reflow); + } + + /// + /// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class + /// + public static ICollection GetDerivedClassesCollection() { + var objects = new List(); + foreach (var type in typeof(ContentTypeEngineBase).Assembly.GetTypes() + .Where(myType => + myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(ContentTypeEngineBase)))) { + objects.Add((ContentTypeEngineBase)Activator.CreateInstance(type)!); } - /// - /// ContentType identifier (shorthand for class name). - /// - public virtual string[] SupportedContentTypes => _supportedContentTypes; - private static readonly string[] _supportedContentTypes = null; - - /// - /// Calculated page size. Set by Sheet view model. - /// - public SizeF PageSize; - - /// - /// Holds content settings for the CTE. These are used as defaults when a Sheet does not - /// specify any. - /// - public ContentSettings ContentSettings { get => contentSettings; set => SetField(ref contentSettings, value); } - private ContentSettings contentSettings;// = new ContentSettings(); - - /// - /// The contents of the file to be printed. - /// - [JsonIgnore] - public string Document { - get => _document; set => - //LogService.TraceMessage($"Document is {document.Length} chars."); - SetField(ref _document, value); + return objects; + } + + /// + /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage). + /// + /// + /// + /// + /// Number of sheets. + public virtual async Task RenderAsync(PrinterResolution? printerResolution, + EventHandler? reflowProgress) { + if (Document == null) { + throw new InvalidOperationException("Document can't be null for Render"); } - internal string _document = null; - - /// - /// The contents encdding of the file to be printed. - /// - [JsonIgnore] - public Encoding Encoding { get => _encoding; set => SetField(ref _encoding, value); } - private Encoding _encoding = Encoding.Default; - - /// - /// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class - /// - public static ICollection GetDerivedClassesCollection() { - var objects = new List(); - foreach (var type in typeof(ContentTypeEngineBase).Assembly.GetTypes() - .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(ContentTypeEngineBase)))) { - objects.Add((ContentTypeEngineBase)Activator.CreateInstance(type)); - } - return objects; + + return await Task.FromResult(0); + } + + /// + /// Paints a single page + /// + /// Graphics with 0,0 being the origin of the Page + /// Page number to print + public abstract void PaintPage(Graphics g, int pageNum); + + /// + /// Creates the appropriate Content Type Engine instance given a content type string. + /// + /// + /// ContentEngine, ContentType, Language + public static (ContentTypeEngineBase? cte, string languageId, string? language) CreateContentTypeEngine( + string? contentType) { + LogService.TraceMessage(); + + Debug.Assert(!string.IsNullOrEmpty(contentType)); + Debug.Assert(ModelLocator.Current.FileTypeMapping != null); + Debug.Assert(ModelLocator.Current.FileTypeMapping.ContentTypes != null); + + // If contentType matches one of our CTE Names, this will succeed. + var cte = GetDerivedClassesCollection() + .FirstOrDefault(c => contentType.Equals(c.GetType().Name, StringComparison.OrdinalIgnoreCase)); + var language = string.Empty; + var languageId = string.Empty; + + if (cte != null) { + languageId = cte.SupportedContentTypes![0]; + language = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(lang => + lang.Id.Equals(languageId, StringComparison.OrdinalIgnoreCase))!.Title; + return (cte, languageId, language); } - /// - /// These are the global StringFormat settings; set here to ensure all rendering and measuring uses same settings - /// - public static readonly StringFormat StringFormat = new StringFormat(StringFormat.GenericTypographic) { - FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.LineLimit | //StringFormatFlags.FitBlackBox | - StringFormatFlags.DisplayFormatControl | StringFormatFlags.MeasureTrailingSpaces, - Alignment = StringAlignment.Near, - LineAlignment = StringAlignment.Near, - Trimming = StringTrimming.None - }; - public static readonly TextRenderingHint TextRenderingHint = TextRenderingHint.ClearTypeGridFit; - - /// - /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage). - /// - /// - /// Number of sheets. - public virtual async Task RenderAsync(System.Drawing.Printing.PrinterResolution printerResolution, EventHandler reflowProgress) { - if (Document == null) { - throw new ArgumentNullException("Document can't be null for Render"); + // { + // "id": "text/ansi", + // "aliases": [ + // "ansi", + // "term" + // ], + // "title": "ANSI Encoded", + // "extensions": [ + // "*.an", + // "*.ansi", + // "*.ans" + // }, + // Is it a file extension? (*.an) + + var extension = ModelLocator.Current.FileTypeMapping.ContentTypes + .FirstOrDefault(l => l.Extensions.Any(i => + CultureInfo.CurrentCulture.CompareInfo.Compare(i, contentType, CompareOptions.IgnoreCase) == 0)); + if (extension != null && !string.IsNullOrEmpty(extension.Id)) { + // Is Id directly supported by a Cte? + cte = GetDerivedClassesCollection().FirstOrDefault(c => c.SupportedContentTypes.Contains(extension.Id)); + if (cte != null) { + return (cte, extension.Id, extension.Title); } - return await Task.FromResult(0); + // It is a language. Needs to be Syntax Highlighted. Use the default Syntax Highlighter CTE + languageId = extension.Id; + language = extension.Title; } + else { + // Is it a content type (Landuage.Id)? (text/ansi) + var lang = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => + l.Id.Equals(contentType, StringComparison.OrdinalIgnoreCase)); + if (lang != null) { + languageId = lang.Id; + language = lang.Title; + } - /// - /// Paints a single page - /// - /// Graphics with 0,0 being the origin of the Page - /// Page number to print - public abstract void PaintPage(Graphics g, int pageNum); - - /// - /// Creates the appropriate Content Type Engine instance given a content type string. - /// - /// - /// ContentEngine, ContentType, Language - public static (ContentTypeEngineBase cte, string languageId, string Language) CreateContentTypeEngine(string contentType) { - Debug.Assert(!string.IsNullOrEmpty(contentType)); - Debug.Assert(ModelLocator.Current.FileTypeMapping != null); - Debug.Assert(ModelLocator.Current.FileTypeMapping.ContentTypes != null); - - // If contentType matches one of our CTE Names, this will succeed. - ContentTypeEngineBase cte = GetDerivedClassesCollection().FirstOrDefault(c => contentType.Equals(c.GetType().Name, StringComparison.OrdinalIgnoreCase)); - string language = string.Empty; - string languageId = string.Empty; + // Is it a language Title? + lang = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => + l.Title.Equals(contentType, StringComparison.OrdinalIgnoreCase)); + if (lang != null) { + languageId = lang.Id; + language = lang.Title; + } - if (cte != null) { - languageId = cte.SupportedContentTypes[0]; - language = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(lang => lang.Id.Equals(languageId, StringComparison.OrdinalIgnoreCase)).Title; - return (cte, languageId, language); + // Is it a language name found in a Language alias? (ansi) + lang = ModelLocator.Current.FileTypeMapping.ContentTypes + .FirstOrDefault(l => l.Aliases.Any(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, contentType, + CompareOptions.IgnoreCase) == 0)); + if (lang != null) { + languageId = lang.Id; + language = lang.Title; } - // { - // "id": "text/ansi", - // "aliases": [ - // "ansi", - // "term" - // ], - // "title": "ANSI Encoded", - // "extensions": [ - // "*.an", - // "*.ansi", - // "*.ans" - // }, - // Is it a file extension? (*.an) - - var extension = ModelLocator.Current.FileTypeMapping.ContentTypes - .FirstOrDefault(l => l.Extensions - .Where(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, contentType, CompareOptions.IgnoreCase) == 0).Count() > 0); - if (extension != null && !string.IsNullOrEmpty(extension.Id)) { - // Is Id directly supported by a Cte? - cte = GetDerivedClassesCollection().FirstOrDefault(c => c.SupportedContentTypes.Contains(extension.Id)); + if (!string.IsNullOrEmpty(language) && !string.IsNullOrEmpty(languageId)) { + // Is the Id supported directly (e.g. text/html is supported directly by HtmlCte) + // If supported by multiple, pick the default. + var id = languageId; + var ctes = GetDerivedClassesCollection() + .Where(c => c!.SupportedContentTypes!.Contains(id.ToLower())); + var contentTypeEngineBases = ctes as ContentTypeEngineBase[] ?? ctes.ToArray(); + cte = contentTypeEngineBases.Count() > 1 + ? contentTypeEngineBases.First(c => + c.GetType().Name == ModelLocator.Current.Settings.DefaultCteClassName) + : contentTypeEngineBases.FirstOrDefault(); + if (cte != null) { - return (cte, extension.Id, extension.Title); + return (cte, languageId, + ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => + l.Id.Equals(languageId, StringComparison.OrdinalIgnoreCase))!.Title); } // It is a language. Needs to be Syntax Highlighted. Use the default Syntax Highlighter CTE - languageId = extension.Id; - language = extension.Title; + //languageId = lang.Id; + //language = lang.Title; } - else { - // Is it a content type (Landuage.Id)? (text/ansi) - var lang = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => l.Id.Equals(contentType, StringComparison.OrdinalIgnoreCase)); - if (lang != null) { - languageId = lang.Id; - language = lang.Title; - } + } - // Is it a language Title? - lang = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => l.Title.Equals(contentType, StringComparison.OrdinalIgnoreCase)); - if (lang != null) { - languageId = lang.Id; - language = lang.Title; - } + if (string.IsNullOrEmpty(languageId)) { + // Didn't find a content type so use default CTE + cte = GetDerivedClassesCollection().FirstOrDefault(c => + c!.SupportedContentTypes!.Contains(ModelLocator.Current.Settings.DefaultContentType)); + languageId = cte.SupportedContentTypes[0]; + language = ModelLocator.Current.FileTypeMapping.ContentTypes + .FirstOrDefault(l => l.Id.Equals(languageId, StringComparison.OrdinalIgnoreCase)) + ?.Title; + } + else { + // It is a language. Needs to be Syntax Highlighted. Use the default Syntax Highlighter CTE + cte = GetDerivedClassesCollection().FirstOrDefault(c => + c.GetType().Name.Equals(DefaultSyntaxHighlighterCteNameClassName, StringComparison.OrdinalIgnoreCase)); + //if (string.IsNullOrWhiteSpace(language)) { + // language = contentType; + //} + } - // Is it a language name found in a Language alias? (ansi) - lang = ModelLocator.Current.FileTypeMapping.ContentTypes - .FirstOrDefault(l => l.Aliases - .Where(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, contentType, CompareOptions.IgnoreCase) == 0).Count() > 0); - if (lang != null) { - languageId = lang.Id; - language = lang.Title; - } + return (cte, languageId, language); + } - if (!string.IsNullOrEmpty(language) && !string.IsNullOrEmpty(languageId)) { - // Is the Id supported directly (e.g. text/html is supported directly by HtmlCte) - // If supported by muplitple, pick the default. - var ctes = GetDerivedClassesCollection().Where(c => c.SupportedContentTypes.Contains(languageId.ToLower())); - if (ctes != null) { - if (ctes.Count() > 1) { - cte = ctes.First(c => c.GetType().Name == DefaultCteClassName); - } - else { - cte = ctes.FirstOrDefault(); - } - if (cte != null) { - return (cte, languageId, ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => l.Id.Equals(languageId, StringComparison.OrdinalIgnoreCase)).Title); - } - } - - // It is a language. Needs to be Syntax Highlighted. Use the default Syntax Highlighter CTE - //languageId = lang.Id; - //language = lang.Title; - } - } + /// + /// Returns the content type name (e.g. "text/plain") given a file path. If the content type + /// cannot be determined from FilesAssociations the default of "text/plain" is returned. + /// + /// + /// The content type + public static string? GetContentType(string filePath) { + var contentType = ModelLocator.Current.Settings.DefaultContentType; + + if (string.IsNullOrEmpty(filePath)) { + return contentType; + } - if (string.IsNullOrEmpty(languageId)) { - // Didn't find a content type so use default CTE - cte = GetDerivedClassesCollection().FirstOrDefault(c => c.SupportedContentTypes.Contains(DefaultContentType)); - languageId = cte.SupportedContentTypes[0]; - language = ModelLocator.Current.FileTypeMapping.ContentTypes.FirstOrDefault(l => l.Id.Equals(languageId, StringComparison.OrdinalIgnoreCase)).Title; + // Expand path + filePath = Path.GetFullPath(filePath); + + // If there's a file extension get the content type from the file type association mapper + var ext = Path.GetExtension(filePath).ToLower(); + if (ext != string.Empty) { + // BUGBUG: This assumes all extensions in FilesAssociations are lowercase + if (ModelLocator.Current.FileTypeMapping.FilesAssociations.TryGetValue("*" + ext, out var ct)) { + // Now find Id in Languages + contentType = ModelLocator.Current.FileTypeMapping.ContentTypes + .Where(lang => lang.Id.Equals(ct, StringComparison.OrdinalIgnoreCase)) + .DefaultIfEmpty(new ContentType { Id = ModelLocator.Current.Settings.DefaultContentType }) + .First().Id; } else { - // It is a language. Needs to be Syntax Highlighted. Use the default Syntax Highlighter CTE - cte = GetDerivedClassesCollection().FirstOrDefault(c => c.GetType().Name.Equals(DefaultSyntaxHighlighterCteNameClassName, StringComparison.OrdinalIgnoreCase)); - //if (string.IsNullOrWhiteSpace(language)) { - // language = contentType; - //} + // No direct file extension, look in Languages + contentType = ModelLocator.Current.FileTypeMapping.ContentTypes + .Where(lang => lang.Extensions + .Count(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, "*" + ext, + CompareOptions.IgnoreCase) == + 0 || + CultureInfo.CurrentCulture.CompareInfo.Compare(i, ext, CompareOptions.IgnoreCase) == + 0) > 0) + .DefaultIfEmpty(new ContentType { Id = ModelLocator.Current.Settings.DefaultContentType }) + .First().Id; } - - return (cte, languageId, language); } - - /// - /// Returns the content type name (e.g. "text/plain") given a file path. If the content type - /// cannot be determiend from FilesAssocaitons the default of "text/plain" is returned. - /// - /// - /// The content type - public static string GetContentType(string filePath) { - var contentType = DefaultContentType; - - if (string.IsNullOrEmpty(filePath)) { - return contentType; - } - - // Expand path - filePath = Path.GetFullPath(filePath); - - // If there's a file extension get the content type from the file type association mapper - var ext = Path.GetExtension(filePath).ToLower(); - if (ext != string.Empty) { - // BUGBUG: This assumes all extensions in FilesAssociations are lowercase - if (ModelLocator.Current.FileTypeMapping.FilesAssociations.TryGetValue("*" + ext, out var ct)) { - // Now find Id in Languages - contentType = ModelLocator.Current.FileTypeMapping.ContentTypes - .Where(lang => lang.Id.Equals(ct, StringComparison.OrdinalIgnoreCase)) - .DefaultIfEmpty(new ContentType() { Id = DefaultContentType }) - .First().Id; - } - else { - // No direct file extension, look in Languages - contentType = ModelLocator.Current.FileTypeMapping.ContentTypes - .Where(lang => lang.Extensions.Where(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, "*" + ext, CompareOptions.IgnoreCase) == 0 || - CultureInfo.CurrentCulture.CompareInfo.Compare(i, ext, CompareOptions.IgnoreCase) == 0).Count() > 0) - .DefaultIfEmpty(new ContentType() { Id = DefaultContentType }) - .First().Id; - } + else { + // Empty means no extension (e.g. .\.ssh\config) - use filename + if (ModelLocator.Current.FileTypeMapping.FilesAssociations.TryGetValue("*" + Path.GetFileName(filePath), + out var ct)) { + contentType = ct; } else { - // Empty means no extension (e.g. .\.ssh\config) - use filename - if (ModelLocator.Current.FileTypeMapping.FilesAssociations.TryGetValue("*" + Path.GetFileName(filePath), out var ct)) { - contentType = ct; - } - else { - // No direct file extension, look in Languages - contentType = ModelLocator.Current.FileTypeMapping.ContentTypes - .Where(lang => lang.Extensions.Where(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, Path.GetFileName(filePath), CompareOptions.IgnoreCase) == 0).Count() > 0) - .DefaultIfEmpty(new ContentType() { Id = DefaultContentType }) - .First().Id; - } + // No direct file extension, look in Languages + contentType = ModelLocator.Current.FileTypeMapping.ContentTypes + .Where(lang => lang.Extensions.Count(i => CultureInfo.CurrentCulture.CompareInfo.Compare(i, + Path.GetFileName(filePath), + CompareOptions.IgnoreCase) == 0) > 0) + .DefaultIfEmpty(new ContentType { Id = ModelLocator.Current.Settings.DefaultContentType }) + .First().Id; } - - // If not text or html, is it a language? - //if (!contentType.Equals("text/plain") && !contentType.Equals("text/html")) { - // // Technically, because we got the assocation from FilesAssocation, this should always work - // if (!((List)ModelLocator.Current.Associations.Languages).Exists(lang => lang.Id == contentType)) - // contentType = "text/plain"; - //} - return contentType; } - public abstract Task SetDocumentAsync(string document); + // If not text or html, is it a language? + //if (!contentType.Equals("text/plain") && !contentType.Equals("text/html")) { + // // Technically, because we got the assocation from FilesAssocation, this should always work + // if (!((List)ModelLocator.Current.Associations.Languages).Exists(lang => lang.Id == contentType)) + // contentType = "text/plain"; + //} + return contentType; } + + public abstract Task SetDocumentAsync(string document); } diff --git a/src/WinPrint.Core/ContentTypeEngines/HtmlCte.cs b/src/WinPrint.Core/ContentTypeEngines/HtmlCte.cs index 9bc3563..ee35101 100644 --- a/src/WinPrint.Core/ContentTypeEngines/HtmlCte.cs +++ b/src/WinPrint.Core/ContentTypeEngines/HtmlCte.cs @@ -1,232 +1,206 @@ using System; using System.Drawing; +using System.Drawing.Printing; using System.IO; -using System.Text; using System.Threading.Tasks; using LiteHtmlSharp; -using Serilog; using WinPrint.Core.Models; using WinPrint.Core.Services; using WinPrint.LiteHtml; -namespace WinPrint.Core.ContentTypeEngines { +namespace WinPrint.Core.ContentTypeEngines; - /// - /// Implements generic HTML file type support. - /// - public class HtmlCte : ContentTypeEngineBase, IDisposable { - private static readonly string[] _supportedContentTypes = { "text/html" }; - public override string[] SupportedContentTypes => _supportedContentTypes; - - public static HtmlCte Create() { - var content = new HtmlCte(); - content.CopyPropertiesFrom(ModelLocator.Current.Settings.HtmlContentTypeEngineSettings); - return content; - } +/// +/// Implements generic HTML file type support. Uses LiteHtml. +/// +public class HtmlCte : ContentTypeEngineBase, IDisposable { + private static readonly string?[]? _supportedContentTypes = ["text/html"]; - internal GDIPlusContainer _litehtml; - internal bool _ready = false; // Loaded Rendered + // Protected implementation of Dispose pattern. + // Flag: Has Dispose already been called? + private bool _disposed; - //public HtmlFileContent() { - // type = "text/html"; - //} - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + private Bitmap? _htmlBitmap; + + internal GDIPlusContainer? _liteHtml; + internal bool _ready; // Loaded Rendered + public override string?[]? SupportedContentTypes => _supportedContentTypes; + + //public HtmlFileContent() { + // type = "text/html"; + //} + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public static HtmlCte Create() { + var content = new HtmlCte(); + content.CopyPropertiesFrom(ModelLocator.Current.Settings.HtmlContentTypeEngineSettings); + return content; + } + + private void Dispose(bool disposing) { + if (_disposed) { + return; } - // Protected implementation of Dispose pattern. - // Flag: Has Dispose already been called? - private bool _disposed = false; + if (disposing) { + //if (litehtml != null) + // litehtml.Dispose(); + } - private void Dispose(bool disposing) { - if (_disposed) { - return; - } + _disposed = true; + } - if (disposing) { - //if (litehtml != null) - // litehtml.Dispose(); - } - _disposed = true; + public override async Task SetDocumentAsync(string doc) { + _ready = false; + _liteHtml = null; + _htmlBitmap = null; + Document = doc; + return await Task.FromResult(true); + } + + /// + /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage) + /// + /// + /// + /// + public override async Task RenderAsync(PrinterResolution? printerResolution, EventHandler? reflowProgress) { + LogService.TraceMessage(); + + _ready = false; + + reflowProgress?.Invoke(this, "HtmlFileContent.RenderAsync"); + await base.RenderAsync(printerResolution, reflowProgress).ConfigureAwait(false); + + var width = (int)PageSize.Width; // (printerResolution.X * PageSize.Width / 100); + var height = (int)PageSize.Height; // (printerResolution.Y * PageSize.Height / 100); + LogService.TraceMessage( + $"HtmlFileContent.RenderAsync - Page size: {width}x{height} @ {printerResolution!.X}x{printerResolution.Y} dpi"); + + string css; + try { + // TODO: Make sure winprint.css is in the same dir as .config file once setup is impl + using var cssStream = new StreamReader("winprint.css"); + css = await cssStream.ReadToEndAsync(); + cssStream.Close(); + reflowProgress?.Invoke(this, "Read winprint.css"); + } + catch { + css = IncludedWinPrintCss.CssString; } - private Bitmap _htmlBitmap; + // BUGBUG: wihtout knowing the relative root path fo the html document we can't load any resources + var resources = new HtmlResources(""); + _liteHtml = new GDIPlusContainer(css, resources.GetResourceString, resources.GetResourceBytes) { + Diagnostics = ContentSettings!.Diagnostics, + Size = new LiteHtmlSize(width, 0), + PageHeight = height + }; + + _htmlBitmap = new Bitmap(width, height); + //htmlBitmap.SetResolution(printerResolution.X, printerResolution.Y); + var g = Graphics.FromImage(_htmlBitmap); + g.PageUnit = GraphicsUnit.Display; + g.TextRenderingHint = TextRenderingHint; + + //g.FillRectangle(Brushes.LightYellow, new Rectangle(0, 0, width, height)); + + LogService.TraceMessage( + $"HtmlFileContent.RenderAsync() Graphic is {_htmlBitmap.Width} x {_htmlBitmap.Height} @ {g.DpiX} x {g.DpiY} dpi. PageUnit = {g.PageUnit.ToString()}"); + _liteHtml.Graphics = g; + _liteHtml.StringFormat = StringFormat; + _liteHtml.Grayscale = ContentSettings.Grayscale; + _liteHtml.Darkness = ContentSettings.Darkness; + _liteHtml.PrintBackground = ContentSettings.PrintBackground; + + //Logging.TraceMessage("_liteHtml.Document.CreateFromString(document)"); + reflowProgress?.Invoke(this, "_liteHtml.Document.CreateFromString(document)"); + _liteHtml.Document.CreateFromString(Document); + //Logging.TraceMessage("back from _liteHtml.Document.CreateFromString(document)"); + reflowProgress?.Invoke(this, "back from _liteHtml.Document.CreateFromString(document)"); + + _liteHtml.Document.OnMediaChanged(); + + // TODO: Use return of Render() to get "best width" + var bestWidth = _liteHtml.Document.Render(width); + reflowProgress?.Invoke(this, "Done with Render"); + // Note, setting viewport does nothing + //_liteHtml.SetViewport(new LiteHtmlPoint(0, 0), new LiteHtmlSize(width, height)); + _liteHtml.Graphics = null; + + //Logging.TraceMessage($"_liteHtml {_liteHtml.Document.Width()}x{_liteHtml.Document.Height()} bestWidth = {bestWidth}"); + + var n = (_liteHtml.Document.Height() / height) + 1; + Logging.TraceMessage($"HtmlFileContent.RenderAsync - {n} pages."); + _ready = true; + return n; + } - public override async Task SetDocumentAsync(string doc) { - _ready = false; - _litehtml = null; - _htmlBitmap = null; - Document = doc; - return await Task.FromResult(true); + /// + /// Paints a single page + /// + /// Graphics with 0,0 being the origin of the Page + /// Page number to print + public override void PaintPage(Graphics g, int pageNum) { + //if (pageNum > NumPages) { + // Logging.TraceMessage($"HtmlFileContent.PaintPage({pageNum}) when NumPages is {NumPages}"); + // return; + //} + if (_liteHtml == null || _ready == false) { + //Log.Debug($"HtmlFileContent.PaintPage({pageNum}) when _liteHtml is not ready."); + return; } - /// - /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage) - /// - /// - /// - public override async Task RenderAsync(System.Drawing.Printing.PrinterResolution printerResolution, EventHandler reflowProgress) { - LogService.TraceMessage(); - - _ready = false; - - reflowProgress?.Invoke(this, "HtmlFileContent.RenderAsync"); - await base.RenderAsync(printerResolution, reflowProgress).ConfigureAwait(false) ; - - var width = (int)PageSize.Width;// (printerResolution.X * PageSize.Width / 100); - var height = (int)PageSize.Height;// (printerResolution.Y * PageSize.Height / 100); - LogService.TraceMessage($"HtmlFileContent.RenderAsync - Page size: {width}x{height} @ {printerResolution.X}x{printerResolution.Y} dpi"); - - string css; - try { - // TODO: Make sure winprint.css is in the same dir as .config file once setup is impl - using var cssStream = new StreamReader("winprint.css"); - css = await cssStream.ReadToEndAsync(); - cssStream.Close(); - reflowProgress?.Invoke(this, "Read winprint.css"); - } - catch { - css = IncludedWinPrintCss.CssString; - } - - // BUGBUG: wihtout knowing the relative root path fo the html document we can't load any resources - var resources = new HtmlResources(""); - _litehtml = new GDIPlusContainer(css, resources.GetResourceString, resources.GetResourceBytes) { - Diagnostics = ContentSettings.Diagnostics, - Size = new LiteHtmlSize(width, 0), - PageHeight = height - }; - - _htmlBitmap = new Bitmap(width, height); - //htmlBitmap.SetResolution(printerResolution.X, printerResolution.Y); - var g = Graphics.FromImage(_htmlBitmap); + SizeF pagesizeInPixels; + var state = g.Save(); + + if (g.PageUnit == GraphicsUnit.Display) { + // Print + pagesizeInPixels = new SizeF(PageSize.Width, PageSize.Height); + } + else { + // Preview + pagesizeInPixels = new SizeF(PageSize.Width / 100 * g.DpiX, PageSize.Height / 100 * g.DpiY); g.PageUnit = GraphicsUnit.Display; - g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; - - //g.FillRectangle(Brushes.LightYellow, new Rectangle(0, 0, width, height)); - - LogService.TraceMessage($"HtmlFileContent.RenderAsync() Graphic is {_htmlBitmap.Width} x {_htmlBitmap.Height} @ {g.DpiX} x {g.DpiY} dpi. PageUnit = {g.PageUnit.ToString()}"); - _litehtml.Graphics = g; - _litehtml.StringFormat = ContentTypeEngineBase.StringFormat; - _litehtml.Grayscale = ContentSettings.Grayscale; - _litehtml.Darkness = ContentSettings.Darkness; - _litehtml.PrintBackground = ContentSettings.PrintBackground; - - //Logging.TraceMessage("litehtml.Document.CreateFromString(document)"); - reflowProgress?.Invoke(this, "litehtml.Document.CreateFromString(document)"); - _litehtml.Document.CreateFromString(Document); - //Logging.TraceMessage("back from litehtml.Document.CreateFromString(document)"); - reflowProgress?.Invoke(this, "back from litehtml.Document.CreateFromString(document)"); - - _litehtml.Document.OnMediaChanged(); - - // TODO: Use return of Render() to get "best width" - var bestWidth = _litehtml.Document.Render(width); - reflowProgress?.Invoke(this, "Done with Render"); - // Note, setting viewport does nothing - //litehtml.SetViewport(new LiteHtmlPoint(0, 0), new LiteHtmlSize(width, height)); - _litehtml.Graphics = null; - - //Logging.TraceMessage($"Litehtml_DocumentSizeKnown {_litehtml.Document.Width()}x{_litehtml.Document.Height()} bestWidth = {bestWidth}"); - - var n = _litehtml.Document.Height() / height + 1; - Logging.TraceMessage($"HtmlFileContent.RenderAsync - {n} pages."); - _ready = true; - return n; } - /// - /// Gets next page from stream. Returns false if no more pages - /// - /// - /// - /// - // TODO: Deal with PageFeeds - // TODO: Support different forms of linewrap (truncate/clip, word, line) - // TODO: Support custom line wrap symbol - //private bool NextPage(StreamReader streamToPrint, out Page page) { - // page = new Page(containingSheet); - - // // define context used for determining glyph metrics. - // using Bitmap bitmap = new Bitmap(1, 1); - // Graphics g = Graphics.FromImage(bitmap); - // //g = Graphics.FromHwnd(PrintPreview.Instance.Handle); - // g.PageUnit = GraphicsUnit.Document; + //if (litehtml.Graphics == null) { + // Logging.TraceMessage($"new print job. Rendering again"); + // // This is a new print job. Re-render with new DPI + // litehtml.Graphics = g; + // litehtml.SetViewport(new LiteHtmlPoint(0, 0), new LiteHtmlSize(PageSize.Width, PageSize.Height)); + // int bestWidth = litehtml.Document.Render((int)PageSize.Width); + //} - // SizeF size = HtmlRender.MeasureGdiPlus(g, html, containingSheet.GetPageWidth()); + LogService.TraceMessage( + $"HtmlFileContent.PaintPage({pageNum} - {g.DpiX}x{g.DpiY} dpi. PageUnit = {g.PageUnit.ToString()})"); - // return false; - //} + _liteHtml.Graphics = g; + g.TextRenderingHint = TextRenderingHint; - /// - /// Paints a single page - /// - /// Graphics with 0,0 being the origin of the Page - /// Page number to print - public override void PaintPage(Graphics g, int pageNum) { - - //if (pageNum > NumPages) { - // Logging.TraceMessage($"HtmlFileContent.PaintPage({pageNum}) when NumPages is {NumPages}"); - // return; - //} - if (_litehtml == null || _ready == false) { - //Log.Debug($"HtmlFileContent.PaintPage({pageNum}) when litehtml is not ready."); - return; - } - SizeF pagesizeInPixels; - var state = g.Save(); - - if (g.PageUnit == GraphicsUnit.Display) { - // Print - pagesizeInPixels = new SizeF(PageSize.Width, PageSize.Height); - } - else { - // Preview - pagesizeInPixels = new SizeF(PageSize.Width / 100 * g.DpiX, PageSize.Height / 100 * g.DpiY); - g.PageUnit = GraphicsUnit.Display; - } - - - //if (litehtml.Graphics == null) { - // Logging.TraceMessage($"new print job. Rendering again"); - // // This is a new print job. Re-render with new DPI - // litehtml.Graphics = g; - // litehtml.SetViewport(new LiteHtmlPoint(0, 0), new LiteHtmlSize(PageSize.Width, PageSize.Height)); - // int bestWidth = litehtml.Document.Render((int)PageSize.Width); - //} - - LogService.TraceMessage($"HtmlFileContent.PaintPage({pageNum} - {g.DpiX}x{g.DpiY} dpi. PageUnit = {g.PageUnit.ToString()})"); - - _litehtml.Graphics = g; - g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; - - var yPos = (pageNum - 1) * (int)Math.Round(PageSize.Height); - - if (!ContentSettings.Diagnostics) { - g.SetClip(new Rectangle(0, 0, (int)Math.Round(PageSize.Width), (int)Math.Round(PageSize.Height))); - } - - var size = new LiteHtmlSize(Math.Round(PageSize.Width), Math.Round(PageSize.Height)); - _litehtml.Document.Draw(-0, -yPos, new position { - x = 0, - y = 0, - width = (int)size.Width, - height = (int)size.Height - }); - _litehtml.Graphics = null; - - //litehtml.SetViewport(new LiteHtmlPoint(0, yPos), new LiteHtmlSize(Math.Round(PageSize.Width), Math.Round(PageSize.Height))); - //litehtml.ScrollOffset = new LiteHtmlPoint(0, yP - // os); - //litehtml.Draw(); - - //g.DrawImage(htmlBitmap, 0, 0); - //g.DrawRectangle(Pens.Red, new Rectangle(0, 0, (int)Math.Round(PageSize.Width), (int)Math.Round(PageSize.Height))); - - g.Restore(state); + var yPos = (pageNum - 1) * (int)Math.Round(PageSize.Height); + if (!ContentSettings.Diagnostics) { + g.SetClip(new Rectangle(0, 0, (int)Math.Round(PageSize.Width), (int)Math.Round(PageSize.Height))); } + + var size = new LiteHtmlSize(Math.Round(PageSize.Width), Math.Round(PageSize.Height)); + _liteHtml.Document.Draw(-0, -yPos, + new position { x = 0, y = 0, width = (int)size.Width, height = (int)size.Height }); + _liteHtml.Graphics = null; + + //litehtml.SetViewport(new LiteHtmlPoint(0, yPos), new LiteHtmlSize(Math.Round(PageSize.Width), Math.Round(PageSize.Height))); + //litehtml.ScrollOffset = new LiteHtmlPoint(0, yP + // os); + //litehtml.Draw(); + + //g.DrawImage(htmlBitmap, 0, 0); + //g.DrawRectangle(Pens.Red, new Rectangle(0, 0, (int)Math.Round(PageSize.Width), (int)Math.Round(PageSize.Height))); + + g.Restore(state); } } diff --git a/src/WinPrint.Core/ContentTypeEngines/TextCte.cs b/src/WinPrint.Core/ContentTypeEngines/TextCte.cs index 61322b0..d050113 100644 --- a/src/WinPrint.Core/ContentTypeEngines/TextCte.cs +++ b/src/WinPrint.Core/ContentTypeEngines/TextCte.cs @@ -1,9 +1,10 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint using System; using System.Collections.Generic; using System.Drawing; +using System.Drawing.Printing; using System.IO; using System.Runtime.InteropServices; using System.Text; @@ -11,369 +12,397 @@ using Serilog; using WinPrint.Core.Models; using WinPrint.Core.Services; +using Font = System.Drawing.Font; -namespace WinPrint.Core.ContentTypeEngines { +namespace WinPrint.Core.ContentTypeEngines; - /// - /// This struct keeps track of which lines are 'real' and thus get a printed line number - /// and which are the result of wrapping. - /// - internal struct WrappedLine { - internal int nonWrappedLineNumber; // 0 if wrapped - internal string text; // contents of this part of the line +/// +/// This struct keeps track of which lines are 'real' and thus get a printed line number +/// and which are the result of wrapping. +/// +internal struct WrappedLine { + internal int _nonWrappedLineNumber; // 0 if wrapped + internal string _text; // contents of this part of the line #if DEBUG - internal string textNonWrapped; + internal string _textNonWrapped; #endif - } +} - /// - /// Implements text/plain file type support. - /// - public class TextCte : ContentTypeEngineBase, IDisposable { - private static readonly string[] _supportedContentTypes = { "text/plain" }; - /// - /// ContentType identifier (shorthand for class name). - /// - public override string[] SupportedContentTypes => _supportedContentTypes; - - public static TextCte Create() { - var engine = new TextCte(); - // Populate it with the common settings - engine.CopyPropertiesFrom(ModelLocator.Current.Settings.TextContentTypeEngineSettings); - return engine; - } +/// +/// Implements text/plain file type support with word/line wrapping. No formmating other +/// than line numbers. +/// +public class TextCte : ContentTypeEngineBase, IDisposable { + private static readonly string?[]? _supportedContentTypes = { "text/plain" }; + private Font? _cachedFont; - // All of the lines of the text file, after reflow/line-wrap - private List _wrappedLines; + // Protected implementation of Dispose pattern. + // Flag: Has Dispose already been called? + private bool _disposed; - private float _lineHeight; - private int _linesPerPage; + private float _lineHeight; + private int _linesPerPage; + private int _minLineLen; - private float lineNumberWidth; - private int _minLineLen; - private System.Drawing.Font _cachedFont; + // All of the lines of the text file, after reflow/line-wrap + private List? _wrappedLines; - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } + private float _lineNumberWidth; - // Protected implementation of Dispose pattern. - // Flag: Has Dispose already been called? - private bool _disposed = false; + /// + /// ContentType identifier (shorthand for class name). + /// + public override string?[]? SupportedContentTypes => _supportedContentTypes; - private void Dispose(bool disposing) { - LogService.TraceMessage($"disposing: {disposing}"); + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } - if (_disposed) { - return; - } + public static TextCte Create() { + var engine = new TextCte(); + // Populate it with the common settings + engine.CopyPropertiesFrom(ModelLocator.Current.Settings.TextContentTypeEngineSettings); + return engine; + } - if (disposing) { - if (_cachedFont != null) { - _cachedFont.Dispose(); - } + private void Dispose(bool disposing) { + LogService.TraceMessage($"disposing: {disposing}"); - _wrappedLines = null; - } - _disposed = true; + if (_disposed) { + return; } - // TODO: Pass doc around by ref to save copies - public override async Task SetDocumentAsync(string doc) { - Document = doc; - return await Task.FromResult(true); + if (disposing) { + if (_cachedFont != null) { + _cachedFont.Dispose(); + } + + _wrappedLines = null; } - /// - /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage). - /// - /// - /// - public override async Task RenderAsync(System.Drawing.Printing.PrinterResolution printerResolution, EventHandler reflowProgress) { - LogService.TraceMessage(); + _disposed = true; + } - if (Document == null) { - throw new ArgumentNullException("document can't be null for Render"); - } + // TODO: Pass doc around by ref to save copies + public override async Task SetDocumentAsync(string doc) { + Document = doc; + return await Task.FromResult(true); + } - var dpiX = printerResolution.X; - var dpiY = printerResolution.Y; + /// + /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage). + /// + /// + /// + /// + /// + public override async Task RenderAsync(PrinterResolution? printerResolution, EventHandler? reflowProgress) { + LogService.TraceMessage(); + + if (Document is null) { + throw new InvalidOperationException("Document can't be null for Render"); + } - // BUGBUG: On Windows we can use the printer's resolution to be more accurate. But on Linux we - // have to use 96dpi. See https://github.com/mono/libgdiplus/issues/623, etc... - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || dpiX < 0 || dpiY < 0) { - dpiX = dpiY = 96; - } + if (printerResolution is null) { + throw new ArgumentNullException(nameof(printerResolution)); + } - // Create a representative Graphcis used for determining glyph metrics. - using var bitmap = new Bitmap(1, 1); - bitmap.SetResolution(dpiX, dpiY); - var g = Graphics.FromImage(bitmap); - g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" + var dpiX = printerResolution.X; + var dpiY = printerResolution.Y; - // Calculate the number of lines per page; first we need our font. Keep it around. - _cachedFont = new System.Drawing.Font(ContentSettings.Font.Family, ContentSettings.Font.Size / 72F * 96, ContentSettings.Font.Style, GraphicsUnit.Pixel); // World? - Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, _cachedFont.Style); + // BUGBUG: On Windows we can use the printer's resolution to be more accurate. But on Linux we + // have to use 96dpi. See https://github.com/mono/libgdiplus/issues/623, etc... + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || dpiX < 0 || dpiY < 0) { + dpiX = dpiY = 96; + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - _cachedFont.Dispose(); - _cachedFont = new System.Drawing.Font(ContentSettings.Font.Family, ContentSettings.Font.Size, ContentSettings.Font.Style, GraphicsUnit.Point); - Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, _cachedFont.Style); - g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" - } + // Create a representative Graphcis used for determining glyph metrics. + using var bitmap = new Bitmap(1, 1); + bitmap.SetResolution(dpiX, dpiY); + var g = Graphics.FromImage(bitmap); + g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" + + // Calculate the number of lines per page; first we need our font. Keep it around. + _cachedFont = new Font(ContentSettings!.Font.Family, ContentSettings.Font.Size / 72F * 96, + ContentSettings.Font.Style, GraphicsUnit.Pixel); // World? + Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, + _cachedFont.Style); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + _cachedFont.Dispose(); + _cachedFont = new Font(ContentSettings.Font.Family, ContentSettings.Font.Size, ContentSettings.Font.Style, + GraphicsUnit.Point); + Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, + _cachedFont.Style); + g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" + } - _lineHeight = _cachedFont.GetHeight(dpiY); + _lineHeight = _cachedFont.GetHeight(dpiY); - if (PageSize.Height < _lineHeight) { - throw new InvalidOperationException("The line height is greater than page height."); - } + if (PageSize.Height < _lineHeight) { + throw new InvalidOperationException("The line height is greater than page height."); + } - // Round down # of lines per page to ensure lines don't clip on bottom - _linesPerPage = (int)Math.Floor(PageSize.Height / _lineHeight); + // Round down # of lines per page to ensure lines don't clip on bottom + _linesPerPage = (int)Math.Floor(PageSize.Height / _lineHeight); - // 3 digits + 1 wide - Will support 999 lines before line numbers start to not fit - // TODO: Make line number width dynamic - // Note, MeasureString is actually dependent on lineNumberWidth! - lineNumberWidth = ContentSettings.LineNumbers ? MeasureString(g, new string('0', 4)).Width : 0; + // 3 digits + 1 wide - Will support 999 lines before line numbers start to not fit + // TODO: Make line number width dynamic + // Note, MeasureString is actually dependent on lineNumberWidth! + _lineNumberWidth = ContentSettings.LineNumbers ? MeasureString(g, new string('0', 4)).Width : 0; - // This is the shortest line length (in chars) that we think we'll see. - // This is used as a performance optimization (probably premature) and - // could be 0 with no functional change. - _minLineLen = (int)((PageSize.Width - lineNumberWidth) / MeasureString(g, "W").Width); + // This is the shortest line length (in chars) that we think we'll see. + // This is used as a performance optimization (probably premature) and + // could be 0 with no functional change. + _minLineLen = (int)((PageSize.Width - _lineNumberWidth) / MeasureString(g, "W").Width); - // Note, MeasureLines may increment numPages due to form feeds and line wrapping - _wrappedLines = LineWrapDocument(g, Document); // new List(); + // Note, MeasureLines may increment numPages due to form feeds and line wrapping + _wrappedLines = LineWrapDocument(g, Document); // new List(); - var n = (int)Math.Ceiling(_wrappedLines.Count / (double)_linesPerPage); + var n = (int)Math.Ceiling(_wrappedLines.Count / (double)_linesPerPage); - Log.Debug("Rendered {pages} pages of {linesperpage} lines per page, for a total of {lines} lines.", n, _linesPerPage, _wrappedLines.Count); + Log.Debug("Rendered {pages} pages of {linesperpage} lines per page, for a total of {lines} lines.", n, + _linesPerPage, _wrappedLines.Count); - return await Task.FromResult(n); - } + return await Task.FromResult(n); + } - /// - /// This does the heavy-weight task of ensuring each line will fit PageSize.Width by - /// wrapping them. It also does tab expansion (which is naive for variable-pitched fonts) and - /// Supports form-feeds. - /// - /// - /// - /// - private List LineWrapDocument(Graphics g, string document) { - // TODO: Profile for performance - // LogService.TraceMessage(); - var wrapped = new List(); - - - string line; - var lineCount = 0; - - // convert string to stream - var byteArray = Encoding.UTF8.GetBytes(document); - var stream = new MemoryStream(byteArray); - var reader = new StreamReader(stream); - while ((line = reader.ReadLine()) != null) { - // Expand tabs - if (ContentSettings.TabSpaces > 0) { - line = line.Replace("\t", new string(' ', ContentSettings.TabSpaces)); - } + /// + /// This does the heavy-weight task of ensuring each line will fit PageSize.Width by + /// wrapping them. It also does tab expansion (which is naive for variable-pitched fonts) and + /// Supports form-feeds. + /// + /// + /// + /// + private List LineWrapDocument(Graphics g, string document) { + // TODO: Profile for performance + // LogService.TraceMessage(); + var wrapped = new List(); + + + var lineCount = 0; + + // convert string to stream + var byteArray = Encoding.UTF8.GetBytes(document); + var stream = new MemoryStream(byteArray); + var reader = new StreamReader(stream); + while (reader.ReadLine() is { } line) { + // Expand tabs + if (ContentSettings!.TabSpaces > 0) { + line = line.Replace("\t", new string(' ', ContentSettings.TabSpaces)); + } - ++lineCount; - if (ContentSettings.NewPageOnFormFeed && line.Contains("\f")) { - lineCount = ExpandFormFeeds(g, wrapped, line, lineCount); - } - else { - //Log.Debug("Line {num}: {line}", lineCount, line); - lineCount = AddLine(g, wrapped, line, lineCount); - } + ++lineCount; + if (ContentSettings.NewPageOnFormFeed && line.Contains("\f")) { + lineCount = ExpandFormFeeds(g, wrapped, line, lineCount); + } + else { + //Log.Debug("Line {num}: {line}", lineCount, line); + lineCount = AddLine(g, wrapped, line, lineCount); } - return wrapped; } - /// - /// Form feeds - /// treat a FF the same as the end of a line; next line is first line of next page - /// FF at start of line - That line should be at top of next page - /// FF in middle of line - Text up to FF should be on current page, text after should be at top of - /// next page - /// FF at end of line - Next line should be top of next page - /// - /// - /// - /// - /// - /// - /// - private int ExpandFormFeeds(Graphics g, List list, string line, int lineCount) { - var lineToAdd = ""; - - for (var i = 0; i < line.Length; i++) { - if (line[i] == '\f') { - if (lineToAdd.Length > 0) { - // FF was NOT at start of line. Add it. - AddLine(g, list, lineToAdd, lineCount); - // if we're not at the end of the line t increment line # - if (i < line.Length - 1) { - lineCount++; - } + return wrapped; + } + + /// + /// Form feeds + /// treat a FF the same as the end of a line; next line is first line of next page + /// FF at start of line - That line should be at top of next page + /// FF in middle of line - Text up to FF should be on current page, text after should be at top of + /// next page + /// FF at end of line - Next line should be top of next page + /// + /// + /// + /// + /// + /// + private int ExpandFormFeeds(Graphics g, List list, string line, int lineCount) { + var lineToAdd = ""; + + for (var i = 0; i < line.Length; i++) { + if (line[i] == '\f') { + if (lineToAdd.Length > 0) { + // FF was NOT at start of line. Add it. + AddLine(g, list, lineToAdd, lineCount); + // if we're not at the end of the line t increment line # + if (i < line.Length - 1) { + lineCount++; } + } - // Add blank lines to get to next page - while (list.Count % _linesPerPage != 0) { - var newLine = new WrappedLine() { text = "", nonWrappedLineNumber = 0 }; + // Add blank lines to get to next page + while (list.Count % _linesPerPage != 0) { + var newLine = new WrappedLine { _text = "", _nonWrappedLineNumber = 0 }; #if DEBUG - newLine.textNonWrapped = line; + newLine._textNonWrapped = line; #endif - list.Add(newLine); - } - // Now on next line - lineToAdd = ""; - } - else { - lineToAdd += line[i]; + list.Add(newLine); } + + // Now on next line + lineToAdd = ""; } - if (lineToAdd.Length > 0) { - AddLine(g, list, lineToAdd, lineCount); + else { + lineToAdd += line[i]; } + } - return lineCount; + if (lineToAdd.Length > 0) { + AddLine(g, list, lineToAdd, lineCount); } - /// - /// Add a 'full length' line to the wrapped lines list. This function (which is recursive) - /// parses the passed line, finding the truncated version that will JUST fit in Page.Width - /// using GDI+'s MeasureString functionality. It then adds that truncated line to the wrapped line - /// list and runs recursively on the remainder. - /// - /// - /// - /// The, potentially, too-long line to wrap. - /// - /// - /// - private int AddLine(Graphics g, List wrappedList, string lineToAdd, int lineCount) { - // TODO: Profile AddLine for performance - MeasureString(g, lineToAdd, out var numCharsThatFit, out var l1); - //Log.Debug(" AddLine: {lineToAdd} - this line should {not}wrap", lineToAdd, lineToAdd.Length <= numCharsThatFit ? "not " : ""); - if (lineToAdd.Length > numCharsThatFit) { // TODO: should this be >? - // This line wraps. Figure out by how much. - // Starting at minLineLen into the line, keep trying until it wraps again - // For fixed-pitch fonts, minLineLen will match exactly, so all this is not needed - // But for variable-pitched fonts, we have to char-by-char - var start = 0; - var end = _minLineLen; - for (var i = _minLineLen; i <= lineToAdd.Length; i++) { - var truncatedLine = lineToAdd[start..end++]; - MeasureString(g, truncatedLine, out var numCharsThatFitTruncated, out var l2); - if (truncatedLine.Length > numCharsThatFitTruncated) { - - // The truncated line fnow too big, so shorten it by one char and add it - truncatedLine = truncatedLine[0..^1]; - var wl = new WrappedLine() { text = truncatedLine, nonWrappedLineNumber = lineCount }; + return lineCount; + } + + /// + /// Add a 'full length' line to the wrapped lines list. This function (which is recursive) + /// parses the passed line, finding the truncated version that will JUST fit in Page.Width + /// using GDI+'s MeasureString functionality. It then adds that truncated line to the wrapped line + /// list and runs recursively on the remainder. + /// + /// + /// + /// The, potentially, too-long line to wrap. + /// + /// + private int AddLine(Graphics g, List wrappedList, string lineToAdd, int lineCount) { + // TODO: Profile AddLine for performance + MeasureString(g, lineToAdd, out var numCharsThatFit, out var l1); + //Log.Debug(" AddLine: {lineToAdd} - this line should {not}wrap", lineToAdd, lineToAdd.Length <= numCharsThatFit ? "not " : ""); + if (lineToAdd.Length > numCharsThatFit) { + // TODO: should this be >? + // This line wraps. Figure out by how much. + // Starting at minLineLen into the line, keep trying until it wraps again + // For fixed-pitch fonts, minLineLen will match exactly, so all this is not needed + // But for variable-pitched fonts, we have to char-by-char + var start = 0; + var end = _minLineLen; + for (var i = _minLineLen; i <= lineToAdd.Length; i++) { + var truncatedLine = lineToAdd[start..end++]; + MeasureString(g, truncatedLine, out var numCharsThatFitTruncated, out var l2); + if (truncatedLine.Length > numCharsThatFitTruncated) { + // The truncated line now too big, so shorten it by one char and add it + truncatedLine = truncatedLine[..^1]; + var wl = new WrappedLine { _text = truncatedLine, _nonWrappedLineNumber = lineCount }; #if DEBUG - wl.textNonWrapped = lineToAdd; - //Log.Debug(" Adding shorter line to list: {truncatedLine}, {nonWrappedLineNumber}, {textNonWrapped}", wl.text, wl.nonWrappedLineNumber, wl.textNonWrapped); + wl._textNonWrapped = lineToAdd; + //Log.Debug(" Adding shorter line to list: {truncatedLine}, {nonWrappedLineNumber}, {textNonWrapped}", wl.text, wl.nonWrappedLineNumber, wl.textNonWrapped); #endif - wrappedList.Add(wl); + wrappedList.Add(wl); - // Recurse with the rest of the line - AddLine(g, wrappedList, lineToAdd[truncatedLine.Length..^0], 0); + // Recurse with the rest of the line + AddLine(g, wrappedList, lineToAdd[truncatedLine.Length..], 0); - // exit for loop - break; - } + // exit for loop + break; } } - else { - var wl = new WrappedLine() { text = lineToAdd, nonWrappedLineNumber = lineCount }; + } + else { + var wl = new WrappedLine { _text = lineToAdd, _nonWrappedLineNumber = lineCount }; #if DEBUG - wl.textNonWrapped = lineToAdd; - //Log.Debug(" Adding passed to list: {truncatedLine}, {nonWrappedLineNumber}, {textNonWrapped}", wl.text, wl.nonWrappedLineNumber, wl.textNonWrapped); + wl._textNonWrapped = lineToAdd; + //Log.Debug(" Adding passed to list: {truncatedLine}, {nonWrappedLineNumber}, {textNonWrapped}", wl.text, wl.nonWrappedLineNumber, wl.textNonWrapped); #endif - wrappedList.Add(wl); - } - return lineCount; + wrappedList.Add(wl); } - private SizeF MeasureString(Graphics g, string text) { - return MeasureString(g, text, out var charsFitted, out var linesFilled); + return lineCount; + } + + private SizeF MeasureString(Graphics g, string text) { + return MeasureString(g, text, out var charsFitted, out var linesFilled); + } + + /// + /// Measures how much width a string will take, given current page settings (including line numbers) + /// + /// + /// + /// + /// + /// + private SizeF MeasureString(Graphics? g, string text, out int charsFitted, out int linesFilled) { + if (g is null) { + // define context used for determining glyph metrics. + using var bitmap = new Bitmap(1, 1); + g = Graphics.FromImage(bitmap); + //g = Graphics.FromHwnd(PrintPreview.Instance.Handle); + g.PageUnit = GraphicsUnit.Display; } - /// - /// Measures how much width a string will take, given current page settings (including line numbers) - /// - /// - /// - /// - /// - /// - private SizeF MeasureString(Graphics g, string text, out int charsFitted, out int linesFilled) { - if (g is null) { - // define context used for determining glyph metrics. - using var bitmap = new Bitmap(1, 1); - g = Graphics.FromImage(bitmap); - //g = Graphics.FromHwnd(PrintPreview.Instance.Handle); - g.PageUnit = GraphicsUnit.Display; - } + g.TextRenderingHint = TextRenderingHint; - g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; + // determine width + var fontHeight = _lineHeight; + // Use page settings including lineNumberWidth + var proposedSize = new SizeF(PageSize.Width - _lineNumberWidth, _lineHeight + (_lineHeight / 2)); + var size = g.MeasureString(text, _cachedFont!, proposedSize, StringFormat, out charsFitted, out linesFilled); - // determine width - var fontHeight = _lineHeight; - // Use page settings including lineNumberWidth - var proposedSize = new SizeF(PageSize.Width - lineNumberWidth, _lineHeight + (_lineHeight / 2)); - var size = g.MeasureString(text, _cachedFont, proposedSize, ContentTypeEngineBase.StringFormat, out charsFitted, out linesFilled); + // TODO: HACK to work around MeasureString not working right on Linux + //if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + // linesFilled = 1; + return size; + } - // TODO: HACK to work around MeasureString not working right on Linux - //if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - // linesFilled = 1; - return size; + /// + /// Paints a single page with line numbers. + /// + /// Graphics with 0,0 being the origin of the Page + /// Page number to print + public override void PaintPage(Graphics? g, int pageNum) { + LogService.TraceMessage($"{pageNum}"); + if (_wrappedLines == null) { + Log.Debug("wrappedLines must not be null"); + return; } - /// - /// Paints a single page. - /// - /// Graphics with 0,0 being the origin of the Page - /// Page number to print - public override void PaintPage(Graphics g, int pageNum) { - LogService.TraceMessage($"{pageNum}"); - if (_wrappedLines == null) { - Log.Debug("wrappedLines must not be null"); - return; - } + if (g is null) { + return; + } - g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; - - // Paint each line of the file (each element of _wrappedLines that go on pageNum - var firstLineInWrappedLines = _linesPerPage * (pageNum - 1); - int i; - for (i = firstLineInWrappedLines; i < firstLineInWrappedLines + _linesPerPage && i < _wrappedLines.Count; i++) { - var yPos = (i - (_linesPerPage * (pageNum - 1))) * _lineHeight; - var x = ContentSettings.LineNumberSeparator ? (int)(lineNumberWidth - 6 - MeasureString(g, $"{_wrappedLines[i].nonWrappedLineNumber}").Width) : 0; - // Line #s - if (_wrappedLines[i].nonWrappedLineNumber > 0) { - if (ContentSettings.LineNumbers && lineNumberWidth != 0) { - // TOOD: Figure out how to make the spacig around separator more dynamic - // TODO: Allow a different (non-monospace) font for line numbers - g.DrawString($"{_wrappedLines[i].nonWrappedLineNumber}", _cachedFont, Brushes.Gray, x, yPos, ContentTypeEngineBase.StringFormat); - } + g.TextRenderingHint = TextRenderingHint; + + // Paint each line of the file (each element of _wrappedLines that go on pageNum + var firstLineInWrappedLines = _linesPerPage * (pageNum - 1); + int i; + for (i = firstLineInWrappedLines; + i < firstLineInWrappedLines + _linesPerPage && i < _wrappedLines.Count; + i++) { + var yPos = (i - (_linesPerPage * (pageNum - 1))) * _lineHeight; + + // Right justify line number + var x = ContentSettings!.LineNumberSeparator + ? (int)(_lineNumberWidth - 6 - MeasureString(g, $"{_wrappedLines[i]._nonWrappedLineNumber}").Width) + : 0; + + // Line #s + if (_wrappedLines[i]._nonWrappedLineNumber > 0) { + if (ContentSettings.LineNumbers && _lineNumberWidth != 0) { + // TOOD: Figure out how to make the spacing around separator more dynamic + // TODO: Allow a different (non-monospace) font for line numbers + g.DrawString($"{_wrappedLines[i]._nonWrappedLineNumber}", _cachedFont, Brushes.Gray, x, yPos, + StringFormat); } + } - // Line # separator (draw even if there's no line number, but stop at end of doc) - // TODO: Support setting color of line #s and separator - if (ContentSettings.LineNumbers && ContentSettings.LineNumberSeparator && lineNumberWidth != 0) { - g.DrawLine(Pens.Gray, lineNumberWidth - 2, yPos, lineNumberWidth - 2, yPos + _lineHeight); - } + // Line # separator (draw even if there's no line number, but stop at end of doc) + // TODO: Support setting color of line #s and separator + if (ContentSettings.LineNumbers && ContentSettings.LineNumberSeparator && _lineNumberWidth != 0) { + g.DrawLine(Pens.Gray, _lineNumberWidth - 2, yPos, _lineNumberWidth - 2, yPos + _lineHeight); + } - // Text - g.DrawString(_wrappedLines[i].text, _cachedFont, Brushes.Black, lineNumberWidth, yPos, ContentTypeEngineBase.StringFormat); - if (ContentSettings.Diagnostics) { - g.DrawRectangle(Pens.Red, lineNumberWidth, yPos, PageSize.Width - lineNumberWidth, _lineHeight); - } + // Text + g.DrawString(_wrappedLines[i]._text, _cachedFont, Brushes.Black, _lineNumberWidth, yPos, StringFormat); + if (ContentSettings.Diagnostics) { + g.DrawRectangle(Pens.Red, _lineNumberWidth, yPos, PageSize.Width - _lineNumberWidth, _lineHeight); } - Log.Debug("Painted {lineOnPage} lines.", i - 1); } + + Log.Debug("Painted {lineOnPage} lines.", i - 1); } } diff --git a/src/WinPrint.Core/Helpers/Diagnostics.cs b/src/WinPrint.Core/Helpers/Diagnostics.cs index ce73ac3..6a84485 100644 --- a/src/WinPrint.Core/Helpers/Diagnostics.cs +++ b/src/WinPrint.Core/Helpers/Diagnostics.cs @@ -1,34 +1,32 @@ using System.Runtime.InteropServices; -namespace WinPrint.Core.Helpers { - internal class Diagnostics { +namespace WinPrint.Core.Helpers; - [DllImport("libgdiplus", ExactSpelling = true)] - internal static extern string GetLibgdiplusVersion(); +internal class Diagnostics { - ///// - ///// Gets the version of libgdiplus. - ///// Solution found here: https://github.com/dotnet/corefx/issues/37846 - ///// - ///// - //public static string GetlibgdiplusVersion() { - // try { - // using (var process = Process.Start(new ProcessStartInfo { - // FileName = "dpkg-query", - // Arguments = "--showformat '${Version}' --show libgdiplus", - // RedirectStandardOutput = true, - // UseShellExecute = false - // })) { - // process.WaitForExit(); - // return process.StandardOutput.ReadToEnd(); - // } - // } - // catch (Exception ex) { - // return $"Unable to determine libgdiplus version using `dpkg-query`. exception: {ex}"; - // } - //} + [DllImport("libgdiplus", ExactSpelling = true)] + internal static extern string GetLibgdiplusVersion(); - } + ///// + ///// Gets the version of libgdiplus. + ///// Solution found here: https://github.com/dotnet/corefx/issues/37846 + ///// + ///// + //public static string GetlibgdiplusVersion() { + // try { + // using (var process = Process.Start(new ProcessStartInfo { + // FileName = "dpkg-query", + // Arguments = "--showformat '${Version}' --show libgdiplus", + // RedirectStandardOutput = true, + // UseShellExecute = false + // })) { + // process.WaitForExit(); + // return process.StandardOutput.ReadToEnd(); + // } + // } + // catch (Exception ex) { + // return $"Unable to determine libgdiplus version using `dpkg-query`. exception: {ex}"; + // } + //} } - diff --git a/src/WinPrint.Core/Helpers/FileSystemSafeWatcher.cs b/src/WinPrint.Core/Helpers/FileSystemSafeWatcher.cs index 3f2f114..325f7bf 100644 --- a/src/WinPrint.Core/Helpers/FileSystemSafeWatcher.cs +++ b/src/WinPrint.Core/Helpers/FileSystemSafeWatcher.cs @@ -7,33 +7,23 @@ using System.IO; using System.Timers; -namespace menelabs.core { +namespace WinPrint.Core.Helpers { /// /// This class wraps FileSystemEventArgs and RenamedEventArgs objects and detection of duplicate events. /// - public class DelayedEvent { - private readonly FileSystemEventArgs _args; + public class DelayedEvent(FileSystemEventArgs args) { + private readonly FileSystemEventArgs _args = args; + + public FileSystemEventArgs? Args => _args; /// /// Only delayed events that are unique will be fired. /// - private bool _delayed; - - public DelayedEvent(FileSystemEventArgs args) { - _delayed = false; - _args = args; - } - - public FileSystemEventArgs Args => _args; - - public bool Delayed { - get => _delayed; - set => _delayed = value; - } + public bool Delayed { get; set; } = false; #pragma warning disable CA1720 // Identifier contains type name - public virtual bool IsDuplicate(object obj) { + public virtual bool IsDuplicate(object? obj) { #pragma warning restore CA1720 // Identifier contains type name var delayedEvent = obj as DelayedEvent; if (delayedEvent == null) { @@ -69,21 +59,21 @@ public virtual bool IsDuplicate(object obj) { /// FileSystemSafeWatcher will capture all events from the FileSystemWatcher object. /// The captured events will be delayed by at least ConsolidationInterval milliseconds in order /// to be able to eliminate duplicate events. When duplicate events are found, the last event - /// is droped and the first event is fired (the reverse is not recomended because it could - /// cause some events not be fired at all since the last event will become the first event and - /// it won't fire a if a new similar event arrives imediately afterwards). + /// is dropped and the first event is fired (the reverse is not recommended because it could + /// cause some events not be fired at all since the last event will become the first event, and + /// it won't fire if a new similar event arrives immediately afterward). /// public class FileSystemSafeWatcher { #pragma warning restore CA1001 // Types that own disposable fields should be disposable - private readonly FileSystemWatcher _fileSystemWatcher; + private readonly FileSystemWatcher? _fileSystemWatcher; /// /// Lock order is _enterThread, _events.SyncRoot /// - private readonly object _enterThread = new object(); // Only one timer event is processed at any given moment - private ArrayList _events; + private readonly object? _enterThread = new object(); // Only one timer event is processed at any given moment + private ArrayList? _events; - private Timer _serverTimer; + private Timer? _serverTimer; private int _consolidationInterval = 1000; // milliseconds #region Delegate to FileSystemWatcher @@ -108,15 +98,15 @@ public FileSystemSafeWatcher(string path, string filter) { /// /// true if the component is enabled; otherwise, false. The default is false. If you are using the component on a designer in Visual Studio 2005, the default is true. public bool EnableRaisingEvents { - get => _fileSystemWatcher.EnableRaisingEvents; + get => _fileSystemWatcher!.EnableRaisingEvents; set { - _fileSystemWatcher.EnableRaisingEvents = value; + _fileSystemWatcher!.EnableRaisingEvents = value; if (value) { - _serverTimer.Start(); + _serverTimer!.Start(); } else { - _serverTimer.Stop(); - _events.Clear(); + _serverTimer!.Stop(); + _events!.Clear(); } } } @@ -125,24 +115,24 @@ public bool EnableRaisingEvents { /// /// The filter string. The default is "*.*" (Watches all files.) public string Filter { - get => _fileSystemWatcher.Filter; - set => _fileSystemWatcher.Filter = value; + get => _fileSystemWatcher!.Filter; + set => _fileSystemWatcher!.Filter = value; } /// /// Gets or sets a value indicating whether subdirectories within the specified path should be monitored. /// /// true if you want to monitor subdirectories; otherwise, false. The default is false. public bool IncludeSubdirectories { - get => _fileSystemWatcher.IncludeSubdirectories; - set => _fileSystemWatcher.IncludeSubdirectories = value; + get => _fileSystemWatcher!.IncludeSubdirectories; + set => _fileSystemWatcher!.IncludeSubdirectories = value; } /// /// Gets or sets the size of the internal buffer. /// /// The internal buffer size. The default is 8192 (8K). public int InternalBufferSize { - get => _fileSystemWatcher.InternalBufferSize; - set => _fileSystemWatcher.InternalBufferSize = value; + get => _fileSystemWatcher!.InternalBufferSize; + set => _fileSystemWatcher!.InternalBufferSize = value; } /// /// Gets or sets the type of changes to watch for. @@ -150,8 +140,8 @@ public int InternalBufferSize { /// One of the System.IO.NotifyFilters values. The default is the bitwise OR combination of LastWrite, FileName, and DirectoryName. /// The value is not a valid bitwise OR combination of the System.IO.NotifyFilters values. public NotifyFilters NotifyFilter { - get => _fileSystemWatcher.NotifyFilter; - set => _fileSystemWatcher.NotifyFilter = value; + get => _fileSystemWatcher!.NotifyFilter; + set => _fileSystemWatcher!.NotifyFilter = value; } /// /// Gets or sets the path of the directory to watch. @@ -159,43 +149,43 @@ public NotifyFilters NotifyFilter { /// The path to monitor. The default is an empty string (""). /// The specified path contains wildcard characters.-or- The specified path contains invalid path characters. public string Path { - get => _fileSystemWatcher.Path; - set => _fileSystemWatcher.Path = value; + get => _fileSystemWatcher!.Path; + set => _fileSystemWatcher!.Path = value; } /// /// Gets or sets the object used to marshal the event handler calls issued as a result of a directory change. /// /// The System.ComponentModel.ISynchronizeInvoke that represents the object used to marshal the event handler calls issued as a result of a directory change. The default is null. public ISynchronizeInvoke SynchronizingObject { - get => _fileSystemWatcher.SynchronizingObject; - set => _fileSystemWatcher.SynchronizingObject = value; + get => _fileSystemWatcher!.SynchronizingObject; + set => _fileSystemWatcher!.SynchronizingObject = value; } /// /// Occurs when a file or directory in the specified System.IO.FileSystemWatcher.Path is changed. /// - public event FileSystemEventHandler Changed; + public event FileSystemEventHandler? Changed; /// /// Occurs when a file or directory in the specified System.IO.FileSystemWatcher.Path is created. /// - public event FileSystemEventHandler Created; + public event FileSystemEventHandler? Created; /// /// Occurs when a file or directory in the specified System.IO.FileSystemWatcher.Path is deleted. /// - public event FileSystemEventHandler Deleted; + public event FileSystemEventHandler? Deleted; /// /// Occurs when the internal buffer overflows. /// - public event ErrorEventHandler Error; + public event ErrorEventHandler? Error; /// /// Occurs when a file or directory in the specified System.IO.FileSystemWatcher.Path is renamed. /// - public event RenamedEventHandler Renamed; + public event RenamedEventHandler? Renamed; /// /// Begins the initialization of a System.IO.FileSystemWatcher used on a form or used by another component. The initialization occurs at run time. /// public void BeginInit() { - _fileSystemWatcher.BeginInit(); + _fileSystemWatcher!.BeginInit(); } /// /// Releases the unmanaged resources used by the System.IO.FileSystemWatcher and optionally releases the managed resources. @@ -207,52 +197,42 @@ public void Dispose() { /// Ends the initialization of a System.IO.FileSystemWatcher used on a form or used by another component. The initialization occurs at run time. /// public void EndInit() { - _fileSystemWatcher.EndInit(); + _fileSystemWatcher!.EndInit(); } /// /// Raises the System.IO.FileSystemWatcher.Changed event. /// /// A System.IO.FileSystemEventArgs that contains the event data. protected void OnChanged(FileSystemEventArgs e) { - if (Changed != null) { - Changed(this, e); - } + Changed?.Invoke(this, e); } /// /// Raises the System.IO.FileSystemWatcher.Created event. /// /// A System.IO.FileSystemEventArgs that contains the event data. protected void OnCreated(FileSystemEventArgs e) { - if (Created != null) { - Created(this, e); - } + Created?.Invoke(this, e); } /// /// Raises the System.IO.FileSystemWatcher.Deleted event. /// /// A System.IO.FileSystemEventArgs that contains the event data. protected void OnDeleted(FileSystemEventArgs e) { - if (Deleted != null) { - Deleted(this, e); - } + Deleted?.Invoke(this, e); } /// /// Raises the System.IO.FileSystemWatcher.Error event. /// /// An System.IO.ErrorEventArgs that contains the event data. protected void OnError(ErrorEventArgs e) { - if (Error != null) { - Error(this, e); - } + Error?.Invoke(this, e); } /// /// Raises the System.IO.FileSystemWatcher.Renamed event. /// /// A System.IO.RenamedEventArgs that contains the event data. - protected void OnRenamed(RenamedEventArgs e) { - if (Renamed != null) { - Renamed(this, e); - } + protected void OnRenamed(RenamedEventArgs? e) { + Renamed?.Invoke(this, e); } /// /// A synchronous method that returns a structure that contains specific information on the change that occurred, given the type of change you want to monitor. @@ -281,7 +261,7 @@ public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int ti private void Initialize() { _events = ArrayList.Synchronized(new ArrayList(32)); - _fileSystemWatcher.Changed += new FileSystemEventHandler(FileSystemEventHandler); + _fileSystemWatcher!.Changed += new FileSystemEventHandler(FileSystemEventHandler); _fileSystemWatcher.Created += new FileSystemEventHandler(FileSystemEventHandler); _fileSystemWatcher.Deleted += new FileSystemEventHandler(FileSystemEventHandler); _fileSystemWatcher.Error += new ErrorEventHandler(ErrorEventHandler); @@ -293,17 +273,13 @@ private void Initialize() { } private void Uninitialize() { - if (_fileSystemWatcher != null) { - _fileSystemWatcher.Dispose(); - } + _fileSystemWatcher?.Dispose(); - if (_serverTimer != null) { - _serverTimer.Dispose(); - } + _serverTimer?.Dispose(); } private void FileSystemEventHandler(object sender, FileSystemEventArgs e) { - _events.Add(new DelayedEvent(e)); + _events!.Add(new DelayedEvent(e)); } private void ErrorEventHandler(object sender, ErrorEventArgs e) { @@ -311,23 +287,22 @@ private void ErrorEventHandler(object sender, ErrorEventArgs e) { } private void RenamedEventHandler(object sender, RenamedEventArgs e) { - _events.Add(new DelayedEvent(e)); + _events!.Add(new DelayedEvent(e)); } private void ElapsedEventHandler(object sender, ElapsedEventArgs e) { // We don't fire the events inside the lock. We will queue them here until // the code exits the locks. - Queue eventsToBeFired = null; - if (System.Threading.Monitor.TryEnter(_enterThread)) { + Queue? eventsToBeFired = null; + if (System.Threading.Monitor.TryEnter(_enterThread!)) { // Only one thread at a time is processing the events try { eventsToBeFired = new Queue(32); // Lock the collection while processing the events - lock (_events.SyncRoot) { - DelayedEvent current; + lock (_events!.SyncRoot) { for (var i = 0; i < _events.Count; i++) { - current = _events[i] as DelayedEvent; - if (current.Delayed) { + var current = _events[i] as DelayedEvent; + if (current!.Delayed) { // This event has been delayed already so we can fire it // We just need to remove any duplicates for (var j = i + 1; j < _events.Count; j++) { @@ -339,7 +314,7 @@ private void ElapsedEventHandler(object sender, ElapsedEventArgs e) { } var raiseEvent = true; - if (current.Args.ChangeType == WatcherChangeTypes.Created || current.Args.ChangeType == WatcherChangeTypes.Changed) { + if (current!.Args.ChangeType == WatcherChangeTypes.Created || current.Args.ChangeType == WatcherChangeTypes.Changed) { //check if the file has been completely copied (can be opened for read) FileStream stream = null; try { @@ -373,30 +348,29 @@ private void ElapsedEventHandler(object sender, ElapsedEventArgs e) { } } finally { - System.Threading.Monitor.Exit(_enterThread); + System.Threading.Monitor.Exit(_enterThread!); } } // else - this timer event was skipped, processing will happen during the next timer event // Now fire all the events if any events are in eventsToBeFired - RaiseEvents(eventsToBeFired); + RaiseEvents(eventsToBeFired!); } public int ConsolidationInterval { get => _consolidationInterval; set { _consolidationInterval = value; - _serverTimer.Interval = value; + _serverTimer!.Interval = value; } } #pragma warning disable CA1030 // Use events where appropriate - protected void RaiseEvents(Queue deQueue) { + protected void RaiseEvents(Queue? deQueue) { #pragma warning restore CA1030 // Use events where appropriate - if ((deQueue != null) && (deQueue.Count > 0)) { - DelayedEvent de; + if (deQueue is { Count: > 0 }) { while (deQueue.Count > 0) { - de = deQueue.Dequeue() as DelayedEvent; + var de = deQueue.Dequeue() as DelayedEvent; switch (de.Args.ChangeType) { case WatcherChangeTypes.Changed: OnChanged(de.Args); diff --git a/src/WinPrint.Core/Helpers/FileWatcher.cs b/src/WinPrint.Core/Helpers/FileWatcher.cs index 1b829a2..f8cd34f 100644 --- a/src/WinPrint.Core/Helpers/FileWatcher.cs +++ b/src/WinPrint.Core/Helpers/FileWatcher.cs @@ -1,93 +1,91 @@ using System; using System.IO; -using menelabs.core; using WinPrint.Core.Services; -namespace WinPrint.Core.Helpers { - public class FileWatcher : IDisposable { +namespace WinPrint.Core.Helpers; +public class FileWatcher : IDisposable { #pragma warning disable IDE0052 // Remove unread private members - private FileSystemSafeWatcher fileWatcher; + private FileSystemSafeWatcher? _fileWatcher; #pragma warning restore IDE0052 // Remove unread private members - public FileWatcher(string file) { - fileWatcher = CreateFileWatcher(file); - } + public FileWatcher(string file) { + _fileWatcher = CreateFileWatcher(file); + } - public event EventHandler ChangedEvent; - /// - /// OnChangeEvent is raised whenever the CommandTable is updated due to - /// user commands file changes - /// - protected virtual void OnChangedEvent() { - // Make a temporary copy of the event to avoid possibility of - // a race condition if the last subscriber unsubscribes - // immediately after the null check and before the event is raised. - var handler = ChangedEvent; - - // Event will be null if there are no subscribers - if (handler != null) { - handler(this, null); - } - } + public event EventHandler? ChangedEvent; - private FileSystemSafeWatcher CreateFileWatcher(string path) { + /// + /// OnChangeEvent is raised whenever the CommandTable is updated due to + /// user commands file changes + /// + protected virtual void OnChangedEvent() { + // Make a temporary copy of the event to avoid possibility of + // a race condition if the last subscriber unsubscribes + // immediately after the null check and before the event is raised. + var handler = ChangedEvent; - // Create a new FileSystemSafeWatcher and set its properties. - var watcher = new FileSystemSafeWatcher { - Path = Path.GetDirectoryName(path), - /* Watch for changes in LastAccess and LastWrite times, and - the renaming of files or directories. */ - NotifyFilter = NotifyFilters.LastWrite, - Filter = Path.GetFileName(path) - }; + // Event will be null if there are no subscribers + handler?.Invoke(this, null); + } - // Add event handlers. - watcher.Changed += new FileSystemEventHandler(OnChanged); - watcher.Created += new FileSystemEventHandler(OnChanged); - watcher.Deleted += new FileSystemEventHandler(OnChanged); - watcher.Renamed += new RenamedEventHandler(OnRenamed); + private FileSystemSafeWatcher CreateFileWatcher(string path) { + // Create a new FileSystemSafeWatcher and set its properties. + var watcher = new FileSystemSafeWatcher { + Path = Path.GetDirectoryName(path), + /* Watch for changes in LastAccess and LastWrite times, and + the renaming of files or directories. */ + NotifyFilter = NotifyFilters.LastWrite, + Filter = Path.GetFileName(path) + }; - // Begin watching. - watcher.EnableRaisingEvents = true; - LogService.TraceMessage($"FileSystemSafeWatcher: Watching {watcher.Path}\\{watcher.Filter} for changes."); - return watcher; + // Add event handlers. + watcher.Changed += OnChanged; + watcher.Created += OnChanged; + watcher.Deleted += OnChanged; + watcher.Renamed += OnRenamed; - } + // Begin watching. + watcher.EnableRaisingEvents = true; + LogService.TraceMessage($"FileSystemSafeWatcher: Watching {watcher.Path}\\{watcher.Filter} for changes."); + return watcher; + } - private void OnChanged(object source, FileSystemEventArgs e) { - //Logger.Instance.Log4.Info($"Commands:{e.FullPath} changed."); - OnChangedEvent(); - } + private void OnChanged(object source, FileSystemEventArgs e) { + //Logger.Instance.Log4.Info($"Commands:{e.FullPath} changed."); + OnChangedEvent(); + } - private void OnRenamed(object source, RenamedEventArgs e) { - // Specify what is done when a file is renamed. - LogService.TraceMessage($"FileSystemSafeWatcher:{e.OldFullPath} renamed to {e.FullPath}"); - } + private void OnRenamed(object source, RenamedEventArgs e) { + // Specify what is done when a file is renamed. + LogService.TraceMessage($"FileSystemSafeWatcher:{e.OldFullPath} renamed to {e.FullPath}"); + } + + #region IDisposable Support + + private bool _disposedValue; // To detect redundant calls - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) { - if (!disposedValue) { - if (disposing) { - if (fileWatcher != null) { - fileWatcher.Changed -= OnChanged; - fileWatcher.Created -= OnChanged; - fileWatcher.Deleted -= OnChanged; - fileWatcher.Renamed -= OnRenamed; - fileWatcher = null; - } + protected virtual void Dispose(bool disposing) { + if (!_disposedValue) { + if (disposing) { + if (_fileWatcher != null) { + _fileWatcher.Changed -= OnChanged; + _fileWatcher.Created -= OnChanged; + _fileWatcher.Deleted -= OnChanged; + _fileWatcher.Renamed -= OnRenamed; + _fileWatcher = null; } - disposedValue = true; } - } - public void Dispose() { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - GC.SuppressFinalize(this); + _disposedValue = true; } - #endregion } + + public void Dispose() { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion } diff --git a/src/WinPrint.Core/Helpers/HtmlResources.cs b/src/WinPrint.Core/Helpers/HtmlResources.cs index 33f2a85..ac15644 100644 --- a/src/WinPrint.Core/Helpers/HtmlResources.cs +++ b/src/WinPrint.Core/Helpers/HtmlResources.cs @@ -1,88 +1,52 @@ using System; using System.IO; -using System.Net.Http; using WinPrint.Core.Services; -namespace WinPrint.LiteHtml { - public class HtmlResources { - //private HttpClient _httpClient = null; - private string _lastUrl = null; - private string filePath; +namespace WinPrint.LiteHtml; - public HtmlResources(string filePath) { - this.filePath = filePath; - } - - public byte[] GetResourceBytes(string resource) { - LogService.TraceMessage($"{resource}"); - var data = new byte[0]; - if (string.IsNullOrWhiteSpace(resource)) { - return data; - } +public class HtmlResources(string filePath) { + //private HttpClient _httpClient = null; + private readonly string? _lastUrl = null; - try { - data = File.ReadAllBytes($"{Path.GetDirectoryName(filePath)}\\{resource}"); - } - catch (Exception e) { - LogService.TraceMessage($"GetResourceBytes({resource}) - {e.Message}"); - } + public byte[] GetResourceBytes(string resource) { + LogService.TraceMessage($"{resource}"); + var data = Array.Empty(); + if (string.IsNullOrWhiteSpace(resource)) { return data; } - public string GetResourceString(string resource) { - LogService.TraceMessage($"{resource}"); - var data = string.Empty; - if (string.IsNullOrWhiteSpace(resource)) { - return data; - } - try { - if (resource.StartsWith("file:")) { - var urlBuilder = new UriBuilder(resource); - using var reader = new StreamReader(urlBuilder.Path); - data = reader.ReadToEnd(); - } - else { - // TODO: Implement loading external html resources - //var url = GetUrlForRequest(resource); - //data = _httpClient.GetStringAsync(url).Result; - } - return data; - } - catch (Exception e) { - LogService.TraceMessage($"GetResourceString({resource}) - {e.Message}"); - return data; - } + try { + data = File.ReadAllBytes($"{Path.GetDirectoryName(filePath)}\\{resource}"); + } + catch (Exception e) { + LogService.TraceMessage($"GetResourceBytes({resource}) - {e.Message}"); } - private string GetUrlForRequest(string resource) { - LogService.TraceMessage($"{resource}"); + return data; + } - try { - UriBuilder urlBuilder; + public string GetResourceString(string resource) { + LogService.TraceMessage($"{resource}"); + var data = string.Empty; + if (string.IsNullOrWhiteSpace(resource)) { + return data; + } - if (resource.StartsWith("file:")) { - urlBuilder = new UriBuilder { - Scheme = "file", - Host = "", - Path = resource - }; - } - else if (resource.StartsWith("//") || resource.StartsWith("http:") || resource.StartsWith("https:")) { - urlBuilder = new UriBuilder(resource.TrimStart(new char[] { '/' })); - } - else { - urlBuilder = new UriBuilder(_lastUrl) { - Path = resource - }; - } - var requestUrl = urlBuilder.ToString(); - return requestUrl; + try { + if (resource.StartsWith("file:")) { + var urlBuilder = new UriBuilder(resource); + using var reader = new StreamReader(urlBuilder.Path); + data = reader.ReadToEnd(); } - catch { - LogService.TraceMessage($"GetUrlForReqeust({resource}) returning null."); - return null; - } - } + // TODO: Implement loading external html resources + //var url = GetUrlForRequest(resource); + //data = _httpClient.GetStringAsync(url).Result; + return data; + } + catch (Exception e) { + LogService.TraceMessage($"GetResourceString({resource}) - {e.Message}"); + return data; + } } } diff --git a/src/WinPrint.Core/Helpers/Native.cs b/src/WinPrint.Core/Helpers/Native.cs deleted file mode 100644 index 16c3ccf..0000000 --- a/src/WinPrint.Core/Helpers/Native.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; - -namespace WinPrint { - internal class NativeMethods { - - [DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut); - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "")] - public static string FileExtentionInfo(AssocStr assocStr, string doctype) { - uint pcchOut = 0; - AssocQueryString(AssocF.Verify, assocStr, doctype, null, null, ref pcchOut); - - var pszOut = new StringBuilder((int)pcchOut); - AssocQueryString(AssocF.Verify, assocStr, doctype, null, pszOut, ref pcchOut); - return pszOut.ToString(); - } - - [Flags] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1714:Flags enums should have plural names", Justification = "")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "")] - public enum AssocF { - Init_NoRemapCLSID = 0x1, - Init_ByExeName = 0x2, - Open_ByExeName = 0x2, - Init_DefaultToStar = 0x4, - Init_DefaultToFolder = 0x8, - NoUserSettings = 0x10, - NoTruncate = 0x20, - Verify = 0x40, - RemapRunDll = 0x80, - NoFixUps = 0x100, - IgnoreBaseClass = 0x200 - } - - public enum AssocStr { - Command = 1, - Executable, - FriendlyDocName, - FriendlyAppName, - NoOpen, - ShellNewValue, - DDECommand, - DDEIfExec, - DDEApplication, - DDETopic - } - } -} diff --git a/src/WinPrint.Core/Models/ContentSettings.cs b/src/WinPrint.Core/Models/ContentSettings.cs index 89147ce..27ef6ea 100644 --- a/src/WinPrint.Core/Models/ContentSettings.cs +++ b/src/WinPrint.Core/Models/ContentSettings.cs @@ -1,87 +1,86 @@ -namespace WinPrint.Core.Models { - /// - /// Model for page content (properties that impact how content w/in a page is printed). - /// Each Sheet prints sheet.Columns by sheet.Rows Pages. - /// - public class ContentSettings : ModelBase { +namespace WinPrint.Core.Models; - /// - /// Font used for content. Will override any content font settings specified by a ContentType provider. - /// - [SafeForTelemetry] - public Core.Models.Font Font { get => _font; set => SetField(ref _font, value); } - private Core.Models.Font _font = new Font(); +/// +/// Model for page content (properties that impact how content w/in a page is printed). +/// Each Sheet prints sheet.Columns by sheet.Rows Pages. +/// +public class ContentSettings : ModelBase { + private int _darkness; + private bool _diagnostics; + private bool _disableFontStyles; + private Font _font = new(); + private bool _grayscale; + private bool _lineNumbers = true; + private bool _lineNumberSeparator = true; + private bool _newPageOnFormFeed; + private bool _printBackground = true; + private string _style = string.Empty; + private int _tabSpaces = 4; - /// - /// if True, print content background, if present. Otherwise, all backgrounds will be paper color. - /// - [SafeForTelemetry] - public bool PrintBackground { get => _printBackground; set => SetField(ref _printBackground, value); } - private bool _printBackground = true; + /// + /// Font used for content. Will override any content font settings specified by a ContentType provider. + /// + [SafeForTelemetry] + public Font Font { get => _font; set => SetField(ref _font, value); } - /// - /// If True, all content will be printed in grayscale. Use Darkness property to change how - /// dark the grey is. - /// - [SafeForTelemetry] - public bool Grayscale { get => _grayscale; set => SetField(ref _grayscale, value); } - private bool _grayscale = false; + /// + /// if True, print content background, if present. Otherwise, all backgrounds will be paper color. + /// + [SafeForTelemetry] + public bool PrintBackground { get => _printBackground; set => SetField(ref _printBackground, value); } - /// - /// Darkness factor. 0 = RGB. 100 = black. - /// - [SafeForTelemetry] - public int Darkness { get => _darkness; set => SetField(ref _darkness, value); } - private int _darkness = 0; + /// + /// If True, all content will be printed in grayscale. Use Darkness property to change how + /// dark the grey is. + /// + [SafeForTelemetry] + public bool Grayscale { get => _grayscale; set => SetField(ref _grayscale, value); } - /// - /// Style to use for formatting. Dependent on Content Engine. For AnsiCte, represents a Pygments.org style name. - /// - [SafeForTelemetry] - public string Style { get => _style; set => SetField(ref _style, value); } - private string _style = string.Empty; + /// + /// Darkness factor. 0 = RGB. 100 = black. + /// + [SafeForTelemetry] + public int Darkness { get => _darkness; set => SetField(ref _darkness, value); } - /// - /// Disables font styles (bold, italic, underline). - /// - [SafeForTelemetry] - public bool DisableFontStyles { get => _disableFontStyles; set => SetField(ref _disableFontStyles, value); } - private bool _disableFontStyles = false; + /// + /// Style to use for formatting. Dependent on Content Engine. For AnsiCte, represents a Pygments.org style name. + /// + [SafeForTelemetry] + public string Style { get => _style; set => SetField(ref _style, value); } - /// - /// If true, content will be drawn with line numbers (if supported) - /// - [SafeForTelemetry] - public bool LineNumbers { get => _linenumbers; set => SetField(ref _linenumbers, value); } - private bool _linenumbers = true; + /// + /// Disables font styles (bold, italic, underline). + /// + [SafeForTelemetry] + public bool DisableFontStyles { get => _disableFontStyles; set => SetField(ref _disableFontStyles, value); } - /// - /// If true, a line number separator will be drawn (if supported) - /// - [SafeForTelemetry] - public bool LineNumberSeparator { get => _lineNumberSeparator; set => SetField(ref _lineNumberSeparator, value); } - private bool _lineNumberSeparator = true; + /// + /// If true, content will be drawn with line numbers (if supported) + /// + [SafeForTelemetry] + public bool LineNumbers { get => _lineNumbers; set => SetField(ref _lineNumbers, value); } - /// - /// Number of spaces per tab character (if supported) - /// - [SafeForTelemetry] - public int TabSpaces { get => _tabSpaces; set => SetField(ref _tabSpaces, value); } - private int _tabSpaces = 4; + /// + /// If true, a line number separator will be drawn (if supported) + /// + [SafeForTelemetry] + public bool LineNumberSeparator { get => _lineNumberSeparator; set => SetField(ref _lineNumberSeparator, value); } - /// - /// If true formfeed characters will start a new page - /// - [SafeForTelemetry] - public bool NewPageOnFormFeed { get => _newPageOnFormFeed; set => SetField(ref _newPageOnFormFeed, value); } - private bool _newPageOnFormFeed = false; + /// + /// Number of spaces per tab character (if supported) + /// + [SafeForTelemetry] + public int TabSpaces { get => _tabSpaces; set => SetField(ref _tabSpaces, value); } - /// - /// If true, content will be drawn with diagnostic info and/or rules. - /// - [SafeForTelemetry] - public bool Diagnostics { get => _diagnostics; set => SetField(ref _diagnostics, value); } - private bool _diagnostics = false; + /// + /// If true formfeed characters will start a new page + /// + [SafeForTelemetry] + public bool NewPageOnFormFeed { get => _newPageOnFormFeed; set => SetField(ref _newPageOnFormFeed, value); } - } + /// + /// If true, content will be drawn with diagnostic info and/or rules. + /// + [SafeForTelemetry] + public bool Diagnostics { get => _diagnostics; set => SetField(ref _diagnostics, value); } } diff --git a/src/WinPrint.Core/Models/FileTypeMapping.cs b/src/WinPrint.Core/Models/FileTypeMapping.cs index 9a3c9e7..2409f99 100644 --- a/src/WinPrint.Core/Models/FileTypeMapping.cs +++ b/src/WinPrint.Core/Models/FileTypeMapping.cs @@ -1,52 +1,52 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace WinPrint.Core.Models { - /// - /// https://stackoverflow.com/questions/59516258/database-of-file-extensions-to-file-type-language-mappings - /// - /// - public class FileTypeMapping : ModelBase { - //"files.associations": { - // "*.myphp": "php" - //} - - //"languages": [{ - // "id": "text/x-java", - // "title": "java", - // "extensions": [ ".java", ".jav" ], - // "aliases": [ "Java", "java" ] - //}] - - [JsonPropertyName("files.associations")] - public Dictionary FilesAssociations { get; set; } - - // DO NOT RENAME THIS - Legacy - [JsonPropertyName("languages")] - public IList ContentTypes { get; set; } +namespace WinPrint.Core.Models; + +/// +/// https://stackoverflow.com/questions/59516258/database-of-file-extensions-to-file-type-language-mappings +/// +public class FileTypeMapping : ModelBase { + //"files.associations": { + // "*.myphp": "php" + //} + + //"languages": [{ + // "id": "text/x-java", + // "title": "java", + // "extensions": [ ".java", ".jav" ], + // "aliases": [ "Java", "java" ] + //}] + + [JsonPropertyName("files.associations")] + public Dictionary? FilesAssociations { get; set; } + + // DO NOT RENAME THIS - Legacy + [JsonPropertyName("languages")] public IList? ContentTypes { get; set; } +} + +public class ContentType { + [JsonPropertyName("id")] + [SafeForTelemetry] + public string? Id { get; set; } + + [JsonPropertyName("extensions")] + [SafeForTelemetry] + public IList? Extensions { get; set; } + + [JsonPropertyName("aliases")] + [SafeForTelemetry] + public IList? Aliases { get; set; } + + [JsonPropertyName("title")] + [SafeForTelemetry] + public string? Title { get; set; } + + public override int GetHashCode() { + return Id.GetHashCode(); } - public class ContentType { - [JsonPropertyName("id")] - [SafeForTelemetry] - public string Id { get; set; } - [JsonPropertyName("extensions")] - [SafeForTelemetry] - public IList Extensions { get; set; } - [JsonPropertyName("aliases")] - [SafeForTelemetry] - public IList Aliases { get; set; } - - [JsonPropertyName("title")] - [SafeForTelemetry] - public string Title { get; set; } - - public override int GetHashCode() { - return (Id).GetHashCode(); - } - - public override bool Equals(object obj) { - return Id.Equals(((ContentType)obj).Id); - } + public override bool Equals(object obj) { + return Id.Equals(((ContentType)obj).Id); } } diff --git a/src/WinPrint.Core/Models/Font.cs b/src/WinPrint.Core/Models/Font.cs index 0ec64d0..cfe4ab0 100644 --- a/src/WinPrint.Core/Models/Font.cs +++ b/src/WinPrint.Core/Models/Font.cs @@ -2,109 +2,108 @@ using System.Drawing; using System.Globalization; -namespace WinPrint.Core.Models { - public class Font : ICloneable { - - private string family = "sansserif"; - private FontStyle style = FontStyle.Regular; - private float size = 8F; - - /// - /// Font name or font family name (e.g. "Courier New" or "monospace" - /// - [SafeForTelemetry] - public string Family { - get => family; - - set => family = value;//SetField(ref family, value); - } - - /// - /// Font style (Regular, Bold, Italic, Underline, Strikeout) - /// - [SafeForTelemetry] - public FontStyle Style { - get => style; set { - if (!Enum.IsDefined(typeof(FontStyle), value)) { - value = FontStyle.Bold | FontStyle.Italic; - } - - style = value; - // SetField(ref style, value); - +namespace WinPrint.Core.Models; + +public class Font : ICloneable { + private FontStyle _style = FontStyle.Regular; + + /// + /// Font name or font family name (e.g. "Courier New" or "monospace" + /// + [SafeForTelemetry] + public string Family { + get; + set; + //SetField(ref family, value); + } = "sansserif"; + + /// + /// Font style (Regular, Bold, Italic, Underline, Strikeout) + /// + [SafeForTelemetry] + public FontStyle Style { + get => _style; + set { + if (!Enum.IsDefined(typeof(FontStyle), value)) { + value = FontStyle.Bold | FontStyle.Italic; } - } - - /// - /// Font size in points. - /// - [SafeForTelemetry] - public float Size { - get => size; - set => size = value;//SetField(ref size, value); - } - public object Clone() { - return MemberwiseClone(); + _style = value; + // SetField(ref style, value); } + } - public override int GetHashCode() { - return HashCode.Combine(Family, Size, Style); - } + /// + /// Font size in points. + /// + [SafeForTelemetry] + public float Size { + get; + set; + //SetField(ref size, value); + } = 8F; + + public object Clone() { + return MemberwiseClone(); + } - public override bool Equals(object obj) { - if (!(obj is Font font)) { - return false; - } + public override int GetHashCode() { + return HashCode.Combine(Family, Size, Style); + } - return font.Family == Family - && font.Size == Size - && font.Style == Style; + public override bool Equals(object? obj) { + if (!(obj is Font font)) { + return false; } - public static bool operator ==(Font m1, Font m2) { - if (m1 is null) { - return m2 is null; - } - if (m2 is null) { - return false; - } - return m1.Equals(m2); - } + return font.Family == Family + && font.Size == Size + && font.Style == Style; + } - /// - /// Tests whether two objects are different. - /// - public static bool operator !=(Font m1, Font m2) { - return !(m1 == m2); + public static bool operator ==(Font? m1, Font? m2) { + if (m1 is null) { + return m2 is null; } - /// - /// Provides some interesting information for the Font in String form. - /// - public override string ToString() { - return $"{Family}, {Size.ToString(CultureInfo.InvariantCulture)}pt, {Style.ToString()}"; + if (m2 is null) { + return false; } - //public Font() { - //} - //public void Dispose() { - // Dispose(true); - // GC.SuppressFinalize(this); - //} - - //// Protected implementation of Dispose pattern. - //// Flag: Has Dispose already been called? - //private bool disposed = false; - //protected virtual void Dispose(bool disposing) { - // if (disposed) - // return; - - // if (disposing) { - // //if (font != null) font.Dispose(); - // } - // disposed = true; - //} + return m1.Equals(m2); + } + /// + /// Tests whether two objects are different. + /// + public static bool operator !=(Font? m1, Font? m2) { + return !(m1 == m2); } + + /// + /// Provides some interesting information for the Font in String form. + /// + public override string ToString() { + return $"{Family}, {Size.ToString(CultureInfo.InvariantCulture)}pt, {Style.ToString()}"; + } + + //public Font() { + //} + //public void Dispose() { + // Dispose(true); + // GC.SuppressFinalize(this); + //} + + //// Protected implementation of Dispose pattern. + //// Flag: Has Dispose already been called? + //private bool disposed = false; + //protected virtual void Dispose(bool disposing) { + // if (disposed) + // return; + + // if (disposing) { + // //if (font != null) font.Dispose(); + // } + // disposed = true; + //} } diff --git a/src/WinPrint.Core/Models/FormWindowState.cs b/src/WinPrint.Core/Models/FormWindowState.cs new file mode 100644 index 0000000..aa01936 --- /dev/null +++ b/src/WinPrint.Core/Models/FormWindowState.cs @@ -0,0 +1,21 @@ +// Copyright Kindel, LLC - http://www.kindel.com +// Published under the MIT License at https://github.com/tig/winprint + +namespace WinPrint.Core.Models; + +public enum FormWindowState { + // + // Summary: + // A default sized window. + Normal = 0, + + // + // Summary: + // A minimized window. + Minimized = 1, + + // + // Summary: + // A maximized window. + Maximized = 2 +} diff --git a/src/WinPrint.Core/Models/HeaderFooter.cs b/src/WinPrint.Core/Models/HeaderFooter.cs index 95d10f4..2b1de41 100644 --- a/src/WinPrint.Core/Models/HeaderFooter.cs +++ b/src/WinPrint.Core/Models/HeaderFooter.cs @@ -2,124 +2,119 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace WinPrint.Core.Models { +namespace WinPrint.Core.Models; + +/// +/// Knows how to paint a header or footer. +/// Single line of text? TODO: Might want to suppport wrapping. +/// Format: A .NET Interpolated String. Two tabstops. +/// $ +/// Three segement +/// Left Aligned Centered Right Aligned +/// Format: Left/Centered/Right can be delimited with either tab char (\t) or | +/// {FullyQualifiedPath}|Modified: {FileDate:F}|{Page:D3}/{NumPages} +/// {FullyQualifiedPath}\tModified: {FileDate:F}\t{Page:D3}/{NumPages} +/// Macros +/// DatePrinted +/// DateRevised +/// Page +/// NumPages +/// FileName +/// FilePath +/// FullyQualifiedPath +/// FileExtension +/// FileTYpe +/// Title +/// Options +/// Top padding +/// Bottom padding +/// Right padding +/// Left Padding +/// top, left, right, bottom border +/// border pen style +/// border color +/// font +/// +// TODO: How to deal with clipping +// 1) Order of print - Left, Right, Center (center wins) +// 2) Elipsis - different based on macro. E.g. FullFilePath is "Start...FileName" where FileName is truncated last. +// 3) Clipped (never overwritten - ugly) +// 4) Wrapped (post MLP) +public abstract class HeaderFooter : ModelBase { + private bool _bottomBorder; + private bool _enabled; + private Font? _font; + private bool _leftBorder; + private bool _rightBorder; + private string? _text; + private bool _topBorder; + private int _verticalPadding; + /// - /// Knows how to paint a header or footer. - /// Single line of text? TODO: Might want to suppport wrapping. - /// Format: A .NET Interpolated String. Two tabstops. - /// $ - /// Three segement - /// Left Aligned Centered Right Aligned - /// - /// Format: Left/Centered/Right can be delimited with either tab char (\t) or | - /// {FullyQualifiedPath}|Modified: {FileDate:F}|{Page:D3}/{NumPages} - /// {FullyQualifiedPath}\tModified: {FileDate:F}\t{Page:D3}/{NumPages} - /// - /// Macros - /// DatePrinted - /// DateRevised - /// Page - /// NumPages - /// FileName - /// FilePath - /// FullyQualifiedPath - /// FileExtension - /// FileTYpe - /// Title - /// Options - /// Top padding - /// Bottom padding - /// Right padding - /// Left Padding - /// top, left, right, bottom border - /// border pen style - /// border color - /// font + /// Header text. May contain macros (e.g. {FileName} or {Page} /// - // TODO: How to deal with clipping - // 1) Order of print - Left, Right, Center (center wins) - // 2) Elipsis - different based on macro. E.g. FullFilePath is "Start...FileName" where FileName is truncated last. - // 3) Clipped (never overwritten - ugly) - // 4) Wrapped (post MLP) - public abstract class HeaderFooter : ModelBase { - private string text; - private Font font; - private bool leftBorder; - private bool topBorder; - private bool rightBorder; - private bool bottomBorder; - private bool enabled; - private int verticalPadding; + public string? Text { get => _text; set => SetField(ref _text, value); } - /// - /// Header text. May contain macros (e.g. {FileName} or {Page} - /// - public string Text { get => text; set => SetField(ref text, value); } - - /// - /// Provides a telemetry-safe version of Text (a comma delmited list with only the macros used). See - /// HeaderFooterViewModel for more details on how macros are parsed. - /// - [JsonIgnore] - [SafeForTelemetry] - public string MacrosUsed { - get { - var matches = Regex.Matches(Text, @"(?\{)+(?[\w\.\[\]]+)(?:[^}]+)?(?\})+") - .Cast() - .Select(match => match.Value) - .ToList(); - return string.Join(", ", from macro in matches select macro); - } + /// + /// Provides a telemetry-safe version of Text (a comma delimited list with only the macros used). See + /// HeaderFooterViewModel for more details on how macros are parsed. + /// + [JsonIgnore] + [SafeForTelemetry] + public string MacrosUsed { + get { + var matches = Regex.Matches(Text, @"(?\{)+(?[\w\.\[\]]+)(?:[^}]+)?(?\})+") + .Select(match => match.Value) + .ToList(); + return string.Join(", ", from macro in matches select macro); } + } - /// - /// Font used for header or footer text - /// - [SafeForTelemetry] - public Font Font { get => font; set => SetField(ref font, value); } + /// + /// Font used for header or footer text + /// + [SafeForTelemetry] + public Font? Font { get => _font; set => SetField(ref _font, value); } - /// - /// Enables or disables printing of left border of heder/footer - /// - [SafeForTelemetry] - public bool LeftBorder { get => leftBorder; set => SetField(ref leftBorder, value); } - /// - /// Enables or disables printing of Top border of heder/footer - /// - [SafeForTelemetry] - public bool TopBorder { get => topBorder; set => SetField(ref topBorder, value); } - /// - /// Enables or disables printing of Right border of heder/footer - /// - [SafeForTelemetry] - public bool RightBorder { get => rightBorder; set => SetField(ref rightBorder, value); } - /// - /// Enables or disables printing of Bottom border of heder/footer - /// - [SafeForTelemetry] - public bool BottomBorder { get => bottomBorder; set => SetField(ref bottomBorder, value); } + /// + /// Enables or disables printing of left border of heder/footer + /// + [SafeForTelemetry] + public bool LeftBorder { get => _leftBorder; set => SetField(ref _leftBorder, value); } - /// - /// Enable or disable header/footer - /// - [SafeForTelemetry] - public bool Enabled { get => enabled; set => SetField(ref enabled, value); } + /// + /// Enables or disables printing of Top border of heder/footer + /// + [SafeForTelemetry] + public bool TopBorder { get => _topBorder; set => SetField(ref _topBorder, value); } - /// - /// Vertical padding below header / above footer in 100ths of an inch - /// - [SafeForTelemetry] - public int VerticalPadding { get => verticalPadding; set => SetField(ref verticalPadding, value); } + /// + /// Enables or disables printing of Right border of heder/footer + /// + [SafeForTelemetry] + public bool RightBorder { get => _rightBorder; set => SetField(ref _rightBorder, value); } - public HeaderFooter() { - } - } - public class Header : HeaderFooter { - public Header() : base() { - } - } - public class Footer : HeaderFooter { - public Footer() : base() { - } - } + /// + /// Enables or disables printing of Bottom border of heder/footer + /// + [SafeForTelemetry] + public bool BottomBorder { get => _bottomBorder; set => SetField(ref _bottomBorder, value); } + + /// + /// Enable or disable header/footer + /// + [SafeForTelemetry] + public bool Enabled { get => _enabled; set => SetField(ref _enabled, value); } + + /// + /// Vertical padding below header / above footer in 100ths of an inch + /// + [SafeForTelemetry] + public int VerticalPadding { get => _verticalPadding; set => SetField(ref _verticalPadding, value); } +} + +public class Header : HeaderFooter { +} + +public class Footer : HeaderFooter { } diff --git a/src/WinPrint.Core/Models/Macros.cs b/src/WinPrint.Core/Models/Macros.cs index c355a55..e4ca485 100644 --- a/src/WinPrint.Core/Models/Macros.cs +++ b/src/WinPrint.Core/Models/Macros.cs @@ -1,165 +1,183 @@ using System; +using System.Globalization; using System.IO; -using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Expressions; using System.Text.RegularExpressions; -using WinPrint.Core.Models; - -namespace WinPrint.Core { - sealed public class Macros { - /// - /// The SheetModel the Macros will pull data from. - /// - public SheetViewModel svm; - - /// - /// Number of sheets to be printed. - /// - public int NumPages => svm.NumSheets; - /// - /// The extension (including the period "."). - /// - public string FileExtension => string.IsNullOrEmpty(svm.File) ? "" : Path.GetExtension(svm.File); - /// - /// The file name and extension. If FileName was not provided, Title will be used. - /// - public string FileName => GetFileNameOrTitle(); - /// - /// The Title of the print request. - /// - public string Title => svm.Title; - /// - /// The file name of the file without the extension and period ".". - /// - public string FileNameWithoutExtension => string.IsNullOrEmpty(svm.File) ? "" : Path.GetFileNameWithoutExtension(svm.File); - /// - /// The directory for the specified string without the filename or extension. - /// - public string FileDirectoryName => string.IsNullOrEmpty(svm.File) ? "" : Path.GetDirectoryName(FullPath); - /// - /// The absolute path for the file. - /// - public string FullPath => IsValidFilename(svm.File) ? Path.GetFullPath(svm.File) : (string.IsNullOrEmpty(svm.File) ? "" : svm.File); - /// - /// The time and date when printed. - /// - public DateTime DatePrinted => DateTime.Now; - /// - /// The time and date the file was last revised. - /// - public DateTime DateRevised => IsValidFilename(svm.File) ? File.GetLastWriteTime(svm.File) : DateTime.MinValue; - /// - /// The time and date the file was created. - /// - public DateTime DateCreated => IsValidFilename(svm.File) ? File.GetCreationTime(svm.File) : DateTime.MinValue; - - /// - /// The language (e.g. "C#" or "java"). - /// - public string Language => string.IsNullOrEmpty(svm.Language) ? string.Empty : svm.Language; - - /// - /// The Contetn Type (e.g. "text/x-csharp") - /// - public string ContentType => string.IsNullOrEmpty(svm.ContentType) ? string.Empty : svm.ContentType; - /// - /// The file content type engine name (e.g. "TextCte", "AnsiCte"). - /// - public string CteName => svm.ContentEngine == null ? "" : svm.ContentEngine.GetType().Name; - - /// - /// The style used for formatting (e.g. "default" or "colorful"; from Pygments.org). - /// - public string Style => svm.ContentEngine == null || svm.ContentEngine.ContentSettings == null ? "" : svm.ContentEngine.ContentSettings.Style; - - - /// - /// The current sheet number. - /// - public int Page { get; set; } - - public Macros(SheetViewModel svm) { - this.svm = svm; - } - // https://stackoverflow.com/questions/62771/how-do-i-check-if-a-given-string-is-a-legal-valid-file-name-under-windows#62855 - private bool IsValidFilename(string testName) { - if (string.IsNullOrEmpty(testName)) { - return false; - } +namespace WinPrint.Core.Models; + +public sealed class Macros(SheetViewModel svm) { + /// + /// The SheetModel the Macros will pull data from. + /// + public SheetViewModel SheetViewModel { get; set; } = svm; + + /// + /// Number of sheets to be printed. + /// + public int NumPages => SheetViewModel.NumSheets; + + /// + /// The extension (including the period "."). + /// + public string FileExtension => + string.IsNullOrEmpty(SheetViewModel.File) ? "" : Path.GetExtension(SheetViewModel.File); + + /// + /// The file name and extension. If FileName was not provided, Title will be used. + /// + public string FileName => GetFileNameOrTitle(); + + /// + /// The Title of the print request. + /// + public string Title => SheetViewModel.Title; + + /// + /// The file name of the file without the extension and period ".". + /// + public string FileNameWithoutExtension => + string.IsNullOrEmpty(SheetViewModel.File) ? "" : Path.GetFileNameWithoutExtension(SheetViewModel.File); + + /// + /// The directory for the specified string without the filename or extension. + /// + public string FileDirectoryName => + (string.IsNullOrEmpty(SheetViewModel.File) ? "" : Path.GetDirectoryName(FullPath)) ?? string.Empty; + + /// + /// The absolute path for the file. + /// + public string FullPath => IsValidFilename(SheetViewModel.File) ? Path.GetFullPath(SheetViewModel.File) : + string.IsNullOrEmpty(SheetViewModel.File) ? "" : SheetViewModel.File; + + /// + /// The time and date when printed. + /// + public DateTime DatePrinted => DateTime.Now; + + /// + /// The time and date the file was last revised. + /// + public DateTime DateRevised => IsValidFilename(SheetViewModel.File) + ? File.GetLastWriteTime(SheetViewModel.File) + : DateTime.MinValue; + + /// + /// The time and date the file was created. + /// + public DateTime DateCreated => IsValidFilename(SheetViewModel.File) + ? File.GetCreationTime(SheetViewModel.File) + : DateTime.MinValue; + + /// + /// The language (e.g. "C#" or "java"). + /// + public string Language => string.IsNullOrEmpty(SheetViewModel.Language) ? string.Empty : SheetViewModel.Language; + + /// + /// The Contetn Type (e.g. "text/x-csharp") + /// + public string? ContentType => + string.IsNullOrEmpty(SheetViewModel.ContentType) ? string.Empty : SheetViewModel.ContentType; + + /// + /// The file content type engine name (e.g. "TextCte", "AnsiCte"). + /// + public string CteName => SheetViewModel.ContentEngine.GetType().Name; + + /// + /// The style used for formatting (e.g. "default" or "colorful"; from Pygments.org). + /// + public string Style => SheetViewModel.ContentEngine.ContentSettings == null + ? "" + : SheetViewModel.ContentEngine.ContentSettings.Style; + + + /// + /// The current sheet number. + /// + public int Page { get; set; } + + // https://stackoverflow.com/questions/62771/how-do-i-check-if-a-given-string-is-a-legal-valid-file-name-under-windows#62855 + private bool IsValidFilename(string testName) { + if (string.IsNullOrEmpty(testName)) { + return false; + } - var containsABadCharacter = new Regex("[" - + Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]"); - if (containsABadCharacter.IsMatch(testName)) { return false; }; + var containsABadCharacter = new Regex("[" + + Regex.Escape(new string(Path.GetInvalidPathChars())) + "]"); + if (containsABadCharacter.IsMatch(testName)) { return false; } - // other checks for UNC, drive-path format, etc + ; - if (!File.Exists(testName)) { - return false; - } + // other checks for UNC, drive-path format, etc - return true; + if (!File.Exists(testName)) { + return false; } - // Title and FileName are synomous. - private string GetFileNameOrTitle() { - var retval = ""; - - if (string.IsNullOrEmpty(svm.File)) { - return retval; - } + return true; + } - try { - retval = Path.GetFileName(svm.File); - } - catch (ArgumentException) { - // invalid char in path - retval = svm.File; - } + // Title and FileName are synomous. + private string GetFileNameOrTitle() { + var retval = ""; + if (string.IsNullOrEmpty(SheetViewModel.File)) { return retval; } - /// - /// Replaces macros of the form "{property:format}" using regex and Dynamic Invoke - /// From https://stackoverflow.com/questions/39874172/dynamic-string-interpolation/39900731#39900731 - /// and https://haacked.com/archive/2009/01/14/named-formats-redux.aspx/ - /// - /// Note this does not work perfectly. Specifically some invalid format specifiers just cause - /// string.Format to generate garbage (e.g. {DatePrinted:HelloWorld}) - /// - /// A string with macros to be replaced - /// - /// - public string ReplaceMacros(string value) { - return Regex.Replace(value, @"(?\{)+(?[\w\.\[\]]+)(?:[^}]+)?(?\})+", match => { - var p = Expression.Parameter(typeof(Macros), "Macros"); - - var startGroup = match.Groups["start"]; - var propertyGroup = match.Groups["property"]; - var formatGroup = match.Groups["format"]; - var endGroup = match.Groups["end"]; - - try { - // Generate and parse a LambdaExpression - var e = DynamicExpressionParser.ParseLambda(new[] { p }, null, propertyGroup.Value); - var computedValue = e.Compile().DynamicInvoke(this); - if (formatGroup.Success) { - // There's a format specifier - // The following does: string.Format("{0:formatGroup.Value}", computedValue) - return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0" + formatGroup.Value + "}", computedValue); - } - else { - // Get here when there is no format specifier. - return (computedValue ?? "").ToString(); - } - } - catch { //(ParseException ex) { - // Non-existant Property or other parse error - return match.Groups[0].Value; - } - }); + try { + retval = Path.GetFileName(SheetViewModel.File); + } + catch (ArgumentException) { + // invalid char in path + retval = SheetViewModel.File; } + + return retval; + } + + /// + /// Replaces macros of the form "{property:format}" using regex and Dynamic Invoke + /// From https://stackoverflow.com/questions/39874172/dynamic-string-interpolation/39900731#39900731 + /// and https://haacked.com/archive/2009/01/14/named-formats-redux.aspx/ + /// Note this does not work perfectly. Specifically some invalid format specifiers just cause + /// string.Format to generate garbage (e.g. {DatePrinted:HelloWorld}) + /// + /// A string with macros to be replaced + /// + /// + /// + public string ReplaceMacros(string? value) { + return Regex.Replace(value!, @"(?\{)+(?[\w\.\[\]]+)(?:[^}]+)?(?\})+", match => { + var p = Expression.Parameter(typeof(Macros), "Macros"); + + var startGroup = match.Groups["start"]; + var propertyGroup = match.Groups["property"]; + var formatGroup = match.Groups["format"]; + var endGroup = match.Groups["end"]; + + try { + // Generate and parse a LambdaExpression + var e = DynamicExpressionParser.ParseLambda(new[] { p }, null, propertyGroup.Value); + var computedValue = e.Compile().DynamicInvoke(this); + if (formatGroup.Success) { + // There's a format specifier + // The following does: string.Format("{0:formatGroup.Value}", computedValue) + return string.Format(CultureInfo.InvariantCulture, "{0" + formatGroup.Value + "}", computedValue); + } + + // Get here when there is no format specifier. + return (computedValue ?? "").ToString()!; + } + catch { + //(ParseException ex) { + // Non-existent Property or other parse error + return match.Groups[0].Value; + } + }); } } diff --git a/src/WinPrint.Core/Models/ModelBase.cs b/src/WinPrint.Core/Models/ModelBase.cs index 9cdbbac..04edfff 100644 --- a/src/WinPrint.Core/Models/ModelBase.cs +++ b/src/WinPrint.Core/Models/ModelBase.cs @@ -5,125 +5,128 @@ using System.Runtime.CompilerServices; using System.Text.Json; -namespace WinPrint.Core.Models { +namespace WinPrint.Core.Models; - [System.AttributeUsage(System.AttributeTargets.Property)] - public class SafeForTelemetry : System.Attribute { - } +[AttributeUsage(AttributeTargets.Property)] +public class SafeForTelemetry : Attribute { +} - public abstract class ModelBase : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } +public abstract class ModelBase : INotifyPropertyChanged { + public event PropertyChangedEventHandler? PropertyChanged; - protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) { - if (EqualityComparer.Default.Equals(field, value)) { - return false; - } + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } - field = value; - OnPropertyChanged(propertyName); - return true; + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { + if (EqualityComparer.Default.Equals(field, value)) { + return false; } - public override string ToString() { - return JsonSerializer.Serialize(this, GetType()); - //return base.ToString(); - } + field = value; + OnPropertyChanged(propertyName); + return true; + } - /// - /// Returns a dictionary containing all of the properites of the object that - /// are safe to track via telemetry. Use the [SafeForTelemetry] attribute on any - /// property of a class drived from ModelBase to enable emitting to telemetry. - /// - /// A dictionary with the properties and values (as strings). Suitable for calling TrackEvent(). - public virtual IDictionary GetTelemetryDictionary() { - var dictionary = new Dictionary(); - foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(this)) { - if (property.Attributes.Contains(new SafeForTelemetry())) { - var value = property.GetValue(this); - if (value != null) { - if (property.PropertyType.IsSubclassOf(typeof(ModelBase))) { - // Go deep - var propDict = ((ModelBase)value).GetTelemetryDictionary(); - dictionary.Add(property.Name, JsonSerializer.Serialize(propDict, propDict.GetType())); - } - else { - dictionary.Add(property.Name, JsonSerializer.Serialize(value, value.GetType())); - } + public override string ToString() { + return JsonSerializer.Serialize(this, GetType()); + //return base.ToString(); + } + + /// + /// Returns a dictionary containing all the properties of the object that + /// are safe to track via telemetry. Use the [SafeForTelemetry] attribute on any + /// property of a class derived from ModelBase to enable emitting to telemetry. + /// + /// A dictionary with the properties and values (as strings). Suitable for calling TrackEvent(). + public virtual IDictionary GetTelemetryDictionary() { + var dictionary = new Dictionary(); + foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(this)) { + if (property.Attributes.Contains(new SafeForTelemetry())) { + var value = property.GetValue(this); + if (value != null) { + if (property.PropertyType.IsSubclassOf(typeof(ModelBase))) { + // Go deep + var propDict = ((ModelBase)value).GetTelemetryDictionary(); + dictionary.Add(property.Name, JsonSerializer.Serialize(propDict, propDict.GetType())); + } + else { + dictionary.Add(property.Name, JsonSerializer.Serialize(value, value.GetType())); } } } - return dictionary; } - /// - /// System.Text.Json does not support copying a deserialized object to an existing instance. - /// To work around this, ModelBase implements a 'deep, memberwise clone' method. - /// `Named CopyPropertiesFrom` to make it clear what it does. - /// TOOD: When System.Text.Json implements `PopulateObject` revisit - /// https://github.com/dotnet/corefx/issues/37627 - /// - /// - public virtual void CopyPropertiesFrom(ModelBase source) { - if (source is null) { - return; - } + return dictionary; + } - var sourceProps = source.GetType().GetProperties().Where(x => x.CanRead).ToList(); - var destProps = GetType().GetProperties().Where(x => x.CanWrite).ToList(); - foreach (var (sourceProp, destProp) in - // check if the property can be set or no. - from sourceProp in sourceProps - where destProps.Any(x => x.Name == sourceProp.Name) - let destProp = destProps.First(x => x.Name == sourceProp.Name) - where destProp.CanWrite - select (sourceProp, destProp)) { - // "System.Collections.Generic.IList` - if (sourceProp.Name != "Sheets") { - if (sourceProp.PropertyType.IsSubclassOf(typeof(ModelBase))) { - // Property is subclass of ModelBase - Recurse through sub-objects - if ((ModelBase)sourceProp.GetValue(source, null) != null) { - if ((ModelBase)destProp.GetValue(this) is null) { - // Destination is null. Create it. - destProp.SetValue(this, (ModelBase)Activator.CreateInstance(destProp.PropertyType)); + /// + /// System.Text.Json does not support copying a deserialized object to an existing instance. + /// To work around this, ModelBase implements a 'deep, memberwise clone' method. + /// `Named CopyPropertiesFrom` to make it clear what it does. + /// TODO: When System.Text.Json implements `PopulateObject` revisit + /// https://github.com/dotnet/corefx/issues/37627 + /// + /// + public virtual void CopyPropertiesFrom(ModelBase? source) { + if (source is null) { + return; + } - } - ((ModelBase)destProp.GetValue(this)).CopyPropertiesFrom((ModelBase)sourceProp.GetValue(source, null)); - } - else { - destProp.SetValue(this, null); + var sourceProps = source.GetType().GetProperties().Where(x => x.CanRead).ToList(); + var destProps = GetType().GetProperties().Where(x => x.CanWrite).ToList(); + foreach (var (sourceProp, destProp) in + // check if the property can be set or no. + from sourceProp in sourceProps + where destProps.Any(x => x.Name == sourceProp.Name) + let destProp = destProps.First(x => x.Name == sourceProp.Name) + where destProp.CanWrite + select (sourceProp, destProp)) { + // "System.Collections.Generic.IList` + if (sourceProp.Name != "Sheets") { + if (sourceProp.PropertyType.IsSubclassOf(typeof(ModelBase))) { + // Property is subclass of ModelBase - Recurse through sub-objects + if ((ModelBase)sourceProp.GetValue(source, null)! != null) { + if ((ModelBase)destProp.GetValue(this)! is null) { + // Destination is null. Create it. + destProp.SetValue(this, (ModelBase)Activator.CreateInstance(destProp.PropertyType)); } + + ((ModelBase)destProp.GetValue(this)).CopyPropertiesFrom( + (ModelBase)sourceProp.GetValue(source, null)); } else { - destProp.SetValue(this, sourceProp.GetValue(source, null), null); + destProp.SetValue(this, null); } } else { - var sourceList = (Dictionary)sourceProp.GetValue(source); - if (sourceList == null) { - sourceList = new Dictionary(); - } + destProp.SetValue(this, sourceProp.GetValue(source, null), null); + } + } + else { + var sourceList = (Dictionary)sourceProp.GetValue(source); + if (sourceList == null) { + sourceList = new Dictionary(); + } - var destList = (Dictionary)destProp.GetValue(this); - if (destList == null) { - destList = new Dictionary(); - } + var destList = (Dictionary)destProp.GetValue(this); + if (destList == null) { + destList = new Dictionary(); + } - foreach (var src in sourceList) { - if (destList.ContainsKey(src.Key)) { - destList[src.Key].CopyPropertiesFrom(src.Value); - } - else { - destList[src.Key] = new SheetSettings(); - destList[src.Key].CopyPropertiesFrom(src.Value); - } + foreach (var src in sourceList) { + if (destList.ContainsKey(src.Key)) { + destList[src.Key].CopyPropertiesFrom(src.Value); } - if (destProp.GetValue(this) is null) { - destProp.SetValue(this, destList, null); + else { + destList[src.Key] = new SheetSettings(); + destList[src.Key].CopyPropertiesFrom(src.Value); } } + + if (destProp.GetValue(this) is null) { + destProp.SetValue(this, destList, null); + } } } } diff --git a/src/WinPrint.Core/Models/ModelLocator.cs b/src/WinPrint.Core/Models/ModelLocator.cs index 012b4c4..a85c912 100644 --- a/src/WinPrint.Core/Models/ModelLocator.cs +++ b/src/WinPrint.Core/Models/ModelLocator.cs @@ -1,38 +1,37 @@ - -using GalaSoft.MvvmLight.Ioc; +using GalaSoft.MvvmLight.Ioc; using WinPrint.Core.Services; //using WinPrint.Services; //using WinPrint.Views; -namespace WinPrint.Core.Models { - public class ModelLocator { - private static ModelLocator _current; +namespace WinPrint.Core.Models; - public static ModelLocator Current => _current ?? (_current = new ModelLocator()); +public class ModelLocator { + private static ModelLocator? _current; - private ModelLocator() { - // Register the models via the Servcies Factory - SimpleIoc.Default.Register(SettingsService.Create); - SimpleIoc.Default.Register(FileTypeMappingService.Create); - SimpleIoc.Default.Register(); - } + private ModelLocator() { + // Register the models via the Services Factory + SimpleIoc.Default.Register(SettingsService.Create); + SimpleIoc.Default.Register(FileTypeMappingService.Create); + SimpleIoc.Default.Register(); + } + + public static ModelLocator? Current => _current ??= new ModelLocator(); - public Models.Settings Settings => SimpleIoc.Default.GetInstance(); + public Settings Settings => SimpleIoc.Default.GetInstance(); - public Models.Options Options => SimpleIoc.Default.GetInstance(); - public Models.FileTypeMapping FileTypeMapping => SimpleIoc.Default.GetInstance(); + public Options Options => SimpleIoc.Default.GetInstance(); + public FileTypeMapping FileTypeMapping => SimpleIoc.Default.GetInstance(); - public void Register() - where VM : class { - SimpleIoc.Default.Register(); - } + public void Register() + where VM : class { + SimpleIoc.Default.Register(); + } - public static void Reset() { - _current = null; - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - } + public static void Reset() { + _current = null; + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); } } diff --git a/src/WinPrint.Core/Models/Options.cs b/src/WinPrint.Core/Models/Options.cs index f0b513b..6142283 100644 --- a/src/WinPrint.Core/Models/Options.cs +++ b/src/WinPrint.Core/Models/Options.cs @@ -4,97 +4,88 @@ using CommandLine; using CommandLine.Text; -namespace WinPrint.Core.Models { - public class Options : ModelBase { - - // Files - [JsonIgnore] - [Value(0, Required = true, MetaName = "", HelpText = "One or more files to be printed.")] - public IEnumerable Files { get; set; } - - /// - /// Provides the count of files specified on the command line for telemetry purposes. - /// - [JsonIgnore] - [SafeForTelemetry] - // TODO: This won't work once we support wildcards - public int NumFiles => Files.Count(); - - // Print options - [SafeForTelemetry] - [Option('s', "sheet", Required = false, HelpText = "Sheet definition to use for formatting. Use sheet ID or friendly name.")] - public string Sheet { get; set; } - - [SafeForTelemetry] - [Option('l', "landscape", Required = false, Default = false, HelpText = "Force landscape orientation.")] - public bool Landscape { get; set; } - - [SafeForTelemetry] - [Option('r', "portrait", Required = false, Default = false, HelpText = "Force portrait orientation.")] - public bool Portrait { get; set; } - - [SafeForTelemetry] - [Option('p', "printer", HelpText = "Printer name.")] - public string Printer { get; set; } - - [SafeForTelemetry] - [Option('z', "paper-size", HelpText = "Paper size name.")] - public string PaperSize { get; set; } - - [SafeForTelemetry] - [Option('f', "from-sheet", Default = 0, HelpText = "Number of first sheet to print (may be used with --to-sheet).")] - public int FromPage { get; set; } - - [SafeForTelemetry] - [Option('t', "to-sheet", Default = 0, HelpText = "Number of last sheet to print (may be used with --from-sheet).")] - public int ToPage { get; set; } - - [SafeForTelemetry] - [Option('c', "count-sheets", Default = false, Required = false, HelpText = "Exit code is set to number of sheets that would be printed. Use --verbose to display the count.")] - public bool CountPages { get; set; } - - [SafeForTelemetry] - [Option('e', "content-type-engine", Default = "", Required = false, HelpText = "Name of the Content Type Engine to use for rendering (\"text/plain\", \"text/html\", or \"\".")] - public string ContentType { get; set; } - - // App Options - [SafeForTelemetry] - [Option('v', "verbose", Default = false, HelpText = "Verbose console output (log is always verbose).")] - public bool Verbose { get; set; } - - [SafeForTelemetry] - [Option('d', "debug", Default = false, HelpText = "Debug-level console & log output.")] - public bool Debug { get; set; } - - [SafeForTelemetry] - [Option('g', "gui", Default = false, SetName = "gui", HelpText = "Show WinPrint GUI (to preview or change sheet settings).")] - public bool Gui { get; set; } - - [Usage(ApplicationAlias = "winprint")] - public static IEnumerable Examples => new List() { - new Example("Print Program.cs in landscape mode", new Options { - Files = new List() { { "Program.cs" } }, - Landscape = true - }), - new Example("Print all .cs files on a specific printer with a specific paper size", new Options { - Files = new List() { { "*.cs" } }, - Printer = "Fabricam 535", - PaperSize = "A4" - }), - new Example("Print the first two sheets of Program.cs", new Options { - Files = new List() { { "Program.cs" } }, - FromPage = 1, - ToPage = 2 - }), - new Example("Print Program.cs using the 2 Up sheet definition", new Options { - Files = new List() { { "Program.cs" } }, - Sheet = "2 Up" - }), - new Example("Print tapes.pas using C-like syntax highlighting.", new Options { - Files = new List() { { "tapes.pas" } }, - ContentType= "clike" - }) - }; - - } +namespace WinPrint.Core.Models; + +public class Options : ModelBase { + // Files + [JsonIgnore] + [Value(0, Required = true, MetaName = "", HelpText = "One or more files to be printed.")] + public IEnumerable? Files { get; set; } + + /// + /// Provides the count of files specified on the command line for telemetry purposes. + /// + [JsonIgnore] + [SafeForTelemetry] + // TODO: This won't work once we support wildcards + public int NumFiles => Files!.Count(); + + // Print options + [SafeForTelemetry] + [Option('s', "sheet", Required = false, + HelpText = "Sheet definition to use for formatting. Use sheet ID or friendly name.")] + public string? Sheet { get; set; } + + [SafeForTelemetry] + [Option('l', "landscape", Required = false, Default = false, HelpText = "Force landscape orientation.")] + public bool Landscape { get; set; } + + [SafeForTelemetry] + [Option('r', "portrait", Required = false, Default = false, HelpText = "Force portrait orientation.")] + public bool Portrait { get; set; } + + [SafeForTelemetry] + [Option('p', "printer", HelpText = "Printer name.")] + public string? Printer { get; set; } + + [SafeForTelemetry] + [Option('z', "paper-size", HelpText = "Paper size name.")] + public string? PaperSize { get; set; } + + [SafeForTelemetry] + [Option('f', "from-sheet", Default = 0, HelpText = "Number of first sheet to print (may be used with --to-sheet).")] + public int FromPage { get; set; } + + [SafeForTelemetry] + [Option('t', "to-sheet", Default = 0, HelpText = "Number of last sheet to print (may be used with --from-sheet).")] + public int ToPage { get; set; } + + [SafeForTelemetry] + [Option('c', "count-sheets", Default = false, Required = false, + HelpText = "Exit code is set to number of sheets that would be printed. Use --verbose to display the count.")] + public bool CountPages { get; set; } + + [SafeForTelemetry] + [Option('e', "content-type-engine", Default = "", Required = false, + HelpText = + "Name of the Content Type Engine to use for rendering (\"text/plain\", \"text/html\", or \"\".")] + public string? ContentType { get; set; } + + // App Options + [SafeForTelemetry] + [Option('v', "verbose", Default = false, HelpText = "Verbose console output (log is always verbose).")] + public bool Verbose { get; set; } + + [SafeForTelemetry] + [Option('d', "debug", Default = false, HelpText = "Debug-level console & log output.")] + public bool Debug { get; set; } + + [SafeForTelemetry] + [Option('g', "gui", Default = false, SetName = "gui", + HelpText = "Show WinPrint GUI (to preview or change sheet settings).")] + public bool Gui { get; set; } + + [Usage(ApplicationAlias = "winprint")] + public static IEnumerable Examples => new List { + new("Print Program.cs in landscape mode", + new Options { Files = new List { "Program.cs" }, Landscape = true }), + new("Print all .cs files on a specific printer with a specific paper size", + new Options { Files = new List { "*.cs" }, Printer = "Fabricam 535", PaperSize = "A4" }), + new("Print the first two sheets of Program.cs", + new Options { Files = new List { "Program.cs" }, FromPage = 1, ToPage = 2 }), + new("Print Program.cs using the 2 Up sheet definition", + new Options { Files = new List { "Program.cs" }, Sheet = "2 Up" }), + new("Print tapes.pas using C-like syntax highlighting.", + new Options { Files = new List { "tapes.pas" }, ContentType = "clike" }) + }; } diff --git a/src/WinPrint.Core/Models/Settings.cs b/src/WinPrint.Core/Models/Settings.cs index f1e92dd..3ce1c0a 100644 --- a/src/WinPrint.Core/Models/Settings.cs +++ b/src/WinPrint.Core/Models/Settings.cs @@ -5,387 +5,394 @@ using System.Text.Json.Serialization; using WinPrint.Core.ContentTypeEngines; -namespace WinPrint.Core.Models { - - // - // Summary: - // Specifies how a form window is displayed. - public enum FormWindowState { - // - // Summary: - // A default sized window. - Normal = 0, - // - // Summary: - // A minimized window. - Minimized = 1, - // - // Summary: - // A maximized window. - Maximized = 2 - } +namespace WinPrint.Core.Models; - public class WindowSize { +// +// Summary: +// Specifies how a form window is displayed. - public WindowSize() { - } - public WindowSize(int width, int height) { - Width = width; - Height = height; - } +public class WindowSize { + public WindowSize() { + } - public int Width { get; set; } - public int Height { get; set; } + public WindowSize(int width, int height) { + Width = width; + Height = height; } - public class WindowLocation { - public WindowLocation() { - } - public WindowLocation(int x, int y) { - X = x; - Y = y; - } - public int X { get; set; } - public int Y { get; set; } + public int Width { get; set; } + public int Height { get; set; } +} +public class WindowLocation { + public WindowLocation() { } - public class Settings : ModelBase { - /// - /// Window location - /// - [SafeForTelemetry] - public WindowLocation Location { get => location; set => SetField(ref location, value); } - private WindowLocation location; - - /// - /// Window size - /// - [SafeForTelemetry] - public WindowSize Size { get => size; set => SetField(ref size, value); } - private WindowSize size; - - [SafeForTelemetry] - public FormWindowState WindowState { get => windowState; set => SetField(ref windowState, value); } - private FormWindowState windowState; - - /// - /// Default sheet (guid) - /// - [SafeForTelemetry] - public Guid DefaultSheet { get => defaultSheet; set => SetField(ref defaultSheet, value); } - private Guid defaultSheet; - - /// - /// Sheet definitons - /// - public Dictionary Sheets { get; set; } - - [JsonIgnore] - [SafeForTelemetry] - public int NumSheets { - get { - if (Sheets == null) { - return 0; - } + public WindowLocation(int x, int y) { + X = x; + Y = y; + } - return Sheets.Count; + public int X { get; set; } + public int Y { get; set; } +} + +public class Settings : ModelBase { + private Font _diagnosticRulesFont = new() { Family = "sansserif", Size = 8F, Style = FontStyle.Regular }; + private bool _previewBounds; + private bool _previewContentBounds; + private bool _previewHardMargins; + private bool _previewHeaderFooterBounds; + private bool _previewMargins; + private bool _previewPageBounds; + private bool _previewPageSize; + private bool _previewPrintableArea; + private bool _printBounds; + private bool _printContentBounds; + private bool _printDialog; + private bool _printHardMargins; + private bool _printHeaderFooterBounds; + private bool _printMargins; + private bool _printPageBounds; + private bool _printPageSize; + private bool _printPrintableArea; + private Guid defaultSheet; + private WindowLocation location; + private WindowSize size; + private FormWindowState windowState; + + /// + /// Window location + /// + [SafeForTelemetry] + public WindowLocation Location { get => location; set => SetField(ref location, value); } + + /// + /// Window size + /// + [SafeForTelemetry] + public WindowSize Size { get => size; set => SetField(ref size, value); } + + [SafeForTelemetry] + public FormWindowState WindowState { get => windowState; set => SetField(ref windowState, value); } + + /// + /// Default sheet (guid) + /// + [SafeForTelemetry] + public Guid DefaultSheet { get => defaultSheet; set => SetField(ref defaultSheet, value); } + + /// + /// Sheet definitons + /// + public Dictionary Sheets { get; set; } + + [JsonIgnore] + [SafeForTelemetry] + public int NumSheets { + get { + if (Sheets == null) { + return 0; } + + return Sheets.Count; } + } - /// - /// Content type handlers - /// - public AnsiCte AnsiContentTypeEngineSettings { get; set; } - public TextCte TextContentTypeEngineSettings { get; set; } - public HtmlCte HtmlContentTypeEngineSettings { get; set; } - - /// - /// File Type Mappings - note legacy property name. Don't change. - /// - [JsonPropertyName("languageAssociations")] - public FileTypeMapping FileTypeMapping { get; set; } - - [JsonIgnore] - [SafeForTelemetry] - public int NumFilesAssociations { - get { - if (FileTypeMapping == null || FileTypeMapping.FilesAssociations == null) { - return 0; - } + [SafeForTelemetry] public string? DefaultContentType { get; set; } // "text/plain"; - return FileTypeMapping.FilesAssociations.Count; - } - } + [SafeForTelemetry] public string DefaultCteClassName { get; set; } // "AnsiCte"; - [JsonIgnore] - [SafeForTelemetry] - public int NumLanguages { - get { - if (FileTypeMapping == null || FileTypeMapping.ContentTypes == null) { - return 0; - } + [SafeForTelemetry] public string DefaultSyntaxHighlighterCteNameClassName { get; set; } // "AnsiCte"; + + /// + /// Content type handlers + /// + public AnsiCte AnsiContentTypeEngineSettings { get; set; } - return FileTypeMapping.ContentTypes.Count; + public TextCte TextContentTypeEngineSettings { get; set; } + public HtmlCte HtmlContentTypeEngineSettings { get; set; } + + /// + /// File Type Mappings - note legacy property name. Don't change. + /// + [JsonPropertyName("languageAssociations")] + public FileTypeMapping FileTypeMapping { get; set; } + + [JsonIgnore] + [SafeForTelemetry] + public int NumFilesAssociations { + get { + if (FileTypeMapping == null || FileTypeMapping.FilesAssociations == null) { + return 0; } + + return FileTypeMapping.FilesAssociations.Count; } + } - /// - /// Diagnostic settings - /// - // TOOD: These should go on printPreview model? - /// - /// Font used for diagnostic rules - /// - [SafeForTelemetry] - public Font DiagnosticRulesFont { get => _diagnosticRulesFont; set => SetField(ref _diagnosticRulesFont, value); } - - private Font _diagnosticRulesFont = new Font() { Family = "sansserif", Size = 8F, Style = FontStyle.Regular }; - private bool _previewPrintableArea = false; - private bool _printPrintableArea = false; - private bool _previewPageSize = false; - private bool _printPageSize = false; - private bool _previewMargins = false; - private bool _printMargins = false; - private bool _previewHardMargins = false; - private bool _printHardMargins = false; - private bool _printBounds = false; - private bool _previewBounds = false; - private bool _printContentBounds = false; - private bool _previewContentBounds = false; - private bool _printHeaderFooterBounds = false; - private bool _previewHeaderFooterBounds = false; - private bool _printPageBounds = false; - private bool _previewPageBounds = false; - private bool _printDialog; - - [SafeForTelemetry] - public bool PreviewPrintableArea { get => _previewPrintableArea; set => SetField(ref _previewPrintableArea, value); } - [SafeForTelemetry] - public bool PrintPrintableArea { get => _printPrintableArea; set => SetField(ref _printPrintableArea, value); } - [SafeForTelemetry] - public bool PreviewPaperSize { get => _previewPageSize; set => SetField(ref _previewPageSize, value); } - [SafeForTelemetry] - public bool PrintPaperSize { get => _printPageSize; set => SetField(ref _printPageSize, value); } - [SafeForTelemetry] - public bool PreviewMargins { get => _previewMargins; set => SetField(ref _previewMargins, value); } - [SafeForTelemetry] - public bool PrintMargins { get => _printMargins; set => SetField(ref _printMargins, value); } - [SafeForTelemetry] - public bool PreviewHardMargins { get => _previewHardMargins; set => SetField(ref _previewHardMargins, value); } - [SafeForTelemetry] - public bool PrintHardMargins { get => _printHardMargins; set => SetField(ref _printHardMargins, value); } - [SafeForTelemetry] - public bool PrintBounds { get => _printBounds; set => SetField(ref _printBounds, value); } - [SafeForTelemetry] - public bool PreviewBounds { get => _previewBounds; set => SetField(ref _previewBounds, value); } - [SafeForTelemetry] - public bool PrintContentBounds { get => _printContentBounds; set => SetField(ref _printContentBounds, value); } - [SafeForTelemetry] - public bool PreviewContentBounds { get => _previewContentBounds; set => SetField(ref _previewContentBounds, value); } - [SafeForTelemetry] - public bool PrintHeaderFooterBounds { get => _printHeaderFooterBounds; set => SetField(ref _printHeaderFooterBounds, value); } - [SafeForTelemetry] - public bool PreviewHeaderFooterBounds { get => _previewHeaderFooterBounds; set => SetField(ref _previewHeaderFooterBounds, value); } - [SafeForTelemetry] - public bool PreviewPageBounds { get => _previewPageBounds; set => SetField(ref _previewPageBounds, value); } - [SafeForTelemetry] - public bool PrintPageBounds { get => _printPageBounds; set => SetField(ref _printPageBounds, value); } - - /// - /// If true, print dialog is shown when printing - /// - [SafeForTelemetry] - public bool ShowPrintDialog { get => _printDialog; set => SetField(ref _printDialog, value); } - - public Settings() { + [JsonIgnore] + [SafeForTelemetry] + public int NumLanguages { + get { + if (FileTypeMapping == null || FileTypeMapping.ContentTypes == null) { + return 0; + } + return FileTypeMapping.ContentTypes.Count; } + } - /// - /// Creates a default set of settings that can be persisted to create - /// the .config.json file. - /// - /// A Settings object with default settings. - public static Settings CreateDefaultSettings() { - var monoSpaceFamily = "monospace"; - var sansSerifFamily = "sansserif"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - monoSpaceFamily = "Consolas"; - sansSerifFamily = "Calibri"; - } + /// + /// Diagnostic settings + /// + // TOOD: These should go on printPreview model? + /// + /// Font used for diagnostic rules + /// + [SafeForTelemetry] + public Font DiagnosticRulesFont { get => _diagnosticRulesFont; set => SetField(ref _diagnosticRulesFont, value); } + + [SafeForTelemetry] + public bool PreviewPrintableArea { + get => _previewPrintableArea; + set => SetField(ref _previewPrintableArea, value); + } - var defaultContentFontFamily = monoSpaceFamily; - var defaultContentFontSize = 8F; - var defaultContentFontStyle = FontStyle.Regular; + [SafeForTelemetry] + public bool PrintPrintableArea { get => _printPrintableArea; set => SetField(ref _printPrintableArea, value); } - var defaultHFFontFamily = sansSerifFamily; - var defaultHFFontSize = 10F; - var defaultHFFontStyle = FontStyle.Bold; + [SafeForTelemetry] + public bool PreviewPaperSize { get => _previewPageSize; set => SetField(ref _previewPageSize, value); } - var defaultHeaderText = "{DateRevised:D}|{FileName}|Language: {Language}"; - var defualtFooterText = "Printed with love by WinPrint||Page {Page} of {NumPages}"; + [SafeForTelemetry] public bool PrintPaperSize { get => _printPageSize; set => SetField(ref _printPageSize, value); } - var settings = new Settings { + [SafeForTelemetry] + public bool PreviewMargins { get => _previewMargins; set => SetField(ref _previewMargins, value); } - //settings.size = new WindowSize(1024, 800); - //settings.location = new WindowLocation(100, 100); + [SafeForTelemetry] public bool PrintMargins { get => _printMargins; set => SetField(ref _printMargins, value); } - AnsiContentTypeEngineSettings = new AnsiCte() { - ContentSettings = new ContentSettings() { - Style = "pastie", - } - }, + [SafeForTelemetry] + public bool PreviewHardMargins { get => _previewHardMargins; set => SetField(ref _previewHardMargins, value); } - TextContentTypeEngineSettings = new TextCte() { - // This font will be overriddent by Sheet defined fonts (if any) - //ContentSettings = new ContentSettings() { - // Font = new Font() { Family = defaultContentFontFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle }, - // Darkness = 100, - // Grayscale = false, - // PrintBackground = true - //}, - //LineNumbers = true, - //LineNumberSeparator = false, - //NewPageOnFormFeed = false, - //TabSpaces = 4 - }, + [SafeForTelemetry] + public bool PrintHardMargins { get => _printHardMargins; set => SetField(ref _printHardMargins, value); } - // Html fonts are determined by: - // 1) Sheet (all HTML & CSS ignored) - // 2) winprint.css (Body -> Font, Pre -> Monospace Font) - // 3) HtmlileContent settings - HtmlContentTypeEngineSettings = new HtmlCte() { - //ContentSettings = new ContentSettings() { - // Font = new Font() { Family = sansSerifFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle }, - // Darkness = 100, - // Grayscale = false, - // PrintBackground = true - //}, - }, + [SafeForTelemetry] public bool PrintBounds { get => _printBounds; set => SetField(ref _printBounds, value); } - DefaultSheet = Uuid.DefaultSheet, - Sheets = new Dictionary() - }; - - // Create default 2 Up sheet - var sheet = new SheetSettings() { - Name = "Default 2-Up", - Columns = 2, - Rows = 1, - Landscape = true, - Padding = 3, - PageSeparator = false, - ContentSettings = new ContentSettings() { - Font = new Font() { Family = defaultContentFontFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle }, - Style = "pastie", - Darkness = 100, - Grayscale = false, - PrintBackground = true - } - }; - sheet.Header.Enabled = true; - sheet.Header.Text = defaultHeaderText; - sheet.Header.BottomBorder = true; - sheet.Header.Font = new Font() { Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle }; - sheet.Header.VerticalPadding = 1; - - sheet.Footer.Enabled = true; - sheet.Footer.TopBorder = true; - sheet.Footer.Text = defualtFooterText; - sheet.Footer.Font = new Font() { Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle }; - sheet.Footer.VerticalPadding = 1; - - sheet.Margins.Left = sheet.Margins.Top = sheet.Margins.Right = sheet.Margins.Bottom = 30; - settings.Sheets.Add(Uuid.DefaultSheet.ToString(), sheet); - - // Create default 1 Up sheet - sheet = new SheetSettings() { - Name = "Default 1-Up", - Columns = 1, - Rows = 1, - Landscape = false, - Padding = 3, - PageSeparator = false, - ContentSettings = new ContentSettings() { - Font = new Font() { Family = defaultContentFontFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle }, - Style = "pastie", - Darkness = 100, - Grayscale = false, - PrintBackground = true, - LineNumberSeparator = true, - LineNumbers = true, + [SafeForTelemetry] public bool PreviewBounds { get => _previewBounds; set => SetField(ref _previewBounds, value); } - } - }; - - sheet.Header.Enabled = true; - sheet.Header.Text = defaultHeaderText; - sheet.Header.BottomBorder = true; - sheet.Header.Font = new Font() { Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle }; - sheet.Header.VerticalPadding = 1; - - sheet.Footer.Enabled = true; - sheet.Footer.Text = defualtFooterText; - sheet.Footer.TopBorder = true; - sheet.Footer.Font = new Font() { Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle }; - sheet.Footer.VerticalPadding = 1; - - sheet.Margins.Left = sheet.Margins.Top = sheet.Margins.Right = sheet.Margins.Bottom = 30; - settings.Sheets.Add(Uuid.DefaultSheet1Up.ToString(), sheet); - - settings.FileTypeMapping = new FileTypeMapping() { - FilesAssociations = new Dictionary() { - // Enables printing our own config files - { "*.config", "application/json" }, - // Enables printing HTML - { "*.htm", "text/html" }, - { "*.html", "text/html" }, - // Enables Icon which Unicon is based on - { "*.icon", "text/unicon" }, + [SafeForTelemetry] + public bool PrintContentBounds { get => _printContentBounds; set => SetField(ref _printContentBounds, value); } - }, - // text/plain - because it is not defined by Pygments - // text/ansi - because it is not defined by Pygments - // icon - Icon is so esoteric it makes a good test - ContentTypes = new List() { - new ContentType() { - Id = "text/plain", - Title = "Plain Text", - Extensions = new List() { - "*.txt" - }, - Aliases = new List() { - "text", - } - }, - new ContentType() { - Id = "text/ansi", - Title = "ANSI Text", - Extensions = new List() { - "*.an", - "*.ans", - "*.ansi", - }, - Aliases = new List() { - "ansi", - } - }, - //new ContentType() { - // Id = "text/x-icon", - // Title = "Icon Programming Language", - // Extensions = new List() { - // "*.icon" - // }, - // Aliases = new List() { - // "Unicon" - // } - //} - } - }; + [SafeForTelemetry] + public bool PreviewContentBounds { + get => _previewContentBounds; + set => SetField(ref _previewContentBounds, value); + } - return settings; + [SafeForTelemetry] + public bool PrintHeaderFooterBounds { + get => _printHeaderFooterBounds; + set => SetField(ref _printHeaderFooterBounds, value); + } + + [SafeForTelemetry] + public bool PreviewHeaderFooterBounds { + get => _previewHeaderFooterBounds; + set => SetField(ref _previewHeaderFooterBounds, value); + } + + [SafeForTelemetry] + public bool PreviewPageBounds { get => _previewPageBounds; set => SetField(ref _previewPageBounds, value); } + + [SafeForTelemetry] + public bool PrintPageBounds { get => _printPageBounds; set => SetField(ref _printPageBounds, value); } + + /// + /// If true, print dialog is shown when printing + /// + [SafeForTelemetry] + public bool ShowPrintDialog { get => _printDialog; set => SetField(ref _printDialog, value); } + + /// + /// Creates a default set of settings that can be persisted to create + /// the .config.json file. + /// + /// A Settings object with default settings. + public static Settings CreateDefaultSettings() { + var monoSpaceFamily = "monospace"; + var sansSerifFamily = "sansserif"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + monoSpaceFamily = "Consolas"; + sansSerifFamily = "Calibri"; } + var defaultContentFontFamily = monoSpaceFamily; + var defaultContentFontSize = 8F; + var defaultContentFontStyle = FontStyle.Regular; + + var defaultHFFontFamily = sansSerifFamily; + var defaultHFFontSize = 10F; + var defaultHFFontStyle = FontStyle.Bold; + + var defaultHeaderText = "{DateRevised:D}|{FileName}|Language: {Language}"; + var defualtFooterText = "Printed with love by WinPrint||Page {Page} of {NumPages}"; + + var settings = new Settings { + //settings.size = new WindowSize(1024, 800); + //settings.location = new WindowLocation(100, 100); + + DefaultContentType = "text/plain", + DefaultCteClassName = "AnsiCte", + DefaultSyntaxHighlighterCteNameClassName = "AnsiCte", + AnsiContentTypeEngineSettings = new AnsiCte { ContentSettings = new ContentSettings { Style = "pastie" } }, + TextContentTypeEngineSettings = new TextCte { + // This font will be overriddent by Sheet defined fonts (if any) + //ContentSettings = new ContentSettings() { + // Font = new Font() { Family = defaultContentFontFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle }, + // Darkness = 100, + // Grayscale = false, + // PrintBackground = true + //}, + //LineNumbers = true, + //LineNumberSeparator = false, + //NewPageOnFormFeed = false, + //TabSpaces = 4 + }, + + // Html fonts are determined by: + // 1) Sheet (all HTML & CSS ignored) + // 2) winprint.css (Body -> Font, Pre -> Monospace Font) + // 3) HtmlileContent settings + HtmlContentTypeEngineSettings = new HtmlCte { + //ContentSettings = new ContentSettings() { + // Font = new Font() { Family = sansSerifFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle }, + // Darkness = 100, + // Grayscale = false, + // PrintBackground = true + //}, + }, + DefaultSheet = Uuid.DefaultSheet, + Sheets = new Dictionary() + }; + + // Create default 2 Up sheet + var sheet = new SheetSettings { + Name = "Default 2-Up", + Columns = 2, + Rows = 1, + Landscape = true, + Padding = 3, + PageSeparator = false, + ContentSettings = new ContentSettings { + Font = new Font { + Family = defaultContentFontFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle + }, + Style = "pastie", + Darkness = 100, + Grayscale = false, + PrintBackground = true + } + }; + sheet.Header.Enabled = true; + sheet.Header.Text = defaultHeaderText; + sheet.Header.BottomBorder = true; + sheet.Header.Font = new Font { + Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle + }; + sheet.Header.VerticalPadding = 1; + + sheet.Footer.Enabled = true; + sheet.Footer.TopBorder = true; + sheet.Footer.Text = defualtFooterText; + sheet.Footer.Font = new Font { + Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle + }; + sheet.Footer.VerticalPadding = 1; + + sheet.Margins.Left = sheet.Margins.Top = sheet.Margins.Right = sheet.Margins.Bottom = 30; + settings.Sheets.Add(Uuid.DefaultSheet.ToString(), sheet); + + // Create default 1 Up sheet + sheet = new SheetSettings { + Name = "Default 1-Up", + Columns = 1, + Rows = 1, + Landscape = false, + Padding = 3, + PageSeparator = false, + ContentSettings = new ContentSettings { + Font = new Font { + Family = defaultContentFontFamily, Size = defaultContentFontSize, Style = defaultContentFontStyle + }, + Style = "pastie", + Darkness = 100, + Grayscale = false, + PrintBackground = true, + LineNumberSeparator = true, + LineNumbers = true + } + }; + + sheet.Header.Enabled = true; + sheet.Header.Text = defaultHeaderText; + sheet.Header.BottomBorder = true; + sheet.Header.Font = new Font { + Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle + }; + sheet.Header.VerticalPadding = 1; + + sheet.Footer.Enabled = true; + sheet.Footer.Text = defualtFooterText; + sheet.Footer.TopBorder = true; + sheet.Footer.Font = new Font { + Family = defaultHFFontFamily, Size = defaultHFFontSize, Style = defaultHFFontStyle + }; + sheet.Footer.VerticalPadding = 1; + + sheet.Margins.Left = sheet.Margins.Top = sheet.Margins.Right = sheet.Margins.Bottom = 30; + settings.Sheets.Add(Uuid.DefaultSheet1Up.ToString(), sheet); + + settings.FileTypeMapping = new FileTypeMapping { + FilesAssociations = new Dictionary { + // Enables printing our own config files + { "*.config", "application/json" }, + // Enables printing HTML + { "*.htm", "text/html" }, + { "*.html", "text/html" }, + // Enables Icon which Unicon is based on + { "*.icon", "text/unicon" } + }, + // text/plain - because it is not defined by Pygments + // text/ansi - because it is not defined by Pygments + // icon - Icon is so esoteric it makes a good test + ContentTypes = new List { + new() { + Id = "text/plain", + Title = "Plain Text", + Extensions = new List { "*.txt" }, + Aliases = new List { "text" } + }, + new() { + Id = "text/ansi", + Title = "ANSI Text", + Extensions = new List { "*.an", "*.ans", "*.ansi" }, + Aliases = new List { "ansi" } + } + //new ContentType() { + // Id = "text/x-icon", + // Title = "Icon Programming Language", + // Extensions = new List() { + // "*.icon" + // }, + // Aliases = new List() { + // "Unicon" + // } + //} + } + }; + + return settings; } } diff --git a/src/WinPrint.Core/Models/SheetSettings.cs b/src/WinPrint.Core/Models/SheetSettings.cs index bd8acb3..5d6c41b 100644 --- a/src/WinPrint.Core/Models/SheetSettings.cs +++ b/src/WinPrint.Core/Models/SheetSettings.cs @@ -1,116 +1,106 @@ using System.Drawing.Printing; -namespace WinPrint.Core.Models { +namespace WinPrint.Core.Models; + +/// +/// Defines the settings for a Sheet (Sheet Definition) +/// +public class SheetSettings : ModelBase { + private int _columns = 1; + + private ContentSettings? _contentSettings; + private int _darkness; + private Footer _footer = new(); + private bool _grayscale; + + private Header _header = new(); + + private bool _landscape; + private Margins _margins = new(0, 0, 0, 0); + + //private Guid id; + private string _name = ""; + private int _padding = 3; + private bool _pageSeparator; + private bool _printBackground = true; + private int _rows = 1; + + /// + /// Sheet name (e.g. "2up Landscape") + /// + [SafeForTelemetry] + public string Name { get => _name; set => SetField(ref _name, value); } + + /// + /// Landscape or Portrait layout + /// + [SafeForTelemetry] + public bool Landscape { get => _landscape; set => SetField(ref _landscape, value); } + + /// + /// Number of rows of pages per sheet + /// + [SafeForTelemetry] + public int Rows { get => _rows; set => SetField(ref _rows, value); } + + /// + /// Number of columns of pages per sheet + /// + [SafeForTelemetry] + public int Columns { get => _columns; set => SetField(ref _columns, value); } + + /// + /// Padding between rows and columns of pages on sheet in 100ths of an inch. + /// + [SafeForTelemetry] + public int Padding { get => _padding; set => SetField(ref _padding, value); } + + [SafeForTelemetry] public bool PageSeparator { get => _pageSeparator; set => SetField(ref _pageSeparator, value); } + + /// + /// Sheet margins in 100ths of an inch. Impacts headers, footers, and content. + /// + [SafeForTelemetry] + public Margins Margins { get => _margins; set => SetField(ref _margins, value); } + /// - /// Defines the settings for a Sheet (Sheet Definition) + /// Font used for content. Will override any content font settings specified by a ContentType provider. /// - public class SheetSettings : ModelBase { - - //private Guid id; - private string name = ""; - private int rows = 1; - private int columns = 1; - private int padding = 3; - private bool pageSeparator; - private Margins margins = new Margins(0, 0, 0, 0); - - private bool landscape; - - private Header header = new Header(); - private Footer footer = new Footer(); - - private ContentSettings contentSettings; - - /// - /// Unique identifier for this Sheet definition. - /// - //public Guid ID { get => id; set => SetField(ref id, value); } - - /// - /// Sheet name (e.g. "2up Landscape") - /// - [SafeForTelemetry] - public string Name { get => name; set => SetField(ref name, value); } - - /// - /// Landscae or Portrait layout - /// - [SafeForTelemetry] - public bool Landscape { get => landscape; set => SetField(ref landscape, value); } - - /// - /// Number of rows of pages per sheet - /// - [SafeForTelemetry] - public int Rows { get => rows; set => SetField(ref rows, value); } - /// - /// Number of columns of pages per sheet - /// - [SafeForTelemetry] - public int Columns { get => columns; set => SetField(ref columns, value); } - - /// - /// Padding between rows and columns of pages on sheet in 100ths of an inch. - /// - [SafeForTelemetry] - public int Padding { get => padding; set => SetField(ref padding, value); } - - [SafeForTelemetry] - public bool PageSeparator { get => pageSeparator; set => SetField(ref pageSeparator, value); } - - /// - /// Sheet margins in 100ths of an inch. Impacts headers, footers, and content. - /// - [SafeForTelemetry] - public Margins Margins { get => margins; set => SetField(ref margins, value); } - - /// - /// Font used for content. Will override any content font settings specified by a ContentType provider. - /// - [SafeForTelemetry] - public ContentSettings ContentSettings { - get => - //if (contentSettings is null) - // contentSettings = new ContentSettings(); - contentSettings; - set => SetField(ref contentSettings, value); - } - - /// - /// Header printed at bottom of each sheet - /// - [SafeForTelemetry] - public Header Header { get => header; set => SetField(ref header, value); } - - /// - /// Footer printed at top of each sheet - /// - [SafeForTelemetry] - public Footer Footer { get => footer; set => SetField(ref footer, value); } - - // The following members are runtime-only and do NOT get persisted, hence "internal" - /// - /// if True, print content background, if present. Otherwise, all backgrounds will be paper color. - /// - internal bool PrintBackground { get => printBackground; set => SetField(ref printBackground, value); } - private bool printBackground = true; - - /// - /// If True, all content will be printed in grayscale. Use Darkness property to change how - /// dark the grey is. - /// - internal bool Grayscale { get => grayscale; set => SetField(ref grayscale, value); } - private bool grayscale = false; - - /// - /// Darkness factor. 0 = RGB. 100 = black. - /// - internal int Darkness { get => darkness; set => SetField(ref darkness, value); } - private int darkness = 0; - - public SheetSettings() { - // Don't specify defaults in constructor; do it through default settings in - // SettingsService.CreateDefaultSettingsFile - } + [SafeForTelemetry] + public ContentSettings? ContentSettings { + get => + //if (contentSettings is null) + // contentSettings = new ContentSettings(); + _contentSettings; + set => SetField(ref _contentSettings, value); } + + /// + /// Header printed at bottom of each sheet + /// + [SafeForTelemetry] + public Header Header { get => _header; set => SetField(ref _header, value); } + + /// + /// Footer printed at top of each sheet + /// + [SafeForTelemetry] + public Footer Footer { get => _footer; set => SetField(ref _footer, value); } + + // The following members are runtime-only and do NOT get persisted, hence "internal" + /// + /// if True, print content background, if present. Otherwise, all backgrounds will be paper color. + /// + internal bool PrintBackground { get => _printBackground; set => SetField(ref _printBackground, value); } + + /// + /// If True, all content will be printed in grayscale. Use Darkness property to change how + /// dark the grey is. + /// + internal bool Grayscale { get => _grayscale; set => SetField(ref _grayscale, value); } + + /// + /// Darkness factor. 0 = RGB. 100 = black. + /// + internal int Darkness { get => _darkness; set => SetField(ref _darkness, value); } } diff --git a/src/WinPrint.Core/Models/Uuid.cs b/src/WinPrint.Core/Models/Uuid.cs index 6602158..70a00e7 100644 --- a/src/WinPrint.Core/Models/Uuid.cs +++ b/src/WinPrint.Core/Models/Uuid.cs @@ -1,19 +1,18 @@ using System; -namespace WinPrint.Core { +namespace WinPrint.Core; - // WinPrint 2.0 has been assigned the following GUIDs by Microsoft - // - // Available range: 0002A5xx-0000-0000-C000-000000000046 - // - // {0002A500-0000-0000-C000-000000000046} - Wix Installer WinPrint UpgradeCode (product family code), Default Sheet ID - // {0002A501-0000-0000-C000-000000000046} - Wix Installer Product Code, Default 1SheetUp ID - // - public static class Uuid { - public static readonly Guid UpgradeCode = Guid.Parse("{0002A500-0000-0000-C000-000000000046}"); - public static readonly Guid ProductCode = Guid.Parse("{0002A501-0000-0000-C000-000000000046}"); +// WinPrint 2 has been assigned the following GUIDs by Microsoft +// +// Available range: 0002A5xx-0000-0000-C000-000000000046 +// +// {0002A500-0000-0000-C000-000000000046} - Wix Installer WinPrint UpgradeCode (product family code), Default Sheet ID +// {0002A501-0000-0000-C000-000000000046} - Wix Installer Product Code, Default 1SheetUp ID +// +public static class Uuid { + public static readonly Guid UpgradeCode = Guid.Parse("{0002A500-0000-0000-C000-000000000046}"); + public static readonly Guid ProductCode = Guid.Parse("{0002A501-0000-0000-C000-000000000046}"); - public static readonly Guid DefaultSheet = Guid.Parse("{0002A500-0000-0000-C000-000000000046}"); - public static readonly Guid DefaultSheet1Up = Guid.Parse("{0002A501-0000-0000-C000-000000000046}"); - } + public static readonly Guid DefaultSheet = Guid.Parse("{0002A500-0000-0000-C000-000000000046}"); + public static readonly Guid DefaultSheet1Up = Guid.Parse("{0002A501-0000-0000-C000-000000000046}"); } diff --git a/src/WinPrint.Core/Print.cs b/src/WinPrint.Core/Print.cs index 62bdabb..4a9f686 100644 --- a/src/WinPrint.Core/Print.cs +++ b/src/WinPrint.Core/Print.cs @@ -1,217 +1,228 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Drawing.Printing; using System.Text; using System.Threading.Tasks; using Serilog; using WinPrint.Core.Services; -namespace WinPrint.Core { +namespace WinPrint.Core; + +/// +/// The Print class is the top-level class for initiating print jobs with winprint. It is the +/// primary class apps like winprint.exe, Out-WinPrint, and winprintgui use to configure, start, +/// and manage print jobs. +/// +public class Print : IDisposable { + // The Windows printer document + + // The WinPrint "document" + private int _curSheet; + + // Protected implementation of Dispose pattern. + // Flag: Has Dispose already been called? + private bool _disposed; + private int _sheetsPrinted; + + public Print() { + PrintDocument.BeginPrint += BeginPrint; + PrintDocument.EndPrint += EndPrint; + PrintDocument.QueryPageSettings += QueryPageSettings; + PrintDocument.PrintPage += PrintSheet; + } + + public SheetViewModel SheetViewModel { get; } = new(); + + public PrintDocument PrintDocument { get; } = new(); + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } /// - /// The Print class is the top-level class for initiating print jobs with winprint. It is the - /// primary class apps like winprint.exe, out-winprint, and winprintgui use to configure, start, - /// and manage print jobs. + /// Invoked after each sheet has been printed. /// - public class Print : IDisposable { - // The WinPrint "document" - private readonly SheetViewModel svm = new SheetViewModel(); - public SheetViewModel SheetViewModel => svm; - // The Windows printer document - private readonly PrintDocument printDoc = new PrintDocument(); - public PrintDocument PrintDocument => printDoc; - private int curSheet = 0; - private int sheetsPrinted = 0; - - public Print() { - printDoc.BeginPrint += new PrintEventHandler(BeginPrint); - printDoc.EndPrint += new PrintEventHandler(EndPrint); - printDoc.QueryPageSettings += new QueryPageSettingsEventHandler(QueryPageSettings); - printDoc.PrintPage += new PrintPageEventHandler(PrintSheet); - } + public event EventHandler? PrintingSheet; + + protected void OnPrintingSheet(int sheetNum) { + PrintingSheet?.Invoke(this, sheetNum); + } + + /// + /// Sets the printer to be used for printing. + /// + /// + public void SetPrinter(string printerName) { + Log.Debug(LogService.GetTraceMsg("{p}"), printerName); + if (!string.IsNullOrEmpty(printerName)) { + try { + PrintDocument.PrinterSettings.PrinterName = printerName; + ServiceLocator.Current.TelemetryService.TrackEvent("Set Printer", + new Dictionary { ["printerName"] = printerName }); + } + catch (NullReferenceException) { + // On Linux if an invalid printer name is passed in we get a + // NullReferenceException. + throw new InvalidPrinterException(PrintDocument.PrinterSettings); + } - /// - /// Invoked after each sheet has been printed. - /// - public event EventHandler PrintingSheet; - protected void OnPrintingSheet(int sheetNum) { - PrintingSheet?.Invoke(this, sheetNum); + if (!PrintDocument.PrinterSettings.IsValid) { + throw new InvalidPrinterException(PrintDocument.PrinterSettings); + } } + } - /// - /// Sets the printer to be used for printing. - /// - /// - public void SetPrinter(string printerName) { - Log.Debug(LogService.GetTraceMsg("{p}"), printerName); - if (!string.IsNullOrEmpty(printerName)) { - try { - PrintDocument.PrinterSettings.PrinterName = printerName; - ServiceLocator.Current.TelemetryService.TrackEvent("Set Printer", - properties: new Dictionary { ["printerName"] = printerName }); - } - catch (NullReferenceException) { - // On Linux if an invalid printer name is passed in we get a - // NullReferenceException. - throw new InvalidPrinterException(PrintDocument.PrinterSettings); - } - if (!PrintDocument.PrinterSettings.IsValid) { - throw new InvalidPrinterException(PrintDocument.PrinterSettings); + /// + /// Sets the paper size to be used for printing. + /// + /// + public void SetPaperSize(string paperSizeName) { + if (!string.IsNullOrEmpty(paperSizeName)) { + var found = false; + foreach (PaperSize size in PrintDocument.PrinterSettings.PaperSizes) { + if (size.PaperName.Equals(paperSizeName, StringComparison.InvariantCultureIgnoreCase)) { + PrintDocument.DefaultPageSettings.PaperSize = size; + found = true; } } - } - /// - /// Sets the paper size to be used for printing. - /// - /// - public void SetPaperSize(string paperSizeName) { - if (!string.IsNullOrEmpty(paperSizeName)) { - var found = false; + if (!found) { + var sb = new StringBuilder(); + sb.Append( + $"'{paperSizeName}' is not a valid paper size for the '{PrintDocument.PrinterSettings.PrinterName}' printer."); + sb.Append(Environment.NewLine); + sb.Append($"'{PrintDocument.PrinterSettings.PrinterName}' supports these printer sizes:"); + sb.Append(Environment.NewLine); foreach (PaperSize size in PrintDocument.PrinterSettings.PaperSizes) { - if (size.PaperName.Equals(paperSizeName, StringComparison.InvariantCultureIgnoreCase)) { - PrintDocument.DefaultPageSettings.PaperSize = size; - found = true; - } - } - if (!found) { - var sb = new StringBuilder(); - sb.Append($"'{paperSizeName}' is not a valid paper size for the '{PrintDocument.PrinterSettings.PrinterName}' printer."); - sb.Append(Environment.NewLine); - sb.Append($"'{PrintDocument.PrinterSettings.PrinterName}' supports these printer sizes:"); + sb.Append($" {size.PaperName}"); sb.Append(Environment.NewLine); - foreach (PaperSize size in PrintDocument.PrinterSettings.PaperSizes) { - sb.Append($" {size.PaperName}"); - sb.Append(Environment.NewLine); - } - throw new Exception(sb.ToString()); - } - else { - ServiceLocator.Current.TelemetryService.TrackEvent("Set Paper Size", - properties: new Dictionary { ["paperSizeName"] = paperSizeName }); } + + throw new Exception(sb.ToString()); } - } - /// - /// Prints the current job wihtout actually printing, returning the number of sheets that would have been printed. - /// - /// - /// - /// The number of sheets that would have been printed. - /// - public async Task CountSheets(int fromSheet = 1, int toSheet = 0) { - // BUGBUG: Ignores from/to - SheetViewModel.SetPrinterPageSettings(PrintDocument.DefaultPageSettings); - await SheetViewModel.ReflowAsync().ConfigureAwait(false); - - ServiceLocator.Current.TelemetryService.TrackEvent("Count Sheets", - properties: new Dictionary { - ["type"] = SheetViewModel.ContentEngine.GetType().Name, - ["contentType"] = SheetViewModel.ContentType, - ["language"] = SheetViewModel.Language, - ["printer"] = PrintDocument.PrinterSettings.PrinterName, - ["fromSheet"] = fromSheet.ToString(), - ["toSheet"] = toSheet.ToString(), - }, - metrics: new Dictionary { ["sheetsPrinted"] = SheetViewModel.NumSheets }); ; - return SheetViewModel.NumSheets; + ServiceLocator.Current.TelemetryService.TrackEvent("Set Paper Size", + new Dictionary { ["paperSizeName"] = paperSizeName }); } + } - /// - /// Executes the print job. - /// - /// The number of sheets that were printed. - public async Task DoPrint() { - PrintDocument.DocumentName = SheetViewModel.File; - SheetViewModel.SetPrinterPageSettings(PrintDocument.DefaultPageSettings); - await SheetViewModel.ReflowAsync().ConfigureAwait(false); - - PrintDocument.PrinterSettings.FromPage = PrintDocument.PrinterSettings.FromPage == 0 ? 1 : PrintDocument.PrinterSettings.FromPage; - PrintDocument.PrinterSettings.ToPage = PrintDocument.PrinterSettings.ToPage == 0 ? SheetViewModel.NumSheets : PrintDocument.PrinterSettings.ToPage; - - curSheet = PrintDocument.PrinterSettings.FromPage; - PrintDocument.Print(); - - ServiceLocator.Current.TelemetryService.TrackEvent("Print Complete", - properties: new Dictionary { - ["type"] = SheetViewModel.ContentEngine.GetType().Name, - ["contentType"] = SheetViewModel.ContentType, - ["language"] = SheetViewModel.Language, - ["printer"] = PrintDocument.PrinterSettings.PrinterName, - ["fromSheet"] = PrintDocument.PrinterSettings.FromPage.ToString(), - ["toSheet"] = PrintDocument.PrinterSettings.ToPage.ToString(), - }, - metrics: new Dictionary { ["sheetsPrinted"] = sheetsPrinted }); - - return sheetsPrinted; - } + /// + /// Prints the current job without actually printing, returning the number of sheets that would have been printed. + /// + /// + /// + /// The number of sheets that would have been printed. + public async Task CountSheets(int fromSheet = 1, int toSheet = 0) { + // BUGBUG: Ignores from/to + SheetViewModel.SetPrinterPageSettings(PrintDocument.DefaultPageSettings); + await SheetViewModel.ReflowAsync().ConfigureAwait(false); + + ServiceLocator.Current.TelemetryService.TrackEvent("Count Sheets", + new Dictionary { + ["type"] = SheetViewModel.ContentEngine.GetType().Name, + ["contentType"] = SheetViewModel.ContentType, + ["language"] = SheetViewModel.Language, + ["printer"] = PrintDocument.PrinterSettings.PrinterName, + ["fromSheet"] = fromSheet.ToString(), + ["toSheet"] = toSheet.ToString() + }, + new Dictionary { ["sheetsPrinted"] = SheetViewModel.NumSheets }); + ; + return SheetViewModel.NumSheets; + } - #region System.Drawing.Printing Event Handlers - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] - // Occurs when the Print() method is called and before the first page of the document prints. - private void BeginPrint(object sender, PrintEventArgs ev) { - LogService.TraceMessage($"Print.BeginPrint"); - sheetsPrinted = 0; + /// + /// Executes the print job. + /// + /// The number of sheets that were printed. + public async Task DoPrint() { + PrintDocument.DocumentName = SheetViewModel.File; + SheetViewModel.SetPrinterPageSettings(PrintDocument.DefaultPageSettings); + await SheetViewModel.ReflowAsync().ConfigureAwait(false); + + PrintDocument.PrinterSettings.FromPage = + PrintDocument.PrinterSettings.FromPage == 0 ? 1 : PrintDocument.PrinterSettings.FromPage; + PrintDocument.PrinterSettings.ToPage = PrintDocument.PrinterSettings.ToPage == 0 + ? SheetViewModel.NumSheets + : PrintDocument.PrinterSettings.ToPage; + + _curSheet = PrintDocument.PrinterSettings.FromPage; + PrintDocument.Print(); + + ServiceLocator.Current.TelemetryService.TrackEvent("Print Complete", + new Dictionary { + ["type"] = SheetViewModel.ContentEngine.GetType().Name, + ["contentType"] = SheetViewModel.ContentType, + ["language"] = SheetViewModel.Language, + ["printer"] = PrintDocument.PrinterSettings.PrinterName, + ["fromSheet"] = PrintDocument.PrinterSettings.FromPage.ToString(), + ["toSheet"] = PrintDocument.PrinterSettings.ToPage.ToString() + }, + new Dictionary { ["sheetsPrinted"] = _sheetsPrinted }); + + return _sheetsPrinted; + } + + protected virtual void Dispose(bool disposing) { + if (_disposed) { + return; } - // Occurs when the last page of the document has printed. - private void EndPrint(object sender, PrintEventArgs ev) { - LogService.TraceMessage($"Print.EndPrint"); - // Reset so PrintPreviewDialog Print button works - curSheet = PrintDocument.PrinterSettings.FromPage; + if (disposing) { + PrintDocument.Dispose(); } - // Occurs immediately before each PrintPage event. - private void QueryPageSettings(object sender, QueryPageSettingsEventArgs e) { + _disposed = true; + } - LogService.TraceMessage($"Print.QueryPageSettings"); - } + #region System.Drawing.Printing Event Handlers - // The PrintPage event is raised for each page to be printed. - private void PrintSheet(object sender, PrintPageEventArgs ev) { - LogService.TraceMessage($"Sheet {curSheet}"); - OnPrintingSheet(curSheet); + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] + // Occurs when the Print() method is called and before the first page of the document prints. + private void BeginPrint(object sender, PrintEventArgs ev) { + LogService.TraceMessage("Print.BeginPrint"); + _sheetsPrinted = 0; + } - if (ev.PageSettings.PrinterSettings.PrintRange == PrintRange.SomePages) { - while (curSheet < PrintDocument.PrinterSettings.FromPage) { - // Blow through pages up to fromPage - curSheet++; - } - } - if (curSheet <= PrintDocument.PrinterSettings.ToPage) { - // BUGBUG: LINUX - On pages > 1 in landscape mode, landscape mode is lost - SheetViewModel.PrintSheet(ev.Graphics, curSheet); - sheetsPrinted++; - } - curSheet++; - ev.HasMorePages = curSheet <= PrintDocument.PrinterSettings.ToPage; - } - #endregion + // Occurs when the last page of the document has printed. + private void EndPrint(object sender, PrintEventArgs ev) { + LogService.TraceMessage("Print.EndPrint"); + // Reset so PrintPreviewDialog Print button works + _curSheet = PrintDocument.PrinterSettings.FromPage; + } - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } + // Occurs immediately before each PrintPage event. + private void QueryPageSettings(object sender, QueryPageSettingsEventArgs e) { + LogService.TraceMessage("Print.QueryPageSettings"); + } - // Protected implementation of Dispose pattern. - // Flag: Has Dispose already been called? - private bool disposed = false; + // The PrintPage event is raised for each page to be printed. + private void PrintSheet(object sender, PrintPageEventArgs ev) { + LogService.TraceMessage($"Sheet {_curSheet}"); + OnPrintingSheet(_curSheet); - protected virtual void Dispose(bool disposing) { - if (disposed) { - return; + if (ev.PageSettings.PrinterSettings.PrintRange == PrintRange.SomePages) { + while (_curSheet < PrintDocument.PrinterSettings.FromPage) { + // Blow through pages up to fromPage + _curSheet++; } + } - if (disposing) { - if (printDoc != null) { - printDoc.Dispose(); - } - } - disposed = true; + if (_curSheet <= PrintDocument.PrinterSettings.ToPage) { + // BUGBUG: LINUX - On pages > 1 in landscape mode, landscape mode is lost + SheetViewModel.PrintSheet(ev.Graphics, _curSheet); + _sheetsPrinted++; } + + _curSheet++; + ev.HasMorePages = _curSheet <= PrintDocument.PrinterSettings.ToPage; } + + #endregion + } diff --git a/src/WinPrint.Core/Services/FileTypeMappingService.cs b/src/WinPrint.Core/Services/FileTypeMappingService.cs index 0ae4bcd..309b391 100644 --- a/src/WinPrint.Core/Services/FileTypeMappingService.cs +++ b/src/WinPrint.Core/Services/FileTypeMappingService.cs @@ -3,53 +3,66 @@ using System.Linq; using System.Text.Json; using WinPrint.Core.Models; +using WinPrint.Core.Properties; -namespace WinPrint.Core.Services { - public class FileTypeMappingService { - // Factory - creates - static public FileTypeMapping Create() { - // - LogService.TraceMessage("FileAssociationsService.Create()"); - return ServiceLocator.Current.FileTypeMappingService.Load(); +namespace WinPrint.Core.Services; + +/// +/// Provides a mapping from a file extension to a CTE. +/// THe resource file `FileTypeMapping.json` is generated by `./tools/pygments_lexers.py` +/// TOOD: Convert to use `pygmentize `pygmentize -L lexers --json` (see +/// https://github.com/pygments/pygments/issues/1437) +/// +public class FileTypeMappingService { + // Factory - creates + public static FileTypeMapping Create() { + // + LogService.TraceMessage("FileAssociationsService.Create()"); + return ServiceLocator.Current.FileTypeMappingService.Load(); + } + + /// + /// Loads file associations (extensions to language mapping) from resources. Should be + /// called after settings file has been loaded because it merges any associations + /// defined there in. + /// + /// + public FileTypeMapping? Load() { + FileTypeMapping? associations = null; + var jsonOptions = new JsonSerializerOptions { + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + //PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + associations = JsonSerializer.Deserialize(Resources.languages, jsonOptions); + + if (associations is null) { + return null; } - /// - /// Loads file assocations (extentions to language mapping) from resources. Should be - /// called after settings file has been loaded because it merges any associations - /// defined there in. - /// - /// - public FileTypeMapping Load() { - FileTypeMapping associations = null; - var jsonOptions = new JsonSerializerOptions { - WriteIndented = true, - AllowTrailingCommas = true, - PropertyNameCaseInsensitive = true, - //PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReadCommentHandling = JsonCommentHandling.Skip - }; - - associations = JsonSerializer.Deserialize(Properties.Resources.languages, jsonOptions); - - // TODO: Consider callilng into lang-map and/or Pygments to update extensions at runtime? - // https://github.com/jonschlinkert/lang-map - - // Merge in any assocations set in settings file - Debug.Assert(ModelLocator.Current.Settings.FileTypeMapping != null); - Debug.Assert(ModelLocator.Current.Settings.FileTypeMapping.ContentTypes != null); + // TODO: Consider calling into lang-map and/or Pygments to update extensions at runtime? + // https://github.com/jonschlinkert/lang-map + + // Merge in any associations set in settings file + Debug.Assert(ModelLocator.Current.Settings.FileTypeMapping != null); + Debug.Assert(ModelLocator.Current.Settings.FileTypeMapping.ContentTypes != null); + if (ModelLocator.Current.Settings.FileTypeMapping.FilesAssociations != null) { foreach (var fa in ModelLocator.Current.Settings.FileTypeMapping.FilesAssociations) { - associations.FilesAssociations[fa.Key] = fa.Value; + associations.FilesAssociations![fa.Key] = fa.Value; } + } - // Merge in any language defintions set in settings file - var langs = new List(associations.ContentTypes); - var langsSettings = new List(ModelLocator.Current.Settings.FileTypeMapping.ContentTypes); - // TODO: overide Equals and GetHashCode for Language - var result = langsSettings.Union(langs).ToList(); + // Merge in any language defintions set in settings file + var langs = new List(associations.ContentTypes!); + var langsSettings = new List(ModelLocator.Current.Settings.FileTypeMapping.ContentTypes); + // TODO: override Equals and GetHashCode for Language + var result = langsSettings.Union(langs).ToList(); - associations.ContentTypes = result; + associations.ContentTypes = result; - return associations; - } + return associations; } } diff --git a/src/WinPrint.Core/Services/LogService.cs b/src/WinPrint.Core/Services/LogService.cs index 72b05c9..626b333 100644 --- a/src/WinPrint.Core/Services/LogService.cs +++ b/src/WinPrint.Core/Services/LogService.cs @@ -2,112 +2,116 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Serilog; using Serilog.Core; using Serilog.Events; using WinPrint.Core.Helpers; -namespace WinPrint.Core.Services { +namespace WinPrint.Core.Services; + +/// +/// Configures Serilog for logging to the console and logfiles. Provides simple helper +/// functions for tracing. +/// +public class LogService { + public string? LogPath { get; set; } + public LoggingLevelSwitch MasterLevelSwitch { get; set; } = new(); + public LoggingLevelSwitch FileLevelSwitch { get; set; } = new(); + public LoggingLevelSwitch ConsoleLevelSwitch { get; set; } = new(); + public LoggingLevelSwitch DebugLevelSwitch { get; set; } = new(); + /// - /// Configures Serilog for logging to the console and logfiles. Provides simple helper - /// functions for tracing. + /// Starts Serilog-based logging. /// - public class LogService { - public string LogPath { get; set; } - public LoggingLevelSwitch MasterLevelSwitch { get; set; } = new LoggingLevelSwitch(); - public LoggingLevelSwitch FileLevelSwitch { get; set; } = new LoggingLevelSwitch(); - public LoggingLevelSwitch ConsoleLevelSwitch { get; set; } = new LoggingLevelSwitch(); - public LoggingLevelSwitch DebugLevelSwitch { get; set; } = new LoggingLevelSwitch(); + /// The name used to identify the log entries; emitted in the first log entry of each run. + /// Provides the ILogEventSink for the console. + /// If true, the console log will emit LogEventLevel.Debug entries + /// If true, the console log will emit LogEventLevel.Information entries. + public void Start(string appName, ILogEventSink? consoleSink = null, bool debug = false, bool verbose = false) { + MasterLevelSwitch.MinimumLevel = LogEventLevel.Verbose; + DebugLevelSwitch.MinimumLevel = LogEventLevel.Debug; - /// - /// Starts Serilog-based logging. - /// - /// The name used to identify the log entries; emitted in the first log entry of each run. - /// Provides the ILogEventSink for the console. - /// If true, the console log will emit LogEventLevel.Debug entries - /// If true, the console log will emit LogEventLevel.Information entries. - public void Start(string appName, ILogEventSink consoleSink = null, bool debug = false, bool verbose = false) { - MasterLevelSwitch.MinimumLevel = LogEventLevel.Verbose; - DebugLevelSwitch.MinimumLevel = LogEventLevel.Debug; - - ConsoleLevelSwitch.MinimumLevel = (verbose ? LogEventLevel.Information : LogEventLevel.Warning); - ConsoleLevelSwitch.MinimumLevel = (debug ? LogEventLevel.Debug : ConsoleLevelSwitch.MinimumLevel); + ConsoleLevelSwitch.MinimumLevel = verbose ? LogEventLevel.Information : LogEventLevel.Warning; + ConsoleLevelSwitch.MinimumLevel = debug ? LogEventLevel.Debug : ConsoleLevelSwitch.MinimumLevel; #if DEBUG - FileLevelSwitch.MinimumLevel = LogEventLevel.Debug; + FileLevelSwitch.MinimumLevel = LogEventLevel.Debug; #else // TODO: Keep this at Debug until after Beta, then change it to Information FileLevelSwitch.MinimumLevel = LogEventLevel.Debug; #endif - var productVersion = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LogService)).Location).FileVersion; - LogPath = $"{SettingsService.SettingsPath}{Path.DirectorySeparatorChar}logs{Path.DirectorySeparatorChar}{appName}.log".Replace(@"file:\", ""); + var productVersion = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LogService))!.Location) + .FileVersion; + LogPath = + $"{SettingsService.SettingsPath}{Path.DirectorySeparatorChar}logs{Path.DirectorySeparatorChar}{appName}.log" + .Replace(@"file:\", ""); - if (consoleSink == null) { - ConsoleLevelSwitch.MinimumLevel = LogEventLevel.Fatal; - } + if (consoleSink == null) { + ConsoleLevelSwitch.MinimumLevel = LogEventLevel.Fatal; + } - // Setup logging - if (consoleSink == null) { - // GUI - Log.Logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(MasterLevelSwitch) - .WriteTo.Debug(levelSwitch: DebugLevelSwitch) - .WriteTo.File(LogPath, shared: true, levelSwitch: FileLevelSwitch) - .CreateLogger(); - } - else { - // Console / Powershell - Log.Logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(MasterLevelSwitch) - .WriteTo.Sink(consoleSink, levelSwitch: ConsoleLevelSwitch) - .WriteTo.Debug(levelSwitch: DebugLevelSwitch) - .WriteTo.File(LogPath, shared: true, levelSwitch: FileLevelSwitch) - .CreateLogger(); - } + // Setup logging + if (consoleSink == null) { + // GUI + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(MasterLevelSwitch) + .WriteTo.Debug(levelSwitch: DebugLevelSwitch) + .WriteTo.File(LogPath, shared: true, levelSwitch: FileLevelSwitch) + .CreateLogger(); + } + else { + // Console / Powershell + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(MasterLevelSwitch) + .WriteTo.Sink(consoleSink, levelSwitch: ConsoleLevelSwitch) + .WriteTo.Debug(levelSwitch: DebugLevelSwitch) + .WriteTo.File(LogPath, shared: true, levelSwitch: FileLevelSwitch) + .CreateLogger(); + } - Log.Debug("--------- {app} {v} ---------", appName, productVersion); - if (ServiceLocator.Current.TelemetryService.TelemetryEnabled) { + Log.Debug("--------- {app} {v} ---------", appName, productVersion); + if (ServiceLocator.Current.TelemetryService.TelemetryEnabled) { #if CI_BUILD var msg = "CI_BUILD so no telemetry will be tracked."; #else - var msg = string.IsNullOrEmpty(TelemetryService.Key) ? "However, telemetry key is missing so no telemetry will be tracked." : ""; + var msg = string.IsNullOrEmpty(TelemetryService.Key) + ? "However, telemetry key is missing so no telemetry will be tracked." + : ""; #endif - Log.Debug($"Telemetry is enabled. {msg}"); - } - Log.Debug("Logging to {path}", ServiceLocator.Current.LogService.LogPath); - Log.Debug("OS Environment: {os}, architecture: {arch}, .NET version: {dotnet}", - Environment.OSVersion, Environment.Is64BitProcess ? "x64" : "x86", Environment.Version); - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Log.Debug("libgdiplus version: {v}", Diagnostics.GetLibgdiplusVersion()); - } + Log.Debug($"Telemetry is enabled. {msg}"); } - public LogService() { + Log.Debug("Logging to {path}", ServiceLocator.Current.LogService.LogPath); + Log.Debug("OS Environment: {os}, architecture: {arch}, .NET version: {dotnet}", + Environment.OSVersion, Environment.Is64BitProcess ? "x64" : "x86", Environment.Version); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + Log.Debug("libgdiplus version: {v}", Diagnostics.GetLibgdiplusVersion()); } + } - public static void TraceMessage(string msg = "", - [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", - [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) { - Log.Debug($"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber} {memberName}: {{msg}}", msg); - } + public static void TraceMessage(string msg = "", + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0) { + Log.Debug($"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber} {memberName}: {{msg}}", msg); + } - /// - /// Generates a trace message. - /// e.g. `Log.Debug(LogService.GetTraceMsg());` - /// `Log.Debug(LogService.GetTraceMsg("{n} PageUnit: {pu}"), sheetNum, graphics.PageUnit);` - /// - /// Optional string that will be appended. This can be a Serilog messageTemplate. - /// - /// - /// - /// - public static string GetTraceMsg(string msg = "", - [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", - [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) { - return $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber} {memberName}: {msg}"; - } + /// + /// Generates a trace message. + /// e.g. `Log.Debug(LogService.GetTraceMsg());` + /// `Log.Debug(LogService.GetTraceMsg("{n} PageUnit: {pu}"), sheetNum, graphics.PageUnit);` + /// + /// Optional string that will be appended. This can be a Serilog messageTemplate. + /// + /// + /// + /// + public static string GetTraceMsg(string msg = "", + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0) { + return $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber} {memberName}: {msg}"; } } diff --git a/src/WinPrint.Core/Services/PygmentsConverterService.cs b/src/WinPrint.Core/Services/PygmentsConverterService.cs index 5d9bbef..52d53b2 100644 --- a/src/WinPrint.Core/Services/PygmentsConverterService.cs +++ b/src/WinPrint.Core/Services/PygmentsConverterService.cs @@ -1,117 +1,384 @@ using System; -using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Reflection; using System.Text; using System.Threading.Tasks; -using LiteHtmlSharp; -using Microsoft.VisualBasic; using Serilog; -namespace WinPrint.Core.Services { +namespace WinPrint.Core.Services; + +/// +/// Converts (syntax highlights) a document using the Pygments +/// Syntax highlighter (https://pygments.org/) +/// +public class PygmentsConverterService { + private static bool _pygmentsInstalled; + private static string? _scriptsPath = string.Empty; + private TaskCompletionSource? _eventHandled; + + private Process? _proc; + + internal PygmentsConverterService Create() { + return new PygmentsConverterService(); + } + + /// - /// Converts (syntax highlights) a document using the Pygments - /// Syntax highlighter (https://pygments.org/) + /// Check that Python 3.x is installed and that pygmentize.exe works /// - public class PygmentsConverterService { - internal PygmentsConverterService Create() { - return new PygmentsConverterService(); + /// true if pygmentize.exe is working, false otherwise + message + public (bool installed, string message) CheckInstall() { + if (_pygmentsInstalled) { + return (true, "Pygments is is functional."); } - public PygmentsConverterService() { + var message = String.Empty; + + var proc = new Process(); + + proc.StartInfo.RedirectStandardInput = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.StartInfo.RedirectStandardError = true; + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.CreateNoWindow = true; + proc.EnableRaisingEvents = false; + + // Test for Python 3.x + var python = false; + try { + Log.Information("Source code formatting: Verifying Python is installed"); + proc.StartInfo.FileName = "python.exe"; + proc.StartInfo.Arguments = "-V"; + Log.Debug("Pygments: Starting process {proc} {args}", proc.StartInfo.FileName, proc.StartInfo.Arguments); + proc.Start(); + + if (proc.WaitForExit(5000)) { + var output = proc.StandardOutput.ReadLine(); + if (output != null && output.StartsWith("Python ")) { + python = true; + Log.Debug("Pygments: Python is installed: {output}", output); + } + else { + message = + $"Python does not appear to be installed.\n{proc.StartInfo.FileName} failed to start: {output}"; + } + } + else { + message = $"Could not launch {proc.StartInfo.FileName}; timeout."; + } + } + catch (Exception ex) { + // Console and WinForms are different + message = $"{proc.StartInfo.FileName} {proc.StartInfo.Arguments} failed:\n{ex.Message}"; + } + + if (!python) { + message = + $"Python 3.x must be installed (and on the PATH) for source code formatting to work.\n\nType `winget install python` at a command prompt to install Python.\n\n{message}"; + Log.Debug("Pygments: {message}", message); + return (_pygmentsInstalled, message); + } + + // Get python scripts folder + // python -c 'import os,sysconfig;print(sysconfig.get_path("scripts",f"{os.name}_user"))' + // from https://stackoverflow.com/questions/62162970/programmatically-determine-pip-user-install-location-scripts-directory/62167797#62167797 + try { + Log.Information("Source code formatting: Verifying Python scripts location"); + proc.StartInfo.FileName = "python.exe"; + proc.StartInfo.Arguments = + "-c \"import os,sysconfig;print(sysconfig.get_path('scripts',f'{os.name}_user'))\""; + Log.Debug("Pygments: Starting process {proc} {args}", proc.StartInfo.FileName, proc.StartInfo.Arguments); + proc.Start(); + + if (proc.WaitForExit(5000)) { + _scriptsPath = proc.StandardOutput.ReadLine(); + if (!string.IsNullOrEmpty(_scriptsPath) && _scriptsPath.ToLowerInvariant().EndsWith("scripts")) { + Log.Debug("Pygments: Python scripts folder: {scriptsPath}", _scriptsPath); + message = _scriptsPath; + } + else { + _scriptsPath = proc.StandardError.ReadToEnd(); + + Log.Debug("Pygments: Python error: {scriptsPath}", _scriptsPath); + message = + $"Could not find the Python scripts folder.\n{proc.StartInfo.FileName} failed to start: {_scriptsPath}"; + } + } + else { + message = $"Could not launch {proc.StartInfo.FileName}; timeout."; + } + } + catch (Exception ex) { + // Console and WinForms are different + message = $"{proc.StartInfo.FileName} {proc.StartInfo.Arguments} failed:\n{ex.Message}"; + } + if (string.IsNullOrEmpty(_scriptsPath)) { + message = $"Could not find Pygments. Source code formatting will not work.\n\n{message}"; + Log.Debug("Pygments: {message}", message); + return (_pygmentsInstalled, message); } - private Process _proc; - private TaskCompletionSource _eventHandled; + // Test pygmentize.exe - Which is installed in the app's Program File dir + try { + Log.Information("Source code formatting: Verifying Pygments is installed"); + proc.StartInfo.FileName = @$"{_scriptsPath}\pygmentize.exe"; + proc.StartInfo.Arguments = "-V"; + Log.Debug("Pygments: Starting process {proc} {args}", proc.StartInfo.FileName, proc.StartInfo.Arguments); + proc.Start(); - public async Task ConvertAsync(string document, string style, string language) { - if (_proc != null) { - throw new InvalidOperationException("ConvertAsync already in progress."); + if (proc.WaitForExit(5000)) { + var output = proc.StandardOutput.ReadLine(); + if (output != null && output.StartsWith("Pygments version ")) { + _pygmentsInstalled = true; + message = $"Pygments is functional: {output}"; + Log.Debug("Pygments: {output}", message); + return (_pygmentsInstalled, message); + } + + message = + $"Pygments is not installed. {proc.StartInfo.FileName} failed to start: {(output == null ? "no output" : output)}"; + // Try to install it + (_pygmentsInstalled, message) = InstallPygments(); } - if (_eventHandled != null) { - throw new InvalidOperationException("ConvertAsync already in progress."); + else { + message = $"Could not launch {proc.StartInfo.FileName}; timeout."; } + } + catch (Exception ex) { + // Console and WinForms are different + message = $"{proc.StartInfo.FileName} {proc.StartInfo.Arguments} failed:\n{ex.Message}"; + Log.Debug("Pygments: {message}", message); + // Try to install it + (_pygmentsInstalled, message) = InstallPygments(); + } - string file = Path.GetTempFileName(); - _proc = new Process(); - _proc.StartInfo.FileName = @$"{Path.GetDirectoryName(Assembly.GetAssembly(typeof(PygmentsConverterService)).Location)}\pygmentize.exe"; - _proc.StartInfo.Arguments = $"-P outencoding=utf-8 -f 16m -O style=\"{(string.IsNullOrEmpty(style) ? "default" : style)}\" -l \"{language}\" -o \"{file}.an\" \"{file}\""; - _proc.StartInfo.RedirectStandardInput = true; - _proc.StartInfo.RedirectStandardOutput = true; - _proc.StartInfo.RedirectStandardError = true; - _proc.StartInfo.UseShellExecute = false; - _proc.StartInfo.CreateNoWindow = true; - _proc.EnableRaisingEvents = true; - _proc.Exited += Proc_Exited; - - _eventHandled = new TaskCompletionSource(); - - try { - Log.Debug("Writing temp file {file}", file); - await File.WriteAllTextAsync(file, document, Encoding.UTF8).ConfigureAwait(true); - Log.Debug("Starting {pyg} {args}", _proc.StartInfo.FileName, _proc.StartInfo.Arguments); - _proc.Start(); - - Log.Debug("Waiting for pygments to exit"); - await Task.WhenAny(_eventHandled.Task, Task.Delay(10000)).ConfigureAwait(true); - - if (_proc.ExitCode != 0) { - var stdErr = _proc.StandardError.ReadToEnd(); - if (stdErr.StartsWith("Usage:")) { - stdErr = "Invalid command line."; - } - document = $"Pygments encountered an error (exit code: {_proc.ExitCode}): {stdErr}"; - Log.Information("{document}", document); - // TODO: This should really throw an exception - throw new InvalidOperationException(document); + if (!_pygmentsInstalled) { + message = $"Pygments error. Source code formatting will not function.\n\n{message}"; + } + + Log.Debug("Pygments: {message}", message); + return (_pygmentsInstalled, message); + } + + ///// + ///// Installs Python via Winget + ///// + ///// true if python.exe is working, false otherwise + message + //private (bool installed, string message) InstallPython() { + // string message = String.Empty; + + // Process proc = new Process(); + + // proc.StartInfo.RedirectStandardInput = true; + // proc.StartInfo.RedirectStandardOutput = true; + // proc.StartInfo.RedirectStandardError = true; + // proc.StartInfo.UseShellExecute = false; + // proc.StartInfo.CreateNoWindow = true; + // proc.EnableRaisingEvents = false; + + // // Install Python via winget + // bool python = false; + // try { + // Log.Information("Source code formatting: Installing Python"); + + // proc.StartInfo.FileName = "winget.exe"; + // proc.StartInfo.Arguments = $"install Python"; + // Log.Debug("Pygments: Attempting to install Python via: {cmd} {args}", proc.StartInfo.FileName, proc.StartInfo.Arguments); + // proc.Start(); + + // // TODO: 30 secs is a long time without status updates + // if (proc.WaitForExit(30000)) { + // string output = proc.StandardOutput.ReadToEnd(); + // if (output != null && + // (output.Contains("Successfully installed"))) { + // python = true; + // Log.Debug("Pygments: Python is installed: {output}", output); + // message = "Python is installed"; + // } + // else { + // output = proc.StandardError.ReadToEnd(); + + // message = $"Error installing Python.\n{proc.StartInfo.FileName} failed to start: {output}"; + // } + // } + // else { + // message = $"Could not launch {proc.StartInfo.FileName}; timeout."; + // } + // } + // catch (System.Exception ex) { // Console and WinForms are different + // message = $"{proc.StartInfo.FileName} {proc.StartInfo.Arguments} failed:\n{ex.Message}"; + // } + + // if (!python) { + // message = $"Pygments: Python must be installed for source code formatting to work.\n\n{message}"; + // Log.Debug("{message}", message); + // return (python, message); + // } + + // return (python, message); + //} + + + /// + /// Installs pygments via pip install + /// + /// true if pygmentize.exe is working, false otherwise + message + private static (bool installed, string message) InstallPygments() { + string message; + + var proc = new Process(); + + proc.StartInfo.RedirectStandardInput = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.StartInfo.RedirectStandardError = true; + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.CreateNoWindow = true; + proc.EnableRaisingEvents = false; + + // Install Pygments via pip install + var pygments = false; + try { + Log.Information("Source code formatting: Installing Pygments"); + + proc.StartInfo.FileName = "pip.exe"; + proc.StartInfo.Arguments = "install Pygments --upgrade"; + Log.Debug("Pygments: Attempting to install Pygments via: {cmd} {args}", proc.StartInfo.FileName, + proc.StartInfo.Arguments); + proc.Start(); + + // TODO: 30 secs is a long time without status updates + if (proc.WaitForExit(30000)) { + var output = proc.StandardOutput.ReadToEnd(); + if ((output.Contains("Successfully installed Pygments") || + output.Contains("Requirement already satisfied"))) { + pygments = true; + Log.Debug("Pygments: Pygments is installed: {output}", output); + message = "Pygments is installed"; } else { - if (!string.IsNullOrEmpty($"{file}.an") && File.Exists($"{file}.an")) { - Log.Debug("Reading {file}", $"{file}.an"); - document = await File.ReadAllTextAsync($"{file}.an", Encoding.UTF8).ConfigureAwait(true); + output = proc.StandardError.ReadToEnd(); - // HACK: Because of this bug: https://github.com/pygments/pygments/issues/1435 - if (document[^1] == '\n') - document = document.Remove(document.Length - 1, 1); - } - else { - // TODO: This should really throw an exception - var stdErr = _proc.StandardError.ReadToEnd(); - document = $"Pygments failed to create converter file: {stdErr}"; - Log.Information("{document}", document); - throw new InvalidOperationException(document); - } + message = $"Error installing Pygments.\n{proc.StartInfo.FileName} failed to start: {output}"; } } - catch (System.ComponentModel.Win32Exception e) { - // TODO: Better error message (output of stderr?) - document = $"Could not format document:\n{ _proc.StartInfo.FileName} { _proc.StartInfo.Arguments} failed:\n{e.Message}"; - Log.Error(e, "{document}", document); - throw new InvalidOperationException(document); + else { + message = $"Could not launch {proc.StartInfo.FileName}; timeout."; } - finally { - // Clean up - if (!string.IsNullOrEmpty(file) && File.Exists(file)) { - File.Delete(file); + } + catch (Exception ex) { + // Console and WinForms are different + message = $"{proc.StartInfo.FileName} {proc.StartInfo.Arguments} failed:\n{ex.Message}"; + } + + if (!pygments) { + message = $"Pygments: Pygments must be installed for source code formatting to work.\n\n{message}"; + Log.Debug("{message}", message); + return (pygments, message); + } + + return (pygments, message); + } + + public async Task ConvertAsync(string document, string style, string language) { + LogService.TraceMessage(); + + if (_proc != null || _eventHandled != null) { + throw new InvalidOperationException("Pygments: ConvertAsync already in progress."); + } + + if (string.IsNullOrEmpty(_scriptsPath)) { + throw new InvalidOperationException("Pygments: Pygments is not configured; script path is not set."); + } + + var file = Path.GetTempFileName(); + _proc = new Process(); + _proc.StartInfo.FileName = @$"{_scriptsPath}\pygmentize.exe"; + _proc.StartInfo.Arguments = + $"-P outencoding=utf-8 -f 16m -O style=\"{(string.IsNullOrEmpty(style) ? "default" : style)}\" -l \"{language}\" -o \"{file}.an\" \"{file}\""; + _proc.StartInfo.RedirectStandardInput = true; + _proc.StartInfo.RedirectStandardOutput = true; + _proc.StartInfo.RedirectStandardError = true; + _proc.StartInfo.UseShellExecute = false; + _proc.StartInfo.CreateNoWindow = true; + _proc.EnableRaisingEvents = true; + _proc.Exited += Proc_Exited; + + _eventHandled = new TaskCompletionSource(); + + try { + Log.Debug("Pygments: Writing temp file {file}", file); + await File.WriteAllTextAsync(file, document, Encoding.UTF8).ConfigureAwait(true); + Log.Debug("Pygments: Starting {pyg} {args}", _proc.StartInfo.FileName, _proc.StartInfo.Arguments); + _proc.Start(); + + Log.Debug("Waiting for pygments to exit"); + await Task.WhenAny(_eventHandled.Task, Task.Delay(10000)).ConfigureAwait(true); + + if (_proc.ExitCode != 0) { + var stdErr = await _proc.StandardError.ReadToEndAsync(); + Log.Debug("Pygments: StandardError: {stdErr}", stdErr); + var stdOut = await _proc.StandardOutput.ReadToEndAsync(); + Log.Debug("Pygments: StandardOutput: {stdOut}", stdOut); + if (stdErr.StartsWith("Usage:")) { + stdErr = "Invalid command line."; } + + document = $"Pygments: pygmentize.exe error (exit code: {_proc.ExitCode}): {stdErr}"; + Log.Debug("{document}", document); + throw new InvalidOperationException(document); + } + else { if (!string.IsNullOrEmpty($"{file}.an") && File.Exists($"{file}.an")) { - File.Delete($"{file}.an"); + Log.Debug("Pygments: Reading {file}", $"{file}.an"); + document = await File.ReadAllTextAsync($"{file}.an", Encoding.UTF8).ConfigureAwait(true); + + // HACK: Because of this bug: https://github.com/pygments/pygments/issues/1435 + if (document[^1] == '\n') { + document = document.Remove(document.Length - 1, 1); + } + } + else { + // TODO: This should really throw an exception + var stdErr = await _proc.StandardError.ReadToEndAsync(); + document = $"Pygments failed to create converter file: {stdErr}"; + Log.Debug("Pygments: {document}", document); + throw new InvalidOperationException(document); } - _proc.Exited -= Proc_Exited; - _proc?.Dispose(); - _proc = null; - _eventHandled = null; + } + } + catch (Win32Exception e) { + // TODO: Better error message (output of stderr?) + document = + $"Could not format document:\n{_proc.StartInfo.FileName} {_proc.StartInfo.Arguments} failed:\n{e.Message}"; + Log.Debug(e, "{document}", document); + throw new InvalidOperationException(document); + } + finally { + // Clean up + if (!string.IsNullOrEmpty(file) && File.Exists(file)) { + File.Delete(file); + } + + if (!string.IsNullOrEmpty($"{file}.an") && File.Exists($"{file}.an")) { + File.Delete($"{file}.an"); } - return document; + _proc.Exited -= Proc_Exited; + _proc?.Dispose(); + _proc = null; + _eventHandled = null; } - private void Proc_Exited(object sender, EventArgs e) { - Log.Debug("pygmatize exited: Time: {exitTime}, ExitCode: {exitCode}, ElapsedTime: {elapsedTime}ms", _proc.ExitTime, _proc.ExitCode, Math.Round((_proc.ExitTime - _proc.StartTime).TotalMilliseconds)); + return document; + } + + private void Proc_Exited(object? sender, EventArgs e) { + Log.Debug("Pygments: pygmatize exited: Time: {exitTime}, ExitCode: {exitCode}, ElapsedTime: {elapsedTime}ms", + _proc!.ExitTime, _proc.ExitCode, Math.Round((_proc.ExitTime - _proc.StartTime).TotalMilliseconds)); + if (_eventHandled != null) { _eventHandled.TrySetResult(true); } } diff --git a/src/WinPrint.Core/Services/ServiceLocator.cs b/src/WinPrint.Core/Services/ServiceLocator.cs index 223fc48..b36c0d4 100644 --- a/src/WinPrint.Core/Services/ServiceLocator.cs +++ b/src/WinPrint.Core/Services/ServiceLocator.cs @@ -1,46 +1,42 @@ using GalaSoft.MvvmLight.Ioc; -namespace WinPrint { - //public class Logger { - // static WinPrint.Core.Services.LogService Log = ServiceLocator.Current.LogService; - //} -} +namespace WinPrint.Core.Services; + +public class ServiceLocator { + private static ServiceLocator? _current; + + private ServiceLocator() { + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(true); + SimpleIoc.Default.Register(true); + SimpleIoc.Default.Register(); + SimpleIoc.Default.Register(); + } + + public static ServiceLocator Current => _current ??= new ServiceLocator(); + + public LogService LogService => SimpleIoc.Default.GetInstance(); + public TelemetryService TelemetryService => SimpleIoc.Default.GetInstance(); + public SettingsService SettingsService => SimpleIoc.Default.GetInstance(); + public FileTypeMappingService FileTypeMappingService => SimpleIoc.Default.GetInstance(); + public UpdateService UpdateService => SimpleIoc.Default.GetInstance(); + + public PygmentsConverterService PygmentsConverterService => + SimpleIoc.Default.GetInstance(); + + public void Register() + where VM : class { + SimpleIoc.Default.Register(); + } -namespace WinPrint.Core.Services { - public class ServiceLocator { - private static ServiceLocator _current; - - public static ServiceLocator Current => _current ?? (_current = new ServiceLocator()); - - private ServiceLocator() { - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(true); - SimpleIoc.Default.Register(true); - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - } - - public LogService LogService => SimpleIoc.Default.GetInstance(); - public TelemetryService TelemetryService => SimpleIoc.Default.GetInstance(); - public SettingsService SettingsService => SimpleIoc.Default.GetInstance(); - public FileTypeMappingService FileTypeMappingService => SimpleIoc.Default.GetInstance(); - public UpdateService UpdateService => SimpleIoc.Default.GetInstance(); - public PygmentsConverterService PygmentsConverterService => SimpleIoc.Default.GetInstance(); - - public void Register() - where VM : class { - SimpleIoc.Default.Register(); - } - - public static void Reset() { - _current = null; - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Unregister(); - } + public static void Reset() { + _current = null; + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Unregister(); } } diff --git a/src/WinPrint.Core/Services/SettingsService.cs b/src/WinPrint.Core/Services/SettingsService.cs index 945f0a8..0dfc6cd 100644 --- a/src/WinPrint.Core/Services/SettingsService.cs +++ b/src/WinPrint.Core/Services/SettingsService.cs @@ -10,214 +10,218 @@ using WinPrint.Core.Helpers; using WinPrint.Core.Models; -namespace WinPrint.Core.Services { - // TODO: Implement settings validation with appropriate alerting - public class SettingsService { - private JsonSerializerOptions jsonOptions; - private string settingsFileName = "WinPrint.config.json"; - public string SettingsFileName { get => settingsFileName; set => settingsFileName = value; } - - private FileWatcher watcher; - - public SettingsService() { - SettingsFileName = $"{SettingsPath}{Path.DirectorySeparatorChar}{SettingsFileName}"; - Log.Debug("Settings file path: {settingsFileName}", SettingsFileName); - - jsonOptions = new JsonSerializerOptions { - WriteIndented = true, - AllowTrailingCommas = true, - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReadCommentHandling = JsonCommentHandling.Skip - }; - jsonOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); - } - - /// - /// Reads settings from settings file (WinPrint.config.json). - /// If file does not exist, it is created. - /// - /// - public Settings ReadSettings() { - Settings settings = null; - string jsonString; - FileStream fs = null; - - try { - //Logger.Instance.Log4.Info($"Loading user-defined commands from {userCommandsFile}."); - fs = new FileStream(SettingsFileName, FileMode.Open, FileAccess.Read); - jsonString = File.ReadAllText(SettingsFileName); - Log.Debug("ReadSettings: Deserializing from {settingsFileName}", SettingsFileName); - settings = JsonSerializer.Deserialize(jsonString, jsonOptions); - - ServiceLocator.Current.TelemetryService.TrackEvent("Read Settings", properties: settings.GetTelemetryDictionary()); - } - catch (FileNotFoundException) { - Log.Information("Settings file was not found; creating {settingsFileName} with defaults.", SettingsFileName); - settings = Settings.CreateDefaultSettings(); - - ServiceLocator.Current.TelemetryService.TrackEvent("Create Default Settings", properties: settings.GetTelemetryDictionary()); +namespace WinPrint.Core.Services; + +// TODO: Implement settings validation with appropriate alerting +public class SettingsService { + private readonly JsonSerializerOptions _jsonOptions; + + private FileWatcher? _watcher; + + public SettingsService() { + SettingsFileName = $"{SettingsPath}{Path.DirectorySeparatorChar}{SettingsFileName}"; + Log.Debug("Settings file path: {settingsFileName}", SettingsFileName); + + _jsonOptions = new JsonSerializerOptions { + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReadCommentHandling = JsonCommentHandling.Skip + }; + _jsonOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + } - SaveSettings(settings); - } - catch (JsonException je) { - ReportJsonParsingError(je); + public string SettingsFileName { get; set; } = "WinPrint.config.json"; + + /// + /// Gets the path to the settings file. + /// Default is %appdata%\Kindel\winprint. + /// However, if the exe was started from somewhere else, work in "portable mode" and + /// use the dir containing the exe as the path. + /// + public static string? SettingsPath { + get { + // Get dir of .exe + var path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(SettingsService))!.Location); + var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + //Log.Debug("path = {path}", path); + //Log.Debug("appdata = {appdata}", appdata); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + // Your OSX code here. } - catch (Exception ex) { - ReportUnknownFileError(ex); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + // } - finally { - if (fs != null) { - fs.Close(); + else { + var fvi = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(SettingsService))!.Location); + + // is this in Kindel\winprint? + if (path.Contains($@"{fvi.CompanyName}{Path.DirectorySeparatorChar}{fvi.ProductName}")) { + // We're running %programfiles%\Kindel\winprint; use %appdata%\Kindel\winprint. + path = + $@"{appdata}{Path.DirectorySeparatorChar}{fvi.CompanyName}{Path.DirectorySeparatorChar}{fvi.ProductName}"; } - } - // Enable file watcher - // Disable file watcher if it's active - if (watcher != null) { - watcher.ChangedEvent -= Watcher_ChangedEvent; - watcher.Dispose(); - watcher = null; + // TODO: Remove internal knowledge of Out-WinPrint from here + if (path.Contains($@"Program Files{Path.DirectorySeparatorChar}PowerShell")) { + path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(SettingsService))!.Location); + } } - // watch .command file for changes - watcher = new FileWatcher(Path.GetFullPath(SettingsFileName)); - watcher.ChangedEvent += Watcher_ChangedEvent; - - // TODO: Setup subscribing to all properties and automatically saving settings when they change + return path; + } + } - return settings; + /// + /// Reads settings from settings file (WinPrint.config.json). + /// If file does not exist, it is created. + /// + /// + public Settings? ReadSettings() { + Settings? settings = null; + FileStream? fs = null; + + try { + //Logger.Instance.Log4.Info($"Loading user-defined commands from {userCommandsFile}."); + fs = new FileStream(SettingsFileName, FileMode.Open, FileAccess.Read); + var jsonString = File.ReadAllText(SettingsFileName); + Log.Debug("ReadSettings: Deserializing from {settingsFileName}", SettingsFileName); + settings = JsonSerializer.Deserialize(jsonString, _jsonOptions); + + if (settings != null) { + ServiceLocator.Current.TelemetryService.TrackEvent("Read Settings", settings.GetTelemetryDictionary()); + } } + catch (FileNotFoundException) { + Log.Information("Settings file was not found; creating {settingsFileName} with defaults.", + SettingsFileName); + settings = Settings.CreateDefaultSettings(); - private void ReportUnknownFileError(Exception ex) { - // TODO: Graceful error handling for .config file - ServiceLocator.Current.TelemetryService.TrackException(ex, false); - Log.Error(ex, "SettingsService: Error with {settingsFileName}", SettingsFileName); + ServiceLocator.Current.TelemetryService.TrackEvent("Create Default Settings", + settings.GetTelemetryDictionary()); + + SaveSettings(settings); + } + catch (JsonException je) { + ReportJsonParsingError(je); + } + catch (Exception ex) { + ReportUnknownFileError(ex); + } + finally { + fs?.Close(); } - private void ReportJsonParsingError(JsonException je) { - ServiceLocator.Current.TelemetryService.TrackException(je, false); - // je.Message is of form: Message = ". Path: $.sheets | LineNumber: 6 | BytePositionInLine: 42." - var toFind = " Path: "; - var path = je.Message[(je.Message.IndexOf(toFind) + toFind.Length)..^0]; - var ex = new Exception($"Error parsing {SettingsFileName} at {path}"); - Log.Error(ex, "Error parsing {file} at {path}", SettingsFileName, path); + // Enable file watcher + // Disable file watcher if it's active + if (_watcher != null) { + _watcher.ChangedEvent -= Watcher_ChangedEvent; + _watcher.Dispose(); + _watcher = null; } - private void Watcher_ChangedEvent(object sender, EventArgs e) { - Log.Debug("Settings file changed: {file}", SettingsFileName); - ServiceLocator.Current.TelemetryService.TrackEvent("Settings File Changed"); + // watch .command file for changes + _watcher = new FileWatcher(Path.GetFullPath(SettingsFileName)); + _watcher.ChangedEvent += Watcher_ChangedEvent; - try { - var jsonString = File.ReadAllText(SettingsFileName); - var changedSettings = JsonSerializer.Deserialize(jsonString, jsonOptions); + // TODO: Setup subscribing to all properties and automatically saving settings when they change - if (ModelLocator.Current.Settings == null) { - // This can happen if settings failed to load when app started. - SimpleIoc.Default.Unregister(); - SimpleIoc.Default.Register(); - } - // CopyPropertiesFrom does a deep, property-by property copy from the passed instance - ModelLocator.Current.Settings.CopyPropertiesFrom(changedSettings); - } - catch (FileNotFoundException fnfe) { - // TODO: Graceful error handling for .config file - ServiceLocator.Current.TelemetryService.TrackException(fnfe, false); - Log.Error(fnfe, "Settings file changed but was then not found.", SettingsFileName); - } - catch (JsonException je) { - ReportJsonParsingError(je); - } - catch (Exception ex) { - ReportUnknownFileError(ex); - } - } + return settings; + } - /// - /// Saves Settings to settings file (WinPrint.config.json). Set `saveCTESettings=true` when - /// creating default. `false` otherwise. - /// - /// - /// If true Content Type Engine settings will be saved. - /// If true the file change watcher will be activated - public void SaveSettings(Models.Settings settings, bool saveCTESettings = true, bool watchChanges = false) { - ServiceLocator.Current.TelemetryService.TrackEvent("Save Settings", properties: settings.GetTelemetryDictionary()); - using var document = JsonDocument.Parse(JsonSerializer.Serialize(settings, jsonOptions), new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }); - - if (document.RootElement.ValueKind != JsonValueKind.Object) { - return; - } + private void ReportUnknownFileError(Exception ex) { + // TODO: Graceful error handling for .config file + ServiceLocator.Current.TelemetryService.TrackException(ex); + Log.Error(ex, "SettingsService: Error with {settingsFileName}", SettingsFileName); + } - // Disable file watcher - if (watcher != null) { - watcher.ChangedEvent -= Watcher_ChangedEvent; - watcher.Dispose(); - watcher = null; - } + private void ReportJsonParsingError(JsonException je) { + ServiceLocator.Current.TelemetryService.TrackException(je); + // je.Message is of form: Message = ". Path: $.sheets | LineNumber: 6 | BytePositionInLine: 42." + var toFind = " Path: "; + var path = je.Message[(je.Message.IndexOf(toFind, StringComparison.Ordinal) + toFind.Length)..]; + var ex = new Exception($"Error parsing {SettingsFileName} at {path}"); + Log.Error(ex, "Error parsing {file} at {path}", SettingsFileName, path); + } - using var fs = File.Create(SettingsFileName); - using var writer = new Utf8JsonWriter(fs, options: new JsonWriterOptions { Indented = true }); - writer.WriteStartObject(); - foreach (var property in document.RootElement.EnumerateObject()) { - if (saveCTESettings || !property.Name.ToLowerInvariant().Contains("contenttypeengine")) { - property.WriteTo(writer); - } - } + private void Watcher_ChangedEvent(object? sender, EventArgs e) { + Log.Debug("Settings file changed: {file}", SettingsFileName); + ServiceLocator.Current.TelemetryService.TrackEvent("Settings File Changed"); - writer.WriteEndObject(); - writer.Flush(); + try { + var jsonString = File.ReadAllText(SettingsFileName); + var changedSettings = JsonSerializer.Deserialize(jsonString, _jsonOptions); - if (watchChanges) { - // watch .command file for changes - watcher = new FileWatcher(Path.GetFullPath(SettingsFileName)); - watcher.ChangedEvent += Watcher_ChangedEvent; + if (ModelLocator.Current?.Settings == null) { + // This can happen if settings failed to load when app started. + SimpleIoc.Default.Unregister(); + SimpleIoc.Default.Register(); } + + // CopyPropertiesFrom does a deep, property-by property copy from the passed instance + ModelLocator.Current?.Settings.CopyPropertiesFrom(changedSettings); + } + catch (FileNotFoundException fnfe) { + // TODO: Graceful error handling for .config file + ServiceLocator.Current.TelemetryService.TrackException(fnfe); + Log.Error(fnfe, "Settings file changed but was then not found.", SettingsFileName); + } + catch (JsonException je) { + ReportJsonParsingError(je); } + catch (Exception ex) { + ReportUnknownFileError(ex); + } + } - // Factory - creates - static public Settings Create() { - LogService.TraceMessage(); - var settingsService = ServiceLocator.Current.SettingsService.ReadSettings(); - return settingsService; + /// + /// Saves Settings to settings file (WinPrint.config.json). Set `saveCTESettings=true` when + /// creating default. `false` otherwise. + /// + /// + /// If true Content Type Engine settings will be saved. + /// If true the file change watcher will be activated + public void SaveSettings(Settings settings, bool saveCTESettings = true, bool watchChanges = false) { + ServiceLocator.Current.TelemetryService.TrackEvent("Save Settings", settings.GetTelemetryDictionary()); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(settings, _jsonOptions), + new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }); + + if (document.RootElement.ValueKind != JsonValueKind.Object) { + return; } - /// - /// Gets the path to the settings file. - /// Default is %appdata%\Kindel Systems\winprint. - /// However, if the exe was started from somewhere else, work in "portable mode" and - /// use the dir containing the exe as the path. - /// - public static string SettingsPath { - get { - // Get dir of .exe - var path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(SettingsService)).Location); - var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - //Log.Debug("path = {path}", path); - //Log.Debug("appdata = {appdata}", appdata); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - // Your OSX code here. - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - // - } - else { - var fvi = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(SettingsService)).Location); - - // is this in Kindel Systems\winprint? - if (path.Contains($@"{fvi.CompanyName}{Path.DirectorySeparatorChar}{fvi.ProductName}")) { - // We're running %programfiles%\Kindel Systems\winprint; use %appdata%\Kindel Systems\winprint. - path = $@"{appdata}{Path.DirectorySeparatorChar}{fvi.CompanyName}{Path.DirectorySeparatorChar}{fvi.ProductName}"; - } - // TODO: Remove internal knowledge of out-winprint from here - if (path.Contains($@"Program Files{Path.DirectorySeparatorChar}PowerShell")) { - path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(SettingsService)).Location); - } - } + // Disable file watcher + if (_watcher != null) { + _watcher.ChangedEvent -= Watcher_ChangedEvent; + _watcher.Dispose(); + _watcher = null; + } - return path; + using var fs = File.Create(SettingsFileName); + using var writer = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true }); + writer.WriteStartObject(); + foreach (var property in document.RootElement.EnumerateObject()) { + if (saveCTESettings || !property.Name.ToLowerInvariant().Contains("contenttypeengine")) { + property.WriteTo(writer); } } + writer.WriteEndObject(); + writer.Flush(); + + if (watchChanges) { + // watch .command file for changes + _watcher = new FileWatcher(Path.GetFullPath(SettingsFileName)); + _watcher.ChangedEvent += Watcher_ChangedEvent; + } + } + + // Factory - creates + public static Settings? Create() { + LogService.TraceMessage(); + var settingsService = ServiceLocator.Current.SettingsService.ReadSettings(); + return settingsService; } } diff --git a/src/WinPrint.Core/Services/TelemetryService.cs b/src/WinPrint.Core/Services/TelemetryService.cs index d1d4ffc..2523b23 100644 --- a/src/WinPrint.Core/Services/TelemetryService.cs +++ b/src/WinPrint.Core/Services/TelemetryService.cs @@ -24,10 +24,10 @@ public TelemetryClient GetTelemetryClient() { private Stopwatch runtime; - public void Start(string appName, IDictionary startProperties = null) { + public void Start(string appName, IDictionary startProperties = null) { runtime = System.Diagnostics.Stopwatch.StartNew(); - var val = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Kindel Systems\winprint", "Telemetry", 0); + var val = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Kindel\winprint", "Telemetry", 0); TelemetryEnabled = (val != null && val.ToString() == "1") ? true : false; // Setup telemetry via Azure Application Insights. @@ -62,7 +62,7 @@ public void Start(string appName, IDictionary startProperties = if (startProperties == null) { - startProperties = new Dictionary(); + startProperties = new Dictionary(); } // Merged passed in properites @@ -89,7 +89,7 @@ public void SetUser(string user) { telemetry.Context.User.AuthenticatedUserId = user; } - public void TrackEvent(string key, IDictionary properties = null, IDictionary metrics = null) { + public void TrackEvent(string key, IDictionary properties = null, IDictionary metrics = null) { if (TelemetryEnabled && telemetry != null) { telemetry.TrackEvent(key, properties, metrics); } diff --git a/src/WinPrint.Core/Services/TelemetryService.tt b/src/WinPrint.Core/Services/TelemetryService.tt index f51e750..4d37bef 100644 --- a/src/WinPrint.Core/Services/TelemetryService.tt +++ b/src/WinPrint.Core/Services/TelemetryService.tt @@ -3,7 +3,7 @@ <#@ output extension=".tt.cs" #> <# string id = System.Environment.GetEnvironmentVariable("winprint_telemetryId"); - if (id == null) id = ""; + if (id == null) id = "get key from portal.azure.com"; #> // // This code was generated by a tool. Any changes made manually will be lost diff --git a/src/WinPrint.Core/Services/UpdateService.cs b/src/WinPrint.Core/Services/UpdateService.cs index 8a910be..976c093 100644 --- a/src/WinPrint.Core/Services/UpdateService.cs +++ b/src/WinPrint.Core/Services/UpdateService.cs @@ -9,161 +9,166 @@ using Octokit; using Serilog; -namespace WinPrint.Core.Services { +namespace WinPrint.Core.Services; + +/// +/// Implements version checks, updated version downloads, and installs. +/// +public class UpdateService { + private string? _tempFilename; + /// - /// Implements version checks, updated version downloads, and installs. + /// Any error messages from failed update checks or downloads /// - public class UpdateService { - - /// - /// Fired whenever a check for latest version has completed. - /// - public event EventHandler GotLatestVersion; - protected void OnGotLatestVersion(Version latestVersion) { - GotLatestVersion?.Invoke(this, latestVersion); - } + public string? ErrorMessage { get; private set; } - /// - /// Fired when a download kicked off by StartUpgrade completes - /// - public event EventHandler DownloadComplete; - protected void OnDownloadComplete(string path) { - DownloadComplete?.Invoke(this, path); - } + /// + /// Provides the current version number + /// + public static Version CurrentVersion => + new(FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(UpdateService))!.Location).FileVersion!); - public event EventHandler DownloadProgressChanged; - protected void OnDownloadProgressChanged(DownloadProgressChangedEventArgs e) { - DownloadProgressChanged?.Invoke(this, e); - } + /// + /// Contains the version number of the latest version found online (only valid after GotLatestVersion) + /// + public Version LatestVersion { get; private set; } = new(0, 0); - /// - /// Any error messages from failed update checks or downloads - /// - public string ErrorMessage { get; private set; } - /// - /// Provides the current version number - /// - public static Version CurrentVersion => new Version(FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(UpdateService)).Location).ProductVersion); + /// + /// Uri to the release notes page (only valid after GotLatestVersion) + /// + public Uri? ReleasePageUri { get; set; } - /// - /// Contains the version number of the latest version found online (only valid after GotLatestVersion) - /// - public Version LatestVersion { get; private set; } + /// + /// Uri to the installer file (only valid after GotLatestVersion) + /// + public Uri? InstallerUri { get; set; } + /// + /// Fired whenever a check for latest version has completed. + /// + public event EventHandler? GotLatestVersion; - /// - /// Uri to the release notes page (only valid after GotLatestVersion) - /// - public System.Uri ReleasePageUri { get; set; } + protected void OnGotLatestVersion(Version latestVersion) { + GotLatestVersion?.Invoke(this, latestVersion); + } - /// - /// Uri to the installer file (only valid after GotLatestVersion) - /// - public System.Uri InstallerUri { get; set; } + /// + /// Fired when a download kicked off by StartUpgrade completes + /// + public event EventHandler? DownloadComplete; - private string _tempFilename; + protected void OnDownloadComplete(string path) { + DownloadComplete?.Invoke(this, path); + } - public UpdateService() { - LatestVersion = new Version(0, 0); - } + public event EventHandler? DownloadProgressChanged; - /// - /// Compares current version ot latest online version. - /// > 0 - Current version is newer - /// = 0 - Same version - /// < 0 - A newer version available - /// - public int CompareVersions() { - return CurrentVersion.CompareTo(LatestVersion); - } + protected void OnDownloadProgressChanged(DownloadProgressChangedEventArgs e) { + DownloadProgressChanged?.Invoke(this, e); + } + + /// + /// Compares current version ot latest online version. + /// > 0 - Current version is newer + /// = 0 - Same version + /// < 0 - A newer version available + /// + /// + public int CompareVersions() { + return CurrentVersion.CompareTo(LatestVersion); + } - /// - /// Checks for updated version online. - /// - /// - public async Task GetLatestVersionAsync(CancellationToken token) { - InstallerUri = new Uri("https://github.com/tig/winprint/releases"); - using var client = new WebClient(); - try { - var github = new GitHubClient(new Octokit.ProductHeaderValue("tig-winprint")); - var allReleases = await github.Repository.Release.GetAll("tig", "winprint").ConfigureAwait(false); - - //#if DEBUG - // Debug.WriteLine("Pausing 2s to simulate version check taking a long time..."); - // Thread.Sleep(2000); - // Debug.WriteLine("Done pausing."); - //#endif - token.ThrowIfCancellationRequested(); - - // Get all releases and pre-releases + /// + /// Checks for updated version online. + /// + /// + public async Task GetLatestVersionAsync(CancellationToken token) { + LogService.TraceMessage(); + InstallerUri = new Uri("https://github.com/tig/winprint/releases"); + using var client = new WebClient(); + try { + var github = new GitHubClient(new ProductHeaderValue("tig-winprint")); + var allReleases = await github.Repository.Release.GetAll("tig", "winprint").ConfigureAwait(false); + + //#if DEBUG + // Debug.WriteLine("Pausing 2s to simulate version check taking a long time..."); + // Thread.Sleep(2000); + // Debug.WriteLine("Done pausing."); + //#endif + token.ThrowIfCancellationRequested(); + + // Get all releases and pre-releases #if DEBUG - var releases = allReleases.Where(r => r.Prerelease).OrderByDescending(r => new Version(r.TagName.Replace('v', ' '))).ToArray(); + var releases = allReleases.Where(r => r.Prerelease) + .OrderByDescending(r => new Version(r.TagName.Replace('v', ' '))).ToArray(); #else - var releases = allReleases.Where(r => !r.Prerelease).OrderByDescending(r => new Version(r.TagName.Replace('v', ' '))).ToArray(); + var releases = + allReleases.Where(r => !r.Prerelease).OrderByDescending(r => new Version(r.TagName.Replace('v', ' '))).ToArray(); #endif - //Log.Debug("Releases {releases}", JsonSerializer.Serialize(releases, options: new JsonSerializerOptions() { WriteIndented = true })); - if (releases.Length > 0) { - Log.Debug("The latest release is tagged at {TagName} and is named {Name}. Download Url: {BrowserDownloadUrl}", - releases[0].TagName, releases[0].Name, releases[0].Assets[0].BrowserDownloadUrl); - - LatestVersion = new Version(releases[0].TagName.Replace('v', ' ')); - ReleasePageUri = new Uri(releases[0].HtmlUrl); - InstallerUri = new Uri(releases[0].Assets[0].BrowserDownloadUrl); - } - else { - ErrorMessage = "No release found."; - } + //Log.Debug("Releases {releases}", JsonSerializer.Serialize(releases, options: new JsonSerializerOptions() { WriteIndented = true })); + if (releases.Length > 0) { + Log.Debug( + "The latest release is tagged at {TagName} and is named {Name}. Download Url: {BrowserDownloadUrl}", + releases[0].TagName, releases[0].Name, releases[0].Assets[0].BrowserDownloadUrl); + + LatestVersion = new Version(releases[0].TagName.Replace('v', ' ')); + ReleasePageUri = new Uri(releases[0].HtmlUrl); + InstallerUri = new Uri(releases[0].Assets[0].BrowserDownloadUrl); } - catch (Exception e) { - ErrorMessage = $"({ReleasePageUri}) {e.Message}"; - ServiceLocator.Current.TelemetryService.TrackException(e); + else { + ErrorMessage = "No release found."; } - - OnGotLatestVersion(LatestVersion); - return LatestVersion; } - - /// - /// Starts an upgrade. Must be called after GotLatestVersion has been fired. - /// - public async Task StartUpgradeAsync() { - Debug.WriteLine("StartUpgradeAsync"); - // Download file - _tempFilename = Path.GetTempFileName() + ".msi"; - //Log.Information($"{this.GetType().Name}: Downloading {InstallerUri.AbsoluteUri} to {_tempFilename}..."); - - var client = new WebClient(); - client.DownloadDataCompleted += Client_DownloadDataCompleted; - client.DownloadProgressChanged += Client_DownloadProgressChanged; - File.WriteAllBytes(_tempFilename, await client.DownloadDataTaskAsync(InstallerUri).ConfigureAwait(false)); - return _tempFilename; + catch (Exception e) { + ErrorMessage = $"({ReleasePageUri}) {e.Message}"; + Log.Warning("Update: {msg}", ErrorMessage); + ServiceLocator.Current.TelemetryService.TrackException(e); } - private void Client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) { - //Log.Debug($"{this.GetType().Name}: Download progress {e.ProgressPercentage}%"); - //if (e.ProgressPercentage % 33 == 0) { - // Log.Information($"{this.GetType().Name}: Download progress {e.ProgressPercentage}%"); - //} - OnDownloadProgressChanged(e); - } + OnGotLatestVersion(LatestVersion); + return LatestVersion; + } - private void Client_DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e) { - //try { - // // If the request was not canceled and did not throw - // // an exception, display the resource. - // if (!e.Cancelled && e.Error == null) { - // //File.WriteAllBytes(_tempFilename, (byte[])e.Result); - // } - //} - //finally { + /// + /// Starts an upgrade. Must be called after GotLatestVersion has been fired. + /// + public async Task StartUpgradeAsync() { + Debug.WriteLine("StartUpgradeAsync"); + // Download file + _tempFilename = Path.GetTempFileName() + ".msi"; + //Log.Information($"{this.GetType().Name}: Downloading {InstallerUri.AbsoluteUri} to {_tempFilename}..."); + + var client = new WebClient(); + client.DownloadDataCompleted += Client_DownloadDataCompleted; + client.DownloadProgressChanged += Client_DownloadProgressChanged; + await File.WriteAllBytesAsync(_tempFilename, await client.DownloadDataTaskAsync(InstallerUri).ConfigureAwait(false)); + return _tempFilename; + } - //} - ////Log.Information($"{this.GetType().Name}: Download complete"); - ////Log.Information($"{this.GetType().Name}: Exiting and running installer ({_tempFilename})..."); + private void Client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) { + //Log.Debug($"{this.GetType().Name}: Download progress {e.ProgressPercentage}%"); + //if (e.ProgressPercentage % 33 == 0) { + // Log.Information($"{this.GetType().Name}: Download progress {e.ProgressPercentage}%"); + //} + OnDownloadProgressChanged(e); + } + private void Client_DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e) { + //try { + // // If the request was not canceled and did not throw + // // an exception, display the resource. + // if (!e.Cancelled && e.Error == null) { + // //File.WriteAllBytes(_tempFilename, (byte[])e.Result); + // } + //} + //finally { - OnDownloadComplete(_tempFilename); - } + //} + ////Log.Information($"{this.GetType().Name}: Download complete"); + ////Log.Information($"{this.GetType().Name}: Exiting and running installer ({_tempFilename})..."); + + + OnDownloadComplete(_tempFilename!); } } diff --git a/src/WinPrint.Core/ViewModels/FooterViewModel.cs b/src/WinPrint.Core/ViewModels/FooterViewModel.cs new file mode 100644 index 0000000..8ff2834 --- /dev/null +++ b/src/WinPrint.Core/ViewModels/FooterViewModel.cs @@ -0,0 +1,29 @@ +// Copyright Kindel Systems, LLC - http://www.kindel.com +// Published under the MIT License at https://github.com/tig/winprint + +using System.Drawing; +using WinPrint.Core.Models; + +namespace WinPrint.Core; + +public class FooterViewModel(SheetViewModel svm, HeaderFooter hf) : HeaderFooterViewModel(svm, hf) { + internal override RectangleF CalcBounds() { + var h = SheetViewModel.GetFontHeight(Font) + VerticalPadding; + if (!Enabled) { + return new RectangleF(0, 0, 0, 0); + } + + if (_svm != null) { + return new RectangleF(_svm.Bounds.Left + _svm.Margins.Left, + _svm.Bounds.Bottom - _svm.Margins.Bottom - h, + _svm.Bounds.Width - _svm.Margins.Left - _svm.Margins.Right, + h); + } + + return new RectangleF(0, 0, 0, 0); + } + + internal override bool IsAlignTop() { + return false; + } +} diff --git a/src/WinPrint.Core/ViewModels/HeaderFooterViewModel.cs b/src/WinPrint.Core/ViewModels/HeaderFooterViewModel.cs index 2141efe..1040ab2 100644 --- a/src/WinPrint.Core/ViewModels/HeaderFooterViewModel.cs +++ b/src/WinPrint.Core/ViewModels/HeaderFooterViewModel.cs @@ -2,286 +2,265 @@ using System.Drawing; using Serilog; using WinPrint.Core.Models; +using WinPrint.Core.ViewModels; +using Font = WinPrint.Core.Models.Font; -namespace WinPrint.Core { - /// - /// Knows how to paint a header or footer. - /// - // TODO: Add a Padding property to provide padding below bottom of header/above top of footer - public abstract class HeaderFooterViewModel : ViewModels.ViewModelBase, IDisposable { - - private string _text; - private Core.Models.Font _font; - private bool _leftBorder; - private bool _topBorder; - private bool _rightBorder; - private bool _bottomBorder; - private bool _enabled; - // TODO: Make settable - private int _verticalPadding = 10; // Vertical padding below/above header/footer in 100ths of inch - - public string Text { get => _text; set => SetField(ref _text, value); } - - /// - /// Font used for header or footer text - /// - public Core.Models.Font Font { get => _font; set => SetField(ref _font, value); } - - /// - /// Enables or disables printing of left border of heder/footer - /// - public bool LeftBorder { get => _leftBorder; set => SetField(ref _leftBorder, value); } - /// - /// Enables or disables printing of Top border of heder/footer - /// - public bool TopBorder { get => _topBorder; set => SetField(ref _topBorder, value); } - /// - /// Enables or disables printing of Right border of heder/footer - /// - public bool RightBorder { get => _rightBorder; set => SetField(ref _rightBorder, value); } - /// - /// Enables or disables printing of Bottom border of heder/footer - /// - public bool BottomBorder { get => _bottomBorder; set => SetField(ref _bottomBorder, value); } - - /// - /// Enable or disable header/footer - /// - public bool Enabled { get => _enabled; set => SetField(ref _enabled, value); } - - public int VerticalPadding { get => _verticalPadding; set => SetField(ref _verticalPadding, value); } - - /// - /// Header/Footer bounds (page minus margins) - /// - public RectangleF Bounds => CalcBounds(); - - internal SheetViewModel _svm; - - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } +namespace WinPrint.Core; - // Protected implementation of Dispose pattern. - // Flag: Has Dispose already been called? - private bool _disposed = false; - protected virtual void Dispose(bool disposing) { - if (_disposed) { - return; - } +/// +/// Knows how to paint a header or footer. +/// +// TODO: Add a Padding property to provide padding below bottom of header/above top of footer +public abstract class HeaderFooterViewModel : ViewModelBase, IDisposable { + private bool _bottomBorder; - if (disposing) { - //if (Font != null) Font.Dispose(); - } - _disposed = true; + // Protected implementation of Dispose pattern. + // Flag: Has Dispose already been called? + private bool _disposed; + private bool _enabled; + private Font? _font; + private bool _leftBorder; + private bool _rightBorder; + + internal SheetViewModel? _svm; + + private string? _text; + + private bool _topBorder; + + // TODO: Make settable + private int _verticalPadding = 10; // Vertical padding below/above header/footer in 100ths of inch + + /// + protected HeaderFooterViewModel(SheetViewModel? svm, HeaderFooter? hf) { + if (hf is null) { + throw new ArgumentNullException(nameof(hf)); } - /// - /// Calcuate the Header or Footer bounds (position and size on sheet) based on containing document and font size. - /// - /// - internal abstract RectangleF CalcBounds(); + _svm = svm ?? throw new ArgumentNullException(nameof(svm)); - internal abstract bool IsAlignTop(); + Text = hf.Text; + LeftBorder = hf.LeftBorder; + RightBorder = hf.RightBorder; + TopBorder = hf.TopBorder; + BottomBorder = hf.BottomBorder; - public void Paint(Graphics g, int sheetNum) { - if (!Enabled) { - return; - } + // Font can be null (provided by Sheet definition) + if (hf.Font != null) { + Font = (Font)hf.Font.Clone(); + } - if (g is null) { - throw new ArgumentNullException(nameof(g)); + Enabled = hf.Enabled; + VerticalPadding = hf.VerticalPadding; + + // Wire up changes from Header / Footer models + hf.PropertyChanged += (s, e) => { + var reflow = false; + switch (e.PropertyName) { + case "Text": + Text = hf.Text; + break; + case "LeftBorder": + LeftBorder = hf.LeftBorder; + break; + case "RightBorder": + RightBorder = hf.RightBorder; + break; + case "TopBorder": + TopBorder = hf.TopBorder; + break; + case "BottomBorder": + BottomBorder = hf.BottomBorder; + break; + case "Font": + Font = hf.Font; + reflow = true; + break; + case "Enabled": + Enabled = hf.Enabled; + reflow = true; + break; + case "VerticalPadding": + VerticalPadding = hf.VerticalPadding; + reflow = true; + break; + default: + throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); } - var boundsHF = CalcBounds(); - boundsHF.Y += IsAlignTop() ? 0 : _verticalPadding; - boundsHF.Height -= _verticalPadding; + OnSettingsChanged(reflow); + }; + } - if (LeftBorder) { - g.DrawLine(Pens.Black, boundsHF.Left, boundsHF.Top, boundsHF.Left, boundsHF.Bottom); - } + public string? Text { get => _text; set => SetField(ref _text, value); } - if (TopBorder) { - g.DrawLine(Pens.Black, boundsHF.Left, boundsHF.Top, boundsHF.Right, boundsHF.Top); - } + /// + /// Font used for header or footer text + /// + public Font? Font { get => _font; set => SetField(ref _font, value); } - if (RightBorder) { - g.DrawLine(Pens.Black, boundsHF.Right, boundsHF.Top, boundsHF.Right, boundsHF.Bottom); - } + /// + /// Enables or disables printing of left border of heder/footer + /// + public bool LeftBorder { get => _leftBorder; set => SetField(ref _leftBorder, value); } - if (BottomBorder) { - g.DrawLine(Pens.Black, boundsHF.Left, boundsHF.Bottom, boundsHF.Right, boundsHF.Bottom); - } + /// + /// Enables or disables printing of Top border of heder/footer + /// + public bool TopBorder { get => _topBorder; set => SetField(ref _topBorder, value); } - Log.Debug($"{GetType().Name}: Expanding Macros - {Text}"); - var macros = new Macros(_svm) { - Page = sheetNum - }; - var parts = macros.ReplaceMacros(Text).Split('\t', '|'); + /// + /// Enables or disables printing of Right border of heder/footer + /// + public bool RightBorder { get => _rightBorder; set => SetField(ref _rightBorder, value); } - // Left\tCenter\tRight - if (parts == null || parts.Length == 0) { - return; - } + /// + /// Enables or disables printing of Bottom border of heder/footer + /// + public bool BottomBorder { get => _bottomBorder; set => SetField(ref _bottomBorder, value); } - using var tempFont = CreateTempFont(g); + /// + /// Enable or disable header/footer + /// + public bool Enabled { get => _enabled; set => SetField(ref _enabled, value); } - using var fmt = new StringFormat(StringFormat.GenericTypographic) { - Trimming = StringTrimming.None, - // BUGBUG: This is a work around for https://stackoverflow.com/questions/59159919/stringformat-trimming-changes-vertical-placement-of-text - // (turning on NoWrap). - FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.NoWrap | StringFormatFlags.NoClip - }; + public int VerticalPadding { get => _verticalPadding; set => SetField(ref _verticalPadding, value); } - fmt.LineAlignment = IsAlignTop() ? StringAlignment.Near : StringAlignment.Far; + /// + /// Header/Footer bounds (page minus margins) + /// + public RectangleF Bounds => CalcBounds(); - // Center goes first - it has priority - ensure it gets drawn completely where - // Left & Right can be trimmed - var sizeCenter = new SizeF(0, 0); + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } - if (parts.Length > 1) { - fmt.Alignment = StringAlignment.Center; - sizeCenter = g.MeasureString(parts[1], tempFont, (int)boundsHF.Width, fmt); - // g.DrawRectangle(Pens.Purple, boundsHF.Left, boundsHF.Top, boundsHF.Width, boundsHF.Height); - g.DrawString(parts[1], tempFont, Brushes.Black, boundsHF, fmt); - } + protected virtual void Dispose(bool disposing) { + if (_disposed) { + return; + } - // Left - // Remove the space taken up by the center from the bounds - var textCenterBounds = (boundsHF.Width - sizeCenter.Width) / 2; + if (disposing) { + //if (Font != null) Font.Dispose(); + } - var boundsLeft = new RectangleF(boundsHF.X, boundsHF.Y, textCenterBounds, boundsHF.Height); - var sizeLeft = g.MeasureString(parts[0], tempFont, (int)textCenterBounds, fmt); + _disposed = true; + } - fmt.Alignment = StringAlignment.Near; - fmt.Trimming = StringTrimming.None; - //g.DrawRectangle(Pens.Orange, boundsLeft.X, boundsLeft.Y, boundsLeft.Width, boundsLeft.Height); - g.DrawString(parts[0], tempFont, Brushes.Black, boundsLeft, fmt); - - //Right - var boundsRight = new RectangleF(boundsHF.X + (boundsHF.Width - textCenterBounds), boundsHF.Y, textCenterBounds, boundsHF.Height); - if (parts.Length > 2) { - fmt.Alignment = StringAlignment.Far; - fmt.Trimming = StringTrimming.None; - //g.DrawRectangle(Pens.Blue, boundsRight.X, boundsRight.Y, boundsRight.Width, boundsRight.Height); - g.DrawString(parts[2], tempFont, Brushes.Black, boundsRight, fmt); - } + /// + /// Calculate the Header or Footer bounds (position and size on sheet) based on containing document and font size. + /// + /// + internal abstract RectangleF CalcBounds(); + + internal abstract bool IsAlignTop(); + + public void Paint(Graphics? g, int sheetNum) { + if (!Enabled) { + return; } - /// - /// Get a font suitable for printing or preview. If no font was specified just return default system font. - /// - /// - /// - private System.Drawing.Font CreateTempFont(Graphics g) { - System.Drawing.Font tempFont; + if (g is null) { + throw new ArgumentNullException(nameof(g)); + } - if (Font == null) { - return System.Drawing.SystemFonts.DefaultFont; - } + var boundsHF = CalcBounds(); + boundsHF.Y += IsAlignTop() ? 0 : _verticalPadding; + boundsHF.Height -= _verticalPadding; - if (g.PageUnit == GraphicsUnit.Display) { - tempFont = new System.Drawing.Font(Font.Family, Font.Size, Font.Style, GraphicsUnit.Point); - } - else { - // Convert font to pixel units if we're in preview - tempFont = new System.Drawing.Font(Font.Family, Font.Size / 72F * 96F, Font.Style, GraphicsUnit.Pixel); - } + if (LeftBorder) { + g.DrawLine(Pens.Black, boundsHF.Left, boundsHF.Top, boundsHF.Left, boundsHF.Bottom); + } - return tempFont; + if (TopBorder) { + g.DrawLine(Pens.Black, boundsHF.Left, boundsHF.Top, boundsHF.Right, boundsHF.Top); } + if (RightBorder) { + g.DrawLine(Pens.Black, boundsHF.Right, boundsHF.Top, boundsHF.Right, boundsHF.Bottom); + } - // if bool is true, reflow. Otherwise just paint - public event EventHandler SettingsChanged; - protected void OnSettingsChanged(bool reflow) { - SettingsChanged?.Invoke(this, reflow); + if (BottomBorder) { + g.DrawLine(Pens.Black, boundsHF.Left, boundsHF.Bottom, boundsHF.Right, boundsHF.Bottom); } - public HeaderFooterViewModel(SheetViewModel svm, HeaderFooter hf) { - if (svm is null) { - throw new ArgumentNullException(nameof(svm)); - } + Log.Debug($"{GetType().Name}: Expanding Macros - {Text}"); + var macros = new Macros(_svm) { Page = sheetNum }; + var parts = macros.ReplaceMacros(Text).Split('\t', '|'); - if (hf is null) { - throw new ArgumentNullException(nameof(hf)); - } + // Left\tCenter\tRight + if (parts.Length == 0) { + return; + } - this._svm = svm; + using var tempFont = CreateTempFont(g); - Text = hf.Text; - LeftBorder = hf.LeftBorder; - RightBorder = hf.RightBorder; - TopBorder = hf.TopBorder; - BottomBorder = hf.BottomBorder; + using var fmt = new StringFormat(StringFormat.GenericTypographic) { + Trimming = StringTrimming.None, + // BUGBUG: This is a work around for https://stackoverflow.com/questions/59159919/stringformat-trimming-changes-vertical-placement-of-text + // (turning on NoWrap). + FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.NoWrap | StringFormatFlags.NoClip + }; - // Font can be null (provided by Sheet definition) - if (hf.Font != null) { - Font = (Core.Models.Font)hf.Font.Clone(); - } + fmt.LineAlignment = IsAlignTop() ? StringAlignment.Near : StringAlignment.Far; - Enabled = hf.Enabled; - VerticalPadding = hf.VerticalPadding; - - // Wire up changes from Header / Footer models - hf.PropertyChanged += (s, e) => { - var reflow = false; - switch (e.PropertyName) { - case "Text": Text = hf.Text; break; - case "LeftBorder": LeftBorder = hf.LeftBorder; break; - case "RightBorder": RightBorder = hf.RightBorder; break; - case "TopBorder": TopBorder = hf.TopBorder; break; - case "BottomBorder": BottomBorder = hf.BottomBorder; break; - case "Font": Font = hf.Font; reflow = true; break; - case "Enabled": Enabled = hf.Enabled; reflow = true; break; - case "VerticalPadding": VerticalPadding = hf.VerticalPadding; reflow = true; break; - default: - throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); - } - OnSettingsChanged(reflow); - }; + // Center goes first - it has priority - ensure it gets drawn completely where + // Left & Right can be trimmed + var sizeCenter = new SizeF(0, 0); + + if (parts.Length > 1) { + fmt.Alignment = StringAlignment.Center; + sizeCenter = g.MeasureString(parts[1], tempFont, (int)boundsHF.Width, fmt); + // g.DrawRectangle(Pens.Purple, boundsHF.Left, boundsHF.Top, boundsHF.Width, boundsHF.Height); + g.DrawString(parts[1], tempFont, Brushes.Black, boundsHF, fmt); } - } - public class HeaderViewModel : HeaderFooterViewModel { - public HeaderViewModel(SheetViewModel svm, HeaderFooter hf) : base(svm, hf) { } + // Left + // Remove the space taken up by the center from the bounds + var textCenterBounds = (boundsHF.Width - sizeCenter.Width) / 2; - internal override RectangleF CalcBounds() { - var h = SheetViewModel.GetFontHeight(Font) + VerticalPadding; - if (Enabled) { - return new RectangleF(_svm.Bounds.Left + _svm.Margins.Left, - _svm.Bounds.Top + _svm.Margins.Top, - _svm.Bounds.Width - _svm.Margins.Left - _svm.Margins.Right, - h); - } - else { - return new RectangleF(0, 0, 0, 0); - } - } + var boundsLeft = new RectangleF(boundsHF.X, boundsHF.Y, textCenterBounds, boundsHF.Height); + var sizeLeft = g.MeasureString(parts[0], tempFont, (int)textCenterBounds, fmt); - internal override bool IsAlignTop() { - return true; + fmt.Alignment = StringAlignment.Near; + fmt.Trimming = StringTrimming.None; + //g.DrawRectangle(Pens.Orange, boundsLeft.X, boundsLeft.Y, boundsLeft.Width, boundsLeft.Height); + g.DrawString(parts[0], tempFont, Brushes.Black, boundsLeft, fmt); + + //Right + var boundsRight = new RectangleF(boundsHF.X + (boundsHF.Width - textCenterBounds), boundsHF.Y, textCenterBounds, + boundsHF.Height); + if (parts.Length > 2) { + fmt.Alignment = StringAlignment.Far; + fmt.Trimming = StringTrimming.None; + //g.DrawRectangle(Pens.Blue, boundsRight.X, boundsRight.Y, boundsRight.Width, boundsRight.Height); + g.DrawString(parts[2], tempFont, Brushes.Black, boundsRight, fmt); } } - public class FooterViewModel : HeaderFooterViewModel { - public FooterViewModel(SheetViewModel svm, HeaderFooter hf) : base(svm, hf) { } - - internal override RectangleF CalcBounds() { - var h = SheetViewModel.GetFontHeight(Font) + VerticalPadding; - if (Enabled) { - return new RectangleF(_svm.Bounds.Left + _svm.Margins.Left, - _svm.Bounds.Bottom - _svm.Margins.Bottom - h, - _svm.Bounds.Width - _svm.Margins.Left - _svm.Margins.Right, - h); - } - else { - return new RectangleF(0, 0, 0, 0); - } - } - internal override bool IsAlignTop() { - return false; + + /// + /// Get a font suitable for printing or preview. If no font was specified just return default system font. + /// + /// + /// + private System.Drawing.Font? CreateTempFont(Graphics? g) { + if (Font == null) { + return SystemFonts.DefaultFont; } + + var tempFont = g.PageUnit == GraphicsUnit.Display ? new System.Drawing.Font(Font.Family, Font.Size, Font.Style, GraphicsUnit.Point) : + // Convert font to pixel units if we're in preview + new System.Drawing.Font(Font.Family, Font.Size / 72F * 96F, Font.Style, GraphicsUnit.Pixel); + + return tempFont; } -} + + // if bool is true, reflow. Otherwise just paint + public event EventHandler? SettingsChanged; + + protected void OnSettingsChanged(bool reflow) { + SettingsChanged?.Invoke(this, reflow); + } +} diff --git a/src/WinPrint.Core/ViewModels/HeaderViewModel.cs b/src/WinPrint.Core/ViewModels/HeaderViewModel.cs new file mode 100644 index 0000000..3eff4a1 --- /dev/null +++ b/src/WinPrint.Core/ViewModels/HeaderViewModel.cs @@ -0,0 +1,25 @@ +// Copyright Kindel Systems, LLC - http://www.kindel.com +// Published under the MIT License at https://github.com/tig/winprint + +using System.Drawing; +using WinPrint.Core.Models; + +namespace WinPrint.Core; + +public class HeaderViewModel(SheetViewModel? svm, HeaderFooter? hf) : HeaderFooterViewModel(svm, hf) { + internal override RectangleF CalcBounds() { + var h = SheetViewModel.GetFontHeight(Font) + VerticalPadding; + if (Enabled && _svm is {}) { + return new RectangleF(_svm.Bounds.Left + _svm.Margins.Left, + _svm.Bounds.Top + _svm.Margins.Top, + _svm.Bounds.Width - _svm.Margins.Left - _svm.Margins.Right, + h); + } + + return new RectangleF(0, 0, 0, 0); + } + + internal override bool IsAlignTop() { + return true; + } +} diff --git a/src/WinPrint.Core/ViewModels/SheetViewModel.cs b/src/WinPrint.Core/ViewModels/SheetViewModel.cs index 7e63fc1..0d3da9e 100644 --- a/src/WinPrint.Core/ViewModels/SheetViewModel.cs +++ b/src/WinPrint.Core/ViewModels/SheetViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Printing; @@ -9,1072 +10,1157 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using Octokit; using Serilog; using UtfUnknown; using WinPrint.Core.ContentTypeEngines; using WinPrint.Core.Models; using WinPrint.Core.Services; +using WinPrint.Core.ViewModels; +using Font = WinPrint.Core.Models.Font; + +namespace WinPrint.Core; + +/// +/// The WinPrint Document ViewModel - knows how to paint a document, independent of platform +/// (assuming System.Drawing and System.Printing). +/// +public class SheetViewModel : ViewModelBase { + private Rectangle _bounds; + private int _cols; + private RectangleF _contentBounds; + private ContentTypeEngineBase? _contentEngine; + private ContentSettings? _contentSettings; + private string? _contentType; + private Encoding? _encoding; + private string? _file; + + private FooterViewModel? _footerVM; + + private HeaderViewModel? _headerVM; + + private bool _landscape; + private string? _language; + private bool _loading; + + // These properties are all defined by user and sync'd with the Sheet model + private Margins? _margins; + + private int _numPages; + private int _padding; + private bool _pageSeparator; + + private Size _paperSize; + private RectangleF _printableArea; + private bool _ready; + private int _rows; + + private Font? _rulesFont; + + private SheetSettings? _sheet; + private string? _title; + + public Margins? Margins { get => _margins; set => SetField(ref _margins, value); } + public bool Landscape { get => _landscape; set => SetField(ref _landscape, value); } + public Font? DiagnosticRulesFont { get => _rulesFont; set => SetField(ref _rulesFont, value); } + public HeaderViewModel? Header { get => _headerVM; set => SetField(ref _headerVM, value); } + public FooterViewModel? Footer { get => _footerVM; set => SetField(ref _footerVM, value); } + + public int Rows { get => _rows; set => SetField(ref _rows, value); } + + public int Columns { get => _cols; set => SetField(ref _cols, value); } + + public int Padding { get => _padding; set => SetField(ref _padding, value); } + + public bool PageSeparator { get => _pageSeparator; set => SetField(ref _pageSeparator, value); } + + public ContentSettings? ContentSettings { get => _contentSettings; set => SetField(ref _contentSettings, value); } + + /// + /// The fully qualified path of the file being printed. Used for header/footer display purposes only. + /// + public string File { + get => _file; + set => SetField(ref _file, value); + } + + /// + /// The fully qualified path of the file being printed. Used for header/footer display purposes only. + /// + public string Title { + get => _title!; + set => SetField(ref _title, value); + } + + /// + /// Content type (e.g. text/x-csharp). Language.Id + /// + public string? ContentType { + get => _contentType; + set => SetField(ref _contentType, value); + } -namespace WinPrint.Core { /// - /// The WinPrint Document ViewModel - knows how to paint a document, independent of platform - /// (assuming System.Drawing and System.Printing). + /// The Language (Language.Title or Alias) /// - public class SheetViewModel : ViewModels.ViewModelBase { + public string Language { + get => _language!; + set => SetField(ref _language, value); + } - private SheetSettings _sheet; + /// + /// The character encoding of the document + /// + public Encoding? Encoding { get => _encoding; set => SetField(ref _encoding, value); } - // These properties are all defined by user and sync'd with the Sheet model - private Margins _margins; - public Margins Margins { get => _margins; set => SetField(ref _margins, value); } + /// + /// The number of sheets (NOT Pages) that will be printed. + /// + public int NumSheets { + get { + if (ContentEngine == null || Rows == 0 || Columns == 0) { + return 0; + } + + return (int)Math.Ceiling((double)_numPages / (Rows * Columns)); + } + } - private bool _landscape; - public bool Landscape { get => _landscape; set => SetField(ref _landscape, value); } + public ContentTypeEngineBase? ContentEngine { get => _contentEngine; set => SetField(ref _contentEngine, value); } - private Core.Models.Font _rulesFont; - public Core.Models.Font DiagnosticRulesFont { get => _rulesFont; set => SetField(ref _rulesFont, value); } + // These properties are all either calculated or dependent on printer settings + /// + /// Size of the Sheet of Paper in 100ths of an inch. + /// + public Size PaperSize { get => _paperSize; set => _paperSize = value; } - private HeaderViewModel _headerVM; - public HeaderViewModel Header { get => _headerVM; set => SetField(ref _headerVM, value); } + /// + /// Angle pages is rotated by + /// + public int LandscapeAngle { get; set; } - private FooterViewModel _footerVM; - public FooterViewModel Footer { get => _footerVM; set => SetField(ref _footerVM, value); } + public PrinterResolution? PrinterResolution { get; set; } - public int Rows { get => _rows; set => SetField(ref _rows, value); } - private int _rows; + public bool PrintInColor { get; set; } - public int Columns { get => _cols; set => SetField(ref _cols, value); } - private int _cols; + //public RectangleF PrintableArea { get => printableArea; set => printableArea = value; } + /// + /// The physical bounds of the Sheet of paper as provided by PageSettings. + /// + public Rectangle Bounds { get => _bounds; set => _bounds = value; } - public int Padding { get => _padding; set => SetField(ref _padding, value); } - private int _padding; + public float HardMarginX { get; set; } + public float HardMarginY { get; set; } - public bool PageSeparator { get => _pageSeparator; set => SetField(ref _pageSeparator, value); } - private bool _pageSeparator; + /// + /// The printable area. Bounds minus margins and header/footer. + /// + public RectangleF ContentBounds { get => _contentBounds; private set => _contentBounds = value; } - public ContentSettings ContentSettings { get => _contentSettings; set => SetField(ref _contentSettings, value); } - private ContentSettings _contentSettings; + /// + /// True if we're in the middle of loading the file. False otherwise. + /// + public bool Loading { + get => _loading; + set { + if (value != _loading) { + OnLoaded(value); + } - /// - /// The fully qualified path of the file being printed. Used for header/footer display purposes only. - /// - public string File { - get => _file; - set => SetField(ref _file, value); - } - private string _file; - - /// - /// The fully qualified path of the file being printed. Used for header/footer display purposes only. - /// - public string Title { - get => _title; - set => SetField(ref _title, value); - } - private string _title; - - /// - /// Content type (e.g. text/x-csharp). Language.Id - /// - public string ContentType { - get => _contentType; - set => SetField(ref _contentType, value); + SetField(ref _loading, value); } - private string _contentType; - - /// - /// The Language (Language.Title or Alias) - /// - public string Language { - get => _language; - set => SetField(ref _language, value); - } - private string _language; - - /// - /// The charcter encoding of the document - /// - public Encoding Encoding { get => _encoding; set => SetField(ref _encoding, value); } - private Encoding _encoding; - - private int _numPages; - - /// - /// The number of sheets (NOT Pages) that will be printed. - /// - public int NumSheets { - get { - if (ContentEngine == null || Rows == 0 || Columns == 0) { - return 0; - } + } - return (int)Math.Ceiling((double)_numPages / (Rows * Columns)); + /// + /// True if we're in the middle of reflowing. False otherwise. + /// + public bool Ready { + get => _ready; + set { + if (value != _ready) { + OnReadyChanged(value); } - } - public ContentTypeEngineBase ContentEngine { get => _contentEngine; set => SetField(ref _contentEngine, value); } - private ContentTypeEngineBase _contentEngine; - - private Size _paperSize; - private RectangleF _printableArea; - private Rectangle _bounds; - private RectangleF _contentBounds; - - // These properties are all either calculated or dependent on printer settings - /// - /// Size of the Sheet of Paper in 100ths of an inch. - /// - public Size PaperSize { get => _paperSize; set => _paperSize = value; } - - /// - /// Angle pages is rotated by - /// - public int LandscapeAngle { get; set; } - public PrinterResolution PrinterResolution { get; set; } - - public bool PrintInColor { get; set; } - - //public RectangleF PrintableArea { get => printableArea; set => printableArea = value; } - /// - /// The phyisical bounds of the Sheet of paper as provided by PageSettings. - /// - public Rectangle Bounds { get => _bounds; set => _bounds = value; } - public float HardMarginX { get; set; } - public float HardMarginY { get; set; } - - /// - /// The printable area. Bounds minus margins and header/footer. - /// - public RectangleF ContentBounds { get => _contentBounds; private set => _contentBounds = value; } - - /// - /// Subscribe to know when file has been loaded by the SheetViewModel. - /// - public event EventHandler Loaded; - protected void OnLoaded(bool l) { - Loaded?.Invoke(this, l); + SetField(ref _ready, value); } + } - /// - /// True if we're in the middle of loading the file. False otherwise. - /// - public bool Loading { - get => _loading; - set { - if (value != _loading) { - OnLoaded(value); - } + /// + /// Subscribe to know when file has been loaded by the SheetViewModel. + /// + public event EventHandler? Loaded; - SetField(ref _loading, value); - } + protected void OnLoaded(bool l) { + Loaded?.Invoke(this, l); + } + + /// + /// Subscribe to know when the SheetViewModel is ready for preview/printing. + /// + public event EventHandler? ReadyChanged; + + protected void OnReadyChanged(bool r) { + ReadyChanged?.Invoke(this, r); + } + + /// + /// Subscribe to be notified when the Printer PageSettings have been set. + /// + public event EventHandler? PageSettingsSet; + + protected void OnPageSettingsSet() { + PageSettingsSet?.Invoke(this, null); + } + + public event EventHandler? ReflowProgress; + + protected void OnReflowProgress(string msg) { + ReflowProgress?.Invoke(this, msg); + } + + // if bool is true, reflow. Otherwise just paint + public event EventHandler? SettingsChanged; + + protected void OnSettingsChanged(bool reflow, string propertyName) { + LogService.TraceMessage(); + + SettingsChanged?.Invoke(this, + new SheetViewModelSettingsChangedEvent { Reflow = reflow, PropertyName = propertyName }); + } + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) { + base.OnPropertyChanged(propertyName); + } + + /// + /// Call this to reset the SVM before loading a new file (enables painting print preview status cleanly) + /// + public void Reset() { + Ready = false; + if (ContentEngine != null) { + ContentEngine.PropertyChanged -= OnContentEnginePropertyChanged(); + ContentEngine = null; } - private bool _loading; - - /// - /// Subscribe to know when the SheetViewModel is ready for preview/printing. - /// - public event EventHandler ReadyChanged; - protected void OnReadyChanged(bool r) { - ReadyChanged?.Invoke(this, r); + + _numPages = 0; + } + + /// + /// Call SetSheet when the Sheet has changed. + /// + /// new Sheet definition to use + public void SetSheet(SheetSettings? newSheet) { + if (newSheet == null) { + return; } - /// - /// True if we're in the middle of reflowing. False otherwise. - /// - public bool Ready { - get => _ready; - set { - if (value != _ready) { - OnReadyChanged(value); - } + LogService.TraceMessage($"{newSheet.Name}"); + // TODO: Add font info + // TODO: Add header footer details (borders etc...). + ServiceLocator.Current.TelemetryService.TrackEvent("Set Sheet Settings", newSheet.GetTelemetryDictionary()); - SetField(ref _ready, value); - } + if (newSheet is null) { + throw new ArgumentNullException(nameof(newSheet)); } - private bool _ready; - - /// - /// Subscribe to be notified when the Printer PageSettings have been set. - /// - public event EventHandler PageSettingsSet; - protected void OnPageSettingsSet() { - PageSettingsSet?.Invoke(this, null); + + Reset(); + + if (_sheet != null) { + _sheet.PropertyChanged -= OnSheetPropertyChanged(); } - public event EventHandler ReflowProgress; - protected void OnReflowProgress(string msg) { - ReflowProgress?.Invoke(this, msg); + _sheet = newSheet; + Landscape = newSheet.Landscape; + DiagnosticRulesFont = (Font)ModelLocator.Current!.Settings.DiagnosticRulesFont.Clone(); + Rows = newSheet.Rows; + Columns = newSheet.Columns; + Padding = newSheet.Padding; + PageSeparator = newSheet.PageSeparator; + Margins = (Margins)newSheet.Margins.Clone(); + + if (_contentSettings != null) { + _contentSettings.PropertyChanged -= OnContentSettingsPropertyChanged(); } - public class SheetViewModelSettingsChangedEvent { - public bool Reflow { get; set; } - public string PropertyName { get; set; } + ContentSettings = newSheet.ContentSettings; + if (ContentSettings != null) { + ContentSettings.PropertyChanged += OnContentSettingsPropertyChanged(); } - // if bool is true, reflow. Otherwise just paint - public event EventHandler SettingsChanged; - protected void OnSettingsChanged(bool reflow, string propertyName) { - LogService.TraceMessage(); - SettingsChanged?.Invoke(this, new SheetViewModelSettingsChangedEvent() { Reflow = reflow, PropertyName = propertyName }) ; + if (_headerVM != null) { + _headerVM.SettingsChanged -= OnHeaderVmOnSettingsChanged; } - public SheetViewModel() { + _headerVM = new HeaderViewModel(this, newSheet.Header); + + _headerVM.SettingsChanged += OnHeaderVmOnSettingsChanged; + if (_footerVM != null) { + _footerVM.SettingsChanged -= OnFooterVmOnSettingsChanged; } - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) { - base.OnPropertyChanged(propertyName); + _footerVM = new FooterViewModel(this, newSheet.Footer); + + _footerVM.SettingsChanged += OnFooterVmOnSettingsChanged; + // Subscribe to all settings properties + newSheet.PropertyChanged += OnSheetPropertyChanged(); + + return; + + void OnHeaderVmOnSettingsChanged(object? s, bool reflow) { + OnSettingsChanged(reflow, "Header"); } - /// - /// Call this to reset the SVM before loading a new file (enables painting print preview status cleanly) - /// - public void Reset() { - Ready = false; - if (ContentEngine != null) { - ContentEngine.PropertyChanged -= OnContentEnginePropertyChanged(); - ContentEngine = null; - } - _numPages = 0; + void OnFooterVmOnSettingsChanged(object? s, bool reflow) { + OnSettingsChanged(reflow, "Footer"); } + } - /// - /// Call SetSheet when the Sheet has changed. - /// - /// new Sheet defintiion to use - public void SetSheet(SheetSettings newSheet) { - LogService.TraceMessage($"{newSheet.Name}"); - // TODO: Add font info - // TODO: Add header footer details (borders etc...). - ServiceLocator.Current.TelemetryService.TrackEvent("Set Sheet Settings", properties: newSheet.GetTelemetryDictionary()); - - if (newSheet is null) { - throw new ArgumentNullException(nameof(newSheet)); - } + /// + /// Loads the specified file via the appropriate Content Type Engine. + /// + /// Fully qualified path to File to load. + /// If null or empty, the file extension will be used to determine content type engine. + /// True if content type engine was initialized. False otherwise. + public async Task LoadFileAsync(string filePath, string? contentType = null) { + LogService.TraceMessage($"{filePath} - {contentType}"); + File = filePath ?? ""; + + //filePath = Path.GetFullPath(filePath); + //Log.Debug("full path = {path}", filePath); + + if (string.IsNullOrEmpty(contentType)) { + // Use file extension to determine contentType + contentType = ContentTypeEngineBase.GetContentType(File); + } - Reset(); - if (_sheet != null) { - _sheet.PropertyChanged -= OnSheetPropertyChanged(); - } + // If there's no file, this sets things up with an empty file which is good for + // print preview during startup. + var document = ""; + Encoding = Encoding.UTF8; + if (string.IsNullOrEmpty(File)) { + return await LoadStringAsync(document, contentType).ConfigureAwait(true); + } + // LoadAsync will throw FNFE if file was not found. Loading will remain true in this case... - _sheet = newSheet; - Landscape = newSheet.Landscape; - DiagnosticRulesFont = (Core.Models.Font)ModelLocator.Current.Settings.DiagnosticRulesFont.Clone(); - Rows = newSheet.Rows; - Columns = newSheet.Columns; - Padding = newSheet.Padding; - PageSeparator = newSheet.PageSeparator; - Margins = (Margins)newSheet.Margins.Clone(); - - if (_contentSettings != null) { - _contentSettings.PropertyChanged -= OnContentSettingsPropertyChanged(); + using var fileStream = System.IO.File.OpenRead(File); + var detected = CharsetDetector.DetectFromStream(fileStream).Detected; + if (detected != null) { + Log.Debug("File encoding detected: {encoding}", detected); + Encoding = detected.Encoding; + } + else { + // Not detected. We know CharsetDetector gets confused on ANSI encoded files (rightfully so). + // Does this file have ESC[ sequences? + if (fileStream.Length != 0 && !StreamHasAnsiEsc(fileStream)) { + throw new InvalidOperationException( + $"This file is not supported by winprint; could not determine the file encoding of '{Path.GetFullPath(File)}'."); } - ContentSettings = newSheet.ContentSettings; - if (ContentSettings != null) { - ContentSettings.PropertyChanged += OnContentSettingsPropertyChanged(); - } + Log.Debug("File encoding NOT detected, looks ANSI; using default: {encoding}", Encoding); + } - if (_headerVM != null) { - _headerVM.SettingsChanged -= (s, reflow) => OnSettingsChanged(reflow, "Header"); - } + fileStream.Position = 0; + using var streamReader = new StreamReader(fileStream, Encoding); + document = await streamReader.ReadToEndAsync().ConfigureAwait(true); - Header = new HeaderViewModel(this, newSheet.Header); - _headerVM.SettingsChanged += (s, reflow) => OnSettingsChanged(reflow, "Header"); - if (_footerVM != null) { - _footerVM.SettingsChanged -= (s, reflow) => OnSettingsChanged(reflow, "Footer"); - } + return await LoadStringAsync(document, contentType).ConfigureAwait(true); + } - Footer = new FooterViewModel(this, newSheet.Footer); - _footerVM.SettingsChanged += (s, reflow) => OnSettingsChanged(reflow, "Footer"); + private bool StreamHasAnsiEsc(Stream stream) { + var containsStr = false; + var streamBytes = new byte[stream.Length]; + stream.Position = 0; + stream.Read(streamBytes, 0, (int)stream.Length); + var stringOfStream = Encoding.UTF8.GetString(streamBytes); + // Look for the Default foreground color esc sequence + // Note, just looking for the ESC[ sequence is not enough as PDF files + // include. + if (stringOfStream.Contains("\u001B[39;00m")) { + containsStr = true; + } + + return containsStr; + } - // Subscribe to all settings properties - newSheet.PropertyChanged += OnSheetPropertyChanged(); + /// + /// Loads the specified file via the appropriate Content Type Engine. + /// + /// Document contents to load. + /// The content type engine to use. + /// True if content type engine was initialized. False otherwise. + public async Task LoadStringAsync(string document, string? contentType) { + var retval = false; + LogService.TraceMessage(); + if (document == null) { + // TODO: Determine what could cause this and what user-friendly message would be + throw new ArgumentNullException("Document can't be null."); } - /// - /// Loads the specified file via the appropriate Content Type Engine. - /// - /// Fully qualified path to File to load. - /// If null or empty, the file extension will be used to determine content type engine. - /// True if content type engine was initialized. False otherwise. - public async Task LoadFileAsync(string filePath, string contentType = null) { - LogService.TraceMessage($"{filePath} - {contentType}"); - File = filePath ?? ""; - - //filePath = Path.GetFullPath(filePath); - //Log.Debug("full path = {path}", filePath); - - if (string.IsNullOrEmpty(contentType)) { - // Use file extension to determine contentType - contentType = ContentTypeEngineBase.GetContentType(File); - } + Reset(); + Loading = true; + try { + (ContentEngine, ContentType, Language) = ContentTypeEngineBase.CreateContentTypeEngine(contentType); - // If there's no file, this sets things up with an empty file which is good for - // print preview during startup. - var document = ""; - Encoding = Encoding.UTF8; - if (!string.IsNullOrEmpty(File)) { - // LoadAsync will throw FNFE if file was not found. Loading will remain true in this case... + // Content settings in Sheet take precidence over Engine + if (ContentEngine.ContentSettings is null) { + ContentEngine.ContentSettings = new ContentSettings(); + // TODO: set some defaults + } - using var fileStream = System.IO.File.OpenRead(File); - var detected = CharsetDetector.DetectFromStream(fileStream).Detected; - if (detected != null) { - Log.Debug("File encoding detected: {encoding}", detected); - Encoding = detected.Encoding; + if (ContentSettings != null) { + ContentEngine.ContentSettings.CopyPropertiesFrom(ContentSettings); + } + + if (ContentEngine.SupportedContentTypes.Contains("text/ansi") && + !ContentEngine.SupportedContentTypes.Contains(ContentType)) { + var (pygmentsInstalled, message) = ServiceLocator.Current.PygmentsConverterService.CheckInstall(); + if (pygmentsInstalled) { + // Convert the document to ANSI using Pygments + // TODO: Spin up a thread + Log.Information("Applying source code formatting."); + document = await ServiceLocator.Current.PygmentsConverterService + .ConvertAsync(document, ContentEngine.ContentSettings.Style, Language).ConfigureAwait(true); } - else - { - // Not detected. We know CharsetDetector gets confused on ANSI encoded files (rightfully so). - // Does this file have ESC[ sequences? - if (fileStream.Length != 0 && !StreamHasAnsiEsc(fileStream)) { - throw new InvalidOperationException($"This file is not supported by winprint; could not determine the file encoding of '{Path.GetFullPath(File)}'."); - } - Log.Debug("File encoding NOT detected, looks ANSI; using default: {encoding}", Encoding); + else { + Log.Information("Treating file as plain text."); } - - fileStream.Position = 0; - using var streamReader = new StreamReader(fileStream, Encoding); - document = await streamReader.ReadToEndAsync().ConfigureAwait(false); } - return await LoadStringAsync(document, contentType).ConfigureAwait(false); + + ContentEngine.Encoding = Encoding; + retval = await ContentEngine.SetDocumentAsync(document).ConfigureAwait(true); + } + catch (Exception e) { + Loading = false; + throw e; + } + finally { + // Set this last to notify loading is done with File valid + Loading = false; } - private bool StreamHasAnsiEsc(Stream stream) { - bool containsStr = false; - byte[] streamBytes = new byte[stream.Length]; - stream.Position = 0; - stream.Read(streamBytes, 0, (int)stream.Length); - string stringOfStream = Encoding.UTF8.GetString(streamBytes); - // Look for the Default foreground color esc sequence - // Note, just looking for the ESC[ sequence is not enough as PDF files - // include. - if (stringOfStream.Contains("\u001B[39;00m")) { - containsStr = true; - } - return containsStr; + return retval; + } + + /// + /// Set the page setting from a PageSettings instance. Note that accessing + /// PageSettings can be expensive so we cache the values instead of just holding + /// a PageSettings instance. + /// + /// + /// + public void SetPrinterPageSettings(PageSettings pageSettings) { + LogService.TraceMessage(); + if (pageSettings is null) { + throw new ArgumentNullException(nameof(pageSettings)); } - /// - /// Loads the specified file via the appropriate Content Type Engine. - /// - /// Document contents to load. - /// The content type engine to use. - /// True if content type engine was initialized. False otherwise. - public async Task LoadStringAsync(string document, string contentType) { - bool retval = false; - LogService.TraceMessage(); - if (document == null) { - // TODO: Determine what could cause this and what user-friendly message would be - throw new ArgumentNullException("Document can't be null."); - } + // On Linux, PageSettings.Bounds is determined from PageSettings.Margins. + // On Windows, it has no effect. Regardelss, here we set Bounds to 0 to work around this. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + pageSettings.Margins = new Margins(0, 0, 0, 0); + } - Reset(); - Loading = true; + // The following elements of PageSettings are dependent + // Landscape + // LandscapeAngle (Landscape) + // PrintableArea (Landscape) + // PaperSize (Landscape) + // HardMarginX, HardMarginY (Landscape, LandscapeAngle) + + LandscapeAngle = pageSettings.PrinterSettings.LandscapeAngle; + + // 0 degrees + // Top + // Left Right + // Bottom + // + // 90 degrees + // Left + // Bottom Top + // Right + // + // 270 degress + // Right + // Top Bottom + // Left + // The PageSettings class accesses print APIs and thus is slow + // Cache settings. + if (_sheet != null && _sheet.Landscape) { + // Translate page settings for landscape mode + // HardMarginX/Y should NOT be used for anything - use printableArea instead + HardMarginX = pageSettings.HardMarginY; + HardMarginY = pageSettings.HardMarginX; + + _printableArea.X = pageSettings.PrintableArea.Y; + _printableArea.Y = pageSettings.PrintableArea.X; + + _printableArea.Width = pageSettings.PrintableArea.Height; + _printableArea.Height = pageSettings.PrintableArea.Width; + + _paperSize.Height = pageSettings.PaperSize.Width; + _paperSize.Width = pageSettings.PaperSize.Height; + } + else { + // HardMarginX/Y should NOT be used for anything - use printableArea instead + HardMarginX = pageSettings.HardMarginX; + HardMarginY = pageSettings.HardMarginY; + + _printableArea.X = pageSettings.PrintableArea.X; + _printableArea.Y = pageSettings.PrintableArea.Y; + _printableArea.Width = pageSettings.PrintableArea.Width; + _printableArea.Height = pageSettings.PrintableArea.Height; + + _paperSize.Width = pageSettings.PaperSize.Width; + _paperSize.Height = pageSettings.PaperSize.Height; + } - try { - (ContentEngine, ContentType, Language) = ContentTypeEngineBase.CreateContentTypeEngine(contentType); + PrinterResolution = pageSettings.PrinterResolution; - // Content settings in Sheet take precidence over Engine - if (ContentEngine.ContentSettings is null) { - ContentEngine.ContentSettings = new ContentSettings(); - // TODO: set some defaults - } + // TODO: Do something if printer is set to color or bw? + PrintInColor = pageSettings.Color; - if (ContentSettings != null) { - ContentEngine.ContentSettings.CopyPropertiesFrom(ContentSettings); - } + // Bounds represents page size area, auto adjusted for landscape + Bounds = pageSettings.Bounds; - if (ContentEngine.SupportedContentTypes.Contains("text/ansi") && !ContentEngine.SupportedContentTypes.Contains(ContentType)) { - // Syntax highlight - // TODO: Spin up a thread - document = await ServiceLocator.Current.PygmentsConverterService.ConvertAsync(document, ContentEngine.ContentSettings.Style, Language).ConfigureAwait(true); - } + // PrintableArea is Bounds minus HardMargins, but more accurate. + // HardMarginX/Y should NOT be used for anything. + // + // BUGBUG: On Linux, PageSettings.PrintableArea is all 0s. + // BUGBUG: On Linux, not seeing HardMargins > 0 ever (e.g. HP laser should have 0.16". No idea how to fix this. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + _printableArea.X = Bounds.X - HardMarginX; + _printableArea.Y = Bounds.Y - HardMarginY; + _printableArea.Width = Bounds.Width - (HardMarginX * 2); + _printableArea.Height = Bounds.Height - (HardMarginY * 2); + } - ContentEngine.Encoding = Encoding; - retval = await ContentEngine.SetDocumentAsync(document).ConfigureAwait(false); - } - catch (Exception e){ - Loading = false; - throw e; - } - finally { - // Set this last to notify loading is done with File valid - Loading = false; + // Content bounds represents printable area, minus margins and header/footer. + _contentBounds.Location = new PointF(_sheet.Margins.Left, _sheet.Margins.Top + _headerVM.Bounds.Height); + _contentBounds.Width = Bounds.Width - _sheet.Margins.Left - _sheet.Margins.Right; + _contentBounds.Height = Bounds.Height - _sheet.Margins.Top - _sheet.Margins.Bottom - _headerVM.Bounds.Height - + _footerVM.Bounds.Height; + if (ContentEngine is null) { + LogService.TraceMessage("SheetViewModel.ReflowAsync - Content is null"); + } + else { + // TODO: Figure out a better way for the content engine to get page size. + ContentEngine.PageSize = new SizeF(GetPageWidth(), GetPageHeight()); + } - } - return retval; + Log.Debug("Printer Resolution: {w} x {h}DPI", PrinterResolution.X, PrinterResolution.Y); + Log.Debug("Paper Size: {w} x {h}\"", PaperSize.Width / 100F, PaperSize.Height / 100F); + Log.Debug("Hard Margins: {w} x {h}\"", HardMarginX / 100F, HardMarginY / 100F); + Log.Debug("Printable Area: {left}\", {top}\", {right}\", {bottom}\" ({w} x {h}\")", + _printableArea.Left / 100F, _printableArea.Top / 100F, _printableArea.Right / 100, + _printableArea.Bottom / 100, _printableArea.Width / 100, _printableArea.Height / 100); + Log.Debug("Bounds: {left}\", {top}\", {right}\", {bottom}\" ({w}x{h}\")", + Bounds.Left / 100F, Bounds.Top / 100F, Bounds.Right / 100F, Bounds.Bottom / 100F, Bounds.Width / 100F, + Bounds.Height / 100F); + Log.Debug("Content Bounds: {left}\", {top}\", {right}\", {bottom}\" ({w} x {h}\")", + ContentBounds.Left / 100F, ContentBounds.Top / 100F, ContentBounds.Right / 100F, + ContentBounds.Bottom / 100F, ContentBounds.Width / 100F, ContentBounds.Height / 100F); + Log.Debug("Page Size: {w} x {h}\"", GetPageWidth() / 100F, GetPageHeight() / 100F); + + OnPageSettingsSet(); + } + + /// + /// Reflows the sheet based on page settings from a PageSettings instance. Caches those settings + /// for performance (and for platform independence). + /// + /// + public async Task ReflowAsync() { + LogService.TraceMessage(); + if (Loading) { + Log.Debug("SheetViewModel.ReflowAsync - Still loading; can't reflow, returning"); + return; } - /// - /// Set the page setting from a PageSettings instance. Note that accessing - /// PageSettings can be expensive so we cache the values instead of just holding - /// a PageSettings instance. - /// - /// - /// - public void SetPrinterPageSettings(PageSettings pageSettings) { - LogService.TraceMessage(); - if (pageSettings is null) { - throw new ArgumentNullException(nameof(pageSettings)); - } + Ready = false; - // On Linux, PageSettings.Bounds is determined from PageSettings.Margins. - // On Windows, it has no effect. Regardelss, here we set Bounds to 0 to work around this. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - pageSettings.Margins = new Margins(0, 0, 0, 0); - } + if (ContentEngine is null) { + LogService.TraceMessage("SheetViewModel.ReflowAsync - ContentEngine is null"); + return; + } - // The following elements of PageSettings are dependent - // Landscape - // LandscapeAngle (Landscape) - // PrintableArea (Landscape) - // PaperSize (Landscape) - // HardMarginX, HardMarginY (Landscape, LandscapeAngle) - - LandscapeAngle = pageSettings.PrinterSettings.LandscapeAngle; - - // 0 degrees - // Top - // Left Right - // Bottom - // - // 90 degrees - // Left - // Bottom Top - // Right - // - // 270 degress - // Right - // Top Bottom - // Left - // The PageSettings class accesses print APIs and thus is slow - // Cache settings. - if (_sheet != null && _sheet.Landscape) { - // Translate page settings for landscape mode - // HardMarginX/Y should NOT be used for anything - use printableArea instead - HardMarginX = pageSettings.HardMarginY; - HardMarginY = pageSettings.HardMarginX; - - _printableArea.X = pageSettings.PrintableArea.Y; - _printableArea.Y = pageSettings.PrintableArea.X; - - _printableArea.Width = pageSettings.PrintableArea.Height; - _printableArea.Height = pageSettings.PrintableArea.Width; - - _paperSize.Height = pageSettings.PaperSize.Width; - _paperSize.Width = pageSettings.PaperSize.Height; - } - else { - // HardMarginX/Y should NOT be used for anything - use printableArea instead - HardMarginX = pageSettings.HardMarginX; - HardMarginY = pageSettings.HardMarginY; + _numPages = await ContentEngine.RenderAsync(PrinterResolution, ReflowProgress).ConfigureAwait(false); - _printableArea.X = pageSettings.PrintableArea.X; - _printableArea.Y = pageSettings.PrintableArea.Y; - _printableArea.Width = pageSettings.PrintableArea.Width; - _printableArea.Height = pageSettings.PrintableArea.Height; + CheckPrintOutsideHardMargins(); + Log.Debug("SheetView Model is ready. {n} pages {w}x{h}\"", _numPages, Bounds.Width / 100F, + Bounds.Height / 100F); + Ready = true; + } - _paperSize.Width = pageSettings.PaperSize.Width; - _paperSize.Height = pageSettings.PaperSize.Height; - } - PrinterResolution = pageSettings.PrinterResolution; - - // TODO: Do something if printer is set to color or bw? - PrintInColor = pageSettings.Color; - - // Bounds represents page size area, auto adjusted for landscape - Bounds = pageSettings.Bounds; - - // PrintableArea is Bounds minus HardMargins, but more accurate. - // HardMarginX/Y should NOT be used for anything. - // - // BUGBUG: On Linux, PageSettings.PrintableArea is all 0s. - // BUGBUG: On Linux, not seeing HardMargins > 0 ever (e.g. HP laser should have 0.16". No idea how to fix this. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - _printableArea.X = Bounds.X - HardMarginX; - _printableArea.Y = Bounds.Y - HardMarginY; - _printableArea.Width = Bounds.Width - (HardMarginX * 2); - _printableArea.Height = Bounds.Height - (HardMarginY * 2); - } + public bool CheckPrintOutsideHardMargins() { + var leftMax = (int)Math.Round(_printableArea.X); + var topMax = (int)Math.Round(_printableArea.Top); + var rightMax = (int)Math.Round(_bounds.Width - _printableArea.Right); + var bottomMax = (int)Math.Round(_bounds.Height - _printableArea.Bottom); - // Content bounds represents printable area, minus margins and header/footer. - _contentBounds.Location = new PointF(_sheet.Margins.Left, _sheet.Margins.Top + _headerVM.Bounds.Height); - _contentBounds.Width = Bounds.Width - _sheet.Margins.Left - _sheet.Margins.Right; - _contentBounds.Height = Bounds.Height - _sheet.Margins.Top - _sheet.Margins.Bottom - _headerVM.Bounds.Height - _footerVM.Bounds.Height; - if (ContentEngine is null) { - LogService.TraceMessage("SheetViewModel.ReflowAsync - Content is null"); - } - else { - // TODO: Figure out a better way for the content engine to get page size. - ContentEngine.PageSize = new SizeF(GetPageWidth(), GetPageHeight()); - } + if (Margins.Left < leftMax || Margins.Top < topMax || Margins.Right < rightMax || Margins.Bottom < bottomMax) { + Log.Warning( + $"Margins are set outside of printable area - Maximum values: Left: {leftMax / 100F}\", Right: {rightMax / 100F}\", Top: {topMax / 100F}\", Bottom: {bottomMax / 100F}\""); + return false; + } + + return true; + } - Log.Debug("Printer Resolution: {w} x {h}DPI", PrinterResolution.X, PrinterResolution.Y); - Log.Debug("Paper Size: {w} x {h}\"", PaperSize.Width / 100F, PaperSize.Height / 100F); - Log.Debug("Hard Margins: {w} x {h}\"", HardMarginX / 100F, HardMarginY / 100F); - Log.Debug("Printable Area: {left}\", {top}\", {right}\", {bottom}\" ({w} x {h}\")", - _printableArea.Left / 100F, _printableArea.Top / 100F, _printableArea.Right / 100, _printableArea.Bottom / 100, _printableArea.Width / 100, _printableArea.Height / 100); - Log.Debug("Bounds: {left}\", {top}\", {right}\", {bottom}\" ({w}x{h}\")", - Bounds.Left / 100F, Bounds.Top / 100F, Bounds.Right / 100F, Bounds.Bottom / 100F, Bounds.Width / 100F, Bounds.Height / 100F); - Log.Debug("Content Bounds: {left}\", {top}\", {right}\", {bottom}\" ({w} x {h}\")", - ContentBounds.Left / 100F, ContentBounds.Top / 100F, ContentBounds.Right / 100F, ContentBounds.Bottom / 100F, ContentBounds.Width / 100F, ContentBounds.Height / 100F); - Log.Debug("Page Size: {w} x {h}\"", GetPageWidth() / 100F, GetPageHeight() / 100F); - - OnPageSettingsSet(); + public SheetSettings FindSheet(string sheetName, out string sheetID) { + SheetSettings sheet = null; + if (ModelLocator.Current.Settings == null) { + throw new InvalidOperationException("Find Sheet failed. Settings are invalid."); } - /// - /// Reflows the sheet based on page settings from a PageSettings instance. Caches those settings - /// for performance (and for platform independence). - /// - /// - public async Task ReflowAsync() { - LogService.TraceMessage(); - if (Loading) { - Log.Debug($"SheetViewModel.ReflowAsync - Still loading; can't reflow, returning"); - return; + sheetID = ModelLocator.Current.Settings.DefaultSheet.ToString(); + if (!string.IsNullOrEmpty(sheetName) && + !sheetName.Equals("default", StringComparison.InvariantCultureIgnoreCase)) { + if (!ModelLocator.Current.Settings.Sheets.TryGetValue(sheetName, out sheet)) { + // Wasn't a GUID or isn't valid + var s = ModelLocator.Current.Settings.Sheets + .Where(s => s.Value.Name.Equals(sheetName, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); + + if (s.Value is null) { + throw new InvalidOperationException($"Sheet definiton not found ({sheetName})."); + } + + sheetID = s.Key; + sheet = s.Value; } + } + else { + sheet = ModelLocator.Current.Settings.Sheets.GetValueOrDefault(sheetID); + } - Ready = false; + return sheet; + } - if (ContentEngine is null) { - LogService.TraceMessage("SheetViewModel.ReflowAsync - ContentEngine is null"); - return; + private PropertyChangedEventHandler OnSheetPropertyChanged() { + return (s, e) => { + var reflow = false; + LogService.TraceMessage($"sheet.PropertyChanged: {e.PropertyName}"); + switch (e.PropertyName) { + case "Landscape": + Landscape = _sheet.Landscape; + reflow = true; + break; + + case "Margins": + Margins = _sheet.Margins; + reflow = true; + break; + + case "DiagnosticRulesFont": + DiagnosticRulesFont = ModelLocator.Current.Settings.DiagnosticRulesFont; + break; + + case "Rows": + Rows = _sheet.Rows; + reflow = true; + break; + + case "Columns": + Columns = _sheet.Columns; + reflow = true; + break; + + case "Padding": + Padding = _sheet.Padding; + reflow = true; + break; + + case "PageSeparator": + PageSeparator = _sheet.PageSeparator; + break; + + default: + throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); } - _numPages = await ContentEngine.RenderAsync(PrinterResolution, ReflowProgress).ConfigureAwait(false); + OnSettingsChanged(reflow, e.PropertyName); + }; + } - CheckPrintOutsideHardMargins(); - Log.Debug("SheetView Model is ready. {n} pages {w}x{h}\"", _numPages, Bounds.Width / 100F, Bounds.Height / 100F); - Ready = true; - } + private PropertyChangedEventHandler OnContentSettingsPropertyChanged() { + return (s, e) => { + var reflow = false; + LogService.TraceMessage($"{e.PropertyName}"); + switch (e.PropertyName) { + case "Font": + ContentSettings.Font = _sheet.ContentSettings.Font; + reflow = true; + break; + + case "PrintBackground": + ContentSettings.PrintBackground = _sheet.ContentSettings.PrintBackground; + reflow = false; + break; + + case "Grayscale": + ContentSettings.Grayscale = _sheet.ContentSettings.Grayscale; + reflow = false; + break; + + case "Darkness": + ContentSettings.Darkness = _sheet.ContentSettings.Darkness; + reflow = false; + break; + + case "LineNumbers": + ContentSettings.LineNumbers = _sheet.ContentSettings.LineNumbers; + reflow = true; + break; + + case "LineNumberSeparator": + ContentSettings.LineNumberSeparator = _sheet.ContentSettings.LineNumberSeparator; + reflow = true; + break; + + case "TabSpaces": + ContentSettings.TabSpaces = _sheet.ContentSettings.TabSpaces; + reflow = true; + break; + + case "NewPageOnFormFeed": + ContentSettings.NewPageOnFormFeed = _sheet.ContentSettings.NewPageOnFormFeed; + reflow = true; + break; + + case "Diagnostics": + ContentSettings.Diagnostics = _sheet.ContentSettings.Diagnostics; + reflow = true; + break; + + case "Style": + ContentSettings.Style = _sheet.ContentSettings.Style; + reflow = true; + break; + + case "DisableFontStyles": + ContentSettings.DisableFontStyles = _sheet.ContentSettings.DisableFontStyles; + reflow = true; + break; + + default: + throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); + } - public bool CheckPrintOutsideHardMargins() { - var leftMax = (int)Math.Round(_printableArea.X); - var topMax = (int)Math.Round(_printableArea.Top); - var rightMax = (int)Math.Round(_bounds.Width - _printableArea.Right); - var bottomMax = (int)Math.Round(_bounds.Height - _printableArea.Bottom); + OnSettingsChanged(reflow, e.PropertyName); + }; + } - if (Margins.Left < leftMax || Margins.Top < topMax || Margins.Right < rightMax || Margins.Bottom < bottomMax) { - Log.Warning($"Margins are set outside of printable area - Maximum values: Left: {leftMax / 100F}\", Right: {rightMax / 100F}\", Top: {topMax / 100F}\", Bottom: {bottomMax / 100F}\""); - return false; + private PropertyChangedEventHandler OnContentEnginePropertyChanged() { + return (s, e) => { + var reflow = false; + LogService.TraceMessage($"SheetViewModel.PropertyChanged: {e.PropertyName}"); + switch (e.PropertyName) { + case "TabSpaces": + reflow = true; + break; + + case "NewPageOnFormFeed": + reflow = true; + break; + + case "ContentSettings": + reflow = true; + break; + + default: + throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); } - return true; - } - public SheetSettings FindSheet(string sheetName, out string sheetID) { - SheetSettings sheet = null; - if (ModelLocator.Current.Settings == null) { - throw new InvalidOperationException($"Find Sheet failed. Settings are invalid."); + if (e.PropertyName == "NumPages") { + return; } - sheetID = ModelLocator.Current.Settings.DefaultSheet.ToString(); - if (!string.IsNullOrEmpty(sheetName) && !sheetName.Equals("default", StringComparison.InvariantCultureIgnoreCase)) { - if (!ModelLocator.Current.Settings.Sheets.TryGetValue(sheetName, out sheet)) { - // Wasn't a GUID or isn't valid - var s = ModelLocator.Current.Settings.Sheets - .Where(s => s.Value.Name.Equals(sheetName, StringComparison.InvariantCultureIgnoreCase)) - .FirstOrDefault(); + OnSettingsChanged(reflow, e.PropertyName); + }; + } - if (s.Value is null) { - throw new InvalidOperationException($"Sheet definiton not found ({sheetName})."); - } + public static float GetFontHeight(Font? font) { + //if (font is null) throw new ArgumentNullException(nameof(font)); - sheetID = s.Key; - sheet = s.Value; - } + //Log.Debug(LogService.GetTraceMsg(), $"{font.Family}, {font.Size}, {font.Style}"); + System.Drawing.Font f = null; + float h = 0; + try { + if (font != null) { + f = new System.Drawing.Font(font.Family, font.Size, font.Style, GraphicsUnit.Point); } else { - sheet = ModelLocator.Current.Settings.Sheets.GetValueOrDefault(sheetID); + f = SystemFonts.DefaultFont; } - return sheet; + h = f.GetHeight(100); } - - private System.ComponentModel.PropertyChangedEventHandler OnSheetPropertyChanged() { - return (s, e) => { - var reflow = false; - LogService.TraceMessage($"sheet.PropertyChanged: {e.PropertyName}"); - switch (e.PropertyName) { - case "Landscape": - Landscape = _sheet.Landscape; - reflow = true; - break; - - case "Margins": - Margins = _sheet.Margins; - reflow = true; - break; - - case "DiagnosticRulesFont": - DiagnosticRulesFont = ModelLocator.Current.Settings.DiagnosticRulesFont; - break; - - case "Rows": - Rows = _sheet.Rows; - reflow = true; - break; - - case "Columns": - Columns = _sheet.Columns; - reflow = true; - break; - - case "Padding": - Padding = _sheet.Padding; - reflow = true; - break; - - case "PageSeparator": - PageSeparator = _sheet.PageSeparator; - break; - - default: - throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); - } - OnSettingsChanged(reflow, e.PropertyName); - }; + catch (Exception e) { + // TODO: We shouldn't keep this exception here + Log.Error(e, "Failed to create font. {msg} ({font})", e.Message, + $"{font.Family}, {font.Size}, {font.Style}"); } - - private System.ComponentModel.PropertyChangedEventHandler OnContentSettingsPropertyChanged() { - return (s, e) => { - var reflow = false; - LogService.TraceMessage($"{e.PropertyName}"); - switch (e.PropertyName) { - case "Font": - ContentSettings.Font = _sheet.ContentSettings.Font; - reflow = true; - break; - - case "PrintBackground": - ContentSettings.PrintBackground = _sheet.ContentSettings.PrintBackground; - reflow = false; - break; - - case "Grayscale": - ContentSettings.Grayscale = _sheet.ContentSettings.Grayscale; - reflow = false; - break; - - case "Darkness": - ContentSettings.Darkness = _sheet.ContentSettings.Darkness; - reflow = false; - break; - - case "LineNumbers": - ContentSettings.LineNumbers = _sheet.ContentSettings.LineNumbers; - reflow = true; - break; - - case "LineNumberSeparator": - ContentSettings.LineNumberSeparator = _sheet.ContentSettings.LineNumberSeparator; - reflow = true; - break; - - case "TabSpaces": - ContentSettings.TabSpaces = _sheet.ContentSettings.TabSpaces; - reflow = true; - break; - - case "NewPageOnFormFeed": - ContentSettings.NewPageOnFormFeed = _sheet.ContentSettings.NewPageOnFormFeed; - reflow = true; - break; - - case "Diagnostics": - ContentSettings.Diagnostics = _sheet.ContentSettings.Diagnostics; - reflow = true; - break; - - case "Style": - ContentSettings.Style = _sheet.ContentSettings.Style; - reflow = true; - break; - - case "DisableFontStyles": - ContentSettings.DisableFontStyles = _sheet.ContentSettings.DisableFontStyles; - reflow = true; - break; - - default: - throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); - } - OnSettingsChanged(reflow, e.PropertyName); - }; + finally { + f?.Dispose(); } - private System.ComponentModel.PropertyChangedEventHandler OnContentEnginePropertyChanged() { - return (s, e) => { - var reflow = false; - LogService.TraceMessage($"SheetViewModel.PropertyChanged: {e.PropertyName}"); - switch (e.PropertyName) { - case "TabSpaces": - reflow = true; - break; - - case "NewPageOnFormFeed": - reflow = true; - break; - - case "ContentSettings": - reflow = true; - break; - - default: - throw new InvalidOperationException($"Property change not handled: {e.PropertyName}"); - } - if (e.PropertyName == "NumPages") { - return; - } - - OnSettingsChanged(reflow, e.PropertyName); - }; - } + return h; + } - public static float GetFontHeight(Core.Models.Font font) { - //if (font is null) throw new ArgumentNullException(nameof(font)); + public int GetPageColumn(int n) { return (n - 1) % Columns; } + public int GetPageRow(int n) { return (n - 1) % (Rows * Columns) / Columns; } - //Log.Debug(LogService.GetTraceMsg(), $"{font.Family}, {font.Size}, {font.Style}"); - System.Drawing.Font f = null; - float h = 0; - try { - if (font != null) { - f = new System.Drawing.Font(font.Family, font.Size, font.Style, GraphicsUnit.Point); - } - else { - f = System.Drawing.SystemFonts.DefaultFont; - } + internal float GetXPadding(int n) { return GetPageColumn(n) == 0 ? 0F : _padding / Columns; } + internal float GetYPadding(int n) { return GetPageRow(n) == 0 ? 0F : _padding / Rows; } - h = f.GetHeight(100); - } - catch (Exception e) { - // TODO: We shouldn't keep this exception here - Log.Error(e, "Failed to create font. {msg} ({font})", e.Message, $"{font.Family}, {font.Size}, {font.Style}"); - } - finally { - f?.Dispose(); - } - return h; - } + public float GetPageX(int n) { + //Log.Debug(LogService.GetTraceMsg("{n}. {p}"), n, Padding); - public int GetPageColumn(int n) { return (n - 1) % Columns; } - public int GetPageRow(int n) { return ((n - 1) % (Rows * Columns)) / Columns; } + var f = ContentBounds.Left + (GetPageWidth() * GetPageColumn(n)); + f += Padding * GetPageColumn(n); + return f; + } - internal float GetXPadding(int n) { return GetPageColumn(n) == 0 ? 0F : (_padding / (Columns)); } - internal float GetYPadding(int n) { return GetPageRow(n) == 0 ? 0F : (_padding / (Rows)); } + public float GetPageY(int n) { + //Log.Debug(LogService.GetTraceMsg("{n}. {p}"), n, Padding); - public float GetPageX(int n) { - //Log.Debug(LogService.GetTraceMsg("{n}. {p}"), n, Padding); + var f = ContentBounds.Top + (GetPageHeight() * GetPageRow(n)); + f += Padding * GetPageRow(n); + return f; + } - var f = ContentBounds.Left + (GetPageWidth() * GetPageColumn(n)); - f += Padding * GetPageColumn(n); - return f; - } - public float GetPageY(int n) { - //Log.Debug(LogService.GetTraceMsg("{n}. {p}"), n, Padding); + // If Columns == 1 there's no padding. But if Columns > 1 padding applies. Width is width - (padding/columns-1) (10/2 = 5) + public float GetPageWidth() { return (ContentBounds.Width / Columns) - (Padding * (Columns - 1) / Columns); } + public float GetPageHeight() { return (ContentBounds.Height / Rows) - (Padding * (Rows - 1) / Rows); } - var f = ContentBounds.Top + (GetPageHeight() * GetPageRow(n)); - f += Padding * GetPageRow(n); - return f; + /// + /// Prints the content of a single Sheet to a Graphics. + /// + /// Graphics to print on + /// Sheet to print. 1-based. + public void PrintSheet(Graphics graphics, int sheetNum) { + var state = graphics.Save(); + //Log.Debug(LogService.GetTraceMsg("{n} PageUnit: {pu}"), sheetNum, graphics.PageUnit); + if (graphics.PageUnit == GraphicsUnit.Display) { + // In print mode, adjust origin to account for hard margins + // In print mode, 0,0 is top, left - hard margins + graphics.TranslateTransform(-_printableArea.Left, -_printableArea.Top); + PaintSheet(graphics, sheetNum); } - - // If Columns == 1 there's no padding. But if Columns > 1 padding applies. Width is width - (padding/columns-1) (10/2 = 5) - public float GetPageWidth() { return (ContentBounds.Width / Columns) - (Padding * (Columns - 1) / Columns); } - public float GetPageHeight() { return (ContentBounds.Height / Rows) - (Padding * (Rows - 1) / Rows); } - - /// - /// Prints the content of a single Sheet to a Graphics. - /// - /// Graphics to print on - /// Sheet to print. 1-based. - public void PrintSheet(Graphics graphics, int sheetNum) { - var state = graphics.Save(); - //Log.Debug(LogService.GetTraceMsg("{n} PageUnit: {pu}"), sheetNum, graphics.PageUnit); - if (graphics.PageUnit == GraphicsUnit.Display) { - // In print mode, adjust origin to account for hard margins - // In print mode, 0,0 is top, left - hard margins - graphics.TranslateTransform(-_printableArea.Left, -_printableArea.Top); - PaintSheet(graphics, sheetNum); - } - else { - PaintSheet(graphics, sheetNum); - } - graphics.Restore(state); + else { + PaintSheet(graphics, sheetNum); } - private void PaintSheet(Graphics g, int sheetNum) { - LogService.TraceMessage($"{sheetNum}"); - // background needs to be filled image scaling to work right - //g.FillRectangle(Brushes.White, Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height); + graphics.Restore(state); + } - _headerVM.Paint(g, sheetNum); - _footerVM.Paint(g, sheetNum); + private void PaintSheet(Graphics g, int sheetNum) { + LogService.TraceMessage($"{sheetNum}"); + // background needs to be filled image scaling to work right + //g.FillRectangle(Brushes.White, Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height); - if (Loading) { - Log.Debug($"SheetViewModel.PaintSheet - Loading; can't paint"); - return; - } + _headerVM.Paint(g, sheetNum); + _footerVM.Paint(g, sheetNum); - if (!Ready) { - Log.Debug($"SheetViewModel.PaintSheet - Not Ready; can't paint"); - return; - } + if (Loading) { + Log.Debug("SheetViewModel.PaintSheet - Loading; can't paint"); + return; + } - var pagesPerSheet = _rows * _cols; - // 1-based; assume 4-up... - var startPage = (sheetNum - 1) * pagesPerSheet + 1; - var endPage = startPage + pagesPerSheet - 1; + if (!Ready) { + Log.Debug("SheetViewModel.PaintSheet - Not Ready; can't paint"); + return; + } - for (var pageOnSheet = startPage; pageOnSheet <= endPage; pageOnSheet++) { - var xPos = GetPageX(pageOnSheet); - var yPos = GetPageY(pageOnSheet); - var w = GetPageWidth(); - var h = GetPageHeight(); + var pagesPerSheet = _rows * _cols; + // 1-based; assume 4-up... + var startPage = ((sheetNum - 1) * pagesPerSheet) + 1; + var endPage = startPage + pagesPerSheet - 1; - // Move origin to page's x & y - g.TranslateTransform(xPos, yPos); + for (var pageOnSheet = startPage; pageOnSheet <= endPage; pageOnSheet++) { + var xPos = GetPageX(pageOnSheet); + var yPos = GetPageY(pageOnSheet); + var w = GetPageWidth(); + var h = GetPageHeight(); - if (ModelLocator.Current.Settings.PrintPageBounds || ModelLocator.Current.Settings.PreviewPageBounds) { - PaintPageNum(g, pageOnSheet); - } + // Move origin to page's x & y + g.TranslateTransform(xPos, yPos); - if (_pageSeparator) { - // If there will be a page to the left of this page, draw vert separator - if (Columns > 1 && GetPageColumn(pageOnSheet) < (Columns - 1)) { - g.DrawLine(Pens.Black, w + (Padding / 2), Padding / 2, w + (Padding / 2), h - Padding); - } + if (ModelLocator.Current.Settings.PrintPageBounds || ModelLocator.Current.Settings.PreviewPageBounds) { + PaintPageNum(g, pageOnSheet); + } - // If there will be a page below this one, draw a horz separator - if (Rows > 1 && GetPageRow(pageOnSheet) < (Rows - 1)) { - g.DrawLine(Pens.Black, Padding / 2, h + (Padding / 2), w - Padding, h + (Padding / 2)); - } + if (_pageSeparator) { + // If there will be a page to the left of this page, draw vert separator + if (Columns > 1 && GetPageColumn(pageOnSheet) < Columns - 1) { + g.DrawLine(Pens.Black, w + (Padding / 2), Padding / 2, w + (Padding / 2), h - Padding); } - if (ContentEngine != null) { - ContentEngine.PaintPage(g, pageOnSheet); + // If there will be a page below this one, draw a horz separator + if (Rows > 1 && GetPageRow(pageOnSheet) < Rows - 1) { + g.DrawLine(Pens.Black, Padding / 2, h + (Padding / 2), w - Padding, h + (Padding / 2)); } + } - // Translate back - g.TranslateTransform(-xPos, -yPos); + if (ContentEngine != null) { + ContentEngine.PaintPage(g, pageOnSheet); } - // If margins are too big, warn by printing a red border - if (g.PageUnit != GraphicsUnit.Display) { - using var errorPen = new Pen(Color.Gray) { - DashStyle = DashStyle.Dash, - Width = 4 - }; + // Translate back + g.TranslateTransform(-xPos, -yPos); + } - var leftMax = (int)Math.Round(_printableArea.X); - var topMax = (int)Math.Round(_printableArea.Top); - var rightMax = (int)Math.Round(_bounds.Width - _printableArea.Right); - var bottomMax = (int)Math.Round(_bounds.Height - _printableArea.Bottom); + // If margins are too big, warn by printing a red border + if (g.PageUnit != GraphicsUnit.Display) { + using var errorPen = new Pen(Color.Gray) { DashStyle = DashStyle.Dash, Width = 4 }; - if (Margins.Left < leftMax) { - g.DrawLine(errorPen, _printableArea.X, 0, _printableArea.X, _bounds.Height); - } + var leftMax = (int)Math.Round(_printableArea.X); + var topMax = (int)Math.Round(_printableArea.Top); + var rightMax = (int)Math.Round(_bounds.Width - _printableArea.Right); + var bottomMax = (int)Math.Round(_bounds.Height - _printableArea.Bottom); - if (Margins.Top < topMax) { - g.DrawLine(errorPen, 0, _printableArea.Top, _bounds.Width, _printableArea.Top); - } + if (Margins.Left < leftMax) { + g.DrawLine(errorPen, _printableArea.X, 0, _printableArea.X, _bounds.Height); + } - if (Margins.Right < rightMax) { - g.DrawLine(errorPen, _printableArea.Right, 0, _printableArea.Right, _bounds.Height); - } + if (Margins.Top < topMax) { + g.DrawLine(errorPen, 0, _printableArea.Top, _bounds.Width, _printableArea.Top); + } - if (Margins.Bottom < bottomMax) { - g.DrawLine(errorPen, 0, _printableArea.Bottom, _bounds.Width, _printableArea.Bottom); - } + if (Margins.Right < rightMax) { + g.DrawLine(errorPen, _printableArea.Right, 0, _printableArea.Right, _bounds.Height); + } - if (Margins.Left < leftMax || Margins.Top < topMax || Margins.Right < rightMax || Margins.Bottom < bottomMax) { - using var font = new System.Drawing.Font(FontFamily.GenericSansSerif, 14, FontStyle.Bold, GraphicsUnit.Point); - var msg = $"Margins are set outside of printable area {Environment.NewLine}Maximum values: Left: {leftMax / 100F}\", Right: {rightMax / 100F}\", Top: {topMax / 100F}\", Bottom: {bottomMax / 100F}\""; - ServiceLocator.Current.TelemetryService.TrackEvent("Margins of of bounds", new Dictionary { ["Message"] = msg }); - var size = g.MeasureString(msg, font); - using var fmt = new StringFormat(StringFormat.GenericDefault) { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; - g.DrawString(msg, font, Brushes.Gray, _bounds, fmt); - - // Draw hatch outside printable area - g.SetClip(_bounds); - var r = new Rectangle((int)Math.Floor(_printableArea.Left), (int)Math.Floor(_printableArea.Top), (int)Math.Ceiling(_printableArea.Width) + 1, (int)Math.Ceiling(_printableArea.Height) + 1); - g.ExcludeClip(r); - using var brush = new HatchBrush(HatchStyle.LightUpwardDiagonal, Color.Gray, Color.White); - g.FillRectangle(brush, _bounds); + if (Margins.Bottom < bottomMax) { + g.DrawLine(errorPen, 0, _printableArea.Bottom, _bounds.Width, _printableArea.Bottom); + } - } + if (Margins.Left < leftMax || Margins.Top < topMax || Margins.Right < rightMax || + Margins.Bottom < bottomMax) { + using var font = new System.Drawing.Font(FontFamily.GenericSansSerif, 14, FontStyle.Bold, + GraphicsUnit.Point); + var msg = + $"Margins are set outside of printable area {Environment.NewLine}Maximum values: Left: {leftMax / 100F}\", Right: {rightMax / 100F}\", Top: {topMax / 100F}\", Bottom: {bottomMax / 100F}\""; + ServiceLocator.Current.TelemetryService.TrackEvent("Margins of of bounds", + new Dictionary { ["Message"] = msg }); + var size = g.MeasureString(msg, font); + using var fmt = new StringFormat(StringFormat.GenericDefault) { + Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center + }; + g.DrawString(msg, font, Brushes.Gray, _bounds, fmt); + + // Draw hatch outside printable area + g.SetClip(_bounds); + var r = new Rectangle((int)Math.Floor(_printableArea.Left), (int)Math.Floor(_printableArea.Top), + (int)Math.Ceiling(_printableArea.Width) + 1, (int)Math.Ceiling(_printableArea.Height) + 1); + g.ExcludeClip(r); + using var brush = new HatchBrush(HatchStyle.LightUpwardDiagonal, Color.Gray, Color.White); + g.FillRectangle(brush, _bounds); } - PaintRules(g); } - /// - /// Paint a diagnostic page number centered on Page. - /// - /// - /// - internal void PaintPageNum(Graphics g, int pageNum) { - var settings = ModelLocator.Current.Settings; + PaintRules(g); + } - System.Drawing.Font font; + /// + /// Paint a diagnostic page number centered on Page. + /// + /// + /// + internal void PaintPageNum(Graphics g, int pageNum) { + var settings = ModelLocator.Current.Settings; - if (g.PageUnit == GraphicsUnit.Display) { - font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, 48, settings.DiagnosticRulesFont.Style, GraphicsUnit.Point); - } - else { - // Convert font to pixel units if we're in preview - font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, 48 / 72F * 96F, settings.DiagnosticRulesFont.Style, GraphicsUnit.Pixel); - } + System.Drawing.Font font; - float xPos = 0; // GetPageX(pageNum); - float yPos = 0; // GetPageY(pageNum); + if (g.PageUnit == GraphicsUnit.Display) { + font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, 48, settings.DiagnosticRulesFont.Style, + GraphicsUnit.Point); + } + else { + // Convert font to pixel units if we're in preview + font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, 48 / 72F * 96F, + settings.DiagnosticRulesFont.Style, GraphicsUnit.Pixel); + } - g.DrawRectangle(Pens.DarkGray, xPos, yPos, GetPageWidth(), GetPageHeight()); + float xPos = 0; // GetPageX(pageNum); + float yPos = 0; // GetPageY(pageNum); - // Draw row,col in top, left - // % (Rows * Columns) - //g.DrawString($"{GetPageColumn(pageNum)},{GetPageRow(pageNum)}", font, Brushes.Orange, xPos, yPos, StringFormat.GenericTypographic); + g.DrawRectangle(Pens.DarkGray, xPos, yPos, GetPageWidth(), GetPageHeight()); - // Draw page # in center - var size = g.MeasureString($"{pageNum}", font); - g.DrawString($"{pageNum}", font, Brushes.DarkGray, xPos + (GetPageWidth() / 2 - size.Width / 2), yPos + (GetPageHeight() / 2 - size.Height / 2), StringFormat.GenericTypographic); - font.Dispose(); - } + // Draw row,col in top, left + // % (Rows * Columns) + //g.DrawString($"{GetPageColumn(pageNum)},{GetPageRow(pageNum)}", font, Brushes.Orange, xPos, yPos, StringFormat.GenericTypographic); - /// - /// Paint diagnostic rules on Sheet - /// - /// - internal void PaintRules(Graphics g) { - var settings = ModelLocator.Current.Settings; - var preview = g.PageUnit != GraphicsUnit.Display; - System.Drawing.Font font; - if (g.PageUnit == GraphicsUnit.Display) { - font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, settings.DiagnosticRulesFont.Size, settings.DiagnosticRulesFont.Style, GraphicsUnit.Point); - } - else { - // Convert font to pixel units if we're in preview - font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, settings.DiagnosticRulesFont.Size / 72F * 96F, settings.DiagnosticRulesFont.Style, GraphicsUnit.Pixel); - } + // Draw page # in center + var size = g.MeasureString($"{pageNum}", font); + g.DrawString($"{pageNum}", font, Brushes.DarkGray, xPos + ((GetPageWidth() / 2) - (size.Width / 2)), + yPos + ((GetPageHeight() / 2) - (size.Height / 2)), StringFormat.GenericTypographic); + font.Dispose(); + } - // Draw Rules that are physical - if ((settings.PrintPaperSize && !preview) || (settings.PreviewPaperSize && preview)) { - // Draw paper size - DrawRule(g, font, Color.Gray, $"", new Point(PaperSize.Width / 4, preview ? 0 : (int)-_printableArea.Y), - new Point(PaperSize.Width / 4, PaperSize.Height), 4F, true); - DrawRule(g, font, Color.Gray, $"{PaperSize.Width / 100F}\"x{PaperSize.Height / 100F}\"", - new Point(preview ? 0 : (int)-_printableArea.X, PaperSize.Height / 4), new Point(PaperSize.Width, PaperSize.Height / 4), 4F, true); - } + /// + /// Paint diagnostic rules on Sheet + /// + /// + internal void PaintRules(Graphics g) { + var settings = ModelLocator.Current.Settings; + var preview = g.PageUnit != GraphicsUnit.Display; + System.Drawing.Font font; + if (g.PageUnit == GraphicsUnit.Display) { + font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, settings.DiagnosticRulesFont.Size, + settings.DiagnosticRulesFont.Style, GraphicsUnit.Point); + } + else { + // Convert font to pixel units if we're in preview + font = new System.Drawing.Font(settings.DiagnosticRulesFont.Family, + settings.DiagnosticRulesFont.Size / 72F * 96F, settings.DiagnosticRulesFont.Style, GraphicsUnit.Pixel); + } + + // Draw Rules that are physical + if ((settings.PrintPaperSize && !preview) || (settings.PreviewPaperSize && preview)) { + // Draw paper size + DrawRule(g, font, Color.Gray, "", new Point(PaperSize.Width / 4, preview ? 0 : (int)-_printableArea.Y), + new Point(PaperSize.Width / 4, PaperSize.Height), 4F, true); + DrawRule(g, font, Color.Gray, $"{PaperSize.Width / 100F}\"x{PaperSize.Height / 100F}\"", + new Point(preview ? 0 : (int)-_printableArea.X, PaperSize.Height / 4), + new Point(PaperSize.Width, PaperSize.Height / 4), 4F, true); + } - // Hard Margins - // NOTE: HardMarginX & HardMarginY appear to be useless. As int's they are less accurate than - // printableArea.X & Y. - if ((settings.PrintHardMargins && !preview) || (settings.PreviewHardMargins && preview)) { - //GraphicsState state = g.Save(); - //g.TranslateTransform(-HardMarginX, -HardMarginY); - if (_sheet.Landscape) { - g.DrawString($"Landscape Angle = {LandscapeAngle}°", font, Brushes.YellowGreen, HardMarginX, HardMarginY); - if (LandscapeAngle == 270) { - // 270 degrees - marginX is on bottom and marginY is left - DrawRule(g, font, Color.YellowGreen, $"HardMarginX - {HardMarginX / 100F}\"", new Point(_sheet.Margins.Left, PaperSize.Height - (int)HardMarginX), new Point(PaperSize.Width - _sheet.Margins.Right, PaperSize.Height - (int)HardMarginX), 5F); - DrawRule(g, font, Color.YellowGreen, $"HardMarginY - {HardMarginY / 100F}\"", new Point((int)HardMarginY, _sheet.Margins.Top), new Point((int)HardMarginY, PaperSize.Height - _sheet.Margins.Bottom), 5F); - } - else { - // 90 degrees - marginX is on top and marginY is on right - DrawRule(g, font, Color.YellowGreen, $"HardMarginX - {HardMarginX / 100F}\"", new Point(_sheet.Margins.Left, (int)HardMarginX), new Point(PaperSize.Width - _sheet.Margins.Right, (int)HardMarginX), 5F); - DrawRule(g, font, Color.YellowGreen, $"HardMarginY - {HardMarginY / 100F}\"", new Point(PaperSize.Width - (int)HardMarginY, _sheet.Margins.Top), new Point(PaperSize.Width - (int)HardMarginY, PaperSize.Height - _sheet.Margins.Bottom), 5F); - } + // Hard Margins + // NOTE: HardMarginX & HardMarginY appear to be useless. As int's they are less accurate than + // printableArea.X & Y. + if ((settings.PrintHardMargins && !preview) || (settings.PreviewHardMargins && preview)) { + //GraphicsState state = g.Save(); + //g.TranslateTransform(-HardMarginX, -HardMarginY); + if (_sheet.Landscape) { + g.DrawString($"Landscape Angle = {LandscapeAngle}°", font, Brushes.YellowGreen, HardMarginX, + HardMarginY); + if (LandscapeAngle == 270) { + // 270 degrees - marginX is on bottom and marginY is left + DrawRule(g, font, Color.YellowGreen, $"HardMarginX - {HardMarginX / 100F}\"", + new Point(_sheet.Margins.Left, PaperSize.Height - (int)HardMarginX), + new Point(PaperSize.Width - _sheet.Margins.Right, PaperSize.Height - (int)HardMarginX), 5F); + DrawRule(g, font, Color.YellowGreen, $"HardMarginY - {HardMarginY / 100F}\"", + new Point((int)HardMarginY, _sheet.Margins.Top), + new Point((int)HardMarginY, PaperSize.Height - _sheet.Margins.Bottom), 5F); } else { - // 0 degrees - marginX is left and marginY is top + // 90 degrees - marginX is on top and marginY is on right DrawRule(g, font, Color.YellowGreen, $"HardMarginX - {HardMarginX / 100F}\"", - new Point((int)HardMarginX, 0), - new Point((int)HardMarginX, PaperSize.Height), 5F); + new Point(_sheet.Margins.Left, (int)HardMarginX), + new Point(PaperSize.Width - _sheet.Margins.Right, (int)HardMarginX), 5F); DrawRule(g, font, Color.YellowGreen, $"HardMarginY - {HardMarginY / 100F}\"", - new Point(0, (int)HardMarginY), - new Point(PaperSize.Width, (int)HardMarginY), 5F); + new Point(PaperSize.Width - (int)HardMarginY, _sheet.Margins.Top), + new Point(PaperSize.Width - (int)HardMarginY, PaperSize.Height - _sheet.Margins.Bottom), 5F); } - //g.Restore(state); } - - // Margins - if ((settings.PrintMargins && !preview) || (settings.PreviewMargins && preview)) { - DrawRule(g, font, Color.Blue, $"Left Margin - {_sheet.Margins.Left / 100F}\"", new Point(_sheet.Margins.Left, _sheet.Margins.Top), new Point(_sheet.Margins.Left, Bounds.Bottom - _sheet.Margins.Bottom), 2F); - DrawRule(g, font, Color.Blue, $"Right Margin - {_sheet.Margins.Right / 100F}\"", new Point(Bounds.Right - _sheet.Margins.Right, _sheet.Margins.Top), new Point(Bounds.Right - _sheet.Margins.Right, Bounds.Bottom - _sheet.Margins.Bottom), 2F); - DrawRule(g, font, Color.Blue, $"Top Margin - {_sheet.Margins.Top / 100F}\"", new Point(_sheet.Margins.Left, _sheet.Margins.Top), new Point(Bounds.Right - _sheet.Margins.Right, _sheet.Margins.Top), 2F); - DrawRule(g, font, Color.Blue, $"Bottom Margin - {_sheet.Margins.Bottom / 100F}\"", new Point(_sheet.Margins.Left, Bounds.Bottom - _sheet.Margins.Bottom), new Point(Bounds.Right - _sheet.Margins.Right, Bounds.Bottom - _sheet.Margins.Bottom), 2F); - } - - // These rules depend on Hard Margins - // Bounds - if ((settings.PrintBounds && !preview) || (settings.PreviewBounds && preview)) { - DrawRule(g, font, Color.Green, $"Left Bounds - {Bounds.Left / 100F}\"", new Point(Bounds.Left, Bounds.Top), new Point(Bounds.Left, Bounds.Bottom), 3F); - DrawRule(g, font, Color.Green, $"Right Bounds - {Bounds.Right / 100F}\"", new Point(Bounds.Right, Bounds.Top), new Point(Bounds.Right, Bounds.Bottom), 3F); - DrawRule(g, font, Color.Green, $"Top Bounds - {Bounds.Top / 100F}\"", new Point(Bounds.Left, Bounds.Top), new Point(Bounds.Right, Bounds.Top), 3F); - DrawRule(g, font, Color.Green, $"Bottom Bounds - {Bounds.Bottom / 100F}\"", new Point(Bounds.Left, Bounds.Bottom), new Point(Bounds.Right, Bounds.Bottom), 3F); + else { + // 0 degrees - marginX is left and marginY is top + DrawRule(g, font, Color.YellowGreen, $"HardMarginX - {HardMarginX / 100F}\"", + new Point((int)HardMarginX, 0), + new Point((int)HardMarginX, PaperSize.Height), 5F); + DrawRule(g, font, Color.YellowGreen, $"HardMarginY - {HardMarginY / 100F}\"", + new Point(0, (int)HardMarginY), + new Point(PaperSize.Width, (int)HardMarginY), 5F); } + //g.Restore(state); + } - // Header - if ((settings.PreviewHeaderFooterBounds && preview) || (settings.PrintHeaderFooterBounds && !preview)) { - g.FillRectangle(Brushes.Gray, _headerVM.Bounds); - g.FillRectangle(Brushes.Gray, _footerVM.Bounds); - } + // Margins + if ((settings.PrintMargins && !preview) || (settings.PreviewMargins && preview)) { + DrawRule(g, font, Color.Blue, $"Left Margin - {_sheet.Margins.Left / 100F}\"", + new Point(_sheet.Margins.Left, _sheet.Margins.Top), + new Point(_sheet.Margins.Left, Bounds.Bottom - _sheet.Margins.Bottom), 2F); + DrawRule(g, font, Color.Blue, $"Right Margin - {_sheet.Margins.Right / 100F}\"", + new Point(Bounds.Right - _sheet.Margins.Right, _sheet.Margins.Top), + new Point(Bounds.Right - _sheet.Margins.Right, Bounds.Bottom - _sheet.Margins.Bottom), 2F); + DrawRule(g, font, Color.Blue, $"Top Margin - {_sheet.Margins.Top / 100F}\"", + new Point(_sheet.Margins.Left, _sheet.Margins.Top), + new Point(Bounds.Right - _sheet.Margins.Right, _sheet.Margins.Top), 2F); + DrawRule(g, font, Color.Blue, $"Bottom Margin - {_sheet.Margins.Bottom / 100F}\"", + new Point(_sheet.Margins.Left, Bounds.Bottom - _sheet.Margins.Bottom), + new Point(Bounds.Right - _sheet.Margins.Right, Bounds.Bottom - _sheet.Margins.Bottom), 2F); + } - // ContentBounds - between headers & footers - if ((settings.PrintContentBounds && !preview) || (settings.PreviewContentBounds && preview)) { - g.FillRectangle(Brushes.LightGray, _contentBounds); - } + // These rules depend on Hard Margins + // Bounds + if ((settings.PrintBounds && !preview) || (settings.PreviewBounds && preview)) { + DrawRule(g, font, Color.Green, $"Left Bounds - {Bounds.Left / 100F}\"", new Point(Bounds.Left, Bounds.Top), + new Point(Bounds.Left, Bounds.Bottom), 3F); + DrawRule(g, font, Color.Green, $"Right Bounds - {Bounds.Right / 100F}\"", + new Point(Bounds.Right, Bounds.Top), new Point(Bounds.Right, Bounds.Bottom), 3F); + DrawRule(g, font, Color.Green, $"Top Bounds - {Bounds.Top / 100F}\"", new Point(Bounds.Left, Bounds.Top), + new Point(Bounds.Right, Bounds.Top), 3F); + DrawRule(g, font, Color.Green, $"Bottom Bounds - {Bounds.Bottom / 100F}\"", + new Point(Bounds.Left, Bounds.Bottom), new Point(Bounds.Right, Bounds.Bottom), 3F); + } - // Printable area - //if ((PrintableArea.Width != PaperSize.Width) || (PrintableArea.Height != PaperSize.Height)) { - // if ((settings.PrintPrintableArea && !preview) || (settings.PreviewPrintableArea && preview)) { - // //g.FillRectangle(Brushes.LightGray, PrintableArea ); - // g.DrawRectangle(Pens.Red, printableArea.X, printableArea.Y, printableArea.Width, printableArea.Height); - // } - //} + // Header + if ((settings.PreviewHeaderFooterBounds && preview) || (settings.PrintHeaderFooterBounds && !preview)) { + g.FillRectangle(Brushes.Gray, _headerVM.Bounds); + g.FillRectangle(Brushes.Gray, _footerVM.Bounds); + } - font.Dispose(); + // ContentBounds - between headers & footers + if ((settings.PrintContentBounds && !preview) || (settings.PreviewContentBounds && preview)) { + g.FillRectangle(Brushes.LightGray, _contentBounds); } - internal static void DrawRule(Graphics g, System.Drawing.Font font, Color color, string text, Point start, Point end, float labelDiv, bool arrow = false) { - using var pen = new Pen(color); + // Printable area + //if ((PrintableArea.Width != PaperSize.Width) || (PrintableArea.Height != PaperSize.Height)) { + // if ((settings.PrintPrintableArea && !preview) || (settings.PreviewPrintableArea && preview)) { + // //g.FillRectangle(Brushes.LightGray, PrintableArea ); + // g.DrawRectangle(Pens.Red, printableArea.X, printableArea.Y, printableArea.Width, printableArea.Height); + // } + //} - if (arrow) { - pen.Width = 3; - pen.StartCap = LineCap.ArrowAnchor; - pen.EndCap = LineCap.ArrowAnchor; - } - g.DrawLine(pen, start, end); - var textSize = g.MeasureString(text, font); - using Brush brush = new SolidBrush(color); - if (start.X == end.X) { - // Vertical - - var state = g.Save(); - g.RotateTransform(90); - var x = start.X + (textSize.Height / 2F); - var y = (start.Y + end.Y) / labelDiv - (textSize.Width / 2F); - - // Weird hack - If we're zoomed, we need to multiply by the zoom factor (element[1]). - using var tx = g.Transform; - g.TranslateTransform(x * tx.Elements[1], y * tx.Elements[1], MatrixOrder.Append); - - var textRect = new RectangleF(new PointF(0, 0), textSize); - g.FillRectangles(Brushes.White, new RectangleF[] { textRect }); - g.DrawString(text, font, brush, 0, 0); - g.Restore(state); + font.Dispose(); + } - } - else { - // Horizontal - var x = ((start.X + end.X) / labelDiv) - (textSize.Width / 2F); - var y = start.Y - (textSize.Height / 2F); - var textRect = new RectangleF(new PointF(x, y), textSize); - g.FillRectangles(Brushes.White, new RectangleF[] { textRect }); - g.DrawString(text, font, brush, x, y); - } + internal static void DrawRule(Graphics g, System.Drawing.Font font, Color color, string text, Point start, + Point end, float labelDiv, bool arrow = false) { + using var pen = new Pen(color); + + if (arrow) { + pen.Width = 3; + pen.StartCap = LineCap.ArrowAnchor; + pen.EndCap = LineCap.ArrowAnchor; + } + + g.DrawLine(pen, start, end); + var textSize = g.MeasureString(text, font); + using Brush brush = new SolidBrush(color); + if (start.X == end.X) { + // Vertical + + var state = g.Save(); + g.RotateTransform(90); + var x = start.X + (textSize.Height / 2F); + var y = ((start.Y + end.Y) / labelDiv) - (textSize.Width / 2F); + + // Weird hack - If we're zoomed, we need to multiply by the zoom factor (element[1]). + using var tx = g.Transform; + g.TranslateTransform(x * tx.Elements[1], y * tx.Elements[1], MatrixOrder.Append); + + var textRect = new RectangleF(new PointF(0, 0), textSize); + g.FillRectangles(Brushes.White, textRect); + g.DrawString(text, font, brush, 0, 0); + g.Restore(state); + } + else { + // Horizontal + var x = ((start.X + end.X) / labelDiv) - (textSize.Width / 2F); + var y = start.Y - (textSize.Height / 2F); + var textRect = new RectangleF(new PointF(x, y), textSize); + g.FillRectangles(Brushes.White, textRect); + g.DrawString(text, font, brush, x, y); } } + + public class SheetViewModelSettingsChangedEvent { + public bool Reflow { get; set; } + public string PropertyName { get; set; } + } } diff --git a/src/WinPrint.Core/ViewModels/ViewModelBase.cs b/src/WinPrint.Core/ViewModels/ViewModelBase.cs index b04d675..ec2bb00 100644 --- a/src/WinPrint.Core/ViewModels/ViewModelBase.cs +++ b/src/WinPrint.Core/ViewModels/ViewModelBase.cs @@ -2,22 +2,22 @@ using System.ComponentModel; using System.Runtime.CompilerServices; -namespace WinPrint.Core.ViewModels { - public abstract class ViewModelBase : INotifyPropertyChanged { +namespace WinPrint.Core.ViewModels; - public event PropertyChangedEventHandler PropertyChanged; - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } +public abstract class ViewModelBase : INotifyPropertyChanged { + public event PropertyChangedEventHandler? PropertyChanged; - protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) { - if (EqualityComparer.Default.Equals(field, value)) { - return false; - } + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } - field = value; - OnPropertyChanged(propertyName); - return true; + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { + if (EqualityComparer.Default.Equals(field, value)) { + return false; } + + field = value; + OnPropertyChanged(propertyName); + return true; } } diff --git a/src/WinPrint.Core/WinPrint.Core.csproj b/src/WinPrint.Core/WinPrint.Core.csproj index 70c5d75..4724073 100644 --- a/src/WinPrint.Core/WinPrint.Core.csproj +++ b/src/WinPrint.Core/WinPrint.Core.csproj @@ -1,98 +1,112 @@  - - netcoreapp3.1 - AnyCPU;x64;x86 - 2.0.5.102 - Kindel Systems - winprint - Charlie Kindel - winprint Core Engine - Copyright Kindel Systems, LLC - No release notes. - https://github.com/tig/winprint - 8.0 - + + net8.0-windows + AnyCPU;x64;x86 + 2.2.0.000 + Kindel + winprint + Tig Kindel + winprint Core Engine + Copyright Kindel, LLC + No release notes. + https://github.com/tig/winprint + enable + README.md + https://github.com/tig/winprint + LICENSE + - - DEBUG - + + DEBUG + - - - - + + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - + + + True + \ + + + True + \ + + - - - - + + + + - - - True - True - Resources.resx - - - True - True - TelemetryService.tt - - - True - True - TelemetryService.tt - - + + + + + + + + + + + + + + + - - - PublicResXFileCodeGenerator - Resources.Designer.cs - Never - - - - - TextTemplatingFileGenerator - TelemetryService.tt.cs - - + + + True + True + Resources.resx + + + True + True + TelemetryService.tt + + + True + True + TelemetryService.tt + + - - - + + + PublicResXFileCodeGenerator + Resources.Designer.cs + Never + + + + + + TextTemplatingFileGenerator + TelemetryService.tt.cs + + + + + + diff --git a/src/WinPrint.Core/WinPrint.Core.msbump b/src/WinPrint.Core/WinPrint.Core.msbump deleted file mode 100644 index fee6de8..0000000 --- a/src/WinPrint.Core/WinPrint.Core.msbump +++ /dev/null @@ -1,11 +0,0 @@ -{ - Configurations: { - "Debug": { - BumpRevision: true - }, - - "Release": { - BumpRevision: false - } - } -} \ No newline at end of file diff --git a/src/WinPrint.LiteHtml/WinPrint.LiteHtml.csproj b/src/WinPrint.LiteHtml/WinPrint.LiteHtml.csproj index 830bfc7..cdd7a63 100644 --- a/src/WinPrint.LiteHtml/WinPrint.LiteHtml.csproj +++ b/src/WinPrint.LiteHtml/WinPrint.LiteHtml.csproj @@ -2,18 +2,19 @@ - netcoreapp3.1 + net8.0 winprint LiteHtml WinPrint.LiteHtml - 2.0.5.100 - Kindel Systems + 2.1.0.1 + Kindel winprint - Charlie Kindel + Tig Kindel winprint LiteHtml Adapter - Copyright Kindel Systems, LLC + Copyright Kindel, LLC No release notes. https://github.com/tig/winprint + AnyCPU;x64 @@ -21,10 +22,18 @@ TRACE;WINDOWS + + TRACE;WINDOWS + + TRACE;WINDOWS + + TRACE;WINDOWS + + @@ -36,19 +45,23 @@ - + Always - - - + + + + + + + diff --git a/src/WinPrint.WinForms/GuiLogSink.cs b/src/WinPrint.WinForms/GuiLogSink.cs new file mode 100644 index 0000000..bb49dc1 --- /dev/null +++ b/src/WinPrint.WinForms/GuiLogSink.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Windows.Forms; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +//using TTRider.PowerShellAsync; + +namespace WinPrint.WinForms { + + public class GuiLogSink : ILogEventSink { + public static GuiLogSink Instance => _instance.Value; + + private static readonly Lazy _instance = new Lazy(() => new GuiLogSink()); + + public ITextFormatter TextFormatter { get; set; } + + public Control OutputWindow; + + public GuiLogSink() { + TextFormatter = new Serilog.Formatting.Display.MessageTemplateTextFormatter("{Message:lj}"); + } + + public void Emit(LogEvent logEvent) { + if (logEvent == null) { + throw new ArgumentNullException(nameof(logEvent)); + } + + if (OutputWindow == null) { + throw new ArgumentNullException("GuiLogSink: Output not set"); + } + + using var strWriter = new StringWriter(); + TextFormatter.Format(logEvent, strWriter); + try { + var msg = $"{strWriter}"; + switch (logEvent.Level) { + // -Verbose + case LogEventLevel.Verbose: + case LogEventLevel.Information: + OutputWindow.Text = msg; + break; + + // -Debug + case LogEventLevel.Debug: + OutputWindow.Text = msg; + break; + + case LogEventLevel.Warning: + OutputWindow.Text = msg; + break; + + // The Write-Error cmdlet declares a non-terminating error. + case LogEventLevel.Error: + //cmdlet.WriteDebug("error: " + strWriter.ToString()); + var ex = logEvent.Exception; + if (logEvent.Exception == null) { + ex = new Exception(); + } + + OutputWindow.Text = msg; + break; + + case LogEventLevel.Fatal: + //_cmdlet.WriteDebug("fatal: " + strWriter.ToString()); + //var fatal = new ErrorRecord(logEvent.Exception, errorId: msg, errorCategory: ErrorCategory.InvalidOperation, targetObject: null); + //cmdlet.ThrowTerminatingError(fatal); + OutputWindow.Text = msg; + break; + + default: + //OutputWindow.Text = msg; + + break; + } + } + catch (Exception e) { + Debug.WriteLine(e.Message); + } + } + } +} diff --git a/src/WinPrint.WinForms/MainWindow.cs b/src/WinPrint.WinForms/MainWindow.cs index 27c0da1..6bd78ed 100644 --- a/src/WinPrint.WinForms/MainWindow.cs +++ b/src/WinPrint.WinForms/MainWindow.cs @@ -1,4 +1,4 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint using System; @@ -30,14 +30,14 @@ public partial class MainWindow : Form { private string activeFile; private OpenFileDialog openFileDialog = new OpenFileDialog(); - private WinPrint.WinForms.PrintPreview printPreview; + public WinPrint.WinForms.PrintPreview PrintPreview; public MainWindow() { InitializeComponent(); Icon = Resources.printer_and_fax_w; - printPreview = new PrintPreview { + PrintPreview = new PrintPreview { Dock = dummyButton.Dock, Anchor = dummyButton.Anchor, BackColor = dummyButton.BackColor, @@ -50,8 +50,7 @@ public MainWindow() { TabIndex = 1, TabStop = true }; - printPreview.Click += new System.EventHandler(printPreview_Click); - + PrintPreview.Click += new System.EventHandler(printPreview_Click); Color(); @@ -60,7 +59,7 @@ public MainWindow() { if (LicenseManager.UsageMode != LicenseUsageMode.Designtime) { panelRight.Controls.Remove(dummyButton); - panelRight.Controls.Add(printPreview); + panelRight.Controls.Add(PrintPreview); printersCB.Enabled = false; paperSizesCB.Enabled = false; } @@ -79,7 +78,6 @@ public MainWindow() { fontDialog.AllowVectorFonts = false; // fontDialog.AllowVerticalFonts = false; fontDialog.ShowHelp = false; - } private void Color() { @@ -127,7 +125,7 @@ private void Color() { fromLabel.BackColor = back; pagesLabel.BackColor = back; - printPreview.ForeColor = text; + PrintPreview.ForeColor = text; printersCB.ForeColor = text; paperSizesCB.ForeColor = text; landscapeCheckbox.ForeColor = text; @@ -187,8 +185,8 @@ protected override void Dispose(bool disposing) { printDoc.Dispose(); } - if (printPreview != null) { - printPreview.Dispose(); + if (PrintPreview != null) { + PrintPreview.Dispose(); } } disposed = true; @@ -202,17 +200,17 @@ protected override void Dispose(bool disposing) { // TODO: Refactor PropertyChanged lambdas to be functions so they can be -= private void SetupSheetViewModelNotifications() { //LogService.TraceMessage(); - if (printPreview.SheetViewModel != null) { + if (PrintPreview.SheetViewModel != null) { LogService.TraceMessage("SetupSheetViewModelNotifications was already called"); return; } - printPreview.SheetViewModel = new SheetViewModel(); - printPreview.SheetViewModel.PropertyChanged += PropertyChangedEventHandler; - printPreview.SheetViewModel.SettingsChanged += SettingsChangedEventHandler; - printPreview.SheetViewModel.PageSettingsSet += PageSettingsSetEventHandler; - printPreview.SheetViewModel.Loaded += FileLoadedEventHandler; - printPreview.SheetViewModel.ReadyChanged += ReadyChangedEventHandler; + PrintPreview.SheetViewModel = new SheetViewModel(); + PrintPreview.SheetViewModel.PropertyChanged += PropertyChangedEventHandler; + PrintPreview.SheetViewModel.SettingsChanged += SettingsChangedEventHandler; + PrintPreview.SheetViewModel.PageSettingsSet += PageSettingsSetEventHandler; + PrintPreview.SheetViewModel.Loaded += FileLoadedEventHandler; + PrintPreview.SheetViewModel.ReadyChanged += ReadyChangedEventHandler; } private void FileLoadedEventHandler(object sender, bool loading) { @@ -249,14 +247,14 @@ private void ReadyChangedEventHandler(object sender, bool ready) { } if (string.IsNullOrEmpty(activeFile)) { - printPreview.Invalidate(); - printPreview.Text = Resources.HelloMsg; + PrintPreview.Invalidate(); + PrintPreview.Text = Resources.HelloMsg; } else { // Document is loaded and flowed. We're ready to preview and/or pring. - printPreview.CurrentSheet = 1; - printPreview.Invalidate(); - printPreview.Text = ""; + PrintPreview.CurrentSheet = 1; + PrintPreview.Invalidate(); + PrintPreview.Text = ""; } } } @@ -270,49 +268,49 @@ private void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs switch (e.PropertyName) { case "Landscape": LogService.TraceMessage($" Checking checkbox: {ModelLocator.Current.Settings.Sheets.GetValueOrDefault(ModelLocator.Current.Settings.DefaultSheet.ToString()).Landscape}"); - landscapeCheckbox.Checked = printPreview.SheetViewModel.Landscape; + landscapeCheckbox.Checked = PrintPreview.SheetViewModel.Landscape; break; case "Header": - headerTextBox.Text = printPreview.SheetViewModel.Header.Text; - enableHeader.Checked = printPreview.SheetViewModel.Header.Enabled; - headerFooterFontLink.Text = printPreview.SheetViewModel.Header.Font.ToString(); + headerTextBox.Text = PrintPreview.SheetViewModel.Header.Text; + enableHeader.Checked = PrintPreview.SheetViewModel.Header.Enabled; + headerFooterFontLink.Text = PrintPreview.SheetViewModel.Header.Font.ToString(); break; case "Footer": - footerTextBox.Text = printPreview.SheetViewModel.Footer.Text; - enableFooter.Checked = printPreview.SheetViewModel.Footer.Enabled; + footerTextBox.Text = PrintPreview.SheetViewModel.Footer.Text; + enableFooter.Checked = PrintPreview.SheetViewModel.Footer.Enabled; break; case "Margins": - topMargin.Value = printPreview.SheetViewModel.Margins.Top / 100M; - leftMargin.Value = printPreview.SheetViewModel.Margins.Left / 100M; - rightMargin.Value = printPreview.SheetViewModel.Margins.Right / 100M; - bottomMargin.Value = printPreview.SheetViewModel.Margins.Bottom / 100M; + topMargin.Value = PrintPreview.SheetViewModel.Margins.Top / 100M; + leftMargin.Value = PrintPreview.SheetViewModel.Margins.Left / 100M; + rightMargin.Value = PrintPreview.SheetViewModel.Margins.Right / 100M; + bottomMargin.Value = PrintPreview.SheetViewModel.Margins.Bottom / 100M; // Keep PrintDocument updated for WinForms.PrintPreview - printDoc.PrinterSettings.DefaultPageSettings.Margins = (Margins)printPreview.SheetViewModel.Margins.Clone(); + printDoc.PrinterSettings.DefaultPageSettings.Margins = (Margins)PrintPreview.SheetViewModel.Margins.Clone(); break; case "PageSeparator": - pageSeparator.Checked = printPreview.SheetViewModel.PageSeparator; + pageSeparator.Checked = PrintPreview.SheetViewModel.PageSeparator; break; case "Rows": - rows.Value = printPreview.SheetViewModel.Rows; + rows.Value = PrintPreview.SheetViewModel.Rows; break; case "Columns": - columns.Value = printPreview.SheetViewModel.Columns; + columns.Value = PrintPreview.SheetViewModel.Columns; break; case "Padding": - padding.Value = printPreview.SheetViewModel.Padding / 100M; + padding.Value = PrintPreview.SheetViewModel.Padding / 100M; break; case "File": - Text = $"winprint - {printPreview.SheetViewModel.File}"; - printPreview.CurrentSheet = 1; + Text = $"winprint - {PrintPreview.SheetViewModel.File}"; + PrintPreview.CurrentSheet = 1; break; case "Title": @@ -331,8 +329,8 @@ private void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs break; case "ContentSettings": - contentFontLink.Text = printPreview.SheetViewModel.ContentSettings.Font.ToString(); - lineNumbers.Checked = printPreview.SheetViewModel.ContentSettings.LineNumbers; + contentFontLink.Text = PrintPreview.SheetViewModel.ContentSettings.Font.ToString(); + lineNumbers.Checked = PrintPreview.SheetViewModel.ContentSettings.LineNumbers; //lineNumberSeparator.Checked = printPreview.SheetViewModel.ContentSettings.lineNumberSeparator; break; @@ -374,7 +372,7 @@ private void SettingsChangedEventHandler(object o, SheetViewModel.SheetViewModel //else { // printPreview.Invalidate(); //} - printPreview.Invalidate(); + PrintPreview.Invalidate(); } } } @@ -409,7 +407,7 @@ private void MainWindow_Load(object sender, EventArgs e) { WindowState = (System.Windows.Forms.FormWindowState)ModelLocator.Current.Settings.WindowState; - printPreview.KeyUp += (s, e) => { + PrintPreview.KeyUp += (s, e) => { if (e.KeyCode == Keys.F5) { //printPreview.Invalidate(true); Log.Debug("-------- F5 ---------"); @@ -488,20 +486,20 @@ private void MainWindow_Load(object sender, EventArgs e) { // Select default Sheet var newSheet = ModelLocator.Current.Settings.Sheets.GetValueOrDefault(ModelLocator.Current.Settings.DefaultSheet.ToString()); if (!string.IsNullOrEmpty(ModelLocator.Current.Options.Sheet)) { - newSheet = printPreview.SheetViewModel.FindSheet(ModelLocator.Current.Options.Sheet, out var sheetID); + newSheet = PrintPreview.SheetViewModel.FindSheet(ModelLocator.Current.Options.Sheet, out var sheetID); } comboBoxSheet.Text = newSheet.Name; // This will cause a flurry of property change notifications, setting all UI elements - printPreview.SheetViewModel.SetSheet(newSheet); + PrintPreview.SheetViewModel.SetSheet(newSheet); // Must set landscape after printer/paper selection // --l and --o if (ModelLocator.Current.Options.Landscape) { - printPreview.SheetViewModel.Landscape = true; + PrintPreview.SheetViewModel.Landscape = true; } if (ModelLocator.Current.Options.Portrait) { - printPreview.SheetViewModel.Landscape = false; + PrintPreview.SheetViewModel.Landscape = false; } // Override content-type @@ -510,14 +508,20 @@ private void MainWindow_Load(object sender, EventArgs e) { // TODO: (nothing?) } - printPreview.Select(); - printPreview.Focus(); + PrintPreview.Select(); + PrintPreview.Focus(); //this.Cursor = Cursors.Default; if (ModelLocator.Current.Options.Files != null && ModelLocator.Current.Options.Files.ToList().Count > 0) { activeFile = ModelLocator.Current.Options.Files.ToList()[0]; } + // Verify Pygments is installed + (bool installed, string message) = ServiceLocator.Current.PygmentsConverterService.CheckInstall(); + if (!installed) { + MessageBox.Show(message, "Warning"); + } + // By running this on a different thread, we enable the main window to show // as quickly as possible; making startup seem faster. //Task.Run(() => Start()); @@ -595,9 +599,9 @@ private void LoadFile() { } else { // Reset View Model - printPreview.SheetViewModel.Reset(); - printPreview.Invalidate(); - printPreview.Text = Resources.LoadingMsg; + PrintPreview.SheetViewModel.Reset(); + PrintPreview.Invalidate(); + PrintPreview.Text = Resources.LoadingMsg; // On another thread // - load file @@ -608,7 +612,7 @@ private void LoadFile() { var fileToPrint = activeFile; try { BeginInvoke((Action)(() => { - printPreview.Text = $"{stage}..."; + PrintPreview.Text = $"{stage}..."; })); // This is an IO bound operation. // TODO: This does not need to run on another thread if we are using async/await correctly @@ -617,24 +621,24 @@ private void LoadFile() { if (!string.IsNullOrEmpty(fileToPrint) && !Path.IsPathFullyQualified(fileToPrint)) { fileToPrint = Path.GetFullPath(fileToPrint, Directory.GetCurrentDirectory()); } - await printPreview.SheetViewModel.LoadFileAsync(fileToPrint, ModelLocator.Current.Options.ContentType).ConfigureAwait(false); + await PrintPreview.SheetViewModel.LoadFileAsync(fileToPrint, ModelLocator.Current.Options.ContentType).ConfigureAwait(false); // Set landscape. This causes other DefaultPageSettings to change // These are CPU bound operations. // TODO: Do not use async/await for CPU bound operations https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth stage = "Getting Printer Page Settings"; BeginInvoke((Action)(() => { - printPreview.Text = $"{stage}..."; + PrintPreview.Text = $"{stage}..."; })); - printDoc.DefaultPageSettings.Landscape = printPreview.SheetViewModel.Landscape; - printPreview.SheetViewModel.SetPrinterPageSettings(printDoc.DefaultPageSettings); + printDoc.DefaultPageSettings.Landscape = PrintPreview.SheetViewModel.Landscape; + PrintPreview.SheetViewModel.SetPrinterPageSettings(printDoc.DefaultPageSettings); stage = "Rendering"; BeginInvoke((Action)(() => { - printPreview.Text = $"{stage}..."; + PrintPreview.Text = $"{stage}..."; })); - await printPreview.SheetViewModel.ReflowAsync().ConfigureAwait(false); + await PrintPreview.SheetViewModel.ReflowAsync().ConfigureAwait(false); } catch (DirectoryNotFoundException dnfe) { @@ -664,6 +668,9 @@ private void LoadFile() { // Set Loading to false in case of an error //printPreview.SheetViewModel.Loading = false; //printPreview.SheetViewModel.Ready = false; + PrintPreview.Select(); + PrintPreview.Focus(); + } }); } @@ -674,7 +681,7 @@ private void ShowMessage(string message) { BeginInvoke((Action)(() => ShowMessage(message))); } else { - printPreview.Text = message; + PrintPreview.Text = message; } } @@ -699,7 +706,7 @@ private void SheetChanged() { LogService.TraceMessage(); var newSheet = ModelLocator.Current.Settings.Sheets.GetValueOrDefault(ModelLocator.Current.Settings.DefaultSheet.ToString()); comboBoxSheet.Text = newSheet.Name; - printPreview.SheetViewModel.SetSheet(newSheet); + PrintPreview.SheetViewModel.SetSheet(newSheet); //SheetSettingsChanged(); LoadFile(); } @@ -835,7 +842,7 @@ private async void printButton_Click(object sender, EventArgs args) { try { ShowMessage("Preparing to print.."); - await print.SheetViewModel.LoadFileAsync(printPreview.SheetViewModel.File, ModelLocator.Current.Options.ContentType).ConfigureAwait(false); + await print.SheetViewModel.LoadFileAsync(PrintPreview.SheetViewModel.File, ModelLocator.Current.Options.ContentType).ConfigureAwait(false); print.SetPrinter(printDoc.PrinterSettings.PrinterName); print.SetPaperSize(printDoc.DefaultPageSettings.PaperSize.PaperName); diff --git a/src/WinPrint.WinForms/PrintPreview.cs b/src/WinPrint.WinForms/PrintPreview.cs index 4f4d448..75332e3 100644 --- a/src/WinPrint.WinForms/PrintPreview.cs +++ b/src/WinPrint.WinForms/PrintPreview.cs @@ -156,6 +156,14 @@ protected override void OnKeyUp(KeyEventArgs e) { #endif break; + case Keys.Home: + Home(); + break; + + case Keys.End: + End(); + break; + #if TERMINAL case Keys.Right: pygCte?.DecoderClient.MoveCursor(null, libvt100.Direction.Forward, 1); @@ -190,6 +198,18 @@ private void PageDown() { ServiceLocator.Current.TelemetryService.TrackEvent("Print Preview Page Down", new Dictionary { ["Page"] = CurrentSheet.ToString() }); } + private void Home() { + CurrentSheet = 1; + Invalidate(); + ServiceLocator.Current.TelemetryService.TrackEvent("Print Preview Home", new Dictionary { ["Page"] = CurrentSheet.ToString() }); + } + + private void End() { + CurrentSheet = _svm.NumSheets; + Invalidate(); + ServiceLocator.Current.TelemetryService.TrackEvent("Print Preview End", new Dictionary { ["Page"] = CurrentSheet.ToString() }); + } + protected override void OnTextChanged(EventArgs e) { // Invalidate previous Invalidate(_messageRect); @@ -265,7 +285,6 @@ protected override void OnPaint(PaintEventArgs e) { }; private void PaintMessage(PaintEventArgs e) { // While in error or loading & reflowing show Text - Log.Information("Status: {status}", Text); var rect = GetTextRect(e.Graphics); //e.Graphics.FillRectangle(SystemBrushes.Control, _messageRect); e.Graphics.DrawString(Text, new Font(Font.FontFamily, 14F, FontStyle.Regular, GraphicsUnit.Point), SystemBrushes.ControlText, rect, _messageStringFormat); diff --git a/src/WinPrint.WinForms/Program.cs b/src/WinPrint.WinForms/Program.cs index 2f5b5c4..83f8c87 100644 --- a/src/WinPrint.WinForms/Program.cs +++ b/src/WinPrint.WinForms/Program.cs @@ -1,7 +1,8 @@ -// Copyright Kindel Systems, LLC - http://www.kindel.com +// Copyright Kindel, LLC - http://www.kindel.com // Published under the MIT License at https://github.com/tig/winprint using System; +using System.Diagnostics; using System.Threading.Tasks; using System.Windows.Forms; using CommandLine; @@ -10,6 +11,7 @@ using Serilog.Events; using WinPrint.Core.Models; using WinPrint.Core.Services; +using WinPrint.WinForms; namespace WinPrint.Winforms { internal static class Program { @@ -18,22 +20,32 @@ internal static class Program { /// [STAThread] private static void Main(string[] args) { + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + ServiceLocator.Current.TelemetryService.Start(AppDomain.CurrentDomain.FriendlyName); - ServiceLocator.Current.LogService.Start(AppDomain.CurrentDomain.FriendlyName); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + bool debug = false; + + var main = new MainWindow(); + GuiLogSink.Instance.OutputWindow = main; + if (args.Length > 0) { var parser = new Parser(with => with.EnableDashDash = true); var result = parser.ParseArguments(args); result .WithParsed(o => { if (o.Debug) { + ServiceLocator.Current.LogService.Start(AppDomain.CurrentDomain.FriendlyName, GuiLogSink.Instance, debug: true, verbose: true); ServiceLocator.Current.LogService.ConsoleLevelSwitch.MinimumLevel = LogEventLevel.Debug; ServiceLocator.Current.LogService.MasterLevelSwitch.MinimumLevel = LogEventLevel.Debug; } else { + ServiceLocator.Current.LogService.Start(AppDomain.CurrentDomain.FriendlyName, GuiLogSink.Instance, debug: false, verbose: true); ServiceLocator.Current.LogService.ConsoleLevelSwitch.MinimumLevel = LogEventLevel.Information; } ServiceLocator.Current.TelemetryService.TrackEvent("Command Line Options", properties: o.GetTelemetryDictionary()); @@ -44,11 +56,6 @@ private static void Main(string[] args) { parser.Dispose(); } - Application.SetHighDpiMode(HighDpiMode.SystemAware); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - var main = new MainWindow(); Application.Run(main); main.Dispose(); diff --git a/src/WinPrint.WinForms/Properties/launchSettings.json b/src/WinPrint.WinForms/Properties/launchSettings.json new file mode 100644 index 0000000..bfc77e4 --- /dev/null +++ b/src/WinPrint.WinForms/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "WinPrint.WinForms": { + "commandName": "Project", + "commandLineArgs": "Program.cs", + "workingDirectory": "..\\..\\testfiles" + } + } +} \ No newline at end of file diff --git a/src/WinPrint.WinForms/WinPrint.WinForms.csproj b/src/WinPrint.WinForms/WinPrint.WinForms.csproj index 94855c9..d243b67 100644 --- a/src/WinPrint.WinForms/WinPrint.WinForms.csproj +++ b/src/WinPrint.WinForms/WinPrint.WinForms.csproj @@ -1,9 +1,9 @@  - + WinExe - netcoreapp3.1 + net8.0-windows true true WinPrint.Winforms.Program @@ -12,33 +12,34 @@ winprint GUI winprintgui - 2.0.5.102 - Kindel Systems + 2.1.0.275 + Kindel winprint - Charlie Kindel + Tig Kindel winprint GUI App - Copyright Kindel Systems, LLC + Copyright Kindel, LLC No release notes. https://github.com/tig/winprint en-US + AnyCPU;x64 1701;1702 + + + 1701;1702 + + - + - - - - - True @@ -55,7 +56,7 @@ - + \ No newline at end of file diff --git a/src/WinPrint.WinForms/WinPrint.WinForms.msbump b/src/WinPrint.WinForms/WinPrint.WinForms.msbump deleted file mode 100644 index fee6de8..0000000 --- a/src/WinPrint.WinForms/WinPrint.WinForms.msbump +++ /dev/null @@ -1,11 +0,0 @@ -{ - Configurations: { - "Debug": { - BumpRevision: true - }, - - "Release": { - BumpRevision: false - } - } -} \ No newline at end of file diff --git a/src/WinPrint.sln b/src/WinPrint.sln index 28deffe..600905e 100644 --- a/src/WinPrint.sln +++ b/src/WinPrint.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29503.13 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32728.150 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinPrint.WinForms", "WinPrint.WinForms\WinPrint.WinForms.csproj", "{6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}" EndProject @@ -19,106 +19,185 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LiteHtmlLib", "LiteHtmlShar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellAsync", "PowershellAsync\PowerShellAsync\PowerShellAsync.csproj", "{F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8503AB6C-847F-4764-BB7C-DFFFE5E87A01}" + ProjectSection(SolutionItems) = preProject + ..\.gitignore = ..\.gitignore + README.md = README.md + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{9563EBB8-F752-4E4B-942E-F1A5C1A601BF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|x64.ActiveCfg = Debug|Any CPU - {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|x64.Build.0 = Debug|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|ARM.Build.0 = Debug|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|ARM64.Build.0 = Debug|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|x64.ActiveCfg = Debug|x64 + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|x64.Build.0 = Debug|x64 {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|x86.ActiveCfg = Debug|Any CPU {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Debug|x86.Build.0 = Debug|Any CPU {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|Any CPU.Build.0 = Release|Any CPU - {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|x64.ActiveCfg = Release|Any CPU - {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|x64.Build.0 = Release|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|ARM.ActiveCfg = Release|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|ARM.Build.0 = Release|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|ARM64.ActiveCfg = Release|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|ARM64.Build.0 = Release|Any CPU + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|x64.ActiveCfg = Release|x64 + {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|x64.Build.0 = Release|x64 {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|x86.ActiveCfg = Release|Any CPU {6DEA2A6F-7ECA-411C-B25A-2B7293AFD336}.Release|x86.Build.0 = Release|Any CPU {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|ARM.ActiveCfg = Debug|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|ARM.Build.0 = Debug|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|ARM64.Build.0 = Debug|Any CPU {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|x64.ActiveCfg = Debug|x64 {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|x64.Build.0 = Debug|x64 {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|x86.ActiveCfg = Debug|x86 {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Debug|x86.Build.0 = Debug|x86 {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|Any CPU.ActiveCfg = Release|Any CPU {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|Any CPU.Build.0 = Release|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|ARM.ActiveCfg = Release|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|ARM.Build.0 = Release|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|ARM64.ActiveCfg = Release|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|ARM64.Build.0 = Release|Any CPU {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|x64.ActiveCfg = Release|x64 {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|x64.Build.0 = Release|x64 - {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|x86.ActiveCfg = Release|Any CPU - {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|x86.Build.0 = Release|Any CPU + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|x86.ActiveCfg = Release|x86 + {78B6744F-DF3E-4D86-ACE5-18854A0AF54E}.Release|x86.Build.0 = Release|x86 {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|x64.ActiveCfg = Debug|Any CPU - {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|x64.Build.0 = Debug|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|ARM.ActiveCfg = Debug|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|ARM.Build.0 = Debug|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|ARM64.Build.0 = Debug|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|x64.ActiveCfg = Debug|x64 + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|x64.Build.0 = Debug|x64 {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|x86.ActiveCfg = Debug|Any CPU {A82F3B2B-5E3A-49B8-8552-983D06081186}.Debug|x86.Build.0 = Debug|Any CPU {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|Any CPU.ActiveCfg = Release|Any CPU {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|Any CPU.Build.0 = Release|Any CPU - {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|x64.ActiveCfg = Release|Any CPU - {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|x64.Build.0 = Release|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|ARM.ActiveCfg = Release|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|ARM.Build.0 = Release|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|ARM64.ActiveCfg = Release|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|ARM64.Build.0 = Release|Any CPU + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|x64.ActiveCfg = Release|x64 + {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|x64.Build.0 = Release|x64 {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|x86.ActiveCfg = Release|Any CPU {A82F3B2B-5E3A-49B8-8552-983D06081186}.Release|x86.Build.0 = Release|Any CPU {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|x64.ActiveCfg = Debug|Any CPU - {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|x64.Build.0 = Debug|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|ARM.Build.0 = Debug|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|ARM64.Build.0 = Debug|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|x64.ActiveCfg = Debug|x64 + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|x64.Build.0 = Debug|x64 {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|x86.ActiveCfg = Debug|Any CPU {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Debug|x86.Build.0 = Debug|Any CPU {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|Any CPU.Build.0 = Release|Any CPU - {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|x64.ActiveCfg = Release|Any CPU - {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|x64.Build.0 = Release|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|ARM.ActiveCfg = Release|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|ARM.Build.0 = Release|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|ARM64.ActiveCfg = Release|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|ARM64.Build.0 = Release|Any CPU + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|x64.ActiveCfg = Release|x64 + {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|x64.Build.0 = Release|x64 {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|x86.ActiveCfg = Release|Any CPU {DC42119D-15B4-42C2-A45E-518BA4EDDFAE}.Release|x86.Build.0 = Release|Any CPU {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|x64.ActiveCfg = Debug|Any CPU - {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|x64.Build.0 = Debug|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|ARM.Build.0 = Debug|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|ARM64.Build.0 = Debug|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|x64.ActiveCfg = Debug|x64 + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|x64.Build.0 = Debug|x64 {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|x86.ActiveCfg = Debug|Any CPU {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Debug|x86.Build.0 = Debug|Any CPU {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|Any CPU.ActiveCfg = Release|Any CPU {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|Any CPU.Build.0 = Release|Any CPU - {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|x64.ActiveCfg = Release|Any CPU - {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|x64.Build.0 = Release|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|ARM.ActiveCfg = Release|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|ARM.Build.0 = Release|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|ARM64.ActiveCfg = Release|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|ARM64.Build.0 = Release|Any CPU + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|x64.ActiveCfg = Release|x64 + {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|x64.Build.0 = Release|x64 {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|x86.ActiveCfg = Release|Any CPU {6FCD9AFA-22D9-4E13-BA11-96B5C1949842}.Release|x86.Build.0 = Release|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|ARM.ActiveCfg = Debug|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|ARM.Build.0 = Debug|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|ARM64.Build.0 = Debug|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|x64.ActiveCfg = Debug|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|x64.Build.0 = Debug|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|x86.ActiveCfg = Debug|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Debug|x86.Build.0 = Debug|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|Any CPU.ActiveCfg = Release|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|Any CPU.Build.0 = Release|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|ARM.ActiveCfg = Release|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|ARM.Build.0 = Release|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|ARM64.ActiveCfg = Release|Any CPU + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|ARM64.Build.0 = Release|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|x64.ActiveCfg = Release|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|x64.Build.0 = Release|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|x86.ActiveCfg = Release|Any CPU {84B31CE1-C4EF-4C86-BE34-1EE180C7E651}.Release|x86.Build.0 = Release|Any CPU - {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|Any CPU.ActiveCfg = Debug|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|Any CPU.Build.0 = Debug|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|ARM.ActiveCfg = Debug|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|ARM.Build.0 = Debug|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|ARM64.ActiveCfg = Debug|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|ARM64.Build.0 = Debug|x64 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|x64.ActiveCfg = Debug|x64 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|x64.Build.0 = Debug|x64 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|x86.ActiveCfg = Debug|Win32 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Debug|x86.Build.0 = Debug|Win32 - {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|Any CPU.ActiveCfg = Release|Win32 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|Any CPU.ActiveCfg = Release|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|Any CPU.Build.0 = Release|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|ARM.ActiveCfg = Release|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|ARM.Build.0 = Release|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|ARM64.ActiveCfg = Release|x64 + {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|ARM64.Build.0 = Release|x64 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|x64.ActiveCfg = Release|x64 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|x64.Build.0 = Release|x64 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|x86.ActiveCfg = Release|Win32 {2E1F5B1E-430B-4FFF-963C-92229154F261}.Release|x86.Build.0 = Release|Win32 {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|ARM.Build.0 = Debug|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|ARM64.Build.0 = Debug|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|x64.ActiveCfg = Debug|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|x64.Build.0 = Debug|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|x86.ActiveCfg = Debug|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Debug|x86.Build.0 = Debug|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|Any CPU.ActiveCfg = Release|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|Any CPU.Build.0 = Release|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|ARM.ActiveCfg = Release|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|ARM.Build.0 = Release|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|ARM64.ActiveCfg = Release|Any CPU + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|ARM64.Build.0 = Release|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|x64.ActiveCfg = Release|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|x64.Build.0 = Release|Any CPU {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31}.Release|x86.ActiveCfg = Release|Any CPU @@ -127,6 +206,11 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {84B31CE1-C4EF-4C86-BE34-1EE180C7E651} = {9563EBB8-F752-4E4B-942E-F1A5C1A601BF} + {2E1F5B1E-430B-4FFF-963C-92229154F261} = {9563EBB8-F752-4E4B-942E-F1A5C1A601BF} + {F23D33CB-14D5-471E-9A5B-BFD5C14C8A31} = {9563EBB8-F752-4E4B-942E-F1A5C1A601BF} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7FA61957-25B2-40E5-B7AF-AC404DAF2B20} EndGlobalSection diff --git a/src/WinPrint.sln.DotSettings b/src/WinPrint.sln.DotSettings new file mode 100644 index 0000000..f4e2048 --- /dev/null +++ b/src/WinPrint.sln.DotSettings @@ -0,0 +1,7 @@ + + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + True + True + True + True \ No newline at end of file diff --git a/testfiles/MainWindow 2x.cs b/testfiles/MainWindow 2x.cs index e8fd92e..580c786 100644 --- a/testfiles/MainWindow 2x.cs +++ b/testfiles/MainWindow 2x.cs @@ -2,15 +2,15 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Drawing; -using System.Drawing.Printing; -using System.IO; -using System.Linq; -using System.Windows.Forms; -using WinPrint.Core; -using WinPrint.Core.Models; using WinPrint.Core.Services; +// This is a very long comment to test wrapping of comments in source code. It goes on and on... +using test; // This is a long comment that extends way to the right in order to test wrapping + +// private string file = "..\\..\\..\\..\\..\\..\\tests\\formfeeds.txt"; +// private string file = "..\\..\\..\\..\\..\\..\\tests\\TEST.TXT"; + // HAS TAB BEFORE private string file = "..\\..\\..\\..\\..\\..\\tests\\long html doc as text.TXT"; + namespace WinPrint { public partial class MainWindow : Form { @@ -23,8 +23,8 @@ public partial class MainWindow : Form { //private string file = "..\\..\\..\\..\\..\\..\\tests\\formfeeds.txt"; //private string file = "..\\..\\..\\..\\..\\..\\tests\\TEST.TXT"; //private string file = "..\\..\\..\\..\\..\\..\\tests\\long html doc as text.TXT"; - //private string file = @"C:\Users\ckindel\source\winprint\tests\test.html"; - private string file = ""; //@"..\..\..\..\..\..\proto\winforms\WinPrint\ViewModels\SheetViewModel.cs"; + // This is a very long comment to test wrapping of comments in source code. It goes on and on... + private string file = ""; // this is a long comment that extends way to the right in order to test wrapping [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] public MainWindow() { diff --git a/testfiles/_test.cs b/testfiles/_test.cs new file mode 100644 index 0000000..dee6078 --- /dev/null +++ b/testfiles/_test.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using WinPrint.Core.Services; + +// This is a very long comment to test wrapping of comments in source code. It starts at left column and it goes on and on... +using test; // This is a long comment that starts with a statement and extends way to the right in order to test wrapping. + +// private string file = "..\\..\\..\\..\\..\\..\\tests\\formfeeds.txt"; +// private string file = "..\\..\\..\\..\\..\\..\\tests\\TEST.TXT"; + // HAS TAB BEFORE private string file = "..\\..\\..\\..\\..\\..\\tests\\long html doc as text.TXT"; + +namespace WinPrint { + public partial class MainWindow : Form { + + // The Windows printer document + private PrintDocument printDoc = new PrintDocument(); + + // Winprint Print Preview control + Application.UseSystemConsole = _useSystemConsole; + Application.Init (); + Application.HeightAsBuffer = _heightAsBuffer; + + // Set this here because not initialized until driver is loaded + _baseColorScheme = Colors.Base; + + StringBuilder aboutMessage = new StringBuilder (); + aboutMessage.AppendLine (@""); + aboutMessage.AppendLine (@"UI Catalog is a comprehensive sample library for Terminal.Gui"); + aboutMessage.AppendLine (@""); + aboutMessage.AppendLine (@" _______ _ _ _____ _ "); + aboutMessage.appendline (@" |__ __| (_) | | / ____| (_)"); + aboutMessage. (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ "); + aboutMessage.1234567890 (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | |"); + aboutMessage.WWWWWWWWWW (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | |"); + aboutMessage........... (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_|"); + aboutMessage.AppendLine (@""); + aboutMessage.AppendLine ($"Using Terminal.Gui Version: {FileVersionInfo.GetVersionInfo (typeof (Terminal.Gui.Application).Assembly.Location).FileVersion}"); + aboutMessage.AppendLine (@""); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] + public MainWindow() { + InitializeComponent(); + + Icon = Resources.printersCB; + + if (LicenseManager.UsageMode != LicenseUsageMode.Designtime) { + this.panelRight.Controls.Remove(this.dummyButton); + this.panelRight.Controls.Add(this.printPreview); + printersCB.Enabled = false; + paperSizesCB.Enabled = false; + } + } + + bool disposed = false; + // Protected implementation of Dispose pattern. + protected override void Dispose(bool disposing) { + } + } +} + +// Last line \ No newline at end of file diff --git a/tests/WinPrint.Core.UnitTests/Cte/BaseCteTests.cs b/tests/WinPrint.Core.UnitTests/Cte/BaseCteTests.cs index 8feab87..6c534ba 100644 --- a/tests/WinPrint.Core.UnitTests/Cte/BaseCteTests.cs +++ b/tests/WinPrint.Core.UnitTests/Cte/BaseCteTests.cs @@ -151,7 +151,7 @@ public void CreateContentTypeEngineTests() input = "text"; (cte, contentType, language) = ContentTypeEngineBase.CreateContentTypeEngine(input); Assert.NotNull(cte); - Assert.Equal(ContentTypeEngineBase.DefaultCteClassName, cte.GetType().Name); + Assert.Equal(ModelLocator.Current.Settings.DefaultCteClassName, cte.GetType().Name); Assert.Equal("text/plain", contentType); Assert.Equal("Plain Text", language); @@ -194,7 +194,7 @@ public void CreateContentTypeEngineTests() input = "text/plain"; (cte, contentType, language) = ContentTypeEngineBase.CreateContentTypeEngine(input); Assert.NotNull(cte); - Assert.Equal(ContentTypeEngineBase.DefaultCteClassName, cte.GetType().Name); + Assert.Equal(ModelLocator.Current.Settings.DefaultCteClassName, cte.GetType().Name); Assert.Equal(input, contentType); Assert.Equal("Plain Text", language); diff --git a/tests/WinPrint.Core.UnitTests/Services/PygmentsConverterServiceTests.cs b/tests/WinPrint.Core.UnitTests/Services/PygmentsConverterServiceTests.cs index fc24c89..7d2849e 100644 --- a/tests/WinPrint.Core.UnitTests/Services/PygmentsConverterServiceTests.cs +++ b/tests/WinPrint.Core.UnitTests/Services/PygmentsConverterServiceTests.cs @@ -25,7 +25,7 @@ public async void ConvertAsyncTest() var input = $@"using system;"; // "using system;" | out-file using.cs // pygmentize -O 16m,style=friendly .\using.cs | out-file using.ans - var expectedOutput = "\u001b[38;2;0;112;32;01musing\u001b[39;00m \u001b[38;2;14;132;181;01msystem\u001b[39;00m;"; + var expectedOutput = "\u001b[38;2;0;112;32;01musing\u001b[39;00m\u001b[38;2;187;187;187m \u001b[39m\u001b[38;2;14;132;181;01msystem\u001b[39;00m;"; var output = await ps.ConvertAsync(input, "friendly", "c#"); Assert.Equal(expectedOutput, output); } diff --git a/tests/WinPrint.Core.UnitTests/WinPrint.Core.UnitTests.csproj b/tests/WinPrint.Core.UnitTests/WinPrint.Core.UnitTests.csproj index 8588aa0..c3fc12b 100644 --- a/tests/WinPrint.Core.UnitTests/WinPrint.Core.UnitTests.csproj +++ b/tests/WinPrint.Core.UnitTests/WinPrint.Core.UnitTests.csproj @@ -1,20 +1,22 @@ - netcoreapp3.1 + net6.0-windows false + + AnyCPU;x64 - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tools/package-lock.json b/tools/package-lock.json deleted file mode 100644 index d581869..0000000 --- a/tools/package-lock.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "clipboard": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz", - "integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==", - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" - }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", - "requires": { - "delegate": "^3.1.2" - } - }, - "lang-map": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/lang-map/-/lang-map-0.4.0.tgz", - "integrity": "sha1-3jiwd4Ogp9sLwILGxf9pxW41SrU=", - "requires": { - "language-map": "^1.1.0" - } - }, - "language-map": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/language-map/-/language-map-1.4.0.tgz", - "integrity": "sha512-5XHMCqKQ/14VXwJTKZX7MZwsVyTrR0bwCsRBwrBq3nP4w7liMJiR0ixXgI29EF/T1/U6be8cYK2y0ibWW9kx2w==" - }, - "prismjs": { - "version": ">=1.25.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz", - "integrity": "sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==", - "requires": { - "clipboard": "^2.0.0" - } - }, - "python-shell": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-1.0.8.tgz", - "integrity": "sha512-jMKagerg3alm6j+Prq5t/M3dTgEppy5vC6ns+LqAjfuHiT8olfK3PMokpqpeEcWEqvDnUcAOhp6SQzaLBtTzRw==" - }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" - }, - "tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - } - } -} diff --git a/tools/prism_languages.json b/tools/prism_languages.json deleted file mode 100644 index e69de29..0000000 diff --git a/tools/pygmentize.exe b/tools/pygmentize.exe index 574454d..16d6899 100644 Binary files a/tools/pygmentize.exe and b/tools/pygmentize.exe differ diff --git a/tools/pygments_lexers.py b/tools/pygments_lexers.py index 28ef2ac..e383014 100644 --- a/tools/pygments_lexers.py +++ b/tools/pygments_lexers.py @@ -1,3 +1,6 @@ +# This is needed because Pygments doesn't suppport `-L lexers`. +# See https://github.com/pygments/pygments/issues/1437 +# import pygments import json from pygments.lexers import get_all_lexers