5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 02:19:43 +08:00

Chore/tidy up (#1713)

* Tidy up dev mode output

* Rename `appng` package to `app`

* Remove `crypto`,`parse`,`messagedispatcher`,`servicebus`,`runtime`,`subsystem`,`ffenestri` packages and `.vscode` dir

* Remove misc `fs` functions

* Remove `str` package

* Fix redundant alias
This commit is contained in:
Lea Anthony 2022-08-09 07:42:11 +10:00 committed by GitHub
parent c13c4a29f5
commit 57c9158551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 19 additions and 26894 deletions

3
.gitignore vendored
View File

@ -27,8 +27,5 @@ v2/pkg/parser/testproject/frontend/wails
v2/test/kitchensink/frontend/public
v2/test/kitchensink/build/darwin/desktop/kitchensink
v2/test/kitchensink/frontend/package.json.md5
/v2/internal/ffenestri/windows/test/cmake-build-debug/
!v2/internal/ffenestri/windows/x64/webview2.dll
!v2/internal/ffenestri/windows/x64/WebView2Loader.dll
.idea/
v2/cmd/wails/internal/commands/initialise/templates/testtemplates/

48
.vscode/launch.json vendored
View File

@ -1,48 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Test cmd package",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/cmd/"
},
{
"name": "Wails Init",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/wails/main.go",
"env": {},
"cwd": "/tmp",
"args": [
"init",
"-name",
"runtime",
"-dir",
"runtime",
"-output",
"runtime",
"-template",
"vuebasic"
]
},
{
"name": "Wails Update Pre",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/wails/main.go",
"env": {},
"cwd": "/tmp",
"args": [
"update",
"-pre"
]
}
]
}

View File

@ -1,8 +0,0 @@
{
"go.formatTool": "goimports",
"eslint.alwaysShowStatus": true,
"files.associations": {
"__locale": "c",
"ios": "c"
}
}

View File

@ -485,13 +485,11 @@ func runFrontendDevWatcherCommand(cwd string, devCommand string, discoverViteSer
}
}
atomic.StoreInt32(&state, stateStopped)
LogGreen("DevWatcher command exited!")
wg.Done()
}()
return func() {
if atomic.CompareAndSwapInt32(&state, stateRunning, stateCanceling) {
LogGreen("Killing DevWatcher command...")
killProc(cmd, devCommand)
}
cancel()

View File

@ -4,7 +4,7 @@
package dev
import (
"os"
"bytes"
"os/exec"
"strconv"
)
@ -16,11 +16,15 @@ func killProc(cmd *exec.Cmd, devCommand string) {
// For whatever reason, killing an npm script on windows just doesn't exit properly with cancel
if cmd != nil && cmd.Process != nil {
kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
kill.Stderr = os.Stderr
kill.Stdout = os.Stdout
var errorBuffer bytes.Buffer
var stdoutBuffer bytes.Buffer
kill.Stderr = &errorBuffer
kill.Stdout = &stdoutBuffer
err := kill.Run()
if err != nil {
if err.Error() != "exit status 1" {
println(stdoutBuffer.String())
println(errorBuffer.String())
LogRed("Error from '%s': %s", devCommand, err.Error())
}
}

View File

@ -1,7 +1,7 @@
//go:build bindings
// +build bindings
package appng
package app
import (
"os"

View File

@ -1,7 +1,7 @@
//go:build darwin && !bindings
// +build darwin,!bindings
package appng
package app
import (
"github.com/wailsapp/wails/v2/internal/logger"

View File

@ -1,7 +1,7 @@
//go:build debug
// +build debug
package appng
package app
func IsDebug() bool {
return true

View File

@ -1,7 +1,7 @@
//go:build !dev && !production && !bindings && darwin
// +build !dev,!production,!bindings,darwin
package appng
package app
import (
"fmt"

View File

@ -1,7 +1,7 @@
//go:build !dev && !production && !bindings && linux
// +build !dev,!production,!bindings,linux
package appng
package app
import (
"fmt"

View File

@ -1,7 +1,7 @@
//go:build !dev && !production && !bindings && windows
// +build !dev,!production,!bindings,windows
package appng
package app
import (
"os/exec"

View File

@ -1,7 +1,7 @@
//go:build dev
// +build dev
package appng
package app
import (
"context"

View File

@ -1,7 +1,7 @@
//go:build linux && !bindings
// +build linux,!bindings
package appng
package app
import (
"github.com/wailsapp/wails/v2/internal/logger"

View File

@ -1,7 +1,7 @@
//go:build !debug
// +build !debug
package appng
package app
func IsDebug() bool {
return false

View File

@ -1,7 +1,7 @@
//go:build production
// +build production
package appng
package app
import (
"context"

View File

@ -1,7 +1,7 @@
//go:build windows && !bindings
// +build windows,!bindings
package appng
package app
import (
"github.com/wailsapp/wails/v2/internal/logger"

View File

@ -1,17 +0,0 @@
package crypto
import (
"crypto/rand"
"fmt"
"log"
)
// RandomID returns a random ID as a string
func RandomID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Fatal(err)
}
return fmt.Sprintf("%x", b)
}

View File

@ -1,19 +0,0 @@
# 3rd Party Licenses
## Webview
Whilst not using the library directly, there is certainly some code that is inspired by or used from the webview library.
Homepage: https://github.com/webview/webview
License: https://github.com/webview/webview/blob/master/LICENSE
## vec
Homepage: https://github.com/rxi/vec
License: https://github.com/rxi/vec/blob/master/LICENSE
## json
Homepage: http://git.ozlabs.org/?p=ccan;a=tree;f=ccan/json;hb=HEAD
License: http://git.ozlabs.org/?p=ccan;a=blob;f=licenses/BSD-MIT;h=89de354795ec7a7cdab07c091029653d3618540d;hb=HEAD
## hashmap
Homepage: https://github.com/sheredom/hashmap.h
License: https://github.com/sheredom/hashmap.h/blob/master/LICENSE

View File

@ -1,7 +0,0 @@
## Windows
- Left and Right Win keys act the same
- Accelerators will automatically add appropriate text into the menu items. This can be prevented by adding a tab
character to the menu label
- Tooltips + styling with font currently unsupported
-

View File

@ -1,98 +0,0 @@
// +build !windows
//
// Created by Lea Anthony on 6/1/21.
//
#include "common.h"
// Credit: https://stackoverflow.com/a/8465083
char* concat(const char *string1, const char *string2)
{
const size_t len1 = strlen(string1);
const size_t len2 = strlen(string2);
char *result = malloc(len1 + len2 + 1);
strcpy(result, string1);
memcpy(result + len1, string2, len2 + 1);
return result;
}
// 10k is more than enough for a log message
#define MAXMESSAGE 1024*10
char abortbuffer[MAXMESSAGE];
void ABORT(const char *message, ...) {
const char *temp = concat("FATAL: ", message);
va_list args;
va_start(args, message);
vsnprintf(abortbuffer, MAXMESSAGE, temp, args);
printf("%s\n", &abortbuffer[0]);
MEMFREE(temp);
va_end(args);
exit(1);
}
int freeHashmapItem(void *const context, struct hashmap_element_s *const e) {
free(e->data);
return -1;
}
const char* getJSONString(JsonNode *item, const char* key) {
// Get key
JsonNode *node = json_find_member(item, key);
const char *result = "";
if ( node != NULL && node->tag == JSON_STRING) {
result = node->string_;
}
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);
}
return result;
}
JsonNode* mustJSONObject(JsonNode *node, const char* key) {
struct JsonNode* result = getJSONObject(node, key);
if ( result == NULL ) {
ABORT_JSON(node, key);
}
return result;
}
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;
}
return false;
}
bool getJSONInt(JsonNode *item, const char* key, int *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_NUMBER) {
*result = (int) node->number_;
return true;
}
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

@ -1,36 +0,0 @@
//
// Created by Lea Anthony on 6/1/21.
//
#ifndef COMMON_H
#define COMMON_H
#include <stdio.h>
#include <stdarg.h>
#include "string.h"
#include "hashmap.h"
#include "vec.h"
#include "json.h"
#define STREQ(a,b) strcmp(a, b) == 0
#define STREMPTY(string) strlen(string) == 0
#define STRCOPY(a) concat(a, "")
#define STR_HAS_CHARS(input) input != NULL && strlen(input) > 0
#define MEMFREE(input) free((void*)input); input = NULL;
#define FREE_AND_SET(variable, value) if( variable != NULL ) { MEMFREE(variable); } variable = 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);
const char* getJSONString(JsonNode *item, const char* key);
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 getJSONInt(JsonNode *item, const char* key, int *result);
JsonNode* mustParseJSON(const char* JSON);
#endif //ASSETS_C_COMMON_H

View File

@ -1,99 +0,0 @@
////
//// 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_reg(mainWindow, s("contentView"));
// Get the triggering event
id menuEvent = msg_reg(mainWindow, s("currentEvent"));
if( contextMenu->nsmenu == NULL ) {
// GetMenu creates the NSMenu
contextMenu->nsmenu = GetMenu(contextMenu->menu);
}
// Show popup
((id(*)(id, SEL, id, id, id))objc_msgSend)(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
}

View File

@ -1,33 +0,0 @@
////
//// Created by Lea Anthony on 6/1/21.
////
//
#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

View File

@ -1,65 +0,0 @@
#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 +0,0 @@
//
// Created by Lea Anthony on 7/1/21.
//
#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

File diff suppressed because one or more lines are too long

View File

@ -1,64 +0,0 @@
// Credit: https://gist.github.com/ysc3839/b08d2bff1c7dacde529bed1d37e85ccf
#pragma once
typedef enum _WINDOWCOMPOSITIONATTRIB
{
WCA_UNDEFINED = 0,
WCA_NCRENDERING_ENABLED = 1,
WCA_NCRENDERING_POLICY = 2,
WCA_TRANSITIONS_FORCEDISABLED = 3,
WCA_ALLOW_NCPAINT = 4,
WCA_CAPTION_BUTTON_BOUNDS = 5,
WCA_NONCLIENT_RTL_LAYOUT = 6,
WCA_FORCE_ICONIC_REPRESENTATION = 7,
WCA_EXTENDED_FRAME_BOUNDS = 8,
WCA_HAS_ICONIC_BITMAP = 9,
WCA_THEME_ATTRIBUTES = 10,
WCA_NCRENDERING_EXILED = 11,
WCA_NCADORNMENTINFO = 12,
WCA_EXCLUDED_FROM_LIVEPREVIEW = 13,
WCA_VIDEO_OVERLAY_ACTIVE = 14,
WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15,
WCA_DISALLOW_PEEK = 16,
WCA_CLOAK = 17,
WCA_CLOAKED = 18,
WCA_ACCENT_POLICY = 19,
WCA_FREEZE_REPRESENTATION = 20,
WCA_EVER_UNCLOAKED = 21,
WCA_VISUAL_OWNER = 22,
WCA_HOLOGRAPHIC = 23,
WCA_EXCLUDED_FROM_DDA = 24,
WCA_PASSIVEUPDATEMODE = 25,
WCA_USEDARKMODECOLORS = 26,
WCA_LAST = 27
} WINDOWCOMPOSITIONATTRIB;
typedef struct _WINDOWCOMPOSITIONATTRIBDATA
{
WINDOWCOMPOSITIONATTRIB Attrib;
PVOID pvData;
SIZE_T cbData;
} WINDOWCOMPOSITIONATTRIBDATA;
typedef enum _ACCENT_STATE
{
ACCENT_DISABLED = 0,
ACCENT_ENABLE_GRADIENT = 1,
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
ACCENT_ENABLE_BLURBEHIND = 3,
ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, // RS4 1803
ACCENT_ENABLE_HOSTBACKDROP = 5, // RS5 1809
ACCENT_INVALID_STATE = 6
} ACCENT_STATE;
typedef struct _ACCENT_POLICY
{
ACCENT_STATE AccentState;
DWORD AccentFlags;
DWORD GradientColor;
DWORD AnimationId;
} ACCENT_POLICY;
typedef BOOL (WINAPI *pfnGetWindowCompositionAttribute)(HWND, WINDOWCOMPOSITIONATTRIBDATA*);
typedef BOOL (WINAPI *pfnSetWindowCompositionAttribute)(HWND, WINDOWCOMPOSITIONATTRIBDATA*);

View File

@ -1,178 +0,0 @@
package ffenestri
import (
"runtime"
"strings"
"unsafe"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/pkg/options"
)
/*
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
#cgo darwin LDFLAGS: -framework WebKit -lobjc
#cgo windows CXXFLAGS: -std=c++11
#cgo windows,amd64 LDFLAGS: -L./windows/x64 -lWebView2Loader -lgdi32 -lole32 -lShlwapi -luser32 -loleaut32 -ldwmapi
#include <stdlib.h>
#include "ffenestri.h"
*/
import "C"
// Application is our main application object
type Application struct {
config *options.App
memory []unsafe.Pointer
// This is the main app pointer
app *C.struct_Application
// Manages menus
menuManager *menumanager.Manager
// Logger
logger logger.CustomLogger
}
func (a *Application) saveMemoryReference(mem unsafe.Pointer) {
a.memory = append(a.memory, mem)
}
func (a *Application) string2CString(str string) *C.char {
result := C.CString(str)
a.saveMemoryReference(unsafe.Pointer(result))
return result
}
func init() {
runtime.LockOSThread()
}
// NewApplicationWithConfig creates a new application based on the given config
func NewApplicationWithConfig(config *options.App, logger *logger.Logger, menuManager *menumanager.Manager) *Application {
return &Application{
config: config,
logger: logger.CustomLogger("Ffenestri"),
menuManager: menuManager,
}
}
func (a *Application) freeMemory() {
for _, mem := range a.memory {
// fmt.Printf("Freeing memory: %+v\n", mem)
C.free(mem)
}
}
// bool2Cint converts a Go boolean to a C integer
func (a *Application) bool2Cint(value bool) C.int {
if value {
return C.int(1)
}
return C.int(0)
}
// dispatcher is the interface to send messages to
var dispatcher *messagedispatcher.DispatchClient
// Dispatcher is what we register out client with
type Dispatcher interface {
RegisterClient(client messagedispatcher.Client) *messagedispatcher.DispatchClient
}
// DispatchClient is the means for passing messages to the backend
type DispatchClient interface {
SendMessage(string)
}
func intToColour(colour int) (C.int, C.int, C.int, C.int) {
var alpha = C.int(colour & 0xFF)
var blue = C.int((colour >> 8) & 0xFF)
var green = C.int((colour >> 16) & 0xFF)
var red = C.int((colour >> 24) & 0xFF)
return red, green, blue, alpha
}
// Run the application
func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug bool) error {
title := a.string2CString(a.config.Title)
width := C.int(a.config.Width)
height := C.int(a.config.Height)
resizable := a.bool2Cint(!a.config.DisableResize)
devtools := a.bool2Cint(a.config.DevTools)
fullscreen := a.bool2Cint(a.config.Fullscreen)
startHidden := a.bool2Cint(a.config.StartHidden)
logLevel := C.int(a.config.LogLevel)
hideWindowOnClose := a.bool2Cint(a.config.HideWindowOnClose)
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel, hideWindowOnClose)
// Save app reference
a.app = (*C.struct_Application)(app)
// Set Min Window Size
minWidth := C.int(a.config.MinWidth)
minHeight := C.int(a.config.MinHeight)
C.SetMinWindowSize(a.app, minWidth, minHeight)
// Set Max Window Size
maxWidth := C.int(a.config.MaxWidth)
maxHeight := C.int(a.config.MaxHeight)
C.SetMaxWindowSize(a.app, maxWidth, maxHeight)
// Set debug if needed
C.SetDebug(app, a.bool2Cint(debug))
if a.config.Frameless {
C.DisableFrame(a.app)
}
if a.config.RGBA != 0 {
r, g, b, alpha := intToColour(a.config.RGBA)
C.SetColour(a.app, r, g, b, alpha)
}
// Escape bindings so C doesn't freak out
bindings = strings.ReplaceAll(bindings, `"`, `\"`)
// Set bindings
C.SetBindings(app, a.string2CString(bindings))
// save the dispatcher in a package variable so that the C callbacks
// can access it
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
// Process platform settings
err := a.processPlatformSettings()
if err != nil {
return err
}
// Check we could initialise the application
if app != nil {
// Yes - Save memory reference and run app, cleaning up afterwards
a.saveMemoryReference(unsafe.Pointer(app))
C.Run(app, 0, nil)
} else {
// Oh no! We couldn't initialise the application
a.logger.Fatal("Cannot initialise Application.")
}
//println("\n\n\n\n\n\nhererererer\n\n\n\n")
a.freeMemory()
return nil
}
// messageFromWindowCallback is called by any messages sent in
// webkit to window.external.invoke. It relays the message on to
// the dispatcher.
//export messageFromWindowCallback
func messageFromWindowCallback(data *C.char) {
dispatcher.DispatchMessage(C.GoString(data))
}

View File

@ -1,56 +0,0 @@
#ifndef __FFENESTRI_H__
#define __FFENESTRI_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
struct Application;
extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose);
extern void SetMinWindowSize(struct Application*, int minWidth, int minHeight);
extern void SetMaxWindowSize(struct Application*, int maxWidth, int maxHeight);
extern void Run(struct Application*, int argc, char **argv);
extern void DestroyApplication(struct Application*);
extern void SetDebug(struct Application*, int flag);
extern void SetBindings(struct Application*, const char *bindings);
extern void ExecJS(struct Application*, const char *script);
extern void Hide(struct Application*);
extern void Show(struct Application*);
extern void Center(struct Application*);
extern void Maximise(struct Application*);
extern void Unmaximise(struct Application*);
extern void ToggleMaximise(struct Application*);
extern void Minimise(struct Application*);
extern void Unminimise(struct Application*);
extern void ToggleMinimise(struct Application*);
extern void SetColour(struct Application*, int red, int green, int blue, int alpha);
extern void SetSize(struct Application*, int width, int height);
extern void SetPosition(struct Application*, int x, int y);
extern void Quit(struct Application*);
extern void SetTitle(struct Application*, const char *title);
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 SetApplicationMenu(struct Application*, const char *);
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
extern void SetTrayMenu(struct Application*, const char *menuTrayJSON);
extern void DeleteTrayMenuByID(struct Application*, const char *id);
extern void UpdateTrayMenuLabel(struct Application*, const char* JSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);
extern void WebviewIsTransparent(struct Application*);
extern void WindowIsTranslucent(struct Application*);
extern void* GetWindowHandle(struct Application*);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,268 +0,0 @@
//go:build !windows
// +build !windows
package ffenestri
/*
#include "ffenestri.h"
*/
import "C"
import (
goruntime "runtime"
"strconv"
"strings"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
)
// Client is our implementation of messageDispatcher.Client
type Client struct {
app *Application
logger logger.CustomLogger
}
func newClient(app *Application) *Client {
return &Client{
app: app,
logger: app.logger,
}
}
// Quit the application
func (c *Client) Quit() {
c.app.logger.Trace("Got shutdown message")
C.Quit(c.app.app)
}
// NotifyEvent will pass on the event message to the frontend
func (c *Client) NotifyEvent(message string) {
eventMessage := `window.wails._.Notify(` + strconv.Quote(message) + `);`
c.app.logger.Trace("eventMessage = %+v", eventMessage)
C.ExecJS(c.app.app, c.app.string2CString(eventMessage))
}
// CallResult contains the result of the call from JS
func (c *Client) CallResult(message string) {
callbackMessage := `window.wails._.Callback(` + strconv.Quote(message) + `);`
c.app.logger.Trace("callbackMessage = %+v", callbackMessage)
C.ExecJS(c.app.app, c.app.string2CString(callbackMessage))
}
// WindowSetTitle sets the window title to the given string
func (c *Client) WindowSetTitle(title string) {
C.SetTitle(c.app.app, c.app.string2CString(title))
}
// WindowFullscreen will set the window to be fullscreen
func (c *Client) WindowFullscreen() {
C.Fullscreen(c.app.app)
}
// WindowUnfullscreen will unfullscreen the window
func (c *Client) WindowUnfullscreen() {
C.UnFullscreen(c.app.app)
}
// WindowShow will show the window
func (c *Client) WindowShow() {
C.Show(c.app.app)
}
// WindowHide will hide the window
func (c *Client) WindowHide() {
C.Hide(c.app.app)
}
// WindowCenter will hide the window
func (c *Client) WindowCenter() {
C.Center(c.app.app)
}
// WindowMaximise will maximise the window
func (c *Client) WindowMaximise() {
C.Maximise(c.app.app)
}
// WindowMinimise will minimise the window
func (c *Client) WindowMinimise() {
C.Minimise(c.app.app)
}
// WindowUnmaximise will unmaximise the window
func (c *Client) WindowUnmaximise() {
C.Unmaximise(c.app.app)
}
// WindowUnminimise will unminimise the window
func (c *Client) WindowUnminimise() {
C.Unminimise(c.app.app)
}
// WindowPosition will position the window to x,y on the
// monitor that the window is mostly on
func (c *Client) WindowPosition(x int, y int) {
C.SetPosition(c.app.app, C.int(x), C.int(y))
}
// WindowSize will resize the window to the given
// width and height
func (c *Client) WindowSize(width int, height int) {
C.SetSize(c.app.app, C.int(width), C.int(height))
}
func (c *Client) WindowSetMinSize(width int, height int) {
C.SetMinWindowSize(c.app.app, C.int(width), C.int(height))
}
func (c *Client) WindowSetMaxSize(width int, height int) {
C.SetMaxWindowSize(c.app.app, C.int(width), C.int(height))
}
// WindowSetColour sets the window colour
func (c *Client) WindowSetColour(colour int) {
r, g, b, a := intToColour(colour)
C.SetColour(c.app.app, r, g, b, a)
}
// OpenFileDialog will open a dialog with the given title and filter
func (c *Client) OpenFileDialog(dialogOptions runtime.OpenDialogOptions, callbackID string) {
filters := []string{}
if goruntime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.OpenDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(false),
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),
c.app.bool2Cint(dialogOptions.CanCreateDirectories),
c.app.bool2Cint(dialogOptions.ResolvesAliases),
c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories),
)
}
// OpenDirectoryDialog will open a dialog with the given title and filter
func (c *Client) OpenDirectoryDialog(dialogOptions runtime.OpenDialogOptions, callbackID string) {
filters := []string{}
if goruntime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.OpenDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(false), // Files
c.app.bool2Cint(true), // Directories
c.app.bool2Cint(false), // Multiple
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),
c.app.bool2Cint(dialogOptions.CanCreateDirectories),
c.app.bool2Cint(dialogOptions.ResolvesAliases),
c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories),
)
}
// OpenMultipleFilesDialog will open a dialog with the given title and filter
func (c *Client) OpenMultipleFilesDialog(dialogOptions runtime.OpenDialogOptions, callbackID string) {
filters := []string{}
if goruntime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.OpenDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(true),
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),
c.app.bool2Cint(dialogOptions.CanCreateDirectories),
c.app.bool2Cint(dialogOptions.ResolvesAliases),
c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories),
)
}
// SaveDialog will open a dialog with the given title and filter
func (c *Client) SaveDialog(dialogOptions *runtime.SaveDialogOptions, callbackID string) {
filters := []string{}
if goruntime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.SaveDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),
c.app.bool2Cint(dialogOptions.CanCreateDirectories),
c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories),
)
}
// MessageDialog will open a message dialog with the given options
func (c *Client) MessageDialog(dialogOptions runtime.MessageDialogOptions, callbackID string) {
// Sanity check button length
if len(dialogOptions.Buttons) > 4 {
c.app.logger.Error("Given %d message dialog buttons. Maximum is 4", len(dialogOptions.Buttons))
return
}
// Process buttons
buttons := []string{"", "", "", ""}
for i, button := range dialogOptions.Buttons {
buttons[i] = button
}
C.MessageDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(string(dialogOptions.Type)),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(dialogOptions.Message),
c.app.string2CString(buttons[0]),
c.app.string2CString(buttons[1]),
c.app.string2CString(buttons[2]),
c.app.string2CString(buttons[3]),
c.app.string2CString(dialogOptions.DefaultButton),
c.app.string2CString(dialogOptions.CancelButton))
}
func (c *Client) DarkModeEnabled(callbackID string) {
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
}
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
}
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) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
}
func (c *Client) DeleteTrayMenuByID(id string) {
C.DeleteTrayMenuByID(c.app.app, c.app.string2CString(id))
}

View File

@ -1,310 +0,0 @@
//go:build windows
// +build windows
package ffenestri
/*
#include "ffenestri.h"
*/
import "C"
import (
"encoding/json"
"log"
"strconv"
"syscall"
"github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/sys/windows"
"github.com/wailsapp/wails/v2/internal/logger"
)
// Client is our implementation of messageDispatcher.Client
type Client struct {
app *Application
logger logger.CustomLogger
}
func newClient(app *Application) *Client {
return &Client{
app: app,
logger: app.logger,
}
}
// Quit the application
func (c *Client) Quit() {
c.app.logger.Trace("Got shutdown message")
C.Quit(c.app.app)
}
// NotifyEvent will pass on the event message to the frontend
func (c *Client) NotifyEvent(message string) {
eventMessage := `window.wails._.Notify(` + strconv.Quote(message) + `);`
c.app.logger.Trace("eventMessage = %+v", eventMessage)
C.ExecJS(c.app.app, c.app.string2CString(eventMessage))
}
// CallResult contains the result of the call from JS
func (c *Client) CallResult(message string) {
callbackMessage := `window.wails._.Callback(` + strconv.Quote(message) + `);`
c.app.logger.Trace("callbackMessage = %+v", callbackMessage)
C.ExecJS(c.app.app, c.app.string2CString(callbackMessage))
}
// WindowSetTitle sets the window title to the given string
func (c *Client) WindowSetTitle(title string) {
C.SetTitle(c.app.app, c.app.string2CString(title))
}
// WindowFullscreen will set the window to be fullscreen
func (c *Client) WindowFullscreen() {
C.Fullscreen(c.app.app)
}
// WindowUnfullscreen will unfullscreen the window
func (c *Client) WindowUnfullscreen() {
C.UnFullscreen(c.app.app)
}
// WindowShow will show the window
func (c *Client) WindowShow() {
C.Show(c.app.app)
}
// WindowHide will hide the window
func (c *Client) WindowHide() {
C.Hide(c.app.app)
}
// WindowCenter will hide the window
func (c *Client) WindowCenter() {
C.Center(c.app.app)
}
// WindowMaximise will maximise the window
func (c *Client) WindowMaximise() {
C.Maximise(c.app.app)
}
// WindowMinimise will minimise the window
func (c *Client) WindowMinimise() {
C.Minimise(c.app.app)
}
// WindowUnmaximise will unmaximise the window
func (c *Client) WindowUnmaximise() {
C.Unmaximise(c.app.app)
}
// WindowUnminimise will unminimise the window
func (c *Client) WindowUnminimise() {
C.Unminimise(c.app.app)
}
// WindowPosition will position the window to x,y on the
// monitor that the window is mostly on
func (c *Client) WindowPosition(x int, y int) {
C.SetPosition(c.app.app, C.int(x), C.int(y))
}
// WindowSize will resize the window to the given
// width and height
func (c *Client) WindowSize(width int, height int) {
C.SetSize(c.app.app, C.int(width), C.int(height))
}
// WindowSetMinSize sets the minimum window size
func (c *Client) WindowSetMinSize(width int, height int) {
C.SetMinWindowSize(c.app.app, C.int(width), C.int(height))
}
// WindowSetMaxSize sets the maximum window size
func (c *Client) WindowSetMaxSize(width int, height int) {
C.SetMaxWindowSize(c.app.app, C.int(width), C.int(height))
}
// WindowSetColour sets the window colour
func (c *Client) WindowSetColour(colour int) {
r, g, b, a := intToColour(colour)
C.SetColour(c.app.app, r, g, b, a)
}
func convertFilters(filters []runtime.FileFilter) []cfd.FileFilter {
var result []cfd.FileFilter
for _, filter := range filters {
result = append(result, cfd.FileFilter(filter))
}
return result
}
// OpenFileDialog will open a dialog with the given title and filter
func (c *Client) OpenFileDialog(options runtime.OpenDialogOptions, callbackID string) {
config := cfd.DialogConfig{
Folder: options.DefaultDirectory,
FileFilters: convertFilters(options.Filters),
FileName: options.DefaultFilename,
}
thisdialog, err := cfd.NewOpenFileDialog(config)
if err != nil {
log.Fatal(err)
}
thisdialog.SetParentWindowHandle(uintptr(C.GetWindowHandle(c.app.app)))
defer func(thisdialog cfd.OpenFileDialog) {
err := thisdialog.Release()
if err != nil {
log.Fatal(err)
}
}(thisdialog)
result, err := thisdialog.ShowAndGetResult()
if err != nil && err != cfd.ErrorCancelled {
log.Fatal(err)
}
dispatcher.DispatchMessage("DO" + callbackID + "|" + result)
}
// OpenDirectoryDialog will open a dialog with the given title and filter
func (c *Client) OpenDirectoryDialog(dialogOptions runtime.OpenDialogOptions, callbackID string) {
config := cfd.DialogConfig{
Title: dialogOptions.Title,
Role: "PickFolder",
Folder: dialogOptions.DefaultDirectory,
}
thisDialog, err := cfd.NewSelectFolderDialog(config)
if err != nil {
log.Fatal()
}
thisDialog.SetParentWindowHandle(uintptr(C.GetWindowHandle(c.app.app)))
defer func(thisDialog cfd.SelectFolderDialog) {
err := thisDialog.Release()
if err != nil {
log.Fatal(err)
}
}(thisDialog)
result, err := thisDialog.ShowAndGetResult()
if err != nil && err != cfd.ErrorCancelled {
log.Fatal(err)
}
dispatcher.DispatchMessage("DD" + callbackID + "|" + result)
}
// OpenMultipleFilesDialog will open a dialog with the given title and filter
func (c *Client) OpenMultipleFilesDialog(dialogOptions runtime.OpenDialogOptions, callbackID string) {
config := cfd.DialogConfig{
Title: dialogOptions.Title,
Role: "OpenMultipleFiles",
FileFilters: convertFilters(dialogOptions.Filters),
FileName: dialogOptions.DefaultFilename,
Folder: dialogOptions.DefaultDirectory,
}
thisdialog, err := cfd.NewOpenMultipleFilesDialog(config)
if err != nil {
log.Fatal(err)
}
thisdialog.SetParentWindowHandle(uintptr(C.GetWindowHandle(c.app.app)))
defer func(thisdialog cfd.OpenMultipleFilesDialog) {
err := thisdialog.Release()
if err != nil {
log.Fatal(err)
}
}(thisdialog)
result, err := thisdialog.ShowAndGetResults()
if err != nil && err != cfd.ErrorCancelled {
log.Fatal(err)
}
resultJSON, err := json.Marshal(result)
if err != nil {
log.Fatal(err)
}
dispatcher.DispatchMessage("D*" + callbackID + "|" + string(resultJSON))
}
// SaveDialog will open a dialog with the given title and filter
func (c *Client) SaveDialog(dialogOptions runtime.SaveDialogOptions, callbackID string) {
saveDialog, err := cfd.NewSaveFileDialog(cfd.DialogConfig{
Title: dialogOptions.Title,
Role: "SaveFile",
FileFilters: convertFilters(dialogOptions.Filters),
FileName: dialogOptions.DefaultFilename,
Folder: dialogOptions.DefaultDirectory,
})
if err != nil {
log.Fatal(err)
}
saveDialog.SetParentWindowHandle(uintptr(C.GetWindowHandle(c.app.app)))
err = saveDialog.Show()
if err != nil {
log.Fatal(err)
}
result, err := saveDialog.GetResult()
if err != nil && err != cfd.ErrorCancelled {
log.Fatal(err)
}
dispatcher.DispatchMessage("DS" + callbackID + "|" + result)
}
// MessageDialog will open a message dialog with the given options
func (c *Client) MessageDialog(options runtime.MessageDialogOptions, callbackID string) {
title, err := syscall.UTF16PtrFromString(options.Title)
if err != nil {
log.Fatal(err)
}
message, err := syscall.UTF16PtrFromString(options.Message)
if err != nil {
log.Fatal(err)
}
var flags uint32
switch options.Type {
case runtime.InfoDialog:
flags = windows.MB_OK | windows.MB_ICONINFORMATION
case runtime.ErrorDialog:
flags = windows.MB_ICONERROR | windows.MB_OK
case runtime.QuestionDialog:
flags = windows.MB_YESNO
case runtime.WarningDialog:
flags = windows.MB_OK | windows.MB_ICONWARNING
}
button, _ := windows.MessageBox(windows.HWND(C.GetWindowHandle(c.app.app)), message, title, flags|windows.MB_SYSTEMMODAL)
// This maps MessageBox return values to strings
responses := []string{"", "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "", "", "Try Again", "Continue"}
result := "Error"
if int(button) < len(responses) {
result = responses[button]
}
dispatcher.DispatchMessage("DM" + callbackID + "|" + result)
}
// DarkModeEnabled sets the application to use dark mode
func (c *Client) DarkModeEnabled(callbackID string) {
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
}
// SetApplicationMenu sets the application menu
func (c *Client) SetApplicationMenu(_ string) {
c.updateApplicationMenu()
}
// SetTrayMenu sets the tray menu
func (c *Client) SetTrayMenu(trayMenuJSON string) {
C.SetTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
}
// UpdateTrayMenuLabel updates a tray menu label
func (c *Client) UpdateTrayMenuLabel(JSON string) {
C.UpdateTrayMenuLabel(c.app.app, c.app.string2CString(JSON))
}
// UpdateContextMenu will update the current context menu
func (c *Client) UpdateContextMenu(contextMenuJSON string) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
}
// DeleteTrayMenuByID will remove a tray menu based on the given id
func (c *Client) DeleteTrayMenuByID(id string) {
C.DeleteTrayMenuByID(c.app.app, c.app.string2CString(id))
}

