Changeset 11138:97333c41d579 in orange


Ignore:
Timestamp:
10/18/12 18:22:02 (18 months ago)
Author:
Ales Erjavec <ales.erjavec@…>
Branch:
default
Message:

Moved anchor point management to NodeAnchorItem.

Location:
Orange/OrangeCanvas/canvas
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • Orange/OrangeCanvas/canvas/items/linkitem.py

    r11132 r11138  
    136136        """ 
    137137        if item is not None and anchor is not None: 
    138             if anchor not in item.outputAnchors: 
     138            if anchor not in item.outputAnchors(): 
    139139                raise ValueError("Anchor must be belong to the item") 
    140140 
     
    184184        """ 
    185185        if item is not None and anchor is not None: 
    186             if anchor not in item.inputAnchors: 
     186            if anchor not in item.inputAnchors(): 
    187187                raise ValueError("Anchor must be belong to the item") 
    188188 
  • Orange/OrangeCanvas/canvas/items/nodeitem.py

    r11134 r11138  
    55 
    66from xml.sax.saxutils import escape 
     7 
     8import numpy 
    79 
    810from PyQt4.QtGui import ( 
     
    1719from PyQt4.QtCore import pyqtProperty as Property 
    1820 
    19 from .utils import saturated, radial_gradient, sample_path 
     21from .utils import saturated, radial_gradient 
    2022 
    2123from ...registry import NAMED_COLORS 
     
    203205 
    204206 
     207class AnchorPoint(QGraphicsObject): 
     208    """A anchor indicator on the NodeAnchorItem 
     209    """ 
     210 
     211    scenePositionChanged = Signal(QPointF) 
     212    anchorDirectionChanged = Signal(QPointF) 
     213 
     214    def __init__(self, *args): 
     215        QGraphicsObject.__init__(self, *args) 
     216        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True) 
     217        self.setFlag(QGraphicsItem.ItemHasNoContents, True) 
     218 
     219        self.__direction = QPointF() 
     220 
     221    def anchorScenePos(self): 
     222        """Return anchor position in scene coordinates. 
     223        """ 
     224        return self.mapToScene(QPointF(0, 0)) 
     225 
     226    def setAnchorDirection(self, direction): 
     227        """Set the preferred direction (QPointF) in item coordinates. 
     228        """ 
     229        if self.__direction != direction: 
     230            self.__direction = direction 
     231            self.anchorDirectionChanged.emit(direction) 
     232 
     233    def anchorDirection(self): 
     234        """Return the preferred anchor direction. 
     235        """ 
     236        return self.__direction 
     237 
     238    def itemChange(self, change, value): 
     239        if change == QGraphicsItem.ItemScenePositionHasChanged: 
     240            self.scenePositionChanged.emit(value.toPointF()) 
     241 
     242        return QGraphicsObject.itemChange(self, change, value) 
     243 
     244    def boundingRect(self,): 
     245        return QRectF() 
     246 
     247 
    205248class NodeAnchorItem(QGraphicsPathItem): 
    206249    """The left/right widget input/output anchors. 
     
    227270        # Does this item have any anchored links. 
    228271        self.anchored = False 
     272 
     273        self.__anchorPath = QPainterPath() 
     274        self.__points = [] 
     275        self.__pointPositions = [] 
     276 
    229277        self.__fullStroke = None 
    230278        self.__dottedStroke = None 
    231279 
     280        self.__layoutRequested = False 
     281 
    232282    def setAnchorPath(self, path): 
    233283        """Set the anchor's curve path as a QPainterPath. 
    234284        """ 
    235         self.anchorPath = path 
     285        self.__anchorPath = path 
    236286        # Create a stroke of the path. 
    237287        stroke_path = QPainterPathStroker() 
     
    252302            self.setBrush(self.normalBrush) 
    253303 
     304    def anchorPath(self): 
     305        """Return the QPainterPath of the anchor path (a curve on 
     306        which the anchor points lie) 
     307 
     308        """ 
     309        return self.__anchorPath 
     310 
    254311    def setAnchored(self, anchored): 
    255         """Set the items anchored state. 
     312        """Set the items anchored state. When false the item draws it self 
     313        with a dotted stroke. 
     314 
    256315        """ 
    257316        self.anchored = anchored 
     
    270329        raise NotImplementedError 
    271330 
     331    def count(self): 
     332        """Return the number of anchor points. 
     333        """ 
     334        return len(self.__points) 
     335 
     336    def addAnchor(self, anchor, position=0.5): 
     337        """Add a new AnchorPoint to this item and return it's index. 
     338        """ 
     339        return self.insertAnchor(self.count(), anchor, position) 
     340 
     341    def insertAnchor(self, index, anchor, position=0.5): 
     342        """Insert a new AnchorPoint at `index`. 
     343        """ 
     344        if anchor in self.__points: 
     345            raise ValueError("%s already added." % anchor) 
     346 
     347        self.__points.insert(index, anchor) 
     348        self.__pointPositions.insert(index, position) 
     349 
     350        anchor.setParentItem(self) 
     351        anchor.setPos(self.__anchorPath.pointAtPercent(position)) 
     352        anchor.destroyed.connect(self.__onAnchorDestroyed) 
     353 
     354        self.__updatePositions() 
     355 
     356        self.setAnchored(bool(self.__points)) 
     357 
     358        return index 
     359 
     360    def removeAnchor(self, anchor): 
     361        """Remove and delete the anchor point. 
     362        """ 
     363        anchor = self.takeAnchor(anchor) 
     364 
     365        anchor.hide() 
     366        anchor.setParentItem(None) 
     367        anchor.deleteLater() 
     368 
     369    def takeAnchor(self, anchor): 
     370        """Remove the anchor but don't delete it. 
     371        """ 
     372        index = self.__points.index(anchor) 
     373 
     374        del self.__points[index] 
     375        del self.__pointPositions[index] 
     376 
     377        anchor.destroyed.disconnect(self.__onAnchorDestroyed) 
     378 
     379        self.__updatePositions() 
     380 
     381        self.setAnchored(bool(self.__points)) 
     382 
     383        return anchor 
     384 
     385    def __onAnchorDestroyed(self, anchor): 
     386        try: 
     387            index = self.__points.index(anchor) 
     388        except ValueError: 
     389            return 
     390 
     391        del self.__points[index] 
     392        del self.__pointPositions[index] 
     393 
     394        self.__scheduleDelayedLayout() 
     395 
     396    def anchorPoints(self): 
     397        """Return a list of anchor points. 
     398        """ 
     399        return list(self.__points) 
     400 
     401    def anchorPoint(self, index): 
     402        """Return the anchor point at `index`. 
     403        """ 
     404        return self.__points[index] 
     405 
     406    def setAnchorPositions(self, positions): 
     407        """Set the anchor positions in percentages (0..1) along 
     408        the path curve. 
     409 
     410        """ 
     411        if self.__pointPositions != positions: 
     412            self.__pointPositions = positions 
     413 
     414            self.__updatePositions() 
     415 
     416    def anchorPositions(self): 
     417        """Return the positions of anchor points as a list floats where 
     418        each float is between 0 and 1 and specifies where along the anchor 
     419        path does the point lie (0 is at start 1 is at the end). 
     420 
     421        """ 
     422        return self.__pointPositions 
     423 
    272424    def shape(self): 
    273425        # Use stroke without the doted line (poor mouse cursor collision) 
     
    285437        return QGraphicsPathItem.hoverLeaveEvent(self, event) 
    286438 
     439    def __scheduleDelayedLayout(self): 
     440        if not self.__layoutRequested: 
     441            self.__layoutRequested = True 
     442            QTimer.singleShot(0, self.__updatePositions) 
     443 
     444    def __updatePositions(self): 
     445        """Update anchor points positions. 
     446        """ 
     447        for point, t in zip(self.__points, self.__pointPositions): 
     448            pos = self.__anchorPath.pointAtPercent(t) 
     449            point.setPos(pos) 
     450 
     451        self.__layoutRequested = False 
     452 
    287453 
    288454class SourceAnchorItem(NodeAnchorItem): 
     
    298464 
    299465 
    300 class AnchorPoint(QGraphicsObject): 
    301     """A anchor indicator on the WidgetAnchorItem 
    302     """ 
    303     scenePositionChanged = Signal(QPointF) 
    304  
    305     def __init__(self, *args): 
    306         QGraphicsObject.__init__(self, *args) 
    307         self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True) 
    308         self.setFlag(QGraphicsItem.ItemHasNoContents, True) 
    309  
    310     def anchorScenePos(self): 
    311         """Return anchor position in scene coordinates. 
    312         """ 
    313         return self.mapToScene(QPointF(0, 0)) 
    314  
    315     def itemChange(self, change, value): 
    316         if change == QGraphicsItem.ItemScenePositionHasChanged: 
    317             self.scenePositionChanged.emit(value.toPointF()) 
    318  
    319         return QGraphicsObject.itemChange(self, change, value) 
    320  
    321     def boundingRect(self,): 
    322         return QRectF(0, 0, 1, 1) 
    323  
    324  
    325466class MessageIcon(QGraphicsPixmapItem): 
    326467    def __init__(self, *args, **kwargs): 
     
    332473    icon = style.standardIcon(standard_pixmap) 
    333474    return icon.pixmap(16, 16) 
     475 
     476 
     477def linspace(count): 
     478    return map(float, 
     479               numpy.linspace(0.0, 1.0, count + 2, endpoint=True)[1:-1]) 
    334480 
    335481 
     
    372518 
    373519        # round anchor indicators on the anchor path 
    374         self.inputAnchors = [] 
    375         self.outputAnchors = [] 
     520#        self.inputAnchors = [] 
     521#        self.outputAnchors = [] 
    376522 
    377523        # title text item 
     
    390536        self.__warning = None 
    391537        self.__info = None 
     538 
     539        self.__anchorLayout = None 
    392540 
    393541        self.setZValue(self.Z_VALUE) 
     
    587735            raise ValueError("Widget has no inputs.") 
    588736 
    589         anchor = AnchorPoint(self) 
    590         self.inputAnchors.append(anchor) 
    591  
    592         self._layoutAnchors(self.inputAnchors, 
    593                             self.inputAnchorItem.anchorPath) 
    594  
    595         self.inputAnchorItem.setAnchored(bool(self.inputAnchors)) 
     737        anchor = AnchorPoint() 
     738        self.inputAnchorItem.addAnchor(anchor) 
     739 
     740        self.inputAnchorItem.setAnchorPositions( 
     741            linspace(self.inputAnchorItem.count()) 
     742        ) 
     743 
    596744        return anchor 
    597745 
     
    599747        """Remove input anchor. 
    600748        """ 
    601         self.inputAnchors.remove(anchor) 
    602         anchor.setParentItem(None) 
    603  
    604         if anchor.scene(): 
    605             anchor.scene().removeItem(anchor) 
    606  
    607         self._layoutAnchors(self.inputAnchors, 
    608                             self.inputAnchorItem.anchorPath) 
    609  
    610         self.inputAnchorItem.setAnchored(bool(self.inputAnchors)) 
     749        self.inputAnchorItem.removeAnchor(anchor) 
     750 
     751        self.inputAnchorItem.setAnchorPositions( 
     752            linspace(self.inputAnchorItem.count()) 
     753        ) 
    611754 
    612755    def newOutputAnchor(self): 
     
    617760 
    618761        anchor = AnchorPoint(self) 
    619  
    620         self.outputAnchors.append(anchor) 
    621  
    622         self._layoutAnchors(self.outputAnchors, 
    623                             self.outputAnchorItem.anchorPath) 
    624  
    625         self.outputAnchorItem.setAnchored(bool(self.outputAnchors)) 
     762        self.outputAnchorItem.addAnchor(anchor) 
     763 
     764        self.outputAnchorItem.setAnchorPositions( 
     765            linspace(self.outputAnchorItem.count()) 
     766        ) 
     767 
    626768        return anchor 
    627769 
     
    629771        """Remove output anchor. 
    630772        """ 
    631         self.outputAnchors.remove(anchor) 
    632         anchor.hide() 
    633         anchor.setParentItem(None) 
    634  
    635         if anchor.scene(): 
    636             anchor.scene().removeItem(anchor) 
    637  
    638         self._layoutAnchors(self.outputAnchors, 
    639                             self.outputAnchorItem.anchorPath) 
    640  
    641         self.outputAnchorItem.setAnchored(bool(self.outputAnchors)) 
    642  
    643     def _layoutAnchors(self, anchors, path): 
    644         """Layout `anchors` on the `path`. 
    645         TODO: anchor reordering (spring force optimization?). 
    646  
    647         """ 
    648         n_points = len(anchors) + 2 
    649         if anchors: 
    650             points = sample_path(path, n_points) 
    651             for p, anchor in zip(points[1:-1], anchors): 
    652                 anchor.setPos(p) 
     773        self.outputAnchorItem.removeAnchor(anchor) 
     774 
     775        self.outputAnchorItem.setAnchorPositions( 
     776            linspace(self.outputAnchorItem.count()) 
     777        ) 
     778 
     779    def inputAnchors(self): 
     780        """Return a list of input anchor points. 
     781        """ 
     782        return self.inputAnchorItem.anchorPoints() 
     783 
     784    def outputAnchors(self): 
     785        """Return a list of output anchor points. 
     786        """ 
     787        return self.outputAnchorItem.anchorPoints() 
     788 
     789    def setAnchorRotation(self, angle): 
     790        """Set the anchor rotation. 
     791        """ 
     792        self.inputAnchorItem.setRotation(angle) 
     793        self.outputAnchorItem.setRotation(angle) 
     794        self.anchorGeometryChanged.emit() 
     795 
     796    def anchorRotation(self): 
     797        """Return the anchor rotation. 
     798        """ 
     799        return self.inputAnchorItem.rotation() 
    653800 
    654801    def boundingRect(self): 
  • Orange/OrangeCanvas/canvas/items/tests/test_linkitem.py

    r11102 r11138  
    4848        anchor2 = discretize_item.newInputAnchor() 
    4949 
    50         self.assertSequenceEqual(file_item.outputAnchors, [anchor1]) 
    51         self.assertSequenceEqual(discretize_item.inputAnchors, [anchor2]) 
     50        self.assertSequenceEqual(file_item.outputAnchors(), [anchor1]) 
     51        self.assertSequenceEqual(discretize_item.inputAnchors(), [anchor2]) 
    5252 
    5353        link.setSourceItem(file_item, anchor1) 
     
    5959            link.setSourceItem(file_item, AnchorPoint()) 
    6060 
    61         self.assertSequenceEqual(file_item.outputAnchors, [anchor1]) 
     61        self.assertSequenceEqual(file_item.outputAnchors(), [anchor1]) 
    6262 
    6363        anchor2 = file_item.newOutputAnchor() 
    6464 
    6565        link.setSourceItem(file_item, anchor2) 
    66         self.assertSequenceEqual(file_item.outputAnchors, [anchor1, anchor2]) 
     66        self.assertSequenceEqual(file_item.outputAnchors(), [anchor1, anchor2]) 
    6767        self.assertIs(link.sourceAnchor, anchor2) 
    6868 
     
    7777        self.scene.addItem(link) 
    7878 
    79         self.assertTrue(len(nb_item.inputAnchors) == 1) 
    80         self.assertTrue(len(discretize_item.outputAnchors) == 1) 
    81         self.assertTrue(len(discretize_item.inputAnchors) == 1) 
    82         self.assertTrue(len(file_item.outputAnchors) == 1) 
     79        self.assertTrue(len(nb_item.inputAnchors()) == 1) 
     80        self.assertTrue(len(discretize_item.outputAnchors()) == 1) 
     81        self.assertTrue(len(discretize_item.inputAnchors()) == 1) 
     82        self.assertTrue(len(file_item.outputAnchors()) == 1) 
    8383 
    8484        link.removeLink() 
    8585 
    86         self.assertTrue(len(nb_item.inputAnchors) == 0) 
    87         self.assertTrue(len(discretize_item.outputAnchors) == 0) 
    88         self.assertTrue(len(discretize_item.inputAnchors) == 1) 
    89         self.assertTrue(len(file_item.outputAnchors) == 1) 
     86        self.assertTrue(len(nb_item.inputAnchors()) == 0) 
     87        self.assertTrue(len(discretize_item.outputAnchors()) == 0) 
     88        self.assertTrue(len(discretize_item.inputAnchors()) == 1) 
     89        self.assertTrue(len(file_item.outputAnchors()) == 1) 
    9090 
    9191        self.app.exec_() 
  • Orange/OrangeCanvas/canvas/items/tests/test_nodeitem.py

    r11134 r11138  
     1from PyQt4.QtGui import QPainterPath, QGraphicsEllipseItem 
    12 
    2 from .. import NodeItem 
     3from .. import NodeItem, AnchorPoint, NodeAnchorItem 
    34 
    45from . import TestItems 
     
    67 
    78class TestNodeItem(TestItems): 
     9    def setUp(self): 
     10        TestItems.setUp(self) 
     11        from ....registry.tests import small_testing_registry 
     12        self.reg = small_testing_registry() 
     13 
     14        self.data_desc = self.reg.category("Data") 
     15        self.classify_desc = self.reg.category("Classify") 
     16 
     17        self.file_desc = self.reg.widget( 
     18            "Orange.OrangeWidgets.Data.OWFile.OWFile" 
     19        ) 
     20        self.discretize_desc = self.reg.widget( 
     21            "Orange.OrangeWidgets.Data.OWDiscretize.OWDiscretize" 
     22        ) 
     23        self.bayes_desc = self.reg.widget( 
     24            "Orange.OrangeWidgets.Classify.OWNaiveBayes.OWNaiveBayes" 
     25        ) 
    826 
    927    def test_nodeitem(self): 
    10         from ....registry.tests import small_testing_registry 
    11         reg = small_testing_registry() 
    12  
    13         data_desc = reg.category("Data") 
    14  
    15         file_desc = reg.widget("Orange.OrangeWidgets.Data.OWFile.OWFile") 
    16  
    1728        file_item = NodeItem() 
    18         file_item.setWidgetDescription(file_desc) 
    19         file_item.setWidgetCategory(data_desc) 
     29        file_item.setWidgetDescription(self.file_desc) 
     30        file_item.setWidgetCategory(self.data_desc) 
    2031 
    2132        file_item.setTitle("File Node") 
     
    4253        file_item.setPos(100, 100) 
    4354 
    44         discretize_desc = reg.widget( 
    45             "Orange.OrangeWidgets.Data.OWDiscretize.OWDiscretize" 
    46         ) 
    47  
    4855        discretize_item = NodeItem() 
    49         discretize_item.setWidgetDescription(discretize_desc) 
    50         discretize_item.setWidgetCategory(data_desc) 
     56        discretize_item.setWidgetDescription(self.discretize_desc) 
     57        discretize_item.setWidgetCategory(self.data_desc) 
    5158 
    5259        self.scene.addItem(discretize_item) 
    5360        discretize_item.setPos(300, 100) 
    5461 
    55         classify_desc = reg.category("Classify") 
    56  
    57         bayes_desc = reg.widget( 
    58             "Orange.OrangeWidgets.Classify.OWNaiveBayes.OWNaiveBayes" 
    59         ) 
    60  
    6162        nb_item = NodeItem() 
    62         nb_item.setWidgetDescription(bayes_desc) 
    63         nb_item.setWidgetCategory(classify_desc) 
     63        nb_item.setWidgetDescription(self.bayes_desc) 
     64        nb_item.setWidgetCategory(self.classify_desc) 
    6465 
    6566        self.scene.addItem(nb_item) 
     
    9596                file_item.setWarningMessage(None) 
    9697 
     98            discretize_item.setAnchorRotation(50 - p) 
     99 
    97100        progress() 
    98101 
    99102        self.app.exec_() 
     103 
     104    def test_nodeanchors(self): 
     105        file_item = NodeItem() 
     106        file_item.setWidgetDescription(self.file_desc) 
     107        file_item.setWidgetCategory(self.data_desc) 
     108 
     109        file_item.setTitle("File Node") 
     110 
     111        self.scene.addItem(file_item) 
     112        file_item.setPos(100, 100) 
     113 
     114        discretize_item = NodeItem() 
     115        discretize_item.setWidgetDescription(self.discretize_desc) 
     116        discretize_item.setWidgetCategory(self.data_desc) 
     117 
     118        self.scene.addItem(discretize_item) 
     119        discretize_item.setPos(300, 100) 
     120 
     121        nb_item = NodeItem() 
     122        nb_item.setWidgetDescription(self.bayes_desc) 
     123        nb_item.setWidgetCategory(self.classify_desc) 
     124 
     125        with self.assertRaises(ValueError): 
     126            file_item.newInputAnchor() 
     127 
     128        anchor = file_item.newOutputAnchor() 
     129        self.assertIsInstance(anchor, AnchorPoint) 
     130 
     131        self.app.exec_() 
     132 
     133    def test_anchoritem(self): 
     134        anchoritem = NodeAnchorItem(None) 
     135        self.scene.addItem(anchoritem) 
     136 
     137        path = QPainterPath() 
     138        path.addEllipse(0, 0, 100, 100) 
     139 
     140        anchoritem.setAnchorPath(path) 
     141 
     142        anchor = AnchorPoint() 
     143        anchoritem.addAnchor(anchor) 
     144 
     145        ellipse1 = QGraphicsEllipseItem(-3, -3, 6, 6) 
     146        ellipse2 = QGraphicsEllipseItem(-3, -3, 6, 6) 
     147        self.scene.addItem(ellipse1) 
     148        self.scene.addItem(ellipse2) 
     149 
     150        anchor.scenePositionChanged.connect(ellipse1.setPos) 
     151 
     152        with self.assertRaises(ValueError): 
     153            anchoritem.addAnchor(anchor) 
     154 
     155        anchor1 = AnchorPoint() 
     156        anchoritem.addAnchor(anchor1) 
     157 
     158        anchor1.scenePositionChanged.connect(ellipse2.setPos) 
     159 
     160        self.assertSequenceEqual(anchoritem.anchorPoints(), [anchor, anchor1]) 
     161 
     162        self.assertSequenceEqual(anchoritem.anchorPositions(), [0.5, 0.5]) 
     163        anchoritem.setAnchorPositions([0.5, 0.0]) 
     164 
     165        self.assertSequenceEqual(anchoritem.anchorPositions(), [0.5, 0.0]) 
     166 
     167        def advance(): 
     168            t = anchoritem.anchorPositions() 
     169            t = map(lambda t: (t + 0.05) % 1.0, t) 
     170            anchoritem.setAnchorPositions(t) 
     171            self.singleShot(20, advance) 
     172 
     173        advance() 
     174 
     175        self.app.exec_() 
  • Orange/OrangeCanvas/canvas/tests/test_scene.py

    r11113 r11138  
    6363        self.assertTrue(link2.sourceItem is None and link2.sinkItem is None) 
    6464 
    65         self.assertSequenceEqual(file_item.outputAnchors, []) 
    66         self.assertSequenceEqual(disc_item.inputAnchors, []) 
    67         self.assertSequenceEqual(disc_item.outputAnchors, []) 
    68         self.assertSequenceEqual(bayes_item.outputAnchors, []) 
     65        self.assertSequenceEqual(file_item.outputAnchors(), []) 
     66        self.assertSequenceEqual(disc_item.inputAnchors(), []) 
     67        self.assertSequenceEqual(disc_item.outputAnchors(), []) 
     68        self.assertSequenceEqual(bayes_item.outputAnchors(), []) 
    6969 
    7070        # And add one link again 
     
    7373        self.assertSequenceEqual(self.scene.link_items(), [link1]) 
    7474 
    75         self.assertTrue(file_item.outputAnchors) 
    76         self.assertTrue(disc_item.inputAnchors) 
     75        self.assertTrue(file_item.outputAnchors()) 
     76        self.assertTrue(disc_item.inputAnchors()) 
    7777 
    7878        self.app.exec_() 
Note: See TracChangeset for help on using the changeset viewer.