From 53a3638fa87192a2e87a3d3fc1b2790f582bdb63 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Tue, 13 Apr 2021 05:43:37 +1000 Subject: [PATCH] ANSI support for tray labels --- v2/cmd/wails/main.go | 1 + v2/go.mod | 2 +- v2/go.sum | 3 +- v2/internal/ffenestri/menu_darwin.c | 99 ++++++++++++++++++++ v2/internal/ffenestri/menu_darwin.h | 1 + v2/internal/ffenestri/traymenu_darwin.c | 16 +++- v2/internal/ffenestri/traymenu_darwin.h | 4 +- v2/internal/ffenestri/traymenustore_darwin.c | 4 +- v2/internal/menumanager/processedMenu.go | 18 ++++ v2/internal/menumanager/traymenu.go | 27 ++++++ 10 files changed, 167 insertions(+), 8 deletions(-) diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go index fb2edf24..db1c75b8 100644 --- a/v2/cmd/wails/main.go +++ b/v2/cmd/wails/main.go @@ -67,5 +67,6 @@ func main() { err = app.Run() if err != nil { println("\n\nERROR: " + err.Error()) + os.Exit(1) } } diff --git a/v2/go.mod b/v2/go.mod index 309bab2a..c539925d 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -4,13 +4,13 @@ go 1.16 require ( github.com/Masterminds/semver v1.5.0 - github.com/davecgh/go-spew v1.1.1 github.com/fatih/structtag v1.2.0 github.com/fsnotify/fsnotify v1.4.9 github.com/gorilla/websocket v1.4.1 github.com/imdario/mergo v0.3.11 github.com/jackmordaunt/icns v1.0.0 github.com/leaanthony/clir v1.0.4 + github.com/leaanthony/go-ansi-parser v1.0.1 github.com/leaanthony/gosod v0.0.4 github.com/leaanthony/slicer v1.5.0 github.com/matryer/is v1.4.0 diff --git a/v2/go.sum b/v2/go.sum index 1c9268be..6cde95b6 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,7 +1,6 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= @@ -42,6 +41,8 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU= github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= +github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= github.com/leaanthony/gosod v0.0.4 h1:v4hepo4IyL8E8c9qzDsvYcA0KGh7Npf8As74K5ibQpI= github.com/leaanthony/gosod v0.0.4/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU= github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= diff --git a/v2/internal/ffenestri/menu_darwin.c b/v2/internal/ffenestri/menu_darwin.c index c7f1c59f..a4bd5828 100644 --- a/v2/internal/ffenestri/menu_darwin.c +++ b/v2/internal/ffenestri/menu_darwin.c @@ -579,6 +579,105 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c return item; } +// getColour returns the colour from a styledLabel based on the key +const char* getColour(JsonNode *styledLabelEntry, const char* key) { + JsonNode* colEntry = getJSONObject(styledLabelEntry, key); + if( colEntry == NULL ) { + return NULL; + } + return getJSONString(colEntry, "hex"); +} + +id createAttributedStringFromStyledLabel(JsonNode *styledLabel, const char* fontName, int fontSize) { + + // Create result + id attributedString = ALLOC_INIT("NSMutableAttributedString"); + msg_reg(attributedString, s("autorelease")); + + // Create new Dictionary + id dictionary = ALLOC_INIT("NSMutableDictionary"); + msg_reg(dictionary, s("autorelease")); + + // Use default font + CGFloat fontSizeFloat = (CGFloat)fontSize; + id font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuBarFontOfSize:"), fontSizeFloat); + + // Check user supplied font + if( STR_HAS_CHARS(fontName) ) { + id fontNameAsNSString = str(fontName); + id userFont = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat); + if( userFont != NULL ) { + font = userFont; + } + } + + id fan = lookupStringConstant(str("NSFontAttributeName")); + id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName")); + id NSBackgroundColorAttributeName = lookupStringConstant(str("NSBackgroundColorAttributeName")); + + // Loop over styled text creating NSAttributedText and appending to result + JsonNode *styledLabelEntry; + json_foreach(styledLabelEntry, styledLabel) { + + // Clear dictionary + msg_reg(dictionary, s("removeAllObjects")); + + // Add font to dictionary + msg_id_id(dictionary, s("setObject:forKey:"), font, fan); + + // Get Text + const char* thisLabel = mustJSONString(styledLabelEntry, "Label"); + + // Get foreground colour + const char *hexColour = getColour(styledLabelEntry, "FgCol"); + if( hexColour != NULL) { + unsigned short r, g, b, a; + + // white by default + r = g = b = a = 255; + int count = sscanf(hexColour, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a); + if (count > 0) { + id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"), + (CGFloat)r / (CGFloat)255.0, + (CGFloat)g / (CGFloat)255.0, + (CGFloat)b / (CGFloat)255.0, + (CGFloat)a / (CGFloat)255.0); + msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName); + } + } + + // Get background colour + hexColour = getColour(styledLabelEntry, "BgCol"); + if( hexColour != NULL) { + unsigned short r, g, b, a; + + // white by default + r = g = b = a = 255; + int count = sscanf(hexColour, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a); + if (count > 0) { + id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"), + (CGFloat)r / (CGFloat)255.0, + (CGFloat)g / (CGFloat)255.0, + (CGFloat)b / (CGFloat)255.0, + (CGFloat)a / (CGFloat)255.0); + msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName); + } + } + + // Create AttributedText + id thisString = ALLOC("NSMutableAttributedString"); + msg_reg(thisString, s("autorelease")); + msg_id_id(thisString, s("initWithString:attributes:"), str(thisLabel), dictionary); + + // Append text to result + msg_id(attributedString, s("appendAttributedString:"), thisString); + } + + return attributedString; + +} + + id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) { // Create new Dictionary diff --git a/v2/internal/ffenestri/menu_darwin.h b/v2/internal/ffenestri/menu_darwin.h index 1ab4e5a9..3fa09cb5 100644 --- a/v2/internal/ffenestri/menu_darwin.h +++ b/v2/internal/ffenestri/menu_darwin.h @@ -112,5 +112,6 @@ void processMenuData(Menu *menu, JsonNode *menuData); void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup); id GetMenu(Menu *menu); id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA); +id createAttributedStringFromStyledLabel(JsonNode *styledLabel, const char* fontName, int fontSize); #endif //ASSETS_C_MENU_DARWIN_H diff --git a/v2/internal/ffenestri/traymenu_darwin.c b/v2/internal/ffenestri/traymenu_darwin.c index d22aa0a5..a78dadd2 100644 --- a/v2/internal/ffenestri/traymenu_darwin.c +++ b/v2/internal/ffenestri/traymenu_darwin.c @@ -42,6 +42,8 @@ TrayMenu* NewTrayMenu(const char* menuJSON) { result->disabled = false; getJSONBool(processedJSON, "Disabled", &result->disabled); + result->styledLabel = getJSONObject(processedJSON, "StyledLabel"); + // Create the menu JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu"); result->menu = NewMenu(processedMenu); @@ -63,7 +65,7 @@ void DumpTrayMenu(TrayMenu* trayMenu) { } -void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled) { +void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled, JsonNode *styledLabel) { // Exit early if NULL if( trayMenu->label == NULL ) { @@ -71,7 +73,12 @@ void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName } // Update button label id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button")); - id attributedString = createAttributedString(label, fontName, fontSize, RGBA); + id attributedString = NULL; + if( styledLabel != NULL) { + attributedString = createAttributedStringFromStyledLabel(styledLabel, fontName, fontSize); + } else { + attributedString = createAttributedString(label, fontName, fontSize, RGBA); + } if( tooltip != NULL ) { msg_id(statusBarButton, s("setToolTip:"), str(tooltip)); @@ -125,7 +132,7 @@ void ShowTrayMenu(TrayMenu* trayMenu) { UpdateTrayIcon(trayMenu); // Update the label if needed - UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled); + UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled, trayMenu->styledLabel); // Update the menu id menu = GetMenu(trayMenu->menu); @@ -161,6 +168,7 @@ void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) { // Copy the other data currentMenu->ID = newMenu->ID; currentMenu->label = newMenu->label; + currentMenu->styledLabel = newMenu->styledLabel; currentMenu->trayIconPosition = newMenu->trayIconPosition; currentMenu->icon = newMenu->icon; @@ -220,7 +228,7 @@ void LoadTrayIcons() { int length = atoi((const char *)lengthAsString); // Create the icon and add to the hashmap - id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), data, length); + id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), (id)data, length); id trayImage = ALLOC("NSImage"); msg_id(trayImage, s("initWithData:"), imageData); hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage); diff --git a/v2/internal/ffenestri/traymenu_darwin.h b/v2/internal/ffenestri/traymenu_darwin.h index 298e1131..683881fe 100644 --- a/v2/internal/ffenestri/traymenu_darwin.h +++ b/v2/internal/ffenestri/traymenu_darwin.h @@ -29,6 +29,8 @@ typedef struct { JsonNode* processedJSON; + JsonNode* styledLabel; + id delegate; } TrayMenu; @@ -38,7 +40,7 @@ void DumpTrayMenu(TrayMenu* trayMenu); void ShowTrayMenu(TrayMenu* trayMenu); void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu); void UpdateTrayIcon(TrayMenu *trayMenu); -void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled); +void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled, JsonNode *styledLabel); void LoadTrayIcons(); void UnloadTrayIcons(); diff --git a/v2/internal/ffenestri/traymenustore_darwin.c b/v2/internal/ffenestri/traymenustore_darwin.c index c6a30a89..cefa6052 100644 --- a/v2/internal/ffenestri/traymenustore_darwin.c +++ b/v2/internal/ffenestri/traymenustore_darwin.c @@ -127,7 +127,9 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) { bool disabled = false; getJSONBool(parsedUpdate, "Disabled", &disabled); - UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled); + JsonNode *styledLabel = getJSONObject(parsedUpdate, "StyledLabel"); + + UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled, styledLabel); } diff --git a/v2/internal/menumanager/processedMenu.go b/v2/internal/menumanager/processedMenu.go index bdb4fe4e..25f318ec 100644 --- a/v2/internal/menumanager/processedMenu.go +++ b/v2/internal/menumanager/processedMenu.go @@ -2,6 +2,9 @@ package menumanager import ( "encoding/json" + "strings" + + "github.com/leaanthony/go-ansi-parser" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu/keys" @@ -41,11 +44,25 @@ type ProcessedMenuItem struct { // Tooltip Tooltip string `json:",omitempty"` + + // Styled label + StyledLabel []*ansi.StyledText } func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem { ID := menuItemMap.menuItemToIDMap[menuItem] + + // Parse ANSI text + var styledLabel []*ansi.StyledText + tempLabel := menuItem.Label + if strings.Contains(tempLabel, "\033[") { + parsedLabel, err := ansi.Parse(menuItem.Label) + if err == nil { + styledLabel = parsedLabel + } + } + result := &ProcessedMenuItem{ ID: ID, Label: menuItem.Label, @@ -63,6 +80,7 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr MacTemplateImage: menuItem.MacTemplateImage, MacAlternate: menuItem.MacAlternate, Tooltip: menuItem.Tooltip, + StyledLabel: styledLabel, } if menuItem.SubMenu != nil { diff --git a/v2/internal/menumanager/traymenu.go b/v2/internal/menumanager/traymenu.go index d6901c26..61343720 100644 --- a/v2/internal/menumanager/traymenu.go +++ b/v2/internal/menumanager/traymenu.go @@ -4,8 +4,11 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "sync" + "github.com/leaanthony/go-ansi-parser" + "github.com/pkg/errors" "github.com/wailsapp/wails/v2/pkg/menu" ) @@ -36,6 +39,7 @@ type TrayMenu struct { menu *menu.Menu ProcessedMenu *WailsMenu trayMenu *menu.TrayMenu + StyledLabel []*ansi.StyledText } func (t *TrayMenu) AsJSON() (string, error) { @@ -48,6 +52,16 @@ func (t *TrayMenu) AsJSON() (string, error) { func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { + // Parse ANSI text + var styledLabel []*ansi.StyledText + tempLabel := trayMenu.Label + if strings.Contains(tempLabel, "\033[") { + parsedLabel, err := ansi.Parse(tempLabel) + if err == nil { + styledLabel = parsedLabel + } + } + result := &TrayMenu{ Label: trayMenu.Label, FontName: trayMenu.FontName, @@ -60,6 +74,7 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { RGBA: trayMenu.RGBA, menuItemMap: NewMenuItemMap(), trayMenu: trayMenu, + StyledLabel: styledLabel, } result.menuItemMap.AddMenu(trayMenu.Menu) @@ -158,6 +173,17 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { Tooltip string Image string MacTemplateImage bool + StyledLabel []*ansi.StyledText + } + + // Parse ANSI text + var styledLabel []*ansi.StyledText + tempLabel := trayMenu.Label + if strings.Contains(tempLabel, "\033[") { + parsedLabel, err := ansi.Parse(tempLabel) + if err == nil { + styledLabel = parsedLabel + } } update := &LabelUpdate{ @@ -170,6 +196,7 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { Image: trayMenu.Image, MacTemplateImage: trayMenu.MacTemplateImage, RGBA: trayMenu.RGBA, + StyledLabel: styledLabel, } data, err := json.Marshal(update)