File diff suppressed because it is too large Load Diff

View File

@ -1,99 +0,0 @@
package ffenestri
/*
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
#cgo darwin LDFLAGS: -framework WebKit -framework CoreFoundation -lobjc
#include "ffenestri.h"
#include "ffenestri_darwin.h"
*/
import "C"
func (a *Application) processPlatformSettings() error {
mac := a.config.Mac
titlebar := mac.TitleBar
// HideTitle
if titlebar.HideTitle {
C.HideTitle(a.app)
}
// HideTitleBar
if titlebar.HideTitleBar {
C.HideTitleBar(a.app)
}
// Full Size Content
if titlebar.FullSizeContent {
C.FullSizeContent(a.app)
}
// Toolbar
if titlebar.UseToolbar {
C.UseToolbar(a.app)
}
if titlebar.HideToolbarSeparator {
C.HideToolbarSeparator(a.app)
}
if titlebar.TitlebarAppearsTransparent {
C.TitlebarAppearsTransparent(a.app)
}
// Process window Appearance
if mac.Appearance != "" {
C.SetAppearance(a.app, a.string2CString(string(mac.Appearance)))
}
// Set activation policy
C.SetActivationPolicy(a.app, C.int(mac.ActivationPolicy))
// Check if the webview should be transparent
if mac.WebviewIsTransparent {
C.WebviewIsTransparent(a.app)
}
// Check if window should be translucent
if mac.WindowIsTranslucent {
C.WindowIsTranslucent(a.app)
}
// 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()
if err != nil {
return err
}
if trays != nil {
for _, tray := range trays {
C.AddTrayMenu(a.app, a.string2CString(tray))
}
}
// 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 URL Handlers
if a.config.Mac.URLHandlers != nil {
C.HasURLHandlers(a.app)
}
return nil
}

View File

@ -1,154 +0,0 @@
#ifndef FFENESTRI_DARWIN_H
#define FFENESTRI_DARWIN_H
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <CoreFoundation/CoreFoundation.h>
#include "json.h"
#include "hashmap.h"
#include "stdlib.h"
typedef struct {
long maj;
long min;
long patch;
} OSVersion;
// Macros to make it slightly more sane
#define msg objc_msgSend
#define msg_reg ((id(*)(id, SEL))objc_msgSend)
#define msg_id ((id(*)(id, SEL, id))objc_msgSend)
#define msg_id_id ((id(*)(id, SEL, id, id))objc_msgSend)
#define msg_bool ((id(*)(id, SEL, BOOL))objc_msgSend)
#define msg_int ((id(*)(id, SEL, int))objc_msgSend)
#define msg_uint ((id(*)(id, SEL, unsigned int))objc_msgSend)
#define msg_float ((id(*)(id, SEL, float))objc_msgSend)
#define kInternetEventClass 'GURL'
#define kAEGetURL 'GURL'
#define keyDirectObject '----'
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
#define str(input) ((id(*)(id, SEL, const char *))objc_msgSend)(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) ((id(*)(id, SEL, id, unsigned short))objc_msgSend)(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg_reg(input, s("UTF8String"))
#define url(input) msg_id(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg_reg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg_reg(msg_reg(c(classname), s("alloc")), s("init"))
#if defined (__aarch64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("bounds"))
#define GET_OSVERSION(receiver) ((OSVersion(*)(id, SEL))objc_msgSend)(processInfo, s("operatingSystemVersion"));
#endif
#if defined (__x86_64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#define GET_OSVERSION(receiver) ((OSVersion(*)(id, SEL))objc_msgSend_stret)(processInfo, s("operatingSystemVersion"));
#endif
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))objc_msgSend)(receiver, s("backingScaleFactor"))
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
#define MAIN_WINDOW_CALL(str) msg_reg(app->mainWindow, s((str)))
#define NSBackingStoreBuffered 2
#define NSWindowStyleMaskBorderless 0
#define NSWindowStyleMaskTitled 1
#define NSWindowStyleMaskClosable 2
#define NSWindowStyleMaskMiniaturizable 4
#define NSWindowStyleMaskResizable 8
#define NSWindowStyleMaskFullscreen 1 << 14
#define NSVisualEffectMaterialWindowBackground 12
#define NSVisualEffectBlendingModeBehindWindow 0
#define NSVisualEffectStateFollowsWindowActiveState 0
#define NSVisualEffectStateActive 1
#define NSVisualEffectStateInactive 2
#define NSViewWidthSizable 2
#define NSViewHeightSizable 16
#define NSWindowBelow -1
#define NSWindowAbove 1
#define NSSquareStatusItemLength -2.0
#define NSVariableStatusItemLength -1.0
#define NSWindowTitleHidden 1
#define NSWindowStyleMaskFullSizeContentView 1 << 15
#define NSEventModifierFlagCommand 1 << 20
#define NSEventModifierFlagOption 1 << 19
#define NSEventModifierFlagControl 1 << 18
#define NSEventModifierFlagShift 1 << 17
#define NSControlStateValueMixed -1
#define NSControlStateValueOff 0
#define NSControlStateValueOn 1
#define NSApplicationActivationPolicyRegular 0
#define NSApplicationActivationPolicyAccessory 1
#define NSApplicationActivationPolicyProhibited 2
// Unbelievably, if the user swaps their button preference
// then right buttons are reported as left buttons
#define NSEventMaskLeftMouseDown 1 << 1
#define NSEventMaskLeftMouseUp 1 << 2
#define NSEventMaskRightMouseDown 1 << 3
#define NSEventMaskRightMouseUp 1 << 4
#define NSEventTypeLeftMouseDown 1
#define NSEventTypeLeftMouseUp 2
#define NSEventTypeRightMouseDown 3
#define NSEventTypeRightMouseUp 4
#define NSNoImage 0
#define NSImageOnly 1
#define NSImageLeft 2
#define NSImageRight 3
#define NSImageBelow 4
#define NSImageAbove 5
#define NSImageOverlaps 6
#define NSAlertStyleWarning 0
#define NSAlertStyleInformational 1
#define NSAlertStyleCritical 2
#define NSAlertFirstButtonReturn 1000
#define NSAlertSecondButtonReturn 1001
#define NSAlertThirdButtonReturn 1002
#define BrokenImage "iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAMAAABl5a5YAAABj1BMVEWopan///+koqSWk5P9/v3///////////+AgACMiovz8/PB0fG9z+3i4+WysbGBfX1Erh80rACLiYqBxolEsDhHlDEbqQDDx+CNho7W1tj4+/bw+O3P5Mn4/f/W1tbK6sX////b2dn////////////8/Pz6+vro6Ojj4+P////G1PL////EzNydmp2cmZnd3eDF1PHs8v/o8P/Q3vrS3vfE0vCdmpqZkpr19/3N2vXI1vPH1fOgnqDg6frP3PbCytvHx8irqq6HhIZtuGtjnlZetU1Xs0NWskBNsi7w9v/d6P7w9P3S4Pzr8Pvl7PrY5PrU4PjQ3fjD1Ozo6Om30NjGzNi7ubm34K+UxKmbnaWXlJeUjpSPi4tppF1TtjxSsTf2+f7L2PTr7e3H2+3V7+q+0uXg4OPg4eLR1uG7z+Hg4ODGzODV2N7V1trP5dmxzs65vcfFxMWq0cKxxr+/vr+0s7apxbWaxrCv2qao05+dlp2Uuo2Dn4F8vIB6xnyAoHmAym9zqGpctENLryNFsgoblJpnAAAAKnRSTlP+hP7+5ZRmYgL+/f39/f39/f38/Pz8/Pv69+7j083My8GocnBPTTMWEgjxeITOAAABEklEQVQY0y3KZXuCYBiG4ceYuu7u3nyVAaKOMBBQ7O5Yd3f3fvheDnd9u8/jBkGwNxP6sjOWVQvY/ftrzfT6bd3yEhCnYZqiaYoKiwX/gXkFiHySTcUTLJMsZ9v8nQvgssWYOEKedKpcOO6CUXD5IlGEY5hLUbyDAAZ6HRf1bnkoavOsFQibg+Q4nuNYL+ON5PHD5nBaraRVyxnzGf6BJzUi2QQCQgMyk8tleL7dg1owpJ17D5IkvV100EingeOopPyo6vfAuXF+9hbDTknZCIaUoeK4efKwG4iT6xDewd7imGlid7gGwv37b6Oh9jwaTdOf/Tc1qH7UZVmuP6G5qZfBr9cAGNy4KiDd4tXIs7tS+QO9aUKvPAIKuQAAAABJRU5ErkJggg=="
struct Application;
int releaseNSObject(void *const context, struct hashmap_element_s *const e);
void TitlebarAppearsTransparent(struct Application* app);
void HideTitle(struct Application* app);
void HideTitleBar(struct Application* app);
void FullSizeContent(struct Application* app);
void UseToolbar(struct Application* app);
void HideToolbarSeparator(struct Application* app);
void DisableFrame(struct Application* app);
void SetAppearance(struct Application* app, const char *);
void WebviewIsTransparent(struct Application* app);
void WindowIsTranslucent(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 SetActivationPolicy(struct Application* app, int policy);
void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
id createImageFromBase64Data(const char *data, bool isTemplateImage);
#endif

View File

@ -1,995 +0,0 @@
#ifndef __FFENESTRI_LINUX_H__
#define __FFENESTRI_LINUX_H__
#include "common.h"
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
// References to assets
extern const unsigned char runtime;
#include "icon.h"
#include "assets.h"
// Constants
#define PRIMARY_MOUSE_BUTTON 1
#define MIDDLE_MOUSE_BUTTON 2
#define SECONDARY_MOUSE_BUTTON 3
// MAIN DEBUG FLAG
int debug;
// Debug works like sprintf but mutes if the global debug flag is true
// Credit: https://stackoverflow.com/a/20639708
void Debug(char *message, ...)
{
if (debug)
{
char *temp = concat("TRACE | Ffenestri (C) | ", message);
message = concat(temp, "\n");
free(temp);
va_list args;
va_start(args, message);
vprintf(message, args);
free(message);
va_end(args);
}
}
extern void messageFromWindowCallback(const char *);
typedef void (*ffenestriCallback)(const char *);
struct Application
{
// Gtk Data
GtkApplication *application;
GtkWindow *mainWindow;
GtkWidget *webView;
int signalInvoke;
int signalWindowDrag;
int signalButtonPressed;
int signalButtonReleased;
int signalLoadChanged;
// Saves the events for the drag mouse button
GdkEventButton *dragButtonEvent;
// The number of the default drag button
int dragButton;
// Window Data
const char *title;
char *id;
int width;
int height;
int resizable;
int devtools;
int startHidden;
int fullscreen;
int minWidth;
int minHeight;
int maxWidth;
int maxHeight;
int frame;
// User Data
char *HTML;
// Callback
ffenestriCallback sendMessageToBackend;
// Bindings
const char *bindings;
// Lock - used for sync operations (Should we be using g_mutex?)
int lock;
};
void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden)
{
// Setup main application struct
struct Application *result = malloc(sizeof(struct Application));
result->title = title;
result->width = width;
result->height = height;
result->resizable = resizable;
result->devtools = devtools;
result->fullscreen = fullscreen;
result->minWidth = 0;
result->minHeight = 0;
result->maxWidth = 0;
result->maxHeight = 0;
result->frame = 1;
result->startHidden = startHidden;
// Default drag button is PRIMARY
result->dragButton = PRIMARY_MOUSE_BUTTON;
result->sendMessageToBackend = (ffenestriCallback)messageFromWindowCallback;
// Create a unique ID based on the current unix timestamp
char temp[11];
sprintf(&temp[0], "%d", (int)time(NULL));
result->id = concat("wails.app", &temp[0]);
// Create the main GTK application
GApplicationFlags flags = G_APPLICATION_FLAGS_NONE;
result->application = gtk_application_new(result->id, flags);
// Return the application struct
return (void *)result;
}
void DestroyApplication(struct Application *app)
{
Debug("Destroying Application");
g_application_quit(G_APPLICATION(app->application));
// Release the GTK ID string
if (app->id != NULL)
{
free(app->id);
app->id = NULL;
}
else
{
Debug("Almost a double free for app->id");
}
// Free the bindings
if (app->bindings != NULL)
{
free((void *)app->bindings);
app->bindings = NULL;
}
else
{
Debug("Almost a double free for app->bindings");
}
// Disconnect signal handlers
WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager((WebKitWebView *)app->webView);
g_signal_handler_disconnect(manager, app->signalInvoke);
if( app->frame == 0) {
g_signal_handler_disconnect(manager, app->signalWindowDrag);
g_signal_handler_disconnect(app->webView, app->signalButtonPressed);
g_signal_handler_disconnect(app->webView, app->signalButtonReleased);
}
g_signal_handler_disconnect(app->webView, app->signalLoadChanged);
// Release the main GTK Application
if (app->application != NULL)
{
g_object_unref(app->application);
app->application = NULL;
}
else
{
Debug("Almost a double free for app->application");
}
Debug("Finished Destroying Application");
}
// Quit will stop the gtk application and free up all the memory
// used by the application
void Quit(struct Application *app)
{
Debug("Quit Called");
gtk_window_close((GtkWindow *)app->mainWindow);
g_application_quit((GApplication *)app->application);
DestroyApplication(app);
}
// SetTitle sets the main window title to the given string
void SetTitle(struct Application *app, const char *title)
{
gtk_window_set_title(app->mainWindow, title);
}
// Fullscreen sets the main window to be fullscreen
void Fullscreen(struct Application *app)
{
gtk_window_fullscreen(app->mainWindow);
}
// UnFullscreen resets the main window after a fullscreen
void UnFullscreen(struct Application *app)
{
gtk_window_unfullscreen(app->mainWindow);
}
void setMinMaxSize(struct Application *app)
{
GdkGeometry size;
size.min_width = size.min_height = size.max_width = size.max_height = 0;
int flags = 0;
if (app->maxHeight > 0 && app->maxWidth > 0)
{
size.max_height = app->maxHeight;
size.max_width = app->maxWidth;
flags |= GDK_HINT_MAX_SIZE;
}
if (app->minHeight > 0 && app->minWidth > 0)
{
size.min_height = app->minHeight;
size.min_width = app->minWidth;
flags |= GDK_HINT_MIN_SIZE;
}
gtk_window_set_geometry_hints(app->mainWindow, NULL, &size, flags);
}
char *fileDialogInternal(struct Application *app, GtkFileChooserAction chooserAction, char **args) {
GtkFileChooserNative *native;
GtkFileChooserAction action = chooserAction;
gint res;
char *filename;
char *title = args[0];
char *filter = args[1];
native = gtk_file_chooser_native_new(title,
app->mainWindow,
action,
"_Open",
"_Cancel");
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
// If we have filters, process them
if (filter[0] != '\0') {
GtkFileFilter *file_filter = gtk_file_filter_new();
gchar **filters = g_strsplit(filter, ",", -1);
gint i;
for(i = 0; filters && filters[i]; i++) {
gtk_file_filter_add_pattern(file_filter, filters[i]);
// Debug("Adding filter pattern: %s\n", filters[i]);
}
gtk_file_filter_set_name(file_filter, filter);
gtk_file_chooser_add_filter(chooser, file_filter);
g_strfreev(filters);
}
res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT)
{
filename = gtk_file_chooser_get_filename(chooser);
}
g_object_unref(native);
return filename;
}
// openFileDialogInternal opens a dialog to select a file
// NOTE: The result is a string that will need to be freed!
char *openFileDialogInternal(struct Application *app, char **args)
{
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_OPEN, args);
}
// saveFileDialogInternal opens a dialog to select a file
// NOTE: The result is a string that will need to be freed!
char *saveFileDialogInternal(struct Application *app, char **args)
{
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_SAVE, args);
}
// openDirectoryDialogInternal opens a dialog to select a directory
// NOTE: The result is a string that will need to be freed!
char *openDirectoryDialogInternal(struct Application *app, char **args)
{
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, args);
}
void SetMinWindowSize(struct Application *app, int minWidth, int minHeight)
{
app->minWidth = minWidth;
app->minHeight = minHeight;
}
void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight)
{
app->maxWidth = maxWidth;
app->maxHeight = maxHeight;
}
// SetColour sets the colour of the webview to the given colour string
void SetColour(struct Application *app, int red, int green, int blue, int alpha)
{
// GdkRGBA rgba;
// rgba.
// gboolean result = gdk_rgba_parse(&rgba, colourString);
// if (result == FALSE)
// {
// return 0;
// }
// // Debug("Setting webview colour to: %s", colourString);
// webkit_web_view_get_background_color((WebKitWebView *)(app->webView), &rgba);
}
// DisableFrame disables the window frame
void DisableFrame(struct Application *app)
{
app->frame = 0;
}
void syncCallback(GObject *source_object,
GAsyncResult *res,
void *data)
{
struct Application *app = (struct Application *)data;
app->lock = 0;
}
void syncEval(struct Application *app, const gchar *script)
{
WebKitWebView *webView = (WebKitWebView *)(app->webView);
// wait for lock to free
while (app->lock == 1)
{
g_main_context_iteration(0, true);
}
// Set lock
app->lock = 1;
webkit_web_view_run_javascript(
webView,
script,
NULL, syncCallback, (void*)app);
while (app->lock == 1)
{
g_main_context_iteration(0, true);
}
}
void asyncEval(WebKitWebView *webView, const gchar *script)
{
webkit_web_view_run_javascript(
webView,
script,
NULL, NULL, NULL);
}
typedef void (*dispatchMethod)(struct Application *app, void *);
struct dispatchData
{
struct Application *app;
dispatchMethod method;
void *args;
};
gboolean executeMethod(gpointer data)
{
struct dispatchData *d = (struct dispatchData *)data;
(d->method)(d->app, d->args);
g_free(d);
return FALSE;
}
void ExecJS(struct Application *app, char *js)
{
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)syncEval;
data->args = js;
data->app = app;
gdk_threads_add_idle(executeMethod, data);
}
typedef char *(*dialogMethod)(struct Application *app, void *);
struct dialogCall
{
struct Application *app;
dialogMethod method;
void *args;
void *filter;
char *result;
int done;
};
gboolean executeMethodWithReturn(gpointer data)
{
struct dialogCall *d = (struct dialogCall *)data;
d->result = (d->method)(d->app, d->args);
d->done = 1;
return FALSE;
}
char *OpenFileDialog(struct Application *app, char *title, char *filter)
{
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
data->result = NULL;
data->done = 0;
data->method = (dialogMethod)openFileDialogInternal;
const char* dialogArgs[]={ title, filter };
data->args = dialogArgs;
data->app = app;
gdk_threads_add_idle(executeMethodWithReturn, data);
while (data->done == 0)
{
usleep(100000);
}
g_free(data);
return data->result;
}
char *SaveFileDialog(struct Application *app, char *title, char *filter)
{
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
data->result = NULL;
data->done = 0;
data->method = (dialogMethod)saveFileDialogInternal;
const char* dialogArgs[]={ title, filter };
data->args = dialogArgs;
data->app = app;
gdk_threads_add_idle(executeMethodWithReturn, data);
while (data->done == 0)
{
usleep(100000);
}
Debug("Dialog done");
Debug("Result = %s\n", data->result);
g_free(data);
// Fingers crossed this wasn't freed by g_free above
return data->result;
}
char *OpenDirectoryDialog(struct Application *app, char *title, char *filter)
{
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
data->result = NULL;
data->done = 0;
data->method = (dialogMethod)openDirectoryDialogInternal;
const char* dialogArgs[]={ title, filter };
data->args = dialogArgs;
data->app = app;
gdk_threads_add_idle(executeMethodWithReturn, data);
while (data->done == 0)
{
usleep(100000);
}
Debug("Directory Dialog done");
Debug("Result = %s\n", data->result);
g_free(data);
// Fingers crossed this wasn't freed by g_free above
return data->result;
}
// Sets the icon to the XPM stored in icon
void setIcon(struct Application *app)
{
GdkPixbuf *appIcon = gdk_pixbuf_new_from_xpm_data((const char **)icon);
gtk_window_set_icon(app->mainWindow, appIcon);
}
static void load_finished_cb(WebKitWebView *webView,
WebKitLoadEvent load_event,
struct Application *app)
{
switch (load_event)
{
// case WEBKIT_LOAD_STARTED:
// /* New load, we have now a provisional URI */
// // printf("Start downloading %s\n", webkit_web_view_get_uri(web_view));
// /* Here we could start a spinner or update the
// * location bar with the provisional URI */
// break;
// case WEBKIT_LOAD_REDIRECTED:
// // printf("Redirected to: %s\n", webkit_web_view_get_uri(web_view));
// break;
// case WEBKIT_LOAD_COMMITTED:
// /* The load is being performed. Current URI is
// * the final one and it won't change unless a new
// * load is requested or a navigation within the
// * same page is performed */
// // printf("Loading: %s\n", webkit_web_view_get_uri(web_view));
// break;
case WEBKIT_LOAD_FINISHED:
/* Load finished, we can now stop the spinner */
// printf("Finished loading: %s\n", webkit_web_view_get_uri(web_view));
// Bindings
Debug("Binding Methods");
syncEval(app, app->bindings);
// Setup IPC commands
Debug("Setting up IPC methods");
const char *invoke = "window.wailsInvoke=function(message){window.webkit.messageHandlers.external.postMessage(message);};window.wailsDrag=function(message){window.webkit.messageHandlers.windowDrag.postMessage(message);};window.wailsContextMenuMessage=function(message){window.webkit.messageHandlers.contextMenu.postMessage(message);};";
syncEval(app, invoke);
// Runtime
Debug("Setting up Wails runtime");
syncEval(app, &runtime);
// Loop over assets
int index = 1;
while (1)
{
// Get next asset pointer
const char *asset = assets[index];
// If we have no more assets, break
if (asset == 0x00)
{
break;
}
// sync eval the asset
syncEval(app, asset);
index++;
};
// Set the icon
setIcon(app);
// Setup fullscreen
if (app->fullscreen)
{
Debug("Going fullscreen");
Fullscreen(app);
}
// Setup resize
gtk_window_resize(GTK_WINDOW(app->mainWindow), app->width, app->height);
if (app->resizable)
{
gtk_window_set_default_size(GTK_WINDOW(app->mainWindow), app->width, app->height);
}
else
{
gtk_widget_set_size_request(GTK_WIDGET(app->mainWindow), app->width, app->height);
gtk_window_resize(GTK_WINDOW(app->mainWindow), app->width, app->height);
// Fix the min/max to the window size for good measure
app->minHeight = app->maxHeight = app->height;
app->minWidth = app->maxWidth = app->width;
}
gtk_window_set_resizable(GTK_WINDOW(app->mainWindow), app->resizable ? TRUE : FALSE);
setMinMaxSize(app);
// Centre by default
gtk_window_set_position(app->mainWindow, GTK_WIN_POS_CENTER);
// Show window and focus
if( app->startHidden == 0) {
gtk_widget_show_all(GTK_WIDGET(app->mainWindow));
gtk_widget_grab_focus(app->webView);
}
break;
}
}
static gboolean disable_context_menu_cb(
WebKitWebView *web_view,
WebKitContextMenu *context_menu,
GdkEvent *event,
WebKitHitTestResult *hit_test_result,
gpointer user_data)
{
return TRUE;
}
static void printEvent(const char *message, GdkEventButton *event)
{
Debug("%s: [button:%d] [x:%f] [y:%f] [time:%d]",
message,
event->button,
event->x_root,
event->y_root,
event->time);
}
static void dragWindow(WebKitUserContentManager *contentManager,
WebKitJavascriptResult *result,
struct Application *app)
{
// If we get this message erroneously, ignore
if (app->dragButtonEvent == NULL)
{
return;
}
// Ignore non-toplevel widgets
GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(app->webView));
if (!GTK_IS_WINDOW(window))
{
return;
}
// Initiate the drag
printEvent("Starting drag with event", app->dragButtonEvent);
gtk_window_begin_move_drag(app->mainWindow,
app->dragButton,
app->dragButtonEvent->x_root,
app->dragButtonEvent->y_root,
app->dragButtonEvent->time);
// Clear the event
app->dragButtonEvent = NULL;
return;
}
gboolean buttonPress(GtkWidget *widget, GdkEventButton *event, struct Application *app)
{
if (event->type == GDK_BUTTON_PRESS && event->button == app->dragButton)
{
app->dragButtonEvent = event;
}
return FALSE;
}
gboolean buttonRelease(GtkWidget *widget, GdkEventButton *event, struct Application *app)
{
if (event->type == GDK_BUTTON_RELEASE && event->button == app->dragButton)
{
app->dragButtonEvent = NULL;
}
return FALSE;
}
static void sendMessageToBackend(WebKitUserContentManager *contentManager,
WebKitJavascriptResult *result,
struct Application *app)
{
#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
JSCValue *value = webkit_javascript_result_get_js_value(result);
char *message = jsc_value_to_string(value);
#else
JSGlobalContextRef context = webkit_javascript_result_get_global_context(result);
JSValueRef value = webkit_javascript_result_get_value(result);
JSStringRef js = JSValueToStringCopy(context, value, NULL);
size_t messageSize = JSStringGetMaximumUTF8CStringSize(js);
char *message = g_new(char, messageSize);
JSStringGetUTF8CString(js, message, messageSize);
JSStringRelease(js);
#endif
app->sendMessageToBackend(message);
g_free(message);
}
void SetDebug(struct Application *app, int flag)
{
debug = flag;
}
// getCurrentMonitorGeometry gets the geometry of the monitor
// that the window is mostly on.
GdkRectangle getCurrentMonitorGeometry(GtkWindow *window) {
// Get the monitor that the window is currently on
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
GdkMonitor *monitor = gdk_display_get_monitor_at_window (display, gdk_window);
// Get the geometry of the monitor
GdkRectangle result;
gdk_monitor_get_geometry (monitor,&result);
return result;
}
/*******************
* Window Position *
*******************/
// Position holds an x/y corrdinate
struct Position {
int x;
int y;
};
// Internal call for setting the position of the window.
void setPositionInternal(struct Application *app, struct Position *pos) {
// Get the monitor geometry
GdkRectangle m = getCurrentMonitorGeometry(app->mainWindow);
// Move the window relative to the monitor
gtk_window_move(app->mainWindow, m.x + pos->x, m.y + pos->y);
// Free memory
free(pos);
}
// SetPosition sets the position of the window to the given x/y
// coordinates. The x/y values are relative to the monitor
// the window is mostly on.
void SetPosition(struct Application *app, int x, int y) {
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)setPositionInternal;
struct Position *pos = malloc(sizeof(struct Position));
pos->x = x;
pos->y = y;
data->args = pos;
data->app = app;
gdk_threads_add_idle(executeMethod, data);
}
/***************
* Window Size *
***************/
// Size holds a width/height
struct Size {
int width;
int height;
};
// Internal call for setting the size of the window.
void setSizeInternal(struct Application *app, struct Size *size) {
gtk_window_resize(app->mainWindow, size->width, size->height);
free(size);
}
// SetSize sets the size of the window to the given width/height
void SetSize(struct Application *app, int width, int height) {
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)setSizeInternal;
struct Size *size = malloc(sizeof(struct Size));
size->width = width;
size->height = height;
data->args = size;
data->app = app;
gdk_threads_add_idle(executeMethod, data);
}
// centerInternal will center the main window on the monitor it is mostly in
void centerInternal(struct Application *app)
{
// Get the geometry of the monitor
GdkRectangle m = getCurrentMonitorGeometry(app->mainWindow);
// Get the window width/height
int windowWidth, windowHeight;
gtk_window_get_size(app->mainWindow, &windowWidth, &windowHeight);
// Place the window at the center of the monitor
gtk_window_move(app->mainWindow, ((m.width - windowWidth) / 2) + m.x, ((m.height - windowHeight) / 2) + m.y);
}
// Center the window
void Center(struct Application *app) {
// Setup a call to centerInternal on the main thread
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)centerInternal;
data->app = app;
// Add call to main thread
gdk_threads_add_idle(executeMethod, data);
}
// hideInternal hides the main window
void hideInternal(struct Application *app) {
gtk_widget_hide (GTK_WIDGET(app->mainWindow));
}
// Hide places the hideInternal method onto the main thread for execution
void Hide(struct Application *app) {
// Setup a call to hideInternal on the main thread
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)hideInternal;
data->app = app;
// Add call to main thread
gdk_threads_add_idle(executeMethod, data);
}
// showInternal shows the main window
void showInternal(struct Application *app) {
gtk_widget_show_all(GTK_WIDGET(app->mainWindow));
gtk_widget_grab_focus(app->webView);
}
// Show places the showInternal method onto the main thread for execution
void Show(struct Application *app) {
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)showInternal;
data->app = app;
gdk_threads_add_idle(executeMethod, data);
}
// maximiseInternal maximises the main window
void maximiseInternal(struct Application *app) {
gtk_window_maximize(GTK_WINDOW(app->mainWindow));
}
// Maximise places the maximiseInternal method onto the main thread for execution
void Maximise(struct Application *app) {
// Setup a call to maximiseInternal on the main thread
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)maximiseInternal;
data->app = app;
// Add call to main thread
gdk_threads_add_idle(executeMethod, data);
}
// unmaximiseInternal unmaximises the main window
void unmaximiseInternal(struct Application *app) {
gtk_window_unmaximize(GTK_WINDOW(app->mainWindow));
}
// Unmaximise places the unmaximiseInternal method onto the main thread for execution
void Unmaximise(struct Application *app) {
// Setup a call to unmaximiseInternal on the main thread
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)unmaximiseInternal;
data->app = app;
// Add call to main thread
gdk_threads_add_idle(executeMethod, data);
}
void DarkModeEnabled(struct Application* app, char *callbackID) {}
void SetApplicationMenu(struct Application* app, const char *menuJSON) {}
void AddTrayMenu(struct Application* app, const char *menuTrayJSON) {}
void SetTrayMenu(struct Application* app, const char *menuTrayJSON) {}
void DeleteTrayMenuByID(struct Application* app, const char *id) {}
void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {}
void AddContextMenu(struct Application* app, char *contextMenuJSON) {}
void UpdateContextMenu(struct Application* app, char *contextMenuJSON) {}
void WebviewIsTransparent(struct Application* app) {}
void WindowIsTranslucent(struct Application* app) {}
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 SaveDialog(struct Application* app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) {}
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) {}
// minimiseInternal minimises the main window
void minimiseInternal(struct Application *app) {
gtk_window_iconify(app->mainWindow);
}
// Minimise places the minimiseInternal method onto the main thread for execution
void Minimise(struct Application *app) {
// Setup a call to minimiseInternal on the main thread
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)minimiseInternal;
data->app = app;
// Add call to main thread
gdk_threads_add_idle(executeMethod, data);
}
// unminimiseInternal unminimises the main window
void unminimiseInternal(struct Application *app) {
gtk_window_present(app->mainWindow);
}
// Unminimise places the unminimiseInternal method onto the main thread for execution
void Unminimise(struct Application *app) {
// Setup a call to unminimiseInternal on the main thread
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)unminimiseInternal;
data->app = app;
// Add call to main thread
gdk_threads_add_idle(executeMethod, data);
}
void SetBindings(struct Application *app, const char *bindings)
{
const char *temp = concat("window.wailsbindings = \"", bindings);
const char *jscall = concat(temp, "\";");
free((void *)temp);
app->bindings = jscall;
}
// This is called when the close button on the window is pressed
gboolean close_button_pressed(GtkWidget *widget,
GdkEvent *event,
struct Application *app)
{
app->sendMessageToBackend("WC"); // Window Close
return TRUE;
}
static void setupWindow(struct Application *app)
{
// Create the window
GtkWidget *mainWindow = gtk_application_window_new(app->application);
// Save reference
app->mainWindow = GTK_WINDOW(mainWindow);
// Setup frame
gtk_window_set_decorated((GtkWindow *)mainWindow, app->frame);
// Setup title
gtk_window_set_title(GTK_WINDOW(mainWindow), app->title);
// Setup script handler
WebKitUserContentManager *contentManager = webkit_user_content_manager_new();
// Setup the invoke handler
webkit_user_content_manager_register_script_message_handler(contentManager, "external");
app->signalInvoke = g_signal_connect(contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), app);
// Setup the window drag handler if this is a frameless app
if ( app->frame == 0 ) {
webkit_user_content_manager_register_script_message_handler(contentManager, "windowDrag");
app->signalWindowDrag = g_signal_connect(contentManager, "script-message-received::windowDrag", G_CALLBACK(dragWindow), app);
// Setup the mouse handlers
app->signalButtonPressed = g_signal_connect(app->webView, "button-press-event", G_CALLBACK(buttonPress), app);
app->signalButtonReleased = g_signal_connect(app->webView, "button-release-event", G_CALLBACK(buttonRelease), app);
}
GtkWidget *webView = webkit_web_view_new_with_user_content_manager(contentManager);
// Save reference
app->webView = webView;
// Add the webview to the window
gtk_container_add(GTK_CONTAINER(mainWindow), webView);
// Load default HTML
app->signalLoadChanged = g_signal_connect(G_OBJECT(webView), "load-changed", G_CALLBACK(load_finished_cb), app);
// Load the user's HTML
// assets[0] is the HTML because the asset array is bundled like that by convention
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webView), assets[0]);
// Check if we want to enable the dev tools
if (app->devtools)
{
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webView));
// webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
webkit_settings_set_enable_developer_extras(settings, true);
}
else
{
g_signal_connect(G_OBJECT(webView), "context-menu", G_CALLBACK(disable_context_menu_cb), app);
}
// Listen for close button signal
g_signal_connect(GTK_WIDGET(mainWindow), "delete-event", G_CALLBACK(close_button_pressed), app);
}
static void activate(GtkApplication* _, struct Application *app)
{
setupWindow(app);
}
void Run(struct Application *app, int argc, char **argv)
{
g_signal_connect(app->application, "activate", G_CALLBACK(activate), app);
g_application_run(G_APPLICATION(app->application), argc, argv);
}
#endif

View File

@ -1,17 +0,0 @@
package ffenestri
/*
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include "ffenestri.h"
#include "ffenestri_linux.h"
*/
import "C"
func (a *Application) processPlatformSettings() error {
return nil
}

View File

@ -1,6 +0,0 @@
#ifndef FFENESTRI_LINUX_H
#define FFENESTRI_LINUX_H
#endif

View File

@ -1,906 +0,0 @@
// Some code may be inspired by or directly used from Webview (c) zserge.
// License included in README.md
#include "ffenestri_windows.h"
#include "shellscalingapi.h"
#include "wv2ComHandler_windows.h"
#include <functional>
#include <atomic>
#include <Shlwapi.h>
#include <locale>
#include <codecvt>
#include "windows/WebView2.h"
#include <winuser.h>
#include "effectstructs_windows.h"
#include <Shlobj.h>
int debug = 0;
DWORD mainThread;
#define WS_EX_NOREDIRECTIONBITMAP 0x00200000L
// --- Assets
extern const unsigned char runtime;
extern const unsigned char *defaultDialogIcons[];
// dispatch will execute the given `func` pointer
void dispatch(dispatchFunction func) {
PostThreadMessage(mainThread, WM_APP, 0, (LPARAM) new dispatchFunction(func));
}
void processKeyPress(UINT key) {
// Get state of Control
bool controlPressed = GetKeyState(VK_CONTROL) >> 15 != 0;
bool altPressed = GetKeyState(VK_MENU) >> 15 != 0;
bool shiftPressed = GetKeyState(VK_SHIFT) >> 15 != 0;
// Save the modifier keys
BYTE modState = 0;
if ( GetKeyState(VK_CONTROL) >> 15 != 0 ) { modState |= 1; }
if ( GetKeyState(VK_MENU) >> 15 != 0 ) { modState |= 2; }
if ( GetKeyState(VK_SHIFT) >> 15 != 0 ) { modState |= 4; }
if ( GetKeyState(VK_LWIN) >> 15 != 0 ) { modState |= 8; }
if ( GetKeyState(VK_RWIN) >> 15 != 0 ) { modState |= 8; }
// Notify app of keypress
handleKeypressInGo(key, modState);
}
LPWSTR cstrToLPWSTR(const char *cstr) {
int wchars_num = MultiByteToWideChar( CP_UTF8 , 0 , cstr , -1, NULL , 0 );
wchar_t* wstr = new wchar_t[wchars_num+1];
MultiByteToWideChar( CP_UTF8 , 0 , cstr , -1, wstr , wchars_num );
return wstr;
}
// Credit: https://stackoverflow.com/a/9842450
char* LPWSTRToCstr(LPWSTR input) {
int length = WideCharToMultiByte(CP_UTF8, 0, input, -1, 0, 0, NULL, NULL);
char* output = new char[length];
WideCharToMultiByte(CP_UTF8, 0, input, -1, output , length, NULL, NULL);
return output;
}
// Credit: https://building.enlyze.com/posts/writing-win32-apps-like-its-2020-part-3/
typedef int (__cdecl *PGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE,UINT*,UINT*);
void getDPIForWindow(struct Application *app)
{
HMODULE hShcore = LoadLibraryW(L"shcore");
if (hShcore)
{
PGetDpiForMonitor pGetDpiForMonitor = reinterpret_cast<PGetDpiForMonitor>(GetProcAddress(hShcore, "GetDpiForMonitor"));
if (pGetDpiForMonitor)
{
HMONITOR hMonitor = MonitorFromWindow(app->window, MONITOR_DEFAULTTOPRIMARY);
pGetDpiForMonitor(hMonitor, (MONITOR_DPI_TYPE)0, &app->dpix, &app->dpiy);
}
} else {
// We couldn't get the window's DPI above, so get the DPI of the primary monitor
// using an API that is available in all Windows versions.
HDC hScreenDC = GetDC(0);
app->dpix = GetDeviceCaps(hScreenDC, LOGPIXELSX);
app->dpiy = GetDeviceCaps(hScreenDC, LOGPIXELSY);
ReleaseDC(0, hScreenDC);
}
}
struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {
// Create application
struct Application *result = (struct Application*)malloc(sizeof(struct Application));
result->window = nullptr;
result->webview = nullptr;
result->webviewController = nullptr;
result->title = title;
result->width = width;
result->height = height;
result->resizable = resizable;
result->devtools = devtools;
result->fullscreen = fullscreen;
result->startHidden = startHidden;
result->logLevel = logLevel;
result->hideWindowOnClose = hideWindowOnClose;
result->webviewIsTranparent = false;
result->WindowIsTranslucent = false;
result->disableWindowIcon = false;
// Min/Max Width/Height
result->minWidth = 0;
result->minHeight = 0;
result->maxWidth = 0;
result->maxHeight = 0;
// Default colour
result->backgroundColour.R = 255;
result->backgroundColour.G = 255;
result->backgroundColour.B = 255;
result->backgroundColour.A = 255;
// Have a frame by default
result->frame = 1;
// Capture Main Thread
mainThread = GetCurrentThreadId();
// Startup url
result->startupURL = nullptr;
// Used to remember the window location when going fullscreen
result->previousPlacement = { sizeof(result->previousPlacement) };
// DPI
result->dpix = result->dpiy = 0;
return result;
}
void* GetWindowHandle(struct Application *app) {
return (void*)app->window;
}
void SetMinWindowSize(struct Application* app, int minWidth, int minHeight) {
app->minWidth = (LONG)minWidth;
app->minHeight = (LONG)minHeight;
}
void SetMaxWindowSize(struct Application* app, int maxWidth, int maxHeight) {
app->maxWidth = (LONG)maxWidth;
app->maxHeight = (LONG)maxHeight;
}
void SetBindings(struct Application *app, const char *bindings) {
std::string temp = std::string("window.wailsbindings = \"") + std::string(bindings) + std::string("\";");
app->bindings = new char[temp.length()+1];
memcpy(app->bindings, temp.c_str(), temp.length()+1);
}
void performShutdown(struct Application *app) {
if( app->startupURL != nullptr ) {
delete[] app->startupURL;
}
messageFromWindowCallback("WC");
}
// Credit: https://gist.github.com/ysc3839/b08d2bff1c7dacde529bed1d37e85ccf
void enableTranslucentBackground(struct Application *app) {
HMODULE hUser = GetModuleHandleA("user32.dll");
if (hUser)
{
pfnSetWindowCompositionAttribute setWindowCompositionAttribute = (pfnSetWindowCompositionAttribute)GetProcAddress(hUser, "SetWindowCompositionAttribute");
if (setWindowCompositionAttribute)
{
ACCENT_POLICY accent = { ACCENT_ENABLE_BLURBEHIND, 0, 0, 0 };
WINDOWCOMPOSITIONATTRIBDATA data;
data.Attrib = WCA_ACCENT_POLICY;
data.pvData = &accent;
data.cbData = sizeof(accent);
setWindowCompositionAttribute(app->window, &data);
}
}
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
struct Application *app = (struct Application *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
switch(msg) {
case WM_CREATE: {
createApplicationMenu(hwnd);
break;
}
case WM_COMMAND:
menuClicked(LOWORD(wParam));
break;
case WM_CLOSE: {
DestroyWindow( app->window );
break;
}
case WM_DESTROY: {
if( app->hideWindowOnClose ) {
Hide(app);
} else {
PostQuitMessage(0);
}
break;
}
case WM_SIZE: {
if ( app == NULL ) {
return 0;
}
if( app->webviewController != nullptr) {
RECT bounds;
GetClientRect(app->window, &bounds);
app->webviewController->put_Bounds(bounds);
}
break;
}
case WM_KEYDOWN:
// This is needed because webview2 is sometimes not in focus
// https://github.com/MicrosoftEdge/WebView2Feedback/issues/1541
processKeyPress(wParam);
break;
case WM_GETMINMAXINFO: {
// Exit early if this is called before the window is created.
if ( app == NULL ) {
return 0;
}
// update DPI
getDPIForWindow(app);
double DPIScaleX = app->dpix/96.0;
double DPIScaleY = app->dpiy/96.0;
RECT rcWind;
POINT ptDiff;
GetWindowRect(hwnd, &rcWind);
int widthExtra = (rcWind.right - rcWind.left);
int heightExtra = (rcWind.bottom - rcWind.top);
LPMINMAXINFO mmi = (LPMINMAXINFO) lParam;
if (app->minWidth > 0 && app->minHeight > 0) {
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX;
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY;
}
if (app->maxWidth > 0 && app->maxHeight > 0) {
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX;
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY;
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX;
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY;
}
return 0;
}
default:
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return 0;
}
void init(struct Application *app, const char* js) {
LPCWSTR wjs = cstrToLPWSTR(js);
app->webview->AddScriptToExecuteOnDocumentCreated(wjs, nullptr);
delete[] wjs;
}
void execJS(struct Application* app, const char *script) {
LPWSTR s = cstrToLPWSTR(script);
app->webview->ExecuteScript(s, nullptr);
delete[] s;
}
void loadAssets(struct Application* app) {
// setup window.wailsInvoke
std::string initialCode = std::string("window.wailsInvoke=function(m){window.chrome.webview.postMessage(m)};");
// Load bindings
initialCode += std::string(app->bindings);
delete[] app->bindings;
// Load runtime
initialCode += std::string((const char*)&runtime);
int index = 1;
while(1) {
// Get next asset pointer
const unsigned char *asset = assets[index];
// If we have no more assets, break
if (asset == 0x00) {
break;
}
initialCode += std::string((const char*)asset);
index++;
};
// Disable context menu if not in debug mode
if( debug != 1 ) {
initialCode += std::string("wails._.DisableDefaultContextMenu();");
}
initialCode += std::string("window.wailsInvoke('completed');");
// Keep a copy of the code
app->initialCode = new char[initialCode.length()+1];
memcpy(app->initialCode, initialCode.c_str(), initialCode.length()+1);
execJS(app, app->initialCode);
// Show app if we need to
if( app->startHidden == false ) {
Show(app);
}
}
// This is called when all our assets are loaded into the DOM
void completed(struct Application* app) {
delete[] app->initialCode;
app->initialCode = nullptr;
// Process whether window should show by default
int startVisibility = SW_SHOWNORMAL;
if ( app->startHidden == 1 ) {
startVisibility = SW_HIDE;
}
// Fix for webview2 bug: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077
// Will be fixed in next stable release
app->webviewController->put_IsVisible(false);
app->webviewController->put_IsVisible(true);
// Private setTitle as we're on the main thread
if( app->frame == 1) {
setTitle(app, app->title);
}
ShowWindow(app->window, startVisibility);
UpdateWindow(app->window);
SetFocus(app->window);
if( app->startupURL == nullptr ) {
messageFromWindowCallback("SS");
return;
}
std::string readyMessage = std::string("SS") + std::string(app->startupURL);
messageFromWindowCallback(readyMessage.c_str());
}
//
bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb) {
debug = debugEnabled;
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
std::atomic_flag flag = ATOMIC_FLAG_INIT;
flag.test_and_set();
char currentExePath[MAX_PATH];
GetModuleFileNameA(NULL, currentExePath, MAX_PATH);
char *currentExeName = PathFindFileNameA(currentExePath);
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> wideCharConverter;
std::wstring userDataFolder =
wideCharConverter.from_bytes(std::getenv("APPDATA"));
std::wstring currentExeNameW = wideCharConverter.from_bytes(currentExeName);
ICoreWebView2Controller *controller;
ICoreWebView2* webview;
HRESULT res = CreateCoreWebView2EnvironmentWithOptions(
nullptr, (userDataFolder + L"/" + currentExeNameW).c_str(), nullptr,
new wv2ComHandler(app, app->window, cb,
[&](ICoreWebView2Controller *webviewController) {
controller = webviewController;
controller->get_CoreWebView2(&webview);
webview->AddRef();
ICoreWebView2Settings* settings;
webview->get_Settings(&settings);
if ( debugEnabled == 0 ) {
settings->put_AreDefaultContextMenusEnabled(FALSE);
}
// Fix for invisible webview
if( app->startHidden ) {}
controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
flag.clear();
}));
if (!SUCCEEDED(res))
{
switch (res)
{
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
{
MessageBox(
app->window,
L"Couldn't find Edge installation. "
"Do you have a version installed that's compatible with this "
"WebView2 SDK version?",
nullptr, MB_OK);
}
break;
case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
{
MessageBox(
app->window, L"User data folder cannot be created because a file with the same name already exists.", nullptr, MB_OK);
}
break;
case E_ACCESSDENIED:
{
MessageBox(
app->window, L"Unable to create user data folder, Access Denied.", nullptr, MB_OK);
}
break;
case E_FAIL:
{
MessageBox(
app->window, L"Edge runtime unable to start", nullptr, MB_OK);
}
break;
default:
{
MessageBox(app->window, L"Failed to create WebView2 environment", nullptr, MB_OK);
}
}
}
if (res != S_OK) {
CoUninitialize();
return false;
}
MSG msg = {};
while (flag.test_and_set() && GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
app->webviewController = controller;
app->webview = webview;
// Resize WebView to fit the bounds of the parent window
RECT bounds;
GetClientRect(app->window, &bounds);
app->webviewController->put_Bounds(bounds);
// Let the backend know we have initialised
app->webview->AddScriptToExecuteOnDocumentCreated(L"window.chrome.webview.postMessage('initialised');", nullptr);
// Load the HTML
LPCWSTR html = (LPCWSTR) cstrToLPWSTR((char*)assets[0]);
app->webview->Navigate(html);
if( app->webviewIsTranparent ) {
wchar_t szBuff[64];
ICoreWebView2Controller2 *wc2;
wc2 = nullptr;
app->webviewController->QueryInterface(IID_ICoreWebView2Controller2, (void**)&wc2);
COREWEBVIEW2_COLOR wvColor;
wvColor.R = app->backgroundColour.R;
wvColor.G = app->backgroundColour.G;
wvColor.B = app->backgroundColour.B;
wvColor.A = app->backgroundColour.A == 0 ? 0 : 255;
if( app->WindowIsTranslucent ) {
wvColor.A = 0;
}
HRESULT result = wc2->put_DefaultBackgroundColor(wvColor);
if (!SUCCEEDED(result))
{
switch (result)
{
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
{
MessageBox(
app->window,
L"Couldn't find Edge installation. "
"Do you have a version installed that's compatible with this "
"WebView2 SDK version?",
nullptr, MB_OK);
}
break;
case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
{
MessageBox(
app->window, L"User data folder cannot be created because a file with the same name already exists.", nullptr, MB_OK);
}
break;
case E_ACCESSDENIED:
{
MessageBox(
app->window, L"Unable to create user data folder, Access Denied.", nullptr, MB_OK);
}
break;
case E_FAIL:
{
MessageBox(
app->window, L"Edge runtime unable to start", nullptr, MB_OK);
}
break;
default:
{
MessageBox(app->window, L"Failed to create WebView2 environment", nullptr, MB_OK);
}
}
}
}
messageFromWindowCallback("Ej{\"name\":\"wails:launched\",\"data\":[]}");
return true;
}
void initialCallback(std::string message) {
printf("MESSAGE=%s\n", message);
}
void Run(struct Application* app, int argc, char **argv) {
// Register the window class.
const wchar_t CLASS_NAME[] = L"Ffenestri";
WNDCLASSEX wc = { };
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = CLASS_NAME;
if( app->disableWindowIcon == false ) {
wc.hIcon = LoadIcon(wc.hInstance, MAKEINTRESOURCE(100));
wc.hIconSm = LoadIcon(wc.hInstance, MAKEINTRESOURCE(100));
}
// Configure translucency
DWORD dwExStyle = 0;
if ( app->WindowIsTranslucent) {
dwExStyle = WS_EX_NOREDIRECTIONBITMAP;
wc.hbrBackground = CreateSolidBrush(RGB(255,255,255));
}
RegisterClassEx(&wc);
// Process window style
DWORD windowStyle = WS_OVERLAPPEDWINDOW | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
if (app->resizable == 0) {
windowStyle &= ~WS_MAXIMIZEBOX;
windowStyle &= ~WS_THICKFRAME;
}
if ( app->frame == 0 ) {
windowStyle &= ~WS_OVERLAPPEDWINDOW;
windowStyle &= ~WS_CAPTION;
windowStyle |= WS_POPUP;
}
// Create the window.
app->window = CreateWindowEx(
dwExStyle, // Optional window styles.
CLASS_NAME, // Window class
L"", // Window text
windowStyle, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, app->width, app->height,
NULL, // Parent window
NULL, // Menu
wc.hInstance, // Instance handle
NULL // Additional application data
);
if (app->window == NULL)
{
return;
}
if ( app->fullscreen ) {
fullscreen(app);
}
// Credit: https://stackoverflow.com/a/35482689
if( app->disableWindowIcon && app->frame == 1 ) {
int extendedStyle = GetWindowLong(app->window, GWL_EXSTYLE);
SetWindowLong(app->window, GWL_EXSTYLE, extendedStyle | WS_EX_DLGMODALFRAME);
SetWindowPos(nullptr, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
}
if ( app->WindowIsTranslucent ) {
// Enable the translucent background effect
enableTranslucentBackground(app);
// Setup transparency of main window. This allows the blur to show through.
SetLayeredWindowAttributes(app->window,RGB(255,255,255),0,LWA_COLORKEY);
}
// Store application pointer in window handle
SetWindowLongPtr(app->window, GWLP_USERDATA, (LONG_PTR)app);
// private center() as we are on main thread
center(app);
// Add webview2
initWebView2(app, debug, initialCallback);
// Main event loop
MSG msg;
BOOL res;
while ((res = GetMessage(&msg, NULL, 0, 0)) != -1) {
if (msg.hwnd) {
TranslateMessage(&msg);
DispatchMessage(&msg);
continue;
}
if (msg.message == WM_APP) {
dispatchFunction *f = (dispatchFunction*) msg.lParam;
(*f)();
delete(f);
} else if (msg.message == WM_QUIT) {
performShutdown(app);
return;
}
}
}
void SetDebug(struct Application* app, int flag) {
debug = flag;
}
void ExecJS(struct Application* app, const char *script) {
ON_MAIN_THREAD(
execJS(app, script);
);
}
void hide(struct Application* app) {
ShowWindow(app->window, SW_HIDE);
}
void Hide(struct Application* app) {
ON_MAIN_THREAD(
hide(app);
);
}
void show(struct Application* app) {
ShowWindow(app->window, SW_SHOW);
}
void Show(struct Application* app) {
ON_MAIN_THREAD(
show(app);
);
}
void DisableWindowIcon(struct Application* app) {
app->disableWindowIcon = true;
}
void center(struct Application* app) {
HMONITOR currentMonitor = MonitorFromWindow(app->window, MONITOR_DEFAULTTONEAREST);
MONITORINFO info = {0};
info.cbSize = sizeof(info);
GetMonitorInfoA(currentMonitor, &info);
RECT workRect = info.rcWork;
LONG screenMiddleW = (workRect.right - workRect.left) / 2;
LONG screenMiddleH = (workRect.bottom - workRect.top) / 2;
RECT winRect;
if (app->frame == 1) {
GetWindowRect(app->window, &winRect);
} else {
GetClientRect(app->window, &winRect);
}
LONG winWidth = winRect.right - winRect.left;
LONG winHeight = winRect.bottom - winRect.top;
LONG windowX = screenMiddleW - (winWidth / 2);
LONG windowY = screenMiddleH - (winHeight / 2);
SetWindowPos(app->window, HWND_TOP, windowX, windowY, winWidth, winHeight, SWP_NOSIZE);
}
void Center(struct Application* app) {
ON_MAIN_THREAD(
center(app);
);
}
UINT getWindowPlacement(struct Application* app) {
WINDOWPLACEMENT lpwndpl;
lpwndpl.length = sizeof(WINDOWPLACEMENT);
BOOL result = GetWindowPlacement(app->window, &lpwndpl);
if( result == 0 ) {
// TODO: Work out what this call failing means
return -1;
}
return lpwndpl.showCmd;
}
int isMaximised(struct Application* app) {
return getWindowPlacement(app) == SW_SHOWMAXIMIZED;
}
void maximise(struct Application* app) {
ShowWindow(app->window, SW_MAXIMIZE);
}
void Maximise(struct Application* app) {
ON_MAIN_THREAD(
maximise(app);
);
}
void unmaximise(struct Application* app) {
ShowWindow(app->window, SW_RESTORE);
}
void Unmaximise(struct Application* app) {
ON_MAIN_THREAD(
unmaximise(app);
);
}
void ToggleMaximise(struct Application* app) {
if(isMaximised(app)) {
return Unmaximise(app);
}
return Maximise(app);
}
int isMinimised(struct Application* app) {
return getWindowPlacement(app) == SW_SHOWMINIMIZED;
}
void minimise(struct Application* app) {
ShowWindow(app->window, SW_MINIMIZE);
}
void Minimise(struct Application* app) {
ON_MAIN_THREAD(
minimise(app);
);
}
void unminimise(struct Application* app) {
ShowWindow(app->window, SW_RESTORE);
}
void Unminimise(struct Application* app) {
ON_MAIN_THREAD(
unminimise(app);
);
}
void ToggleMinimise(struct Application* app) {
if(isMinimised(app)) {
return Unminimise(app);
}
return Minimise(app);
}
void SetColour(struct Application* app, int red, int green, int blue, int alpha) {
app->backgroundColour.R = red;
app->backgroundColour.G = green;
app->backgroundColour.B = blue;
app->backgroundColour.A = alpha;
}
void SetSize(struct Application* app, int width, int height) {
if( app->maxWidth > 0 && width > app->maxWidth ) {
width = app->maxWidth;
}
if ( app->maxHeight > 0 && height > app->maxHeight ) {
height = app->maxHeight;
}
SetWindowPos(app->window, nullptr, 0, 0, width, height, SWP_NOMOVE);
}
void setPosition(struct Application* app, int x, int y) {
HMONITOR currentMonitor = MonitorFromWindow(app->window, MONITOR_DEFAULTTONEAREST);
MONITORINFO info = {0};
info.cbSize = sizeof(info);
GetMonitorInfoA(currentMonitor, &info);
RECT workRect = info.rcWork;
LONG newX = workRect.left + x;
LONG newY = workRect.top + y;
SetWindowPos(app->window, HWND_TOP, newX, newY, 0, 0, SWP_NOSIZE);
}
void SetPosition(struct Application* app, int x, int y) {
ON_MAIN_THREAD(
setPosition(app, x, y);
);
}
void Quit(struct Application* app) {
// Override the hide window on close flag
app->hideWindowOnClose = 0;
ON_MAIN_THREAD(
DestroyWindow(app->window);
);
}
// Credit: https://stackoverflow.com/a/6693107
void setTitle(struct Application* app, const char *title) {
LPCTSTR text = cstrToLPWSTR(title);
SetWindowText(app->window, text);
delete[] text;
}
void SetTitle(struct Application* app, const char *title) {
ON_MAIN_THREAD(
setTitle(app, title);
);
}
void fullscreen(struct Application* app) {
// Ensure we aren't in fullscreen
if (app->isFullscreen) return;
app->isFullscreen = true;
app->previousWindowStyle = GetWindowLong(app->window, GWL_STYLE);
MONITORINFO mi = { sizeof(mi) };
if (GetWindowPlacement(app->window, &(app->previousPlacement)) && GetMonitorInfo(MonitorFromWindow(app->window, MONITOR_DEFAULTTOPRIMARY), &mi)) {
SetWindowLong(app->window, GWL_STYLE, app->previousWindowStyle & ~WS_OVERLAPPEDWINDOW);
SetWindowPos(app->window, HWND_TOP,
mi.rcMonitor.left,
mi.rcMonitor.top,
mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top,
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
}
}
void Fullscreen(struct Application* app) {
ON_MAIN_THREAD(
fullscreen(app);
show(app);
);
}
void unfullscreen(struct Application* app) {
if (app->isFullscreen) {
SetWindowLong(app->window, GWL_STYLE, app->previousWindowStyle);
SetWindowPlacement(app->window, &(app->previousPlacement));
SetWindowPos(app->window, NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
app->isFullscreen = false;
}
}
void UnFullscreen(struct Application* app) {
ON_MAIN_THREAD(
unfullscreen(app);
);
}
void DisableFrame(struct Application* app) {
app->frame = 0;
}
// WebviewIsTransparent will make the webview transparent
// revealing the window underneath
void WebviewIsTransparent(struct Application *app) {
app->webviewIsTranparent = true;
}
void WindowIsTranslucent(struct Application *app) {
app->WindowIsTranslucent = true;
}
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 SaveDialog(struct Application* app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) {
}
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) {
}
void DarkModeEnabled(struct Application* app, char *callbackID) {
}
void SetApplicationMenu(struct Application* app, const char *applicationMenuJSON) {
}
void AddTrayMenu(struct Application* app, const char *menuTrayJSON) {
}
void SetTrayMenu(struct Application* app, const char *menuTrayJSON) {
}
void DeleteTrayMenuByID(struct Application* app, const char *id) {
}
void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
}
void AddContextMenu(struct Application* app, char *contextMenuJSON) {
}
void UpdateContextMenu(struct Application* app, char *contextMenuJSON) {
}

View File

@ -1,198 +0,0 @@
package ffenestri
import "C"
/*
#cgo windows CXXFLAGS: -std=c++11
#cgo windows,amd64 LDFLAGS: -lgdi32 -lole32 -lShlwapi -luser32 -loleaut32 -ldwmapi
#include "ffenestri.h"
extern void DisableWindowIcon(struct Application* app);
*/
import "C"
import (
"github.com/ztrue/tracerr"
"os"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Setup the global caches
var globalCheckboxCache = NewCheckboxCache()
var globalRadioGroupCache = NewRadioGroupCache()
var globalRadioGroupMap = NewRadioGroupMap()
var globalApplicationMenu *Menu
type menuType string
const (
appMenuType menuType = "ApplicationMenu"
contextMenuType
trayMenuType
)
func (a *Application) processPlatformSettings() error {
menuManager = a.menuManager
config := a.config.Windows
if config == nil {
return nil
}
// Check if the webview should be transparent
if config.WebviewIsTransparent {
C.WebviewIsTransparent(a.app)
}
if config.WindowIsTranslucent {
C.WindowIsTranslucent(a.app)
}
if config.DisableWindowIcon {
C.DisableWindowIcon(a.app)
}
// Unfortunately, we need to store this in the package variable so the C callback can see it
applicationMenu = a.menuManager.GetProcessedApplicationMenu()
//
//// Process tray
//trays, err := a.menuManager.GetTrayMenus()
//if err != nil {
// return err
//}
//if trays != nil {
// for _, tray := range trays {
// C.AddTrayMenu(a.app, a.string2CString(tray))
// }
//}
//
//// 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 URL Handlers
//if a.config.Mac.URLHandlers != nil {
// C.HasURLHandlers(a.app)
//}
return nil
}
func (c *Client) updateApplicationMenu() {
applicationMenu = c.app.menuManager.GetProcessedApplicationMenu()
createApplicationMenu(uintptr(C.GetWindowHandle(c.app.app)))
}
/* ---------------------------------------------------------------------------------
Application Menu
----------------
There's only 1 application menu and this is where we create it. This method
is called from C after the window is created and the WM_CREATE message has
been sent.
*/
func checkFatal(err error) {
if err != nil {
tracerr.PrintSourceColor(err)
globalRadioGroupCache.Dump()
globalRadioGroupMap.Dump()
os.Exit(1)
}
}
//export createApplicationMenu
func createApplicationMenu(hwnd uintptr) {
if applicationMenu == nil {
return
}
var err error
window := win32Window(hwnd)
if globalApplicationMenu != nil {
checkFatal(globalApplicationMenu.Destroy())
}
globalApplicationMenu, err = createMenu(applicationMenu, appMenuType)
checkFatal(err)
err = setWindowMenu(window, globalApplicationMenu.menu)
checkFatal(err)
}
//export handleKeypressInGo
func handleKeypressInGo(keycode uint16, modifiers uint8) {
//fmt.Printf("Key code: %#x\n", keycode)
menuID, menuType := getCallbackForKeyPress(keycode, modifiers)
if menuID == "" {
return
}
err := menuManager.ProcessClick(menuID, "", string(menuType), "")
if err != nil {
println(err.Error())
}
}
/*
This method is called by C when a menu item is pressed
*/
//export menuClicked
func menuClicked(id uint32) {
win32MenuID := win32MenuItemID(id)
//println("Got click from menu id", win32MenuID)
// Get the menu from the cache
menuItemDetails := getMenuCacheEntry(win32MenuID)
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
//println("Got click from menu id", win32MenuID, "- wails menu ID", wailsMenuID)
//spew.Dump(menuItemDetails)
switch menuItemDetails.item.Type {
case menu.CheckboxType:
// Determine if the menu is set or not
res, _, err := win32GetMenuState.Call(uintptr(menuItemDetails.parent), uintptr(id), uintptr(MF_BYCOMMAND))
if int(res) == -1 {
checkFatal(err)
}
flag := MF_CHECKED
if uint32(res) == MF_CHECKED {
flag = MF_UNCHECKED
}
for _, menuid := range globalCheckboxCache.win32MenuIDsForWailsMenuID(wailsMenuID) {
//println("setting menuid", menuid, "with flag", flag)
menuItemDetails := getMenuCacheEntry(menuid)
res, _, err = win32CheckMenuItem.Call(uintptr(menuItemDetails.parent), uintptr(menuid), uintptr(flag))
if int(res) == -1 {
checkFatal(err)
}
}
case menu.RadioType:
err := selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
checkFatal(err)
}
// Print the click error - it's not fatal
err := menuManager.ProcessClick(menuItemDetails.item.ID, "", string(menuItemDetails.menuType), "")
if err != nil {
println(err.Error())
}
}

View File

@ -1,95 +0,0 @@
#ifndef _FFENESTRI_WINDOWS_H
#define _FFENESTRI_WINDOWS_H
#define WIN32_LEAN_AND_MEAN
#define UNICODE 1
#include "ffenestri.h"
#include <windows.h>
#include <wingdi.h>
#include <functional>
#include <codecvt>
#include "windows/WebView2.h"
#include "assets.h"
// TODO:
//#include "userdialogicons.h"
struct Application{
// Window specific
HWND window;
ICoreWebView2 *webview;
ICoreWebView2Controller* webviewController;
// Application
const char *title;
int width;
int height;
int resizable;
int devtools;
int fullscreen;
int startHidden;
int logLevel;
int hideWindowOnClose;
int minSizeSet;
LONG minWidth;
LONG minHeight;
int maxSizeSet;
LONG maxWidth;
LONG maxHeight;
int frame;
char *startupURL;
bool webviewIsTranparent;
bool WindowIsTranslucent;
COREWEBVIEW2_COLOR backgroundColour;
bool disableWindowIcon;
// Used by fullscreen/unfullscreen
bool isFullscreen;
WINDOWPLACEMENT previousPlacement;
DWORD previousWindowStyle;
// placeholders
char* bindings;
char* initialCode;
// DPI
UINT dpix;
UINT dpiy;
};
#define ON_MAIN_THREAD(code) dispatch( [=]{ code; } )
typedef std::function<void()> dispatchFunction;
typedef std::function<void(const std::string)> messageCallback;
typedef std::function<void(ICoreWebView2Controller *)> comHandlerCallback;
void center(struct Application*);
void setTitle(struct Application* app, const char *title);
void fullscreen(struct Application* app);
void unfullscreen(struct Application* app);
char* LPWSTRToCstr(LPWSTR input);
// called when the DOM is ready
void loadAssets(struct Application* app);
// called when the application assets have been loaded into the DOM
void completed(struct Application* app);
// Processes the given keycode
void processKeyPress(UINT key);
// Callback
extern "C" {
void DisableWindowIcon(struct Application* app);
void messageFromWindowCallback(const char *);
void* GetWindowHandle(struct Application*);
void createApplicationMenu(HWND hwnd);
void menuClicked(UINT id);
void handleKeypressInGo(UINT, BYTE);
}
#endif

View File

@ -1,518 +0,0 @@
/*
The latest version of this library is available on GitHub;
https://github.com/sheredom/hashmap.h
*/
/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org/>
*/
#ifndef SHEREDOM_HASHMAP_H_INCLUDED
#define SHEREDOM_HASHMAP_H_INCLUDED
#if defined(_MSC_VER)
// Workaround a bug in the MSVC runtime where it uses __cplusplus when not
// defined.
#pragma warning(push, 0)
#pragma warning(disable : 4668)
#endif
#include <stdlib.h>
#include <string.h>
#if (defined(_MSC_VER) && defined(__AVX__)) || \
(!defined(_MSC_VER) && defined(__SSE4_2__))
#define HASHMAP_SSE42
#endif
#if defined(HASHMAP_SSE42)
#include <nmmintrin.h>
#endif
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
#if defined(_MSC_VER)
#pragma warning(push)
// Stop MSVC complaining about not inlining functions.
#pragma warning(disable : 4710)
// Stop MSVC complaining about inlining functions!
#pragma warning(disable : 4711)
#elif defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
#endif
#if defined(_MSC_VER)
#define HASHMAP_USED
#elif defined(__GNUC__)
#define HASHMAP_USED __attribute__((used))
#else
#define HASHMAP_USED
#endif
/* We need to keep keys and values. */
struct hashmap_element_s {
const char *key;
unsigned key_len;
int in_use;
void *data;
};
/* A hashmap has some maximum size and current size, as well as the data to
* hold. */
struct hashmap_s {
unsigned table_size;
unsigned size;
struct hashmap_element_s *data;
};
#define HASHMAP_MAX_CHAIN_LENGTH (8)
#if defined(__cplusplus)
extern "C" {
#endif
/// @brief Create a hashmap.
/// @param initial_size The initial size of the hashmap. Must be a power of two.
/// @param out_hashmap The storage for the created hashmap.
/// @return On success 0 is returned.
///
/// Note that the initial size of the hashmap must be a power of two, and
/// creation of the hashmap will fail if this is not the case.
static int hashmap_create(const unsigned initial_size,
struct hashmap_s *const out_hashmap) HASHMAP_USED;
/// @brief Put an element into the hashmap.
/// @param hashmap The hashmap to insert into.
/// @param key The string key to use.
/// @param len The length of the string key.
/// @param value The value to insert.
/// @return On success 0 is returned.
///
/// The key string slice is not copied when creating the hashmap entry, and thus
/// must remain a valid pointer until the hashmap entry is removed or the
/// hashmap is destroyed.
static int hashmap_put(struct hashmap_s *const hashmap, const char *const key,
const unsigned len, void *const value) HASHMAP_USED;
/// @brief Get an element from the hashmap.
/// @param hashmap The hashmap to get from.
/// @param key The string key to use.
/// @param len The length of the string key.
/// @return The previously set element, or NULL if none exists.
static void *hashmap_get(const struct hashmap_s *const hashmap,
const char *const key,
const unsigned len) HASHMAP_USED;
/// @brief Remove an element from the hashmap.
/// @param hashmap The hashmap to remove from.
/// @param key The string key to use.
/// @param len The length of the string key.
/// @return On success 0 is returned.
static int hashmap_remove(struct hashmap_s *const hashmap,
const char *const key,
const unsigned len) HASHMAP_USED;
/// @brief Iterate over all the elements in a hashmap.
/// @param hashmap The hashmap to iterate over.
/// @param f The function pointer to call on each element.
/// @param context The context to pass as the first argument to f.
/// @return If the entire hashmap was iterated then 0 is returned. Otherwise if
/// the callback function f returned non-zero then non-zero is returned.
static int hashmap_iterate(const struct hashmap_s *const hashmap,
int (*f)(void *const context, void *const value),
void *const context) HASHMAP_USED;
/// @brief Iterate over all the elements in a hashmap.
/// @param hashmap The hashmap to iterate over.
/// @param f The function pointer to call on each element.
/// @param context The context to pass as the first argument to f.
/// @return If the entire hashmap was iterated then 0 is returned.
/// Otherwise if the callback function f returned positive then the positive
/// value is returned. If the callback function returns -1, the current item
/// is removed and iteration continues.
static int hashmap_iterate_pairs(struct hashmap_s *const hashmap,
int (*f)(void *const, struct hashmap_element_s *const),
void *const context) HASHMAP_USED;
/// @brief Get the size of the hashmap.
/// @param hashmap The hashmap to get the size of.
/// @return The size of the hashmap.
static unsigned
hashmap_num_entries(const struct hashmap_s *const hashmap) HASHMAP_USED;
/// @brief Destroy the hashmap.
/// @param hashmap The hashmap to destroy.
static void hashmap_destroy(struct hashmap_s *const hashmap) HASHMAP_USED;
static unsigned hashmap_crc32_helper(const char *const s,
const unsigned len) HASHMAP_USED;
static unsigned
hashmap_hash_helper_int_helper(const struct hashmap_s *const m,
const char *const keystring,
const unsigned len) HASHMAP_USED;
static int hashmap_match_helper(const struct hashmap_element_s *const element,
const char *const key,
const unsigned len) HASHMAP_USED;
static int hashmap_hash_helper(const struct hashmap_s *const m,
const char *const key, const unsigned len,
unsigned *const out_index) HASHMAP_USED;
static int hashmap_rehash_iterator(void *const new_hash,
struct hashmap_element_s *const e) HASHMAP_USED;
static int hashmap_rehash_helper(struct hashmap_s *const m) HASHMAP_USED;
#if defined(__cplusplus)
}
#endif
#if defined(__cplusplus)
#define HASHMAP_CAST(type, x) static_cast<type>(x)
#define HASHMAP_PTR_CAST(type, x) reinterpret_cast<type>(x)
#define HASHMAP_NULL NULL
#else
#define HASHMAP_CAST(type, x) ((type)x)
#define HASHMAP_PTR_CAST(type, x) ((type)x)
#define HASHMAP_NULL 0
#endif
int hashmap_create(const unsigned initial_size,
struct hashmap_s *const out_hashmap) {
if (0 == initial_size || 0 != (initial_size & (initial_size - 1))) {
return 1;
}
out_hashmap->data =
HASHMAP_CAST(struct hashmap_element_s *,
calloc(initial_size, sizeof(struct hashmap_element_s)));
if (!out_hashmap->data) {
return 1;
}
out_hashmap->table_size = initial_size;
out_hashmap->size = 0;
return 0;
}
int hashmap_put(struct hashmap_s *const m, const char *const key,
const unsigned len, void *const value) {
unsigned int index;
/* Find a place to put our value. */
while (!hashmap_hash_helper(m, key, len, &index)) {
if (hashmap_rehash_helper(m)) {
return 1;
}
}
/* Set the data. */
m->data[index].data = value;
m->data[index].key = key;
m->data[index].key_len = len;
m->data[index].in_use = 1;
m->size++;
return 0;
}
void *hashmap_get(const struct hashmap_s *const m, const char *const key,
const unsigned len) {
unsigned int curr;
unsigned int i;
/* Find data location */
curr = hashmap_hash_helper_int_helper(m, key, len);
/* Linear probing, if necessary */
for (i = 0; i < HASHMAP_MAX_CHAIN_LENGTH; i++) {
if (m->data[curr].in_use) {
if (hashmap_match_helper(&m->data[curr], key, len)) {
return m->data[curr].data;
}
}
curr = (curr + 1) % m->table_size;
}
/* Not found */
return HASHMAP_NULL;
}
int hashmap_remove(struct hashmap_s *const m, const char *const key,
const unsigned len) {
unsigned int i;
unsigned int curr;
/* Find key */
curr = hashmap_hash_helper_int_helper(m, key, len);
/* Linear probing, if necessary */
for (i = 0; i < HASHMAP_MAX_CHAIN_LENGTH; i++) {
if (m->data[curr].in_use) {
if (hashmap_match_helper(&m->data[curr], key, len)) {
/* Blank out the fields including in_use */
memset(&m->data[curr], 0, sizeof(struct hashmap_element_s));
/* Reduce the size */
m->size--;
return 0;
}
}
curr = (curr + 1) % m->table_size;
}
return 1;
}
int hashmap_iterate(const struct hashmap_s *const m,
int (*f)(void *const, void *const), void *const context) {
unsigned int i;
/* Linear probing */
for (i = 0; i < m->table_size; i++) {
if (m->data[i].in_use) {
if (!f(context, m->data[i].data)) {
return 1;
}
}
}
return 0;
}
int hashmap_iterate_pairs(struct hashmap_s *const hashmap,
int (*f)(void *const, struct hashmap_element_s *const),
void *const context) {
unsigned int i;
struct hashmap_element_s *p;
int r;
/* Linear probing */
for (i = 0; i < hashmap->table_size; i++) {
p=&hashmap->data[i];
if (p->in_use) {
r=f(context, p);
switch (r)
{
case -1: /* remove item */
memset(p, 0, sizeof(struct hashmap_element_s));
hashmap->size--;
break;
case 0: /* continue iterating */
break;
default: /* early exit */
return 1;
}
}
}
return 0;
}
void hashmap_destroy(struct hashmap_s *const m) {
free(m->data);
memset(m, 0, sizeof(struct hashmap_s));
}
unsigned hashmap_num_entries(const struct hashmap_s *const m) {
return m->size;
}
unsigned hashmap_crc32_helper(const char *const s, const unsigned len) {
unsigned i;
unsigned crc32val = 0;
#if defined(HASHMAP_SSE42)
for (i = 0; i < len; i++) {
crc32val = _mm_crc32_u8(crc32val, HASHMAP_CAST(unsigned char, s[i]));
}
return crc32val;
#else
// Using polynomial 0x11EDC6F41 to match SSE 4.2's crc function.
static const unsigned crc32_tab[] = {
0x00000000U, 0xF26B8303U, 0xE13B70F7U, 0x1350F3F4U, 0xC79A971FU,
0x35F1141CU, 0x26A1E7E8U, 0xD4CA64EBU, 0x8AD958CFU, 0x78B2DBCCU,
0x6BE22838U, 0x9989AB3BU, 0x4D43CFD0U, 0xBF284CD3U, 0xAC78BF27U,
0x5E133C24U, 0x105EC76FU, 0xE235446CU, 0xF165B798U, 0x030E349BU,
0xD7C45070U, 0x25AFD373U, 0x36FF2087U, 0xC494A384U, 0x9A879FA0U,
0x68EC1CA3U, 0x7BBCEF57U, 0x89D76C54U, 0x5D1D08BFU, 0xAF768BBCU,
0xBC267848U, 0x4E4DFB4BU, 0x20BD8EDEU, 0xD2D60DDDU, 0xC186FE29U,
0x33ED7D2AU, 0xE72719C1U, 0x154C9AC2U, 0x061C6936U, 0xF477EA35U,
0xAA64D611U, 0x580F5512U, 0x4B5FA6E6U, 0xB93425E5U, 0x6DFE410EU,
0x9F95C20DU, 0x8CC531F9U, 0x7EAEB2FAU, 0x30E349B1U, 0xC288CAB2U,
0xD1D83946U, 0x23B3BA45U, 0xF779DEAEU, 0x05125DADU, 0x1642AE59U,
0xE4292D5AU, 0xBA3A117EU, 0x4851927DU, 0x5B016189U, 0xA96AE28AU,
0x7DA08661U, 0x8FCB0562U, 0x9C9BF696U, 0x6EF07595U, 0x417B1DBCU,
0xB3109EBFU, 0xA0406D4BU, 0x522BEE48U, 0x86E18AA3U, 0x748A09A0U,
0x67DAFA54U, 0x95B17957U, 0xCBA24573U, 0x39C9C670U, 0x2A993584U,
0xD8F2B687U, 0x0C38D26CU, 0xFE53516FU, 0xED03A29BU, 0x1F682198U,
0x5125DAD3U, 0xA34E59D0U, 0xB01EAA24U, 0x42752927U, 0x96BF4DCCU,
0x64D4CECFU, 0x77843D3BU, 0x85EFBE38U, 0xDBFC821CU, 0x2997011FU,
0x3AC7F2EBU, 0xC8AC71E8U, 0x1C661503U, 0xEE0D9600U, 0xFD5D65F4U,
0x0F36E6F7U, 0x61C69362U, 0x93AD1061U, 0x80FDE395U, 0x72966096U,
0xA65C047DU, 0x5437877EU, 0x4767748AU, 0xB50CF789U, 0xEB1FCBADU,
0x197448AEU, 0x0A24BB5AU, 0xF84F3859U, 0x2C855CB2U, 0xDEEEDFB1U,
0xCDBE2C45U, 0x3FD5AF46U, 0x7198540DU, 0x83F3D70EU, 0x90A324FAU,
0x62C8A7F9U, 0xB602C312U, 0x44694011U, 0x5739B3E5U, 0xA55230E6U,
0xFB410CC2U, 0x092A8FC1U, 0x1A7A7C35U, 0xE811FF36U, 0x3CDB9BDDU,
0xCEB018DEU, 0xDDE0EB2AU, 0x2F8B6829U, 0x82F63B78U, 0x709DB87BU,
0x63CD4B8FU, 0x91A6C88CU, 0x456CAC67U, 0xB7072F64U, 0xA457DC90U,
0x563C5F93U, 0x082F63B7U, 0xFA44E0B4U, 0xE9141340U, 0x1B7F9043U,
0xCFB5F4A8U, 0x3DDE77ABU, 0x2E8E845FU, 0xDCE5075CU, 0x92A8FC17U,
0x60C37F14U, 0x73938CE0U, 0x81F80FE3U, 0x55326B08U, 0xA759E80BU,
0xB4091BFFU, 0x466298FCU, 0x1871A4D8U, 0xEA1A27DBU, 0xF94AD42FU,
0x0B21572CU, 0xDFEB33C7U, 0x2D80B0C4U, 0x3ED04330U, 0xCCBBC033U,
0xA24BB5A6U, 0x502036A5U, 0x4370C551U, 0xB11B4652U, 0x65D122B9U,
0x97BAA1BAU, 0x84EA524EU, 0x7681D14DU, 0x2892ED69U, 0xDAF96E6AU,
0xC9A99D9EU, 0x3BC21E9DU, 0xEF087A76U, 0x1D63F975U, 0x0E330A81U,
0xFC588982U, 0xB21572C9U, 0x407EF1CAU, 0x532E023EU, 0xA145813DU,
0x758FE5D6U, 0x87E466D5U, 0x94B49521U, 0x66DF1622U, 0x38CC2A06U,
0xCAA7A905U, 0xD9F75AF1U, 0x2B9CD9F2U, 0xFF56BD19U, 0x0D3D3E1AU,
0x1E6DCDEEU, 0xEC064EEDU, 0xC38D26C4U, 0x31E6A5C7U, 0x22B65633U,
0xD0DDD530U, 0x0417B1DBU, 0xF67C32D8U, 0xE52CC12CU, 0x1747422FU,
0x49547E0BU, 0xBB3FFD08U, 0xA86F0EFCU, 0x5A048DFFU, 0x8ECEE914U,
0x7CA56A17U, 0x6FF599E3U, 0x9D9E1AE0U, 0xD3D3E1ABU, 0x21B862A8U,
0x32E8915CU, 0xC083125FU, 0x144976B4U, 0xE622F5B7U, 0xF5720643U,
0x07198540U, 0x590AB964U, 0xAB613A67U, 0xB831C993U, 0x4A5A4A90U,
0x9E902E7BU, 0x6CFBAD78U, 0x7FAB5E8CU, 0x8DC0DD8FU, 0xE330A81AU,
0x115B2B19U, 0x020BD8EDU, 0xF0605BEEU, 0x24AA3F05U, 0xD6C1BC06U,
0xC5914FF2U, 0x37FACCF1U, 0x69E9F0D5U, 0x9B8273D6U, 0x88D28022U,
0x7AB90321U, 0xAE7367CAU, 0x5C18E4C9U, 0x4F48173DU, 0xBD23943EU,
0xF36E6F75U, 0x0105EC76U, 0x12551F82U, 0xE03E9C81U, 0x34F4F86AU,
0xC69F7B69U, 0xD5CF889DU, 0x27A40B9EU, 0x79B737BAU, 0x8BDCB4B9U,
0x988C474DU, 0x6AE7C44EU, 0xBE2DA0A5U, 0x4C4623A6U, 0x5F16D052U,
0xAD7D5351U};
for (i = 0; i < len; i++) {
crc32val = crc32_tab[(HASHMAP_CAST(unsigned char, crc32val) ^
HASHMAP_CAST(unsigned char, s[i]))] ^
(crc32val >> 8);
}
return crc32val;
#endif
}
unsigned hashmap_hash_helper_int_helper(const struct hashmap_s *const m,
const char *const keystring,
const unsigned len) {
unsigned key = hashmap_crc32_helper(keystring, len);
/* Robert Jenkins' 32 bit Mix Function */
key += (key << 12);
key ^= (key >> 22);
key += (key << 4);
key ^= (key >> 9);
key += (key << 10);
key ^= (key >> 2);
key += (key << 7);
key ^= (key >> 12);
/* Knuth's Multiplicative Method */
key = (key >> 3) * 2654435761;
return key % m->table_size;
}
int hashmap_match_helper(const struct hashmap_element_s *const element,
const char *const key, const unsigned len) {
return (element->key_len == len) && (0 == memcmp(element->key, key, len));
}
int hashmap_hash_helper(const struct hashmap_s *const m, const char *const key,
const unsigned len, unsigned *const out_index) {
unsigned int curr;
unsigned int i;
/* If full, return immediately */
if (m->size >= m->table_size) {
return 0;
}
/* Find the best index */
curr = hashmap_hash_helper_int_helper(m, key, len);
/* Linear probing */
for (i = 0; i < HASHMAP_MAX_CHAIN_LENGTH; i++) {
if (!m->data[curr].in_use) {
*out_index = curr;
return 1;
}
if (m->data[curr].in_use &&
hashmap_match_helper(&m->data[curr], key, len)) {
*out_index = curr;
return 1;
}
curr = (curr + 1) % m->table_size;
}
return 0;
}
int hashmap_rehash_iterator(void *const new_hash,
struct hashmap_element_s *const e) {
int temp=hashmap_put(HASHMAP_PTR_CAST(struct hashmap_s *, new_hash),
e->key, e->key_len, e->data);
if (0<temp) {
return 1;
}
/* clear old value to avoid stale pointers */
return -1;
}
/*
* Doubles the size of the hashmap, and rehashes all the elements
*/
int hashmap_rehash_helper(struct hashmap_s *const m) {
/* If this multiplication overflows hashmap_create will fail. */
unsigned new_size = 2 * m->table_size;
struct hashmap_s new_hash;
int flag = hashmap_create(new_size, &new_hash);
if (0!=flag) {
return flag;
}
/* copy the old elements to the new table */
flag = hashmap_iterate_pairs(m, hashmap_rehash_iterator, HASHMAP_PTR_CAST(void *, &new_hash));
if (0!=flag) {
return flag;
}
hashmap_destroy(m);
/* put new hash into old hash structure by copying */
memcpy(m, &new_hash, sizeof(struct hashmap_s));
return 0;
}
#if defined(_MSC_VER)
#pragma warning(pop)
#elif defined(__clang__)
#pragma clang diagnostic pop
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@ -1,122 +0,0 @@
/*
Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com)
All rights reserved.
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.
Source: http://git.ozlabs.org/?p=ccan;a=tree;f=ccan/json;hb=HEAD
*/
#ifndef CCAN_JSON_H
#define CCAN_JSON_H
#include <stdbool.h>
#include <stddef.h>
typedef enum {
JSON_NULL,
JSON_BOOL,
JSON_STRING,
JSON_NUMBER,
JSON_ARRAY,
JSON_OBJECT,
} JsonTag;
typedef struct JsonNode JsonNode;
struct JsonNode
{
/* only if parent is an object or array (NULL otherwise) */
JsonNode *parent;
JsonNode *prev, *next;
/* only if parent is an object (NULL otherwise) */
char *key; /* Must be valid UTF-8. */
JsonTag tag;
union {
/* JSON_BOOL */
bool bool_;
/* JSON_STRING */
char *string_; /* Must be valid UTF-8. */
/* JSON_NUMBER */
double number_;
/* JSON_ARRAY */
/* JSON_OBJECT */
struct {
JsonNode *head, *tail;
} children;
};
};
/*** Encoding, decoding, and validation ***/
JsonNode *json_decode (const char *json);
char *json_encode (const JsonNode *node);
char *json_encode_string (const char *str);
char *json_stringify (const JsonNode *node, const char *space);
void json_delete (JsonNode *node);
bool json_validate (const char *json);
/*** Lookup and traversal ***/
JsonNode *json_find_element (JsonNode *array, int index);
JsonNode *json_find_member (JsonNode *object, const char *key);
JsonNode *json_first_child (const JsonNode *node);
#define json_foreach(i, object_or_array) \
for ((i) = json_first_child(object_or_array); \
(i) != NULL; \
(i) = (i)->next)
/*** Construction and manipulation ***/
JsonNode *json_mknull(void);
JsonNode *json_mkbool(bool b);
JsonNode *json_mkstring(const char *s);
JsonNode *json_mknumber(double n);
JsonNode *json_mkarray(void);
JsonNode *json_mkobject(void);
void json_append_element(JsonNode *array, JsonNode *element);
void json_prepend_element(JsonNode *array, JsonNode *element);
void json_append_member(JsonNode *object, const char *key, JsonNode *value);
void json_prepend_member(JsonNode *object, const char *key, JsonNode *value);
void json_remove_from_parent(JsonNode *node);
/*** Debugging ***/
/*
* Look for structure and encoding problems in a JsonNode or its descendents.
*
* If a problem is detected, return false, writing a description of the problem
* to errmsg (unless errmsg is NULL).
*/
bool json_check(const JsonNode *node, char errmsg[256]);
// Added by Lea Anthony 28/11/2020
int json_array_length(JsonNode *array);
#endif

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +0,0 @@
//
// 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",
};
typedef struct _NSRange {
unsigned long location;
unsigned long length;
} NSRange;
#define NSFontWeightUltraLight -0.8
#define NSFontWeightThin -0.6
#define NSFontWeightLight -0.4
#define NSFontWeightRegular 0.0
#define NSFontWeightMedium 0.23
#define NSFontWeightSemibold 0.3
#define NSFontWeightBold 0.4
#define NSFontWeightHeavy 0.56
#define NSFontWeightBlack 0.62
extern void messageFromWindowCallback(const char *);
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;
typedef struct {
id menuItem;
Menu *menu;
const char *menuID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
// 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, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate, JsonNode* styledLabel);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup);
id GetMenu(Menu *menu);
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA);
id createAttributedStringFromStyledLabel(JsonNode *styledLabel, const char* fontName, int fontSize);
#endif //ASSETS_C_MENU_DARWIN_H

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,266 +0,0 @@
//
// Created by Lea Anthony on 12/1/21.
//
#include "common.h"
#include "traymenu_darwin.h"
#include "trayicons.h"
extern Class trayMenuDelegateClass;
// A cache for all our tray menu icons
// Global because it's a singleton
struct hashmap_s trayIconCache;
TrayMenu* NewTrayMenu(const char* menuJSON) {
TrayMenu* result = malloc(sizeof(TrayMenu));
/*
{"ID":"0","Label":"Test Tray Label","Icon":"","ProcessedMenu":{"Menu":{"Items":[{"ID":"0","Label":"Show Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"1","Label":"Hide Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"2","Label":"Minimise Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"3","Label":"Unminimise Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0}]},"RadioGroups":null}}
*/
JsonNode* processedJSON = json_decode(menuJSON);
if( processedJSON == NULL ) {
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, "Image");
result->fontName = getJSONString(processedJSON, "FontName");
result->RGBA = getJSONString(processedJSON, "RGBA");
getJSONBool(processedJSON, "MacTemplateImage", &result->templateImage);
result->fontSize = 0;
getJSONInt(processedJSON, "FontSize", &result->fontSize);
result->tooltip = NULL;
result->tooltip = getJSONString(processedJSON, "Tooltip");
result->disabled = false;
getJSONBool(processedJSON, "Disabled", &result->disabled);
result->styledLabel = getJSONObject(processedJSON, "StyledLabel");
// Create the menu
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
result->delegate = NULL;
// 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;
return result;
}
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 UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled, JsonNode *styledLabel) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
return;
}
// Update button label
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
id attributedString = NULL;
if( styledLabel != NULL) {
attributedString = createAttributedStringFromStyledLabel(styledLabel, fontName, fontSize);
} else {
attributedString = createAttributedString(label, fontName, fontSize, RGBA);
}
if( tooltip != NULL ) {
msg_id(statusBarButton, s("setToolTip:"), str(tooltip));
}
msg_bool(statusBarButton, s("setEnabled:"), !disabled);
msg_id(statusBarButton, s("setAttributedTitle:"), attributedString);
}
void UpdateTrayIcon(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->icon == NULL ) {
return;
}
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
// Empty icon means remove it
if( STREMPTY(trayMenu->icon) ) {
// Remove image
msg_id(statusBarButton, s("setImage:"), NULL);
return;
}
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
// If we don't have the image in the icon cache then assume it's base64 encoded image data
if (trayImage == NULL) {
trayImage = createImageFromBase64Data(trayMenu->icon, trayMenu->templateImage);
}
msg_int(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg_id(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_reg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = ((id(*)(id, SEL, CGFloat))objc_msgSend)(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg_reg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
msg_uint(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled, trayMenu->styledLabel);
// Update the menu
id menu = GetMenu(trayMenu->menu);
objc_setAssociatedObject(menu, "trayMenuID", str(trayMenu->ID), OBJC_ASSOCIATION_ASSIGN);
// Create delegate
id trayMenuDelegate = msg_reg((id)trayMenuDelegateClass, s("new"));
msg_id(menu, s("setDelegate:"), trayMenuDelegate);
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
// Create menu delegate
trayMenu->delegate = trayMenuDelegate;
msg_id(trayMenu->statusbaritem, s("setMenu:"), menu);
}
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets
// updated with the data from the new menu.
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
// Delete the old menu
DeleteMenu(currentMenu->menu);
if( currentMenu->delegate != NULL ) {
msg_reg(currentMenu->delegate, s("release"));
currentMenu->delegate = NULL;
}
// 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->styledLabel = newMenu->styledLabel;
currentMenu->trayIconPosition = newMenu->trayIconPosition;
currentMenu->icon = newMenu->icon;
}
void DeleteTrayMenu(TrayMenu* trayMenu) {
// Delete the menu
DeleteMenu(trayMenu->menu);
if( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
trayMenu->delegate = NULL;
}
// Free JSON
if (trayMenu->processedJSON != NULL ) {
json_delete(trayMenu->processedJSON);
}
// Free the status item
if ( trayMenu->statusbaritem != NULL ) {
id statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
msg_id(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
msg_reg(trayMenu->statusbaritem, s("release"));
trayMenu->statusbaritem = NULL;
}
// Free the tray menu memory
MEMFREE(trayMenu);
}
void DeleteTrayMenuKeepStatusBarItem(TrayMenu* trayMenu) {
// Delete the menu
DeleteMenu(trayMenu->menu);
if( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
trayMenu->delegate = NULL;
}
// Free JSON
if (trayMenu->processedJSON != NULL ) {
json_delete(trayMenu->processedJSON);
}
// Free the tray menu memory
MEMFREE(trayMenu);
}
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;
}
int length = atoi((const char *)lengthAsString);
// Create the icon and add to the hashmap
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), (id)data, length);
id trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage);
}
}
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

