diff --git a/addons/dragdrop/dragdrop_windows.c b/addons/dragdrop/dragdrop_windows.c new file mode 100644 index 0000000..94d934f --- /dev/null +++ b/addons/dragdrop/dragdrop_windows.c @@ -0,0 +1,176 @@ +#include +#include +#include + +#include "vector.h" +#include "_cgo_export.h" + +#define STDULONGMETHODIMP STDMETHODIMP_(ULONG) +#define DRAGDROP_MAXFILEPATHSIZE 32767 + +typedef struct +{ + IDropTargetVtbl *_vtbl; + long _refCount; + + uint32_t _handle; + HWND _windowHandle; +} DragDropHandler; + +static void screenToClient(HWND windowHandle, POINTL *point) +{ + ScreenToClient(windowHandle, (LPPOINT)point); +} + +static STDMETHODIMP QueryInterface(IDropTarget *this, REFIID riid, LPVOID *ppvObj) +{ + // Always set out parameter to NULL, validating it first. + if (!ppvObj) + return E_INVALIDARG; + *ppvObj = NULL; + if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IDropTarget)) + { + // Increment the reference count and return the pointer. + *ppvObj = (LPVOID)this; + ((DragDropHandler *)this)->_vtbl->AddRef(this); + return NOERROR; + } + return E_NOINTERFACE; +} + +static STDULONGMETHODIMP AddRef(IDropTarget *this) +{ + InterlockedIncrement(&((DragDropHandler *)this)->_refCount); + return ((DragDropHandler *)this)->_refCount; +} + +static STDULONGMETHODIMP Release(IDropTarget *this) +{ + // Decrement the object's internal counter. + ULONG ulRefCount = InterlockedDecrement(&((DragDropHandler *)this)->_refCount); + if (0 == ((DragDropHandler *)this)->_refCount) + { + GlobalFree(this); + } + return ulRefCount; +} + +static STDMETHODIMP DragEnter(IDropTarget *this, __RPC__in_opt IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, __RPC__inout DWORD *pdwEffect) +{ + *pdwEffect = DROPEFFECT_COPY; + + wchar_t filePath[DRAGDROP_MAXFILEPATHSIZE]; + + FORMATETC format; + format.cfFormat = CF_HDROP; + format.ptd = NULL; + format.dwAspect = DVASPECT_CONTENT; + format.lindex = -1; + format.tymed = TYMED_HGLOBAL; + + STGMEDIUM medium; + + HRESULT result = pDataObj->lpVtbl->GetData(pDataObj, &format, &medium); + if (result != S_OK) + return E_UNEXPECTED; + HDROP drop = (HDROP)GlobalLock(medium.hGlobal); + + clearFiles(((DragDropHandler *)this)->_handle); + UINT numberOfFiles = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0); + for (UINT fileIndex = 0; fileIndex < numberOfFiles; fileIndex++) + { + UINT length = DragQueryFileW(drop, fileIndex, filePath, DRAGDROP_MAXFILEPATHSIZE); + addFile(((DragDropHandler *)this)->_handle, &filePath[0], length); + } + + GlobalUnlock(medium.hGlobal); + if (medium.pUnkForRelease != NULL) + medium.pUnkForRelease->lpVtbl->Release(medium.pUnkForRelease); + + screenToClient(((DragDropHandler *)this)->_windowHandle, &pt); + onDragEnter(((DragDropHandler *)this)->_handle, pt.x, pt.y); + + return S_OK; +} + +static STDMETHODIMP DragOver(IDropTarget *this, DWORD grfKeyState, POINTL pt, __RPC__inout DWORD *pdwEffect) +{ + *pdwEffect = DROPEFFECT_COPY; + screenToClient(((DragDropHandler *)this)->_windowHandle, &pt); + onDragOver(((DragDropHandler *)this)->_handle, pt.x, pt.y); + return S_OK; +} + +static STDMETHODIMP DragLeave(IDropTarget *this) +{ + onDragLeave(((DragDropHandler *)this)->_handle); + return S_OK; +} + +static STDMETHODIMP Drop(IDropTarget *this, __RPC__in_opt IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, __RPC__inout DWORD *pdwEffect) +{ + *pdwEffect = DROPEFFECT_COPY; + screenToClient(((DragDropHandler *)this)->_windowHandle, &pt); + onDrop(((DragDropHandler *)this)->_handle, pt.x, pt.y); + return S_OK; +} + +static const IDropTargetVtbl DragDropHandlerVtbl = { + QueryInterface, + AddRef, + Release, + DragEnter, + DragOver, + DragLeave, + Drop, +}; + +static DragDropHandler *newDragDropHandler(HWND windowHandle, uint32_t handle) +{ + DragDropHandler *handler = malloc(sizeof(DragDropHandler)); + handler->_vtbl = (IDropTargetVtbl *)&DragDropHandlerVtbl; + handler->_refCount = 0; + handler->_windowHandle = windowHandle; + handler->_handle = handle; + return handler; +} + +BOOL dragDropInitialized = FALSE; +typedef vector(DragDropHandler *) DragDropHandlers; +DragDropHandlers handlers; + +static void VerifyResult(CHAR *component, HRESULT result) +{ + if (result == S_OK) + return; + printf("%s failed (error code: %d)\n", component, result); +} + +static void initDragDrop(void) +{ + if (dragDropInitialized == TRUE) + return; + + HRESULT result = OleInitialize(NULL); + VerifyResult("OleInitialize", result); + + vector_init(handlers); + + dragDropInitialized = TRUE; +} + +uint32_t RegisterHandler(void *window) +{ + initDragDrop(); + + size_t handle = handlers.count + 1; + HWND windowHandle = (HWND)window; + DragDropHandler *handler = newDragDropHandler(windowHandle, handle); + vector_append(DragDropHandler *, handlers, handler); + + DragAcceptFiles(windowHandle, TRUE); + HRESULT result = RegisterDragDrop(windowHandle, (IDropTarget *)handler); + VerifyResult("RegisterDragDrop", result); + + return (uint32_t)handle; +} diff --git a/addons/dragdrop/dragdrop_windows.go b/addons/dragdrop/dragdrop_windows.go new file mode 100644 index 0000000..b506407 --- /dev/null +++ b/addons/dragdrop/dragdrop_windows.go @@ -0,0 +1,96 @@ +// +build windows + +package dragdrop + +import ( + "unsafe" + + "golang.org/x/text/encoding/unicode" + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg/ui" +) + +/* +#cgo LDFLAGS: -lole32 -luuid + +#include +#include +#include +#include + +extern uint32_t RegisterHandler(void* windowHandle); +*/ +import "C" + +//export clearFiles +func clearFiles(handle uint32) { + handler := handlers[handle] + handler.Files = nil + handlers[handle] = handler +} + +//export addFile +func addFile(handle uint32, pathNative *C.wchar_t, pathNativeLength C.UINT) { + pathBytes := C.GoBytes(unsafe.Pointer(pathNative), C.int(pathNativeLength)*C.sizeof_wchar_t) + decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder() + path, err := decoder.Bytes(pathBytes) + if err != nil { + return + } + handler := handlers[handle] + handler.Files = append(handler.Files, string(path)) + handlers[handle] = handler +} + +//export onDragEnter +func onDragEnter(handle uint32, x, y C.LONG) { + invokeHandler(handle, func(target ui.DragDropEventTarget, files []string) { + target.DragEnter(pos(x, y), files) + }) +} + +//export onDragOver +func onDragOver(handle uint32, x, y C.LONG) { + invokeHandler(handle, func(target ui.DragDropEventTarget, _ []string) { + target.DragMove(pos(x, y)) + }) +} + +//export onDragLeave +func onDragLeave(handle uint32) { + invokeHandler(handle, func(target ui.DragDropEventTarget, _ []string) { + target.DragLeave() + }) +} + +//export onDrop +func onDrop(handle uint32, x, y C.LONG) { + invokeHandler(handle, func(target ui.DragDropEventTarget, files []string) { + target.Drop(pos(x, y), files) + }) +} + +type handler struct { + Target ui.DragDropEventTarget + Files []string +} + +var handlers map[uint32]handler = map[uint32]handler{} + +func invokeHandler(handle uint32, invoke func(ui.DragDropEventTarget, []string)) { + handler := handlers[handle] + invoke(handler.Target, handler.Files) +} + +func pos(x, y C.LONG) geom.PointF32 { return geom.Pt(int(x), int(y)).ToF32() } + +type provider struct{} + +func (p provider) Register(windowHandle uintptr, target ui.DragDropEventTarget) { + handle := C.RegisterHandler(unsafe.Pointer(windowHandle)) + handlers[uint32(handle)] = handler{target, nil} +} + +func init() { + ui.DefaultDragDropProvider = provider{} +} diff --git a/addons/dragdrop/vector.h b/addons/dragdrop/vector.h new file mode 100644 index 0000000..12aff66 --- /dev/null +++ b/addons/dragdrop/vector.h @@ -0,0 +1,26 @@ +#ifndef __VECTOR_H__ +#define __VECTOR_H__ + +#define vector(type) \ + struct \ + { \ + type *items; \ + size_t count; \ + } + +#define vector_init(v) \ + do \ + { \ + (v).items = 0; \ + (v).count = 0; \ + } while (0); + +#define vector_append(type, v, i) \ + do \ + { \ + (v).count++; \ + (v).items = (type *)realloc((v).items, sizeof(type) * (v).count); \ + (v).items[(v).count - 1] = i; \ + } while (0); + +#endif // __VECTOR_H__ diff --git a/addons/drop/drop_windows.c b/addons/drop/drop_windows.c index ff6dc28..cef325f 100644 --- a/addons/drop/drop_windows.c +++ b/addons/drop/drop_windows.c @@ -7,8 +7,8 @@ #define droppedFilePathSize 32767 +BOOL dropHookInitialized = FALSE; HHOOK nextHook; -uint32_t nextDropID = 0; LRESULT CALLBACK DragAndDropHook( _In_ int nCode, @@ -24,29 +24,40 @@ LRESULT CALLBACK DragAndDropHook( case WM_DROPFILES: { wchar_t droppedFilePath[droppedFilePathSize]; - uint32_t dropID = nextDropID++; + clearDrop(); HDROP drop = (HDROP)message->wParam; - POINT position; - DragQueryPoint(drop, &position); - droppedFilesPosition(dropID, position.x, position.y); UINT numberOfFiles = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0); for (UINT fileIndex = 0; fileIndex < numberOfFiles; fileIndex++) { UINT length = DragQueryFileW(drop, fileIndex, droppedFilePath, droppedFilePathSize); - droppedFilesFile(dropID, &droppedFilePath[0], length); + addDroppedFile(&droppedFilePath[0], length); } - droppedFilesDrop(dropID); + POINT position; + DragQueryPoint(drop, &position); + + dropFinished(position.x, position.y); } break; } return CallNextHookEx(nextHook, nCode, wParam, lParam); } +void initDropHook(HWND windowHandle) +{ + if (dropHookInitialized == TRUE) + return; + + DWORD threadId = GetWindowThreadProcessId(windowHandle, NULL); + nextHook = SetWindowsHookEx(WH_GETMESSAGE, DragAndDropHook, NULL, threadId); + + dropHookInitialized = TRUE; +} + void SetDragAndDropHook(void *window) { HWND windowHandle = (HWND)window; + initDropHook(windowHandle); + DragAcceptFiles(windowHandle, TRUE); - DWORD threadId = GetWindowThreadProcessId(windowHandle, NULL); - nextHook = SetWindowsHookEx(WH_GETMESSAGE, DragAndDropHook, NULL, threadId); } diff --git a/addons/drop/drop_windows.go b/addons/drop/drop_windows.go index 774382e..291b84b 100644 --- a/addons/drop/drop_windows.go +++ b/addons/drop/drop_windows.go @@ -3,11 +3,11 @@ package drop import ( - "errors" "unsafe" "golang.org/x/text/encoding/unicode" "opslag.de/schobers/geom" + "opslag.de/schobers/zntg/ui" ) /* @@ -18,53 +18,48 @@ void SetDragAndDropHook(void* window); */ import "C" -var handler Dropper = nil -var drops map[uint32]droppedFiles = map[uint32]droppedFiles{} - -type droppedFiles struct { - X, Y int - Files []string +type handler struct { + Target ui.DragDropEventTarget + Files []string } -//export droppedFilesPosition -func droppedFilesPosition(id C.uint32_t, x, y C.INT) { - files := drops[uint32(id)] - files.X = int(x) - files.Y = int(y) - drops[uint32(id)] = files +var targets map[ui.DragDropEventTarget]struct{} = map[ui.DragDropEventTarget]struct{}{} +var droppedFiles []string + +//export clearDrop +func clearDrop() { + droppedFiles = nil } -//export droppedFilesFile -func droppedFilesFile(id C.uint32_t, filePath *C.wchar_t, filePathLength C.UINT) { - files := drops[uint32(id)] +//export dropFinished +func dropFinished(x, y C.INT) { + for target := range targets { + target.Drop(geom.PtF32(float32(x), float32(y)), droppedFiles) + } +} + +//export addDroppedFile +func addDroppedFile(filePath *C.wchar_t, filePathLength C.UINT) { pathBytes := C.GoBytes(unsafe.Pointer(filePath), C.int(filePathLength)*C.sizeof_wchar_t) decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder() path, err := decoder.Bytes(pathBytes) if err != nil { return } - files.Files = append(files.Files, string(path)) - drops[uint32(id)] = files + droppedFiles = append(droppedFiles, string(path)) } -//export droppedFilesDrop -func droppedFilesDrop(id C.uint32_t) { - h := handler - if h == nil { - return - } - drop, ok := drops[uint32(id)] - if !ok { - return - } - h.FilesDropped(geom.Pt(drop.X, drop.Y).ToF32(), drop.Files) +func RegisterAsDefaultProvider() { + ui.DefaultDragDropProvider = &provider{} } -func Register(dropper Dropper) error { - if handler != nil { - return errors.New(`can only register single dropper`) - } - handler = dropper - C.SetDragAndDropHook(unsafe.Pointer(dropper.WindowHandle())) - return nil +type provider struct{} + +func (p provider) Register(windowHandle uintptr, target ui.DragDropEventTarget) { + C.SetDragAndDropHook(unsafe.Pointer(windowHandle)) + targets[target] = struct{}{} +} + +func init() { + ui.DefaultDragDropProvider = provider{} } diff --git a/addons/drop/dropper.go b/addons/drop/dropper.go deleted file mode 100644 index adc9bdd..0000000 --- a/addons/drop/dropper.go +++ /dev/null @@ -1,9 +0,0 @@ -package drop - -import "opslag.de/schobers/geom" - -type Dropper interface { - WindowHandle() uintptr - - FilesDropped(geom.PointF32, []string) -} diff --git a/allg5ui/renderer.go b/allg5ui/renderer.go index 6789b11..f19536f 100644 --- a/allg5ui/renderer.go +++ b/allg5ui/renderer.go @@ -10,6 +10,7 @@ import ( "opslag.de/schobers/allg5" "opslag.de/schobers/geom" + "opslag.de/schobers/zntg/addons/drop" "opslag.de/schobers/zntg/ui" ) @@ -47,6 +48,11 @@ func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) { }) clean = nil + if ui.DefaultDragDropProvider != nil { + // make sure we fall back on simple drop (OLE implementation doesn't seem to work, reason unknown yet). + drop.RegisterAsDefaultProvider() + } + return &Renderer{disp, eq, nil, user, &ui.OSResources{}, dispPos(disp), ui.KeyState{}, ui.KeyModifierNone, ui.MouseCursorDefault}, nil } @@ -152,6 +158,10 @@ func (r *Renderer) Refresh() { r.user.EmitEvent() } +func (r *Renderer) Stamp() float64 { + return allg5.GetTime() +} + // Renderer implementation (lifetime) func (r *Renderer) Destroy() error { diff --git a/sdlui/renderer.go b/sdlui/renderer.go index 19f50c7..2bd19b2 100644 --- a/sdlui/renderer.go +++ b/sdlui/renderer.go @@ -205,6 +205,10 @@ func (r *Renderer) Refresh() { sdl.PushEvent(e) } +func (r *Renderer) Stamp() float64 { + return .001 * float64(sdl.GetTicks()) +} + // Lifetime func (r *Renderer) Destroy() error { diff --git a/ui/dragdrop.go b/ui/dragdrop.go new file mode 100644 index 0000000..ae90a22 --- /dev/null +++ b/ui/dragdrop.go @@ -0,0 +1,46 @@ +package ui + +import "opslag.de/schobers/geom" + +type DragDropEventTarget interface { + DragEnter(geom.PointF32, []string) + DragMove(geom.PointF32) + DragLeave() + Drop(geom.PointF32, []string) +} + +type DragDropProvider interface { + Register(windowHandle uintptr, target DragDropEventTarget) +} + +var DefaultDragDropProvider DragDropProvider = nil + +type dragDropEventTarget struct { + renderer Renderer + events []Event +} + +func (t *dragDropEventTarget) eventBase() EventBase { + return EventBase{StampInSeconds: t.renderer.Stamp()} +} + +func (t *dragDropEventTarget) pushEvent(e Event) { + t.events = append(t.events, e) + t.renderer.Refresh() +} + +func (t *dragDropEventTarget) DragEnter(pos geom.PointF32, files []string) { + t.pushEvent(&DisplayDragEnterEvent{EventBase: t.eventBase(), X: pos.X, Y: pos.Y, Files: files}) +} + +func (t *dragDropEventTarget) DragMove(pos geom.PointF32) { + t.pushEvent(&DisplayDragMoveEnter{EventBase: t.eventBase(), X: pos.X, Y: pos.Y}) +} + +func (t *dragDropEventTarget) DragLeave() { + t.pushEvent(&DisplayDragLeaveEvent{EventBase: t.eventBase()}) +} + +func (t *dragDropEventTarget) Drop(pos geom.PointF32, files []string) { + t.pushEvent(&DisplayDropEvent{EventBase: t.eventBase(), X: pos.X, Y: pos.Y, Files: files}) +} diff --git a/ui/event.go b/ui/event.go index 55f38c1..790dee7 100644 --- a/ui/event.go +++ b/ui/event.go @@ -6,6 +6,31 @@ type DisplayCloseEvent struct { EventBase } +type DisplayDragEnterEvent struct { + EventBase + X, Y float32 + Files []string +} + +type DisplayDragLeaveEvent struct { + EventBase +} + +type DisplayDragMoveEnter struct { + EventBase + X, Y float32 +} + +type DisplayDropEvent struct { + EventBase + X, Y float32 + Files []string +} + +func (e DisplayDropEvent) Pos() geom.PointF32 { + return geom.PtF32(e.X, e.Y) +} + type DisplayMoveEvent struct { EventBase Bounds geom.RectangleF32 diff --git a/ui/examples/03_dragdrop/dragdrop.go b/ui/examples/03_dragdrop/dragdrop.go new file mode 100644 index 0000000..3fae73e --- /dev/null +++ b/ui/examples/03_dragdrop/dragdrop.go @@ -0,0 +1,122 @@ +package main + +import ( + "image/color" + "log" + "strings" + "time" + + "opslag.de/schobers/geom" + + _ "opslag.de/schobers/zntg/addons/dragdrop" // import drag & drop functionality + _ "opslag.de/schobers/zntg/sdlui" // import the renderer for the UI + + "opslag.de/schobers/zntg/ui" +) + +type dropFiles struct { + ui.StackPanel + + ctx ui.Context + files *ui.Paragraph + ping *ping +} + +type ping struct { + ui.OverlayBase + + circle ui.Texture + position geom.PointF32 + over bool + tick time.Time +} + +func (p *ping) Render(ctx ui.Context) { + elapsed := time.Since(p.tick) + const animationDuration = 500 * time.Millisecond + if elapsed > animationDuration { + return + } + tint := color.Gray{Y: uint8(elapsed * 255 / animationDuration)} + center := geom.PtF32(float32(p.circle.Width()), float32(p.circle.Height())).Mul(.5) + ctx.Renderer().DrawTexturePointOptions(p.circle, p.position.Sub(center), ui.DrawOptions{Tint: tint}) + ctx.Animate() +} + +func (d *dropFiles) Handle(ctx ui.Context, e ui.Event) bool { + switch e := e.(type) { + case *ui.DisplayDragMoveEnter: + d.ping.tick = time.Now() + d.ping.position = geom.PtF32(e.X, e.Y) + d.ping.over = true + d.ctx.Animate() + return true + case *ui.DisplayDropEvent: + d.files.Text = "Files dropped:\n" + strings.Join(e.Files, "\n") + d.ping.tick = time.Now() + d.ping.position = geom.PtF32(e.X, e.Y) + d.ping.over = false + d.ctx.Animate() + return true + } + return d.StackPanel.Handle(ctx, e) +} + +func newCircle() ui.ImageSource { + const side = 16 + center := geom.PtF32(.5*side, .5*side) + return &ui.AlphaPixelImageSource{ + ImageAlphaPixelTestFn: func(p geom.PointF32) uint8 { + dist := p.Distance(center) + if dist < 7 { + return 255 + } else if dist > 8 { + return 0 + } + return uint8((8 - dist) * 255 * 0.5) + }, + Size: geom.Pt(side, side), + } +} + +func (d *dropFiles) Init(ctx ui.Context) error { + d.ctx = ctx + + _, err := ctx.Fonts().CreateFontPath("default", "../resources/font/OpenSans-Regular.ttf", 14) + if err != nil { + return err + } + + d.files = ui.BuildParagraph("", nil) + + pingCircle, err := ctx.Renderer().CreateTexture(newCircle()) + if err != nil { + return err + } + d.ping = &ping{circle: pingCircle} + ctx.Overlays().AddOnTop("ping", d.ping, true) + + d.Background = color.White + d.Children = []ui.Control{ + &ui.Label{Text: "Drop files on this window!"}, + d.files, + } + return nil +} + +func run() error { + var render, err = ui.NewRendererDefault("Files Drop Example", 800, 600) + if err != nil { + return err + } + defer render.Destroy() + + return ui.RunWait(render, ui.DefaultStyle(), &dropFiles{}, true) +} + +func main() { + var err = run() + if err != nil { + log.Fatal(err) + } +} diff --git a/ui/renderer.go b/ui/renderer.go index 91af1c4..fb69f67 100644 --- a/ui/renderer.go +++ b/ui/renderer.go @@ -12,6 +12,7 @@ type Renderer interface { // Events PushEvents(t EventTarget, wait bool) bool Refresh() + Stamp() float64 // in seconds // Lifetime Destroy() error diff --git a/ui/ui.go b/ui/ui.go index 576f8a5..cc918ab 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -26,6 +26,13 @@ func RunWait(r Renderer, s *Style, view Control, wait bool) error { return err } } + + dragDropTarget := &dragDropEventTarget{renderer: r} + dragDrop := DefaultDragDropProvider + if dragDrop != nil { + dragDrop.Register(r.WindowHandle(), dragDropTarget) + } + anim := time.NewTicker(30 * time.Millisecond) go func() { for { @@ -62,6 +69,12 @@ func RunWait(r Renderer, s *Style, view Control, wait bool) error { } else { ctx.tooltip.Text = tooltip } + + dragDropEvents := dragDropTarget.events + dragDropTarget.events = nil + for _, e := range dragDropEvents { + ctx.Handle(e) + } } return nil }