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 }