1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

vlib: add a clipboard module (Windows, macOS, X)

This commit is contained in:
Abdullah Atta 2019-11-17 07:40:03 +05:00 committed by Alexander Medvednikov
parent 3c03051bcf
commit 200fcd41ce
6 changed files with 700 additions and 1 deletions

View File

@ -0,0 +1,45 @@
module clipboard
// create a new clipboard
pub fn new() &Clipboard {
return new_clipboard()
}
// copy some text into the clipboard
pub fn (cb mut Clipboard) copy(text string) bool {
return cb.set_text(text)
}
// get the text from the clipboard
pub fn (cb mut Clipboard) paste() string {
return cb.get_text()
}
// clear the clipboard
pub fn (cb mut Clipboard) clear_all() {
cb.clear()
}
// destroy the clipboard
pub fn (cb mut Clipboard) destroy() {
cb.free()
}
// check if we own the clipboard
pub fn (cb mut Clipboard) check_ownership() bool {
return cb.has_ownership()
}
// check if clipboard can be used
pub fn (cb &Clipboard) is_available() bool {
return cb.check_availability()
}
// create a new PRIMARY clipboard (only supported on Linux)
pub fn new_primary() &Clipboard {
$if linux {
return new_x11_clipboard(.primary)
} $else {
panic("Primary clipboard is not supported on non-Linux systems.")
}
}

View File

@ -0,0 +1,66 @@
module clipboard
#include <libkern/OSAtomic.h>
#include <Cocoa/Cocoa.h>
#flag -framework Cocoa
struct Clipboard {
pb voidptr
last_cb_serial i64
}
fn new_clipboard() &Clipboard{
mut pb := voidptr(0)
#pb = [NSPasteboard generalPasteboard];
cb := &Clipboard{
pb: pb
}
return cb
}
fn (cb &Clipboard) check_availability() bool {
return cb.pb != C.NULL
}
fn (cb &Clipboard) clear(){
#[cb->pb clearContents];
}
fn (cb &Clipboard) free(){
//nothing to free
}
fn (cb &Clipboard) has_ownership() bool {
if cb.last_cb_serial == 0 {return false}
#return [cb->pb changeCount] == cb->last_cb_serial;
return false
}
fn (cb &Clipboard) set_text(text string) bool {
#NSString *ns_clip;
mut ret := false
#ns_clip = [[ NSString alloc ] initWithBytesNoCopy:text.str length:text.len encoding:NSUTF8StringEncoding freeWhenDone: false];
#[cb->pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
#ret = [cb->pb setString:ns_clip forType:NSStringPboardType];
#[ns_clip release];
mut serial := 0
#serial = [cb->pb changeCount];
C.OSAtomicCompareAndSwapLong(cb.last_cb_serial, serial, &cb.last_cb_serial)
return ret
}
fn (cb &Clipboard) get_text() string {
#NSString *ns_clip;
mut utf8_clip := byteptr(0)
#ns_clip = [cb->pb stringForType:NSStringPboardType]; //NSPasteboardTypeString
#if (ns_clip == nil) {
# return tos3(""); //in case clipboard is empty
#}
#utf8_clip = [ns_clip UTF8String];
return string(utf8_clip)
}

View File

