Changed the way dialogs work so that they do real modality properly and implemented such on Windows.

This commit is contained in:
Pietro Gagliardi 2014-08-26 12:52:32 -04:00
parent e7490ce49b
commit adbe5303e7
7 changed files with 78 additions and 67 deletions

View File

@ -2,10 +2,19 @@
package ui package ui
// OpenFile opens a dialog box that asks the user to choose a file. type windowDialog interface {
// It returns the selected filename, or an empty string if no file was chosen. openFile(f func(filename string))
// All events stop while OpenFile is executing. (TODO move to doc.go) }
// If possible on a given system, OpenFile() will not dereference links; it will return the link file itself.
func OpenFile() (filename string) { // OpenFile opens a dialog box that asks the user to choose a file.
return openFile() // The dialog box is modal to win, which mut not be nil.
// Some time after the dialog box is closed, OpenFile runs f on the main thread, passing filename.
// filename is the selected filename, or an empty string if no file was chosen.
// OpenFile does not ensure that f remains alive; the programmer is responsible for this.
// If possible on a given system, OpenFile() will not dereference links; it will return the link file itself.
func OpenFile(win Window, f func(filename string)) {
if win == nil {
panic("Window passed to OpenFile() cannot be nil")
}
win.openFile(f)
} }

View File

