From f27d100afd1f10a029ff362413d2b002f21a348f Mon Sep 17 00:00:00 2001 From: Jerome Lelasseux Date: Sun, 10 Nov 2019 23:56:10 +0100 Subject: [PATCH] Fixed Issue #10 : b/# bad display in chord symbols on Mac OSX Retina. Improved Time Signature rendering. --- .../ui/cl_editor/barrenderer/BR_Chords.java | 14 +- ItemRenderer/nbproject/genfiles.properties | 8 +- ItemRenderer/nbproject/project.xml | 8 + .../jjazz/ui/itemrenderer/IR_ChordSymbol.java | 254 ++++-------------- .../IR_ChordSymbolSettingsImpl.java | 22 +- .../ui/itemrenderer/IR_TimeSignature.java | 86 +++--- .../api/IR_ChordSymbolSettings.java | 8 +- .../resources/ScaleDegrees-Times.ttf | Bin 0 -> 11636 bytes .../jjazz/ui/itemrenderer/resources/marl.ttf | Bin 42308 -> 0 bytes .../jjazz/ui/utilities/TextLayoutUtils.java | 203 ++++++++------ 10 files changed, 257 insertions(+), 346 deletions(-) create mode 100644 ItemRenderer/src/org/jjazz/ui/itemrenderer/resources/ScaleDegrees-Times.ttf delete mode 100644 ItemRenderer/src/org/jjazz/ui/itemrenderer/resources/marl.ttf diff --git a/CL_Editor/src/org/jjazz/ui/cl_editor/barrenderer/BR_Chords.java b/CL_Editor/src/org/jjazz/ui/cl_editor/barrenderer/BR_Chords.java index c258531e34..0a08e2c9e2 100644 --- a/CL_Editor/src/org/jjazz/ui/cl_editor/barrenderer/BR_Chords.java +++ b/CL_Editor/src/org/jjazz/ui/cl_editor/barrenderer/BR_Chords.java @@ -344,7 +344,7 @@ public PrefSizePanel() ChordLeadSheetItem item1 = null, item2; try { - item1 = clif.createChordSymbol(null, new ExtChordSymbol("C7#9"), new Position(0, 0)); + item1 = clif.createChordSymbol(null, new ExtChordSymbol("C#7#9b5"), new Position(0, 0)); } catch (ParseException ex) { Exceptions.printStackTrace(ex); @@ -388,15 +388,21 @@ public Dimension getPreferredSize() } } - int V_MARGIN = 1; + int V_PADDING; if (zoomVFactor > 66) { - V_MARGIN = 2; + V_PADDING = 3; + } else if (zoomVFactor > 33) + { + V_PADDING = 2; + } else + { + V_PADDING = 1; } Insets in = getInsets(); int pWidth = irWidthSum + irs.size() * 5 + in.left + in.right; - int pHeight = irMaxHeight + 2 * V_MARGIN + in.top + in.bottom; + int pHeight = irMaxHeight + V_PADDING + in.top + in.bottom; Dimension d = new Dimension(pWidth, pHeight); // LOGGER2.severe("PrefSizePanel.getPreferredSize() d=" + d); diff --git a/ItemRenderer/nbproject/genfiles.properties b/ItemRenderer/nbproject/genfiles.properties index 6a557aad8a..69fb3a434c 100644 --- a/ItemRenderer/nbproject/genfiles.properties +++ b/ItemRenderer/nbproject/genfiles.properties @@ -1,8 +1,8 @@ -build.xml.data.CRC32=2a4c4752 +build.xml.data.CRC32=2944459f build.xml.script.CRC32=d2bd02d2 -build.xml.stylesheet.CRC32=15ca8a54@2.76.1 +build.xml.stylesheet.CRC32=15ca8a54@2.77 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=2a4c4752 +nbproject/build-impl.xml.data.CRC32=2944459f nbproject/build-impl.xml.script.CRC32=f4d95b06 -nbproject/build-impl.xml.stylesheet.CRC32=49aa68b0@2.76.1 +nbproject/build-impl.xml.stylesheet.CRC32=49aa68b0@2.77 diff --git a/ItemRenderer/nbproject/project.xml b/ItemRenderer/nbproject/project.xml index b0b705bc3b..901ddfb1c2 100644 --- a/ItemRenderer/nbproject/project.xml +++ b/ItemRenderer/nbproject/project.xml @@ -46,6 +46,14 @@ 1.0 + + org.openide.modules + + + + 7.53 + + org.openide.util diff --git a/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbol.java b/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbol.java index 327014f627..fa357673c7 100644 --- a/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbol.java +++ b/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbol.java @@ -24,8 +24,10 @@ import java.awt.*; import java.awt.font.FontRenderContext; -import java.awt.font.GlyphVector; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; import java.beans.PropertyChangeEvent; +import java.text.AttributedString; import java.util.ArrayList; import java.util.logging.Logger; import org.jjazz.harmony.StandardScaleInstance; @@ -40,6 +42,7 @@ import org.jjazz.ui.itemrenderer.api.IR_Copiable; import org.jjazz.ui.itemrenderer.api.IR_Type; import org.jjazz.ui.itemrenderer.api.ItemRenderer; +import org.jjazz.ui.utilities.TextLayoutUtils; /** * An ItemRenderer for ChordSymbols. @@ -51,59 +54,11 @@ public class IR_ChordSymbol extends ItemRenderer implements IR_Copiable { - private static final float EXT_FACTOR_SIZE = 0.8f; - private static final float EXT_FACTOR_OFFSET_HEIGHT = 0.3f; - private static final float SHARP_BASE_FACTOR_SIZE = (14f / 16); - private static final float SHARP_EXT_FACTOR_SIZE = (14f / 16); - private static final float FLAT_BASE_FACTOR_SIZE = (12f / 16); - private static final float FLAT_EXT_FACTOR_SIZE = (12f / 16); - // The fonts for the Sharp & Flat - private Font sharpBaseFont; - private Font sharpExtensionFont; - private Font flatBaseFont; - private Font flatExtensionFont; - /** - * List of the glyphVectors to draw the base of the ChordEvent. - */ - private final ArrayList baseGlyphVectors = new ArrayList<>(); - /** - * Width of each base GlyphVector. - */ - private final ArrayList baseGlyphVectorsWidths = new ArrayList<>(); - /** - * List of the glyphVectors to draw the bass part of the base of the ChordEvent. - */ - private final ArrayList bassGlyphVectors = new ArrayList<>(); - /** - * Width of each base bassGlyphVector. - */ - private final ArrayList bassGlyphVectorsWidths = new ArrayList<>(); - /** - * List of the glyphVectors to draw the extension of the ChordEvent. - */ - private final ArrayList extensionGlyphVectors = new ArrayList<>(); - /** - * Width of each extension GlyphVector. - */ - private final ArrayList extensionGlyphVectorsWidths = new ArrayList<>(); - /** - * Font used for the extension part. - */ - private Font extensionFont; - /** - * Offset of the height of the extension part compared to the base. - */ - private float extensionOffset; - /** - * Copy mode. - */ + private static final float MUSIC_FONT_BASE_FACTOR_SIZE = (14f / 16); + private AttributedString attChordString; private boolean copyMode; - /** - * The settings for this object. - */ private IR_ChordSymbolSettings settings; private int zoomFactor = 50; - private boolean isMacOS = false; private static final Logger LOGGER = Logger.getLogger(IR_ChordSymbol.class.getSimpleName()); @SuppressWarnings("LeakingThisInConstructor") @@ -119,7 +74,6 @@ public IR_ChordSymbol(CLI_ChordSymbol item) settings.addPropertyChangeListener(this); setForeground(item.getData().getAlternateChordSymbol() == null ? settings.getColor() : settings.getAltColor()); setFont(settings.getFont()); - isMacOS = System.getProperty("os.name").toLowerCase().contains("mac"); } @Override @@ -135,49 +89,30 @@ public void modelChanged() /** * Calculate the preferredSize() depending on chord symbol, font and zoomFactor. *

- * Also precalculate data to speed it paintComponent(). - *