@ -0,0 +1,418 @@
// Currently there is only X11 Selections support and no way to handle Wayland
// but since Wayland isn't extremely adopted, we are covering almost all Linux distros.
module clipboard
import (
time
sync
math
)
#flag -lX11
#include <X11/Xlib.h>
// X11
struct C.Display
struct C.Atom
struct C.Window
fn C.XInitThreads() int
fn C.XCloseDisplay(d &Display)
fn C.XFlush(d &Display)
fn C.XDestroyWindow(d &Display, w Window)
fn C.XNextEvent(d Display, e &XEvent)
fn C.XSetSelectionOwner(d &Display, a Atom, w Window, time int)
fn C.XGetSelectionOwner(d &Display, a Atom) Window
fn C.XChangeProperty(d &Display, requestor Window, property Atom, typ Atom, format int, mode int, data voidptr, nelements int) int
fn C.XSendEvent(d &Display, requestor Window, propogate int, mask i64, event &XEvent)
fn C.XInternAtom(d &Display, typ byteptr, only_if_exists int) Atom
fn C.XCreateSimpleWindow(d &Display, root Window, x int, y int, width u32, height u32, border_width u32, border u64, background u64) Window
fn C.XOpenDisplay(name byteptr) &Display
fn C.XConvertSelection(d &Display, selection Atom, target Atom, property Atom, requestor Window, time int) int
fn C.XSync(d &Display, discard int) int
fn C.XGetWindowProperty(d &Display, w Window, property Atom, offset i64, length i64, delete int, req_type Atom, actual_type_return &Atom, actual_format_return &int, nitems &i64, bytes_after_return &i64, prop_return &byteptr) int
fn C.XDeleteProperty(d &Display, w Window, property Atom) int
struct C.XSelectionRequestEvent{
mut:
selection Atom
display &Display /* Display the event was read from */
owner Window
requestor Window
target Atom
property Atom
time int
}
struct C.XSelectionEvent{
mut:
@type int
selection Atom
display &Display /* Display the event was read from */
requestor Window
target Atom
property Atom
time int
}
struct C.XSelectionClearEvent{
mut:
window Window
selection Atom
}
struct C.XDestroyWindowEvent {
mut:
window Window
}
struct C.XEvent{
mut:
@type int
xselectionrequest XSelectionRequestEvent
xselection XSelectionEvent
xselectionclear XSelectionClearEvent
xdestroywindow XDestroyWindowEvent
}
const (
atom_names = ["TARGETS", "CLIPBOARD", "PRIMARY", "SECONDARY", "TEXT", "UTF8_STRING", "text/plain", "text/html"]
)
//UNSUPPORTED TYPES: MULTIPLE, INCR, TIMESTAMP, image/bmp, image/jpeg, image/tiff, image/png
// all the atom types we need
// currently we only support text
// in the future, maybe we can extend this
// to support other mime types
enum atom_type {
xa_atom = 0, //value 4
xa_string = 1, //value 31
targets = 2,
clipboard = 3,
primary = 4,
secondary = 5,
text = 6,
utf8_string = 7
text_plain = 8,
text_html = 9
}
struct Clipboard {
display &Display
mut:
selection Atom //the selection atom
window Window
atoms []Atom
mutex sync.Mutex
text string // text data sent or received
got_text bool // used to confirm that we have got the text
is_owner bool // to save selection owner state
}
struct Property{
actual_type Atom
actual_format int
nitems int
data byteptr
}
fn new_clipboard() &Clipboard {
return new_x11_clipboard(.clipboard)
}
// Initialize a new clipboard of the given selection type.
// We can initialize multiple clipboard instances and use them separately
fn new_x11_clipboard(selection atom_type) &Clipboard {
if !(selection in [.clipboard, .primary, .secondary]) {
panic("Wrong atom_type. Must be one of .primary, .secondary or .clipboard.")
}
//init x11 thread support
status := XInitThreads()
if status == 0 {
println("WARN: this system does not support threads; clipboard will cause the program to lock.")
}
display := new_display()
if display == C.NULL {
println("ERROR: No X Server running. Clipboard cannot be used.")
return &Clipboard{}
}
mut cb := &Clipboard{
display: display
window: create_xwindow(display)
mutex: sync.new_mutex()
}
cb.intern_atoms()
cb.selection = cb.get_atom(selection)
// start the listener on another thread or
// we will be locked and will have to hard exit
go cb.start_listener()
return cb
}
fn (cb &Clipboard) check_availability() bool {
return cb.display != C.NULL
}
fn (cb mut Clipboard) free() {
XDestroyWindow(cb.display, cb.window)
cb.window = Window(C.None)
//FIX ME: program hangs when closing display
//XCloseDisplay(cb.display)
}
fn (cb mut Clipboard) clear(){
cb.mutex.lock()
XSetSelectionOwner(cb.display, cb.selection, Window(C.None), C.CurrentTime)
XFlush(cb.display)
cb.is_owner = false
cb.text = ""
cb.mutex.unlock()
}
fn (cb &Clipboard) has_ownership() bool {
return cb.is_owner
}
fn (cb &Clipboard) take_ownership(){
XSetSelectionOwner(cb.display, cb.selection, cb.window, C.CurrentTime)
XFlush(cb.display)
}
fn (cb mut Clipboard) set_text(text string) bool {
if cb.window == Window(C.None) {return false}
mut ret := false
cb.mutex.lock()
cb.text = text
cb.is_owner = true
cb.take_ownership()
XFlush(cb.display)
cb.mutex.unlock()
// sleep a little bit
time.sleep(1)
return cb.is_owner
}
fn (cb mut Clipboard) get_text() string {
if cb.window == Window(C.None) {return ""}
if cb.is_owner {
return cb.text
}
cb.got_text = false
//Request a list of possible conversions, if we're pasting.
XConvertSelection(cb.display, cb.selection, cb.get_atom(.targets), cb.selection, cb.window, C.CurrentTime)
//wait for the text to arrive
mut retries := 5
for {
if cb.got_text || retries == 0 {break}
time.usleep(50000)
retries--
}
return cb.text
}
// this function is crucial to handling all the different data types
// if we ever support other mimetypes they should be handled here
fn (cb mut Clipboard) transmit_selection(xse &XSelectionEvent) bool {
if xse.target == cb.get_atom(.targets) {
targets := cb.get_supported_targets()
XChangeProperty(xse.display, xse.requestor, xse.property, cb.get_atom(.xa_atom), 32, C.PropModeReplace, targets.data, targets.len)
} else if cb.is_supported_target(xse.target) && cb.is_owner && cb.text != "" {
cb.mutex.lock()
XChangeProperty(xse.display, xse.requestor, xse.property, xse.target, 8, C.PropModeReplace, cb.text.str, cb.text.len)
cb.mutex.unlock()
} else {
return false
}
return true
}
fn (cb mut Clipboard) start_listener(){
event := XEvent{}
mut sent_request := false
mut to_be_requested := Atom(0)
for {
XNextEvent(cb.display, &event)
if (event.@type == 0) {
println("error")
continue
}
match event.@type {
C.DestroyNotify {
if event.xdestroywindow.window == cb.window {
return // we are done
}
}
C.SelectionClear {
if event.xselectionclear.window == cb.window && event.xselectionclear.selection == cb.selection {
cb.mutex.lock()
cb.is_owner = false
cb.text = ""
cb.mutex.unlock()
}
}
C.SelectionRequest {
if event.xselectionrequest.selection == cb.selection {
mut xsre := &XSelectionRequestEvent{}
xsre = &event.xselectionrequest
mut xse := XSelectionEvent{
@type: C.SelectionNotify // 31
display: xsre.display
requestor: xsre.requestor
selection: xsre.selection
time: xsre.time
target: xsre.target
property: xsre.property
}
if !cb.transmit_selection(&xse) {
xse.property = new_atom(C.None)
}
XSendEvent(cb.display, xse.requestor, 0, C.PropertyChangeMask, &xse)
XFlush(cb.display)
}
}
C.SelectionNotify {
if event.xselection.selection == cb.selection && event.xselection.property != Atom(C.None) {
if event.xselection.target == cb.get_atom(.targets) && !sent_request {
sent_request = true
prop := read_property(cb.display, cb.window, cb.selection)
to_be_requested = cb.pick_target(prop)
if to_be_requested != Atom(0) {
XConvertSelection(cb.display, cb.selection, to_be_requested, cb.selection, cb.window, C.CurrentTime)
}
} else if event.xselection.target == to_be_requested {
sent_request = false
to_be_requested = Atom(0)
cb.mutex.lock()
prop := read_property(event.xselection.display, event.xselection.requestor, event.xselection.property)
XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property)
if cb.is_supported_target(prop.actual_type) {
cb.got_text = true
cb.text = string(prop.data) //TODO: return byteptr to support other mimetypes
}
cb.mutex.unlock()
}
}
}
C.PropertyNotify {}
}
}
}
// Helpers
// Initialize all the atoms we need
fn (cb mut Clipboard) intern_atoms(){
cb.atoms << Atom(4) //XA_ATOM
cb.atoms << Atom(31) //XA_STRING
for i, name in atom_names{
only_if_exists := if i == int(atom_type.utf8_string) {1} else {0}
cb.atoms << XInternAtom(cb.display, name.str, only_if_exists)
if i == int(atom_type.utf8_string) && cb.atoms[i] == Atom(C.None) {
cb.atoms[i] = cb.get_atom(.xa_string)
}
}
}
fn read_property(d &Display, w Window, p Atom) Property {
actual_type := Atom(0)
actual_format := 0
nitems := 0
bytes_after := 0
ret := byteptr(0)
mut read_bytes := 1024
for {
if(ret != 0){
C.XFree(ret)
}
XGetWindowProperty(d, w, p, 0, read_bytes, 0, C.AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &ret)
read_bytes *= 2
if bytes_after == 0 {break}
}
return Property{actual_type, actual_format, nitems, ret}
}
// Finds the best target given a local copy of a property.
fn (cb &Clipboard) pick_target(prop Property) Atom {
//The list of targets is a list of atoms, so it should have type XA_ATOM
//but it may have the type TARGETS instead.
if((prop.actual_type != cb.get_atom(.xa_atom) && prop.actual_type != cb.get_atom(.targets)) || prop.actual_format != 32)
{
//This would be really broken. Targets have to be an atom list
//and applications should support this. Nevertheless, some
//seem broken (MATLAB 7, for instance), so ask for STRING
//next instead as the lowest common denominator
return cb.get_atom(.xa_string)
}
else
{
atom_list := &Atom(prop.data)
mut to_be_requested := Atom(0)
//This is higher than the maximum priority.
mut priority := math.max_i32
supported_targets := cb.get_supported_targets()
for i := 0; i < prop.nitems; i++ {
//See if this data type is allowed and of higher priority (closer to zero)
//than the present one.
if cb.is_supported_target(atom_list[i]) {
index := cb.get_target_index(atom_list[i])
if(priority > index && index >= 0)
{
priority = index
to_be_requested = atom_list[i]
}
}
}
return to_be_requested
}
}
fn (cb &Clipboard) get_atoms(types ...atom_type) []Atom {
mut atoms := []Atom
for typ in types {
atoms << cb.atoms[typ]
}
return atoms
}
fn (cb &Clipboard) get_atom(typ atom_type) Atom {
return cb.atoms[typ]
}
fn (cb &Clipboard) is_supported_target(target Atom) bool {
return cb.get_target_index(target) >= 0
}
fn (cb &Clipboard) get_target_index(target Atom) int {
for i, atom in cb.get_supported_targets() {
if atom == target {return i}
}
return -1
}
fn (cb &Clipboard) get_supported_targets() []Atom {
return cb.get_atoms(atom_type.utf8_string, .xa_string, .text, .text_plain, .text_html)
}
fn new_atom(value int) &Atom {
mut atom := &Atom{}
atom = value
return atom
}
fn create_xwindow(display &Display) Window {
N := int(C.DefaultScreen(display))
return XCreateSimpleWindow(display, C.RootWindow(display, N), 0, 0, 1, 1, 0, C.BlackPixel(display, N), C.WhitePixel(display, N))
}
fn new_display() &Display {
return XOpenDisplay(C.NULL)
}

View File

@ -0,0 +1,23 @@
import clipboard
fn run_test(is_primary bool){
mut cb := if is_primary {clipboard.new_primary()}else{clipboard.new()}
if !cb.is_available() {return}
assert cb.check_ownership() == false
assert cb.copy("I am a good boy!") == true
assert cb.check_ownership() == true
assert cb.paste() == "I am a good boy!"
cb.clear_all()
assert cb.paste().len <= 0
cb.destroy()
}
fn test_primary(){
$if linux {
run_test(true)
}
}
fn test_clipboard(){
run_test(false)
}

View File

@ -0,0 +1,147 @@
module clipboard
import time
#include <windows.h>
struct C.HWND
struct C.WPARAM
struct C.LPARAM
struct C.LRESULT
struct C.HGLOBAL
struct C.WNDCLASSEX {
cbSize int
lpfnWndProc voidptr
lpszClassName &u16
}
fn C.RegisterClassEx(class WNDCLASSEX) int
fn C.GetClipboardOwner() &HWND
fn C.CreateWindowEx(dwExStyle i64, lpClassName &u16, lpWindowName &u16, dwStyle i64, x int, y int, nWidth int, nHeight int, hWndParent i64, hMenu voidptr, hInstance voidptr, lpParam voidptr) &HWND
fn C.MultiByteToWideChar(CodePage u32, dwFlags u16, lpMultiByteStr byteptr, cbMultiByte int, lpWideCharStr u16, cchWideChar int) int
fn C.EmptyClipboard()
fn C.CloseClipboard()
fn C.GlobalAlloc(uFlag u32, size i64) HGLOBAL
fn C.GlobalFree(buf HGLOBAL)
fn C.GlobalLock(buf HGLOBAL)
fn C.GlobalUnlock(buf HGLOBAL)
fn C.SetClipboardData(uFormat u32, data voidptr) C.HANDLE
fn C.GetClipboardData(uFormat u32) C.HANDLE
fn C.DefWindowProc(hwnd HWND, msg u32, wParam WPARAM, lParam LPARAM) LRESULT
fn C.SetLastError(error i64)
fn C.OpenClipboard(hwnd HWND) int
fn C.DestroyWindow(hwnd HWND)
struct Clipboard {
max_retries int
retry_delay int
mut:
hwnd HWND
}
fn (cb &Clipboard) get_clipboard_lock() bool {
mut retries := cb.max_retries
mut last_error := u32(0)
for {
retries--
if retries < 0 {
break
}
last_error = GetLastError()
if OpenClipboard(cb.hwnd) > 0 {
return true
} else if last_error != u32(C.ERROR_ACCESS_DENIED) {
return false
}
time.sleep(cb.retry_delay)
}
SetLastError(last_error)
return false
}
fn new_clipboard() &Clipboard {
mut cb := &Clipboard {
max_retries: 5
retry_delay: 5
}
wndclass := WNDCLASSEX{
cbSize: sizeof(WNDCLASSEX)
lpfnWndProc: voidptr(&DefWindowProc)
lpszClassName: "clipboard".to_wide()
}
if RegisterClassEx(&wndclass) <= 0 && GetLastError() != u32(C.ERROR_CLASS_ALREADY_EXISTS) {
println("Failed registering class.")
}
hwnd := CreateWindowEx(0, wndclass.lpszClassName, wndclass.lpszClassName, 0, 0, 0, 0, 0, C.HWND_MESSAGE, C.NULL, C.NULL, C.NULL)
if hwnd == C.NULL {
println("Error creating window!")
}
cb.hwnd = hwnd
return cb
}
fn (cb &Clipboard) check_availability() bool {
return cb.hwnd != HWND(C.NULL)
}
fn (cb &Clipboard) has_ownership() bool {
return GetClipboardOwner() == cb.hwnd
}
fn (cb &Clipboard) clear() {
if !cb.get_clipboard_lock() {return}
EmptyClipboard()
CloseClipboard()
}
fn (cb &Clipboard) free(){
DestroyWindow(cb.hwnd)
}
// the string.to_wide doesn't work with SetClipboardData, don't know why
fn to_wide(text string) &HGLOBAL {
len_required := MultiByteToWideChar(C.CP_UTF8, C.MB_ERR_INVALID_CHARS, text.str, text.len + 1, C.NULL, 0)
buf := GlobalAlloc(C.GMEM_MOVEABLE, sizeof(u16) * len_required)
if buf != HGLOBAL(C.NULL) {
mut locked := &u16(GlobalLock(buf))
MultiByteToWideChar(C.CP_UTF8, C.MB_ERR_INVALID_CHARS, text.str, text.len + 1, locked, len_required)
locked[len_required - 1] = u16(0)
GlobalUnlock(buf)
}
return buf
}
fn (cb &Clipboard) set_text(text string) bool {
buf := to_wide(text)
if !cb.get_clipboard_lock() {
GlobalFree(buf)
return false
} else {
/* EmptyClipboard must be called to properly update clipboard ownership */
EmptyClipboard()
if SetClipboardData(C.CF_UNICODETEXT, buf) == HANDLE(C.NULL) {
println("SetClipboardData: Failed.")
CloseClipboard()
GlobalFree(buf)
return false
}
}
/* CloseClipboard appears to change the sequence number... */
CloseClipboard()
return true
}
fn (cb &Clipboard) get_text() string {
if !cb.get_clipboard_lock() {
return ""
}
h_data := GetClipboardData(C.CF_UNICODETEXT)
if h_data == HANDLE(C.NULL) {
CloseClipboard()
return ""
}
str := string_from_wide(&u16(GlobalLock(h_data)))
GlobalUnlock(h_data)
return str
}

View File

@ -297,7 +297,7 @@ fn (p mut Parser) parse(pass Pass) {
}
p.fgenln('\n')
p.builtin_mod = p.mod == 'builtin'
p.can_chash = p.mod=='ui' || p.mod == 'darwin'// TODO tmp remove
p.can_chash = p.mod in ['ui','darwin','clipboard']// TODO tmp remove
// Import pass - the first and the smallest pass that only analyzes imports
// if we are a building module get the full module name from v.mod
fq_mod := if p.pref.build_mode == .build_module && p.v.mod.ends_with(p.mod) {