Proposal improve of look and feel of Juce's applications

Ok, here’s another proposal. And if we allow for multiple sets of property names to coexist by agreeing on a minimal set of “standard JUCE tags”?

I came out with the following code where, after declaring the property names that you deem to be better suited to your use case, you can change:

#define StyleSyntax CSS

and let’s say set it to your QML list of properties:

#define StyleSyntax QML

The syntax is defined in compile time, the properties accessed in O(log(N)) and the getters and setters for each property are defined in the same place with lambdas. It also allows you to declare your own non-compatible properties in your custom Components. I welcome your suggestions to improve the code.

enum StyleIds {
  backgroundColourId,
  borderBottomColourId,
  borderBottomWidthId,
  borderLeftColourId,
  borderLeftWidthId,
  borderRightColourId,
  borderRightWidthId,
  borderTopColourId,
  borderTopWidthId,
  outlineColourId,
  outlineWidthId,
  caretColourId,
  cursorId,
  directionId,
  fontFamilyId,
  fontKerningId,
  fontSizeId,
  fontStyleId,
  textColourId,
  colourId,
  fontWeightId,
  xId,
  yId,
  heightId,
  widthId,
  marginBottomId,
  marginLeftId,
  marginRightId,
  marginTopId,
  opacityId,
  paddingBottomId,
  paddingLeftId,
  paddingRightId,
  paddingTopId,
  horizontalAlignId,
  verticalAlignId,
  zIndexId,
  numStyleIds
};

const String CSS[numStyleIds] = {
  "background-color", //backgroundColourId,
  "border-bottom-color", //borderBottomColourId,
  "border-bottom-width", //borderBottomWidthId,
  "border-left-color", //borderLeftColourId,
  "border-left-width", //borderLeftWidthId,
  "border-right-color", //borderRightColourId,
  "border-right-width", //borderRightWidthId,
  "border-top-color", //borderTopColourId,
  "border-top-width", //borderToptWidthId,
  "outline-color", //outlineColourId,
  "outline-width", //outlineWidthId,
  "caret-color", //caretColourId,
  "cursor", //cursorId,
  "direction", //directionId,
  "font-family", //fontFamilyId,
  "font-kerning", //fontKerningId,
  "font-size", //fontSizeId,
  "font-style", //fontStyleId,
  "color", //textColourId,
  "color", //colourId,
  "font-weight", //fontWeightId,
  "left", //xId,
  "top", //yId,
  "height", //heightId,
  "width", //widthId,
  "margin-bottom", //marginBottomId,
  "margin-left", //marginLeftId,
  "margin-right", //marginRightId,
  "margin-top", //marginTopId,
  "opacity", //opacityId,
  "padding-bottom", //paddingBottomId,
  "padding-left", //paddingLeftId,
  "padding-right", //paddingRightId,
  "padding-top", //paddingTopId,
  "text-align", //horizontalAlignId,
  "vertical-algin", //verticalAlignId,
  "z-index" //zIndexId
};

//change this line to compile with a different syntax
#define StyleSyntax CSS  

#if (JUCE_DEBUG)
  #define logError(errorString){ Logger::writeToLog(errorString); }
#else
  #define logError(errorString){}
#endif

//list of fonts available in the system
const StringArray SYSTEM_TYPEFACE_NAMES=Font::findAllTypefaceNames();

static Colour parseColour(const String &str){
  String colourStr(str.fromFirstOccurrenceOf("#",false,false));
  if (colourStr.isEmpty())
    colourStr=str;
  if (colourStr.length()==6) //alpha missing
    colourStr="FF"+colourStr;
  return Colour(colourStr.getHexValue32());
}

static String removeInvisibleChars(const String &str) {
  int start=0;
  while (start<str.length() && (str[start]==' ' || str[start]=='\n' || str[start]=='\r' || str[start]=='\t' )) start++;
  int end=str.length()-1;
  while (end>0 && (str[start]==' ' || str[start]=='\n' || str[start]=='\r' || str[start]=='\t' )) end--;
  return str.substring(start,end+1);
}

