Retina & theme aware dialog icons. Default icons + override ability

This commit is contained in:
Lea Anthony
2020-12-30 22:36:23 +11:00
parent 91fb3501c5
commit 2a93e2694d
32 changed files with 390 additions and 31 deletions

File diff suppressed because one or more lines are too long

View File

@@ -31,6 +31,7 @@ extern void ToggleFullscreen(void *app);
extern void DisableFrame(void *app);
extern void OpenDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(void *appPointer, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton);
extern void DarkModeEnabled(void *appPointer, char *callbackID);
extern void UpdateMenu(void *app, char *menuAsJSON);
extern void UpdateTray(void *app, char *menuAsJSON);

View File

@@ -153,6 +153,39 @@ func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string
)
}
// MessageDialog will open a message dialog with the given options
func (c *Client) MessageDialog(dialogOptions *options.MessageDialog, callbackID string) {
// Sanity check button length
if len(dialogOptions.Buttons) > 4 {
c.app.logger.Error("Given %d message dialog buttons. Maximum is 4", len(dialogOptions.Buttons))
return
}
// Reverse
// Process buttons
buttons := []string{"", "", "", ""}
count := 0
for i := len(dialogOptions.Buttons) - 1; i >= 0; i-- {
buttons[count] = dialogOptions.Buttons[i]
count++
}
C.MessageDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(string(dialogOptions.Type)),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(dialogOptions.Message),
c.app.string2CString(dialogOptions.Icon),
c.app.string2CString(buttons[0]),
c.app.string2CString(buttons[1]),
c.app.string2CString(buttons[2]),
c.app.string2CString(buttons[3]),
c.app.string2CString(dialogOptions.DefaultButton),
c.app.string2CString(dialogOptions.CancelButton))
}
func (c *Client) DarkModeEnabled(callbackID string) {
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
}

View File

@@ -23,9 +23,11 @@
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))msg)(receiver, s("backingScaleFactor"))
#define STREQ(a,b) strcmp(a, b) == 0
#define STRCOPY(a) concat(a, "")
#define STR_HAS_CHARS(input) input != NULL && strlen(input) > 0
#define MEMFREE(input) free((void*)input); input = NULL;
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
@@ -103,7 +105,8 @@ extern const unsigned char runtime;
extern const unsigned char *trayIcons[];
// Dialog icons
extern const unsigned char *dialogIcons[];
extern const unsigned char *defaultDialogIcons[];
extern const unsigned char *userDialogIcons[];
// MAIN DEBUG FLAG
int debug;
@@ -287,6 +290,14 @@ void Fatal(struct Application *app, const char *message, ... ) {
va_end(args);
}
bool isRetina(struct Application *app) {
CGFloat scale = GET_BACKINGSCALEFACTOR(app->mainWindow);
if( (int)scale == 1 ) {
return false;
}
return true;
}
void TitlebarAppearsTransparent(struct Application* app) {
app->titlebarAppearsTransparent = 1;
}
@@ -1168,32 +1179,124 @@ void SetPosition(struct Application *app, int x, int y) {
);
}
void processDialogButton(id alert, char *buttonTitle, char *cancelButton, char *defaultButton) {
// If this button is set
if( STR_HAS_CHARS(buttonTitle) ) {
id button = msg(alert, s("addButtonWithTitle:"), str(buttonTitle));
if ( STREQ( buttonTitle, defaultButton) ) {
msg(button, s("setKeyEquivalent:"), str("\r"));
}
if ( STREQ( buttonTitle, cancelButton) ) {
msg(button, s("setKeyEquivalent:"), str("\033"));
}
}
}
void showDialog(struct Application *app) {
extern void MessageDialog(struct Application *app, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton) {
ON_MAIN_THREAD(
id alert = ALLOC_INIT("NSAlert");
msg(alert, s("setAlertStyle:"), NSAlertStyleInformational);
msg(alert, s("setMessageText:"), str("Hello World!"));
msg(alert, s("setInformativeText:"), str("Hello again World!"));
msg(alert, s("addButtonWithTitle:"), str("One"));
msg(alert, s("addButtonWithTitle:"), str("Two"));
msg(alert, s("addButtonWithTitle:"), str("Three"));
msg(alert, s("addButtonWithTitle:"), str("Four"));
id dialogImage = hashmap_get(&dialogIconCache, "info", strlen("info"));
char *dialogType = type;
char *dialogIcon = type;
// Default to info type
if( dialogType == NULL ) {
dialogType = "info";
}
// Set the dialog style
if( STREQ(dialogType, "info") || STREQ(dialogType, "question") ) {
msg(alert, s("setAlertStyle:"), NSAlertStyleInformational);
} else if( STREQ(dialogType, "warning") ) {
msg(alert, s("setAlertStyle:"), NSAlertStyleWarning);
} else if( STREQ(dialogType, "error") ) {
msg(alert, s("setAlertStyle:"), NSAlertStyleCritical);
}
// Set title if given
if( strlen(title) > 0 ) {
msg(alert, s("setMessageText:"), str(title));
}
// Set message if given
if( strlen(message) > 0) {
msg(alert, s("setInformativeText:"), str(message));
}
// Process buttons
processDialogButton(alert, button1, cancelButton, defaultButton);
processDialogButton(alert, button2, cancelButton, defaultButton);
processDialogButton(alert, button3, cancelButton, defaultButton);
processDialogButton(alert, button4, cancelButton, defaultButton);
// Check for custom dialog icon
if( strlen(icon) > 0 ) {
dialogIcon = icon;
}
// Determine what dialog icon we are looking for
id dialogImage;
// Look for `name-theme2x` first
char *themeIcon = concat(dialogIcon, (isDarkMode(app) ? "-dark" : "-light") );
if( isRetina(app) ) {
char *dialogIcon2x = concat(themeIcon, "2x");
dialogImage = hashmap_get(&dialogIconCache, dialogIcon2x, strlen(dialogIcon2x));
// if (dialogImage != NULL ) printf("Using %s\n", dialogIcon2x);
MEMFREE(dialogIcon2x);
// Now look for non-themed icon `name2x`
if ( dialogImage == NULL ) {
dialogIcon2x = concat(dialogIcon, "2x");
dialogImage = hashmap_get(&dialogIconCache, dialogIcon2x, strlen(dialogIcon2x));
// if (dialogImage != NULL ) printf("Using %s\n", dialogIcon2x);
MEMFREE(dialogIcon2x);
}
}
// If we don't have a retina icon, try the 1x name-theme icon
if( dialogImage == NULL ) {
dialogImage = hashmap_get(&dialogIconCache, themeIcon, strlen(themeIcon));
// if (dialogImage != NULL ) printf("Using %s\n", themeIcon);
}
// Free the theme icon memory
MEMFREE(themeIcon);
// Finally try the name itself
if( dialogImage == NULL ) {
dialogImage = hashmap_get(&dialogIconCache, dialogIcon, strlen(dialogIcon));
// if (dialogImage != NULL ) printf("Using %s\n", dialogIcon);
}
msg(alert, s("setIcon:"), dialogImage);
// Run modal
char *buttonPressed;
int response = (int)msg(alert, s("runModal"));
if( response == NSAlertFirstButtonReturn ) {
printf("FIRST BUTTON PRESSED");
buttonPressed = button1;
}
else if( response == NSAlertSecondButtonReturn ) {
printf("SECOND BUTTON PRESSED");
buttonPressed = button2;
}
else if( response == NSAlertThirdButtonReturn ) {
printf("THIRD BUTTON PRESSED");
buttonPressed = button3;
}
else {
printf("FORTH BUTTON PRESSED");
buttonPressed = button4;
}
// Construct callback message. Format "DS<callbackID>|<selected button index>"
const char *callback = concat("DM", callbackID);
const char *header = concat(callback, "|");
const char *responseMessage = concat(header, buttonPressed);
// Send message to backend
app->sendMessageToBackend(responseMessage);
// Free memory
MEMFREE(header);
MEMFREE(callback);
MEMFREE(responseMessage);
);
}
@@ -2467,14 +2570,7 @@ void UpdateContextMenus(struct Application *app, const char *contextMenusAsJSON)
);
}
void processDialogIcons(struct Application *app) {
// Allocate the Dialog icon hashmap
if( 0 != hashmap_create((const unsigned)4, &dialogIconCache)) {
// Couldn't allocate map
Fatal(app, "Not enough memory to allocate dialogIconCache!");
return;
}
void processDialogIcons(struct hashmap_s *hashmap, const unsigned char *dialogIcons[]) {
unsigned int count = 0;
while( 1 ) {
@@ -2496,11 +2592,25 @@ void processDialogIcons(struct Application *app) {
id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
id dialogImage = ALLOC("NSImage");
msg(dialogImage, s("initWithData:"), imageData);
hashmap_put(&dialogIconCache, (const char *)name, strlen((const char *)name), dialogImage);
hashmap_put(hashmap, (const char *)name, strlen((const char *)name), dialogImage);
}
}
void processUserDialogIcons(struct Application *app) {
// Allocate the Dialog icon hashmap
if( 0 != hashmap_create((const unsigned)4, &dialogIconCache)) {
// Couldn't allocate map
Fatal(app, "Not enough memory to allocate dialogIconCache!");
return;
}
processDialogIcons(&dialogIconCache, defaultDialogIcons);
processDialogIcons(&dialogIconCache, userDialogIcons);
}
void Run(struct Application *app, int argc, char **argv) {
@@ -2688,7 +2798,7 @@ void Run(struct Application *app, int argc, char **argv) {
}
// Process dialog icons
processDialogIcons(app);
processUserDialogIcons(app);
// We set it to be invisible by default. It will become visible when everything has initialised
msg(app->mainWindow, s("setIsVisible:"), NO);

View File

@@ -17,6 +17,7 @@ type Client interface {
CallResult(message string)
OpenDialog(dialogOptions *options.OpenDialog, callbackID string)
SaveDialog(dialogOptions *options.SaveDialog, callbackID string)
MessageDialog(dialogOptions *options.MessageDialog, callbackID string)
WindowSetTitle(title string)
WindowShow()
WindowHide()

View File

@@ -42,6 +42,9 @@ func dialogMessageParser(message string) (*parsedMessage, error) {
case 'S':
topic = "dialog:saveselected:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData}
case 'M':
topic = "dialog:messageselected:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData}
}
default:

View File

@@ -416,7 +416,20 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
for _, client := range d.clients {
client.frontend.SaveDialog(dialogOptions, callbackID)
}
case "message":
dialogOptions, ok := result.Data().(*options.MessageDialog)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:message' : %#v", result.Data())
return
}
// This is hardcoded in the sender too
callbackID := splitTopic[3]
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.MessageDialog(dialogOptions, callbackID)
}
default:
d.logger.Error("Unknown dialog type: %s", dialogType)
}

View File

@@ -12,6 +12,7 @@ import (
type Dialog interface {
Open(dialogOptions *options.OpenDialog) []string
Save(dialogOptions *options.SaveDialog) string
Message(dialogOptions *options.MessageDialog) string
}
// dialog exposes the Dialog interface
@@ -92,3 +93,28 @@ func (r *dialog) Save(dialogOptions *options.SaveDialog) string {
return result.Data().(string)
}
// Message show a message to the user
func (r *dialog) Message(dialogOptions *options.MessageDialog) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:messageselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:select:message:" + uniqueCallback
r.bus.Publish(message, dialogOptions)
// Wait for result
var result *servicebus.Message = <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string)
}

