Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CADability.Forms/CadCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ private static CADability.Substitutes.PaintEventArgs Subst(System.Windows.Forms.
private string currenToolTip;
private string currentCursor;
private Action<int> 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();
Expand Down
2 changes: 2 additions & 0 deletions CADability.Forms/CadControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }
/// <summary>
/// Default constructor
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions CADability.Forms/CadFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ namespace CADability.Forms
/// </summary>
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;
Expand Down
22 changes: 18 additions & 4 deletions CADability.Forms/GdiFontProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@
using System.Drawing.Drawing2D;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace CADability.Forms
{
/// <summary>
/// A <see cref="FontProvider"/> that gets the fonts from GDI and GDI+, as CADability did before issue #412: the glyph
/// outlines from <see cref="GraphicsPath.AddString(string, FontFamily, int, float, PointF, StringFormat)"/>, 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 <see cref="FontProvider.Current"/> to an instance of this class to use it;
/// the default is the <see cref="ManagedFontProvider"/>.
/// outlines from <see cref="GraphicsPath.AddString(string, FontFamily, int, float, PointF, StringFormat)"/>, 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 (<see cref="CadControl"/>, <see cref="CadCanvas"/>, <see cref="CadFrame"/>, <see cref="PaintToOpenGL"/> or
/// <see cref="PaintToGDI"/>), this provider becomes the default, so that Windows Forms applications show their texts
/// as before. An application that sets <see cref="FontProvider.Current"/> itself keeps its choice, e.g.
/// <c>FontProvider.Current = new ManagedFontProvider();</c> for the same fonts as on the other platforms.
/// </summary>
public class GdiFontProvider : FontProvider
{
private static int madeDefault;
/// <summary>
/// Makes a <see cref="GdiFontProvider"/> 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.
/// </summary>
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<string> familyNames; // upper case

Expand Down
2 changes: 2 additions & 0 deletions CADability.Forms/PaintToGDI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public void Dispose()
LazyInitializer.EnsureInitialized(ref fontFamilyNames, () =>
new HashSet<string>(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()
{
Expand Down
3 changes: 3 additions & 0 deletions CADability.Forms/PaintToOpenGL.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ internal PaintToOpenGLException(String msg)
/// </summary>
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;
Expand Down
49 changes: 42 additions & 7 deletions CADability/FontProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,63 @@ namespace CADability
/// Provides the fonts for <see cref="Text"/> and dimensions: the names of the font families, their metrics and the outlines
/// of the glyphs. The kernel gets all fonts from <see cref="Current"/>, which works without any user interface. The default
/// is a <see cref="ManagedFontProvider"/>, 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 <see cref="Current"/> itself.
/// </summary>
public abstract class FontProvider
{
private static FontProvider current;
private static bool chosenByApplication;
private static readonly object sync = new object();
/// <summary>
/// The font provider the kernel uses. Setting it clears the cached glyphs, so that texts are rebuilt with the new
/// provider, like <see cref="Text.ClearFontCache"/>: set it before texts are painted, or rebuild the display lists of
/// the views afterwards (<see cref="ProjectedModel.InvalidateDisplayLists"/>).
/// the views afterwards (<see cref="ProjectedModel.InvalidateDisplayLists"/>). A provider set here is kept:
/// CADability.Forms only replaces the default with its GDI provider while the application has not set one.
/// </summary>
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();
}
}
/// <summary>
/// Makes the provider made by <paramref name="create"/> the default, unless the application has set
/// <see cref="Current"/> itself. CADability.Forms calls this to use GDI for the fonts, as CADability did before issue #412.
/// </summary>
internal static void SetDefault(Func<FontProvider> 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();
}
/// <summary>
/// The names of all font families this provider knows, e.g. for a list to choose from.
/// </summary>
public abstract string[] GetFamilyNames();
Expand Down
2 changes: 2 additions & 0 deletions CADability/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
144 changes: 144 additions & 0 deletions tests/CADability.Tests/FontProviderDefaultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Reflection;

namespace CADability.Tests
{
/// <summary>
/// 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
/// <see cref="FontProvider"/>, so they do not run in parallel with other tests and restore it afterwards.
/// </summary>
[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);

/// <summary>
/// Runs <paramref name="test"/> as in a fresh process, where the application has not chosen a provider yet, and restores
/// the provider of the other tests afterwards.
/// </summary>
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
}
}
Loading