static bool isStringOnlyNumeric(const String &str )
  {
    if (str.length() == String(str.getFloatValue()).length())
      return true;

    return false;
  }

class Styled {    
public:
  typedef std::function<void(const var&)> Setter;
  typedef std::function<var()> Getter;
  typedef std::pair<Setter,Getter> PropertyMethods;
  typedef std::map<String, PropertyMethods> Style;  

  Styled(Style &style,const String &name=String()): name(name),style(style){}
  virtual ~Styled(){}   

  bool hasProperty(const String &propertyName) {
    return (style.find(propertyName)!=style.end());
  }

  String getSupportedProperties() {
    String keys="";
    for (auto it : style) {
      keys+=it.first+",";
    }
    return keys.substring(0,keys.length()-1);
  }

  void setProperty(const String &property, const var &value) {
    auto it=style.find(property);
    if (it!=style.end()){
      it->second.first(value);
    } else {
      logError("setProperty error: '"+String(property)+ "' is not a supported property in component '"+name+"'")
    } 
  }

  void setProperty(StyleIds propertyId,const var &value) {
    setProperty(StyleSyntax[propertyId],value);
  }

  var getProperty(const String &property) {
    auto it=style.find(property);
    if (it!=style.end()){
      return it->second.second();
    } else {
      logError("getProperty error: '"+String(property)+ "' is not a supported property in component '"+name+"'")
      return var::null;
    }
  }

  var getProperty(StyleIds propertyId) {
    return getProperty(StyleSyntax[propertyId]);
  }

  virtual void styleChanged(const String &styleStr){}

  bool setStyle(const String &styleStr, bool callStyleChanged=true) {
    bool error=false;
    StringArray attributeList;
    attributeList.addTokens(styleStr,";","");
    for (auto attribute : attributeList) {
      if (removeInvisibleChars(attribute).isNotEmpty()){
        StringArray fields;
        fields.addTokens(attribute,":","\"'");
        if (fields.size()==2){
          setProperty(removeInvisibleChars(fields[0]),removeInvisibleChars(fields[1]));
        } else {
          logError("style error: wrong format '"+attribute+"' in Component '"+name+"'")
          error=true;
        }
      }
    }
    if (callStyleChanged)
      styleChanged(styleStr);
    return error;
  }
private:
  const String name;
  Style &style;
};