@ -3,73 +3,60 @@
#include "winapi_windows.h" #include "winapi_windows.h"
#include "_cgo_export.h" #include "_cgo_export.h"
static LRESULT CALLBACK dialogSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR id, DWORD_PTR data)
{
switch (uMsg) {
case WM_COMMAND:
// we must re-enable other windows in the right order (see http://blogs.msdn.com/b/oldnewthing/archive/2004/02/27/81155.aspx)
// see http://stackoverflow.com/questions/25494914/is-there-something-like-cdn-filecancel-analogous-to-cdn-fileok-for-getting-when
if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDCANCEL)
SendMessageW(msgwin, msgEndModal, 0, 0);
break; // let the dialog handle it now
case WM_NCDESTROY:
if ((*fv_RemoveWindowSubclass)(hwnd, dialogSubProc, id) == FALSE)
xpanic("error removing dialog subclass (which was for its own event handler)", GetLastError());
}
return (*fv_DefSubclassProc)(hwnd, uMsg, wParam, lParam);
}
// this should be reasonable // this should be reasonable
#define NFILENAME 4096 #define NFILENAME 4096
static UINT_PTR CALLBACK openSaveFileHook(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) struct openFileData {
{
if (uMsg == WM_INITDIALOG) {
HWND parent; HWND parent;
void *f;
WCHAR *filenameBuffer;
};
parent = GetParent(hwnd); static DWORD WINAPI doOpenFile(LPVOID data)
if (parent == NULL)
xpanic("error gettign parent of OpenFile() dialog for event handling", GetLastError());
if ((*fv_SetWindowSubclass)(parent, dialogSubProc, 0, (DWORD_PTR) NULL) == FALSE)
xpanic("error subclassing OpenFile() dialog to give it its own event handler", GetLastError());
} else if (uMsg == WM_NOTIFY) {
OFNOTIFY *of = (OFNOTIFY *) lParam;
if (of->hdr.code == CDN_FILEOK)
SendMessageW(msgwin, msgEndModal, 0, 0);
}
return 0;
}
WCHAR *openFile(void)
{ {
struct openFileData *o = (struct openFileData *) data;
OPENFILENAMEW ofn; OPENFILENAMEW ofn;
DWORD err; DWORD err;
WCHAR *filenameBuffer;
// freed on the Go side o->filenameBuffer[0] = L'\0'; // required by GetOpenFileName() to indicate no previous filename
filenameBuffer = (WCHAR *) malloc((NFILENAME + 1) * sizeof (WCHAR));
if (filenameBuffer == NULL)
xpanic("memory exhausted in OpenFile()", GetLastError());
filenameBuffer[0] = L'\0'; // required by GetOpenFileName() to indicate no previous filename
ZeroMemory(&ofn, sizeof (OPENFILENAMEW)); ZeroMemory(&ofn, sizeof (OPENFILENAMEW));
ofn.lStructSize = sizeof (OPENFILENAMEW); ofn.lStructSize = sizeof (OPENFILENAMEW);
ofn.hwndOwner = NULL; ofn.hwndOwner = o->parent;
ofn.hInstance = hInstance; ofn.hInstance = hInstance;
ofn.lpstrFilter = NULL; // no filters ofn.lpstrFilter = NULL; // no filters
ofn.lpstrFile = filenameBuffer; ofn.lpstrFile = o->filenameBuffer;
ofn.nMaxFile = NFILENAME + 1; // TODO include + 1? ofn.nMaxFile = NFILENAME + 1; // TODO include + 1?
ofn.lpstrInitialDir = NULL; // let system decide ofn.lpstrInitialDir = NULL; // let system decide
ofn.lpstrTitle = NULL; // let system decide ofn.lpstrTitle = NULL; // let system decide
// TODO OFN_SHAREAWARE? // TODO OFN_SHAREAWARE?
ofn.Flags = OFN_ENABLEHOOK | OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_FORCESHOWHIDDEN | OFN_HIDEREADONLY | OFN_LONGNAMES | OFN_NOCHANGEDIR | OFN_NODEREFERENCELINKS | OFN_NOTESTFILECREATE | OFN_PATHMUSTEXIST; ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_FORCESHOWHIDDEN | OFN_HIDEREADONLY | OFN_LONGNAMES | OFN_NOCHANGEDIR | OFN_NODEREFERENCELINKS | OFN_NOTESTFILECREATE | OFN_PATHMUSTEXIST;
ofn.lpfnHook = openSaveFileHook;
SendMessageW(msgwin, msgBeginModal, 0, 0);
if (GetOpenFileNameW(&ofn) == FALSE) { if (GetOpenFileNameW(&ofn) == FALSE) {
err = CommDlgExtendedError(); err = CommDlgExtendedError();
if (err == 0) // user cancelled if (err != 0) // user cancelled
return NULL;
xpaniccomdlg("error running open file dialog", err); xpaniccomdlg("error running open file dialog", err);
free(o->filenameBuffer); // free now so we can set it to NULL without leaking
o->filenameBuffer = NULL;
} }
return filenameBuffer; if (PostMessageW(msgwin, msgOpenFileDone, (WPARAM) (o->filenameBuffer), (LPARAM) (o->f)) == 0)
xpanic("error posting OpenFile() finished message to message-only window", GetLastError());
free(o); // won't free o->f or o->filenameBuffer in above invocation
return 0;
}
void openFile(HWND hwnd, void *f)
{
struct openFileData *o;
// freed by the thread
o = (struct openFileData *) malloc(sizeof (struct openFileData));
if (o == NULL)
xpanic("memory exhausted allocating data structure in OpenFile()", GetLastError());
o->parent = hwnd;
o->f = f;
// freed on the Go side
o->filenameBuffer = (WCHAR *) malloc((NFILENAME + 1) * sizeof (WCHAR));
if (o->filenameBuffer == NULL)
xpanic("memory exhausted allocating filename buffer in OpenFile()", GetLastError());
if (CreateThread(NULL, 0, doOpenFile, (LPVOID) o, 0, NULL) == NULL)
xpanic("error creating thread for running OpenFIle()", GetLastError());
} }

View File