@ -1,51 +0,0 @@
//
// Created by Lea Anthony on 12/1/21.
//
#ifndef TRAYMENU_DARWIN_H
#define TRAYMENU_DARWIN_H
#include "common.h"
#include "menu_darwin.h"
typedef struct {
const char *label;
const char *icon;
const char *ID;
const char *tooltip;
bool templateImage;
const char *fontName;
int fontSize;
const char *RGBA;
bool disabled;
Menu* menu;
id statusbaritem;
unsigned int trayIconPosition;
JsonNode* processedJSON;
JsonNode* styledLabel;
id delegate;
} TrayMenu;
TrayMenu* NewTrayMenu(const char *trayJSON);
void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayIcon(TrayMenu *trayMenu);
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled, JsonNode *styledLabel);
void LoadTrayIcons();
void UnloadTrayIcons();
void DeleteTrayMenu(TrayMenu* trayMenu);
void DeleteTrayMenuKeepStatusBarItem(TrayMenu* trayMenu);
#endif //TRAYMENU_DARWIN_H

View File

@ -1,173 +0,0 @@
//
// Created by Lea Anthony on 12/1/21.
//
#include "common.h"
#include "traymenustore_darwin.h"
#include "traymenu_darwin.h"
#include <stdlib.h>
TrayMenuStore* NewTrayMenuStore() {
TrayMenuStore* result = malloc(sizeof(TrayMenuStore));
// Allocate Tray Menu Store
if( 0 != hashmap_create((const unsigned)4, &result->trayMenuMap)) {
ABORT("[NewTrayMenuStore] Not enough memory to allocate trayMenuMap!");
}
if (pthread_mutex_init(&result->lock, NULL) != 0) {
printf("\n mutex init has failed\n");
exit(1);
}
return result;
}
int dumpTrayMenu(void *const context, struct hashmap_element_s *const e) {
DumpTrayMenu(e->data);
return 0;
}
void DumpTrayMenuStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL);
pthread_mutex_unlock(&store->lock);
}
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
pthread_mutex_lock(&store->lock);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
}
int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
ShowTrayMenu(e->data);
// 0 to retain element, -1 to delete.
return 0;
}
void ShowTrayMenusInStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
}
pthread_mutex_unlock(&store->lock);
}
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data);
return -1;
}
void DeleteTrayMenuStore(TrayMenuStore *store) {
// Delete context menus
if (hashmap_num_entries(&store->trayMenuMap) > 0) {
if (0 != hashmap_iterate_pairs(&store->trayMenuMap, freeTrayMenu, NULL)) {
ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
}
}
// Destroy tray menu map
hashmap_destroy(&store->trayMenuMap);
pthread_mutex_destroy(&store->lock);
}
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
return result;
}
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
}
return result;
}
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* ID) {
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, ID, strlen(ID));
pthread_mutex_unlock(&store->lock);
DeleteTrayMenu(menu);
}
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);
const char *fontName = getJSONString(parsedUpdate, "FontName");
const char *RGBA = getJSONString(parsedUpdate, "RGBA");
int fontSize = 0;
getJSONInt(parsedUpdate, "FontSize", &fontSize);
const char *tooltip = getJSONString(parsedUpdate, "Tooltip");
bool disabled = false;
getJSONBool(parsedUpdate, "Disabled", &disabled);
JsonNode *styledLabel = getJSONObject(parsedUpdate, "StyledLabel");
UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled, styledLabel);
json_delete(parsedUpdate);
}
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
// DumpTrayMenu(newMenu);
// Get the current menu
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
// If we don't have a menu, we create one
if ( currentMenu == NULL ) {
// Store the new menu
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show it
ShowTrayMenu(newMenu);
return;
}
// DumpTrayMenu(currentMenu);
// Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem;
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
pthread_mutex_unlock(&store->lock);
// Delete the current menu
DeleteTrayMenuKeepStatusBarItem(currentMenu);
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show the updated menu
ShowTrayMenu(newMenu);
}

