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:
parent
c13c4a29f5
commit
57c9158551
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
48
.vscode/launch.json
vendored
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -1,8 +0,0 @@
|
||||
{
|
||||
"go.formatTool": "goimports",
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"files.associations": {
|
||||
"__locale": "c",
|
||||
"ios": "c"
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
//go:build bindings
|
||||
// +build bindings
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
@ -1,7 +1,7 @@
|
||||
//go:build darwin && !bindings
|
||||
// +build darwin,!bindings
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
@ -1,7 +1,7 @@
|
||||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
func IsDebug() bool {
|
||||
return true
|
@ -1,7 +1,7 @@
|
||||
//go:build !dev && !production && !bindings && darwin
|
||||
// +build !dev,!production,!bindings,darwin
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,7 +1,7 @@
|
||||
//go:build !dev && !production && !bindings && linux
|
||||
// +build !dev,!production,!bindings,linux
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,7 +1,7 @@
|
||||
//go:build !dev && !production && !bindings && windows
|
||||
// +build !dev,!production,!bindings,windows
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"os/exec"
|
@ -1,7 +1,7 @@
|
||||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,7 +1,7 @@
|
||||
//go:build linux && !bindings
|
||||
// +build linux,!bindings
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
@ -1,7 +1,7 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
func IsDebug() bool {
|
||||
return false
|
@ -1,7 +1,7 @@
|
||||
//go:build production
|
||||
// +build production
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,7 +1,7 @@
|
||||
//go:build windows && !bindings
|
||||
// +build windows,!bindings
|
||||
|
||||
package appng
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
@ -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)
|
||||
}
|
@ -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
|
@ -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
|
||||
-
|
@ -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;
|
||||
}
|
@ -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
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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
|
@ -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);
|
||||
|
||||
}
|
@ -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
@ -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*);
|
@ -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))
|
||||
}
|
@ -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
|
@ -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))
|
||||
}
|
@ -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
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
|
||||
#ifndef FFENESTRI_LINUX_H
|
||||
#define FFENESTRI_LINUX_H
|
||||
|
||||
|
||||
#endif
|
@ -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) {
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
@ -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
@ -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
@ -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
@ -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);
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
@ -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
|
@ -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++;
|
||||
}
|
||||
}
|
@ -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
|
@ -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
@ -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__
|
@ -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>`
|
@ -1 +0,0 @@
|
||||
The version of WebView2 SDK used: 1.0.992.28
|
@ -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
|
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package x64
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed WebView2Loader.dll
|
||||
var WebView2Loader []byte
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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]
|
||||
}
|
@ -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,
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
//}
|
||||
//
|
@ -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
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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])
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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())`
|
@ -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
|
||||
}
|
0
v2/internal/runtime/js/package-lock.json
generated
0
v2/internal/runtime/js/package-lock.json
generated
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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"`
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user