@ -9,11 +9,17 @@ import (
// #include "winapi_windows.h" // #include "winapi_windows.h"
import "C" import "C"
func openFile() string { func (w *window) openFile(f func(filename string)) {
name := C.openFile() C.openFile(w.hwnd, unsafe.Pointer(&f))
}
//export finishOpenFile
func finishOpenFile(name *C.WCHAR, fp unsafe.Pointer) {
f := (*func(string))(fp)
if name == nil { if name == nil {
return "" (*f)("")
return
} }
defer C.free(unsafe.Pointer(name)) defer C.free(unsafe.Pointer(name))
return wstrToString(name) (*f)(wstrToString(name))
} }

View File

@ -146,6 +146,9 @@ static LRESULT CALLBACK msgwinproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM l
endModal(); endModal();
EnumThreadWindows(GetCurrentThreadId(), beginEndModalAll, msgEndModal); EnumThreadWindows(GetCurrentThreadId(), beginEndModalAll, msgEndModal);
return 0; return 0;
case msgOpenFileDone:
finishOpenFile((WCHAR *) wParam, (void *) lParam);
return 0;
default: default:
return DefWindowProcW(hwnd, uMsg, wParam, lParam); return DefWindowProcW(hwnd, uMsg, wParam, lParam);
} }

View File

@ -40,6 +40,7 @@ enum {
msgAreaKeyUp, msgAreaKeyUp,
msgLoadImageList, msgLoadImageList,
msgTableMakeInitialCheckboxImageList, msgTableMakeInitialCheckboxImageList,
msgOpenFileDone,
}; };
// uitask_windows.c // uitask_windows.c
@ -149,6 +150,6 @@ enum {
extern HIMAGELIST makeCheckboxImageList(HWND, HTHEME *); extern HIMAGELIST makeCheckboxImageList(HWND, HTHEME *);
// dialog_windows.c // dialog_windows.c
extern WCHAR *openFile(void); extern void openFile(HWND, void *);
#endif #endif

View File

@ -25,6 +25,8 @@ type Window interface {
// If this handler returns true, the Window is closed as defined by Close above. // If this handler returns true, the Window is closed as defined by Close above.
// If this handler returns false, the Window is not closed. // If this handler returns false, the Window is not closed.
OnClosing(func() bool) OnClosing(func() bool)
windowDialog
} }
// NewWindow creates a new Window with the given title text, size, and control. // NewWindow creates a new Window with the given title text, size, and control.

View File

@ -75,6 +75,13 @@ func (a *areaHandler) Paint(r image.Rectangle) *image.RGBA {
func (a *areaHandler) Mouse(me MouseEvent) { fmt.Printf("%#v\n", me) } func (a *areaHandler) Mouse(me MouseEvent) { fmt.Printf("%#v\n", me) }
func (a *areaHandler) Key(ke KeyEvent) bool { fmt.Printf("%#v %q\n", ke, ke.Key); return a.handled } func (a *areaHandler) Key(ke KeyEvent) bool { fmt.Printf("%#v %q\n", ke, ke.Key); return a.handled }
func (tw *testwin) openFile(fn string) {
if fn == "" {
fn = "<no file selected>"
}
tw.fnlabel.SetText(fn)
}
func (tw *testwin) addfe() { func (tw *testwin) addfe() {
tw.festart = NewButton("Start") tw.festart = NewButton("Start")
tw.festart.OnClicked(func() { tw.festart.OnClicked(func() {
@ -106,11 +113,7 @@ func (tw *testwin) addfe() {
}) })
tw.openbtn = NewButton("Open") tw.openbtn = NewButton("Open")
tw.openbtn.OnClicked(func() { tw.openbtn.OnClicked(func() {
fn := OpenFile() OpenFile(tw.w, tw.openFile)
if fn == "" {
fn = "<no file selected>"
}
tw.fnlabel.SetText(fn)
}) })
tw.fnlabel = NewStandaloneLabel("<no file selected>") tw.fnlabel = NewStandaloneLabel("<no file selected>")
tw.festack = NewVerticalStack(tw.festart, tw.felabel, tw.festop, tw.festack = NewVerticalStack(tw.festart, tw.felabel, tw.festop,