--- nono/wx/wxlogmonitor.cpp 2026/04/29 17:04:36 1.1.1.3 +++ nono/wx/wxlogmonitor.cpp 2026/04/29 17:04:56 1.1.1.7 @@ -6,6 +6,7 @@ #include "wxlogmonitor.h" #include "logger.h" +#include "sjis.h" // // ログモニター @@ -13,41 +14,74 @@ // イベントテーブル wxBEGIN_EVENT_TABLE(WXLogMonitor, inherited) - EVT_TIMER(wxID_ANY, WXLogMonitor::OnTimer) + EVT_SIZE(WXLogMonitor::OnSize) + EVT_SCROLL(WXLogMonitor::OnScroll) wxEND_EVENT_TABLE() // コンストラクタ -WXLogMonitor::WXLogMonitor(wxWindow *parent, wxWindowID id) - : inherited(parent, id, _("Log")) +WXLogMonitor::WXLogMonitor(wxWindow *parent) + : inherited(parent, wxID_ANY, _("Log")) { col = 80; row = 40; - logbuf.reset(new uint8 [row * col]); - memset(&logbuf[0], 0x20, row * col); - text.Init(col, row); - - wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL); - - // テキストスクリーン - screen = new WXTextScreen(this, col, row, text.GetBuf()); - topsizer->Add(screen, BorderFlags); + monitor.obj = &log; + monitor.func = (MonitorCallback_t)&LogMonitor::MonitorUpdate; + monitor.SetSize(col, row); + // プライベート利用なので Regist() は不要 + + log.Init(this); + + // → + // +--+-----------------------+--+--+ + // | |↓+------------------+ | | | + // | | | 上余白(padding1) | | | | + // |左| +------------------+ |右| | + // |余| | TextScreen | |余| <- VScroll + // |白| +------------------+ |白| | + // | | | 下余白(padding2) | | | | + // | | +------------------+ | | | + // +--+-----------------------+--+--+ + + topsizer = new wxBoxSizer(wxHORIZONTAL); + // 左余白 + topsizer->Add(new WXPaddingPanel(this), 0, wxEXPAND); + // 中列の縦 sizer + auto *vbox = new wxBoxSizer(wxVERTICAL); + topsizer->Add(vbox, 0, wxEXPAND); + + // パディングパネル(上下) + // ウィンドウを1行ずつ伸縮させるためには、クライアント領域がフォント高さの + // 整数倍になってないといけない。TextScreen はそれ自身が行の整数倍の大きさ + // だが、上下の余白はその時点のフォントサイズによって変更する必要がある。 + // 先にサイズ不定のパネルだけ置いといて、フォントサイズ確定後に大きさを + // 確定させる。DoSize() 参照。 + padding1 = new WXPaddingPanel(this); + padding2 = new WXPaddingPanel(this); + screen = new WXTextScreen(this, monitor); + vbox->Add(padding1, 0, wxEXPAND); + vbox->Add(screen, 1, wxEXPAND); + vbox->Add(padding2, 0, wxEXPAND); + + // 右余白 + topsizer->Add(new WXPaddingPanel(this), 0, wxEXPAND); + // スクロールバー + vscroll = new wxScrollBar(this, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxSB_VERTICAL); + topsizer->Add(vscroll, 0, wxEXPAND); SetSizer(topsizer); - // 大きさを固定 - Layout(); - Fit(); - - // 最初に一度描画しておく。 - // ウィンドウが表示されてもまだログが出てない場合、タイマーイベントが - // おきても更新が起きず、グレーの背景が見えてしまうため。 - // (ウィンドウの表示が早い場合や、ログレベルが低い場合は有り得る) - screen->Refresh(); - - // 更新タイマーは適当な短さにしておく - timer.SetOwner(this); - timer.Start(10); + // ウィンドウサイズを確定させる + DoSize(); + + // スクロールのために (パネル領域での) MouseWheel イベントもここで + // 受け取りたい。スクロールバー上のマウスホイール操作は最初から Scroll + // イベントとして処理されているので問題ないが、パネル領域でのマウス + // ホイール操作はここ(wxFrame)ではなくパネルに対して飛んでくる。 + // ここで一緒に処理したほうが楽なので、こちらに回す。 + screen->Connect(wxEVT_MOUSEWHEEL, + wxMouseEventHandler(WXLogMonitor::OnMouseWheel), NULL, this); } // デストラクタ @@ -55,109 +89,259 @@ WXLogMonitor::~WXLogMonitor() { } -// タイマーイベント +// フォントサイズが確定したら、それによってウィンドウサイズを再計算する。 void -WXLogMonitor::OnTimer(wxTimerEvent& event) +WXLogMonitor::DoSize() { - char buf[1024]; - bool updated = false; + CalcVSize(0); +} - // キューから読めるだけ全部ログバッファに書き込む - while (gLogger.Read(buf, sizeof(buf))) { - Append(buf); - updated = true; +// サイズ変更イベント +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->SetScrollbar(pos, thumbsize, range, pagesize); +} + +// マウスホイールイベント (WXTextScreen 上で起きたやつをこっちに回してある) +void +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); +} + +// スクロールイベント (全部入り) +void +WXLogMonitor::OnScroll(wxScrollEvent& event) +{ + // Thumb の上端の位置 (0 なら Thumb が上端に接している) + 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; + + // Thumb の位置を更新 + vscroll->SetThumbPosition(pos); +} + +// +// モニターオブジェクト +// + +// コンストラクタ +LogMonitor::LogMonitor() + : inherited("LogMonitor") +{ +} + +// デストラクタ +LogMonitor::~LogMonitor() +{ +} + +void +LogMonitor::Init(WXLogMonitor *parent_) +{ + parent = parent_; + + // バックログは今の所固定長 + logbuf.Init(parent->col, backlog); + logbuf.Mode = TextScreen::Ring; +} + +// モニターテキストを更新 +void +LogMonitor::MonitorUpdate(Monitor *, TextScreen& screen) +{ + char buf[1024]; - // ログの追加がなければここまで - if (!updated) { - return; + // どこでどうするかはあるけど、 + // とりあえず row は常に現在の TextScreen の高さということにしておく + parent->row = screen.GetRow(); + + // XXX: 生成時にやるべき + screen.Mode = TextScreen::Ring; + + // キューから読めるだけバックログに書き込む。 + // 読めるだけと言っても while ループで読めるだけ読んでしまうと、 + // 流速が早すぎた場合にこのループから抜けるタイミングがなくなる可能性が + // (可能性としては) あるので、適当に1画面分くらいで打ち切る。 + // 取り込み残した分は次回取り込まれるし、その流速も上回るようだと + // どのみち gLogger の固定長キューで落ちる。 + for (int i = 0; i < parent->row; i++) { + if (gLogger.Read(buf, sizeof(buf)) == false) + break; + Append(buf); } - // ここは 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++) { + screen.Putc(logbuf.Getc()); + } } -// フォントサイズ変更 +// 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 を追加 +// バックログに行を追加。 +// +// 引数の utfbuf は UTF-8 で改行コードのない1行ずつ。 +// これをバックログの最新位置に書き込む。古い行は順次上書きされる。 // -// logbuf は textbuf(, prevbuf) とかと同じ col * row バイトの単純バッファ -// 形式。ただしこれを行ごとの循環バッファとして使っていて開始位置が target。 -// target は logbuf からの距離 (logbuf[] のインデックス) で表し、行は必ず -// ウィンドウの行頭から追加することにすると target は col の倍数をとる。 +// バックログは col * backlog バイトの Shift_JIS バッファ。 +// 属性は持たず文字コードのみ。漢字は2バイト幅をとる。 +// 横幅 col が固定なので書き込む時点で行数は表示用のそれと一致する。 // -// +---------------------------+ -// logbuf -> | | -// |前行 | -// target -> |次に書く行 (今表示されてる一番古い行) -// | | -// | | -// | | -// +---------------------------+ +// cursor はこれから書き込む位置の行頭。cursor は必ず行頭だけをポイント +// しながら移動する。 // -// ついでに utfbuf は UTF-8 だが、表示用バッファ(textbuf) は Shift_JIS -// なのでここで変換する。 +// 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桁で漢字が来た場合は? + logbuf.Locate(0, cursor); - spos = 0; - for (;;) { - // この行に書き込む長さ - int len = std::min(sjislen - spos, col); - if (len < 1) { - break; + for (s = 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++); } + } - // コピー。残りは空白で埋める - memcpy(&logbuf[target], &sjis[spos], len); - if (len < col) { - memset(&logbuf[target + len], 0x20, col - len); - } - - 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(); }