Compare commits

...

13 Commits

Author SHA1 Message Date
Lea Anthony
480dcb7895 TEMP COMMIT 2021-01-22 15:26:58 +11:00
Lea Anthony
c8d89cf002 [WIP] Use simpler menu id mechanism. Smaller json. Remove pointer reliance. 2021-01-18 06:07:55 +11:00
Lea Anthony
fc669ede37 Tidy up debug output 2021-01-16 14:43:32 +11:00
Lea Anthony
47bca0be88 Support updating tray labels in an efficient manner 2021-01-16 11:35:49 +11:00
Lea Anthony
7ac8cc6b8b Add Menu.Merge() to combine 2 menus 2021-01-15 11:53:55 +11:00
Lea Anthony
b435ec1217 v2.0.0-alpha.10 2021-01-14 20:43:26 +11:00
Lea Anthony
688d4fee6a UpdateTrayMenu -> SetTrayMenu (upsert). Support no menus on trays. 2021-01-14 16:13:59 +11:00
Lea Anthony
29ffeaa9f3 Misc tidy up 2021-01-14 13:55:20 +11:00
Lea Anthony
742e4ba2cb Remove WailsInit and WailsShutdown methodsr 2021-01-14 11:07:06 +11:00
Lea Anthony
0a0063de1f bump version 2021-01-14 00:28:15 +11:00
Lea Anthony
1b7d1e61cc v2.0.0-alpha.8 2021-01-13 23:47:15 +11:00
Lea Anthony
15a273458e Ensure build directory exists when building 2021-01-13 23:46:47 +11:00
Lea Anthony
eef8eb756f Bump version 2021-01-13 22:56:09 +11:00
78 changed files with 4792 additions and 2144 deletions

View File

@@ -1,3 +1,3 @@
package main
var version = "v2.0.0-alpha.6"
var version = "v2.0.0-alpha.10"

View File

@@ -43,6 +43,10 @@ type App struct {
// Application Stores
loglevelStore *runtime.Store
appconfigStore *runtime.Store
// Startup/Shutdown
startupCallback func(*runtime.Runtime)
shutdownCallback func()
}
// Create App
@@ -76,11 +80,13 @@ func CreateApp(appoptions *options.App) (*App, error) {
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger, menuManager)
result := &App{
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
menuManager: menuManager,
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
menuManager: menuManager,
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
}
result.options = appoptions
@@ -112,7 +118,7 @@ func (a *App) Run() error {
return err
}
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, a.startupCallback, a.shutdownCallback)
if err != nil {
return err
}

View File

@@ -34,26 +34,6 @@ func (b *Bindings) Add(structPtr interface{}) error {
structName := splitName[1]
methodName := splitName[2]
// Is this WailsInit?
if method.IsWailsInit() {
err := b.db.AddWailsInit(method)
if err != nil {
return err
}
b.logger.Trace("Registered WailsInit method: %s", method.Name)
continue
}
// Is this WailsShutdown?
if method.IsWailsShutdown() {
err := b.db.AddWailsShutdown(method)
if err != nil {
return err
}
b.logger.Trace("Registered WailsShutdown method: %s", method.Name)
continue
}
// Add it as a regular method
b.db.AddMethod(packageName, structName, methodName, method)

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// BoundMethod defines all the data related to a Go method that is
@@ -17,58 +16,6 @@ type BoundMethod struct {
Method reflect.Value `json:"-"`
}
// IsWailsInit returns true if the method name is "WailsInit"
func (b *BoundMethod) IsWailsInit() bool {
return strings.HasSuffix(b.Name, "WailsInit")
}
// IsWailsShutdown returns true if the method name is "WailsShutdown"
func (b *BoundMethod) IsWailsShutdown() bool {
return strings.HasSuffix(b.Name, "WailsShutdown")
}
// VerifyWailsInit checks if the WailsInit signature is correct
func (b *BoundMethod) VerifyWailsInit() error {
// Must only have 1 input
if b.InputCount() != 1 {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Check input type
if !b.Inputs[0].IsType("*runtime.Runtime") {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Must only have 1 output
if b.OutputCount() != 1 {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Check output type
if !b.Outputs[0].IsError() {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Input must be of type Runtime
return nil
}
// VerifyWailsShutdown checks if the WailsShutdown signature is correct
func (b *BoundMethod) VerifyWailsShutdown() error {
// Must have no inputs
if b.InputCount() != 0 {
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
}
// Must have no outputs
if b.OutputCount() != 0 {
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
}
// Input must be of type Runtime
return nil
}
// InputCount returns the number of inputs this bound method has
func (b *BoundMethod) InputCount() int {
return len(b.Inputs)

View File

@@ -15,10 +15,6 @@ type DB struct {
// It used for performance gains at runtime
methodMap map[string]*BoundMethod
// These are slices of methods registered using WailsInit and WailsShutdown
wailsInitMethods []*BoundMethod
wailsShutdownMethods []*BoundMethod
// Lock to ensure sync access to the data
lock sync.RWMutex
}
@@ -94,38 +90,6 @@ func (d *DB) AddMethod(packageName string, structName string, methodName string,
}
// AddWailsInit checks the given method is a WailsInit method and if it
// is, adds it to the list of WailsInit methods
func (d *DB) AddWailsInit(method *BoundMethod) error {
err := method.VerifyWailsInit()
if err != nil {
return err
}
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
d.wailsInitMethods = append(d.wailsInitMethods, method)
return nil
}
// AddWailsShutdown checks the given method is a WailsInit method and if it
// is, adds it to the list of WailsShutdown methods
func (d *DB) AddWailsShutdown(method *BoundMethod) error {
err := method.VerifyWailsShutdown()
if err != nil {
return err
}
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
d.wailsShutdownMethods = append(d.wailsShutdownMethods, method)
return nil
}
// ToJSON converts the method map to JSON
func (d *DB) ToJSON() (string, error) {
@@ -138,13 +102,3 @@ func (d *DB) ToJSON() (string, error) {
// Return zero copy string as this string will be read only
return *(*string)(unsafe.Pointer(&bytes)), err
}
// WailsInitMethods returns the list of registered WailsInit methods
func (d *DB) WailsInitMethods() []*BoundMethod {
return d.wailsInitMethods
}
// WailsShutdownMethods returns the list of registered WailsInit methods
func (d *DB) WailsShutdownMethods() []*BoundMethod {
return d.wailsShutdownMethods
}

View File

@@ -0,0 +1,40 @@
package counter
import "sync"
type Counter struct {
initialValue uint64
value uint64
lock sync.Mutex
}
func NewCounter(initialValue uint64) *Counter {
return &Counter{
initialValue: initialValue,
value: initialValue,
}
}
// SetValue sets the value to the given value
func (c *Counter) SetValue(value uint64) {
c.lock.Lock()
c.value = value
c.lock.Unlock()
}
// Increment adds 1 to the counter and returns its value
func (c *Counter) Increment() uint64 {
var result uint64
c.lock.Lock()
c.value++
result = c.value
c.lock.Unlock()
return result
}
// Reset the value to the initial value
func (c *Counter) Reset() {
c.lock.Lock()
c.value = c.initialValue
c.lock.Unlock()
}

View File

@@ -10,4 +10,8 @@ License: http://git.ozlabs.org/?p=ccan;a=blob;f=licenses/BSD-MIT;h=89de354795ec7
## hashmap
Homepage: https://github.com/sheredom/hashmap.h
License: https://github.com/sheredom/hashmap.h/blob/master/LICENSE
License: https://github.com/sheredom/hashmap.h/blob/master/LICENSE
## utf8.h
Homepage: https://github.com/sheredom/utf8.h
License: https://github.com/sheredom/utf8.h/blob/master/LICENSE

View File

@@ -38,21 +38,31 @@ int freeHashmapItem(void *const context, struct hashmap_element_s *const e) {
const char* getJSONString(JsonNode *item, const char* key) {
// Get key
JsonNode *node = json_find_member(item, key);
const char *result = "";
const char *result = NULL;
if ( node != NULL && node->tag == JSON_STRING) {
result = node->string_;
}
return result;
}
const char* getJSONStringDefault(JsonNode *item, const char* key, const char* defaultValue) {
const char* result = getJSONString(item, key);
if ( result == NULL ) result = defaultValue;
return result;
}
void ABORT_JSON(JsonNode *node, const char* key) {
ABORT("Unable to read required key '%s' from JSON: %s\n", key, json_encode(node));
}
const char* mustJSONString(JsonNode *node, const char* key) {
const char* result = getJSONString(node, key);
if ( result == NULL ) {
ABORT_JSON(node, key);
const char* mustJSONString(JsonNode *item, const char* key) {
JsonNode *member = json_find_member(item, key);
if ( member == NULL ) {
ABORT_JSON(item, key);
}
const char *result = "";
if ( member != NULL && member->tag == JSON_STRING) {
result = member->string_;
}
return result;
}
@@ -68,15 +78,23 @@ JsonNode* getJSONObject(JsonNode* node, const char* key) {
return json_find_member(node, key);
}
bool getJSONBool(JsonNode *item, const char* key, bool *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_BOOL) {
*result = node->bool_;
return true;
bool getJSONBool(JsonNode *node, const char* key) {
JsonNode *result = json_find_member(node, key);
if ( result != NULL && result->tag == JSON_BOOL) {
return result->bool_;
}
return false;
}
int mustJSONInt(JsonNode *node, const char* key) {
JsonNode *result = json_find_member(node, key);
if ( result == NULL || result->tag != JSON_NUMBER) {
ABORT_JSON(result, key);
}
return (int) result->number_;
}
bool getJSONInt(JsonNode *item, const char* key, int *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_NUMBER) {
@@ -86,3 +104,10 @@ bool getJSONInt(JsonNode *item, const char* key, int *result) {
return false;
}
JsonNode* mustParseJSON(const char* JSON) {
JsonNode* parsedUpdate = json_decode(JSON);
if ( parsedUpdate == NULL ) {
ABORT("Unable to decode JSON: %s\n", JSON);
}
return parsedUpdate;
}

View File

@@ -18,21 +18,45 @@
#define STREQ(a,b) strcmp(a, b) == 0
#define STREMPTY(string) strlen(string) == 0
#define STRCOPY(a) concat(a, "")
#define STRCOPY(str) (str == NULL ? NULL : concat(str, ""))
#define STR_HAS_CHARS(input) input != NULL && strlen(input) > 0
#define MEMFREE(input) free((void*)input); input = NULL;
#define MEMFREE(input) if(input != NULL) { free((void*)input); input = NULL; }
#define FREE_AND_SET(variable, value) if( variable != NULL ) { MEMFREE(variable); } variable = value
#define NEW(struct) malloc(sizeof(struct));
#define HASHMAP_INIT(hashmap, initialSize, name) \
if( 0 != hashmap_create((const unsigned)initialSize, &hashmap)) { \
ABORT("Not enough memory to allocate %s!\n", name); \
}
#define HASHMAP_PUT(hashmap, key, value) hashmap_put(&hashmap, key, strlen(key), value);
#define HASHMAP_GET(hashmap, key) hashmap_get(&hashmap, key, strlen(key));
#define HASHMAP_DESTROY(hashmap) hashmap_destroy(&hashmap);
#define HASHMAP_SIZE(hashmap) hashmap_num_entries(&hashmap)
#define HASHMAP_ITERATE(hashmap, function, context) if( hashmap_num_entries(&hashmap) > 0 ) { \
if (0!=hashmap_iterate_pairs(&hashmap, function, context)) { \
ABORT("failed to iterate hashmap entries!"); \
} \
}
#define JSON_ADD_STRING(jsonObject, key, value) if( value != NULL ) { json_append_member(jsonObject, (char*)key, json_mkstring(value)); }
#define JSON_ADD_NUMBER(jsonObject, key, value) json_append_member(jsonObject, (char*)key, json_mknumber(value));
#define JSON_ADD_OBJECT(jsonObject, key, value) if( value != NULL ) { json_append_member(jsonObject, (char*)key, value); }
#define JSON_ADD_BOOL(jsonObject, key, value) json_append_member(jsonObject, (char*)key, json_mkbool(value));
#define JSON_ADD_ELEMENT(jsonObject, value) json_append_element(jsonObject, value);
// Credit: https://stackoverflow.com/a/8465083
char* concat(const char *string1, const char *string2);
void ABORT(const char *message, ...);
int freeHashmapItem(void *const context, struct hashmap_element_s *const e);
int freeHashmapItem(void *context, struct hashmap_element_s *e);
const char* getJSONString(JsonNode *item, const char* key);
const char* getJSONStringDefault(JsonNode *item, const char* key, const char* defaultValue);
const char* mustJSONString(JsonNode *node, const char* key);
JsonNode* getJSONObject(JsonNode* node, const char* key);
JsonNode* mustJSONObject(JsonNode *node, const char* key);
bool getJSONBool(JsonNode *item, const char* key, bool *result);
bool getJSONBool(JsonNode *item, const char* key);
bool getJSONInt(JsonNode *item, const char* key, int *result);
int mustJSONInt(JsonNode *node, const char* key);
JsonNode* mustParseJSON(const char* JSON);
#endif //ASSETS_C_COMMON_H

View File

@@ -1,99 +1,99 @@
////
//// Created by Lea Anthony on 6/1/21.
//////
////// Created by Lea Anthony on 6/1/21.
//////
////
//
#include "ffenestri_darwin.h"
#include "common.h"
#include "contextmenus_darwin.h"
#include "menu_darwin.h"
ContextMenu* NewContextMenu(const char* contextMenuJSON) {
ContextMenu* result = malloc(sizeof(ContextMenu));
JsonNode* processedJSON = json_decode(contextMenuJSON);
if( processedJSON == NULL ) {
ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", contextMenuJSON);
}
// Save reference to this json
result->processedJSON = processedJSON;
result->ID = mustJSONString(processedJSON, "ID");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
result->nsmenu = NULL;
result->menu->menuType = ContextMenuType;
result->menu->parentData = result;
result->contextMenuData = NULL;
return result;
}
ContextMenu* GetContextMenuByID(ContextMenuStore* store, const char *contextMenuID) {
return (ContextMenu*)hashmap_get(&store->contextMenuMap, (char*)contextMenuID, strlen(contextMenuID));
}
void DeleteContextMenu(ContextMenu* contextMenu) {
// Free Menu
DeleteMenu(contextMenu->menu);
// Delete any context menu data we may have stored
if( contextMenu->contextMenuData != NULL ) {
MEMFREE(contextMenu->contextMenuData);
}
// Free JSON
if (contextMenu->processedJSON != NULL ) {
json_delete(contextMenu->processedJSON);
contextMenu->processedJSON = NULL;
}
// Free context menu
free(contextMenu);
}
int freeContextMenu(void *const context, struct hashmap_element_s *const e) {
DeleteContextMenu(e->data);
return -1;
}
void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData) {
// If no context menu ID was given, abort
if( contextMenuID == NULL ) {
return;
}
ContextMenu* contextMenu = GetContextMenuByID(store, contextMenuID);
// We don't need the ID now
MEMFREE(contextMenuID);
if( contextMenu == NULL ) {
// Free context menu data
if( contextMenuData != NULL ) {
MEMFREE(contextMenuData);
return;
}
}
// We need to store the context menu data. Free existing data if we have it
// and set to the new value.
FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
// Grab the content view and show the menu
id contentView = msg(mainWindow, s("contentView"));
// Get the triggering event
id menuEvent = msg(mainWindow, s("currentEvent"));
if( contextMenu->nsmenu == NULL ) {
// GetMenu creates the NSMenu
contextMenu->nsmenu = GetMenu(contextMenu->menu);
}
// Show popup
msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
}
//#include "ffenestri_darwin.h"
//#include "common.h"
//#include "contextmenus_darwin.h"
//#include "menu_darwin.h"
//
//ContextMenu* NewContextMenu(const char* contextMenuJSON, struct TrayMenuStore *store) {
// ContextMenu* result = malloc(sizeof(ContextMenu));
//
// JsonNode* processedJSON = json_decode(contextMenuJSON);
// if( processedJSON == NULL ) {
// ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", contextMenuJSON);
// }
// // Save reference to this json
// result->processedJSON = processedJSON;
//
// result->ID = mustJSONString(processedJSON, "ID");
// JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
//
//// result->menu = NewMenu(processedMenu);
// result->nsmenu = NULL;
// result->menu->menuType = ContextMenuType;
// result->menu->store = store;
// result->contextMenuData = NULL;
// return result;
//}
//
//ContextMenu* GetContextMenuByID(ContextMenuStore* store, const char *contextMenuID) {
// return (ContextMenu*)hashmap_get(&store->contextMenuMap, (char*)contextMenuID, strlen(contextMenuID));
//}
//
//void DeleteContextMenu(ContextMenu* contextMenu) {
// // Free Menu
// DeleteMenu(contextMenu->menu);
//
// // Delete any context menu data we may have stored
// if( contextMenu->contextMenuData != NULL ) {
// MEMFREE(contextMenu->contextMenuData);
// }
//
// // Free JSON
// if (contextMenu->processedJSON != NULL ) {
// json_delete(contextMenu->processedJSON);
// contextMenu->processedJSON = NULL;
// }
//
// // Free context menu
// free(contextMenu);
//}
//
//int freeContextMenu(void *const context, struct hashmap_element_s *const e) {
// DeleteContextMenu(e->data);
// return -1;
//}
//
//void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData) {
//
// // If no context menu ID was given, abort
// if( contextMenuID == NULL ) {
// return;
// }
//
// ContextMenu* contextMenu = GetContextMenuByID(store, contextMenuID);
//
// // We don't need the ID now
// MEMFREE(contextMenuID);
//
// if( contextMenu == NULL ) {
// // Free context menu data
// if( contextMenuData != NULL ) {
// MEMFREE(contextMenuData);
// return;
// }
// }
//
// // We need to store the context menu data. Free existing data if we have it
// // and set to the new value.
// FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
//
// // Grab the content view and show the menu
// id contentView = msg(mainWindow, s("contentView"));
//
// // Get the triggering event
// id menuEvent = msg(mainWindow, s("currentEvent"));
//
//// if( contextMenu->nsmenu == NULL ) {
//// // GetMenu creates the NSMenu
//// contextMenu->nsmenu = GetMenu(contextMenu->menu);
//// }
//
// // Show popup
// msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
//
//}
//

View File

@@ -1,33 +1,33 @@
//////
////// Created by Lea Anthony on 6/1/21.
//////
////
//// Created by Lea Anthony on 6/1/21.
////
//#ifndef CONTEXTMENU_DARWIN_H
//#define CONTEXTMENU_DARWIN_H
//
#ifndef CONTEXTMENU_DARWIN_H
#define CONTEXTMENU_DARWIN_H
#include "json.h"
#include "menu_darwin.h"
#include "contextmenustore_darwin.h"
typedef struct {
const char* ID;
id nsmenu;
Menu* menu;
JsonNode* processedJSON;
// Context menu data is given by the frontend when clicking a context menu.
// We send this to the backend when an item is selected
const char* contextMenuData;
} ContextMenu;
ContextMenu* NewContextMenu(const char* contextMenuJSON);
ContextMenu* GetContextMenuByID( ContextMenuStore* store, const char *contextMenuID);
void DeleteContextMenu(ContextMenu* contextMenu);
int freeContextMenu(void *const context, struct hashmap_element_s *const e);
void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData);
#endif //CONTEXTMENU_DARWIN_H
//#include "json.h"
//#include "menu_darwin.h"
//#include "contextmenustore_darwin.h"
//
//typedef struct {
// const char* ID;
// id nsmenu;
// Menu* menu;
//
// JsonNode* processedJSON;
//
// // Context menu data is given by the frontend when clicking a context menu.
// // We send this to the backend when an item is selected
// const char* contextMenuData;
//} ContextMenu;
//
//
//ContextMenu* NewContextMenu(const char* contextMenuJSON);
//
//ContextMenu* GetContextMenuByID( ContextMenuStore* store, const char *contextMenuID);
//void DeleteContextMenu(ContextMenu* contextMenu);
//int freeContextMenu(void *const context, struct hashmap_element_s *const e);
//
//void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData);
//
//#endif //CONTEXTMENU_DARWIN_H

View File

@@ -1,65 +1,65 @@
#include "contextmenus_darwin.h"
#include "contextmenustore_darwin.h"
ContextMenuStore* NewContextMenuStore() {
ContextMenuStore* result = malloc(sizeof(ContextMenuStore));
// Allocate Context Menu Store
if( 0 != hashmap_create((const unsigned)4, &result->contextMenuMap)) {
ABORT("[NewContextMenus] Not enough memory to allocate contextMenuStore!");
}
return result;
}
void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON) {
ContextMenu* newMenu = NewContextMenu(contextMenuJSON);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->contextMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
}
ContextMenu* GetContextMenuFromStore(ContextMenuStore* store, const char* menuID) {
// Get the current menu
return hashmap_get(&store->contextMenuMap, menuID, strlen(menuID));
}
void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON) {
ContextMenu* newContextMenu = NewContextMenu(menuJSON);
// Get the current menu
ContextMenu *currentMenu = GetContextMenuFromStore(store, newContextMenu->ID);
if ( currentMenu == NULL ) {
ABORT("Attempted to update unknown context menu with ID '%s'.", newContextMenu->ID);
}
hashmap_remove(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID));
// Save the status bar reference
DeleteContextMenu(currentMenu);
hashmap_put(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID), newContextMenu);
}
void DeleteContextMenuStore(ContextMenuStore* store) {
// Guard against NULLs
if( store == NULL ) {
return;
}
// Delete context menus
if( hashmap_num_entries(&store->contextMenuMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&store->contextMenuMap, freeContextMenu, NULL)) {
ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
}
}
// Free context menu hashmap
hashmap_destroy(&store->contextMenuMap);
}
//
//#include "contextmenus_darwin.h"
//#include "contextmenustore_darwin.h"
//
//ContextMenuStore* NewContextMenuStore() {
//
// ContextMenuStore* result = malloc(sizeof(ContextMenuStore));
//
// // Allocate Context Menu Store
// if( 0 != hashmap_create((const unsigned)4, &result->contextMenuMap)) {
// ABORT("[NewContextMenus] Not enough memory to allocate contextMenuStore!");
// }
//
// return result;
//}
//
//void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON) {
// ContextMenu* newMenu = NewContextMenu(contextMenuJSON);
//
// //TODO: check if there is already an entry for this menu
// hashmap_put(&store->contextMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
//}
//
//ContextMenu* GetContextMenuFromStore(ContextMenuStore* store, const char* menuID) {
// // Get the current menu
// return hashmap_get(&store->contextMenuMap, menuID, strlen(menuID));
//}
//
//void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON) {
// ContextMenu* newContextMenu = NewContextMenu(menuJSON);
//
// // Get the current menu
// ContextMenu *currentMenu = GetContextMenuFromStore(store, newContextMenu->ID);
// if ( currentMenu == NULL ) {
// ABORT("Attempted to update unknown context menu with ID '%s'.", newContextMenu->ID);
// }
//
// hashmap_remove(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID));
//
// // Save the status bar reference
// DeleteContextMenu(currentMenu);
//
// hashmap_put(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID), newContextMenu);
//
//}
//
//
//void DeleteContextMenuStore(ContextMenuStore* store) {
//
// // Guard against NULLs
// if( store == NULL ) {
// return;
// }
//
// // Delete context menus
// if( hashmap_num_entries(&store->contextMenuMap) > 0 ) {
// if (0 != hashmap_iterate_pairs(&store->contextMenuMap, freeContextMenu, NULL)) {
// ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
// }
// }
//
// // Free context menu hashmap
// hashmap_destroy(&store->contextMenuMap);
//
//}

View File

@@ -1,27 +1,27 @@
////
//// Created by Lea Anthony on 7/1/21.
////
//
// Created by Lea Anthony on 7/1/21.
//#ifndef CONTEXTMENUSTORE_DARWIN_H
//#define CONTEXTMENUSTORE_DARWIN_H
//
#ifndef CONTEXTMENUSTORE_DARWIN_H
#define CONTEXTMENUSTORE_DARWIN_H
#include "common.h"
typedef struct {
int dummy;
// This is our context menu store which keeps track
// of all instances of ContextMenus
struct hashmap_s contextMenuMap;
} ContextMenuStore;
ContextMenuStore* NewContextMenuStore();
void DeleteContextMenuStore(ContextMenuStore* store);
void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON);
void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON);
#endif //CONTEXTMENUSTORE_DARWIN_H
//#include "common.h"
//
//typedef struct {
//
// int dummy;
//
// // This is our context menu store which keeps track
// // of all instances of ContextMenus
// struct hashmap_s contextMenuMap;
//
//} ContextMenuStore;
//
//ContextMenuStore* NewContextMenuStore();
//
//void DeleteContextMenuStore(ContextMenuStore* store);
//void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON);
//
//void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON);
//
//#endif //CONTEXTMENUSTORE_DARWIN_H

View File

@@ -30,14 +30,15 @@ extern void Fullscreen(struct Application*);
extern void UnFullscreen(struct Application*);
extern void ToggleFullscreen(struct Application*);
extern void DisableFrame(struct Application*);
extern void OpenDialog(struct Application*, 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(struct Application*, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(struct Application*, 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(struct Application*, char *callbackID);
extern void OpenDialog(struct Application*, const char *callbackID, const char *title, const char *filters, const char *defaultFilename, const char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(struct Application*, const char *callbackID, const char *title, const char *filters, const char *defaultFilename, const char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(struct Application*, const char *callbackID, const char *type, const char *title, const char *message, const char *icon, const char *button1, const char *button2, const char *button3, const char *button4, const char *defaultButton, const char *cancelButton);
extern void DarkModeEnabled(struct Application*, const char *callbackID);
extern void SetApplicationMenu(struct Application*, const char *);
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
extern void UpdateTrayMenu(struct Application*, const char *menuTrayJSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);
extern void SetTrayMenu(struct Application*, const char *menuTrayJSON);
extern void UpdateTrayMenuLabel(struct Application*, const char* JSON);
extern void AddContextMenu(struct Application*, const char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, const char *contextMenuJSON);
#endif

View File

@@ -12,10 +12,10 @@ package ffenestri
import "C"
import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
// Client is our implentation of messageDispatcher.Client
@@ -120,7 +120,7 @@ func (c *Client) WindowSetColour(colour int) {
}
// OpenDialog will open a dialog with the given title and filter
func (c *Client) OpenDialog(dialogOptions *options.OpenDialog, callbackID string) {
func (c *Client) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
C.OpenDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
@@ -138,7 +138,7 @@ func (c *Client) OpenDialog(dialogOptions *options.OpenDialog, callbackID string
}
// SaveDialog will open a dialog with the given title and filter
func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string) {
func (c *Client) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
C.SaveDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
@@ -152,7 +152,7 @@ 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) {
func (c *Client) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
// Sanity check button length
if len(dialogOptions.Buttons) > 4 {
@@ -188,8 +188,12 @@ func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
}
func (c *Client) UpdateTrayMenu(trayMenuJSON string) {
C.UpdateTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
func (c *Client) SetTrayMenu(trayMenuJSON string) {
C.SetTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
}
func (c *Client) UpdateTrayMenuLabel(JSON string) {
C.UpdateTrayMenuLabel(c.app.app, c.app.string2CString(JSON))
}
func (c *Client) UpdateContextMenu(contextMenuJSON string) {

View File

@@ -1,11 +1,10 @@
#ifdef FFENESTRI_DARWIN
#include "ffenestri.h"
#include "ffenestri_darwin.h"
#include "menu_darwin.h"
#include "contextmenus_darwin.h"
#include "traymenustore_darwin.h"
#include "traymenu_darwin.h"
#include "menu.h"
//#include "contextmenus_darwin.h"
//#include "traymenustore_darwin.h"
//#include "traymenu_darwin.h"
// References to assets
#include "assets.h"
@@ -14,6 +13,10 @@ extern const unsigned char runtime;
// Dialog icons
extern const unsigned char *defaultDialogIcons[];
#include "userdialogicons.h"
#include "menu_darwin.h"
extern void messageFromWindowCallback(const char *);
typedef void (*ffenestriCallback)(const char *);
// MAIN DEBUG FLAG
int debug;
@@ -21,10 +24,6 @@ int debug;
// A cache for all our dialog icons
struct hashmap_s dialogIconCache;
// Context menu data is given by the frontend when clicking a context menu.
// We send this to the backend when an item is selected;
const char *contextMenuData;
// Dispatch Method
typedef void (^dispatchMethod)(void);
@@ -53,9 +52,6 @@ void dumpHashmap(const char *name, struct hashmap_s *hashmap) {
printf("}\n");
}
extern void messageFromWindowCallback(const char *);
typedef void (*ffenestriCallback)(const char *);
void HideMouse() {
msg(c("NSCursor"), s("hide"));
}
@@ -109,14 +105,16 @@ struct Application {
int hideToolbarSeparator;
int windowBackgroundIsTranslucent;
// Menu
Menu *applicationMenu;
// // Menu
// Menu *applicationMenu;
//
// // Tray
// TrayMenuStore* trayMenuStore;
// Tray
TrayMenuStore* trayMenuStore;
MenuManager* menuManager;
// Context Menus
ContextMenuStore *contextMenuStore;
// ContextMenuStore *contextMenuStore;
// Callback
ffenestriCallback sendMessageToBackend;
@@ -124,6 +122,9 @@ struct Application {
// Bindings
const char *bindings;
// Flags
bool running;
};
// Debug works like sprintf but mutes if the global debug flag is true
@@ -253,8 +254,15 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
Show(app);
}
// Setup initial trays
ShowTrayMenus(app->menuManager);
// TODO: Check this actually does reduce flicker
msg(app->config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("suppressesIncrementalRendering"));
// Notify backend we are ready (system startup)
app->sendMessageToBackend("SS");
} else if( strcmp(name, "windowDrag") == 0 ) {
// Guard against null events
if( app->mouseEvent != NULL ) {
@@ -316,7 +324,8 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
const char* contextMenuData = STRCOPY(contextMenuDataNode->string_);
ON_MAIN_THREAD(
ShowContextMenu(app->contextMenuStore, app->mainWindow, contextMenuID, contextMenuData);
//TODO: FIX UP
// ShowContextMenu(app->contextMenuStore, app->mainWindow, contextMenuID, contextMenuData);
);
json_delete(contextMenuMessageJSON);
@@ -387,7 +396,7 @@ int releaseNSObject(void *const context, struct hashmap_element_s *const e) {
}
void destroyContextMenus(struct Application *app) {
DeleteContextMenuStore(app->contextMenuStore);
// DeleteContextMenuStore(app->contextMenuStore);
}
void freeDialogIconCache(struct Application *app) {
@@ -420,19 +429,8 @@ void DestroyApplication(struct Application *app) {
msg( c("NSEvent"), s("removeMonitor:"), app->mouseUpMonitor);
}
// Delete the application menu if we have one
if( app->applicationMenu != NULL ) {
DeleteMenu(app->applicationMenu);
}
// Delete the tray menu store
DeleteTrayMenuStore(app->trayMenuStore);
// Delete the context menu store
DeleteContextMenuStore(app->contextMenuStore);
// Destroy the context menus
destroyContextMenus(app);
DeleteMenuManager(app->menuManager);
// Free dialog icon cache
freeDialogIconCache(app);
@@ -579,7 +577,7 @@ void SetPosition(struct Application *app, int x, int y) {
);
}
void processDialogButton(id alert, char *buttonTitle, char *cancelButton, char *defaultButton) {
void processDialogButton(id alert, const char *buttonTitle, const char *cancelButton, const char *defaultButton) {
// If this button is set
if( STR_HAS_CHARS(buttonTitle) ) {
id button = msg(alert, s("addButtonWithTitle:"), str(buttonTitle));
@@ -592,11 +590,11 @@ void processDialogButton(id alert, char *buttonTitle, char *cancelButton, char *
}
}
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) {
extern void MessageDialog(struct Application *app, const char *callbackID, const char *type, const char *title, const char *message, const char *icon, const char *button1, const char *button2, const char *button3, const char *button4, const char *defaultButton, const char *cancelButton) {
ON_MAIN_THREAD(
id alert = ALLOC_INIT("NSAlert");
char *dialogType = type;
char *dialogIcon = type;
const char *dialogType = type;
const char *dialogIcon = type;
// Default to info type
if( dialogType == NULL ) {
@@ -633,6 +631,8 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type,
dialogIcon = icon;
}
// TODO: move dialog icons + methods to own file
// Determine what dialog icon we are looking for
id dialogImage = NULL;
// Look for `name-theme2x` first
@@ -672,7 +672,7 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type,
}
// Run modal
char *buttonPressed;
const char *buttonPressed;
int response = (int)msg(alert, s("runModal"));
if( response == NSAlertFirstButtonReturn ) {
buttonPressed = button1;
@@ -703,7 +703,7 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type,
}
// OpenDialog opens a dialog to select files/directories
void OpenDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories) {
void OpenDialog(struct Application *app, const char *callbackID, const char *title, const char *filters, const char *defaultFilename, const char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories) {
Debug(app, "OpenDialog Called with callback id: %s", callbackID);
// Create an open panel
@@ -791,7 +791,7 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi
}
// SaveDialog opens a dialog to select files/directories
void SaveDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) {
void SaveDialog(struct Application *app, const char *callbackID, const char *title, const char *filters, const char *defaultFilename, const char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) {
Debug(app, "SaveDialog Called with callback id: %s", callbackID);
// Create an open panel
@@ -906,28 +906,41 @@ void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight)
}
void SetDebug(void *applicationPointer, int flag) {
void SetDebug(struct Application *applicationPointer, int flag) {
debug = flag;
}
// SetContextMenus sets the context menu map for this application
// AddContextMenu sets the context menu map for this application
void AddContextMenu(struct Application *app, const char *contextMenuJSON) {
AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
// AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
}
void UpdateContextMenu(struct Application *app, const char* contextMenuJSON) {
UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON);
// UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON);
}
void AddTrayMenu(struct Application *app, const char *trayMenuJSON) {
AddTrayMenuToStore(app->trayMenuStore, trayMenuJSON);
AddTrayMenuToManager(app->menuManager, trayMenuJSON);
}
void UpdateTrayMenu(struct Application *app, const char* trayMenuJSON) {
UpdateTrayMenuInStore(app->trayMenuStore, trayMenuJSON);
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
TrayMenu *menu = AddTrayMenuToManager(app->menuManager, trayMenuJSON);
if( app->running ) {
ON_MAIN_THREAD(
ShowTrayMenu(menu)
);
}
}
//void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
// ON_MAIN_THREAD(
// UpdateTrayMenuLabelInStore(app->trayMenuStore, JSON);
// );
//}
void SetBindings(struct Application *app, const char *bindings) {
const char* temp = concat("window.wailsbindings = \"", bindings);
const char* jscall = concat(temp, "\";");
@@ -1014,7 +1027,7 @@ void createDelegate(struct Application *app) {
class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "v@:@");
// All Menu Items use a common callback
class_addMethod(delegateClass, s("menuItemCallback:"), (IMP)menuItemCallback, "v@:@");
class_addMethod(delegateClass, s("menuItemCallback:"), (IMP)PlatformMenuItemCallback, "v@:@");
// Script handler
class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@");
@@ -1071,402 +1084,69 @@ const char* getInitialState(struct Application *app) {
return result;
}
void parseMenuRole(struct Application *app, id parentMenu, JsonNode *item) {
const char *roleName = item->string_;
if ( STREQ(roleName, "appMenu") ) {
createDefaultAppMenu(parentMenu);
return;
}
if ( STREQ(roleName, "editMenu")) {
createDefaultEditMenu(parentMenu);
return;
}
if ( STREQ(roleName, "hide")) {
addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE);
return;
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE);
return;
}
if ( STREQ(roleName, "front")) {
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE);
return;
}
if ( STREQ(roleName, "undo")) {
addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE);
return;
}
if ( STREQ(roleName, "redo")) {
addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE);
return;
}
if ( STREQ(roleName, "cut")) {
addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE);
return;
}
if ( STREQ(roleName, "copy")) {
addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE);
return;
}
if ( STREQ(roleName, "paste")) {
addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE);
return;
}
if ( STREQ(roleName, "delete")) {
addMenuItem(parentMenu, "Delete", "delete:", "", FALSE);
return;
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
return;
}
if ( STREQ(roleName, "minimize")) {
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE);
return;
}
if ( STREQ(roleName, "zoom")) {
addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE);
return;
}
if ( STREQ(roleName, "quit")) {
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE);
return;
}
if ( STREQ(roleName, "togglefullscreen")) {
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE);
return;
}
}
id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char *menuCallback) {
id item = ALLOC("NSMenuItem");
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s(menuCallback), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg(parentMenu, s("addItem:"), item);
return item;
}
id parseCheckboxMenuItem(struct Application *app, id parentmenu, const char
*title, const char *menuid, bool disabled, bool checked, const char *key,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(menuItemMap, (char*)menuid, strlen(menuid), item);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid);
msg(item, s("setRepresentedObject:"), wrappedId);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s(checkboxCallbackFunction), str(key));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id parseRadioMenuItem(struct Application *app, id parentmenu, const char *title,
const char *menuid, bool disabled, bool checked, const char *acceleratorkey,
struct hashmap_s *menuItemMap, const char *radioCallbackFunction) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(menuItemMap, (char*)menuid, strlen(menuid), item);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s(radioCallbackFunction), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
*radioCallbackFunction, const char *menuCallbackFunction) {
// Check if this item is hidden and if so, exit early!
bool hidden = false;
getJSONBool(item, "Hidden", &hidden);
if( hidden ) {
return;
}
// Get the role
JsonNode *role = json_find_member(item, "Role");
if( role != NULL ) {
parseMenuRole(app, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenu) {
// Get item label
parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
if ( label == NULL) {
label = "(empty)";
}
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
}
bool disabled = false;
getJSONBool(item, "Disabled", &disabled);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "Accelerator");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, menuCallbackFunction);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
}
else if ( STREQ(type->string_, "Checkbox")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, checkboxCallbackFunction);
}
else if ( STREQ(type->string_, "Radio")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, radioCallbackFunction);
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
}
}
void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction, const char *menuCallbackFunction) {
JsonNode *items = json_find_member(menu, "Items");
if( items == NULL ) {
// Parse error!
Fatal(app, "Unable to find Items in Menu");
return;
}
// Iterate items
JsonNode *item;
json_foreach(item, items) {
// Get item label
parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
}
void dumpMemberList(const char *name, id *memberList) {
void *member = memberList[0];
int count = 0;
printf("%s = %p -> [ ", name, memberList);
while( member != NULL ) {
printf("%p ", member);
count = count + 1;
member = memberList[count];
}
printf("]\n");
}
void processRadioGroup(JsonNode *radioGroup, struct hashmap_s *menuItemMap,
struct hashmap_s *radioGroupMap) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// dumpMemberList("memberList", memberList);
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
}
// updateMenu replaces the current menu with the given one
void updateMenu(struct Application *app, const char *menuAsJSON) {
Debug(app, "Menu is now: %s", menuAsJSON);
ON_MAIN_THREAD (
DeleteMenu(app->applicationMenu);
Menu* newMenu = NewApplicationMenu(menuAsJSON);
id menu = GetMenu(newMenu);
app->applicationMenu = newMenu;
msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), menu);
);
}
//void dumpMemberList(const char *name, id *memberList) {
// void *member = memberList[0];
// int count = 0;
// printf("%s = %p -> [ ", name, memberList);
// while( member != NULL ) {
// printf("%p ", member);
// count = count + 1;
// member = memberList[count];
// }
// printf("]\n");
//}
// SetApplicationMenu sets the initial menu for the application
void SetApplicationMenu(struct Application *app, const char *menuAsJSON) {
if ( app->applicationMenu == NULL ) {
app->applicationMenu = NewApplicationMenu(menuAsJSON);
return;
}
//void SetApplicationMenu(struct Application *app, const char *menuAsJSON) {
// if ( app->applicationMenu == NULL ) {
// app->applicationMenu = NewApplicationMenu(menuAsJSON, (struct TrayMenuStore *) app->trayMenuStore);
// return;
// }
//
// // Update menu
// updateMenu(app, menuAsJSON);
//}
// Update menu
updateMenu(app, menuAsJSON);
}
void processDialogIcons(struct hashmap_s *hashmap, const unsigned char *dialogIcons[]) {
unsigned int count = 0;
while( 1 ) {
const unsigned char *name = dialogIcons[count++];
if( name == 0x00 ) {
break;
}
const unsigned char *lengthAsString = dialogIcons[count++];
if( name == 0x00 ) {
break;
}
const unsigned char *data = dialogIcons[count++];
if( data == 0x00 ) {
break;
}
int length = atoi((const char *)lengthAsString);
// Create the icon and add to the hashmap
id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
id dialogImage = ALLOC("NSImage");
msg(dialogImage, s("initWithData:"), imageData);
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 processDialogIcons(struct hashmap_s *hashmap, const unsigned char *dialogIcons[]) {
//
// unsigned int count = 0;
// while( 1 ) {
// const unsigned char *name = dialogIcons[count++];
// if( name == 0x00 ) {
// break;
// }
// const unsigned char *lengthAsString = dialogIcons[count++];
// if( name == 0x00 ) {
// break;
// }
// const unsigned char *data = dialogIcons[count++];
// if( data == 0x00 ) {
// break;
// }
// int length = atoi((const char *)lengthAsString);
//
// // Create the icon and add to the hashmap
// id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
// id dialogImage = ALLOC("NSImage");
// msg(dialogImage, s("initWithData:"), imageData);
// 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) {
@@ -1501,6 +1181,9 @@ void Run(struct Application *app, int argc, char **argv) {
makeWindowBackgroundTranslucent(app);
}
// We set it to be invisible by default. It will become visible when everything has initialised
msg(app->mainWindow, s("setIsVisible:"), NO);
// Setup webview
id config = msg(c("WKWebViewConfiguration"), s("new"));
msg(config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str("suppressesIncrementalRendering"));
@@ -1639,20 +1322,15 @@ void Run(struct Application *app, int argc, char **argv) {
msg(wkwebview, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("drawsBackground"));
}
// If we have an application menu, process it
if( app->applicationMenu != NULL ) {
id menu = GetMenu(app->applicationMenu);
msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), menu);
}
// Setup initial trays
ShowTrayMenusInStore(app->trayMenuStore);
// // If we have an application menu, process it
// if( app->applicationMenu != NULL ) {
// msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), app->applicationMenu->menu);
// }
// Process dialog icons
processUserDialogIcons(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);
app->running = true;
// Finally call run
Debug(app, "Run called");
@@ -1662,7 +1340,7 @@ void Run(struct Application *app, int argc, char **argv) {
}
void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) {
struct Application* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) {
// Load the tray icons
LoadTrayIcons();
@@ -1703,13 +1381,10 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
result->delegate = NULL;
// Menu
result->applicationMenu = NULL;
// Tray
result->trayMenuStore = NewTrayMenuStore();
result->menuManager = NewMenuManager();
// Context Menus
result->contextMenuStore = NewContextMenuStore();
// result->contextMenuStore = NewContextMenuStore();
// Window Appearance
result->titlebarAppearsTransparent = 0;
@@ -1717,8 +1392,7 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
result->sendMessageToBackend = (ffenestriCallback) messageFromWindowCallback;
result->running = false;
return (void*) result;
}
#endif

View File

@@ -58,15 +58,15 @@ func (a *Application) processPlatformSettings() error {
C.WindowBackgroundIsTranslucent(a.app)
}
// Process menu
//applicationMenu := options.GetApplicationMenu(a.config)
applicationMenu := a.menuManager.GetApplicationMenuJSON()
if applicationMenu != "" {
C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
}
//// Process menu
////applicationMenu := options.GetApplicationMenu(a.config)
//applicationMenu := a.menuManager.GetApplicationMenuJSON()
//if applicationMenu != "" {
// C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
//}
// Process tray
trays, err := a.menuManager.GetTrayMenus()
trays, err := a.menuManager.GetTrayMenusAsJSON()
if err != nil {
return err
}
@@ -76,16 +76,16 @@ func (a *Application) processPlatformSettings() error {
}
}
// Process context menus
contextMenus, err := a.menuManager.GetContextMenus()
if err != nil {
return err
}
if contextMenus != nil {
for _, contextMenu := range contextMenus {
C.AddContextMenu(a.app, a.string2CString(contextMenu))
}
}
//// Process context menus
//contextMenus, err := a.menuManager.GetContextMenus()
//if err != nil {
// return err
//}
//if contextMenus != nil {
// for _, contextMenu := range contextMenus {
// C.AddContextMenu(a.app, a.string2CString(contextMenu))
// }
//}
return nil
}

View File

@@ -20,6 +20,7 @@
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define RELEASE(input) if( input != NULL ) { msg(input, s("release")); }
#define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
@@ -107,7 +108,6 @@ void SetAppearance(struct Application* app, const char *);
void WebviewIsTransparent(struct Application* app);
void WindowBackgroundIsTranslucent(struct Application* app);
void SetTray(struct Application* app, const char *, const char *, const char *);
void SetContextMenus(struct Application* app, const char *);
void AddTrayMenu(struct Application* app, const char *);
//void SetContextMenus(struct Application* app, const char *);
#endif

View File

@@ -0,0 +1,276 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
Menu* NewMenu(struct JsonNode* menuData, struct JsonNode* radioData, MenuManager* manager) {
if( menuData == NULL ) return NULL;
if( manager == NULL ) return NULL;
Menu *result = NEW(Menu);
// No label by default
result->label = STRCOPY(getJSONStringDefault(menuData, "l", ""));
// Setup platform specific menu data
SetupMenuPlatformData(result);
// Init menu item list
vec_init(&result->menuItems);
// Get the menu items
JsonNode* items = getJSONObject(menuData, "i");
if( items != NULL ) {
// Process the menu data
JsonNode *item;
json_foreach(item, items) {
const char *ID = mustJSONString(item, "I");
MenuItem *menuItem = HASHMAP_GET(manager->menuItems, ID);
if (menuItem == NULL) {
// Process each menu item
menuItem = processMenuItem(result, item, manager);
// Filter out separators
if (menuItem->ID != NULL) {
HASHMAP_PUT(manager->menuItems, menuItem->ID, menuItem);
}
}
AddMenuItemToMenu(result, menuItem, manager);
}
if (radioData != NULL) {
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioData) {
// Get item label
processRadioGroup(result, radioGroup, manager);
}
}
}
return result;
}
MenuItem* processMenuItem(Menu *menu, JsonNode *item, MenuManager *manager) {
// Get the role
const char *role = getJSONString(item, "r");
if( role != NULL ) {
// Roles override everything else
// return NewMenuItemForRole(role, menu, item, manager);
}
Menu* submenu = NULL;
// Check if this is a submenu
// JsonNode *submenuData = json_find_member(item, "S");
// if( submenuData != NULL ) {
// submenu = NewMenu(submenuData)
// // Get the label
// JsonNode *menuNameNode = json_find_member(item, "l");
// const char *name = "";
// if ( menuNameNode != NULL) {
// name = menuNameNode->string_;
// }
//
// id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
// id thisMenu = createMenu(str(name));
//
// msg(thisMenuItem, s("setSubmenu:"), thisMenu);
// msg(parentMenu, s("addItem:"), thisMenuItem);
//
// JsonNode *submenuItems = json_find_member(submenu, "i");
// // If we have no items, just return
// if ( submenuItems == NULL ) {
// return;
// }
//
// // Loop over submenu items
// JsonNode *item;
// json_foreach(item, submenuItems) {
// // Get item label
// processMenuItem(menu, thisMenu, item);
// }
//
// return;
// }
// Get the ID
const char *menuItemID = mustJSONString(item, "I");
// Get the label(s)
const char* label = getJSONStringDefault(item, "l", "");
const char* alternateLabel = getJSONString(item, "L");
bool checked = getJSONBool(item, "c");
bool hidden = getJSONBool(item, "h");
bool disabled = getJSONBool(item, "d");
const char* RGBA = getJSONString(item, "R");
const char* font = getJSONString(item, "F");
const char* image = getJSONString(item, "i");
int fontSize = 0;
getJSONInt(item, "F", &fontSize);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "a");
const char *acceleratorKey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorKey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = STRCOPY(modifier->string_);
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// has callback?
bool hasCallback = getJSONBool(item, "C");
// Get the Type
const char *type = mustJSONString(item, "t");
MenuItem* menuItem;
enum MenuItemType menuItemType;
if( STREQ(type, "t")) {
menuItemType = Text;
}
else if ( STREQ(type, "s")) {
menuItemType = Separator;
}
else if ( STREQ(type, "c")) {
menuItemType = Checkbox;
}
else if ( STREQ(type, "r")) {
menuItemType = Radio;
} else {
menuItemType = -1;
ABORT("Unknown Menu Item type '%s'!", type);
}
menuItem = NewMenuItem(menuItemType, menuItemID, label, alternateLabel, disabled, hidden, checked, RGBA, font, fontSize, image, acceleratorKey, modifiers, hasCallback, submenu);
return menuItem;
}
void DeleteMenu(Menu* menu) {
// NULL guard
if( menu == NULL ) return;
// Delete the platform specific menu data
DeleteMenuPlatformData(menu);
// Clean up other data
MEMFREE(menu->label);
// Clear the menu items vector
vec_deinit(&menu->menuItems);
}
const char* MenuAsJSON(Menu* menu) {
if( menu == NULL ) return NULL;
JsonNode *jsonObject = json_mkobject();
JSON_ADD_STRING(jsonObject, "Label", menu->label);
return json_encode(jsonObject);
}
JsonNode* MenuAsJSONObject(Menu* menu) {
if( menu == NULL ) return NULL;
JsonNode *jsonObject = json_mkobject();
JSON_ADD_STRING(jsonObject, "Label", menu->label);
if( vec_size(&menu->menuItems) > 0 ) {
JsonNode* items = json_mkarray();
int i; MenuItem *menuItem;
vec_foreach(&menu->menuItems, menuItem, i) {
JSON_ADD_ELEMENT(items, MenuItemAsJSONObject(menuItem));
}
JSON_ADD_OBJECT(jsonObject, "Items", items);
}
return jsonObject;
}
void processRadioGroup(Menu *menu, JsonNode *radioGroup, MenuManager* manager) {
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
int groupLength = json_array_length(radioGroup);
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
MenuItem* memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
MenuItem* menuItem = HASHMAP_GET(manager->menuItems, (char*)member->string_);
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
HASHMAP_PUT(menu->radioGroups, member->string_, newMemberList);
}
}
void AddMenuItemToMenu(Menu* menu, MenuItem* menuItem, MenuManager* manager) {
vec_push(&menu->menuItems, menuItem);
PlatformAddMenuItemToMenu(menu, menuItem, manager);
}
// Creates a JSON message for the given menuItemID and data
const char* CreateMenuClickedMessage(const char *menuItemID, const char *data) {
JsonNode *jsonObject = json_mkobject();
if (menuItemID == NULL ) {
ABORT("Item ID NULL for menu!!\n");
}
json_append_member(jsonObject, "i", json_mkstring(menuItemID));
if (data != NULL) {
json_append_member(jsonObject, "data", json_mkstring(data));
}
const char *payload = json_encode(jsonObject);
json_delete(jsonObject);
const char *result = concat("MC", payload);
MEMFREE(payload);
return result;
}

View File

@@ -0,0 +1,178 @@
//
// Created by Lea Anthony on 18/1/21.
//
#ifndef MENU_H
#define MENU_H
#include "common.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2, Separator = 3};
static const char *MenuItemTypeAsString[] = {
"Text", "Checkbox", "Radio", "Separator",
};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
typedef struct {
// Menu label
const char *label;
// A list of menuItem IDs that make up this menu
vec_void_t menuItems;
// The platform specific menu data
void *platformData;
} Menu;
typedef struct {
// ID of the tray
const char *ID;
// The tray label
const char *Label;
// The icon name
const char *Icon;
// The menu
Menu* Menu;
// Platform specific data
void* platformData;
} TrayMenu;
typedef struct {
Menu* menu;
} ApplicationMenu;
typedef struct {
const char* ID;
Menu* menu;
} ContextMenu;
typedef struct {
// This is our menu item map using the menuItem ID as a key
// map[string]*MenuItem
struct hashmap_s menuItems;
// This is our context menu map using the context menu ID as a key
// map[string]*ContextMenu
struct hashmap_s contextMenus;
// This is our tray menu map using the tray menu ID as a key
// map[string]*TrayMenu
struct hashmap_s trayMenus;
// Application Menu
Menu* applicationMenu;
// Context menu data
const char* contextMenuData;
} MenuManager;
typedef struct {
MenuManager* manager;
const char *menuItemID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
typedef struct {
const char *ID;
const char* label;
const char* alternateLabel;
bool disabled;
bool hidden;
const char* colour;
const char* font;
int fontSize;
const char* image;
bool checked;
// Keyboard shortcut
const char* acceleratorKey;
const char** modifiers;
// Type of menuItem
enum MenuItemType type;
// Indicates if the menuItem has a callback
bool hasCallback;
// The platform specific menu data
void* platformData;
// Submenu
Menu* submenu;
// Radio group for this item
vec_void_t radioGroup;
// Data for handling callbacks
MenuItemCallbackData *callbackData;
} MenuItem;
// MenuItem
MenuItem* NewMenuItem(enum MenuItemType type, const char* ID, const char* label, const char* alternateLabel, bool disabled, bool hidden, bool checked, const char* colour, const char* font, int fontsize, const char* image, const char* acceleratorKey, const char** modifiers, bool hasCallback, Menu* submenu);
void DeleteMenuItem(MenuItem* menuItem);
JsonNode* MenuItemAsJSONObject(MenuItem* menuItem);
// Menu
Menu* NewMenu(JsonNode* menuData, JsonNode* radioData, MenuManager* menuManager);
void DeleteMenu(Menu* menu);
const char* MenuAsJSON(Menu* menu);
JsonNode* MenuAsJSONObject(Menu* menu);
void AddMenuItemToMenu(Menu* menu, MenuItem* menuItem, MenuManager* manager);
MenuItem* processMenuItem(Menu *menu, JsonNode *item, MenuManager *manager);
void processRadioGroup(Menu *menu, JsonNode *radioGroup, MenuManager* manager);
// Tray
TrayMenu* NewTrayMenu(JsonNode* trayJSON, MenuManager *manager);
void DeleteTrayMenu(TrayMenu *trayMenu);
const char* TrayMenuAsJSON(TrayMenu* menu);
// MenuManager
MenuManager* NewMenuManager();
void DeleteMenuManager(MenuManager* manager);
TrayMenu* AddTrayMenuToManager(MenuManager* manager, const char* trayMenuJSON);
void ShowTrayMenus(MenuManager* manager);
// Callbacks
MenuItemCallbackData* NewMenuItemCallbackData(MenuManager *manager, const char* menuItemID, enum MenuItemType menuItemType);
void DeleteMenuItemCallbackData(MenuItemCallbackData* callbackData);
const char* CreateMenuClickedMessage(const char *menuItemID, const char *data);
// Platform
void SetupMenuPlatformData(Menu* menu);
void DeleteMenuPlatformData(Menu* menu);
void SetupMenuItemPlatformData(MenuItem* menuItem);
void DeleteMenuItemPlatformData(MenuItem* menuItem);
void SetupTrayMenuPlatformData(TrayMenu* menu);
void DeleteTrayMenuPlatformData(TrayMenu* menu);
void PlatformAddMenuItemToMenu(Menu *menu, MenuItem* menuItem, MenuManager* manager);
void PlatformUpdateTrayIcon(TrayMenu *menu);
// Platform specific methods
void UnloadTrayIcons();
void LoadTrayIcons();
void ShowTrayMenu(TrayMenu* trayMenu);
#endif //MENU_H

View File

@@ -1,174 +1,35 @@
//
// Created by Lea Anthony on 6/1/21.
// Created by Lea Anthony on 18/1/21.
//
#include "ffenestri.h"
#include "ffenestri_darwin.h"
#include "menu_darwin.h"
#include "contextmenus_darwin.h"
#include "menu.h"
#include "trayicons.h"
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData) {
// A cache for all our tray menu icons
// Global because it's a singleton
struct hashmap_s trayIconCache;
Menu *result = malloc(sizeof(Menu));
result->processedMenu = menuData;
// No title by default
result->title = "";
// Initialise menuCallbackDataCache
vec_init(&result->callbackDataCache);
// Allocate MenuItem Map
if( 0 != hashmap_create((const unsigned)16, &result->menuItemMap)) {
ABORT("[NewMenu] Not enough memory to allocate menuItemMap!");
}
// Allocate the Radio Group Map
if( 0 != hashmap_create((const unsigned)4, &result->radioGroupMap)) {
ABORT("[NewMenu] Not enough memory to allocate radioGroupMap!");
}
// Init other members
result->menu = NULL;
result->parentData = NULL;
return result;
void SetupMenuPlatformData(Menu* menu) {
MacMenu* result = NEW(MacMenu);
result->Menu = ALLOC("NSMenu");
msg(result->Menu, s("initWithTitle:"), str(menu->label));
msg(result->Menu, s("setAutoenablesItems:"), NO);
menu->platformData = (void*)result;
}
Menu* NewApplicationMenu(const char *menuAsJSON) {
void DeleteMenuPlatformData(Menu* menu) {
// Parse the menu json
JsonNode *processedMenu = json_decode(menuAsJSON);
if( processedMenu == NULL ) {
// Parse error!
ABORT("Unable to parse Menu JSON: %s", menuAsJSON);
}
// Return if null
if( menu == NULL || menu->platformData == NULL) return;
Menu *result = NewMenu(processedMenu);
result->menuType = ApplicationMenuType;
return result;
MacMenu* macMenu = (MacMenu*) menu->platformData;
RELEASE(macMenu->Menu);
macMenu->Menu = NULL;
}
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType) {
MenuItemCallbackData* result = malloc(sizeof(MenuItemCallbackData));
result->menu = menu;
result->menuID = menuID;
result->menuItem = menuItem;
result->menuItemType = menuItemType;
// Store reference to this so we can destroy later
vec_push(&menu->callbackDataCache, result);
return result;
}
void DeleteMenu(Menu *menu) {
// Free menu item hashmap
hashmap_destroy(&menu->menuItemMap);
// Free radio group members
if( hashmap_num_entries(&menu->radioGroupMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&menu->radioGroupMap, freeHashmapItem, NULL)) {
ABORT("[DeleteMenu] Failed to release radioGroupMap entries!");
}
}
// Free radio groups hashmap
hashmap_destroy(&menu->radioGroupMap);
// Free up the processed menu memory
if (menu->processedMenu != NULL) {
json_delete(menu->processedMenu);
menu->processedMenu = NULL;
}
// Release the vector memory
vec_deinit(&menu->callbackDataCache);
// Free nsmenu if we have it
if ( menu->menu != NULL ) {
msg(menu->menu, s("release"));
}
free(menu);
}
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID) {
JsonNode *jsonObject = json_mkobject();
json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID));
json_append_member(jsonObject, "menuType", json_mkstring(MenuTypeAsString[(int)menuType]));
if (data != NULL) {
json_append_member(jsonObject, "data", json_mkstring(data));
}
if (parentID != NULL) {
json_append_member(jsonObject, "parentID", json_mkstring(parentID));
}
const char *payload = json_encode(jsonObject);
json_delete(jsonObject);
const char *result = concat("MC", payload);
MEMFREE(payload);
return result;
}
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender) {
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
const char *message;
// Update checkbox / radio item
if( callbackData->menuItemType == Checkbox) {
// Toggle state
bool state = msg(callbackData->menuItem, s("state"));
msg(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
} else if( callbackData->menuItemType == Radio ) {
// Check the menu items' current state
bool selected = msg(callbackData->menuItem, s("state"));
// If it's already selected, exit early
if (selected) return;
// Get this item's radio group members and turn them off
id *members = (id*)hashmap_get(&(callbackData->menu->radioGroupMap), (char*)callbackData->menuID, strlen(callbackData->menuID));
// Uncheck all members of the group
id thisMember = members[0];
int count = 0;
while(thisMember != NULL) {
msg(thisMember, s("setState:"), NSControlStateValueOff);
count = count + 1;
thisMember = members[count];
}
// check the selected menu item
msg(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
}
const char *menuID = callbackData->menuID;
const char *data = NULL;
enum MenuType menuType = callbackData->menu->menuType;
const char *parentID = NULL;
// Generate message to send to backend
if( menuType == ContextMenuType ) {
// Get the context menu data from the menu
ContextMenu* contextMenu = (ContextMenu*) callbackData->menu->parentData;
data = contextMenu->contextMenuData;
parentID = contextMenu->ID;
} else if ( menuType == TrayMenuType ) {
parentID = (const char*) callbackData->menu->parentData;
}
message = createMenuClickedMessage(menuID, data, menuType, parentID);
// Notify the backend
messageFromWindowCallback(message);
MEMFREE(message);
}
id processAcceleratorKey(const char *key) {
@@ -340,161 +201,6 @@ id processAcceleratorKey(const char *key) {
return str(key);
}
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
}
id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
return item;
}
id createMenuItem(id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg(item, s("autorelease"));
return item;
}
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), !disabled);
msg(menu, s("addItem:"), item);
return item;
}
id createMenu(id title) {
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("setAutoenablesItems:"), NO);
// msg(menu, s("autorelease"));
return menu;
}
void createDefaultAppMenu(id parentMenu) {
// App Menu
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu);
msg(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
// Edit Menu
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
addSeparator(editMenu);
addMenuItem(editMenu, "Cut", "cut:", "x", FALSE);
addMenuItem(editMenu, "Copy", "copy:", "c", FALSE);
addMenuItem(editMenu, "Paste", "paste:", "v", FALSE);
addMenuItem(editMenu, "Select All", "selectAll:", "a", FALSE);
}
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
const char *roleName = item->string_;
if ( STREQ(roleName, "appMenu") ) {
createDefaultAppMenu(parentMenu);
return;
}
if ( STREQ(roleName, "editMenu")) {
createDefaultEditMenu(parentMenu);
return;
}
if ( STREQ(roleName, "hide")) {
addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE);
return;
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE);
return;
}
if ( STREQ(roleName, "front")) {
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE);
return;
}
if ( STREQ(roleName, "undo")) {
addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE);
return;
}
if ( STREQ(roleName, "redo")) {
addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE);
return;
}
if ( STREQ(roleName, "cut")) {
addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE);
return;
}
if ( STREQ(roleName, "copy")) {
addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE);
return;
}
if ( STREQ(roleName, "paste")) {
addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE);
return;
}
if ( STREQ(roleName, "delete")) {
addMenuItem(parentMenu, "Delete", "delete:", "", FALSE);
return;
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
return;
}
if ( STREQ(roleName, "minimize")) {
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE);
return;
}
if ( STREQ(roleName, "zoom")) {
addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE);
return;
}
if ( STREQ(roleName, "quit")) {
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE);
return;
}
if ( STREQ(roleName, "togglefullscreen")) {
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE);
return;
}
}
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers) {
@@ -527,282 +233,286 @@ unsigned long parseModifiers(const char **modifiers) {
return result;
}
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey) {
void SetupMenuItemPlatformData(MenuItem* menuItem) {
// Create the platform data
MacMenuItem *macMenuItem = NEW(MacMenuItem);
menuItem->platformData = macMenuItem;
// Create the NSMenuItem
id item = ALLOC("NSMenuItem");
macMenuItem->MenuItem = item;
// Store the item in the menu item map
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
// // TODO: Process ROLE
// if( menuItem->role != NULL ) {
//
// }
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
id key = processAcceleratorKey(menuItem->acceleratorKey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(menuItem->label),
s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("setEnabled:"), !menuItem->disabled);
msg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
if( menuItem->modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(menuItem->modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
menuItem->modifiers = NULL;
}
msg(parentMenu, s("addItem:"), item);
return item;
}
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
void DeleteMenuItemPlatformData(MenuItem* menuItem) {
// Check if this item is hidden and if so, exit early!
bool hidden = false;
getJSONBool(item, "Hidden", &hidden);
if( hidden ) {
if( menuItem == NULL || menuItem->platformData == NULL) return;
MacMenuItem* macMenuItem = (MacMenuItem*) menuItem->platformData;
RELEASE(macMenuItem->MenuItem);
MEMFREE(macMenuItem);
}
void PlatformAddMenuItemToMenu(Menu *menu, MenuItem* menuItem, MenuManager* manager) {
// Return if null
if( menu == NULL || menu->platformData == NULL) return;
if( menuItem == NULL || menuItem->platformData == NULL) return;
// Don't add if the item is hidden
if( menuItem->hidden ) return;
// Setup callback
if( menuItem->hasCallback ) {
// Create a MenuItemCallbackData
MacMenuItem* macMenuItem = (MacMenuItem*) menuItem->platformData;
menuItem->callbackData = NewMenuItemCallbackData(manager, menuItem->ID, Text);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuItem->callbackData);
msg(macMenuItem->MenuItem, s("setRepresentedObject:"), wrappedId);
}
MacMenu* macMenu = (MacMenu*) menu->platformData;
MacMenuItem* macMenuItem = (MacMenuItem*) menuItem->platformData;
msg(macMenu->Menu, s("addItem:"), macMenuItem->MenuItem);
}
void SetupTrayMenuPlatformData(TrayMenu* menu) {
MacTrayMenu* result = NEW(MacTrayMenu);
result->statusBarItem = NULL;
// TODO: Work out how to make this customisable
result->iconPosition = NSImageLeft;
menu->platformData = (void*)result;
}
void DeleteTrayMenuPlatformData(TrayMenu* menu) {
// Return if null
if( menu == NULL || menu->platformData == NULL) return;
MacTrayMenu* macMenu = (MacTrayMenu*) menu->platformData;
RELEASE(macMenu->statusBarItem);
macMenu->statusBarItem = NULL;
}
void PlatformUpdateTrayIcon(TrayMenu* trayMenu) {
MacTrayMenu* macTrayMenu = (MacTrayMenu*) trayMenu->platformData;
id statusBarButton = msg(macTrayMenu->statusBarItem, s("button"));
// Empty icon means remove it
if (trayMenu->Icon == NULL || strlen(trayMenu->Icon) == 0) {
msg(statusBarButton, s("setImage:"), NULL);
return;
}
// Get the role
JsonNode *role = json_find_member(item, "Role");
if( role != NULL ) {
processMenuRole(menu, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
if ( label == NULL) {
label = "(empty)";
}
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
}
bool disabled = false;
getJSONBool(item, "Disabled", &disabled);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "Accelerator");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
}
else if ( STREQ(type->string_, "Checkbox")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
processCheckboxMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
else if ( STREQ(type->string_, "Radio")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
id trayImage = HASHMAP_GET(trayIconCache, trayMenu->Icon);
msg(statusBarButton, s("setImagePosition:"), macTrayMenu->iconPosition);
msg(statusBarButton, s("setImage:"), trayImage);
}
void processMenuData(Menu *menu, JsonNode *menuData) {
JsonNode *items = json_find_member(menuData, "Items");
if( items == NULL ) {
// Parse error!
ABORT("Unable to find 'Items' in menu JSON!");
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label) {
// Exit early if NULL
if( trayMenu->Label == NULL ) return;
// Update button label
MacTrayMenu* macTrayMenu = (MacTrayMenu*) trayMenu->platformData;
id statusBarButton = msg(macTrayMenu->statusBarItem, s("button"));
msg(statusBarButton, s("setTitle:"), str(label));
}
void ShowTrayMenu(TrayMenu* trayMenu) {
if( trayMenu == NULL || trayMenu->platformData == NULL ) return;
MacTrayMenu* macTrayMenu = (MacTrayMenu*) trayMenu->platformData;
// Create a status bar item if we don't have one
if( macTrayMenu->statusBarItem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
macTrayMenu->statusBarItem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(macTrayMenu->statusBarItem, s("retain"));
}
// Iterate items
JsonNode *item;
json_foreach(item, items) {
// Process each menu item
processMenuItem(menu, menu->menu, item);
id statusBarButton = msg(macTrayMenu->statusBarItem, s("button"));
msg(statusBarButton, s("setImagePosition:"), macTrayMenu->iconPosition);
// Update the icon if needed
PlatformUpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->Label);
// If we don't have a menu, return
if( trayMenu->Menu == NULL ) return;
// Update the menu
MacMenu* macMenu = (MacMenu*) trayMenu->Menu->platformData;
msg(macTrayMenu->statusBarItem, s("setMenu:"), macMenu->Menu);
}
void LoadTrayIcons() {
// Allocate the Tray Icons
if( 0 != hashmap_create((const unsigned)4, &trayIconCache)) {
// Couldn't allocate map
ABORT("Not enough memory to allocate trayIconCache!");
}
unsigned int count = 0;
while( 1 ) {
const unsigned char *name = trayIcons[count++];
if( name == 0x00 ) {
break;
}
const unsigned char *lengthAsString = trayIcons[count++];
if( name == 0x00 ) {
break;
}
const unsigned char *data = trayIcons[count++];
if( data == 0x00 ) {
break;
}
char *c;
unsigned long length = strtol((const char *)lengthAsString, &c, 10);
// Create the icon and add to the hashmap
id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
id trayImage = ALLOC("NSImage");
msg(trayImage, s("initWithData:"), imageData);
HASHMAP_PUT(trayIconCache, (const char *)name, trayImage);
}
}
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(&menu->menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(&menu->radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
void UnloadTrayIcons() {
// Release the tray cache images
HASHMAP_ITERATE(trayIconCache, releaseNSObject, NULL);
HASHMAP_DESTROY(trayIconCache);
}
//
//// Callback for text menu items
//void menuItemCallback(id self, SEL cmd, id sender) {
// MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
// if( callbackData == NULL) {
// return;
// }
//
// struct TrayMenuStore *store = callbackData->store;
// const char* menuItemID = callbackData->menuItemID;
// id nsmenu = GetMenuItemFromStore((TrayMenuStore *) store, menuItemID);
// if ( nsmenu == NULL ) {
// // The menu has been deleted!
// return;
// }
//
// const char *message;
//
// // Update checkbox / radio item
// if( callbackData->menuItemType == Checkbox) {
// // Toggle state
// bool state = msg(nsmenu, s("state"));
// msg(nsmenu, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
// } else if( callbackData->menuItemType == Radio ) {
// // Check the menu items' current state
// bool selected = msg(nsmenu, s("state"));
//
// // If it's already selected, exit early
// if (selected) return;
//
// // Get this item's radio group members and turn them off
// id *members = (id*)hashmap_get(&(callbackData->menu->radioGroupMap), (char*)callbackData->menuItemID, strlen(callbackData->menuItemID));
//
// // Uncheck all members of the group
// id thisMember = members[0];
// int count = 0;
// while(thisMember != NULL) {
// msg(thisMember, s("setState:"), NSControlStateValueOff);
// count = count + 1;
// thisMember = members[count];
// }
//
// // check the selected menu item
// msg(nsmenu, s("setState:"), NSControlStateValueOn);
// }
//
// message = createMenuClickedMessage(menuItemID, GetContextMenuDataFromStore((TrayMenuStore *) store));
//
// // Notify the backend
// messageFromWindowCallback(message);
// MEMFREE(message);
//}
id GetMenu(Menu *menu) {
void PlatformMenuItemCallback(id self, SEL cmd, id sender) {
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
const char *message;
// Pull out the menu data
JsonNode *menuData = json_find_member(menu->processedMenu, "Menu");
if( menuData == NULL ) {
ABORT("Unable to find Menu data: %s", menu->processedMenu);
MenuManager* manager = callbackData->manager;
MenuItem* menuItem = HASHMAP_GET(manager->menuItems, callbackData->menuItemID);
if( menuItem == NULL ) return;
MacMenuItem *macMenuItem = menuItem->platformData;
id nsMenuItem = macMenuItem->MenuItem;
// Update checkbox / radio item
if( callbackData->menuItemType == Checkbox) {
// Toggle state
bool state = msg(nsMenuItem, s("state"));
msg(nsMenuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
} else if( callbackData->menuItemType == Radio ) {
// Check the menu items' current state
bool selected = msg(nsMenuItem, s("state"));
// If it's already selected, exit early
if (selected) return;
int i=0; const char *groupMenuItemID;
vec_foreach(&menuItem->radioGroup, groupMenuItemID, i) {
// Get member
MenuItem* groupMember = HASHMAP_GET(manager->menuItems, groupMenuItemID);
MacMenuItem* groupMacMenuItem = (MacMenuItem*) groupMember->platformData;
msg(groupMacMenuItem->MenuItem, s("setState:"), NSControlStateValueOff);
}
// check the selected menu item
msg(nsMenuItem, s("setState:"), NSControlStateValueOn);
}
menu->menu = createMenu(str(""));
message = CreateMenuClickedMessage(callbackData->menuItemID, manager->contextMenuData);
// Process the menu data
processMenuData(menu, menuData);
// Create the radiogroup cache
JsonNode *radioGroups = json_find_member(menu->processedMenu, "RadioGroups");
if( radioGroups == NULL ) {
// Parse error!
ABORT("Unable to find RadioGroups data: %s", menu->processedMenu);
}
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioGroups) {
// Get item label
processRadioGroupJSON(menu, radioGroup);
}
return menu->menu;
// Notify the backend
messageFromWindowCallback(message);
MEMFREE(message);
}

View File

@@ -1,100 +1,21 @@
//
// Created by Lea Anthony on 6/1/21.
// Created by Lea Anthony on 18/1/21.
//
#ifndef MENU_DARWIN_H
#define MENU_DARWIN_H
#include "common.h"
#include "ffenestri_darwin.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
extern void messageFromWindowCallback(const char *);
#include "menu.h"
typedef struct {
const char *title;
/*** Internal ***/
// The decoded version of the Menu JSON
JsonNode *processedMenu;
struct hashmap_s menuItemMap;
struct hashmap_s radioGroupMap;
// Vector to keep track of callback data memory
vec_void_t callbackDataCache;
// The NSMenu for this menu
id menu;
// The parent data, eg ContextMenuStore or Tray
void *parentData;
// The commands for the menu callbacks
const char *callbackCommand;
// This indicates if we are an Application Menu, tray menu or context menu
enum MenuType menuType;
} Menu;
id Menu;
} MacMenu;
typedef struct {
id menuItem;
Menu *menu;
const char *menuID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
id MenuItem;
} MacMenuItem;
typedef struct {
id statusBarItem;
int iconPosition;
} MacTrayMenu;
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData);
Menu* NewApplicationMenu(const char *menuAsJSON);
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType);
void DeleteMenu(Menu *menu);
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID);
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender);
id processAcceleratorKey(const char *key);
void addSeparator(id menu);
id createMenuItemNoAutorelease( id title, const char *action, const char *key);
id createMenuItem(id title, const char *action, const char *key);
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled);
id createMenu(id title);
void createDefaultAppMenu(id parentMenu);
void createDefaultEditMenu(id parentMenu);
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item);
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers);
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey);
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
id GetMenu(Menu *menu);
#endif //ASSETS_C_MENU_DARWIN_H
void PlatformMenuItemCallback(id self, SEL cmd, id sender);

View File

@@ -0,0 +1,542 @@
//
// Created by Lea Anthony on 6/1/21.
//
#include "ffenestri_darwin.h"
#include "menu_darwin_old.h"
#include "contextmenus_darwin.h"
#include "traymenustore_darwin.h"
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData, JsonNode *radioGroups, struct TrayMenuStore *store) {
Menu *result = malloc(sizeof(Menu));
// No title by default
result->title = "";
// Initialise menuCallbackDataCache
vec_init(&result->callbackDataCache);
// Allocate MenuItem Map
if( 0 != hashmap_create((const unsigned)16, &result->menuItemMap)) {
ABORT("[NewMenu] Not enough memory to allocate menuItemMap!");
}
// Allocate the Radio Group Map
if( 0 != hashmap_create((const unsigned)4, &result->radioGroupMap)) {
ABORT("[NewMenu] Not enough memory to allocate radioGroupMap!");
}
// Init other members
result->menu = NULL;
result->store = store;
// Process the menu
ProcessMenu(result, menuData, radioGroups);
return result;
}
Menu* NewApplicationMenu(const char *menuAsJSON, struct TrayMenuStore *store) {
// Parse the menu json
JsonNode *processedMenu = json_decode(menuAsJSON);
if( processedMenu == NULL ) {
// Parse error!
ABORT("Unable to parse Menu JSON: %s", menuAsJSON);
}
// TODO - fixup
Menu *result = NewMenu(processedMenu, NULL, store);
result->menuType = ApplicationMenuType;
return result;
}
//TODO: Put traymenu store in callback instead of menu as it'll never be null
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, const char *menuItemID, enum MenuItemType menuItemType) {
MenuItemCallbackData* result = malloc(sizeof(MenuItemCallbackData));
result->store = menu->store;
result->menuItemID = STRCOPY(menuItemID);
result->menuItemType = menuItemType;
// Store reference to this so we can destroy later
vec_push(&menu->callbackDataCache, result);
return result;
}
void DeleteMenu(Menu *menu) {
if( menu == NULL ) {
return;
}
// Free menu item hashmap
hashmap_destroy(&menu->menuItemMap);
// Free radio group members
if( hashmap_num_entries(&menu->radioGroupMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&menu->radioGroupMap, freeHashmapItem, NULL)) {
ABORT("[DeleteMenu] Failed to release radioGroupMap entries!");
}
}
// Free radio groups hashmap
hashmap_destroy(&menu->radioGroupMap);
// Release the callback cache memory
int i; MenuItemCallbackData *callback;
vec_foreach(&menu->callbackDataCache, callback, i) {
MEMFREE(callback->menuItemID);
}
vec_deinit(&menu->callbackDataCache);
// Free nsmenu if we have it
if ( menu->menu != NULL ) {
msg(menu->menu, s("release"));
}
free(menu);
}
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
}
id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
return item;
}
id createMenuItem(id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg(item, s("autorelease"));
return item;
}
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), !disabled);
msg(menu, s("addItem:"), item);
return item;
}
id createMenu(id title) {
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("setAutoenablesItems:"), NO);
// msg(menu, s("autorelease"));
return menu;
}
void createDefaultAppMenu(id parentMenu) {
// App Menu
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu);
msg(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
// Edit Menu
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
addSeparator(editMenu);
addMenuItem(editMenu, "Cut", "cut:", "x", FALSE);
addMenuItem(editMenu, "Copy", "copy:", "c", FALSE);
addMenuItem(editMenu, "Paste", "paste:", "v", FALSE);
addMenuItem(editMenu, "Select All", "selectAll:", "a", FALSE);
}
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
const char *roleName = item->string_;
if ( STREQ(roleName, "appMenu") ) {
createDefaultAppMenu(parentMenu);
return;
}
if ( STREQ(roleName, "editMenu")) {
createDefaultEditMenu(parentMenu);
return;
}
if ( STREQ(roleName, "hide")) {
addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE);
return;
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE);
return;
}
if ( STREQ(roleName, "front")) {
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE);
return;
}
if ( STREQ(roleName, "undo")) {
addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE);
return;
}
if ( STREQ(roleName, "redo")) {
addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE);
return;
}
if ( STREQ(roleName, "cut")) {
addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE);
return;
}
if ( STREQ(roleName, "copy")) {
addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE);
return;
}
if ( STREQ(roleName, "paste")) {
addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE);
return;
}
if ( STREQ(roleName, "delete")) {
addMenuItem(parentMenu, "Delete", "delete:", "", FALSE);
return;
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
return;
}
if ( STREQ(roleName, "minimize")) {
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE);
return;
}
if ( STREQ(roleName, "zoom")) {
addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE);
return;
}
if ( STREQ(roleName, "quit")) {
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE);
return;
}
if ( STREQ(roleName, "togglefullscreen")) {
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE);
return;
}
}
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuItemID, bool disabled, bool checked, const char *acceleratorkey, bool hascallback) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
// hashmap_put(&menu->menuItemMap, (char*)menuItemID, strlen(menuItemID), item);
SaveMenuItemInStore((TrayMenuStore *) menu->store, menuItemID, item);
if( hascallback ) {
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, menuItemID, Radio);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
}
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuItemID, bool disabled, bool checked, const char *key, bool hascallback) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
// hashmap_put(&menu->menuItemMap, (char*)menuItemID, strlen(menuItemID), item);
SaveMenuItemInStore((TrayMenuStore *) menu->store, menuItemID, item);
if( hascallback ) {
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, menuItemID, Checkbox);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
}
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuItemID, bool disabled, const char *acceleratorkey, const char **modifiers, bool hascallback) {
id item = ALLOC("NSMenuItem");
SaveMenuItemInStore((TrayMenuStore *) menu->store, menuItemID, item);
if( hascallback ) {
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, menuItemID, Text);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
}
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg(parentMenu, s("addItem:"), item);
return item;
}
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Check if this item is hidden and if so, exit early!
bool hidden = getJSONBool(item, "h");
if( hidden ) {
return;
}
// Get the role
JsonNode *role = json_find_member(item, "r");
if( role != NULL ) {
processMenuRole(menu, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "S");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "l");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "i");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "l");
if ( label == NULL) {
label = "(empty)";
}
const char *menuItemID = getJSONString(item, "I");
if ( menuItemID == NULL) {
menuItemID = "";
}
bool disabled = getJSONBool(item, "d");
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "a");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// has callback?
bool hascallback = getJSONBool(item, "C");
// Get the Type
const char *type = mustJSONString(item, "t");
if( STREQ(type, "t")) {
processTextMenuItem(menu, parentMenu, label, menuItemID, disabled, acceleratorkey, modifiers, hascallback);
}
else if ( STREQ(type, "s")) {
addSeparator(parentMenu);
}
else if ( STREQ(type, "c")) {
// Get checked state
bool checked = getJSONBool(item, "c");
processCheckboxMenuItem(menu, parentMenu, label, menuItemID, disabled, checked, "", hascallback);
}
else if ( STREQ(type, "r")) {
// Get checked state
bool checked = getJSONBool(item, "c");
processRadioMenuItem(menu, parentMenu, label, menuItemID, disabled, checked, "", hascallback);
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
}
void processMenuData(Menu *menu, JsonNode *menuData) {
// Iterate items
JsonNode *item;
json_foreach(item, menuData) {
// Process each menu item
processMenuItem(menu, menu->menu, item);
}
}
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(&menu->menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(&menu->radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
}
id ProcessMenu(Menu *menu, JsonNode *menuData, JsonNode *radioGroups) {
// exit if we have no meny
if( menuData == NULL ) {
return NULL;
}
menu->menu = createMenu(str(""));
// Process the menu data
processMenuData(menu, menuData);
if( radioGroups != NULL ) {
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioGroups) {
// Get item label
processRadioGroupJSON(menu, radioGroup);
}
}
return menu->menu;
}

View File

@@ -0,0 +1,97 @@
//
// Created by Lea Anthony on 6/1/21.
//
#ifndef MENU_DARWIN_H
#define MENU_DARWIN_H
#include "common.h"
#include "ffenestri_darwin.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
extern void messageFromWindowCallback(const char *);
struct TrayMenuStore;
typedef struct {
const char *title;
/*** Internal ***/
struct hashmap_s menuItemMap;
struct hashmap_s radioGroupMap;
// Vector to keep track of callback data memory
vec_void_t callbackDataCache;
// The NSMenu for this menu
id menu;
// A reference to the Menu store
struct TrayMenuStore *store;
// The commands for the menu callbacks
const char *callbackCommand;
// This indicates if we are an Application Menu, tray menu or context menu
enum MenuType menuType;
} Menu;
typedef struct {
struct TrayMenuStore *store;
Menu *menu;
const char *menuItemID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData, JsonNode *radioGroups, struct TrayMenuStore *store);
Menu* NewApplicationMenu(const char *menuAsJSON, struct TrayMenuStore *store);
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, const char *menuItemID, enum MenuItemType menuItemType);
void DeleteMenu(Menu *menu);
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data);
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender);
id processAcceleratorKey(const char *key);
void addSeparator(id menu);
id createMenuItemNoAutorelease( id title, const char *action, const char *key);
id createMenuItem(id title, const char *action, const char *key);
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled);
id createMenu(id title);
void createDefaultAppMenu(id parentMenu);
void createDefaultEditMenu(id parentMenu);
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item);
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers);
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey, bool hasCallback);
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key, bool hasCallback);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, bool hasCallback);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
id ProcessMenu(Menu *menu, JsonNode *menuData, JsonNode *radioGroup);
#endif //ASSETS_C_MENU_DARWIN_H

View File

@@ -0,0 +1,101 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
MenuItem* NewMenuItem(enum MenuItemType type, const char* ID, const char* label, const char* alternateLabel, bool disabled, bool hidden, bool checked, const char* colour, const char* font, int fontsize, const char* image, const char* acceleratorKey, const char** modifiers, bool hasCallback, Menu* submenu) {
MenuItem *result = NEW(MenuItem);
// Setup
result->ID = STRCOPY(ID);
result->label = STRCOPY(label);
result->alternateLabel = STRCOPY(alternateLabel);
result->disabled = disabled;
result->hidden = hidden;
result->colour = STRCOPY(colour);
result->font = STRCOPY(font);
result->fontSize = fontsize;
result->image = STRCOPY(image);
result->acceleratorKey = STRCOPY(acceleratorKey);
result->modifiers = modifiers;
result->hasCallback = hasCallback;
result->type = type;
result->checked = checked;
result->submenu = submenu;
result->callbackData = NULL;
vec_init(&result->radioGroup);
SetupMenuItemPlatformData(result);
return result;
}
void DeleteMenuItem(MenuItem* menuItem) {
MEMFREE(menuItem->ID);
MEMFREE(menuItem->label);
MEMFREE(menuItem->alternateLabel);
MEMFREE(menuItem->colour);
MEMFREE(menuItem->font);
MEMFREE(menuItem->image);
MEMFREE(menuItem->acceleratorKey);
// Iterate the modifiers and free elements
if( menuItem->modifiers != NULL ) {
int i = 0;
const char *nextItem = menuItem->modifiers[0];
while (nextItem != NULL) {
MEMFREE(nextItem);
i++;
nextItem = menuItem->modifiers[i];
}
MEMFREE(menuItem->modifiers);
}
DeleteMenuItemCallbackData(menuItem->callbackData);
DeleteMenuItemPlatformData(menuItem);
MEMFREE(menuItem);
}
JsonNode* MenuItemAsJSONObject(MenuItem* menuItem) {
JsonNode* result = json_mkobject();
JSON_ADD_STRING(result, "ID", menuItem->ID);
JSON_ADD_STRING(result, "label", menuItem->label);
JSON_ADD_STRING(result, "alternateLabel", menuItem->alternateLabel);
JSON_ADD_BOOL(result, "disabled", menuItem->disabled);
JSON_ADD_BOOL(result, "hidden", menuItem->hidden);
JSON_ADD_STRING(result, "colour", menuItem->colour);
JSON_ADD_STRING(result, "font", menuItem->font);
JSON_ADD_NUMBER(result, "fontsize", menuItem->fontSize);
JSON_ADD_STRING(result, "image", menuItem->image);
JSON_ADD_STRING(result, "acceleratorKey", menuItem->acceleratorKey);
JSON_ADD_BOOL(result, "hasCallback", menuItem->hasCallback);
JSON_ADD_STRING(result, "type", MenuItemTypeAsString[menuItem->type]);
JSON_ADD_BOOL(result, "checked", menuItem->checked);
return result;
}
MenuItemCallbackData* NewMenuItemCallbackData(MenuManager *manager, const char* menuItemID, enum MenuItemType menuItemType) {
MenuItemCallbackData* result = NEW(MenuItemCallbackData);
result->manager = manager;
result->menuItemID = STRCOPY(menuItemID);
result->menuItemType = menuItemType;
return result;
}
void DeleteMenuItemCallbackData(MenuItemCallbackData* callbackData) {
if( callbackData == NULL ) return;
MEMFREE(callbackData->menuItemID);
MEMFREE(callbackData);
}

View File

@@ -0,0 +1,81 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
MenuManager* NewMenuManager() {
MenuManager* result = malloc(sizeof(MenuManager));
// Allocate Hashmaps
HASHMAP_INIT(result->menuItems, 32, "menuItems");
HASHMAP_INIT(result->contextMenus, 4, "contextMenus");
HASHMAP_INIT(result->trayMenus, 4, "trayMenus");
// Initialise other data
result->applicationMenu = NULL;
result->contextMenuData = NULL;
return result;
}
int deleteTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data);
return -1; // Remove from hashmap
}
int deleteMenuItem(void *const context, struct hashmap_element_s *const e) {
DeleteMenuItem(e->data);
return -1; // Remove from hashmap
}
void DeleteMenuManager(MenuManager* manager) {
// Iterate hashmaps and delete items
HASHMAP_ITERATE(manager->trayMenus, deleteTrayMenu, NULL);
HASHMAP_ITERATE(manager->menuItems, deleteMenuItem, NULL);
// Delete applicationMenu
DeleteMenu(manager->applicationMenu);
// Delete Hashmaps
HASHMAP_DESTROY(manager->trayMenus);
HASHMAP_DESTROY(manager->contextMenus);
HASHMAP_DESTROY(manager->menuItems);
// Delete context menu data
MEMFREE(manager->contextMenuData);
}
TrayMenu* AddTrayMenuToManager(MenuManager* manager, const char* trayMenuJSON) {
// Parse JSON
struct JsonNode* parsedJSON = mustParseJSON(trayMenuJSON);
// Get the ID
const char *ID = mustJSONString(parsedJSON, "I");
// Check if there is already an entry for this menu
TrayMenu* existingTrayMenu = HASHMAP_GET(manager->trayMenus, ID);
if ( existingTrayMenu != NULL ) {
json_delete(parsedJSON);
return existingTrayMenu;
}
// Create new menu
TrayMenu* newMenu = NewTrayMenu(parsedJSON, manager);
HASHMAP_PUT(manager->trayMenus, newMenu->ID, newMenu);
return newMenu;
}
int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
ShowTrayMenu(e->data);
// 0 to retain element, -1 to delete.
return 0;
}
void ShowTrayMenus(MenuManager* manager) {
HASHMAP_ITERATE(manager->trayMenus, showTrayMenu, NULL);
}

View File

@@ -0,0 +1,69 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
TrayMenu* NewTrayMenu(JsonNode* parsedJSON, MenuManager *manager) {
// NULL GUARD
if(parsedJSON == NULL) ABORT("[NewTrayMenu] parsedJSON == NULL");
// Create new tray menu
TrayMenu* result = NEW(TrayMenu);
// Initialise other data
result->ID = STRCOPY(mustJSONString(parsedJSON, "I"));
result->Label = STRCOPY(getJSONString(parsedJSON, "l"));
result->Icon = STRCOPY(getJSONString(parsedJSON, "i"));
// Process menu
struct JsonNode* menuJSON = getJSONObject(parsedJSON, "m");
struct JsonNode* radioJSON = getJSONObject(parsedJSON, "r");
result->Menu = NewMenu(menuJSON, radioJSON, manager);
// Setup platform data
SetupTrayMenuPlatformData(result);
return result;
}
void DeleteTrayMenu(TrayMenu *trayMenu) {
// NULL guard
if( trayMenu == NULL ) return;
// Free the strings
MEMFREE(trayMenu->ID);
MEMFREE(trayMenu->Label);
MEMFREE(trayMenu->Icon);
// Delete the menu
DeleteMenu(trayMenu->Menu);
// Delete the platform data
DeleteTrayMenuPlatformData(trayMenu);
// Free tray menu
MEMFREE(trayMenu);
}
const char* TrayMenuAsJSON(TrayMenu* menu) {
JsonNode *jsonObject = json_mkobject();
JSON_ADD_STRING(jsonObject, "ID", menu->ID);
JSON_ADD_STRING(jsonObject, "Label", menu->Label);
JSON_ADD_STRING(jsonObject, "Icon", menu->Icon);
JSON_ADD_OBJECT(jsonObject, "Menu", MenuAsJSONObject(menu->Menu));
return json_encode(jsonObject);
}
void UpdateTrayIcon(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->Icon == NULL ) {
return;
}
PlatformUpdateTrayIcon(trayMenu);
}

View File

@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.17)
project(test_app)
set(CMAKE_CXX_STANDARD 14)
set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -g")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -fsanitize=leak -g")
add_executable(test_app test.c)
add_library(menus STATIC ../menu_manager.c ../menu.c ../menu_tray.c ../menu_item.c )
add_library(common STATIC ../common.c ../utf8.h)
add_library(json STATIC ../json.c)
add_library(vec STATIC ../vec.c)
if( CMAKE_HOST_APPLE )
find_library(WEBKIT WebKit)
add_library(runtime STATIC ../runtime_darwin.c)
add_library(ffenestri STATIC ../ffenestri_darwin.c)
add_library(defaulticons STATIC ../defaultdialogicons_darwin.c)
add_library(platform STATIC ../menu_darwin.c)
target_link_libraries(test_app objc ${WEBKIT} ffenestri platform runtime)
endif()
target_link_libraries(test_app vec json common menus)
include_directories(..)
include_directories(.)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.17)
project(test_menumanager)
set(CMAKE_CXX_STANDARD 14)
set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -g")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -fsanitize=leak -g")
add_executable(test_menumanager test.c minunit.h)
add_library(menus STATIC ../menu_manager.c ../menu.c ../menu_tray.c ../menu_item.c)
add_library(common STATIC ../common.c ../utf8.h)
add_library(json STATIC ../json.c)
add_library(vec STATIC ../vec.c)
if( CMAKE_HOST_APPLE )
add_library(platform STATIC ../menu_darwin.c)
target_link_libraries(test_menumanager objc)
endif()
target_link_libraries(test_menumanager vec json common menus platform)
include_directories(..)
include_directories(.)

View File

@@ -0,0 +1,20 @@
Copyright (c) 2012 David Siñuela Pastor, siu.4coders@gmail.com
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,391 @@
/*
* Copyright (c) 2012 David Siñuela Pastor, siu.4coders@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef MINUNIT_MINUNIT_H
#define MINUNIT_MINUNIT_H
#ifdef __cplusplus
extern "C" {
#endif
#if defined(_WIN32)
#include <Windows.h>
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#define __func__ __FUNCTION__
#endif
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
/* Change POSIX C SOURCE version for pure c99 compilers */
#if !defined(_POSIX_C_SOURCE) || _POSIX_C_SOURCE < 200112L
#undef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200112L
#endif
#include <unistd.h> /* POSIX flags */
#include <time.h> /* clock_gettime(), time() */
#include <sys/time.h> /* gethrtime(), gettimeofday() */
#include <sys/resource.h>
#include <sys/times.h>
#include <string.h>
#if defined(__MACH__) && defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_time.h>
#endif
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
#define __func__ __extension__ __FUNCTION__
#endif
#else
#error "Unable to define timers for an unknown OS."
#endif
#include <stdio.h>
#include <math.h>
/* Maximum length of last message */
#define MINUNIT_MESSAGE_LEN 1024
/* Accuracy with which floats are compared */
#define MINUNIT_EPSILON 1E-12
/* Misc. counters */
static int minunit_run = 0;
static int minunit_assert = 0;
static int minunit_fail = 0;
static int minunit_status = 0;
/* Timers */
static double minunit_real_timer = 0;
static double minunit_proc_timer = 0;
/* Last message */
static char minunit_last_message[MINUNIT_MESSAGE_LEN];
/* Test setup and teardown function pointers */
static void (*minunit_setup)(void) = NULL;
static void (*minunit_teardown)(void) = NULL;
/* Definitions */
#define MU_TEST(method_name) static void method_name(void)
#define MU_TEST_SUITE(suite_name) static void suite_name(void)
#define MU__SAFE_BLOCK(block) do {\
block\
} while(0)
/* Run test suite and unset setup and teardown functions */
#define MU_RUN_SUITE(suite_name) MU__SAFE_BLOCK(\
suite_name();\
minunit_setup = NULL;\
minunit_teardown = NULL;\
)
/* Configure setup and teardown functions */
#define MU_SUITE_CONFIGURE(setup_fun, teardown_fun) MU__SAFE_BLOCK(\
minunit_setup = setup_fun;\
minunit_teardown = teardown_fun;\
)
/* Test runner */
#define MU_RUN_TEST(test) MU__SAFE_BLOCK(\
if (minunit_real_timer==0 && minunit_proc_timer==0) {\
minunit_real_timer = mu_timer_real();\
minunit_proc_timer = mu_timer_cpu();\
}\
if (minunit_setup) (*minunit_setup)();\
minunit_status = 0;\
test();\
minunit_run++;\
if (minunit_status) {\
minunit_fail++;\
printf("F");\
printf("\n%s\n", minunit_last_message);\
}\
fflush(stdout);\
if (minunit_teardown) (*minunit_teardown)();\
)
/* Report */
#define MU_REPORT() MU__SAFE_BLOCK(\
double minunit_end_real_timer;\
double minunit_end_proc_timer;\
printf("\n\n%d tests, %d assertions, %d failures\n", minunit_run, minunit_assert, minunit_fail);\
minunit_end_real_timer = mu_timer_real();\
minunit_end_proc_timer = mu_timer_cpu();\
printf("\nFinished in %.8f seconds (real) %.8f seconds (proc)\n\n",\
minunit_end_real_timer - minunit_real_timer,\
minunit_end_proc_timer - minunit_proc_timer);\
)
#define MU_EXIT_CODE minunit_fail
/* Assertions */
#define mu_check(test) MU__SAFE_BLOCK(\
minunit_assert++;\
if (!(test)) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %s", __func__, __FILE__, __LINE__, #test);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_fail(message) MU__SAFE_BLOCK(\
minunit_assert++;\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %s", __func__, __FILE__, __LINE__, message);\
minunit_status = 1;\
return;\
)
#define mu_assert(test, message) MU__SAFE_BLOCK(\
minunit_assert++;\
if (!(test)) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %s", __func__, __FILE__, __LINE__, message);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_assert_int_eq(expected, result) MU__SAFE_BLOCK(\
int minunit_tmp_e;\
int minunit_tmp_r;\
minunit_assert++;\
minunit_tmp_e = (expected);\
minunit_tmp_r = (result);\
if (minunit_tmp_e != minunit_tmp_r) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %d expected but was %d", __func__, __FILE__, __LINE__, minunit_tmp_e, minunit_tmp_r);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_assert_double_eq(expected, result) MU__SAFE_BLOCK(\
double minunit_tmp_e;\
double minunit_tmp_r;\
minunit_assert++;\
minunit_tmp_e = (expected);\
minunit_tmp_r = (result);\
if (fabs(minunit_tmp_e-minunit_tmp_r) > MINUNIT_EPSILON) {\
int minunit_significant_figures = 1 - log10(MINUNIT_EPSILON);\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %.*g expected but was %.*g", __func__, __FILE__, __LINE__, minunit_significant_figures, minunit_tmp_e, minunit_significant_figures, minunit_tmp_r);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_assert_string_eq(expected, result) MU__SAFE_BLOCK(\
const char* minunit_tmp_e = expected;\
const char* minunit_tmp_r = result;\
minunit_assert++;\
if (!minunit_tmp_e) {\
minunit_tmp_e = "<null pointer>";\
}\
if (!minunit_tmp_r) {\
minunit_tmp_r = "<null pointer>";\
}\
if(strcmp(minunit_tmp_e, minunit_tmp_r) != 0) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: '%s' expected but was '%s'", __func__, __FILE__, __LINE__, minunit_tmp_e, minunit_tmp_r);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
/*
* The following two functions were written by David Robert Nadeau
* from http://NadeauSoftware.com/ and distributed under the
* Creative Commons Attribution 3.0 Unported License
*/
/**
* Returns the real time, in seconds, or -1.0 if an error occurred.
*
* Time is measured since an arbitrary and OS-dependent start time.
* The returned real time is only useful for computing an elapsed time
* between two calls to this function.
*/
static double mu_timer_real(void)
{
#if defined(_WIN32)
/* Windows 2000 and later. ---------------------------------- */
LARGE_INTEGER Time;
LARGE_INTEGER Frequency;
QueryPerformanceFrequency(&Frequency);
QueryPerformanceCounter(&Time);
Time.QuadPart *= 1000000;
Time.QuadPart /= Frequency.QuadPart;
return (double)Time.QuadPart / 1000000.0;
#elif (defined(__hpux) || defined(hpux)) || ((defined(__sun__) || defined(__sun) || defined(sun)) && (defined(__SVR4) || defined(__svr4__)))
/* HP-UX, Solaris. ------------------------------------------ */
return (double)gethrtime( ) / 1000000000.0;
#elif defined(__MACH__) && defined(__APPLE__)
/* OSX. ----------------------------------------------------- */
static double timeConvert = 0.0;
if ( timeConvert == 0.0 )
{
mach_timebase_info_data_t timeBase;
(void)mach_timebase_info( &timeBase );
timeConvert = (double)timeBase.numer /
(double)timeBase.denom /
1000000000.0;
}
return (double)mach_absolute_time( ) * timeConvert;
#elif defined(_POSIX_VERSION)
/* POSIX. --------------------------------------------------- */
struct timeval tm;
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
{
struct timespec ts;
#if defined(CLOCK_MONOTONIC_PRECISE)
/* BSD. --------------------------------------------- */
const clockid_t id = CLOCK_MONOTONIC_PRECISE;
#elif defined(CLOCK_MONOTONIC_RAW)
/* Linux. ------------------------------------------- */
const clockid_t id = CLOCK_MONOTONIC_RAW;
#elif defined(CLOCK_HIGHRES)
/* Solaris. ----------------------------------------- */
const clockid_t id = CLOCK_HIGHRES;
#elif defined(CLOCK_MONOTONIC)
/* AIX, BSD, Linux, POSIX, Solaris. ----------------- */
const clockid_t id = CLOCK_MONOTONIC;
#elif defined(CLOCK_REALTIME)
/* AIX, BSD, HP-UX, Linux, POSIX. ------------------- */
const clockid_t id = CLOCK_REALTIME;
#else
const clockid_t id = (clockid_t)-1; /* Unknown. */
#endif /* CLOCK_* */
if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 )
return (double)ts.tv_sec +
(double)ts.tv_nsec / 1000000000.0;
/* Fall thru. */
}
#endif /* _POSIX_TIMERS */
/* AIX, BSD, Cygwin, HP-UX, Linux, OSX, POSIX, Solaris. ----- */
gettimeofday( &tm, NULL );
return (double)tm.tv_sec + (double)tm.tv_usec / 1000000.0;
#else
return -1.0; /* Failed. */
#endif
}
/**
* Returns the amount of CPU time used by the current process,
* in seconds, or -1.0 if an error occurred.
*/
static double mu_timer_cpu(void)
{
#if defined(_WIN32)
/* Windows -------------------------------------------------- */
FILETIME createTime;
FILETIME exitTime;
FILETIME kernelTime;
FILETIME userTime;
/* This approach has a resolution of 1/64 second. Unfortunately, Windows' API does not offer better */
if ( GetProcessTimes( GetCurrentProcess( ),
&createTime, &exitTime, &kernelTime, &userTime ) != 0 )
{
ULARGE_INTEGER userSystemTime;
memcpy(&userSystemTime, &userTime, sizeof(ULARGE_INTEGER));
return (double)userSystemTime.QuadPart / 10000000.0;
}
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
/* AIX, BSD, Cygwin, HP-UX, Linux, OSX, and Solaris --------- */
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
/* Prefer high-res POSIX timers, when available. */
{
clockid_t id;
struct timespec ts;
#if _POSIX_CPUTIME > 0
/* Clock ids vary by OS. Query the id, if possible. */
if ( clock_getcpuclockid( 0, &id ) == -1 )
#endif
#if defined(CLOCK_PROCESS_CPUTIME_ID)
/* Use known clock id for AIX, Linux, or Solaris. */
id = CLOCK_PROCESS_CPUTIME_ID;
#elif defined(CLOCK_VIRTUAL)
/* Use known clock id for BSD or HP-UX. */
id = CLOCK_VIRTUAL;
#else
id = (clockid_t)-1;
#endif
if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 )
return (double)ts.tv_sec +
(double)ts.tv_nsec / 1000000000.0;
}
#endif
#if defined(RUSAGE_SELF)
{
struct rusage rusage;
if ( getrusage( RUSAGE_SELF, &rusage ) != -1 )
return (double)rusage.ru_utime.tv_sec +
(double)rusage.ru_utime.tv_usec / 1000000.0;
}
#endif
#if defined(_SC_CLK_TCK)
{
const double ticks = (double)sysconf( _SC_CLK_TCK );
struct tms tms;
if ( times( &tms ) != (clock_t)-1 )
return (double)tms.tms_utime / ticks;
}
#endif
#if defined(CLOCKS_PER_SEC)
{
clock_t cl = clock( );
if ( cl != (clock_t)-1 )
return (double)cl / (double)CLOCKS_PER_SEC;
}
#endif
#endif
return -1; /* Failed. */
}
#ifdef __cplusplus
}
#endif
#endif /* MINUNIT_MINUNIT_H */

View File

@@ -0,0 +1,71 @@
//
// Created by Lea Anthony on 12/1/21.
//
#include "minunit.h"
#include "menu.h"
#define empty "{\"I\":\"T1\"}"
#define emptyExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":null}"
#define labelOnly "{\"I\":\"T1\",\"l\":\"test\"}"
#define labelOnlyExpected "{\"ID\":\"T1\",\"Label\":\"test\",\"Icon\":null,\"Menu\":null}"
#define iconOnly "{\"I\":\"T1\",\"i\":\"svelte\"}"
#define iconOnlyExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":\"svelte\",\"Menu\":null}"
#define iconLabel "{\"I\":\"T1\",\"l\":\"test\",\"i\":\"svelte\"}"
#define iconLabelExpected "{\"ID\":\"T1\",\"Label\":\"test\",\"Icon\":\"svelte\",\"Menu\":null}"
#define blankLabel "{\"I\":\"T1\"}"
#define blankLabelExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":null}"
#define blankMenu "{\"I\":\"T1\"}"
#define blankMenuExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":null}"
#define menuTextItem "{\"I\":\"T1\",\"l\":\"test\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\"}]}"
#define menuTextItemExpected "{\"ID\":\"T1\",\"Label\":\"test\",\"Icon\":null,\"Menu\":{\"Label\":\"\",\"Items\":[{\"ID\":\"1\",\"label\":\"test\",\"alternateLabel\":null,\"disabled\":false,\"hidden\":false,\"colour\":null,\"font\":null,\"fontsize\":0,\"image\":null,\"acceleratorKey\":null,\"hasCallback\":false,\"type\":\"Text\",\"checked\":false}]}}"
#define checkboxItem "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"c\",\"c\":true}]}"
#define checkboxItemExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":{\"Label\":\"\",\"Items\":[{\"ID\":\"1\",\"label\":\"test\",\"alternateLabel\":null,\"disabled\":false,\"hidden\":false,\"colour\":null,\"font\":null,\"fontsize\":0,\"image\":null,\"acceleratorKey\":null,\"hasCallback\":false,\"type\":\"Checkbox\",\"checked\":true}]}}"
#define radioGroupItems "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"option 1\",\"t\":\"r\",\"c\":true},{\"I\":\"2\",\"l\":\"option 2\",\"t\":\"r\"},{\"I\":\"3\",\"l\":\"option 3\",\"t\":\"r\"}],\"r\":[{\"Members\":[\"1\",\"2\",\"3\"],\"Length\":3}]}"
#define radioGroupItemsExpected ""
#define callbackItem "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"Preferences\",\"t\":\"t\",\"C\":true}]}"
#define callbackItemExpected ""
const char* tests[] = {
empty, emptyExpected,
labelOnly, labelOnlyExpected,
iconOnly, iconOnlyExpected,
iconLabel, iconLabelExpected,
blankLabel, blankLabelExpected,
blankMenu, blankMenuExpected,
menuTextItem, menuTextItemExpected,
checkboxItem, checkboxItemExpected,
radioGroupItems, radioGroupItemsExpected,
callbackItem, callbackItemExpected,
};
MU_TEST(manager_creation) {
MenuManager* manager = NewMenuManager();
mu_assert(manager->applicationMenu == NULL, "app menu");
mu_assert(manager->contextMenuData == NULL, "context menu data");
mu_assert_int_eq(hashmap_num_entries(&manager->contextMenus), 0);
mu_assert_int_eq(hashmap_num_entries(&manager->trayMenus), 0);
mu_assert_int_eq(hashmap_num_entries(&manager->menuItems), 0);
DeleteMenuManager(manager);
}
MU_TEST(add_tray) {
for( int count = 0; count < sizeof(tests) / sizeof(tests[0]); count += 2 ) {
MenuManager* manager = NewMenuManager();
TrayMenu* tray = AddTrayMenu(manager, tests[count]);
const char* trayJSON = TrayMenuAsJSON(tray);
mu_assert_string_eq(tests[count+1], trayJSON);
DeleteMenuManager(manager);
}
}
MU_TEST_SUITE(test_suite) {
MU_RUN_TEST(manager_creation);
MU_RUN_TEST(add_tray);
}
int main(int argc, char *argv[]) {
MU_RUN_SUITE(test_suite);
MU_REPORT();
return MU_EXIT_CODE;
}

View File

@@ -10,7 +10,7 @@
// Global because it's a singleton
struct hashmap_s trayIconCache;
TrayMenu* NewTrayMenu(const char* menuJSON) {
TrayMenu* NewTrayMenu(const char* menuJSON, struct TrayMenuStore* store) {
TrayMenu* result = malloc(sizeof(TrayMenu));
/*
@@ -21,26 +21,29 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", menuJSON);
}
// Save reference to this json
result->processedJSON = processedJSON;
// TODO: Make this configurable
result->trayIconPosition = NSImageLeft;
result->ID = mustJSONString(processedJSON, "ID");
result->label = mustJSONString(processedJSON, "Label");
result->icon = mustJSONString(processedJSON, "Icon");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->ID = STRCOPY(mustJSONString(processedJSON, "I"));
result->label = STRCOPY(getJSONString(processedJSON, "l"));
result->icon = STRCOPY(getJSONString(processedJSON, "i"));
result->menu = NULL;
// Create the menu
result->menu = NewMenu(processedMenu);
JsonNode* menu = getJSONObject(processedJSON, "m");
if( menu != NULL ) {
JsonNode* radioGroups = getJSONObject(processedJSON, "r");
// Create the menu
result->menu = NewMenu(menu, radioGroups, store);
result->menu->menuType = TrayMenuType;
}
// Init tray status bar item
result->statusbaritem = NULL;
// Set the menu type and store the tray ID in the parent data
result->menu->menuType = TrayMenuType;
result->menu->parentData = (void*) result->ID;
// Free JSON
json_delete(processedJSON);
return result;
}
@@ -49,42 +52,8 @@ void DumpTrayMenu(TrayMenu* trayMenu) {
printf(" ['%s':%p] = { label: '%s', icon: '%s', menu: %p, statusbar: %p }\n", trayMenu->ID, trayMenu, trayMenu->label, trayMenu->icon, trayMenu->menu, trayMenu->statusbaritem );
}
void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one
if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayMenuIcon(trayMenu);
// Update the label if needed
UpdateTrayMenuLabel(trayMenu);
// Update the menu
id menu = GetMenu(trayMenu->menu);
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
}
void UpdateTrayMenuLabel(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
return;
}
// We don't check for a
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setTitle:"), str(trayMenu->label));
}
void UpdateTrayMenuIcon(TrayMenu *trayMenu) {
void UpdateTrayIcon(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->icon == NULL ) {
@@ -105,6 +74,33 @@ void UpdateTrayMenuIcon(TrayMenu *trayMenu) {
msg(statusBarButton, s("setImage:"), trayImage);
}
void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one
if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label);
// Update the menu
if (trayMenu->menu != NULL ) {
msg(trayMenu->statusbaritem, s("setMenu:"), trayMenu->menu->menu);
}
}
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets
// updated with the data from the new menu.
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
@@ -115,17 +111,11 @@ void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
// Set the new one
currentMenu->menu = newMenu->menu;
// Delete the old JSON
json_delete(currentMenu->processedJSON);
// Set the new JSON
currentMenu->processedJSON = newMenu->processedJSON;
// Copy the other data
currentMenu->ID = newMenu->ID;
currentMenu->label = newMenu->label;
currentMenu->ID = STRCOPY(newMenu->ID);
currentMenu->label = STRCOPY(newMenu->label);
currentMenu->trayIconPosition = newMenu->trayIconPosition;
currentMenu->icon = newMenu->icon;
currentMenu->icon = STRCOPY(newMenu->icon);
}
@@ -137,10 +127,10 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
// Delete the menu
DeleteMenu(trayMenu->menu);
// Free JSON
if (trayMenu->processedJSON != NULL ) {
json_delete(trayMenu->processedJSON);
}
// Free strings
MEMFREE(trayMenu->label);
MEMFREE(trayMenu->icon);
MEMFREE(trayMenu->ID);
// Free the status item
if ( trayMenu->statusbaritem != NULL ) {
@@ -186,14 +176,3 @@ void LoadTrayIcons() {
}
}
void UnloadTrayIcons() {
// Release the tray cache images
if( hashmap_num_entries(&trayIconCache) > 0 ) {
if (0!=hashmap_iterate_pairs(&trayIconCache, releaseNSObject, NULL)) {
ABORT("failed to release hashmap entries!");
}
}
//Free radio groups hashmap
hashmap_destroy(&trayIconCache);
}

View File

@@ -6,7 +6,7 @@
#define TRAYMENU_DARWIN_H
#include "common.h"
#include "menu_darwin.h"
#include "menu_darwin_old.h"
typedef struct {
@@ -18,17 +18,14 @@ typedef struct {
id statusbaritem;
int trayIconPosition;
JsonNode* processedJSON;
} TrayMenu;
TrayMenu* NewTrayMenu(const char *trayJSON);
TrayMenu* NewTrayMenu(const char *trayJSON, struct TrayMenuStore* store);
void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayMenuIcon(TrayMenu *trayMenu);
void UpdateTrayMenuLabel(TrayMenu *trayMenu);
void UpdateTrayIcon(TrayMenu *trayMenu);
void UpdateTrayLabel(TrayMenu *trayMenu, const char*);
void LoadTrayIcons();
void UnloadTrayIcons();

View File

@@ -16,6 +16,11 @@ TrayMenuStore* NewTrayMenuStore() {
ABORT("[NewTrayMenuStore] Not enough memory to allocate trayMenuMap!");
}
// Allocate menu item store
if( 0 != hashmap_create((const unsigned)8, &result->menuItemMap)) {
ABORT("[NewTrayMenuStore] Not enough memory to allocate menuItemMap!");
}
return result;
}
@@ -29,7 +34,7 @@ void DumpTrayMenuStore(TrayMenuStore* store) {
}
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
TrayMenu* newMenu = NewTrayMenu(menuJSON, (struct TrayMenuStore *) store);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
@@ -65,6 +70,9 @@ void DeleteTrayMenuStore(TrayMenuStore *store) {
// Destroy tray menu map
hashmap_destroy(&store->trayMenuMap);
// Destroy menu item map
hashmap_destroy(&store->menuItemMap);
}
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
@@ -72,13 +80,50 @@ TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
}
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
}
return result;
}
id GetMenuItemFromStore(TrayMenuStore* store, const char* menuItemID) {
return hashmap_get(&store->menuItemMap, menuItemID, strlen(menuItemID));
}
void SaveMenuItemInStore(TrayMenuStore* store, const char* menuItemID, id nsmenuitem) {
hashmap_put(&store->menuItemMap, menuItemID, strlen(menuItemID), nsmenuitem);
}
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON);
// Get the data out
const char* ID = mustJSONString(parsedUpdate, "ID");
const char* Label = mustJSONString(parsedUpdate, "Label");
// Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
UpdateTrayLabel(menu, Label);
}
TrayMenu* UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON, (struct TrayMenuStore *) store);
// Get the current menu
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
// If we don't have a menu, we create one
if ( currentMenu == NULL ) {
ABORT("Attempted to update unknown tray menu with ID '%s'.", newMenu->ID);
// Store the new menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
return newMenu;
}
// Save the status bar reference
@@ -90,18 +135,16 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
DeleteMenu(currentMenu->menu);
currentMenu->menu = NULL;
// Free JSON
if (currentMenu->processedJSON != NULL ) {
json_delete(currentMenu->processedJSON);
currentMenu->processedJSON = NULL;
}
// Free the tray menu memory
MEMFREE(currentMenu);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
// Show the updated menu
ShowTrayMenu(newMenu);
return newMenu;
}
const char* GetContextMenuDataFromStore(TrayMenuStore *store) {
return store->contextMenuData;
}

View File

@@ -5,6 +5,8 @@
#ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H
#include "traymenu_darwin.h"
typedef struct {
int dummy;
@@ -13,13 +15,26 @@ typedef struct {
// It maps tray IDs to TrayMenu*
struct hashmap_s trayMenuMap;
// This is our menu item map
// It maps menu Item IDs to NSMenuItems
struct hashmap_s menuItemMap;
const char* contextMenuData;
} TrayMenuStore;
TrayMenuStore* NewTrayMenuStore();
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON);
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
TrayMenu* UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store);
void SaveMenuItemInStore(TrayMenuStore* store, const char* menuItemID, id nsmenuitem);
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID);
id GetMenuItemFromStore(TrayMenuStore* store, const char* menuItemID);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
const char* GetContextMenuDataFromStore(TrayMenuStore *store);
#endif //TRAYMENUSTORE_DARWIN_H

1292
v2/internal/ffenestri/utf8.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,8 @@
( vec_splice_(vec_unpack_(v), start, count),\
(v)->length -= (count) )
#define vec_size(v) \
(v)->length
#define vec_swapsplice(v, start, count)\
( vec_swapsplice_(vec_unpack_(v), start, count),\

View File

@@ -1,46 +0,0 @@
package menumanager
import "github.com/wailsapp/wails/v2/pkg/menu"
func (m *Manager) SetApplicationMenu(applicationMenu *menu.Menu) error {
if applicationMenu == nil {
return nil
}
m.applicationMenu = applicationMenu
// Reset the menu map
m.applicationMenuItemMap = NewMenuItemMap()
// Add the menu to the menu map
m.applicationMenuItemMap.AddMenu(applicationMenu)
return m.processApplicationMenu()
}
func (m *Manager) GetApplicationMenuJSON() string {
return m.applicationMenuJSON
}
// UpdateApplicationMenu reprocesses the application menu to pick up structure
// changes etc
// Returns the JSON representation of the updated menu
func (m *Manager) UpdateApplicationMenu() (string, error) {
m.applicationMenuItemMap = NewMenuItemMap()
m.applicationMenuItemMap.AddMenu(m.applicationMenu)
err := m.processApplicationMenu()
return m.applicationMenuJSON, err
}
func (m *Manager) processApplicationMenu() error {
// Process the menu
processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
applicationMenuJSON, err := processedApplicationMenu.AsJSON()
if err != nil {
return err
}
m.applicationMenuJSON = applicationMenuJSON
return nil
}

View File

@@ -1,60 +1,65 @@
package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
type ContextMenu struct {
ID string
ProcessedMenu *WailsMenu
menuItemMap *MenuItemMap
menu *menu.Menu
}
func (t *ContextMenu) AsJSON() (string, error) {
data, err := json.Marshal(t)
if err != nil {
return "", err
}
return string(data), nil
}
func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
result := &ContextMenu{
ID: contextMenu.ID,
menu: contextMenu.Menu,
menuItemMap: NewMenuItemMap(),
}
result.menuItemMap.AddMenu(contextMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result
}
import "github.com/wailsapp/wails/v2/pkg/menu"
//
//import (
// "encoding/json"
// "fmt"
// "github.com/wailsapp/wails/v2/pkg/menu"
//)
//
//type ContextMenu struct {
// ID string
// ProcessedMenu *WailsMenu
// menuItemMap *MenuItemMap
// menu *menu.Menu
//}
//
//func (t *ContextMenu) AsJSON() (string, error) {
// data, err := json.Marshal(t)
// if err != nil {
// return "", err
// }
// return string(data), nil
//}
//
//func (m *Manager) NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
//
// result := &ContextMenu{
// ID: contextMenu.ID,
// menu: contextMenu.Menu,
// menuItemMap: NewMenuItemMap(),
// }
//
// result.menuItemMap.AddMenu(contextMenu.Menu)
// result.ProcessedMenu = m.NewWailsMenu(result.menuItemMap, result.menu)
//
// return result
//}
//
func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
newContextMenu := NewContextMenu(contextMenu)
// Save the references
m.contextMenus[contextMenu.ID] = newContextMenu
m.contextMenuPointers[contextMenu] = contextMenu.ID
//
// newContextMenu := m.NewContextMenu(contextMenu)
//
// // Save the references
// m.contextMenus[contextMenu.ID] = newContextMenu
// m.contextMenuPointers[contextMenu] = contextMenu.ID
}
//
func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) {
contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
if !contextMenuKnown {
return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
}
// Create the updated context menu
updatedContextMenu := NewContextMenu(contextMenu)
// Save the reference
m.contextMenus[contextMenuID] = updatedContextMenu
return updatedContextMenu.AsJSON()
// contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
// if !contextMenuKnown {
// return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
// }
//
// // Create the updated context menu
// updatedContextMenu := m.NewContextMenu(contextMenu)
//
// // Save the reference
// m.contextMenus[contextMenuID] = updatedContextMenu
//
// return updatedContextMenu.AsJSON()
return "", nil
}

View File

@@ -1,72 +0,0 @@
package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"sync"
)
// MenuItemMap holds a mapping between menuIDs and menu items
type MenuItemMap struct {
idToMenuItemMap map[string]*menu.MenuItem
menuItemToIDMap map[*menu.MenuItem]string
// We use a simple counter to keep track of unique menu IDs
menuIDCounter int64
menuIDCounterMutex sync.Mutex
}
func NewMenuItemMap() *MenuItemMap {
result := &MenuItemMap{
idToMenuItemMap: make(map[string]*menu.MenuItem),
menuItemToIDMap: make(map[*menu.MenuItem]string),
}
return result
}
func (m *MenuItemMap) AddMenu(menu *menu.Menu) {
for _, item := range menu.Items {
m.processMenuItem(item)
}
}
func (m *MenuItemMap) Dump() {
println("idToMenuItemMap:")
for key, value := range m.idToMenuItemMap {
fmt.Printf(" %s\t%p\n", key, value)
}
println("\nmenuItemToIDMap")
for key, value := range m.menuItemToIDMap {
fmt.Printf(" %p\t%s\n", key, value)
}
}
// GenerateMenuID returns a unique string ID for a menu item
func (m *MenuItemMap) generateMenuID() string {
m.menuIDCounterMutex.Lock()
result := fmt.Sprintf("%d", m.menuIDCounter)
m.menuIDCounter++
m.menuIDCounterMutex.Unlock()
return result
}
func (m *MenuItemMap) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu.Items {
m.processMenuItem(submenuitem)
}
}
// Create a unique ID for this menu item
menuID := m.generateMenuID()
// Store references
m.idToMenuItemMap[menuID] = item
m.menuItemToIDMap[item] = menuID
}
func (m *MenuItemMap) getMenuItemByID(menuId string) *menu.MenuItem {
return m.idToMenuItemMap[menuId]
}

View File

@@ -2,89 +2,53 @@ package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/counter"
"github.com/wailsapp/wails/v2/pkg/menu"
)
type Manager struct {
// The application menu.
applicationMenu *menu.Menu
applicationMenuJSON string
// MenuItemMap is a map of all menu items against a generated ID
menuItemMap map[string]*menu.MenuItem
menuItemIDCounter *counter.Counter
processedMenuItems map[*menu.MenuItem]*ProcessedMenuItem
// Our application menu mappings
applicationMenuItemMap *MenuItemMap
// Menus
menuIDCounter *counter.Counter
// Context menus
contextMenus map[string]*ContextMenu
contextMenuPointers map[*menu.ContextMenu]string
// Tray menu stores
trayMenus map[string]*TrayMenu
trayMenuPointers map[*menu.TrayMenu]string
// Map wails menus to internal menus
trayMenuMap map[*menu.TrayMenu]*TrayMenu
trayMenuIDCounter *counter.Counter
}
func NewManager() *Manager {
return &Manager{
applicationMenuItemMap: NewMenuItemMap(),
contextMenus: make(map[string]*ContextMenu),
contextMenuPointers: make(map[*menu.ContextMenu]string),
trayMenus: make(map[string]*TrayMenu),
trayMenuPointers: make(map[*menu.TrayMenu]string),
trayMenuMap: make(map[*menu.TrayMenu]*TrayMenu),
trayMenuIDCounter: counter.NewCounter(0),
menuIDCounter: counter.NewCounter(0),
menuItemMap: make(map[string]*menu.MenuItem),
menuItemIDCounter: counter.NewCounter(0),
processedMenuItems: make(map[*menu.MenuItem]*ProcessedMenuItem),
}
}
func (m *Manager) getMenuItemByID(menuMap *MenuItemMap, menuId string) *menu.MenuItem {
return menuMap.idToMenuItemMap[menuId]
}
func (m *Manager) ProcessClick(menuID string, data string, menuType string, parentID string) error {
var menuItemMap *MenuItemMap
switch menuType {
case "ApplicationMenu":
menuItemMap = m.applicationMenuItemMap
case "ContextMenu":
contextMenu := m.contextMenus[parentID]
if contextMenu == nil {
return fmt.Errorf("unknown context menu: %s", parentID)
}
menuItemMap = contextMenu.menuItemMap
case "TrayMenu":
trayMenu := m.trayMenus[parentID]
if trayMenu == nil {
return fmt.Errorf("unknown tray menu: %s", parentID)
}
menuItemMap = trayMenu.menuItemMap
default:
return fmt.Errorf("unknown menutype: %s", menuType)
}
// Get the menu item
menuItem := menuItemMap.getMenuItemByID(menuID)
func (m *Manager) ProcessClick(menuID string, data string) error {
// Get item from callback map
menuItem := m.menuItemMap[menuID]
if menuItem == nil {
return fmt.Errorf("Cannot process menuid %s - unknown", menuID)
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
return fmt.Errorf("menuItem doesn't exist")
}
if menuItem.Click == nil {
// No callback
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
return fmt.Errorf("menuItem 'does not have a callback")
}
// Create new Callback struct
callbackData := &menu.CallbackData{
MenuItem: menuItem,
ContextData: data,
}
// Call back!
// Callback!
go menuItem.Click(callbackData)
return nil
}

View File

@@ -2,70 +2,165 @@ package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
type ProcessedMenuItem struct {
ID string
// Label is what appears as the menu text
Label string
// Role is a predefined menu type
Role menu.Role `json:"Role,omitempty"`
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:"Accelerator,omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type
// Disabled makes the item unselectable
Disabled bool
// Hidden ensures that the item is not shown in the menu
Hidden bool
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *ProcessedMenu `json:"SubMenu,omitempty"`
type ProcessedMenu struct {
ID string `json:"I"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red
Foreground int
// Background colour
Background int
Items []*ProcessedMenuItem `json:"i,omitempty"`
RadioGroups []*RadioGroup `json:"r,omitempty"`
}
func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
func (m *Manager) ProcessMenu(menu *menu.Menu) *ProcessedMenu {
wm := m.NewWailsMenu(menu)
if len(wm.Menu) == 0 {
return nil
}
return &ProcessedMenu{
ID: m.generateMenuID(),
Items: wm.Menu,
RadioGroups: wm.RadioGroups,
}
}
type ProcessedMenuItem struct {
// ID of the menu item
ID string `json:"I"`
// Label is what appears as the menu text
Label string `json:"l,omitempty"`
// AlternateLabel is a secondary label (Used by Mac)
AlternateLabel string `json:"L,omitempty"`
// Role is a predefined menu type
Role menu.Role `json:"r,omitempty"`
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:"a,omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type `json:"t,omitempty"`
// Font to use for the menu item
Font string `json:"f,omitempty"`
// Font to use for the menu item
FontSize int `json:"F,omitempty"`
// RGBA is the colour of the menu item
RGBA string `json:"R,omitempty"`
// Image is an image for the menu item (base64 string)
Image string `json:"i,omitempty"`
// Disabled makes the item unselectable
Disabled *bool `json:"d,omitempty"`
// Hidden ensures that the item is not shown in the menu
Hidden *bool `json:"h,omitempty"`
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked *bool `json:"c,omitempty"`
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu []*ProcessedMenuItem `json:"s,omitempty"`
// Indicates if this item has a callback
HasCallback *bool `json:"C,omitempty"`
}
func (m *Manager) generateMenuItemID() string {
return fmt.Sprintf("%d", m.menuItemIDCounter.Increment())
}
func (m *Manager) generateMenuID() string {
return fmt.Sprintf("%d", m.menuIDCounter.Increment())
}
func (m *Manager) NewProcessedMenuItem(menuItem *menu.MenuItem) *ProcessedMenuItem {
// Check if this menu has already been processed.
// This is to prevent duplicates.
existingMenuItem := m.processedMenuItems[menuItem]
if existingMenuItem != nil {
return &ProcessedMenuItem{ID: existingMenuItem.ID}
}
ID := m.generateMenuItemID()
ID := menuItemMap.menuItemToIDMap[menuItem]
result := &ProcessedMenuItem{
ID: ID,
Label: menuItem.Label,
Role: menuItem.Role,
Accelerator: menuItem.Accelerator,
Type: menuItem.Type,
Disabled: menuItem.Disabled,
Hidden: menuItem.Hidden,
Checked: menuItem.Checked,
Foreground: menuItem.Foreground,
Background: menuItem.Background,
ID: ID,
Label: menuItem.Label,
AlternateLabel: menuItem.AlternateLabel,
Role: menuItem.Role,
Accelerator: menuItem.Accelerator,
Type: menuItem.Type,
Font: menuItem.Font,
FontSize: menuItem.FontSize,
RGBA: menuItem.RGBA,
Image: menuItem.Image,
Disabled: nil,
Hidden: nil,
Checked: nil,
SubMenu: nil,
HasCallback: nil,
}
if menuItem.Hidden {
result.Hidden = new(bool)
*result.Hidden = true
}
if menuItem.Disabled {
result.Disabled = new(bool)
*result.Disabled = true
}
if menuItem.Checked {
result.Checked = new(bool)
*result.Checked = true
}
if menuItem.Click != nil {
result.HasCallback = new(bool)
*result.HasCallback = true
}
if menuItem.SubMenu != nil {
result.SubMenu = NewProcessedMenu(menuItemMap, menuItem.SubMenu)
result.SubMenu = m.NewProcessedMenu(menuItem.SubMenu)
}
// Add menu item to item map
m.menuItemMap[ID] = menuItem
// Add processed Item to processedMenuItems
m.processedMenuItems[menuItem] = result
return result
}
type ProcessedMenu struct {
Items []*ProcessedMenuItem
}
func (m *Manager) NewProcessedMenu(menu *menu.Menu) []*ProcessedMenuItem {
func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu {
if menu == nil {
return nil
}
if menu.Items == nil {
return nil
}
var result []*ProcessedMenuItem
result := &ProcessedMenu{}
for _, item := range menu.Items {
processedMenuItem := NewProcessedMenuItem(menuItemMap, item)
result.Items = append(result.Items, processedMenuItem)
processedMenuItem := m.NewProcessedMenuItem(item)
result = append(result, processedMenuItem)
}
return result
@@ -74,8 +169,8 @@ func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu
// WailsMenu is the original menu with the addition
// of radio groups extracted from the menu data
type WailsMenu struct {
Menu *ProcessedMenu
RadioGroups []*RadioGroup
Menu []*ProcessedMenuItem `json:",omitempty"`
RadioGroups []*RadioGroup `json:",omitempty"`
currentRadioGroup []string
}
@@ -85,11 +180,12 @@ type RadioGroup struct {
Length int
}
func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu {
func (m *Manager) NewWailsMenu(menu *menu.Menu) *WailsMenu {
result := &WailsMenu{}
// Process the menus
result.Menu = NewProcessedMenu(menuItemMap, menu)
result.Menu = m.NewProcessedMenu(menu)
// Process the radio groups
result.processRadioGroups()
@@ -106,17 +202,7 @@ func (w *WailsMenu) AsJSON() (string, error) {
return string(menuAsJSON), nil
}
func (w *WailsMenu) processRadioGroups() {
// Loop over top level menus
for _, item := range w.Menu.Items {
// Process MenuItem
w.processMenuItem(item)
}
w.finaliseRadioGroup()
}
func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
func (w *WailsMenu) processMenuItemForRadioGroups(item *ProcessedMenuItem) {
switch item.Type {
@@ -127,8 +213,8 @@ func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
w.finaliseRadioGroup()
// Process each submenu item
for _, subitem := range item.SubMenu.Items {
w.processMenuItem(subitem)
for _, subitem := range item.SubMenu {
w.processMenuItemForRadioGroups(subitem)
}
case menu.RadioType:
// Add the item to the radio group
@@ -138,6 +224,20 @@ func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
}
}
func (w *WailsMenu) processRadioGroups() {
if w.Menu == nil {
return
}
// Loop over top level menus
for _, item := range w.Menu {
// Process MenuItem
w.processMenuItemForRadioGroups(item)
}
w.finaliseRadioGroup()
}
func (w *WailsMenu) finaliseRadioGroup() {
// If we were processing a radio group, fix up the references

View File

@@ -4,27 +4,13 @@ import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"sync"
)
var trayMenuID int
var trayMenuIDMutex sync.Mutex
func generateTrayID() string {
trayMenuIDMutex.Lock()
result := fmt.Sprintf("%d", trayMenuID)
trayMenuID++
trayMenuIDMutex.Unlock()
return result
}
type TrayMenu struct {
ID string
Label string
Icon string
menuItemMap *MenuItemMap
menu *menu.Menu
ProcessedMenu *WailsMenu
ID string `json:"I"`
Label string `json:"l,omitempty"`
Icon string `json:"i,omitempty"`
Menu *ProcessedMenu `json:"m,omitempty"`
}
func (t *TrayMenu) AsJSON() (string, error) {
@@ -35,52 +21,44 @@ func (t *TrayMenu) AsJSON() (string, error) {
return string(data), nil
}
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
func (m *Manager) newTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := &TrayMenu{
Label: trayMenu.Label,
Icon: trayMenu.Icon,
menu: trayMenu.Menu,
menuItemMap: NewMenuItemMap(),
result := TrayMenu{
ID: m.generateTrayID(),
Label: trayMenu.Label,
Icon: trayMenu.Icon,
Menu: m.ProcessMenu(trayMenu.Menu),
}
result.menuItemMap.AddMenu(trayMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return &result
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := m.newTrayMenu(trayMenu)
// Add to map
m.trayMenuMap[trayMenu] = result
return result
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) {
newTrayMenu := NewTrayMenu(trayMenu)
// Hook up a new ID
trayID := generateTrayID()
newTrayMenu.ID = trayID
// Save the references
m.trayMenus[trayID] = newTrayMenu
m.trayMenuPointers[trayMenu] = trayID
func (m *Manager) generateTrayID() string {
return fmt.Sprintf("T%d", m.trayMenuIDCounter.Increment())
}
func (m *Manager) UpdateTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return "", fmt.Errorf("unknown Tray Menu '%s'. Please add the tray menu using AddTrayMenu()", trayMenu.Label)
func (m *Manager) GetTrayMenus() ([]*TrayMenu, error) {
var result []*TrayMenu
for _, trayMenu := range m.trayMenuMap {
result = append(result, trayMenu)
}
// Create the updated tray menu
updatedTrayMenu := NewTrayMenu(trayMenu)
updatedTrayMenu.ID = trayID
// Save the reference
m.trayMenus[trayID] = updatedTrayMenu
return updatedTrayMenu.AsJSON()
return result, nil
}
func (m *Manager) GetTrayMenus() ([]string, error) {
result := []string{}
for _, trayMenu := range m.trayMenus {
func (m *Manager) GetTrayMenusAsJSON() ([]string, error) {
var result []string
for _, trayMenu := range m.trayMenuMap {
JSON, err := trayMenu.AsJSON()
if err != nil {
return nil, err
@@ -91,15 +69,11 @@ func (m *Manager) GetTrayMenus() ([]string, error) {
return result, nil
}
func (m *Manager) GetContextMenus() ([]string, error) {
result := []string{}
for _, contextMenu := range m.contextMenus {
JSON, err := contextMenu.AsJSON()
if err != nil {
return nil, err
}
result = append(result, JSON)
}
return result, nil
// SetTrayMenu updates or creates a menu
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
return "", nil
}
func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
return "", nil
}

View File

@@ -0,0 +1,99 @@
package menumanager
import (
"github.com/matryer/is"
"github.com/wailsapp/wails/v2/pkg/menu"
"testing"
)
func TestManager_AddTrayMenu(t *testing.T) {
is := is.New(t)
simpleLabel := menu.Text("test", nil, nil)
checkbox := menu.Checkbox("test", true, nil, nil)
radioGroup1 := menu.Radio("option 1", true, nil, nil)
radioGroup2 := menu.Radio("option 2", false, nil, nil)
radioGroup3 := menu.Radio("option 3", false, nil, nil)
callback := menu.Text("Preferences", nil, func(_ *menu.CallbackData) {})
empty := &menu.TrayMenu{}
labelOnly := &menu.TrayMenu{Label: "test"}
iconOnly := &menu.TrayMenu{Icon: "svelte"}
iconLabel := &menu.TrayMenu{Icon: "svelte", Label: "test"}
blankLabel := &menu.TrayMenu{Label: ""}
blankMenu := &menu.TrayMenu{Menu: menu.NewMenu()}
menuTextItem := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabel)}
checkboxItem := &menu.TrayMenu{Menu: menu.NewMenuFromItems(checkbox)}
radioGroupItems := &menu.TrayMenu{Menu: menu.NewMenuFromItems(radioGroup1, radioGroup2, radioGroup3)}
callbackItem := &menu.TrayMenu{Menu: menu.NewMenuFromItems(callback)}
tests := []struct {
trayMenu *menu.TrayMenu
want string
}{
{empty, "{\"I\":\"T1\"}"},
{labelOnly, "{\"I\":\"T1\",\"l\":\"test\"}"},
{iconOnly, "{\"I\":\"T1\",\"i\":\"svelte\"}"},
{iconLabel, "{\"I\":\"T1\",\"l\":\"test\",\"i\":\"svelte\"}"},
{blankLabel, "{\"I\":\"T1\"}"},
{blankMenu, "{\"I\":\"T1\"}"},
{menuTextItem, "{\"I\":\"T1\",\"m\":{\"I\":\"1\",\"i\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\"}]}}"},
{checkboxItem, "{\"I\":\"T1\",\"m\":{\"I\":\"1\",\"i\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"c\",\"c\":true}]}}"},
{radioGroupItems, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"option 1\",\"t\":\"r\",\"c\":true},{\"I\":\"2\",\"l\":\"option 2\",\"t\":\"r\"},{\"I\":\"3\",\"l\":\"option 3\",\"t\":\"r\"}],\"r\":[{\"Members\":[\"1\",\"2\",\"3\"],\"Length\":3}]}"},
{callbackItem, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"Preferences\",\"t\":\"t\",\"C\":true}]}"},
}
for _, tt := range tests {
m := NewManager()
got := m.AddTrayMenu(tt.trayMenu)
JSON, err := got.AsJSON()
is.NoErr(err)
is.Equal(JSON, tt.want)
}
}
func TestManager_CallbackMap(t *testing.T) {
is := is.New(t)
simpleLabel := menu.Text("test", nil, nil)
simpleLabelWithCallback := menu.Text("test", nil, func(_ *menu.CallbackData) {})
checkboxWithCallback := menu.Checkbox("test", true, nil, func(_ *menu.CallbackData) {})
submenu := menu.SubMenu("test", menu.NewMenuFromItems(checkboxWithCallback))
blankMenu := &menu.TrayMenu{Menu: menu.NewMenu()}
noCallback := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabel)}
oneMenuWithCallback := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback)}
duplicateCallbacks := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback, simpleLabelWithCallback)}
twoMenusWithCallbacks := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback, checkboxWithCallback)}
duplicateMenusWithCallbacks := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback, checkboxWithCallback, simpleLabelWithCallback, checkboxWithCallback)}
submenuWithCallback := &menu.TrayMenu{Menu: menu.NewMenuFromItems(submenu)}
duplicateSubmenus := &menu.TrayMenu{Menu: menu.NewMenuFromItems(submenu, submenu)}
tests := []struct {
trayMenu *menu.TrayMenu
trays int
menuItems int
JSON string
}{
{blankMenu, 1, 0, "{\"I\":\"T1\"}"},
{noCallback, 1, 1, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\"}]}"},
{oneMenuWithCallback, 1, 1, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true}]}"},
{duplicateCallbacks, 1, 1, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true},{\"I\":\"1\"}]}"},
{twoMenusWithCallbacks, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true},{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true}]}"},
{duplicateMenusWithCallbacks, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true},{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true},{\"I\":\"1\"},{\"I\":\"2\"}]}"},
{submenuWithCallback, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"S\",\"s\":[{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true}]}]}"},
{duplicateSubmenus, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"S\",\"s\":[{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true}]},{\"I\":\"1\"}]}"},
}
for _, test := range tests {
m := NewManager()
tm := m.AddTrayMenu(test.trayMenu)
is.Equal(len(m.trayMenuMap), test.trays)
is.Equal(len(m.menuItemMap), test.menuItems)
JSON, err := tm.AsJSON()
is.NoErr(err)
is.Equal(JSON, test.JSON)
}
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
)
// Client defines what a frontend client can do
@@ -13,9 +13,9 @@ type Client interface {
Quit()
NotifyEvent(message string)
CallResult(message string)
OpenDialog(dialogOptions *options.OpenDialog, callbackID string)
SaveDialog(dialogOptions *options.SaveDialog, callbackID string)
MessageDialog(dialogOptions *options.MessageDialog, callbackID string)
OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string)
SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string)
MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string)
WindowSetTitle(title string)
WindowShow()
WindowHide()
@@ -31,7 +31,8 @@ type Client interface {
WindowSetColour(colour int)
DarkModeEnabled(callbackID string)
SetApplicationMenu(menuJSON string)
UpdateTrayMenu(trayMenuJSON string)
SetTrayMenu(trayMenuJSON string)
UpdateTrayMenuLabel(JSON string)
UpdateContextMenu(contextMenuJSON string)
}

View File

@@ -8,8 +8,8 @@ import (
// systemMessageParser does what it says on the tin!
func systemMessageParser(message string) (*parsedMessage, error) {
// Sanity check: system messages must be at least 4 bytes
if len(message) < 4 {
// Sanity check: system messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("system message was an invalid length")
}
@@ -23,6 +23,9 @@ func systemMessageParser(message string) (*parsedMessage, error) {
// Format of system response messages: S<command><callbackID>|<payload>
// DarkModeEnabled
case 'D':
if len(message) < 4 {
return nil, fmt.Errorf("system message was an invalid length")
}
message = message[1:]
idx := strings.IndexByte(message, '|')
if idx < 0 {
@@ -34,6 +37,10 @@ func systemMessageParser(message string) (*parsedMessage, error) {
topic := "systemresponse:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData == "T"}
// This is our startup hook - the frontend is now ready
case 'S':
topic := "hooks:startup"
responseMessage = &parsedMessage{Topic: topic, Data: nil}
default:
return nil, fmt.Errorf("Invalid message to systemMessageParser()")
}

View File

@@ -2,6 +2,7 @@ package messagedispatcher
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv"
"strings"
"sync"
@@ -10,7 +11,6 @@ import (
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/options"
)
// Dispatcher translates messages received from the frontend
@@ -370,7 +370,7 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
dialogType := splitTopic[2]
switch dialogType {
case "open":
dialogOptions, ok := result.Data().(*options.OpenDialog)
dialogOptions, ok := result.Data().(*dialog.OpenDialog)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:open' : %#v", result.Data())
return
@@ -384,7 +384,7 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
client.frontend.OpenDialog(dialogOptions, callbackID)
}
case "save":
dialogOptions, ok := result.Data().(*options.SaveDialog)
dialogOptions, ok := result.Data().(*dialog.SaveDialog)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:save' : %#v", result.Data())
return
@@ -398,7 +398,7 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
client.frontend.SaveDialog(dialogOptions, callbackID)
}
case "message":
dialogOptions, ok := result.Data().(*options.MessageDialog)
dialogOptions, ok := result.Data().(*dialog.MessageDialog)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:message' : %#v", result.Data())
return
@@ -445,10 +445,10 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
client.frontend.SetApplicationMenu(updatedMenu)
}
case "updatetraymenu":
updatedTrayMenu, ok := result.Data().(string)
case "settraymenu":
trayMenuJSON, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenu' : %#v",
d.logger.Error("Invalid data for 'menufrontend:settraymenu' : %#v",
result.Data())
return
}
@@ -456,8 +456,9 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
// 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.UpdateTrayMenu(updatedTrayMenu)
client.frontend.SetTrayMenu(trayMenuJSON)
}
case "updatecontextmenu":
updatedContextMenu, ok := result.Data().(string)
if !ok {
@@ -472,6 +473,20 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
client.frontend.UpdateContextMenu(updatedContextMenu)
}
case "updatetraymenulabel":
updatedTrayMenuLabel, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
result.Data())
return
}
// 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.UpdateTrayMenuLabel(updatedTrayMenuLabel)
}
default:
d.logger.Error("Unknown menufrontend command: %s", command)
}

View File

@@ -10,8 +10,6 @@ import (
"golang.org/x/tools/go/packages"
)
var internalMethods = slicer.String([]string{"WailsInit", "Wails Shutdown"})
var structCache = make(map[string]*ParsedStruct)
var boundStructs = make(map[string]*ParsedStruct)
var boundMethods = []string{}
@@ -49,7 +47,7 @@ func ParseProject(projectPath string) (BoundStructs, error) {
cfg := &packages.Config{Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo}
pkgs, err := packages.Load(cfg, projectPath)
if err != nil {
fmt.Fprintf(os.Stderr, "load: %v\n", err)
_, _ = fmt.Fprintf(os.Stderr, "load: %v\n", err)
os.Exit(1)
}
if packages.PrintErrors(pkgs) > 0 {
@@ -203,10 +201,6 @@ func ParseProject(projectPath string) (BoundStructs, error) {
// This is a struct pointer method
i, ok := se.X.(*ast.Ident)
if ok {
// We want to ignore Internal functions
if internalMethods.Contains(x.Name.Name) {
continue
}
// If we haven't already found this struct,
// Create a placeholder in the cache
parsedStruct := structCache[i.Name]
@@ -437,4 +431,6 @@ func ParseProject(projectPath string) (BoundStructs, error) {
println()
println("}")
println()
return nil, nil
}

View File

@@ -26,6 +26,7 @@ func NewProcess(logger *clilogger.CLILogger, cmd string, args ...string) *Proces
// Start the process
func (p *Process) Start() error {
p.cmd.
err := p.cmd.Start()
if err != nil {
return err

View File

@@ -5,14 +5,14 @@ import (
"github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/options"
dialogoptions "github.com/wailsapp/wails/v2/pkg/options/dialog"
)
// Dialog defines all Dialog related operations
type Dialog interface {
Open(dialogOptions *options.OpenDialog) []string
Save(dialogOptions *options.SaveDialog) string
Message(dialogOptions *options.MessageDialog) string
Open(dialogOptions *dialogoptions.OpenDialog) []string
Save(dialogOptions *dialogoptions.SaveDialog) string
Message(dialogOptions *dialogoptions.MessageDialog) string
}
// dialog exposes the Dialog interface
@@ -45,7 +45,7 @@ func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
}
// Open prompts the user to select a file
func (r *dialog) Open(dialogOptions *options.OpenDialog) []string {
func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
@@ -70,7 +70,7 @@ func (r *dialog) Open(dialogOptions *options.OpenDialog) []string {
}
// Save prompts the user to select a file
func (r *dialog) Save(dialogOptions *options.SaveDialog) string {
func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
@@ -95,7 +95,7 @@ func (r *dialog) Save(dialogOptions *options.SaveDialog) string {
}
// Message show a message to the user
func (r *dialog) Message(dialogOptions *options.MessageDialog) string {
func (r *dialog) Message(dialogOptions *dialogoptions.MessageDialog) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()

View File

@@ -9,7 +9,8 @@ import (
type Menu interface {
UpdateApplicationMenu()
UpdateContextMenu(contextMenu *menu.ContextMenu)
UpdateTrayMenu(trayMenu *menu.TrayMenu)
SetTrayMenu(trayMenu *menu.TrayMenu)
UpdateTrayMenuLabel(trayMenu *menu.TrayMenu)
}
type menuRuntime struct {
@@ -31,6 +32,10 @@ func (m *menuRuntime) UpdateContextMenu(contextMenu *menu.ContextMenu) {
m.bus.Publish("menu:updatecontextmenu", contextMenu)
}
func (m *menuRuntime) UpdateTrayMenu(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:updatetraymenu", trayMenu)
func (m *menuRuntime) SetTrayMenu(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:settraymenu", trayMenu)
}
func (m *menuRuntime) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:updatetraymenulabel", trayMenu)
}

View File

@@ -44,6 +44,7 @@ func (r *system) IsDarkMode() bool {
systemResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
return false
}
message := "system:isdarkmode:" + uniqueCallback

View File

@@ -5,8 +5,6 @@ import (
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/internal/servicebus"
"os"
"time"
)
// Binding is the Binding subsystem. It manages all service bus messages
@@ -26,13 +24,6 @@ type Binding struct {
runtime *runtime.Runtime
}
func showError(err error) {
// Add a slight delay so log buffer clears
time.Sleep(1 * time.Second)
println("\n\n\n\n\n\n")
println("Fatal Error in WailsInit(): " + err.Error())
}
// NewBinding creates a new binding subsystem. Uses the given bindings db for reference.
func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *binding.Bindings, runtime *runtime.Runtime) (*Binding, error) {
@@ -56,16 +47,6 @@ func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *bin
runtime: runtime,
}
// Call WailsInit methods once the frontend is loaded
runtime.Events.On("wails:loaded", func(...interface{}) {
result.logger.Trace("Calling WailsInit() methods")
err := result.CallWailsInit()
if err != nil {
showError(err)
os.Exit(1)
}
})
return result, nil
}
@@ -94,34 +75,6 @@ func (b *Binding) Start() error {
return nil
}
// CallWailsInit will callback to the registered WailsInit
// methods with the runtime object
func (b *Binding) CallWailsInit() error {
for _, wailsinit := range b.bindings.DB().WailsInitMethods() {
_, err := wailsinit.Call([]interface{}{b.runtime})
if err != nil {
return err
}
}
return nil
}
// CallWailsShutdown will callback to the registered WailsShutdown
// methods with the runtime object
func (b *Binding) CallWailsShutdown() error {
for _, wailsshutdown := range b.bindings.DB().WailsShutdownMethods() {
_, err := wailsshutdown.Call([]interface{}{})
if err != nil {
return err
}
}
return nil
}
func (b *Binding) shutdown() {
err := b.CallWailsShutdown()
if err != nil {
showError(err)
}
b.logger.Trace("Shutdown")
}

View File

@@ -3,6 +3,7 @@ package subsystem
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strings"
"github.com/wailsapp/wails/v2/internal/binding"
@@ -10,7 +11,6 @@ import (
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/options"
)
// Call is the Call subsystem. It manages all service bus messages
@@ -131,7 +131,7 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
darkModeEnabled := c.runtime.System.IsDarkMode()
c.sendResult(darkModeEnabled, payload, clientID)
case "Dialog.Open":
dialogOptions := new(options.OpenDialog)
dialogOptions := new(dialog.OpenDialog)
err := json.Unmarshal(payload.Args[0], dialogOptions)
if err != nil {
c.logger.Error("Error decoding: %s", err)
@@ -139,7 +139,7 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
result := c.runtime.Dialog.Open(dialogOptions)
c.sendResult(result, payload, clientID)
case "Dialog.Save":
dialogOptions := new(options.SaveDialog)
dialogOptions := new(dialog.SaveDialog)
err := json.Unmarshal(payload.Args[0], dialogOptions)
if err != nil {
c.logger.Error("Error decoding: %s", err)
@@ -147,7 +147,7 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
result := c.runtime.Dialog.Save(dialogOptions)
c.sendResult(result, payload, clientID)
case "Dialog.Message":
dialogOptions := new(options.MessageDialog)
dialogOptions := new(dialog.MessageDialog)
err := json.Unmarshal(payload.Args[0], dialogOptions)
if err != nil {
c.logger.Error("Error decoding: %s", err)

View File

@@ -80,9 +80,7 @@ func (m *Menu) Start() error {
type ClickCallbackMessage struct {
MenuItemID string `json:"menuItemID"`
MenuType string `json:"menuType"`
Data string `json:"data"`
ParentID string `json:"parentID"`
}
var callbackData ClickCallbackMessage
@@ -93,7 +91,7 @@ func (m *Menu) Start() error {
return
}
err = m.menuManager.ProcessClick(callbackData.MenuItemID, callbackData.Data, callbackData.MenuType, callbackData.ParentID)
err = m.menuManager.ProcessClick(callbackData.MenuItemID, callbackData.Data)
if err != nil {
m.logger.Trace("%s", err.Error())
}
@@ -120,16 +118,27 @@ func (m *Menu) Start() error {
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatecontextmenu", updatedMenu)
case "updatetraymenu":
case "settraymenu":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
updatedMenu, err := m.menuManager.UpdateTrayMenu(trayMenu)
updatedMenu, err := m.menuManager.SetTrayMenu(trayMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatetraymenu", updatedMenu)
m.bus.Publish("menufrontend:settraymenu", updatedMenu)
case "updatetraymenulabel":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
updatedLabel, err := m.menuManager.UpdateTrayMenuLabel(trayMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatetraymenulabel", updatedLabel)
default:
m.logger.Error("unknown menu message: %+v", menuMessage)

View File

@@ -14,7 +14,13 @@ import (
type Runtime struct {
quitChannel <-chan *servicebus.Message
runtimeChannel <-chan *servicebus.Message
running bool
// The hooks channel allows us to hook into frontend startup
hooksChannel <-chan *servicebus.Message
startupCallback func(*runtime.Runtime)
shutdownCallback func()
running bool
logger logger.CustomLogger
@@ -23,7 +29,7 @@ type Runtime struct {
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, error) {
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(*runtime.Runtime), shutdownCallback func()) (*Runtime, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
@@ -37,11 +43,20 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, er
return nil, err
}
// Subscribe to log messages
hooksChannel, err := bus.Subscribe("hooks:")
if err != nil {
return nil, err
}
result := &Runtime{
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus),
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
hooksChannel: hooksChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus),
startupCallback: startupCallback,
shutdownCallback: shutdownCallback,
}
return result, nil
@@ -59,6 +74,21 @@ func (r *Runtime) Start() error {
case <-r.quitChannel:
r.running = false
break
case hooksMessage := <-r.hooksChannel:
r.logger.Trace(fmt.Sprintf("Received hooksmessage: %+v", hooksMessage))
messageSlice := strings.Split(hooksMessage.Topic(), ":")
hook := messageSlice[1]
switch hook {
case "startup":
if r.startupCallback != nil {
go r.startupCallback(r.runtime)
} else {
r.logger.Error("no startup callback registered!")
}
default:
r.logger.Error("unknown hook message: %+v", hooksMessage)
continue
}
case runtimeMessage := <-r.runtimeChannel:
r.logger.Trace(fmt.Sprintf("Received message: %+v", runtimeMessage))
// Topics have the format: "runtime:category:call"
@@ -99,6 +129,9 @@ func (r *Runtime) GoRuntime() *runtime.Runtime {
}
func (r *Runtime) shutdown() {
if r.shutdownCallback != nil {
go r.shutdownCallback()
}
r.logger.Trace("Shutdown")
}

View File

@@ -3,7 +3,7 @@ package webserver
import (
"context"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"net/http"
"strings"
@@ -20,11 +20,27 @@ type WebClient struct {
running bool
}
func (wc *WebClient) OpenDialog(dialogOptions *options.OpenDialog, callbackID string) {
func (wc *WebClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) SaveDialog(dialogOptions *options.SaveDialog, callbackID string) {
func (wc *WebClient) SetApplicationMenu(menuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) UpdateTrayMenu(trayMenuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) UpdateContextMenu(contextMenuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
wc.logger.Info("Not implemented in server build")
}
@@ -84,10 +100,6 @@ func (wc *WebClient) UpdateTrayIcon(name string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) UpdateContextMenus(contextMenus *menu.ContextMenus) {
wc.logger.Info("Not implemented in server build")
}
// Quit terminates the webclient session
func (wc *WebClient) Quit() {
wc.running = false

View File

@@ -63,6 +63,14 @@ func (d *DesktopBuilder) BuildBaseAssets(assets *html.AssetBundle, options *Opti
return err
}
// Make dir if it doesn't exist
if !fs.DirExists(assetDir) {
err := fs.Mkdir(assetDir)
if err != nil {
return err
}
}
// Dump assets as C
assetsFile, err := assets.WriteToCFile(assetDir)
if err != nil {

View File

@@ -12,6 +12,14 @@ func (m *Menu) Append(item *MenuItem) {
m.Items = append(m.Items, item)
}
// Merge will append the items in the given menu
// into this menu
func (m *Menu) Merge(menu *Menu) {
for _, item := range menu.Items {
m.Items = append(m.Items, item)
}
}
func (m *Menu) Prepend(item *MenuItem) {
m.Items = append([]*MenuItem{item}, m.Items...)
}

View File

@@ -9,6 +9,8 @@ import (
type MenuItem struct {
// Label is what appears as the menu text
Label string
// AlternateLabel is a secondary label (Used by Mac)
AlternateLabel string
// Role is a predefined menu type
Role Role `json:"Role,omitempty"`
// Accelerator holds a representation of a key binding
@@ -21,6 +23,14 @@ type MenuItem struct {
Hidden bool
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool
// Font to use for the menu item
Font string
// Font to use for the menu item
FontSize int
// RGBA is the colour of the menu item
RGBA string
// Image is an image for the menu item (base64 string)
Image string
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *Menu `json:"SubMenu,omitempty"`

View File

@@ -5,13 +5,13 @@ type Type string
const (
// TextType is the text menuitem type
TextType Type = "Text"
TextType Type = "t"
// SeparatorType is the Separator menuitem type
SeparatorType Type = "Separator"
SeparatorType Type = "s"
// SubmenuType is the Submenu menuitem type
SubmenuType Type = "Submenu"
SubmenuType Type = "S"
// CheckboxType is the Checkbox menuitem type
CheckboxType Type = "Checkbox"
CheckboxType Type = "c"
// RadioType is the Radio menuitem type
RadioType Type = "Radio"
RadioType Type = "r"
)

View File

@@ -13,10 +13,8 @@ var Default = &App{
DevTools: false,
RGBA: 0xFFFFFFFF,
Mac: &mac.Options{
TitleBar: mac.TitleBarDefault(),
Appearance: mac.DefaultAppearance,
WebviewIsTransparent: false,
WindowBackgroundIsTranslucent: false,
TitleBar: mac.TitleBarDefault(),
Appearance: mac.DefaultAppearance,
},
Logger: logger.NewDefaultLogger(),
LogLevel: logger.INFO,

View File

@@ -1,4 +1,4 @@
package options
package dialog
// OpenDialog contains the options for the OpenDialog runtime method
type OpenDialog struct {

View File

@@ -1,6 +1,7 @@
package options
import (
wailsruntime "github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/pkg/menu"
"log"
"runtime"
@@ -30,6 +31,8 @@ type App struct {
Mac *mac.Options
Logger logger.Logger `json:"-"`
LogLevel logger.LogLevel
Startup func(*wailsruntime.Runtime) `json:"-"`
Shutdown func() `json:"-"`
}
// MergeDefaults will set the minimum default values for an application

View File

@@ -44,11 +44,6 @@ func (p *Parser) parseStructMethods(boundStruct *Struct) error {
continue
}
// We want to ignore Internal functions
if funcDecl.Name.Name == "WailsInit" || funcDecl.Name.Name == "WailsShutdown" {
continue
}
// If this method is not Public, ignore
if string(funcDecl.Name.Name[0]) != strings.ToUpper((string(funcDecl.Name.Name[0]))) {
continue

10
v2/pkg/str/str.go Normal file
View File

@@ -0,0 +1,10 @@
package str
import (
"fmt"
"time"
)
func UnixNow() string {
return fmt.Sprintf("%+v", time.Now().Unix())
}

View File

@@ -2,7 +2,7 @@ package main
import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
)
// Dialog struct
@@ -18,16 +18,16 @@ func (l *Dialog) WailsInit(runtime *wails.Runtime) error {
}
// Open Dialog
func (l *Dialog) Open(options *options.OpenDialog) []string {
func (l *Dialog) Open(options *dialog.OpenDialog) []string {
return l.runtime.Dialog.Open(options)
}
// Save Dialog
func (l *Dialog) Save(options *options.SaveDialog) string {
func (l *Dialog) Save(options *dialog.SaveDialog) string {
return l.runtime.Dialog.Save(options)
}
// Message Dialog
func (l *Dialog) Message(options *options.MessageDialog) string {
func (l *Dialog) Message(options *dialog.MessageDialog) string {
return l.runtime.Dialog.Message(options)
}

View File

@@ -1,10 +1,10 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"io/ioutil"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
)
type Notepad struct {
@@ -21,7 +21,7 @@ func (n *Notepad) WailsInit(runtime *wails.Runtime) error {
// successful save.
func (n *Notepad) SaveNotes(notes string) (bool, error) {
selectedFile := n.runtime.Dialog.Save(&options.SaveDialog{
selectedFile := n.runtime.Dialog.Save(&dialog.SaveDialog{
DefaultFilename: "notes.md",
Filters: "*.md",
})

View File

@@ -1,10 +1,10 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"io/ioutil"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
)
type Notepad struct {
@@ -18,7 +18,7 @@ func (n *Notepad) WailsInit(runtime *wails.Runtime) error {
func (n *Notepad) LoadNotes() (string, error) {
selectedFiles := n.runtime.Dialog.Open(&options.OpenDialog{
selectedFiles := n.runtime.Dialog.Open(&dialog.OpenDialog{
DefaultFilename: "notes.md",
Filters: "*.md",
AllowFiles: true,

View File

@@ -1,10 +1,10 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"io/ioutil"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
)
type Notepad struct {
@@ -21,7 +21,7 @@ func (n *Notepad) WailsInit(runtime *wails.Runtime) error {
// successful save.
func (n *Notepad) SaveNotes(notes string) (bool, error) {
selectedFile := n.runtime.Dialog.Save(&options.SaveDialog{
selectedFile := n.runtime.Dialog.Save(&dialog.SaveDialog{
DefaultFilename: "notes.md",
Filters: "*.md",
})

View File

@@ -2,8 +2,6 @@ module test
go 1.13
require (
github.com/wailsapp/wails/v2 v2.0.0-alpha
)
require github.com/wailsapp/wails/v2 v2.0.0-alpha
replace github.com/wailsapp/wails/v2 v2.0.0-alpha => ../../../v2

View File

@@ -1,112 +1,90 @@
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/colors v1.2.0/go.mod h1:miw1R2JIE19cclPxsXqNdzLZsk4DP4iF+m88bRc7kfM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU=
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/gosod v0.0.4/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU=
github.com/leaanthony/mewn v0.10.7/go.mod h1:CRkTx8unLiSSilu/Sd7i1LwrdaAL+3eQ3ses99qGMEQ=
github.com/leaanthony/slicer v1.4.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/slicer v1.4.1/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/spinner v0.5.3/go.mod h1:oHlrvWicr++CVV7ALWYi+qHk/XNA91D9IJ48IqmpVUo=
github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs=
github.com/leaanthony/wincursor v0.1.0/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba/go.mod h1:iLnlXG2Pakcii2CU0cbY07DRCSvpWNa7nFxtevhOChk=
github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
github.com/tdewolff/minify/v2 v2.9.5/go.mod h1:jshtBj/uUJH6JX1fuxTLnnHOA1RVJhF5MM+leJzDKb4=
github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
github.com/tdewolff/parse/v2 v2.5.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/wailsapp/wails v1.8.0 h1:gnQhpwoGM8s2GD5PZrgMKU1PO3pQ9cdKKJgwtkNz2f4=
github.com/wailsapp/wails v1.8.0/go.mod h1:XFZunea+USOCMMgBlz0A0JHLL3oWrRhnOl4baZlRpxo=
github.com/wailsapp/wails v1.9.1 h1:ez/TK8YpU9lvOZ9nkgzUXsWu+xOPFVO57zTy0n5w3hc=
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -123,12 +101,14 @@ golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82/go.mod h1:Cj7w3i3Rnn0Xh82u
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/AlecAivazis/survey.v1 v1.8.4/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

View File

@@ -34,6 +34,8 @@ func main() {
TrayMenus: Tray.createTrayMenus(),
},
LogLevel: logger.TRACE,
Startup: Tray.start,
Shutdown: Tray.shutdown,
})
if err != nil {

View File

@@ -21,8 +21,8 @@ type Tray struct {
done bool
}
// WailsInit is called at application startup
func (t *Tray) WailsInit(runtime *wails.Runtime) error {
// This is called at application startup
func (t *Tray) start(runtime *wails.Runtime) {
// Perform your setup here
t.runtime = runtime
@@ -36,7 +36,6 @@ func (t *Tray) WailsInit(runtime *wails.Runtime) error {
// t.runtime.Tray.SetIcon("dark")
//})
return nil
}
func (t *Tray) showWindow(_ *menu.CallbackData) {
@@ -55,7 +54,7 @@ func (t *Tray) minimiseWindow(_ *menu.CallbackData) {
t.runtime.Window.Minimise()
}
func (t *Tray) WailsShutdown() {
func (t *Tray) shutdown() {
t.done = true
}
@@ -71,19 +70,19 @@ func (t *Tray) decrementcounter() int {
func (t *Tray) SvelteIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = "svelte"
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
t.runtime.Menu.SetTrayMenu(t.secondTrayMenu)
}
func (t *Tray) NoIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = ""
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
t.runtime.Menu.SetTrayMenu(t.secondTrayMenu)
}
func (t *Tray) LightIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = "light"
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
t.runtime.Menu.SetTrayMenu(t.secondTrayMenu)
}
func (t *Tray) DarkIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = "dark"
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
t.runtime.Menu.SetTrayMenu(t.secondTrayMenu)
}
//func (t *Tray) removeMenu(_ *menu.MenuItem) {
@@ -146,7 +145,7 @@ func (t *Tray) createTrayMenus() []*menu.TrayMenu {
counter := t.incrementcounter()
trayLabel := "Updated Label " + strconv.Itoa(counter)
secondTrayMenu.Label = trayLabel
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
t.runtime.Menu.SetTrayMenu(t.secondTrayMenu)
}),
menu.SubMenu("Select Icon", menu.NewMenuFromItems(
menu.Text("Svelte", nil, t.SvelteIcon),

View File

@@ -1,10 +1,10 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"time"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
)
// RuntimeTest to test the runtimes
@@ -75,7 +75,7 @@ func (r *RuntimeTest) SetColour(colour int) {
// OpenFileDialog will call the Runtime.Dialog.OpenDialog method requesting File selection
func (r *RuntimeTest) OpenFileDialog(title string, filter string) []string {
dialogOptions := &options.OpenDialog{
dialogOptions := &dialog.OpenDialog{
Title: title,
Filters: filter,
AllowFiles: true,
@@ -85,7 +85,7 @@ func (r *RuntimeTest) OpenFileDialog(title string, filter string) []string {
// OpenDirectoryDialog will call the Runtime.Dialog.OpenDialog method requesting File selection
func (r *RuntimeTest) OpenDirectoryDialog(title string, filter string) []string {
dialogOptions := &options.OpenDialog{
dialogOptions := &dialog.OpenDialog{
Title: title,
Filters: filter,
AllowDirectories: true,
@@ -95,7 +95,7 @@ func (r *RuntimeTest) OpenDirectoryDialog(title string, filter string) []string
// OpenDialog will call the Runtime.Dialog.OpenDialog method requesting both Files and Directories
func (r *RuntimeTest) OpenDialog(title string, filter string) []string {
dialogOptions := &options.OpenDialog{
dialogOptions := &dialog.OpenDialog{
Title: title,
Filters: filter,
AllowDirectories: true,
@@ -106,7 +106,7 @@ func (r *RuntimeTest) OpenDialog(title string, filter string) []string {
// OpenDialogMultiple will call the Runtime.Dialog.OpenDialog method allowing multiple selection
func (r *RuntimeTest) OpenDialogMultiple(title string, filter string) []string {
dialogOptions := &options.OpenDialog{
dialogOptions := &dialog.OpenDialog{
Title: title,
Filters: filter,
AllowDirectories: true,
@@ -118,7 +118,7 @@ func (r *RuntimeTest) OpenDialogMultiple(title string, filter string) []string {
// OpenDialogAllOptions will call the Runtime.Dialog.OpenDialog method allowing multiple selection
func (r *RuntimeTest) OpenDialogAllOptions(filter string, defaultDir string, defaultFilename string) []string {
dialogOptions := &options.OpenDialog{
dialogOptions := &dialog.OpenDialog{
DefaultDirectory: defaultDir,
DefaultFilename: defaultFilename,
Filters: filter,
@@ -134,7 +134,7 @@ func (r *RuntimeTest) OpenDialogAllOptions(filter string, defaultDir string, def
// SaveFileDialog will call the Runtime.Dialog.SaveDialog method requesting a File selection
func (r *RuntimeTest) SaveFileDialog(title string, filter string) string {
dialogOptions := &options.SaveDialog{
dialogOptions := &dialog.SaveDialog{
Title: title,
Filters: filter,
}
@@ -143,7 +143,7 @@ func (r *RuntimeTest) SaveFileDialog(title string, filter string) string {
// SaveDialogAllOptions will call the Runtime.Dialog.SaveDialog method allowing multiple selection
func (r *RuntimeTest) SaveDialogAllOptions(filter string, defaultDir string, defaultFilename string) string {
dialogOptions := &options.SaveDialog{
dialogOptions := &dialog.SaveDialog{
DefaultDirectory: defaultDir,
DefaultFilename: defaultFilename,
Filters: filter,