View File

@ -1,36 +0,0 @@
//
// Created by Lea Anthony on 7/1/21.
//
#ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H
#include "traymenu_darwin.h"
#include <pthread.h>
typedef struct {
int dummy;
// This is our tray menu map
// It maps tray IDs to TrayMenu*
struct hashmap_s trayMenuMap;
pthread_mutex_t lock;
} TrayMenuStore;
TrayMenuStore* NewTrayMenuStore();
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON);
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store);
TrayMenu* GetTrayMenuByID(TrayMenuStore* store, const char* menuID);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);
#endif //TRAYMENUSTORE_DARWIN_H

View File

@ -1,115 +0,0 @@
// +build !windows
/**
* Copyright (c) 2014 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See LICENSE for details.
*/
#include "vec.h"
int vec_expand_(char **data, int *length, int *capacity, int memsz) {
if (*length + 1 > *capacity) {
void *ptr;
int n = (*capacity == 0) ? 1 : *capacity << 1;
ptr = realloc(*data, n * memsz);
if (ptr == NULL) return -1;
*data = ptr;
*capacity = n;
}
return 0;
}
int vec_reserve_(char **data, int *length, int *capacity, int memsz, int n) {
(void) length;
if (n > *capacity) {
void *ptr = realloc(*data, n * memsz);
if (ptr == NULL) return -1;
*data = ptr;
*capacity = n;
}
return 0;
}
int vec_reserve_po2_(
char **data, int *length, int *capacity, int memsz, int n
) {
int n2 = 1;
if (n == 0) return 0;
while (n2 < n) n2 <<= 1;
return vec_reserve_(data, length, capacity, memsz, n2);
}
int vec_compact_(char **data, int *length, int *capacity, int memsz) {
if (*length == 0) {
free(*data);
*data = NULL;
*capacity = 0;
return 0;
} else {
void *ptr;
int n = *length;
ptr = realloc(*data, n * memsz);
if (ptr == NULL) return -1;
*capacity = n;
*data = ptr;
}
return 0;
}
int vec_insert_(char **data, int *length, int *capacity, int memsz,
int idx
) {
int err = vec_expand_(data, length, capacity, memsz);
if (err) return err;
memmove(*data + (idx + 1) * memsz,
*data + idx * memsz,
(*length - idx) * memsz);
return 0;
}
void vec_splice_(char **data, int *length, int *capacity, int memsz,
int start, int count
) {
(void) capacity;
memmove(*data + start * memsz,
*data + (start + count) * memsz,
(*length - start - count) * memsz);
}
void vec_swapsplice_(char **data, int *length, int *capacity, int memsz,
int start, int count
) {
(void) capacity;
memmove(*data + start * memsz,
*data + (*length - count) * memsz,
count * memsz);
}
void vec_swap_(char **data, int *length, int *capacity, int memsz,
int idx1, int idx2
) {
unsigned char *a, *b, tmp;
int count;
(void) length;
(void) capacity;
if (idx1 == idx2) return;
a = (unsigned char*) *data + idx1 * memsz;
b = (unsigned char*) *data + idx2 * memsz;
count = memsz;
while (count--) {
tmp = *a;
*a = *b;
*b = tmp;
a++, b++;
}
}

View File

@ -1,181 +0,0 @@
/**
* Copyright (c) 2014 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See LICENSE for details.
*/
#ifndef VEC_H
#define VEC_H
#include <stdlib.h>
#include <string.h>
#define VEC_VERSION "0.2.1"
#define vec_unpack_(v)\
(char**)&(v)->data, &(v)->length, &(v)->capacity, sizeof(*(v)->data)
#define vec_t(T)\
struct { T *data; int length, capacity; }
#define vec_init(v)\
memset((v), 0, sizeof(*(v)))
#define vec_deinit(v)\
( free((v)->data),\
vec_init(v) )
#define vec_push(v, val)\
( vec_expand_(vec_unpack_(v)) ? -1 :\
((v)->data[(v)->length++] = (val), 0), 0 )
#define vec_pop(v)\
(v)->data[--(v)->length]
#define vec_splice(v, start, count)\
( vec_splice_(vec_unpack_(v), start, count),\
(v)->length -= (count) )
#define vec_swapsplice(v, start, count)\
( vec_swapsplice_(vec_unpack_(v), start, count),\
(v)->length -= (count) )
#define vec_insert(v, idx, val)\
( vec_insert_(vec_unpack_(v), idx) ? -1 :\
((v)->data[idx] = (val), 0), (v)->length++, 0 )
#define vec_sort(v, fn)\
qsort((v)->data, (v)->length, sizeof(*(v)->data), fn)
#define vec_swap(v, idx1, idx2)\
vec_swap_(vec_unpack_(v), idx1, idx2)
#define vec_truncate(v, len)\
((v)->length = (len) < (v)->length ? (len) : (v)->length)
#define vec_clear(v)\
((v)->length = 0)
#define vec_first(v)\
(v)->data[0]
#define vec_last(v)\
(v)->data[(v)->length - 1]
#define vec_reserve(v, n)\
vec_reserve_(vec_unpack_(v), n)
#define vec_compact(v)\
vec_compact_(vec_unpack_(v))
#define vec_pusharr(v, arr, count)\
do {\
int i__, n__ = (count);\
if (vec_reserve_po2_(vec_unpack_(v), (v)->length + n__) != 0) break;\
for (i__ = 0; i__ < n__; i__++) {\
(v)->data[(v)->length++] = (arr)[i__];\
}\
} while (0)
#define vec_extend(v, v2)\
vec_pusharr((v), (v2)->data, (v2)->length)
#define vec_find(v, val, idx)\
do {\
for ((idx) = 0; (idx) < (v)->length; (idx)++) {\
if ((v)->data[(idx)] == (val)) break;\
}\
if ((idx) == (v)->length) (idx) = -1;\
} while (0)
#define vec_remove(v, val)\
do {\
int idx__;\
vec_find(v, val, idx__);\
if (idx__ != -1) vec_splice(v, idx__, 1);\
} while (0)
#define vec_reverse(v)\
do {\
int i__ = (v)->length / 2;\
while (i__--) {\
vec_swap((v), i__, (v)->length - (i__ + 1));\
}\
} while (0)
#define vec_foreach(v, var, iter)\
if ( (v)->length > 0 )\
for ( (iter) = 0;\
(iter) < (v)->length && (((var) = (v)->data[(iter)]), 1);\
++(iter))
#define vec_foreach_rev(v, var, iter)\
if ( (v)->length > 0 )\
for ( (iter) = (v)->length - 1;\
(iter) >= 0 && (((var) = (v)->data[(iter)]), 1);\
--(iter))
#define vec_foreach_ptr(v, var, iter)\
if ( (v)->length > 0 )\
for ( (iter) = 0;\
(iter) < (v)->length && (((var) = &(v)->data[(iter)]), 1);\
++(iter))
#define vec_foreach_ptr_rev(v, var, iter)\
if ( (v)->length > 0 )\
for ( (iter) = (v)->length - 1;\
(iter) >= 0 && (((var) = &(v)->data[(iter)]), 1);\
--(iter))
int vec_expand_(char **data, int *length, int *capacity, int memsz);
int vec_reserve_(char **data, int *length, int *capacity, int memsz, int n);
int vec_reserve_po2_(char **data, int *length, int *capacity, int memsz,
int n);
int vec_compact_(char **data, int *length, int *capacity, int memsz);
int vec_insert_(char **data, int *length, int *capacity, int memsz,
int idx);
void vec_splice_(char **data, int *length, int *capacity, int memsz,
int start, int count);
void vec_swapsplice_(char **data, int *length, int *capacity, int memsz,
int start, int count);
void vec_swap_(char **data, int *length, int *capacity, int memsz,
int idx1, int idx2);
typedef vec_t(void*) vec_void_t;
typedef vec_t(char*) vec_str_t;
typedef vec_t(int) vec_int_t;
typedef vec_t(char) vec_char_t;
typedef vec_t(float) vec_float_t;
typedef vec_t(double) vec_double_t;
#endif

View File

@ -1,68 +0,0 @@
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
/* File created by MIDL compiler version 8.01.0622 */
/* @@MIDL_FILE_HEADING( ) */
/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 500
#endif
/* verify that the <rpcsal.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCSAL_H_VERSION__
#define __REQUIRED_RPCSAL_H_VERSION__ 100
#endif
#include "rpc.h"
#include "rpcndr.h"
#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif /* __RPCNDR_H_VERSION__ */
#ifndef __eventtoken_h__
#define __eventtoken_h__
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif
/* Forward Declarations */
#ifdef __cplusplus
extern "C"{
#endif
/* interface __MIDL_itf_eventtoken_0000_0000 */
/* [local] */
// Microsoft Windows
// Copyright (c) Microsoft Corporation. All rights reserved.
#pragma once
typedef struct EventRegistrationToken
{
__int64 value;
} EventRegistrationToken;
extern RPC_IF_HANDLE __MIDL_itf_eventtoken_0000_0000_v0_0_c_ifspec;
extern RPC_IF_HANDLE __MIDL_itf_eventtoken_0000_0000_v0_0_s_ifspec;
/* Additional Prototypes for ALL interfaces */
/* end of Additional Prototypes */
#ifdef __cplusplus
}
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@ -1,144 +0,0 @@
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef __core_webview2_environment_options_h__
#define __core_webview2_environment_options_h__
#include <objbase.h>
#include <wrl/implements.h>
#include "webview2.h"
#define CORE_WEBVIEW_TARGET_PRODUCT_VERSION L"91.0.864.35"
#define COREWEBVIEW2ENVIRONMENTOPTIONS_STRING_PROPERTY(p) \
public: \
HRESULT STDMETHODCALLTYPE get_##p(LPWSTR* value) override { \
if (!value) \
return E_POINTER; \
*value = m_##p.Copy(); \
if ((*value == nullptr) && (m_##p.Get() != nullptr)) \
return HRESULT_FROM_WIN32(GetLastError()); \
return S_OK; \
} \
HRESULT STDMETHODCALLTYPE put_##p(LPCWSTR value) override { \
LPCWSTR result = m_##p.Set(value); \
if ((result == nullptr) && (value != nullptr)) \
return HRESULT_FROM_WIN32(GetLastError()); \
return S_OK; \
} \
\
protected: \
AutoCoMemString m_##p;
#define COREWEBVIEW2ENVIRONMENTOPTIONS_BOOL_PROPERTY(p) \
public: \
HRESULT STDMETHODCALLTYPE get_##p(BOOL* value) override { \
if (!value) \
return E_POINTER; \
*value = m_##p; \
return S_OK; \
} \
HRESULT STDMETHODCALLTYPE put_##p(BOOL value) override { \
m_##p = value; \
return S_OK; \
} \
\
protected: \
BOOL m_##p = FALSE;
// This is a base COM class that implements ICoreWebView2EnvironmentOptions.
template <typename allocate_fn_t,
allocate_fn_t allocate_fn,
typename deallocate_fn_t,
deallocate_fn_t deallocate_fn>
class CoreWebView2EnvironmentOptionsBase
: public Microsoft::WRL::Implements<
Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
ICoreWebView2EnvironmentOptions> {
public:
CoreWebView2EnvironmentOptionsBase() {
// Initialize the target compatible browser version value to the version of
// the browser binaries corresponding to this version of the SDK.
m_TargetCompatibleBrowserVersion.Set(CORE_WEBVIEW_TARGET_PRODUCT_VERSION);
}
protected:
~CoreWebView2EnvironmentOptionsBase(){};
class AutoCoMemString {
public:
AutoCoMemString() {}
~AutoCoMemString() { Release(); }
void Release() {
if (m_string) {
deallocate_fn(m_string);
m_string = nullptr;
}
}
LPCWSTR Set(LPCWSTR str) {
Release();
if (str) {
m_string = MakeCoMemString(str);
}
return m_string;
}
LPCWSTR Get() { return m_string; }
LPWSTR Copy() {
if (m_string)
return MakeCoMemString(m_string);
return nullptr;
}
protected:
LPWSTR MakeCoMemString(LPCWSTR source) {
const size_t length = wcslen(source);
const size_t bytes = (length + 1) * sizeof(*source);
// Ensure we didn't overflow during our size calculation.
if (bytes <= length) {
return nullptr;
}
wchar_t* result = reinterpret_cast<wchar_t*>(allocate_fn(bytes));
if (result)
memcpy(result, source, bytes);
return result;
}
LPWSTR m_string = nullptr;
};
COREWEBVIEW2ENVIRONMENTOPTIONS_STRING_PROPERTY(AdditionalBrowserArguments)
COREWEBVIEW2ENVIRONMENTOPTIONS_STRING_PROPERTY(Language)
COREWEBVIEW2ENVIRONMENTOPTIONS_STRING_PROPERTY(TargetCompatibleBrowserVersion)
COREWEBVIEW2ENVIRONMENTOPTIONS_BOOL_PROPERTY(
AllowSingleSignOnUsingOSPrimaryAccount)
};
template <typename allocate_fn_t,
allocate_fn_t allocate_fn,
typename deallocate_fn_t,
deallocate_fn_t deallocate_fn>
class CoreWebView2EnvironmentOptionsBaseClass
: public Microsoft::WRL::RuntimeClass<
Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
CoreWebView2EnvironmentOptionsBase<allocate_fn_t,
allocate_fn,
deallocate_fn_t,
deallocate_fn>> {
public:
CoreWebView2EnvironmentOptionsBaseClass() {}
protected:
~CoreWebView2EnvironmentOptionsBaseClass() override{};
};
typedef CoreWebView2EnvironmentOptionsBaseClass<decltype(&::CoTaskMemAlloc),
::CoTaskMemAlloc,
decltype(&::CoTaskMemFree),
::CoTaskMemFree>
CoreWebView2EnvironmentOptions;
#endif // __core_webview2_environment_options_h__

View File

@ -1,11 +0,0 @@
# Build
This script will download the given webview2 sdk version and copy out the files necessary for building Wails apps.
## Prerequistes
- nuget
## Usage
`updatesdk.bat <version>`

View File

@ -1 +0,0 @@
The version of WebView2 SDK used: 1.0.992.28

View File

@ -1,18 +0,0 @@
@echo off
IF %1.==. GOTO NoVersion
nuget install microsoft.web.webview2 -Version %1 -OutputDirectory . >NUL || goto :eof
echo Downloaded microsoft.web.webview2.%1
set sdk_version=%1
set native_dir="%~dp0\microsoft.web.webview2.%sdk_version%\build\native"
copy "%native_dir%\include\*.h" .. >NUL
copy "%native_dir%\x64\WebView2Loader.dll" "..\x64" >NUL
@REM @rd /S /Q "microsoft.web.webview2.%sdk_version%"
del /s version.txt >nul 2>&1
echo The version of WebView2 SDK used: %sdk_version% > sdkversion.txt
echo SDK updated to %sdk_version%
goto :eof
:NoVersion
echo Please provide a version number, EG: 1.0.664.37
goto :eof

View File

@ -1,8 +0,0 @@
// +build windows
package x64
import _ "embed"
//go:embed WebView2Loader.dll
var WebView2Loader []byte

View File

@ -1,95 +0,0 @@
//go:build windows
// +build windows
package ffenestri
import (
"fmt"
"os"
"sync"
"text/tabwriter"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/menumanager"
)
/* ---------------------------------------------------------------------------------
Checkbox Cache
--------------
The checkbox cache keeps a list of IDs that are associated with the same checkbox menu item.
This can happen when a checkbox is used in an application menu and a tray menu, eg "start at login".
The cache is used to bulk toggle the menu items when one is clicked.
*/
type CheckboxCache struct {
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
mutex sync.RWMutex
}
func NewCheckboxCache() *CheckboxCache {
return &CheckboxCache{
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
}
}
func (c *CheckboxCache) Dump() {
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
println("---------------- Checkbox", c, "Dump ----------------")
for _, processedMenu := range c.cache {
println("Menu", processedMenu)
for wailsMenuItemID, win32menus := range processedMenu {
println(" WailsMenu: ", wailsMenuItemID)
menus := slicer.String()
for _, win32menu := range win32menus {
menus.Add(fmt.Sprintf("%v", win32menu))
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", wailsMenuItemID, menus.Join(", "))
_ = w.Flush()
}
}
}
func (c *CheckboxCache) addToCheckboxCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, menuID win32MenuItemID) {
// Get map for menu
if c.cache[menu] == nil {
c.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
}
menuMap := c.cache[menu]
// Ensure we have a slice
if menuMap[item] == nil {
menuMap[item] = []win32MenuItemID{}
}
c.mutex.Lock()
menuMap[item] = append(menuMap[item], menuID)
c.mutex.Unlock()
}
func (c *CheckboxCache) removeMenuFromCheckboxCache(menu *menumanager.ProcessedMenu) {
c.mutex.Lock()
delete(c.cache, menu)
c.mutex.Unlock()
}
// win32MenuIDsForWailsMenuID returns all win32menuids that are used for a wails menu item id across
// all menus
func (c *CheckboxCache) win32MenuIDsForWailsMenuID(item wailsMenuItemID) []win32MenuItemID {
c.mutex.Lock()
result := []win32MenuItemID{}
for _, menu := range c.cache {
ids := menu[item]
if ids != nil {
result = append(result, ids...)
}
}
c.mutex.Unlock()
return result
}

