mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Drop use of the Qt gestures system
It is a buggy mess, so implement our own gesture recognizer based on the raw touch events
This commit is contained in:
parent
d095bd4f83
commit
0a62b1f98d
217
src/calibre/gui2/gestures.py
Normal file
217
src/calibre/gui2/gestures.py
Normal file
@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QApplication, QEvent, QMouseEvent, QObject, QPointF, Qt, QTouchDevice,
|
||||
pyqtSignal
|
||||
)
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from calibre.utils.monotonic import monotonic
|
||||
|
||||
touch_supported = False
|
||||
if iswindows and sys.getwindowsversion()[:2] >= (6, 2): # At least windows 7
|
||||
touch_supported = True
|
||||
|
||||
HOLD_THRESHOLD = 1.0 # seconds
|
||||
TAP_THRESHOLD = 50 # manhattan pixels
|
||||
FLICK_DISTANCE = 100 # manhattan pixels
|
||||
|
||||
Tap, TapAndHold, Flick = 'Tap', 'TapAndHold', 'Flick'
|
||||
Left, Right, Up, Down = 'Left', 'Right', 'Up', 'Down'
|
||||
|
||||
|
||||
class TouchPoint(object):
|
||||
|
||||
def __init__(self, tp):
|
||||
self.creation_time = self.last_update_time = self.time_of_last_move = monotonic()
|
||||
self.start_screen_position = self.current_screen_position = self.previous_screen_position = QPointF(tp.screenPos())
|
||||
self.time_since_last_update = -1
|
||||
self.total_movement = 0
|
||||
|
||||
def update(self, tp):
|
||||
now = monotonic()
|
||||
self.time_since_last_update = now - self.last_update_time
|
||||
self.last_update_time = now
|
||||
self.previous_screen_position, self.current_screen_position = self.current_screen_position, QPointF(tp.screenPos())
|
||||
movement = (self.current_screen_position - self.previous_screen_position).manhattanLength()
|
||||
self.total_movement += movement
|
||||
if movement > 5:
|
||||
self.time_of_last_move = now
|
||||
|
||||
@property
|
||||
def flick_type(self):
|
||||
x_movement = self.current_screen_position.x() - self.start_screen_position.x()
|
||||
y_movement = self.current_screen_position.y() - self.start_screen_position.y()
|
||||
xabs, yabs = map(abs, (x_movement, y_movement))
|
||||
if max(xabs, yabs) < FLICK_DISTANCE or min(xabs/max(yabs, 0.01), yabs/max(xabs, 0.01)) > 0.3:
|
||||
return
|
||||
d = x_movement if xabs > yabs else y_movement
|
||||
axis = (Left, Right) if xabs > yabs else (Up, Down)
|
||||
return axis[0 if d < 0 else 1]
|
||||
|
||||
@property
|
||||
def flick_live(self):
|
||||
x_movement = self.current_screen_position.x() - self.previous_screen_position.x()
|
||||
y_movement = self.current_screen_position.y() - self.previous_screen_position.y()
|
||||
return x_movement, y_movement
|
||||
|
||||
|
||||
class State(QObject):
|
||||
|
||||
tapped = pyqtSignal(object)
|
||||
flicked = pyqtSignal(object)
|
||||
flicking = pyqtSignal(object, object)
|
||||
tap_hold_started = pyqtSignal(object)
|
||||
tap_hold_updated = pyqtSignal(object)
|
||||
tap_hold_finished = pyqtSignal(object)
|
||||
|
||||
def __init__(self):
|
||||
QObject.__init__(self)
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
self.possible_gestures = set()
|
||||
self.touch_points = {}
|
||||
self.hold_started = False
|
||||
self.hold_data = None
|
||||
|
||||
def start(self):
|
||||
self.clear()
|
||||
self.possible_gestures = {Tap, TapAndHold, Flick}
|
||||
|
||||
def update(self, ev, boundary='update'):
|
||||
if boundary == 'start':
|
||||
self.start()
|
||||
|
||||
for tp in ev.touchPoints():
|
||||
tpid = tp.id()
|
||||
if tpid not in self.touch_points:
|
||||
self.touch_points[tpid] = TouchPoint(tp)
|
||||
else:
|
||||
self.touch_points[tpid].update(tp)
|
||||
|
||||
if len(self.touch_points) > 1:
|
||||
self.possible_gestures.clear()
|
||||
|
||||
if boundary == 'end':
|
||||
self.check_for_holds()
|
||||
self.finalize()
|
||||
self.clear()
|
||||
else:
|
||||
self.check_for_holds()
|
||||
if {Flick} & self.possible_gestures:
|
||||
tp = next(self.touch_points.itervalues())
|
||||
self.flicking.emit(*tp.flick_live)
|
||||
|
||||
def check_for_holds(self):
|
||||
if not {TapAndHold} & self.possible_gestures:
|
||||
return
|
||||
now = monotonic()
|
||||
tp = next(self.touch_points.itervalues())
|
||||
if now - tp.time_of_last_move < HOLD_THRESHOLD:
|
||||
return
|
||||
if self.hold_started:
|
||||
if TapAndHold in self.possible_gestures:
|
||||
self.tap_hold_updated.emit(tp)
|
||||
else:
|
||||
self.possible_gestures &= {TapAndHold}
|
||||
if tp.total_movement > TAP_THRESHOLD:
|
||||
self.possible_gestures.clear()
|
||||
else:
|
||||
self.possible_gestures = {TapAndHold}
|
||||
self.hold_started = True
|
||||
self.hold_data = now
|
||||
self.tap_hold_started.emit(tp)
|
||||
|
||||
def finalize(self):
|
||||
if Tap in self.possible_gestures:
|
||||
tp = next(self.touch_points.itervalues())
|
||||
if tp.total_movement <= TAP_THRESHOLD:
|
||||
self.tapped.emit(tp)
|
||||
return
|
||||
|
||||
if Flick in self.possible_gestures:
|
||||
tp = next(self.touch_points.itervalues())
|
||||
st = tp.flick_type
|
||||
if st is not None:
|
||||
self.flicked.emit(st)
|
||||
return
|
||||
|
||||
if not self.hold_started:
|
||||
return
|
||||
|
||||
if TapAndHold in self.possible_gestures:
|
||||
tp = next(self.touch_points.itervalues())
|
||||
self.tap_hold_finished.emit(tp)
|
||||
return
|
||||
|
||||
|
||||
def send_click(view, pos, button=Qt.LeftButton, double_click=False):
|
||||
if double_click:
|
||||
ev = QMouseEvent(QEvent.MouseButtonDblClick, pos, button, button, QApplication.keyboardModifiers())
|
||||
QApplication.postEvent(view.viewport(), ev)
|
||||
return
|
||||
ev = QMouseEvent(QEvent.MouseButtonPress, pos, button, button, QApplication.keyboardModifiers())
|
||||
QApplication.postEvent(view.viewport(), ev)
|
||||
ev = QMouseEvent(QEvent.MouseButtonRelease, pos, button, button, QApplication.keyboardModifiers())
|
||||
QApplication.postEvent(view.viewport(), ev)
|
||||
|
||||
|
||||
class GestureManager(QObject):
|
||||
|
||||
def __init__(self, view):
|
||||
QObject.__init__(self, view)
|
||||
self.state = State()
|
||||
self.state.flicked.connect(self.handle_flick)
|
||||
self.state.tapped.connect(self.handle_tap)
|
||||
self.state.flicking.connect(self.handle_flicking)
|
||||
self.state.tap_hold_started.connect(partial(self.handle_tap_hold, 'start'))
|
||||
self.state.tap_hold_updated.connect(partial(self.handle_tap_hold, 'update'))
|
||||
self.state.tap_hold_finished.connect(partial(self.handle_tap_hold, 'end'))
|
||||
self.evmap = {QEvent.TouchBegin: 'start', QEvent.TouchUpdate: 'update', QEvent.TouchEnd: 'end'}
|
||||
|
||||
def handle_event(self, ev):
|
||||
if not touch_supported:
|
||||
return
|
||||
etype = ev.type()
|
||||
if etype in (QEvent.MouseButtonPress, QEvent.MouseMove, QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick):
|
||||
if ev.source() in (Qt.MouseEventSynthesizedBySystem, Qt.MouseEventSynthesizedByQt):
|
||||
# swallow fake mouse events generated from touch events
|
||||
ev.ignore()
|
||||
return False
|
||||
return
|
||||
boundary = self.evmap.get(etype, None)
|
||||
if boundary is None or ev.device().type() != QTouchDevice.TouchScreen:
|
||||
return
|
||||
self.state.update(ev, boundary=boundary)
|
||||
ev.accept()
|
||||
return True
|
||||
|
||||
def close_open_menu(self):
|
||||
m = getattr(self.parent(), 'context_menu', None)
|
||||
if m is not None and m.isVisible():
|
||||
m.close()
|
||||
return True
|
||||
|
||||
def handle_flick(self, direction):
|
||||
if self.close_open_menu():
|
||||
return
|
||||
raise NotImplementedError('TODO: Implement')
|
||||
|
||||
def handle_flicking(self, x, y):
|
||||
raise NotImplementedError('TODO: Implement')
|
||||
|
||||
def handle_tap(self, tp):
|
||||
if self.close_open_menu():
|
||||
return
|
||||
raise NotImplementedError('TODO: Implement')
|
||||
|
||||
def handle_tap_hold(self, action, tp):
|
||||
if action == 'end':
|
||||
raise NotImplementedError('TODO: Implement')
|
@ -15,18 +15,18 @@ from textwrap import wrap
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QListView, QSize, QStyledItemDelegate, QModelIndex, Qt, QImage, pyqtSignal,
|
||||
QTimer, QPalette, QColor, QItemSelection, QPixmap, QApplication, QScroller,
|
||||
QTimer, QPalette, QColor, QItemSelection, QPixmap, QApplication,
|
||||
QMimeData, QUrl, QDrag, QPoint, QPainter, QRect, pyqtProperty, QEvent,
|
||||
QPropertyAnimation, QEasingCurve, pyqtSlot, QHelpEvent, QAbstractItemView,
|
||||
QStyleOptionViewItem, QToolTip, QByteArray, QBuffer, QBrush, qRed, qGreen,
|
||||
qBlue, QItemSelectionModel, QIcon, QFont, QMouseEvent)
|
||||
qBlue, QItemSelectionModel, QIcon, QFont)
|
||||
|
||||
from calibre import fit_image, prints, prepare_string_for_xml, human_readable
|
||||
from calibre.constants import DEBUG, config_dir, islinux, iswindows
|
||||
from calibre.constants import DEBUG, config_dir, islinux
|
||||
from calibre.ebooks.metadata import fmt_sidx, rating_to_stars
|
||||
from calibre.utils import join_with_timeout
|
||||
from calibre.utils.monotonic import monotonic
|
||||
from calibre.gui2 import gprefs, config, rating_font, empty_index
|
||||
from calibre.gui2.gestures import GestureManager
|
||||
from calibre.gui2.library.caches import CoverCache, ThumbnailCache
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
|
||||
@ -626,66 +626,8 @@ class CoverDelegate(QStyledItemDelegate):
|
||||
# }}}
|
||||
|
||||
|
||||
has_gestures = iswindows
|
||||
# Enabling gesture support on X11/OS X causes Qt to send fake touch events when
|
||||
# right clicking/dragging with the mouse, which breaks things, for example:
|
||||
# https://bugs.launchpad.net/bugs/1707282 and
|
||||
# https://www.mobileread.com/forums/showthread.php?t=289057
|
||||
|
||||
|
||||
def send_click(view, pos, button=Qt.LeftButton, double_click=False):
|
||||
if double_click:
|
||||
ev = QMouseEvent(QEvent.MouseButtonDblClick, pos, button, button, QApplication.keyboardModifiers())
|
||||
QApplication.postEvent(view.viewport(), ev)
|
||||
return
|
||||
ev = QMouseEvent(QEvent.MouseButtonPress, pos, button, button, QApplication.keyboardModifiers())
|
||||
QApplication.postEvent(view.viewport(), ev)
|
||||
ev = QMouseEvent(QEvent.MouseButtonRelease, pos, button, button, QApplication.keyboardModifiers())
|
||||
QApplication.postEvent(view.viewport(), ev)
|
||||
|
||||
|
||||
def handle_gesture(ev, view):
|
||||
tap = ev.gesture(Qt.TapGesture)
|
||||
if tap and tap.state() == Qt.GestureFinished:
|
||||
p, view.last_tap_at = view.last_tap_at, monotonic()
|
||||
interval = QApplication.instance().doubleClickInterval() / 1000
|
||||
double_click = monotonic() - p < interval
|
||||
send_click(view, tap.position(), double_click=double_click)
|
||||
ev.accept(Qt.TapGesture)
|
||||
return True
|
||||
th = ev.gesture(Qt.TapAndHoldGesture)
|
||||
if th and th.state() in (Qt.GestureStarted, Qt.GestureUpdated, Qt.GestureFinished):
|
||||
if th.state() == Qt.GestureFinished:
|
||||
send_click(view, th.position(), button=Qt.RightButton)
|
||||
ev.accept(Qt.TapAndHoldGesture)
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def setup_gestures(view):
|
||||
if has_gestures:
|
||||
v = view.viewport()
|
||||
view.scroller = QScroller.grabGesture(v, QScroller.TouchGesture)
|
||||
v.grabGesture(Qt.TapGesture)
|
||||
v.grabGesture(Qt.TapAndHoldGesture)
|
||||
view.last_tap_at = 0
|
||||
|
||||
|
||||
def gesture_viewport_event(view, ev):
|
||||
if not has_gestures:
|
||||
return
|
||||
et = ev.type()
|
||||
if et in (QEvent.MouseButtonPress, QEvent.MouseMove, QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick):
|
||||
if ev.source() in (Qt.MouseEventSynthesizedBySystem, Qt.MouseEventSynthesizedByQt):
|
||||
ev.ignore()
|
||||
return False
|
||||
elif et == QEvent.Gesture:
|
||||
return handle_gesture(ev, view)
|
||||
|
||||
|
||||
# The View {{{
|
||||
|
||||
|
||||
@setup_dnd_interface
|
||||
class GridView(QListView):
|
||||
|
||||
@ -694,7 +636,7 @@ class GridView(QListView):
|
||||
|
||||
def __init__(self, parent):
|
||||
QListView.__init__(self, parent)
|
||||
setup_gestures(self)
|
||||
self.gesture_manager = GestureManager(self)
|
||||
setup_dnd_interface(self)
|
||||
self.setUniformItemSizes(True)
|
||||
self.setWrapping(True)
|
||||
@ -731,7 +673,7 @@ class GridView(QListView):
|
||||
t.timeout.connect(self.update_memory_cover_cache_size)
|
||||
|
||||
def viewportEvent(self, ev):
|
||||
ret = gesture_viewport_event(self, ev)
|
||||
ret = self.gesture_manager.handle_event(ev)
|
||||
if ret is not None:
|
||||
return ret
|
||||
return QListView.viewportEvent(self, ev)
|
||||
|
@ -21,7 +21,8 @@ from calibre.gui2.library.delegates import (RatingDelegate, PubDateDelegate,
|
||||
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate,
|
||||
CcEnumDelegate, CcNumberDelegate, LanguagesDelegate)
|
||||
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
|
||||
from calibre.gui2.library.alternate_views import AlternateViews, setup_dnd_interface, setup_gestures, gesture_viewport_event
|
||||
from calibre.gui2.library.alternate_views import AlternateViews, setup_dnd_interface
|
||||
from calibre.gui2.gestures import GestureManager
|
||||
from calibre.utils.config import tweaks, prefs
|
||||
from calibre.gui2 import error_dialog, gprefs, FunctionDispatcher
|
||||
from calibre.gui2.library import DEFAULT_SORT
|
||||
@ -193,14 +194,14 @@ class BooksView(QTableView): # {{{
|
||||
def viewportEvent(self, event):
|
||||
if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']):
|
||||
return False
|
||||
ret = gesture_viewport_event(self, event)
|
||||
ret = self.gesture_manager.handle_event(event)
|
||||
if ret is not None:
|
||||
return ret
|
||||
return QTableView.viewportEvent(self, event)
|
||||
|
||||
def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
|
||||
QTableView.__init__(self, parent)
|
||||
setup_gestures(self)
|
||||
self.gesture_manager = GestureManager(self)
|
||||
self.default_row_height = self.verticalHeader().defaultSectionSize()
|
||||
self.gui = parent
|
||||
self.setProperty('highlight_current_item', 150)
|
||||
|
Loading…
x
Reference in New Issue
Block a user