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): |