1#----------------------------------------------------------------------------
2# Name:         GUIMode.py
3# Purpose:
4#
5# Author:
6#
7# Created:
8# Version:
9# Date:
10# Licence:
11# Tags:         phoenix-port
12#----------------------------------------------------------------------------
13"""
14
15Module that holds the GUI modes used by FloatCanvas
16
17Note that this can only be imported after a wx.App() has been created.
18
19This approach was inspired by Christian Blouin, who also wrote the initial
20version of the code.
21
22"""
23
24import wx
25import numpy as N
26
27from . import FCEvents, Resources
28from .Utilities import BBox
29
30
31class Cursors(object):
32    """
33    Class to hold the standard Cursors
34
35    """
36    def __init__(self):
37        if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac
38            self.HandCursor = wx.Cursor(Resources.getHand16Image())
39            self.GrabHandCursor = wx.Cursor(Resources.getGrabHand16Image())
40
41            img = Resources.getMagPlus16Image()
42            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
43            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
44            self.MagPlusCursor = wx.Cursor(img)
45
46            img = Resources.getMagMinus16Image()
47            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
48            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
49            self.MagMinusCursor = wx.Cursor(img)
50        else: # use 24X24 cursors for GTK and Windows
51            self.HandCursor = wx.Cursor(Resources.getHandImage())
52            self.GrabHandCursor = wx.Cursor(Resources.getGrabHandImage())
53
54            img = Resources.getMagPlusImage()
55            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
56            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
57            self.MagPlusCursor = wx.Cursor(img)
58
59            img = Resources.getMagMinusImage()
60            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
61            img.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
62            self.MagMinusCursor = wx.Cursor(img)
63
64
65class GUIBase(object):
66    """
67    Basic Mouse mode and baseclass for other GUImode.
68
69    This one does nothing with any event
70
71    """
72    def __init__(self, Canvas=None):
73        """
74        Default class constructor.
75
76        :param `Canvas`: the canvas the GUI mode is attached too
77
78        """
79        self.Canvas = Canvas # set the FloatCanvas for the mode
80                             # it gets set when the Mode is set on the Canvas.
81        self.Cursors = Cursors()
82
83    Cursor = wx.NullCursor
84    def UnSet(self):
85        """
86        this method gets called by FloatCanvas when a new mode is being set
87        on the Canvas
88        """
89        pass
90    # Handlers
91    def OnLeftDown(self, event):
92        pass
93    def OnLeftUp(self, event):
94        pass
95    def OnLeftDouble(self, event):
96        pass
97    def OnRightDown(self, event):
98        pass
99    def OnRightUp(self, event):
100        pass
101    def OnRightDouble(self, event):
102        pass
103    def OnMiddleDown(self, event):
104        pass
105    def OnMiddleUp(self, event):
106        pass
107    def OnMiddleDouble(self, event):
108        pass
109    def OnWheel(self, event):
110        pass
111    def OnMove(self, event):
112        pass
113    def OnKeyDown(self, event):
114        pass
115    def OnKeyUp(self, event):
116        pass
117    def UpdateScreen(self):
118        """
119        Update gets called if the screen has been repainted in the middle of a zoom in
120        so the Rubber Band Box can get updated. Other GUIModes may require something similar
121        """
122        pass
123
124
125## some mix-ins for use with the other modes:
126class ZoomWithMouseWheel():
127    def OnWheel(self, event):
128        point = event.Position
129        if event.GetWheelRotation() < 0:
130            self.Canvas.Zoom(0.9, point, centerCoords = "pixel", keepPointInPlace=True)
131        else:
132            self.Canvas.Zoom(1.1, point, centerCoords = "pixel", keepPointInPlace=True)
133
134
135class GUIMouse(GUIBase):
136    """
137
138    Mouse mode checks for a hit test, and if nothing is hit,
139    raises a FloatCanvas mouse event for each event.
140
141    """
142
143    Cursor = wx.NullCursor
144
145    # Handlers
146    def OnLeftDown(self, event):
147        EventType = FCEvents.EVT_FC_LEFT_DOWN
148        if not self.Canvas.HitTest(event, EventType):
149            self.Canvas._RaiseMouseEvent(event, EventType)
150
151    def OnLeftUp(self, event):
152        EventType = FCEvents.EVT_FC_LEFT_UP
153        if not self.Canvas.HitTest(event, EventType):
154            self.Canvas._RaiseMouseEvent(event, EventType)
155
156    def OnLeftDouble(self, event):
157        EventType = FCEvents.EVT_FC_LEFT_DCLICK
158        if not self.Canvas.HitTest(event, EventType):
159                self.Canvas._RaiseMouseEvent(event, EventType)
160
161    def OnMiddleDown(self, event):
162        EventType = FCEvents.EVT_FC_MIDDLE_DOWN
163        if not self.Canvas.HitTest(event, EventType):
164            self.Canvas._RaiseMouseEvent(event, EventType)
165
166    def OnMiddleUp(self, event):
167        EventType = FCEvents.EVT_FC_MIDDLE_UP
168        if not self.Canvas.HitTest(event, EventType):
169            self.Canvas._RaiseMouseEvent(event, EventType)
170
171    def OnMiddleDouble(self, event):
172        EventType = FCEvents.EVT_FC_MIDDLE_DCLICK
173        if not self.Canvas.HitTest(event, EventType):
174            self.Canvas._RaiseMouseEvent(event, EventType)
175
176    def OnRightDown(self, event):
177        EventType = FCEvents.EVT_FC_RIGHT_DOWN
178        if not self.Canvas.HitTest(event, EventType):
179            self.Canvas._RaiseMouseEvent(event, EventType)
180
181    def OnRightUp(self, event):
182        EventType = FCEvents.EVT_FC_RIGHT_UP
183        if not self.Canvas.HitTest(event, EventType):
184            self.Canvas._RaiseMouseEvent(event, EventType)
185
186    def OnRightDouble(self, event):
187        EventType = FCEvents.EVT_FC_RIGHT_DCLICK
188        if not self.Canvas.HitTest(event, EventType):
189            self.Canvas._RaiseMouseEvent(event, EventType)
190
191    def OnWheel(self, event):
192        EventType = FCEvents.EVT_FC_MOUSEWHEEL
193        self.Canvas._RaiseMouseEvent(event, EventType)
194
195    def OnMove(self, event):
196        ## The Move event always gets raised, even if there is a hit-test
197        EventType = FCEvents.EVT_FC_MOTION
198        # process the object hit test for EVT_MOTION bindings
199        self.Canvas.HitTest(event, EventType)
200        # process enter and leave events
201        self.Canvas.MouseOverTest(event)
202        # then raise the event on the canvas
203        self.Canvas._RaiseMouseEvent(event, EventType)
204
205
206class GUIMove(ZoomWithMouseWheel, GUIBase):
207    """
208    Mode that moves the image (pans).
209    It doesn't change any coordinates, it only changes what the viewport is
210    """
211    def __init__(self, canvas=None):
212        GUIBase.__init__(self, canvas)
213        self.Cursor = self.Cursors.HandCursor
214        self.GrabCursor = self.Cursors.GrabHandCursor
215        self.StartMove = None
216        self.MidMove = None
217        self.PrevMoveXY = None
218
219        ## timer to give a delay when moving so that buffers aren't re-built too many times.
220        self.MoveTimer = wx.PyTimer(self.OnMoveTimer)
221
222    def OnLeftDown(self, event):
223        self.Canvas.SetCursor(self.GrabCursor)
224        self.Canvas.CaptureMouse()
225        self.StartMove = N.array( event.GetPosition() )
226        self.MidMove = self.StartMove
227        self.PrevMoveXY = (0,0)
228
229    def OnLeftUp(self, event):
230        self.Canvas.SetCursor(self.Cursor)
231        if self.StartMove is not None:
232            self.EndMove = N.array(event.GetPosition())
233            DiffMove = self.MidMove-self.EndMove
234            self.Canvas.MoveImage(DiffMove, 'Pixel', ReDraw=True)
235
236    def OnMove(self, event):
237        # Always raise the Move event.
238        self.Canvas._RaiseMouseEvent(event, FCEvents.EVT_FC_MOTION)
239        if event.Dragging() and event.LeftIsDown() and not self.StartMove is None:
240            self.EndMove = N.array(event.GetPosition())
241            self.MoveImage(event)
242            DiffMove = self.MidMove-self.EndMove
243            self.Canvas.MoveImage(DiffMove, 'Pixel', ReDraw=False)# reset the canvas without re-drawing
244            self.MidMove = self.EndMove
245            self.MoveTimer.Start(30, oneShot=True)
246
247    def OnMoveTimer(self, event=None):
248        self.Canvas.Draw()
249
250    def UpdateScreen(self):
251        ## The screen has been re-drawn, so StartMove needs to be reset.
252        self.StartMove = self.MidMove
253
254    def MoveImage(self, event ):
255        #xy1 = N.array( event.GetPosition() )
256        xy1 = self.EndMove
257        wh = self.Canvas.PanelSize
258        xy_tl = xy1 - self.StartMove
259        dc = wx.ClientDC(self.Canvas)
260        x1,y1 = self.PrevMoveXY
261        x2,y2 = xy_tl
262        w,h = self.Canvas.PanelSize
263        ##fixme: This sure could be cleaner!
264        ##   This is all to fill in the background with the background color
265        ##   without flashing as the image moves.
266        if x2 > x1 and y2 > y1:
267            xa = xb = x1
268            ya = yb = y1
269            wa = w
270            ha = y2 - y1
271            wb = x2-  x1
272            hb = h
273        elif x2 > x1 and y2 <= y1:
274            xa = x1
275            ya = y1
276            wa = x2 - x1
277            ha = h
278            xb = x1
279            yb = y2 + h
280            wb = w
281            hb = y1 - y2
282        elif x2 <= x1 and y2 > y1:
283            xa = x1
284            ya = y1
285            wa = w
286            ha = y2 - y1
287            xb = x2 + w
288            yb = y1
289            wb = x1 - x2
290            hb = h - y2 + y1
291        elif x2 <= x1 and y2 <= y1:
292            xa = x2 + w
293            ya = y1
294            wa = x1 - x2
295            ha = h
296            xb = x1
297            yb = y2 + h
298            wb = w
299            hb = y1 - y2
300
301        dc.SetPen(wx.TRANSPARENT_PEN)
302        dc.SetBrush(self.Canvas.BackgroundBrush)
303        dc.DrawRectangle(xa, ya, wa, ha)
304        dc.DrawRectangle(xb, yb, wb, hb)
305        self.PrevMoveXY = xy_tl
306        if self.Canvas._ForeDrawList:
307            dc.DrawBitmap(self.Canvas._ForegroundBuffer,xy_tl)
308        else:
309            dc.DrawBitmap(self.Canvas._Buffer,xy_tl)
310        #self.Canvas.Update()
311
312
313class GUIZoomIn(ZoomWithMouseWheel, GUIBase):
314    """
315    Mode to zoom in.
316    """
317    def __init__(self, canvas=None):
318        GUIBase.__init__(self, canvas)
319        self.StartRBBox = None
320        self.PrevRBBox = None
321        self.Cursor = self.Cursors.MagPlusCursor
322
323    def OnLeftDown(self, event):
324        self.StartRBBox = N.array( event.GetPosition() )
325        self.PrevRBBox = None
326        self.Canvas.CaptureMouse()
327
328    def OnLeftUp(self, event):
329        if event.LeftUp() and not self.StartRBBox is None:
330            self.PrevRBBox = None
331            EndRBBox = event.GetPosition()
332            StartRBBox = self.StartRBBox
333            # if mouse has moved less that ten pixels, don't use the box.
334            if ( abs(StartRBBox[0] - EndRBBox[0]) > 10
335                    and abs(StartRBBox[1] - EndRBBox[1]) > 10 ):
336                EndRBBox = self.Canvas.PixelToWorld(EndRBBox)
337                StartRBBox = self.Canvas.PixelToWorld(StartRBBox)
338                self.Canvas.ZoomToBB( BBox.fromPoints(N.r_[EndRBBox,StartRBBox]) )
339            else:
340                Center = self.Canvas.PixelToWorld(StartRBBox)
341                self.Canvas.Zoom(1.5,Center)
342            self.StartRBBox = None
343
344    def OnMove(self, event):
345        # Always raise the Move event.
346        self.Canvas._RaiseMouseEvent(event,FCEvents.EVT_FC_MOTION)
347        if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None):
348            xy0 = self.StartRBBox
349            xy1 = N.array( event.GetPosition() )
350            wh  = abs(xy1 - xy0)
351            wh[0] = max(wh[0], int(wh[1]*self.Canvas.AspectRatio))
352            wh[1] = int(wh[0] / self.Canvas.AspectRatio)
353            xy_c = (xy0 + xy1) / 2
354            dc = wx.ClientDC(self.Canvas)
355            dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
356            dc.SetBrush(wx.TRANSPARENT_BRUSH)
357            dc.SetLogicalFunction(wx.XOR)
358            if self.PrevRBBox:
359                dc.DrawRectangle(*self.PrevRBBox)
360            self.PrevRBBox = ( xy_c - wh/2, wh )
361            dc.DrawRectangle( *self.PrevRBBox )
362
363    def UpdateScreen(self):
364        """
365        Update gets called if the screen has been repainted in the middle of a zoom in
366        so the Rubber Band Box can get updated
367        """
368        #if False:
369        if self.PrevRBBox is not None:
370            dc = wx.ClientDC(self.Canvas)
371            dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
372            dc.SetBrush(wx.TRANSPARENT_BRUSH)
373            dc.SetLogicalFunction(wx.XOR)
374            dc.DrawRectangle(*self.PrevRBBox)
375
376    def OnRightDown(self, event):
377        self.Canvas.Zoom(1/1.5, event.GetPosition(), centerCoords="pixel")
378
379
380class GUIZoomOut(ZoomWithMouseWheel, GUIBase):
381    """
382    Mode to zoom out.
383    """
384    def __init__(self, Canvas=None):
385        GUIBase.__init__(self, Canvas)
386        self.Cursor = self.Cursors.MagMinusCursor
387
388    def OnLeftDown(self, event):
389        self.Canvas.Zoom(1/1.5, event.GetPosition(), centerCoords="pixel")
390
391    def OnRightDown(self, event):
392        self.Canvas.Zoom(1.5, event.GetPosition(), centerCoords="pixel")
393
394    def OnMove(self, event):
395        # Always raise the Move event.
396        self.Canvas._RaiseMouseEvent(event,FCEvents.EVT_FC_MOTION)
397