wip: printing from word processor, word processor improvements
This commit is contained in:
@@ -27,6 +27,11 @@ enum FileDialogMode : uint8_t {
|
||||
FILE_DIALOG_SAVE = 2,
|
||||
};
|
||||
|
||||
enum PrintDialogMode : uint8_t {
|
||||
PRINT_DIALOG_SETUP = 1,
|
||||
PRINT_DIALOG_SUBMIT = 2,
|
||||
};
|
||||
|
||||
struct Request {
|
||||
uint8_t kind;
|
||||
uint8_t mode;
|
||||
@@ -35,6 +40,8 @@ struct Request {
|
||||
char suggested_name[128];
|
||||
char source_path[256];
|
||||
char job_name[128];
|
||||
char printer_uri[256];
|
||||
uint32_t copies;
|
||||
char result_path[256];
|
||||
};
|
||||
|
||||
@@ -42,6 +49,9 @@ struct Result {
|
||||
char status[16];
|
||||
char path[256];
|
||||
char job_id[64];
|
||||
char printer_uri[256];
|
||||
char printer_name[128];
|
||||
uint32_t copies;
|
||||
char message[160];
|
||||
};
|
||||
|
||||
@@ -147,6 +157,8 @@ inline bool write_request_file(const char* path, const Request* req) {
|
||||
"suggested_name=%s\n"
|
||||
"source_path=%s\n"
|
||||
"job_name=%s\n"
|
||||
"printer_uri=%s\n"
|
||||
"copies=%u\n"
|
||||
"result_path=%s\n",
|
||||
(unsigned)req->kind,
|
||||
(unsigned)req->mode,
|
||||
@@ -155,6 +167,8 @@ inline bool write_request_file(const char* path, const Request* req) {
|
||||
req->suggested_name,
|
||||
req->source_path,
|
||||
req->job_name,
|
||||
req->printer_uri,
|
||||
(unsigned)req->copies,
|
||||
req->result_path);
|
||||
if (n <= 0 || n >= (int)sizeof(text)) return false;
|
||||
return write_text_file(path, text);
|
||||
@@ -174,6 +188,8 @@ inline bool read_request_file(const char* path, Request* req) {
|
||||
line_value(text, "suggested_name", req->suggested_name, sizeof(req->suggested_name));
|
||||
line_value(text, "source_path", req->source_path, sizeof(req->source_path));
|
||||
line_value(text, "job_name", req->job_name, sizeof(req->job_name));
|
||||
line_value(text, "printer_uri", req->printer_uri, sizeof(req->printer_uri));
|
||||
if (line_value(text, "copies", value, sizeof(value))) req->copies = parse_u32(value);
|
||||
line_value(text, "result_path", req->result_path, sizeof(req->result_path));
|
||||
return req->kind != 0 && req->result_path[0] != '\0';
|
||||
}
|
||||
@@ -185,10 +201,16 @@ inline bool write_result_file(const char* path, const Result* res) {
|
||||
"status=%s\n"
|
||||
"path=%s\n"
|
||||
"job_id=%s\n"
|
||||
"printer_uri=%s\n"
|
||||
"printer_name=%s\n"
|
||||
"copies=%u\n"
|
||||
"message=%s\n",
|
||||
res->status,
|
||||
res->path,
|
||||
res->job_id,
|
||||
res->printer_uri,
|
||||
res->printer_name,
|
||||
(unsigned)res->copies,
|
||||
res->message);
|
||||
if (n <= 0 || n >= (int)sizeof(text)) return false;
|
||||
return write_text_file(path, text);
|
||||
@@ -200,9 +222,13 @@ inline bool read_result_file(const char* path, Result* res) {
|
||||
if (!read_text_file(path, text, sizeof(text))) return false;
|
||||
|
||||
reset_result(res);
|
||||
char value[256];
|
||||
line_value(text, "status", res->status, sizeof(res->status));
|
||||
line_value(text, "path", res->path, sizeof(res->path));
|
||||
line_value(text, "job_id", res->job_id, sizeof(res->job_id));
|
||||
line_value(text, "printer_uri", res->printer_uri, sizeof(res->printer_uri));
|
||||
line_value(text, "printer_name", res->printer_name, sizeof(res->printer_name));
|
||||
if (line_value(text, "copies", value, sizeof(value))) res->copies = parse_u32(value);
|
||||
line_value(text, "message", res->message, sizeof(res->message));
|
||||
return res->status[0] != '\0';
|
||||
}
|
||||
@@ -311,6 +337,8 @@ inline bool print_file(const char* title,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
Request req = {};
|
||||
req.kind = REQUEST_KIND_PRINT;
|
||||
req.mode = PRINT_DIALOG_SUBMIT;
|
||||
req.copies = 1;
|
||||
safe_copy(req.title, sizeof(req.title), title ? title : "Print");
|
||||
safe_copy(req.source_path, sizeof(req.source_path), source_path);
|
||||
safe_copy(req.job_name, sizeof(req.job_name), job_name);
|
||||
@@ -321,4 +349,27 @@ inline bool print_file(const char* title,
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline bool configure_print(const char* title,
|
||||
const char* initial_printer_uri,
|
||||
const char* job_name,
|
||||
char* out_printer_uri, int out_printer_uri_len,
|
||||
char* out_printer_name, int out_printer_name_len,
|
||||
uint32_t* out_copies = nullptr,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
Request req = {};
|
||||
req.kind = REQUEST_KIND_PRINT;
|
||||
req.mode = PRINT_DIALOG_SETUP;
|
||||
req.copies = out_copies && *out_copies > 0 ? *out_copies : 1;
|
||||
safe_copy(req.title, sizeof(req.title), title ? title : "Print");
|
||||
safe_copy(req.job_name, sizeof(req.job_name), job_name ? job_name : "");
|
||||
safe_copy(req.printer_uri, sizeof(req.printer_uri), initial_printer_uri);
|
||||
|
||||
Result res = {};
|
||||
bool ok = run_request(&req, &res, out_message, out_message_len);
|
||||
if (ok && out_printer_uri && out_printer_uri_len > 0) safe_copy(out_printer_uri, out_printer_uri_len, res.printer_uri);
|
||||
if (ok && out_printer_name && out_printer_name_len > 0) safe_copy(out_printer_name, out_printer_name_len, res.printer_name);
|
||||
if (ok && out_copies) *out_copies = res.copies > 0 ? res.copies : 1;
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace gui::dialogs
|
||||
|
||||
@@ -30,7 +30,9 @@ static constexpr const char* SPOOL_DONE_DIR = "0:/spool/print/done";
|
||||
static constexpr const char* SPOOL_FAILED_DIR = "0:/spool/print/failed";
|
||||
static constexpr const char* SPOOL_DOCS_DIR = "0:/spool/print/docs";
|
||||
static constexpr const char* SPOOL_TMP_DIR = "0:/spool/print/tmp";
|
||||
static constexpr const char* SPOOL_PRINTERS_DIR = "0:/spool/print/printers";
|
||||
static constexpr const char* SPOOL_DAEMON_PID_PATH = "0:/spool/print/daemon.pid";
|
||||
static constexpr const char* SPOOL_DAEMON_STATE_PATH = "0:/spool/print/daemon.state";
|
||||
static constexpr const char* SPOOL_PROBE_STATE_PATH = "0:/spool/print/printer-probe.state";
|
||||
static constexpr const char* PRINTD_BINARY = "0:/os/printd.elf";
|
||||
static constexpr int MAX_PATH_LEN = 256;
|
||||
@@ -58,6 +60,7 @@ struct JobMeta {
|
||||
char debug_info[MAX_DEBUG_LEN];
|
||||
int remote_job_id;
|
||||
int size_bytes;
|
||||
int copies;
|
||||
};
|
||||
|
||||
struct IppUri {
|
||||
@@ -98,6 +101,15 @@ struct ProbeState {
|
||||
IppCapabilities caps;
|
||||
};
|
||||
|
||||
struct KnownPrinter {
|
||||
char uri[MAX_PATH_LEN];
|
||||
char display_name[128];
|
||||
char last_seen[32];
|
||||
char status_message[128];
|
||||
bool ok;
|
||||
bool is_default;
|
||||
};
|
||||
|
||||
struct IppPrintResult {
|
||||
bool ok;
|
||||
bool use_tls;
|
||||
@@ -134,6 +146,10 @@ inline void zero_probe_state(ProbeState* state) {
|
||||
if (state) memset(state, 0, sizeof(*state));
|
||||
}
|
||||
|
||||
inline void zero_known_printer(KnownPrinter* printer) {
|
||||
if (printer) memset(printer, 0, sizeof(*printer));
|
||||
}
|
||||
|
||||
inline void safe_copy(char* dst, int dst_len, const char* src) {
|
||||
if (dst == nullptr || dst_len <= 0) return;
|
||||
if (src == nullptr) {
|
||||
@@ -207,7 +223,8 @@ inline bool ensure_spool_dirs() {
|
||||
&& ensure_dir(SPOOL_DONE_DIR)
|
||||
&& ensure_dir(SPOOL_FAILED_DIR)
|
||||
&& ensure_dir(SPOOL_DOCS_DIR)
|
||||
&& ensure_dir(SPOOL_TMP_DIR);
|
||||
&& ensure_dir(SPOOL_TMP_DIR)
|
||||
&& ensure_dir(SPOOL_PRINTERS_DIR);
|
||||
}
|
||||
|
||||
inline bool path_join(char* out, int out_len, const char* a, const char* b) {
|
||||
@@ -370,9 +387,136 @@ inline bool read_default_printer_uri(char* out, int out_len) {
|
||||
return read_text_file_line(DEFAULT_PRINTER_PATH, out, out_len);
|
||||
}
|
||||
|
||||
inline uint32_t hash_uri_key(const char* text) {
|
||||
uint32_t hash = 2166136261u;
|
||||
if (!text) return hash;
|
||||
for (const unsigned char* p = (const unsigned char*)text; *p; ++p) {
|
||||
hash ^= (uint32_t)*p;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline void make_known_printer_path(const char* uri, char* out, int out_len) {
|
||||
unsigned hash = (unsigned)hash_uri_key(uri);
|
||||
char leaf[32];
|
||||
snprintf(leaf, sizeof(leaf), "%08x.printer", hash);
|
||||
path_join(out, out_len, SPOOL_PRINTERS_DIR, leaf);
|
||||
}
|
||||
|
||||
inline bool save_known_printer_atomic(const KnownPrinter* printer) {
|
||||
if (printer == nullptr || printer->uri[0] == '\0') return false;
|
||||
|
||||
char path[MAX_PATH_LEN];
|
||||
make_known_printer_path(printer->uri, path, sizeof(path));
|
||||
|
||||
char text[768];
|
||||
int n = snprintf(text, sizeof(text),
|
||||
"uri=%s\n"
|
||||
"display_name=%s\n"
|
||||
"last_seen=%s\n"
|
||||
"status_message=%s\n"
|
||||
"ok=%d\n",
|
||||
printer->uri,
|
||||
printer->display_name,
|
||||
printer->last_seen,
|
||||
printer->status_message,
|
||||
printer->ok ? 1 : 0);
|
||||
if (n <= 0 || n >= (int)sizeof(text)) return false;
|
||||
return write_text_file_atomic(path, text);
|
||||
}
|
||||
|
||||
inline bool load_known_printer_from_path(const char* path, KnownPrinter* printer) {
|
||||
if (path == nullptr || printer == nullptr) return false;
|
||||
zero_known_printer(printer);
|
||||
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
|
||||
char line[384];
|
||||
while (fgets(line, sizeof(line), f) != nullptr) {
|
||||
trim_line(line);
|
||||
char* eq = strchr(line, '=');
|
||||
if (!eq) continue;
|
||||
*eq++ = '\0';
|
||||
|
||||
if (strcmp(line, "uri") == 0) safe_copy(printer->uri, sizeof(printer->uri), eq);
|
||||
else if (strcmp(line, "display_name") == 0) safe_copy(printer->display_name, sizeof(printer->display_name), eq);
|
||||
else if (strcmp(line, "last_seen") == 0) safe_copy(printer->last_seen, sizeof(printer->last_seen), eq);
|
||||
else if (strcmp(line, "status_message") == 0) safe_copy(printer->status_message, sizeof(printer->status_message), eq);
|
||||
else if (strcmp(line, "ok") == 0) printer->ok = atoi(eq) != 0;
|
||||
}
|
||||
fclose(f);
|
||||
return printer->uri[0] != '\0';
|
||||
}
|
||||
|
||||
inline bool remember_printer_uri(const char* uri,
|
||||
const char* display_name,
|
||||
bool ok,
|
||||
const char* status_message,
|
||||
const char* last_seen) {
|
||||
if (uri == nullptr || *uri == '\0') return false;
|
||||
ensure_spool_dirs();
|
||||
|
||||
KnownPrinter printer = {};
|
||||
safe_copy(printer.uri, sizeof(printer.uri), uri);
|
||||
safe_copy(printer.display_name, sizeof(printer.display_name), display_name ? display_name : "");
|
||||
safe_copy(printer.last_seen, sizeof(printer.last_seen), last_seen ? last_seen : "");
|
||||
safe_copy(printer.status_message, sizeof(printer.status_message), status_message ? status_message : "");
|
||||
printer.ok = ok;
|
||||
return save_known_printer_atomic(&printer);
|
||||
}
|
||||
|
||||
inline int list_known_printers(KnownPrinter* out, int max) {
|
||||
if (out == nullptr || max <= 0) return 0;
|
||||
ensure_spool_dirs();
|
||||
|
||||
int count = 0;
|
||||
DIR* d = opendir(SPOOL_PRINTERS_DIR);
|
||||
if (d) {
|
||||
struct dirent* ent = nullptr;
|
||||
while ((ent = readdir(d)) != nullptr && count < max) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
char path[MAX_PATH_LEN];
|
||||
path_join(path, sizeof(path), SPOOL_PRINTERS_DIR, ent->d_name);
|
||||
if (load_known_printer_from_path(path, &out[count]))
|
||||
count++;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
char default_uri[MAX_PATH_LEN] = {};
|
||||
bool have_default = read_default_printer_uri(default_uri, sizeof(default_uri));
|
||||
for (int i = 0; i < count; i++) {
|
||||
out[i].is_default = have_default && strcmp(out[i].uri, default_uri) == 0;
|
||||
}
|
||||
|
||||
for (int i = 1; i < count; i++) {
|
||||
KnownPrinter tmp = out[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0) {
|
||||
bool move = false;
|
||||
if (tmp.is_default && !out[j].is_default) move = true;
|
||||
else if (tmp.is_default == out[j].is_default) {
|
||||
const char* ta = tmp.display_name[0] ? tmp.display_name : tmp.uri;
|
||||
const char* tb = out[j].display_name[0] ? out[j].display_name : out[j].uri;
|
||||
if (strcmp(ta, tb) < 0) move = true;
|
||||
}
|
||||
if (!move) break;
|
||||
out[j + 1] = out[j];
|
||||
j--;
|
||||
}
|
||||
out[j + 1] = tmp;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
inline bool write_default_printer_uri(const char* uri) {
|
||||
ensure_spool_dirs();
|
||||
return write_text_file_atomic(DEFAULT_PRINTER_PATH, uri);
|
||||
bool ok = write_text_file_atomic(DEFAULT_PRINTER_PATH, uri);
|
||||
if (ok && uri && *uri) remember_printer_uri(uri, nullptr, false, "", "");
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline bool parse_args(ArgList* out) {
|
||||
@@ -623,14 +767,14 @@ inline bool infer_doc_format(const char* path, char* out, int out_len) {
|
||||
}
|
||||
|
||||
inline void generate_job_id(char* out, int out_len) {
|
||||
static uint32_t seq = 0;
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
uint32_t rnd = 0;
|
||||
if (montauk::getrandom(&rnd, sizeof(rnd)) < 0) {
|
||||
rnd = (uint32_t)(now ^ (uint64_t)montauk::getpid());
|
||||
}
|
||||
snprintf(out, (size_t)out_len, "%llu-%04x",
|
||||
uint32_t cur = ++seq;
|
||||
unsigned pid = (unsigned)(montauk::getpid() & 0xFFFF);
|
||||
snprintf(out, (size_t)out_len, "%llu-%04x-%04x",
|
||||
(unsigned long long)now,
|
||||
(unsigned)(rnd & 0xFFFFu));
|
||||
pid,
|
||||
(unsigned)(cur & 0xFFFFu));
|
||||
}
|
||||
|
||||
inline bool save_job_to_path_atomic(const char* path, const JobMeta* job) {
|
||||
@@ -668,6 +812,7 @@ inline bool save_job_to_path_atomic(const char* path, const JobMeta* job) {
|
||||
fprintf(f, "debug_info=%s\n", copy.debug_info);
|
||||
fprintf(f, "remote_job_id=%d\n", copy.remote_job_id);
|
||||
fprintf(f, "size_bytes=%d\n", copy.size_bytes);
|
||||
fprintf(f, "copies=%d\n", copy.copies);
|
||||
fclose(f);
|
||||
|
||||
if (rename(tmp, path) != 0) {
|
||||
@@ -706,8 +851,10 @@ inline bool load_job_from_path(const char* path, JobMeta* job) {
|
||||
else if (strcmp(line, "debug_info") == 0) safe_copy(job->debug_info, sizeof(job->debug_info), eq);
|
||||
else if (strcmp(line, "remote_job_id") == 0) job->remote_job_id = atoi(eq);
|
||||
else if (strcmp(line, "size_bytes") == 0) job->size_bytes = atoi(eq);
|
||||
else if (strcmp(line, "copies") == 0) job->copies = atoi(eq);
|
||||
}
|
||||
fclose(f);
|
||||
if (job->copies <= 0) job->copies = 1;
|
||||
return job->id[0] != '\0';
|
||||
}
|
||||
|
||||
@@ -806,7 +953,14 @@ inline bool load_probe_state(const char* path, ProbeState* state) {
|
||||
|
||||
inline bool save_printer_probe_state(const ProbeState* state) {
|
||||
ensure_spool_dirs();
|
||||
return save_probe_state_atomic(SPOOL_PROBE_STATE_PATH, state);
|
||||
bool ok = save_probe_state_atomic(SPOOL_PROBE_STATE_PATH, state);
|
||||
if (ok && state && state->printer_uri[0]) {
|
||||
const char* name = state->caps.printer_name[0] ? state->caps.printer_name : nullptr;
|
||||
const char* msg = state->message[0] ? state->message
|
||||
: (state->caps.status_message[0] ? state->caps.status_message : "");
|
||||
remember_printer_uri(state->printer_uri, name, state->ok, msg, state->probed_at);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline bool load_printer_probe_state(ProbeState* state) {
|
||||
@@ -853,6 +1007,17 @@ inline bool daemon_is_running(int* out_pid) {
|
||||
uint32_t sig = montauk::wait_handle(handle, Montauk::IPC_SIGNAL_EXITED, 0);
|
||||
montauk::close(handle);
|
||||
if (sig & Montauk::IPC_SIGNAL_EXITED) return false;
|
||||
|
||||
Montauk::ProcInfo procs[128];
|
||||
int proc_count = montauk::proclist(procs, (int)(sizeof(procs) / sizeof(procs[0])));
|
||||
bool matched = false;
|
||||
for (int i = 0; i < proc_count; i++) {
|
||||
if (procs[i].pid != pid) continue;
|
||||
if (strcmp(procs[i].name, PRINTD_BINARY) == 0) matched = true;
|
||||
break;
|
||||
}
|
||||
if (!matched) return false;
|
||||
|
||||
if (out_pid) *out_pid = pid;
|
||||
return true;
|
||||
}
|
||||
@@ -1709,6 +1874,7 @@ inline bool probe_printer_uri(const char* printer_uri, ProbeState* out, char* er
|
||||
inline bool ipp_print_buffer(const char* printer_uri,
|
||||
const char* job_name,
|
||||
const char* user_name,
|
||||
int copies,
|
||||
const char* doc_format,
|
||||
const uint8_t* data, int data_len,
|
||||
IppPrintResult* out,
|
||||
@@ -1738,6 +1904,7 @@ inline bool ipp_print_buffer(const char* printer_uri,
|
||||
const char* use_job_name = (job_name && *job_name) ? job_name : "MontaukOS Print Job";
|
||||
const char* use_user = (user_name && *user_name) ? user_name : "montauk";
|
||||
const char* use_format = (doc_format && *doc_format) ? doc_format : "application/octet-stream";
|
||||
int use_copies = copies > 0 ? copies : 1;
|
||||
if (out) {
|
||||
out->request_id = (int)request_id;
|
||||
safe_copy(out->document_format, sizeof(out->document_format), use_format);
|
||||
@@ -1752,6 +1919,7 @@ inline bool ipp_print_buffer(const char* printer_uri,
|
||||
&& bb_put_attr_str(&req, 0x45, "printer-uri", uri.normalized)
|
||||
&& bb_put_attr_str(&req, 0x42, "requesting-user-name", use_user)
|
||||
&& bb_put_attr_str(&req, 0x42, "job-name", use_job_name)
|
||||
&& bb_put_attr_int(&req, 0x21, "copies", use_copies)
|
||||
&& bb_put_attr_str(&req, 0x49, "document-format", use_format)
|
||||
&& bb_put8(&req, 0x03)
|
||||
&& bb_put_bytes(&req, data, data_len);
|
||||
@@ -1802,7 +1970,8 @@ inline bool submit_document_job(const char* source_path,
|
||||
const char* printer_uri,
|
||||
const char* job_name_override,
|
||||
char* out_job_id, int out_job_id_len,
|
||||
char* err, int err_len) {
|
||||
char* err, int err_len,
|
||||
int copies = 1) {
|
||||
ensure_spool_dirs();
|
||||
|
||||
char resolved_uri[MAX_PATH_LEN];
|
||||
@@ -1843,6 +2012,77 @@ inline bool submit_document_job(const char* source_path,
|
||||
uri.normalized, job.doc_format, job.source_name);
|
||||
job.remote_job_id = 0;
|
||||
job.size_bytes = size_bytes;
|
||||
job.copies = copies > 0 ? copies : 1;
|
||||
|
||||
char job_path[MAX_PATH_LEN];
|
||||
make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path));
|
||||
if (!save_job_to_path_atomic(job_path, &job)) {
|
||||
remove(doc_path);
|
||||
safe_copy(err, err_len, "failed to create queued job");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensure_daemon_running(err, err_len)) return false;
|
||||
|
||||
safe_copy(out_job_id, out_job_id_len, job.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool submit_document_buffer_job(const uint8_t* data, int data_len,
|
||||
const char* source_name,
|
||||
const char* doc_format,
|
||||
const char* printer_uri,
|
||||
const char* job_name_override,
|
||||
int copies = 1,
|
||||
char* out_job_id = nullptr, int out_job_id_len = 0,
|
||||
char* err = nullptr, int err_len = 0) {
|
||||
ensure_spool_dirs();
|
||||
|
||||
if (data == nullptr || data_len < 0) {
|
||||
safe_copy(err, err_len, "missing print document");
|
||||
return false;
|
||||
}
|
||||
|
||||
char resolved_uri[MAX_PATH_LEN];
|
||||
if (printer_uri && *printer_uri) safe_copy(resolved_uri, sizeof(resolved_uri), printer_uri);
|
||||
else if (!read_default_printer_uri(resolved_uri, sizeof(resolved_uri))) {
|
||||
safe_copy(err, err_len, "no default printer is configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
IppUri uri = {};
|
||||
if (!parse_ipp_uri(resolved_uri, &uri, err, err_len)) return false;
|
||||
|
||||
char job_id[48];
|
||||
generate_job_id(job_id, sizeof(job_id));
|
||||
|
||||
char doc_path[MAX_PATH_LEN];
|
||||
make_doc_file_path(job_id, doc_path, sizeof(doc_path));
|
||||
if (!write_file_atomic(doc_path, data, data_len, err, err_len)) return false;
|
||||
|
||||
JobMeta job = {};
|
||||
safe_copy(job.id, sizeof(job.id), job_id);
|
||||
safe_copy(job.state, sizeof(job.state), "queued");
|
||||
safe_copy(job.source_kind, sizeof(job.source_kind), "document");
|
||||
safe_copy(job.job_name, sizeof(job.job_name),
|
||||
(job_name_override && *job_name_override) ? job_name_override
|
||||
: (source_name && *source_name ? source_name : "document"));
|
||||
safe_copy(job.user_name, sizeof(job.user_name), "montauk");
|
||||
safe_copy(job.printer_uri, sizeof(job.printer_uri), uri.normalized);
|
||||
safe_copy(job.doc_path, sizeof(job.doc_path), doc_path);
|
||||
safe_copy(job.doc_format, sizeof(job.doc_format),
|
||||
(doc_format && *doc_format) ? doc_format : "application/octet-stream");
|
||||
safe_copy(job.source_name, sizeof(job.source_name),
|
||||
(source_name && *source_name) ? source_name : "document");
|
||||
now_string(job.created_at, sizeof(job.created_at));
|
||||
safe_copy(job.updated_at, sizeof(job.updated_at), job.created_at);
|
||||
safe_copy(job.status_message, sizeof(job.status_message), "Queued");
|
||||
snprintf(job.debug_info, sizeof(job.debug_info),
|
||||
"printer=%s format=%s source=%s",
|
||||
uri.normalized, job.doc_format, job.source_name);
|
||||
job.remote_job_id = 0;
|
||||
job.size_bytes = data_len;
|
||||
job.copies = copies > 0 ? copies : 1;
|
||||
|
||||
char job_path[MAX_PATH_LEN];
|
||||
make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path));
|
||||
@@ -1860,7 +2100,8 @@ inline bool submit_document_job(const char* source_path,
|
||||
|
||||
inline bool submit_test_page_job(const char* printer_uri,
|
||||
char* out_job_id, int out_job_id_len,
|
||||
char* err, int err_len) {
|
||||
char* err, int err_len,
|
||||
int copies = 1) {
|
||||
ensure_spool_dirs();
|
||||
|
||||
char resolved_uri[MAX_PATH_LEN];
|
||||
@@ -1889,6 +2130,7 @@ inline bool submit_test_page_job(const char* printer_uri,
|
||||
snprintf(job.debug_info, sizeof(job.debug_info),
|
||||
"printer=%s format=auto source=%s",
|
||||
uri.normalized, job.source_name);
|
||||
job.copies = copies > 0 ? copies : 1;
|
||||
|
||||
char job_path[MAX_PATH_LEN];
|
||||
make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path));
|
||||
|
||||
+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