Skip to content

Instantly share code, notes, and snippets.

@ma8ma
Last active January 12, 2019 10:07
Show Gist options
  • Save ma8ma/b928d11715e7af6d0a67149b195f5640 to your computer and use it in GitHub Desktop.
Save ma8ma/b928d11715e7af6d0a67149b195f5640 to your computer and use it in GitHub Desktop.
JD タッチスクリーンによるスレビューのスクロール機能を追加するパッチ (実機で未テスト)
2ちゃんねるブラウザ JD のGTK3版に
タッチスクリーンによるスレビューのスクロール機能を追加するパッチです
ライセンス:GPL v2
要件:GTK+ 3.14以上
使い方
https://github.com/ma8ma/JD/tree/gtk3-preview にパッチを適用してビルド(--with-gtkmm3を付ける)
環境変数GDK_CORE_DEVICE_EVENTSは設定しないでJDを起動する
スレビューをタッチして上下にスワイプするとスクロールし、指を離すと減速して止まる(慣性スクロール)
タップ操作がマウスの左ボタンクリック、2本指タップが右ボタンクリックとして扱われるはず。
テストモード
環境変数GTK_TEST_TOUCHSCREENを設定してJDを起動すると
マウスの左クリック+上下ドラッグでスレビューをスクロールできる
注意
* タッチスクリーンが搭載された実機ではテストしていない
* _DEBUGマクロを設定してログを表示すると慣性スクロールの性能が低下する
* タッチ操作だとテキストの上でドラッグしても範囲選択されないはず
* マウスの中ボタンに割り当てられているオートスクロールをタッチ操作で実行する仕組みがない
変更の要約
GtkGesture APIを使ってスレビューに縦スワイプの処理を追加する
ドラッグによるテキストの範囲選択は省略されている。
検証が必要
* 慣性スクロールの使い勝手はどうか。
慣性スクロールは定数(kDecelerationFriction, kDecelerationRatio, kInitialDyScale)
の値をいじることで調整できる。
* タップ操作の使い勝手はどうか。
* ダブル、トリプルタップで範囲選択ができるか。
* 2本指タップでコンテキストメニューを表示できるか。
* ドラッグによるテキストの範囲選択をジェスチャーに割り当てるべきか。
* オートスクロールの開始/終了をジェスチャーに割り当てるべきか。
diff --git a/src/article/drawareabase.cpp b/src/article/drawareabase.cpp
index e32933b..062b0a9 100644
--- a/src/article/drawareabase.cpp
+++ b/src/article/drawareabase.cpp
@@ -155,6 +155,9 @@ DrawAreaBase::~DrawAreaBase()
std::cout << "DrawAreaBase::~DrawAreaBase " << m_url << std::endl;;
#endif
+#if GTKMM_CHECK_VERSION(3,14,0)
+ cancel_deceleration();
+#endif
if( m_layout_tree ) delete m_layout_tree;
m_layout_tree = NULL;
clear();
@@ -211,8 +214,12 @@ void DrawAreaBase::setup( const bool show_abone, const bool show_scrbar, const b
m_view.signal_expose_event().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_expose_event ));
#endif
m_view.signal_scroll_event().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_scroll_event ));
+#if GTKMM_CHECK_VERSION(3,14,0)
+ setup_event_controller();
+#else
m_view.signal_button_press_event().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_button_press_event ));
m_view.signal_button_release_event().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_button_release_event ));
+#endif
m_view.signal_motion_notify_event().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_motion_notify_event ));
m_view.signal_key_press_event().connect( sigc::mem_fun(*this, &DrawAreaBase::slot_key_press_event ));
m_view.signal_key_release_event().connect( sigc::mem_fun(*this, &DrawAreaBase::slot_key_release_event ));
@@ -5306,6 +5313,11 @@ bool DrawAreaBase::slot_button_press_event( GdkEventButton* event )
set_selection( caret_left, caret_right );
redraw_force = true;
+#if GTKMM_CHECK_VERSION(3,14,0)
+ // GtkGestureMultiPressは指を離す度にreleasedが発行される
+ // ダブルクリックの途中でfalseに戻らないように設定する
+ m_drugging = true;
+#endif
}
}
}
@@ -5636,3 +5648,216 @@ bool DrawAreaBase::slot_key_release_event( GdkEventKey* event )
}
+
+#if GTKMM_CHECK_VERSION(3,14,0)
+void DrawAreaBase::setup_event_controller()
+{
+ m_view.add_events( Gdk::TOUCH_MASK );
+
+ m_gesture_multipress = Gtk::GestureMultiPress::create( m_view );
+ // 既存のシグナルハンドラに処理を委譲するので0を指定してどのボタンもリッスンする
+ m_gesture_multipress->set_button( 0 );
+ m_gesture_multipress->set_exclusive( true );
+ m_gesture_multipress->set_touch_only( false );
+ m_gesture_multipress->signal_pressed().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_multipress_pressed ) );
+ m_gesture_multipress->signal_released().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_multipress_released ) );
+
+ m_gesture_pan = Gtk::GesturePan::create( m_view, Gtk::ORIENTATION_VERTICAL );
+ m_gesture_pan->set_touch_only( true );
+ m_gesture_pan->signal_drag_begin().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_pan_begin ) );
+ m_gesture_pan->signal_drag_update().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_pan_update ) );
+ m_gesture_pan->signal_end().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_gesture_end ) );
+
+ m_gesture_swipe = Gtk::GestureSwipe::create( m_view );
+ m_gesture_pan->group( m_gesture_swipe );
+ m_gesture_swipe->set_touch_only( true );
+ m_gesture_swipe->signal_swipe().connect( sigc::mem_fun( *this, &DrawAreaBase::slot_swipe ) );
+}
+
+
+//
+// イベント構造体を取得してslot_button_(press|release)_eventに処理を委譲する
+//
+void DrawAreaBase::slot_multipress_pressed( int n_press, double, double )
+{
+ GdkEventSequence* const sequence = m_gesture_multipress->get_current_sequence();
+ const GdkEvent* const event = m_gesture_multipress->get_last_event( sequence );
+ if( !event ) return;
+
+ const unsigned int n_points = m_gesture_multipress->property_n_points();
+#ifdef _DEBUG
+ std::cout << "DrawAreaBase::slot_multipress_pressed "
+ << "n_press = " << n_press << ", event = " << event->type << ", n_points = " << n_points
+ << std::endl;
+#endif
+
+ GdkEventButton button_event = event->button;
+ if( n_press == 2 ) {
+ button_event.type = GDK_DOUBLE_BUTTON_PRESS;
+ }
+ else if( n_press == 3 ) {
+ button_event.type = GDK_TRIPLE_BUTTON_PRESS;
+ }
+ if( n_points == 2 ) {
+ // 2本指タップは右ボタンクリックとして扱う
+ button_event.button = GDK_BUTTON_SECONDARY;
+ }
+ slot_button_press_event( &button_event );
+}
+
+void DrawAreaBase::slot_multipress_released( int n_press, double, double )
+{
+ static_cast< void >( n_press );
+
+ GdkEventSequence* const sequence = m_gesture_multipress->get_current_sequence();
+ const GdkEvent* const event = m_gesture_multipress->get_last_event( sequence );
+ if( !event ) return;
+
+ const unsigned int n_points = m_gesture_multipress->property_n_points();
+#ifdef _DEBUG
+ std::cout << "DrawAreaBase::slot_multipress_released "
+ << "n_press = " << n_press << ", event = " << event->type << ", n_points = " << n_points
+ << std::endl;
+#endif
+
+ GdkEventButton button_event = event->button;
+ if( n_points == 2 ) {
+ button_event.button = GDK_BUTTON_SECONDARY;
+ }
+ slot_button_release_event( &button_event );
+}
+
+
+void DrawAreaBase::slot_pan_begin( double start_x, double start_y )
+{
+ static_cast< void >( start_x );
+
+ cancel_deceleration();
+
+ Gtk::EventSequenceState state;
+ if ( !m_vscrbar ) {
+ state = Gtk::EVENT_SEQUENCE_DENIED;
+ }
+ else { // capture_button_press == true
+ const auto adjust = m_vscrbar->get_adjustment();
+ m_drag_start_y = adjust->get_value();
+
+ state = Gtk::EVENT_SEQUENCE_CLAIMED;
+ }
+
+#ifdef _DEBUG
+ std::cout << "DrawAreaBase::slot_pan_begin y = " << m_drag_start_y << std::endl;
+#endif
+
+ GdkEventSequence* const sequence = m_gesture_pan->get_current_sequence();
+ m_gesture_pan->set_sequence_state( sequence, state );
+}
+
+void DrawAreaBase::slot_pan_update( double offset_x, double offset_y )
+{
+ assert( m_vscrbar );
+ static_cast< void >( offset_x );
+
+ GdkEventSequence* const sequence = m_gesture_pan->get_last_updated_sequence();
+ if( m_gesture_pan->get_sequence_state( sequence ) != Gtk::EVENT_SEQUENCE_CLAIMED ) return;
+
+ const double y = m_drag_start_y - offset_y;
+ const auto adjust = m_vscrbar->get_adjustment();
+
+#ifdef _DEBUG
+ std::cout << "DrawAreaBase::slot_pan_update y = " << y << std::endl;
+#endif
+
+ if( y <= 0 ) return;
+ if( y >= adjust->get_upper() - adjust->get_page_size() ) return;
+
+ // スワイプスクロールの場合は必ずスクロール処理を実施する
+ m_wait_scroll = 0;
+
+ m_scrollinfo.reset();
+ m_scrollinfo.y = static_cast< int >( y );
+ m_scrollinfo.mode = SCROLL_TO_Y;
+
+ exec_scroll();
+}
+
+void DrawAreaBase::slot_gesture_end( GdkEventSequence* sequence )
+{
+ if( !m_gesture_pan->handles_sequence( sequence ) ) {
+ m_gesture_pan->set_state( Gtk::EVENT_SEQUENCE_DENIED );
+ }
+}
+
+
+
+namespace
+{
+ constexpr double kDecelerationFriction = 4.0; // スクロール速度の係数
+ constexpr double kDecelerationRatio = 0.000001; // 経過時間を切り下げて減速をゆるやかにする
+ constexpr double kInitialDyScale = 0.01; // pixcels/secからpixcels/frameに換算する係数
+}
+
+//
+// 慣性スクロールを実行する (指を離した後も減速しながらスクロールする)
+//
+void DrawAreaBase::slot_swipe( double velocity_x, double velocity_y )
+{
+ if( !m_vscrbar ) return;
+
+ static_cast< void >( velocity_x );
+
+ GdkEventSequence* const sequence = m_gesture_swipe->get_last_updated_sequence();
+ if( m_gesture_swipe->get_sequence_state( sequence ) != Gtk::EVENT_SEQUENCE_CLAIMED ) return;
+
+#ifdef _DEBUG
+ std::cout << "DrawAreaBase::slot_swipe vx = " << velocity_x << ", vy = " << velocity_y << std::endl;
+#endif
+ GdkFrameClock* const borrowed_clock = gtk_widget_get_frame_clock( GTK_WIDGET( this->gobj() ) );
+ m_deceleration.last_time = gdk_frame_clock_get_frame_time( borrowed_clock );
+ m_deceleration.elapsed = 0.0;
+ m_deceleration.initial_dy = velocity_y * kInitialDyScale;
+ m_deceleration.id = gtk_widget_add_tick_callback( GTK_WIDGET( this->gobj() ),
+ &DrawAreaBase::deceleration_tick_cb, nullptr, nullptr );
+}
+
+gboolean DrawAreaBase::deceleration_tick_cb( GtkWidget* cwidget, GdkFrameClock* clock, gpointer )
+{
+ Gtk::Widget* const widget = Glib::wrap( cwidget );
+ return dynamic_cast< DrawAreaBase* >( widget )->deceleration_tick_impl( clock );
+}
+
+gboolean DrawAreaBase::deceleration_tick_impl( GdkFrameClock* clock )
+{
+ const gint64 current_time = gdk_frame_clock_get_frame_time( clock );
+ m_deceleration.elapsed += ( current_time - m_deceleration.last_time ) * kDecelerationRatio;
+ m_deceleration.last_time = current_time;
+ const double exp_part = std::exp( -kDecelerationFriction * m_deceleration.elapsed );
+ const double dy = -kDecelerationFriction * exp_part * m_deceleration.initial_dy;
+
+#ifdef _DEBUG
+ std::cout << "DrawAreaBase::deceleration_tick_impl dy = " << dy << std::endl;
+#endif
+ if( std::fabs( dy ) < 1.0 ) {
+ cancel_deceleration();
+ return G_SOURCE_REMOVE;
+ }
+
+ // スワイプスクロールの場合は必ずスクロール処理を実施する
+ m_wait_scroll = 0;
+
+ m_scrollinfo.reset();
+ m_scrollinfo.dy = static_cast< int >( dy );
+ m_scrollinfo.mode = SCROLL_NORMAL;
+
+ exec_scroll();
+ return G_SOURCE_CONTINUE;
+}
+
+void DrawAreaBase::cancel_deceleration()
+{
+ if( m_deceleration.id ) {
+ gtk_widget_remove_tick_callback( GTK_WIDGET( this->gobj() ), m_deceleration.id );
+ m_deceleration.id = 0;
+ }
+}
+#endif // GTKMM_CHECK_VERSION(3,14,0)
diff --git a/src/article/drawareabase.h b/src/article/drawareabase.h
index c4d18d8..0e368cf 100644
--- a/src/article/drawareabase.h
+++ b/src/article/drawareabase.h
@@ -235,6 +235,25 @@ namespace ARTICLE
// 自分の書き込みに対するレスマークアイコン
Glib::RefPtr< Gdk::Pixbuf > m_pixbuf_refer_post;
+#if GTKMM_CHECK_VERSION(3,14,0)
+ // マウスのクリックとタッチスクリーンのタップ
+ Glib::RefPtr< Gtk::GestureMultiPress > m_gesture_multipress;
+
+ // タッチスクリーンのスクロール
+ Glib::RefPtr< Gtk::GesturePan > m_gesture_pan;
+ Glib::RefPtr< Gtk::GestureSwipe > m_gesture_swipe;
+ double m_drag_start_y;
+
+ // 慣性スクロール
+ struct DecelerationInfo
+ {
+ double elapsed; // 換算された経過時間
+ double initial_dy; // スケーリングされた初速度(pixcels/frame)
+ gint64 last_time; // 前回コールバックが呼び出された時間(frame)
+ guint id = 0; // コールバックのID
+ } m_deceleration;
+#endif
+
public:
SIG_BUTTON_PRESS sig_button_press(){ return m_sig_button_press; }
@@ -484,6 +503,24 @@ namespace ARTICLE
bool slot_key_press_event( GdkEventKey* event );
bool slot_key_release_event( GdkEventKey* event );
+
+#if GTKMM_CHECK_VERSION(3,14,0)
+ void setup_event_controller();
+
+ void slot_multipress_pressed( int n_press, double x, double y );
+ void slot_multipress_released( int n_press, double x, double y );
+
+ void slot_pan_begin( double start_x, double start_y );
+ void slot_pan_update( double offset_x, double offset_y );
+ void slot_gesture_end( GdkEventSequence* sequence );
+
+ void slot_swipe( double velocity_x, double velocity_y );
+
+ // 慣性スクロール
+ static gboolean deceleration_tick_cb( GtkWidget* cwidget, GdkFrameClock* clock, gpointer );
+ gboolean deceleration_tick_impl( GdkFrameClock* clock );
+ void cancel_deceleration();
+#endif
};
@ma8ma
Copy link
Author

ma8ma commented Dec 8, 2018

https://mao.5ch.net/test/read.cgi/linux/1540656394/246-249 よりスクロールとテキストの範囲選択が混ざると報告があった。
これはシグナルハンドラでタッチ操作のフリック/スワイプ(スクロール)とドラッグ(範囲選択)を振り分けていないためだと思われる。

@ma8ma
Copy link
Author

ma8ma commented Jan 5, 2019

https://mao.5ch.net/test/read.cgi/linux/1540656394/334 のレポートからタッチ中のスクロールが動作していないことが分かった。そこでスクロール処理を行うシグナルハンドラの接続をGtkGestureSwipeのupdateシグナルからGtkGesturePanのdrag_updateシグナルへ変更する。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment