/* * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores * CA 94065 USA or visit www.oracle.com if you need additional information or * have any questions. */ package com.codename1.ui; import com.codename1.cloud.BindTarget; import com.codename1.io.Log; import com.codename1.ui.geom.*; import com.codename1.ui.plaf.DefaultLookAndFeel; import com.codename1.ui.plaf.LookAndFeel; import com.codename1.ui.plaf.Style; import com.codename1.ui.plaf.UIManager; import com.codename1.ui.util.EventDispatcher; /** * <p>Allows displaying a single line of text and icon (both optional) with different alignment options. This class * is a base class for several components allowing them to declare alignment/icon * appearance universally.</p> * <p> * Label text can be positioned in one of 4 locations as such: * </p> * <script src="https://gist.github.com/codenameone/3bfd03a497bc09700128.js"></script> * <img src="https://www.codenameone.com/img/developer-guide/components-label-text-position.png" alt="Label text positioning" /> * * * @author Chen Fishbein */ public class Label extends Component { /** * Fallback to the old default look and feel renderer for cases where compatibility is essential */ private boolean legacyRenderer; private String text = ""; private Image icon; private Image maskedIcon; private int valign = BOTTOM; private int textPosition = RIGHT; private int gap = 2; private int shiftText = 0; private boolean tickerRunning = false; private static boolean defaultTickerEnabled = true; private boolean tickerEnabled = defaultTickerEnabled; private long tickerStartTime; private long tickerDelay; private boolean rightToLeft; private boolean endsWith3Points = true; private Object mask; private String maskName; private EventDispatcher textBindListeners = null; private boolean shouldLocalize = true; private boolean showEvenIfBlank = false; private int shiftMillimeters = 1; private int stringWidthUnselected = -1; private boolean autoSizeMode; private Font originalFont; private int widthAtLastCheck = -1; /** * Constructs a new label with the specified string of text, left justified. * * @param text the string that the label presents. */ public Label(String text) { noBind = true; setUIID("Label"); this.text = text; localize(); setFocusable(false); endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * Constructs a new label with the specified string of text and uiid * * @param text the string that the label presents. * @param uiid the uiid for the label */ public Label(String text, String uiid) { noBind = true; this.text = text; localize(); setFocusable(false); setUIID(uiid); endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * Construct an empty label */ public Label() { this(""); endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * Constructs a new label with the specified icon * * @param icon the image that the label presents. */ public Label(Image icon) { this(""); this.icon = icon; if(icon != null && icon.requiresDrawImage()) { legacyRenderer = true; } endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * Constructs a new label with the specified icon and UIID * * @param icon the image that the label presents. * @param uiid the uiid for the label */ public Label(Image icon, String uiid) { this("", uiid); this.icon = icon; if(icon != null && icon.requiresDrawImage()) { legacyRenderer = true; } endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * Constructs a new label with the specified icon text and UIID * * @param text the text of the label * @param icon the image that the label presents. * @param uiid the uiid for the label */ public Label(String text, Image icon, String uiid) { this(text, uiid); this.icon = icon; if(icon != null && icon.requiresDrawImage()) { legacyRenderer = true; } endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * Constructs a new label with the specified icon and text * * @param text the text of the label * @param icon the image that the label presents. */ public Label(String text, Image icon) { this(text); this.icon = icon; if(icon != null && icon.requiresDrawImage()) { legacyRenderer = true; } endsWith3Points = UIManager.getInstance().getLookAndFeel().isDefaultEndsWith3Points(); } /** * {@inheritDoc} */ public int getBaselineResizeBehavior() { switch(valign) { case TOP: return BRB_CONSTANT_ASCENT; case BOTTOM: return BRB_CONSTANT_DESCENT; case CENTER: return BRB_CENTER_OFFSET; } return BRB_OTHER; } @Override public int getBaseline(int width, int height) { Style s = getStyle(); Font f = s.getFont(); int innerHeight = height-s.getVerticalPadding(); return s.getPaddingTop()+(innerHeight-f.getHeight())/2+f.getAscent(); } /** * Sets the Label text * * @param text the string that the label presents. */ public void setText(String text){ widthAtLastCheck = -1; this.text = text; localize(); stringWidthUnselected = -1; setShouldCalcPreferredSize(true); repaint(); } private void localize() { if(shouldLocalize) { this.text = getUIManager().localize(text, text); } } /** * {@inheritDoc} */ void initComponentImpl() { super.initComponentImpl(); UIManager manager = getUIManager(); LookAndFeel lf = manager.getLookAndFeel(); if(hasFocus()) { if(lf instanceof DefaultLookAndFeel) { ((DefaultLookAndFeel)lf).focusGained(this); } } // solves the case of a user starting a ticker before adding the component // into the container if(isTickerEnabled() && isTickerRunning() && !isCellRenderer()) { getComponentForm().registerAnimatedInternal(this); } checkAnimation(); if(maskName != null && mask == null) { setMask(UIManager.getInstance().getThemeMaskConstant(maskName)); } if(getIcon() != null) { getIcon().lock(); } } /** * {@inheritDoc} */ void deinitializeImpl() { super.deinitializeImpl(); Form f = getComponentForm(); if(f != null) { f.deregisterAnimated(this); } if(getIcon() != null) { getIcon().unlock(); } } /** * Returns the label text * * @return the label text */ public String getText(){ return text; } /** * Sets the Label icon, if the icon is unmodified a repaint would not be triggered * * @param icon the image that the label presents. */ public void setIcon(Image icon){ if(this.icon == icon) { return; } widthAtLastCheck = -1; if(icon != null) { if(icon.requiresDrawImage()) { legacyRenderer = true; } if(mask != null) { maskedIcon = icon.applyMaskAutoScale(mask); } } this.icon = icon; setShouldCalcPreferredSize(true); checkAnimation(); repaint(); } void checkAnimation() { super.checkAnimation(); if(icon != null && icon.isAnimation()) { Form parent = getComponentForm(); if(parent != null) { // animations are always running so the internal animation isn't // good enough. We never want to stop this sort of animation parent.registerAnimated(this); } } } /** * Returns the labels icon * * @return the labels icon */ public Image getIcon(){ return icon; } /** * Sets the Alignment of the Label to one of: CENTER, LEFT, RIGHT * * @param align alignment value * @see #CENTER * @see #LEFT * @see #RIGHT * @deprecated use Style.setAlignment instead */ public void setAlignment(int align){ getSelectedStyle().setAlignment(align); getUnselectedStyle().setAlignment(align); } /** * Sets the vertical alignment of the Label to one of: CENTER, TOP, BOTTOM * <strong>The valign property is only relevant relatively to the icon and not the entire label, this will * only work when there is an icon</strong> * * @param valign alignment value * @see #CENTER * @see #TOP * @see #BOTTOM */ public void setVerticalAlignment(int valign) { if(valign != CENTER && valign != TOP && valign != BOTTOM){ throw new IllegalArgumentException("Alignment can't be set to " + valign); } this.valign = valign; } /** * Returns the vertical alignment of the Label, this will only work when the icon * is in the side of the text and not above or below it. * <strong>The valign property is only relevant relatively to the icon and not the entire label, this will * only work when there is an icon</strong> * * @return the vertical alignment of the Label one of: CENTER, TOP, BOTTOM * @see #CENTER * @see #TOP * @see #BOTTOM */ public int getVerticalAlignment(){ return valign; } /** * Returns the alignment of the Label * * @return the alignment of the Label one of: CENTER, LEFT, RIGHT * @see #CENTER * @see #LEFT * @see #RIGHT * @deprecated use Style.getAlignment instead */ public int getAlignment(){ return getStyle().getAlignment(); } /** * Sets the position of the text relative to the icon if exists * * @param textPosition alignment value (LEFT, RIGHT, BOTTOM or TOP) * @see #LEFT * @see #RIGHT * @see #BOTTOM * @see #TOP */ public void setTextPosition(int textPosition) { if (textPosition != LEFT && textPosition != RIGHT && textPosition != BOTTOM && textPosition != TOP) { throw new IllegalArgumentException("Text position can't be set to " + textPosition); } this.textPosition = textPosition; } /** * Returns The position of the text relative to the icon * * @return The position of the text relative to the icon, one of: LEFT, RIGHT, BOTTOM, TOP * @see #LEFT * @see #RIGHT * @see #BOTTOM * @see #TOP */ public int getTextPosition(){ return textPosition; } /** * Set the gap in pixels between the icon/text to the Label boundaries * * @param gap the gap in pixels */ public void setGap(int gap) { this.gap = gap; } /** * Returns the gap in pixels between the icon/text to the Label boundaries * * @return the gap in pixels between the icon/text to the Label boundaries */ public int getGap() { return gap; } /** * {@inheritDoc} */ protected String paramString() { return super.paramString() + ", text = " +getText() + ", gap = " + gap; } /** * {@inheritDoc} */ public void paint(Graphics g) { if(legacyRenderer) { initAutoResize(); getUIManager().getLookAndFeel().drawLabel(g, this); return; } paintImpl(g); } void paintImpl(Graphics g) { initAutoResize(); Object icn = null; Image i = getIconFromState(); if(i != null) { icn = i.getImage(); } else { // optimize away a common usage pattern for drawing the background only if(text == null || text.equals("") || text.equals(" ")) { return; } } //getUIManager().getLookAndFeel().drawLabel(g, this); int cmpX = getX() + g.getTranslateX(); int cmpY = getY() + g.getTranslateY(); int cmpHeight = getHeight(); int cmpWidth = getWidth(); Style s = getStyle(); Font f = s.getFont(); String t = text; if(text == null) { t = ""; } Display.impl.drawLabelComponent(g.getGraphics(), cmpX, cmpY, cmpHeight, cmpWidth, s, t, icn, null, 0, gap, isRTL(), false, textPosition, getStringWidth(f), tickerRunning, shiftText, endsWith3Points, valign); } void initAutoResize() { if(autoSizeMode) { Style s = getUnselectedStyle(); int p = s.getHorizontalPadding(); int w = getWidth(); if(w > p + 10) { if(originalFont == null) { originalFont = s.getFont(); } else { if(w == widthAtLastCheck) { return; } } Font currentFont = originalFont; float fontSize = currentFont.getPixelSize(); if(fontSize < 1) { Log.p("Autosize disabled probably because component wasn't using native fonts for UIID: " + getUIID()); autoSizeMode = false; return; } widthAtLastCheck = w; autoSizeMode = false; while(calcPreferredSize().getWidth() < w) { fontSize++; currentFont = currentFont.derive(fontSize, currentFont.getStyle()); getAllStyles().setFont(currentFont); } while(calcPreferredSize().getWidth() > w) { fontSize--; currentFont = currentFont.derive(fontSize, currentFont.getStyle()); getAllStyles().setFont(currentFont); } autoSizeMode = true; } } } void calcSizeAutoSize() { if(autoSizeMode && originalFont != null) { getAllStyles().setFont(originalFont); } } /** * {@inheritDoc} */ protected Dimension calcPreferredSize(){ calcSizeAutoSize(); return getUIManager().getLookAndFeel().getLabelPreferredSize(this); } /** * Simple getter to return how many pixels to shift the text inside the Label * @return number of pixels to shift */ public int getShiftText() { return shiftText; } /** * This method shifts the text from it's position in pixels. * The value can be positive/negative to move the text to the right/left * * @param shiftText The number of pixels to move the text */ public void setShiftText(int shiftText) { this.shiftText = shiftText; } /** * Returns true if a ticker should be started since there is no room to show * the text in the label. * * @return true if a ticker should start running */ public boolean shouldTickerStart() { if(!tickerEnabled){ return false; } Style style = getStyle(); int txtW = style.getFont().stringWidth(getText()); int textSpaceW = getAvaliableSpaceForText(); return txtW > textSpaceW && textSpaceW > 0; } Image getIconFromState() { return getMaskedIcon(); } int getAvaliableSpaceForText() { Style style = getStyle(); int textSpaceW = getWidth() - style.getHorizontalPadding(); Image icon = getIconFromState(); if (icon != null && (getTextPosition() == Label.RIGHT || getTextPosition() == Label.LEFT)) { textSpaceW = textSpaceW - icon.getWidth(); } return textSpaceW; } /** * This method will start the text ticker */ public void startTicker() { startTicker(getUIManager().getLookAndFeel().getTickerSpeed(), true); } /** * This method will start the text ticker * * @param delay the delay in millisecods between animation intervals * @param rightToLeft if true move the text to the left */ public void startTicker(long delay, boolean rightToLeft){ //return if ticker is not enabled if(!tickerEnabled){ return; } if(!isCellRenderer()){ Form parent = getComponentForm(); if(parent != null) { parent.registerAnimatedInternal(this); } } tickerStartTime = System.currentTimeMillis(); tickerDelay = delay; tickerRunning = true; this.rightToLeft = rightToLeft; if (isRTL()) { this.rightToLeft = !this.rightToLeft; } } /** * Stops the text ticker */ public void stopTicker(){ tickerRunning = false; setShiftText(0); deregisterAnimatedInternal(); } /** * {@inheritDoc} */ void tryDeregisterAnimated() { if(tickerEnabled || tickerRunning) { return; } super.tryDeregisterAnimated(); } /** * Returns true if the ticker is running * * @return true if the ticker is running */ public boolean isTickerRunning() { return tickerRunning; } /** * Sets the Label to allow ticking of the text. * By default is true * * @param tickerEnabled */ public void setTickerEnabled(boolean tickerEnabled) { this.tickerEnabled = tickerEnabled; } /** * This method return true if the ticker is enabled on this Label * * @return tickerEnabled */ public boolean isTickerEnabled() { return tickerEnabled; } /** * If the Label text is too long fit the text to the widget and adds "{@code ...}" * points at the end. By default this is set to {@code false} for faster performance. * * @param endsWith3Points true if text should add "..." at the end */ public void setEndsWith3Points(boolean endsWith3Points){ this.endsWith3Points = endsWith3Points; } /** * If the Label text is too long fit the text to the widget and adds "{@code ...}" * points at the end. By default this is set to {@code false} for faster performance. * * @return true if this Label adds "..." when the text is too long */ public boolean isEndsWith3Points() { return endsWith3Points; } /** * {@inheritDoc} */ public boolean animate() { boolean animateTicker = false; if(tickerRunning && tickerStartTime + tickerDelay < System.currentTimeMillis()){ tickerStartTime = System.currentTimeMillis(); if(rightToLeft){ shiftText -= Display.getInstance().convertToPixels(shiftMillimeters, true); if(shiftText + getStringWidth(getStyle().getFont()) < 0) { shiftText = getStringWidth(getStyle().getFont()); } }else{ shiftText += Display.getInstance().convertToPixels(shiftMillimeters, true); if(getStringWidth(getStyle().getFont()) - shiftText < 0) { shiftText = -getStringWidth(getStyle().getFont()); } } animateTicker = true; } // if we have an animated icon then just let it do its thing... boolean val = icon != null && icon.isAnimation() && icon.animate(); boolean parent = super.animate(); return val || parent || animateTicker; } /** * Allows disabling/enabling tickers globally * * @return the defaultTickerEnabled */ public static boolean isDefaultTickerEnabled() { return defaultTickerEnabled; } /** * Allows disabling/enabling tickers globally * @param aDefaultTickerEnabled the defaultTickerEnabled to set */ public static void setDefaultTickerEnabled(boolean aDefaultTickerEnabled) { defaultTickerEnabled = aDefaultTickerEnabled; } /** * A mask image can be applied to the label (see the image mask method for details) * which allows for things like rounded image appearance etc. * * @param mask the mask returned from the image object */ public void setMask(Object mask) { this.mask = mask; } /** * Returns the mask matching the given image * * @return the mask for the given label */ public Object getMask() { return mask; } /** * Determines the name of the mask from the image constants thus allowing the mask to be applied from the theme * @return the maskName */ public String getMaskName() { return maskName; } /** * Determines the name of the mask from the image constants thus allowing the mask to be applied from the theme * @param maskName the maskName to set */ public void setMaskName(String maskName) { this.maskName = maskName; setMask(UIManager.getInstance().getThemeMaskConstant(maskName)); repaint(); } /** * {@inheritDoc} */ public String[] getPropertyNames() { return new String[] {"maskName"}; } /** * {@inheritDoc} */ public Class[] getPropertyTypes() { return new Class[] { String.class }; } /** * {@inheritDoc} */ public String[] getPropertyTypeNames() { return new String[] {"String"}; } /** * {@inheritDoc} */ public Object getPropertyValue(String name) { if(name.equals("maskName")) { return getMaskName(); } return null; } /** * {@inheritDoc} */ public String setPropertyValue(String name, Object value) { if(name.equals("maskName")) { setMaskName((String)value); return null; } return super.setPropertyValue(name, value); } /** * If a mask is applied returns the icon with a mask, otherwise returns the icon * @return the icon masked or otherwise */ public Image getMaskedIcon() { if(maskedIcon != null) { return maskedIcon; } if(mask != null) { if(icon != null) { maskedIcon = icon.applyMaskAutoScale(mask); return maskedIcon; } } return icon; } /** * {@inheritDoc} */ public String[] getBindablePropertyNames() { return new String[] {"text"}; } /** * {@inheritDoc} */ public Class[] getBindablePropertyTypes() { return new Class[] {String.class}; } /** * {@inheritDoc} */ public void bindProperty(String prop, BindTarget target) { if(prop.equals("text")) { if(textBindListeners == null) { textBindListeners = new EventDispatcher(); } textBindListeners.addListener(target); return; } super.bindProperty(prop, target); } /** * {@inheritDoc} */ public void unbindProperty(String prop, BindTarget target) { if(prop.equals("text")) { if(textBindListeners == null) { return; } textBindListeners.removeListener(target); if(!textBindListeners.hasListeners()) { textBindListeners = null; } return; } super.unbindProperty(prop, target); } /** * {@inheritDoc} */ public Object getBoundPropertyValue(String prop) { if(prop.equals("text")) { return getText(); } return super.getBoundPropertyValue(prop); } /** * {@inheritDoc} */ public void setBoundPropertyValue(String prop, Object value) { if(prop.equals("text")) { setText((String)value); return; } super.setBoundPropertyValue(prop, value); } /** * Indicates if text should be localized when set to the label, by default * all text is localized so this allows disabling automatic localization for * a specific label. * @return the shouldLocalize value */ public boolean isShouldLocalize() { return shouldLocalize; } /** * Indicates if text should be localized when set to the label, by default * all text is localized so this allows disabling automatic localization for * a specific label. * @param shouldLocalize the shouldLocalize to set */ public void setShouldLocalize(boolean shouldLocalize) { this.shouldLocalize = shouldLocalize; } /** * Returns the number of millimeters that should be shifted in tickering * * @return the shiftMillimeters */ public int getShiftMillimeters() { return shiftMillimeters; } /** * Sets the millimeters that should be shifted in tickering * * @param shiftMillimeters the shiftMillimeters to set */ public void setShiftMillimeters(int shiftMillimeters) { this.shiftMillimeters = shiftMillimeters; } /** * By default labels and subclasses become 0 sized when they are blank even ignoring their padding * setting this to true makes the padding take effect even in a blank field. * @return the showEvenIfBlank */ public boolean isShowEvenIfBlank() { return showEvenIfBlank; } /** * By default labels and subclasses become 0 sized when they are blank even ignoring their padding * setting this to true makes the padding take effect even in a blank field. * @param showEvenIfBlank the showEvenIfBlank to set */ public void setShowEvenIfBlank(boolean showEvenIfBlank) { this.showEvenIfBlank = showEvenIfBlank; } /** * This method is equivalent to label.getStyle().getFont().stringWidth(label.getText()) but its faster * @param fnt the font is passed as an optimization to save a call to getStyle * @return the string width */ public int getStringWidth(Font fnt) { if(isUnselectedStyle) { // very optimized way to get the string width of a label for the common unselected case in larger lists if(stringWidthUnselected < 0) { stringWidthUnselected = fnt.stringWidth(text); } return stringWidthUnselected; } return fnt.stringWidth(text); } /** * Fallback to the old default look and feel renderer for cases where compatibility is essential * @return the legacyRenderer */ public boolean isLegacyRenderer() { return legacyRenderer; } /** * Fallback to the old default look and feel renderer for cases where compatibility is essential * @param legacyRenderer the legacyRenderer to set */ public void setLegacyRenderer(boolean legacyRenderer) { this.legacyRenderer = legacyRenderer; } @Override public void styleChanged(String propertyName, Style source) { super.styleChanged(propertyName, source); // If we're using a custom font, we need to use the legacy renderer. if (Style.FONT.equals(propertyName) && source.getFont() instanceof CustomFont) { setLegacyRenderer(true); } } /** * Autosize mode automatically shrinks/grows the font of the label to fit in the available width, it carries * a noticeable performance penalty and we recommend you avoid using it unless absolutely necessary * @return the autoSizeMode */ public boolean isAutoSizeMode() { return autoSizeMode; } /** * Autosize mode automatically shrinks/grows the font of the label to fit in the available width, it carries * a noticeable performance penalty and we recommend you avoid using it unless absolutely necessary * @param autoSizeMode the autoSizeMode to set */ public void setAutoSizeMode(boolean autoSizeMode) { this.autoSizeMode = autoSizeMode; } }