From f9e55033200405053bc277b65105de294ea0d393 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 30 Mar 2023 13:10:58 -0400 Subject: [PATCH] Pasting implemented (nonworking) --- backends/x/event.go | 15 ++-- backends/x/selection.go | 19 ++++- backends/x/selectionclaim.go | 145 +++++++++++++++++++++++++++++++++-- backends/x/window.go | 7 +- 4 files changed, 171 insertions(+), 15 deletions(-) diff --git a/backends/x/event.go b/backends/x/event.go index c355134..53552d4 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -257,12 +257,15 @@ func (window *window) handleSelectionClear ( connection *xgbutil.XUtil, event xevent.SelectionClearEvent, ) { - // TODO: schedule the claim to be deleted. when the event loop fires we - // will check to see if the claim is scheduled to be deleted and if it - // is, delete it. - if window.selectionClaim != nil { - window.selectionClaim.scheduledDelete = true - } + window.selectionClaim = nil +} + +func (window *window) handleSelectionRequest ( + connection *xgbutil.XUtil, + event xevent.SelectionRequestEvent, +) { + if window.selectionClaim == nil { return } + window.selectionClaim.handleSelectionRequest(connection, event) } func (window *window) compressExpose ( diff --git a/backends/x/selection.go b/backends/x/selection.go index 26959fa..ddfb9c9 100644 --- a/backends/x/selection.go +++ b/backends/x/selection.go @@ -146,6 +146,23 @@ func targetToMime (name string) (data.Mime, confidence) { } } +func mimeToTargets (mime data.Mime) (names []string) { + names = append(names, mime.String()) + switch mime { + case data.M("application", "pdf"): + names = append(names, "ADOBE_PORTABLE_DOCUMENT_FORMAT") + case data.M("image", "pict"): + names = append(names, "APPLE_PICT") + case data.M("application", "postscript"): + names = append(names, "POSTSCRIPT") + case data.MimeFile: + names = append(names, "FILE_NAME") + case data.MimePlain: + names = append(names, "UTF8_STRING", "TEXT", "STRING") + } + return +} + func (request *selectionRequest) handleSelectionNotify ( connection *xgbutil.XUtil, event xevent.SelectionNotifyEvent, @@ -195,7 +212,7 @@ func (request *selectionRequest) handleSelectionNotify ( // selection owner transfer the data in the selection in the following // manner. The selection requestor starts the transfer process by // deleting the (type==INCR) property forming the reply to the - // selection. + // selection. incr, err := xprop.Atm(request.window.backend.connection, "INCR") if err != nil { request.die(err); return } if reply.Type == incr { diff --git a/backends/x/selectionclaim.go b/backends/x/selectionclaim.go index 39f0260..3ded6a8 100644 --- a/backends/x/selectionclaim.go +++ b/backends/x/selectionclaim.go @@ -1,24 +1,155 @@ package x +import "io" +import "github.com/jezek/xgb" +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xprop" +import "github.com/jezek/xgbutil/xevent" import "git.tebibyte.media/sashakoshka/tomo/data" type selectionClaim struct { + window *window data data.Data - scheduledDelete bool + name xproto.Atom } -func (window *window) newSelectionClaim (data data.Data) *selectionClaim { - return &selectionClaim{ +func (window *window) claimSelection (name xproto.Atom, data data.Data) *selectionClaim { + // Follow: + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.1 + + // A client wishing to acquire ownership of a particular selection + // should call SetSelectionOwner. The client should set the specified + // selection to the atom that represents the selection, set the + // specified owner to some window that the client created, and set the + // specified time to some time between the current last-change time of + // the selection concerned and the current server time. This time value + // usually will be obtained from the timestamp of the event that + // triggers the acquisition of the selection. Clients should not set the + // time value to CurrentTime, because if they do so, they have no way of + // finding when they gained ownership of the selection. Clients must use + // a window they created so that requestors can route events to the + // owner of the selection. + err := xproto.SetSelectionOwnerChecked ( + window.backend.connection.Conn(), + window.xWindow.Id, name, 0).Check() // FIXME: should not be zero + if err != nil { return nil } + + return &selectionClaim { + window: window, data: data, + name: name, } } -func (claim *selectionClaim) idle () bool { - // TODO +func (window *window) refuseSelectionRequest (request xevent.SelectionRequestEvent) { + // ... refuse the SelectionRequest by sending the requestor window a + // SelectionNotify event with the property set to None (by means of a + // SendEvent request with an empty event mask). + event := xproto.SelectionNotifyEvent { + Requestor: request.Requestor, + Selection: request.Selection, + Target: request.Target, + Property: 0, + }.Bytes() + xproto.SendEvent ( + window.backend.connection.Conn(), + false, request.Requestor, 0, string(event)) +} + +func (window *window) fulfillSelectionRequest ( + data []byte, + format byte, + request xevent.SelectionRequestEvent, +) { + die := func () { window.refuseSelectionRequest(request) } + + // If the specified property is not None, the owner should place the + // data resulting from converting the selection into the specified + // property on the requestor window and should set the property's type + // to some appropriate value, which need not be the same as the + // specified target. + err := xproto.ChangePropertyChecked ( + window.backend.connection.Conn(), + xproto.PropModeReplace, window.xWindow.Id, + request.Property, + request.Target, format, + uint32(len(data) / (int(format) / 8)), data).Check() + if err != nil { die() } + + // If the property is successfully stored, the owner should acknowledge + // the successful conversion by sending the requestor window a + // SelectionNotify event (by means of a SendEvent request with an empty + // mask). + event := xproto.SelectionNotifyEvent { + Requestor: request.Requestor, + Selection: request.Selection, + Target: request.Target, + Property: request.Property, + }.Bytes() + xproto.SendEvent ( + window.backend.connection.Conn(), + false, request.Requestor, 0, string(event)) } func (claim *selectionClaim) handleSelectionRequest ( - // TODO + connection *xgbutil.XUtil, + event xevent.SelectionRequestEvent, ) { - // TODO + // Follow: + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.2 + + die := func () { claim.window.refuseSelectionRequest(event) } + + // When a requestor wants the value of a selection, the owner receives a + // SelectionRequest event. The specified owner and selection will be the + // values that were specified in the SetSelectionOwner request. The + // owner should compare the timestamp with the period it has owned the + // selection and, if the time is outside, refuse the SelectionRequest. + if event.Selection != claim.name { die(); return } + + // If the specified property is None , the requestor is an obsolete + // client. Owners are encouraged to support these clients by using the + // specified target atom as the property name to be used for the reply. + if event.Property == 0 { + event.Property = event.Target + } + + // Otherwise, the owner should use the target to decide the form into + // which the selection should be converted. Some targets may be defined + // such that requestors can pass parameters along with the request. The + // owner will find these parameters in the property named in the + // selection request. The type, format, and contents of this property + // are dependent upon the definition of the target. If the target is not + // defined to have parameters, the owner should ignore the property if + // it is present. If the selection cannot be converted into a form based + // on the target (and parameters, if any), the owner should refuse the + // SelectionRequest as previously described. + targetName, err := xprop.AtomName ( + claim.window.backend.connection, event.Target) + if err != nil { die(); return } + + switch targetName { + case "TARGETS": + targetNames := []string { } + for mime := range claim.data { + targetNames = append(targetNames, mimeToTargets(mime)...) + } + data := make([]byte, len(targetNames) * 4) + for index, name := range targetNames { + atom, err := xprop.Atm(claim.window.backend.connection, name) + if err != nil { die(); return } + xgb.Put32(data[:index * 4], uint32(atom)) + } + claim.window.fulfillSelectionRequest(data, 8, event) + + default: + mime, confidence := targetToMime(targetName) + if confidence == confidenceNone { die(); return } + reader, ok := claim.data[mime] + if !ok { die(); return } + data, err := io.ReadAll(reader) + if err != nil { die() } + claim.window.fulfillSelectionRequest(data, 32, event) + } } diff --git a/backends/x/window.go b/backends/x/window.go index 945daf9..2d0b750 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -102,6 +102,8 @@ func (backend *Backend) newWindow ( Connect(backend.connection, window.xWindow.Id) xevent.SelectionClearFun(window.handleSelectionClear). Connect(backend.connection, window.xWindow.Id) + xevent.SelectionRequestFun(window.handleSelectionRequest). + Connect(backend.connection, window.xWindow.Id) window.SetTheme(backend.theme) window.SetConfig(backend.config) @@ -288,7 +290,10 @@ func (window *window) Hide () { } func (window *window) Copy (data data.Data) { - // TODO + selectionName := "CLIPBOARD" + selectionAtom, err := xprop.Atm(window.backend.connection, selectionName) + if err != nil { return } + window.selectionClaim = window.claimSelection(selectionAtom, data) } func (window *window) Paste (callback func (data.Data, error), accept ...data.Mime) {