class StyledLabel :  public Label,
                     public Styled{  

  Style style = {
    {StyleSyntax[StyleIds::fontFamilyId], PropertyMethods(
      [this](const var &value){
        //sets the font to the first typeface available in this system
        StringArray typefaceList;
        String listOfFamilies=value.toString();
        typefaceList.addTokens(listOfFamilies,",","\"'");
        for (auto typeface:typefaceList){
          if (SYSTEM_TYPEFACE_NAMES.contains(removeInvisibleChars(typeface),true)){
            //get the lower and upper cases right
            fontFamily=SYSTEM_TYPEFACE_NAMES[SYSTEM_TYPEFACE_NAMES.indexOf(removeInvisibleChars(typeface),true)];
            break;
          }
        }
        //check if the fontStyle is valid with the chosen font family
        if (fontStyle.isNotEmpty()) {
          StringArray systemStyles=juce::Font::findAllTypefaceStyles(fontFamily);          
          if (systemStyles.contains(fontStyle,true)){
            //get the lower and upper cases right
            fontStyle=systemStyles[systemStyles.indexOf(fontStyle,true)];
          }
          else {
            logError("Label '"+getName()+"' with font-family "+fontFamily+" has not the following style available: "+fontStyle);
            fontStyle="";
          }
        }
        updateFont();
      },
      [this](){
        return var(fontFamily);
      })
    },
    {StyleSyntax[StyleIds::fontSizeId], PropertyMethods(
      [this](const var &value){
        String sizeStr=value.toString();

        if (sizeStr.endsWith("pt")) {
          sizeStr=sizeStr.upToLastOccurrenceOf("pt",false,true);
          if (isStringOnlyNumeric(sizeStr)) {
            fontSize=sizeStr.getFloatValue();
            fontSizeIsInPt=true;
          } else {
            logError("Label '"+getName()+"' has a font-size with wrong format: "+sizeStr);
          }

        } else if (sizeStr.endsWith("px")) {
          sizeStr=sizeStr.upToLastOccurrenceOf("px",false,true);
          if (isStringOnlyNumeric(sizeStr)) {
            fontSize=sizeStr.getFloatValue();
            fontSizeIsInPt=false;
          }  else {
            logError("Label '"+getName()+"' has a font-size with wrong format: "+sizeStr);
          }

        } else {
          if (isStringOnlyNumeric(sizeStr)) {
            fontSize=sizeStr.getFloatValue();
            fontSizeIsInPt=true;
          } else {
            logError("Label '"+getName()+"' has a font-size with wrong format: "+sizeStr);
          }
        }
        updateFont();
      },
      [this](){
        return var(String(fontSize)+String((fontSizeIsInPt)? "pt":"px"));
      })
    },
    {StyleSyntax[StyleIds::fontStyleId], PropertyMethods(
      [this](const var &value){                    
        /* this is not the implementation of the CSS font-style
           because in CSS font-style only indicates italics 
           (font-weight is used for bold)
           but this implements what both JUCE and most graphic tools mean by style
        */
        fontStyle=value.toString();

        if (fontFamily.isNotEmpty()) {
          //if the typefaceName is already set, check if it is a valid style

          StringArray systemStyles=juce::Font::findAllTypefaceStyles(fontFamily);
          if (systemStyles.contains(fontStyle,true)){
            //get the lower and upper cases right
            fontStyle=systemStyles[systemStyles.indexOf(fontStyle,true)];
          }
          else {
            logError("Label '"+getName()+"' with font-family "+fontFamily+" has not the following style available: "+fontStyle); 
            fontStyle="";
          }
        }
        updateFont();
      },
      [this](){
        return var(fontStyle);
      })
    },
    {StyleSyntax[StyleIds::textColourId], PropertyMethods(
      [this](const var &value){
        fontColor=parseColour(value.toString());
      },
      [this](){
        return var("#"+String::toHexString((int)fontColor.getARGB()));
      })
    },
    {StyleSyntax[StyleIds::horizontalAlignId], PropertyMethods(
      [this](const var &value){
        String str=value.toString().toLowerCase();
        if (str == "center") {
          justification=Justification::centredTop;
        } else if (str == "left") {
          justification=Justification::topLeft;
        } else if (str == "right") {
          justification=Justification::topRight;
        } else if (str == "justify") {
          //A label only displays 1 line of text, so it can't really justify anything
          justification=Justification::bottomLeft;
        }
      },
      [this](){
        if (justification==Justification::centredTop) {
          return var("center");
        } else if (justification==Justification::topLeft) {
          return var("left");
        } else if (justification==Justification::topRight) {
          return var("right");
        }
        return var("center");
      })
    }
  };

  void updateFont() {
    if (fontFamily.isNotEmpty()) font.setTypefaceName(fontFamily);
    if (fontStyle.isNotEmpty()) font.setTypefaceStyle(fontStyle);

    if (fontSizeIsInPt)
      font=font.withPointHeight(fontSize);
    else
      font=font.withHeight(fontSize);      
    setFont(font);
  }

  ...
  
  StyledLabel (const String& name, const String& labelText)
    : Label(name,labelText), Styled (style){
  }

private:
    Font font;
    Colour fontColor=Colours::black;    
    String fontFamily="";
    String fontStyle="";
    float fontSize=12;
    bool fontSizeIsInPt=true;
    Justification justification;
  
}

Examples:

StyledLabel label;

// backwards compatibility
label->setProperty(textColourId,"#FFFFFF");

/* using Strings to refer to the properties 
   with property names that can be set to match the syntax in your style files
  (for faster parsing) */
label->setProperty("color","#FFFFFF"); 

// or if you prefer to set multiple properties at once
label->setStyle("font-family:Arial;color:#FFFFFF"); 

EDIT: std::map could be replaced by std::unordered_map which is a hash table that should give you O(1) access with a good hash function, but since the list of properties is rather short it is probably not worth it.

4 Likes