Skip to content
Snippets Groups Projects
Commit c4cba58f authored by sqweek's avatar sqweek
Browse files

dialog: simple cross-platform dialog API

parents
Branches
No related tags found
No related merge requests found
package cocoa
// #cgo darwin LDFLAGS: -framework Cocoa
// #include <objc/NSObjcRuntime.h>
// #include <stdlib.h>
// #include "dlg.h"
import "C"
import (
"bytes"
"errors"
"unsafe"
)
func YesNoDlg(msg string, title string) bool {
p := C.AlertDlgParams{
msg: C.CString(msg),
}
defer C.free(unsafe.Pointer(p.msg))
if title != "" {
p.title = C.CString(title)
defer C.free(unsafe.Pointer(p.title))
}
return C.alertDlg(&p) == C.DLG_OK
}
func FileDlg(save int, title string) (string, error) {
buf := make([]byte, 1024)
p := C.FileDlgParams{
save: C.int(save),
buf: (*C.char)(unsafe.Pointer(&buf[0])),
nbuf: C.int(cap(buf)),
}
if title != "" {
p.title = C.CString(title)
defer C.free(unsafe.Pointer(p.title))
}
switch C.fileDlg(&p) {
case C.DLG_OK:
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")
}
typedef struct {
char* msg;
char* title;
} AlertDlgParams;
typedef struct {
int save; /* non-zero => save dialog, zero => open dialog */
char* buf; /* buffer to store selected file */
int nbuf; /* number of bytes allocated at buf */
char* title; /* title for dialog box (can be nil) */
} FileDlgParams;
typedef enum {
DLG_OK,
DLG_CANCEL,
DLG_URLFAIL,
} DlgResult;
DlgResult alertDlg(AlertDlgParams*);
DlgResult fileDlg(FileDlgParams*);
#import <Cocoa/Cocoa.h>
#include "dlg.h"
@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]];
[alert addButtonWithTitle:@"Yes"];
[alert addButtonWithTitle:@"No"];
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->save) {
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]];
}
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 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
\ No newline at end of file
dlgs.go 0 → 100644
/* 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"
)
/* Cancelled is an error returned when a user cancels/closes a dialog. */
var Cancelled = errors.New("Cancelled")
type Dlg struct {
Title string
}
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()
}
/* FileFilter represents a category of files (eg. audio files, spreadsheets). */
type FileFilter struct {
Desc string
Patterns []string
}
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 FileFilter to the dialog. */
func (b *FileBuilder) Filter(desc string, patterns ...string) *FileBuilder {
filt := FileFilter{desc, patterns}
if len(filt.Patterns) == 0 {
filt.Patterns = append(filt.Patterns, "*.*")
}
b.Filters = append(b.Filters, filt)
return b
}
/* Load spawns the file selection dialog using the configured settings,
asking the user to select a single file. Returns Cancelled 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
Cancelled 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()
}
package dialog
import (
"github.com/sqweek/dialog/cocoa"
)
func (b *MsgBuilder) yesNo() bool {
return cocoa.YesNoDlg(b.Msg, b.Dlg.Title)
}
func (b *FileBuilder) load() (string, error) {
return b.run(0)
}
func (b *FileBuilder) save() (string, error) {
return b.run(1)
}
func (b *FileBuilder) run(save int) (string, error) {
f, err := cocoa.FileDlg(save, b.Dlg.Title)
if f == "" && err == nil {
return "", Cancelled
}
return f, err
}
package dialog
import (
"os"
"path/filepath"
"github.com/mattn/go-gtk/gtk"
)
func init() {
gtk.Init(nil)
}
func closeDialog(dlg *gtk.Dialog) {
dlg.Destroy()
/* 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 gtk.EventsPending() {
gtk.MainIteration()
}
}
func (b *MsgBuilder) yesNo() bool {
dlg := gtk.NewMessageDialog(nil, 0, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, "%s", b.Msg)
dlg.SetTitle(firstOf(b.Dlg.Title, "Confirm?"))
defer closeDialog(&dlg.Dialog)
return dlg.Run() == gtk.RESPONSE_YES
}
func (b *FileBuilder) load() (string, error) {
return chooseFile("Load", gtk.FILE_CHOOSER_ACTION_OPEN, b)
}
func (b *FileBuilder) save() (string, error) {
f, err := chooseFile("Save", gtk.FILE_CHOOSER_ACTION_SAVE, b)
if err != nil {
return "", err
}
_, err = os.Stat(f)
if !os.IsNotExist(err) && !MsgDlg("%s already exists, overwrite?", filepath.Base(f)).yesNo() {
return "", Cancelled
}
return f, nil
}
func chooseFile(title string, action gtk.FileChooserAction, b *FileBuilder) (string, error) {
dlg := gtk.NewFileChooserDialog(firstOf(b.Dlg.Title, title), nil, action, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
for _, filt := range b.Filters {
filter := gtk.NewFileFilter()
filter.SetName(filt.Desc)
for _, p := range filt.Patterns {
filter.AddPattern(p)
}
dlg.AddFilter(filter)
}
if b.StartDir != "" {
dlg.SetCurrentFolder(b.StartDir)
}
r := dlg.Run()
defer closeDialog(&dlg.Dialog)
if r == gtk.RESPONSE_ACCEPT {
return dlg.GetFilename(), nil
}
return "", Cancelled
}
package dialog
import (
"fmt"
"github.com/AllenDang/w32"
"reflect"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
)
type WinDlgError int
func (e WinDlgError) Error() string {
return fmt.Sprintf("CommDlgExtendedError: %#x", e)
}
func err() error {
e := w32.CommDlgExtendedError()
if e == 0 {
return Cancelled
}
return WinDlgError(e)
}
func (b *MsgBuilder) yesNo() bool {
r := w32.MessageBox(w32.HWND(0), firstOf(b.Dlg.Title, "Confirm?"), b.Msg, w32.MB_YESNO)
return r == w32.IDYES
}
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
}
fmt.Println(w32.CommDlgExtendedError())
fmt.Printf("%#v\n", d)
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)
}
for _, filt := range b.Filters {
s := fmt.Sprintf("%s\000%s\000", filt.Desc, strings.Join(filt.Patterns, ";"))
d.filters = append(d.filters, utf16.Encode([]rune(s))...)
}
if d.filters != nil {
d.filters = append(d.filters, 0, 0)
d.opf.Filter = utf16ptr(d.filters)
}
return d
}
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? *?
file, err := dialog.File().Title("Save As").Filter("All Files", "*.*").Save()
fmt.Println(file)
fmt.Println("Error:", err)
}
package main
import (
"fmt"
"github.com/skelterjohn/go.wde"
"github.com/sqweek/dialog"
"image"
"image/color"
"image/draw"
_ "github.com/skelterjohn/go.wde/init"
)
var loadR, saveR image.Rectangle
func events(events <-chan interface{}) {
for ei := range events {
switch e := ei.(type) {
case wde.MouseUpEvent:
switch e.Which {
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").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 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()
}
util.go 0 → 100644
package dialog
func firstOf(args ...string) string {
for _, arg := range args {
if arg != "" {
return arg
}
}
return ""
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment