Add common text clipboard support (#2228)

This commit is contained in:
Zámbó, Levente 2023-01-05 06:41:07 +01:00 committed by GitHub
commit 0a32c1124e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 403 additions and 21 deletions

View file

@ -0,0 +1,36 @@
//go:build darwin
package darwin
import (
"os/exec"
)
func (f *Frontend) ClipboardGetText() (string, error) {
pasteCmd := exec.Command("pbpaste")
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func (f *Frontend) ClipboardSetText(text string) error {
copyCmd := exec.Command("pbcopy")
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

View file

@ -0,0 +1,49 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
static gchar* GetClipboardText() {
GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
return gtk_clipboard_wait_for_text(clip);
}
static void SetClipboardText(gchar* text) {
GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
gtk_clipboard_set_text(clip, text, -1);
clip = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
gtk_clipboard_set_text(clip, text, -1);
}
*/
import "C"
import "sync"
func (f *Frontend) ClipboardGetText() (string, error) {
var text string
var wg sync.WaitGroup
wg.Add(1)
invokeOnMainThread(func() {
ctxt := C.GetClipboardText()
defer C.g_free(C.gpointer(ctxt))
text = C.GoString(ctxt)
wg.Done()
})
wg.Wait()
return text, nil
}
func (f *Frontend) ClipboardSetText(text string) error {
invokeOnMainThread(func() {
ctxt := (*C.gchar)(C.CString(text))
defer C.g_free(C.gpointer(ctxt))
C.SetClipboardText(ctxt)
})
return nil
}

View file

@ -71,7 +71,6 @@ static void install_signal_handlers()
#endif
}
*/
import "C"
import (

View file

@ -0,0 +1,16 @@
//go:build windows
// +build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
)
func (f *Frontend) ClipboardGetText() (string, error) {
return win32.GetClipboardText()
}
func (f *Frontend) ClipboardSetText(text string) error {
return win32.SetClipboardText(text)
}

View file

@ -0,0 +1,143 @@
//go:build windows
/*
* Based on code originally from https://github.com/atotto/clipboard. Copyright (c) 2013 Ato Araki. All rights reserved.
*/
package win32
import (
"runtime"
"syscall"
"time"
"unsafe"
)
const (
cfUnicodetext = 13
gmemMoveable = 0x0002
)
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
func waitOpenClipboard() error {
started := time.Now()
limit := started.Add(time.Second)
var r uintptr
var err error
for time.Now().Before(limit) {
r, _, err = procOpenClipboard.Call(0)
if r != 0 {
return nil
}
time.Sleep(time.Millisecond)
}
return err
}
func GetClipboardText() (string, error) {
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if formatAvailable, _, err := procIsClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
return "", err
}
err := waitOpenClipboard()
if err != nil {
return "", err
}
h, _, err := procGetClipboardData.Call(cfUnicodetext)
if h == 0 {
_, _, _ = procCloseClipboard.Call()
return "", err
}
l, _, err := kernelGlobalLock.Call(h)
if l == 0 {
_, _, _ = procCloseClipboard.Call()
return "", err
}
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
r, _, err := kernelGlobalUnlock.Call(h)
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return "", err
}
closed, _, err := procCloseClipboard.Call()
if closed == 0 {
return "", err
}
return text, nil
}
func SetClipboardText(text string) error {
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := waitOpenClipboard()
if err != nil {
return err
}
r, _, err := procEmptyClipboard.Call(0)
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
data, err := syscall.UTF16FromString(text)
if err != nil {
return err
}
// "If the hMem parameter identifies a memory object, the object must have
// been allocated using the function with the GMEM_MOVEABLE flag."
h, _, err := kernelGlobalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
if h == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
defer func() {
if h != 0 {
kernelGlobalFree.Call(h)
}
}()
l, _, err := kernelGlobalLock.Call(h)
if l == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
r, _, err = kernelLstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
r, _, err = kernelGlobalUnlock.Call(h)
if r == 0 {
if err.(syscall.Errno) != 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
}
r, _, err = procSetClipboardData.Call(cfUnicodetext, h)
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
h = 0 // suppress deferred cleanup
closed, _, err := procCloseClipboard.Call()
if closed == 0 {
return err
}
return nil
}

View file

@ -13,16 +13,22 @@ type HANDLE uintptr
type HMONITOR HANDLE
var (
moduser32 = syscall.NewLazyDLL("user32.dll")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procGetWindowLong = moduser32.NewProc("GetWindowLongW")
procSetClassLong = moduser32.NewProc("SetClassLongW")
procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW")
procShowWindow = moduser32.NewProc("ShowWindow")
procIsWindowVisible = moduser32.NewProc("IsWindowVisible")
procGetWindowRect = moduser32.NewProc("GetWindowRect")
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW")
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow")
moduser32 = syscall.NewLazyDLL("user32.dll")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procGetWindowLong = moduser32.NewProc("GetWindowLongW")
procSetClassLong = moduser32.NewProc("SetClassLongW")
procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW")
procShowWindow = moduser32.NewProc("ShowWindow")
procIsWindowVisible = moduser32.NewProc("IsWindowVisible")
procGetWindowRect = moduser32.NewProc("GetWindowRect")
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW")
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow")
procIsClipboardFormatAvailable = moduser32.NewProc("IsClipboardFormatAvailable")
procOpenClipboard = moduser32.NewProc("OpenClipboard")
procCloseClipboard = moduser32.NewProc("CloseClipboard")
procEmptyClipboard = moduser32.NewProc("EmptyClipboard")
procGetClipboardData = moduser32.NewProc("GetClipboardData")
procSetClipboardData = moduser32.NewProc("SetClipboardData")
)
var (
moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
@ -33,6 +39,14 @@ var (
modwingdi = syscall.NewLazyDLL("gdi32.dll")
procCreateSolidBrush = modwingdi.NewProc("CreateSolidBrush")
)
var (
kernel32 = syscall.NewLazyDLL("kernel32")
kernelGlobalAlloc = kernel32.NewProc("GlobalAlloc")
kernelGlobalFree = kernel32.NewProc("GlobalFree")
kernelGlobalLock = kernel32.NewProc("GlobalLock")
kernelGlobalUnlock = kernel32.NewProc("GlobalUnlock")
kernelLstrcpy = kernel32.NewProc("lstrcpyW")
)
var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo()

View file

@ -1,6 +1,8 @@
package dispatcher
import (
"encoding/json"
"errors"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"strings"
@ -44,6 +46,21 @@ func (d *Dispatcher) processSystemCall(payload callMessage, sender frontend.Fron
return sender.WindowIsFullscreen(), nil
case "Environment":
return runtime.Environment(d.ctx), nil
case "ClipboardGetText":
t, err := sender.ClipboardGetText()
return t, err
case "ClipboardSetText":
if len(payload.Args) < 1 {
return false, errors.New("empty argument, cannot set clipboard")
}
var arg string
if err := json.Unmarshal(payload.Args[0], &arg); err != nil {
return false, err
}
if err := sender.ClipboardSetText(arg); err != nil {
return false, err
}
return true, nil
default:
return nil, fmt.Errorf("unknown systemcall message: %s", payload.Name)
}

View file

@ -121,4 +121,8 @@ type Frontend interface {
// Browser
BrowserOpenURL(url string)
// Clipboard
ClipboardGetText() (string, error)
ClipboardSetText(text string) error
}

View file

@ -0,0 +1,34 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {Call} from "./calls";
/**
* Set the Size of the window
*
* @export
* @param {string} text
*/
export function ClipboardSetText(text) {
return Call(":wails:ClipboardSetText", [text]);
}
/**
* Get the text content of the clipboard
*
* @export
* @return {Promise<{string}>} Text content of the clipboard
*/
export function ClipboardGetText() {
return Call(":wails:ClipboardGetText");
}

View file

@ -15,6 +15,7 @@ import {SetBindings} from "./bindings";
import * as Window from "./window";
import * as Screen from "./screen";
import * as Browser from "./browser";
import * as Clipboard from "./clipboard";
export function Quit() {
@ -39,6 +40,7 @@ window.runtime = {
...Window,
...Browser,
...Screen,
...Clipboard,
EventsOn,
EventsOnce,
EventsOnMultiple,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -225,3 +225,11 @@ export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;

View file

@ -192,3 +192,11 @@ export function Hide() {
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}

View file

@ -0,0 +1,13 @@
package runtime
import "context"
func ClipboardGetText(ctx context.Context) (string, error) {
appFrontend := getFrontend(ctx)
return appFrontend.ClipboardGetText()
}
func ClipboardSetText(ctx context.Context, text string) error {
appFrontend := getFrontend(ctx)
return appFrontend.ClipboardSetText(text)
}

View file

@ -0,0 +1,28 @@
---
sidebar_position: 8
---
# Clipboard
This part of the runtime provides access to the operating system's clipboard.<br/>
The current implementation only handles text.
### ClipboardGetText
This method reads the currently stored text from the clipboard.
Go: `ClipboardGetText(ctx context.Context) (string, error)`<br/>
Returns: a string (if the clipboard is empty an empty string will be returned) or an error.
JS: `ClipboardGetText(): Promise<string>`<br/>
Returns: a promise with a string result (if the clipboard is empty an empty string will be returned).
### ClipboardSetText
This method writes a text to the clipboard.
Go: `ClipboardSetText(ctx context.Context, text string) error`<br/>
Returns: an error if there is any.
JS: `ClipboardSetText(text: string): Promise<boolean>`<br/>
Returns: a promise with true result if the text was successfully set on the clipboard, false otherwise.

View file

@ -15,6 +15,7 @@ It has utility methods for:
- [Events](events.mdx)
- [Browser](browser.mdx)
- [Log](log.mdx)
- [Clipboard](clipboard.mdx)
The Go Runtime is available through importing `github.com/wailsapp/wails/v2/pkg/runtime`. All methods in this package
take a context as the first parameter. This context should be obtained from the [OnStartup](../options.mdx#onstartup)