Implement native Windows KSwitcher with configurable hotkeys and clipboard history
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
#include "richtext.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <richedit.h>
|
||||
#include <string_view>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string escape(const std::wstring &text)
|
||||
{
|
||||
std::string out;
|
||||
for (wchar_t c : text)
|
||||
{
|
||||
if (c == '\r')
|
||||
continue;
|
||||
if (c == '\n')
|
||||
out += "\\line ";
|
||||
else if (c == '\t')
|
||||
out += "\\tab ";
|
||||
else if (c == '\\' || c == '{' || c == '}')
|
||||
{
|
||||
out += '\\';
|
||||
out += char(c);
|
||||
}
|
||||
else if (c >= 32 && c < 127)
|
||||
out += char(c);
|
||||
else
|
||||
out += "\\u" + std::to_string((int)(short)c) + "?";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const std::string header = "{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Segoe UI;}}\\f0\\fs21\\uc1 ";
|
||||
std::wstring entities(std::string_view input)
|
||||
{
|
||||
std::wstring out;
|
||||
for (size_t i = 0; i < input.size();)
|
||||
{
|
||||
if (input[i] != '&')
|
||||
{
|
||||
auto end = input.find('&', i);
|
||||
if (end == input.npos)
|
||||
end = input.size();
|
||||
out += wide(std::string(input.substr(i, end - i)));
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
auto end = input.find(';', i + 1);
|
||||
if (end != input.npos && end - i < 14)
|
||||
{
|
||||
auto name = std::string(input.substr(i + 1, end - i - 1));
|
||||
uint32_t c = 0;
|
||||
if (name == "amp")
|
||||
c = '&';
|
||||
else if (name == "lt")
|
||||
c = '<';
|
||||
else if (name == "gt")
|
||||
c = '>';
|
||||
else if (name == "quot")
|
||||
c = '"';
|
||||
else if (name == "apos")
|
||||
c = '\'';
|
||||
else if (name == "nbsp")
|
||||
c = 160;
|
||||
else if (name.starts_with('#'))
|
||||
try
|
||||
{
|
||||
size_t used{};
|
||||
bool hex = name.size() > 1 && (name[1] == 'x' || name[1] == 'X');
|
||||
auto digits = name.substr(hex ? 2 : 1);
|
||||
auto n = std::stoul(digits, &used, hex ? 16 : 10);
|
||||
if (used == digits.size() && n <= 0x10ffff && !(n >= 0xd800 && n <= 0xdfff))
|
||||
c = (uint32_t)n;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (c)
|
||||
{
|
||||
if (c > 0xffff)
|
||||
{
|
||||
c -= 0x10000;
|
||||
out += wchar_t(0xd800 + (c >> 10));
|
||||
out += wchar_t(0xdc00 + (c & 1023));
|
||||
}
|
||||
else
|
||||
out += wchar_t(c);
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out += L'&';
|
||||
++i;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
std::string htmlRtf(std::string html)
|
||||
{
|
||||
auto start = html.find("<!--StartFragment-->");
|
||||
auto end = html.find("<!--EndFragment-->");
|
||||
if (start != html.npos && end != html.npos && end >= start + 20)
|
||||
html = html.substr(start + 20, end - start - 20);
|
||||
else
|
||||
{
|
||||
auto tag = html.find('<');
|
||||
if (tag == html.npos)
|
||||
return {};
|
||||
html.erase(0, tag);
|
||||
}
|
||||
std::string out = header;
|
||||
std::vector<std::string> stack;
|
||||
std::string skip;
|
||||
size_t chars = 0;
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < html.size() && chars < 4096 && out.size() < 55000;)
|
||||
{
|
||||
if (html[i] != '<')
|
||||
{
|
||||
auto next = html.find('<', i);
|
||||
if (next == html.npos)
|
||||
next = html.size();
|
||||
if (skip.empty())
|
||||
{
|
||||
auto text = entities(std::string_view(html).substr(i, next - i));
|
||||
text.resize(std::min(text.size(), 4096 - chars));
|
||||
chars += text.size();
|
||||
out += escape(text);
|
||||
any |= !text.empty();
|
||||
}
|
||||
i = next;
|
||||
continue;
|
||||
}
|
||||
if (html.compare(i, 4, "<!--") == 0)
|
||||
{
|
||||
auto e = html.find("-->", i + 4);
|
||||
if (e == html.npos)
|
||||
break;
|
||||
i = e + 3;
|
||||
continue;
|
||||
}
|
||||
size_t e = i + 1;
|
||||
char quote = 0;
|
||||
for (; e < html.size(); ++e)
|
||||
{
|
||||
char c = html[e];
|
||||
if (quote)
|
||||
{
|
||||
if (c == quote)
|
||||
quote = 0;
|
||||
}
|
||||
else if (c == '\'' || c == '"')
|
||||
quote = c;
|
||||
else if (c == '>')
|
||||
break;
|
||||
}
|
||||
if (e == html.size())
|
||||
break;
|
||||
auto tag = html.substr(i + 1, e - i - 1);
|
||||
i = e + 1;
|
||||
std::transform(tag.begin(), tag.end(), tag.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
size_t p = 0;
|
||||
while (p < tag.size() && isspace((unsigned char)tag[p]))
|
||||
++p;
|
||||
bool close = p < tag.size() && tag[p] == '/';
|
||||
if (close)
|
||||
++p;
|
||||
size_t begin = p;
|
||||
while (p < tag.size() && (isalnum((unsigned char)tag[p]) || tag[p] == '-'))
|
||||
++p;
|
||||
auto name = tag.substr(begin, p - begin);
|
||||
if (!skip.empty())
|
||||
{
|
||||
if (close && name == skip)
|
||||
skip.clear();
|
||||
continue;
|
||||
}
|
||||
if (name == "script" || name == "style" || name == "head")
|
||||
{
|
||||
if (!close)
|
||||
skip = name;
|
||||
continue;
|
||||
}
|
||||
if (name.empty())
|
||||
continue;
|
||||
bool block = name == "p" || name == "div" || name == "li" || name == "tr" || name == "blockquote";
|
||||
if (close)
|
||||
{
|
||||
auto it = std::find(stack.rbegin(), stack.rend(), name);
|
||||
if (it != stack.rend())
|
||||
{
|
||||
size_t n = std::distance(stack.rbegin(), it) + 1;
|
||||
out.append(n, '}');
|
||||
stack.resize(stack.size() - n);
|
||||
}
|
||||
if (block && any)
|
||||
out += "\\line ";
|
||||
continue;
|
||||
}
|
||||
if (name == "br" || name == "hr")
|
||||
{
|
||||
out += "\\line ";
|
||||
continue;
|
||||
}
|
||||
if (name == "img" || name == "meta" || name == "link" || name == "input" || tag.ends_with('/'))
|
||||
continue;
|
||||
if (stack.size() >= 128)
|
||||
return {};
|
||||
if (block && any && !out.ends_with("\\line "))
|
||||
out += "\\line ";
|
||||
out += '{';
|
||||
stack.push_back(name);
|
||||
std::string compact = tag;
|
||||
std::erase_if(compact, [](unsigned char c) { return isspace(c); });
|
||||
if (name == "b" || name == "strong" || compact.find("font-weight:bold") != compact.npos ||
|
||||
compact.find("font-weight:700") != compact.npos ||
|
||||
compact.find("font-weight:600") != compact.npos)
|
||||
out += "\\b ";
|
||||
if (name == "i" || name == "em" || compact.find("font-style:italic") != compact.npos)
|
||||
out += "\\i ";
|
||||
if (name == "u" || compact.find("text-decoration:underline") != compact.npos ||
|
||||
compact.find("text-decoration-line:underline") != compact.npos)
|
||||
out += "\\ul ";
|
||||
if (name == "s" || name == "strike" || name == "del" || compact.find("line-through") != compact.npos)
|
||||
out += "\\strike ";
|
||||
if (compact.find("font-weight:normal") != compact.npos ||
|
||||
compact.find("font-weight:400") != compact.npos)
|
||||
out += "\\b0 ";
|
||||
if (compact.find("font-style:normal") != compact.npos)
|
||||
out += "\\i0 ";
|
||||
if (name == "li")
|
||||
out += "\\bullet\\tab ";
|
||||
}
|
||||
if (!any)
|
||||
return {};
|
||||
out.append(stack.size(), '}');
|
||||
out += '}';
|
||||
return out;
|
||||
}
|
||||
struct Stream
|
||||
{
|
||||
const std::string &text;
|
||||
size_t at{};
|
||||
};
|
||||
DWORD CALLBACK readStream(DWORD_PTR cookie, LPBYTE buffer, LONG size, LONG *read)
|
||||
{
|
||||
auto &s = *(Stream *)cookie;
|
||||
*read = (LONG)std::min(size_t(size), s.text.size() - s.at);
|
||||
memcpy(buffer, s.text.data() + s.at, *read);
|
||||
s.at += *read;
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
std::string richPreview(const Clip &clip)
|
||||
{
|
||||
for (const auto &f : clip.formats)
|
||||
if (f.name == L"Rich Text Format" && f.data.size() <= 65536)
|
||||
{
|
||||
std::string rtf(f.data.begin(), f.data.end());
|
||||
while (!rtf.empty() && rtf.back() == 0)
|
||||
rtf.pop_back();
|
||||
if (rtf.starts_with("{\\rtf"))
|
||||
return rtf;
|
||||
}
|
||||
for (const auto &f : clip.formats)
|
||||
if (f.name == L"HTML Format" && f.data.size() <= 1024 * 1024)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto rtf = htmlRtf(std::string(f.data.begin(), f.data.end()));
|
||||
if (!rtf.empty())
|
||||
return rtf;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
auto text = clip.text();
|
||||
if (text.size() > 4096)
|
||||
text.resize(4096);
|
||||
return header + escape(text) + '}';
|
||||
}
|
||||
HWND createRichText(HWND parent, int id, bool visible)
|
||||
{
|
||||
static HMODULE rich = LoadLibraryExW(L"Msftedit.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||
if (!rich)
|
||||
return nullptr;
|
||||
return CreateWindowExW(0, L"RICHEDIT50W", L"",
|
||||
WS_CHILD | ES_MULTILINE | ES_READONLY |
|
||||
(visible ? (WS_VISIBLE | WS_VSCROLL | ES_AUTOVSCROLL) : 0),
|
||||
0, 0, 1, 1, parent, (HMENU)(INT_PTR)id, GetModuleHandleW(nullptr), nullptr);
|
||||
}
|
||||
bool setRichText(HWND window, const std::string &text)
|
||||
{
|
||||
if (!window)
|
||||
return false;
|
||||
Stream data{text};
|
||||
EDITSTREAM stream{(DWORD_PTR)&data, 0, readStream};
|
||||
SendMessageW(window, EM_STREAMIN, SF_RTF, (LPARAM)&stream);
|
||||
SendMessageW(window, EM_EMPTYUNDOBUFFER, 0, 0);
|
||||
return stream.dwError == 0;
|
||||
}
|
||||
} // namespace ks
|
||||
Reference in New Issue
Block a user