View File

@@ -127,9 +127,9 @@ func (d *DesktopBuilder) processDialogIcons(assetDir string, options *Options) e
}
// Setup target
targetFilename := "dialogicons"
targetFilename := "userdialogicons"
targetFile := filepath.Join(assetDir, targetFilename+".c")
//d.addFileToDelete(targetFile)
d.addFileToDelete(targetFile)
var dataBytes []byte
@@ -137,7 +137,7 @@ func (d *DesktopBuilder) processDialogIcons(assetDir string, options *Options) e
var cdata strings.Builder
// Write header
header := `// dialogicons.c
header := `// userdialogicons.c
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL.
// This file was auto-generated. DO NOT MODIFY.
@@ -156,16 +156,16 @@ func (d *DesktopBuilder) processDialogIcons(assetDir string, options *Options) e
}
iconname := strings.TrimSuffix(filepath.Base(filename), ".png")
dialogIconName := fmt.Sprintf("dialogIcon%dName", count)
dialogIconName := fmt.Sprintf("userDialogIcon%dName", count)
variableList.Add(dialogIconName)
cdata.WriteString(fmt.Sprintf("const unsigned char %s[] = { %s0x00 };\n", dialogIconName, d.convertToHexLiteral([]byte(iconname))))
dialogIconLength := fmt.Sprintf("dialogIcon%dLength", count)
dialogIconLength := fmt.Sprintf("userDialogIcon%dLength", count)
variableList.Add(dialogIconLength)
lengthAsString := strconv.Itoa(len(dataBytes))
cdata.WriteString(fmt.Sprintf("const unsigned char %s[] = { %s0x00 };\n", dialogIconLength, d.convertToHexLiteral([]byte(lengthAsString))))
dialogIconData := fmt.Sprintf("dialogIcon%dData", count)
dialogIconData := fmt.Sprintf("userDialogIcon%dData", count)
variableList.Add(dialogIconData)
cdata.WriteString(fmt.Sprintf("const unsigned char %s[] = { ", dialogIconData))
@@ -178,7 +178,7 @@ func (d *DesktopBuilder) processDialogIcons(assetDir string, options *Options) e
}
// Write out main dialogIcons data
cdata.WriteString("const unsigned char *dialogIcons[] = { ")
cdata.WriteString("const unsigned char *userDialogIcons[] = { ")
cdata.WriteString(variableList.Join(", "))
if len(dialogIconFilenames) > 0 {
cdata.WriteString(", ")

View File

@@ -0,0 +1,3 @@
# Default Dialog Icons
This directory contains the default dialog icons. These are pre-compiled into a single C file (`defaultDialogIcons.c`) which resides in the `ffenestri` directory. If these icons are ever updated, then there is a need to run: `go run build.go` in this directory. This will generate a new `defaultDialogIcons.c` file in the `ffenestri` directory.

View File

@@ -0,0 +1,89 @@
package main
import (
"fmt"
"github.com/leaanthony/slicer"
"io/ioutil"
"log"
"path/filepath"
"strconv"
"strings"
)
func convertToHexLiteral(bytes []byte) string {
result := ""
for _, b := range bytes {
result += fmt.Sprintf("0x%x, ", b)
}
return result
}
func main() {
dialogIconFilenames, err := filepath.Glob("*.png")
if err != nil {
log.Fatal(err)
}
// Setup target
targetFile := "../../../../../../../internal/ffenestri/defaultdialogicons.c"
var dataBytes []byte
// Use a strings builder
var cdata strings.Builder
// Write header
header := `// defaultdialogicons.c
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL.
// This file was auto-generated. DO NOT MODIFY.
`
cdata.WriteString(header)
var variableList slicer.StringSlicer
// Loop over icons
for count, filename := range dialogIconFilenames {
// Load the tray icon
dataBytes, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
iconname := strings.TrimSuffix(filepath.Base(filename), ".png")
dialogIconName := fmt.Sprintf("defaultDialogIcon%dName", count)
variableList.Add(dialogIconName)
cdata.WriteString(fmt.Sprintf("const unsigned char %s[] = { %s0x00 };\n", dialogIconName, convertToHexLiteral([]byte(iconname))))
dialogIconLength := fmt.Sprintf("defaultDialogIcon%dLength", count)
variableList.Add(dialogIconLength)
lengthAsString := strconv.Itoa(len(dataBytes))
cdata.WriteString(fmt.Sprintf("const unsigned char %s[] = { %s0x00 };\n", dialogIconLength, convertToHexLiteral([]byte(lengthAsString))))
dialogIconData := fmt.Sprintf("defaultDialogIcon%dData", count)
variableList.Add(dialogIconData)
cdata.WriteString(fmt.Sprintf("const unsigned char %s[] = { ", dialogIconData))
// Convert each byte to hex
for _, b := range dataBytes {
cdata.WriteString(fmt.Sprintf("0x%x, ", b))
}
cdata.WriteString("0x00 };\n")
}
// Write out main dialogIcons data
cdata.WriteString("const unsigned char *defaultDialogIcons[] = { ")
cdata.WriteString(variableList.Join(", "))
if len(dialogIconFilenames) > 0 {
cdata.WriteString(", ")
}
cdata.WriteString("0x00 };\n")
err = ioutil.WriteFile(targetFile, []byte(cdata.String()), 0600)
if err != nil {
log.Fatal(err)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -25,3 +25,23 @@ type SaveDialog struct {
CanCreateDirectories bool
TreatPackagesAsDirectories bool
}
type DialogType string
const (
InfoDialog DialogType = "info"
WarningDialog DialogType = "warning"
ErrorDialog DialogType = "error"
QuestionDialog DialogType = "question"
)
// MessageDialog contains the options for the Message dialogs, EG Info, Warning, etc runtime methods
type MessageDialog struct {
Type DialogType
Title string
Message string
Buttons []string
DefaultButton string
CancelButton string
Icon string
}

View File

@@ -26,3 +26,22 @@ func (l *Dialog) Open(options *options.OpenDialog) []string {
func (l *Dialog) Save(options *options.SaveDialog) string {
return l.runtime.Dialog.Save(options)
}
// Message Dialog
func (l *Dialog) Message(options *options.MessageDialog) string {
return l.runtime.Dialog.Message(options)
}
// Message Dialog
func (l *Dialog) Test() string {
return l.runtime.Dialog.Message(&options.MessageDialog{
Type: options.InfoDialog,
Title: " ",
Message: "I am a longer message but these days, can't be too long!",
// Buttons are declared in the order they should be appear in
Buttons: []string{"test", "Cancel", "OK"},
DefaultButton: "OK",
CancelButton: "Cancel",
//Icon: "wails",
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB