1#    Copyright (C) 2008 Jeremy S. Sanders
2#    Email: Jeremy Sanders <jeremy@jeremysanders.net>
3#
4#    This program is free software; you can redistribute it and/or modify
5#    it under the terms of the GNU General Public License as published by
6#    the Free Software Foundation; either version 2 of the License, or
7#    (at your option) any later version.
8#
9#    This program is distributed in the hope that it will be useful,
10#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#    GNU General Public License for more details.
13#
14#    You should have received a copy of the GNU General Public License along
15#    with this program; if not, write to the Free Software Foundation, Inc.,
16#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17##############################################################################
18
19"""
20Classes for moving widgets around
21
22Control items have a createGraphicsItem method which returns a graphics
23item to control the object
24"""
25
26from __future__ import division
27import math
28
29from ..compat import crange, czip
30from .. import qtall as qt
31from .. import document
32from .. import setting
33
34def _(text, disambiguation=None, context='controlgraph'):
35    """Translate text."""
36    return qt.QCoreApplication.translate(context, text, disambiguation)
37
38##############################################################################
39
40class _ScaledShape:
41    """Mixing class for shapes which can return scaled positions.
42
43    The control items are plotted on a zoomed plot, so the raw
44    unscaled coordinates need to be scaled by params.cgscale to be in
45    the correct position.
46
47    We could scale everything to achieve this, but the line widths and
48    size of objects would be changed.
49
50    """
51
52    def setScaledPos(self, x, y):
53        self.setPos(x*self.params.cgscale, y*self.params.cgscale)
54
55    def scaledPos(self):
56        return self.pos()/self.params.cgscale
57
58    def setScaledLine(self, x1, y1, x2, y2):
59        s = self.params.cgscale
60        self.setLine(x1*s, y1*s, x2*s, y2*s)
61
62    def setScaledLinePos(self, x1, y1, x2, y2):
63        s = self.params.cgscale
64        self.setPos(x1*s, y1*s)
65        self.setLine(0,0,(x2-x1)*s,(y2-y1)*s)
66
67    def setScaledRect(self, x, y, w, h):
68        s = self.params.cgscale
69        self.setRect(x*s, y*s, w*s, h*s)
70
71    def scaledX(self):
72        return self.x()/self.params.cgscale
73
74    def scaledY(self):
75        return self.y()/self.params.cgscale
76
77    def scaledRect(self):
78        r = self.rect()
79        s = self.params.cgscale
80        return qt.QRectF(r.left()/s, r.top()/s, r.width()/s, r.height()/s)
81
82##############################################################################
83
84class _ShapeCorner(qt.QGraphicsRectItem, _ScaledShape):
85    """Representing the corners of the rectangle."""
86    def __init__(self, parent, params, rotator=False):
87        qt.QGraphicsRectItem.__init__(self, parent)
88        self.params = params
89        if rotator:
90            self.setBrush( qt.QBrush(setting.settingdb.color('cntrlline')) )
91            self.setRect(-3, -3, 6, 6)
92        else:
93            self.setBrush(qt.QBrush(setting.settingdb.color('cntrlcorner')) )
94            self.setRect(-5, -5, 10, 10)
95        self.setPen(qt.QPen(qt.Qt.NoPen))
96        self.setFlag(qt.QGraphicsItem.ItemIsMovable)
97        self.setZValue(3.)
98
99    def mouseMoveEvent(self, event):
100        """Notify parent on move."""
101        qt.QGraphicsRectItem.mouseMoveEvent(self, event)
102        self.parentItem().updateFromCorner(self, event)
103
104    def mouseReleaseEvent(self, event):
105        """Notify parent on unclicking."""
106        qt.QGraphicsRectItem.mouseReleaseEvent(self, event)
107        self.parentItem().updateWidget()
108
109##############################################################################
110
111def controlLinePen():
112    """Get pen for lines around shapes."""
113    return qt.QPen(setting.settingdb.color('cntrlline'), 2, qt.Qt.DotLine)
114
115class _EdgeLine(qt.QGraphicsLineItem, _ScaledShape):
116    """Line used for edges of resizing box."""
117    def __init__(self, parent, params, ismovable=True):
118        qt.QGraphicsLineItem.__init__(self, parent)
119        self.setPen(controlLinePen())
120        self.setZValue(2.)
121        self.params = params
122        if ismovable:
123            self.setFlag(qt.QGraphicsItem.ItemIsMovable)
124            self.setCursor(qt.Qt.SizeAllCursor)
125
126    def mouseMoveEvent(self, event):
127        """Notify parent on move."""
128        qt.QGraphicsLineItem.mouseMoveEvent(self, event)
129        self.parentItem().updateFromLine(self, self.scaledPos())
130
131    def mouseReleaseEvent(self, event):
132        """Notify parent on unclicking."""
133        qt.QGraphicsLineItem.mouseReleaseEvent(self, event)
134        self.parentItem().updateWidget()
135
136##############################################################################
137
138class ControlMarginBox(object):
139    def __init__(self, widget, posn, maxposn, painthelper,
140                 ismovable = True, isresizable = True):
141        """Create control box item.
142
143        widget: widget this is controllng
144        posn: coordinates of box [x1, y1, x2, y2]
145        maxposn: coordinates of biggest possibe box
146        painthelper: painterhelper to get scaling from
147        ismovable: box can be moved
148        isresizable: box can be resized
149        """
150
151        # save values
152        self.posn = posn
153        self.maxposn = maxposn
154        self.widget = widget
155        self.ismovable = ismovable
156        self.isresizable = isresizable
157
158        # we need these later to convert back to original units
159        self.document = painthelper.document
160        self.pagesize = painthelper.pagesize
161        self.cgscale = painthelper.cgscale
162        self.dpi = painthelper.dpi
163
164    def createGraphicsItem(self, parent):
165        return _GraphMarginBox(parent, self)
166
167    def setWidgetMargins(self):
168        """A helpful routine for setting widget margins after
169        moving or resizing.
170
171        This is called by the widget after receiving updateControlItem
172        """
173        s = self.widget.settings
174
175        # get margins in pixels
176        left = self.posn[0] - self.maxposn[0]
177        right = self.maxposn[2] - self.posn[2]
178        top = self.posn[1] - self.maxposn[1]
179        bottom = self.maxposn[3] - self.posn[3]
180
181        # set up fake painthelper containing veusz scalings
182        helper = document.PaintHelper(
183            self.document, self.pagesize,
184            scaling=self.cgscale, dpi=self.dpi)
185
186        # convert to physical units
187        left = s.get('leftMargin').convertInverse(left, helper)
188        right = s.get('rightMargin').convertInverse(right, helper)
189        top = s.get('topMargin').convertInverse(top, helper)
190        bottom = s.get('bottomMargin').convertInverse(bottom, helper)
191
192        # modify widget margins
193        operations = (
194            document.OperationSettingSet(s.get('leftMargin'), left),
195            document.OperationSettingSet(s.get('rightMargin'), right),
196            document.OperationSettingSet(s.get('topMargin'), top),
197            document.OperationSettingSet(s.get('bottomMargin'), bottom)
198            )
199        self.widget.document.applyOperation(
200            document.OperationMultiple(operations, descr=_('resize margins')))
201
202    def setPageSize(self):
203        """Helper for setting document/page widget size.
204
205        This is called by the widget after receiving updateControlItem
206        """
207        s = self.widget.settings
208
209        # get margins in pixels
210        width = self.posn[2] - self.posn[0]
211        height = self.posn[3] - self.posn[1]
212
213        # set up fake painter containing veusz scalings
214        helper = document.PaintHelper(
215            self.document, self.pagesize,
216            scaling=self.cgscale, dpi=self.dpi)
217
218        # convert to physical units
219        width = s.get('width').convertInverse(width, helper)
220        height = s.get('height').convertInverse(height, helper)
221
222        # modify widget margins
223        operations = (
224            document.OperationSettingSet(s.get('width'), width),
225            document.OperationSettingSet(s.get('height'), height),
226            )
227        self.widget.document.applyOperation(
228            document.OperationMultiple(operations, descr=_('change page size')))
229
230class _GraphMarginBox(qt.QGraphicsItem):
231    """A box which can be moved or resized.
232
233    Can automatically set margins or widget
234    """
235
236    # posn coords of each corner
237    mapcornertoposn = ( (0, 1), (2, 1), (0, 3), (2, 3) )
238
239    def __init__(self, parent, params):
240        """Create control box item."""
241
242        qt.QGraphicsItem.__init__(self, parent)
243        self.params = params
244
245        self.setZValue(2.)
246
247        # create corners of box
248        self.corners = [_ShapeCorner(self, params) for i in crange(4)]
249
250        # lines connecting corners
251        self.lines = [
252            _EdgeLine(self, params, ismovable=params.ismovable)
253            for i in crange(4)]
254
255        # hide corners if box is not resizable
256        if not params.isresizable:
257            for c in self.corners:
258                c.hide()
259
260        self.updateCornerPosns()
261
262    def updateCornerPosns(self):
263        """Update all corners from updated box."""
264
265        par = self.params
266        pos = par.posn
267        # update cursors
268        self.corners[0].setCursor(qt.Qt.SizeFDiagCursor)
269        self.corners[1].setCursor(qt.Qt.SizeBDiagCursor)
270        self.corners[2].setCursor(qt.Qt.SizeBDiagCursor)
271        self.corners[3].setCursor(qt.Qt.SizeFDiagCursor)
272
273        # trim box to maximum size
274        pos[0] = max(pos[0], par.maxposn[0])
275        pos[1] = max(pos[1], par.maxposn[1])
276        pos[2] = min(pos[2], par.maxposn[2])
277        pos[3] = min(pos[3], par.maxposn[3])
278
279        # move corners
280        for corner, (xindex, yindex) in czip(self.corners,
281                                             self.mapcornertoposn):
282            corner.setScaledPos(pos[xindex], pos[yindex])
283
284        # move lines
285        w, h = pos[2]-pos[0], pos[3]-pos[1]
286        self.lines[0].setScaledLinePos(pos[0], pos[1], pos[0]+w, pos[1])
287        self.lines[1].setScaledLinePos(pos[2], pos[1], pos[2], pos[1]+h)
288        self.lines[2].setScaledLinePos(pos[2], pos[3], pos[2]-w, pos[3])
289        self.lines[3].setScaledLinePos(pos[0], pos[3], pos[0], pos[3]-h)
290
291    def updateFromLine(self, line, thispos):
292        """Edge line of box was moved - update bounding box."""
293
294        par = self.params
295        # need old coordinate to work out how far line has moved
296        try:
297            li = self.lines.index(line)
298        except ValueError:
299            return
300        ox = par.posn[ (0, 2, 2, 0)[li] ]
301        oy = par.posn[ (1, 1, 3, 3)[li] ]
302
303        # add on deltas to box coordinates
304        dx, dy = thispos.x()-ox, thispos.y()-oy
305
306        # make sure box can't be moved outside the allowed region
307        if dx > 0:
308            dx = min(dx, par.maxposn[2]-par.posn[2])
309        else:
310            dx = -min(abs(dx), abs(par.maxposn[0]-par.posn[0]))
311        if dy > 0:
312            dy = min(dy, par.maxposn[3]-par.posn[3])
313        else:
314            dy = -min(abs(dy), abs(par.maxposn[1]-par.posn[1]))
315
316        # move the box
317        par.posn[0] += dx
318        par.posn[1] += dy
319        par.posn[2] += dx
320        par.posn[3] += dy
321
322        # update corner coords and other line coordinates
323        self.updateCornerPosns()
324
325    def updateFromCorner(self, corner, event):
326        """Move corner of box to new position."""
327        try:
328            index = self.corners.index(corner)
329        except ValueError:
330            return
331
332        pos = self.params.posn
333        pos[ self.mapcornertoposn[index][0] ] = corner.scaledX()
334        pos[ self.mapcornertoposn[index][1] ] = corner.scaledY()
335
336        # this is needed if the corners move past each other
337        if pos[0] > pos[2]:
338            # swap x
339            pos[0], pos[2] = pos[2], pos[0]
340            self.corners[0], self.corners[1] = self.corners[1], self.corners[0]
341            self.corners[2], self.corners[3] = self.corners[3], self.corners[2]
342        if pos[1] > pos[3]:
343            # swap y
344            pos[1], pos[3] = pos[3], pos[1]
345            self.corners[0], self.corners[2] = self.corners[2], self.corners[0]
346            self.corners[1], self.corners[3] = self.corners[3], self.corners[1]
347
348        self.updateCornerPosns()
349
350    def boundingRect(self):
351        return qt.QRectF(0, 0, 0, 0)
352
353    def paint(self, painter, option, widget):
354        pass
355
356    def updateWidget(self):
357        """Update widget margins."""
358        self.params.widget.updateControlItem(self.params)
359
360##############################################################################
361
362class ControlResizableBox(object):
363    """Control a resizable box.
364    Item resizes centred around a position
365    """
366
367    def __init__(self, widget, phelper, posn, dims, angle, allowrotate=False):
368        """Initialise with widget and boxbounds shape.
369        Rotation is allowed if allowrotate is set
370        """
371        self.widget = widget
372        self.posn = posn
373        self.dims = dims
374        self.angle = angle
375        self.allowrotate = allowrotate
376        self.cgscale = phelper.cgscale
377
378    def createGraphicsItem(self, parent):
379        return _GraphResizableBox(parent, self)
380
381class _GraphResizableBox(qt.QGraphicsItem):
382    """Control a resizable box.
383    Item resizes centred around a position
384    """
385
386    def __init__(self, parent, params):
387        """Initialise with widget and boxbounds shape.
388        Rotation is allowed if allowrotate is set
389        """
390
391        qt.QGraphicsItem.__init__(self, parent)
392        self.params = params
393
394        # create child graphicsitem for each corner
395        self.corners = [_ShapeCorner(self, params) for i in crange(4)]
396        self.corners[0].setCursor(qt.Qt.SizeFDiagCursor)
397        self.corners[1].setCursor(qt.Qt.SizeBDiagCursor)
398        self.corners[2].setCursor(qt.Qt.SizeBDiagCursor)
399        self.corners[3].setCursor(qt.Qt.SizeFDiagCursor)
400        for c in self.corners:
401            c.setToolTip(_('Hold shift to resize symmetrically'))
402
403        # lines connecting corners
404        self.lines = [
405            _EdgeLine(self, params, ismovable=True) for i in crange(4)]
406
407        # whether box is allowed to be rotated
408        self.rotator = None
409        if params.allowrotate:
410            self.rotator = _ShapeCorner(self, params, rotator=True)
411            self.rotator.setCursor(qt.Qt.CrossCursor)
412
413        self.updateCorners()
414
415    def updateFromCorner(self, corner, event):
416        """Take position and update corners."""
417
418        par = self.params
419
420        x = corner.scaledX()-par.posn[0]
421        y = corner.scaledY()-par.posn[1]
422
423        if corner in self.corners:
424            # rotate position back
425            angle = -par.angle/180.*math.pi
426            s, c = math.sin(angle), math.cos(angle)
427            tx = x*c-y*s
428            ty = x*s+y*c
429
430            if event.modifiers() & qt.Qt.ShiftModifier:
431                # expand around centre
432                par.dims[0] = abs(tx*2)
433                par.dims[1] = abs(ty*2)
434            else:
435                # moved distances of corner point
436                mdx = par.dims[0]*0.5 - abs(tx)
437                mdy = par.dims[1]*0.5 - abs(ty)
438
439                # The direction to move the centre depends on which corner
440                # it is. This makes the other side of the box stay in the
441                # same place.
442                signx = 1 if tx<0 else -1
443                signy = 1 if ty<0 else -1
444
445                # compute how much to move box centre by
446                dx = 0.5*signx*mdx
447                dy = 0.5*signy*mdy
448                rdx =  dx*c+dy*s  # rotate forwards again
449                rdy = -dx*s+dy*c
450
451                par.posn[0] += rdx
452                par.posn[1] += rdy
453                par.dims[0] -= mdx
454                par.dims[1] -= mdy
455
456        elif corner is self.rotator:
457            # work out angle relative to centre of widget
458            angle = math.atan2(y, x)
459            # change to degrees from correct direction
460            par.angle = round((angle*(180/math.pi) + 90.) % 360, 2)
461
462        self.updateCorners()
463
464    def updateCorners(self):
465        """Update corners on size."""
466        par = self.params
467
468        # update corners
469        angle = par.angle/180.*math.pi
470        s, c = math.sin(angle), math.cos(angle)
471
472        for corn, (xd, yd) in czip(
473                self.corners, ((-1, -1), (1, -1), (-1, 1), (1, 1))):
474            dx, dy = xd*par.dims[0]*0.5, yd*par.dims[1]*0.5
475            corn.setScaledPos(
476                dx*c-dy*s + par.posn[0],
477                dx*s+dy*c + par.posn[1])
478
479        if self.rotator:
480            # set rotator position (constant distance)
481            dx, dy = 0, -par.dims[1]*0.5
482            nx = dx*c-dy*s
483            ny = dx*s+dy*c
484            self.rotator.setScaledPos(nx+par.posn[0], ny+par.posn[1])
485
486        self.linepos = []
487        corn = self.corners
488        for i, (ci1, ci2) in enumerate(((0, 1), (2, 0), (1, 3), (2, 3))):
489            pos1 = corn[ci1].scaledX(), corn[ci1].scaledY()
490            self.lines[i].setScaledLinePos(
491                pos1[0], pos1[1],
492                corn[ci2].scaledX(), corn[ci2].scaledY())
493            self.linepos.append(pos1)
494
495    def updateFromLine(self, line, thispos):
496        """Edge line of box was moved - update bounding box."""
497
498        # need old coordinate to work out how far line has moved
499        oldpos = self.linepos[self.lines.index(line)]
500
501        dx = line.scaledX() - oldpos[0]
502        dy = line.scaledY() - oldpos[1]
503        self.params.posn[0] += dx
504        self.params.posn[1] += dy
505
506        # update corner coords and other line coordinates
507        self.updateCorners()
508
509    def updateWidget(self):
510        """Tell the user the graphicsitem has been moved or resized."""
511        self.params.widget.updateControlItem(self.params)
512
513    def boundingRect(self):
514        """Intentionally zero bounding rect."""
515        return qt.QRectF(0, 0, 0, 0)
516
517    def paint(self, painter, option, widget):
518        """Intentionally empty painter."""
519
520##############################################################################
521
522class ControlMovableBox(ControlMarginBox):
523    """Item for user display for controlling widget.
524    This is a dotted movable box with an optional "cross" where
525    the real position of the widget is
526    """
527
528    def __init__(self, widget, posn, painthelper, crosspos=None):
529        ControlMarginBox.__init__(self, widget, posn,
530                                  [-10000, -10000, 10000, 10000],
531                                  painthelper, isresizable=False)
532        self.deltacrosspos = (crosspos[0] - self.posn[0],
533                              crosspos[1] - self.posn[1])
534
535    def createGraphicsItem(self, parent):
536        return _GraphMovableBox(parent, self)
537
538class _GraphMovableBox(_GraphMarginBox):
539    def __init__(self, parent, params):
540        _GraphMarginBox.__init__(self, parent, params)
541        self.cross = _ShapeCorner(self, params)
542        self.cross.setCursor(qt.Qt.SizeAllCursor)
543        self.updateCornerPosns()
544
545    def updateCornerPosns(self):
546        _GraphMarginBox.updateCornerPosns(self)
547
548        par = self.params
549        if hasattr(self, 'cross'):
550            # this fails if called before self.cross is initialised!
551            self.cross.setScaledPos(
552                par.deltacrosspos[0] + par.posn[0],
553                par.deltacrosspos[1] + par.posn[1])
554
555    def updateFromCorner(self, corner, event):
556        if corner == self.cross:
557            # if cross moves, move whole box
558            par = self.params
559            cx, cy = self.cross.scaledX(), self.cross.scaledY()
560            dx = cx - (par.deltacrosspos[0] + par.posn[0])
561            dy = cy - (par.deltacrosspos[1] + par.posn[1])
562
563            par.posn[0] += dx
564            par.posn[1] += dy
565            par.posn[2] += dx
566            par.posn[3] += dy
567            self.updateCornerPosns()
568        else:
569            _GraphMarginBox.updateFromCorner(self, corner, event)
570
571##############################################################################
572
573class ControlLine(object):
574    """For controlling the position and ends of a line."""
575    def __init__(self, widget, phelper, x1, y1, x2, y2):
576        self.widget = widget
577        self.line = x1, y1, x2, y2
578        self.cgscale = phelper.cgscale
579
580    def createGraphicsItem(self, parent):
581        return _GraphLine(parent, self)
582
583class _GraphLine(qt.QGraphicsLineItem, _ScaledShape):
584    """Represents the line as a graphics item."""
585    def __init__(self, parent, params):
586        qt.QGraphicsLineItem.__init__(self, parent)
587        self.params = params
588        l = self.params.line
589        self.setScaledLine(l[0], l[1], l[2], l[3])
590        self.setCursor(qt.Qt.SizeAllCursor)
591        self.setFlag(qt.QGraphicsItem.ItemIsMovable)
592        self.setPen(controlLinePen())
593        self.setZValue(1.)
594
595        self.p0 = _ShapeCorner(self, params, rotator=True)
596        self.p0.setScaledPos(params.line[0], params.line[1])
597        self.p0.setCursor(qt.Qt.CrossCursor)
598        self.p1 = _ShapeCorner(self, params, rotator=True)
599        self.p1.setScaledPos(params.line[2], params.line[3])
600        self.p1.setCursor(qt.Qt.CrossCursor)
601
602    def updateFromCorner(self, corner, event):
603        """Take position and update ends of line."""
604        c = (self.p0.scaledX(), self.p0.scaledY(),
605             self.p1.scaledX(), self.p1.scaledY())
606        self.setScaledLine(*c)
607
608    def mouseReleaseEvent(self, event):
609        """If widget has moved, tell it."""
610        qt.QGraphicsItem.mouseReleaseEvent(self, event)
611        self.updateWidget()
612
613    def updateWidget(self):
614        """Update caller with position and line positions."""
615
616        x, y = self.scaledX(), self.scaledY()
617        pt1 = self.p0.scaledX()+x, self.p0.scaledY()+y
618        pt2 = self.p1.scaledX()+x, self.p1.scaledY()+y
619
620        self.params.widget.updateControlItem(self.params, pt1, pt2)
621
622#############################################################################
623
624class _AxisGraphicsLineItem(qt.QGraphicsLineItem, _ScaledShape):
625    def __init__(self, parent, params):
626        qt.QGraphicsLineItem.__init__(self, parent)
627        self.parent = parent
628        self.params = params
629
630        self.setPen(controlLinePen())
631        self.setZValue(2.)
632        self.setFlag(qt.QGraphicsItem.ItemIsMovable)
633
634    def mouseReleaseEvent(self, event):
635        """Notify finished."""
636        qt.QGraphicsLineItem.mouseReleaseEvent(self, event)
637        self.parent.updateWidget()
638
639    def mouseMoveEvent(self, event):
640        """Move the axis."""
641        qt.QGraphicsLineItem.mouseMoveEvent(self, event)
642        self.parent.doLineUpdate()
643
644
645
646class ControlAxisLine(object):
647    """Controlling position of an axis."""
648
649    def __init__(self, widget, painthelper, direction,
650                 minpos, maxpos, axispos, maxposn):
651        self.widget = widget
652        self.direction = direction
653        if minpos > maxpos:
654            minpos, maxpos = maxpos, minpos
655        self.minpos = self.minzoom = self.minorig = minpos
656        self.maxpos = self.maxzoom = self.maxorig = maxpos
657        self.axisorigpos = self.axispos = axispos
658        self.maxposn = maxposn
659        self.cgscale = painthelper.cgscale
660        self.reset = None
661
662    def done_reset(self):
663        return self.reset is not None
664
665    def zoomed(self):
666        """Is this a zoom?"""
667        return self.minzoom != self.minorig or self.maxzoom != self.maxorig
668
669    def moved(self):
670        """Has axis moved?"""
671        return (
672            self.minpos != self.minorig or self.maxpos != self.maxorig or
673            self.axisorigpos != self.axispos
674        )
675
676    def createGraphicsItem(self, parent):
677        return _GraphAxisLine(parent, self)
678
679class _AxisRange(_ShapeCorner):
680    """A control item which allows double click for a reset."""
681
682    def mouseDoubleClickEvent(self, event):
683        qt.QGraphicsRectItem.mouseDoubleClickEvent(self, event)
684        self.parentItem().resetRange(self)
685
686class _GraphAxisLine(qt.QGraphicsItem):
687
688    # cursors to use
689    curs = {
690        True: qt.Qt.SizeVerCursor,
691        False: qt.Qt.SizeHorCursor
692    }
693    curs_zoom = {
694        True: qt.Qt.SplitVCursor,
695        False: qt.Qt.SplitHCursor
696    }
697
698    def __init__(self, parent, params):
699        """Line is about to be shown."""
700        qt.QGraphicsItem.__init__(self, parent)
701        self.params = params
702        self.pts = [
703            _ShapeCorner(self, params), _ShapeCorner(self, params),
704            _AxisRange(self, params), _AxisRange(self, params)
705        ]
706        self.line = _AxisGraphicsLineItem(self, params)
707
708        # set cursors and tooltips for items
709        self.horz = (params.direction == 'horizontal')
710        for p in self.pts[0:2]:
711            p.setCursor(self.curs[not self.horz])
712            p.setToolTip("Move axis ends")
713        for p in self.pts[2:]:
714            p.setCursor(self.curs_zoom[not self.horz])
715            p.setToolTip("Change axis scale. Double click to reset end.")
716        self.line.setCursor( self.curs[self.horz] )
717        self.line.setToolTip("Move axis position")
718        self.setZValue(2.)
719
720        self.updatePos()
721
722    def updatePos(self):
723        """Set ends of line and line positions from stored values."""
724        par = self.params
725        scaling = par.cgscale
726        mxp = par.maxposn
727
728        def _clip(*args):
729            """Clip positions to bounds of box given coords."""
730            par.minpos = max(par.minpos, mxp[args[0]])
731            par.maxpos = min(par.maxpos, mxp[args[1]])
732            par.axispos = max(par.axispos, mxp[args[2]])
733            par.axispos = min(par.axispos, mxp[args[3]])
734
735        # distance zoom boxes offset from axis
736        offset = 15/scaling
737
738        if self.horz:
739            _clip(0, 2, 1, 3)
740
741            # set positions
742            if par.zoomed():
743                self.line.setScaledPos(par.minzoom, par.axispos)
744                self.line.setScaledLine(0, 0, par.maxzoom-par.minzoom, 0)
745            else:
746                self.line.setScaledPos(par.minpos, par.axispos)
747                self.line.setScaledLine(0, 0, par.maxpos-par.minpos, 0)
748            self.pts[0].setScaledPos(par.minpos, par.axispos)
749            self.pts[1].setScaledPos(par.maxpos, par.axispos)
750            self.pts[2].setScaledPos(par.minzoom, par.axispos-offset)
751            self.pts[3].setScaledPos(par.maxzoom, par.axispos-offset)
752        else:
753            _clip(1, 3, 0, 2)
754
755            # set positions
756            if par.zoomed():
757                self.line.setScaledPos(par.axispos, par.minzoom)
758                self.line.setScaledLine(0, 0, 0, par.maxzoom-par.minzoom)
759            else:
760                self.line.setScaledPos(par.axispos, par.minpos)
761                self.line.setScaledLine(0, 0, 0, par.maxpos-par.minpos)
762            self.pts[0].setScaledPos(par.axispos, par.minpos)
763            self.pts[1].setScaledPos(par.axispos, par.maxpos)
764            self.pts[2].setScaledPos(par.axispos+offset, par.minzoom)
765            self.pts[3].setScaledPos(par.axispos+offset, par.maxzoom)
766
767    def updateFromCorner(self, corner, event):
768        """Ends of axis have moved, so update values."""
769
770        par = self.params
771        pt = (corner.scaledY(), corner.scaledX())[self.horz]
772        # which end has moved?
773        if corner is self.pts[0]:
774            # horizonal or vertical axis?
775            par.minpos = pt
776        elif corner is self.pts[1]:
777            par.maxpos = pt
778        elif corner is self.pts[2]:
779            par.minzoom = pt
780        elif corner is self.pts[3]:
781            par.maxzoom = pt
782
783        # swap round end points if min > max
784        if par.minpos > par.maxpos:
785            par.minpos, par.maxpos = par.maxpos, par.minpos
786            self.pts[0], self.pts[1] = self.pts[1], self.pts[0]
787
788        self.updatePos()
789
790    def doLineUpdate(self):
791        """Line has moved, so update position."""
792        if self.horz:
793            self.params.axispos = self.line.scaledY()
794        else:
795            self.params.axispos = self.line.scaledX()
796        self.updatePos()
797
798    def updateWidget(self):
799        """Tell widget to update."""
800        self.params.widget.updateControlItem(self.params)
801
802    def boundingRect(self):
803        """Intentionally zero bounding rect."""
804        return qt.QRectF(0, 0, 0, 0)
805
806    def paint(self, painter, option, widget):
807        """Intentionally empty painter."""
808
809    def resetRange(self, corner):
810        """User wants to reset range."""
811
812        if corner is self.pts[2]:
813            self.params.reset = 0
814        else:
815            self.params.reset = 1
816        self.updateWidget()
817