View File

@ -1,29 +0,0 @@
//go:build windows && debug
// +build windows,debug
package ffenestri
import (
"fmt"
"github.com/ztrue/tracerr"
"runtime"
"strings"
)
func wall(err error, inputs ...interface{}) error {
if err == nil {
return nil
}
pc, _, _, _ := runtime.Caller(1)
funcName := runtime.FuncForPC(pc).Name()
splitName := strings.Split(funcName, ".")
message := "[" + splitName[len(splitName)-1] + "]"
if len(inputs) > 0 {
params := []string{}
for _, param := range inputs {
params = append(params, fmt.Sprintf("%v", param))
}
message += "(" + strings.Join(params, " ") + ")"
}
return tracerr.Errorf(message)
}

View File

@ -1,48 +0,0 @@
//go:build windows && !debug
// +build windows,!debug
package ffenestri
import "C"
import (
"fmt"
"golang.org/x/sys/windows"
"log"
"os"
"runtime"
"strings"
"syscall"
)
func wall(err error, inputs ...interface{}) error {
if err == nil {
return nil
}
pc, _, _, _ := runtime.Caller(1)
funcName := runtime.FuncForPC(pc).Name()
splitName := strings.Split(funcName, ".")
message := "[" + splitName[len(splitName)-1] + "]"
if len(inputs) > 0 {
params := []string{}
for _, param := range inputs {
params = append(params, fmt.Sprintf("%v", param))
}
message += "(" + strings.Join(params, " ") + ")"
}
title, err := syscall.UTF16PtrFromString("Fatal Error")
if err != nil {
log.Fatal(err)
}
text, err := syscall.UTF16PtrFromString("There has been a fatal error. Details:\n" + message)
if err != nil {
log.Fatal(err)
}
var flags uint32 = windows.MB_ICONERROR | windows.MB_OK
_, err = windows.MessageBox(0, text, title, flags|windows.MB_SYSTEMMODAL)
os.Exit(1)
return err
}

View File

@ -1,233 +0,0 @@
//go:build windows
// +build windows
package ffenestri
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"runtime"
"strings"
)
//-------------------- Types ------------------------
type win32MenuItemID uint32
type win32Menu uintptr
type win32Window uintptr
type wailsMenuItemID string // The internal menu ID
type Menu struct {
wailsMenu *menumanager.WailsMenu
menu win32Menu
menuType menuType
// A list of all checkbox and radio menuitems we
// create for this menu
checkboxes []win32MenuItemID
radioboxes []win32MenuItemID
initiallySelectedRadioItems []win32MenuItemID
}
func createMenu(wailsMenu *menumanager.WailsMenu, menuType menuType) (*Menu, error) {
mainMenu, err := createWin32Menu()
if err != nil {
return nil, err
}
result := &Menu{
wailsMenu: wailsMenu,
menu: mainMenu,
menuType: menuType,
}
// Process top level menus
for _, toplevelmenu := range applicationMenu.Menu.Items {
err := result.processMenuItem(result.menu, toplevelmenu)
if err != nil {
return nil, err
}
}
err = result.processRadioGroups()
if err != nil {
return nil, err
}
return result, nil
}
func (m *Menu) processMenuItem(parent win32Menu, menuItem *menumanager.ProcessedMenuItem) error {
// Ignore hidden items
if menuItem.Hidden {
return nil
}
// Calculate the flags for this menu item
flags := uintptr(calculateFlags(menuItem))
switch menuItem.Type {
case menu.SubmenuType:
submenu, err := createWin32PopupMenu()
if err != nil {
return err
}
for _, submenuItem := range menuItem.SubMenu.Items {
err = m.processMenuItem(submenu, submenuItem)
if err != nil {
return err
}
}
err = appendWin32MenuItem(parent, flags, uintptr(submenu), menuItem.Label)
if err != nil {
return err
}
case menu.TextType, menu.CheckboxType, menu.RadioType:
win32ID := addMenuCacheEntry(parent, m.menuType, menuItem, m.wailsMenu.Menu)
if menuItem.Accelerator != nil {
m.processAccelerator(menuItem)
}
label := menuItem.Label
//label := fmt.Sprintf("%s (%d)", menuItem.Label, win32ID)
err := appendWin32MenuItem(parent, flags, uintptr(win32ID), label)
if err != nil {
return err
}
if menuItem.Type == menu.CheckboxType {
// We need to maintain a list of this menu's checkboxes
m.checkboxes = append(m.checkboxes, win32ID)
globalCheckboxCache.addToCheckboxCache(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
}
if menuItem.Type == menu.RadioType {
// We need to maintain a list of this menu's radioitems
m.radioboxes = append(m.radioboxes, win32ID)
globalRadioGroupMap.addRadioGroupMapping(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
if menuItem.Checked {
m.initiallySelectedRadioItems = append(m.initiallySelectedRadioItems, win32ID)
}
}
case menu.SeparatorType:
err := appendWin32MenuItem(parent, flags, 0, "")
if err != nil {
return err
}
}
return nil
}
func (m *Menu) processRadioGroups() error {
for _, rg := range applicationMenu.RadioGroups {
startWailsMenuID := wailsMenuItemID(rg.Members[0])
endWailsMenuID := wailsMenuItemID(rg.Members[len(rg.Members)-1])
startIDs := globalRadioGroupMap.getRadioGroupMapping(startWailsMenuID)
endIDs := globalRadioGroupMap.getRadioGroupMapping(endWailsMenuID)
var radioGroupMaps = []*radioGroupStartEnd{}
for index := range startIDs {
startID := startIDs[index]
endID := endIDs[index]
thisRadioGroup := &radioGroupStartEnd{
startID: startID,
endID: endID,
}
radioGroupMaps = append(radioGroupMaps, thisRadioGroup)
}
// Set this for each member
for _, member := range rg.Members {
id := wailsMenuItemID(member)
globalRadioGroupCache.addToRadioGroupCache(m.wailsMenu.Menu, id, radioGroupMaps)
}
}
// Enable all initially checked radio items
for _, win32MenuID := range m.initiallySelectedRadioItems {
menuItemDetails := getMenuCacheEntry(win32MenuID)
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
err := selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
if err != nil {
return err
}
}
return nil
}
func (m *Menu) Destroy() error {
// Release the MenuIDs
releaseMenuIDsForProcessedMenu(m.wailsMenu.Menu)
// Unload this menu's checkboxes from the cache
globalCheckboxCache.removeMenuFromCheckboxCache(m.wailsMenu.Menu)
// Unload this menu's radio groups from the cache
globalRadioGroupCache.removeMenuFromRadioBoxCache(m.wailsMenu.Menu)
globalRadioGroupMap.removeMenuFromRadioGroupMapping(m.wailsMenu.Menu)
// Free up callbacks
resetCallbacks()
// Delete menu
return destroyWin32Menu(m.menu)
}
func (m *Menu) processAccelerator(menuitem *menumanager.ProcessedMenuItem) {
// Add in shortcut to label if there is no "\t" override
if !strings.Contains(menuitem.Label, "\t") {
menuitem.Label += "\t" + keys.Stringify(menuitem.Accelerator, runtime.GOOS)
}
// Calculate the modifier
var modifiers uint8
for _, mod := range menuitem.Accelerator.Modifiers {
switch mod {
case keys.ControlKey, keys.CmdOrCtrlKey:
modifiers |= 1
case keys.OptionOrAltKey:
modifiers |= 2
case keys.ShiftKey:
modifiers |= 4
//case keys.SuperKey:
// modifiers |= 8
}
}
var keycode = calculateKeycode(strings.ToLower(menuitem.Accelerator.Key))
if keycode == 0 {
fmt.Printf("WARNING: Key '%s' is unsupported in windows. Cannot bind callback.", menuitem.Accelerator.Key)
return
}
addMenuCallback(keycode, modifiers, menuitem.ID, m.menuType)
}
var flagMap = map[menu.Type]uint32{
menu.TextType: MF_STRING,
menu.SeparatorType: MF_SEPARATOR,
menu.SubmenuType: MF_STRING | MF_POPUP,
menu.CheckboxType: MF_STRING,
menu.RadioType: MF_STRING,
}
func calculateFlags(menuItem *menumanager.ProcessedMenuItem) uint32 {
result := flagMap[menuItem.Type]
if menuItem.Disabled {
result |= MF_DISABLED
}
if menuItem.Type == menu.CheckboxType && menuItem.Checked {
result |= MF_CHECKED
}
return result
}

View File

@ -1,75 +0,0 @@
//go:build windows
// +build windows
package ffenestri
import (
"github.com/leaanthony/idgen"
"github.com/wailsapp/wails/v2/internal/menumanager"
"sync"
)
/**
MenuCache
---------
When windows calls back to Go (when an item is clicked), we need to
be able to retrieve information about the menu item:
- The menu that the menuitem is part of (parent)
- The original processed menu item
- The type of the menu (application, context or tray)
This cache is built up when a menu is created.
*/
// TODO: Make this like the other caches
type menuCacheEntry struct {
parent win32Menu
menuType menuType
item *menumanager.ProcessedMenuItem
processedMenu *menumanager.ProcessedMenu
}
var idGenerator = idgen.New()
var menuCache = map[win32MenuItemID]*menuCacheEntry{}
var menuCacheLock sync.RWMutex
var wailsMenuIDtoWin32IDMap = map[wailsMenuItemID]win32MenuItemID{}
// This releases the menuIDs back to the id generator
var winIDsOwnedByProcessedMenu = map[*menumanager.ProcessedMenu][]win32MenuItemID{}
func releaseMenuIDsForProcessedMenu(processedMenu *menumanager.ProcessedMenu) {
for _, menuID := range winIDsOwnedByProcessedMenu[processedMenu] {
idGenerator.ReleaseID(uint(menuID))
}
delete(winIDsOwnedByProcessedMenu, processedMenu)
}
func addMenuCacheEntry(parent win32Menu, typ menuType, wailsMenuItem *menumanager.ProcessedMenuItem, processedMenu *menumanager.ProcessedMenu) win32MenuItemID {
menuCacheLock.Lock()
defer menuCacheLock.Unlock()
id, err := idGenerator.NewID()
checkFatal(err)
menuID := win32MenuItemID(id)
menuCache[menuID] = &menuCacheEntry{
parent: parent,
menuType: typ,
item: wailsMenuItem,
processedMenu: processedMenu,
}
// save the mapping
wailsMenuIDtoWin32IDMap[wailsMenuItemID(wailsMenuItem.ID)] = menuID
// keep track of menuids owned by this menu (so we can release the ids)
winIDsOwnedByProcessedMenu[processedMenu] = append(winIDsOwnedByProcessedMenu[processedMenu], menuID)
return menuID
}
func getMenuCacheEntry(id win32MenuItemID) *menuCacheEntry {
menuCacheLock.Lock()
defer menuCacheLock.Unlock()
return menuCache[id]
}

View File

@ -1,126 +0,0 @@
package ffenestri
type callbackData struct {
menuID string
menuType menuType
}
var callbacks = map[uint16]map[uint8]callbackData{}
func addMenuCallback(key uint16, modifiers uint8, menuID string, menutype menuType) {
if callbacks[key] == nil {
callbacks[key] = make(map[uint8]callbackData)
}
callbacks[key][modifiers] = callbackData{
menuID: menuID,
menuType: menutype,
}
}
func resetCallbacks() {
callbacks = map[uint16]map[uint8]callbackData{}
}
func getCallbackForKeyPress(key uint16, modifiers uint8) (string, menuType) {
if callbacks[key] == nil {
return "", ""
}
result := callbacks[key][modifiers]
return result.menuID, result.menuType
}
func calculateKeycode(key string) uint16 {
return keymap[key]
}
// TODO: Complete this list
var keymap = map[string]uint16{
"0": 0x30,
"1": 0x31,
"2": 0x32,
"3": 0x33,
"4": 0x34,
"5": 0x35,
"6": 0x36,
"7": 0x37,
"8": 0x38,
"9": 0x39,
"a": 0x41,
"b": 0x42,
"c": 0x43,
"d": 0x44,
"e": 0x45,
"f": 0x46,
"g": 0x47,
"h": 0x48,
"i": 0x49,
"j": 0x4A,
"k": 0x4B,
"l": 0x4C,
"m": 0x4D,
"n": 0x4E,
"o": 0x4F,
"p": 0x50,
"q": 0x51,
"r": 0x52,
"s": 0x53,
"t": 0x54,
"u": 0x55,
"v": 0x56,
"w": 0x57,
"x": 0x58,
"y": 0x59,
"z": 0x5A,
"backspace": 0x08,
"tab": 0x09,
"return": 0x0D,
"enter": 0x0D,
"escape": 0x1B,
"left": 0x25,
"right": 0x27,
"up": 0x26,
"down": 0x28,
"space": 0x20,
"delete": 0x2E,
"home": 0x24,
"end": 0x23,
"page up": 0x21,
"page down": 0x22,
"f1": 0x70,
"f2": 0x71,
"f3": 0x72,
"f4": 0x73,
"f5": 0x74,
"f6": 0x75,
"f7": 0x76,
"f8": 0x77,
"f9": 0x78,
"f10": 0x79,
"f11": 0x7A,
"f12": 0x7B,
"f13": 0x7C,
"f14": 0x7D,
"f15": 0x7E,
"f16": 0x7F,
"f17": 0x80,
"f18": 0x81,
"f19": 0x82,
"f20": 0x83,
"f21": 0x84,
"f22": 0x85,
"f23": 0x86,
"f24": 0x87,
// Windows doesn't have these apparently so use 0 for unsupported
"f25": 0,
"f26": 0,
"f27": 0,
"f28": 0,
"f29": 0,
"f30": 0,
"f31": 0,
"f32": 0,
"f33": 0,
"f34": 0,
"f35": 0,
}

View File

@ -1,195 +0,0 @@
//go:build windows
// +build windows
package ffenestri
import (
"fmt"
"github.com/leaanthony/slicer"
"os"
"sync"
"text/tabwriter"
"github.com/wailsapp/wails/v2/internal/menumanager"
)
/* ---------------------------------------------------------------------------------
Radio Groups
------------
Radio groups are stored by the ProcessedMenu as a list of menu ids.
Windows only cares about the start and end ids of the group so we
preprocess the radio groups and store this data in a radioGroupMap.
When a radio button is clicked, we use the menu id to read in the
radio group data and call CheckMenuRadioItem to update the group.
*/
type radioGroupStartEnd struct {
startID win32MenuItemID
endID win32MenuItemID
}
type RadioGroupCache struct {
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd
mutex sync.RWMutex
}
func NewRadioGroupCache() *RadioGroupCache {
return &RadioGroupCache{
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd),
}
}
func (c *RadioGroupCache) Dump() {
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
println("---------------- RadioGroupCache", c, "Dump ----------------")
for menu, processedMenu := range c.cache {
println("Menu", menu)
_, _ = fmt.Fprintf(w, "Wails ID \tWindows ID Pairs\n")
for wailsMenuItemID, radioGroupStartEnd := range processedMenu {
menus := slicer.String()
for _, se := range radioGroupStartEnd {
menus.Add(fmt.Sprintf("[%d -> %d]", se.startID, se.endID))
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", wailsMenuItemID, menus.Join(", "))
_ = w.Flush()
}
}
}
func (c *RadioGroupCache) addToRadioGroupCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, radioGroupMaps []*radioGroupStartEnd) {
c.mutex.Lock()
// Get map for menu
if c.cache[menu] == nil {
c.cache[menu] = make(map[wailsMenuItemID][]*radioGroupStartEnd)
}
menuMap := c.cache[menu]
// Ensure we have a slice
if menuMap[item] == nil {
menuMap[item] = []*radioGroupStartEnd{}
}
menuMap[item] = radioGroupMaps
c.mutex.Unlock()
}
func (c *RadioGroupCache) removeMenuFromRadioBoxCache(menu *menumanager.ProcessedMenu) {
c.mutex.Lock()
delete(c.cache, menu)
c.mutex.Unlock()
}
func (c *RadioGroupCache) getRadioGroupMappings(wailsMenuID wailsMenuItemID) []*radioGroupStartEnd {
c.mutex.Lock()
result := []*radioGroupStartEnd{}
for _, menugroups := range c.cache {
groups := menugroups[wailsMenuID]
if groups != nil {
result = append(result, groups...)
}
}
c.mutex.Unlock()
return result
}
type RadioGroupMap struct {
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
mutex sync.RWMutex
}
func NewRadioGroupMap() *RadioGroupMap {
return &RadioGroupMap{
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
}
}
func (c *RadioGroupMap) Dump() {
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
println("---------------- RadioGroupMap", c, "Dump ----------------")
for _, processedMenu := range c.cache {
_, _ = fmt.Fprintf(w, "Menu\tWails ID \tWindows IDs\n")
for wailsMenuItemID, win32menus := range processedMenu {
menus := slicer.String()
for _, win32menu := range win32menus {
menus.Add(fmt.Sprintf("%v", win32menu))
}
_, _ = fmt.Fprintf(w, "%p\t%s\t%s\n", processedMenu, wailsMenuItemID, menus.Join(", "))
_ = w.Flush()
}
}
}
func (m *RadioGroupMap) addRadioGroupMapping(menu *menumanager.ProcessedMenu, item wailsMenuItemID, win32ID win32MenuItemID) {
m.mutex.Lock()
// Get map for menu
if m.cache[menu] == nil {
m.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
}
menuMap := m.cache[menu]
// Ensure we have a slice
if menuMap[item] == nil {
menuMap[item] = []win32MenuItemID{}
}
menuMap[item] = append(menuMap[item], win32ID)
m.mutex.Unlock()
}
func (m *RadioGroupMap) removeMenuFromRadioGroupMapping(menu *menumanager.ProcessedMenu) {
m.mutex.Lock()
delete(m.cache, menu)
m.mutex.Unlock()
}
func (m *RadioGroupMap) getRadioGroupMapping(wailsMenuID wailsMenuItemID) []win32MenuItemID {
m.mutex.Lock()
result := []win32MenuItemID{}
for _, menuids := range m.cache {
ids := menuids[wailsMenuID]
if ids != nil {
result = append(result, ids...)
}
}
m.mutex.Unlock()
return result
}
func selectRadioItemFromWailsMenuID(wailsMenuID wailsMenuItemID, win32MenuID win32MenuItemID) error {
radioItemGroups := globalRadioGroupCache.getRadioGroupMappings(wailsMenuID)
// Figure out offset into group
var offset win32MenuItemID = 0
for _, radioItemGroup := range radioItemGroups {
if win32MenuID >= radioItemGroup.startID && win32MenuID <= radioItemGroup.endID {
offset = win32MenuID - radioItemGroup.startID
break
}
}
for _, radioItemGroup := range radioItemGroups {
selectedMenuID := radioItemGroup.startID + offset
menuItemDetails := getMenuCacheEntry(selectedMenuID)
if menuItemDetails != nil {
if menuItemDetails.parent != 0 {
err := selectRadioItem(selectedMenuID, radioItemGroup.startID, radioItemGroup.endID, menuItemDetails.parent)
if err != nil {
return err
}
}
}
}
return nil
}

View File

@ -1,131 +0,0 @@
//go:build windows
// +build windows
package ffenestri
import (
"unsafe"
"github.com/wailsapp/wails/v2/internal/menumanager"
"golang.org/x/sys/windows"
)
var (
// DLL stuff
user32 = windows.NewLazySystemDLL("User32.dll")
win32CreateMenu = user32.NewProc("CreateMenu")
win32DestroyMenu = user32.NewProc("DestroyMenu")
win32CreatePopupMenu = user32.NewProc("CreatePopupMenu")
win32AppendMenuW = user32.NewProc("AppendMenuW")
win32SetMenu = user32.NewProc("SetMenu")
win32CheckMenuItem = user32.NewProc("CheckMenuItem")
win32GetMenuState = user32.NewProc("GetMenuState")
win32CheckMenuRadioItem = user32.NewProc("CheckMenuRadioItem")
applicationMenu *menumanager.WailsMenu
menuManager *menumanager.Manager
)
const MF_BITMAP uint32 = 0x00000004
const MF_CHECKED uint32 = 0x00000008
const MF_DISABLED uint32 = 0x00000002
const MF_ENABLED uint32 = 0x00000000
const MF_GRAYED uint32 = 0x00000001
const MF_MENUBARBREAK uint32 = 0x00000020
const MF_MENUBREAK uint32 = 0x00000040
const MF_OWNERDRAW uint32 = 0x00000100
const MF_POPUP uint32 = 0x00000010
const MF_SEPARATOR uint32 = 0x00000800
const MF_STRING uint32 = 0x00000000
const MF_UNCHECKED uint32 = 0x00000000
const MF_BYCOMMAND uint32 = 0x00000000
const MF_BYPOSITION uint32 = 0x00000400
const WM_SIZE = 5
const WM_GETMINMAXINFO = 36
type Win32Rect struct {
Left int32
Top int32
Right int32
Bottom int32
}
// ------------------- win32 calls -----------------------
func createWin32Menu() (win32Menu, error) {
res, _, err := win32CreateMenu.Call()
if res == 0 {
return 0, wall(err)
}
return win32Menu(res), nil
}
func destroyWin32Menu(menu win32Menu) error {
res, _, err := win32DestroyMenu.Call(uintptr(menu))
if res == 0 {
return wall(err, "Menu:", menu)
}
return nil
}
func createWin32PopupMenu() (win32Menu, error) {
res, _, err := win32CreatePopupMenu.Call()
if res == 0 {
return 0, wall(err)
}
return win32Menu(res), nil
}
func appendWin32MenuItem(menu win32Menu, flags uintptr, submenuOrID uintptr, label string) error {
menuText, err := windows.UTF16PtrFromString(label)
if err != nil {
return err
}
res, _, err := win32AppendMenuW.Call(
uintptr(menu),
flags,
submenuOrID,
uintptr(unsafe.Pointer(menuText)),
)
if res == 0 {
return wall(err, "Menu", menu, "Flags", flags, "submenuOrID", submenuOrID, "label", label)
}
return nil
}
func setWindowMenu(window win32Window, menu win32Menu) error {
res, _, err := win32SetMenu.Call(uintptr(window), uintptr(menu))
if res == 0 {
return wall(err, "window", window, "menu", menu)
}
return nil
}
func selectRadioItem(selectedMenuID, startMenuItemID, endMenuItemID win32MenuItemID, parent win32Menu) error {
res, _, err := win32CheckMenuRadioItem.Call(uintptr(parent), uintptr(startMenuItemID), uintptr(endMenuItemID), uintptr(selectedMenuID), uintptr(MF_BYCOMMAND))
if int(res) == 0 {
return wall(err, selectedMenuID, startMenuItemID, endMenuItemID, parent)
}
return nil
}
//
//func getWindowRect(window win32Window) (*Win32Rect, error) {
// var windowRect Win32Rect
// res, _, err := win32GetWindowRect.Call(uintptr(window), uintptr(unsafe.Pointer(&windowRect)))
// if res == 0 {
// return nil, err
// }
// return &windowRect, nil
//}
//
//func getClientRect(window win32Window) (*Win32Rect, error) {
// var clientRect Win32Rect
// res, _, err := win32GetClientRect.Call(uintptr(window), uintptr(unsafe.Pointer(&clientRect)))
// if res == 0 {
// return nil, err
// }
// return &clientRect, nil
//}
//

View File

@ -1,126 +0,0 @@
#ifndef WV2COMHANDLER_H
#define WV2COMHANDLER_H
#include "ffenestri_windows.h"
#include "windows/WebView2.h"
#include <locale>
#include <codecvt>
class wv2ComHandler
: public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
public ICoreWebView2WebMessageReceivedEventHandler,
public ICoreWebView2PermissionRequestedEventHandler,
public ICoreWebView2AcceleratorKeyPressedEventHandler
{
struct Application *app;
HWND window;
messageCallback mcb;
comHandlerCallback cb;
public:
wv2ComHandler(struct Application *app, HWND window, messageCallback mcb, comHandlerCallback cb) {
this->app = app;
this->window = window;
this->mcb = mcb;
this->cb = cb;
}
ULONG STDMETHODCALLTYPE AddRef() { return 1; }
ULONG STDMETHODCALLTYPE Release() { return 1; }
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) {
return S_OK;
}
HRESULT STDMETHODCALLTYPE Invoke(HRESULT res,
ICoreWebView2Environment *env) {
env->CreateCoreWebView2Controller(window, this);
return S_OK;
}
HRESULT STDMETHODCALLTYPE Invoke(HRESULT res,
ICoreWebView2Controller *controller) {
controller->AddRef();
ICoreWebView2 *webview;
::EventRegistrationToken token;
controller->get_CoreWebView2(&webview);
controller->add_AcceleratorKeyPressed(this, &token);
webview->add_WebMessageReceived(this, &token);
webview->add_PermissionRequested(this, &token);
cb(controller);
return S_OK;
}
// This is our keyboard callback method
HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2Controller *controller, ICoreWebView2AcceleratorKeyPressedEventArgs * args) {
// Prevent WebView2 from processing the key
args->put_Handled(TRUE);
COREWEBVIEW2_KEY_EVENT_KIND kind;
args->get_KeyEventKind(&kind);
if (kind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN ||
kind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN)
{
UINT key;
args->get_VirtualKey(&key);
COREWEBVIEW2_PHYSICAL_KEY_STATUS status;
args->get_PhysicalKeyStatus(&status);
if (!status.WasKeyDown)
{
processKeyPress(key);
}
}
return S_OK;
}
// This is called when JS posts a message back to webkit
HRESULT STDMETHODCALLTYPE Invoke(
ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) {
LPWSTR message;
args->TryGetWebMessageAsString(&message);
if ( message == nullptr ) {
return S_OK;
}
const char *m = LPWSTRToCstr(message);
// check for internal messages
if (strcmp(m, "completed") == 0) {
completed(app);
return S_OK;
}
else if (strcmp(m, "initialised") == 0) {
loadAssets(app);
return S_OK;
}
else if (strcmp(m, "wails-drag") == 0) {
// We don't drag in fullscreen mode
if (!app->isFullscreen) {
ReleaseCapture();
SendMessage(this->window, WM_NCLBUTTONDOWN, HTCAPTION, 0);
}
return S_OK;
}
else {
messageFromWindowCallback(m);
}
delete[] m;
return S_OK;
}
HRESULT STDMETHODCALLTYPE
Invoke(ICoreWebView2 *sender,
ICoreWebView2PermissionRequestedEventArgs *args) {
printf("DDDDDDDDDDDD\n");
COREWEBVIEW2_PERMISSION_KIND kind;
args->get_PermissionKind(&kind);
if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) {
args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW);
}
return S_OK;
}
};
#endif

View File