*/ @Override public Dimension getPreferredSize() { + // The chord symbol to show ExtChordSymbol ecs = (ExtChordSymbol) getModel().getData(); - Font f = getFont(); - int zFactor = getZoomFactor(); - Graphics2D g2 = (Graphics2D) getGraphics(); - assert g2 != null : "g2=" + g2 + " ecs=" + ecs + " f=" + f + " zFactor=" + zFactor; + // Prepare the graphics context + Graphics2D g2 = (Graphics2D) getGraphics(); + assert g2 != null; g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - float factor = 0.5f + (zFactor / 100f); - float zBaseFontSize = factor * f.getSize2D(); - zBaseFontSize = Math.max(zBaseFontSize, 11); - Font zBaseFont = f.deriveFont(zBaseFontSize); - - // Update fonts - extensionFont = zBaseFont.deriveFont(zBaseFontSize * EXT_FACTOR_SIZE); - sharpBaseFont = settings.getMusicFont().deriveFont(zBaseFontSize * SHARP_BASE_FACTOR_SIZE); - sharpExtensionFont = settings.getMusicFont().deriveFont(zBaseFontSize * SHARP_EXT_FACTOR_SIZE); - flatBaseFont = settings.getMusicFont().deriveFont(zBaseFontSize * FLAT_BASE_FACTOR_SIZE); - flatExtensionFont = settings.getMusicFont().deriveFont(zBaseFontSize * FLAT_EXT_FACTOR_SIZE); - - // Update extensionOffset - float baseHeight = g2.getFontMetrics(zBaseFont).getHeight(); - extensionOffset = baseHeight * EXT_FACTOR_OFFSET_HEIGHT; - - float width = 0; - float height = 0; - - // Clear previous data - baseGlyphVectors.clear(); - baseGlyphVectorsWidths.clear(); - bassGlyphVectors.clear(); - bassGlyphVectorsWidths.clear(); - extensionGlyphVectors.clear(); - extensionGlyphVectorsWidths.clear(); - - // Ok we can precalculate + // The fonts to be used + FontRenderContext frc = g2.getFontRenderContext(); + Font font = getFont(); + Font musicFont = settings.getMusicFont(); + + // Make font size depend on the zoom factor + float factor = 0.5f + (getZoomFactor() / 100f); + float zFontSize = factor * font.getSize2D(); + zFontSize = Math.max(zFontSize, 12); + + // Get the strings making up the chord symbol : base [bass] extension String base = ecs.getRootNote().toRelativeNoteString() + ecs.getChordType().getBase(); String bass = ""; if (!ecs.getBassNote().equalsRelativePitch(ecs.getRootNote())) @@ -186,107 +121,34 @@ public Dimension getPreferredSize() } String extension = ecs.getChordType().getExtension(); - // Split out the flats and sharps... - java.util.List baseStrings = splitStringAlt(base); - java.util.List bassStrings = splitStringAlt(bass); - java.util.List extensionStrings = splitStringAlt(extension); - - FontRenderContext frc = g2.getFontRenderContext(); - - LOGGER.fine("getPreferredSize() -- baseStrings=" + baseStrings + " bassStrings=" + bassStrings + " extStrings=" + extensionStrings); - - // Base of the ChordSymbol - GlyphVector gv; - for (String s : baseStrings) - { - switch (s) - { - case "b": - gv = flatBaseFont.createGlyphVector(frc, settings.getFlatGlyphCode()); - break; - case "#": - gv = sharpBaseFont.createGlyphVector(frc, settings.getSharpGlyphCode()); - break; - default: - gv = zBaseFont.createGlyphVector(frc, s); - break; - } - baseGlyphVectors.add(gv); - LOGGER.fine("getPreferredSize() base glyph bounds=" + gv.getPixelBounds(frc, 1, 0)); - } - - // Bass part of the base of the ChordSymbol - for (String s : bassStrings) + // Create the AttributedString from the strings + String strChord = base + bass + extension; + String strChord2 = strChord.replace('#', settings.getSharpCharInMusicFont()).replace('b', settings.getFlatCharInMusicFont()); + attChordString = new AttributedString(strChord2); + attChordString.addAttribute(TextAttribute.SIZE, zFontSize); // Default attribute + attChordString.addAttribute(TextAttribute.FAMILY, font.getFontName()); // Default attribute + // Use the music font for all the # and b symbols + for (int i = 0; i < strChord.length(); i++) { - switch (s) + if (strChord.charAt(i) == '#' || strChord.charAt(i) == 'b') { - case "b": - gv = flatBaseFont.createGlyphVector(frc, settings.getFlatGlyphCode()); - break; - case "#": - gv = sharpBaseFont.createGlyphVector(frc, settings.getSharpGlyphCode()); - break; - default: - gv = zBaseFont.createGlyphVector(frc, s); - break; + attChordString.addAttribute(TextAttribute.FAMILY, musicFont.getFontName(), i, i + 1); } - bassGlyphVectors.add(gv); - LOGGER.fine("getPreferredSize() bass glyph bounds=" + gv.getPixelBounds(frc, 1, 0)); - } - - // Extension of the ChordSymbol - for (String s : extensionStrings) - { - switch (s) - { - case "b": - gv = flatExtensionFont.createGlyphVector(frc, settings.getFlatGlyphCode()); - break; - case "#": - gv = sharpExtensionFont.createGlyphVector(frc, settings.getSharpGlyphCode()); - break; - default: - gv = extensionFont.createGlyphVector(frc, s); - break; - } - extensionGlyphVectors.add(gv); - LOGGER.fine("getPreferredSize() extension glyph bounds=" + gv.getPixelBounds(frc, 1, 0)); - } - - // Calculate width - for (GlyphVector gvi : baseGlyphVectors) - { - Rectangle r = gvi.getPixelBounds(frc, 1, 0); - int w = r.width + 1; - int h = r.height + 1; - height = Math.max(height, h); - width += w; - baseGlyphVectorsWidths.add(new Float(w)); } - - for (GlyphVector gvi : bassGlyphVectors) + // Superscript for the extension + if (!extension.isEmpty()) { - Rectangle r = gvi.getPixelBounds(frc, 1, 0); - int w = r.width + 1; - int h = r.height + 1; - height = Math.max(height, h); - width += w; - bassGlyphVectorsWidths.add(new Float(w)); + attChordString.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER, base.length() + bass.length(), strChord.length()); } - for (GlyphVector gvi : extensionGlyphVectors) - { - Rectangle r = gvi.getPixelBounds(frc, 1, 0); - int w = gvi.getPixelBounds(frc, 1, 0).width; - int h = (int) extensionOffset + r.height + 1; - height = Math.max(height, h); - width += w; - extensionGlyphVectorsWidths.add(new Float(w)); - } - g2.dispose(); + // Create the TextLayout to get its dimension + TextLayout textLayout = new TextLayout(attChordString.getIterator(), frc); + int w = (int) TextLayoutUtils.getWidth(textLayout, strChord2, false); + int h = (int) TextLayoutUtils.getHeight(textLayout, frc); Insets in = getInsets(); - Dimension d = new Dimension((int) width + 2 + in.left + in.right, (int) height + 2 + in.top + in.bottom); + final int PADDING = 1; + Dimension d = new Dimension((int) w + 2 * PADDING + in.left + in.right, h + 2 * PADDING + in.top + in.bottom); LOGGER.fine("getPreferredSize() result d=" + d + " (insets=" + in + ")"); return d; } @@ -381,40 +243,16 @@ public void paintComponent(Graphics g) // Paint background super.paintComponent(g); + Insets in = this.getInsets(); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // LOGGER.severe("paintComponent() model=" + chordSymbol + " prefSize=" + getPreferredSize() + " g2=" + g2); - float x = 2; - float y = getHeight() - 3; - - // Draw base - for (int i = 0; i < baseGlyphVectors.size(); i++) - { - GlyphVector gv = baseGlyphVectors.get(i); - g2.drawGlyphVector(gv, x, y); - x += baseGlyphVectorsWidths.get(i); - } + float x = in.left; + float y = getHeight() - 1 - in.bottom; - // Draw extension - y = y - extensionOffset; - - for (int i = 0; i < extensionGlyphVectors.size(); i++) - { - GlyphVector gv = extensionGlyphVectors.get(i); - g2.drawGlyphVector(gv, x, y); - x += extensionGlyphVectorsWidths.get(i); - } - - // Draw bass part if any - y = y + extensionOffset; - for (int i = 0; i < bassGlyphVectors.size(); i++) - { - GlyphVector gv = bassGlyphVectors.get(i); - g2.drawGlyphVector(gv, x, y); - x += bassGlyphVectorsWidths.get(i); - } + g2.drawString(attChordString.getIterator(), x, y); // Draw the scale presence indicator ExtChordSymbol ecs = (ExtChordSymbol) getModel().getData(); @@ -473,10 +311,12 @@ public void showCopyMode(boolean b) } /** + * Split the string around b or # chars. + *

* Example : s=F7b9 will return list v[0] = F7, v1[1]=b v[2]=9 */ @SuppressWarnings("empty-statement") - private java.util.List splitStringAlt(String str) + private java.util.List splitStringSharpOrFlat(String str) { StringBuilder sb = new StringBuilder(); ArrayList v = new ArrayList<>(); diff --git a/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbolSettingsImpl.java b/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbolSettingsImpl.java index 6bc056b994..209fe03f7a 100644 --- a/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbolSettingsImpl.java +++ b/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_ChordSymbolSettingsImpl.java @@ -25,11 +25,11 @@ import java.awt.Color; import java.awt.Font; import java.awt.FontFormatException; +import java.awt.GraphicsEnvironment; import java.beans.PropertyChangeListener; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,11 +48,10 @@ @ServiceProvider(service = FontColorUserSettingsProvider.class) } ) - public class IR_ChordSymbolSettingsImpl extends IR_ChordSymbolSettings implements FontColorUserSettingsProvider, FontColorUserSettingsProvider.FCSetting { - private static final String MUSIC_FONT_PATH = "resources/marl.ttf"; + private static final String MUSIC_FONT_PATH = "resources/ScaleDegrees-Times.ttf"; private static Font MUSIC_FONT; /** * The Preferences of this object. @@ -72,6 +71,7 @@ public IR_ChordSymbolSettingsImpl() { InputStream is = IR_ChordSymbol.class.getResourceAsStream(MUSIC_FONT_PATH); MUSIC_FONT = Font.createFont(Font.TRUETYPE_FONT, is); + GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(MUSIC_FONT); // So it is available in getAvailableFontFamilyNames() etc. } catch (IOException | FontFormatException e) { LOGGER.log(Level.SEVERE, "Can''t access " + MUSIC_FONT_PATH); @@ -142,23 +142,15 @@ public Font getMusicFont() } @Override - public int[] getSharpGlyphCode() + public char getSharpCharInMusicFont() { - int[] code = - { - 0x90 - }; - return code; + return '#'; } @Override - public int[] getFlatGlyphCode() + public char getFlatCharInMusicFont() { - int[] code = - { - 0x42 - }; - return code; + return 'b'; } @Override diff --git a/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_TimeSignature.java b/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_TimeSignature.java index ec7bff8e8a..18c933afcb 100644 --- a/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_TimeSignature.java +++ b/ItemRenderer/src/org/jjazz/ui/itemrenderer/IR_TimeSignature.java @@ -28,13 +28,20 @@ import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; +import java.awt.Rectangle; import java.awt.RenderingHints; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; +import java.text.AttributedString; +import java.text.Bidi; import java.util.logging.Logger; import org.jjazz.harmony.TimeSignature; import org.jjazz.leadsheet.chordleadsheet.api.item.CLI_Section; import org.jjazz.ui.itemrenderer.api.*; +import org.jjazz.ui.utilities.TextLayoutUtils; public class IR_TimeSignature extends ItemRenderer implements IR_Copiable { @@ -74,9 +81,10 @@ public class IR_TimeSignature extends ItemRenderer implements IR_Copiable /** * Save the time signature between updates. */ + private AttributedString attrStrUpper; + private AttributedString attrStrLower; TimeSignature timeSignature; private int zoomFactor = 50; - private Font zFont; private static final Logger LOGGER = Logger.getLogger(IR_TimeSignature.class.getSimpleName()); @SuppressWarnings("LeakingThisInConstructor") @@ -93,8 +101,7 @@ public IR_TimeSignature(CLI_Section item) settings.addPropertyChangeListener(this); // Init - zFont = settings.getFont(); - setFont(zFont); + setFont(settings.getFont()); setForeground(settings.getColor()); } @@ -150,40 +157,47 @@ public int getZoomFactor() @Override public Dimension getPreferredSize() { - final int H_MARGIN = 2; - final int V_MARGIN = 1; - final int V_GAP = 0; // 0 because font height seems overestimated always - Font f = getFont(); - int zFactor = getZoomFactor(); - Graphics2D g2 = (Graphics2D) getGraphics(); - assert g2 != null : "g2=" + g2 + " timeSignature=" + timeSignature + " f=" + f + " zFactor=" + zFactor; + final int PADDING = 1; + final int MIDDLE_HGAP = 2; Insets in = getInsets(); + Font font = getFont(); + Graphics2D g2 = (Graphics2D) getGraphics(); + assert g2 != null; + FontRenderContext frc = g2.getFontRenderContext(); - float factor = 0.5f + (zFactor / 100f); + // Font size depends on zoom factor + float factor = 0.5f + (getZoomFactor() / 100f); float zFontSize = factor * getFont().getSize2D(); zFontSize = Math.max(zFontSize, 9); - zFont = getFont().deriveFont(zFontSize); - - // Calculate bounds and strings position - FontMetrics fm = g2.getFontMetrics(zFont); - - // Width and X - Rectangle2D rUpper = fm.getStringBounds(upperString, g2); - Rectangle2D rLower = fm.getStringBounds(lowerString, g2); - // LOGGER.fine("getPreferredSize() rUpper=" + rUpper + " rLower=" + rLower); - int upperWidth = (int) Math.round(rUpper.getWidth()); - int lowerWidth = (int) Math.round(rLower.getWidth()); - int pw = Math.max(upperWidth, lowerWidth) + 2 * H_MARGIN + in.left + in.right; - upperX = Math.round(pw / 2f - upperWidth / 2f); - lowerX = Math.round(pw / 2f - lowerWidth / 2f); - - // Height and Y - int upperHeight = (int) Math.ceil(rUpper.getHeight()) - 4; - int lowerHeight = (int) Math.ceil(rLower.getHeight()) - 4; - int ph = upperHeight + V_GAP + lowerHeight + 2 * V_MARGIN + in.top + in.bottom; - upperY = in.top + V_MARGIN - (int) Math.round(rUpper.getY()); // getY() must be a negative value (relative to baseline) - lowerY = in.top + V_MARGIN + upperHeight - (int) Math.round(rLower.getY()); // getY() must be a negative value (relative to baseline) + + // Create the AttributedStrings + attrStrLower = new AttributedString(lowerString); + attrStrLower.addAttribute(TextAttribute.SIZE, zFontSize); + attrStrLower.addAttribute(TextAttribute.FAMILY, font.getFontName()); + attrStrUpper = new AttributedString(upperString); + attrStrUpper.addAttribute(TextAttribute.SIZE, zFontSize); + attrStrUpper.addAttribute(TextAttribute.FAMILY, font.getFontName()); + + // Create the TextLayout to get its dimension + TextLayout textLayoutUpper = new TextLayout(attrStrUpper.getIterator(), frc); + TextLayout textLayoutLower = new TextLayout(attrStrLower.getIterator(), frc); + int wUpper = (int) TextLayoutUtils.getWidth(textLayoutUpper, upperString, true); + int wLower = (int) TextLayoutUtils.getWidth(textLayoutLower, lowerString, true); + int hUpper = (int) TextLayoutUtils.getHeight(textLayoutUpper, frc); + int hLower = (int) TextLayoutUtils.getHeight(textLayoutLower, frc); + + // Set preferred size + int maxWidth = Math.max(wUpper, wLower); + int pw = in.left + PADDING + maxWidth + PADDING + in.right; + int ph = in.top + PADDING + hUpper + MIDDLE_HGAP + hLower + PADDING + in.bottom; + + // Also set the position + upperX = in.left + PADDING; + lowerX = upperX; + + lowerY = ph - 1 - in.bottom - PADDING + 1; // - (int) Math.ceil(textLayoutLower.getDescent()); + upperY = in.top + PADDING + hUpper; Dimension d = new Dimension(pw, ph); LOGGER.fine("getPreferredSize() d=" + d); @@ -200,9 +214,11 @@ public void paintComponent(Graphics g) Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - g2.setFont(zFont); - g2.drawString(upperString, upperX, upperY); - g2.drawString(lowerString, lowerX, lowerY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Draw the upper and lower string + g2.drawString(attrStrUpper.getIterator(), upperX, upperY); + g2.drawString(this.attrStrLower.getIterator(), lowerX, lowerY); if (copyMode) { diff --git a/ItemRenderer/src/org/jjazz/ui/itemrenderer/api/IR_ChordSymbolSettings.java b/ItemRenderer/src/org/jjazz/ui/itemrenderer/api/IR_ChordSymbolSettings.java index 7ac39a3cad..a3ea524efa 100644 --- a/ItemRenderer/src/org/jjazz/ui/itemrenderer/api/IR_ChordSymbolSettings.java +++ b/ItemRenderer/src/org/jjazz/ui/itemrenderer/api/IR_ChordSymbolSettings.java @@ -92,14 +92,14 @@ public static IR_ChordSymbolSettings getDefault() abstract public Font getMusicFont(); /** - * @return The glyph code for the Sharp symbol in the music font. + * @return The char representing the Sharp symbol in the music font. */ - abstract public int[] getSharpGlyphCode(); + abstract public char getSharpCharInMusicFont(); /** - * @return The glyph code for the Sharp symbol in the music font. + * @return The char representing the Flat symbol in the music font. */ - abstract public int[] getFlatGlyphCode(); + abstract public char getFlatCharInMusicFont(); abstract public void addPropertyChangeListener(PropertyChangeListener listener); diff --git a/ItemRenderer/src/org/jjazz/ui/itemrenderer/resources/ScaleDegrees-Times.ttf b/ItemRenderer/src/org/jjazz/ui/itemrenderer/resources/ScaleDegrees-Times.ttf new file mode 100644 index 0000000000000000000000000000000000000000..32129a22d5f0a1d723f66bc91727b039698bc8e5 GIT binary patch literal 11636 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 M3@%Bw%O70-1>Se_F#rGn literal 0 HcmV?d00001 diff --git a/ItemRenderer/src/org/jjazz/ui/itemrenderer/resources/marl.ttf b/ItemRenderer/src/org/jjazz/ui/itemrenderer/resources/marl.ttf deleted file mode 100644 index cfd444fde9b6fe8347241ee57df820f442944009..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42308 zcmeIbd6*o>btf2^k@sEq(OuQmRoz|PjlQqK(Eth{2oNAnf&c-K0EvSDNP++ef)r1Q zq9lrrWj{0?b|^H{vy9Vs9zF5=$$Lt;{@3vRh11v}U69_v*w0{-4V=FB z;zP4Nh~=@#(Vg>fzegyB{74c zh2kZH8gGf#Ia_4E60Ixv&ikTum2Hx8(YnSA=`gMkzUYeKy2KQACR&%7&K`=^Ijgdd zM(YZ`^IWv9vO4=eqjilb(n0hHQ%qcs+8M5g?F`q$c82R=JHz#`o#A@e&Tw6{bNkjE z*VpcS;QqUg*G`^0dl7FJY8Ox6S=({+_+4i&p1c3_SZyoY&bG20?0QyX_p%4r{p>Dw z9P209Id+y^jMgsT^CCOV?!@N~b`;;ZOB_9i@1JI4*t>Pd(F?WLW3KuGi(yeEZ$eK+2p;j zw#3FByuS7lAH2SC^mMUt`i@QG=*MaFqQO+~f{RaurNHSWwvy%0^K~}JR^R*nFaPb+ z^Is{sg|V-HePT{N{CEF&=dsG6ANzlHe)Rhr{^CFG`NA(2|LLKxuYdGE{afMBMo#_t z-#_+8i;w-{zZ`n%E7{Rc?!50`zx&~ay7?QgJoa(_&0n8-^jH5|M?U+F#lYjS_uit-X&i%b#e9PhTSKeFCZ2!uyYrnqj->Ms*=>O%f-@fZN{*Q0|_^sFd z&Obc*E#r|NJ5>DcZ@&L@(DQT8o;tJc%vT0};&=Ak_PM|N?F-wAm%sAvvwO|&eR2#v z^xKTJE2&fv_`c`4uH)FYWtpa7=(?s!5{CD4zbajR$MvT-y~h8SxxAUJcv)5O{yAC3 zS)WrByn`cT@Bw4<>;!&SXZzbV$uQcctSek%n##GXDqt5)m1GHnSYn$WoNN`@`o+n` z;1&9pCKq49#w&|#dP>@Q`ir=jvGw%Pm)bWSSea^lkr|i2vhv{U{A{i{JKr$IhpN>R zCk%*c+{NX~>;N0b-|g)oX=0*1$x_3dnY>VgfKs`xmK|T^jUlzXqMYSSDqQ*7!7ITl z(&Q_X_?U=)zQ87uKR-b~-^yx@`h?V|=d$_PsfF2@9PJpIpTlZZnw^=S8){5pkJynv z;F@NTGAu6ZvZ*PC?U+V7+O{;+unU%#PFpx?D~fNJ(T1t2K1LS3{v@KN%?hl+ezLts z8W?B~YL=lXGLt2lo46aA1tdjRR8!S-UC}h%unp5RE!#FM%l3J$pZ7Rwx~NtyQ&VMC zvJLbKexcI|g~dty)%c$A)hjJ%A`%*P$tHQo4G8;fGl|~ zlQLa)02y8>fEC5ZUx66|dJ}g|ZxLr^*j)0?=LBnjGpkZzHrK#!<(c`iLv<)$_`Q0oiHG(HxQsjnK7X>7&NpdeGRruxp+w4(8vq*pwE zBXV_!HI*dRR7|Xyd`PUuQetHbdC+QMDXA8eOhK}3sgP09#c(CBX#;s%Q}cP7tChTM zpLg9#M#=QLuGE{x=b@%+xdX$JXD^PHO<5fs;ZW8%WCdgIVn4@P>^ioK{aAaWw0n1Z zPkyYL;*%4-*$hifj8CMEbY?6&K0cAjjIZM>H}P#l96vN|sB;aqv_{>qMct7}&rOV% zm^2-;52TkEL>*~DBZ>x*hd&xn-dE!xlwhW5wmV6m-5ohI)EsKmwHye7Ka8i&Xo9Sf zXJ;1X=bHFR6JsqFK!vU}%Ik^sU62f>BpaF+SiY_;=quM00=L&yZ3t!Tv^`yIVQ0xV zf?(W$pw?7XJD}kpR@C>>rt4^sCmQG5wpZ$Y;5n9J>5l5qK6%@YO3hDe?mYMwCu6icbDyy~uj+$=$AgLz3(dxF$@10*CW7V!6>F)h_Y|C9ON zeDCaRA6^SJyuv$?phJ`9v%a0dz-y}#mnFt!2vSU@F&nI36usOPlS=YO75Y)gipJvN z^pv>j<$nTwwm@?_*&dei`F4TjvYE8!S~^ds0^h-$3H>V2Kx@+w1aJ}rm<)f7?lQVr zu@((Jhvkz$p6}c-23>sDXiRkH8c&D!uJWw7dwpamM+DGT3>P9AptWB^Ys>6hdqV2z zY4@^nAurl#@={T>+(3(2Hlu4L&$ZF5vH|*d*J^XJ)vWB zTVJDIQ+N>foXk_b`OGABUn)}xyO3v#rCKe&09}<{Ujs*~%!b~YW4A(UAiAdmOUb4^ z$IxV!b9F9dGbtZLhxTG!Iax*nIc$7HY_<%sr_sJ9ro`@h3#>Mo&!l?B67NUGw8@29 zo@gDv&CV_e-R3*&r`REOJA1mlPP*fc_R;M(w}-uh2X5Zfo}3uYdJflycnvb@CceF4 zs(jZPb^hRi>o-lSQ_8J}CMR-gcI$kFOSO>xlNd9h(2lh($SC>~TqE9+Bc{ggNOr^> z)DZ;_3Q#C=Ad+;5YWy{H4S&lXY0b9QK{x7%uU1|2rDgZ&;wmQ#AmH3i{E}-AoXq^}KxLPSvwKQ^zkt zd|9TubW5dEBF_0q7;2n3EMPCN-)P?|ee#p-7n^T=%U!2$Sv^0|@I6;~-&-C#xU)Uk zpYr$c+dnjcJs;uO2l*{GmgVMMr{yi@5qI9sKAG&wPl_?M zN-kg?38$InbQbz8X|`f+fdVts6nZT>nY3I|nef@9b(|r106NVaE-)cM<86g%F;1T9 z=zXLi^g@D&|b$NtXrh3nJTXuCy+CYl~5pdrcDT$n3{>?>B;QrwEi zRYUhV_t9StmwZJD6q#cV;V^8t)Br=6Hn=hGP1w5Wa9J@N2go$TDVdnnvWjG2R?~f1;ijvZG`;CCg2;0D4*Sm--OsVlv~QB0 zf4=>mN1i-;XydT=&ZnNdaQ4Wdtv76>F|Kh3W4z5*-^34`;U}NqkF2Gk{`g({)Lrj< z>i!G2AGuNAro8Lvtv6u2>)AIQ-_^p1UmcHlp_d3nEoLO4=7-}x$v4C@V)S=3-PP>* zB-1?qM#le-LNY8TK|*Qq6IaTG<)y{)dtY6K{7^FG^$VKi#7@X3QVD$W@7W);@0Pyw zrS_LU_0q?mzIYor<(EJI(u*H|_UVT%9zS;572I-&-+YE&c#1#qG5*0{;Geo>ZG(er ze(u9>tp)bTj)>l9RslqvfRkw|MN@eD{|>4B!2zxba8Noj)wgF2}$U zG_7F(lP7=+-PEo|xDbRcgKo%W7#F~VfJl%;?DE`e#)Mj=P0u%S>Y_F^ytp_Hcv517 zkq_hDX05g_IZoTvG|sr$`HA(DVB)Th?j)Jh5pyGfI+4a*BT&upmQ6Lysyy~@Bp64mtTQ(x))cqSs&Zm?vrY@c7KU;Gv&*fN{?J})I3wQ zd`}7u=%5yhlaawG^n=blWmqkFPc?B*@eR>*O}1sOtH;d&yc$*jAkh*Oc{!CT^I|UC zR`g;q0FI@nDwUM3%RDHG_JRs~+1e%T&0v08+Fmp_&!keM&%kC|w!Nk0+S}DLHJ!AQ z`K}h3g{E?Kdz(L?z0_wGsJjS$q(0NMwyvrQ^_SA&i^<7IweRwVmZYObbX+U7n)8Lp z$wf;~r*&WexH7|acjLnkvWk7V2%H<^32~moxv)If53te?-7!0sRt5rRh+P!;j zF0?zZytX34+rptjsGWANhgSb;*Pfpx9KlcJ^eQgBn=o5;`TOivxH2rb8-BKZqclI? zUg#_4b1*y&399hK)cE)mj)GMr6RLA6o6Y%tc98cC^S)7wDaAtGR(M_Q<%*l@EvI-O z&A@0+MZLYa9`{WB4Q+K1fDug{fdEAWECNu3taSoNyzh135s)ZST2pL3Nuhbc#H1yz zmk9q5`e>`MP*@L0t&z`lM=Th8n)F&v>-sk*18GqCCkUJvv#fDm8- z%}}^5`Opz%Rh=HvjEXGF(u87;D3TXcC1{QKCKNa5ZM?>HVe@N{FbnKSbiy%Mkb;|_ zSfa-j3}#7H8I)44fHx$PbA%B@(v7-`ZrZ>t3w#J_hLqWzz?qd*<=SPR@^=%AU*_`y02^>$l9Ych4J-UjNvpO^;o-VIij!_?pVocOKw} z_>*@$^rn0G-Tb~Awxk0kCEvVPfmQnGDV^_@AHHkboXZ>X%;0Og+GlWVh))eFKDjIG9&4jVJqk@UD=y{R`7+cSkGuGpAQK%BYx*@afK z6fd1)>6i0`}^F7?>ccLV{EBq;OT1T}euX>#CfR$xfuNpo=6&#;C^{Y)0B2 zehDn+$k7R0Nl6l9O7{$$K*;B`3~VC2Ojh<_DvB#gF1|^Z1NUBeL0JvIb5#qXbE(m2 zYPS(-aY-;f#0+$hqwE%FS1aHHn*>7naC=gknQ70CPfv~ZmJ0;PVVawsY7R8u1j(g6 zfT>~^gc7sII{FIXsT~bIxzf?*#m)}iHIwZA8PWY1Nu-Un=EF{axnG6F4-8;F>adI9oAM39oJr2Az@fD%z=z5{CzIhi)DkpVk~KXIl!hj5 zNm3wX6>WRHUgu)GZiYlY3vHypu5TBlVzFKFx$K*APE#DFq@A>si$!pxgLHMk94jX8 z27Xld`y%EcRHT*Aow7i!0a}}eY1<~2t>Kno4vJ0e5_2nBGRr;%J#iH<#=5k6b$iXw z+|*~}SBqAzQpNBf*QD?~ z7Mi5iwP=PMYR>dYG<0;_19}2H0Ls(-jDvS2p3+?)VZDk6x+d zxs-QQ)l~W*aRWg9IuH-3Y^&HXOhv^v&>NZ~^pwm0f!)l0iM^G5zP(R+^2zqw9)0}L z(|7D%SVbRI^XK2nBr75iDHO5k8LAc}+(HlxB91_@kaisoj)~Z@MlJSa zl8jFh84IDtAaJcv^Uq~TdQ3Ivrdy;-ypAJt^UI%c_5Nx4joGPbAaD8*m!v?^9W1bm zJPCl4uP(pIT^LP%{9W4WepOA$N`1A^d;pGOkiao0Nkb9_(6zRq_cTaTijQM^p!0p% zl1=%4f$vPrhz|>m!)HD0M0-T)?QKI~!vycm^XkQ1e$g`>ErWC>Esn zbOcIovR8VkS4f?>rYg~U*of4JTRLh)M~Rs2dXGszABLCZS&Ycr+f88)>g1Y$>I$n+ zmf#qNpOwNQAnQ$oz`0mi6Y5Z6_7uDnHCSTH*o|=e<|s}9QqVArr|>^~qFg>v780rj zoAhxZp*FSiLPBvMnK}3v7+mx$Et#0^)W)W~er9*+&!nRo?) zpFvih08MmZ4N7&@(99- zo`C=PI5gvGwcS@J6|zdH08Xhe*{D=lp->?ypsS8kSvs`A9g7LAID#Z2id(Fj{Qats zf6EWtX$&ghLP04psMM5e=p!2wtZvDJS%klV8kheK9^ogT(Z8*|AhlZUm4*44dX3sG zvsx8xud!;SD(jU>wYOI^erlW#;|2!RQKwpwhC3u96Nm01aU-Ntg;Q%+7R6@ZFl#06 zsTJPS{M0fcClVA5NTp?tPS31>Hq((HBnKLLHnE4>(uUFG%}ciQK$04v$M*8?z#{x5 zgsJ^e`>=HHz3sCnPoLa-!^(yEYCgkHpWL%+=Z%|&GeRV!Y=*H z8FtS-XMnVjeBXQ>Uw4onJp!qIT-|T0TE993k-lHO^;U!v#;o3z=P}QRw2PeQ!f`Gv zM6wb)e^)6g3BtydHQBvM%HAu){PL1{YHpf58Ou<0tpuqpjL9n}0C5E8Ej=kI(R;iE z_2JG=NNMisiDP1`BkZZ?#@8(A7zoma)Dg*&@L5PnnN3a%?C5+2QrnXLTyQp!;}eM4 zykeZ|80oKXoae+ipQmyDP&m#pW_E&8cCsIDZ<2QHYVV%dv3={R*3`s`o_sDXvmM)8 z^HUS0e2xLTY3Xxw^Ru&afy<44J~Fgr6JM{YR#j~*sv8>fbJF@HBS_2>WXL3jQKHk6 z-HnMpk`L?Iu4Iqz5@Tt(v9d~Ewn~~?Ski4)qD!gU#`N@(<7c|FDjc;- zd*|OJdl*h9uQpp~dkNgKtD-}&bmz;fr=MomvKm6{8&_#TOp#cPql5M$ z__W6Ah{YYkym&u6`6DCk(f;A)(BMF$-d{9jT5a^F0b&i*5wBs&9Cj9 zLkq*SNSKazkCd%sw?uuCW+TZy84-N~$1&7XSOZ-{kG#Z%>3Kv^A>yUg)LZy#?p9?- zaWy$(sWUGiTnrFSUGEHxf7o8D#Xs&6*CostAWn$UZOLd4e*a!t-|1|ZPR=ok# zO`n-<4K-S@=Vx`Y>Yx!f5zQd14s?TTm>T?}nhJdpvWQG$dI6=;0lw+B8Lw>XHx13; z^d|QY$WlX=2Bch0wsg~uKLx(!ur_-HzIKW#Uct`7svR3^kM}i8DMUVIM|oc=fW8L& zwvd;bLnU5pxN5IfgjFE{agLn{BDfKh5n+8KP{>HYC?bxHSd_7`B#Vv_i>{Ss5wj;O z0Z#&2UXjRdEkLuodM_FEx?#XmtC^}m2Rd8M*%^hqh8cJ>DsTFZr>(kbkKhaN=#L{R zt$~p59fJ-E(C72tr4_vU2dkp0uaTG}x(=hYnt^7MXM|8s~%>fwO3CrgxB$20iIG*q}KY zbFu(3V?eX}xdhXV1O$B~g@o80b;CJ!G1(0l#e5C8V7){}tKJY2A_NwQ_q6G$!pwBO zkP~)#$g)lPPA=<9~3f|@f%wtUFrn_>;Nh9 z;kid(fLnrIWn-U`Hhs%-l||W9P00nKgGd4s5hzlMD>d3lI7Z6Jf+ONJ?#bNK%qpg6 z`37#v3WW!lenzcee8}~J)Rb?hB!(N<_^<6p zCGu)`eZ4&eD3vMC1$qgatXhTmPgP)wY3WQ)xd)yp*kzeaZ(p?!jw;{t6$~BMf#+BE zJYQA43@lEMvq~=jMh-2*wh#p`=d!85Rh?2%L|4jv)t)lq#T>g_l9cc+L#;pn=^=I= zO2UZt@L*AJZ|KPiKjZDVFS?>HB9XF;T=%zFNi7Sr75YJamogiHefbew(0Twy#?0rEpCT6et(64uX|f;0K7p zMLd+ErVQA)g+dYMq|=~yFLW^wQ(}J6D3(BS&jomlnH-q~m5P;z@XhAZW;z4vXEIrY z@!F>CxUT0oE`Eroc%ScaCF_(mNxO=gG0(=IO_>*mZNl2FZl9$c~b6^<_oRddU*c&Qo&a74z{|CwWY~}^ z7#Wg8A=j9r0DZt;DRO8&j2Wc`wE6~ky^h12e~f*+y<2+x@%EcX-gNJoBm4L5xM9OI z6p6kH<`RCUm=5ZI`&9XX`)}R5c|=<{(ccGs?6G@Kp90#uf5-L>?NuvhrUx1bfy}W} zC;D?tDs}`%sE-MFjb@W&^#bgSZokMLPu~9H;`Zl;ntia&5r2qzJU5k)FhWyUOZgaN zP6)b8$c( zK=79w_%3(=LPCIN0H%PbI*gH&+31sH;Gpn1P{?7*GJL@1eTnZtj&kS|1+F_1Y)Hw} z9A1@F$$~1Wt5Ab+c{{)~p>tc6?n4{~29PVm`N78&!_tji(h&3u&NG1Ofywj0cI6C3 z>SOBQJMeAQ6W<<%tW+eM-mr%&xB%?T@525Y7qVejySE#?vINJ4@)N7jlbtYo!I!ah z1PvtFDI&JL38|&d%uMBi9PF;|B2IQ3+(bEH=b^t7%800l*!y(t942UBSO0h{C%S{9 z!yn6Ki&0ZZh@v_0hM__9M>!zhg%viA^i67&&EDU>PRz=r#c|u}`o^x^x+NJ_l8u2M zvvr41aa-fhPMbT7-Wk|e&(LwpJFTCY*&Ve%ya(y>KVSYEb`Z$U6g$}-m8PfLGnGMi zbYw7_aghcI>p!)WH7T zaG@ydd->q*Ff_B*vzNqiUma~3M`*inAteH|uZYF&L|@~MKZ852;+{7l@%CTa4@nO` z*nZQoyKg;oQ+xf~Y;!1m?(E$M58Sk^y?W)s?DUF}p@F_qCLqzIH^>W`_3O+80lmBf!nvN zY^@koU|6=J&W7wt5HcCp1kH@1T)LJ}80ryPN=rDQs~RL{uZT6;?7?I=JSc?TkHWwy zyxzWSy?$$F$7}8WiPvfOY<(^4f+5K;x`QQH8bq-P)H4R2lOcfSa;}_&%h!k?v_g#S zgc_C(-Ja08E~*4~j+x`W!_wU!!tA~TReCj7PABjXZru2XD_1t4(+~iMc(s9$P&+oa zivr!U(g-u>R!(7X6Y9|FBW0FwyJ*zMVq94U2wFzd3S1I-V3a|~+cY?I)o2ubZ=#yyA3HD)Fr8gsrk#bghD!$9iN}t>V<&#sO6-!eTW7HMv zi9moAFrwhQ6(b=U0_HMVrmgNoFXs8_c_bKMj;85c4`Ic5xro#d(p3>%3S=K>UJgSgF9$5N#scXJ<7fWx4L)3e4#+MupcE zy4h`ln(!2{)9rC-<;wOd)|wn1=qVwOAq$jto()%ec@MH(2M7A9$j&p(Y9+;_Y&>3t~SQ8~+2CI!t(A!r0G(esVvG%F}pT*n@>x1fp2yLN8euzJ<(41wBEL47u` zR09nG!4Niu9PUzugmOfgB0G?z%z;?%EVLrMb9w4Obxi7#S1F#*Hj|3y>(VI=nBDx(Y}iPS^|8R$H)Tq)8SDP-{SCD`4YVEOmM zf;j{4{`h!%qR_1Ord?;W;_AH>I5RZ8zlPB}$O~CB=tm-8JQOg7F@#CZ0UcUm#zcgT zC<@gpNzrkl=<9-Yg4Y7s{6I{G1XPQC5Xf?k9c(wG{{D77TSKCC&GqDL8Uo6edvvwR zl#=fuytk(_iYW4gZjq377%iPp-X7MUys3V{CNt|~LKrFKiA?$|i7k;M7e?uL8j?B< z({(2hWXdr(a<-!p*oa&vB*ylieE=g#vTSs<1zCwWBFY!~4*G}CzMCKu-i%DdJf=Uxfi{zsIzn0ja#?g`0yjz ziQ~sl+{&9{{K*Djz?=5)$u~dFj~o_Y;C{a8B6|7B`|drtVeLX%H@r6=zGVfIXtuFnu%3$G6metlnm zU7Yw@0LGprSh7GEyI69{A}sj}uZ5w10B}qg6Re2I+bjqd|e?zZq8A zl(c0_`-bM`b7r%$kUEA-IDCiEnR7fX`8J0 z<)x_*pIQ!PJ7v6+WTn|AwBe(fha_lC)_nu`WMcMF(kWOy2%dBtC5M1^g=bt$LaHpp zri~~!!yH>GHcj0^T%IlK1~dm#ssQCzpfZ$QuVk7EC*d0+Cf|`p#8?~`WAO>Z34fOT zsx;dEp!CHrwtsEYFMR5SkACE>k3ae*z#-9~jL`TmeEOpwc-O$gu-6#GbTI1qXW18%J^#hmKadOa9pwMDc!KoTCna9jxPSiD$8=TE$b*EC-Po^i z^{9V!C(|4EPDDc=tle=qk#8b&I2El}Ff1!JLU+`w$PG>SvOj=`&ng=MlC&FW#6!p- z*&{vtaQhKf@z0z(v}&}^=j;F=X-N}a2?-yj^A}05g_^l^5Gkm4HufKdPmyY ztnPW!U8heS+Q;;MrF_rbx0aQe4QvlWWH!Zf4Q055z`JljVm;I~z_T>XZZ$N%5^&I3Ht2WTXB8@{6Mq60$B(;KhIs4*;JGZ-xz&R`>5k+$Uy! zok}!GN*eVw${EzbaSSEnYW7COP9rK71vwBI?IXn)c_xk~?dk!tM=nCdhZ`=^Sdf4P zco%`xYA%hiY08`ium$v2LiGsE@;u!!T?;-YXpD}7lxa;%X@*mVPm;8Ad?^j*6XGp| ze-S>QY1Bir5w}=FKa&rBbhN#qI$Y1VCQl9VYHS$gb236lID7|cj|PW4mjL(17@klR zFfIH*AN**tpGK*lma&--H{Nx@$A%Q7Bs{aYI}@Z;q)fa8KdK_74vYv7yj`ioZL0Z` ziETL9(qys9xWgILFmu>^+m&3m?ZIYNtxo$z-d4m=jJe5%y5S@SKrF5YB^#<~rV7Xz z&nbqifW~mtl2x2#8`z8R5Ugvbr9z=ybUZj;IF;7v{t#)<#J`;%D|iiI*BPM}>J^DOQmdfTv5MLnpd!3m-r)RHy8(2_AmSENQJfB$N9 z5;^}JghDF2hFx+EC*!0H-OVEMonl9*ru%pREjt0rK)p_ms# zS_*kX^2SEX$i!!g+ zGb21eNjXDxnc}*zgDcVfMfr{x$B{LJZU|?jcuJ})m)2O1W2!JAg~LmqiCkA5*gedG z9cs+ah|rdyK?K{Yko0=l36KaUTWQD0Bco9V%w<>xLI(`>5R$9GGo~7#S_LKu6vIfF zaJ8cr0E$*1P7aPj-3C`ueaP}mON3WG|mL2fFJmW5wRh51`_HG#YOY#nBdEMii(0l^*|Y!9UdY|H5Fu`SFI z^;B;+O8~fiG(QNtBWkI|29vBd7|#o2Gm42p%mfJKn^59eK%YcFspSH)??YG@{P>7r zv@>Q^O&i?z0aF5+ZKhnw%%Sv_x6aXYOO?b91WsU@gD{7OVH$IK8WD<8`TW-65?eM?@|bgC|Lp25P*}>=h~7Upo#V_nZigy6;%0wGNYaz_(?}56$ zfIA#OZp2D(*{$qNzz8VHtbXg>n|5#8xMBUm#P|w;(U{lw-n4PU#5j14X7+ljoDDq6ZeqzMYMSZLuHYWTu^gnSr5RG$g9hLkntPusqu@`U zZ%P^hYZMRR$ePj<9@Tk6S`a(puk*ZNBl%n$heL(tpHM>G0X*geYQj<8;7GH#7lLlu zZSj$;rKTF{Sgoo~0<~3Bsp&B$xt+-svkiGsLN5<&qX_Vi$m%YaKv>u7jVPcOGFKE| zB69~=Wwx=$3qvQT+6pWj#5Q110mauGlNUMk zB}@1913)uXje-{p&9|L2{Og#s5c+_4JJW#GQ$*Sz*tOb8Wvk- z)REm*$f1rWfwd5H318ssR4bGzIhuxBF!7qX6^>U zxVM52mWq53N+|Eks7_urr_{P<)|oUID?ozzLZFZT!xr3+p~5NT6Z1YSPUJL4L|lJWB#A)X zy81DSqbKLPFcq&rc@qWQD*2^Sxn3^#89!gmR>^k&TZ3$}06M}nY_d&=%DD%9yMKTC zmg>G8+qZ36oF1uRO4zX-al^yK0?k}}@5afh;sZg9)CHQn$oJ8a)=Rx+ooQ@;vT6I{ zshAL6lD#OFEa2OOh2~2`(`3YU4wLM@Vpmdpi%aNrS@obzz*y5DY&v!uMX73Ojdshh z;kp|Nstv)e>o&E9{78Ao}}h@uLQYI3i%qPqimCUQzQWKBW|TAWO| zJ0!7U4(euF$@mTQ`4xPW6h6xZE)g#^(Ust*0$#*Za|ksdedF@qb@Ze;p>I&dmekxB z=}KcOB5i5rDs|}^`i8ZPAyN?f#uXYzS7$;;kw(>-LVth?MGi)YbD>V_+%7`f#DR( zX`N-0t<8q!5|nFEQ(TlF5NoI_1K~kQC%Up0B2VK=U-%(pj|{3^L_ADywu7o+t(;9L!0oIczA|jv>*G$f1qs2`xtks89j0<;l(5T#J02%^`#$ zK&9ePI$?PmaK;D~R1ohX6;m{f0Q!OL)}VSC#&D8;W{i@BlGA5Y?S+sL0O-C5%R{0z z^HK2>x+@$xBt|1yiP9%P5|+rMND?6eD0w3x=AjpIjV5Lqgj-Wloi*8*HQY!PY0FQ70 zfm4%4mN1L+%Z$4g>_h+>lyZfDo5EZ(0;Mq@5)81kxT;3E4l}KVkt+=2ypslLlqHt2 zsvPc`#`CBf0Mpp^11lGpPQo_!2m_#bVmt2FK?E~J5>W)Ri4tTK0Hd-TDnendf)s`PNII2HrP7FiK}{vWI(Y9%XZrgyX$R$~O&Ge_ zH0)xVdV}<%-C}%IMeF+d+BK)h6=0~n3d<@6)RxW!dUfj*)|YHuA8~aU=R_*62w>4# zVKA|19QtD1N(YYVxEW*}sFneg!B9*f=t|0jUZ(j7{pDHJfv$iMMwcSM>9rbu`7E;P zQEw#I&MTgS5-p1Cuz1vo;&Wivc-SMuE!nV~7;bZ z1hRRu0O%1Js#1{=q}a%-z$U7Oq)wLBNlc@+Pt~FMZSLR%Bjh5 z*rW)Bx^C^_!qjA47^Nwc?&?k2qp=|_q(@|phms@`Atu|L?3~S_bI>&qgCq!P7?Lf7 z@hOIABbJ58*|=iYByj$Ys2ufRVCxlt!VrWonG&Yw+{BViMJcCYXqw8)o^J;5Wt)B< z#fuxpol6bP5vjd&M=6z-BQq4jRlSTCw9;8v7dd2}4niONYWoP}W_yy2)V%SL;pR{s z<^NIBEpU+N;p@%j@X%1R)>rFC0l*USLR?Sx{1VptYI+R`!_DDdSz95q0fmp310Q)b zYQ4WNuT-;XdNxIr@`2DGf|&#&jrk@_8jM3?lU2|}`bSs|tt-uiwiH9B+O;0bL0};a zRZ%8ws97gP6Y`zLG0A%BC5U~JJ&P&Vr7WVdJrD95TPEtxd!DKmk-|b0 z&|Fkj8Ah33RRaR#x?01w74q7J8JqtkkdYL89D|4_+63-vO4ncC-W+UNY)wv#HOg>s z=!Qs6cR4-tsC$v?i7(O!k~NRuNuLY&tFoun-4#yFU8fOk^-V#!ca zrDV8b6r)PhW-KJL?CR<;qLxEN5_Nj;^aIFB5uz>-jFq<6i+?=j0)Q8cXU8$^Av5E0 zERvq$Kp}K0Viaji>wq;RH6~@yb8Ki#XeClPp)o}%Z)5>2qcI_DG}%v)#w3%Nn2Pl! zQIbjbl#UFTgysgg14%Q!i;^vv0Xy*H`;cK`+BrOUA=R|VpJ>lqp)ZTMTE-ZDAN*`x zo9^DOO(!h6y*svV1PhJ~SBnKOA*y(8H@Cs_%ZgnoNT>n%9TR394ZoNlJA!Z-t4;_) zafcXUB0<{Y@%>Fo+BDQdA@jtsCdT9yyGSNdx6wJ_cj^S!7XZF_h`*8C$QAe6OuF|O zNI8JHXXmJzw|WaQ(G#Az*J;v)5f1NX_plfR&dUe3s~awyIDTy3Zmq7KymSA!V&2=U+!}0F7BzK| z#T73gAE`bjR74>pBjiMcGzhRN1gt`dNSzXO$ogf{rCW25eaO!zSqP;u91tyFdOC$~ zQwI@p|09iRxTpfe%2}`jP(R868>6oPI9n9IeDIBgG#ry9ZNN3;6jC=8$&pg_AQS*o zH&Hp{hYDyQ?N)}&{yc0mlGv-zr?;V6-jsCsaQjH(wwrIfZeyDy_h4`N(9L_dUbk_6 zasn~{E{HVed#ydH?Lrd7g~+Jfq8K+}&vf47aTD%{I?G~*lLF$fP$LBBsv`%zt49D$ zT%t!{fDodc)G89&$>Ad`uu*!bieyz(#erLIsb2BgA??ot)G#Qh*egpYv8C(xES2}K z7t$7b>1OEehk%Ds?cE=~+Efti4mM}A;>t6y&VB+qDrwvzt^yAf3}go-)H(_&No{g3 zN4>F}3c6MnSp;!FA(<(dSfSdZm>B|fWlyy{{+52!f5d~9eAB@Ly8>Q>wxEZWNfvnZ zL*T0&?H-A2ZZE}3QjHr~?ln}kLRv%@MvEr6r9iZ?+$2)yOlx9D$0%roK|{g8xL&xC z(g@s4ZxLBy3H;elqmHjXA)&}$DVwV&LFclcD#2YZM#MCGKSo3YmKAxhD3?r`8g5|z zn;35P_mnXOq7n@2S!N0Zi$~lZ5!PNNbF4*)8Iih7xB=9cC9uH~y%-PzfYAs+z@rwH z@nOB6+en)afrLJ2W#*P2=){sP|0AmEe1AzM5D*iDyu{og=JYOWTpjU}Ec1d)u`m^9 zX;mGXiJy-wk$H9<{ICPkrcLeZ7&9E;*!o(G}>r4b&Qk_2b6uI;fA zxXcl4i@+B|t&6~y$S1}i95Fwhtn)F&n;@u0DqY}6|~WlznkljCF6v^q38Y!`uPhX%FC ztq8RP2(s8rqPSc#pAyhlIOkwYMtox|=i3s6b1Bw>v;+}9mO~9*$dMLor6$=-u$|!J zA}b0A*-Xk*T&@L{g@gqZ>>EUcw9a8ODF}QSLg*o~6;#hF!g(oE85|=8=Y}ea)N@_W z0m7it@hCVziD(kERT*(%Y1jg9u*Oxg>r-)eqn3kM6}}ws*$n#PM^T%9W(ENYXffF^ z*F!?4U^!E5eyVCnzI_LE){GqQK{!TQWonTg6%|!aTo0=^mh+_CbqaT(ctvtBizD`2>C8&|%$T|>o&up+;x&aTj$j$p9m%JV1E zwL3vf*RIoFENX$*ZjX+J_j2VgQl0+L!-qR&xMhritBUr&)?W;(_d8yo74d`Z5P|q- zJlA*woX|gsEmOhZfIQR~8J0(z6D#ELu|9<0 zD21^V!_9$4p!*Z!qa#CuX+2e7QZq((MTlOQ@I>6(T_|8k)X$1!Op{F}*?CgTO-pf9 zzePV>=VrDr$Oh$FM`>H7F^_*+lN;-8>1j&h74{x&{J;wY|~Z z^a|r~sMg`L82R*U;*O?Co&)h@jyM4xwn%N#Q=wxsZJm#64jSW zcn~5&FXGt&T}5sNiiD?F%2+Gw%mG(t^S3a1Q5P{3I!g*hPl&ieEeZ|K*+Ru~ z#S&wlG6wddw$E{(wN%@uT;@Gx+#_~LdS4%v_Q_=cE*nb0!>$;4LApJ~{4Xvo51R-X zwU;UOvR;~-u4_^CW}5G)kcWuvpUq97=yK6SelSAjQ49r9ya)$Jq+xZKqV%bhJJsQm zP)Jipu1}~nOrqs$FwRyNezD(e()|W5KN0&VONo7ODqBB*?quCex)BYv<(NX)K=B#f2Lu-KO*=f0Snv4xdnm4)@v8>Vv>k&y6W zg#3k=0Tq8p19?@#7n>MO9Ug>gm3u86aVPO9@yTR8s4nIe zy{7a|E-EPhdq6pLy=#-3$2doYO(Xl{y9(mgS6{a z(lUdYDga_6{K&h`lo90@cO4l?Z>%e;)j>-6>2{_f<=#l=zD%U{Kue;<37WS~MM z<^GoL+rMk`{8&kT@`*Q}J$d5TzH&)st5zG3qo`K>u5)MaWzjlnqxB<9iFWV5d0(_< z@%`B}4`!z)`N)nNxAN=i{rtAIt9jo;x2p#qRUgome1BaTT3oBN4=VdkKcKw*+*!A; zDvfk(6B1;i>uDgyvEV}G?Tkz^Uc#F>j%sD#=`Xc zRM(x}nF3lE*6=tk!!~b_pIWP~5}yNFl7}Rum!KNc>*uKxSBu=AXhMo4nM$IES&}6X z&s&I#e|0t!mQ5OQhj+sw%ZjQOu0BsJV_(>(lycus@1eI_n~W4&@|L8#Oj@Wl2&{$HA*+&3>W+kpL6>EV4z7@{Rdg^1bwI<@f$*Re2N}c2LrAQT zOA75optOrWxU1F*6t|K71K1>D98y$XroDoAqB zVo4xz6v7c{POl}CjBmrU;t`>?VZ*RBV_7rRC}M-b<3eq%MmFm^VX>}Q(H>Zr{J1tA=Y-h5Ca$fseL$&1y*1Brgw0_xoa7Eu;YU+TfL;axm{+sD&j6qcdbtT;3)4tyjKA>t@_ z7m_^^KJmRnf01?F6o^9DLCuKh@M|E9ChQF0a71aCpekyngdHHLZ6WTU39zfoHnwx( zne}o}HdC^U$4h6D<-kA#n3vr~naec_3Etf>1MyHwfTSYHyAgPnjTB_6;)}XW7Ai3| z5$*|b1r>mo;f_Pv2*oXeRz$67$U0y25{+1PRg|L$a*SgfA(-&ru?mITC8FJHiE*3` z0zH+|!}oqfJ^;ZL??j=?{~aT8o#2@mG5O)*mP@5l2Cvc}Uj6<3xZCb;yz%IIT$QdU z`ZUbeCNAIGd7`X{v}A60^kFt5Nj-*aQYDU>8O*-@pv46ovWY#s9VUTkyoi2 z;;FA9GxX$*8e4iikr;wb5(zT8pP} zP?YqH@ZeGAraUuRN9^0w=t!-M#4JXEZ{=>t2N+#w*P@Z(T1U7qld_3cCIVT)c(+a* z3*tNoE}{?);e(z;=t>mSg6N`dOiMBz{FhS5PmH2lT+`ZjMR3bi!QkR98T_kD7HKC$ z&k+EBPa_oYXMxiq22uN8QTT5G%YQ-Feu^;@*n#yOB<%yf^A904y^Q5qEdR(@W+Rq= zL1O+-V)-6pxdT`}kLqog;2=KFSaCm=Z!*?{-|c+@4=)NRK?gV$u2=gwWBqk3FELif zENIg(zBe+&*ywJ?R;)wytBZ_HqMegp2VA_3vFZ0RHjBjk z*)O5!_Rll6kj3&D6sS7O*vdN)Op4>HalJKC;2g?f#X@6KwVEVp^vBOY24nNJL!`LG-V~^sT z$4@i%7X0oLxUVO0ttU4!_BQ<9+XonX2fq6bocj*^_ER5X?44s+K*Oie)~D~r@?RKx z7ry(<9f+AU@MHAkj&M1F&%eZivjj(e2vULefg0kI_S5p2XhG{F+!2nexJFid@6hE} z7%g;8ywEik5!dzkXx|}x-os?^yNg(-?I*Fkhy~PR;1GPDw7>8eZPRxw;6FFxGmB4Z zGhP41XqiE5H=RTK!gF=e<{9MM#mfoQC!jXaZ^Y-*wP~H&NXzcam&Ez#UD}ow@cAO{ zkFL3hHi!2liF3|k2H1`J+KpLnH^lR9(324KxD8=DjL!Yh@$mfa@5kRI-%i~A@LC$q zgPa$?OZOGhce99f?b4Ro9=DIyiB5E#_|t-Sn)X-mDXs;d?l8Q4VcTT1R~GBEO$#_f z(4Wqy^Js~W#cc! z2>U4e7@mCcarOyh`g{^`JU_{Piv2YE8TPa6-{I+*pT;QqIrj7HCH6VYai3?u%)Y>W zh5ah~BKs2iGW&J*8|*9WH`#Bo-)3KBzr+3m`(5@m_IvE>?DyFpu>Z)u!Tykall>?5 zE%wLkPuQQbKVyH+zRmsuqxY`_A;Z~|$u-cCUcKRbif!a>iap0i2A2{ z=!&)Qn#=bNgQon0dnn|cKJ-)k>W^0H;I1pO8h4ZJFtX0WWlG6t{e1bUWU zQ_wZz9$pZAyb8U%2K~IY)6*NFy(fE{>IuXz>i4T(#0jtEwLG`bnKTZFXE2$tBjB4m z*im*&aL=9WBs;|*y@9FR4Ht>oqv=IYjEbhrL(u;RnEptiy{e!HrNUzz3L&(E|!pg3(KBF0H-PzI6Si zt(Uf6y6MusOSfD)cB&paUHZ(W&t7`z(w8rN{n9rt{n@2Um;UwAcOiq= zrP`%2oVPwYZ`Y-p#d)`0I*Rk&dg<9qFXFsky!1<#e&fwkQe-B|4eFmfG7cf$O3nS&*>=OI$?EBKFG=<+NIwNsYk1f;U>|EJJ5c}zgy0CIhT}!w zKxF}JV+~cqM3yie#~ufDPmt)0lo*7);0X57V^Qdv2)`9ivZ03o;01HIEZT?C9|+R| z?CN=$qNZs0MP#a@;sHI#m+}GN&PC->JpV#L$ULl8YB<`52ajpUdqU=U)+oEw33|k!AIxzck^(vY)IJ#LK&=raBx;%A5}MtMP;1r9 zdbVe3rsoAgt_K$p?K7!38C4CybJ`GuMn!F?j2Rw*f*(Rr8=RNI{aaM}Ma!GLcyI?A z09%{dhi{;~jcAYtu#DOly>Qns=nOdH5Q4AZ2=*g`mRg5xv>AkeNkcDPbNZ-#R^E4f zOGj#8Dpja}`1nOC{D3FeAU(u%0}+*Io76rS+8_rt9OtER|27go4XtSP1=I``DL;7O z`Y@DG@D21q`(Tjc9;n^egGejTg4zcdAS8u@l09f2MN;7)aE9=1P<5+LxutfIDsvll z5Tw(kJ`f+jh%^s9Ygi~7XL%`9v@$Knl1!vwrc&rSYB;(m^iU(G zoc2(j$VsQO*`7LxPYI&vo}7WQx4P|TP_)FfQJ4gY*hVHpL&ZdDwdUk-{|GF1Oug3` zz~e5!7jCD0wvYCKK4JS%Gb?PLmH}I##2qs3!M9=iJl7UHfbw}b=-8e1`Aq}ub9*yB zYTCJ(OfFX$0P*SB-sqkJ8imFNS=9QlY|n=GMl-Wnbe&-#KwWbS7#@@Pm&cQnNWGMmGM)?N?IBeKeJ__dJPEd6Q;~#_ouhYvi zgAs%2x?9NxS+p;kEfo5ih=vD6sC`9bae~cLIVAJgj_;s-x|z$-P_|KzOm~auA0Nfg zZKLLnJP>3L$B!QD+mM8KG9Wy?-21t`*g=eh>mEVZKlRd zw9o6yrBG|h%jJs2{t++-C}M$q$|$g-n{K+G1fXQ#N)FepLIJF0+NiRJ5Ob9O2=Is| z+pK$I1u&VVBmWvNP|kMKHnu@ajzz1b?hmaI%r{{eHaK~SK%>E;5Hm2`Q##D zlll*ru+ozzGYr4KkS_SPUnrEzjWLn*h2QYa9@`5H)5{js6eyYUWE7{gi$!GLn8-lI zc|BA?&+vW69PlSm^$o;8Z7IBPeaFi>*aj^*o_plr9&kFI&g zvD_5em#QMNI1P#8TEnTi9z+~F1}d%Lg|K4R&%4;h(*(GMoIBsaQCf^1JTeE>p$O!P zSt*-FchKpOE{31-x>?SZ(@wfvu2zRgF-J#P_8e2`9fwv~RdKk4oz4-rl~x(Rna0D1uBx zyWdS^O_XS?Xj$7&WmD8Xw^AWd=!^FCV{*x&eZD=GUR5FW!pzb`z+;V2%-=30G@1zvK1*+YNT5lKn4*%TIme zxxc>MU;I~W5!%2$z5c-ifAO{U6ZCocJD0zTm^4CZ=!toBMtGjMj;-8Q$E5!W`a_#A diff --git a/JJazz UI Utilities/src/org/jjazz/ui/utilities/TextLayoutUtils.java b/JJazz UI Utilities/src/org/jjazz/ui/utilities/TextLayoutUtils.java index 0a22fbe9ee..adcb2a956a 100644 --- a/JJazz UI Utilities/src/org/jjazz/ui/utilities/TextLayoutUtils.java +++ b/JJazz UI Utilities/src/org/jjazz/ui/utilities/TextLayoutUtils.java @@ -16,12 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - package org.jjazz.ui.utilities; -import java.awt.Font; import java.awt.Rectangle; import java.awt.Shape; +import java.awt.font.FontRenderContext; import java.awt.font.TextHitInfo; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; @@ -32,50 +31,86 @@ /** * Utilities related to text layout and TextLayoutPart. - * + * * @author Miloslav Metelka */ -final class TextLayoutUtils { - +public final class TextLayoutUtils +{ + // -J-Dorg.netbeans.modules.editor.lib2.view.TextLayoutUtils.level=FINE private static final Logger LOG = Logger.getLogger(TextLayoutUtils.class.getName()); - - private TextLayoutUtils() { + + private TextLayoutUtils() + { // NO instances } - public static float getHeight(TextLayout textLayout) { - float height = textLayout.getAscent() + textLayout.getDescent() + textLayout.getLeading(); + /** + * Return the minimum height value of using getPixelBounds() or using getAscent(). + *

+ * Useful to take into account differences on the Mac OS Retina. Works with uppercase TextLayout only ! + * + * @param textLayout + * @param frc + * @return + */ + public static int getHeight(TextLayout textLayout, FontRenderContext frc) + { + float height = textLayout.getAscent(); // + textLayout.getDescent() + textLayout.getLeading(); + int height2 = getHeight2(textLayout, frc); // Ceil to whole points since when doing a compound TextLayout and then // using TextLayoutUtils.getRealAlloc() with its TL.getVisualHighlightShape() and doing // Graphics2D.fill(Shape) on the returned shape then for certain fonts such as // Lucida Sans Typewriter size=10 on Ubuntu 10.04 the background is rendered one pixel down for certain lines // so there appear white lines inside a selection. - return (float) Math.ceil(height); + return (int) Math.ceil(Math.min((float)height, height2)); + } + + /** + * Return the height of the TextLayout using getPixelBounds(). + *

+ * + * @param textLayout + * @param frc + * @return + */ + public static int getHeight2(TextLayout textLayout, FontRenderContext frc) + { + Rectangle r = textLayout.getPixelBounds(frc, 0, 0); + return r.height; } - + /** * Compute a most appropriate width of the given text layout. + *

+ * Take into account bug on MacOSX Retina HiDPI screens. + * + * @return float Equivalent to an integer value. */ - public static float getWidth(TextLayout textLayout, String textLayoutText, Font font) { + public static float getWidth(TextLayout textLayout, String textLayoutText, boolean isItalic) + { // For italic fonts the textLayout.getAdvance() includes some extra horizontal space. // On the other hand index2X() for TL.getCharacterCount() is width along baseline // so when TL ends with e.g. 'd' char the end of 'd' char is cut off. float width; int tlLen = textLayoutText.length(); - if (!font.isItalic() || - tlLen == 0 || - Character.isWhitespace(textLayoutText.charAt(tlLen - 1)) || - Bidi.requiresBidi(textLayoutText.toCharArray(), 0, textLayoutText.length())) + if (!isItalic + || tlLen == 0 + || Character.isWhitespace(textLayoutText.charAt(tlLen - 1)) + || Bidi.requiresBidi(textLayoutText.toCharArray(), 0, textLayoutText.length())) { width = textLayout.getAdvance(); - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("TLUtils.getWidth(\"" + debugText(textLayoutText) + // NOI18N - "\"): Using TL.getAdvance()=" + width + // NOI18N -// textLayoutDump(textLayout) + + if (LOG.isLoggable(Level.FINE)) + { + LOG.fine("TLUtils.getWidth(\"" + debugText(textLayoutText) + + // NOI18N + "\"): Using TL.getAdvance()=" + width + + // NOI18N + // textLayoutDump(textLayout) + '\n'); } - } else { + } else + { // Compute pixel bounds (with frc being null - means use textLayout's frc; and with default bounds) Rectangle pixelBounds = textLayout.getPixelBounds(null, 0, 0); width = (float) pixelBounds.getMaxX(); @@ -83,50 +118,55 @@ public static float getWidth(TextLayout textLayout, String textLayoutText, Font // TL.getAdvance() gives a correct result in that case. // Therefore use a minimum of both values (on all platforms). float tlAdvance = textLayout.getAdvance(); - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("TLUtils.getWidth(\"" + debugText(textLayoutText) + // NOI18N - "\"): Using minimum of TL.getPixelBounds().getMaxX()=" + width + // NOI18N - " or TL.getAdvance()=" + tlAdvance + - textLayoutDump(textLayout) + - '\n'); + if (LOG.isLoggable(Level.FINE)) + { + LOG.fine("TLUtils.getWidth(\"" + debugText(textLayoutText) + + // NOI18N + "\"): Using minimum of TL.getPixelBounds().getMaxX()=" + width + + // NOI18N + " or TL.getAdvance()=" + tlAdvance + + textLayoutDump(textLayout) + + '\n'); } width = Math.min(width, tlAdvance); } - + // For RTL text the hit-info of the first char is above the hit-info of ending char. // However textLayout.isLeftToRight() returns true in case of mixture of LTR and RTL text // in a single textLayout. - // Ceil the width to avoid rendering artifacts. width = (float) Math.ceil(width); return width; } - - private static String textLayoutDump(TextLayout textLayout) { - return "\n TL.getAdvance()=" + textLayout.getAdvance() + // NOI18N - "\n TL.getBounds()=" + textLayout.getBounds() + // NOI18N + + private static String textLayoutDump(TextLayout textLayout) + { + return "\n TL.getAdvance()=" + textLayout.getAdvance() + + // NOI18N + "\n TL.getBounds()=" + textLayout.getBounds() + + // NOI18N "\n TL: " + textLayout; // NOI18N } - - - - public static String toStringShort(TextLayout textLayout) { + + public static String toStringShort(TextLayout textLayout) + { return "[" + textLayout.getCharacterCount() + "]W=" + textLayout.getAdvance(); // NOI18N } - public static String toString(TextLayout textLayout) { - return toStringShort(textLayout) + "; " + // NOI18N + public static String toString(TextLayout textLayout) + { + return toStringShort(textLayout) + "; " + + // NOI18N textLayout.toString(); } - - - /** + + /** * Get real allocation (possibly not rectangular) of a part of layout. *
* It's used when rendering the text layout for filling background highlights of the view. * * @param length Total number of characters for which the allocation is computed. - * @param alloc Allocation given by a parent view. + * @param alloc Allocation given by a parent view. * @return */ public static Shape getRealAlloc(TextLayout textLayout, Rectangle2D textLayoutRect, @@ -148,26 +188,31 @@ public static Shape getRealAlloc(TextLayout textLayout, Rectangle2D textLayoutRe // Shape ret2 = textLayout.getVisualHighlightShape(startHit.getCharIndex(), endHit.getCharIndex(), textLayoutRect); return ret; } - - public static Rectangle2D.Double shape2Bounds(Shape s) { + + public static Rectangle2D.Double shape2Bounds(Shape s) + { Rectangle2D r; - if (s instanceof Rectangle2D) { + if (s instanceof Rectangle2D) + { r = (Rectangle2D) s; - } else { + } else + { r = s.getBounds2D(); } return new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } - + /** - * Append the character description to the given string buffer - * translating the special characters (and '\') into escape sequences. + * Append the character description to the given string buffer translating the special characters (and '\') into escape + * sequences. * * @param sb non-null string buffer to append to. * @param ch character to be debugged. */ - public static void debugChar(StringBuffer sb, char ch) { - switch (ch) { + public static void debugChar(StringBuffer sb, char ch) + { + switch (ch) + { case '\n': sb.append("\\n"); // NOI18N break; @@ -191,16 +236,18 @@ public static void debugChar(StringBuffer sb, char ch) { break; } } - + /** - * Append the character description to the given string builder - * translating the special characters (and '\') into escape sequences. + * Append the character description to the given string builder translating the special characters (and '\') into escape + * sequences. * * @param sb non-null string buffer to append to. * @param ch character to be debugged. */ - public static void debugChar(StringBuilder sb, char ch) { - switch (ch) { + public static void debugChar(StringBuilder sb, char ch) + { + switch (ch) + { case '\n': sb.append("\\n"); // NOI18N break; @@ -224,54 +271,56 @@ public static void debugChar(StringBuilder sb, char ch) { break; } } - + /** - * Return the text description of the given character - * translating the special characters (and '\') into escape sequences. + * Return the text description of the given character translating the special characters (and '\') into escape sequences. * * @param ch char to debug. * @return non-null debug text. */ - public static String debugChar(char ch) { + public static String debugChar(char ch) + { StringBuilder sb = new StringBuilder(); debugChar(sb, ch); return sb.toString(); } - + /** - * Append the text description to the given string buffer - * translating the special characters (and '\') into escape sequences. + * Append the text description to the given string buffer translating the special characters (and '\') into escape sequences. * - * @param sb non-null string buffer to append to. + * @param sb non-null string buffer to append to. * @param text non-null text to be debugged. */ - public static void debugText(StringBuffer sb, CharSequence text) { - for (int i = 0; i < text.length(); i++) { + public static void debugText(StringBuffer sb, CharSequence text) + { + for (int i = 0; i < text.length(); i++) + { debugChar(sb, text.charAt(i)); } } - + /** - * Append the text description to the given string builder - * translating the special characters (and '\') into escape sequences. + * Append the text description to the given string builder translating the special characters (and '\') into escape sequences. * - * @param sb non-null string builder to append to. + * @param sb non-null string builder to append to. * @param text non-null text to be debugged. */ - public static void debugText(StringBuilder sb, CharSequence text) { - for (int i = 0; i < text.length(); i++) { + public static void debugText(StringBuilder sb, CharSequence text) + { + for (int i = 0; i < text.length(); i++) + { debugChar(sb, text.charAt(i)); } } - + /** - * Create text description as String - * translating the special characters (and '\') into escape sequences. + * Create text description as String translating the special characters (and '\') into escape sequences. * * @param text non-null text to be debugged. * @return non-null string containing the debug text. */ - public static String debugText(CharSequence text) { + public static String debugText(CharSequence text) + { StringBuilder sb = new StringBuilder(); debugText(sb, text); return sb.toString();