mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Add common text clipboard support (#2228)
This commit is contained in:
parent
fc9dd99dea
commit
0a32c1124e
18 changed files with 403 additions and 21 deletions
36
v2/internal/frontend/desktop/darwin/clipboard.go
Normal file
36
v2/internal/frontend/desktop/darwin/clipboard.go
Normal 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()
|
||||
}
|
||||
49
v2/internal/frontend/desktop/linux/clipboard.go
Normal file
49
v2/internal/frontend/desktop/linux/clipboard.go
Normal 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
|
||||
}
|
||||
|
|
@ -71,7 +71,6 @@ static void install_signal_handlers()
|
|||
#endif
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
|
|||
16
v2/internal/frontend/desktop/windows/clipboard.go
Normal file
16
v2/internal/frontend/desktop/windows/clipboard.go
Normal 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)
|
||||
}
|
||||
143
v2/internal/frontend/desktop/windows/win32/clipboard.go
Normal file
143
v2/internal/frontend/desktop/windows/win32/clipboard.go
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,4 +121,8 @@ type Frontend interface {
|
|||
|
||||
// Browser
|
||||
BrowserOpenURL(url string)
|
||||
|
||||
// Clipboard
|
||||
ClipboardGetText() (string, error)
|
||||
ClipboardSetText(text string) error
|
||||
}
|
||||
|
|
|
|||
34
v2/internal/frontend/runtime/desktop/clipboard.js
Normal file
34
v2/internal/frontend/runtime/desktop/clipboard.js
Normal 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");
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
13
v2/pkg/runtime/clipboard.go
Normal file
13
v2/pkg/runtime/clipboard.go
Normal 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)
|
||||
}
|
||||
28
website/docs/reference/runtime/clipboard.mdx
Normal file
28
website/docs/reference/runtime/clipboard.mdx
Normal 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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue