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.
This commit is contained in:
parent
2eee738a9f
commit
fb9cba0d01
1
.github/macaa64-ghactions.ini
vendored
1
.github/macaa64-ghactions.ini
vendored
@ -1,6 +1,7 @@
|
||||
[binaries]
|
||||
c = [ 'clang', '-arch', 'arm64' ]
|
||||
cpp = [ 'clang++', '-arch', 'arm64' ]
|
||||
objcpp = [ 'clang++', '-arch', 'arm64' ]
|
||||
strip = 'strip'
|
||||
|
||||
[host_machine]
|
||||
|
@ -1,6 +1,7 @@
|
||||
[binaries]
|
||||
c = [ 'clang', '-arch', 'arm64' ]
|
||||
cpp = [ 'clang++', '-arch', 'arm64' ]
|
||||
objcpp = [ 'clang++', '-arch', 'arm64' ]
|
||||
strip = 'strip'
|
||||
|
||||
[host_machine]
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include "gui/interface/Engine.h"
|
||||
#include "graphics/Graphics.h"
|
||||
#include "common/platform/Platform.h"
|
||||
#include "common/clipboard/Clipboard.h"
|
||||
#include <iostream>
|
||||
|
||||
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))
|
||||
|
14
src/common/clipboard/Clipboard.h
Normal file
14
src/common/clipboard/Clipboard.h
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include "common/String.h"
|
||||
|
||||
class GameSave;
|
||||
|
||||
namespace Clipboard
|
||||
{
|
||||
const ByteString clipboardFormatName = "application/vnd.powdertoy.save";
|
||||
void SetClipboardData(std::unique_ptr<GameSave> data);
|
||||
const GameSave *GetClipboardData();
|
||||
void Init();
|
||||
void RecreateWindow();
|
||||
}
|
10
src/common/clipboard/ClipboardImpls.template.h
Normal file
10
src/common/clipboard/ClipboardImpls.template.h
Normal file
@ -0,0 +1,10 @@
|
||||
#ifdef CLIPBOARD_IMPLS_DECLARE
|
||||
# define IMPL_DEFINE(subsystem, factory) std::unique_ptr<ClipboardImpl> factory();
|
||||
#endif
|
||||
#ifdef CLIPBOARD_IMPLS_DEFINE
|
||||
# define IMPL_DEFINE(subsystem, factory) { subsystem, factory },
|
||||
#endif
|
||||
|
||||
@impl_defs@
|
||||
|
||||
#undef IMPL_DEFINE
|
71
src/common/clipboard/Cocoa.mm
Normal file
71
src/common/clipboard/Cocoa.mm
Normal file
@ -0,0 +1,71 @@
|
||||
#include "Dynamic.h"
|
||||
#include "Clipboard.h"
|
||||
#include <Cocoa/Cocoa.h>
|
||||
#include <iostream>
|
||||
|
||||
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<char> 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<const char *>([data bytes]);
|
||||
auto size = [data length];
|
||||
gdc.data = std::vector<char>(base, base + size);
|
||||
}
|
||||
return gdc;
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<ClipboardImpl> CocoaClipboardFactory()
|
||||
{
|
||||
return std::make_unique<CocoaClipboardImpl>();
|
||||
}
|
||||
}
|
120
src/common/clipboard/Dynamic.cpp
Normal file
120
src/common/clipboard/Dynamic.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
#include "Dynamic.h"
|
||||
#include "client/GameSave.h"
|
||||
#include "prefs/GlobalPrefs.h"
|
||||
#include "PowderToySDL.h"
|
||||
#include <SDL_syswm.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace Clipboard
|
||||
{
|
||||
#define CLIPBOARD_IMPLS_DECLARE
|
||||
#include "ClipboardImpls.h"
|
||||
#undef CLIPBOARD_IMPLS_DECLARE
|
||||
|
||||
struct ClipboardImplEntry
|
||||
{
|
||||
SDL_SYSWM_TYPE subsystem;
|
||||
std::unique_ptr<ClipboardImpl> (*factory)();
|
||||
} clipboardImpls[] = {
|
||||
#define CLIPBOARD_IMPLS_DEFINE
|
||||
#include "ClipboardImpls.h"
|
||||
#undef CLIPBOARD_IMPLS_DEFINE
|
||||
{ SDL_SYSWM_UNKNOWN, nullptr },
|
||||
};
|
||||
|
||||
std::unique_ptr<GameSave> clipboardData;
|
||||
std::unique_ptr<ClipboardImpl> 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<char> &saveData)
|
||||
{
|
||||
std::tie(std::ignore, saveData) = clipboardData->Serialise();
|
||||
}
|
||||
|
||||
void SetClipboardData(std::unique_ptr<GameSave> data)
|
||||
{
|
||||
clipboardData = std::move(data);
|
||||
InvokeClipboardSetClipboardData();
|
||||
}
|
||||
|
||||
void InvokeClipboardGetClipboardData()
|
||||
{
|
||||
if (clipboard)
|
||||
{
|
||||
auto result = clipboard->GetClipboardData();
|
||||
if (std::holds_alternative<ClipboardImpl::GetClipboardDataUnchanged>(result))
|
||||
{
|
||||
std::cerr << "not getting save from clipboard, data unchanged" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (std::holds_alternative<ClipboardImpl::GetClipboardDataUnknown>(result))
|
||||
{
|
||||
return;
|
||||
}
|
||||
clipboardData.reset();
|
||||
auto *data = std::get_if<ClipboardImpl::GetClipboardDataChanged>(&result);
|
||||
if (!data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
clipboardData = std::make_unique<GameSave>(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<bool>("NativeClipboard.Enabled", true))
|
||||
{
|
||||
for (auto *impl = clipboardImpls; impl->factory; ++impl)
|
||||
{
|
||||
if (impl->subsystem == currentSubsystem)
|
||||
{
|
||||
clipboard = impl->factory();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
InvokeClipboardSetClipboardData();
|
||||
}
|
||||
}
|
45
src/common/clipboard/Dynamic.h
Normal file
45
src/common/clipboard/Dynamic.h
Normal file
@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include "common/String.h"
|
||||
#include <vector>
|
||||
#include <variant>
|
||||
#include <memory>
|
||||
|
||||
class GameSave;
|
||||
|
||||
namespace Clipboard
|
||||
{
|
||||
class ClipboardImpl
|
||||
{
|
||||
public:
|
||||
virtual ~ClipboardImpl() = default;
|
||||
|
||||
virtual void SetClipboardData() = 0;
|
||||
|
||||
struct GetClipboardDataUnchanged
|
||||
{
|
||||
};
|
||||
struct GetClipboardDataChanged
|
||||
{
|
||||
std::vector<char> data;
|
||||
};
|
||||
struct GetClipboardDataFailed
|
||||
{
|
||||
};
|
||||
struct GetClipboardDataUnknown
|
||||
{
|
||||
};
|
||||
using GetClipboardDataResult = std::variant<
|
||||
GetClipboardDataUnchanged,
|
||||
GetClipboardDataChanged,
|
||||
GetClipboardDataFailed,
|
||||
GetClipboardDataUnknown
|
||||
>;
|
||||
virtual GetClipboardDataResult GetClipboardData() = 0;
|
||||
};
|
||||
|
||||
extern std::unique_ptr<GameSave> clipboardData;
|
||||
|
||||
void SerializeClipboard(std::vector<char> &saveData);
|
||||
|
||||
extern int currentSubsystem;
|
||||
}
|
232
src/common/clipboard/External.cpp
Normal file
232
src/common/clipboard/External.cpp
Normal file
@ -0,0 +1,232 @@
|
||||
#include "Dynamic.h"
|
||||
#include "Clipboard.h"
|
||||
#include "client/GameSave.h"
|
||||
#include "prefs/GlobalPrefs.h"
|
||||
#include "PowderToySDL.h"
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
#include <optional>
|
||||
#include <algorithm>
|
||||
#include <signal.h>
|
||||
#include <map>
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
namespace Clipboard
|
||||
{
|
||||
struct Preset
|
||||
{
|
||||
ByteString inCommand;
|
||||
ByteString formatsCommand;
|
||||
ByteString outCommand;
|
||||
std::optional<int> defaultForSubsystem;
|
||||
};
|
||||
std::map<ByteString, Preset> 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<Preset> GetPreset()
|
||||
{
|
||||
std::optional<ByteString> name = GlobalPrefs::Ref().Get("NativeClipboard.External.Type", ByteString("auto"));
|
||||
if (name == "custom")
|
||||
{
|
||||
auto getCommand = [](ByteString key) -> std::optional<ByteString> {
|
||||
auto fullKey = "NativeClipboard.External." + key;
|
||||
auto value = GlobalPrefs::Ref().Get<ByteString>(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<char> 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<std::vector<char>> {
|
||||
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<char> 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<ClipboardImpl> ExternalClipboardFactory()
|
||||
{
|
||||
return std::make_unique<ExternalClipboardImpl>();
|
||||
}
|
||||
}
|
22
src/common/clipboard/Null.cpp
Normal file
22
src/common/clipboard/Null.cpp
Normal file
@ -0,0 +1,22 @@
|
||||
#include "Clipboard.h"
|
||||
#include "client/GameSave.h"
|
||||
|
||||
namespace Clipboard
|
||||
{
|
||||
void SetClipboardData(std::unique_ptr<GameSave> data)
|
||||
{
|
||||
}
|
||||
|
||||
const GameSave *GetClipboardData()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
}
|
||||
|
||||
void RecreateWindow()
|
||||
{
|
||||
}
|
||||
}
|
226
src/common/clipboard/Windows.cpp
Normal file
226
src/common/clipboard/Windows.cpp
Normal file
@ -0,0 +1,226 @@
|
||||
#include "Dynamic.h"
|
||||
#include "Clipboard.h"
|
||||
#include "client/GameSave.h"
|
||||
#include "common/platform/Platform.h"
|
||||
#include "PowderToySDL.h"
|
||||
#include <SDL.h>
|
||||
#include <SDL_syswm.h>
|
||||
#include <iostream>
|
||||
#include <windows.h>
|
||||
|
||||
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<char> saveData;
|
||||
SerializeClipboard(saveData);
|
||||
auto handle = std::unique_ptr<void, decltype(&::GlobalFree)>(::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<void, decltype(&::GlobalUnlock)>(::GlobalLock(handle.get()), ::GlobalUnlock);
|
||||
auto base = reinterpret_cast<char *>(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<WindowsClipboardImpl *>(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<void, decltype(&::GlobalUnlock)>(::GlobalLock(handle), ::GlobalUnlock);
|
||||
if (!data)
|
||||
{
|
||||
std::cerr << "cannot get save from clipboard: GlobalLock failed: " << ::GetLastError() << std::endl;
|
||||
return GetClipboardDataFailed{};
|
||||
}
|
||||
auto base = reinterpret_cast<const char *>(data.get());
|
||||
return GetClipboardDataChanged{ std::vector<char>(base, base + size) };
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<ClipboardImpl> WindowsClipboardFactory()
|
||||
{
|
||||
return std::make_unique<WindowsClipboardImpl>();
|
||||
}
|
||||
}
|
39
src/common/clipboard/meson.build
Normal file
39
src/common/clipboard/meson.build
Normal file
@ -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')
|
@ -4,4 +4,5 @@ common_files += files(
|
||||
'tpt-thread-local.cpp',
|
||||
)
|
||||
|
||||
subdir('clipboard')
|
||||
subdir('platform')
|
||||
|
@ -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<int> transform, Vec2<int> nudge)
|
||||
|
||||
void GameModel::SetClipboard(std::unique_ptr<GameSave> 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
|
||||
|
@ -50,9 +50,6 @@ class GameModel
|
||||
|
||||
private:
|
||||
std::vector<Notification*> notifications;
|
||||
//int clipboardSize;
|
||||
//unsigned char * clipboardData;
|
||||
std::unique_ptr<GameSave> clipboard;
|
||||
std::unique_ptr<GameSave> placeSave;
|
||||
std::unique_ptr<GameSave> transformedPlaceSave;
|
||||
std::deque<String> consoleLog;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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() + ');'
|
||||
|
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "common/String.h"
|
||||
#include <json/json.h>
|
||||
#include <optional>
|
||||
|
||||
class Prefs
|
||||
{
|
||||
@ -35,7 +36,7 @@ public:
|
||||
Prefs(ByteString path);
|
||||
|
||||
template<class Type>
|
||||
Type Get(ByteString path, Type defaultValue) const
|
||||
std::optional<Type> Get(ByteString path) const
|
||||
{
|
||||
auto value = GetJson(root, path);
|
||||
if (value != Json::nullValue)
|
||||
@ -48,6 +49,17 @@ public:
|
||||
{
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<class Type>
|
||||
Type Get(ByteString path, Type defaultValue) const
|
||||
{
|
||||
auto value = Get<Type>(path);
|
||||
if (value)
|
||||
{
|
||||
return *value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user