feat(dialogs): add input and color pickers
This commit is contained in:
@@ -31,7 +31,7 @@ set_target_properties(izo PROPERTIES
|
|||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_compile_definitions(izo PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
target_compile_definitions(izo PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||||
target_link_libraries(izo PRIVATE ole32 shell32 uuid user32)
|
target_link_libraries(izo PRIVATE comdlg32 ole32 shell32 uuid user32)
|
||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
# No link-time desktop dependency. The Linux backend discovers zenity or
|
# No link-time desktop dependency. The Linux backend discovers zenity or
|
||||||
# kdialog at runtime so applications do not inherit GTK or Qt dependencies.
|
# kdialog at runtime so applications do not inherit GTK or Qt dependencies.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
@@ -17,6 +18,59 @@ struct MessageBoxOptions {
|
|||||||
};
|
};
|
||||||
|
|
||||||
MessageResponse ShowMessageBox(const MessageBoxOptions& options,
|
MessageResponse ShowMessageBox(const MessageBoxOptions& options,
|
||||||
std::string* errorMessage = nullptr);
|
std::string* errorMessage = nullptr);
|
||||||
|
MessageResponse ShowMessageBox(std::string_view title, std::string_view message,
|
||||||
|
MessageIcon icon = MessageIcon::Info,
|
||||||
|
MessageButtons buttons = MessageButtons::Ok,
|
||||||
|
std::string* errorMessage = nullptr);
|
||||||
|
|
||||||
|
// Convenience wrappers around ShowMessageBox.
|
||||||
|
MessageResponse ShowErrorBox(std::string_view title, std::string_view message,
|
||||||
|
std::string* errorMessage = nullptr);
|
||||||
|
MessageResponse ShowQuestionBox(std::string_view title, std::string_view message,
|
||||||
|
std::string* errorMessage = nullptr);
|
||||||
|
|
||||||
|
struct InputBoxOptions {
|
||||||
|
std::string title;
|
||||||
|
std::string message;
|
||||||
|
std::string initialValue;
|
||||||
|
bool password = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputBoxResult {
|
||||||
|
MessageResponse response = MessageResponse::Cancel;
|
||||||
|
std::string value;
|
||||||
|
std::string errorMessage;
|
||||||
|
|
||||||
|
explicit operator bool() const noexcept { return response == MessageResponse::Ok; }
|
||||||
|
};
|
||||||
|
|
||||||
|
InputBoxResult ShowInputBox(const InputBoxOptions& options = {});
|
||||||
|
|
||||||
|
struct Color {
|
||||||
|
std::uint8_t red = 0;
|
||||||
|
std::uint8_t green = 0;
|
||||||
|
std::uint8_t blue = 0;
|
||||||
|
|
||||||
|
friend bool operator==(Color left, Color right) noexcept {
|
||||||
|
return left.red == right.red && left.green == right.green && left.blue == right.blue;
|
||||||
|
}
|
||||||
|
friend bool operator!=(Color left, Color right) noexcept { return !(left == right); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ColorPickerOptions {
|
||||||
|
std::string title;
|
||||||
|
Color initialColor{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ColorPickerResult {
|
||||||
|
MessageResponse response = MessageResponse::Cancel;
|
||||||
|
Color color{};
|
||||||
|
std::string errorMessage;
|
||||||
|
|
||||||
|
explicit operator bool() const noexcept { return response == MessageResponse::Ok; }
|
||||||
|
};
|
||||||
|
|
||||||
|
ColorPickerResult PickColor(const ColorPickerOptions& options = {});
|
||||||
|
|
||||||
} // namespace izo
|
} // namespace izo
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
#include <izo/Dialogs.hpp>
|
#include <izo/Dialogs.hpp>
|
||||||
|
#include <izo/MessageBox.hpp>
|
||||||
|
|
||||||
// Platform implementations live in dialogs_windows.cpp and dialogs_linux.cpp.
|
// Platform implementations live in dialogs_windows.cpp and dialogs_linux.cpp.
|
||||||
|
|
||||||
|
namespace izo {
|
||||||
|
|
||||||
|
MessageResponse ShowMessageBox(std::string_view title, std::string_view message,
|
||||||
|
MessageIcon icon, MessageButtons buttons,
|
||||||
|
std::string* errorMessage) {
|
||||||
|
return ShowMessageBox({std::string(title), std::string(message), icon, buttons}, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageResponse ShowErrorBox(std::string_view title, std::string_view message,
|
||||||
|
std::string* errorMessage) {
|
||||||
|
return ShowMessageBox(title, message, MessageIcon::Error, MessageButtons::Ok, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageResponse ShowQuestionBox(std::string_view title, std::string_view message,
|
||||||
|
std::string* errorMessage) {
|
||||||
|
return ShowMessageBox(title, message, MessageIcon::Question, MessageButtons::YesNo, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace izo
|
||||||
// This translation unit ensures the archive always has a platform-independent
|
// This translation unit ensures the archive always has a platform-independent
|
||||||
// object and is a natural home for future shared helpers.
|
// object and is a natural home for future shared helpers.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -63,6 +64,11 @@ command_result run(const std::vector<std::string>& args, std::string_view input
|
|||||||
return {WIFEXITED(status) ? WEXITSTATUS(status) : -1, std::move(output), {}};
|
return {WIFEXITED(status) ? WEXITSTATUS(status) : -1, std::move(output), {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string trimmed(std::string value) {
|
||||||
|
while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) value.pop_back();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
|
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
|
||||||
@@ -109,6 +115,55 @@ MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* er
|
|||||||
return MessageResponse::Error;
|
return MessageResponse::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputBoxResult ShowInputBox(const InputBoxOptions& options) {
|
||||||
|
std::vector<std::string> args;
|
||||||
|
if (on_path("zenity")) {
|
||||||
|
args = {"zenity", "--entry", "--title=" + options.title,
|
||||||
|
"--text=" + options.message, "--entry-text=" + options.initialValue};
|
||||||
|
if (options.password) args.push_back("--hide-text");
|
||||||
|
} else if (on_path("kdialog")) {
|
||||||
|
args = {"kdialog", options.password ? "--password" : "--inputbox", options.message};
|
||||||
|
if (!options.password) args.push_back(options.initialValue);
|
||||||
|
args.push_back("--title"); args.push_back(options.title);
|
||||||
|
} else {
|
||||||
|
return {MessageResponse::Error, {},
|
||||||
|
"No supported input box provider found; install zenity or kdialog"};
|
||||||
|
}
|
||||||
|
auto result = run(args);
|
||||||
|
if (result.code == 0) return {MessageResponse::Ok, trimmed(std::move(result.output)), {}};
|
||||||
|
if (result.code == 1) return {MessageResponse::Cancel, {}, {}};
|
||||||
|
return {MessageResponse::Error, {}, result.output.empty() ? "Input box failed" : trimmed(result.output)};
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPickerResult PickColor(const ColorPickerOptions& options) {
|
||||||
|
char initial[8];
|
||||||
|
std::snprintf(initial, sizeof(initial), "#%02x%02x%02x", options.initialColor.red,
|
||||||
|
options.initialColor.green, options.initialColor.blue);
|
||||||
|
std::vector<std::string> args;
|
||||||
|
if (on_path("zenity")) {
|
||||||
|
args = {"zenity", "--color-selection", "--show-palette", "--title=" + options.title,
|
||||||
|
std::string("--color=") + initial};
|
||||||
|
} else if (on_path("kdialog")) {
|
||||||
|
args = {"kdialog", "--getcolor", initial, "--title", options.title};
|
||||||
|
} else {
|
||||||
|
return {MessageResponse::Error, {},
|
||||||
|
"No supported color picker provider found; install zenity or kdialog"};
|
||||||
|
}
|
||||||
|
auto result = run(args);
|
||||||
|
if (result.code == 1) return {MessageResponse::Cancel, {}, {}};
|
||||||
|
if (result.code != 0)
|
||||||
|
return {MessageResponse::Error, {}, result.output.empty() ? "Color picker failed" : trimmed(result.output)};
|
||||||
|
const auto value = trimmed(std::move(result.output));
|
||||||
|
unsigned red = 0, green = 0, blue = 0;
|
||||||
|
if (std::sscanf(value.c_str(), "#%02x%02x%02x", &red, &green, &blue) != 3 &&
|
||||||
|
std::sscanf(value.c_str(), "rgb(%u,%u,%u)", &red, &green, &blue) != 3) {
|
||||||
|
return {MessageResponse::Error, {}, "Color picker returned an invalid color: " + value};
|
||||||
|
}
|
||||||
|
return {MessageResponse::Ok,
|
||||||
|
{static_cast<std::uint8_t>(red), static_cast<std::uint8_t>(green),
|
||||||
|
static_cast<std::uint8_t>(blue)}, {}};
|
||||||
|
}
|
||||||
|
|
||||||
bool SetClipboardText(std::string_view text, std::string* errorMessage) {
|
bool SetClipboardText(std::string_view text, std::string* errorMessage) {
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
if (on_path("wl-copy")) args = {"wl-copy", "--type", "text/plain;charset=utf-8"};
|
if (on_path("wl-copy")) args = {"wl-copy", "--type", "text/plain;charset=utf-8"};
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
#include <izo/MessageBox.hpp>
|
#include <izo/MessageBox.hpp>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <commdlg.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace izo {
|
namespace izo {
|
||||||
@@ -32,6 +34,79 @@ std::string windows_error(const char* operation) {
|
|||||||
return std::string(operation) + " failed with error " + std::to_string(GetLastError());
|
return std::string(operation) + " failed with error " + std::to_string(GetLastError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr int input_edit_id = 1001;
|
||||||
|
constexpr int input_ok_id = IDOK;
|
||||||
|
constexpr int input_cancel_id = IDCANCEL;
|
||||||
|
|
||||||
|
struct input_window_state {
|
||||||
|
InputBoxOptions options;
|
||||||
|
InputBoxResult result;
|
||||||
|
HWND edit = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
UINT_PTR CALLBACK color_picker_hook(HWND dialog, UINT message, WPARAM, LPARAM lparam) {
|
||||||
|
if (message == WM_INITDIALOG) {
|
||||||
|
const auto* picker = reinterpret_cast<const CHOOSECOLORW*>(lparam);
|
||||||
|
const auto* title = reinterpret_cast<const wchar_t*>(picker->lCustData);
|
||||||
|
if (title && *title) SetWindowTextW(dialog, title);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK input_window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||||
|
auto* state = reinterpret_cast<input_window_state*>(GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
state = static_cast<input_window_state*>(
|
||||||
|
reinterpret_cast<CREATESTRUCTW*>(lparam)->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
|
||||||
|
}
|
||||||
|
if (!state) return DefWindowProcW(window, message, wparam, lparam);
|
||||||
|
switch (message) {
|
||||||
|
case WM_CREATE: {
|
||||||
|
const auto prompt = widen(state->options.message);
|
||||||
|
const auto initial = widen(state->options.initialValue);
|
||||||
|
auto font = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
||||||
|
HWND label = CreateWindowW(L"STATIC", prompt.c_str(), WS_CHILD | WS_VISIBLE,
|
||||||
|
14, 14, 392, 36, window, nullptr, nullptr, nullptr);
|
||||||
|
DWORD edit_style = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL;
|
||||||
|
if (state->options.password) edit_style |= ES_PASSWORD;
|
||||||
|
state->edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", initial.c_str(), edit_style,
|
||||||
|
14, 55, 392, 24, window,
|
||||||
|
reinterpret_cast<HMENU>(input_edit_id), nullptr, nullptr);
|
||||||
|
HWND ok = CreateWindowW(L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP |
|
||||||
|
BS_DEFPUSHBUTTON, 245, 94, 76, 26, window,
|
||||||
|
reinterpret_cast<HMENU>(input_ok_id), nullptr, nullptr);
|
||||||
|
HWND cancel = CreateWindowW(L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||||
|
330, 94, 76, 26, window,
|
||||||
|
reinterpret_cast<HMENU>(input_cancel_id), nullptr, nullptr);
|
||||||
|
for (HWND control : {label, state->edit, ok, cancel})
|
||||||
|
SendMessageW(control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
|
||||||
|
SendMessageW(state->edit, EM_SETSEL, 0, -1);
|
||||||
|
SetFocus(state->edit);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WM_COMMAND:
|
||||||
|
if (LOWORD(wparam) == input_ok_id) {
|
||||||
|
const int length = GetWindowTextLengthW(state->edit);
|
||||||
|
std::wstring value(static_cast<std::size_t>(length) + 1, L'\0');
|
||||||
|
GetWindowTextW(state->edit, value.data(), length + 1);
|
||||||
|
value.resize(static_cast<std::size_t>(length));
|
||||||
|
state->result = {MessageResponse::Ok, narrow(value.c_str()), {}};
|
||||||
|
DestroyWindow(window);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (LOWORD(wparam) == input_cancel_id) {
|
||||||
|
DestroyWindow(window);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_CLOSE:
|
||||||
|
DestroyWindow(window);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return DefWindowProcW(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
|
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
|
||||||
@@ -61,6 +136,65 @@ MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputBoxResult ShowInputBox(const InputBoxOptions& options) {
|
||||||
|
static const wchar_t* class_name = L"iZoInputBox";
|
||||||
|
static const ATOM window_class = [] {
|
||||||
|
WNDCLASSW descriptor{};
|
||||||
|
descriptor.lpfnWndProc = input_window_proc;
|
||||||
|
descriptor.hInstance = GetModuleHandleW(nullptr);
|
||||||
|
descriptor.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||||
|
descriptor.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
|
||||||
|
descriptor.lpszClassName = class_name;
|
||||||
|
return RegisterClassW(&descriptor);
|
||||||
|
}();
|
||||||
|
if (!window_class && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||||
|
return {MessageResponse::Error, {}, windows_error("RegisterClassW")};
|
||||||
|
|
||||||
|
input_window_state state{options};
|
||||||
|
const auto title = widen(options.title);
|
||||||
|
HWND window = CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT, class_name,
|
||||||
|
title.c_str(), WS_CAPTION | WS_SYSMENU,
|
||||||
|
CW_USEDEFAULT, CW_USEDEFAULT, 436, 164, nullptr, nullptr,
|
||||||
|
GetModuleHandleW(nullptr), &state);
|
||||||
|
if (!window) return {MessageResponse::Error, {}, windows_error("CreateWindowExW")};
|
||||||
|
RECT bounds{};
|
||||||
|
GetWindowRect(window, &bounds);
|
||||||
|
const int x = (GetSystemMetrics(SM_CXSCREEN) - (bounds.right - bounds.left)) / 2;
|
||||||
|
const int y = (GetSystemMetrics(SM_CYSCREEN) - (bounds.bottom - bounds.top)) / 2;
|
||||||
|
SetWindowPos(window, HWND_TOP, x, y, 0, 0, SWP_NOSIZE);
|
||||||
|
ShowWindow(window, SW_SHOW);
|
||||||
|
MSG message{};
|
||||||
|
while (IsWindow(window) && GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||||
|
if (!IsDialogMessageW(window, &message)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPickerResult PickColor(const ColorPickerOptions& options) {
|
||||||
|
std::array<COLORREF, 16> custom_colors{};
|
||||||
|
const auto title = widen(options.title);
|
||||||
|
CHOOSECOLORW picker{};
|
||||||
|
picker.lStructSize = sizeof(picker);
|
||||||
|
picker.rgbResult = RGB(options.initialColor.red, options.initialColor.green,
|
||||||
|
options.initialColor.blue);
|
||||||
|
picker.lpCustColors = custom_colors.data();
|
||||||
|
picker.lCustData = reinterpret_cast<LPARAM>(title.c_str());
|
||||||
|
picker.lpfnHook = color_picker_hook;
|
||||||
|
picker.Flags = CC_ENABLEHOOK | CC_FULLOPEN | CC_RGBINIT;
|
||||||
|
if (ChooseColorW(&picker)) {
|
||||||
|
return {MessageResponse::Ok,
|
||||||
|
{GetRValue(picker.rgbResult), GetGValue(picker.rgbResult),
|
||||||
|
GetBValue(picker.rgbResult)}, {}};
|
||||||
|
}
|
||||||
|
const DWORD error = CommDlgExtendedError();
|
||||||
|
if (error == 0) return {MessageResponse::Cancel, options.initialColor, {}};
|
||||||
|
return {MessageResponse::Error, options.initialColor,
|
||||||
|
"ChooseColorW failed with error " + std::to_string(error)};
|
||||||
|
}
|
||||||
|
|
||||||
bool SetClipboardText(std::string_view text, std::string* errorMessage) {
|
bool SetClipboardText(std::string_view text, std::string* errorMessage) {
|
||||||
const auto value = widen(text);
|
const auto value = widen(text);
|
||||||
if (!OpenClipboard(nullptr)) {
|
if (!OpenClipboard(nullptr)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user