This should have been several separate commits
This commit is contained in:
		
							parent
							
								
									6f15ff3366
								
							
						
					
					
						commit
						0aede3502b
					
				| @ -1,14 +1,11 @@ | |||||||
| package x | package x | ||||||
| 
 | 
 | ||||||
| import "bytes" |  | ||||||
| import "image" | import "image" | ||||||
| import "errors" |  | ||||||
| import "git.tebibyte.media/sashakoshka/tomo/input" | import "git.tebibyte.media/sashakoshka/tomo/input" | ||||||
| import "git.tebibyte.media/sashakoshka/tomo/elements" | import "git.tebibyte.media/sashakoshka/tomo/elements" | ||||||
| 
 | 
 | ||||||
| import "github.com/jezek/xgbutil" | import "github.com/jezek/xgbutil" | ||||||
| import "github.com/jezek/xgb/xproto" | import "github.com/jezek/xgb/xproto" | ||||||
| import "github.com/jezek/xgbutil/xprop" |  | ||||||
| import "github.com/jezek/xgbutil/xevent" | import "github.com/jezek/xgbutil/xevent" | ||||||
| 
 | 
 | ||||||
| type scrollSum struct { | type scrollSum struct { | ||||||
| @ -242,58 +239,9 @@ func (window *window) handleSelectionNotify ( | |||||||
| 	connection *xgbutil.XUtil, | 	connection *xgbutil.XUtil, | ||||||
| 	event xevent.SelectionNotifyEvent, | 	event xevent.SelectionNotifyEvent, | ||||||
| ) { | ) { | ||||||
| 	// Follow: |  | ||||||
| 	// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 |  | ||||||
| 	if window.selectionRequest == nil { return } | 	if window.selectionRequest == nil { return } | ||||||
| 	die := func (err error) { | 	window.selectionRequest.handleSelectionNotify(connection, event) | ||||||
| 		window.selectionRequest(nil, err) | 	if !window.selectionRequest.open() { window.selectionRequest = nil } | ||||||
| 		window.selectionRequest = nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// If the property argument is None, the conversion has been refused. |  | ||||||
| 	// This can mean either that there is no owner for the selection, that |  | ||||||
| 	// the owner does not support the conversion implied by the target, or |  | ||||||
| 	// that the server did not have sufficient space to accommodate the |  | ||||||
| 	// data. |  | ||||||
| 	if event.Property == 0 { die(nil); return } |  | ||||||
| 
 |  | ||||||
| 	// When using GetProperty to retrieve the value of a selection, the |  | ||||||
| 	// property argument should be set to the corresponding value in the |  | ||||||
| 	// SelectionNotify event. Because the requestor has no way of knowing |  | ||||||
| 	// beforehand what type the selection owner will use, the type argument |  | ||||||
| 	// should be set to AnyPropertyType. Several GetProperty requests may be |  | ||||||
| 	// needed to retrieve all the data in the selection; each should set the |  | ||||||
| 	// long-offset argument to the amount of data received so far, and the |  | ||||||
| 	// size argument to some reasonable buffer size (see section 2.5). If |  | ||||||
| 	// the returned value of bytes-after is zero, the whole property has |  | ||||||
| 	// been transferred.  |  | ||||||
| 	reply, err := xproto.GetProperty ( |  | ||||||
| 		connection.Conn(), false, window.xWindow.Id, event.Property, |  | ||||||
| 		xproto.GetPropertyTypeAny, 0, (1 << 32) - 1).Reply() |  | ||||||
| 	if err != nil { die(err); return } |  | ||||||
| 	if reply.Format == 0 { |  | ||||||
| 		die(errors.New("x: missing selection property")) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Once all the data in the selection has been retrieved (which may |  | ||||||
| 	// require getting the values of several properties &emdash; see section |  | ||||||
| 	// 2.7), the requestor should delete the property in the SelectionNotify |  | ||||||
| 	// request by using a GetProperty request with the delete argument set |  | ||||||
| 	// to True. As previously discussed, the owner has no way of knowing |  | ||||||
| 	// when the data has been transferred to the requestor unless the |  | ||||||
| 	// property is removed. |  | ||||||
| 	propertyAtom, err := xprop.Atm(window.backend.connection, "TOMO_SELECTION") |  | ||||||
| 	if err != nil { die(err); return } |  | ||||||
| 	err = xproto.DeletePropertyChecked ( |  | ||||||
| 		window.backend.connection.Conn(), |  | ||||||
| 		window.xWindow.Id, |  | ||||||
| 		propertyAtom).Check() |  | ||||||
| 	if err != nil { die(err); return } |  | ||||||
| 
 |  | ||||||
| 	// TODO: possibly do some conversion here? |  | ||||||
| 	window.selectionRequest(bytes.NewReader(reply.Value), nil) |  | ||||||
| 	window.selectionRequest = nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (window *window) compressExpose ( | func (window *window) compressExpose ( | ||||||
|  | |||||||
							
								
								
									
										158
									
								
								backends/x/selection.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								backends/x/selection.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,158 @@ | |||||||
|  | package x | ||||||
|  | 
 | ||||||
|  | import "errors" | ||||||
|  | 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 selReqState int; const ( | ||||||
|  | 	selReqStateClosed selReqState = iota | ||||||
|  | 	selReqStateAwaitSelectionNotify | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type selectionRequest struct { | ||||||
|  | 	state       selReqState | ||||||
|  | 	window      *window | ||||||
|  | 	source      xproto.Atom | ||||||
|  | 	destination xproto.Atom | ||||||
|  | 	accept      []data.Mime | ||||||
|  | 	callback func (data.Data, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: take in multiple formats and check the TARGETS list against them. | ||||||
|  | 
 | ||||||
|  | func (window *window) newSelectionRequest ( | ||||||
|  | 	source, destination xproto.Atom, | ||||||
|  | 	callback func (data.Data, error), | ||||||
|  | 	accept ...data.Mime, | ||||||
|  | ) ( | ||||||
|  | 	request *selectionRequest, | ||||||
|  | ) { | ||||||
|  | 	request = &selectionRequest { | ||||||
|  | 		source:      source, | ||||||
|  | 		destination: destination, | ||||||
|  | 		window:      window, | ||||||
|  | 		accept:      accept, | ||||||
|  | 		callback:    callback, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO: account for all types in accept slice | ||||||
|  | 	targetName := request.accept[0].String() | ||||||
|  | 	if request.accept[0] == data.M("text", "plain") { | ||||||
|  | 		targetName = "UTF8_STRING" | ||||||
|  | 	} | ||||||
|  | 	targetAtom, err := xprop.Atm(window.backend.connection, targetName) | ||||||
|  | 	if err != nil { request.die(err); return } | ||||||
|  | 
 | ||||||
|  | 	// The requestor should set the property argument to the name of a | ||||||
|  | 	// property that the owner can use to report the value of the selection. | ||||||
|  | 	// Requestors should ensure that the named property does not exist on | ||||||
|  | 	// the window before issuing the ConvertSelection. The exception to this | ||||||
|  | 	// rule is when the requestor intends to pass parameters with the | ||||||
|  | 	// request. Some targets may be defined such that requestors can pass | ||||||
|  | 	// parameters along with the request. If the requestor wishes to provide | ||||||
|  | 	// parameters to a request, they should be placed in the specified | ||||||
|  | 	// property on the requestor window before the requestor issues the | ||||||
|  | 	// ConvertSelection request, and this property should be named in the | ||||||
|  | 	// request. | ||||||
|  | 	err = xproto.DeletePropertyChecked ( | ||||||
|  | 		window.backend.connection.Conn(), | ||||||
|  | 		window.xWindow.Id, | ||||||
|  | 		request.destination).Check() | ||||||
|  | 	if err != nil { request.die(err); return } | ||||||
|  | 
 | ||||||
|  | 	// The selection argument specifies the particular selection involved, | ||||||
|  | 	// and the target argument specifies the required form of the | ||||||
|  | 	// information. For information about the choice of suitable atoms to | ||||||
|  | 	// use, see section 2.6. The requestor should set the requestor argument | ||||||
|  | 	// to a window that it created; the owner will place the reply property | ||||||
|  | 	// there. The requestor should set the time argument to the timestamp on | ||||||
|  | 	// the event that triggered the request for the selection value. Note | ||||||
|  | 	// that clients should not specify CurrentTime*. | ||||||
|  | 	err = xproto.ConvertSelectionChecked ( | ||||||
|  | 		request.window.backend.connection.Conn(), | ||||||
|  | 		request.window.xWindow.Id, | ||||||
|  | 		request.source, | ||||||
|  | 		targetAtom, | ||||||
|  | 		request.destination, | ||||||
|  | 		// TODO: *possibly replace this zero with an actual timestamp | ||||||
|  | 		// received from the server. this is non-trivial as we cannot | ||||||
|  | 		// rely on the timestamp of the last received event, because | ||||||
|  | 		// there is a possibility that this method is invoked | ||||||
|  | 		// asynchronously from within tomo.Do(). | ||||||
|  | 		0).Check() | ||||||
|  | 	if err != nil { request.die(err); return } | ||||||
|  | 
 | ||||||
|  | 	request.state = selReqStateAwaitSelectionNotify | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (request *selectionRequest) die (err error) { | ||||||
|  | 	request.callback(nil, err) | ||||||
|  | 	request.state = selReqStateClosed | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (request *selectionRequest) finalize (data data.Data) { | ||||||
|  | 	request.callback(data, nil) | ||||||
|  | 	request.state = selReqStateClosed | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (request *selectionRequest) open () bool { | ||||||
|  | 	return request.state != selReqStateClosed | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (request *selectionRequest) handleSelectionNotify ( | ||||||
|  | 	connection *xgbutil.XUtil, | ||||||
|  | 	event xevent.SelectionNotifyEvent, | ||||||
|  | ) { | ||||||
|  | 	// Follow: | ||||||
|  | 	// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 | ||||||
|  | 	 | ||||||
|  | 	// If the property argument is None, the conversion has been refused. | ||||||
|  | 	// This can mean either that there is no owner for the selection, that | ||||||
|  | 	// the owner does not support the conversion implied by the target, or | ||||||
|  | 	// that the server did not have sufficient space to accommodate the | ||||||
|  | 	// data. | ||||||
|  | 	if event.Property == 0 { request.die(nil); return } | ||||||
|  | 
 | ||||||
|  | 	// TODO: handle INCR | ||||||
|  | 
 | ||||||
|  | 	// When using GetProperty to retrieve the value of a selection, the | ||||||
|  | 	// property argument should be set to the corresponding value in the | ||||||
|  | 	// SelectionNotify event. Because the requestor has no way of knowing | ||||||
|  | 	// beforehand what type the selection owner will use, the type argument | ||||||
|  | 	// should be set to AnyPropertyType. Several GetProperty requests may be | ||||||
|  | 	// needed to retrieve all the data in the selection; each should set the | ||||||
|  | 	// long-offset argument to the amount of data received so far, and the | ||||||
|  | 	// size argument to some reasonable buffer size (see section 2.5). If | ||||||
|  | 	// the returned value of bytes-after is zero, the whole property has | ||||||
|  | 	// been transferred. | ||||||
|  | 	reply, err := xproto.GetProperty ( | ||||||
|  | 		connection.Conn(), false, request.window.xWindow.Id, | ||||||
|  | 		event.Property, xproto.GetPropertyTypeAny, | ||||||
|  | 		0, (1 << 32) - 1).Reply() | ||||||
|  | 	if err != nil { request.die(err); return } | ||||||
|  | 	if reply.Format == 0 { | ||||||
|  | 		request.die(errors.New("x: missing selection property")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Once all the data in the selection has been retrieved (which may | ||||||
|  | 	// require getting the values of several properties &emdash; see section | ||||||
|  | 	// 2.7), the requestor should delete the property in the SelectionNotify | ||||||
|  | 	// request by using a GetProperty request with the delete argument set | ||||||
|  | 	// to True. As previously discussed, the owner has no way of knowing | ||||||
|  | 	// when the data has been transferred to the requestor unless the | ||||||
|  | 	// property is removed. | ||||||
|  | 	if err != nil { request.die(err); return } | ||||||
|  | 	err = xproto.DeletePropertyChecked ( | ||||||
|  | 		request.window.backend.connection.Conn(), | ||||||
|  | 		request.window.xWindow.Id, | ||||||
|  | 		request.destination).Check() | ||||||
|  | 	if err != nil { request.die(err); return } | ||||||
|  | 
 | ||||||
|  | 	// FIXME: get the mime type from the selection owner's response | ||||||
|  | 	request.finalize(data.Bytes(request.accept[0],reply.Value)) | ||||||
|  | } | ||||||
| @ -1,6 +1,5 @@ | |||||||
| package x | package x | ||||||
| 
 | 
 | ||||||
| import "io" |  | ||||||
| import "image" | import "image" | ||||||
| import "errors" | import "errors" | ||||||
| import "github.com/jezek/xgb/xproto" | import "github.com/jezek/xgb/xproto" | ||||||
| @ -34,7 +33,7 @@ type window struct { | |||||||
| 	theme  theme.Theme | 	theme  theme.Theme | ||||||
| 	config config.Config | 	config config.Config | ||||||
| 
 | 
 | ||||||
| 	selectionRequest func (io.Reader, error) | 	selectionRequest *selectionRequest | ||||||
| 
 | 
 | ||||||
| 	metrics struct { | 	metrics struct { | ||||||
| 		width  int | 		width  int | ||||||
| @ -286,75 +285,26 @@ func (window *window) Copy (data data.Data) { | |||||||
| 	// TODO | 	// TODO | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (window *window) Paste (accept data.Mime, callback func (io.Reader, error)) { | func (window *window) Paste (callback func (data.Data, error), accept ...data.Mime) { | ||||||
| 	// Follow: | 	// Follow: | ||||||
| 	// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 | 	// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 | ||||||
| 
 | 	die := func (err error) { callback(nil, err) } | ||||||
| 	die := func (err error) { |  | ||||||
| 		window.selectionRequest = nil |  | ||||||
| 		callback(nil, err) |  | ||||||
| 	} |  | ||||||
| 	if window.selectionRequest != nil { | 	if window.selectionRequest != nil { | ||||||
| 		// TODO: add the request to a queue and take care of it when the | 		// TODO: add the request to a queue and take care of it when the | ||||||
| 		// current selection has completed | 		// current selection has completed | ||||||
| 		die(errors.New("there is already a selection request")) | 		die(errors.New("there is already a selection request")) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	selectionName := "CLIPBOARD" | 	selectionName := "CLIPBOARD" | ||||||
| 	propertyName  := "TOMO_SELECTION" | 	propertyName  := "TOMO_SELECTION" | ||||||
| 	targetName    := accept.String() |  | ||||||
| 	if accept == data.M("text", "plain") { |  | ||||||
| 		targetName = "UTF8_STRING" |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// get atoms |  | ||||||
| 	selectionAtom, err := xprop.Atm(window.backend.connection, selectionName) | 	selectionAtom, err := xprop.Atm(window.backend.connection, selectionName) | ||||||
| 	if err != nil { die(err); return } | 	if err != nil { die(err); return } | ||||||
| 	targetAtom, err := xprop.Atm(window.backend.connection, targetName) |  | ||||||
| 	if err != nil { die(err); return } |  | ||||||
| 	propertyAtom, err := xprop.Atm(window.backend.connection, propertyName) | 	propertyAtom, err := xprop.Atm(window.backend.connection, propertyName) | ||||||
| 	if err != nil { die(err); return } | 	if err != nil { die(err); return } | ||||||
| 
 | 
 | ||||||
| 	// The requestor should set the property argument to the name of a | 	window.selectionRequest = window.newSelectionRequest ( | ||||||
| 	// property that the owner can use to report the value of the selection. | 		selectionAtom, propertyAtom, callback, accept...) | ||||||
| 	// Requestors should ensure that the named property does not exist on |  | ||||||
| 	// the window before issuing the ConvertSelection. The exception to this |  | ||||||
| 	// rule is when the requestor intends to pass parameters with the |  | ||||||
| 	// request. Some targets may be defined such that requestors can pass |  | ||||||
| 	// parameters along with the request. If the requestor wishes to provide |  | ||||||
| 	// parameters to a request, they should be placed in the specified |  | ||||||
| 	// property on the requestor window before the requestor issues the |  | ||||||
| 	// ConvertSelection request, and this property should be named in the |  | ||||||
| 	// request. |  | ||||||
| 	err = xproto.DeletePropertyChecked ( |  | ||||||
| 		window.backend.connection.Conn(), |  | ||||||
| 		window.xWindow.Id, |  | ||||||
| 		propertyAtom).Check() |  | ||||||
| 	if err != nil { die(err); return } |  | ||||||
| 
 |  | ||||||
| 	// The selection argument specifies the particular selection involved, |  | ||||||
| 	// and the target argument specifies the required form of the |  | ||||||
| 	// information. For information about the choice of suitable atoms to |  | ||||||
| 	// use, see section 2.6. The requestor should set the requestor argument |  | ||||||
| 	// to a window that it created; the owner will place the reply property |  | ||||||
| 	// there. The requestor should set the time argument to the timestamp on |  | ||||||
| 	// the event that triggered the request for the selection value. Note |  | ||||||
| 	// that clients should not specify CurrentTime*. |  | ||||||
| 	err = xproto.ConvertSelectionChecked ( |  | ||||||
| 		window.backend.connection.Conn(), |  | ||||||
| 		window.xWindow.Id, |  | ||||||
| 		selectionAtom, |  | ||||||
| 		targetAtom, |  | ||||||
| 		propertyAtom, |  | ||||||
| 		// TODO: *possibly replace this zero with an actual timestamp |  | ||||||
| 		// received from the server. this is non-trivial as we cannot |  | ||||||
| 		// rely on the timestamp of the last received event, because |  | ||||||
| 		// there is a possibility that this method is invoked |  | ||||||
| 		// asynchronously from within tomo.Do(). |  | ||||||
| 		0).Check() |  | ||||||
| 	if err != nil { die(err); return } |  | ||||||
| 
 |  | ||||||
| 	window.selectionRequest = callback |  | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								data/data.go
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								data/data.go
									
									
									
									
									
								
							| @ -35,7 +35,22 @@ func (byteReadCloser) Close () error { return nil } | |||||||
| 
 | 
 | ||||||
| // Text returns plain text Data given a string. | // Text returns plain text Data given a string. | ||||||
| func Text (text string) Data { | func Text (text string) Data { | ||||||
|  | 	return Bytes(MimePlain, []byte(text)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Bytes constructs a Data given a buffer and a mime type. | ||||||
|  | func Bytes (mime Mime, buffer []byte) Data { | ||||||
| 	return Data { | 	return Data { | ||||||
| 		MimePlain: byteReadCloser { bytes.NewReader([]byte(text)) }, | 		mime: byteReadCloser { bytes.NewReader(buffer) }, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // Merge combines several Datas together. If multiple Datas provide a reader for | ||||||
|  | // the same mime type, the ones further on in the list will take precedence. | ||||||
|  | func Merge (individual ...Data) (combined Data) { | ||||||
|  | 	for _, data := range individual { | ||||||
|  | 	for mime, reader := range data { | ||||||
|  | 		combined[mime] = reader | ||||||
|  | 	}} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| package elements | package elements | ||||||
| 
 | 
 | ||||||
| import "io" |  | ||||||
| import "image" | import "image" | ||||||
| import "git.tebibyte.media/sashakoshka/tomo/data" | import "git.tebibyte.media/sashakoshka/tomo/data" | ||||||
| 
 | 
 | ||||||
| @ -35,7 +34,7 @@ type Window interface { | |||||||
| 	// available, the callback is called with the clipboard data. If there | 	// available, the callback is called with the clipboard data. If there | ||||||
| 	// was no data matching the requested mime type found, nil is passed to | 	// was no data matching the requested mime type found, nil is passed to | ||||||
| 	// the callback instead. | 	// the callback instead. | ||||||
| 	Paste (accept data.Mime, callback func (io.Reader, error)) | 	Paste (callback func (data.Data, error), accept ...data.Mime) | ||||||
| 
 | 
 | ||||||
| 	// Show shows the window. The window starts off hidden, so this must be | 	// Show shows the window. The window starts off hidden, so this must be | ||||||
| 	// called after initial setup to make sure it is visible. | 	// called after initial setup to make sure it is visible. | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import "io" | import "io" | ||||||
|  | import "image" | ||||||
| import "git.tebibyte.media/sashakoshka/tomo" | import "git.tebibyte.media/sashakoshka/tomo" | ||||||
| import "git.tebibyte.media/sashakoshka/tomo/data" | import "git.tebibyte.media/sashakoshka/tomo/data" | ||||||
| import "git.tebibyte.media/sashakoshka/tomo/theme" | import "git.tebibyte.media/sashakoshka/tomo/theme" | ||||||
| @ -15,7 +16,7 @@ func main () { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func run () { | func run () { | ||||||
| 	window, _ := tomo.NewWindow(2, 2) | 	window, _ := tomo.NewWindow(256, 2) | ||||||
| 	window.SetTitle("Clipboard") | 	window.SetTitle("Clipboard") | ||||||
| 
 | 
 | ||||||
| 	container := containers.NewContainer(basicLayouts.Vertical { true, true }) | 	container := containers.NewContainer(basicLayouts.Vertical { true, true }) | ||||||
| @ -25,8 +26,41 @@ func run () { | |||||||
| 	copyButton.SetIcon(theme.IconCopy) | 	copyButton.SetIcon(theme.IconCopy) | ||||||
| 	pasteButton := basicElements.NewButton("Paste") | 	pasteButton := basicElements.NewButton("Paste") | ||||||
| 	pasteButton.SetIcon(theme.IconPaste) | 	pasteButton.SetIcon(theme.IconPaste) | ||||||
|  | 	pasteImageButton := basicElements.NewButton("Image") | ||||||
|  | 	pasteImageButton.SetIcon(theme.IconPictures) | ||||||
| 
 | 
 | ||||||
| 	clipboardCallback := func (clipboard io.Reader, err error) { | 	imageClipboardCallback := func (clipboard data.Data, err error) { | ||||||
|  | 		if err != nil { | ||||||
|  | 			popups.NewDialog ( | ||||||
|  | 				popups.DialogKindError, | ||||||
|  | 				window, | ||||||
|  | 				"Error", | ||||||
|  | 				"Cannot get clipboard:\n" + err.Error()) | ||||||
|  | 			return  | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		imageData, ok := clipboard[data.M("image", "png")] | ||||||
|  | 		if !ok { | ||||||
|  | 			popups.NewDialog ( | ||||||
|  | 				popups.DialogKindError, | ||||||
|  | 				window, | ||||||
|  | 				"Clipboard Empty", | ||||||
|  | 				"No image data in clipboard") | ||||||
|  | 			return  | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		img, _, err := image.Decode(imageData) | ||||||
|  | 		if err != nil { | ||||||
|  | 			popups.NewDialog ( | ||||||
|  | 				popups.DialogKindError, | ||||||
|  | 				window, | ||||||
|  | 				"Error", | ||||||
|  | 				"Cannot decode image:\n" + err.Error()) | ||||||
|  | 			return  | ||||||
|  | 		} | ||||||
|  | 		imageWindow(img) | ||||||
|  | 	} | ||||||
|  | 	clipboardCallback := func (clipboard data.Data, err error) { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			popups.NewDialog ( | 			popups.NewDialog ( | ||||||
| 				popups.DialogKindError, | 				popups.DialogKindError, | ||||||
| @ -36,7 +70,8 @@ func run () { | |||||||
| 			return  | 			return  | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		if clipboard == nil { | 		textData, ok := clipboard[data.MimePlain] | ||||||
|  | 		if !ok { | ||||||
| 			popups.NewDialog ( | 			popups.NewDialog ( | ||||||
| 				popups.DialogKindError, | 				popups.DialogKindError, | ||||||
| 				window, | 				window, | ||||||
| @ -45,24 +80,40 @@ func run () { | |||||||
| 			return  | 			return  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		text, _ := io.ReadAll(clipboard) | 		text, _ := io.ReadAll(textData) | ||||||
| 		tomo.Do (func () { | 		textInput.SetValue(string(text)) | ||||||
| 			textInput.SetValue(string(text)) |  | ||||||
| 		}) |  | ||||||
| 	} | 	} | ||||||
| 	copyButton.OnClick (func () { | 	copyButton.OnClick (func () { | ||||||
| 		window.Copy(data.Text(textInput.Value())) | 		window.Copy(data.Text(textInput.Value())) | ||||||
| 	}) | 	}) | ||||||
| 	pasteButton.OnClick (func () { | 	pasteButton.OnClick (func () { | ||||||
| 		window.Paste(data.MimePlain, clipboardCallback) | 		window.Paste(clipboardCallback, data.MimePlain) | ||||||
|  | 	}) | ||||||
|  | 	pasteImageButton.OnClick (func () { | ||||||
|  | 		window.Paste(imageClipboardCallback, data.M("image", "png")) | ||||||
| 	}) | 	}) | ||||||
| 	 | 	 | ||||||
| 	container.Adopt(textInput, true) | 	container.Adopt(textInput, true) | ||||||
| 	controlRow.Adopt(copyButton, true) | 	controlRow.Adopt(copyButton, true) | ||||||
| 	controlRow.Adopt(pasteButton, true) | 	controlRow.Adopt(pasteButton, true) | ||||||
|  | 	controlRow.Adopt(pasteImageButton, true) | ||||||
| 	container.Adopt(controlRow, false) | 	container.Adopt(controlRow, false) | ||||||
| 	window.Adopt(container) | 	window.Adopt(container) | ||||||
| 		 | 		 | ||||||
| 	window.OnClose(tomo.Stop) | 	window.OnClose(tomo.Stop) | ||||||
| 	window.Show() | 	window.Show() | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func imageWindow (image image.Image) { | ||||||
|  | 	window, _ := tomo.NewWindow(2, 2) | ||||||
|  | 	window.SetTitle("Clipboard Image") | ||||||
|  | 	container := containers.NewContainer(basicLayouts.Vertical { true, true }) | ||||||
|  | 	closeButton := basicElements.NewButton("Ok") | ||||||
|  | 	closeButton.SetIcon(theme.IconYes) | ||||||
|  | 	closeButton.OnClick(window.Close) | ||||||
|  | 	 | ||||||
|  | 	container.Adopt(basicElements.NewImage(image), true) | ||||||
|  | 	container.Adopt(closeButton, false) | ||||||
|  | 	window.Adopt(container) | ||||||
|  | 	window.Show() | ||||||
|  | } | ||||||
|  | |||||||
| @ -63,10 +63,12 @@ type Color int; const ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Icon lists a number of cannonical icons, each with its own ID. | // Icon lists a number of cannonical icons, each with its own ID. | ||||||
| type Icon int; const ( | type Icon int | ||||||
| 	// IconNone specifies no icon. |  | ||||||
| 	IconNone = -1 |  | ||||||
| 
 | 
 | ||||||
|  | // IconNone specifies no icon. | ||||||
|  | const IconNone = -1 | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
| 	// Place icons | 	// Place icons | ||||||
| 	IconHome Icon = iota | 	IconHome Icon = iota | ||||||
| 	Icon3DObjects | 	Icon3DObjects | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user