diff --git a/CADability.Forms/CadCanvas.cs b/CADability.Forms/CadCanvas.cs index b37d6969..67259ff7 100644 --- a/CADability.Forms/CadCanvas.cs +++ b/CADability.Forms/CadCanvas.cs @@ -61,6 +61,8 @@ private static CADability.Substitutes.PaintEventArgs Subst(System.Windows.Forms. private string currenToolTip; private string currentCursor; private Action callbackCollapsed; + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static CadCanvas() { GdiFontProvider.MakeDefault(); } public CadCanvas() : base() { toolTip = new ToolTip(); diff --git a/CADability.Forms/CadControl.cs b/CADability.Forms/CadControl.cs index d0fdb7d5..0f38fee6 100644 --- a/CADability.Forms/CadControl.cs +++ b/CADability.Forms/CadControl.cs @@ -23,6 +23,8 @@ public partial class CadControl : UserControl, ICommandHandler #endregion PRIVATE FIELDS + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static CadControl() { GdiFontProvider.MakeDefault(); } /// /// Default constructor /// diff --git a/CADability.Forms/CadFrame.cs b/CADability.Forms/CadFrame.cs index 8417224d..ca8df7a5 100644 --- a/CADability.Forms/CadFrame.cs +++ b/CADability.Forms/CadFrame.cs @@ -20,6 +20,9 @@ namespace CADability.Forms /// public class CadFrame : FrameImpl, IUIService { + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static CadFrame() { GdiFontProvider.MakeDefault(); } + #region PRIVATE FIELDS private ICommandHandler commandHandler; diff --git a/CADability.Forms/GdiFontProvider.cs b/CADability.Forms/GdiFontProvider.cs index 846e3d9e..2875cd91 100644 --- a/CADability.Forms/GdiFontProvider.cs +++ b/CADability.Forms/GdiFontProvider.cs @@ -4,18 +4,32 @@ using System.Drawing.Drawing2D; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; namespace CADability.Forms { /// /// A that gets the fonts from GDI and GDI+, as CADability did before issue #412: the glyph - /// outlines from , the widths and - /// the kerning from gdi32. It knows every font Windows knows (also OpenType fonts with CFF outlines) and synthesizes bold - /// and italic faces a family does not have. Set to an instance of this class to use it; - /// the default is the . + /// outlines from , the widths + /// and the kerning from gdi32. It knows every font Windows knows (also OpenType fonts with CFF outlines) and + /// synthesizes bold and italic faces a family does not have. As soon as one of the main classes of CADability.Forms is + /// used (, , , or + /// ), this provider becomes the default, so that Windows Forms applications show their texts + /// as before. An application that sets itself keeps its choice, e.g. + /// FontProvider.Current = new ManagedFontProvider(); for the same fonts as on the other platforms. /// public class GdiFontProvider : FontProvider { + private static int madeDefault; + /// + /// Makes a the default font provider, unless the application has chosen one. Called by the + /// static constructors of the main classes of CADability.Forms; only the first call does something. + /// + internal static void MakeDefault() + { + if (Interlocked.Exchange(ref madeDefault, 1) == 0) SetDefault(() => new GdiFontProvider()); + } + private readonly Dictionary<(string family, bool bold, bool italic), GdiFontFace> faces = new Dictionary<(string, bool, bool), GdiFontFace>(); private HashSet familyNames; // upper case diff --git a/CADability.Forms/PaintToGDI.cs b/CADability.Forms/PaintToGDI.cs index 6f16beaa..93909014 100644 --- a/CADability.Forms/PaintToGDI.cs +++ b/CADability.Forms/PaintToGDI.cs @@ -65,6 +65,8 @@ public void Dispose() LazyInitializer.EnsureInitialized(ref fontFamilyNames, () => new HashSet(FontFamily.Families.Select(f => f.Name.ToUpper()))); + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static PaintToGDI() { GdiFontProvider.MakeDefault(); } public PaintToGDI(Projection projection, Graphics graphics) : this() { diff --git a/CADability.Forms/PaintToOpenGL.cs b/CADability.Forms/PaintToOpenGL.cs index b5d13251..2efd9271 100644 --- a/CADability.Forms/PaintToOpenGL.cs +++ b/CADability.Forms/PaintToOpenGL.cs @@ -28,6 +28,9 @@ internal PaintToOpenGLException(String msg) /// public class PaintToOpenGL : IPaintTo3D, IPaintTo3DFlatText { + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static PaintToOpenGL() { GdiFontProvider.MakeDefault(); } + #region IPaintTo3D data public static int debugNumTriangles = 0; public static Thread MainThread = null; diff --git a/CADability/FontProvider.cs b/CADability/FontProvider.cs index 634c1c56..2336a899 100644 --- a/CADability/FontProvider.cs +++ b/CADability/FontProvider.cs @@ -13,28 +13,63 @@ namespace CADability /// Provides the fonts for and dimensions: the names of the font families, their metrics and the outlines /// of the glyphs. The kernel gets all fonts from , which works without any user interface. The default /// is a , which reads the TrueType font files of the system itself and therefore works on - /// every platform. CADability.Forms contains a provider based on GDI (GdiFontProvider) for applications that want the - /// fonts exactly as Windows renders them. + /// every platform. CADability.Forms contains a provider based on GDI (GdiFontProvider), which gives the fonts exactly as + /// CADability showed them on Windows before; as soon as CADability.Forms is used, it makes that provider the default, + /// unless the application has set itself. /// public abstract class FontProvider { private static FontProvider current; + private static bool chosenByApplication; + private static readonly object sync = new object(); /// /// The font provider the kernel uses. Setting it clears the cached glyphs, so that texts are rebuilt with the new /// provider, like : set it before texts are painted, or rebuild the display lists of - /// the views afterwards (). + /// the views afterwards (). A provider set here is kept: + /// CADability.Forms only replaces the default with its GDI provider while the application has not set one. /// public static FontProvider Current { - get => LazyInitializer.EnsureInitialized(ref current, () => new ManagedFontProvider()); + get + { + FontProvider res = Volatile.Read(ref current); + if (res != null) return res; + lock (sync) + { + if (current == null) current = new ManagedFontProvider(); + return current; + } + } set { - current = value ?? throw new ArgumentNullException(nameof(value)); - FontCache.GlobalFontCache.Clear(); - Text.ResetFontFamilyNames(); + if (value == null) throw new ArgumentNullException(nameof(value)); + lock (sync) + { + chosenByApplication = true; + current = value; + } + ClearCaches(); } } /// + /// Makes the provider made by the default, unless the application has set + /// itself. CADability.Forms calls this to use GDI for the fonts, as CADability did before issue #412. + /// + internal static void SetDefault(Func create) + { + lock (sync) + { + if (chosenByApplication) return; + current = create(); + } + ClearCaches(); + } + private static void ClearCaches() + { // outside the lock: the caches have locks of their own, and a thread holding one of them may ask for Current + FontCache.GlobalFontCache.Clear(); + Text.ResetFontFamilyNames(); + } + /// /// The names of all font families this provider knows, e.g. for a list to choose from. /// public abstract string[] GetFamilyNames(); diff --git a/CADability/Properties/AssemblyInfo.cs b/CADability/Properties/AssemblyInfo.cs index 6fac604a..daaf7f73 100644 --- a/CADability/Properties/AssemblyInfo.cs +++ b/CADability/Properties/AssemblyInfo.cs @@ -3,3 +3,5 @@ [assembly: CLSCompliant(true)] // the unit tests (tests/CADability.Tests, signed with CADabilityKey.snk) may use internal classes like FontCache [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CADability.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1ad8b0ed092aad57de8d0423919856eab910629e254ad40d6de3709a87cc161162d71827d65177e862b4822364c691d32f20beb81f0f7c17690662c2b397fe7bf556ac85e9dc66c7de56f435df1c5899a22b6fde65c423c1ec4fe3e4bb316838dbc7332ff1d31995a1657f754f942b36b82787d4c9c8e4325b4fb2871bdedbb")] +// CADability.Forms makes its GDI font provider the default (FontProvider.SetDefault) +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CADability.Forms, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1ad8b0ed092aad57de8d0423919856eab910629e254ad40d6de3709a87cc161162d71827d65177e862b4822364c691d32f20beb81f0f7c17690662c2b397fe7bf556ac85e9dc66c7de56f435df1c5899a22b6fde65c423c1ec4fe3e4bb316838dbc7332ff1d31995a1657f754f942b36b82787d4c9c8e4325b4fb2871bdedbb")] diff --git a/tests/CADability.Tests/FontProviderDefaultTests.cs b/tests/CADability.Tests/FontProviderDefaultTests.cs new file mode 100644 index 00000000..89b45b19 --- /dev/null +++ b/tests/CADability.Tests/FontProviderDefaultTests.cs @@ -0,0 +1,144 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; + +namespace CADability.Tests +{ + /// + /// CADability.Forms makes its GDI font provider the default, so that Windows Forms applications show their texts as before + /// issue #412, but a provider the application has set itself is kept. The tests change the static state of + /// , so they do not run in parallel with other tests and restore it afterwards. + /// + [TestClass] + public class FontProviderDefaultTests + { + private sealed class NamedProvider : FontProvider + { + public readonly string Name; + public NamedProvider(string name) { Name = name; } + public override string[] GetFamilyNames() => new string[0]; + public override FontFace GetFace(string familyName, bool bold, bool italic) => null; + } + + private static readonly FieldInfo current = typeof(FontProvider).GetField("current", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly FieldInfo chosenByApplication = typeof(FontProvider).GetField("chosenByApplication", BindingFlags.NonPublic | BindingFlags.Static); + + /// + /// Runs as in a fresh process, where the application has not chosen a provider yet, and restores + /// the provider of the other tests afterwards. + /// + private static void AsInAFreshProcess(Action test) + { + object savedCurrent = current.GetValue(null), savedChosen = chosenByApplication.GetValue(null); + try + { + current.SetValue(null, null); + chosenByApplication.SetValue(null, false); + test(); + } + finally + { + current.SetValue(null, savedCurrent); + chosenByApplication.SetValue(null, savedChosen); + CADability.GeoObject.FontCache.GlobalFontCache.Clear(); + CADability.GeoObject.Text.ResetFontFamilyNames(); + } + } + + [TestMethod] + [DoNotParallelize] + public void TheDefaultIsTheManagedProvider() + { + AsInAFreshProcess(() => Assert.IsInstanceOfType(FontProvider.Current, typeof(ManagedFontProvider))); + } + + [TestMethod] + [DoNotParallelize] + public void AFrontEndCanReplaceTheDefault() + { + AsInAFreshProcess(() => + { + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("front end", ((NamedProvider)FontProvider.Current).Name); + }); + // also after the managed default was already used + AsInAFreshProcess(() => + { + Assert.IsInstanceOfType(FontProvider.Current, typeof(ManagedFontProvider)); + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("front end", ((NamedProvider)FontProvider.Current).Name); + }); + } + + [TestMethod] + [DoNotParallelize] + public void TheProviderOfTheApplicationIsKept() + { + // set before the front end is loaded + AsInAFreshProcess(() => + { + FontProvider.Current = new NamedProvider("application"); + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("application", ((NamedProvider)FontProvider.Current).Name); + }); + // set after the front end is loaded + AsInAFreshProcess(() => + { + FontProvider.SetDefault(() => new NamedProvider("front end")); + FontProvider.Current = new NamedProvider("application"); + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("application", ((NamedProvider)FontProvider.Current).Name); + }); + } + +#if WINDOWS + private static readonly MethodInfo makeDefault = typeof(CADability.Forms.GdiFontProvider).GetMethod("MakeDefault", BindingFlags.NonPublic | BindingFlags.Static); + + [TestMethod] + [DoNotParallelize] + public void CADabilityFormsMakesGdiTheDefault() + { + FieldInfo madeDefault = typeof(CADability.Forms.GdiFontProvider).GetField("madeDefault", BindingFlags.NonPublic | BindingFlags.Static); + object savedMadeDefault = madeDefault.GetValue(null); + try + { + AsInAFreshProcess(() => + { + madeDefault.SetValue(null, 0); + makeDefault.Invoke(null, null); + Assert.IsInstanceOfType(FontProvider.Current, typeof(CADability.Forms.GdiFontProvider)); + }); + AsInAFreshProcess(() => + { + madeDefault.SetValue(null, 0); + FontProvider.Current = new ManagedFontProvider(); + makeDefault.Invoke(null, null); + Assert.IsInstanceOfType(FontProvider.Current, typeof(ManagedFontProvider), "the choice of the application is kept"); + }); + } + finally + { + madeDefault.SetValue(null, savedMadeDefault); + } + } + + [TestMethod] + [DataRow(typeof(CADability.Forms.CadControl))] + [DataRow(typeof(CADability.Forms.CadCanvas))] + [DataRow(typeof(CADability.Forms.CadFrame))] + [DataRow(typeof(CADability.Forms.PaintToOpenGL))] + [DataRow(typeof(CADability.Forms.PaintToGDI))] + public void TheMainClassesOfFormsMakeGdiTheDefault(Type formsClass) + { + // the static constructor calls GdiFontProvider.MakeDefault: look for "call MakeDefault" in its IL + ConstructorInfo cctor = formsClass.TypeInitializer; + Assert.IsNotNull(cctor, formsClass.Name + " has no static constructor"); + byte[] il = cctor.GetMethodBody().GetILAsByteArray(); + byte[] token = BitConverter.GetBytes(makeDefault.MetadataToken); + bool found = false; + for (int i = 0; i + 4 < il.Length && !found; ++i) + found = il[i] == 0x28 && il[i + 1] == token[0] && il[i + 2] == token[1] && il[i + 3] == token[2] && il[i + 4] == token[3]; + Assert.IsTrue(found, formsClass.Name + " does not call GdiFontProvider.MakeDefault"); + } +#endif + } +}