File:  [Isaki's NoNo m68k/m88k emulator] / nono / wx / wxscrollbar.cpp
Revision 1.1.1.5 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:05:57 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2021 nono project
// Licensed under nono-license.txt
//

//
// 独自スクロールバー
//

// GTK のスクロールバーが色々残念なので、独自コントロールを作る。
//
// pos、thumbsize、range、pagesize は wxScrollBar のそれと同じなのでそちら参照。
// 例えば全体で100要素あるうち画面に10要素分だけ表示できる構成の場合、
// range = 100、thumbsize = 10 となる。表示位置は、一番上(左)の要素から表示
// している場合は pos = 0、一番下(右)の要素まで表示している場合は pos = 90。
//
//  0    10                90  100
//  +---+-----------------+---+
//
// ピクセル座標では、上(左)に 2ピクセル、下(右)に1ピクセルのフチがある。
// そのため range として使える領域は bitmap.GetHeight() (または GetWidth())
// から 3 引いたサイズになる。
//
// 左フチ  つまみ     右フチ
//  +--+  ●----○      ++
//   0 1  p1    p2      h-1
//  明□□□□□□□□□明
//  明□□■■ □□□□明
//  明□□■■ □□□□明
//  明□□   □□□□明
//  明明明明明明明明明明明
//
// 明明明明明 0   <- 上フチ
// □□□□明 1   <- 上フチ
// □□□□明
// □■■ 明 p1 ●
// □■■ 明    | つまみ
// □   明    |
// □□□□明 p2 ○
// □□□□明
// □□□□明
// 明明明明明 h-1 <- 下フチ
//
// 明 = UD_LIGHT_GREY
// □ = UD_GREY
// ■ = BGPANEL
//   = UD_BLACK

#include "wxscrollbar.h"

//#define SCROLL_DEBUG 1

#if defined(SCROLL_DEBUG)
#define DPRINTF(fmt...) printf(fmt)
#else
#define DPRINTF(fmt...) /**/
#endif

#define BORDER_LT	(2)		// 上(左) 側のフチのピクセル数
#define BORDER_RB	(1)		// 下(右) 側のフチのピクセル数

// 通知イベント。
// このコントロールがつまみの位置を変更した場合にイベントが発生する。
// SetThumbPosition() などでコードが能動的に変更した場合は発生しない。
// イベントの内容は概ね wxScrollEvent 準拠のはず。
// ただし、引数は wxScrollEvent だが、イベントタイプは EVT_SCROLL_* では
// なく wxCommandEvent の NONO_EVT_SCROLL を使用していること、
// wxScrollBar と異なり自動的に親かだれかにイベントが飛んでこないので
// 必要な人が適宜 Bind() する必要がある、ところが異なる。
wxDEFINE_EVENT(NONO_EVT_SCROLL, wxScrollEvent);

// イベントテーブル
wxBEGIN_EVENT_TABLE(WXScrollBar, inherited)
	EVT_SIZE(WXScrollBar::OnSize)
	EVT_MOUSE_EVENTS(WXScrollBar::OnMouse)
	EVT_TIMER(wxID_ANY, WXScrollBar::OnTimer)
wxEND_EVENT_TABLE()

// コンストラクタ
//
// style は wxSB_VERTICAL か wxSB_HORIZONTAL。
// 本家の wxScrollBar は style のデフォルトが wxSB_HORIZONTAL だが、
// うちではデフォルト引数にはしない。
WXScrollBar::WXScrollBar(wxWindow *parent, wxWindowID id, long style)
	: inherited(parent, id)
{
	SetName("WXScrollBar");

	if (style == wxSB_VERTICAL) {
		is_vertical = true;
	}

	FontChanged();

	timer.SetOwner(this);
}

// デストラクタ
WXScrollBar::~WXScrollBar()
{
}

// フォントサイズ変更
void
WXScrollBar::FontChanged()
{
	inherited::FontChanged();
	DPRINTF("%s %d\n", __method__, font_height);

	switch (font_height) {
	 case 12:
		subpx = 16;
		break;
	 case 16:
		subpx = 20;
		break;
	 case 24:
		subpx = 32;
		break;
	 default:
		PANIC("Unexpected font_height=%d", font_height);
	}

	// つまみの最小サイズ。適当に幅の半分。
	thumbmin = subpx / 2;

	Fit();
}

void
WXScrollBar::Fit()
{
	// 最小サイズを設定。
	wxSize minsize;
	if (is_vertical) {
		minsize.x = subpx;
		minsize.y = font_height;
	} else {
		minsize.x = font_width * 2;
		minsize.y = subpx;
	}
	DPRINTF("%s GetSize=(%d,%d) Best=(%d,%d)\n", __method__,
		GetSize().x, GetSize().y, minsize.x, minsize.y);
	SetSizeHints(minsize, wxDefaultSize);

	// 主方向は最小サイズを下回らなければ維持、
	// 副方向は常に規定サイズ。
	if (is_vertical) {
		SetSize(subpx, GetSize().y);
	} else {
		SetSize(GetSize().x, subpx);
	}
}

// スクロールバーのパラメータを設定する。
// 通知イベントは送出しない。
//
// 各パラメータは wxScrollBar::SetScrollbar() 準拠のはずなのでそちら参照。
// SetScrollbar() は wxScrollBar ではなく wxWindow の仮想関数で、ここで定義
// すると微妙に型が違うオーバーライドになってしまうため、名前を変えてある。
void
WXScrollBar::SetScrollParam(int pos_, int thumbsize_, int range_, int pagesize_)
{
	DPRINTF("SetScrollParam: pos=%d thumbsize=%d range=%d pagesize=%d\n",
		pos_, thumbsize_, range_, pagesize_);

	thumbsize = thumbsize_;
	range = range_;
	pagesize = pagesize_;
	SetThumbPosition(pos_);
}

// つまみの位置を設定する。外部インタフェース。
// 通知イベントは送出しない。
// つまみが移動すれば true を、移動しなければ false を返す。
bool
WXScrollBar::SetThumbPosition(int pos)
{
	// パラメータセット前にリサイズされるとここに来る
	if (range < 1) {
		return false;
	}

	// 単位位置の範囲チェック
	if (pos < 0) {
		pos = 0;
	} else if (pos > range - thumbsize) {
		pos = range - thumbsize;
	}

	// 通常の描画座標は単位値とピクセル数の単純な比で求まる。
	//
	//  0    10                90  100 [unit]
	//  +---+-----------------+---+
	//
	//  0    20                180 200 [px]
	//  +---+-----------------+---+
	//
	// つまみが描画領域に対して小さすぎる場合、ある程度より小さくならないよう
	// に抑制する (thumbmin [px])。
	// 例えばここで描画範囲が 50px の場合、つまみの最小サイズは 8px なので
	// 単位可動域 0-90 を 0-42 ピクセルにマッピングし直して描画する。
	//
	//  0    5 8            42 45  50 [px]
	//  +---+-+-------------+-+---+

	// つまみが小さくなりすぎるのを防ぐ
	int thumbsize_px = thumbsize * range_px / range;
	if (thumbsize_px < thumbmin) {
		thumbsize_px = thumbmin;
	}

	// range はスクロールバー領域全体の長さ (図の 100)。
	// mrange はつまみの大きさを含まない可動域?の長さ (図の 90) とする。
	// [p1, p2) がつまみのピクセル座標。
	int oldp1 = p1;
	int oldp2 = p2;
	int mrange = range - thumbsize;
	if (mrange == 0) {
		// つまみが全体を占めていると可動域 mrange が 0 になる。
		p1 = 0;
	} else {
		int mrange_px = range_px - thumbsize_px;
		p1 = pos * mrange_px / mrange;
	}
	p2 = p1 + thumbsize_px;

	// 可動域の上(左)に2ピクセルの枠がある
	p1 += BORDER_LT;
	p2 += BORDER_LT;

	// 移動していない (表示位置が変わっていない) なら false で帰る。
	// 目一杯を占めていて移動できないつまみをドラッグしてマウス移動
	// しても、つまみが移動しないのだから、スクロールも起こさないため。
	DPRINTF("SetThumbPosition: oldpos=%d newpos=%d oldp=%d-%d p=%d-%d\n",
		thumbpos, pos, oldp1, oldp2, p1, p2);
	if (p1 == oldp1 && p2 == oldp2) {
		return false;
	}

	thumbpos = pos;
	DPRINTF("SetThumbPosition: new pos=%d\n", thumbpos);
	Refresh();
	return true;
}

// つまみの位置を pos に設定し、移動すれば通知イベントを送出する。(内部用)
void
WXScrollBar::MoveThumb(int pos)
{
	if (SetThumbPosition(pos)) {
		// Orientation は wx"SB_"VERTICAL ではなく wxVERTICAL のほう。
		// (値は同じっぽいからいいけど、どうしてこうなった…)
		int orient = is_vertical ? wxVERTICAL : wxHORIZONTAL;
		wxScrollEvent newev(NONO_EVT_SCROLL, GetId(), thumbpos, orient);
		newev.SetEventObject(this);
		AddPendingEvent(newev);
	}
}

void
WXScrollBar::OnSize(wxSizeEvent& event)
{
	const wxSize& size = GetSize();
	DPRINTF("%s size=(%d,%d)\n", __method__, size.x, size.y);

	// スクロールバーの論理パラメータは変わらず、描画情報が変わる。
	if (is_vertical) {
		range_px = size.y - BORDER_LT - BORDER_RB;
	} else {
		range_px = size.x - BORDER_LT - BORDER_RB;
	}

	SetThumbPosition(thumbpos);

	event.Skip();
}

// マウスイベント
void
WXScrollBar::OnMouse(wxMouseEvent& event)
{
	event.Skip();

	// 現在位置
	wxPoint pos = event.GetPosition();
	if (is_vertical) {
		mouse_px = pos.y;
	} else {
		mouse_px = pos.x;
	}
	DPRINTF("OnMouse: mouse_px=%d\n", mouse_px);

	int wheel = event.GetWheelRotation();
	if (wheel != 0) {
		DPRINTF("OnMouse: wheel=%d\n", wheel);

		// ホイールは1回で +-120 が飛んでくる。
		// どこのルールか分からんけどだいたい3行分の移動としているようなので
		// ここでも踏襲する。
		MoveThumb(thumbpos - wheel / 40);
	}

	if (event.LeftUp()) {
		left_click = false;
	}

	if (event.LeftDown() || event.LeftDClick()) {
		left_click = true;

		// 左クリック。間隔が速いとダブルクリックになる。
		// ダブルクリックは LeftDown, LeftUp, LeftDClick, LeftUp の順で来る
		// ので、LeftDown と LeftDClick を同列で扱っておく。
		int r = PosCmp();
		if (r == 0) {
			// つまみ内左クリックでは何も起きないが、
			// ドラッグ用に左クリック開始時点の位置を保存。
			start_thumb = thumbpos;
			start_px = mouse_px;
			DPRINTF("OnMouse: LeftDown thumbpos=%d start_px=%d\n",
				start_thumb, start_px);
		} else {
			// つまみ外の下地部分ならページアップ、ページダウン
			DPRINTF("OnMouse: LeftDown cmp=%d\n", r);
			MoveThumb(thumbpos + r * pagesize);
		}

		// 押しっぱなし判定のためタイマーを開始。
		timer.Start(500);
	}

	if (event.Dragging()) {
		// ドラッグ開始時からの差分を求めて..
		int delta = mouse_px - start_px;
		DPRINTF("OnMouse: Dragging delta=%d\n", delta);

		// ピクセルでの移動量を変換
		MoveThumb(start_thumb + delta * range / range_px);
	}
}

// タイマーイベント
void
WXScrollBar::OnTimer(wxTimerEvent& event)
{
	int r = PosCmp();
	if (left_click && r != 0) {
		// つまみ外で左クリックが続いていたらもう1回動かす
		MoveThumb(thumbpos + r * pagesize);

		// キーリピートと同じ要領で2回目以降は短くする。
		timer.Start(100);
	} else {
		// つまみ内か左クリックが終わっていたら終了
		timer.Stop();
	}
}

// mouse_px がつまみ内なら 0 を、つまみより上(左)なら -1、下(右)なら 1 を返す。
int
WXScrollBar::PosCmp() const
{
	if (mouse_px < p1) {
		return -1;
	}
	if (mouse_px > p2) {
		return 1;
	}
	return 0;
}

// 描画
void
WXScrollBar::Draw()
{
	// パラメータセット前に表示してしまうとここに来る。
	if (__predict_false(range < 1)) {
		return;
	}

	const int w = bitmap.GetWidth();
	const int h = bitmap.GetHeight();
	if (__predict_false(w < 4 || h < 4)) {
		return;
	}

	if (is_vertical) {
		// 背景
		bitmap.DrawLineH(UD_LIGHT_GREY, 0, 0, w);
		bitmap.DrawLineH(UD_LIGHT_GREY, 0, h - 1, w);
		bitmap.DrawLineV(UD_LIGHT_GREY, w - 1, 1, h - 1);
		bitmap.FillRect(UD_GREY, 0, 1, w - 1, h - 2);

		// つまみ
		bitmap.FillRect(BGPANEL, 1, p1, w - 3, p2 - p1 - 1);
		bitmap.DrawLineH(UD_BLACK, 1, p2 - 1, w - 1);
		bitmap.DrawLineV(UD_BLACK, w - 2, p1, p2);
	} else {
		// 背景
		bitmap.DrawLineV(UD_LIGHT_GREY, 0, 0, h);
		bitmap.DrawLineV(UD_LIGHT_GREY, w - 1, 0, h);
		bitmap.DrawLineH(UD_LIGHT_GREY, 1, h - 1, w - 1);
		bitmap.FillRect(UD_GREY, 1, 0, w - 2, h - 1);

		// つまみ
		bitmap.FillRect(BGPANEL, p1, 1, p2 - p1 - 1, h - 3);
		bitmap.DrawLineV(UD_BLACK, p2 - 1, 1, h - 1);
		bitmap.DrawLineH(UD_BLACK, p1, h - 2, p2);
	}
}

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.