Refactor all mouse dragging code into helper classes. This changes the
behaviour when you press or release Shift or Ctrl during the drag: now
this is ignored and the action that you started continues.
From: Marius Gedminas <marius@gedmin.as>
| ... | ... |
@@ -688,6 +688,103 @@ class ZoomToAnimation(MoveToAnimation): |
| 688 | 688 |
MoveToAnimation.animate(self, t) |
| 689 | 689 |
|
| 690 | 690 |
|
| 691 |
+class DragAction(object): |
|
| 692 |
+ |
|
| 693 |
+ def __init__(self, dot_widget): |
|
| 694 |
+ self.dot_widget = dot_widget |
|
| 695 |
+ |
|
| 696 |
+ def on_button_press(self, event): |
|
| 697 |
+ self.startmousex = self.prevmousex = event.x |
|
| 698 |
+ self.startmousey = self.prevmousey = event.y |
|
| 699 |
+ self.start() |
|
| 700 |
+ |
|
| 701 |
+ def on_motion_notify(self, event): |
|
| 702 |
+ deltax = self.prevmousex - event.x |
|
| 703 |
+ deltay = self.prevmousey - event.y |
|
| 704 |
+ self.drag(deltax, deltay) |
|
| 705 |
+ self.prevmousex = event.x |
|
| 706 |
+ self.prevmousey = event.y |
|
| 707 |
+ |
|
| 708 |
+ def on_button_release(self, event): |
|
| 709 |
+ self.stopmousex = event.x |
|
| 710 |
+ self.stopmousey = event.y |
|
| 711 |
+ self.stop() |
|
| 712 |
+ |
|
| 713 |
+ def draw(self, cr): |
|
| 714 |
+ pass |
|
| 715 |
+ |
|
| 716 |
+ def start(self): |
|
| 717 |
+ pass |
|
| 718 |
+ |
|
| 719 |
+ def drag(self, deltax, deltay): |
|
| 720 |
+ pass |
|
| 721 |
+ |
|
| 722 |
+ def stop(self): |
|
| 723 |
+ pass |
|
| 724 |
+ |
|
| 725 |
+ |
|
| 726 |
+class NullAction(DragAction): |
|
| 727 |
+ |
|
| 728 |
+ def on_motion_notify(self, event): |
|
| 729 |
+ dot_widget = self.dot_widget |
|
| 730 |
+ if dot_widget.get_url(event.x, event.y) is not None: |
|
| 731 |
+ dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) |
|
| 732 |
+ else: |
|
| 733 |
+ dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) |
|
| 734 |
+ |
|
| 735 |
+ |
|
| 736 |
+class PanAction(DragAction): |
|
| 737 |
+ |
|
| 738 |
+ def start(self): |
|
| 739 |
+ self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) |
|
| 740 |
+ |
|
| 741 |
+ def drag(self, deltax, deltay): |
|
| 742 |
+ self.dot_widget.x += deltax / self.dot_widget.zoom_ratio |
|
| 743 |
+ self.dot_widget.y += deltay / self.dot_widget.zoom_ratio |
|
| 744 |
+ self.dot_widget.queue_draw() |
|
| 745 |
+ |
|
| 746 |
+ def stop(self): |
|
| 747 |
+ self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) |
|
| 748 |
+ |
|
| 749 |
+ |
|
| 750 |
+class ZoomAction(DragAction): |
|
| 751 |
+ |
|
| 752 |
+ def drag(self, deltax, deltay): |
|
| 753 |
+ self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay) |
|
| 754 |
+ self.dot_widget.queue_draw() |
|
| 755 |
+ |
|
| 756 |
+ def stop(self): |
|
| 757 |
+ self.dot_widget.queue_draw() |
|
| 758 |
+ |
|
| 759 |
+ |
|
| 760 |
+class ZoomAreaAction(DragAction): |
|
| 761 |
+ |
|
| 762 |
+ def drag(self, deltax, deltay): |
|
| 763 |
+ self.dot_widget.queue_draw() |
|
| 764 |
+ |
|
| 765 |
+ def draw(self, cr): |
|
| 766 |
+ cr.save() |
|
| 767 |
+ cr.set_source_rgba(.5, .5, 1.0, 0.25) |
|
| 768 |
+ cr.rectangle(self.startmousex, self.startmousey, |
|
| 769 |
+ self.prevmousex - self.startmousex, |
|
| 770 |
+ self.prevmousey - self.startmousey) |
|
| 771 |
+ cr.fill() |
|
| 772 |
+ cr.set_source_rgba(.5, .5, 1.0, 1.0) |
|
| 773 |
+ cr.set_line_width(1) |
|
| 774 |
+ cr.rectangle(self.startmousex - .5, self.startmousey - .5, |
|
| 775 |
+ self.prevmousex - self.startmousex + 1, |
|
| 776 |
+ self.prevmousey - self.startmousey + 1) |
|
| 777 |
+ cr.stroke() |
|
| 778 |
+ cr.restore() |
|
| 779 |
+ |
|
| 780 |
+ def stop(self): |
|
| 781 |
+ x1, y1 = self.dot_widget.window2graph(self.startmousex, |
|
| 782 |
+ self.startmousey) |
|
| 783 |
+ x2, y2 = self.dot_widget.window2graph(self.stopmousex, |
|
| 784 |
+ self.stopmousey) |
|
| 785 |
+ self.dot_widget.zoom_to_area(x1, y1, x2, y2) |
|
| 786 |
+ |
|
| 787 |
+ |
|
| 691 | 788 |
class DotWidget(gtk.DrawingArea): |
| 692 | 789 |
"""PyGTK widget that draws dot graphs.""" |
| 693 | 790 |
|
| ... | ... |
@@ -715,6 +812,7 @@ class DotWidget(gtk.DrawingArea): |
| 715 | 812 |
self.x, self.y = 0.0, 0.0 |
| 716 | 813 |
self.zoom_ratio = 1.0 |
| 717 | 814 |
self.animation = NoAnimation(self) |
| 815 |
+ self.drag_action = NullAction(self) |
|
| 718 | 816 |
self.presstime = None |
| 719 | 817 |
|
| 720 | 818 |
def set_dotcode(self, dotcode): |
| ... | ... |
@@ -747,12 +845,16 @@ class DotWidget(gtk.DrawingArea): |
| 747 | 845 |
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) |
| 748 | 846 |
cr.paint() |
| 749 | 847 |
|
| 848 |
+ cr.save() |
|
| 750 | 849 |
rect = self.get_allocation() |
| 751 | 850 |
cr.translate(0.5*rect.width, 0.5*rect.height) |
| 752 | 851 |
cr.scale(self.zoom_ratio, self.zoom_ratio) |
| 753 | 852 |
cr.translate(-self.x, -self.y) |
| 754 | 853 |
|
| 755 | 854 |
self.graph.draw(cr) |
| 855 |
+ cr.restore() |
|
| 856 |
+ |
|
| 857 |
+ self.drag_action.draw(cr) |
|
| 756 | 858 |
|
| 757 | 859 |
return False |
| 758 | 860 |
|
| ... | ... |
@@ -771,6 +873,18 @@ class DotWidget(gtk.DrawingArea): |
| 771 | 873 |
self.zoom_ratio = zoom_ratio |
| 772 | 874 |
self.queue_draw() |
| 773 | 875 |
|
| 876 |
+ def zoom_to_area(self, x1, y1, x2, y2): |
|
| 877 |
+ rect = self.get_allocation() |
|
| 878 |
+ width = abs(x1 - x2) |
|
| 879 |
+ height = abs(y1 - y2) |
|
| 880 |
+ self.zoom_ratio = min( |
|
| 881 |
+ float(rect.width)/float(width), |
|
| 882 |
+ float(rect.height)/float(height) |
|
| 883 |
+ ) |
|
| 884 |
+ self.x = (x1 + x2) / 2 |
|
| 885 |
+ self.y = (y1 + y2) / 2 |
|
| 886 |
+ self.queue_draw() |
|
| 887 |
+ |
|
| 774 | 888 |
ZOOM_INCREMENT = 1.25 |
| 775 | 889 |
|
| 776 | 890 |
def on_zoom_in(self, action): |
| ... | ... |
@@ -819,17 +933,25 @@ class DotWidget(gtk.DrawingArea): |
| 819 | 933 |
return True |
| 820 | 934 |
return False |
| 821 | 935 |
|
| 936 |
+ def get_drag_action(self, event): |
|
| 937 |
+ state = event.state |
|
| 938 |
+ if event.button in (1, 2): # left or middle button |
|
| 939 |
+ if state & gtk.gdk.CONTROL_MASK: |
|
| 940 |
+ return ZoomAction |
|
| 941 |
+ elif state & gtk.gdk.SHIFT_MASK: |
|
| 942 |
+ return ZoomAreaAction |
|
| 943 |
+ else: |
|
| 944 |
+ return PanAction |
|
| 945 |
+ return NullAction |
|
| 946 |
+ |
|
| 822 | 947 |
def on_area_button_press(self, area, event): |
| 823 |
- if event.button == 2 or event.button == 1: |
|
| 824 |
- area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) |
|
| 825 |
- self.prevmousex = self.startmousex = event.x |
|
| 826 |
- self.prevmousey = self.startmousey = event.y |
|
| 827 |
- self.presstime = time.time() |
|
| 828 |
- self.animation.stop() |
|
| 829 |
- |
|
| 830 |
- if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): |
|
| 831 |
- return False |
|
| 832 |
- x, y = int(event.x), int(event.y) |
|
| 948 |
+ self.animation.stop() |
|
| 949 |
+ action_type = self.get_drag_action(event) |
|
| 950 |
+ self.drag_action = action_type(self) |
|
| 951 |
+ self.drag_action.on_button_press(event) |
|
| 952 |
+ self.presstime = time.time() |
|
| 953 |
+ self.pressx = event.x |
|
| 954 |
+ self.pressy = event.y |
|
| 833 | 955 |
return False |
| 834 | 956 |
|
| 835 | 957 |
def is_click(self, event, click_fuzz=4, click_timeout=1.0): |
| ... | ... |
@@ -839,29 +961,28 @@ class DotWidget(gtk.DrawingArea): |
| 839 | 961 |
return False |
| 840 | 962 |
# XXX instead of doing this complicated logic, shouldn't we listen |
| 841 | 963 |
# for gtk's clicked event instead? |
| 842 |
- deltax = self.startmousex - event.x |
|
| 843 |
- deltay = self.startmousey - event.y |
|
| 964 |
+ deltax = self.pressx - event.x |
|
| 965 |
+ deltay = self.pressy - event.y |
|
| 844 | 966 |
return (time.time() < self.presstime + click_timeout |
| 845 | 967 |
and math.hypot(deltax, deltay) < click_fuzz) |
| 846 | 968 |
|
| 847 | 969 |
def on_area_button_release(self, area, event): |
| 848 |
- if event.button == 2 or event.button == 1: |
|
| 849 |
- area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) |
|
| 850 |
- self.prevmousex = None |
|
| 851 |
- self.prevmousey = None |
|
| 852 |
- |
|
| 853 |
- if event.button == 1 and self.is_click(event): |
|
| 854 |
- x, y = int(event.x), int(event.y) |
|
| 855 |
- url = self.get_url(x, y) |
|
| 856 |
- if url is not None: |
|
| 857 |
- self.emit('clicked', unicode(url), event)
|
|
| 858 |
- else: |
|
| 859 |
- jump = self.get_jump(x, y) |
|
| 860 |
- if jump is not None: |
|
| 861 |
- jumpx, jumpy = jump |
|
| 862 |
- self.animate_to(jumpx, jumpy) |
|
| 970 |
+ self.drag_action.on_button_release(event) |
|
| 971 |
+ self.drag_action = NullAction(self) |
|
| 972 |
+ if event.button == 1 and self.is_click(event): |
|
| 973 |
+ x, y = int(event.x), int(event.y) |
|
| 974 |
+ url = self.get_url(x, y) |
|
| 975 |
+ if url is not None: |
|
| 976 |
+ self.emit('clicked', unicode(url), event)
|
|
| 977 |
+ else: |
|
| 978 |
+ jump = self.get_jump(x, y) |
|
| 979 |
+ if jump is not None: |
|
| 980 |
+ jumpx, jumpy = jump |
|
| 981 |
+ self.animate_to(jumpx, jumpy) |
|
| 863 | 982 |
|
| 864 | 983 |
return True |
| 984 |
+ if event.button == 1 or event.button == 2: |
|
| 985 |
+ return True |
|
| 865 | 986 |
return False |
| 866 | 987 |
|
| 867 | 988 |
def on_area_scroll_event(self, area, event): |
| ... | ... |
@@ -874,31 +995,7 @@ class DotWidget(gtk.DrawingArea): |
| 874 | 995 |
return False |
| 875 | 996 |
|
| 876 | 997 |
def on_area_motion_notify(self, area, event): |
| 877 |
- x, y = int(event.x), int(event.y) |
|
| 878 |
- state = event.state |
|
| 879 |
- |
|
| 880 |
- if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK: |
|
| 881 |
- deltax = self.prevmousex - event.x |
|
| 882 |
- deltay = self.prevmousey - event.y |
|
| 883 |
- if state & gtk.gdk.CONTROL_MASK: |
|
| 884 |
- # zoom the image |
|
| 885 |
- self.zoom_ratio *= 1.005 ** (deltax + deltay) |
|
| 886 |
- self.queue_draw() |
|
| 887 |
- else: |
|
| 888 |
- # pan the image |
|
| 889 |
- self.x += deltax/self.zoom_ratio |
|
| 890 |
- self.y += deltay/self.zoom_ratio |
|
| 891 |
- self.queue_draw() |
|
| 892 |
- self.prevmousex = x |
|
| 893 |
- self.prevmousey = y |
|
| 894 |
- self.animation.stop() |
|
| 895 |
- else: |
|
| 896 |
- # set cursor |
|
| 897 |
- if self.get_url(x, y) is not None: |
|
| 898 |
- area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) |
|
| 899 |
- else: |
|
| 900 |
- area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) |
|
| 901 |
- |
|
| 998 |
+ self.drag_action.on_motion_notify(event) |
|
| 902 | 999 |
return True |
| 903 | 1000 |
|
| 904 | 1001 |
def animate_to(self, x, y): |