github.com/jmigpin/editor@v1.6.0/driver/xdriver/dragndrop/dnd.go (about)

     1  package dragndrop
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  	"image"
     7  	"log"
     8  	"math"
     9  	"time"
    10  
    11  	"github.com/BurntSushi/xgb"
    12  	"github.com/BurntSushi/xgb/xproto"
    13  	"github.com/jmigpin/editor/driver/xdriver/xutil"
    14  	"github.com/jmigpin/editor/util/syncutil"
    15  	"github.com/jmigpin/editor/util/uiutil/event"
    16  )
    17  
    18  // protocol: https://www.acc.umu.se/~vatten/XDND.html
    19  // explanation with example: http://www.edwardrosten.com/code/dist/x_clipboard-1.1/paste.cc
    20  
    21  // Drag and drop
    22  type Dnd struct {
    23  	conn *xgb.Conn
    24  	win  xproto.Window
    25  	data DndData
    26  	sw   *syncutil.WaitForSet
    27  }
    28  
    29  func NewDnd(conn *xgb.Conn, win xproto.Window) (*Dnd, error) {
    30  	if err := xutil.LoadAtoms(conn, &DndAtoms, false); err != nil {
    31  		return nil, err
    32  	}
    33  	if err := xutil.LoadAtoms(conn, &DropTypeAtoms, false); err != nil {
    34  		return nil, err
    35  	}
    36  	dnd := &Dnd{conn: conn, win: win}
    37  	if err := dnd.setupWindowProperty(); err != nil {
    38  		return nil, err
    39  	}
    40  	dnd.sw = syncutil.NewWaitForSet()
    41  	return dnd, nil
    42  }
    43  
    44  // Allow other applications to know this program is dnd aware.
    45  func (dnd *Dnd) setupWindowProperty() error {
    46  	data := []byte{xproto.AtomBitmap, 0, 0, 0}
    47  	cookie := xproto.ChangePropertyChecked(
    48  		dnd.conn,
    49  		xproto.PropModeAppend, // mode
    50  		dnd.win,
    51  		DndAtoms.XdndAware, // atom
    52  		xproto.AtomAtom,    // type
    53  		32,                 // format: xprop says that it should be 32 bit
    54  		uint32(len(data))/4,
    55  		data)
    56  	return cookie.Check()
    57  }
    58  
    59  //----------
    60  
    61  // Error could be nil.
    62  func (dnd *Dnd) OnClientMessage(ev *xproto.ClientMessageEvent) (ev_ interface{}, _ error, ok bool) {
    63  	if ev.Format != 32 {
    64  		err := fmt.Errorf("dnd event: data format is not 32: %d", ev.Format)
    65  		return nil, err, true
    66  	}
    67  	data := ev.Data.Data32
    68  	switch ev.Type {
    69  	case DndAtoms.XdndEnter:
    70  		// first event to happen on a drag and drop
    71  		dnd.onEnter(data)
    72  	case DndAtoms.XdndPosition:
    73  		// after the enter event, it follows many position events
    74  		ev2, err := dnd.onPosition(data)
    75  		return ev2, err, true
    76  	case DndAtoms.XdndDrop:
    77  		// drag released
    78  		ev2, err := dnd.onDrop(data)
    79  		return ev2, err, true
    80  	case DndAtoms.XdndLeave:
    81  		dnd.clearData()
    82  	}
    83  	return nil, nil, false
    84  }
    85  
    86  //----------
    87  
    88  func (dnd *Dnd) onEnter(data []uint32) {
    89  	dnd.data.hasEnter = true
    90  	dnd.data.enter.win = xproto.Window(data[0])
    91  	dnd.data.enter.moreThan3DataTypes = data[1]&1 == 1
    92  	dnd.data.enter.types = []xproto.Atom{
    93  		xproto.Atom(data[2]),
    94  		xproto.Atom(data[3]),
    95  		xproto.Atom(data[4]),
    96  	}
    97  
    98  	if dnd.data.enter.moreThan3DataTypes {
    99  		atoms, err := dnd.getTypeList(dnd.data.enter.win)
   100  		if err != nil {
   101  			return
   102  		}
   103  		w := &dnd.data.enter.types
   104  		*w = append(*w, atoms...)
   105  
   106  		// DEBUG
   107  		//xutil.PrintAtomsNames(dnd.conn, atoms...)
   108  	}
   109  
   110  	// translate types
   111  	u := []event.DndType{}
   112  	for _, t := range dnd.data.enter.types {
   113  		switch t {
   114  		case DropTypeAtoms.TextURLList:
   115  			u = append(u, event.TextURLListDndT)
   116  		}
   117  	}
   118  	dnd.data.enter.eventTypes = u
   119  }
   120  
   121  //----------
   122  
   123  func (dnd *Dnd) onPosition(data []uint32) (ev interface{}, _ error) {
   124  	// must have had a dnd enter event before
   125  	if !dnd.data.hasEnter {
   126  		return nil, fmt.Errorf("missing dnd enter event")
   127  	}
   128  
   129  	// position event window must be the same as the enter event
   130  	win := xproto.Window(data[0])
   131  	if win != dnd.data.enter.win {
   132  		return nil, fmt.Errorf("bad dnd window: %v (expecting %v)", win, dnd.data.enter.win)
   133  	}
   134  
   135  	// point
   136  	screenPoint := image.Point{int(data[2] >> 16), int(data[2] & 0xffff)}
   137  	p, err := dnd.screenToWindowPoint(screenPoint)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("unable to pass screen to window point: %w", err)
   140  	}
   141  
   142  	dnd.data.hasPosition = true
   143  	dnd.data.position.point = p
   144  	dnd.data.position.action = xproto.Atom(data[4])
   145  
   146  	ev = &event.DndPosition{p, dnd.data.enter.eventTypes, dnd.positionReply}
   147  	return ev, nil
   148  }
   149  
   150  func (dnd *Dnd) positionReply(action event.DndAction) {
   151  	a := dnd.data.position.action
   152  	accept := true
   153  	switch action {
   154  	case event.DndADeny:
   155  		accept = false
   156  	case event.DndACopy:
   157  		a = DndAtoms.XdndActionCopy
   158  	case event.DndAMove:
   159  		a = DndAtoms.XdndActionMove
   160  	case event.DndALink:
   161  		a = DndAtoms.XdndActionLink
   162  	case event.DndAAsk:
   163  		a = DndAtoms.XdndActionAsk
   164  	case event.DndAPrivate:
   165  		a = DndAtoms.XdndActionPrivate
   166  	default:
   167  		log.Printf("unhandled dnd action %v", action)
   168  	}
   169  	dnd.sendStatus(dnd.data.enter.win, a, accept)
   170  }
   171  
   172  //----------
   173  
   174  func (dnd *Dnd) onDrop(data []uint32) (ev interface{}, _ error) {
   175  	// must have had a dnd position event before
   176  	if !dnd.data.hasPosition {
   177  		return nil, fmt.Errorf("missing dnd position event")
   178  	}
   179  
   180  	// drop event window must be the same as the enter event
   181  	win := xproto.Window(data[0])
   182  	if win != dnd.data.enter.win {
   183  		return nil, fmt.Errorf("bad dnd window: %v (expecting %v)", win, dnd.data.enter.win)
   184  	}
   185  
   186  	dnd.data.hasDrop = true
   187  	dnd.data.drop.timestamp = xproto.Timestamp(data[2])
   188  
   189  	ev = &event.DndDrop{dnd.data.position.point, dnd.replyAcceptDrop, dnd.requestDropData}
   190  	return ev, nil
   191  }
   192  func (dnd *Dnd) replyAcceptDrop(v bool) {
   193  	dnd.sendFinished(dnd.data.enter.win, dnd.data.position.action, v)
   194  	dnd.clearData()
   195  }
   196  func (dnd *Dnd) requestDropData(t event.DndType) ([]byte, error) {
   197  	// translate type
   198  	var t2 xproto.Atom
   199  	switch t {
   200  	case event.TextURLListDndT:
   201  		t2 = DropTypeAtoms.TextURLList
   202  	default:
   203  		return nil, fmt.Errorf("unhandled type: %v", t)
   204  	}
   205  
   206  	dnd.sw.Start(1500 * time.Millisecond)
   207  	dnd.requestData(t2)
   208  	v, err := dnd.sw.WaitForSet()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	ev := v.(*xproto.SelectionNotifyEvent)
   213  
   214  	return dnd.extractData(ev)
   215  }
   216  
   217  //----------
   218  
   219  // Called after a request for data.
   220  func (dnd *Dnd) OnSelectionNotify(ev *xproto.SelectionNotifyEvent) {
   221  	if !dnd.data.hasDrop {
   222  		return
   223  	}
   224  	// timestamps must match
   225  	if ev.Time != dnd.data.drop.timestamp {
   226  		return
   227  	}
   228  
   229  	err := dnd.sw.Set(ev)
   230  	if err != nil {
   231  		log.Print(fmt.Errorf("onselectionnotify: %w", err))
   232  	}
   233  }
   234  
   235  //----------
   236  
   237  func (dnd *Dnd) requestData(typ xproto.Atom) {
   238  	// will get selection-notify event
   239  	_ = xproto.ConvertSelection(
   240  		dnd.conn,
   241  		dnd.win,
   242  		DndAtoms.XdndSelection,
   243  		typ,
   244  		xproto.AtomPrimary,
   245  		dnd.data.drop.timestamp)
   246  }
   247  func (dnd *Dnd) extractData(ev *xproto.SelectionNotifyEvent) ([]byte, error) {
   248  	cookie := xproto.GetProperty(
   249  		dnd.conn,
   250  		false, // delete,
   251  		dnd.win,
   252  		ev.Property,    // property that contains the data
   253  		ev.Target,      // type
   254  		0,              // long offset
   255  		math.MaxUint32) // long length
   256  	reply, err := cookie.Reply()
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	return reply.Value, nil
   261  }
   262  
   263  func (dnd *Dnd) sendFinished(win xproto.Window, action xproto.Atom, accepted bool) {
   264  	u := FinishedEvent{dnd.win, accepted, action}
   265  	cme := &xproto.ClientMessageEvent{
   266  		Type:   DndAtoms.XdndFinished,
   267  		Window: win,
   268  		Format: 32,
   269  		Data:   xproto.ClientMessageDataUnionData32New(u.Data32()),
   270  	}
   271  	dnd.sendClientMessage(cme)
   272  }
   273  
   274  func (dnd *Dnd) sendStatus(win xproto.Window, action xproto.Atom, accept bool) {
   275  	flags := uint32(StatusEventSendPositionsFlag)
   276  	if accept {
   277  		flags |= StatusEventAcceptFlag
   278  	}
   279  	u := StatusEvent{dnd.win, flags, action}
   280  	cme := &xproto.ClientMessageEvent{
   281  		Type:   DndAtoms.XdndStatus,
   282  		Window: win,
   283  		Format: 32,
   284  		Data:   xproto.ClientMessageDataUnionData32New(u.Data32()),
   285  	}
   286  	dnd.sendClientMessage(cme)
   287  }
   288  
   289  //----------
   290  
   291  func (dnd *Dnd) sendClientMessage(cme *xproto.ClientMessageEvent) {
   292  	_ = xproto.SendEvent(
   293  		dnd.conn,
   294  		false, // propagate
   295  		cme.Window,
   296  		xproto.EventMaskNoEvent,
   297  		string(cme.Bytes()))
   298  }
   299  
   300  func (dnd *Dnd) screenToWindowPoint(sp image.Point) (image.Point, error) {
   301  	cookie := xproto.GetGeometry(dnd.conn, xproto.Drawable(dnd.win))
   302  	geom, err := cookie.Reply()
   303  	if err != nil {
   304  		return image.Point{}, err
   305  	}
   306  	x := int(geom.X) + int(geom.BorderWidth)
   307  	y := int(geom.Y) + int(geom.BorderWidth)
   308  	winMin := image.Point{x, y}
   309  	return sp.Sub(winMin), nil
   310  }
   311  
   312  func (dnd *Dnd) clearData() {
   313  	dnd.data = DndData{}
   314  }
   315  
   316  //----------
   317  
   318  func (dnd *Dnd) getTypeList(win xproto.Window) ([]xproto.Atom, error) {
   319  	cookie := xproto.GetProperty(
   320  		dnd.conn,
   321  		false, // delete,
   322  		win,
   323  		DndAtoms.XdndTypeList, // property that contains the data
   324  		xproto.AtomAtom,       // type
   325  		0,                     // long offset
   326  		math.MaxUint32)        // long length
   327  	reply, err := cookie.Reply()
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	// convert bytes to []xproto.Atom
   333  	sizeOfAtom := 4 // bytes
   334  	raw := reply.Value
   335  	atoms := make([]xproto.Atom, len(raw)/sizeOfAtom)
   336  	for i := range atoms {
   337  		v := raw[i*sizeOfAtom : (i+1)*sizeOfAtom]
   338  		atoms[i] = xproto.Atom(binary.LittleEndian.Uint32(v))
   339  	}
   340  
   341  	return atoms, nil
   342  }
   343  
   344  //----------
   345  //----------
   346  //----------
   347  
   348  type DndData struct {
   349  	hasEnter    bool
   350  	hasPosition bool
   351  	hasDrop     bool
   352  	enter       struct {
   353  		win                xproto.Window
   354  		types              []xproto.Atom
   355  		moreThan3DataTypes bool
   356  		eventTypes         []event.DndType
   357  	}
   358  	position struct {
   359  		point  image.Point
   360  		action xproto.Atom
   361  	}
   362  	drop struct {
   363  		timestamp xproto.Timestamp
   364  	}
   365  }
   366  
   367  //----------
   368  
   369  var DndAtoms struct {
   370  	XdndAware    xproto.Atom
   371  	XdndEnter    xproto.Atom
   372  	XdndLeave    xproto.Atom
   373  	XdndPosition xproto.Atom
   374  	XdndStatus   xproto.Atom
   375  	XdndDrop     xproto.Atom
   376  	XdndFinished xproto.Atom
   377  
   378  	XdndActionCopy    xproto.Atom
   379  	XdndActionMove    xproto.Atom
   380  	XdndActionLink    xproto.Atom
   381  	XdndActionAsk     xproto.Atom
   382  	XdndActionPrivate xproto.Atom
   383  
   384  	XdndProxy    xproto.Atom
   385  	XdndTypeList xproto.Atom
   386  
   387  	XdndSelection xproto.Atom
   388  }
   389  
   390  //----------
   391  
   392  var DropTypeAtoms struct {
   393  	TextURLList xproto.Atom `loadAtoms:"text/uri-list"` // technically, a URL
   394  }