diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/FontRegistry.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/FontRegistry.java index 484b092d51b..a49b8f57177 100644 --- a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/FontRegistry.java +++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/FontRegistry.java @@ -64,6 +64,10 @@ @NoExtend public class FontRegistry extends ResourceRegistry { + private enum FontStyle { + NORMAL, BOLD, ITALIC + } + /** * FontRecord is a private helper class that holds onto a font * and can be used to generate its bold and italic version. @@ -109,16 +113,29 @@ void dispose() { * Return the base Font. * @return Font */ - public Font getBaseFont() { + private Font getBaseFont() { return baseFont; } + /** + * Return the font for the given style, creating it lazily if necessary. + * @param style the requested style + * @return the font + */ + Font get(FontStyle style) { + return switch (style) { + case NORMAL -> getBaseFont(); + case BOLD -> getBoldFont(); + case ITALIC -> getItalicFont(); + }; + } + /** * Return the bold Font. Create a bold version * of the base font to get it. * @return Font */ - public Font getBoldFont() { + private Font getBoldFont() { if (boldFont != null) { return boldFont; } @@ -132,6 +149,20 @@ public Font getBoldFont() { return boldFont; } + /** + * Returns whether the given style has already been realized for this + * record. + * @param style the style to check + * @return whether the given style is already available + */ + boolean has(FontStyle style) { + return switch (style) { + case NORMAL -> baseFont != null; + case BOLD -> boldFont != null; + case ITALIC -> italicFont != null; + }; + } + /** * Get a version of the base font data with the specified * style. @@ -158,7 +189,7 @@ private FontData[] getModifiedFontData(int style) { * base font to get it. * @return Font */ - public Font getItalicFont() { + private Font getItalicFont() { if (italicFont != null) { return italicFont; } @@ -236,7 +267,7 @@ void invalidate(String symbolicName) { return; } FontRecord defaultRecord = records.get(JFaceResources.DEFAULT_FONT); - Font defaultFont = defaultRecord != null ? defaultRecord.getBaseFont() : null; + Font defaultFont = defaultRecord != null ? defaultRecord.get(FontStyle.NORMAL) : null; replacedRecord.getAllocatedFonts().stream().filter(font -> font != defaultFont).forEach(staleFonts::add); } @@ -609,7 +640,16 @@ Font calculateDefaultFont() { * @return Font */ public Font defaultFont() { - return defaultFontRecord().getBaseFont(); + return defaultFont(FontStyle.NORMAL); + } + + /** + * Return the default font in the given style, creating it if necessary. + * @param style the requested style + * @return the font + */ + private Font defaultFont(FontStyle style) { + return defaultFontRecord(style).get(style); } /** @@ -630,16 +670,20 @@ public FontDescriptor getDescriptor(String symbolicName) { /** - * Returns the default font record. + * Return the default font record that can provide the passed style, creating it + * if necessary. The font record may provide fonts for the current or the main + * display. Only the passed style is guaranteed to be available on the returned + * record. + * + * @param style the requested style + * @return the font record, never null */ - private FontRecord defaultFontRecord() { - FontRecord record = getExistingFontRecord(JFaceResources.DEFAULT_FONT); + private FontRecord defaultFontRecord(FontStyle style) { + FontRecord record = getExistingFontRecord(JFaceResources.DEFAULT_FONT, style); if (record == null && Display.getCurrent() == null) { - // No display for the current thread, so there is none to scope the - // lookup to and none to create a font on. Fall back to the default - // font already realized on the main display, which outlives every - // other display, rather than fail outright. Reached e.g. from - // getFontRecord()'s non-UI-thread fallback. + // no display to scope the lookup to and none to create a font on: use the + // main display's record even if the requested style is not realized there + // yet, so the style gets realized on that display rather than failing DisplayFontRecords mainDisplayRecords = displayToFontRecords.get(mainDisplay); if (mainDisplayRecords != null) { record = mainDisplayRecords.get(JFaceResources.DEFAULT_FONT); @@ -663,24 +707,65 @@ record = createFont(JFaceResources.DEFAULT_FONT, defaultFont.getFontData()); } /** - * Looks up an already-realized font record for the given symbolic name on - * the current display. Returns null if there is none, in which - * case the caller is responsible for creating one on the current display. + * Looks up an already-realized font record for the given symbolic name and + * style. A record the current display has already realized the exact style + * on is used first, so repeated lookups from that display keep returning the + * same font. Otherwise the main display's record is used if it already has + * that style, so callers from any display, including a thread with no + * Display of its own, reuse it instead of allocating a duplicate. Failing + * both, the current display's own record is returned even though it lacks + * the requested style, so that the style gets realized on the display owning + * the record; null is returned only if the current display has + * no record for the name at all, in which case the caller is responsible for + * creating one on it. + *

+ * Handing out the main display's font to another display is safe because + * the main display is assumed to outlive every other display the registry + * is used from, so the font cannot be disposed while another display is + * still using it. + *

+ *

+ * Requiring the exact style to be present is also what keeps the lazy + * creation of styled fonts single-threaded: a record is only ever handed to + * a foreign display once the style it asks for has been realized, so only + * the record's own display ever reaches the creating branch of + * {@link FontRecord#get(FontStyle)}. Note that for {@link FontStyle#NORMAL} + * every existing record matches, since that font is realized when the record + * is created. + *

*/ - private FontRecord getExistingFontRecord(String symbolicName) { + private FontRecord getExistingFontRecord(String symbolicName, FontStyle style) { + FontRecord recordOnCurrentDisplay = null; Display currentDisplay = Display.getCurrent(); - if (currentDisplay == null) { - return null; + if (currentDisplay != null) { + DisplayFontRecords currentDisplayRecords = displayToFontRecords.get(currentDisplay); + if (currentDisplayRecords != null) { + recordOnCurrentDisplay = currentDisplayRecords.get(symbolicName); + // a style this display already realized itself stays the one it gets, so + // repeated lookups keep returning the same font instance + if (recordOnCurrentDisplay != null && recordOnCurrentDisplay.has(style)) { + return recordOnCurrentDisplay; + } + } } - DisplayFontRecords currentDisplayRecords = displayToFontRecords.get(currentDisplay); - return currentDisplayRecords != null ? currentDisplayRecords.get(symbolicName) : null; + + DisplayFontRecords mainDisplayRecords = displayToFontRecords.get(mainDisplay); + if (mainDisplayRecords != null) { + FontRecord recordOnMainDisplay = mainDisplayRecords.get(symbolicName); + // Only return main display record if exact font style already exists + if (recordOnMainDisplay != null && recordOnMainDisplay.has(style)) { + return recordOnMainDisplay; + } + } + + return recordOnCurrentDisplay; } /** * Returns the default font data. Creates it if necessary. */ private FontData[] defaultFontData() { - return defaultFontRecord().baseData; + return defaultFontRecord(FontStyle.NORMAL).baseData; } /** @@ -717,8 +802,7 @@ public FontData[] getFontData(String symbolicName) { * @return the font */ public Font get(String symbolicName) { - - return getFontRecord(symbolicName).getBaseFont(); + return getFont(symbolicName, FontStyle.NORMAL); } /** @@ -737,8 +821,7 @@ public Font get(String symbolicName) { * @since 3.0 */ public Font getBold(String symbolicName) { - - return getFontRecord(symbolicName).getBoldFont(); + return getFont(symbolicName, FontStyle.BOLD); } /** @@ -757,20 +840,20 @@ public Font getBold(String symbolicName) { * @since 3.0 */ public Font getItalic(String symbolicName) { - - return getFontRecord(symbolicName).getItalicFont(); + return getFont(symbolicName, FontStyle.ITALIC); } /** - * Return the font record for the key. + * Return the font for the given key and style. * @param symbolicName The key for the record. - * @return FontRecord + * @param style the requested style + * @return the font */ - private FontRecord getFontRecord(String symbolicName) { + private Font getFont(String symbolicName, FontStyle style) { Assert.isNotNull(symbolicName); - FontRecord existingRecord = getExistingFontRecord(symbolicName); + FontRecord existingRecord = getExistingFontRecord(symbolicName, style); if (existingRecord != null) { - return existingRecord; + return existingRecord.get(style); } FontData[] existingFontData = stringToFontData.get(symbolicName); @@ -778,20 +861,20 @@ private FontRecord getFontRecord(String symbolicName) { FontRecord fontRecord; if (existingFontData == null) { - fontRecord = defaultFontRecord(); + fontRecord = defaultFontRecord(style); } else { fontRecord = createFont(symbolicName, existingFontData); } if (fontRecord == null) { - fontRecord = defaultFontRecord(); + fontRecord = defaultFontRecord(style); if (Display.getCurrent() == null) { // log error but don't throw an exception to preserve existing functionality String msg = "Unable to create font \"" + symbolicName + "\" in a non-UI thread. Using default font instead."; //$NON-NLS-1$ //$NON-NLS-2$ Policy.logException(new SWTException(msg)); } } - return fontRecord; + return fontRecord.get(style); } @Override diff --git a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/resources/FontRegistryTest.java b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/resources/FontRegistryTest.java index 70a0ceea1ad..bbb9ff0f257 100644 --- a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/resources/FontRegistryTest.java +++ b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/resources/FontRegistryTest.java @@ -97,6 +97,101 @@ public void multipleDisplayDispose_italicFont() { testMultipleDisplayDispose(() -> fontRegistry.getItalic(JFaceResources.DEFAULT_FONT)); } + @Test + public void multipleDisplay_reusesMainDisplayFont_whenStyleAlreadyCached() { + assumeTrue(OS.isWindows(), "multiple Display instance only allowed on Windows"); + + FontRegistry fontRegistry = new FontRegistry(); + fontRegistry.put("myfont", new FontData[] { new FontData("Arial", 12, SWT.NORMAL) }); + Font mainFont = fontRegistry.get("myfont"); + + Display secondDisplay = initializeDisplayInSeparateThread(); + try { + Font fontFromSecondDisplayThread = secondDisplay.syncCall(() -> fontRegistry.get("myfont")); + assertEquals(mainFont, fontFromSecondDisplayThread, + "a font already realized on the main display should be reused from any other display's thread"); + assertEquals(Display.getCurrent(), fontFromSecondDisplayThread.getDevice(), + "the reused font is still owned by the main display"); + } finally { + secondDisplay.syncExec(secondDisplay::dispose); + } + } + + @Test + public void multipleDisplay_createsOwnFont_whenStyleNotYetCachedOnMainDisplay() { + assumeTrue(OS.isWindows(), "multiple Display instance only allowed on Windows"); + + FontRegistry fontRegistry = new FontRegistry(); + fontRegistry.put("myfont", new FontData[] { new FontData("Arial", 12, SWT.NORMAL) }); + fontRegistry.get("myfont"); // only realizes the NORMAL style on the main display + + Display secondDisplay = initializeDisplayInSeparateThread(); + Font boldFontOnSecondDisplay; + try { + boldFontOnSecondDisplay = secondDisplay.syncCall(() -> fontRegistry.getBold("myfont")); + + assertEquals(secondDisplay, boldFontOnSecondDisplay.getDevice(), + "bold style is not yet cached on the main display, so it must be created on the requesting display"); + } finally { + secondDisplay.syncExec(secondDisplay::dispose); + } + assertTrue(boldFontOnSecondDisplay.isDisposed(), + "fonts created for the second display must be disposed together with it"); + } + + @Test + public void multipleDisplay_reusesMainDisplayFont_onceStyleBecomesCachedThere() { + assumeTrue(OS.isWindows(), "multiple Display instance only allowed on Windows"); + + FontRegistry fontRegistry = new FontRegistry(); + fontRegistry.put("myfont", new FontData[] { new FontData("Arial", 12, SWT.NORMAL) }); + fontRegistry.get("myfont"); // only realizes the NORMAL style on the main display + + Display firstSecondDisplay = initializeDisplayInSeparateThread(); + Font boldFontOnFirstSecondDisplay = firstSecondDisplay.syncCall(() -> fontRegistry.getBold("myfont")); + assertEquals(firstSecondDisplay, boldFontOnFirstSecondDisplay.getDevice(), + "bold style is not yet cached on the main display, so it must be created on the requesting display"); + firstSecondDisplay.syncExec(firstSecondDisplay::dispose); + + // the main display now also realizes the bold style + Font boldFontOnMainDisplay = fontRegistry.getBold("myfont"); + + Display secondSecondDisplay = initializeDisplayInSeparateThread(); + try { + Font boldFontOnSecondSecondDisplay = secondSecondDisplay.syncCall(() -> fontRegistry.getBold("myfont")); + assertEquals(boldFontOnMainDisplay, boldFontOnSecondSecondDisplay, + "once the main display has realized the requested style, later lookups from any display must reuse it"); + } finally { + secondSecondDisplay.syncExec(secondSecondDisplay::dispose); + } + } + + @Test + public void multipleDisplay_keepsOwnFont_whenMainDisplayRealizesItLater() { + assumeTrue(OS.isWindows(), "multiple Display instance only allowed on Windows"); + + FontRegistry fontRegistry = new FontRegistry(); + fontRegistry.put("myfont", new FontData[] { new FontData("Arial", 12, SWT.NORMAL) }); + + Display secondDisplay = initializeDisplayInSeparateThread(); + try { + // the second display realizes the font before the main display has one to reuse + Font fontOnSecondDisplay = secondDisplay.syncCall(() -> fontRegistry.get("myfont")); + assertEquals(secondDisplay, fontOnSecondDisplay.getDevice(), + "nothing to reuse yet, so the second display must realize a font of its own"); + + // the main display realizing the same font afterwards must not change what + // the second display gets, or it would silently switch instances mid-flight + fontRegistry.get("myfont"); + + Font fontOnSecondDisplayAgain = secondDisplay.syncCall(() -> fontRegistry.get("myfont")); + assertSame(fontOnSecondDisplay, fontOnSecondDisplayAgain, + "a display that realized a font itself must keep getting that same instance"); + } finally { + secondDisplay.syncExec(secondDisplay::dispose); + } + } + @Test public void put_invalidatesCachedFont_onAllDisplays() { assumeTrue(OS.isWindows(), "multiple Display instance only allowed on Windows"); @@ -160,6 +255,9 @@ private static void testMultipleDisplayDispose(Supplier fontSupplier) { assumeTrue(OS.isWindows(), "multiple Display instance only allowed on Windows"); Display secondDisplay = initializeDisplayInSeparateThread(); + // the second display is asked first on purpose: the requested font is not + // realized on the main display yet, so there is nothing to reuse and the + // second display has to realize (and own) a font of its own Font fontOnSecondDisplay = secondDisplay.syncCall(fontSupplier::get); Font fontOnThisDisplayBeforeSecondDisplayDispose = fontSupplier.get();