1999 lines
54 KiB
C++
1999 lines
54 KiB
C++
/*
|
|
* xW.cpp: Win32 frontend for xC
|
|
*
|
|
* Copyright (c) 2023 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
#include "xC-proto.cpp"
|
|
#include "xW-resources.h"
|
|
#include "config.h"
|
|
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#include <richedit.h>
|
|
#undef ERROR
|
|
#undef REGISTERED
|
|
|
|
#include <algorithm>
|
|
#include <clocale>
|
|
#include <ctime>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <string>
|
|
|
|
struct Server {
|
|
Relay::ServerState state = {};
|
|
std::wstring user;
|
|
std::wstring user_modes;
|
|
};
|
|
|
|
struct BufferLineItem {
|
|
CHARFORMAT2 format = {};
|
|
std::wstring text;
|
|
};
|
|
|
|
struct BufferLine {
|
|
/// Leaked from another buffer, but temporarily staying in another one.
|
|
bool leaked = {};
|
|
|
|
bool is_unimportant = {};
|
|
bool is_highlight = {};
|
|
Relay::Rendition rendition = {};
|
|
uint64_t when = {};
|
|
std::vector<BufferLineItem> items;
|
|
};
|
|
|
|
struct Buffer {
|
|
std::wstring buffer_name;
|
|
bool hide_unimportant = {};
|
|
Relay::BufferKind kind = {};
|
|
std::wstring server_name;
|
|
std::vector<BufferLine> lines;
|
|
|
|
// Channel:
|
|
|
|
std::vector<BufferLineItem> topic;
|
|
std::wstring modes;
|
|
|
|
// Stats:
|
|
|
|
uint32_t new_messages = {};
|
|
uint32_t new_unimportant_messages = {};
|
|
bool highlighted = {};
|
|
|
|
// Input:
|
|
|
|
std::wstring input;
|
|
DWORD input_start = {};
|
|
DWORD input_end = {};
|
|
std::vector<std::wstring> history;
|
|
size_t history_at = {};
|
|
};
|
|
|
|
using Callback = std::function<
|
|
void(std::wstring error, const Relay::ResponseData *response)>;
|
|
|
|
struct {
|
|
HWND hwndMain; ///< Main program window
|
|
HWND hwndTopic; ///< static: channel topic
|
|
HWND hwndBufferList; ///< listbox: buffer list
|
|
HWND hwndBuffer; ///< richedit: buffer backlog
|
|
HWND hwndBufferLog; ///< edit: buffer log
|
|
HWND hwndPrompt; ///< static: user name, etc.
|
|
HWND hwndStatus; ///< static: buffer name, etc.
|
|
HWND hwndInput; ///< edit: user input
|
|
|
|
HWND hwndLastFocused; ///< For Alt+Tab, e.g.
|
|
HANDLE date_change_timer; ///< Waitable timer for day changes
|
|
|
|
HICON hicon; ///< Normal program icon
|
|
HICON hiconHighlighted; ///< Highlighted program icon
|
|
|
|
HFONT hfont; ///< Normal variant of the UI font
|
|
HFONT hfontBold; ///< Bold variant of the UI font
|
|
|
|
LOGFONT fontlog; ///< UI font characteristics
|
|
LONG font_height; ///< UI font height in pixels
|
|
|
|
// Networking:
|
|
|
|
std::wstring host; ///< Host as given by user
|
|
std::wstring port; ///< Port/service as given by user
|
|
|
|
addrinfoW *addresses; ///< GetAddrInfo() result
|
|
addrinfoW *addresses_iterator; ///< Currently processed address
|
|
SOCKET socket; ///< Relay socket
|
|
WSAEVENT socket_event; ///< Relay socket event
|
|
HANDLE flush_event; ///< Write buffer has new data
|
|
std::vector<uint8_t> write_buffer; ///< Write buffer
|
|
std::vector<uint8_t> read_buffer; ///< Read buffer
|
|
|
|
// Relay protocol:
|
|
|
|
uint32_t command_seq; ///< Outgoing message counter
|
|
|
|
std::map<uint32_t, Callback> command_callbacks;
|
|
|
|
std::vector<Buffer> buffers; ///< List of all buffers
|
|
std::wstring buffer_current; ///< Current buffer name or ""
|
|
std::wstring buffer_last; ///< Previous buffer name or ""
|
|
|
|
std::map<std::wstring, Server> servers;
|
|
} g;
|
|
|
|
static void
|
|
show_error_message(const wchar_t *message)
|
|
{
|
|
MessageBox(g.hwndMain, message, NULL, MB_ICONERROR | MB_OK | MB_APPLMODAL);
|
|
}
|
|
|
|
static std::wstring
|
|
format_error_message(int err)
|
|
{
|
|
wchar_t *message = NULL;
|
|
if (!FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
|
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
NULL, err, 0, (LPWSTR) &message, 0, NULL))
|
|
return std::to_wstring(err);
|
|
|
|
std::wstring copy = message;
|
|
LocalFree(message);
|
|
return copy;
|
|
}
|
|
|
|
static std::wstring
|
|
window_get_text(HWND hWnd)
|
|
{
|
|
int length = GetWindowTextLength(hWnd);
|
|
std::wstring buffer(length, {});
|
|
GetWindowText(hWnd, buffer.data(), length + 1);
|
|
return buffer;
|
|
}
|
|
|
|
static void
|
|
beep()
|
|
{
|
|
if (!PlaySound(MAKEINTRESOURCE(IDR_BEEP),
|
|
GetModuleHandle(NULL), SND_ASYNC | SND_RESOURCE))
|
|
Beep(800, 100);
|
|
}
|
|
|
|
// --- Networking --------------------------------------------------------------
|
|
|
|
static bool
|
|
relay_try_read(std::wstring &error)
|
|
{
|
|
auto &r = g.read_buffer;
|
|
char buffer[8192] = {};
|
|
int err = {};
|
|
while (true) {
|
|
int n = recv(g.socket, buffer, sizeof buffer, 0);
|
|
if (!n) {
|
|
error = L"Server closed the connection.";
|
|
return false;
|
|
} else if (n != SOCKET_ERROR) {
|
|
r.insert(r.end(), buffer, buffer + n);
|
|
} else if ((err = WSAGetLastError()) != WSAEWOULDBLOCK) {
|
|
error = format_error_message(err);
|
|
return false;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
relay_try_write(std::wstring &error)
|
|
{
|
|
ResetEvent(g.flush_event);
|
|
|
|
auto &w = g.write_buffer;
|
|
int err = {};
|
|
while (!w.empty()) {
|
|
int n = send(g.socket,
|
|
reinterpret_cast<const char *>(w.data()), w.size(), 0);
|
|
if (n != SOCKET_ERROR) {
|
|
w.erase(w.begin(), w.begin() + n);
|
|
} else if ((err = WSAGetLastError()) != WSAEWOULDBLOCK) {
|
|
error = format_error_message(err);
|
|
return false;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
relay_send(Relay::CommandData *data, Callback callback = {})
|
|
{
|
|
Relay::CommandMessage m = {};
|
|
m.command_seq = ++g.command_seq;
|
|
m.data.reset(data);
|
|
LibertyXDR::Writer w;
|
|
m.serialize(w);
|
|
|
|
if (callback)
|
|
g.command_callbacks[m.command_seq] = std::move(callback);
|
|
|
|
uint32_t len = htonl(w.data.size());
|
|
uint8_t *prefix = reinterpret_cast<uint8_t *>(&len);
|
|
g.write_buffer.insert(g.write_buffer.end(), prefix, prefix + sizeof len);
|
|
g.write_buffer.insert(g.write_buffer.end(), w.data.begin(), w.data.end());
|
|
|
|
// There doesn't seem to be a way to cause FD_WRITE without first
|
|
// unsuccessfully trying to send some data, but we don't want to
|
|
// handle any errors at this level.
|
|
SetEvent(g.flush_event);
|
|
}
|
|
|
|
// --- Buffers -----------------------------------------------------------------
|
|
|
|
static Buffer *
|
|
buffer_by_name(const std::wstring &name)
|
|
{
|
|
for (auto &b : g.buffers)
|
|
if (b.buffer_name == name)
|
|
return &b;
|
|
return nullptr;
|
|
}
|
|
|
|
static void
|
|
buffer_activate(const std::wstring &name)
|
|
{
|
|
auto activate = new Relay::CommandData_BufferActivate();
|
|
activate->buffer_name = name;
|
|
relay_send(activate);
|
|
}
|
|
|
|
static void
|
|
buffer_toggle_unimportant(const std::wstring &name)
|
|
{
|
|
auto toggle = new Relay::CommandData_BufferToggleUnimportant();
|
|
toggle->buffer_name = name;
|
|
relay_send(toggle);
|
|
}
|
|
|
|
// --- Current buffer ----------------------------------------------------------
|
|
|
|
static void
|
|
buffer_toggle_log(
|
|
const std::wstring &error, const Relay::ResponseData_BufferLog *response)
|
|
{
|
|
if (!response) {
|
|
show_error_message(error.c_str());
|
|
return;
|
|
}
|
|
|
|
std::wstring log;
|
|
if (!LibertyXDR::utf8_to_wstring(
|
|
response->log.data(), response->log.size(), log)) {
|
|
show_error_message(L"Invalid encoding.");
|
|
return;
|
|
}
|
|
|
|
std::wstring filtered;
|
|
for (auto wch : log) {
|
|
if (wch == L'\n')
|
|
filtered += L"\r\n";
|
|
else
|
|
filtered += wch;
|
|
}
|
|
|
|
SetWindowText(g.hwndBufferLog, filtered.c_str());
|
|
ShowWindow(g.hwndBuffer, SW_HIDE);
|
|
ShowWindow(g.hwndBufferLog, SW_SHOW);
|
|
}
|
|
|
|
static void
|
|
buffer_toggle_log()
|
|
{
|
|
if (IsWindowVisible(g.hwndBufferLog)) {
|
|
ShowWindow(g.hwndBufferLog, SW_HIDE);
|
|
ShowWindow(g.hwndBuffer, SW_SHOW);
|
|
SetWindowText(g.hwndBufferLog, L"");
|
|
return;
|
|
}
|
|
|
|
auto log = new Relay::CommandData_BufferLog();
|
|
log->buffer_name = g.buffer_current;
|
|
relay_send(log, [name = g.buffer_current](auto error, auto response) {
|
|
if (g.buffer_current != name)
|
|
return;
|
|
buffer_toggle_log(error,
|
|
dynamic_cast<const Relay::ResponseData_BufferLog *>(response));
|
|
});
|
|
}
|
|
|
|
static bool
|
|
buffer_at_bottom()
|
|
{
|
|
// It is created with this style, and should retain it indefinitely,
|
|
// however this check works. It is necessary because when richedit
|
|
// hides its scrollbar, it does not bother resetting its values.
|
|
if (!(GetWindowLong(g.hwndBuffer, GWL_STYLE) & WS_VSCROLL))
|
|
return true;
|
|
|
|
SCROLLINFO si = {};
|
|
si.cbSize = sizeof si;
|
|
si.fMask = SIF_ALL;
|
|
GetScrollInfo(g.hwndBuffer, SB_VERT, &si);
|
|
return si.nPos + (int) si.nPage >= si.nMax;
|
|
}
|
|
|
|
static void
|
|
buffer_scroll_to_bottom()
|
|
{
|
|
SendMessage(g.hwndBuffer, EM_SCROLL, SB_BOTTOM, 0);
|
|
}
|
|
|
|
// --- UI state refresh --------------------------------------------------------
|
|
|
|
static void
|
|
refresh_icon()
|
|
{
|
|
HICON icon = g.hicon;
|
|
for (const auto &b : g.buffers)
|
|
if (b.highlighted)
|
|
icon = g.hiconHighlighted;
|
|
|
|
SendMessage(g.hwndMain, WM_SETICON, ICON_SMALL, (LPARAM) icon);
|
|
SendMessage(g.hwndMain, WM_SETICON, ICON_BIG, (LPARAM) icon);
|
|
}
|
|
|
|
static void
|
|
richedit_replacesel(HWND hWnd, const CHARFORMAT2 *cf, const wchar_t *text)
|
|
{
|
|
SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) cf);
|
|
SendMessage(hWnd, EM_REPLACESEL, FALSE, (LPARAM) text);
|
|
}
|
|
|
|
static void
|
|
refresh_topic(const std::vector<BufferLineItem> &topic)
|
|
{
|
|
SetWindowText(g.hwndTopic, L"");
|
|
for (const auto &it : topic)
|
|
richedit_replacesel(g.hwndTopic, &it.format, it.text.c_str());
|
|
}
|
|
|
|
static void
|
|
refresh_buffer_list()
|
|
{
|
|
InvalidateRect(g.hwndBufferList, NULL, TRUE);
|
|
}
|
|
|
|
static std::wstring
|
|
server_state_to_string(Relay::ServerState state)
|
|
{
|
|
switch (state) {
|
|
case Relay::ServerState::DISCONNECTED: return L"disconnected";
|
|
case Relay::ServerState::CONNECTING: return L"connecting";
|
|
case Relay::ServerState::CONNECTED: return L"connected";
|
|
case Relay::ServerState::REGISTERED: return L"registered";
|
|
case Relay::ServerState::DISCONNECTING: return L"disconnecting";
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static void
|
|
refresh_prompt()
|
|
{
|
|
std::wstring prompt;
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
if (!b) {
|
|
prompt = L"Synchronizing...";
|
|
} else if (auto server = g.servers.find(b->server_name);
|
|
server != g.servers.end()) {
|
|
prompt = server->second.user;
|
|
if (!server->second.user_modes.empty())
|
|
prompt += L"(" + server->second.user_modes + L")";
|
|
if (prompt.empty())
|
|
prompt = L"(" + server_state_to_string(server->second.state) + L")";
|
|
}
|
|
SetWindowText(g.hwndPrompt, prompt.c_str());
|
|
}
|
|
|
|
static void
|
|
refresh_status()
|
|
{
|
|
std::wstring status;
|
|
if (!buffer_at_bottom())
|
|
status += L"🡇 ";
|
|
|
|
status += g.buffer_current;
|
|
if (auto b = buffer_by_name(g.buffer_current)) {
|
|
if (!b->modes.empty())
|
|
status += L"(+" + b->modes + L")";
|
|
if (b->hide_unimportant)
|
|
status += L"<H>";
|
|
}
|
|
|
|
// Buffer scrolling would cause a ton of flickering redraws.
|
|
if (window_get_text(g.hwndStatus) != status)
|
|
SetWindowText(g.hwndStatus, status.c_str());
|
|
}
|
|
|
|
// --- Rich Edit formatting ----------------------------------------------------
|
|
|
|
static COLORREF
|
|
convert_color(int16_t color)
|
|
{
|
|
static const uint16_t base16[] = {
|
|
0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
|
|
0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
|
|
};
|
|
if (color < 16) {
|
|
uint8_t r = 0xf & (base16[color] >> 8);
|
|
uint8_t g = 0xf & (base16[color] >> 4);
|
|
uint8_t b = 0xf & (base16[color]);
|
|
return RGB(r * 0x11, g * 0x11, b * 0x11);
|
|
}
|
|
if (color >= 216) {
|
|
uint8_t g = 8 + (color - 216) * 10;
|
|
return RGB(g, g, g);
|
|
}
|
|
|
|
uint8_t i = color - 16, r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6;
|
|
return RGB(
|
|
!r ? 0 : 55 + 40 * r,
|
|
!g ? 0 : 55 + 40 * g,
|
|
!b ? 0 : 55 + 40 * b);
|
|
}
|
|
|
|
static CHARFORMAT2
|
|
default_charformat()
|
|
{
|
|
// Everything we leave out will be kept as it was.
|
|
// So, e.g., there is no way to "unset" a monospace font.
|
|
CHARFORMAT2 reset = {};
|
|
reset.cbSize = sizeof reset;
|
|
reset.dwMask = CFM_BOLD | CFM_ITALIC | CFM_UNDERLINE | CFM_STRIKEOUT |
|
|
CFM_COLOR | CFM_BACKCOLOR | CFM_FACE | CFM_LINK;
|
|
reset.dwEffects = CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR;
|
|
lstrcpyn(reset.szFaceName, g.fontlog.lfFaceName, sizeof reset.szFaceName);
|
|
return reset;
|
|
}
|
|
|
|
static void
|
|
convert_item_formatting(Relay::ItemData *item, CHARFORMAT2 &cf, bool &inverse)
|
|
{
|
|
if (dynamic_cast<Relay::ItemData_Reset *>(item)) {
|
|
cf = default_charformat();
|
|
inverse = false;
|
|
} else if (dynamic_cast<Relay::ItemData_FlipBold *>(item)) {
|
|
cf.dwEffects ^= CFE_BOLD;
|
|
} else if (dynamic_cast<Relay::ItemData_FlipItalic *>(item)) {
|
|
cf.dwEffects ^= CFE_ITALIC;
|
|
} else if (dynamic_cast<Relay::ItemData_FlipUnderline *>(item)) {
|
|
cf.dwEffects ^= CFE_UNDERLINE;
|
|
} else if (dynamic_cast<Relay::ItemData_FlipCrossedOut *>(item)) {
|
|
cf.dwEffects ^= CFE_STRIKEOUT;
|
|
} else if (dynamic_cast<Relay::ItemData_FlipInverse *>(item)) {
|
|
inverse = !inverse;
|
|
} else if (dynamic_cast<Relay::ItemData_FlipMonospace *>(item)) {
|
|
auto reset = default_charformat();
|
|
const auto face = !lstrcmp(cf.szFaceName, reset.szFaceName)
|
|
? L"Courier New"
|
|
: reset.szFaceName;
|
|
lstrcpyn(cf.szFaceName, face, sizeof cf.szFaceName);
|
|
} else if (auto data = dynamic_cast<Relay::ItemData_FgColor *>(item)) {
|
|
if (data->color < 0) {
|
|
cf.dwEffects |= CFE_AUTOCOLOR;
|
|
} else {
|
|
cf.dwEffects &= ~CFE_AUTOCOLOR;
|
|
cf.crTextColor = convert_color(data->color);
|
|
}
|
|
} else if (auto data = dynamic_cast<Relay::ItemData_BgColor *>(item)) {
|
|
if (data->color < 0) {
|
|
cf.dwEffects |= CFE_AUTOBACKCOLOR;
|
|
} else {
|
|
cf.dwEffects &= ~CFE_AUTOBACKCOLOR;
|
|
cf.crBackColor = convert_color(data->color);
|
|
}
|
|
}
|
|
}
|
|
|
|
static std::vector<BufferLineItem>
|
|
convert_items(const std::vector<std::unique_ptr<Relay::ItemData>> &items)
|
|
{
|
|
CHARFORMAT2 cf = default_charformat();
|
|
std::vector<BufferLineItem> result;
|
|
bool inverse = false;
|
|
for (const auto &it : items) {
|
|
auto text = dynamic_cast<Relay::ItemData_Text *>(it.get());
|
|
if (!text) {
|
|
convert_item_formatting(it.get(), cf, inverse);
|
|
continue;
|
|
}
|
|
|
|
BufferLineItem item = {};
|
|
item.format = cf;
|
|
item.text = text->text;
|
|
if (inverse) {
|
|
std::swap(item.format.crTextColor, item.format.crBackColor);
|
|
item.format.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR);
|
|
if (cf.dwEffects & CFE_AUTOCOLOR)
|
|
item.format.crBackColor = GetSysColor(COLOR_WINDOWTEXT);
|
|
if (cf.dwEffects & CFE_AUTOBACKCOLOR)
|
|
item.format.crTextColor = GetSysColor(COLOR_WINDOW);
|
|
}
|
|
result.push_back(std::move(item));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// --- Buffer output -----------------------------------------------------------
|
|
|
|
static BufferLine
|
|
convert_buffer_line(Relay::EventData_BufferLine &line)
|
|
{
|
|
BufferLine self = {};
|
|
self.items = convert_items(line.items);
|
|
self.is_unimportant = line.is_unimportant;
|
|
self.is_highlight = line.is_highlight;
|
|
self.rendition = line.rendition;
|
|
self.when = line.when;
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
buffer_print_date_change(bool &sameline, const tm &last, const tm ¤t)
|
|
{
|
|
if (last.tm_year == current.tm_year &&
|
|
last.tm_mon == current.tm_mon &&
|
|
last.tm_mday == current.tm_mday)
|
|
return;
|
|
|
|
wchar_t buffer[64] = {};
|
|
wcsftime(buffer, sizeof buffer, &L"\n%x"[sameline], ¤t);
|
|
sameline = false;
|
|
|
|
CHARFORMAT2 cf = default_charformat();
|
|
cf.dwEffects |= CFE_BOLD;
|
|
richedit_replacesel(g.hwndBuffer, &cf, buffer);
|
|
}
|
|
|
|
static LONG
|
|
buffer_reset_selection()
|
|
{
|
|
CHARRANGE cr = {};
|
|
cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer);
|
|
SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr);
|
|
return cr.cpMin;
|
|
}
|
|
|
|
static struct tm
|
|
buffer_localtime(time_t time)
|
|
{
|
|
// This isn't critical, so let it fail quietly.
|
|
struct tm result = {};
|
|
(void) localtime_s(&result, &time);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
buffer_print_and_watch_trailing_date_changes()
|
|
{
|
|
time_t current_unix = time(NULL);
|
|
tm current = buffer_localtime(current_unix);
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
if (b && !b->lines.empty()) {
|
|
tm last = buffer_localtime(b->lines.back().when / 1000);
|
|
bool sameline = !buffer_reset_selection();
|
|
buffer_print_date_change(sameline, last, current);
|
|
}
|
|
|
|
current.tm_sec = current.tm_min = current.tm_hour = 0;
|
|
current.tm_mday++;
|
|
current.tm_isdst = -1;
|
|
const time_t midnight = mktime(¤t);
|
|
if (midnight == (time_t) -1 || midnight < current_unix)
|
|
return;
|
|
|
|
// Note that after printing the first trailing update,
|
|
// follow-up updates may be duplicated if timer events arrive too early.
|
|
LARGE_INTEGER li = {};
|
|
li.QuadPart = (midnight - current_unix + 1) * -10000000LL;
|
|
SetWaitableTimer(g.date_change_timer, &li, 0, NULL, NULL, FALSE);
|
|
}
|
|
|
|
static void
|
|
buffer_print_line(std::vector<BufferLine>::const_iterator begin,
|
|
std::vector<BufferLine>::const_iterator line)
|
|
{
|
|
tm current = buffer_localtime(line->when / 1000);
|
|
tm last = buffer_localtime(
|
|
line == begin ? time(NULL) : (line - 1)->when / 1000);
|
|
|
|
// The Rich Edit control makes the window cursor transparent
|
|
// each time you add an independent newline character. Avoid that.
|
|
// (Sadly, this also makes Windows 7 end lines with a bogus space that
|
|
// has the CHARFORMAT2 of what we flush that newline together with.)
|
|
bool sameline = !buffer_reset_selection();
|
|
buffer_print_date_change(sameline, last, current);
|
|
|
|
wchar_t buffer[64] = {};
|
|
wcsftime(buffer, sizeof buffer, &L"\n%H:%M:%S"[sameline], ¤t);
|
|
|
|
CHARFORMAT2 cf = default_charformat();
|
|
cf.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR);
|
|
cf.crTextColor = RGB(0xbb, 0xbb, 0xbb);
|
|
cf.crBackColor = RGB(0xf8, 0xf8, 0xf8);
|
|
richedit_replacesel(g.hwndBuffer, &cf, buffer);
|
|
cf = default_charformat();
|
|
richedit_replacesel(g.hwndBuffer, &cf, L" ");
|
|
|
|
// Tabstops won't quite help us here, since we need it centred.
|
|
std::wstring prefix;
|
|
CHARFORMAT2 pcf = default_charformat();
|
|
lstrcpyn(pcf.szFaceName, L"Courier New", sizeof pcf.szFaceName);
|
|
// This looks better, but it may trigger a repaint bug in richedit.
|
|
#if 1
|
|
pcf.dwEffects |= CFE_BOLD;
|
|
#endif
|
|
switch (line->rendition) {
|
|
break; case Relay::Rendition::BARE:
|
|
break; case Relay::Rendition::INDENT:
|
|
prefix = L" ";
|
|
break; case Relay::Rendition::STATUS:
|
|
prefix = L" - ";
|
|
break; case Relay::Rendition::ERROR:
|
|
prefix = L"=!= ";
|
|
pcf.dwEffects &= ~CFE_AUTOCOLOR;
|
|
pcf.crTextColor = RGB(0xff, 0, 0);
|
|
break; case Relay::Rendition::JOIN:
|
|
prefix = L"——> ";
|
|
pcf.dwEffects &= ~CFE_AUTOCOLOR;
|
|
pcf.crTextColor = RGB(0, 0x88, 0);
|
|
break; case Relay::Rendition::PART:
|
|
prefix = L"<—— ";
|
|
pcf.dwEffects &= ~CFE_AUTOCOLOR;
|
|
pcf.crTextColor = RGB(0x88, 0, 0);
|
|
break; case Relay::Rendition::ACTION:
|
|
prefix = L" * ";
|
|
pcf.dwEffects &= ~CFE_AUTOCOLOR;
|
|
pcf.crTextColor = RGB(0x88, 0, 0);
|
|
}
|
|
|
|
if (line->leaked) {
|
|
pcf.dwEffects &= ~CFE_AUTOCOLOR;
|
|
pcf.crTextColor = GetSysColor(COLOR_GRAYTEXT);
|
|
if (!prefix.empty())
|
|
richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str());
|
|
|
|
for (auto it : line->items) {
|
|
it.format.dwEffects &= ~CFE_AUTOCOLOR;
|
|
it.format.crTextColor = GetSysColor(COLOR_GRAYTEXT);
|
|
it.format.dwEffects |= CFE_AUTOBACKCOLOR;
|
|
richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str());
|
|
}
|
|
} else {
|
|
if (!prefix.empty())
|
|
richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str());
|
|
for (const auto &it : line->items)
|
|
richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str());
|
|
}
|
|
}
|
|
|
|
static void
|
|
buffer_print_separator()
|
|
{
|
|
bool sameline = !buffer_reset_selection();
|
|
|
|
CHARFORMAT2 format = default_charformat();
|
|
format.dwEffects &= ~CFE_AUTOCOLOR;
|
|
format.crTextColor = RGB(0xff, 0x5f, 0x00);
|
|
richedit_replacesel(g.hwndBuffer, &format, &L"\n---"[sameline]);
|
|
}
|
|
|
|
static void
|
|
refresh_buffer(const Buffer &b)
|
|
{
|
|
HCURSOR oldCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));
|
|
SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) FALSE, 0);
|
|
SetWindowText(g.hwndBuffer, L"");
|
|
|
|
// PFM_OFFSET could also be used, but the result isn't very nice.
|
|
//
|
|
// PFM_BORDER is not implemented, at most we can try to construct
|
|
// an OLE object the width of the screen and see how it clips
|
|
// (this is a lot of code).
|
|
size_t i = 0, mark_before = b.lines.size() -
|
|
b.new_messages - b.new_unimportant_messages;
|
|
for (auto line = b.lines.begin(); line != b.lines.end(); ++line) {
|
|
if (i == mark_before)
|
|
buffer_print_separator();
|
|
if (!line->is_unimportant || !b.hide_unimportant)
|
|
buffer_print_line(b.lines.begin(), line);
|
|
|
|
i++;
|
|
}
|
|
|
|
buffer_print_and_watch_trailing_date_changes();
|
|
buffer_scroll_to_bottom();
|
|
|
|
SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0);
|
|
InvalidateRect(g.hwndBuffer, NULL, TRUE);
|
|
SetCursor(oldCursor);
|
|
}
|
|
|
|
// --- Event processing --------------------------------------------------------
|
|
|
|
static void
|
|
relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m)
|
|
{
|
|
// Initial sync: skip all other processing, let highlights be.
|
|
auto bc = buffer_by_name(g.buffer_current);
|
|
if (!bc) {
|
|
b.lines.push_back(convert_buffer_line(m));
|
|
return;
|
|
}
|
|
|
|
// Retained mode is complicated.
|
|
bool display = (!m.is_unimportant || !bc->hide_unimportant) &&
|
|
(b.buffer_name == g.buffer_current || m.leak_to_active);
|
|
bool to_bottom = display &&
|
|
buffer_at_bottom();
|
|
bool visible = display &&
|
|
to_bottom &&
|
|
!IsIconic(g.hwndMain) &&
|
|
!IsWindowVisible(g.hwndBufferLog);
|
|
bool separate = display &&
|
|
!visible && !bc->new_messages && !bc->new_unimportant_messages;
|
|
|
|
auto line = b.lines.insert(b.lines.end(), convert_buffer_line(m));
|
|
if (!(visible || m.leak_to_active) ||
|
|
b.new_messages || b.new_unimportant_messages) {
|
|
if (line->is_unimportant || m.leak_to_active)
|
|
b.new_unimportant_messages++;
|
|
else
|
|
b.new_messages++;
|
|
}
|
|
|
|
if (m.leak_to_active) {
|
|
auto line = bc->lines.insert(bc->lines.end(), convert_buffer_line(m));
|
|
line->leaked = true;
|
|
if (!visible || bc->new_messages || bc->new_unimportant_messages) {
|
|
if (line->is_unimportant)
|
|
bc->new_unimportant_messages++;
|
|
else
|
|
bc->new_messages++;
|
|
}
|
|
}
|
|
if (separate)
|
|
buffer_print_separator();
|
|
if (display)
|
|
buffer_print_line(bc->lines.begin(), bc->lines.end() - 1);
|
|
if (to_bottom)
|
|
buffer_scroll_to_bottom();
|
|
|
|
if (line->is_highlight || (!visible && !line->is_unimportant &&
|
|
b.kind == Relay::BufferKind::PRIVATE_MESSAGE)) {
|
|
beep();
|
|
|
|
if (!visible) {
|
|
b.highlighted = true;
|
|
refresh_icon();
|
|
}
|
|
}
|
|
|
|
refresh_buffer_list();
|
|
}
|
|
|
|
static void
|
|
relay_process_callbacks(uint32_t command_seq,
|
|
const std::wstring& error, const Relay::ResponseData *response)
|
|
{
|
|
auto &callbacks = g.command_callbacks;
|
|
auto handler = callbacks.find(command_seq);
|
|
if (handler == callbacks.end()) {
|
|
// TODO(p): Warn about an unawaited response.
|
|
} else {
|
|
if (handler->second)
|
|
handler->second(error, response);
|
|
callbacks.erase(handler);
|
|
}
|
|
|
|
// We don't particularly care about wraparound issues.
|
|
while (!callbacks.empty() && callbacks.begin()->first <= command_seq) {
|
|
auto front = callbacks.begin();
|
|
if (front->second)
|
|
front->second(L"No response", nullptr);
|
|
callbacks.erase(front);
|
|
}
|
|
}
|
|
|
|
static void
|
|
relay_process_message(const Relay::EventMessage &m)
|
|
{
|
|
switch (m.data->event) {
|
|
case Relay::Event::ERROR:
|
|
{
|
|
auto data = dynamic_cast<Relay::EventData_Error *>(m.data.get());
|
|
relay_process_callbacks(data->command_seq, data->error, nullptr);
|
|
break;
|
|
}
|
|
case Relay::Event::RESPONSE:
|
|
{
|
|
auto data = dynamic_cast<Relay::EventData_Response *>(m.data.get());
|
|
relay_process_callbacks(data->command_seq, {}, data->data.get());
|
|
break;
|
|
}
|
|
|
|
case Relay::Event::PING:
|
|
{
|
|
auto pong = new Relay::CommandData_PingResponse();
|
|
pong->event_seq = m.event_seq;
|
|
relay_send(pong);
|
|
break;
|
|
}
|
|
|
|
case Relay::Event::BUFFER_LINE:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferLine &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
relay_process_buffer_line(*b, data);
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_UPDATE:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferUpdate &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b) {
|
|
b = &*g.buffers.insert(g.buffers.end(), Buffer());
|
|
b->buffer_name = data.buffer_name;
|
|
SendMessage(g.hwndBufferList, LB_ADDSTRING, 0, 0);
|
|
}
|
|
|
|
bool hiding_toggled = b->hide_unimportant != data.hide_unimportant;
|
|
b->hide_unimportant = data.hide_unimportant;
|
|
b->kind = data.context->kind;
|
|
b->server_name.clear();
|
|
if (auto context = dynamic_cast<Relay::BufferContext_Server *>(
|
|
data.context.get()))
|
|
b->server_name = context->server_name;
|
|
if (auto context = dynamic_cast<Relay::BufferContext_Channel *>(
|
|
data.context.get())) {
|
|
b->server_name = context->server_name;
|
|
b->modes = context->modes;
|
|
b->topic = convert_items(context->topic);
|
|
}
|
|
if (auto context = dynamic_cast<Relay::BufferContext_PrivateMessage *>(
|
|
data.context.get()))
|
|
b->server_name = context->server_name;
|
|
|
|
if (b->buffer_name == g.buffer_current) {
|
|
refresh_topic(b->topic);
|
|
refresh_status();
|
|
|
|
if (hiding_toggled)
|
|
refresh_buffer(*b);
|
|
}
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_STATS:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferStats &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
b->new_messages = data.new_messages;
|
|
b->new_unimportant_messages = data.new_unimportant_messages;
|
|
b->highlighted = data.highlighted;
|
|
|
|
refresh_icon();
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_RENAME:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferRename &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
b->buffer_name = data.new_;
|
|
|
|
if (data.buffer_name == g.buffer_current) {
|
|
g.buffer_current = data.new_;
|
|
refresh_status();
|
|
}
|
|
refresh_buffer_list();
|
|
if (data.buffer_name == g.buffer_last)
|
|
g.buffer_last = data.new_;
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_REMOVE:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferRemove &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
int index = b - g.buffers.data();
|
|
SendMessage(g.hwndBufferList, LB_DELETESTRING, index, 0);
|
|
g.buffers.erase(g.buffers.begin() + index);
|
|
|
|
refresh_icon();
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_ACTIVATE:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferActivate &>(*m.data);
|
|
Buffer *old = buffer_by_name(g.buffer_current);
|
|
g.buffer_last = g.buffer_current;
|
|
g.buffer_current = data.buffer_name;
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
if (old) {
|
|
old->new_messages = 0;
|
|
old->new_unimportant_messages = 0;
|
|
old->highlighted = false;
|
|
|
|
old->input = window_get_text(g.hwndInput);
|
|
SendMessage(g.hwndInput, EM_GETSEL,
|
|
(WPARAM) &old->input_start, (LPARAM) &old->input_end);
|
|
|
|
// Note that we effectively overwrite the newest line
|
|
// with the current textarea contents, and jump there.
|
|
old->history_at = old->history.size();
|
|
}
|
|
|
|
if (IsWindowVisible(g.hwndBufferLog))
|
|
buffer_toggle_log();
|
|
if (!IsIconic(g.hwndMain))
|
|
b->highlighted = false;
|
|
SendMessage(g.hwndBufferList, LB_SETCURSEL, b - g.buffers.data(), 0);
|
|
|
|
refresh_icon();
|
|
refresh_topic(b->topic);
|
|
refresh_buffer(*b);
|
|
refresh_prompt();
|
|
refresh_status();
|
|
|
|
SetWindowText(g.hwndInput, b->input.c_str());
|
|
SendMessage(g.hwndInput, EM_SETSEL,
|
|
(WPARAM) b->input_start, (LPARAM) b->input_end);
|
|
SetFocus(g.hwndInput);
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_INPUT:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferInput &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
if (b->history_at == b->history.size())
|
|
b->history_at++;
|
|
b->history.push_back(data.text);
|
|
break;
|
|
}
|
|
case Relay::Event::BUFFER_CLEAR:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_BufferClear &>(*m.data);
|
|
auto b = buffer_by_name(data.buffer_name);
|
|
if (!b)
|
|
break;
|
|
|
|
b->lines.clear();
|
|
if (b->buffer_name == g.buffer_current)
|
|
refresh_buffer(*b);
|
|
break;
|
|
}
|
|
|
|
case Relay::Event::SERVER_UPDATE:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_ServerUpdate &>(*m.data);
|
|
if (!g.servers.count(data.server_name))
|
|
g.servers.emplace(data.server_name, Server());
|
|
|
|
auto &server = g.servers.at(data.server_name);
|
|
server.state = data.data->state;
|
|
|
|
server.user.clear();
|
|
server.user_modes.clear();
|
|
if (auto registered = dynamic_cast<Relay::ServerData_Registered *>(
|
|
data.data.get())) {
|
|
server.user = registered->user;
|
|
server.user_modes = registered->user_modes;
|
|
}
|
|
|
|
refresh_prompt();
|
|
break;
|
|
}
|
|
case Relay::Event::SERVER_RENAME:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_ServerRename &>(*m.data);
|
|
g.servers.insert_or_assign(data.new_, g.servers.at(data.server_name));
|
|
g.servers.erase(data.server_name);
|
|
break;
|
|
}
|
|
case Relay::Event::SERVER_REMOVE:
|
|
{
|
|
auto &data = dynamic_cast<Relay::EventData_ServerRemove &>(*m.data);
|
|
g.servers.erase(data.server_name);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Networking --------------------------------------------------------------
|
|
|
|
static bool
|
|
relay_process_buffer(std::wstring &error)
|
|
{
|
|
auto &b = g.read_buffer;
|
|
size_t offset = 0;
|
|
while (true) {
|
|
LibertyXDR::Reader r;
|
|
r.data = b.data() + offset;
|
|
r.length = b.size() - offset;
|
|
|
|
uint32_t frame_len = 0;
|
|
if (!r.read(frame_len))
|
|
break;
|
|
|
|
r.length = std::min<size_t>(r.length, frame_len);
|
|
if (r.length < frame_len)
|
|
break;
|
|
|
|
Relay::EventMessage m = {};
|
|
if (!m.deserialize(r) || r.length) {
|
|
error = L"Deserialization failed.";
|
|
return false;
|
|
}
|
|
|
|
relay_process_message(m);
|
|
offset += sizeof frame_len + frame_len;
|
|
}
|
|
|
|
b.erase(b.begin(), b.begin() + offset);
|
|
return true;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
relay_destroy_socket()
|
|
{
|
|
closesocket(g.socket);
|
|
g.socket = INVALID_SOCKET;
|
|
WSACloseEvent(g.socket_event);
|
|
g.socket_event = NULL;
|
|
|
|
g.read_buffer.clear();
|
|
g.write_buffer.clear();
|
|
}
|
|
|
|
static bool
|
|
relay_connect_step(std::wstring& error)
|
|
{
|
|
addrinfoW *&p = g.addresses_iterator;
|
|
while (p) {
|
|
g.socket = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
|
if (g.socket != INVALID_SOCKET)
|
|
break;
|
|
p = p->ai_next;
|
|
}
|
|
if (!p) {
|
|
error = L"Failed to create a socket.";
|
|
return false;
|
|
}
|
|
|
|
g.socket_event = WSACreateEvent();
|
|
if (WSAEventSelect(g.socket, g.socket_event,
|
|
FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE))
|
|
error = format_error_message(WSAGetLastError());
|
|
else if (!connect(g.socket, p->ai_addr, (int) p->ai_addrlen))
|
|
error = L"Connection succeeded unexpectedly early.";
|
|
else if (int err = WSAGetLastError(); err != WSAEWOULDBLOCK)
|
|
error = format_error_message(err);
|
|
else
|
|
return true;
|
|
|
|
relay_destroy_socket();
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
relay_process_connect_event(int err, std::wstring &error)
|
|
{
|
|
addrinfoW *&p = g.addresses_iterator;
|
|
if (err) {
|
|
relay_destroy_socket();
|
|
if (!(p = p->ai_next)) {
|
|
error = L"Connection failed.";
|
|
return false;
|
|
}
|
|
return relay_connect_step(error);
|
|
}
|
|
|
|
g.read_buffer.clear();
|
|
g.write_buffer.clear();
|
|
|
|
auto hello = new Relay::CommandData_Hello();
|
|
hello->version = Relay::VERSION;
|
|
relay_send(hello);
|
|
// The message will be flushed at the upcoming FD_WRITE notification.
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
relay_process_socket_event(int event, int err, std::wstring &error)
|
|
{
|
|
if (err) {
|
|
relay_destroy_socket();
|
|
error = format_error_message(err);
|
|
return false;
|
|
}
|
|
|
|
switch (event) {
|
|
case FD_READ:
|
|
if (!relay_try_read(error) ||
|
|
!relay_process_buffer(error) ||
|
|
!relay_try_write(error))
|
|
return false;
|
|
break;
|
|
case FD_WRITE:
|
|
if (!relay_try_write(error))
|
|
return false;
|
|
break;
|
|
case FD_CLOSE:
|
|
// Handling this seems excessive, since we also get EOF while reading.
|
|
// But we may not receive an FD_READ notification for it.
|
|
error = L"Connection closed.";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
relay_process_socket_events(std::wstring &error)
|
|
{
|
|
WSANETWORKEVENTS wne = {};
|
|
if (WSAEnumNetworkEvents(g.socket, g.socket_event, &wne)) {
|
|
error = format_error_message(WSAGetLastError());
|
|
return false;
|
|
}
|
|
|
|
// TODO(p): Offer reconnecting.
|
|
// TODO(p): Consider disabling UI controls while disconnected.
|
|
if (wne.lNetworkEvents & FD_CONNECT &&
|
|
!relay_process_connect_event(wne.iErrorCode[FD_CONNECT_BIT], error))
|
|
return false;
|
|
|
|
for (auto bit : {FD_READ_BIT, FD_WRITE_BIT, FD_CLOSE_BIT})
|
|
if ((wne.lNetworkEvents & (1 << bit)) &&
|
|
!relay_process_socket_event((1 << bit), wne.iErrorCode[bit], error))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// --- Input line --------------------------------------------------------------
|
|
|
|
static void
|
|
input_set_contents(const std::wstring &input)
|
|
{
|
|
SetWindowText(g.hwndInput, input.c_str());
|
|
if (input.size())
|
|
SendMessage(g.hwndInput, EM_SETSEL, input.size(), input.size());
|
|
}
|
|
|
|
static bool
|
|
input_submit()
|
|
{
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
if (!b)
|
|
return false;
|
|
|
|
auto input = new Relay::CommandData_BufferInput();
|
|
input->buffer_name = b->buffer_name;
|
|
input->text = window_get_text(g.hwndInput);
|
|
|
|
// Buffer::history[Buffer::history.size()] is virtual,
|
|
// and is represented either by edit contents when it's currently
|
|
// being edited, or by Buffer::input in all other cases.
|
|
b->history.push_back(input->text);
|
|
b->history_at = b->history.size();
|
|
input_set_contents({});
|
|
|
|
relay_send(input);
|
|
return true;
|
|
}
|
|
|
|
struct InputStamp {
|
|
DWORD start = {};
|
|
DWORD end = {};
|
|
std::wstring input;
|
|
};
|
|
|
|
static InputStamp
|
|
input_stamp()
|
|
{
|
|
DWORD start = {}, end = {};
|
|
SendMessage(g.hwndInput, EM_GETSEL, (WPARAM) &start, (LPARAM) &end);
|
|
return {start, end, window_get_text(g.hwndInput)};
|
|
}
|
|
|
|
static void
|
|
input_complete(const InputStamp &state, const std::wstring &error,
|
|
const Relay::ResponseData_BufferComplete *response)
|
|
{
|
|
if (!response) {
|
|
show_error_message(error.c_str());
|
|
return;
|
|
}
|
|
|
|
std::string utf8;
|
|
if (!LibertyXDR::wstring_to_utf8(state.input.substr(0, state.start), utf8))
|
|
return;
|
|
std::wstring preceding;
|
|
if (!LibertyXDR::utf8_to_wstring(
|
|
reinterpret_cast<const uint8_t *>(utf8.c_str()), response->start,
|
|
preceding))
|
|
return;
|
|
|
|
if (response->completions.size() > 0) {
|
|
auto insert = response->completions.at(0);
|
|
if (response->completions.size() == 1)
|
|
insert += L" ";
|
|
|
|
SendMessage(g.hwndInput, EM_SETSEL, preceding.length(), state.end);
|
|
SendMessage(g.hwndInput, EM_REPLACESEL, TRUE, (LPARAM) insert.c_str());
|
|
}
|
|
|
|
if (response->completions.size() != 1)
|
|
beep();
|
|
|
|
// TODO(p): Show all completion options.
|
|
}
|
|
|
|
static bool
|
|
input_complete()
|
|
{
|
|
// TODO(p): Also add an increasing counter to the stamp.
|
|
auto state = input_stamp();
|
|
if (state.start != state.end)
|
|
return false;
|
|
|
|
std::string utf8;
|
|
if (!LibertyXDR::wstring_to_utf8(state.input.substr(0, state.start), utf8))
|
|
return false;
|
|
|
|
auto complete = new Relay::CommandData_BufferComplete();
|
|
complete->buffer_name = g.buffer_current;
|
|
complete->text = state.input;
|
|
complete->position = utf8.length();
|
|
relay_send(complete, [state](auto error, auto response) {
|
|
auto stamp = input_stamp();
|
|
if (std::make_tuple(stamp.start, stamp.end, stamp.input) !=
|
|
std::make_tuple(state.start, state.end, state.input))
|
|
return;
|
|
input_complete(stamp, error,
|
|
dynamic_cast<const Relay::ResponseData_BufferComplete *>(response));
|
|
});
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
input_up()
|
|
{
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
if (!b || b->history_at < 1)
|
|
return false;
|
|
|
|
if (b->history_at == b->history.size())
|
|
b->input = window_get_text(g.hwndInput);
|
|
input_set_contents(b->history.at(--b->history_at));
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
input_down()
|
|
{
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
if (!b || b->history_at >= b->history.size())
|
|
return false;
|
|
|
|
input_set_contents(++b->history_at == b->history.size()
|
|
? b->input
|
|
: b->history.at(b->history_at));
|
|
return true;
|
|
}
|
|
|
|
static boolean
|
|
input_wants(const MSG *message)
|
|
{
|
|
switch (message->message) {
|
|
case WM_KEYDOWN:
|
|
// Shift-Tab can go to the dialog manager.
|
|
return message->wParam == VK_RETURN ||
|
|
(message->wParam == VK_TAB && !(GetKeyState(VK_SHIFT) & 0x8000));
|
|
case WM_SYSCHAR:
|
|
switch (message->wParam) {
|
|
case 'p': return true;
|
|
case 'n': return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static LRESULT CALLBACK
|
|
input_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
|
UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
|
|
{
|
|
switch (uMsg) {
|
|
case WM_GETDLGCODE:
|
|
{
|
|
// https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243
|
|
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
if (lParam && input_wants((MSG *) lParam))
|
|
lResult |= DLGC_WANTMESSAGE;
|
|
return lResult;
|
|
}
|
|
case WM_SYSCHAR:
|
|
// TODO(p): Emacs-style cursor movement shortcuts.
|
|
switch (wParam) {
|
|
case 'p':
|
|
if (input_up())
|
|
return 0;
|
|
break;
|
|
case 'n':
|
|
if (input_down())
|
|
return 0;
|
|
break;
|
|
}
|
|
break;
|
|
case WM_KEYDOWN:
|
|
{
|
|
HWND scrollable = IsWindowVisible(g.hwndBufferLog)
|
|
? g.hwndBufferLog
|
|
: g.hwndBuffer;
|
|
|
|
switch (wParam) {
|
|
case VK_UP:
|
|
if (input_up())
|
|
return 0;
|
|
break;
|
|
case VK_DOWN:
|
|
if (input_down())
|
|
return 0;
|
|
break;
|
|
case VK_PRIOR:
|
|
SendMessage(scrollable, EM_SCROLL, SB_PAGEUP, 0);
|
|
return 0;
|
|
case VK_NEXT:
|
|
SendMessage(scrollable, EM_SCROLL, SB_PAGEDOWN, 0);
|
|
return 0;
|
|
}
|
|
break;
|
|
}
|
|
case WM_CHAR:
|
|
{
|
|
// This could be implemented more precisely, but it will do.
|
|
relay_send(new Relay::CommandData_Active());
|
|
|
|
switch (wParam) {
|
|
case VK_RETURN:
|
|
if (!input_submit())
|
|
break;
|
|
return 0;
|
|
case VK_TAB:
|
|
if (!input_complete())
|
|
break;
|
|
return 0;
|
|
}
|
|
break;
|
|
}
|
|
case WM_NCDESTROY:
|
|
// https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
|
|
RemoveWindowSubclass(hWnd, input_proc, uIdSubclass);
|
|
break;
|
|
}
|
|
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
// --- General UI --------------------------------------------------------------
|
|
|
|
static LRESULT CALLBACK
|
|
bufferlist_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
|
UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
|
|
{
|
|
switch (uMsg) {
|
|
case WM_MBUTTONUP:
|
|
{
|
|
POINT p = {LOWORD(lParam), HIWORD(lParam)};
|
|
ClientToScreen(hWnd, &p);
|
|
int index = LBItemFromPt(hWnd, p, FALSE);
|
|
if (wParam || index < 0 || (size_t) index > g.buffers.size())
|
|
break;
|
|
|
|
auto input = new Relay::CommandData_BufferInput();
|
|
input->buffer_name = g.buffer_current;
|
|
input->text = L"/buffer close " + g.buffers.at(index).buffer_name;
|
|
relay_send(input);
|
|
return 0;
|
|
}
|
|
case WM_NCDESTROY:
|
|
// https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
|
|
RemoveWindowSubclass(hWnd, bufferlist_proc, uIdSubclass);
|
|
break;
|
|
}
|
|
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
static LRESULT CALLBACK
|
|
richedit_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
|
UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
|
|
{
|
|
switch (uMsg) {
|
|
case WM_GETDLGCODE:
|
|
{
|
|
// https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243
|
|
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
lResult &= ~DLGC_WANTTAB;
|
|
if (lParam &&
|
|
((MSG *) lParam)->message == WM_KEYDOWN &&
|
|
((MSG *) lParam)->wParam == VK_TAB)
|
|
lResult &= ~DLGC_WANTMESSAGE;
|
|
return lResult;
|
|
}
|
|
case WM_VSCROLL:
|
|
{
|
|
// Dragging the scrollbar doesn't result in EN_VSCROLL.
|
|
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
refresh_status();
|
|
return lResult;
|
|
}
|
|
case WM_NCDESTROY:
|
|
// https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
|
|
RemoveWindowSubclass(hWnd, richedit_proc, uIdSubclass);
|
|
break;
|
|
}
|
|
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
static LRESULT CALLBACK
|
|
log_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
|
UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
|
|
{
|
|
switch (uMsg) {
|
|
case WM_GETDLGCODE:
|
|
{
|
|
// https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243
|
|
// https://devblogs.microsoft.com/oldnewthing/20031114-00/?p=41823
|
|
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
lResult &= ~(DLGC_WANTTAB | DLGC_HASSETSEL);
|
|
if (lParam &&
|
|
((MSG *) lParam)->message == WM_KEYDOWN &&
|
|
((MSG *) lParam)->wParam == VK_TAB)
|
|
lResult &= ~DLGC_WANTMESSAGE;
|
|
return lResult;
|
|
}
|
|
case WM_NCDESTROY:
|
|
// https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
|
|
RemoveWindowSubclass(hWnd, log_proc, uIdSubclass);
|
|
break;
|
|
}
|
|
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
static void
|
|
process_resize(UINT w, UINT h)
|
|
{
|
|
// Font height, control height (accounts for padding and borders)
|
|
int fh = g.font_height, ch = fh + 10;
|
|
|
|
int top = 0;
|
|
MoveWindow(g.hwndTopic, 5, 5, w - 10, fh, FALSE);
|
|
top += ch;
|
|
|
|
int bottom = 0;
|
|
MoveWindow(g.hwndInput, 3, h - ch - 3, w - 6, ch, FALSE);
|
|
bottom += ch + 3;
|
|
MoveWindow(g.hwndPrompt, 5, h - bottom - fh - 5, w / 2 - 5, fh, FALSE);
|
|
MoveWindow(g.hwndStatus, w / 2, h - bottom - fh - 5, w / 2 - 5, fh, FALSE);
|
|
bottom += ch;
|
|
|
|
bool to_bottom = buffer_at_bottom();
|
|
MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE);
|
|
MoveWindow(g.hwndBuffer, 156, top, w - 159, h - top - bottom, FALSE);
|
|
MoveWindow(g.hwndBufferLog, 156, top, w - 159, h - top - bottom, FALSE);
|
|
if (to_bottom)
|
|
buffer_scroll_to_bottom();
|
|
|
|
InvalidateRect(g.hwndMain, NULL, TRUE);
|
|
}
|
|
|
|
static void
|
|
process_bufferlist_drawitem(PDRAWITEMSTRUCT dis)
|
|
{
|
|
// Just always redraw it entirely, disregarding dis->itemAction.
|
|
COLORREF foreground = GetSysColor(dis->itemState & ODS_SELECTED
|
|
? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT);
|
|
COLORREF background = GetSysColor(dis->itemState & ODS_SELECTED
|
|
? COLOR_HIGHLIGHT : COLOR_WINDOW);
|
|
if (dis->itemState & ODS_DISABLED)
|
|
foreground = GetSysColor(COLOR_GRAYTEXT);
|
|
|
|
HFONT oldFont = NULL;
|
|
std::wstring text;
|
|
if (dis->itemID != (UINT) -1 && dis->itemID < g.buffers.size()) {
|
|
const Buffer& b = g.buffers.at(dis->itemID);
|
|
text = b.buffer_name;
|
|
if (b.buffer_name != g.buffer_current && b.new_messages) {
|
|
text += L" (" + std::to_wstring(b.new_messages) + L")";
|
|
oldFont = (HFONT) SelectObject(dis->hDC, g.hfontBold);
|
|
}
|
|
if (b.highlighted)
|
|
foreground = RGB(0xff, 0x5f, 0x00);
|
|
}
|
|
|
|
COLORREF oldForeground = SetTextColor(dis->hDC, foreground);
|
|
COLORREF oldBackground = SetBkColor(dis->hDC, background);
|
|
|
|
// Old Windows hardcoded two pixels, and so will we.
|
|
ExtTextOut(dis->hDC, dis->rcItem.left + 2, dis->rcItem.top,
|
|
ETO_CLIPPED | ETO_OPAQUE, &dis->rcItem,
|
|
text.c_str(), text.length(), NULL);
|
|
if (oldFont)
|
|
SelectObject(dis->hDC, oldFont);
|
|
|
|
SetTextColor(dis->hDC, oldForeground);
|
|
SetBkColor(dis->hDC, oldBackground);
|
|
|
|
if (dis->itemState & ODS_FOCUS)
|
|
DrawFocusRect(dis->hDC, &dis->rcItem);
|
|
}
|
|
|
|
static void
|
|
process_bufferlist_notification(WORD code)
|
|
{
|
|
if (code == LBN_SELCHANGE) {
|
|
auto i = (size_t) SendMessage(g.hwndBufferList, LB_GETCURSEL, 0, 0);
|
|
if (i < g.buffers.size())
|
|
buffer_activate(g.buffers.at(i).buffer_name);
|
|
}
|
|
}
|
|
|
|
static void
|
|
process_accelerator(WORD id)
|
|
{
|
|
// Buffer indexes rotated to start after the current buffer.
|
|
std::vector<size_t> rotated(g.buffers.size());
|
|
size_t start = 0;
|
|
for (auto it = g.buffers.begin(); it != g.buffers.end(); ++it)
|
|
if (it->buffer_name == g.buffer_current) {
|
|
start = it - g.buffers.begin();
|
|
break;
|
|
}
|
|
for (auto &index : rotated)
|
|
index = ++start % g.buffers.size();
|
|
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
switch (id) {
|
|
case ID_PREVIOUS_BUFFER:
|
|
if (rotated.size() > 0) {
|
|
size_t i = (rotated.back() ? rotated.back() : g.buffers.size()) - 1;
|
|
buffer_activate(g.buffers[i].buffer_name);
|
|
}
|
|
return;
|
|
case ID_NEXT_BUFFER:
|
|
if (rotated.size() > 0)
|
|
buffer_activate(g.buffers[rotated.front()].buffer_name);
|
|
return;
|
|
case ID_SWITCH_BUFFER:
|
|
if (!g.buffer_last.empty())
|
|
buffer_activate(g.buffer_last);
|
|
return;
|
|
case ID_GOTO_HIGHLIGHT:
|
|
for (auto i : rotated)
|
|
if (g.buffers[i].highlighted) {
|
|
buffer_activate(g.buffers[i].buffer_name);
|
|
break;
|
|
}
|
|
return;
|
|
case ID_GOTO_ACTIVITY:
|
|
for (auto i : rotated)
|
|
if (g.buffers[i].new_messages) {
|
|
buffer_activate(g.buffers[i].buffer_name);
|
|
break;
|
|
}
|
|
return;
|
|
case ID_TOGGLE_UNIMPORTANT:
|
|
if (b)
|
|
buffer_toggle_unimportant(b->buffer_name);
|
|
return;
|
|
case ID_DISPLAY_FULL_LOG:
|
|
if (b)
|
|
buffer_toggle_log();
|
|
return;
|
|
}
|
|
}
|
|
|
|
static LRESULT CALLBACK
|
|
window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (uMsg) {
|
|
case WM_DESTROY:
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
case WM_SIZE:
|
|
process_resize(LOWORD(lParam), HIWORD(lParam));
|
|
return 0;
|
|
case WM_TIMECHANGE:
|
|
_tzset();
|
|
if (auto b = buffer_by_name(g.buffer_current))
|
|
refresh_buffer(*b);
|
|
return 0;
|
|
case WM_SYSCOLORCHANGE:
|
|
// The topic would flicker with WS_EX_TRANSPARENT.
|
|
// The buffer only changed its text colour, not background.
|
|
SendMessage(g.hwndTopic,
|
|
EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE));
|
|
SendMessage(g.hwndBuffer,
|
|
EM_SETBKGNDCOLOR, 1, 0);
|
|
|
|
// XXX: This is incomplete, we'd have to run convert_items() again;
|
|
// essentially only COLOR_GRAYTEXT is reloaded in here.
|
|
if (auto b = buffer_by_name(g.buffer_current))
|
|
refresh_buffer(*b);
|
|
|
|
// Pass it to all child windows, through DefWindowProc().
|
|
break;
|
|
case WM_ACTIVATE:
|
|
if (LOWORD(wParam) == WA_INACTIVE)
|
|
g.hwndLastFocused = GetFocus();
|
|
else if (g.hwndLastFocused)
|
|
SetFocus(g.hwndLastFocused);
|
|
return 0;
|
|
case WM_MEASUREITEM:
|
|
{
|
|
auto mis = (PMEASUREITEMSTRUCT) lParam;
|
|
mis->itemHeight = g.font_height;
|
|
return TRUE;
|
|
}
|
|
case WM_DRAWITEM:
|
|
{
|
|
auto dis = (PDRAWITEMSTRUCT) lParam;
|
|
if (dis->hwndItem == g.hwndBufferList)
|
|
process_bufferlist_drawitem(dis);
|
|
return TRUE;
|
|
}
|
|
case WM_SYSCOMMAND:
|
|
{
|
|
auto b = buffer_by_name(g.buffer_current);
|
|
if (b && wParam == SC_RESTORE) {
|
|
b->highlighted = false;
|
|
refresh_icon();
|
|
}
|
|
// Here we absolutely must pass to DefWindowProc().
|
|
break;
|
|
}
|
|
case WM_COMMAND:
|
|
if (!lParam)
|
|
process_accelerator(LOWORD(wParam));
|
|
else if (lParam == (LPARAM) g.hwndBufferList)
|
|
process_bufferlist_notification(HIWORD(wParam));
|
|
else if (lParam == (LPARAM) g.hwndBuffer &&
|
|
HIWORD(wParam) == EN_VSCROLL)
|
|
refresh_status();
|
|
return 0;
|
|
case WM_NOTIFY:
|
|
switch (((LPNMHDR) lParam)->code) {
|
|
case EN_LINK:
|
|
{
|
|
auto link = (ENLINK *) lParam;
|
|
if (link->msg == WM_LBUTTONUP) {
|
|
TEXTRANGE tr = {};
|
|
tr.chrg = link->chrg;
|
|
tr.lpstrText = new wchar_t[tr.chrg.cpMax - tr.chrg.cpMin + 1]();
|
|
SendMessage(
|
|
link->nmhdr.hwndFrom, EM_GETTEXTRANGE, 0, (LPARAM) &tr);
|
|
ShellExecute(
|
|
NULL, L"open", tr.lpstrText, NULL, NULL, SW_SHOWNORMAL);
|
|
delete[] tr.lpstrText;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
case DM_GETDEFID:
|
|
case DM_SETDEFID:
|
|
break;
|
|
}
|
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
static INT_PTR CALLBACK
|
|
connect_proc(
|
|
HWND hDlg, UINT uMsg, WPARAM wParam, [[maybe_unused]] LPARAM lParam)
|
|
{
|
|
switch (uMsg) {
|
|
case WM_INITDIALOG:
|
|
return TRUE;
|
|
case WM_COMMAND:
|
|
switch (LOWORD(wParam)) {
|
|
case IDOK:
|
|
case IDCANCEL:
|
|
g.host = window_get_text(GetDlgItem(hDlg, IDC_HOST));
|
|
g.port = window_get_text(GetDlgItem(hDlg, IDC_PORT));
|
|
EndDialog(hDlg, LOWORD(wParam));
|
|
return TRUE;
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
get_font()
|
|
{
|
|
// To enable the "Make text bigger" scaling functionality.
|
|
NONCLIENTMETRICS ncm = {};
|
|
ncm.cbSize = sizeof ncm;
|
|
if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0)) {
|
|
g.hfont = CreateFontIndirect(&ncm.lfMessageFont);
|
|
|
|
LOGFONT bold = g.fontlog = ncm.lfMessageFont;
|
|
bold.lfWeight = FW_BOLD;
|
|
g.hfontBold = CreateFontIndirect(&bold);
|
|
}
|
|
|
|
if (!g.hfont)
|
|
g.hfont = g.hfontBold = (HFONT) GetStockObject(DEFAULT_GUI_FONT);
|
|
|
|
// There doesn't seem to be a better way than through a drawing context.
|
|
HDC hdc = GetDC(NULL);
|
|
HFONT oldFont = (HFONT) SelectObject(hdc, g.hfont);
|
|
TEXTMETRIC tm = {};
|
|
GetTextMetrics(hdc, &tm);
|
|
SelectObject(hdc, oldFont);
|
|
ReleaseDC(NULL, hdc);
|
|
|
|
g.font_height = tm.tmHeight;
|
|
}
|
|
|
|
static BOOL CALLBACK
|
|
set_font(HWND child, LPARAM font)
|
|
{
|
|
SendMessage(child, WM_SETFONT, font, (LPARAM) TRUE);
|
|
return TRUE;
|
|
}
|
|
|
|
static bool
|
|
process_messages(HACCEL accelerators)
|
|
{
|
|
MSG message = {};
|
|
while (PeekMessage(&message, NULL, 0, 0, TRUE)) {
|
|
if (message.message == WM_QUIT)
|
|
return false;
|
|
if (TranslateAccelerator(g.hwndMain, accelerators, &message))
|
|
continue;
|
|
|
|
// https://devblogs.microsoft.com/oldnewthing/20031021-00/?p=42083
|
|
if (IsDialogMessage(g.hwndMain, &message))
|
|
continue;
|
|
|
|
TranslateMessage(&message);
|
|
DispatchMessage(&message);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int WINAPI
|
|
wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance,
|
|
PWSTR pCmdLine, int nCmdShow)
|
|
{
|
|
setlocale(LC_ALL, "");
|
|
|
|
WSADATA wd = {};
|
|
int err = {};
|
|
if ((err = WSAStartup(MAKEWORD(2, 2), &wd))) {
|
|
show_error_message(format_error_message(err).c_str());
|
|
return 1;
|
|
}
|
|
|
|
INITCOMMONCONTROLSEX icc = {};
|
|
icc.dwICC = 0;
|
|
icc.dwSize = sizeof icc;
|
|
(void) InitCommonControlsEx(&icc);
|
|
|
|
// TODO(p): The control doesn't seem to support visual styles at all,
|
|
// try to figure out how to immitate it with WM_NCPAINT,
|
|
// GetThemeBackgroundContentRect(), DrawThemeBackground(), etc.,
|
|
// and remember to handle the case of visual styles being disabled,
|
|
// perhaps by using DrawEdge() → InflateRect(-CXBORDER, -CYBORDER).
|
|
// This is by no means simple.
|
|
//
|
|
// Example implementation: https://web.archive.org/web/20210707175627
|
|
// /http://www.codeguru.com/cpp/w-d/dislog/miscellaneous/article.php/c8729
|
|
// /XP-Theme-Support-for-Rich-Edit-and-Custom-Controls.htm
|
|
if (!LoadLibrary(L"Msftedit.dll")) {
|
|
show_error_message(format_error_message(GetLastError()).c_str());
|
|
return 1;
|
|
}
|
|
|
|
// WINE calls WM_MEASUREITEM as soon as the listbox is created.
|
|
// TODO(p): Watch for WM_SETTINGCHANGE/SPI_SETNONCLIENTMETRICS,
|
|
// reset all fonts in all widgets, and the topic background colour.
|
|
get_font();
|
|
|
|
g.hicon =
|
|
LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON));
|
|
g.hiconHighlighted =
|
|
LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HIGHLIGHTED));
|
|
|
|
WNDCLASSEX wc = {};
|
|
wc.cbSize = sizeof wc;
|
|
wc.lpfnWndProc = window_proc;
|
|
wc.hInstance = hInstance;
|
|
wc.hIcon = g.hicon;
|
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
|
|
wc.lpszClassName = TEXT(PROJECT_NAME);
|
|
if (!RegisterClassEx(&wc))
|
|
return 1;
|
|
|
|
g.hwndMain = CreateWindowEx(
|
|
WS_EX_CONTROLPARENT, wc.lpszClassName, TEXT(PROJECT_NAME),
|
|
WS_OVERLAPPEDWINDOW,
|
|
CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL);
|
|
|
|
// We're lucky to not need much user user interface,
|
|
// because Win32 is in many aspects quite difficult to work with.
|
|
HMENU id = 0;
|
|
g.hwndTopic = CreateWindowEx(0, MSFTEDIT_CLASS, L"",
|
|
WS_VISIBLE | WS_CHILD | WS_TABSTOP |
|
|
ES_AUTOHSCROLL | ES_READONLY,
|
|
0, 0, 100, 20, g.hwndMain, ++id, hInstance, NULL);
|
|
g.hwndBufferList = CreateWindowEx(WS_EX_CLIENTEDGE, WC_LISTBOX, L"",
|
|
WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL |
|
|
LBS_NOINTEGRALHEIGHT | LBS_NOTIFY | LBS_NODATA | LBS_OWNERDRAWFIXED,
|
|
0, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL);
|
|
g.hwndBuffer = CreateWindowEx(WS_EX_CLIENTEDGE, MSFTEDIT_CLASS, L"",
|
|
WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL |
|
|
ES_MULTILINE | ES_READONLY | ES_SAVESEL,
|
|
50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL);
|
|
g.hwndBufferLog = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"",
|
|
WS_CHILD | WS_TABSTOP | WS_VSCROLL |
|
|
ES_MULTILINE | ES_READONLY,
|
|
50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL);
|
|
g.hwndPrompt = CreateWindowEx(0, WC_STATIC, L"Connecting...",
|
|
WS_VISIBLE | WS_CHILD,
|
|
0, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL);
|
|
g.hwndStatus = CreateWindowEx(0, WC_STATIC, L"",
|
|
WS_VISIBLE | WS_CHILD | ES_RIGHT,
|
|
50, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL);
|
|
g.hwndInput = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"",
|
|
WS_VISIBLE | WS_CHILD | WS_TABSTOP |
|
|
ES_AUTOHSCROLL,
|
|
0, 80, 100, 20, g.hwndMain, ++id, hInstance, NULL);
|
|
|
|
SendMessage(g.hwndTopic, EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE));
|
|
// The 1 probably means AURL_ENABLEEA only in later versions.
|
|
SendMessage(g.hwndTopic, EM_AUTOURLDETECT, 1, 0);
|
|
SendMessage(g.hwndTopic, EM_SETEVENTMASK, 0, ENM_LINK);
|
|
SendMessage(g.hwndTopic, EM_SETUNDOLIMIT, 0, 0);
|
|
SetWindowSubclass(g.hwndTopic, richedit_proc, 0, 0);
|
|
SendMessage(g.hwndBuffer, EM_AUTOURLDETECT, 1, 0);
|
|
SendMessage(g.hwndBuffer, EM_SETEVENTMASK, 0, ENM_LINK | ENM_SCROLL);
|
|
SendMessage(g.hwndBuffer, EM_SETUNDOLIMIT, 0, 0);
|
|
SetWindowSubclass(g.hwndBufferList, bufferlist_proc, 0, 0);
|
|
SetWindowSubclass(g.hwndBuffer, richedit_proc, 0, 0);
|
|
SetWindowSubclass(g.hwndBufferLog, log_proc, 0, 0);
|
|
|
|
RECT client_rect = {};
|
|
if (GetClientRect(g.hwndMain, &client_rect)) {
|
|
process_resize(client_rect.right - client_rect.left,
|
|
client_rect.bottom - client_rect.top);
|
|
}
|
|
|
|
EnumChildWindows(g.hwndMain, (WNDENUMPROC) set_font, (LPARAM) g.hfont);
|
|
SendMessage(g.hwndPrompt, WM_SETFONT, (WPARAM) g.hfontBold, (LPARAM) TRUE);
|
|
SetFocus(g.hwndInput);
|
|
SetWindowSubclass(g.hwndInput, input_proc, 0, 0);
|
|
ShowWindow(g.hwndMain, nCmdShow);
|
|
|
|
HACCEL accelerators =
|
|
LoadAccelerators(hInstance, MAKEINTRESOURCE(IDA_ACCELERATORS));
|
|
if (!accelerators) {
|
|
show_error_message(format_error_message(GetLastError()).c_str());
|
|
return 1;
|
|
}
|
|
|
|
int argc = 0;
|
|
LPWSTR *argv = CommandLineToArgvW(pCmdLine, &argc);
|
|
if (argc >= 2) {
|
|
g.host = argv[0];
|
|
g.port = argv[1];
|
|
} else if (DialogBox(hInstance, MAKEINTRESOURCE(IDD_CONNECT),
|
|
g.hwndMain, connect_proc) != IDOK) {
|
|
return 0;
|
|
}
|
|
LocalFree(argv);
|
|
|
|
// We have a few suboptimal asynchronous options:
|
|
// a) WSAAsyncGetHostByName() requires us to distinguish hostnames
|
|
// from IP literals manually,
|
|
// b) GetAddrInfoEx() only supports asynchronous operation since Windows 8,
|
|
// c) run this from a thread.
|
|
addrinfoW hints = {};
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_protocol = IPPROTO_TCP;
|
|
err = GetAddrInfo(g.host.c_str(), g.port.c_str(), &hints, &g.addresses);
|
|
if (err) {
|
|
show_error_message(format_error_message(err).c_str());
|
|
return 1;
|
|
}
|
|
|
|
std::wstring error;
|
|
g.addresses_iterator = g.addresses;
|
|
if (!relay_connect_step(error)) {
|
|
show_error_message(error.c_str());
|
|
return 1;
|
|
}
|
|
|
|
if (!(g.date_change_timer = CreateWaitableTimer(NULL, FALSE, NULL)) ||
|
|
!(g.flush_event = CreateEvent(NULL, FALSE, FALSE, NULL))) {
|
|
show_error_message(format_error_message(GetLastError()).c_str());
|
|
return 1;
|
|
}
|
|
|
|
while (process_messages(accelerators)) {
|
|
HANDLE handles[] = {g.date_change_timer, g.flush_event, g.socket_event};
|
|
DWORD count = 3 - !handles[2];
|
|
DWORD result = MsgWaitForMultipleObjects(
|
|
count, handles, FALSE, INFINITE, QS_ALLINPUT);
|
|
if (result == WAIT_FAILED) {
|
|
show_error_message(format_error_message(GetLastError()).c_str());
|
|
return 1;
|
|
}
|
|
if (result >= WAIT_OBJECT_0 + count)
|
|
continue;
|
|
|
|
auto signalled = handles[result];
|
|
if (signalled == g.date_change_timer) {
|
|
bool to_bottom = buffer_at_bottom();
|
|
buffer_print_and_watch_trailing_date_changes();
|
|
if (to_bottom)
|
|
buffer_scroll_to_bottom();
|
|
}
|
|
if (signalled == g.flush_event && !relay_try_write(error)) {
|
|
show_error_message(error.c_str());
|
|
return 1;
|
|
}
|
|
if (signalled == g.socket_event && !relay_process_socket_events(error)) {
|
|
show_error_message(error.c_str());
|
|
return 1;
|
|
}
|
|
}
|
|
FreeAddrInfo(g.addresses);
|
|
WSACleanup();
|
|
CloseHandle(g.date_change_timer);
|
|
CloseHandle(g.flush_event);
|
|
return 0;
|
|
}
|