wip: printing from word processor, word processor improvements
This commit is contained in:
+329
-53
@@ -28,8 +28,8 @@ namespace {
|
||||
|
||||
constexpr int FILE_INIT_W = 720;
|
||||
constexpr int FILE_INIT_H = 520;
|
||||
constexpr int PRINT_INIT_W = 480;
|
||||
constexpr int PRINT_INIT_H = 260;
|
||||
constexpr int PRINT_INIT_W = 640;
|
||||
constexpr int PRINT_INIT_H = 400;
|
||||
|
||||
constexpr int TOOLBAR_H = 32;
|
||||
constexpr int PATHBAR_H = 32;
|
||||
@@ -46,6 +46,7 @@ constexpr int GRID_PAD = 4;
|
||||
constexpr int MAX_ENTRIES = 64;
|
||||
constexpr int MAX_HISTORY = 16;
|
||||
constexpr int MAX_DRIVES = 16;
|
||||
constexpr int MAX_DISCOVERED_PRINTERS = 8;
|
||||
constexpr int BUTTON_H = 30;
|
||||
constexpr int BUTTON_W = 88;
|
||||
|
||||
@@ -137,10 +138,24 @@ struct PrintDialogState {
|
||||
SvgIcon printer_icon;
|
||||
char source_path[256];
|
||||
char job_name[128];
|
||||
char printer_uri[256];
|
||||
char printer_name[128];
|
||||
char status[160];
|
||||
bool can_print;
|
||||
struct PrinterRow {
|
||||
char uri[256];
|
||||
char name[128];
|
||||
char status[128];
|
||||
char seen_at[32];
|
||||
bool ok;
|
||||
bool is_default;
|
||||
} printers[MAX_DISCOVERED_PRINTERS];
|
||||
int printer_count;
|
||||
int selected_printer;
|
||||
uint32_t copies;
|
||||
int queue_count;
|
||||
int active_count;
|
||||
bool daemon_running;
|
||||
bool submit_mode;
|
||||
bool source_ready;
|
||||
char note[160];
|
||||
char error[160];
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
};
|
||||
@@ -173,7 +188,12 @@ struct FileDialogLayout {
|
||||
};
|
||||
|
||||
struct PrintDialogLayout {
|
||||
Rect content_rect;
|
||||
Rect header_rect;
|
||||
Rect printers_rect;
|
||||
Rect detail_rect;
|
||||
Rect copy_minus_btn;
|
||||
Rect copy_value_rect;
|
||||
Rect copy_plus_btn;
|
||||
Rect cancel_btn;
|
||||
Rect printers_btn;
|
||||
Rect refresh_btn;
|
||||
@@ -377,7 +397,13 @@ FileDialogLayout file_layout(const FileDialogState* st) {
|
||||
|
||||
PrintDialogLayout print_layout() {
|
||||
PrintDialogLayout lo = {};
|
||||
lo.content_rect = {0, 0, g_app.win.width, g_app.win.height};
|
||||
lo.header_rect = {0, 0, 0, 0};
|
||||
lo.printers_rect = {16, 16, 212, g_app.win.height - 94};
|
||||
lo.detail_rect = {244, 16, g_app.win.width - 260, g_app.win.height - 94};
|
||||
int copy_y = lo.detail_rect.y + 202;
|
||||
lo.copy_minus_btn = {lo.detail_rect.x + 16, copy_y, 28, 28};
|
||||
lo.copy_value_rect = {lo.copy_minus_btn.x + 36, copy_y, 104, 28};
|
||||
lo.copy_plus_btn = {lo.copy_value_rect.x + lo.copy_value_rect.w + 8, copy_y, 28, 28};
|
||||
int btn_y = g_app.win.height - BUTTON_H - 16;
|
||||
lo.confirm_btn = {g_app.win.width - BUTTON_W - 16, btn_y, BUTTON_W, BUTTON_H};
|
||||
lo.cancel_btn = {lo.confirm_btn.x - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H};
|
||||
@@ -386,10 +412,14 @@ PrintDialogLayout print_layout() {
|
||||
return lo;
|
||||
}
|
||||
|
||||
void dialog_finish(const char* status, const char* path, const char* job_id, const char* message) {
|
||||
void dialog_finish(const char* status, const char* path, const char* job_id, const char* message,
|
||||
const char* printer_uri = "", const char* printer_name = "", uint32_t copies = 0) {
|
||||
safe_copy(g_app.result.status, sizeof(g_app.result.status), status);
|
||||
safe_copy(g_app.result.path, sizeof(g_app.result.path), path);
|
||||
safe_copy(g_app.result.job_id, sizeof(g_app.result.job_id), job_id);
|
||||
safe_copy(g_app.result.printer_uri, sizeof(g_app.result.printer_uri), printer_uri);
|
||||
safe_copy(g_app.result.printer_name, sizeof(g_app.result.printer_name), printer_name);
|
||||
g_app.result.copies = copies;
|
||||
safe_copy(g_app.result.message, sizeof(g_app.result.message), message);
|
||||
dlg::write_result_file(g_app.request.result_path, &g_app.result);
|
||||
g_app.running = false;
|
||||
@@ -1080,35 +1110,149 @@ void draw_file_dialog() {
|
||||
draw_action_button(c, lo.confirm_btn, confirm_label, confirm_enabled, lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
|
||||
}
|
||||
|
||||
void print_refresh(PrintDialogState* st) {
|
||||
st->printer_uri[0] = '\0';
|
||||
st->printer_name[0] = '\0';
|
||||
st->status[0] = '\0';
|
||||
st->can_print = false;
|
||||
int count_dir_entries(const char* path) {
|
||||
DIR* d = opendir(path);
|
||||
if (!d) return 0;
|
||||
|
||||
if (!print::read_default_printer_uri(st->printer_uri, sizeof(st->printer_uri))) {
|
||||
safe_copy(st->status, sizeof(st->status), "No default printer configured");
|
||||
int count = 0;
|
||||
struct dirent* ent = nullptr;
|
||||
while ((ent = readdir(d)) != nullptr) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
count++;
|
||||
}
|
||||
closedir(d);
|
||||
return count;
|
||||
}
|
||||
|
||||
void print_set_error(PrintDialogState* st, const char* msg) {
|
||||
safe_copy(st->error, sizeof(st->error), msg);
|
||||
}
|
||||
|
||||
void print_adjust_copies(PrintDialogState* st, int delta) {
|
||||
if (!st) return;
|
||||
int next = (int)st->copies + delta;
|
||||
if (next < 1) next = 1;
|
||||
if (next > 99) next = 99;
|
||||
st->copies = (uint32_t)next;
|
||||
}
|
||||
|
||||
void print_update_note(PrintDialogState* st) {
|
||||
if (st->printer_count <= 0) {
|
||||
safe_copy(st->note, sizeof(st->note), "No printers discovered from the userspace print spooler yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
print::ProbeState probe = {};
|
||||
if (print::load_printer_probe_state(&probe) && montauk::streq(probe.printer_uri, st->printer_uri)) {
|
||||
if (probe.caps.printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), probe.caps.printer_name);
|
||||
if (probe.message[0]) safe_copy(st->status, sizeof(st->status), probe.message);
|
||||
if (!st->daemon_running) {
|
||||
safe_copy(st->note, sizeof(st->note), "The print spooler is offline. Saved printers are still available.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!st->printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), "Default printer");
|
||||
if (!st->status[0]) safe_copy(st->status, sizeof(st->status), "Uses the system print spooler");
|
||||
|
||||
int fd = montauk::open(st->source_path);
|
||||
if (fd >= 0) {
|
||||
montauk::close(fd);
|
||||
st->can_print = true;
|
||||
if (st->submit_mode) {
|
||||
safe_copy(st->note, sizeof(st->note), "The selected document will be handed to the userspace print spooler.");
|
||||
} else {
|
||||
safe_copy(st->status, sizeof(st->status), "Source document is no longer available");
|
||||
safe_copy(st->note, sizeof(st->note), "This app stores printer settings only for now. Document output wiring will be added later.");
|
||||
}
|
||||
}
|
||||
|
||||
int print_find_printer(const PrintDialogState* st, const char* uri) {
|
||||
if (!st || !uri || !uri[0]) return -1;
|
||||
for (int i = 0; i < st->printer_count; i++) {
|
||||
if (montauk::streq(st->printers[i].uri, uri)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void print_add_printer(PrintDialogState* st,
|
||||
const char* uri,
|
||||
const char* name,
|
||||
const char* status,
|
||||
const char* seen_at,
|
||||
bool ok,
|
||||
bool is_default) {
|
||||
if (!st || !uri || !uri[0]) return;
|
||||
|
||||
int idx = print_find_printer(st, uri);
|
||||
if (idx < 0) {
|
||||
if (st->printer_count >= MAX_DISCOVERED_PRINTERS) return;
|
||||
idx = st->printer_count++;
|
||||
montauk::memset(&st->printers[idx], 0, sizeof(st->printers[idx]));
|
||||
safe_copy(st->printers[idx].uri, sizeof(st->printers[idx].uri), uri);
|
||||
}
|
||||
|
||||
if (name && name[0]) safe_copy(st->printers[idx].name, sizeof(st->printers[idx].name), name);
|
||||
if (status && status[0]) safe_copy(st->printers[idx].status, sizeof(st->printers[idx].status), status);
|
||||
if (seen_at && seen_at[0]) safe_copy(st->printers[idx].seen_at, sizeof(st->printers[idx].seen_at), seen_at);
|
||||
st->printers[idx].ok = ok;
|
||||
st->printers[idx].is_default = is_default;
|
||||
}
|
||||
|
||||
Rect print_row_rect(const PrintDialogLayout& lo, int idx) {
|
||||
return {lo.printers_rect.x + 8, lo.printers_rect.y + 28 + idx * 58, lo.printers_rect.w - 16, 52};
|
||||
}
|
||||
|
||||
bool print_can_confirm(const PrintDialogState* st) {
|
||||
if (!st || st->selected_printer < 0 || st->selected_printer >= st->printer_count) return false;
|
||||
if (!st->submit_mode) return true;
|
||||
return st->source_ready;
|
||||
}
|
||||
|
||||
void print_refresh(PrintDialogState* st) {
|
||||
if (!st) return;
|
||||
|
||||
char preferred_uri[256] = {};
|
||||
if (st->selected_printer >= 0 && st->selected_printer < st->printer_count)
|
||||
safe_copy(preferred_uri, sizeof(preferred_uri), st->printers[st->selected_printer].uri);
|
||||
else if (g_app.request.printer_uri[0])
|
||||
safe_copy(preferred_uri, sizeof(preferred_uri), g_app.request.printer_uri);
|
||||
|
||||
st->printer_count = 0;
|
||||
st->selected_printer = -1;
|
||||
st->error[0] = '\0';
|
||||
st->queue_count = count_dir_entries(print::SPOOL_QUEUE_DIR);
|
||||
st->active_count = count_dir_entries(print::SPOOL_ACTIVE_DIR);
|
||||
|
||||
int pid = -1;
|
||||
st->daemon_running = print::daemon_is_running(&pid);
|
||||
|
||||
print::KnownPrinter known[MAX_DISCOVERED_PRINTERS];
|
||||
int known_count = print::list_known_printers(known, MAX_DISCOVERED_PRINTERS);
|
||||
for (int i = 0; i < known_count; i++) {
|
||||
const char* status = known[i].status_message[0]
|
||||
? known[i].status_message
|
||||
: (known[i].ok ? "Ready for queued jobs" : "Saved printer");
|
||||
print_add_printer(st, known[i].uri, known[i].display_name, status, known[i].last_seen, known[i].ok, known[i].is_default);
|
||||
}
|
||||
|
||||
if (st->printer_count <= 0) {
|
||||
char default_uri[256] = {};
|
||||
bool have_default = print::read_default_printer_uri(default_uri, sizeof(default_uri));
|
||||
print::ProbeState probe = {};
|
||||
bool have_probe = print::load_printer_probe_state(&probe);
|
||||
|
||||
if (have_probe && probe.printer_uri[0]) {
|
||||
const char* status = probe.message[0] ? probe.message
|
||||
: (probe.caps.status_message[0] ? probe.caps.status_message
|
||||
: (probe.ok ? "Ready for queued jobs" : "Saved printer"));
|
||||
print_add_printer(st, probe.printer_uri, probe.caps.printer_name, status, probe.probed_at,
|
||||
probe.ok, have_default && montauk::streq(default_uri, probe.printer_uri));
|
||||
} else if (have_default) {
|
||||
print_add_printer(st, default_uri, "Configured printer", "Saved by the print spooler", "", false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (preferred_uri[0]) st->selected_printer = print_find_printer(st, preferred_uri);
|
||||
if (st->selected_printer < 0) {
|
||||
for (int i = 0; i < st->printer_count; i++) {
|
||||
if (st->printers[i].is_default) {
|
||||
st->selected_printer = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (st->selected_printer < 0 && st->printer_count > 0) st->selected_printer = 0;
|
||||
|
||||
st->source_ready = !st->submit_mode || (st->source_path[0] && path_exists_via_open(st->source_path));
|
||||
print_update_note(st);
|
||||
}
|
||||
|
||||
void init_print_dialog(const dlg::Request& req) {
|
||||
PrintDialogState* st = &g_app.print;
|
||||
montauk::memset(st, 0, sizeof(*st));
|
||||
@@ -1116,6 +1260,8 @@ void init_print_dialog(const dlg::Request& req) {
|
||||
st->printer_icon = svg_load("0:/icons/printer-symbolic.svg", 24, 24, def);
|
||||
safe_copy(st->source_path, sizeof(st->source_path), req.source_path);
|
||||
safe_copy(st->job_name, sizeof(st->job_name), req.job_name);
|
||||
st->submit_mode = req.mode == dlg::PRINT_DIALOG_SUBMIT;
|
||||
st->copies = req.copies > 0 ? req.copies : 1;
|
||||
print_refresh(st);
|
||||
}
|
||||
|
||||
@@ -1124,44 +1270,132 @@ void draw_print_dialog() {
|
||||
Canvas c = g_app.win.canvas();
|
||||
c.fill(colors::WINDOW_BG);
|
||||
PrintDialogLayout lo = print_layout();
|
||||
const auto* selected = (st->selected_printer >= 0 && st->selected_printer < st->printer_count)
|
||||
? &st->printers[st->selected_printer]
|
||||
: nullptr;
|
||||
|
||||
c.fill_rect(0, 0, c.w, 54, Color::from_rgb(0xF7, 0xF7, 0xF7));
|
||||
c.hline(0, 53, c.w, colors::BORDER);
|
||||
if (st->printer_icon.pixels) c.icon(18, 15, st->printer_icon);
|
||||
c.text(54, 16, st->printer_name[0] ? st->printer_name : "Print", colors::TEXT_COLOR);
|
||||
c.fill_rounded_rect(lo.printers_rect.x, lo.printers_rect.y, lo.printers_rect.w, lo.printers_rect.h, 6, colors::WHITE);
|
||||
c.rect(lo.printers_rect.x, lo.printers_rect.y, lo.printers_rect.w, lo.printers_rect.h, colors::BORDER);
|
||||
c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 8, "Printers", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
|
||||
char source_line[256];
|
||||
fit_text_end(path_basename(st->source_path), source_line, sizeof(source_line), c.w - 36);
|
||||
c.text(18, 78, "Document", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
c.text(18, 100, source_line, colors::TEXT_COLOR);
|
||||
if (st->printer_count <= 0) {
|
||||
c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 38, "No printers discovered", colors::TEXT_COLOR);
|
||||
c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 58, "Open Printers to configure one.", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
} else {
|
||||
for (int i = 0; i < st->printer_count; i++) {
|
||||
Rect row = print_row_rect(lo, i);
|
||||
bool selected = i == st->selected_printer;
|
||||
bool hovered = row.contains(st->mouse_x, st->mouse_y);
|
||||
Color bg = selected ? colors::MENU_HOVER
|
||||
: (hovered ? Color::from_rgb(0xF6, 0xF6, 0xF6) : colors::WHITE);
|
||||
c.fill_rounded_rect(row.x, row.y, row.w, row.h, 6, bg);
|
||||
c.rect(row.x, row.y, row.w, row.h, selected ? colors::ACCENT : Color::from_rgb(0xE4, 0xE4, 0xE4));
|
||||
if (st->printer_icon.pixels) c.icon(row.x + 10, row.y + 14, st->printer_icon);
|
||||
|
||||
c.text(18, 136, "Printer", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
char printer_line[256];
|
||||
fit_text_end(st->printer_uri[0] ? st->printer_uri : "Not configured", printer_line, sizeof(printer_line), c.w - 36);
|
||||
c.text(18, 158, printer_line, colors::TEXT_COLOR);
|
||||
char display[128];
|
||||
if (st->printers[i].is_default && st->printers[i].name[0])
|
||||
snprintf(display, sizeof(display), "%s (Default)", st->printers[i].name);
|
||||
else if (st->printers[i].is_default)
|
||||
snprintf(display, sizeof(display), "Default printer");
|
||||
else
|
||||
safe_copy(display, sizeof(display), st->printers[i].name[0] ? st->printers[i].name : st->printers[i].uri);
|
||||
|
||||
char status_line[160];
|
||||
fit_text_end(st->status, status_line, sizeof(status_line), c.w - 36);
|
||||
c.text(18, 190, status_line, st->can_print ? Color::from_rgb(0x66, 0x66, 0x66) : Color::from_rgb(0xC0, 0x33, 0x33));
|
||||
char title[112];
|
||||
fit_text_end(display, title, sizeof(title), row.w - 68);
|
||||
c.text(row.x + 42, row.y + 9, title, colors::TEXT_COLOR);
|
||||
|
||||
char subtitle[256];
|
||||
safe_copy(subtitle, sizeof(subtitle), st->printers[i].uri[0] ? st->printers[i].uri : "Saved printer");
|
||||
fit_text_end(subtitle, title, sizeof(title), row.w - 68);
|
||||
c.text(row.x + 42, row.y + 28, title, Color::from_rgb(0x77, 0x77, 0x77));
|
||||
}
|
||||
}
|
||||
|
||||
c.fill_rounded_rect(lo.detail_rect.x, lo.detail_rect.y, lo.detail_rect.w, lo.detail_rect.h, 6, colors::WHITE);
|
||||
c.rect(lo.detail_rect.x, lo.detail_rect.y, lo.detail_rect.w, lo.detail_rect.h, colors::BORDER);
|
||||
|
||||
int dx = lo.detail_rect.x + 16;
|
||||
int dy = lo.detail_rect.y + 18;
|
||||
c.text(dx, dy, "Document", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
char line[256];
|
||||
const char* job_label = st->job_name[0] ? st->job_name
|
||||
: (st->source_path[0] ? path_basename(st->source_path) : "Untitled document");
|
||||
fit_text_end(job_label, line, sizeof(line), lo.detail_rect.w - 32);
|
||||
c.text(dx, dy + 20, line, colors::TEXT_COLOR);
|
||||
|
||||
dy += 68;
|
||||
c.text(dx, dy, "Printer", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
if (selected) {
|
||||
const char* title = selected->name[0] ? selected->name : selected->uri;
|
||||
fit_text_end(title, line, sizeof(line), lo.detail_rect.w - 32);
|
||||
c.text(dx, dy + 18, line, colors::TEXT_COLOR);
|
||||
|
||||
const char* subtitle = selected->uri[0] ? selected->uri : selected->status;
|
||||
fit_text_end(subtitle, line, sizeof(line), lo.detail_rect.w - 32);
|
||||
c.text(dx, dy + 36, line, Color::from_rgb(0x77, 0x77, 0x77));
|
||||
} else {
|
||||
c.text(dx, dy + 18, "No printer selected", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
}
|
||||
|
||||
dy += 86;
|
||||
c.text(dx, dy, "Copies", Color::from_rgb(0x77, 0x77, 0x77));
|
||||
draw_action_button(c, lo.copy_minus_btn, "-", st->copies > 1,
|
||||
lo.copy_minus_btn.contains(st->mouse_x, st->mouse_y), false);
|
||||
c.fill_rounded_rect(lo.copy_value_rect.x, lo.copy_value_rect.y, lo.copy_value_rect.w, lo.copy_value_rect.h, 4, Color::from_rgb(0xF6, 0xF6, 0xF6));
|
||||
c.rect(lo.copy_value_rect.x, lo.copy_value_rect.y, lo.copy_value_rect.w, lo.copy_value_rect.h, colors::BORDER);
|
||||
snprintf(line, sizeof(line), "%u %s", (unsigned)st->copies, st->copies == 1 ? "copy" : "copies");
|
||||
c.text(lo.copy_value_rect.x + 10, lo.copy_value_rect.y + 7, line, colors::TEXT_COLOR);
|
||||
draw_action_button(c, lo.copy_plus_btn, "+", st->copies < 99,
|
||||
lo.copy_plus_btn.contains(st->mouse_x, st->mouse_y), false);
|
||||
|
||||
if (st->error[0]) {
|
||||
char line[160];
|
||||
fit_text_end(st->error, line, sizeof(line), lo.cancel_btn.x - 24);
|
||||
c.text(16, lo.cancel_btn.y - 22, line, Color::from_rgb(0xC0, 0x33, 0x33));
|
||||
}
|
||||
|
||||
draw_action_button(c, lo.refresh_btn, "Refresh", true, lo.refresh_btn.contains(st->mouse_x, st->mouse_y), false);
|
||||
draw_action_button(c, lo.printers_btn, "Printers", true, lo.printers_btn.contains(st->mouse_x, st->mouse_y), false);
|
||||
draw_action_button(c, lo.cancel_btn, "Cancel", true, lo.cancel_btn.contains(st->mouse_x, st->mouse_y), false);
|
||||
draw_action_button(c, lo.confirm_btn, "Print", st->can_print && st->printer_uri[0], lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
|
||||
draw_action_button(c, lo.confirm_btn, "Print", print_can_confirm(st),
|
||||
lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
|
||||
}
|
||||
|
||||
void print_finish_setup(PrintDialogState* st) {
|
||||
if (!st || st->selected_printer < 0 || st->selected_printer >= st->printer_count) {
|
||||
print_set_error(st, "Choose a printer to continue");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& printer = st->printers[st->selected_printer];
|
||||
dialog_finish("ok", "", "", "Printer selected",
|
||||
printer.uri, printer.name, st->copies);
|
||||
}
|
||||
|
||||
void print_submit(PrintDialogState* st) {
|
||||
if (!st || st->selected_printer < 0 || st->selected_printer >= st->printer_count) {
|
||||
print_set_error(st, "Choose a printer to print");
|
||||
return;
|
||||
}
|
||||
if (!st->source_ready) {
|
||||
print_set_error(st, "Source document is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& printer = st->printers[st->selected_printer];
|
||||
char job_id[64] = {};
|
||||
char err[160] = {};
|
||||
const char* job_name = st->job_name[0] ? st->job_name : path_basename(st->source_path);
|
||||
if (!print::submit_document_job(st->source_path, nullptr, job_name, job_id, sizeof(job_id), err, sizeof(err))) {
|
||||
safe_copy(st->status, sizeof(st->status), err[0] ? err : "Failed to queue print job");
|
||||
if (!print::submit_document_job(st->source_path, printer.uri, job_name,
|
||||
job_id, sizeof(job_id), err, sizeof(err),
|
||||
(int)(st->copies > 0 ? st->copies : 1))) {
|
||||
print_set_error(st, err[0] ? err : "Failed to queue print job");
|
||||
return;
|
||||
}
|
||||
|
||||
char msg[160];
|
||||
snprintf(msg, sizeof(msg), "Queued as %s", job_id);
|
||||
dialog_finish("ok", "", job_id, msg);
|
||||
dialog_finish("ok", "", job_id, msg, printer.uri, printer.name, st->copies);
|
||||
}
|
||||
|
||||
void text_insert(char* buf, int* len, int* cursor, int max_len, char ch) {
|
||||
@@ -1375,8 +1609,27 @@ void handle_print_mouse(const Montauk::WinEvent& ev) {
|
||||
if (lo.cancel_btn.contains(me.x, me.y)) { dialog_cancel(); return; }
|
||||
if (lo.refresh_btn.contains(me.x, me.y)) { print_refresh(st); return; }
|
||||
if (lo.printers_btn.contains(me.x, me.y)) { montauk::spawn("0:/apps/printers/printers.elf"); return; }
|
||||
if (lo.confirm_btn.contains(me.x, me.y) && st->can_print && st->printer_uri[0]) {
|
||||
print_submit(st);
|
||||
if (lo.copy_minus_btn.contains(me.x, me.y) && st->copies > 1) {
|
||||
print_adjust_copies(st, -1);
|
||||
st->error[0] = '\0';
|
||||
return;
|
||||
}
|
||||
if (lo.copy_plus_btn.contains(me.x, me.y) && st->copies < 99) {
|
||||
print_adjust_copies(st, +1);
|
||||
st->error[0] = '\0';
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < st->printer_count; i++) {
|
||||
Rect row = print_row_rect(lo, i);
|
||||
if (row.contains(me.x, me.y)) {
|
||||
st->selected_printer = i;
|
||||
st->error[0] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (lo.confirm_btn.contains(me.x, me.y) && print_can_confirm(st)) {
|
||||
if (st->submit_mode) print_submit(st);
|
||||
else print_finish_setup(st);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1386,11 +1639,34 @@ void handle_print_key(const Montauk::KeyEvent& key) {
|
||||
dialog_cancel();
|
||||
return;
|
||||
}
|
||||
if ((key.ascii == '\n' || key.ascii == '\r') && g_app.print.can_print && g_app.print.printer_uri[0]) {
|
||||
print_submit(&g_app.print);
|
||||
if ((key.ascii == '\n' || key.ascii == '\r') && print_can_confirm(&g_app.print)) {
|
||||
if (g_app.print.submit_mode) print_submit(&g_app.print);
|
||||
else print_finish_setup(&g_app.print);
|
||||
}
|
||||
if (key.ctrl && (key.ascii == 'r' || key.ascii == 'R')) {
|
||||
print_refresh(&g_app.print);
|
||||
return;
|
||||
}
|
||||
if ((key.ascii == '+' || key.ascii == '=') && g_app.print.copies < 99) {
|
||||
print_adjust_copies(&g_app.print, +1);
|
||||
g_app.print.error[0] = '\0';
|
||||
return;
|
||||
}
|
||||
if ((key.ascii == '-' || key.ascii == '_') && g_app.print.copies > 1) {
|
||||
print_adjust_copies(&g_app.print, -1);
|
||||
g_app.print.error[0] = '\0';
|
||||
return;
|
||||
}
|
||||
if ((key.scancode == 0x48 || key.scancode == 0x4B) && g_app.print.printer_count > 0) {
|
||||
if (g_app.print.selected_printer > 0) g_app.print.selected_printer--;
|
||||
else g_app.print.selected_printer = 0;
|
||||
g_app.print.error[0] = '\0';
|
||||
return;
|
||||
}
|
||||
if ((key.scancode == 0x50 || key.scancode == 0x4D) && g_app.print.printer_count > 0) {
|
||||
if (g_app.print.selected_printer < g_app.print.printer_count - 1) g_app.print.selected_printer++;
|
||||
else g_app.print.selected_printer = g_app.print.printer_count - 1;
|
||||
g_app.print.error[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ static void print_job_detail(const JobMeta& job, const char* state, bool verbose
|
||||
|
||||
if (job.printer_uri[0]) printf(" printer: %s\n", job.printer_uri);
|
||||
if (job.doc_format[0]) printf(" format: %s\n", job.doc_format);
|
||||
if (job.copies > 1) printf(" copies: %d\n", job.copies);
|
||||
if (job.source_name[0]) printf(" source: %s\n", job.source_name);
|
||||
if (job.remote_job_id > 0) printf(" printer job id: %d\n", job.remote_job_id);
|
||||
if (job.status_message[0]) printf(" status: %s\n", job.status_message);
|
||||
|
||||
+121
-33
@@ -15,8 +15,30 @@ extern "C" {
|
||||
|
||||
using namespace print;
|
||||
|
||||
static constexpr uint64_t PROBE_REFRESH_OK_MS = 60000;
|
||||
static constexpr uint64_t PROBE_REFRESH_FAIL_MS = 15000;
|
||||
static constexpr bool ENABLE_BACKGROUND_PROBE = false;
|
||||
|
||||
enum class ClaimResult {
|
||||
None,
|
||||
Claimed,
|
||||
Failed,
|
||||
};
|
||||
|
||||
static void write_daemon_state(const char* phase, const char* subject = nullptr,
|
||||
const char* detail = nullptr) {
|
||||
char now[32] = {};
|
||||
now_string(now, sizeof(now));
|
||||
|
||||
char buf[512];
|
||||
int n = snprintf(buf, sizeof(buf),
|
||||
"time=%s\npid=%d\nphase=%s\nsubject=%s\ndetail=%s\n",
|
||||
now,
|
||||
montauk::getpid(),
|
||||
phase ? phase : "",
|
||||
subject ? subject : "",
|
||||
detail ? detail : "");
|
||||
if (n < 0) return;
|
||||
write_text_file_atomic(SPOOL_DAEMON_STATE_PATH, buf);
|
||||
}
|
||||
|
||||
static void log_msg(const char* fmt, ...) {
|
||||
va_list ap;
|
||||
@@ -68,6 +90,41 @@ static bool write_pid_file() {
|
||||
return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid);
|
||||
}
|
||||
|
||||
static bool move_job_file(const char* src, const char* dst, char* err = nullptr, int err_len = 0) {
|
||||
if (err && err_len > 0) err[0] = '\0';
|
||||
if (src == nullptr || dst == nullptr || *src == '\0' || *dst == '\0') {
|
||||
safe_copy(err, err_len, "invalid source or destination path");
|
||||
return false;
|
||||
}
|
||||
if (rename(src, dst) == 0) return true;
|
||||
|
||||
uint8_t* data = nullptr;
|
||||
int len = 0;
|
||||
char io_err[128] = {};
|
||||
if (!read_file_bytes(src, &data, &len, io_err, sizeof(io_err))) {
|
||||
char detail[160];
|
||||
snprintf(detail, sizeof(detail), "rename failed; could not read source (%s)", io_err);
|
||||
safe_copy(err, err_len, detail);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = write_file_atomic(dst, data, len, io_err, sizeof(io_err));
|
||||
free(data);
|
||||
if (!ok) {
|
||||
char detail[160];
|
||||
snprintf(detail, sizeof(detail), "rename failed; could not write destination (%s)", io_err);
|
||||
safe_copy(err, err_len, detail);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remove(src) != 0) {
|
||||
remove(dst);
|
||||
safe_copy(err, err_len, "rename failed; copied destination but could not delete source");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void auto_probe_configured_printer(char* last_uri, int last_uri_len,
|
||||
uint64_t* last_probe_ms, bool* last_probe_ok,
|
||||
bool force = false) {
|
||||
@@ -90,8 +147,7 @@ static void auto_probe_configured_printer(char* last_uri, int last_uri_len,
|
||||
}
|
||||
|
||||
bool uri_changed = strcmp(last_uri, current_uri) != 0;
|
||||
uint64_t refresh_ms = *last_probe_ok ? PROBE_REFRESH_OK_MS : PROBE_REFRESH_FAIL_MS;
|
||||
if (!force && !uri_changed && *last_probe_ms != 0 && now - *last_probe_ms < refresh_ms)
|
||||
if (!force && !uri_changed && *last_probe_ms != 0)
|
||||
return;
|
||||
|
||||
ProbeState state = {};
|
||||
@@ -136,42 +192,59 @@ static void requeue_stale_active_jobs() {
|
||||
set_job_state(&job, "queued", "Re-queued after daemon restart");
|
||||
save_job_to_path_atomic(src, &job);
|
||||
}
|
||||
rename(src, dst);
|
||||
char move_err[160] = {};
|
||||
if (!move_job_file(src, dst, move_err, sizeof(move_err))) {
|
||||
log_msg("warning: failed to requeue stale job %s: %s",
|
||||
ent->d_name, move_err[0] ? move_err : "move failed");
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
static bool claim_next_job(char* out_active_path, int out_active_path_len) {
|
||||
static ClaimResult claim_next_job(char* out_active_path, int out_active_path_len) {
|
||||
DIR* dir = opendir(SPOOL_QUEUE_DIR);
|
||||
if (!dir) return false;
|
||||
if (!dir) return ClaimResult::None;
|
||||
|
||||
bool claimed = false;
|
||||
char best_name[64] = {};
|
||||
struct dirent* ent = nullptr;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
|
||||
char queued[MAX_PATH_LEN];
|
||||
char active[MAX_PATH_LEN];
|
||||
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, ent->d_name);
|
||||
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, ent->d_name);
|
||||
|
||||
if (rename(queued, active) == 0) {
|
||||
safe_copy(out_active_path, out_active_path_len, active);
|
||||
claimed = true;
|
||||
break;
|
||||
}
|
||||
if (best_name[0] == '\0' || strcmp(ent->d_name, best_name) < 0)
|
||||
safe_copy(best_name, sizeof(best_name), ent->d_name);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return claimed;
|
||||
if (best_name[0] == '\0') return ClaimResult::None;
|
||||
|
||||
char queued[MAX_PATH_LEN];
|
||||
char active[MAX_PATH_LEN];
|
||||
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, best_name);
|
||||
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, best_name);
|
||||
|
||||
write_daemon_state("claiming", best_name, queued);
|
||||
char move_err[160] = {};
|
||||
if (!move_job_file(queued, active, move_err, sizeof(move_err))) {
|
||||
char detail[256];
|
||||
snprintf(detail, sizeof(detail), "%s -> %s (%s)",
|
||||
queued, active,
|
||||
move_err[0] ? move_err : "move failed");
|
||||
write_daemon_state("claim-failed", best_name, detail);
|
||||
log_msg("failed to claim job %s: %s", best_name, detail);
|
||||
return ClaimResult::Failed;
|
||||
}
|
||||
write_daemon_state("claimed", best_name, active);
|
||||
safe_copy(out_active_path, out_active_path_len, active);
|
||||
return ClaimResult::Claimed;
|
||||
}
|
||||
|
||||
static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) {
|
||||
save_job_to_path_atomic(active_path, job);
|
||||
char target[MAX_PATH_LEN];
|
||||
make_job_file_path(target_dir, job->id, target, sizeof(target));
|
||||
if (rename(active_path, target) != 0) {
|
||||
char move_err[160] = {};
|
||||
if (!move_job_file(active_path, target, move_err, sizeof(move_err))) {
|
||||
log_msg("warning: failed to move job %s to %s", job->id, target_dir);
|
||||
write_daemon_state("finalize-failed", job->id,
|
||||
move_err[0] ? move_err : target_dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,12 +296,14 @@ static bool try_print_test_page(const char* active_path, JobMeta* job,
|
||||
}
|
||||
|
||||
char detail[MAX_DEBUG_LEN];
|
||||
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes)",
|
||||
attempts[i].mime, len);
|
||||
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes, %d %s)",
|
||||
attempts[i].mime, len,
|
||||
job->copies > 0 ? job->copies : 1,
|
||||
(job->copies > 1) ? "copies" : "copy");
|
||||
persist_job_debug(active_path, job, detail);
|
||||
|
||||
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
|
||||
attempts[i].mime, data, len, result, err, err_len);
|
||||
job->copies, attempts[i].mime, data, len, result, err, err_len);
|
||||
free(data);
|
||||
if (ok) return true;
|
||||
}
|
||||
@@ -245,12 +320,14 @@ static bool try_print_document(const char* active_path, JobMeta* job,
|
||||
if (!read_file_bytes(job->doc_path, &data, &len, err, err_len)) return false;
|
||||
|
||||
char detail[MAX_DEBUG_LEN];
|
||||
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes)",
|
||||
job->doc_format[0] ? job->doc_format : "application/octet-stream", len);
|
||||
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes, %d %s)",
|
||||
job->doc_format[0] ? job->doc_format : "application/octet-stream", len,
|
||||
job->copies > 0 ? job->copies : 1,
|
||||
(job->copies > 1) ? "copies" : "copy");
|
||||
persist_job_debug(active_path, job, detail);
|
||||
|
||||
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
|
||||
job->doc_format, data, len, result, err, err_len);
|
||||
job->copies, job->doc_format, data, len, result, err, err_len);
|
||||
free(data);
|
||||
return ok;
|
||||
}
|
||||
@@ -259,11 +336,13 @@ static void process_job(const char* active_path) {
|
||||
JobMeta job = {};
|
||||
if (!load_job_from_path(active_path, &job)) {
|
||||
log_msg("dropping unreadable job file: %s", active_path);
|
||||
write_daemon_state("unreadable-job", active_path, nullptr);
|
||||
remove(active_path);
|
||||
return;
|
||||
}
|
||||
|
||||
log_msg("processing job %s (%s)", job.id, job.job_name);
|
||||
write_daemon_state("processing", job.id, job.job_name);
|
||||
set_job_state(&job, "printing", "Sending to printer");
|
||||
set_job_debug(&job, "dispatching queued job");
|
||||
save_job_to_path_atomic(active_path, &job);
|
||||
@@ -284,12 +363,14 @@ static void process_job(const char* active_path) {
|
||||
set_job_debug_from_result(&job, &result, nullptr);
|
||||
finalize_job(active_path, SPOOL_DONE_DIR, &job);
|
||||
log_msg("job %s completed (printer job id %d)", job.id, result.job_id);
|
||||
write_daemon_state("completed", job.id, job.status_message);
|
||||
} else {
|
||||
set_job_state(&job, "failed", err[0] ? err : "Print failed");
|
||||
set_job_debug_from_result(&job, &result, err[0] ? err : nullptr);
|
||||
finalize_job(active_path, SPOOL_FAILED_DIR, &job);
|
||||
log_msg("job %s failed: %s", job.id, job.status_message);
|
||||
if (job.debug_info[0]) log_msg("job %s debug: %s", job.id, job.debug_info);
|
||||
write_daemon_state("failed", job.id, job.status_message);
|
||||
}
|
||||
|
||||
if (job.doc_path[0] != '\0') remove(job.doc_path);
|
||||
@@ -316,18 +397,25 @@ extern "C" void _start() {
|
||||
char last_probed_uri[MAX_PATH_LEN] = {};
|
||||
uint64_t last_probe_ms = 0;
|
||||
bool last_probe_ok = false;
|
||||
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
|
||||
&last_probe_ms, &last_probe_ok, true);
|
||||
log_msg("ready");
|
||||
write_daemon_state("ready", nullptr, nullptr);
|
||||
|
||||
for (;;) {
|
||||
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
|
||||
&last_probe_ms, &last_probe_ok);
|
||||
char active_path[MAX_PATH_LEN];
|
||||
if (claim_next_job(active_path, sizeof(active_path))) {
|
||||
ClaimResult claim = claim_next_job(active_path, sizeof(active_path));
|
||||
if (claim == ClaimResult::Claimed) {
|
||||
process_job(active_path);
|
||||
continue;
|
||||
}
|
||||
if (claim == ClaimResult::Failed) {
|
||||
montauk::sleep_ms(1000);
|
||||
continue;
|
||||
}
|
||||
write_daemon_state("idle", nullptr, nullptr);
|
||||
if (ENABLE_BACKGROUND_PROBE) {
|
||||
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
|
||||
&last_probe_ms, &last_probe_ok);
|
||||
}
|
||||
montauk::sleep_ms(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ CXXFLAGS := \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I ../../lib/bearssl/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
@@ -53,8 +54,9 @@ LDFLAGS := \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp document.cpp render.cpp input.cpp stb_truetype_impl.cpp
|
||||
SRCS := main.cpp document.cpp render.cpp input.cpp print_export.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
LIBS := $(LIBDIR)/tls/libtls.a $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libjpegwrite/libjpegwrite.a $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
|
||||
|
||||
@@ -62,9 +64,9 @@ TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(BINDIR)/apps/wordprocessor
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -419,6 +419,10 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp->modified = false;
|
||||
wp->filepath[0] = '\0';
|
||||
wp->filename[0] = '\0';
|
||||
wp->preferred_printer_uri[0] = '\0';
|
||||
wp->preferred_printer_name[0] = '\0';
|
||||
wp->preferred_print_copies = 1;
|
||||
wp->status_msg[0] = '\0';
|
||||
|
||||
wp->show_pathbar = false;
|
||||
wp->pathbar_save_mode = false;
|
||||
@@ -429,6 +433,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp->line_spacing_dropdown_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
|
||||
wp->undo_count = 0;
|
||||
wp->undo_pos = 0;
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include "wordprocessor.hpp"
|
||||
#include <gui/dialogs.hpp>
|
||||
|
||||
static void wp_set_status(WordProcessorState* wp, const char* msg) {
|
||||
if (!msg) msg = "";
|
||||
montauk::strncpy(wp->status_msg, msg, (int)sizeof(wp->status_msg) - 1);
|
||||
wp->status_msg[sizeof(wp->status_msg) - 1] = '\0';
|
||||
}
|
||||
|
||||
static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
char path[256] = {};
|
||||
char msg[160] = {};
|
||||
@@ -17,6 +23,12 @@ static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
}
|
||||
}
|
||||
|
||||
static void wp_open_print_dialog(WordProcessorState* wp) {
|
||||
char msg[160] = {};
|
||||
if (wp_print_document(wp, msg, sizeof(msg)) || msg[0])
|
||||
wp_set_status(wp, msg);
|
||||
}
|
||||
|
||||
static void wp_commit_pathbar(WordProcessorState* wp) {
|
||||
if (!wp->pathbar_text[0]) return;
|
||||
if (wp->pathbar_save_mode) {
|
||||
@@ -28,10 +40,23 @@ static void wp_commit_pathbar(WordProcessorState* wp) {
|
||||
wp->show_pathbar = false;
|
||||
}
|
||||
|
||||
static int wp_special_char_flyout_x() {
|
||||
int dx = WP_BTN_SECTION_X + 24 - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
if (dx + WP_SPECIAL_CHAR_FLYOUT_W > g_win_w) dx = g_win_w - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
return dx < 0 ? 0 : dx;
|
||||
}
|
||||
|
||||
static void wp_close_dropdowns(WordProcessorState* wp) {
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp->line_spacing_dropdown_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
}
|
||||
|
||||
static void wp_insert_special_char(WordProcessorState* wp, uint8_t ch) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
wp_insert_char(wp, (char)ch);
|
||||
wp_history_checkpoint(wp);
|
||||
}
|
||||
|
||||
static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, int edit_y) {
|
||||
@@ -148,6 +173,19 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->special_char_flyout_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
int dx = wp_special_char_flyout_x();
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dh = WP_SPECIAL_CHAR_OPTION_COUNT * WP_SPECIAL_CHAR_ROW_H + 4;
|
||||
if (local_x >= dx && local_x < dx + WP_SPECIAL_CHAR_FLYOUT_W && local_y >= dy && local_y < dy + dh) {
|
||||
int idx = (local_y - dy - 2) / WP_SPECIAL_CHAR_ROW_H;
|
||||
if (idx >= 0 && idx < WP_SPECIAL_CHAR_OPTION_COUNT)
|
||||
wp_insert_special_char(wp, WP_SPECIAL_CHAR_OPTIONS[idx].code);
|
||||
}
|
||||
wp->special_char_flyout_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) && local_y < WP_TOOLBAR_H) {
|
||||
if (local_x >= WP_BTN_OPEN_X && local_x < WP_BTN_OPEN_X + 24 && local_y >= 6 && local_y < 30) {
|
||||
wp_open_pathbar_for_open(wp);
|
||||
@@ -157,6 +195,10 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
wp_save_file(wp);
|
||||
return;
|
||||
}
|
||||
if (local_x >= WP_BTN_PRINT_X && local_x < WP_BTN_PRINT_X + 24 && local_y >= 6 && local_y < 30) {
|
||||
wp_open_print_dialog(wp);
|
||||
return;
|
||||
}
|
||||
if (local_x >= WP_BTN_UNDO_X && local_x < WP_BTN_UNDO_X + 24 && local_y >= 6 && local_y < 30) {
|
||||
wp_close_dropdowns(wp);
|
||||
wp_undo(wp);
|
||||
@@ -237,12 +279,13 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
wp->line_spacing_dropdown_open = !wp->line_spacing_dropdown_open;
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
return;
|
||||
}
|
||||
if (local_x >= WP_BTN_SECTION_X && local_x < WP_BTN_SECTION_X + 24 && local_y >= 6 && local_y < 30) {
|
||||
bool open = !wp->special_char_flyout_open;
|
||||
wp_close_dropdowns(wp);
|
||||
wp_insert_char(wp, (char)0xA7);
|
||||
wp_history_checkpoint(wp);
|
||||
wp->special_char_flyout_open = open;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
@@ -349,6 +392,10 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
|
||||
wp_open_pathbar_for_open(wp);
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && (key.ascii == 'p' || key.ascii == 'P')) {
|
||||
wp_open_print_dialog(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl && !key.alt &&
|
||||
(key.ascii == 'z' || key.ascii == 'Z')) {
|
||||
|
||||
@@ -13,6 +13,7 @@ WordProcessorState g_wp = {};
|
||||
WPFontTable g_wp_fonts = { {{nullptr}}, false };
|
||||
SvgIcon g_icon_folder = {};
|
||||
SvgIcon g_icon_save = {};
|
||||
SvgIcon g_icon_print = {};
|
||||
SvgIcon g_icon_undo = {};
|
||||
SvgIcon g_icon_redo = {};
|
||||
SvgIcon g_icon_align_left = {};
|
||||
@@ -31,6 +32,8 @@ void wp_load_icons() {
|
||||
g_icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def_color);
|
||||
if (!g_icon_save.pixels)
|
||||
g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, def_color);
|
||||
if (!g_icon_print.pixels)
|
||||
g_icon_print = svg_load("0:/icons/printer-symbolic.svg", 16, 16, def_color);
|
||||
if (!g_icon_undo.pixels)
|
||||
g_icon_undo = svg_load("0:/icons/edit-undo-symbolic.svg", 16, 16, def_color);
|
||||
if (!g_icon_redo.pixels)
|
||||
@@ -55,6 +58,7 @@ void wp_cleanup_state() {
|
||||
wp_free_document(&g_wp);
|
||||
if (g_icon_folder.pixels) svg_free(g_icon_folder);
|
||||
if (g_icon_save.pixels) svg_free(g_icon_save);
|
||||
if (g_icon_print.pixels) svg_free(g_icon_print);
|
||||
if (g_icon_undo.pixels) svg_free(g_icon_undo);
|
||||
if (g_icon_redo.pixels) svg_free(g_icon_redo);
|
||||
if (g_icon_align_left.pixels) svg_free(g_icon_align_left);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* print_export.cpp
|
||||
* Word Processor print export and job submission
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "wordprocessor.hpp"
|
||||
#include <gui/dialogs.hpp>
|
||||
#include <print/print.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include <gui/stb_image_write.h>
|
||||
}
|
||||
|
||||
namespace dialogs = gui::dialogs;
|
||||
|
||||
static constexpr int WP_PRINT_PAGE_W = 1275; // 8.5in at 150 DPI
|
||||
static constexpr int WP_PRINT_PAGE_H = 1650; // 11in at 150 DPI
|
||||
static constexpr int WP_PRINT_MARGIN_X = 96;
|
||||
static constexpr int WP_PRINT_MARGIN_Y = 96;
|
||||
static constexpr int WP_PRINT_JPEG_QUALITY = 92;
|
||||
|
||||
struct WpJpegBuffer {
|
||||
uint8_t* data;
|
||||
int size;
|
||||
int capacity;
|
||||
bool failed;
|
||||
};
|
||||
|
||||
static void wp_status_copy(char* out, int out_len, const char* text) {
|
||||
if (!out || out_len <= 0) return;
|
||||
if (!text) text = "";
|
||||
montauk::strncpy(out, text, out_len - 1);
|
||||
out[out_len - 1] = '\0';
|
||||
}
|
||||
|
||||
static void wp_jpeg_write_callback(void* context, void* data, int size) {
|
||||
WpJpegBuffer* buf = (WpJpegBuffer*)context;
|
||||
if (!buf || !data || size <= 0 || buf->failed) return;
|
||||
|
||||
int needed = buf->size + size;
|
||||
if (needed > buf->capacity) {
|
||||
int new_cap = buf->capacity > 0 ? buf->capacity : 256 * 1024;
|
||||
while (new_cap < needed)
|
||||
new_cap *= 2;
|
||||
uint8_t* grown = (uint8_t*)montauk::realloc(buf->data, new_cap);
|
||||
if (!grown) {
|
||||
buf->failed = true;
|
||||
return;
|
||||
}
|
||||
buf->data = grown;
|
||||
buf->capacity = new_cap;
|
||||
}
|
||||
|
||||
montauk::memcpy(buf->data + buf->size, data, size);
|
||||
buf->size += size;
|
||||
}
|
||||
|
||||
static int wp_print_layout_width() {
|
||||
return (WP_PRINT_PAGE_W - WP_PRINT_MARGIN_X * 2) + WP_MARGIN * 2 + WP_SCROLLBAR_W;
|
||||
}
|
||||
|
||||
static void wp_make_base_print_name(WordProcessorState* wp, char* out, int out_len) {
|
||||
const char* src = wp->filename[0] ? wp->filename : "document";
|
||||
int last_dot = -1;
|
||||
for (int i = 0; src[i]; i++) {
|
||||
if (src[i] == '.') last_dot = i;
|
||||
}
|
||||
|
||||
if (last_dot <= 0) {
|
||||
wp_status_copy(out, out_len, src);
|
||||
return;
|
||||
}
|
||||
|
||||
int copy_len = last_dot;
|
||||
if (copy_len >= out_len) copy_len = out_len - 1;
|
||||
montauk::memcpy(out, src, (uint64_t)copy_len);
|
||||
out[copy_len] = '\0';
|
||||
}
|
||||
|
||||
static bool wp_printer_accepts_jpeg(const char* printer_uri, char* err, int err_len) {
|
||||
print::ProbeState probe = {};
|
||||
char probe_err[160] = {};
|
||||
if (!print::probe_printer_uri(printer_uri, &probe, probe_err, sizeof(probe_err)))
|
||||
return true;
|
||||
if (probe.caps.supports_jpeg)
|
||||
return true;
|
||||
|
||||
if (probe.caps.supported_formats[0]) {
|
||||
snprintf(err, (size_t)err_len,
|
||||
"Selected printer does not advertise JPEG support (supports: %s)",
|
||||
probe.caps.supported_formats);
|
||||
} else {
|
||||
wp_status_copy(err, err_len, "Selected printer does not advertise JPEG support");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool wp_build_print_page_map(WordProcessorState* wp,
|
||||
int** out_line_pages,
|
||||
int** out_line_y,
|
||||
int* out_page_count,
|
||||
char* err, int err_len) {
|
||||
if (out_line_pages) *out_line_pages = nullptr;
|
||||
if (out_line_y) *out_line_y = nullptr;
|
||||
if (out_page_count) *out_page_count = 0;
|
||||
|
||||
wp_recompute_wrap(wp, wp_print_layout_width());
|
||||
if (wp->wrap_line_count <= 0) {
|
||||
wp_status_copy(err, err_len, "failed to paginate document");
|
||||
return false;
|
||||
}
|
||||
|
||||
int* line_pages = (int*)montauk::malloc((uint64_t)wp->wrap_line_count * sizeof(int));
|
||||
int* line_y = (int*)montauk::malloc((uint64_t)wp->wrap_line_count * sizeof(int));
|
||||
if (!line_pages || !line_y) {
|
||||
if (line_pages) montauk::mfree(line_pages);
|
||||
if (line_y) montauk::mfree(line_y);
|
||||
wp_status_copy(err, err_len, "out of memory while preparing print pages");
|
||||
return false;
|
||||
}
|
||||
|
||||
int base_y = wp->wrap_lines[0].y;
|
||||
int printable_bottom = WP_PRINT_PAGE_H - WP_PRINT_MARGIN_Y;
|
||||
int current_page = 0;
|
||||
int current_y = WP_PRINT_MARGIN_Y;
|
||||
int prev_rel_bottom = 0;
|
||||
|
||||
for (int i = 0; i < wp->wrap_line_count; i++) {
|
||||
WrapLine* wl = &wp->wrap_lines[i];
|
||||
int rel_y = wl->y - base_y;
|
||||
if (rel_y < 0) rel_y = 0;
|
||||
|
||||
int gap_before = (i == 0) ? 0 : (rel_y - prev_rel_bottom);
|
||||
if (gap_before < 0) gap_before = 0;
|
||||
|
||||
int line_top = current_y + gap_before;
|
||||
if (line_top + wl->height > printable_bottom && current_y > WP_PRINT_MARGIN_Y) {
|
||||
current_page++;
|
||||
current_y = WP_PRINT_MARGIN_Y;
|
||||
line_top = current_y;
|
||||
}
|
||||
|
||||
line_pages[i] = current_page;
|
||||
line_y[i] = line_top;
|
||||
current_y = line_top + wl->height;
|
||||
prev_rel_bottom = rel_y + wl->height;
|
||||
}
|
||||
|
||||
if (out_line_pages) *out_line_pages = line_pages;
|
||||
else montauk::mfree(line_pages);
|
||||
if (out_line_y) *out_line_y = line_y;
|
||||
else montauk::mfree(line_y);
|
||||
if (out_page_count) *out_page_count = current_page + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, int py) {
|
||||
if (!wl->first_in_paragraph || wl->paragraph_idx < 0 || wl->paragraph_idx >= wp->paragraph_count)
|
||||
return;
|
||||
|
||||
ParagraphStyle* para = &wp->paragraphs[wl->paragraph_idx];
|
||||
if (para->list_type == PARA_LIST_NONE) return;
|
||||
|
||||
int marker_x = WP_PRINT_MARGIN_X + para->left_indent + para->first_line_indent;
|
||||
int min_x = WP_PRINT_MARGIN_X - WP_MARGIN + 4;
|
||||
if (marker_x < min_x) marker_x = min_x;
|
||||
|
||||
if (para->list_type == PARA_LIST_BULLET) {
|
||||
fill_circle(c, marker_x + 7, py + wl->height / 2, 3, colors::BLACK);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->run_count <= 0) return;
|
||||
StyledRun* run = &wp->runs[wl->run_idx < wp->run_count ? wl->run_idx : 0];
|
||||
TrueTypeFont* font = wp_get_font(run->font_id, run->flags);
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%d.", wl->list_number > 0 ? wl->list_number : 1);
|
||||
int top_y = py + (wl->height - run->size) / 2;
|
||||
draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::BLACK, run->size);
|
||||
}
|
||||
|
||||
static bool wp_encode_print_page(WordProcessorState* wp,
|
||||
const int* line_pages,
|
||||
const int* line_y,
|
||||
int page_index,
|
||||
uint8_t** out_data,
|
||||
int* out_len,
|
||||
char* err, int err_len) {
|
||||
if (out_data) *out_data = nullptr;
|
||||
if (out_len) *out_len = 0;
|
||||
|
||||
uint64_t pixel_bytes = (uint64_t)WP_PRINT_PAGE_W * WP_PRINT_PAGE_H * sizeof(uint32_t);
|
||||
uint32_t* pixels = (uint32_t*)montauk::malloc(pixel_bytes);
|
||||
if (!pixels) {
|
||||
wp_status_copy(err, err_len, "out of memory while rendering print page");
|
||||
return false;
|
||||
}
|
||||
|
||||
Canvas c(pixels, WP_PRINT_PAGE_W, WP_PRINT_PAGE_H);
|
||||
c.fill(colors::WHITE);
|
||||
|
||||
for (int li = 0; li < wp->wrap_line_count; li++) {
|
||||
if (line_pages[li] != page_index) continue;
|
||||
|
||||
WrapLine* wl = &wp->wrap_lines[li];
|
||||
int py = line_y[li];
|
||||
wp_draw_print_list_marker(c, wp, wl, py);
|
||||
|
||||
int chars_left = wl->char_count;
|
||||
int ri = wl->run_idx;
|
||||
int ro = wl->run_offset;
|
||||
int x = WP_PRINT_MARGIN_X + (wl->x - WP_MARGIN);
|
||||
|
||||
int line_abs_start = wp_wrap_line_start(wp, li);
|
||||
int char_idx = 0;
|
||||
|
||||
while (chars_left > 0 && ri < wp->run_count) {
|
||||
StyledRun* r = &wp->runs[ri];
|
||||
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
|
||||
if (!font || !font->valid) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
GlyphCache* gc = font->get_cache(r->size);
|
||||
int baseline = py + wl->baseline;
|
||||
|
||||
int avail = r->len - ro;
|
||||
int to_draw = avail < chars_left ? avail : chars_left;
|
||||
|
||||
for (int ci = 0; ci < to_draw; ci++) {
|
||||
char ch = r->text[ro + ci];
|
||||
(void)line_abs_start;
|
||||
(void)char_idx;
|
||||
|
||||
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
||||
int adv = font->draw_char_to_buffer(
|
||||
c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, colors::BLACK, gc);
|
||||
x += adv;
|
||||
}
|
||||
char_idx++;
|
||||
}
|
||||
|
||||
chars_left -= to_draw;
|
||||
ro += to_draw;
|
||||
if (ro >= r->len) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t rgb_bytes = (uint64_t)WP_PRINT_PAGE_W * WP_PRINT_PAGE_H * 3;
|
||||
uint8_t* rgb = (uint8_t*)montauk::malloc(rgb_bytes);
|
||||
if (!rgb) {
|
||||
montauk::mfree(pixels);
|
||||
wp_status_copy(err, err_len, "out of memory while encoding print page");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < WP_PRINT_PAGE_H; y++) {
|
||||
uint8_t* dst = rgb + (uint64_t)y * WP_PRINT_PAGE_W * 3;
|
||||
for (int x = 0; x < WP_PRINT_PAGE_W; x++) {
|
||||
uint32_t px = pixels[(uint64_t)y * WP_PRINT_PAGE_W + x];
|
||||
dst[x * 3 + 0] = (uint8_t)((px >> 16) & 0xFF);
|
||||
dst[x * 3 + 1] = (uint8_t)((px >> 8) & 0xFF);
|
||||
dst[x * 3 + 2] = (uint8_t)(px & 0xFF);
|
||||
}
|
||||
}
|
||||
montauk::mfree(pixels);
|
||||
|
||||
WpJpegBuffer jpeg = {};
|
||||
jpeg.capacity = 256 * 1024;
|
||||
jpeg.data = (uint8_t*)montauk::malloc(jpeg.capacity);
|
||||
if (!jpeg.data) {
|
||||
montauk::mfree(rgb);
|
||||
wp_status_copy(err, err_len, "out of memory for JPEG output");
|
||||
return false;
|
||||
}
|
||||
|
||||
int ok = stbi_write_jpg_to_func(wp_jpeg_write_callback, &jpeg,
|
||||
WP_PRINT_PAGE_W, WP_PRINT_PAGE_H, 3, rgb,
|
||||
WP_PRINT_JPEG_QUALITY);
|
||||
montauk::mfree(rgb);
|
||||
|
||||
if (!ok || jpeg.failed || jpeg.size <= 0) {
|
||||
montauk::mfree(jpeg.data);
|
||||
wp_status_copy(err, err_len, "JPEG encoding failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out_data) *out_data = jpeg.data;
|
||||
else montauk::mfree(jpeg.data);
|
||||
if (out_len) *out_len = jpeg.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len) {
|
||||
if (out_status && out_status_len > 0) out_status[0] = '\0';
|
||||
if (!wp) {
|
||||
wp_status_copy(out_status, out_status_len, "No document loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
char printer_uri[256] = {};
|
||||
char printer_name[128] = {};
|
||||
char msg[160] = {};
|
||||
montauk::strncpy(printer_uri, wp->preferred_printer_uri, (int)sizeof(printer_uri) - 1);
|
||||
printer_uri[sizeof(printer_uri) - 1] = '\0';
|
||||
montauk::strncpy(printer_name, wp->preferred_printer_name, (int)sizeof(printer_name) - 1);
|
||||
printer_name[sizeof(printer_name) - 1] = '\0';
|
||||
uint32_t copies = wp->preferred_print_copies > 0 ? wp->preferred_print_copies : 1;
|
||||
|
||||
const char* job_name = wp->filename[0] ? wp->filename : "Untitled document";
|
||||
if (!dialogs::configure_print("Print Document",
|
||||
printer_uri, job_name,
|
||||
printer_uri, sizeof(printer_uri),
|
||||
printer_name, sizeof(printer_name),
|
||||
&copies, msg, sizeof(msg))) {
|
||||
if (msg[0]) wp_status_copy(out_status, out_status_len, msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
montauk::strncpy(wp->preferred_printer_uri, printer_uri, (int)sizeof(wp->preferred_printer_uri) - 1);
|
||||
wp->preferred_printer_uri[sizeof(wp->preferred_printer_uri) - 1] = '\0';
|
||||
montauk::strncpy(wp->preferred_printer_name, printer_name, (int)sizeof(wp->preferred_printer_name) - 1);
|
||||
wp->preferred_printer_name[sizeof(wp->preferred_printer_name) - 1] = '\0';
|
||||
wp->preferred_print_copies = copies > 0 ? copies : 1;
|
||||
|
||||
char err[192] = {};
|
||||
if (!wp_printer_accepts_jpeg(printer_uri, err, sizeof(err))) {
|
||||
wp_status_copy(out_status, out_status_len, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
int* line_pages = nullptr;
|
||||
int* line_y = nullptr;
|
||||
int page_count = 0;
|
||||
if (!wp_build_print_page_map(wp, &line_pages, &line_y, &page_count, err, sizeof(err))) {
|
||||
wp_status_copy(out_status, out_status_len, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
char base_name[96] = {};
|
||||
wp_make_base_print_name(wp, base_name, sizeof(base_name));
|
||||
if (!base_name[0]) wp_status_copy(base_name, sizeof(base_name), "document");
|
||||
|
||||
int submitted_pages = 0;
|
||||
char first_job_id[64] = {};
|
||||
|
||||
for (int page = 0; page < page_count; page++) {
|
||||
uint8_t* jpeg = nullptr;
|
||||
int jpeg_len = 0;
|
||||
if (!wp_encode_print_page(wp, line_pages, line_y, page, &jpeg, &jpeg_len, err, sizeof(err))) {
|
||||
break;
|
||||
}
|
||||
|
||||
char page_job_name[128];
|
||||
char source_name[128];
|
||||
if (page_count > 1) {
|
||||
snprintf(page_job_name, sizeof(page_job_name), "%s (Page %d/%d)", job_name, page + 1, page_count);
|
||||
snprintf(source_name, sizeof(source_name), "%s-page-%03d.jpg", base_name, page + 1);
|
||||
} else {
|
||||
wp_status_copy(page_job_name, sizeof(page_job_name), job_name);
|
||||
snprintf(source_name, sizeof(source_name), "%s.jpg", base_name);
|
||||
}
|
||||
|
||||
char job_id[64] = {};
|
||||
bool ok = print::submit_document_buffer_job(
|
||||
jpeg, jpeg_len,
|
||||
source_name,
|
||||
"image/jpeg",
|
||||
printer_uri,
|
||||
page_job_name,
|
||||
(int)(copies > 0 ? copies : 1),
|
||||
job_id, sizeof(job_id),
|
||||
err, sizeof(err));
|
||||
montauk::mfree(jpeg);
|
||||
if (!ok) break;
|
||||
|
||||
if (submitted_pages == 0)
|
||||
wp_status_copy(first_job_id, sizeof(first_job_id), job_id);
|
||||
submitted_pages++;
|
||||
}
|
||||
|
||||
montauk::mfree(line_pages);
|
||||
montauk::mfree(line_y);
|
||||
|
||||
if (submitted_pages != page_count) {
|
||||
if (submitted_pages > 0) {
|
||||
char partial[192];
|
||||
snprintf(partial, sizeof(partial),
|
||||
"Queued %d of %d pages before failing: %s",
|
||||
submitted_pages, page_count,
|
||||
err[0] ? err : "unknown error");
|
||||
wp_status_copy(out_status, out_status_len, partial);
|
||||
} else {
|
||||
wp_status_copy(out_status, out_status_len, err[0] ? err : "Failed to queue print job");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
char summary[192];
|
||||
const char* label = printer_name[0] ? printer_name : printer_uri;
|
||||
if (page_count == 1) {
|
||||
if (copies > 1) {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued 1 page to %s (%u copies), job %s",
|
||||
label, (unsigned)copies, first_job_id);
|
||||
} else {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued print job %s to %s",
|
||||
first_job_id, label);
|
||||
}
|
||||
} else if (copies > 1) {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued %d pages to %s (%u copies each)",
|
||||
page_count, label, (unsigned)copies);
|
||||
} else {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued %d pages to %s",
|
||||
page_count, label);
|
||||
}
|
||||
|
||||
wp_status_copy(out_status, out_status_len, summary);
|
||||
return true;
|
||||
}
|
||||
@@ -35,6 +35,40 @@ static void wp_draw_ui_icon_button(Canvas& c, int x, int y, int w, int h,
|
||||
wp_draw_ui_button(c, x, y, w, h, fallback, bg, fg, radius);
|
||||
}
|
||||
|
||||
static int wp_special_char_flyout_x() {
|
||||
int dx = WP_BTN_SECTION_X + 24 - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
if (dx + WP_SPECIAL_CHAR_FLYOUT_W > g_win_w) dx = g_win_w - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
return dx < 0 ? 0 : dx;
|
||||
}
|
||||
|
||||
static void wp_fit_ui_text(const char* src, char* out, int out_len, int max_w) {
|
||||
if (!src || !src[0]) {
|
||||
if (out_len > 0) out[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
montauk::strncpy(out, src, out_len - 1);
|
||||
out[out_len - 1] = '\0';
|
||||
if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return;
|
||||
|
||||
int slen = montauk::slen(src);
|
||||
for (int keep = slen; keep > 1; keep--) {
|
||||
char candidate[256];
|
||||
candidate[0] = '.';
|
||||
candidate[1] = '.';
|
||||
int pos = 2;
|
||||
const char* tail = src + (slen - keep);
|
||||
while (*tail && pos < (int)sizeof(candidate) - 1)
|
||||
candidate[pos++] = *tail++;
|
||||
candidate[pos] = '\0';
|
||||
if (text_width(g_ui_font, candidate, fonts::UI_SIZE) <= max_w) {
|
||||
montauk::strncpy(out, candidate, out_len - 1);
|
||||
out[out_len - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ParagraphStyle* wp_current_paragraph_style(WordProcessorState* wp) {
|
||||
if (wp->paragraph_count <= 0) {
|
||||
static ParagraphStyle fallback = { PARA_ALIGN_LEFT, PARA_LIST_NONE, 100, 0, 0, 0, 0, 0 };
|
||||
@@ -110,7 +144,11 @@ void wp_render() {
|
||||
if (g_icon_save.pixels)
|
||||
c.icon(WP_BTN_SAVE_X + 4, 10, g_icon_save);
|
||||
|
||||
c.vline(WP_BTN_SAVE_X + 28, 4, 28, colors::BORDER);
|
||||
c.fill_rounded_rect(WP_BTN_PRINT_X, 6, 24, 24, 3, btn_bg);
|
||||
if (g_icon_print.pixels)
|
||||
c.icon(WP_BTN_PRINT_X + 4, 10, g_icon_print);
|
||||
|
||||
c.vline(WP_BTN_PRINT_X + 28, 4, 28, colors::BORDER);
|
||||
|
||||
wp_draw_ui_icon_button(c, WP_BTN_UNDO_X, 6, 24, 24, g_icon_undo, "U", btn_bg, colors::TEXT_COLOR);
|
||||
wp_draw_ui_icon_button(c, WP_BTN_REDO_X, 6, 24, 24, g_icon_redo, "R", btn_bg, colors::TEXT_COLOR);
|
||||
@@ -167,7 +205,8 @@ void wp_render() {
|
||||
|
||||
c.vline(WP_BTN_SECTION_X - 4, 4, 28, colors::BORDER);
|
||||
|
||||
c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, btn_bg);
|
||||
Color special_bg = wp->special_char_flyout_open ? btn_active : btn_bg;
|
||||
c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, special_bg);
|
||||
{
|
||||
char section[2] = { (char)0xA7, '\0' };
|
||||
TrueTypeFont* font = wp_get_font(FONT_ROBOTO, 0);
|
||||
@@ -328,18 +367,25 @@ void wp_render() {
|
||||
c.fill_rect(0, status_y, c.w, WP_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
|
||||
int status_text_y = status_y + (WP_STATUS_H - sfh) / 2;
|
||||
|
||||
char status_left[192];
|
||||
char status_left[320];
|
||||
const char* name = wp->filename[0] ? wp->filename : "Untitled";
|
||||
snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | %s | %s | %d%%",
|
||||
name, wp->modified ? " *" : "",
|
||||
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size,
|
||||
wp_align_label(cur_para->align), wp_list_label(cur_para->list_type),
|
||||
(int)cur_para->line_spacing);
|
||||
wp_draw_ui_text(c, 4, status_text_y, status_left, colors::PANEL_TEXT);
|
||||
|
||||
char status_right[32];
|
||||
snprintf(status_right, sizeof(status_right), "%d chars ", wp->total_text_len);
|
||||
int sr_w = text_width(g_ui_font, status_right, fonts::UI_SIZE);
|
||||
char status_combined[320];
|
||||
if (wp->status_msg[0]) snprintf(status_combined, sizeof(status_combined), "%s | %s", status_left, wp->status_msg);
|
||||
else montauk::strncpy(status_combined, status_left, (int)sizeof(status_combined) - 1);
|
||||
status_combined[sizeof(status_combined) - 1] = '\0';
|
||||
|
||||
char status_fit[320];
|
||||
wp_fit_ui_text(status_combined, status_fit, sizeof(status_fit), c.w - sr_w - 12);
|
||||
wp_draw_ui_text(c, 4, status_text_y, status_fit, colors::PANEL_TEXT);
|
||||
wp_draw_ui_text(c, c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
|
||||
|
||||
if (wp->font_dropdown_open) {
|
||||
@@ -390,4 +436,24 @@ void wp_render() {
|
||||
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, spacing, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
if (wp->special_char_flyout_open) {
|
||||
int dx = wp_special_char_flyout_x();
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dw = WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
int dh = WP_SPECIAL_CHAR_OPTION_COUNT * WP_SPECIAL_CHAR_ROW_H + 4;
|
||||
TrueTypeFont* glyph_font = wp_get_font(FONT_ROBOTO, 0);
|
||||
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
||||
c.rect(dx, dy, dw, dh, colors::BORDER);
|
||||
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
|
||||
int iy = dy + 2 + i * WP_SPECIAL_CHAR_ROW_H;
|
||||
char glyph[2] = { (char)WP_SPECIAL_CHAR_OPTIONS[i].code, '\0' };
|
||||
c.fill_rect(dx + 2, iy, dw - 4, WP_SPECIAL_CHAR_ROW_H - 2, colors::MENU_BG);
|
||||
draw_text(c, glyph_font ? glyph_font : g_ui_font,
|
||||
dx + 8, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
|
||||
glyph, colors::TEXT_COLOR, fonts::UI_SIZE);
|
||||
wp_draw_ui_text(c, dx + 32, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
|
||||
WP_SPECIAL_CHAR_OPTIONS[i].label, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ extern "C" {
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int INIT_W = 640;
|
||||
static constexpr int INIT_H = 480;
|
||||
static constexpr int INIT_W = 900;
|
||||
static constexpr int INIT_H = 600;
|
||||
static constexpr int WP_TOOLBAR_H = 36;
|
||||
static constexpr int WP_PATHBAR_H = 32;
|
||||
static constexpr int WP_STATUS_H = 24;
|
||||
@@ -41,24 +41,27 @@ static constexpr int WP_LIST_HANGING = -18;
|
||||
static constexpr int WP_LIST_MARKER_W = 20;
|
||||
static constexpr int WP_BTN_OPEN_X = 4;
|
||||
static constexpr int WP_BTN_SAVE_X = 32;
|
||||
static constexpr int WP_BTN_UNDO_X = 66;
|
||||
static constexpr int WP_BTN_REDO_X = 94;
|
||||
static constexpr int WP_BTN_BOLD_X = 128;
|
||||
static constexpr int WP_BTN_ITALIC_X = 156;
|
||||
static constexpr int WP_FONT_DD_X = 188;
|
||||
static constexpr int WP_FONT_DD_W = 84;
|
||||
static constexpr int WP_SIZE_DD_X = 278;
|
||||
static constexpr int WP_SIZE_DD_W = 40;
|
||||
static constexpr int WP_BTN_ALIGN_L_X = 330;
|
||||
static constexpr int WP_BTN_ALIGN_C_X = 358;
|
||||
static constexpr int WP_BTN_ALIGN_R_X = 386;
|
||||
static constexpr int WP_BTN_BULLET_X = 418;
|
||||
static constexpr int WP_BTN_NUMBER_X = 446;
|
||||
static constexpr int WP_BTN_OUTDENT_X = 482;
|
||||
static constexpr int WP_BTN_INDENT_X = 510;
|
||||
static constexpr int WP_LINE_DD_X = 544;
|
||||
static constexpr int WP_LINE_DD_W = 56;
|
||||
static constexpr int WP_BTN_SECTION_X = 608;
|
||||
static constexpr int WP_BTN_PRINT_X = 60;
|
||||
static constexpr int WP_BTN_UNDO_X = 94;
|
||||
static constexpr int WP_BTN_REDO_X = 122;
|
||||
static constexpr int WP_BTN_BOLD_X = 156;
|
||||
static constexpr int WP_BTN_ITALIC_X = 184;
|
||||
static constexpr int WP_FONT_DD_X = 216;
|
||||
static constexpr int WP_FONT_DD_W = 74;
|
||||
static constexpr int WP_SIZE_DD_X = 296;
|
||||
static constexpr int WP_SIZE_DD_W = 36;
|
||||
static constexpr int WP_BTN_ALIGN_L_X = 344;
|
||||
static constexpr int WP_BTN_ALIGN_C_X = 372;
|
||||
static constexpr int WP_BTN_ALIGN_R_X = 400;
|
||||
static constexpr int WP_BTN_BULLET_X = 432;
|
||||
static constexpr int WP_BTN_NUMBER_X = 460;
|
||||
static constexpr int WP_BTN_OUTDENT_X = 496;
|
||||
static constexpr int WP_BTN_INDENT_X = 524;
|
||||
static constexpr int WP_LINE_DD_X = 558;
|
||||
static constexpr int WP_LINE_DD_W = 48;
|
||||
static constexpr int WP_BTN_SECTION_X = 614;
|
||||
static constexpr int WP_SPECIAL_CHAR_ROW_H = 24;
|
||||
static constexpr int WP_SPECIAL_CHAR_FLYOUT_W = 144;
|
||||
|
||||
static constexpr int FONT_ROBOTO = 0;
|
||||
static constexpr int FONT_NOTOSERIF = 1;
|
||||
@@ -79,6 +82,21 @@ static constexpr int WP_SIZE_OPTION_COUNT = 8;
|
||||
inline constexpr int WP_LINE_SPACING_OPTIONS[] = { 100, 125, 150, 200 };
|
||||
static constexpr int WP_LINE_SPACING_OPTION_COUNT = 4;
|
||||
|
||||
struct WpSpecialCharOption {
|
||||
uint8_t code;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
inline constexpr WpSpecialCharOption WP_SPECIAL_CHAR_OPTIONS[] = {
|
||||
{ 0xA7, "Section" },
|
||||
{ 0xB6, "Pilcrow" },
|
||||
{ 0xA9, "Copyright" },
|
||||
{ 0xAE, "Registered" },
|
||||
{ 0xB0, "Degree" },
|
||||
{ 0xB1, "Plus/Minus" },
|
||||
};
|
||||
static constexpr int WP_SPECIAL_CHAR_OPTION_COUNT = 6;
|
||||
|
||||
enum ParagraphAlign : uint8_t {
|
||||
PARA_ALIGN_LEFT = 0,
|
||||
PARA_ALIGN_CENTER = 1,
|
||||
@@ -266,6 +284,10 @@ struct WordProcessorState {
|
||||
bool modified;
|
||||
char filepath[256];
|
||||
char filename[64];
|
||||
char preferred_printer_uri[256];
|
||||
char preferred_printer_name[128];
|
||||
uint32_t preferred_print_copies;
|
||||
char status_msg[160];
|
||||
|
||||
bool show_pathbar;
|
||||
bool pathbar_save_mode;
|
||||
@@ -276,6 +298,7 @@ struct WordProcessorState {
|
||||
bool font_dropdown_open;
|
||||
bool size_dropdown_open;
|
||||
bool line_spacing_dropdown_open;
|
||||
bool special_char_flyout_open;
|
||||
|
||||
UndoSnapshot undo[WP_UNDO_MAX];
|
||||
int undo_count;
|
||||
@@ -289,6 +312,7 @@ extern WordProcessorState g_wp;
|
||||
extern WPFontTable g_wp_fonts;
|
||||
extern SvgIcon g_icon_folder;
|
||||
extern SvgIcon g_icon_save;
|
||||
extern SvgIcon g_icon_print;
|
||||
extern SvgIcon g_icon_undo;
|
||||
extern SvgIcon g_icon_redo;
|
||||
extern SvgIcon g_icon_align_left;
|
||||
@@ -357,6 +381,8 @@ void wp_history_mark_saved(WordProcessorState* wp);
|
||||
bool wp_undo(WordProcessorState* wp);
|
||||
bool wp_redo(WordProcessorState* wp);
|
||||
|
||||
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len);
|
||||
|
||||
void wp_render();
|
||||
void wp_handle_mouse(const Montauk::WinEvent& ev);
|
||||
void wp_handle_key(const Montauk::KeyEvent& key);
|
||||
|
||||
Reference in New Issue
Block a user