Fix hacked GTK3/GLFW crash by vendoring a patched sqweek/dialog
The built hacked binary segfaults on Linux the moment a native GTK3 dialog (file/folder browse) opens while a GLFW/OpenGL context is live: sqweek/dialog's Linux backend never calls XInitThreads() before gtk_init(), and GLFW's own X11 backend installs a process-wide Xlib error handler that collides with GTK's. Vendor a pinned copy of github.com/sqweek/dialog (New BSD) into modding/sqweek-dialog/ so it can be patched like every other vendored source here: sqweek-dialog-patches/01-xinitthreads.patch adds the missing XInitThreads() call, and hacked-patches/01-use-patched-dialog.patch adds a go.mod replace directive pointing hacked's own sqweek/dialog dependency at that local copy. build-hacked.sh now stages both hacked/ and sqweek-dialog/ into a scratch modding/build/ directory and applies their patch series before compiling, rather than building hacked/ in place; .gitignore picks up that new scratch directory. Also expand modding/README.md: number its existing sections and add a worked "Example: Editing a message" walkthrough covering Static World Data, the Messages window's clipboard-only text editing, saving the Mod Location, and the absolute-path requirement for run.sh's mod-folder argument.
This commit is contained in:
+3
-1
@@ -43,5 +43,7 @@
|
|||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
|
|
||||||
# modding/hacked build output (see modding/README.md)
|
# modding/hacked build output and scratch, patched source copies (see
|
||||||
|
# modding/README.md)
|
||||||
/modding/dist/
|
/modding/dist/
|
||||||
|
/modding/build/
|
||||||
|
|||||||
+32
-3
@@ -8,7 +8,7 @@ Both pieces here come from the [InkyBlackness](https://inkyblackness.github.io)
|
|||||||
project and are vendored unmodified, each under its own upstream license (see
|
project and are vendored unmodified, each under its own upstream license (see
|
||||||
below) - neither is relicensed by this repository's own MIT license.
|
below) - neither is relicensed by this repository's own MIT license.
|
||||||
|
|
||||||
## Layout
|
## 1. Layout
|
||||||
|
|
||||||
- `hacked/` - [InkyBlackness HackEd](https://github.com/inkyblackness/hacked),
|
- `hacked/` - [InkyBlackness HackEd](https://github.com/inkyblackness/hacked),
|
||||||
the graphical mod editor. A vendored source snapshot (New BSD License),
|
the graphical mod editor. A vendored source snapshot (New BSD License),
|
||||||
@@ -38,7 +38,7 @@ support (level/resource files, `res/data/`); this folder is about the
|
|||||||
external tools that create and inspect that content, not about changing the
|
external tools that create and inspect that content, not about changing the
|
||||||
engine itself.
|
engine itself.
|
||||||
|
|
||||||
## Building `hacked`
|
## 2. Building `hacked`
|
||||||
|
|
||||||
Packages `dist/<version>.linux64.tgz` and `dist/<version>.win64.zip` (native
|
Packages `dist/<version>.linux64.tgz` and `dist/<version>.win64.zip` (native
|
||||||
Linux build, Windows amd64 cross-compiled via mingw-w64 - both from the same
|
Linux build, Windows amd64 cross-compiled via mingw-w64 - both from the same
|
||||||
@@ -67,7 +67,7 @@ Run the result: `dist/linux/hacked/hacked` on Linux,
|
|||||||
`dist\win\hacked\hacked.exe` on Windows. Needs OpenGL 3.2; usage is documented
|
`dist\win\hacked\hacked.exe` on Windows. Needs OpenGL 3.2; usage is documented
|
||||||
on [hacked's own wiki](https://github.com/inkyblackness/hacked/wiki).
|
on [hacked's own wiki](https://github.com/inkyblackness/hacked/wiki).
|
||||||
|
|
||||||
### Publishing the build-image
|
### 2.1. Publishing the build-image
|
||||||
|
|
||||||
Same registry as the root project's own build-image (see the repository
|
Same registry as the root project's own build-image (see the repository
|
||||||
root's `registry.env.example`), under a separate image name so the two don't
|
root's `registry.env.example`), under a separate image name so the two don't
|
||||||
@@ -82,3 +82,32 @@ Reads `REGISTRY`/`REGISTRY_USER`/`REGISTRY_PASSWORD` and (optionally)
|
|||||||
Don't bump `hacked-build-image/VERSION` just because the Dockerfile changed -
|
Don't bump `hacked-build-image/VERSION` just because the Dockerfile changed -
|
||||||
only once a build with the current version has actually succeeded, same
|
only once a build with the current version has actually succeeded, same
|
||||||
policy as the root project's own `build-image/VERSION`.
|
policy as the root project's own `build-image/VERSION`.
|
||||||
|
|
||||||
|
## 3. Example: Editing a message
|
||||||
|
|
||||||
|
`hacked` has no inline text editor - every text field is edited by right-click
|
||||||
|
-> **Copy to Clipboard**, editing the text in an external editor, then
|
||||||
|
right-click -> **Copy from Clipboard** back into the field. Example: changing
|
||||||
|
Rebecca Lansing's first email.
|
||||||
|
|
||||||
|
1. Run `dist/linux/hacked/hacked` (or the Windows build).
|
||||||
|
2. **Window -> Project.** Under **Static World Data**, **Add...** and browse to
|
||||||
|
the game's whole `res/assets/ss_ee/data/` directory (see the root
|
||||||
|
[README.md](../README.md) section 3 for where that comes from), so every
|
||||||
|
`.res`/`.dat` file is available.
|
||||||
|
3. **Window -> Messages** (F5). **Message Type**: `Mails`. **Index**: `26`
|
||||||
|
(Rebecca's first mail). Edit **Sender**/**Subject**/**Title**/the message
|
||||||
|
text via the clipboard round-trip above.
|
||||||
|
4. Back in the **Project** window, under **Mod Location**, click **Save**
|
||||||
|
(first time: **Browse...** to an empty folder, for example
|
||||||
|
`./modding/mymod`).
|
||||||
|
5. Test it from the root project's desktop build:
|
||||||
|
```sh
|
||||||
|
./dist/run.sh "$(pwd)/modding/mymod"
|
||||||
|
```
|
||||||
|
The mod-folder argument **must be an absolute path**: `run.sh` `cd`s into
|
||||||
|
`dist/` before launching the engine, so a relative path resolves against
|
||||||
|
the wrong directory and the mod silently fails to load (the engine's
|
||||||
|
`ProcessModDirectory()` reports nothing on a bad path). When the path is
|
||||||
|
right, the terminal shows `ProcessModDirectory ...` / `Found resource
|
||||||
|
file: ...` / `Loading mod file ...` for confirmation.
|
||||||
|
|||||||
@@ -14,12 +14,30 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
MODDING_ROOT="$(pwd)"
|
MODDING_ROOT="$(pwd)"
|
||||||
HACKED_SRC="$MODDING_ROOT/hacked"
|
|
||||||
OUT_DIR="$MODDING_ROOT/dist"
|
OUT_DIR="$MODDING_ROOT/dist"
|
||||||
|
SCRATCH_DIR="$MODDING_ROOT/build"
|
||||||
|
HACKED_SRC="$SCRATCH_DIR/hacked"
|
||||||
|
DIALOG_SRC="$SCRATCH_DIR/sqweek-dialog"
|
||||||
|
|
||||||
VERSION="hacked-$(date -u +%Y%m%d)"
|
VERSION="hacked-$(date -u +%Y%m%d)"
|
||||||
echo "Version: $VERSION"
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
echo "== Preparing scratch, patched copies of hacked/ and sqweek-dialog/ =="
|
||||||
|
# hacked/ and sqweek-dialog/ are vendored unmodified (see README.md) - never
|
||||||
|
# built or patched in place. hacked-patches/'s go.mod edit points
|
||||||
|
# sqweek/dialog at ../sqweek-dialog (a sibling directory here), so both
|
||||||
|
# scratch copies must keep that same relative layout.
|
||||||
|
rm -rf "$SCRATCH_DIR"
|
||||||
|
mkdir -p "$SCRATCH_DIR"
|
||||||
|
cp -a "$MODDING_ROOT/hacked" "$HACKED_SRC"
|
||||||
|
cp -a "$MODDING_ROOT/sqweek-dialog" "$DIALOG_SRC"
|
||||||
|
for p in "$MODDING_ROOT"/hacked-patches/*.patch; do
|
||||||
|
patch -p1 -d "$HACKED_SRC" < "$p"
|
||||||
|
done
|
||||||
|
for p in "$MODDING_ROOT"/sqweek-dialog-patches/*.patch; do
|
||||||
|
patch -p1 -d "$DIALOG_SRC" < "$p"
|
||||||
|
done
|
||||||
|
|
||||||
rm -rf "$OUT_DIR"
|
rm -rf "$OUT_DIR"
|
||||||
mkdir -p "$OUT_DIR/linux/hacked" "$OUT_DIR/win/hacked"
|
mkdir -p "$OUT_DIR/linux/hacked" "$OUT_DIR/win/hacked"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
--- a/go.mod
|
||||||
|
+++ b/go.mod
|
||||||
|
@@ -2,6 +2,10 @@
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
+// Local, patched copy fixing a GTK3/GLFW X11 crash on Linux - see
|
||||||
|
+// ../sqweek-dialog-patches/ and README.md.
|
||||||
|
+replace github.com/sqweek/dialog => ../sqweek-dialog
|
||||||
|
+
|
||||||
|
require (
|
||||||
|
github.com/asticode/go-astisub v0.12.1
|
||||||
|
github.com/go-gl/gl v0.0.0-20210315015930-ae072cafe09d
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
--- a/dlgs_linux.go
|
||||||
|
+++ b/dlgs_linux.go
|
||||||
|
@@ -1,8 +1,9 @@
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
-// #cgo pkg-config: gtk+-3.0
|
||||||
|
+// #cgo pkg-config: gtk+-3.0 x11
|
||||||
|
// #include <gtk/gtk.h>
|
||||||
|
// #include <stdlib.h>
|
||||||
|
+// #include <X11/Xlib.h>
|
||||||
|
// static GtkWidget* msgdlg(GtkWindow *parent, GtkDialogFlags flags, GtkMessageType type, GtkButtonsType buttons, char *msg) {
|
||||||
|
// return gtk_message_dialog_new(parent, flags, type, buttons, "%s", msg);
|
||||||
|
// }
|
||||||
|
@@ -13,6 +14,7 @@
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
+ C.XInitThreads()
|
||||||
|
C.gtk_init(nil, nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
run:
|
||||||
|
deadline: 5m
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
|
||||||
|
# linters are disabled if their majority of issues is considered false-positive (intended code)
|
||||||
|
# and the remaining issues (if existing) aren't worth it.
|
||||||
|
disable:
|
||||||
|
- gochecknoinits
|
||||||
|
- gochecknoglobals
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false # disable filtering of defaults for better zero-issue policy
|
||||||
|
max-per-linter: 0 # disable limit; report all issues of a linter
|
||||||
|
max-same-issues: 0 # disable limit; report all issues of the same issue
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2018, the dialog authors.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# dialog
|
||||||
|
Simple cross-platform dialog API for go-lang
|
||||||
|
|
||||||
|
# examples
|
||||||
|
ok := dialog.Message("%s", "Do you want to continue?").Title("Are you sure?").YesNo()
|
||||||
|
|
||||||
|
Creates a dialog box titled "Are you sure?", containing the message "Do you want to continue?",
|
||||||
|
a "Yes" button and a "No" button. Returns true iff the dialog could be displayed and the user
|
||||||
|
pressed the "Yes" button.
|
||||||
|
|
||||||
|
filename, err := dialog.File().Filter("Mp3 audio file", "mp3").Load()
|
||||||
|
|
||||||
|
Creates a file selection dialog allowing the user to select a .mp3 file. The absolute path of
|
||||||
|
the file is returned, unless an error is encountered or the user cancels/closes the dialog.
|
||||||
|
In the latter case, `filename` will be the empty string and `err` will equal `dialog.Cancelled`.
|
||||||
|
|
||||||
|
filename, err := dialog.File().Filter("XML files", "xml").Title("Export to XML").Save()
|
||||||
|
|
||||||
|
Asks the user for a filename to write data into. If the user selects a file which already exists,
|
||||||
|
an additional dialog is spawned to confirm they want to overwrite the existing file.
|
||||||
|
|
||||||
|
directory, err := dialog.Directory().Title("Load images").Browse()
|
||||||
|
|
||||||
|
Asks the user for a directory.
|
||||||
|
|
||||||
|
# platform details
|
||||||
|
* OSX: uses Cocoa's NSAlert/NSSavePanel/NSOpenPanel classes
|
||||||
|
* Win32: uses MessageBox/GetOpenFileName/GetSaveFileName (via package github.com/TheTitanrain/w32)
|
||||||
|
* Linux: uses Gtk's MessageDialog/FileChooserDialog (via cgo; requires gtk3 development packages)
|
||||||
|
|
||||||
|
# build
|
||||||
|
```
|
||||||
|
go mod download
|
||||||
|
go build
|
||||||
|
```
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#include <objc/NSObjCRuntime.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MSG_YESNO,
|
||||||
|
MSG_ERROR,
|
||||||
|
MSG_INFO,
|
||||||
|
} AlertStyle;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char* msg;
|
||||||
|
char* title;
|
||||||
|
AlertStyle style;
|
||||||
|
} AlertDlgParams;
|
||||||
|
|
||||||
|
#define LOADDLG 0
|
||||||
|
#define SAVEDLG 1
|
||||||
|
#define DIRDLG 2 // browse for directory
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int mode; /* which dialog style to invoke (see earlier defines) */
|
||||||
|
char* buf; /* buffer to store selected file */
|
||||||
|
int nbuf; /* number of bytes allocated at buf */
|
||||||
|
char* title; /* title for dialog box (can be nil) */
|
||||||
|
void** exts; /* list of valid extensions (elements actual type is NSString*) */
|
||||||
|
int numext; /* number of items in exts */
|
||||||
|
int relaxext; /* allow other extensions? */
|
||||||
|
} FileDlgParams;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
DLG_OK,
|
||||||
|
DLG_CANCEL,
|
||||||
|
DLG_URLFAIL,
|
||||||
|
} DlgResult;
|
||||||
|
|
||||||
|
DlgResult alertDlg(AlertDlgParams*);
|
||||||
|
DlgResult fileDlg(FileDlgParams*);
|
||||||
|
|
||||||
|
void* NSStr(void* buf, int len);
|
||||||
|
void NSRelease(void* obj);
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#include "dlg.h"
|
||||||
|
|
||||||
|
void* NSStr(void* buf, int len) {
|
||||||
|
return (void*)[[NSString alloc] initWithBytes:buf length:len encoding:NSUTF8StringEncoding];
|
||||||
|
}
|
||||||
|
|
||||||
|
void NSRelease(void* obj) {
|
||||||
|
[(NSObject*)obj release];
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface AlertDlg : NSObject {
|
||||||
|
AlertDlgParams* params;
|
||||||
|
DlgResult result;
|
||||||
|
}
|
||||||
|
+ (AlertDlg*)init:(AlertDlgParams*)params;
|
||||||
|
- (DlgResult)run;
|
||||||
|
@end
|
||||||
|
|
||||||
|
DlgResult alertDlg(AlertDlgParams* params) {
|
||||||
|
return [[AlertDlg init:params] run];
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation AlertDlg
|
||||||
|
+ (AlertDlg*)init:(AlertDlgParams*)params {
|
||||||
|
AlertDlg* d = [AlertDlg alloc];
|
||||||
|
d->params = params;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (DlgResult)run {
|
||||||
|
if(![NSThread isMainThread]) {
|
||||||
|
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
|
||||||
|
return self->result;
|
||||||
|
}
|
||||||
|
NSAlert* alert = [[NSAlert alloc] init];
|
||||||
|
if(self->params->title != nil) {
|
||||||
|
[[alert window] setTitle:[[NSString alloc] initWithUTF8String:self->params->title]];
|
||||||
|
}
|
||||||
|
[alert setMessageText:[[NSString alloc] initWithUTF8String:self->params->msg]];
|
||||||
|
switch (self->params->style) {
|
||||||
|
case MSG_YESNO:
|
||||||
|
[alert addButtonWithTitle:@"Yes"];
|
||||||
|
[alert addButtonWithTitle:@"No"];
|
||||||
|
break;
|
||||||
|
case MSG_ERROR:
|
||||||
|
[alert setIcon:[NSImage imageNamed:NSImageNameCaution]];
|
||||||
|
[alert addButtonWithTitle:@"OK"];
|
||||||
|
break;
|
||||||
|
case MSG_INFO:
|
||||||
|
[alert setIcon:[NSImage imageNamed:NSImageNameInfo]];
|
||||||
|
[alert addButtonWithTitle:@"OK"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self->result = [alert runModal] == NSAlertFirstButtonReturn ? DLG_OK : DLG_CANCEL;
|
||||||
|
return self->result;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface FileDlg : NSObject {
|
||||||
|
FileDlgParams* params;
|
||||||
|
DlgResult result;
|
||||||
|
}
|
||||||
|
+ (FileDlg*)init:(FileDlgParams*)params;
|
||||||
|
- (DlgResult)run;
|
||||||
|
@end
|
||||||
|
|
||||||
|
DlgResult fileDlg(FileDlgParams* params) {
|
||||||
|
return [[FileDlg init:params] run];
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation FileDlg
|
||||||
|
+ (FileDlg*)init:(FileDlgParams*)params {
|
||||||
|
FileDlg* d = [FileDlg alloc];
|
||||||
|
d->params = params;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (DlgResult)run {
|
||||||
|
if(![NSThread isMainThread]) {
|
||||||
|
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
|
||||||
|
} else if(self->params->mode == SAVEDLG) {
|
||||||
|
self->result = [self save];
|
||||||
|
} else {
|
||||||
|
self->result = [self load];
|
||||||
|
}
|
||||||
|
return self->result;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)runPanel:(NSSavePanel*)panel {
|
||||||
|
[panel setFloatingPanel:YES];
|
||||||
|
if(self->params->title != nil) {
|
||||||
|
[panel setTitle:[[NSString alloc] initWithUTF8String:self->params->title]];
|
||||||
|
}
|
||||||
|
if(self->params->numext > 0) {
|
||||||
|
[panel setAllowedFileTypes:[NSArray arrayWithObjects:(NSString**)self->params->exts count:self->params->numext]];
|
||||||
|
}
|
||||||
|
if(self->params->relaxext) {
|
||||||
|
[panel setAllowsOtherFileTypes:YES];
|
||||||
|
}
|
||||||
|
return [panel runModal];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (DlgResult)save {
|
||||||
|
NSSavePanel* panel = [NSSavePanel savePanel];
|
||||||
|
if(![self runPanel:panel]) {
|
||||||
|
return DLG_CANCEL;
|
||||||
|
} else if(![[panel URL] getFileSystemRepresentation:self->params->buf maxLength:self->params->nbuf]) {
|
||||||
|
return DLG_URLFAIL;
|
||||||
|
}
|
||||||
|
return DLG_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (DlgResult)load {
|
||||||
|
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||||||
|
if(self->params->mode == DIRDLG) {
|
||||||
|
[panel setCanChooseDirectories:YES];
|
||||||
|
[panel setCanChooseFiles:NO];
|
||||||
|
}
|
||||||
|
if(![self runPanel:panel]) {
|
||||||
|
return DLG_CANCEL;
|
||||||
|
}
|
||||||
|
NSURL* url = [[panel URLs] objectAtIndex:0];
|
||||||
|
if(![url getFileSystemRepresentation:self->params->buf maxLength:self->params->nbuf]) {
|
||||||
|
return DLG_URLFAIL;
|
||||||
|
}
|
||||||
|
return DLG_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package cocoa
|
||||||
|
|
||||||
|
// #cgo darwin LDFLAGS: -framework Cocoa
|
||||||
|
// #include <stdlib.h>
|
||||||
|
// #include <sys/syslimits.h>
|
||||||
|
// #include "dlg.h"
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertParams struct {
|
||||||
|
p C.AlertDlgParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkAlertParams(msg, title string, style C.AlertStyle) *AlertParams {
|
||||||
|
a := AlertParams{C.AlertDlgParams{msg: C.CString(msg), style: style}}
|
||||||
|
if title != "" {
|
||||||
|
a.p.title = C.CString(title)
|
||||||
|
}
|
||||||
|
return &a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlertParams) run() C.DlgResult {
|
||||||
|
return C.alertDlg(&a.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlertParams) free() {
|
||||||
|
C.free(unsafe.Pointer(a.p.msg))
|
||||||
|
if a.p.title != nil {
|
||||||
|
C.free(unsafe.Pointer(a.p.title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nsStr(s string) unsafe.Pointer {
|
||||||
|
return C.NSStr(unsafe.Pointer(&[]byte(s)[0]), C.int(len(s)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func YesNoDlg(msg, title string) bool {
|
||||||
|
a := mkAlertParams(msg, title, C.MSG_YESNO)
|
||||||
|
defer a.free()
|
||||||
|
return a.run() == C.DLG_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func InfoDlg(msg, title string) {
|
||||||
|
a := mkAlertParams(msg, title, C.MSG_INFO)
|
||||||
|
defer a.free()
|
||||||
|
a.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorDlg(msg, title string) {
|
||||||
|
a := mkAlertParams(msg, title, C.MSG_ERROR)
|
||||||
|
defer a.free()
|
||||||
|
a.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUFSIZE = C.PATH_MAX
|
||||||
|
|
||||||
|
func FileDlg(save bool, title string, exts []string, relaxExt bool) (string, error) {
|
||||||
|
mode := C.LOADDLG
|
||||||
|
if save {
|
||||||
|
mode = C.SAVEDLG
|
||||||
|
}
|
||||||
|
return fileDlg(mode, title, exts, relaxExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DirDlg(title string) (string, error) {
|
||||||
|
return fileDlg(C.DIRDLG, title, nil, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileDlg(mode int, title string, exts []string, relaxExt bool) (string, error) {
|
||||||
|
p := C.FileDlgParams{
|
||||||
|
mode: C.int(mode),
|
||||||
|
nbuf: BUFSIZE,
|
||||||
|
}
|
||||||
|
p.buf = (*C.char)(C.malloc(BUFSIZE))
|
||||||
|
defer C.free(unsafe.Pointer(p.buf))
|
||||||
|
buf := (*(*[BUFSIZE]byte)(unsafe.Pointer(p.buf)))[:]
|
||||||
|
if title != "" {
|
||||||
|
p.title = C.CString(title)
|
||||||
|
defer C.free(unsafe.Pointer(p.title))
|
||||||
|
}
|
||||||
|
if len(exts) > 0 {
|
||||||
|
if len(exts) > 999 {
|
||||||
|
panic("more than 999 extensions not supported")
|
||||||
|
}
|
||||||
|
ptrSize := int(unsafe.Sizeof(&title))
|
||||||
|
p.exts = (*unsafe.Pointer)(C.malloc(C.size_t(ptrSize * len(exts))))
|
||||||
|
defer C.free(unsafe.Pointer(p.exts))
|
||||||
|
cext := (*(*[999]unsafe.Pointer)(unsafe.Pointer(p.exts)))[:]
|
||||||
|
for i, ext := range exts {
|
||||||
|
i := i
|
||||||
|
cext[i] = nsStr(ext)
|
||||||
|
defer C.NSRelease(cext[i])
|
||||||
|
}
|
||||||
|
p.numext = C.int(len(exts))
|
||||||
|
if relaxExt {
|
||||||
|
p.relaxext = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch C.fileDlg(&p) {
|
||||||
|
case C.DLG_OK:
|
||||||
|
// casting to string copies the [about-to-be-freed] bytes
|
||||||
|
return string(buf[:bytes.Index(buf, []byte{0})]), nil
|
||||||
|
case C.DLG_CANCEL:
|
||||||
|
return "", nil
|
||||||
|
case C.DLG_URLFAIL:
|
||||||
|
return "", errors.New("failed to get file-system representation for selected URL")
|
||||||
|
}
|
||||||
|
panic("unhandled case")
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// Package dialog provides a simple cross-platform common dialog API.
|
||||||
|
// Eg. to prompt the user with a yes/no dialog:
|
||||||
|
//
|
||||||
|
// if dialog.MsgDlg("%s", "Do you want to continue?").YesNo() {
|
||||||
|
// // user pressed Yes
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The general usage pattern is to call one of the toplevel *Dlg functions
|
||||||
|
// which return a *Builder structure. From here you can optionally call
|
||||||
|
// configuration functions (eg. Title) to customise the dialog, before
|
||||||
|
// using a launcher function to run the dialog.
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrCancelled is an error returned when a user cancels/closes a dialog.
|
||||||
|
var ErrCancelled = errors.New("Cancelled")
|
||||||
|
|
||||||
|
// Cancelled refers to ErrCancelled.
|
||||||
|
// Deprecated: Use ErrCancelled instead.
|
||||||
|
var Cancelled = ErrCancelled
|
||||||
|
|
||||||
|
// Dlg is the common type for dialogs.
|
||||||
|
type Dlg struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgBuilder is used for creating message boxes.
|
||||||
|
type MsgBuilder struct {
|
||||||
|
Dlg
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message initialises a MsgBuilder with the provided message.
|
||||||
|
func Message(format string, args ...interface{}) *MsgBuilder {
|
||||||
|
return &MsgBuilder{Msg: fmt.Sprintf(format, args...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title specifies what the title of the message dialog will be.
|
||||||
|
func (b *MsgBuilder) Title(title string) *MsgBuilder {
|
||||||
|
b.Dlg.Title = title
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// YesNo spawns the message dialog with two buttons, "Yes" and "No".
|
||||||
|
// Returns true iff the user selected "Yes".
|
||||||
|
func (b *MsgBuilder) YesNo() bool {
|
||||||
|
return b.yesNo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info spawns the message dialog with an information icon and single button, "Ok".
|
||||||
|
func (b *MsgBuilder) Info() {
|
||||||
|
b.info()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error spawns the message dialog with an error icon and single button, "Ok".
|
||||||
|
func (b *MsgBuilder) Error() {
|
||||||
|
b.error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileFilter represents a category of files (eg. audio files, spreadsheets).
|
||||||
|
type FileFilter struct {
|
||||||
|
Desc string
|
||||||
|
Extensions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileBuilder is used for creating file browsing dialogs.
|
||||||
|
type FileBuilder struct {
|
||||||
|
Dlg
|
||||||
|
StartDir string
|
||||||
|
Filters []FileFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
// File initialises a FileBuilder using the default configuration.
|
||||||
|
func File() *FileBuilder {
|
||||||
|
return &FileBuilder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title specifies the title to be used for the dialog.
|
||||||
|
func (b *FileBuilder) Title(title string) *FileBuilder {
|
||||||
|
b.Dlg.Title = title
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter adds a category of files to the types allowed by the dialog. Multiple
|
||||||
|
// calls to Filter are cumulative - any of the provided categories will be allowed.
|
||||||
|
// By default all files can be selected.
|
||||||
|
//
|
||||||
|
// The special extension '*' allows all files to be selected when the Filter is active.
|
||||||
|
func (b *FileBuilder) Filter(desc string, extensions ...string) *FileBuilder {
|
||||||
|
filt := FileFilter{desc, extensions}
|
||||||
|
if len(filt.Extensions) == 0 {
|
||||||
|
filt.Extensions = append(filt.Extensions, "*")
|
||||||
|
}
|
||||||
|
b.Filters = append(b.Filters, filt)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStartDir specifies the initial directory of the dialog.
|
||||||
|
func (b *FileBuilder) SetStartDir(startDir string) *FileBuilder {
|
||||||
|
b.StartDir = startDir
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load spawns the file selection dialog using the configured settings,
|
||||||
|
// asking the user to select a single file. Returns ErrCancelled as the error
|
||||||
|
// if the user cancels or closes the dialog.
|
||||||
|
func (b *FileBuilder) Load() (string, error) {
|
||||||
|
return b.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save spawns the file selection dialog using the configured settings,
|
||||||
|
// asking the user for a filename to save as. If the chosen file exists, the
|
||||||
|
// user is prompted whether they want to overwrite the file. Returns
|
||||||
|
// ErrCancelled as the error if the user cancels/closes the dialog, or selects
|
||||||
|
// not to overwrite the file.
|
||||||
|
func (b *FileBuilder) Save() (string, error) {
|
||||||
|
return b.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectoryBuilder is used for directory browse dialogs.
|
||||||
|
type DirectoryBuilder struct {
|
||||||
|
Dlg
|
||||||
|
StartDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory initialises a DirectoryBuilder using the default configuration.
|
||||||
|
func Directory() *DirectoryBuilder {
|
||||||
|
return &DirectoryBuilder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browse spawns the directory selection dialog using the configured settings,
|
||||||
|
// asking the user to select a single folder. Returns ErrCancelled as the error
|
||||||
|
// if the user cancels or closes the dialog.
|
||||||
|
func (b *DirectoryBuilder) Browse() (string, error) {
|
||||||
|
return b.browse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title specifies the title to be used for the dialog.
|
||||||
|
func (b *DirectoryBuilder) Title(title string) *DirectoryBuilder {
|
||||||
|
b.Dlg.Title = title
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sqweek/dialog/cocoa"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *MsgBuilder) yesNo() bool {
|
||||||
|
return cocoa.YesNoDlg(b.Msg, b.Dlg.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) info() {
|
||||||
|
cocoa.InfoDlg(b.Msg, b.Dlg.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) error() {
|
||||||
|
cocoa.ErrorDlg(b.Msg, b.Dlg.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) load() (string, error) {
|
||||||
|
return b.run(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) save() (string, error) {
|
||||||
|
return b.run(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) run(save bool) (string, error) {
|
||||||
|
star := false
|
||||||
|
var exts []string
|
||||||
|
for _, filt := range b.Filters {
|
||||||
|
for _, ext := range filt.Extensions {
|
||||||
|
if ext == "*" {
|
||||||
|
star = true
|
||||||
|
} else {
|
||||||
|
exts = append(exts, ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if star && save {
|
||||||
|
/* OSX doesn't allow the user to switch visible file types/extensions. Also
|
||||||
|
** NSSavePanel's allowsOtherFileTypes property has no effect for an open
|
||||||
|
** dialog, so if "*" is a possible extension we must always show all files. */
|
||||||
|
exts = nil
|
||||||
|
}
|
||||||
|
f, err := cocoa.FileDlg(save, b.Dlg.Title, exts, star)
|
||||||
|
if f == "" && err == nil {
|
||||||
|
return "", ErrCancelled
|
||||||
|
}
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *DirectoryBuilder) browse() (string, error) {
|
||||||
|
f, err := cocoa.DirDlg(b.Dlg.Title)
|
||||||
|
if f == "" && err == nil {
|
||||||
|
return "", ErrCancelled
|
||||||
|
}
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
// #cgo pkg-config: gtk+-3.0
|
||||||
|
// #include <gtk/gtk.h>
|
||||||
|
// #include <stdlib.h>
|
||||||
|
// static GtkWidget* msgdlg(GtkWindow *parent, GtkDialogFlags flags, GtkMessageType type, GtkButtonsType buttons, char *msg) {
|
||||||
|
// return gtk_message_dialog_new(parent, flags, type, buttons, "%s", msg);
|
||||||
|
// }
|
||||||
|
// static GtkWidget* filedlg(char *title, GtkWindow *parent, GtkFileChooserAction action, char* acceptText) {
|
||||||
|
// return gtk_file_chooser_dialog_new(title, parent, action, "Cancel", GTK_RESPONSE_CANCEL, acceptText, GTK_RESPONSE_ACCEPT, NULL);
|
||||||
|
// }
|
||||||
|
import "C"
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
C.gtk_init(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeDialog(dlg *C.GtkWidget) {
|
||||||
|
C.gtk_widget_destroy(dlg)
|
||||||
|
/* The Destroy call itself isn't enough to remove the dialog from the screen; apparently
|
||||||
|
** that happens once the GTK main loop processes some further events. But if we're
|
||||||
|
** in a non-GTK app the main loop isn't running, so we empty the event queue before
|
||||||
|
** returning from the dialog functions.
|
||||||
|
** Not sure how this interacts with an actual GTK app... */
|
||||||
|
for C.gtk_events_pending() != 0 {
|
||||||
|
C.gtk_main_iteration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMsgDlg(defaultTitle string, flags C.GtkDialogFlags, msgtype C.GtkMessageType, buttons C.GtkButtonsType, b *MsgBuilder) C.gint {
|
||||||
|
cmsg := C.CString(b.Msg)
|
||||||
|
defer C.free(unsafe.Pointer(cmsg))
|
||||||
|
dlg := C.msgdlg(nil, flags, msgtype, buttons, cmsg)
|
||||||
|
ctitle := C.CString(firstOf(b.Dlg.Title, defaultTitle))
|
||||||
|
defer C.free(unsafe.Pointer(ctitle))
|
||||||
|
C.gtk_window_set_title((*C.GtkWindow)(unsafe.Pointer(dlg)), ctitle)
|
||||||
|
defer closeDialog(dlg)
|
||||||
|
return C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dlg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) yesNo() bool {
|
||||||
|
return runMsgDlg("Confirm?", 0, C.GTK_MESSAGE_QUESTION, C.GTK_BUTTONS_YES_NO, b) == C.GTK_RESPONSE_YES
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) info() {
|
||||||
|
runMsgDlg("Information", 0, C.GTK_MESSAGE_INFO, C.GTK_BUTTONS_OK, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) error() {
|
||||||
|
runMsgDlg("Error", 0, C.GTK_MESSAGE_ERROR, C.GTK_BUTTONS_OK, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) load() (string, error) {
|
||||||
|
return chooseFile("Open File", "Open", C.GTK_FILE_CHOOSER_ACTION_OPEN, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) save() (string, error) {
|
||||||
|
f, err := chooseFile("Save File", "Save", C.GTK_FILE_CHOOSER_ACTION_SAVE, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func chooseFile(title string, buttonText string, action C.GtkFileChooserAction, b *FileBuilder) (string, error) {
|
||||||
|
ctitle := C.CString(title)
|
||||||
|
defer C.free(unsafe.Pointer(ctitle))
|
||||||
|
cbuttonText := C.CString(buttonText)
|
||||||
|
defer C.free(unsafe.Pointer(cbuttonText))
|
||||||
|
dlg := C.filedlg(ctitle, nil, action, cbuttonText)
|
||||||
|
fdlg := (*C.GtkFileChooser)(unsafe.Pointer(dlg))
|
||||||
|
|
||||||
|
for _, filt := range b.Filters {
|
||||||
|
filter := C.gtk_file_filter_new()
|
||||||
|
cdesc := C.CString(filt.Desc)
|
||||||
|
defer C.free(unsafe.Pointer(cdesc))
|
||||||
|
C.gtk_file_filter_set_name(filter, cdesc)
|
||||||
|
|
||||||
|
for _, ext := range filt.Extensions {
|
||||||
|
cpattern := C.CString("*." + ext)
|
||||||
|
defer C.free(unsafe.Pointer(cpattern))
|
||||||
|
C.gtk_file_filter_add_pattern(filter, cpattern)
|
||||||
|
}
|
||||||
|
C.gtk_file_chooser_add_filter(fdlg, filter)
|
||||||
|
}
|
||||||
|
if b.StartDir != "" {
|
||||||
|
cdir := C.CString(b.StartDir)
|
||||||
|
defer C.free(unsafe.Pointer(cdir))
|
||||||
|
C.gtk_file_chooser_set_current_folder(fdlg, cdir)
|
||||||
|
}
|
||||||
|
C.gtk_file_chooser_set_do_overwrite_confirmation(fdlg, C.TRUE)
|
||||||
|
r := C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dlg)))
|
||||||
|
defer closeDialog(dlg)
|
||||||
|
if r == C.GTK_RESPONSE_ACCEPT {
|
||||||
|
return C.GoString(C.gtk_file_chooser_get_filename(fdlg)), nil
|
||||||
|
}
|
||||||
|
return "", ErrCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *DirectoryBuilder) browse() (string, error) {
|
||||||
|
return chooseFile("Open Folder", "Open", C.GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, &FileBuilder{Dlg: b.Dlg})
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/TheTitanrain/w32"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WinDlgError int
|
||||||
|
|
||||||
|
func (e WinDlgError) Error() string {
|
||||||
|
return fmt.Sprintf("CommDlgExtendedError: %#x", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func err() error {
|
||||||
|
e := w32.CommDlgExtendedError()
|
||||||
|
if e == 0 {
|
||||||
|
return ErrCancelled
|
||||||
|
}
|
||||||
|
return WinDlgError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) yesNo() bool {
|
||||||
|
r := w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Confirm?"), w32.MB_YESNO)
|
||||||
|
return r == w32.IDYES
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) info() {
|
||||||
|
w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Information"), w32.MB_OK|w32.MB_ICONINFORMATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MsgBuilder) error() {
|
||||||
|
w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Error"), w32.MB_OK|w32.MB_ICONERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
type filedlg struct {
|
||||||
|
buf []uint16
|
||||||
|
filters []uint16
|
||||||
|
opf *w32.OPENFILENAME
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d filedlg) Filename() string {
|
||||||
|
i := 0
|
||||||
|
for i < len(d.buf) && d.buf[i] != 0 {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return string(utf16.Decode(d.buf[:i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) load() (string, error) {
|
||||||
|
d := openfile(w32.OFN_FILEMUSTEXIST, b)
|
||||||
|
if w32.GetOpenFileName(d.opf) {
|
||||||
|
return d.Filename(), nil
|
||||||
|
}
|
||||||
|
return "", err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *FileBuilder) save() (string, error) {
|
||||||
|
d := openfile(w32.OFN_OVERWRITEPROMPT, b)
|
||||||
|
if w32.GetSaveFileName(d.opf) {
|
||||||
|
return d.Filename(), nil
|
||||||
|
}
|
||||||
|
return "", err()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* syscall.UTF16PtrFromString not sufficient because we need to encode embedded NUL bytes */
|
||||||
|
func utf16ptr(utf16 []uint16) *uint16 {
|
||||||
|
if utf16[len(utf16)-1] != 0 {
|
||||||
|
panic("refusing to make ptr to non-NUL terminated utf16 slice")
|
||||||
|
}
|
||||||
|
h := (*reflect.SliceHeader)(unsafe.Pointer(&utf16))
|
||||||
|
return (*uint16)(unsafe.Pointer(h.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func utf16slice(ptr *uint16) []uint16 {
|
||||||
|
hdr := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(ptr)), Len: 1, Cap: 1}
|
||||||
|
slice := *((*[]uint16)(unsafe.Pointer(&hdr)))
|
||||||
|
i := 0
|
||||||
|
for slice[len(slice)-1] != 0 {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
hdr.Len = i
|
||||||
|
slice = *((*[]uint16)(unsafe.Pointer(&hdr)))
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func openfile(flags uint32, b *FileBuilder) (d filedlg) {
|
||||||
|
d.buf = make([]uint16, w32.MAX_PATH)
|
||||||
|
d.opf = &w32.OPENFILENAME{
|
||||||
|
File: utf16ptr(d.buf),
|
||||||
|
MaxFile: uint32(len(d.buf)),
|
||||||
|
Flags: flags,
|
||||||
|
}
|
||||||
|
d.opf.StructSize = uint32(unsafe.Sizeof(*d.opf))
|
||||||
|
if b.StartDir != "" {
|
||||||
|
d.opf.InitialDir, _ = syscall.UTF16PtrFromString(b.StartDir)
|
||||||
|
}
|
||||||
|
if b.Dlg.Title != "" {
|
||||||
|
d.opf.Title, _ = syscall.UTF16PtrFromString(b.Dlg.Title)
|
||||||
|
}
|
||||||
|
for _, filt := range b.Filters {
|
||||||
|
/* build utf16 string of form "Music File\0*.mp3;*.ogg;*.wav;\0" */
|
||||||
|
d.filters = append(d.filters, utf16.Encode([]rune(filt.Desc))...)
|
||||||
|
d.filters = append(d.filters, 0)
|
||||||
|
for _, ext := range filt.Extensions {
|
||||||
|
s := fmt.Sprintf("*.%s;", ext)
|
||||||
|
d.filters = append(d.filters, utf16.Encode([]rune(s))...)
|
||||||
|
}
|
||||||
|
d.filters = append(d.filters, 0)
|
||||||
|
}
|
||||||
|
if d.filters != nil {
|
||||||
|
d.filters = append(d.filters, 0, 0) // two extra NUL chars to terminate the list
|
||||||
|
d.opf.Filter = utf16ptr(d.filters)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
type dirdlg struct {
|
||||||
|
bi *w32.BROWSEINFO
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectdir(b *DirectoryBuilder) (d dirdlg) {
|
||||||
|
d.bi = &w32.BROWSEINFO{Flags: w32.BIF_RETURNONLYFSDIRS | w32.BIF_NEWDIALOGSTYLE}
|
||||||
|
if b.Dlg.Title != "" {
|
||||||
|
d.bi.Title, _ = syscall.UTF16PtrFromString(b.Dlg.Title)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *DirectoryBuilder) browse() (string, error) {
|
||||||
|
d := selectdir(b)
|
||||||
|
res := w32.SHBrowseForFolder(d.bi)
|
||||||
|
if res == 0 {
|
||||||
|
return "", ErrCancelled
|
||||||
|
}
|
||||||
|
return w32.SHGetPathFromIDList(res), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/* Note that spawning a dialog from a non-graphical app like this doesn't
|
||||||
|
** quite work properly in OSX. The dialog appears fine, and mouse
|
||||||
|
** interaction works but keypresses go straight through the dialog.
|
||||||
|
** I'm guessing it has something to do with not having a main loop? */
|
||||||
|
dialog.Message("%s", "Please select a file").Title("Hello world!").Info()
|
||||||
|
file, err := dialog.File().Title("Save As").Filter("All Files", "*").Save()
|
||||||
|
fmt.Println(file)
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
dialog.Message("You chose file: %s", file).Title("Goodbye world!").Error()
|
||||||
|
dialog.Directory().Title("Now find a dir").Browse()
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
|
||||||
|
"github.com/skelterjohn/go.wde" // nolint: goimports
|
||||||
|
_ "github.com/skelterjohn/go.wde/init"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var loadR, saveR image.Rectangle
|
||||||
|
|
||||||
|
func events(events <-chan interface{}) {
|
||||||
|
for ei := range events {
|
||||||
|
switch e := ei.(type) {
|
||||||
|
case wde.MouseUpEvent:
|
||||||
|
switch e.Which { // nolint: gocritic
|
||||||
|
case wde.LeftButton:
|
||||||
|
var f string
|
||||||
|
var err error
|
||||||
|
/* launching dialogs within the event loop like this has
|
||||||
|
** a serious problem in practice. On Linux/Windows the
|
||||||
|
** dialog is not modal, which means events on the main
|
||||||
|
** window still get queued up.
|
||||||
|
**
|
||||||
|
** But the dialog functions are synchronous, so we don't
|
||||||
|
** process any of these events until the dialog closes. End
|
||||||
|
** result is if the user clicks multiple times they end up
|
||||||
|
** facing multiple dialogs, one after the other.
|
||||||
|
**
|
||||||
|
** For this reason, it is recommended to launch dialogs
|
||||||
|
** in a separate goroutine, and arrange modality via some
|
||||||
|
** other mechanism (if desired). */
|
||||||
|
if e.Where.In(loadR) {
|
||||||
|
f, err = dialog.File().Title("LOL").Filter("Image", "png").Filter("Audio", "mp3").Filter("All files", "*").Load()
|
||||||
|
} else {
|
||||||
|
f, err = dialog.File().Title("Hilarious").Save()
|
||||||
|
}
|
||||||
|
fmt.Println(f)
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
}
|
||||||
|
case wde.KeyTypedEvent:
|
||||||
|
switch {
|
||||||
|
case e.Glyph == "a":
|
||||||
|
fmt.Println(dialog.Message("Is this sentence false?").YesNo())
|
||||||
|
case e.Glyph == "b":
|
||||||
|
fmt.Println(dialog.Message("R U OK?").Title("Just checking").YesNo())
|
||||||
|
case e.Glyph == "c":
|
||||||
|
dialog.Message("Operation failed").Error()
|
||||||
|
}
|
||||||
|
case wde.CloseEvent:
|
||||||
|
wde.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
go func() {
|
||||||
|
w, _ := wde.NewWindow(300, 300)
|
||||||
|
loadR = image.Rect(0, 0, 300, 150)
|
||||||
|
saveR = image.Rect(0, 150, 300, 300)
|
||||||
|
w.Show()
|
||||||
|
draw.Draw(w.Screen(), loadR, &image.Uniform{color.RGBA{0, 0xff, 0, 0xff}}, image.ZP, draw.Src)
|
||||||
|
draw.Draw(w.Screen(), saveR, &image.Uniform{color.RGBA{0xff, 0, 0, 0xff}}, image.ZP, draw.Src)
|
||||||
|
w.FlushImage()
|
||||||
|
go events(w.EventChan())
|
||||||
|
}()
|
||||||
|
wde.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/sqweek/dialog
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I=
|
||||||
|
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
func firstOf(args ...string) string {
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg != "" {
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user