source: git/src/cavernlog.cc @ ac20829b

log-selectstereo-2025warn-only-for-hanging-survey
Last change on this file since ac20829b was ac20829b, checked in by Olly Betts <olly@…>, 14 months ago

Reimplement aven's cavern log window

The old version turned the log into HTML and displayed it using
wxHtmlWindow, but that adds a lot of overhead and is especially
inefficient if there are many diagnostics - cavern could finish
almost instantly yet aven could take many seconds to process
the output.

The new version renders directly from the log data. It should
have most of the features from before - the only missing feature
I'm currently aware of is that you can't now select and copy text
from the log window, which wxHtmlWindow provided for free. If
people miss this feature, we could add it to the new implementation.

  • Property mode set to 100644
File size: 18.6 KB
Line 
1/* cavernlog.cc
2 * Run cavern inside an Aven window
3 *
4 * Copyright (C) 2005-2024 Olly Betts
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
19 */
20
21#include <config.h>
22
23#include "aven.h"
24#include "cavernlog.h"
25#include "filename.h"
26#include "mainfrm.h"
27#include "message.h"
28#include "osalloc.h"
29
30#include <errno.h>
31#include <stdio.h>
32#include <stdlib.h>
33
34#include <sys/time.h>
35#include <sys/types.h>
36#include <unistd.h>
37
38#include <wx/process.h>
39
40#define GVIM_COMMAND "gvim +'call cursor($l,$c)' $f"
41#define VIM_COMMAND "x-terminal-emulator -e vim +'call cursor($l,$c)' $f"
42#define NVIM_COMMAND "x-terminal-emulator -e nvim +'call cursor($l,$c)' $f"
43#define GEDIT_COMMAND "gedit $f +$l:$c"
44// Pluma currently ignores the column, but include it assuming some future
45// version will add support.
46#define PLUMA_COMMAND "pluma +$l:$c $f"
47#define EMACS_COMMAND "x-terminal-emulator -e emacs +$l:$c $f"
48#define NANO_COMMAND "x-terminal-emulator -e nano +$l,$c $f"
49#define JED_COMMAND "x-terminal-emulator -e jed $f -g $l"
50#define KATE_COMMAND "kate -l $l -c $c $f"
51
52#ifdef __WXMSW__
53# define DEFAULT_EDITOR_COMMAND "notepad $f"
54#elif defined __WXMAC__
55# define DEFAULT_EDITOR_COMMAND "open -t $f"
56#else
57# define DEFAULT_EDITOR_COMMAND VIM_COMMAND
58#endif
59
60enum { LOG_REPROCESS = 1234, LOG_SAVE = 1235 };
61
62static const wxString badutf8_html(
63    wxT("<span style=\"color:white;background-color:red;\">&#xfffd;</span>"));
64static const wxString badutf8(wxUniChar(0xfffd));
65
66// New event type for signalling cavern output to process.
67wxDEFINE_EVENT(EVT_CAVERN_OUTPUT, wxCommandEvent);
68
69void
70CavernLogWindow::CheckForOutput(bool immediate)
71{
72    timer.Stop();
73    if (cavern_out == NULL) return;
74
75    wxInputStream * in = cavern_out->GetInputStream();
76
77    if (!in->CanRead()) {
78        timer.StartOnce();
79        return;
80    }
81
82    size_t real_size = log_txt.size();
83    size_t allow = 1024;
84    log_txt.resize(real_size + allow);
85    in->Read(&log_txt[real_size], allow);
86    size_t n = in->LastRead();
87    log_txt.resize(real_size + n);
88    if (n) {
89        if (immediate) {
90            ProcessCavernOutput();
91        } else {
92            QueueEvent(new wxCommandEvent(EVT_CAVERN_OUTPUT));
93        }
94    }
95}
96
97int
98CavernLogWindow::OnPaintButton(wxButton* b, int x)
99{
100    if (b) {
101        x -= 4;
102        const wxSize& bsize = b->GetSize();
103        x -= bsize.x;
104        b->SetSize(x, 4, bsize.x, bsize.y);
105        x -= 4;
106    }
107    return x;
108}
109
110void
111CavernLogWindow::OnPaint(wxPaintEvent&)
112{
113    wxPaintDC dc(this);
114    wxFont font = dc.GetFont();
115    wxFont bold_font = font.Bold();
116    wxFont underlined_font = font.Underlined();
117    const wxRegion& region = GetUpdateRegion();
118    const wxRect& rect = region.GetBox();
119    int scroll_x = 0, scroll_y = 0;
120    GetViewStart(&scroll_x, &scroll_y);
121    int fsize = dc.GetFont().GetPixelSize().GetHeight();
122    int limit = min((rect.y + rect.height + fsize - 1) / fsize + scroll_y, int(line_info.size()) - 1);
123    for (int i = max(rect.y / fsize, scroll_y); i <= limit ; ++i) {
124        LineInfo& info = line_info[i];
125        // Leave a small margin to the left.
126        int x = fsize / 2 - scroll_x * fsize;
127        int y = (i - scroll_y) * fsize;
128        unsigned offset = info.start_offset;
129        unsigned len = info.len;
130        if (info.link_len) {
131            dc.SetFont(underlined_font);
132            dc.SetTextForeground(wxColour(192, 0, 192));
133            wxString link = wxString::FromUTF8(&log_txt[offset], info.link_len);
134            offset += info.link_len;
135            len -= info.link_len;
136            dc.DrawText(link, x, y);
137            x += info.link_pixel_width;
138            dc.SetFont(font);
139        }
140        if (info.colour_len) {
141            dc.SetTextForeground(*wxBLACK);
142            {
143                size_t s_len = info.start_offset + info.colour_start - offset;
144                wxString s = wxString::FromUTF8(&log_txt[offset], s_len);
145                offset += s_len;
146                len -= s_len;
147                dc.DrawText(s, x, y);
148                x += dc.GetTextExtent(s).GetWidth();
149            }
150            switch (info.colour) {
151                case LOG_ERROR:
152                    dc.SetTextForeground(*wxRED);
153                    break;
154                case LOG_WARNING:
155                    dc.SetTextForeground(wxColour(0xf2, 0x8C, 0x28));
156                    break;
157                case LOG_INFO:
158                    dc.SetTextForeground(*wxBLUE);
159                    break;
160            }
161            dc.SetFont(bold_font);
162            wxString d = wxString::FromUTF8(&log_txt[offset], info.colour_len);
163            offset += info.colour_len;
164            len -= info.colour_len;
165            dc.DrawText(d, x, y);
166            x += dc.GetTextExtent(d).GetWidth();
167            dc.SetFont(font);
168        }
169        dc.SetTextForeground(*wxBLACK);
170        dc.DrawText(wxString::FromUTF8(&log_txt[offset], len), x, y);
171    }
172    int x = GetClientSize().x;
173    x = OnPaintButton(ok_button, x);
174    x = OnPaintButton(reprocess_button, x);
175    OnPaintButton(save_button, x);
176}
177
178BEGIN_EVENT_TABLE(CavernLogWindow, wxScrolledWindow)
179    EVT_BUTTON(LOG_REPROCESS, CavernLogWindow::OnReprocess)
180    EVT_BUTTON(LOG_SAVE, CavernLogWindow::OnSave)
181    EVT_BUTTON(wxID_OK, CavernLogWindow::OnOK)
182    EVT_COMMAND(wxID_ANY, EVT_CAVERN_OUTPUT, CavernLogWindow::OnCavernOutput)
183    EVT_IDLE(CavernLogWindow::OnIdle)
184    EVT_TIMER(wxID_ANY, CavernLogWindow::OnTimer)
185    EVT_PAINT(CavernLogWindow::OnPaint)
186    EVT_MOTION(CavernLogWindow::OnMouseMove)
187    EVT_LEFT_UP(CavernLogWindow::OnLinkClicked)
188    EVT_END_PROCESS(wxID_ANY, CavernLogWindow::OnEndProcess)
189END_EVENT_TABLE()
190
191wxString escape_for_shell(wxString s, bool protect_dash)
192{
193#ifdef __WXMSW__
194    // Correct quoting rules are insane:
195    //
196    // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
197    //
198    // Thankfully wxExecute passes the command string to CreateProcess(), so
199    // at least we don't need to quote for cmd.exe too.
200    if (s.empty() || s.find_first_of(wxT(" \"\t\n\v")) != s.npos) {
201        // Need to quote.
202        s.insert(0, wxT('"'));
203        for (size_t p = 1; p < s.size(); ++p) {
204            size_t backslashes = 0;
205            while (s[p] == wxT('\\')) {
206                ++backslashes;
207                if (++p == s.size()) {
208                    // Escape all the backslashes, since they're before
209                    // the closing quote we add below.
210                    s.append(backslashes, wxT('\\'));
211                    goto done;
212                }
213            }
214
215            if (s[p] == wxT('"')) {
216                // Escape any preceding backslashes and this quote.
217                s.insert(p, backslashes + 1, wxT('\\'));
218                p += backslashes + 1;
219            }
220        }
221done:
222        s.append(wxT('"'));
223    }
224#else
225    size_t p = 0;
226    if (protect_dash && !s.empty() && s[0u] == '-') {
227        // If the filename starts with a '-', protect it from being
228        // treated as an option by prepending "./".
229        s.insert(0, wxT("./"));
230        p = 2;
231    }
232    while (p < s.size()) {
233        // Exclude a few safe characters which are common in filenames
234        if (!isalnum((unsigned char)s[p]) && strchr("/._-", s[p]) == NULL) {
235            s.insert(p, 1, wxT('\\'));
236            ++p;
237        }
238        ++p;
239    }
240#endif
241    return s;
242}
243
244wxString get_command_path(const wxChar * command_name)
245{
246#ifdef __WXMSW__
247    wxString cmd;
248    {
249        DWORD len = 256;
250        wchar_t *buf = NULL;
251        while (1) {
252            DWORD got;
253            buf = (wchar_t*)osrealloc(buf, len * 2);
254            got = GetModuleFileNameW(NULL, buf, len);
255            if (got < len) break;
256            len += len;
257        }
258        /* Strange Win32 nastiness - strip prefix "\\?\" if present */
259        wchar_t *start = buf;
260        if (wcsncmp(start, L"\\\\?\\", 4) == 0) start += 4;
261        wchar_t * slash = wcsrchr(start, L'\\');
262        if (slash) {
263            cmd.assign(start, slash - start + 1);
264        }
265        osfree(buf);
266    }
267#else
268    wxString cmd = wxString::FromUTF8(msg_exepth());
269#endif
270    cmd += command_name;
271    return cmd;
272}
273
274CavernLogWindow::CavernLogWindow(MainFrm * mainfrm_, const wxString & survey_, wxWindow * parent)
275    : wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
276                       wxFULL_REPAINT_ON_RESIZE),
277      mainfrm(mainfrm_),
278      survey(survey_),
279      timer(this)
280{
281}
282
283CavernLogWindow::~CavernLogWindow()
284{
285    timer.Stop();
286    if (cavern_out) {
287        wxEndBusyCursor();
288        cavern_out->Detach();
289    }
290}
291
292void
293CavernLogWindow::OnMouseMove(wxMouseEvent& e)
294{
295    const auto& pos = e.GetPosition();
296    int fsize = GetFont().GetPixelSize().GetHeight();
297    int scroll_x = 0, scroll_y = 0;
298    GetViewStart(&scroll_x, &scroll_y);
299    unsigned line = pos.y / fsize + scroll_y;
300    unsigned x = pos.x + scroll_x * fsize - fsize / 2;
301    if (line < line_info.size() && x <= line_info[line].link_pixel_width) {
302        SetCursor(wxCursor(wxCURSOR_HAND));
303    } else {
304        SetCursor(wxNullCursor);
305    }
306}
307
308void
309CavernLogWindow::OnLinkClicked(wxMouseEvent& e)
310{
311    const auto& pos = e.GetPosition();
312    int fsize = GetFont().GetPixelSize().GetHeight();
313    int scroll_x = 0, scroll_y = 0;
314    GetViewStart(&scroll_x, &scroll_y);
315    unsigned line = pos.y / fsize + scroll_y;
316    unsigned x = pos.x + scroll_x * fsize - fsize / 2;
317    if (!(line < line_info.size() && x <= line_info[line].link_pixel_width))
318        return;
319
320    const char* cur = &log_txt[line_info[line].start_offset];
321    size_t link_len = line_info[line].link_len;
322    size_t colon = link_len;
323    while (colon > 1 && (unsigned)(cur[--colon] - '0') <= 9) { }
324    size_t colon2 = colon;
325    while (colon > 1 && (unsigned)(cur[--colon] - '0') <= 9) { }
326    if (cur[colon] != ':') {
327        colon = colon2;
328        colon2 = link_len;
329    }
330
331    wxString cmd;
332    wxChar * p = wxGetenv(wxT("SURVEXEDITOR"));
333    if (p) {
334        cmd = p;
335        if (!cmd.find(wxT("$f"))) {
336            cmd += wxT(" $f");
337        }
338    } else {
339        p = wxGetenv(wxT("VISUAL"));
340        if (!p) p = wxGetenv(wxT("EDITOR"));
341        if (!p) {
342            cmd = wxT(DEFAULT_EDITOR_COMMAND);
343        } else {
344            cmd = p;
345            if (cmd == "gvim") {
346                cmd = wxT(GVIM_COMMAND);
347            } else if (cmd == "vim") {
348                cmd = wxT(VIM_COMMAND);
349            } else if (cmd == "nvim") {
350                cmd = wxT(NVIM_COMMAND);
351            } else if (cmd == "gedit") {
352                cmd = wxT(GEDIT_COMMAND);
353            } else if (cmd == "pluma") {
354                cmd = wxT(PLUMA_COMMAND);
355            } else if (cmd == "emacs") {
356                cmd = wxT(EMACS_COMMAND);
357            } else if (cmd == "nano") {
358                cmd = wxT(NANO_COMMAND);
359            } else if (cmd == "jed") {
360                cmd = wxT(JED_COMMAND);
361            } else if (cmd == "kate") {
362                cmd = wxT(KATE_COMMAND);
363            } else {
364                // Escape any $.
365                cmd.Replace(wxT("$"), wxT("$$"));
366                cmd += wxT(" $f");
367            }
368        }
369    }
370    size_t i = 0;
371    while ((i = cmd.find(wxT('$'), i)) != wxString::npos) {
372        if (++i >= cmd.size()) break;
373        switch ((int)cmd[i]) {
374            case wxT('$'):
375                cmd.erase(i, 1);
376                break;
377            case wxT('f'): {
378                wxString f = escape_for_shell(wxString(cur, colon), true);
379                cmd.replace(i - 1, 2, f);
380                i += f.size() - 1;
381                break;
382            }
383            case wxT('l'): {
384                wxString l = escape_for_shell(wxString(cur + colon + 1, colon2 - colon - 1));
385                cmd.replace(i - 1, 2, l);
386                i += l.size() - 1;
387                break;
388            }
389            case wxT('c'): {
390                wxString l;
391                if (colon2 == link_len)
392                    l = wxT("0");
393                else
394                    l = escape_for_shell(wxString(cur + colon2 + 1, link_len - colon2 - 1));
395                cmd.replace(i - 1, 2, l);
396                i += l.size() - 1;
397                break;
398            }
399            default:
400                ++i;
401        }
402    }
403
404    if (wxExecute(cmd, wxEXEC_ASYNC|wxEXEC_MAKE_GROUP_LEADER) >= 0)
405        return;
406
407    wxString m;
408    // TRANSLATORS: %s is replaced by the command we attempted to run.
409    m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
410    m += wxT(" (");
411    m += wxString::FromUTF8(strerror(errno));
412    m += wxT(')');
413    wxGetApp().ReportError(m);
414}
415
416void
417CavernLogWindow::process(const wxString &file)
418{
419    timer.Stop();
420    if (cavern_out) {
421        cavern_out->Detach();
422        cavern_out = NULL;
423    } else {
424        wxBeginBusyCursor();
425    }
426
427    SetFocus();
428    filename = file;
429
430    info_count = 0;
431    link_count = 0;
432    log_txt.resize(0);
433    line_info.resize(0);
434    // Reserve enough that we won't need to grow the allocations in normal cases.
435    log_txt.reserve(16384);
436    line_info.reserve(256);
437    ptr = 0;
438    save_button = nullptr;
439    reprocess_button = nullptr;
440    ok_button = nullptr;
441    DestroyChildren();
442    SetVirtualSize(0, 0);
443
444#ifdef __WXMSW__
445    SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
446#else
447    setenv("SURVEX_UTF8", "1", 1);
448#endif
449
450    wxString escaped_file = escape_for_shell(file, true);
451    wxString cmd = get_command_path(L"cavern");
452    cmd = escape_for_shell(cmd, false);
453    cmd += wxT(" -o ");
454    cmd += escaped_file;
455    cmd += wxT(' ');
456    cmd += escaped_file;
457
458    cavern_out = wxProcess::Open(cmd);
459    if (!cavern_out) {
460        wxString m;
461        m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
462        m += wxT(" (");
463        m += wxString::FromUTF8(strerror(errno));
464        m += wxT(')');
465        wxGetApp().ReportError(m);
466        return;
467    }
468
469    // We want to receive the wxProcessEvent when cavern exits.
470    cavern_out->SetNextHandler(this);
471
472    // Check for output after 500ms if we don't get an idle event sooner.
473    timer.StartOnce(500);
474}
475
476void
477CavernLogWindow::ProcessCavernOutput()
478{
479    // ptr gives the start of the first line we've not yet processed.
480
481    size_t nl;
482    while ((nl = log_txt.find('\n', ptr)) != std::string::npos) {
483        if (nl == ptr || (nl - ptr == 1 && log_txt[ptr] == '\r')) {
484            // Don't show empty lines in the window.
485            ptr = nl + 1;
486            continue;
487        }
488        size_t line_len = nl - ptr - (log_txt[nl - 1] == '\r');
489        // FIXME: Avoid copy, use string_view?
490        string cur(log_txt, ptr, line_len);
491        if (log_txt[ptr] == ' ') {
492            if (expecting_caret_line) {
493                // FIXME: Check the line is only space, `^` and `~`?
494                // Otherwise an error without caret info followed
495                // by an error which contains a '^' gets
496                // mishandled...
497                size_t caret = cur.rfind('^');
498                if (caret != wxString::npos) {
499                    size_t tilde = cur.rfind('~');
500                    if (tilde == wxString::npos || tilde < caret) {
501                        tilde = caret;
502                    }
503                    line_info.back().colour = line_info[line_info.size() - 2].colour;
504                    line_info.back().colour_start = caret;
505                    line_info.back().colour_len = tilde - caret + 1;
506                    expecting_caret_line = false;
507                    ptr = nl + 1;
508                    continue;
509                }
510            }
511            expecting_caret_line = true;
512        }
513        line_info.emplace_back(ptr);
514        line_info.back().len = line_len;
515        size_t colon = cur.find(": ");
516        if (colon != wxString::npos) {
517            size_t link_len = colon;
518            while (colon > 1 && (unsigned)(cur[--colon] - '0') <= 9) { }
519            if (cur[colon] == ':') {
520                line_info.back().link_len = link_len;
521
522                static string info_marker = string(msg(/*info*/485)) + ':';
523                static string warning_marker = string(msg(/*warning*/4)) + ':';
524                static string error_marker = string(msg(/*error*/93)) + ':';
525
526                size_t offset = link_len + 2;
527                if (cur.compare(offset, info_marker.size(), info_marker) == 0) {
528                    // Show "info" marker in blue.
529                    ++info_count;
530                    line_info.back().colour = LOG_INFO;
531                    line_info.back().colour_start = offset;
532                    line_info.back().colour_len = info_marker.size() - 1;
533                } else if (cur.compare(offset, warning_marker.size(), warning_marker) == 0) {
534                    // Show "warning" marker in orange.
535                    line_info.back().colour = LOG_WARNING;
536                    line_info.back().colour_start = offset;
537                    line_info.back().colour_len = warning_marker.size() - 1;
538                } else if (cur.compare(offset, error_marker.size(), error_marker) == 0) {
539                    // Show "error" marker in red.
540                    line_info.back().colour = LOG_ERROR;
541                    line_info.back().colour_start = offset;
542                    line_info.back().colour_len = error_marker.size() - 1;
543                }
544                ++link_count;
545            }
546        }
547
548        int fsize = GetFont().GetPixelSize().GetHeight();
549        SetScrollRate(fsize, fsize);
550
551        auto& info = line_info.back();
552        info.link_pixel_width = GetTextExtent(wxString(&log_txt[ptr], info.link_len)).GetWidth();
553        auto rest_pixel_width = GetTextExtent(wxString(&log_txt[ptr + info.link_len], info.len - info.link_len)).GetWidth();
554        int width = max(GetVirtualSize().GetWidth(),
555                        int(fsize + info.link_pixel_width + rest_pixel_width));
556        int height = line_info.size();
557        SetVirtualSize(width, height * fsize);
558        if (!link_count) {
559            // Auto-scroll until the first diagnostic.
560            int scroll_x = 0, scroll_y = 0;
561            GetViewStart(&scroll_x, &scroll_y);
562            int xs, ys;
563            GetClientSize(&xs, &ys);
564            Scroll(scroll_x, line_info.size() * fsize - ys);
565        }
566        ptr = nl + 1;
567    }
568}
569
570void
571CavernLogWindow::OnEndProcess(wxProcessEvent & evt)
572{
573    bool cavern_success = evt.GetExitCode() == 0;
574
575    // Read and process any remaining buffered output.
576    wxInputStream* in = cavern_out->GetInputStream();
577    while (!in->Eof()) {
578        CheckForOutput(true);
579    }
580
581    wxEndBusyCursor();
582
583    delete cavern_out;
584    cavern_out = NULL;
585
586    // Initially place buttons off the right of the window - they get moved to
587    // the desired position by OnPaintButton().
588    wxPoint off_right(GetSize().x, 0);
589    /* TRANSLATORS: Label for button in aven’s cavern log window which
590     * allows the user to save the log to a file. */
591    save_button = new wxButton(this, LOG_SAVE, wmsg(/*&Save Log*/446), off_right);
592
593    /* TRANSLATORS: Label for button in aven’s cavern log window which
594     * causes the survey data to be reprocessed. */
595    reprocess_button = new wxButton(this, LOG_REPROCESS, wmsg(/*&Reprocess*/184), off_right);
596
597    if (cavern_success) {
598        ok_button = new wxButton(this, wxID_OK, wxString(), off_right);
599        ok_button->SetDefault();
600    }
601
602    Refresh();
603    if (!cavern_success) {
604        return;
605    }
606
607    init_done = false;
608
609    {
610        wxString file3d(filename, 0, filename.length() - 3);
611        file3d.append(wxT("3d"));
612        if (!mainfrm->LoadData(file3d, survey)) {
613            return;
614        }
615    }
616
617    // Don't stay on log if there there are only "info" diagnostics.
618    if (link_count == info_count) {
619        wxCommandEvent dummy;
620        OnOK(dummy);
621    }
622}
623
624void
625CavernLogWindow::OnReprocess(wxCommandEvent &)
626{
627    process(filename);
628}
629
630void
631CavernLogWindow::OnSave(wxCommandEvent &)
632{
633    wxString filelog(filename, 0, filename.length() - 3);
634#ifdef __WXMSW__
635    // We need to consistently use `\` here.
636    filelog.Replace("/", "\\");
637#endif
638    filelog += wxT("log");
639#ifdef __WXMOTIF__
640    wxString ext(wxT("*.log"));
641#else
642    /* TRANSLATORS: Log files from running cavern (extension .log) */
643    wxString ext = wmsg(/*Log files*/447);
644    ext += wxT("|*.log");
645#endif
646    wxFileDialog dlg(this, wmsg(/*Select an output filename*/319),
647                     wxString(), filelog, ext,
648                     wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
649    if (dlg.ShowModal() != wxID_OK) return;
650    filelog = dlg.GetPath();
651    FILE * fh_log = wxFopen(filelog, wxT("w"));
652    if (!fh_log) {
653        wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog.c_str()));
654        return;
655    }
656    fwrite(log_txt.data(), log_txt.size(), 1, fh_log);
657    fclose(fh_log);
658}
659
660void
661CavernLogWindow::OnOK(wxCommandEvent &)
662{
663    if (init_done) {
664        mainfrm->HideLog(this);
665    } else {
666        mainfrm->InitialiseAfterLoad(filename, survey);
667        init_done = true;
668    }
669}
Note: See TracBrowser for help on using the repository browser.