From 200fcd41cee2286f75fb6321bd7bdead981663f2 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sun, 17 Nov 2019 07:40:03 +0500 Subject: [PATCH] vlib: add a clipboard module (Windows, macOS, X) --- vlib/clipboard/clipboard.v | 45 ++++ vlib/clipboard/clipboard_darwin.v | 66 +++++ vlib/clipboard/clipboard_linux.v | 418 +++++++++++++++++++++++++++++ vlib/clipboard/clipboard_test.v | 23 ++ vlib/clipboard/clipboard_windows.v | 147 ++++++++++ vlib/compiler/parser.v | 2 +- 6 files changed, 700 insertions(+), 1 deletion(-) create mode 100644 vlib/clipboard/clipboard.v create mode 100644 vlib/clipboard/clipboard_darwin.v create mode 100644 vlib/clipboard/clipboard_linux.v create mode 100644 vlib/clipboard/clipboard_test.v create mode 100644 vlib/clipboard/clipboard_windows.v diff --git a/vlib/clipboard/clipboard.v b/vlib/clipboard/clipboard.v new file mode 100644 index 0000000000..11262cd10b --- /dev/null +++ b/vlib/clipboard/clipboard.v @@ -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.") + } +} \ No newline at end of file diff --git a/vlib/clipboard/clipboard_darwin.v b/vlib/clipboard/clipboard_darwin.v new file mode 100644 index 0000000000..a4fe6bc20d --- /dev/null +++ b/vlib/clipboard/clipboard_darwin.v @@ -0,0 +1,66 @@ +module clipboard + +#include +#include + +#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) +} \ No newline at end of file diff --git a/vlib/clipboard/clipboard_linux.v b/vlib/clipboard/clipboard_linux.v new file mode 100644 index 0000000000..0f6e2bef57 --- /dev/null +++ b/vlib/clipboard/clipboard_linux.v @@ -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 +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) +} + diff --git a/vlib/clipboard/clipboard_test.v b/vlib/clipboard/clipboard_test.v new file mode 100644 index 0000000000..3ccf79545b --- /dev/null +++ b/vlib/clipboard/clipboard_test.v @@ -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) +} \ No newline at end of file diff --git a/vlib/clipboard/clipboard_windows.v b/vlib/clipboard/clipboard_windows.v new file mode 100644 index 0000000000..cc5cb8cfca --- /dev/null +++ b/vlib/clipboard/clipboard_windows.v @@ -0,0 +1,147 @@ +module clipboard + +import time + +#include + +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 +} \ No newline at end of file diff --git a/vlib/compiler/parser.v b/vlib/compiler/parser.v index bd8563f8c3..1bb54bfae8 100644 --- a/vlib/compiler/parser.v +++ b/vlib/compiler/parser.v @@ -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) {