--- nono/wx/wxlogmonitor.cpp 2026/04/29 17:04:28 1.1.1.1 +++ nono/wx/wxlogmonitor.cpp 2026/04/29 17:05:31 1.1.1.12 @@ -1,161 +1,334 @@ // // nono -// Copyright (C) 2018 isaki@NetBSD.org +// Copyright (C) 2020 nono project +// Licensed under nono-license.txt // -#include "wxheader.h" -#include "wxlogmonitor.h" -#include "logger.h" - // // ログモニター // +#include "wxlogmonitor.h" +#include "logger.h" +#include "mainapp.h" +#include "monitor.h" +#include "sjis.h" + // イベントテーブル wxBEGIN_EVENT_TABLE(WXLogMonitor, inherited) - EVT_TIMER(wxID_ANY, WXLogMonitor::OnTimer) + EVT_SIZE(WXLogMonitor::OnSize) wxEND_EVENT_TABLE() // コンストラクタ -WXLogMonitor::WXLogMonitor(wxWindow *parent, wxWindowID id) - : inherited(parent, id, "ログモニター") +WXLogMonitor::WXLogMonitor(wxWindow *parent) + : inherited(parent, wxID_ANY, _("Log"), DEFAULT_STYLE | wxRESIZE_BORDER) { col = 80; row = 40; - logbuf = new uint8 [row * col]; - memset(logbuf, 0x20, row * col); - text.Init(col, row); + // プライベート利用なので Regist() しない。 + monitor.reset(new Monitor(-1, &log)); + monitor->func = ToMonitorCallback(&LogMonitor::MonitorUpdate); + monitor->SetSize(col, row); + + log.Init(this); + + // → + // +------------+---------+ + // | TextScreen | VScroll | + // +------------+---------+ + + wxBoxSizer *topsizer = new wxBoxSizer(wxHORIZONTAL); // テキストスクリーン - screen = new WXTextScreen(this, col, row, text.GetBuf()); + screen = new WXMonitorPanel(this, monitor.get()); + topsizer->Add(screen, 1, wxEXPAND); - // 大きさを固定 - Layout(); - Fit(); - - // 最初に一度描画しておく。 - // ウィンドウが表示されてもまだログが出てない場合、タイマーイベントが - // おきても更新が起きず、グレーの背景が見えてしまうため。 - // (ウィンドウの表示が早い場合や、ログレベルが低い場合は有り得る) - screen->Refresh(); - - // 更新タイマーは適当な短さにしておく - timer.SetOwner(this); - timer.Start(10); + // スクロールバー + vscroll = new WXScrollBar(this, wxID_ANY, wxSB_VERTICAL); + topsizer->Add(vscroll, 0, wxEXPAND); + + SetSizer(topsizer); + // ウィンドウサイズを確定させる + DoSize(); + + // スクロールのために (パネル領域での) MouseWheel イベントもここで + // 受け取りたい。スクロールバー上のマウスホイール操作はスクロールバーが + // 処理しているので問題ないが、パネル領域でのマウスホイール操作はここ + // (wxFrame)ではなくパネルに対して飛んでくる。 + // ここで一緒に処理したほうが楽なので、こちらに回す。 + screen->Connect(wxEVT_MOUSEWHEEL, + wxMouseEventHandler(WXLogMonitor::OnMouseWheel), NULL, this); + + // スクロールバーからの位置変更通知 + vscroll->Connect(NONO_EVT_SCROLL, + wxScrollEventHandler(WXLogMonitor::OnScroll), NULL, this); + + // XXX ログウィンドウのコピーはまだ動いてない + screen->DisconnectContextMenu(); } // デストラクタ WXLogMonitor::~WXLogMonitor() { - if (logbuf) { - delete logbuf; +} + +bool +WXLogMonitor::Layout() +{ + // 縦リサイズ可能レイアウト + return LayoutTextVResize(screen); +} + +// サイズ変更イベント +void +WXLogMonitor::OnSize(wxSizeEvent& event) +{ + inherited::OnSize(event); + + // スクロールバーを再設定 + SetScroll(); +} + +// スクロールバーを再設定 +void +WXLogMonitor::SetScroll() +{ + int pos; + int thumbsize; + int range; + int pagesize; + + row = screen->GetRow(); + + if (lines <= row) { + pos = 0; + range = row; + } else { + pos = (lines - row) - vpos; + range = lines; } + thumbsize = row; + pagesize = thumbsize - 1; + vscroll->SetScrollParam(pos, thumbsize, range, pagesize); } -// タイマーイベント +// マウスホイールイベント (WXTextScreen 上で起きたやつをこっちに回してある) void -WXLogMonitor::OnTimer(wxTimerEvent& event) +WXLogMonitor::OnMouseWheel(wxMouseEvent& event) +{ + // GetWheelRotation() は上(奥)向きに回すと +120、下(手前)に回すと -120。 + // どこの決まりか知らんけど、スクロールバーのほうはホイール1段階で3行 + // スクロールするのでそれに合わせる。 + int pos = vscroll->GetThumbPosition(); + pos = pos - event.GetWheelRotation() / 40; + + int maxpos = vscroll->GetRange() - vscroll->GetThumbSize(); + if (pos < 0) + pos = 0; + if (pos > maxpos) + pos = maxpos; + + DoScroll(pos); + // スクロールバーの位置を追従 + vscroll->SetThumbPosition(pos); +} + +// スクロールバーからの通知イベント +void +WXLogMonitor::OnScroll(wxScrollEvent& event) +{ + DoScroll(event.GetPosition()); +} + +// スクロール処理 (OnScroll() と OnMouseWheel() から呼ばれる) +void +WXLogMonitor::DoScroll(int pos) +{ + // pos 即ちスクロールバーの Thumb の位置は上端の位置。 + // 対してログウィンドウでは下端を基準にしたほうが処理しやすい。 + // vpos = 0 だと Thumb が下に接していてパネルの最下行が最新行、 + // vpos = 1 だと Thumb が下から1段階分上にありパネルの最下行は最新行の + // 一つ前の行、となる。 + // + // Range=6 Range=6 + // Pos=4 Pos=0 + // ThumbSz=2 ThumbSz=2 + // vpos=0 vpos=4 + // △ △ + // □ ■ + // □ ■ + // □ □ + // □ □ + // ■ □ + // ■ □ + // ▽ ▽ + + // パネル最下行に表示する行の最新行からのオフセット + // (0 なら Thumb が下端に接している) + vpos = vscroll->GetRange() - vscroll->GetThumbSize() - pos; +} + + +// +// モニターオブジェクト +// + +// コンストラクタ +LogMonitor::LogMonitor() + : inherited(OBJ_LOGMONITOR) +{ +} + +// デストラクタ +LogMonitor::~LogMonitor() +{ +} + +void +LogMonitor::Init(WXLogMonitor *parent_) +{ + parent = parent_; + + logger = gMainApp.GetLogger(); + + // バックログは今の所固定長 + logbuf.Init(parent->col, backlog); + logbuf.Mode = TextScreen::Ring; +} + +// モニターテキストを更新 +void +LogMonitor::MonitorUpdate(Monitor *, TextScreen& screen) { char buf[1024]; - bool updated = false; - // キューから読めるだけ全部ログバッファに書き込む - while (gLogger->Read(buf, sizeof(buf))) { + // どこでどうするかはあるけど、 + // とりあえず row は常に現在の TextScreen の高さということにしておく + parent->row = screen.GetRow(); + + // XXX: 生成時にやるべき + screen.Mode = TextScreen::Ring; + + // キューから読めるだけバックログに書き込む。 + // 読めるだけと言っても while ループで読めるだけ読んでしまうと、 + // 流速が早すぎた場合にこのループから抜けるタイミングがなくなる可能性が + // (可能性としては) あるので、適当に1画面分くらいで打ち切る。 + // 取り込み残した分は次回取り込まれるし、その流速も上回るようだと + // どのみち Logger の固定長キューで落ちる。 + for (int i = 0; i < parent->row; i++) { + if (logger->Read(buf, sizeof(buf)) == false) + break; Append(buf); - updated = true; } - // ログの追加がなければここまで - if (!updated) { - return; - } - - // ここは TextScreen の内部構造に直接書き込むことにする。 - uint16 *textbuf = text.GetRawBuf(); + screen.Clear(); - // ログバッファ(循環バッファ)をテキストバッファに再構成。 - // target は logbuf のうち今表示されるべき一番古い行の先頭を指している - // のでこれがテキストバッファの先頭になる。そこから logbuf の末尾まで。 - // target == 0 の時に限りこの1回のコピーで終わり。それ以外の場合は必ず - // logbuf の先頭から target までの後半部分のコピーが発生する。 - for (int i = 0; i < col * row - target; i++) { - textbuf[i] = logbuf[target + i]; + // スクロールバー(vpos)による表示位置計算 + // vpos は行数、v は logbuf の Y 座標 + int v = cursor - parent->vpos; + if (v < 0) { + v += backlog; } - if (target != 0) { - for (int i = 0; i < target; i++) { - textbuf[col * row - target + i] = logbuf[i]; - } + + // 表示位置(v)の Row 行分手前から順に表示 + int srcY = v - parent->row; + if (srcY < 0) { + srcY += backlog; } - // 更新指示 - screen->Refresh(); + // 画面コピー + logbuf.Locate(0, srcY); + for (int i = 0; i < parent->col * parent->row; i++) { + uint16 ch = logbuf.Getc(); + logbuf.Ascend(); + screen.Putc(ch); + } } -// フォントサイズ変更 +// 1行追加された時の共通処理 void -WXLogMonitor::SetFontSize(fontsize_t fontsize) +LogMonitor::IncLines() { - screen->SetFontSize(fontsize); - // ログの再描画は自前で行う必要がある - screen->Refresh(); - // 大きさを変更 - Layout(); - Fit(); + // スクロールバーが下端にない時(バックログを見ている時)は + // ログが追加されても現在位置をキープする。 + if (parent->vpos > 0) { + // 上端に達したら増加させない + if (parent->vpos < backlog - parent->vscroll->GetThumbSize()) { + parent->vpos++; + } + } + + // 有効行数を増やす + if (parent->lines < backlog) { + parent->lines++; + + // 行数が変わったのでスクロールバーを更新 + parent->SetScroll(); + } } -// 表示用のログバッファに buf を追加 +// バックログに行を追加。 // -// logbuf は textbuf(, prevbuf) とかと同じ col * row バイトの単純バッファ -// 形式。ただしこれを行ごとの循環バッファとして使っていて開始位置が target。 -// target は logbuf からの距離 (logbuf[] のインデックス) で表し、行は必ず -// ウィンドウの行頭から追加することにすると target は col の倍数をとる。 +// 引数の utfbuf は UTF-8 で改行コードのない1行ずつ。 +// これをバックログの最新位置に書き込む。古い行は順次上書きされる。 // -// +---------------------------+ -// logbuf -> | | -// |前行 | -// target -> |次に書く行 (今表示されてる一番古い行) -// | | -// | | -// | | -// +---------------------------+ +// バックログは col * backlog バイトの Shift_JIS バッファ。 +// 属性は持たず文字コードのみ。漢字は2バイト幅をとる。 +// 横幅 col が固定なので書き込む時点で行数は表示用のそれと一致する。 // -// ついでに utfbuf は UTF-8 だが、表示用バッファ(textbuf) は Shift_JIS -// なのでここで変換する。 +// cursor はこれから書き込む位置の行頭。cursor は必ず行頭だけをポイント +// しながら移動する。 +// +// logbuf +------------------------ +// | : +// | 前回書き込んだ行 +// cursor -> | 今から書きこむ行 (保存されていた一番古い行) +// : +// | +// +------------------------ void -WXLogMonitor::Append(const char *utfbuf) +LogMonitor::Append(const char *utfbuf) { - const char *sjis; - int sjislen; - int spos; + const char *s; + int x; // 文字コードの変換 wxString utfstr(utfbuf, wxConvUTF8); - sjis = (const char *)utfstr.mb_str(conv); - sjislen = strlen(sjis); - - // XXX 行末残り1桁で漢字が来た場合は? + // mb_str() をキャストせずに一度保持しておく必要がある (scan-build 対策) + auto sjis = utfstr.mb_str(conv); - spos = 0; - for (;;) { - // この行に書き込む長さ - int len = std::min(sjislen - spos, col); - if (len < 1) { - break; - } + logbuf.Locate(0, cursor); - // コピー。残りは空白で埋める - memcpy(logbuf + target, sjis + spos, len); - if (len < col) { - memset(logbuf + target + len, 0x20, col - len); + for (s = (const char *)sjis; *s; ) { + // 最終桁で2バイト文字の1バイト目が来たら + // 代わりに1文字空白を入れる (それによってこの後改行が起きる) + if (SJIS::IsZenkaku(*s)) { + // 漢字ならば 2 バイトセットで扱う + if (logbuf.GetX() >= parent->col - 1) { + // 最終桁には出せないので空白で次の行に送る + logbuf.Putc(' '); + IncLines(); + } + logbuf.Putc(*s++); + // 万が一に備える + if (__predict_true(*s)) { + logbuf.Putc(*s++); + } + } else { + // そうでなければそのまま出力 + logbuf.Putc(*s++); } + } - spos += len; - target += col; - - // 折り返し - if (target >= col * row) { - target = 0; + // 行の終わりまでは空白でクリアする + x = logbuf.GetX(); + if (x > 0) { + for (; x < logbuf.GetCol(); x++) { + logbuf.Putc(' '); } + IncLines(); } + // 次回書き込みの Y 座標 + cursor = logbuf.GetY(); }