@ -14,13 +14,6 @@ import (
"github.com/leaanthony/slicer"
)
// LocalDirectory gets the caller's file directory
// Equivalent to node's __DIRNAME
func LocalDirectory() string {
_, thisFile, _, _ := runtime.Caller(1)
return filepath.Dir(thisFile)
}
// RelativeToCwd returns an absolute path based on the cwd
// and the given relative path
func RelativeToCwd(relativePath string) (string, error) {
@ -340,63 +333,6 @@ func CopyDirExtended(src string, dst string, ignore []string) (err error) {
return
}
// MoveDirExtended recursively moves a directory tree, attempting to preserve permissions.
// Source directory must exist, destination directory must *not* exist. It ignores any files or
// directories that are given through the ignore parameter.
// Symlinks are ignored and skipped.
func MoveDirExtended(src string, dst string, ignore []string) (err error) {
ignoreList := slicer.String(ignore)
src = filepath.Clean(src)
dst = filepath.Clean(dst)
si, err := os.Stat(src)
if err != nil {
return err
}
if !si.IsDir() {
return fmt.Errorf("source is not a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
return fmt.Errorf("destination already exists")
}
err = MkDirs(dst)
if err != nil {
return
}
entries, err := os.ReadDir(src)
if err != nil {
return
}
for _, entry := range entries {
if ignoreList.Contains(entry.Name()) {
continue
}
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
// Skip symlinks.
if entry.Type()&os.ModeSymlink != 0 {
continue
}
err := os.Rename(srcPath, dstPath)
if err != nil {
return err
}
}
return
}
func FindPathToFile(fsys fs.FS, file string) (string, error) {
stat, _ := fs.Stat(fsys, file)
if stat != nil {

View File

@ -1,90 +0,0 @@
package messagedispatcher
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Client defines what a frontend client can do
type Client interface {
Quit()
NotifyEvent(message string)
CallResult(message string)
OpenFileDialog(dialogOptions runtime.OpenDialogOptions, callbackID string)
OpenMultipleFilesDialog(dialogOptions runtime.OpenDialogOptions, callbackID string)
OpenDirectoryDialog(dialogOptions runtime.OpenDialogOptions, callbackID string)
SaveDialog(dialogOptions runtime.SaveDialogOptions, callbackID string)
MessageDialog(dialogOptions runtime.MessageDialogOptions, callbackID string)
WindowSetTitle(title string)
WindowShow()
WindowHide()
WindowCenter()
WindowMaximise()
WindowUnmaximise()
WindowMinimise()
WindowUnminimise()
WindowPosition(x int, y int)
WindowSize(width int, height int)
WindowSetMinSize(width int, height int)
WindowSetMaxSize(width int, height int)
WindowFullscreen()
WindowUnfullscreen()
WindowSetColour(colour int)
DarkModeEnabled(callbackID string)
SetApplicationMenu(menuJSON string)
SetTrayMenu(trayMenuJSON string)
UpdateTrayMenuLabel(JSON string)
UpdateContextMenu(contextMenuJSON string)
DeleteTrayMenuByID(id string)
}
// DispatchClient is what the frontends use to interface with the
// dispatcher
type DispatchClient struct {
id string
logger logger.CustomLogger
bus *servicebus.ServiceBus
// Client
frontend Client
}
func newDispatchClient(id string, frontend Client, logger logger.CustomLogger, bus *servicebus.ServiceBus) *DispatchClient {
return &DispatchClient{
id: id,
frontend: frontend,
logger: logger,
bus: bus,
}
}
// DispatchMessage is called by the front ends. It is passed
// an IPC message, translates it to a more concrete message
// type then publishes it on the service bus.
func (d *DispatchClient) DispatchMessage(incomingMessage string) {
// Parse the message
d.logger.Trace(fmt.Sprintf("Received message: %+v", incomingMessage))
parsedMessage, err := message.Parse(incomingMessage)
if err != nil {
d.logger.Error(err.Error())
return
}
// Save this client id
parsedMessage.ClientID = d.id
d.logger.Trace("I got a parsedMessage: %+v", parsedMessage)
// Publish the parsed message
d.bus.PublishForTarget(parsedMessage.Topic, parsedMessage.Data, d.id)
}

View File

@ -1,38 +0,0 @@
package message
import (
"encoding/json"
"fmt"
)
type CallMessage struct {
Name string `json:"name"`
Args []json.RawMessage `json:"args"`
CallbackID string `json:"callbackID,omitempty"`
}
// callMessageParser does what it says on the tin!
func callMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Call messages must be at least 3 bytes `C{}``
if len(message) < 3 {
return nil, fmt.Errorf("call message was an invalid length")
}
callMessage := new(CallMessage)
m := message[1:]
err := json.Unmarshal([]byte(m), callMessage)
if err != nil {
println(err.Error())
return nil, err
}
topic := "call:invoke"
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: callMessage}
return parsedMessage, nil
}

View File

@ -1,43 +0,0 @@
package message
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// ContextMenusOnMessage is used to emit listener registration requests
// on the service bus
type ContextMenusOnMessage struct {
// MenuID is the id of the menu item we are interested in
MenuID string
// Callback is called when the menu is clicked
Callback func(*menu.MenuItem, string)
}
// contextMenusMessageParser does what it says on the tin!
func contextMenusMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Menu messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("context menus message was an invalid length")
}
var topic string
var data interface{}
// Switch the message type
switch message[1] {
case 'C':
contextMenuData := message[2:]
topic = "contextmenus:clicked"
data = contextMenuData
default:
return nil, fmt.Errorf("invalid menu message: %s", message)
}
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: data}
return parsedMessage, nil
}

View File

@ -1,61 +0,0 @@
package message
import (
"encoding/json"
"fmt"
"strings"
)
// dialogMessageParser does what it says on the tin!
func dialogMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Dialog messages must be at least 4 bytes
if len(message) < 4 {
return nil, fmt.Errorf("dialog message was an invalid length")
}
var topic = "bad topic from dialogMessageParser"
var responseMessage *parsedMessage
// Switch the event type (with or without data)
switch message[0] {
// Format of Dialog response messages: D<dialog type><callbackID>|<[]string as json encoded string>
case 'D':
dialogType := message[1]
message = message[2:]
idx := strings.IndexByte(message, '|')
if idx < 0 {
return nil, fmt.Errorf("Invalid dialog response message format: %+v", message)
}
callbackID := message[:idx]
payloadData := message[idx+1:]
switch dialogType {
case 'O':
topic = "dialog:openselected:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData}
case 'D':
topic = "dialog:opendirectoryselected:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData}
case '*':
var data []string
topic = "dialog:openmultipleselected:" + callbackID
err := json.Unmarshal([]byte(payloadData), &data)
if err != nil {
return nil, err
}
responseMessage = &parsedMessage{Topic: topic, Data: data}
case 'S':
topic = "dialog:saveselected:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData}
case 'M':
topic = "dialog:messageselected:" + callbackID
responseMessage = &parsedMessage{Topic: topic, Data: payloadData}
}
default:
return nil, fmt.Errorf("Invalid message to dialogMessageParser()")
}
return responseMessage, nil
}

View File

@ -1,47 +0,0 @@
package message
import (
"encoding/json"
"fmt"
)
type EventMessage struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
type OnEventMessage struct {
Name string
Callback func(optionalData ...interface{})
Counter int
}
// eventMessageParser does what it says on the tin!
func eventMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Event messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("event message was an invalid length")
}
eventMessage := new(EventMessage)
direction := message[1]
// Switch the event type (with or without data)
switch message[0] {
case 'E':
m := message[2:]
err := json.Unmarshal([]byte(m), eventMessage)
if err != nil {
println(err.Error())
return nil, err
}
}
topic := "event:emit:from:" + string(direction)
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: eventMessage}
return parsedMessage, nil
}

View File

@ -1,36 +0,0 @@
package message
import "fmt"
var logMessageMap = map[byte]string{
'P': "log:print",
'T': "log:trace",
'D': "log:debug",
'I': "log:info",
'W': "log:warning",
'E': "log:error",
'F': "log:fatal",
'S': "log:setlevel",
}
// logMessageParser does what it says on the tin!
func logMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Log messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("log message was an invalid length")
}
// Switch on the log type
messageTopic := logMessageMap[message[1]]
// If the type is invalid, raise error
if messageTopic == "" {
return nil, fmt.Errorf("log message type '%c' invalid", message[1])
}
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: messageTopic, Data: message[2:]}
return parsedMessage, nil
}

View File

@ -1,51 +0,0 @@
package message
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// MenuOnMessage is used to emit listener registration requests
// on the service bus
type MenuOnMessage struct {
// MenuID is the id of the menu item we are interested in
MenuID string
// Callback is called when the menu is clicked
Callback func(*menu.MenuItem)
}
// menuMessageParser does what it says on the tin!
func menuMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Menu messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("event message was an invalid length")
}
var topic string
var data interface{}
// Switch the message type
switch message[1] {
case 'C':
callbackid := message[2:]
topic = "menu:clicked"
data = callbackid
case 'o':
callbackid := message[2:]
topic = "menu:ontrayopen"
data = callbackid
case 'c':
callbackid := message[2:]
topic = "menu:ontrayclose"
data = callbackid
default:
return nil, fmt.Errorf("invalid menu message: %s", message)
}
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: data}
return parsedMessage, nil
}

View File

@ -1,40 +0,0 @@
package message
import "fmt"
// Parse
type parsedMessage struct {
Topic string
ClientID string
Data interface{}
}
// Map of different message parsers based on the header byte of the message
var messageParsers = map[byte]func(string) (*parsedMessage, error){
'L': logMessageParser,
'R': runtimeMessageParser,
'E': eventMessageParser,
'C': callMessageParser,
'W': windowMessageParser,
'D': dialogMessageParser,
'S': systemMessageParser,
'M': menuMessageParser,
'T': trayMessageParser,
'X': contextMenusMessageParser,
'U': urlMessageParser,
}
// Parse will attempt to parse the given message
func Parse(message string) (*parsedMessage, error) {
if len(message) == 0 {
return nil, fmt.Errorf("MessageParser received blank message")
}
parseMethod := messageParsers[message[0]]
if parseMethod == nil {
return nil, fmt.Errorf("message type '%c' invalid", message[0])
}
return parseMethod(message)
}

View File

@ -1,37 +0,0 @@
package message
import "fmt"
// runtimeMessageParser does what it says on the tin!
func runtimeMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Log messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("runtime message was an invalid length")
}
// Switch on the runtime module type
module := message[1]
switch module {
case 'B':
return processBrowserMessage(message)
}
return nil, fmt.Errorf("unknown message: %s", message)
}
// processBrowserMessage expects messages of the following format:
// RB<METHOD><DATA>
// O = Open
func processBrowserMessage(message string) (*parsedMessage, error) {
method := message[2]
switch method {
case 'O':
// Open URL
target := message[3:]
return &parsedMessage{Topic: "runtime:browser:open", Data: target}, nil
}
return nil, fmt.Errorf("unknown browser message: %s", message)
}

View File

@ -1,50 +0,0 @@
package message
import (
"fmt"
"strings"
)
// systemMessageParser does what it says on the tin!
func systemMessageParser(message string) (*parsedMessage, error) {
// Sanity check: system messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("system message was an invalid length")
}
var responseMessage *parsedMessage
// Remove 'S'
message = message[1:]
// Switch the event type (with or without data)
switch message[0] {
// 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 {
return nil, fmt.Errorf("Invalid system response message format")
}
callbackID := message[:idx]
payloadData := message[idx+1:]
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"
startupURL := message[1:]
responseMessage = &parsedMessage{Topic: topic, Data: startupURL}
default:
return nil, fmt.Errorf("Invalid message to systemMessageParser()")
}
return responseMessage, nil
}

View File

@ -1,46 +0,0 @@
package message
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// TrayOnMessage is used to emit listener registration requests
// on the service bus
type TrayOnMessage struct {
// MenuID is the id of the menu item we are interested in
MenuID string
// Callback is called when the menu is clicked
Callback func(*menu.MenuItem)
}
// trayMessageParser does what it says on the tin!
func trayMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Menu messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("tray message was an invalid length")
}
var topic string
var data interface{}
// Switch the message type
switch message[1] {
case 'C':
callbackid := message[2:]
topic = "tray:clicked"
data = callbackid
case 'I':
topic = "trayfrontend:seticon"
data = message[2:]
default:
return nil, fmt.Errorf("invalid tray message: %s", message)
}
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: data}
return parsedMessage, nil
}

View File

@ -1,20 +0,0 @@
package message
import "fmt"
// urlMessageParser does what it says on the tin!
func urlMessageParser(message string) (*parsedMessage, error) {
// Sanity check: URL messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("log message was an invalid length")
}
// Switch on the log type
switch message[1] {
case 'C':
return &parsedMessage{Topic: "url:handler", Data: message[2:]}, nil
default:
return nil, fmt.Errorf("url message type '%c' invalid", message[1])
}
}

View File

@ -1,91 +0,0 @@
package message
import "fmt"
// windowMessageParser does what it says on the tin!
func windowMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Window messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("window message was an invalid length")
}
// Extract event type
windowEvent := message[1]
parsedMessage := &parsedMessage{}
// Switch the windowEvent type
switch windowEvent {
// Closed window
case 'C':
parsedMessage.Topic = "quit"
parsedMessage.Data = "Window Closed"
// Center window
case 'c':
parsedMessage.Topic = "window:center"
parsedMessage.Data = ""
// Hide window
case 'H':
parsedMessage.Topic = "window:hide"
parsedMessage.Data = ""
// Show window
case 'S':
parsedMessage.Topic = "window:show"
parsedMessage.Data = ""
// Position window
case 'p':
parsedMessage.Topic = "window:position:" + message[3:]
parsedMessage.Data = ""
// Set window size
case 's':
parsedMessage.Topic = "window:size:" + message[3:]
parsedMessage.Data = ""
// Maximise window
case 'M':
parsedMessage.Topic = "window:maximise"
parsedMessage.Data = ""
// Unmaximise window
case 'U':
parsedMessage.Topic = "window:unmaximise"
parsedMessage.Data = ""
// Minimise window
case 'm':
parsedMessage.Topic = "window:minimise"
parsedMessage.Data = ""
// Unminimise window
case 'u':
parsedMessage.Topic = "window:unminimise"
parsedMessage.Data = ""
// Fullscreen window
case 'F':
parsedMessage.Topic = "window:fullscreen"
parsedMessage.Data = ""
// UnFullscreen window
case 'f':
parsedMessage.Topic = "window:unfullscreen"
parsedMessage.Data = ""
// Set Title
case 'T':
parsedMessage.Topic = "window:settitle"
parsedMessage.Data = message[2:]
// Unknown event type
default:
return nil, fmt.Errorf("unknown message: %s", message)
}
return parsedMessage, nil
}

View File

@ -1,578 +0,0 @@
package messagedispatcher
import (
"context"
"encoding/json"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Dispatcher translates messages received from the frontend
// and publishes them onto the service bus
type Dispatcher struct {
quitChannel <-chan *servicebus.Message
resultChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
servicebus *servicebus.ServiceBus
logger logger.CustomLogger
// Clients
clients map[string]*DispatchClient
lock sync.RWMutex
// Context for cancellation
ctx context.Context
cancel context.CancelFunc
// internal wait group
wg sync.WaitGroup
}
// New dispatcher. Needs a service bus to send to.
func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, error) {
// Subscribe to call result messages
resultChannel, err := servicebus.Subscribe("call:result")
if err != nil {
return nil, err
}
// Subscribe to event messages
eventChannel, err := servicebus.Subscribe("event:emit")
if err != nil {
return nil, err
}
// Subscribe to quit messages
quitChannel, err := servicebus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to window messages
windowChannel, err := servicebus.Subscribe("window")
if err != nil {
return nil, err
}
// Subscribe to dialog events
dialogChannel, err := servicebus.Subscribe("dialog:select")
if err != nil {
return nil, err
}
systemChannel, err := servicebus.Subscribe("system:")
if err != nil {
return nil, err
}
menuChannel, err := servicebus.Subscribe("menufrontend:")
if err != nil {
return nil, err
}
// Create context
ctx, cancel := context.WithCancel(context.Background())
result := &Dispatcher{
servicebus: servicebus,
eventChannel: eventChannel,
logger: logger.CustomLogger("Message Dispatcher"),
clients: make(map[string]*DispatchClient),
resultChannel: resultChannel,
quitChannel: quitChannel,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
ctx: ctx,
cancel: cancel,
}
return result, nil
}
// Start the subsystem
func (d *Dispatcher) Start() error {
d.logger.Trace("Starting")
d.wg.Add(1)
// Spin off a go routine
go func() {
defer d.logger.Trace("Shutdown")
for {
select {
case <-d.ctx.Done():
d.wg.Done()
return
case <-d.quitChannel:
d.processQuit()
case resultMessage := <-d.resultChannel:
d.processCallResult(resultMessage)
case eventMessage := <-d.eventChannel:
d.processEvent(eventMessage)
case windowMessage := <-d.windowChannel:
d.processWindowMessage(windowMessage)
case dialogMessage := <-d.dialogChannel:
d.processDialogMessage(dialogMessage)
case systemMessage := <-d.systemChannel:
d.processSystemMessage(systemMessage)
case menuMessage := <-d.menuChannel:
d.processMenuMessage(menuMessage)
}
}
}()
return nil
}
func (d *Dispatcher) processQuit() {
d.lock.RLock()
defer d.lock.RUnlock()
for _, client := range d.clients {
client.frontend.Quit()
}
}
// RegisterClient will register the given callback with the dispatcher
// and return a DispatchClient that the caller can use to send messages
func (d *Dispatcher) RegisterClient(client Client) *DispatchClient {
d.lock.Lock()
defer d.lock.Unlock()
// Create ID
id := d.getUniqueID()
d.clients[id] = newDispatchClient(id, client, d.logger, d.servicebus)
return d.clients[id]
}
// RemoveClient will remove the registered client
func (d *Dispatcher) RemoveClient(dc *DispatchClient) {
d.lock.Lock()
defer d.lock.Unlock()
delete(d.clients, dc.id)
}
func (d *Dispatcher) getUniqueID() string {
var uid string
for {
uid = crypto.RandomID()
if d.clients[uid] == nil {
break
}
}
return uid
}
func (d *Dispatcher) processCallResult(result *servicebus.Message) {
target := result.Target()
if target == "" {
// This is an error. Calls are 1:1!
d.logger.Fatal("No target for call result: %+v", result)
}
d.lock.RLock()
client := d.clients[target]
d.lock.RUnlock()
if client == nil {
// This is fatal - unknown target!
d.logger.Fatal("Unknown target for call result: %+v", result)
return
}
d.logger.Trace("Sending message to client %s: R%s", target, result.Data().(string))
client.frontend.CallResult(result.Data().(string))
}
// processSystem
func (d *Dispatcher) processSystemMessage(result *servicebus.Message) {
d.logger.Trace("Got system in message dispatcher: %+v", result)
splitTopic := strings.Split(result.Topic(), ":")
command := splitTopic[1]
callbackID := splitTopic[2]
switch command {
case "isdarkmode":
d.lock.RLock()
for _, client := range d.clients {
client.frontend.DarkModeEnabled(callbackID)
break
}
d.lock.RUnlock()
default:
d.logger.Error("Unknown system command: %s", command)
}
}
// processEvent will
func (d *Dispatcher) processEvent(result *servicebus.Message) {
d.logger.Trace("Got event in message dispatcher: %+v", result)
splitTopic := strings.Split(result.Topic(), ":")
eventType := splitTopic[1]
switch eventType {
case "emit":
eventFrom := splitTopic[3]
if eventFrom == "g" {
// This was sent from Go - notify frontend
eventData := result.Data().(*message.EventMessage)
// Unpack event
payload, err := json.Marshal(eventData)
if err != nil {
d.logger.Error("Unable to marshal eventData: %s", err.Error())
return
}
d.lock.RLock()
for _, client := range d.clients {
client.frontend.NotifyEvent(string(payload))
}
d.lock.RUnlock()
}
default:
d.logger.Error("Unknown event type: %s", eventType)
}
}
// processWindowMessage processes messages intended for the window
func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
d.lock.RLock()
defer d.lock.RUnlock()
splitTopic := strings.Split(result.Topic(), ":")
command := splitTopic[1]
switch command {
case "settitle":
title, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid title for 'window:settitle' : %#v", result.Data())
return
}
// Notify clients
for _, client := range d.clients {
client.frontend.WindowSetTitle(title)
}
case "fullscreen":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowFullscreen()
}
case "unfullscreen":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowUnfullscreen()
}
case "setcolour":
colour, ok := result.Data().(int)
if !ok {
d.logger.Error("Invalid colour for 'window:setcolour' : %#v", result.Data())
return
}
// Notify clients
for _, client := range d.clients {
client.frontend.WindowSetColour(colour)
}
case "show":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowShow()
}
case "hide":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowHide()
}
case "center":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowCenter()
}
case "maximise":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowMaximise()
}
case "unmaximise":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowUnmaximise()
}
case "minimise":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowMinimise()
}
case "unminimise":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowUnminimise()
}
case "position":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:position' : %#v", result.Data())
return
}
x, err1 := strconv.Atoi(splitTopic[2])
y, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:position' : %#v", result.Data())
return
}
// Notify clients
for _, client := range d.clients {
client.frontend.WindowPosition(x, y)
}
case "size":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:size' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:size' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSize(w, h)
}
case "minsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:minsize' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:minsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMinSize(w, h)
}
case "maxsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:maxsize' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:maxsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMaxSize(w, h)
}
default:
d.logger.Error("Unknown window command: %s", command)
}
d.logger.Trace("Got window in message dispatcher: %+v", result)
}
// processDialogMessage processes dialog messages
func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 4 {
d.logger.Error("Invalid dialog message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "select":
dialogType := splitTopic[2]
switch dialogType {
case "open":
dialogOptions, ok := result.Data().(runtime.OpenDialogOptions)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:open' : %#v", result.Data())
return
}
// This is hardcoded in the sender too
callbackID := splitTopic[3]
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.OpenFileDialog(dialogOptions, callbackID)
}
case "openmultiple":
dialogOptions, ok := result.Data().(runtime.OpenDialogOptions)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:openmultiple' : %#v", result.Data())
return
}
// This is hardcoded in the sender too
callbackID := splitTopic[3]
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.OpenMultipleFilesDialog(dialogOptions, callbackID)
}
case "directory":
dialogOptions, ok := result.Data().(runtime.OpenDialogOptions)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:directory' : %#v", result.Data())
return
}
// This is hardcoded in the sender too
callbackID := splitTopic[3]
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.OpenDirectoryDialog(dialogOptions, callbackID)
}
case "save":
dialogOptions, ok := result.Data().(runtime.SaveDialogOptions)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:save' : %#v", result.Data())
return
}
// This is hardcoded in the sender too
callbackID := splitTopic[3]
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.SaveDialog(dialogOptions, callbackID)
}
case "message":
dialogOptions, ok := result.Data().(runtime.MessageDialogOptions)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:message' : %#v", result.Data())
return
}
// This is hardcoded in the sender too
callbackID := splitTopic[3]
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.MessageDialog(dialogOptions, callbackID)
}
default:
d.logger.Error("Unknown dialog type: %s", dialogType)
}
default:
d.logger.Error("Unknown dialog command: %s", command)
}
}
func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 2 {
d.logger.Error("Invalid menu message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "updateappmenu":
updatedMenu, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updateappmenu' : %#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.SetApplicationMenu(updatedMenu)
}
case "settraymenu":
trayMenuJSON, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:settraymenu' : %#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.SetTrayMenu(trayMenuJSON)
}
case "updatecontextmenu":
updatedContextMenu, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatecontextmenu' : %#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.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)
}
case "deletetraymenu":
traymenuid, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
result.Data())
return
}
for _, client := range d.clients {
client.frontend.DeleteTrayMenuByID(traymenuid)
}
default:
d.logger.Error("Unknown menufrontend command: %s", command)
}
}
func (d *Dispatcher) Close() {
d.cancel()
d.wg.Wait()
}

View File

@ -1,10 +0,0 @@
# Parse
Parse will attempt to parse your Wails project to perform a number of tasks:
* Verify that you have bound struct pointers
* Generate JS helper files/docs
It currently checks bindings correctly if your code binds using one of the following methods:
* Literal Binding: `app.Bind(&MyStruct{})`
* Variable Binding: `app.Bind(m)` - m can be `m := &MyStruct{}` or `m := newMyStruct()`
* Function Binding: `app.Bind(newMyStruct())`

View File

@ -1,436 +0,0 @@
package main
import (
"fmt"
"go/ast"
"os"
"strings"
"github.com/leaanthony/slicer"
"golang.org/x/tools/go/packages"
)
var structCache = make(map[string]*ParsedStruct)
var boundStructs = make(map[string]*ParsedStruct)
var boundMethods = []string{}
var boundStructPointerLiterals = []string{}
var boundStructLiterals = slicer.StringSlicer{}
var boundVariables = slicer.StringSlicer{}
var app = ""
var structPointerFunctionDecls = make(map[string]string)
var structFunctionDecls = make(map[string]string)
var variableStructDecls = make(map[string]string)
var variableFunctionDecls = make(map[string]string)
type Parameter struct {
Name string
Type string
}
type ParsedMethod struct {
Struct string
Name string
Comments []string
Inputs []*Parameter
Returns []*Parameter
}
type ParsedStruct struct {
Name string
Methods []*ParsedMethod
}
type BoundStructs []*ParsedStruct
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)
os.Exit(1)
}
if packages.PrintErrors(pkgs) > 0 {
os.Exit(1)
}
// Iterate the packages
for _, pkg := range pkgs {
// Iterate the files
for _, file := range pkg.Syntax {
var wailsPkgVar = ""
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
// Parse import declarations
case *ast.ImportSpec:
// Determine what wails has been imported as
if x.Path.Value == `"github.com/wailsapp/wails/v2"` {
wailsPkgVar = x.Name.Name
}
// Parse calls. We are looking for app.Bind() calls
case *ast.CallExpr:
f, ok := x.Fun.(*ast.SelectorExpr)
if ok {
n, ok := f.X.(*ast.Ident)
if ok {
//Check this is the Bind() call associated with the app variable
if n.Name == app && f.Sel.Name == "Bind" {
if len(x.Args) == 1 {
ce, ok := x.Args[0].(*ast.CallExpr)
if ok {
n, ok := ce.Fun.(*ast.Ident)
if ok {
// We found a bind method using a function call
// EG: app.Bind( newMyStruct() )
boundMethods = append(boundMethods, n.Name)
}
} else {
// We also want to check for Bind( &MyStruct{} )
ue, ok := x.Args[0].(*ast.UnaryExpr)
if ok {
if ue.Op.String() == "&" {
cl, ok := ue.X.(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
// We have found Bind( &MyStruct{} )
boundStructPointerLiterals = append(boundStructPointerLiterals, t.Name)
}
}
}
} else {
// Let's check when the user binds a struct,
// rather than a struct pointer: Bind( MyStruct{} )
// We do this to provide better hints to the user
cl, ok := x.Args[0].(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
boundStructLiterals.Add(t.Name)
}
} else {
// Also check for when we bind a variable
// myVariable := &MyStruct{}
// app.Bind( myVariable )
i, ok := x.Args[0].(*ast.Ident)
if ok {
boundVariables.Add(i.Name)
}
}
}
}
}
}
}
}
// We scan assignments for a number of reasons:
// * Determine the variable containing the main application
// * Determine the type of variables that get used in Bind()
// * Determine the type of variables that get created with var := &MyStruct{}
case *ast.AssignStmt:
for _, rhs := range x.Rhs {
ce, ok := rhs.(*ast.CallExpr)
if ok {
se, ok := ce.Fun.(*ast.SelectorExpr)
if ok {
i, ok := se.X.(*ast.Ident)
if ok {
// Have we found the wails package name?
if i.Name == wailsPkgVar {
// Check we are calling a function to create the app
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
if len(x.Lhs) == 1 {
i, ok := x.Lhs[0].(*ast.Ident)
if ok {
// Found the app variable name
app = i.Name
}
}
}
}
}
} else {
// Check for function assignment
// a := newMyStruct()
fe, ok := ce.Fun.(*ast.Ident)
if ok {
if len(x.Lhs) == 1 {
i, ok := x.Lhs[0].(*ast.Ident)
if ok {
// Store the variable -> Function mapping
// so we can later resolve the type
variableFunctionDecls[i.Name] = fe.Name
}
}
}
}
} else {
// Check for literal assignment of struct
// EG: myvar := MyStruct{}
ue, ok := rhs.(*ast.UnaryExpr)
if ok {
cl, ok := ue.X.(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
if len(x.Lhs) == 1 {
i, ok := x.Lhs[0].(*ast.Ident)
if ok {
variableStructDecls[i.Name] = t.Name
}
}
}
}
}
}
}
// We scan for functions to build up a list of function names
// for a number of reasons:
// * Determine which functions are struct methods that are bound
// * Determine
case *ast.FuncDecl:
if x.Recv != nil {
// This is a struct method
for _, field := range x.Recv.List {
se, ok := field.Type.(*ast.StarExpr)
if ok {
// This is a struct pointer method
i, ok := se.X.(*ast.Ident)
if ok {
// If we haven't already found this struct,
// Create a placeholder in the cache
parsedStruct := structCache[i.Name]
if parsedStruct == nil {
structCache[i.Name] = &ParsedStruct{
Name: i.Name,
}
parsedStruct = structCache[i.Name]
}
// If this method is Public
if string(x.Name.Name[0]) == strings.ToUpper((string(x.Name.Name[0]))) {
structMethod := &ParsedMethod{
Struct: i.Name,
Name: x.Name.Name,
}
// Check if the method has comments.
// If so, save it with the parsed method
if x.Doc != nil {
for _, comment := range x.Doc.List {
stringComment := comment.Text
if strings.HasPrefix(stringComment, "//") {
stringComment = stringComment[2:]
}
structMethod.Comments = append(structMethod.Comments, strings.TrimSpace(stringComment))
}
}
// Save the input parameters
for _, inputField := range x.Type.Params.List {
t, ok := inputField.Type.(*ast.Ident)
if !ok {
continue
}
for _, name := range inputField.Names {
structMethod.Inputs = append(structMethod.Inputs, &Parameter{Name: name.Name, Type: t.Name})
}
}
// Save the output parameters
for _, outputField := range x.Type.Results.List {
t, ok := outputField.Type.(*ast.Ident)
if !ok {
continue
}
if len(outputField.Names) == 0 {
structMethod.Returns = append(structMethod.Returns, &Parameter{Type: t.Name})
} else {
for _, name := range outputField.Names {
structMethod.Returns = append(structMethod.Returns, &Parameter{Name: name.Name, Type: t.Name})
}
}
}
// Append this method to the parsed struct
parsedStruct.Methods = append(parsedStruct.Methods, structMethod)
}
}
}
}
} else {
// This is a function declaration
// We care about its name and return type
// This will allow us to resolve types later
functionName := x.Name.Name
// Look for one that returns a single value
if x.Type != nil && x.Type.Results != nil && x.Type.Results.List != nil {
if len(x.Type.Results.List) == 1 {
// Check for *struct
t, ok := x.Type.Results.List[0].Type.(*ast.StarExpr)
if ok {
s, ok := t.X.(*ast.Ident)
if ok {
// println("*** Function", functionName, "found which returns: *"+s.Name)
structPointerFunctionDecls[functionName] = s.Name
}
} else {
// Check for functions that return a struct
// This is to help us provide hints if the user binds a struct
t, ok := x.Type.Results.List[0].Type.(*ast.Ident)
if ok {
// println("*** Function", functionName, "found which returns: "+t.Name)
structFunctionDecls[functionName] = t.Name
}
}
}
}
}
}
return true
})
// spew.Dump(file)
}
}
/***** Update bound structs ******/
// Resolve bound Methods
for _, method := range boundMethods {
s, ok := structPointerFunctionDecls[method]
if !ok {
s, ok = structFunctionDecls[method]
if !ok {
println("Fatal: Bind statement using", method, "but cannot find", method, "declaration")
} else {
println("Fatal: Cannot bind struct using method `" + method + "` because it returns a struct (" + s + "). Return a pointer to " + s + " instead.")
}
os.Exit(1)
}
structDefinition := structCache[s]
if structDefinition == nil {
println("Fatal: Bind statement using `"+method+"` but cannot find struct", s, "definition")
os.Exit(1)
}
boundStructs[s] = structDefinition
}
// Resolve bound vars
for _, structLiteral := range boundStructPointerLiterals {
s, ok := structCache[structLiteral]
if !ok {
println("Fatal: Bind statement using", structLiteral, "but cannot find", structLiteral, "declaration")
os.Exit(1)
}
boundStructs[structLiteral] = s
}
// Resolve bound variables
boundVariables.Each(func(variable string) {
v, ok := variableStructDecls[variable]
if !ok {
method, ok := variableFunctionDecls[variable]
if !ok {
println("Fatal: Bind statement using variable `" + variable + "` which does not resolve to a struct pointer")
os.Exit(1)
}
// Resolve function name
v, ok = structPointerFunctionDecls[method]
if !ok {
v, ok = structFunctionDecls[method]
if !ok {
println("Fatal: Bind statement using", method, "but cannot find", method, "declaration")
} else {
println("Fatal: Cannot bind variable `" + variable + "` because it resolves to a struct (" + v + "). Return a pointer to " + v + " instead.")
}
os.Exit(1)
}
}
s, ok := structCache[v]
if !ok {
println("Fatal: Bind statement using variable `" + variable + "` which resolves to a `" + v + "` but cannot find its declaration")
os.Exit(1)
}
boundStructs[v] = s
})
// Check for struct literals
boundStructLiterals.Each(func(structName string) {
println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.")
os.Exit(1)
})
// Check for bound variables
// boundVariables.Each(func(varName string) {
// println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.")
// })
// spew.Dump(boundStructs)
// os.Exit(0)
// }
// Inspect the AST and print all identifiers and literals.
println("export {")
noOfStructs := len(boundStructs)
structCount := 0
for _, s := range boundStructs {
structCount++
println()
println(" " + s.Name + ": {")
println()
noOfMethods := len(s.Methods)
for methodCount, m := range s.Methods {
println(" /****************")
for _, comment := range m.Comments {
println(" *", comment)
}
if len(m.Comments) > 0 {
println(" *")
}
inputNames := ""
for _, input := range m.Inputs {
println(" * @param {"+input.Type+"}", input.Name)
inputNames += input.Name + ", "
}
print(" * @return Promise<")
for _, output := range m.Returns {
print(output.Type + "|")
}
println("Error>")
println(" *")
println(" ***/")
if len(inputNames) > 2 {
inputNames = inputNames[:len(inputNames)-2]
}
println(" ", m.Name+": function("+inputNames+") {")
println(" return window.go." + s.Name + "." + m.Name + "(" + inputNames + ");")
print(" }")
if methodCount < noOfMethods-1 {
print(",")
}
println()
println()
}
print(" }")
if structCount < noOfStructs-1 {
print(",")
}
println()
}
println()
println("}")
println()
return nil, nil
}

View File

@ -1,17 +0,0 @@
package servicebus
import (
"context"
"log"
"runtime"
)
func ExtractBus(ctx context.Context) *ServiceBus {
bus := ctx.Value("bus")
if bus == nil {
pc, _, _, _ := runtime.Caller(1)
funcName := runtime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': Application not initialised", funcName)
}
return bus.(*ServiceBus)
}

View File

@ -1,43 +0,0 @@
package servicebus
// Message is a service bus message that contains a
// topic and data
type Message struct {
topic string
data interface{}
target string
}
// NewMessage creates a new message with the given
// topic and data
func NewMessage(topic string, data interface{}) *Message {
return &Message{
topic: topic,
data: data,
}
}
// NewMessageForTarget creates a new message with the given
// topic and data
func NewMessageForTarget(topic string, data interface{}, target string) *Message {
return &Message{
topic: topic,
data: data,
target: target,
}
}
// Topic returns the message topic
func (m *Message) Topic() string {
return m.topic
}
// Data returns the message data
func (m *Message) Data() interface{} {
return m.data
}
// Target returns the message Target
func (m *Message) Target() string {
return m.target
}

View File

@ -1,181 +0,0 @@
package servicebus
import (
"context"
"fmt"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
)
// ServiceBus is a messaging bus for Wails applications
type ServiceBus struct {
listeners map[string][]chan *Message
messageQueue chan *Message
lock sync.RWMutex
closed bool
debug bool
logger logger.CustomLogger
ctx context.Context
cancel context.CancelFunc
}
// New creates a new ServiceBus
// The internal message queue is set to 100 messages
// Listener queues are set to 10
func New(logger *logger.Logger) *ServiceBus {
ctx, cancel := context.WithCancel(context.Background())
return &ServiceBus{
listeners: make(map[string][]chan *Message),
messageQueue: make(chan *Message, 100),
logger: logger.CustomLogger("Service Bus"),
ctx: ctx,
cancel: cancel,
}
}
// dispatch the given message to the listeners
func (s *ServiceBus) dispatchMessage(message *Message) {
// Lock to prevent additions to the listeners
s.lock.RLock()
defer s.lock.RUnlock()
// Iterate over listener's topics
for topic := range s.listeners {
// If the topic matches
if strings.HasPrefix(message.Topic(), topic) {
// Iterate over the listeners
for _, callback := range s.listeners[topic] {
// Process the message
callback <- message
}
}
}
}
// Debug puts the service bus into debug mode.
func (s *ServiceBus) Debug() {
s.debug = true
}
// Start the service bus
func (s *ServiceBus) Start() error {
// Prevent starting when closed
if s.closed {
return fmt.Errorf("cannot call start on closed servicebus")
}
s.logger.Trace("Starting")
go func() {
defer s.logger.Trace("Stopped")
// Loop until we get a quit message
for {
select {
case <-s.ctx.Done():
return
// Listen for messages
case message := <-s.messageQueue:
// Log message if in debug mode
if s.debug {
s.logger.Trace("Got message: { Topic: %s, Interface: %#v }", message.Topic(), message.Data())
}
// Dispatch message
s.dispatchMessage(message)
}
}
}()
return nil
}
// Stop the service bus
func (s *ServiceBus) Stop() error {
// Prevent subscribing when closed
if s.closed {
return fmt.Errorf("cannot call stop on closed servicebus")
}
s.closed = true
// Send quit message
s.cancel()
// Close down subscriber channels
s.lock.Lock()
defer s.lock.Unlock()
for _, subscribers := range s.listeners {
for _, channel := range subscribers {
close(channel)
}
}
// Close message queue
close(s.messageQueue)
return nil
}
// UnSubscribe removes the listeners for the given topic (Use with caution!)
func (s *ServiceBus) UnSubscribe(topic string) {
// Prevent any reads or writes to the listeners whilst
// we create a new one
s.lock.Lock()
defer s.lock.Unlock()
s.listeners[topic] = nil
}
// Subscribe is used to register a listener's interest in a topic
func (s *ServiceBus) Subscribe(topic string) (<-chan *Message, error) {
// Prevent subscribing when closed
if s.closed {
return nil, fmt.Errorf("cannot call subscribe on closed servicebus")
}
// Prevent any reads or writes to the listeners whilst
// we create a new one
s.lock.Lock()
defer s.lock.Unlock()
// Append the new listener
listener := make(chan *Message, 10)
s.listeners[topic] = append(s.listeners[topic], listener)
return (<-chan *Message)(listener), nil
}
// Publish sends the given message on the service bus
func (s *ServiceBus) Publish(topic string, data interface{}) {
// Prevent publish when closed
if s.closed {
return
}
message := NewMessage(topic, data)
s.messageQueue <- message
}
// PublishForTarget sends the given message on the service bus for the given target
func (s *ServiceBus) PublishForTarget(topic string, data interface{}, target string) {
// Prevent publish when closed
if s.closed {
return
}
message := NewMessageForTarget(topic, data, target)
s.messageQueue <- message
}

View File

@ -1,230 +0,0 @@
package servicebus
import (
"sync"
"testing"
"github.com/matryer/is"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Person interface {
FullName() string
}
type person struct {
Firstname string
Lastname string
}
func newPerson(firstname string, lastname string) *person {
result := &person{}
result.Firstname = firstname
result.Lastname = lastname
return result
}
func (p *person) FullName() string {
return p.Firstname + " " + p.Lastname
}
func TestSingleTopic(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
messageChannel, _ := bus.Subscribe("hello")
wg.Add(1)
go func() {
message := <-messageChannel
actual = message.Data().(string)
wg.Done()
}()
bus.Start()
bus.Publish("hello", "I am a message!")
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}
func TestMultipleTopics(t *testing.T) {
is := is.New(t)
var hello string
var world string
var expected string = "Hello World!"
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
// Create subscriptions
helloChannel, _ := bus.Subscribe("hello")
worldChannel, _ := bus.Subscribe("world")
wg.Add(1)
go func() {
counter := 2
for counter > 0 {
select {
case helloMessage := <-helloChannel:
hello = helloMessage.Data().(string)
counter--
case worldMessage := <-worldChannel:
world = worldMessage.Data().(string)
counter--
}
}
wg.Done()
}()
bus.Start()
bus.Publish("hello", "Hello ")
bus.Publish("world", "World!")
wg.Wait()
bus.Stop()
is.Equal(hello+world, expected)
}
func TestSingleTopicWildcard(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
messageChannel, _ := bus.Subscribe("hello")
wg.Add(1)
go func() {
message := <-messageChannel
actual = message.Data().(string)
wg.Done()
}()
bus.Start()
bus.Publish("hello:wildcard:test", "I am a message!")
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}
func TestMultipleTopicsWildcard(t *testing.T) {
is := is.New(t)
var hello string
var world string
var expected string = "Hello World!"
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
helloChannel, _ := bus.Subscribe("hello")
worldChannel, _ := bus.Subscribe("world")
wg.Add(1)
go func() {
counter := 2
for counter > 0 {
select {
case helloMessage := <-helloChannel:
hello = helloMessage.Data().(string)
counter--
case worldMessage := <-worldChannel:
world = worldMessage.Data().(string)
counter--
}
}
wg.Done()
}()
bus.Start()
bus.Publish("hello:wildcard:test", "Hello ")
bus.Publish("world:wildcard:test", "World!")
wg.Wait()
bus.Stop()
is.Equal(hello+world, expected)
}
func TestStructData(t *testing.T) {
is := is.New(t)
var expected string = "Tom Jones"
var actual string
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
messageChannel, _ := bus.Subscribe("person")
wg.Add(1)
go func() {
message := <-messageChannel
p := message.Data().(*person)
actual = p.FullName()
wg.Done()
}()
bus.Start()
bus.Publish("person", newPerson("Tom", "Jones"))
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}
func TestErrors(t *testing.T) {
is := is.New(t)
// Create new bus
bus := New(logger.New())
_, err := bus.Subscribe("person")
is.NoErr(err)
err = bus.Start()
is.NoErr(err)
err = bus.Publish("person", newPerson("Tom", "Jones"))
is.NoErr(err)
err = bus.Stop()
is.NoErr(err)
err = bus.Stop()
is.True(err != nil)
err = bus.Start()
is.True(err != nil)
_, err = bus.Subscribe("person")
is.True(err != nil)
err = bus.Publish("person", newPerson("Tom", "Jones"))
is.True(err != nil)
}

View File

@ -1,64 +0,0 @@
package subsystem
import (
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Binding is the Binding subsystem. It manages all service bus messages
// starting with "binding".
type Binding struct {
bindingChannel <-chan *servicebus.Message
running bool
// Binding db
bindings *binding.Bindings
// logger
logger logger.CustomLogger
}
// NewBinding creates a new binding subsystem. Uses the given bindings db for reference.
func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *binding.Bindings) (*Binding, error) {
// Subscribe to event messages
bindingChannel, err := bus.Subscribe("binding")
if err != nil {
return nil, err
}
result := &Binding{
bindingChannel: bindingChannel,
logger: logger.CustomLogger("Binding Subsystem"),
bindings: bindings,
}
return result, nil
}
// Start the subsystem
func (b *Binding) Start() error {
b.running = true
b.logger.Trace("Starting")
// Spin off a go routine
go func() {
for b.running {
select {
case bindingMessage := <-b.bindingChannel:
b.logger.Trace("Got binding message: %+v", bindingMessage)
}
}
b.logger.Trace("Shutdown")
}()
return nil
}
func (b *Binding) Close() {
b.running = false
}

View File

@ -1,202 +0,0 @@
package subsystem
import (
"context"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Call is the Call subsystem. It manages all service bus messages
// starting with "call".
type Call struct {
callChannel <-chan *servicebus.Message
// quit flag
shouldQuit bool
// bindings DB
DB *binding.DB
// ServiceBus
bus *servicebus.ServiceBus
// logger
logger logger.CustomLogger
// context
ctx context.Context
// parent waitgroup
wg *sync.WaitGroup
}
// NewCall creates a new call subsystem
func NewCall(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB) (*Call, error) {
// Subscribe to event messages
callChannel, err := bus.Subscribe("call:invoke")
if err != nil {
return nil, err
}
result := &Call{
callChannel: callChannel,
logger: logger.CustomLogger("Call Subsystem"),
DB: DB,
bus: bus,
ctx: ctx,
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
}
return result, nil
}
// Start the subsystem
func (c *Call) Start() error {
c.wg.Add(1)
// Spin off a go routine
go func() {
defer c.logger.Trace("Shutdown")
for {
select {
case <-c.ctx.Done():
c.wg.Done()
return
case callMessage := <-c.callChannel:
c.processCall(callMessage)
}
}
}()
return nil
}
func (c *Call) processCall(callMessage *servicebus.Message) {
c.logger.Trace("Got message: %+v", callMessage)
// Extract payload
payload := callMessage.Data().(*message.CallMessage)
// Lookup method
registeredMethod := c.DB.GetMethod(payload.Name)
// Check if it's a system call
if strings.HasPrefix(payload.Name, ".wails.") {
c.processSystemCall(payload, callMessage.Target())
return
}
// Check we have it
if registeredMethod == nil {
c.sendError(fmt.Errorf("Method not registered"), payload, callMessage.Target())
return
}
c.logger.Trace("Got registered method: %+v", registeredMethod)
args, err := registeredMethod.ParseArgs(payload.Args)
if err != nil {
c.sendError(fmt.Errorf("Error parsing arguments: %s", err.Error()), payload, callMessage.Target())
}
result, err := registeredMethod.Call(args)
if err != nil {
c.sendError(err, payload, callMessage.Target())
return
}
c.logger.Trace("registeredMethod.Call: %+v, %+v", result, err)
// process result
c.sendResult(result, payload, callMessage.Target())
}
func (c *Call) processSystemCall(payload *message.CallMessage, clientID string) {
c.logger.Trace("Got internal System call: %+v", payload)
callName := strings.TrimPrefix(payload.Name, ".wails.")
switch callName {
case "Dialog.Open":
var dialogOptions runtime.OpenDialogOptions
err := json.Unmarshal(payload.Args[0], &dialogOptions)
if err != nil {
c.logger.Error("Error decoding: %s", err)
}
result, err := runtime.OpenFileDialog(c.ctx, dialogOptions)
if err != nil {
c.logger.Error("Error: %s", err)
}
c.sendResult(result, payload, clientID)
case "Dialog.Save":
var dialogOptions runtime.SaveDialogOptions
err := json.Unmarshal(payload.Args[0], &dialogOptions)
if err != nil {
c.logger.Error("Error decoding: %s", err)
}
result, err := runtime.SaveFileDialog(c.ctx, dialogOptions)
if err != nil {
c.logger.Error("Error: %s", err)
}
c.sendResult(result, payload, clientID)
case "Dialog.Message":
var dialogOptions runtime.MessageDialogOptions
err := json.Unmarshal(payload.Args[0], &dialogOptions)
if err != nil {
c.logger.Error("Error decoding: %s", err)
}
result, err := runtime.MessageDialog(c.ctx, dialogOptions)
if err != nil {
c.logger.Error("Error: %s", err)
}
c.sendResult(result, payload, clientID)
default:
c.logger.Error("Unknown system call: %+v", callName)
}
}
func (c *Call) sendResult(result interface{}, payload *message.CallMessage, clientID string) {
c.logger.Trace("Sending success result with CallbackID '%s' : %+v\n", payload.CallbackID, result)
incomingMessage := &CallbackMessage{
Result: result,
CallbackID: payload.CallbackID,
}
messageData, err := json.Marshal(incomingMessage)
c.logger.Trace("json incomingMessage data: %+v\n", string(messageData))
if err != nil {
// what now?
c.logger.Fatal(err.Error())
}
c.bus.PublishForTarget("call:result", string(messageData), clientID)
}
func (c *Call) sendError(err error, payload *message.CallMessage, clientID string) {
c.logger.Trace("Sending error result with CallbackID '%s' : %+v\n", payload.CallbackID, err.Error())
incomingMessage := &CallbackMessage{
Err: err.Error(),
CallbackID: payload.CallbackID,
}
messageData, err := json.Marshal(incomingMessage)
c.logger.Trace("json incomingMessage data: %+v\n", string(messageData))
if err != nil {
// what now?
c.logger.Fatal(err.Error())
}
c.bus.PublishForTarget("call:result", string(messageData), clientID)
}
// CallbackMessage defines a message that contains the result of a call
type CallbackMessage struct {
Result interface{} `json:"result"`
Err string `json:"error"`
CallbackID string `json:"callbackid"`
}

View File

@ -1,191 +0,0 @@
package subsystem
import (
"context"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// eventListener holds a callback function which is invoked when
// the event listened for is emitted. It has a counter which indicates
// how the total number of events it is interested in. A value of zero
// means it does not expire (default).
type eventListener struct {
callback func(...interface{}) // Function to call with emitted event data
counter int // The number of times this callback may be called. -1 = infinite
delete bool // Flag to indicate that this listener should be deleted
}
// Event is the Eventing subsystem. It manages all service bus messages
// starting with "event".
type Event struct {
eventChannel <-chan *servicebus.Message
// Event listeners
listeners map[string][]*eventListener
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
// ctx
ctx context.Context
// parent waitgroup
wg *sync.WaitGroup
}
// NewEvent creates a new log subsystem
func NewEvent(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) {
// Subscribe to event messages
eventChannel, err := bus.Subscribe("event")
if err != nil {
return nil, err
}
result := &Event{
eventChannel: eventChannel,
logger: logger.CustomLogger("Event Subsystem"),
listeners: make(map[string][]*eventListener),
ctx: ctx,
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
}
return result, nil
}
// RegisterListener provides a means of subscribing to events of type "eventName"
func (e *Event) RegisterListener(eventName string, callback func(...interface{}), counter int) {
// Create new eventListener
thisListener := &eventListener{
callback: callback,
counter: counter,
delete: false,
}
e.notifyLock.Lock()
// Append the new listener to the listeners slice
e.listeners[eventName] = append(e.listeners[eventName], thisListener)
e.notifyLock.Unlock()
}
// Start the subsystem
func (e *Event) Start() error {
e.logger.Trace("Starting")
e.wg.Add(1)
// Spin off a go routine
go func() {
defer e.logger.Trace("OnShutdown")
for {
select {
case <-e.ctx.Done():
e.wg.Done()
return
case eventMessage := <-e.eventChannel:
splitTopic := strings.Split(eventMessage.Topic(), ":")
eventType := splitTopic[1]
switch eventType {
case "emit":
if len(splitTopic) != 4 {
e.logger.Error("Received emit message with invalid topic format. Expected 4 sections in topic, got %s", splitTopic)
continue
}
eventSource := splitTopic[3]
e.logger.Trace("Got Event Message: %s %+v", eventMessage.Topic(), eventMessage.Data())
event := eventMessage.Data().(*message.EventMessage)
eventName := event.Name
switch eventSource {
case "j":
// Notify Go Subscribers
e.logger.Trace("Notify Go subscribers to event '%s'", eventName)
go e.notifyListeners(eventName, event)
case "g":
// Notify Go listeners
e.logger.Trace("Got Go Event: %s", eventName)
go e.notifyListeners(eventName, event)
default:
e.logger.Error("unknown emit event message: %+v", eventMessage)
}
case "on":
// We wish to subscribe to an event channel
var message *message.OnEventMessage = eventMessage.Data().(*message.OnEventMessage)
eventName := message.Name
callback := message.Callback
e.RegisterListener(eventName, callback, message.Counter)
e.logger.Trace("Registered listener for event '%s' with callback %p", eventName, callback)
default:
e.logger.Error("unknown event message: %+v", eventMessage)
}
}
}
}()
return nil
}
// Notifies listeners for the given event name
func (e *Event) notifyListeners(eventName string, message *message.EventMessage) {
// Get list of event listeners
listeners := e.listeners[eventName]
if listeners == nil {
e.logger.Trace("No listeners for event '%s'", eventName)
return
}
// Lock the listeners
e.notifyLock.Lock()
// We have a dirty flag to indicate that there are items to delete
itemsToDelete := false
// Callback in goroutine
for _, listener := range listeners {
if listener.counter > 0 {
listener.counter--
}
go listener.callback(message.Data...)
if listener.counter == 0 {
listener.delete = true
itemsToDelete = true
}
}
// Do we have items to delete?
if itemsToDelete == true {
// Create a new Listeners slice
var newListeners = []*eventListener{}
// Iterate over current listeners
for _, listener := range listeners {
// If we aren't deleting the listener, add it to the new list
if !listener.delete {
newListeners = append(newListeners, listener)
}
}
// Save new listeners or remove entry
if len(newListeners) > 0 {
e.listeners[eventName] = newListeners
} else {
delete(e.listeners, eventName)
}
}
// Unlock
e.notifyLock.Unlock()
}

View File

@ -1,50 +0,0 @@
package subsystem
import (
"os"
"sync"
"testing"
"github.com/matryer/is"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
func TestSingleTopic(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.TRACE)
bus := servicebus.New(myLogger)
eventSubsystem, _ := NewEvent(bus, myLogger)
eventSubsystem.Start()
eventSubsystem.RegisterListener("test", func(data ...interface{}) {
is.Equal(len(data), 1)
actual = data[0].(string)
wg.Done()
})
wg.Add(1)
eventMessage := &message.EventMessage{
Name: "test",
Data: []interface{}{"I am a message!"},
}
bus.Start()
bus.Publish("event:test:from:j", eventMessage)
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}

View File

@ -1,111 +0,0 @@
package subsystem
import (
"context"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Log is the Logging subsystem. It handles messages with topics starting
// with "log:"
type Log struct {
logChannel <-chan *servicebus.Message
// quit flag
shouldQuit bool
// Logger!
logger *logger.Logger
// Context for shutdown
ctx context.Context
cancel context.CancelFunc
// internal waitgroup
wg sync.WaitGroup
}
// NewLog creates a new log subsystem
func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger) (*Log, error) {
// Subscribe to log messages
logChannel, err := bus.Subscribe("log")
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
result := &Log{
logChannel: logChannel,
logger: logger,
ctx: ctx,
cancel: cancel,
}
return result, nil
}
// Start the subsystem
func (l *Log) Start() error {
l.wg.Add(1)
// Spin off a go routine
go func() {
defer l.logger.Trace("Logger Shutdown")
for l.shouldQuit == false {
select {
case <-l.ctx.Done():
l.wg.Done()
return
case logMessage := <-l.logChannel:
logType := strings.TrimPrefix(logMessage.Topic(), "log:")
switch logType {
case "print":
l.logger.Print(logMessage.Data().(string))
case "trace":
l.logger.Trace(logMessage.Data().(string))
case "debug":
l.logger.Debug(logMessage.Data().(string))
case "info":
l.logger.Info(logMessage.Data().(string))
case "warning":
l.logger.Warning(logMessage.Data().(string))
case "error":
l.logger.Error(logMessage.Data().(string))
case "fatal":
l.logger.Fatal(logMessage.Data().(string))
case "setlevel":
switch inLevel := logMessage.Data().(type) {
case logger.LogLevel:
l.logger.SetLogLevel(inLevel)
case string:
uint64level, err := strconv.ParseUint(inLevel, 10, 8)
if err != nil {
l.logger.Error("Error parsing log level: %+v", inLevel)
continue
}
level := logger.LogLevel(uint64level)
l.logger.SetLogLevel(level)
}
default:
l.logger.Error("unknown log message: %+v", logMessage)
}
}
}
}()
return nil
}
func (l *Log) Close() {
l.cancel()
l.wg.Wait()
}

View File

@ -1,176 +0,0 @@
package subsystem
import (
"context"
"encoding/json"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Menu is the subsystem that handles the operation of menus. It manages all service bus messages
// starting with "menu".
type Menu struct {
menuChannel <-chan *servicebus.Message
// shutdown flag
shouldQuit bool
// logger
logger logger.CustomLogger
// Service Bus
bus *servicebus.ServiceBus
// Menu Manager
menuManager *menumanager.Manager
// ctx
ctx context.Context
// parent waitgroup
wg *sync.WaitGroup
}
// NewMenu creates a new menu subsystem
func NewMenu(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, menuManager *menumanager.Manager) (*Menu, error) {
// Subscribe to menu messages
menuChannel, err := bus.Subscribe("menu:")
if err != nil {
return nil, err
}
result := &Menu{
menuChannel: menuChannel,
logger: logger.CustomLogger("Menu Subsystem"),
bus: bus,
menuManager: menuManager,
ctx: ctx,
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
}
return result, nil
}
// Start the subsystem
func (m *Menu) Start() error {
m.logger.Trace("Starting")
m.wg.Add(1)
// Spin off a go routine
go func() {
defer m.logger.Trace("Shutdown")
for {
select {
case <-m.ctx.Done():
m.wg.Done()
return
case menuMessage := <-m.menuChannel:
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "ontrayopen":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuOpen(trayID)
case "ontrayclose":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuClose(trayID)
case "clicked":
if len(splitTopic) != 2 {
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
continue
}
m.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
type ClickCallbackMessage struct {
MenuItemID string `json:"menuItemID"`
MenuType string `json:"menuType"`
Data string `json:"data"`
ParentID string `json:"parentID"`
}
var callbackData ClickCallbackMessage
payload := []byte(menuMessage.Data().(string))
err := json.Unmarshal(payload, &callbackData)
if err != nil {
m.logger.Error("%s", err.Error())
return
}
err = m.menuManager.ProcessClick(callbackData.MenuItemID, callbackData.Data, callbackData.MenuType, callbackData.ParentID)
if err != nil {
m.logger.Trace("%s", err.Error())
}
// Make sure we catch any menu updates
case "updateappmenu":
updatedMenu, err := m.menuManager.UpdateApplicationMenu()
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updateappmenu", updatedMenu)
case "updatecontextmenu":
contextMenu := menuMessage.Data().(*menu.ContextMenu)
updatedMenu, err := m.menuManager.UpdateContextMenu(contextMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatecontextmenu", updatedMenu)
case "settraymenu":
trayMenu := menuMessage.Data().(*menu.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:settraymenu", updatedMenu)
case "deletetraymenu":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
trayID, err := m.menuManager.GetTrayID(trayMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:deletetraymenu", trayID)
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)
}
}
}
}()
return nil
}

View File

@ -1,103 +0,0 @@
package subsystem
import (
"context"
"fmt"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
"strings"
"sync"
)
// Runtime is the Runtime subsystem. It handles messages with topics starting
// with "runtime:"
type Runtime struct {
runtimeChannel <-chan *servicebus.Message
// The hooks channel allows us to hook into frontend startup
hooksChannel <-chan *servicebus.Message
startupCallback func(ctx context.Context)
shutdownCallback func()
// quit flag
shouldQuit bool
logger logger.CustomLogger
//ctx
ctx context.Context
// OnStartup Hook
startupOnce sync.Once
// Service bus
bus *servicebus.ServiceBus
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(context.Context)) (*Runtime, error) {
// Subscribe to log messages
runtimeChannel, err := bus.Subscribe("runtime:")
if err != nil {
return nil, err
}
// Subscribe to log messages
hooksChannel, err := bus.Subscribe("hooks:")
if err != nil {
return nil, err
}
result := &Runtime{
runtimeChannel: runtimeChannel,
hooksChannel: hooksChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
startupCallback: startupCallback,
bus: bus,
}
result.ctx = context.WithValue(ctx, "bus", bus)
return result, nil
}
// Start the subsystem
func (r *Runtime) Start() error {
// Spin off a go routine
go func() {
defer r.logger.Trace("OnShutdown")
for {
select {
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 {
r.startupOnce.Do(func() {
go func() {
r.startupCallback(r.ctx)
// If we got a url, publish it now startup completed
url, ok := hooksMessage.Data().(string)
if ok && len(url) > 0 {
r.bus.Publish("url:handler", url)
}
}()
})
} else {
r.logger.Warning("no startup callback registered!")
}
default:
r.logger.Error("unknown hook message: %+v", hooksMessage)
continue
}
case <-r.ctx.Done():
return
}
}
}()
return nil
}

Some files were not shown because too many files have changed in this diff Show More