Added drag & drop addon.

- Drop addon is based on WM_DROPFILES, dragdrop addon is based on the OLE IDragDrop interface and thus can registerer more interactions.
- The allg5ui implementation will try to fall back on the drop addon (because the dragdrop addon wouldn't work properly).
- Drop addon is refactored to use the same interface as the dragdrop addon.
This commit is contained in:
Sander Schobers 2021-06-04 17:17:22 +02:00
parent 302ae1c338
commit 3c89748eac
13 changed files with 570 additions and 54 deletions

View File

@ -0,0 +1,176 @@
#include <Windows.h>
#include <oleidl.h>
#include <stdio.h>
#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;
}

View File

@ -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 <Windows.h>
#include <oleidl.h>
#include <stdint.h>
#include <stdlib.h>
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{}
}

26
addons/dragdrop/vector.h Normal file
View File

@ -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__

View File

@ -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);
}

View File

@ -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{}
}

View File

@ -1,9 +0,0 @@
package drop
import "opslag.de/schobers/geom"
type Dropper interface {
WindowHandle() uintptr
FilesDropped(geom.PointF32, []string)
}

View File

@ -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 {

View File

@ -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 {

46
ui/dragdrop.go Normal file
View File

@ -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})
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -12,6 +12,7 @@ type Renderer interface {
// Events
PushEvents(t EventTarget, wait bool) bool
Refresh()
Stamp() float64 // in seconds
// Lifetime
Destroy() error

View File

@ -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
}