From fb9cba0d01b211a949bc36a3cfc8e70a07f0b6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20B=C3=A1lint=20Misius?= Date: Thu, 11 May 2023 17:47:25 +0200 Subject: [PATCH] Some native clipboard support for some platforms I was hoping SDL2 would get this functionality eventually, but nope, proper clipboard support is staged for SDL3, which we're not going to see much of for at least a few more months. This will have to do for 98.0. The feature can be disabled at runtime from powder.pref. Implementation status: - windows (via winapi): has the most friendly api so of course the implementation is flawless and uses every available optimization >_> - macos (via cocoa): I'm bad at cocoa so this is only as good as absolutely necessary; TODO: on-demand rendering - x11 (via xclip): I am NOT implementing icccm2; TODO: remove reliance on external tools - wayland (via wl-clipboard): oh god wayland oh why, you're almost as bad as x11; TODO: remove reliance on external tools - android: TODO; is there even a point? - emscripten: TODO; the tricky bit is that in a browser we can only get clipboard data when the user is giving it to us, so this will require some JS hackery that I'm not mentally prepared for right now; also I think the supported content types are very limited and you can't just define your own x11 and wayland support are handled by a common backend which delegates clipboard management to xclip-like external programs, such as xclip itself or wl-clipboard, and can load custom command line templates from powder.pref for use with other such programs. --- .github/macaa64-ghactions.ini | 1 + cross-examples/macaa64.ini | 1 + meson.build | 3 +- meson_options.txt | 6 + src/PowderToySDL.cpp | 3 + src/common/clipboard/Clipboard.h | 14 ++ .../clipboard/ClipboardImpls.template.h | 10 + src/common/clipboard/Cocoa.mm | 71 ++++++ src/common/clipboard/Dynamic.cpp | 120 +++++++++ src/common/clipboard/Dynamic.h | 45 ++++ src/common/clipboard/External.cpp | 232 ++++++++++++++++++ src/common/clipboard/Null.cpp | 22 ++ src/common/clipboard/Windows.cpp | 226 +++++++++++++++++ src/common/clipboard/meson.build | 39 +++ src/common/meson.build | 1 + src/gui/game/GameModel.cpp | 5 +- src/gui/game/GameModel.h | 3 - src/gui/game/GameView.cpp | 23 +- src/meson.build | 12 + src/prefs/Prefs.h | 14 +- 20 files changed, 836 insertions(+), 15 deletions(-) create mode 100644 src/common/clipboard/Clipboard.h create mode 100644 src/common/clipboard/ClipboardImpls.template.h create mode 100644 src/common/clipboard/Cocoa.mm create mode 100644 src/common/clipboard/Dynamic.cpp create mode 100644 src/common/clipboard/Dynamic.h create mode 100644 src/common/clipboard/External.cpp create mode 100644 src/common/clipboard/Null.cpp create mode 100644 src/common/clipboard/Windows.cpp create mode 100644 src/common/clipboard/meson.build diff --git a/.github/macaa64-ghactions.ini b/.github/macaa64-ghactions.ini index a3b49975b..774d77dfe 100644 --- a/.github/macaa64-ghactions.ini +++ b/.github/macaa64-ghactions.ini @@ -1,6 +1,7 @@ [binaries] c = [ 'clang', '-arch', 'arm64' ] cpp = [ 'clang++', '-arch', 'arm64' ] +objcpp = [ 'clang++', '-arch', 'arm64' ] strip = 'strip' [host_machine] diff --git a/cross-examples/macaa64.ini b/cross-examples/macaa64.ini index a3b49975b..774d77dfe 100644 --- a/cross-examples/macaa64.ini +++ b/cross-examples/macaa64.ini @@ -1,6 +1,7 @@ [binaries] c = [ 'clang', '-arch', 'arm64' ] cpp = [ 'clang++', '-arch', 'arm64' ] +objcpp = [ 'clang++', '-arch', 'arm64' ] strip = 'strip' [host_machine] diff --git a/meson.build b/meson.build index db7963020..3c9cf130b 100644 --- a/meson.build +++ b/meson.build @@ -362,6 +362,7 @@ else endif data_files = [] +powder_deps = [] subdir('src') subdir('resources') @@ -377,7 +378,7 @@ if host_platform == 'emscripten' endif if get_option('build_powder') - powder_deps = [ + powder_deps += [ threads_dep, zlib_dep, png_dep, diff --git a/meson_options.txt b/meson_options.txt index 34b3a7904..697a25f0c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -280,3 +280,9 @@ option( value: '', description: 'Build date string, used by ghactions workflows, not useful otherwise' ) +option( + 'platform_clipboard', + type: 'boolean', + value: true, + description: 'Enable platform clipboard, allows copying simulation data between different windows' +) diff --git a/src/PowderToySDL.cpp b/src/PowderToySDL.cpp index d82aa3f41..524c1ef41 100644 --- a/src/PowderToySDL.cpp +++ b/src/PowderToySDL.cpp @@ -5,6 +5,7 @@ #include "gui/interface/Engine.h" #include "graphics/Graphics.h" #include "common/platform/Platform.h" +#include "common/clipboard/Clipboard.h" #include int desktopWidth = 1280; @@ -113,6 +114,7 @@ void SDLOpen() fprintf(stderr, "Initializing SDL (video subsystem): %s\n", SDL_GetError()); Platform::Exit(-1); } + Clipboard::Init(); SDLSetScreen(); @@ -256,6 +258,7 @@ void SDLSetScreen() Platform::Exit(-1); } SDL_RaiseWindow(sdl_window); + Clipboard::RecreateWindow(); } SDL_RenderSetIntegerScale(sdl_renderer, newFrameOpsNorm.forceIntegerScaling ? SDL_TRUE : SDL_FALSE); if (!(newFrameOpsNorm.resizable && SDL_GetWindowFlags(sdl_window) & SDL_WINDOW_MAXIMIZED)) diff --git a/src/common/clipboard/Clipboard.h b/src/common/clipboard/Clipboard.h new file mode 100644 index 000000000..a25c7047e --- /dev/null +++ b/src/common/clipboard/Clipboard.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include "common/String.h" + +class GameSave; + +namespace Clipboard +{ + const ByteString clipboardFormatName = "application/vnd.powdertoy.save"; + void SetClipboardData(std::unique_ptr data); + const GameSave *GetClipboardData(); + void Init(); + void RecreateWindow(); +} diff --git a/src/common/clipboard/ClipboardImpls.template.h b/src/common/clipboard/ClipboardImpls.template.h new file mode 100644 index 000000000..e0de0bcd0 --- /dev/null +++ b/src/common/clipboard/ClipboardImpls.template.h @@ -0,0 +1,10 @@ +#ifdef CLIPBOARD_IMPLS_DECLARE +# define IMPL_DEFINE(subsystem, factory) std::unique_ptr factory(); +#endif +#ifdef CLIPBOARD_IMPLS_DEFINE +# define IMPL_DEFINE(subsystem, factory) { subsystem, factory }, +#endif + +@impl_defs@ + +#undef IMPL_DEFINE diff --git a/src/common/clipboard/Cocoa.mm b/src/common/clipboard/Cocoa.mm new file mode 100644 index 000000000..20bb79577 --- /dev/null +++ b/src/common/clipboard/Cocoa.mm @@ -0,0 +1,71 @@ +#include "Dynamic.h" +#include "Clipboard.h" +#include +#include + +namespace Clipboard +{ + static int changeCount = -1; + + class CocoaClipboardImpl : public ClipboardImpl + { + public: + void SetClipboardData() final override + { + @autoreleasepool + { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSString *format = [NSString stringWithUTF8String:clipboardFormatName.c_str()]; + changeCount = [pasteboard declareTypes:[NSArray arrayWithObject:format] owner:nil]; + std::vector saveData; + SerializeClipboard(saveData); + const auto *base = &saveData[0]; + auto size = saveData.size(); + NSData *data = [NSData dataWithBytes:base length:size]; + if (![pasteboard setData:data forType:format]) + { + std::cerr << "cannot put save on clipboard: [pasteboard setData] failed" << std::endl; + return; + } + } + std::cerr << "put save on clipboard" << std::endl; + } + + GetClipboardDataResult GetClipboardData() final override + { + GetClipboardDataChanged gdc; + @autoreleasepool + { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + int newChangeCount = [pasteboard changeCount]; + if (changeCount == newChangeCount) + { + return GetClipboardDataUnchanged{}; + } + changeCount = newChangeCount; + NSString *format = [NSString stringWithUTF8String:clipboardFormatName.c_str()]; + NSString *available = [pasteboard availableTypeFromArray:[NSArray arrayWithObject:format]]; + if (![available isEqualToString:format]) + { + std::cerr << "not getting save from clipboard: no data" << std::endl; + return GetClipboardDataFailed{}; + } + NSData *data = [pasteboard dataForType:format]; + if (data == nil) + { + std::cerr << "not getting save from clipboard: [pasteboard dataForType] failed" << std::endl; + return GetClipboardDataFailed{}; + } + auto *base = reinterpret_cast([data bytes]); + auto size = [data length]; + gdc.data = std::vector(base, base + size); + } + return gdc; + } + }; + + std::unique_ptr CocoaClipboardFactory() + { + return std::make_unique(); + } +} diff --git a/src/common/clipboard/Dynamic.cpp b/src/common/clipboard/Dynamic.cpp new file mode 100644 index 000000000..f4910fc00 --- /dev/null +++ b/src/common/clipboard/Dynamic.cpp @@ -0,0 +1,120 @@ +#include "Dynamic.h" +#include "client/GameSave.h" +#include "prefs/GlobalPrefs.h" +#include "PowderToySDL.h" +#include +#include + +namespace Clipboard +{ +#define CLIPBOARD_IMPLS_DECLARE +#include "ClipboardImpls.h" +#undef CLIPBOARD_IMPLS_DECLARE + + struct ClipboardImplEntry + { + SDL_SYSWM_TYPE subsystem; + std::unique_ptr (*factory)(); + } clipboardImpls[] = { +#define CLIPBOARD_IMPLS_DEFINE +#include "ClipboardImpls.h" +#undef CLIPBOARD_IMPLS_DEFINE + { SDL_SYSWM_UNKNOWN, nullptr }, + }; + + std::unique_ptr clipboardData; + std::unique_ptr clipboard; + + void InvokeClipboardSetClipboardData() + { + if (clipboard) + { + if (clipboardData) + { + clipboard->SetClipboardData(); // this either works or it doesn't, we don't care + } + else + { + std::cerr << "cannot put save on clipboard: no data to transfer" << std::endl; + } + } + } + + void SerializeClipboard(std::vector &saveData) + { + std::tie(std::ignore, saveData) = clipboardData->Serialise(); + } + + void SetClipboardData(std::unique_ptr data) + { + clipboardData = std::move(data); + InvokeClipboardSetClipboardData(); + } + + void InvokeClipboardGetClipboardData() + { + if (clipboard) + { + auto result = clipboard->GetClipboardData(); + if (std::holds_alternative(result)) + { + std::cerr << "not getting save from clipboard, data unchanged" << std::endl; + return; + } + if (std::holds_alternative(result)) + { + return; + } + clipboardData.reset(); + auto *data = std::get_if(&result); + if (!data) + { + return; + } + try + { + clipboardData = std::make_unique(data->data); + } + catch (const ParseException &e) + { + std::cerr << "got bad save from clipboard: " << e.what() << std::endl; + return; + } + std::cerr << "got save from clipboard" << std::endl; + } + } + + const GameSave *GetClipboardData() + { + InvokeClipboardGetClipboardData(); + return clipboardData.get(); + } + + void Init() + { + } + + int currentSubsystem; + + void RecreateWindow() + { + // old window is gone (or doesn't exist), associate clipboard data with the new one + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + SDL_GetWindowWMInfo(sdl_window, &info); + clipboard.reset(); + currentSubsystem = info.subsystem; + if (GlobalPrefs::Ref().Get("NativeClipboard.Enabled", true)) + { + for (auto *impl = clipboardImpls; impl->factory; ++impl) + { + if (impl->subsystem == currentSubsystem) + { + clipboard = impl->factory(); + break; + } + } + } + InvokeClipboardSetClipboardData(); + } +} diff --git a/src/common/clipboard/Dynamic.h b/src/common/clipboard/Dynamic.h new file mode 100644 index 000000000..a7ae4caf1 --- /dev/null +++ b/src/common/clipboard/Dynamic.h @@ -0,0 +1,45 @@ +#pragma once +#include "common/String.h" +#include +#include +#include + +class GameSave; + +namespace Clipboard +{ + class ClipboardImpl + { + public: + virtual ~ClipboardImpl() = default; + + virtual void SetClipboardData() = 0; + + struct GetClipboardDataUnchanged + { + }; + struct GetClipboardDataChanged + { + std::vector data; + }; + struct GetClipboardDataFailed + { + }; + struct GetClipboardDataUnknown + { + }; + using GetClipboardDataResult = std::variant< + GetClipboardDataUnchanged, + GetClipboardDataChanged, + GetClipboardDataFailed, + GetClipboardDataUnknown + >; + virtual GetClipboardDataResult GetClipboardData() = 0; + }; + + extern std::unique_ptr clipboardData; + + void SerializeClipboard(std::vector &saveData); + + extern int currentSubsystem; +} diff --git a/src/common/clipboard/External.cpp b/src/common/clipboard/External.cpp new file mode 100644 index 000000000..21d5f34e1 --- /dev/null +++ b/src/common/clipboard/External.cpp @@ -0,0 +1,232 @@ +#include "Dynamic.h" +#include "Clipboard.h" +#include "client/GameSave.h" +#include "prefs/GlobalPrefs.h" +#include "PowderToySDL.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Clipboard +{ + struct Preset + { + ByteString inCommand; + ByteString formatsCommand; + ByteString outCommand; + std::optional defaultForSubsystem; + }; + std::map builtInPresets = { + { "xclip", { + "xclip -selection clipboard -target %s", + "xclip -out -selection clipboard -target TARGETS", + "xclip -out -selection clipboard -target %s", + SDL_SYSWM_X11, + } }, + { "wl-clipboard", { + "wl-copy --type %s", + "wl-paste --list-types", + "wl-paste --type %s", + SDL_SYSWM_WAYLAND, + } }, + }; + + static ByteString SubstFormat(ByteString str) + { + if (auto split = str.SplitBy("%s")) + { + str = split.Before() + clipboardFormatName + split.After(); + } + return str; + } + + static std::optional GetPreset() + { + std::optional name = GlobalPrefs::Ref().Get("NativeClipboard.External.Type", ByteString("auto")); + if (name == "custom") + { + auto getCommand = [](ByteString key) -> std::optional { + auto fullKey = "NativeClipboard.External." + key; + auto value = GlobalPrefs::Ref().Get(fullKey); + if (!value) + { + std::cerr << "custom external clipboard command preset: missing " << fullKey << std::endl; + return std::nullopt; + } + return *value; + }; + auto inCommand = getCommand("In"); + auto formatsCommand = getCommand("Formats"); + auto outCommand = getCommand("Out"); + if (!inCommand || !formatsCommand || !outCommand) + { + return std::nullopt; + } + return Preset{ + SubstFormat(*inCommand), + SubstFormat(*formatsCommand), + SubstFormat(*outCommand), + }; + } + if (name == "auto") + { + name.reset(); + for (auto &[ presetName, preset ] : builtInPresets) + { + if (preset.defaultForSubsystem && *preset.defaultForSubsystem == currentSubsystem) + { + name = presetName; + } + } + if (!name) + { + std::cerr << "no built-in external clipboard command preset for SDL window subsystem " << currentSubsystem << std::endl; + return std::nullopt; + } + } + auto it = builtInPresets.find(*name); + if (it == builtInPresets.end()) + { + std::cerr << "no built-in external clipboard command preset with name " << *name << std::endl; + return std::nullopt; + } + return Preset{ + SubstFormat(it->second.inCommand), + SubstFormat(it->second.formatsCommand), + SubstFormat(it->second.outCommand), + }; + } + + class ExternalClipboardImpl : public ClipboardImpl + { + public: + ExternalClipboardImpl() + { + signal(SIGPIPE, SIG_IGN); // avoids problems with popen + } + + void SetClipboardData() final override + { + auto preset = GetPreset(); + if (!preset) + { + return; + } + auto handle = popen(preset->inCommand.c_str(), "we"); + if (!handle) + { + std::cerr << "failed to set clipboard data: popen: " << strerror(errno) << std::endl; + return; + } + auto bail = false; + std::vector saveData; + SerializeClipboard(saveData); + if (fwrite(&saveData[0], 1, saveData.size(), handle) != saveData.size()) + { + std::cerr << "failed to set clipboard data: fwrite: " << strerror(errno) << std::endl; + bail = true; + } + auto status = pclose(handle); + if (bail) + { + return; + } + if (status == -1) + { + std::cerr << "failed to set clipboard data: pclose: " << strerror(errno) << std::endl; + return; + } + if (status) + { + std::cerr << "failed to set clipboard data: " << preset->inCommand << ": wait4 status code " << status << std::endl; + return; + } + } + + GetClipboardDataResult GetClipboardData() final override + { + auto getTarget = [](ByteString command) -> std::optional> { + if (!command.size()) + { + return std::nullopt; + } + auto handle = popen(command.c_str(), "re"); + if (!handle) + { + std::cerr << "cannot get save from clipboard: popen: " << strerror(errno) << std::endl; + return std::nullopt; + } + constexpr auto blockSize = 0x10000; + std::vector data; + auto bail = false; + while (true) + { + auto pos = data.size(); + data.resize(pos + blockSize); + auto got = fread(&data[pos], 1, blockSize, handle); + if (got != blockSize) + { + if (ferror(handle)) + { + std::cerr << "cannot get save from clipboard: fread: " << strerror(errno) << std::endl; + bail = true; + break; + } + if (feof(handle)) + { + data.resize(data.size() - blockSize + got); + break; + } + } + } + auto status = pclose(handle); + if (bail) + { + return std::nullopt; + } + if (status == -1) + { + std::cerr << "cannot get save from clipboard: pclose: " << strerror(errno) << std::endl; + return std::nullopt; + } + if (status) + { + std::cerr << "cannot get save from clipboard: " << command << ": wait4 status code " << status << std::endl; + return std::nullopt; + } + return data; + }; + auto preset = GetPreset(); + if (!preset) + { + return GetClipboardDataUnknown{}; + } + auto formatsOpt = getTarget(preset->formatsCommand); + if (!formatsOpt) + { + return GetClipboardDataUnknown{}; + } + auto formats = ByteString(formatsOpt->begin(), formatsOpt->end()).PartitionBy('\n'); + if (std::find(formats.begin(), formats.end(), clipboardFormatName) == formats.end()) + { + std::cerr << "not getting save from clipboard: no data" << std::endl; + return GetClipboardDataFailed{}; + } + auto saveDataOpt = getTarget(preset->outCommand); + if (!saveDataOpt) + { + return GetClipboardDataFailed{}; + } + return GetClipboardDataChanged{ std::move(*saveDataOpt) }; + } + }; + + std::unique_ptr ExternalClipboardFactory() + { + return std::make_unique(); + } +} diff --git a/src/common/clipboard/Null.cpp b/src/common/clipboard/Null.cpp new file mode 100644 index 000000000..99d07ce2a --- /dev/null +++ b/src/common/clipboard/Null.cpp @@ -0,0 +1,22 @@ +#include "Clipboard.h" +#include "client/GameSave.h" + +namespace Clipboard +{ + void SetClipboardData(std::unique_ptr data) + { + } + + const GameSave *GetClipboardData() + { + return nullptr; + } + + void Init() + { + } + + void RecreateWindow() + { + } +} diff --git a/src/common/clipboard/Windows.cpp b/src/common/clipboard/Windows.cpp new file mode 100644 index 000000000..2cb0235f3 --- /dev/null +++ b/src/common/clipboard/Windows.cpp @@ -0,0 +1,226 @@ +#include "Dynamic.h" +#include "Clipboard.h" +#include "client/GameSave.h" +#include "common/platform/Platform.h" +#include "PowderToySDL.h" +#include +#include +#include +#include + +namespace Clipboard +{ + class WindowsClipboardImpl : public ClipboardImpl + { + UINT saveClipboardFormat = 0; + HWND ourHwnd = NULL; + DWORD seqNumber = 0; // 0 is invalid + + class ClipboardSession + { + bool open = false; + + public: + ClipboardSession(HWND ourHwnd) + { + if (ourHwnd) + { + open = ::OpenClipboard(ourHwnd); + } + } + + ~ClipboardSession() + { + if (open) + { + ::CloseClipboard(); + } + } + + explicit operator bool() const + { + return open; + } + }; + + void Transfer() + { + if (!saveClipboardFormat) + { + std::cerr << "cannot transfer save data: save clipboard format not registered" << std::endl; + return; + } + if (!clipboardData) + { + std::cerr << "cannot transfer save data: no data to transfer" << std::endl; + return; + } + std::vector saveData; + SerializeClipboard(saveData); + auto handle = std::unique_ptr(::GlobalAlloc(GMEM_MOVEABLE, saveData.size()), GlobalFree); + if (!handle) + { + std::cerr << "cannot transfer save data: GlobalAlloc failed: " << ::GetLastError() << std::endl; + return; + } + { + auto data = std::unique_ptr(::GlobalLock(handle.get()), ::GlobalUnlock); + auto base = reinterpret_cast(data.get()); + std::copy(saveData.begin(), saveData.end(), base); + } + if (!::SetClipboardData(saveClipboardFormat, handle.get())) + { + std::cerr << "cannot transfer save data: SetClipboardData failed: " << ::GetLastError() << std::endl; + return; + } + handle.release(); // windows owns it now + auto newSeqNumber = ::GetClipboardSequenceNumber(); + if (newSeqNumber) + { + seqNumber = newSeqNumber; + } + std::cerr << "transferred save data" << std::endl; + } + + static int TransferWatchWrapper(void *userdata, SDL_Event *event) + { + return reinterpret_cast(userdata)->TransferWatch(event); + } + + int TransferWatch(SDL_Event *event) + { + // SDL documentation says we have to be very careful with what we do here because + // the callback can come from any random thread, and we indeed are: WM_RENDERFORMAT + // and WM_RENDERALLFORMATS are only posted to windows that have announced data on + // the clipboard, and only our main thread ever owns a window, so we don't touch + // the WindowsClipboardImpl outside of these events. + switch (event->type) + { + case SDL_SYSWMEVENT: + switch (event->syswm.msg->msg.win.msg) + { + case WM_RENDERFORMAT: + if (event->syswm.msg->msg.win.wParam == saveClipboardFormat) + { + Transfer(); + } + break; + + case WM_RENDERALLFORMATS: + { + ClipboardSession cs(ourHwnd); + if (cs) + { + if (ourHwnd && ::GetClipboardOwner() == ourHwnd) + { + Transfer(); + } + } + else + { + std::cerr << "cannot place save on clipboard: OpenClipboard failed: " << ::GetLastError() << std::endl; + } + } + break; + } + break; + } + return 0; + } + + public: + WindowsClipboardImpl() + { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + SDL_GetWindowWMInfo(sdl_window, &info); + ourHwnd = info.info.win.window; + saveClipboardFormat = ::RegisterClipboardFormatW(Platform::WinWiden(clipboardFormatName).c_str()); + if (!saveClipboardFormat) + { + std::cerr << "cannot register save clipboard format: RegisterClipboardFormatW failed: " << ::GetLastError() << std::endl; + return; + } + std::cerr << "save clipboard format registered" << std::endl; + SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE); + SDL_AddEventWatch(&WindowsClipboardImpl::TransferWatchWrapper, this); + } + + ~WindowsClipboardImpl() + { + SDL_DelEventWatch(&WindowsClipboardImpl::TransferWatchWrapper, this); + SDL_EventState(SDL_SYSWMEVENT, SDL_DISABLE); + } + + void SetClipboardData() final override + { + if (!saveClipboardFormat) + { + std::cerr << "cannot announce save on clipboard: save clipboard format not registered" << std::endl; + return; + } + ClipboardSession cs(ourHwnd); + if (!cs) + { + std::cerr << "cannot announce save on clipboard: OpenClipboard failed: " << ::GetLastError() << std::endl; + return; + } + if (!::EmptyClipboard()) + { + std::cerr << "cannot announce save on clipboard: EmptyClipboard failed: " << ::GetLastError() << std::endl; + return; + } + ::SetClipboardData(saveClipboardFormat, NULL); + std::cerr << "announced save on clipboard" << std::endl; + } + + GetClipboardDataResult GetClipboardData() final override + { + // Note that the data from the local clipboard is left alone if any error occurs so + // the local clipboard keeps working even in the worst case. + if (!saveClipboardFormat) + { + std::cerr << "cannot get save from clipboard: save clipboard format not registered" << std::endl; + return GetClipboardDataUnknown{}; + } + ClipboardSession cs(ourHwnd); + if (!cs) + { + std::cerr << "cannot get save from clipboard: OpenClipboard failed: " << ::GetLastError() << std::endl; + return GetClipboardDataUnknown{}; + } + auto newSeqNumber = ::GetClipboardSequenceNumber(); + if (seqNumber && newSeqNumber && seqNumber == newSeqNumber) + { + std::cerr << "not getting save from clipboard, data unchanged" << std::endl; + return GetClipboardDataUnchanged{}; + } + seqNumber = newSeqNumber; + if (!::IsClipboardFormatAvailable(saveClipboardFormat)) + { + std::cerr << "not getting save from clipboard: no data" << std::endl; + return GetClipboardDataFailed{}; + } + auto handle = ::GetClipboardData(saveClipboardFormat); + if (!handle) + { + std::cerr << "cannot get save from clipboard: GetClipboardData failed: " << ::GetLastError() << std::endl; + return GetClipboardDataFailed{}; + } + auto size = ::GlobalSize(handle); + auto data = std::unique_ptr(::GlobalLock(handle), ::GlobalUnlock); + if (!data) + { + std::cerr << "cannot get save from clipboard: GlobalLock failed: " << ::GetLastError() << std::endl; + return GetClipboardDataFailed{}; + } + auto base = reinterpret_cast(data.get()); + return GetClipboardDataChanged{ std::vector(base, base + size) }; + } + }; + + std::unique_ptr WindowsClipboardFactory() + { + return std::make_unique(); + } +} diff --git a/src/common/clipboard/meson.build b/src/common/clipboard/meson.build new file mode 100644 index 000000000..1c6626838 --- /dev/null +++ b/src/common/clipboard/meson.build @@ -0,0 +1,39 @@ +if get_option('platform_clipboard') + clipboard_impl_factories = [] + if host_platform == 'windows' + powder_files += files('Windows.cpp') + clipboard_impl_factories += [ + [ 'SDL_SYSWM_WINDOWS', 'WindowsClipboardFactory' ], + ] + elif host_platform == 'darwin' + if get_option('build_powder') + add_languages('objcpp', native: false) + powder_deps += [ + dependency('Cocoa'), + ] + endif + powder_files += files([ + 'Cocoa.mm', + ]) + clipboard_impl_factories += [ + [ 'SDL_SYSWM_COCOA', 'CocoaClipboardFactory' ], + ] + elif host_platform == 'android' + # TODO + elif host_platform == 'emscripten' + # TODO + else + powder_files += files([ + 'External.cpp', + ]) + clipboard_impl_factories += [ + [ 'SDL_SYSWM_X11', 'ExternalClipboardFactory' ], + [ 'SDL_SYSWM_WAYLAND', 'ExternalClipboardFactory' ], + ] + endif + powder_files += files('Dynamic.cpp') +else + powder_files += files('Null.cpp') +endif +render_files += files('Null.cpp') +font_files += files('Null.cpp') diff --git a/src/common/meson.build b/src/common/meson.build index 5f6c63294..b9f0f0199 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -4,4 +4,5 @@ common_files += files( 'tpt-thread-local.cpp', ) +subdir('clipboard') subdir('platform') diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp index f95deec30..f59a36eca 100644 --- a/src/gui/game/GameModel.cpp +++ b/src/gui/game/GameModel.cpp @@ -19,6 +19,7 @@ #include "client/SaveInfo.h" #include "client/http/ExecVoteRequest.h" #include "common/platform/Platform.h" +#include "common/clipboard/Clipboard.h" #include "graphics/Renderer.h" #include "simulation/Air.h" #include "simulation/GOLString.h" @@ -1371,12 +1372,12 @@ void GameModel::TransformPlaceSave(Mat2 transform, Vec2 nudge) void GameModel::SetClipboard(std::unique_ptr save) { - clipboard = std::move(save); + Clipboard::SetClipboardData(std::move(save)); } const GameSave *GameModel::GetClipboard() const { - return clipboard.get(); + return Clipboard::GetClipboardData(); } const GameSave *GameModel::GetTransformedPlaceSave() const diff --git a/src/gui/game/GameModel.h b/src/gui/game/GameModel.h index 2fc8a187f..71be9b598 100644 --- a/src/gui/game/GameModel.h +++ b/src/gui/game/GameModel.h @@ -50,9 +50,6 @@ class GameModel private: std::vector notifications; - //int clipboardSize; - //unsigned char * clipboardData; - std::unique_ptr clipboard; std::unique_ptr placeSave; std::unique_ptr transformedPlaceSave; std::deque consoleLog; diff --git a/src/gui/game/GameView.cpp b/src/gui/game/GameView.cpp index d1379cc90..9e081188d 100644 --- a/src/gui/game/GameView.cpp +++ b/src/gui/game/GameView.cpp @@ -1656,20 +1656,27 @@ void GameView::OnFileDrop(ByteString filename) return; } - auto saveFile = Client::Ref().LoadSaveFile(filename); - if (!saveFile) - return; - if (saveFile->GetError().length()) - { - new ErrorMessage("Error loading save", "Dropped save file could not be loaded: " + saveFile->GetError()); - return; - } + if (filename.EndsWith(".stm")) { + auto saveFile = Client::Ref().GetStamp(filename); + if (!saveFile || !saveFile->GetGameSave()) + { + new ErrorMessage("Error loading stamp", "Dropped stamp could not be loaded: " + saveFile->GetError()); + return; + } c->LoadStamp(saveFile->TakeGameSave()); } else { + auto saveFile = Client::Ref().LoadSaveFile(filename); + if (!saveFile) + return; + if (saveFile->GetError().length()) + { + new ErrorMessage("Error loading save", "Dropped save file could not be loaded: " + saveFile->GetError()); + return; + } c->LoadSaveFile(std::move(saveFile)); } diff --git a/src/meson.build b/src/meson.build index c97103939..87cd5301e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -176,6 +176,18 @@ configure_file( configuration: elements_conf_data ) +clipboard_impl_defs = [] +foreach impl_subsys_factory : clipboard_impl_factories + clipboard_impl_defs += 'IMPL_DEFINE(' + impl_subsys_factory[0] + ', ' + impl_subsys_factory[1] + ')' +endforeach +clipboard_impls_data = configuration_data() +clipboard_impls_data.set('impl_defs', '\n'.join(clipboard_impl_defs)) +configure_file( + input: 'common/clipboard/ClipboardImpls.template.h', + output: 'ClipboardImpls.h', + configuration: clipboard_impls_data +) + simulation_tool_defs = [] foreach tool_name_id : simulation_tool_ids simulation_tool_defs += 'TOOL_DEFINE(' + tool_name_id[0] + ', ' + tool_name_id[1].to_string() + ');' diff --git a/src/prefs/Prefs.h b/src/prefs/Prefs.h index a3339f3d8..ee1712ca2 100644 --- a/src/prefs/Prefs.h +++ b/src/prefs/Prefs.h @@ -1,6 +1,7 @@ #pragma once #include "common/String.h" #include +#include class Prefs { @@ -35,7 +36,7 @@ public: Prefs(ByteString path); template - Type Get(ByteString path, Type defaultValue) const + std::optional Get(ByteString path) const { auto value = GetJson(root, path); if (value != Json::nullValue) @@ -48,6 +49,17 @@ public: { } } + return std::nullopt; + } + + template + Type Get(ByteString path, Type defaultValue) const + { + auto value = Get(path); + if (value) + { + return *value; + } return defaultValue; }