From 906828f3ec1665e08664d7a192dd3f7780454093 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Thu, 9 Apr 2026 20:44:52 +0200 Subject: [PATCH] wip: add clipboard support --- kernel/src/Api/Clipboard.hpp | 220 +++++++++++++++++++ kernel/src/Api/Syscall.cpp | 17 ++ kernel/src/Api/Syscall.hpp | 12 + programs/include/Api/Syscall.hpp | 12 + programs/include/gui/clipboard.hpp | 65 ++++++ programs/include/montauk/syscall.h | 16 ++ programs/src/desktop/main.cpp | 2 + programs/src/texteditor/main.cpp | 77 +++++++ programs/src/wordprocessor/document.cpp | 93 ++++++++ programs/src/wordprocessor/input.cpp | 65 ++++++ programs/src/wordprocessor/wordprocessor.hpp | 3 + 11 files changed, 582 insertions(+) create mode 100644 kernel/src/Api/Clipboard.hpp create mode 100644 programs/include/gui/clipboard.hpp diff --git a/kernel/src/Api/Clipboard.hpp b/kernel/src/Api/Clipboard.hpp new file mode 100644 index 0000000..3d51746 --- /dev/null +++ b/kernel/src/Api/Clipboard.hpp @@ -0,0 +1,220 @@ +/* + * Clipboard.hpp + * SYS_CLIPBOARD_* plain-text clipboard syscalls + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include +#include +#include + +#include "Syscall.hpp" + +namespace Montauk { + + static constexpr int ClipboardMaxSlots = 32; + + struct ClipboardSlot { + bool inUse; + char user[32]; + char* text; + uint32_t len; + uint64_t serial; + }; + + static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots]; + static kcp::Mutex g_clipboardLock; + + static int Sys_ClipboardClear(); + + static const char* ClipboardCurrentUser() { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc == nullptr) return nullptr; + return proc->user[0] ? proc->user : "default"; + } + + static void ClipboardCopyUser(char dst[32], const char* src) { + if (src == nullptr || src[0] == '\0') src = "default"; + int i = 0; + for (; i < 31 && src[i]; i++) + dst[i] = src[i]; + dst[i] = '\0'; + } + + static bool ClipboardUserEquals(const char* a, const char* b) { + int i = 0; + while (a[i] && b[i]) { + if (a[i] != b[i]) return false; + i++; + } + return a[i] == b[i]; + } + + static uint64_t ClipboardNextSerial(uint64_t serial) { + serial++; + return serial == 0 ? 1 : serial; + } + + static ClipboardSlot* ClipboardFindSlotLocked(const char* user) { + for (int i = 0; i < ClipboardMaxSlots; i++) { + if (g_clipboardSlots[i].inUse && ClipboardUserEquals(g_clipboardSlots[i].user, user)) + return &g_clipboardSlots[i]; + } + return nullptr; + } + + static ClipboardSlot* ClipboardEnsureSlotLocked(const char* user) { + ClipboardSlot* freeSlot = nullptr; + for (int i = 0; i < ClipboardMaxSlots; i++) { + ClipboardSlot* slot = &g_clipboardSlots[i]; + if (slot->inUse) { + if (ClipboardUserEquals(slot->user, user)) + return slot; + } else if (freeSlot == nullptr) { + freeSlot = slot; + } + } + + if (freeSlot == nullptr) return nullptr; + + freeSlot->inUse = true; + ClipboardCopyUser(freeSlot->user, user); + freeSlot->text = nullptr; + freeSlot->len = 0; + freeSlot->serial = 0; + return freeSlot; + } + + static int Sys_ClipboardSetText(const char* data, uint32_t len) { + if (len == 0) + return Sys_ClipboardClear(); + if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES || Memory::g_heap == nullptr) + return -1; + + for (uint32_t i = 0; i < len; i++) { + if (data[i] == '\0') + return -1; + } + + const char* currentUser = ClipboardCurrentUser(); + if (currentUser == nullptr) return -1; + + char user[32]; + ClipboardCopyUser(user, currentUser); + + char* newText = (char*)Memory::g_heap->Request(len + 1); + if (newText == nullptr) return -1; + for (uint32_t i = 0; i < len; i++) + newText[i] = data[i]; + newText[len] = '\0'; + + char* oldText = nullptr; + + g_clipboardLock.Acquire(); + ClipboardSlot* slot = ClipboardEnsureSlotLocked(user); + if (slot == nullptr) { + g_clipboardLock.Release(); + Memory::g_heap->Free(newText); + return -1; + } + + oldText = slot->text; + slot->text = newText; + slot->len = len; + slot->serial = ClipboardNextSerial(slot->serial); + g_clipboardLock.Release(); + + if (oldText != nullptr) + Memory::g_heap->Free(oldText); + + return 0; + } + + static int Sys_ClipboardGetInfo(ClipboardInfo* out) { + if (out == nullptr) return -1; + + out->textBytes = 0; + out->_pad = 0; + out->serial = 0; + + const char* currentUser = ClipboardCurrentUser(); + if (currentUser == nullptr) return -1; + + char user[32]; + ClipboardCopyUser(user, currentUser); + + g_clipboardLock.Acquire(); + ClipboardSlot* slot = ClipboardFindSlotLocked(user); + if (slot != nullptr) { + out->textBytes = slot->len; + out->serial = slot->serial; + } + g_clipboardLock.Release(); + return 0; + } + + static int Sys_ClipboardGetText(char* buffer, uint32_t bufferLen, uint32_t* outLen, uint64_t* outSerial) { + if (outLen == nullptr) return -1; + + *outLen = 0; + if (outSerial != nullptr) *outSerial = 0; + + const char* currentUser = ClipboardCurrentUser(); + if (currentUser == nullptr) return -1; + + char user[32]; + ClipboardCopyUser(user, currentUser); + + g_clipboardLock.Acquire(); + ClipboardSlot* slot = ClipboardFindSlotLocked(user); + if (slot == nullptr || slot->len == 0) { + if (slot != nullptr && outSerial != nullptr) + *outSerial = slot->serial; + g_clipboardLock.Release(); + return 0; + } + + *outLen = slot->len; + if (outSerial != nullptr) *outSerial = slot->serial; + if (buffer == nullptr || bufferLen < slot->len) { + g_clipboardLock.Release(); + return -1; + } + + for (uint32_t i = 0; i < slot->len; i++) + buffer[i] = slot->text[i]; + g_clipboardLock.Release(); + return 0; + } + + static int Sys_ClipboardClear() { + const char* currentUser = ClipboardCurrentUser(); + if (currentUser == nullptr) return -1; + + char user[32]; + ClipboardCopyUser(user, currentUser); + + char* oldText = nullptr; + + g_clipboardLock.Acquire(); + ClipboardSlot* slot = ClipboardEnsureSlotLocked(user); + if (slot == nullptr) { + g_clipboardLock.Release(); + return -1; + } + + oldText = slot->text; + slot->text = nullptr; + slot->len = 0; + slot->serial = ClipboardNextSerial(slot->serial); + g_clipboardLock.Release(); + + if (oldText != nullptr) + Memory::g_heap->Free(oldText); + + return 0; + } + +} diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 0eb6b74..958e00b 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -35,6 +35,7 @@ #include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE #include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM #include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT +#include "Clipboard.hpp" // SYS_CLIPBOARD_* // Assembly entry point extern "C" void SyscallEntry(); @@ -422,6 +423,22 @@ namespace Montauk { return (int64_t)Sys_GetLibBase(frame->arg1); case SYS_CRASH_REPORT: return Sys_CrashReport((CrashReportInfo*)frame->arg1); + case SYS_CLIPBOARD_SET_TEXT: + if (frame->arg2 > 0 && !ValidUserPtr(frame->arg1)) return -1; + return Sys_ClipboardSetText((const char*)frame->arg1, (uint32_t)frame->arg2); + case SYS_CLIPBOARD_GET_INFO: + if (!ValidUserPtr(frame->arg1)) return -1; + return Sys_ClipboardGetInfo((ClipboardInfo*)frame->arg1); + case SYS_CLIPBOARD_GET_TEXT: + if (frame->arg2 > 0 && !ValidUserPtr(frame->arg1)) return -1; + if (!ValidUserPtr(frame->arg3)) return -1; + if (frame->arg4 != 0 && !IsUserPtr(frame->arg4)) return -1; + return Sys_ClipboardGetText(IsUserPtr(frame->arg1) ? (char*)frame->arg1 : nullptr, + (uint32_t)frame->arg2, + (uint32_t*)frame->arg3, + IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr); + case SYS_CLIPBOARD_CLEAR: + return Sys_ClipboardClear(); default: return -1; } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index a45edea..c00e776 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -214,6 +214,12 @@ namespace Montauk { /* CrashReport.hpp */ static constexpr uint64_t SYS_CRASH_REPORT = 118; + static constexpr uint64_t SYS_CLIPBOARD_SET_TEXT = 119; + static constexpr uint64_t SYS_CLIPBOARD_GET_INFO = 120; + static constexpr uint64_t SYS_CLIPBOARD_GET_TEXT = 121; + static constexpr uint64_t SYS_CLIPBOARD_CLEAR = 122; + + static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024; static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0; static constexpr uint32_t IPC_SIGNAL_WRITABLE = 1u << 1; @@ -278,6 +284,12 @@ namespace Montauk { uint32_t signals; }; + struct ClipboardInfo { + uint32_t textBytes; + uint32_t _pad; + uint64_t serial; + }; + // Window server shared types struct WinEvent { uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index b3af18c..6817ed3 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -152,6 +152,12 @@ namespace Montauk { /* CrashReport */ static constexpr uint64_t SYS_CRASH_REPORT = 118; + static constexpr uint64_t SYS_CLIPBOARD_SET_TEXT = 119; + static constexpr uint64_t SYS_CLIPBOARD_GET_INFO = 120; + static constexpr uint64_t SYS_CLIPBOARD_GET_TEXT = 121; + static constexpr uint64_t SYS_CLIPBOARD_CLEAR = 122; + + static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024; static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0; static constexpr uint32_t IPC_SIGNAL_WRITABLE = 1u << 1; @@ -225,6 +231,12 @@ namespace Montauk { uint32_t signals; }; + struct ClipboardInfo { + uint32_t textBytes; + uint32_t _pad; + uint64_t serial; + }; + // Window server shared types struct WinEvent { uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale diff --git a/programs/include/gui/clipboard.hpp b/programs/include/gui/clipboard.hpp new file mode 100644 index 0000000..c5ab6e2 --- /dev/null +++ b/programs/include/gui/clipboard.hpp @@ -0,0 +1,65 @@ +/* + * clipboard.hpp + * Simple plain-text clipboard helpers for MontaukOS GUI apps + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include +#include + +namespace gui { + + inline bool clipboard_set_text(const char* text, int len) { + if (len < 0) return false; + if (len > 0 && text == nullptr) return false; + return montauk::clipboard_set_text(text, (uint32_t)len) == 0; + } + + inline bool clipboard_set_text(const char* text) { + return clipboard_set_text(text, text ? montauk::slen(text) : 0); + } + + inline bool clipboard_get_info(Montauk::ClipboardInfo* out) { + if (out == nullptr) return false; + return montauk::clipboard_get_info(out) == 0; + } + + inline bool clipboard_has_text() { + Montauk::ClipboardInfo info = {}; + return clipboard_get_info(&info) && info.textBytes > 0; + } + + inline char* clipboard_get_text_alloc(int* out_len = nullptr, uint64_t* out_serial = nullptr) { + if (out_len) *out_len = 0; + if (out_serial) *out_serial = 0; + + Montauk::ClipboardInfo info = {}; + if (!clipboard_get_info(&info) || info.textBytes == 0) + return nullptr; + + uint32_t cap = info.textBytes + 1; + char* text = (char*)montauk::malloc(cap); + if (text == nullptr) return nullptr; + + uint32_t actual_len = 0; + uint64_t serial = 0; + if (montauk::clipboard_get_text(text, info.textBytes, &actual_len, &serial) != 0 || + actual_len > info.textBytes) { + montauk::mfree(text); + return nullptr; + } + + text[actual_len] = '\0'; + if (out_len) *out_len = (int)actual_len; + if (out_serial) *out_serial = serial; + return text; + } + + inline bool clipboard_clear() { + return montauk::clipboard_clear() == 0; + } + +} diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 14afb21..09d953e 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -494,6 +494,22 @@ namespace montauk { return (int)syscall2(Montauk::SYS_GETUSER, (uint64_t)buf, maxLen); } + // Clipboard + inline int clipboard_set_text(const char* data, uint32_t len) { + return (int)syscall2(Montauk::SYS_CLIPBOARD_SET_TEXT, (uint64_t)data, (uint64_t)len); + } + inline int clipboard_get_info(Montauk::ClipboardInfo* out) { + return (int)syscall1(Montauk::SYS_CLIPBOARD_GET_INFO, (uint64_t)out); + } + inline int clipboard_get_text(char* buf, uint32_t bufLen, uint32_t* outLen, + uint64_t* outSerial = nullptr) { + return (int)syscall4(Montauk::SYS_CLIPBOARD_GET_TEXT, (uint64_t)buf, (uint64_t)bufLen, + (uint64_t)outLen, (uint64_t)outSerial); + } + inline int clipboard_clear() { + return (int)syscall0(Montauk::SYS_CLIPBOARD_CLEAR); + } + // Window server inline int win_create(const char* title, int w, int h, Montauk::WinCreateResult* result) { return (int)syscall4(Montauk::SYS_WINCREATE, (uint64_t)title, (uint64_t)w, (uint64_t)h, (uint64_t)result); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index c69d624..41802bd 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -7,6 +7,7 @@ #include "desktop_internal.hpp" #include #include +#include // ============================================================================ // App Manifest Scanning @@ -187,6 +188,7 @@ void gui::desktop_init(DesktopState* ds) { montauk::memset(&ds->mouse, 0, sizeof(Montauk::MouseState)); montauk::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1); + gui::clipboard_clear(); // Load SVG icons — scalable (colorful) for app menu, symbolic for toolbar/panel Color defColor = colors::ICON_COLOR; diff --git a/programs/src/texteditor/main.cpp b/programs/src/texteditor/main.cpp index 7aba120..918e955 100644 --- a/programs/src/texteditor/main.cpp +++ b/programs/src/texteditor/main.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include extern "C" { @@ -442,6 +443,64 @@ static void set_status(const char* msg) { g_status_msg[sizeof(g_status_msg) - 1] = '\0'; } +static int selection_length() { + if (!g_has_selection) return 0; + int ss, se; + sel_range(&ss, &se); + return se - ss; +} + +static bool copy_selection_to_clipboard() { + int len = selection_length(); + if (len <= 0) { + set_status("Nothing selected"); + return false; + } + + int ss, se; + sel_range(&ss, &se); + if (!gui::clipboard_set_text(g_buffer + ss, len)) { + set_status("Copy failed"); + return false; + } + + set_status("Copied selection"); + return true; +} + +static bool cut_selection_to_clipboard() { + if (!copy_selection_to_clipboard()) + return false; + + delete_selection(); + set_status("Cut selection"); + return true; +} + +static bool paste_from_clipboard() { + int clip_len = 0; + char* clip_text = gui::clipboard_get_text_alloc(&clip_len); + if (clip_text == nullptr || clip_len <= 0) { + if (clip_text) montauk::mfree(clip_text); + set_status("Clipboard is empty"); + return false; + } + + int replaced_len = selection_length(); + if (g_buf_len - replaced_len + clip_len >= MAX_CAP) { + montauk::mfree(clip_text); + set_status("Clipboard text too large"); + return false; + } + + if (g_has_selection) + delete_selection(); + insert_string(clip_text, clip_len); + montauk::mfree(clip_text); + set_status("Pasted clipboard"); + return true; +} + static bool looks_binary(const char* data, int len) { if (!data || len <= 0) return false; @@ -951,6 +1010,24 @@ static bool handle_key(Montauk::KeyEvent& key, int win_id) { return true; } + // Ctrl+C: copy + if (key.ctrl && !key.alt && (key.ascii == 'c' || key.ascii == 'C')) { + copy_selection_to_clipboard(); + return true; + } + + // Ctrl+X: cut + if (key.ctrl && !key.alt && (key.ascii == 'x' || key.ascii == 'X')) { + cut_selection_to_clipboard(); + return true; + } + + // Ctrl+V: paste + if (key.ctrl && !key.alt && (key.ascii == 'v' || key.ascii == 'V')) { + paste_from_clipboard(); + return true; + } + // Arrow keys if (key.scancode == 0x48) { // Up if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); diff --git a/programs/src/wordprocessor/document.cpp b/programs/src/wordprocessor/document.cpp index aeb5735..3d79867 100644 --- a/programs/src/wordprocessor/document.cpp +++ b/programs/src/wordprocessor/document.cpp @@ -194,6 +194,27 @@ static void wp_sync_paragraph_count(WordProcessorState* wp) { } } +static int wp_count_newline_chars(const char* text, int len) { + if (len < 0) return -1; + if (len > 0 && text == nullptr) return -1; + + int count = 0; + for (int i = 0; i < len; i++) { + if (text[i] == '\0') return -1; + if (text[i] == '\n') count++; + } + return count; +} + +static int wp_count_newlines_in_range(WordProcessorState* wp, int start, int end) { + int count = 0; + for (int i = start; i < end; i++) { + if (wp_char_at(wp, i) == '\n') + count++; + } + return count; +} + static void wp_on_insert_newline(WordProcessorState* wp, int abs_pos) { if (wp->paragraph_count >= WP_MAX_PARAGRAPHS) return; int para = wp_find_paragraph_at(wp, abs_pos); @@ -1115,6 +1136,46 @@ void wp_delete_selection(WordProcessorState* wp) { wp_clear_selection(wp); } +char* wp_selection_plain_text_alloc(WordProcessorState* wp, int* out_len) { + if (out_len) *out_len = 0; + if (wp == nullptr || !wp->has_selection) return nullptr; + + int sel_s, sel_e; + wp_sel_range(wp, &sel_s, &sel_e); + int len = sel_e - sel_s; + if (len <= 0) return nullptr; + + char* text = (char*)montauk::malloc(len + 1); + if (text == nullptr) return nullptr; + + for (int i = 0; i < len; i++) + text[i] = wp_char_at(wp, sel_s + i); + text[len] = '\0'; + + if (out_len) *out_len = len; + return text; +} + +bool wp_insert_text(WordProcessorState* wp, const char* text, int len) { + int newline_count = wp_count_newline_chars(text, len); + if (wp == nullptr || newline_count < 0) return false; + if (len == 0) return true; + if (wp->cursor_run < 0 || wp->cursor_run >= wp->run_count) return false; + if (wp->total_text_len + len > WP_MAX_TEXT - 1) return false; + if (wp->paragraph_count + newline_count > WP_MAX_PARAGRAPHS) return false; + + StyledRun* cur = &wp->runs[wp->cursor_run]; + int extra_runs = 0; + if (!wp_same_style(cur, wp->cur_font_id, wp->cur_size, wp->cur_flags)) { + extra_runs = (wp->cursor_offset == 0 || wp->cursor_offset == cur->len) ? 1 : 2; + } + if (wp->run_count + extra_runs > WP_MAX_RUNS) return false; + + for (int i = 0; i < len; i++) + wp_insert_char(wp, text[i]); + return true; +} + void wp_recompute_wrap(WordProcessorState* wp, int content_w, int layout_dpi) { int wrap_width = content_w - WP_MARGIN * 2 - WP_SCROLLBAR_W; if (wrap_width < 50) wrap_width = 50; @@ -1664,6 +1725,38 @@ static bool wp_restore_snapshot(WordProcessorState* wp, const UndoSnapshot* snap return true; } +bool wp_replace_selection_with_text(WordProcessorState* wp, const char* text, int len) { + int newline_count = wp_count_newline_chars(text, len); + if (wp == nullptr || newline_count < 0) return false; + + int sel_len = 0; + int sel_newlines = 0; + if (wp->has_selection) { + int sel_s, sel_e; + wp_sel_range(wp, &sel_s, &sel_e); + sel_len = sel_e - sel_s; + sel_newlines = wp_count_newlines_in_range(wp, sel_s, sel_e); + } + + if (wp->total_text_len - sel_len + len > WP_MAX_TEXT - 1) + return false; + if (wp->paragraph_count - sel_newlines + newline_count > WP_MAX_PARAGRAPHS) + return false; + + UndoSnapshot snap = {}; + if (!wp_capture_snapshot(wp, &snap)) + return false; + + if (wp->has_selection) + wp_delete_selection(wp); + + bool ok = wp_insert_text(wp, text, len); + if (!ok) + wp_restore_snapshot(wp, &snap); + wp_free_snapshot(&snap); + return ok; +} + void wp_save_file(WordProcessorState* wp) { if (wp->filepath[0] == '\0') { wp_open_save_pathbar(wp); diff --git a/programs/src/wordprocessor/input.cpp b/programs/src/wordprocessor/input.cpp index f4cf435..c345fe3 100644 --- a/programs/src/wordprocessor/input.cpp +++ b/programs/src/wordprocessor/input.cpp @@ -6,6 +6,7 @@ #include "wordprocessor.hpp" #include +#include static void wp_set_status(WordProcessorState* wp, const char* msg) { if (!msg) msg = ""; @@ -13,6 +14,57 @@ static void wp_set_status(WordProcessorState* wp, const char* msg) { wp->status_msg[sizeof(wp->status_msg) - 1] = '\0'; } +static bool wp_copy_selection_to_clipboard(WordProcessorState* wp) { + int len = 0; + char* text = wp_selection_plain_text_alloc(wp, &len); + if (text == nullptr || len <= 0) { + if (text) montauk::mfree(text); + wp_set_status(wp, "Nothing selected"); + return false; + } + + bool ok = gui::clipboard_set_text(text, len); + montauk::mfree(text); + if (!ok) { + wp_set_status(wp, "Copy failed"); + return false; + } + + wp_set_status(wp, "Copied selection"); + return true; +} + +static bool wp_cut_selection_to_clipboard(WordProcessorState* wp) { + if (!wp_copy_selection_to_clipboard(wp)) + return false; + + wp_delete_selection(wp); + wp_history_checkpoint(wp); + wp_set_status(wp, "Cut selection"); + return true; +} + +static bool wp_paste_from_clipboard(WordProcessorState* wp) { + int len = 0; + char* text = gui::clipboard_get_text_alloc(&len); + if (text == nullptr || len <= 0) { + if (text) montauk::mfree(text); + wp_set_status(wp, "Clipboard is empty"); + return false; + } + + bool ok = wp_replace_selection_with_text(wp, text, len); + montauk::mfree(text); + if (!ok) { + wp_set_status(wp, "Clipboard text could not be inserted"); + return false; + } + + wp_history_checkpoint(wp); + wp_set_status(wp, "Pasted clipboard"); + return true; +} + static void wp_open_pathbar_for_open(WordProcessorState* wp) { char path[256] = {}; char msg[160] = {}; @@ -584,6 +636,19 @@ void wp_handle_key(const Montauk::KeyEvent& key) { return; } + if (key.ctrl && !key.alt && (key.ascii == 'c' || key.ascii == 'C')) { + wp_copy_selection_to_clipboard(wp); + return; + } + if (key.ctrl && !key.alt && (key.ascii == 'x' || key.ascii == 'X')) { + wp_cut_selection_to_clipboard(wp); + return; + } + if (key.ctrl && !key.alt && (key.ascii == 'v' || key.ascii == 'V')) { + wp_paste_from_clipboard(wp); + return; + } + if (key.scancode == 0x48) { if (key.shift) wp_start_selection(wp); else wp_clear_selection(wp); diff --git a/programs/src/wordprocessor/wordprocessor.hpp b/programs/src/wordprocessor/wordprocessor.hpp index c4cfcb6..778dce0 100644 --- a/programs/src/wordprocessor/wordprocessor.hpp +++ b/programs/src/wordprocessor/wordprocessor.hpp @@ -424,6 +424,9 @@ void wp_set_line_spacing(WordProcessorState* wp, int value); void wp_toggle_list(WordProcessorState* wp, uint8_t list_type); void wp_insert_divider(WordProcessorState* wp, uint8_t divider_type); void wp_delete_selection(WordProcessorState* wp); +char* wp_selection_plain_text_alloc(WordProcessorState* wp, int* out_len); +bool wp_insert_text(WordProcessorState* wp, const char* text, int len); +bool wp_replace_selection_with_text(WordProcessorState* wp, const char* text, int len); void wp_recompute_wrap(WordProcessorState* wp, int content_w, int layout_dpi); int wp_find_wrap_line(WordProcessorState* wp, int abs_pos);