--- nono/wx/wxlogmonitor.cpp 2026/04/29 17:04:30 1.1.1.2 +++ nono/wx/wxlogmonitor.cpp 2026/04/29 17:04:39 1.1.1.4 @@ -1,9 +1,9 @@ // // 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" @@ -13,149 +13,332 @@ // イベントテーブル 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, wxT("ログモニター")) + : inherited(parent, id, _("Log")) { col = 80; row = 40; - logbuf = new uint8 [row * col]; - memset(logbuf, 0x20, row * col); - text.Init(col, row); - - // テキストスクリーン - screen = new WXTextScreen(this, col, row, text.GetBuf()); - - // 大きさを固定 - Layout(); - Fit(); - - // 最初に一度描画しておく。 - // ウィンドウが表示されてもまだログが出てない場合、タイマーイベントが - // おきても更新が起きず、グレーの背景が見えてしまうため。 - // (ウィンドウの表示が早い場合や、ログレベルが低い場合は有り得る) - screen->Refresh(); - - // 更新タイマーは適当な短さにしておく - timer.SetOwner(this); - timer.Start(10); + // バックログは今の所固定長 + logbuf.resize(col * backlog, ' '); + + // → + // +--+-----------------------+--+--+ + // | |↓+------------------+ | | | + // | | | 上余白(padding1) | | | | + // |左| +------------------+ |右| | + // |余| | TextScreen | |余| <- VScroll + // |白| +------------------+ |白| | + // | | | 下余白(padding2) | | | | + // | | +------------------+ | | | + // +--+-----------------------+--+--+ + + topsizer = new wxBoxSizer(wxHORIZONTAL); + // 左余白 + topsizer->AddSpacer(DefaultPadding); + // 中列の縦 sizer + auto *vbox = new wxBoxSizer(wxVERTICAL); + topsizer->Add(vbox, 0, wxEXPAND); + + // パディングパネル(上下) + // ウィンドウを1行ずつ伸縮させるためには、クライアント領域がフォント高さの + // 整数倍になってないといけない。TextScreen はそれ自身が行の整数倍の大きさ + // だが、上下の余白はその時点のフォントサイズによって変更する必要がある。 + // 先にサイズ不定のパネルだけ置いといて、フォントサイズ確定後に大きさを + // 確定させる。DoSize() 参照。 + padding1 = new wxPanel(this, wxID_ANY); + padding2 = new wxPanel(this, wxID_ANY); + vbox->Add(padding1); + screen = new WXTextScreen(this, *this); + vbox->Add(screen, 1, wxEXPAND); + vbox->Add(padding2); + + // 右余白 + topsizer->AddSpacer(DefaultPadding); + // スクロールバー + vscroll = new wxScrollBar(this, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxSB_VERTICAL); + topsizer->Add(vscroll, 0, wxEXPAND); + + SetSizer(topsizer); + + // ウィンドウサイズを確定させる + DoSize(); + + // スクロールのために (パネル領域での) MouseWheel イベントもここで + // 受け取りたい。スクロールバー上のマウスホイール操作は最初から Scroll + // イベントとして処理されているので問題ないが、パネル領域でのマウス + // ホイール操作はここ(wxFrame)ではなくパネルに対して飛んでくる。 + // ここで一緒に処理したほうが楽なので、こちらに回す。 + screen->Connect(wxEVT_MOUSEWHEEL, + wxMouseEventHandler(WXLogMonitor::OnMouseWheel), NULL, this); } // デストラクタ WXLogMonitor::~WXLogMonitor() { - if (logbuf) { - delete logbuf; +} + +// フォントサイズが確定したら、それによってウィンドウサイズを再計算する。 +void +WXLogMonitor::DoSize() +{ + CalcVSize(0); +} + +// サイズ変更イベント +void +WXLogMonitor::OnSize(wxSizeEvent& event) +{ + inherited::OnSize(event); + + // スクロールバーを再設定 + SetScroll(); +} + +// スクロールバーを再設定 +void +WXLogMonitor::SetScroll() +{ + int pos; + int thumbsize; + int range; + int pagesize; + + const wxSize& size = screen->GetSize(); + row = size.GetHeight(); + + 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::OnTimer(wxTimerEvent& event) +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); +} + +// モニターサイズ取得 +nnSize +WXLogMonitor::GetMonitorSize() +{ + return nnSize(col, row); +} + +// バックログから TextScreen へのコピー +#define CopyText(text, row) do { \ + for (int i = 0; i < (row); i++) { \ + text.Locate(0, y++); \ + for (int x = 0; x < col; x++) { \ + text.Putc(*s++); \ + } \ + } \ +} while (0) + +// モニターテキストを更新 +void +WXLogMonitor::MonitorUpdate(TextScreen& text) { char buf[1024]; - bool updated = false; - // キューから読めるだけ全部ログバッファに書き込む - while (gLogger->Read(buf, sizeof(buf))) { + // どこでどうするかはあるけど、 + // とりあえず row は常に現在の TextScreen の高さということにしておく + row = text.GetRow(); + + // キューから読めるだけバックログに書き込む。 + // 読めるだけと言っても while ループで読めるだけ読んでしまうと、 + // 流速が早すぎた場合にこのループから抜けるタイミングがなくなる可能性が + // (可能性としては) あるので、適当に1画面分くらいで打ち切る。 + // 取り込み残した分は次回取り込まれるし、その流速も上回るようだと + // どのみち gLogger の固定長キューで落ちる。 + for (int i = 0; i < row; i++) { + if (gLogger.Read(buf, sizeof(buf)) == false) + break; Append(buf); - updated = true; } - // ログの追加がなければここまで - if (!updated) { - return; + text.Clear(); + + // スクロールバー(vpos)による表示位置計算 + // vpos は行数、v は logbuf[] 先頭からのバイトオフセット + int v = cursor - col * vpos; + if (v < 0) { + v += col * backlog; } - // ここは TextScreen の内部構造に直接書き込むことにする。 - uint16 *textbuf = text.GetRawBuf(); + // 表示位置(v)の Row 行分手前から順に表示 + int size = col * row; + int y = 0; + uint8 *s; + if (v >= size) { + // 折り返しなし + s = &logbuf[v - size]; + CopyText(text, row); + } else { + // 折り返しあり + int row1 = (size - v) / col; + int row2 = row - row1; + s = &logbuf[col * backlog - (size - v)]; + CopyText(text, row1); + s = &logbuf[0]; + CopyText(text, row2); + } +} - // ログバッファ(循環バッファ)をテキストバッファに再構成。 - // target は logbuf のうち今表示されるべき一番古い行の先頭を指している - // のでこれがテキストバッファの先頭になる。そこから logbuf の末尾まで。 - // target == 0 の時に限りこの1回のコピーで終わり。それ以外の場合は必ず - // logbuf の先頭から target までの後半部分のコピーが発生する。 - for (int i = 0; i < col * row - target; i++) { - textbuf[i] = logbuf[target + i]; +// 1行追加された時の共通処理 +void +WXLogMonitor::IncLines() +{ + // カーソルを次行先頭に移動 + cursor += col; + // バッファ末尾まで来たら折り返し + if (cursor >= col * backlog) { + cursor = 0; } - if (target != 0) { - for (int i = 0; i < target; i++) { - textbuf[col * row - target + i] = logbuf[i]; + + // スクロールバーが下端にない時(バックログを見ている時)は + // ログが追加されても現在位置をキープする。 + if (vpos > 0) { + // 上端に達したら増加させない + if (vpos < backlog - vscroll->GetThumbSize()) { + vpos++; } } - // 更新指示 - screen->Refresh(); -} + // 有効行数を増やす + if (lines < backlog) { + lines++; -// フォントサイズ変更 -void -WXLogMonitor::SetFontSize(fontsize_t fontsize) -{ - screen->SetFontSize(fontsize); - // ログの再描画は自前で行う必要がある - screen->Refresh(); - // 大きさを変更 - Layout(); - Fit(); + // 行数が変わったのでスクロールバーを更新 + 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) { const char *sjis; - int sjislen; - int spos; + const char *s; + int x; + bool iskanji; // 文字コードの変換 wxString utfstr(utfbuf, wxConvUTF8); sjis = (const char *)utfstr.mb_str(conv); - sjislen = strlen(sjis); - - // XXX 行末残り1桁で漢字が来た場合は? - spos = 0; - for (;;) { - // この行に書き込む長さ - int len = std::min(sjislen - spos, col); - if (len < 1) { - break; + iskanji = false; + s = sjis; + x = 0; + for (; *s; ) { + // Shift_JIS の1文字目は上位ニブルが 8,9,E。 + // 最終桁で2バイト文字の1バイト目が来たら + // 代わりに1文字空白を入れる (それによってこの後改行が起きる) + if (iskanji == false && + (int16)(0b00000000'11000010 << (*s >> 4)) < 0 && + x >= col - 1) + { + logbuf[cursor + x] = ' '; + } else { + // そうでなければそのまま出力 + logbuf[cursor + x] = *s++; + iskanji = false; } + x++; - // コピー。残りは空白で埋める - memcpy(logbuf + target, sjis + spos, len); - if (len < col) { - memset(logbuf + target + len, 0x20, col - len); + // 行端まで来たら改行 + if (x >= col) { + IncLines(); + // 行頭 + x = 0; } + } - spos += len; - target += col; - - // 折り返し - if (target >= col * row) { - target = 0; - } + // 1行追加し終わって行頭でなければ、改行 + if (x > 0) { + IncLines(); } }